Skip to content

Task handoff records and human-approval state (v0.2.0) - #4

Merged
mrchatam merged 9 commits into
mainfrom
feat/task-handoff
Sep 26, 2026
Merged

mrchatam merged 9 commits into
mainfrom
feat/task-handoff

Conversation

@mrchatam

Copy link
Copy Markdown
Owner

Based on community feedback: when a test fails or the worker needs human approval, the supervisor should keep the task as blocked, with the failed check, the owner and the exact next action, so a later agent can resume it without guessing.

What changes

Handoff record on every finished task (task.handoff, persisted in task.json; returned in full by task_result, summarized in task_status and list_tasks). The daemon derives it deterministically from the verdict and data it already has. The model never writes it. When the worker's RESULT block asks for something, that request is quoted.

Field Meaning
state done, needs_fix, needs_review, needs_approval, needs_input, blocked, retryable, closed
failed_checks tests (command, exit code, failing test names, excerpt), integrity, blocked_calls (with examples), timeout, stall, interrupted, worker_error, worker_status, result_block, no_changes
owner who acts next: supervisor, human or a named owner
next_action one exact instruction, e.g. "Run continue_task with instructions to fix the failing test(s) test_divide …"
resume task_id, suggested tool + args, whether the session can be reused, worktree path, branch, base commit
context worker summary, worker request, remaining concerns, diffstat, diff_path, previous attempts
notes, history, updated_at, updated_by edits by the supervisor or a human

Parked status needs_approval. A task parks when the worker reports needs_approval or needs_input, or reports blocked with a needs: request or after blocked tool calls. The verdict stays blocked. No worker process runs. task_status returns terminal: true, parked: true, so existing pollers stop. Retention keeps the worktree unless retention.parked_days is set. With it set, an expired parked task loses its worktree (the diff is archived) and is closed. continue_task works on parked tasks.

New MCP tools

  • update_handoff(task_id, state?, owner?, next_action?, note?, by?). Setting state needs_approval or needs_input parks a finished task. Any other state unparks it.
  • approve_task(task_id, decision: approve|reject, instructions?, note?, timeout_minutes?, profile?, by?). approve resumes the task through the continue path: same worktree, and the same session when the backend matches, with the approval noted. reject closes the task (status cancelled, worktree kept), or redirects it when you pass instructions. Approval does not lift sandbox rules.
  • list_tasks: new filters status: "needs_attention" (parked tasks plus handoffs that are not done or closed) and status: "parked", and a new owner filter.

CLI: workhorse handoff <id> [--state --owner --next --note --by], workhorse approve <id>, workhorse reject <id>, workhorse attention, workhorse tasks --status/--owner, workhorse cleanup-old --parked-days N.

Also in this PR

  • Worker contract: RESULT statuses needs_approval and needs_input, plus an optional needs: line.
  • test_results.failing_tests is filled when tests fail.
  • health_report adds parked_tasks, oldest_parked_days and needs_attention.
  • Audit events: parked, handoff_updated, approval, closed.
  • cancel_task or cleanup_task on a parked task closes it.
  • Docs: new docs/handoff.md, plus updates to architecture, configuration, security, troubleshooting, README and the delegation skill draft.
  • CHANGELOG: 0.2.0 (Unreleased). package.json is now 0.2.0.

Compatibility

  • Existing statuses, verdicts and MCP tool arguments are unchanged. A parked task's verdict is the existing blocked.
  • Behavior change: a worker that reports blocked with a concrete request or after blocked tool calls used to end as completed + blocked. It now ends as needs_approval + blocked. Without either, it still ends as completed.
  • list_tasks status: "terminal" now also includes parked tasks, since no worker is running.
  • Tasks finished before 0.2.0 get a handoff derived on the fly when asked.

Tests

  • npm test (local, Kilo CLI + bwrap + mock LLM): 76 tests, 75 pass, 0 fail, 1 skipped (the live test). Before this PR: 53 pass, 1 skipped.
  • npm run test:unit: 47 pass. This now includes test/handoff.test.mjs (derivation for each verdict, parsing, failing-test extraction) and test/handoff-flow.test.mjs (a Manager-level flow that needs no backend CLI, so it runs in CI: parking, filters, retention, approve and reject, update_handoff, persistence across restart, legacy tasks).
  • A new mock-LLM integration test covers needs_approval → update_handoff → approve_task → resume in the same session → done. The escape test now also asserts that the task parks.

Every finished task now carries a handoff record (state, failed_checks,
owner, next_action, resume, context, updated_at/updated_by), derived
deterministically from the verdict, daemon-run tests, integrity checks,
blocked tool calls and the worker's RESULT block. It is persisted in
task.json, returned in task_result and summarized in task_status and
list_tasks; tasks from 0.1.0 get a derived record on the fly.

Workers that report needs_approval/needs_input (or blocked with a
request or after blocked tool calls) park the task in status
needs_approval (verdict stays blocked). Parked tasks report
terminal: true, parked: true, are skipped by retention unless
retention.parked_days is set, and can be resumed with continue_task.
The worker contract gains the needs_approval/needs_input statuses and
an optional needs: line.
update_handoff edits owner, next_action, state and notes; parking
states park a finished task, other states unpark it. approve_task
approves (resume via the continue path with the approval noted) or
rejects (close, or redirect with instructions). list_tasks gains the
needs_attention and parked filters and an owner filter. CLI: workhorse
handoff/approve/reject/attention, tasks --status/--owner,
cleanup-old --parked-days. All decisions go to the audit log.
Pure unit tests for each verdict's handoff, Manager-level flow tests
that run without a backend CLI (needs_approval parking, retention,
approve/reject, update_handoff, restart persistence, list filters),
and a mock-LLM integration test through the daemon.
docs/handoff.md (fields, verdict table, parked status, approval flow),
architecture/configuration/security/troubleshooting updates, README
feature and CLI entries, delegation skill guidance (read
handoff.next_action, relay approvals, hand off with update_handoff),
CHANGELOG 0.2.0 (Unreleased) and package version 0.2.0.
cleanup_task on a parked task, or retention with parked_days, now
closes it (status cancelled, handoff closed, archived diff named) so no
parked task is left waiting on a worktree that no longer exists.
Parking a task whose worktree is gone is refused. Handoff history now
records the owner/status change caused by parking.
The run_started task event passed the run kind as `kind`, which
overwrote the audit record's kind ("task") with "initial"/"continue".
It is now `run_kind`. audit() and Manager.event() now guard their
reserved fields centrally (ts, kind; task_id, event, status for task
events): the reserved value wins and a clashing data key is kept as
data_<key>. Regression tests in test/audit.test.mjs and the daemon
audit test; audit record format documented in docs/architecture.md.
1. shutdown()/onExit() mark a running task interrupted without
   finalizing it, and recover() skipped interrupted tasks, so a task
   stopped by `workhorse stop` / SIGTERM had no result and no handoff
   (task_result said it was still active). recover() now finalizes
   interrupted tasks that have no result (idempotent).
2. cleanup_task and retention removed the worktree across several async
   steps before marking it removed, so a continue_task/approve_task in
   that window could queue a run into a worktree being deleted. The task
   is now marked (in memory) before the first await; continue_task,
   approve_task, update_handoff, a second cleanup and retention refuse or
   skip it until the removal is done.

Regression tests: Manager-level tests for both, and the graceful-SIGTERM
recovery integration test now checks the result and handoff.
…, record approval source

- shutdown removes only its own socket (inode check) and pid file; a new daemon waits for a
  previous one that is still stopping before recovery; workhorse stop/restart wait for exit
- update_handoff state=closed on a parked task goes through closeTask (cancelled/closed)
- approvals, handoff updates and closes record source {channel, auth} next to free-text by
- tests: restart race, closed-on-parked, source; helpers wait for the daemon pid
@mrchatam
mrchatam merged commit 9450b92 into main Sep 26, 2026
1 check passed
mrchatam added a commit that referenced this pull request Sep 26, 2026
v0.3.0: token savings (wait_task, brief view, auto-fix/escalation, usage report, opt-in savers) — stacked on #4
@mrchatam
mrchatam deleted the feat/task-handoff branch September 26, 2026 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant