Repository navigation
Task handoff records and human-approval state (v0.2.0) - #4
Merged
Merged
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 intask.json; returned in full bytask_result, summarized intask_statusandlist_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.statedone,needs_fix,needs_review,needs_approval,needs_input,blocked,retryable,closedfailed_checkstests(command, exit code, failing test names, excerpt),integrity,blocked_calls(with examples),timeout,stall,interrupted,worker_error,worker_status,result_block,no_changesownersupervisor,humanor a named ownernext_actionresumecontextnotes,history,updated_at,updated_byParked status
needs_approval. A task parks when the worker reportsneeds_approvalorneeds_input, or reportsblockedwith aneeds:request or after blocked tool calls. The verdict staysblocked. No worker process runs.task_statusreturnsterminal: true, parked: true, so existing pollers stop. Retention keeps the worktree unlessretention.parked_daysis set. With it set, an expired parked task loses its worktree (the diff is archived) and is closed.continue_taskworks on parked tasks.New MCP tools
update_handoff(task_id, state?, owner?, next_action?, note?, by?). Setting stateneeds_approvalorneeds_inputparks a finished task. Any other state unparks it.approve_task(task_id, decision: approve|reject, instructions?, note?, timeout_minutes?, profile?, by?).approveresumes the task through the continue path: same worktree, and the same session when the backend matches, with the approval noted.rejectcloses the task (statuscancelled, worktree kept), or redirects it when you pass instructions. Approval does not lift sandbox rules.list_tasks: new filtersstatus: "needs_attention"(parked tasks plus handoffs that are not done or closed) andstatus: "parked", and a newownerfilter.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
needs_approvalandneeds_input, plus an optionalneeds:line.test_results.failing_testsis filled when tests fail.health_reportaddsparked_tasks,oldest_parked_daysandneeds_attention.parked,handoff_updated,approval,closed.cancel_taskorcleanup_taskon a parked task closes it.docs/handoff.md, plus updates to architecture, configuration, security, troubleshooting, README and the delegation skill draft.CHANGELOG: 0.2.0 (Unreleased).package.jsonis now 0.2.0.Compatibility
blocked.blockedwith a concrete request or after blocked tool calls used to end ascompleted+blocked. It now ends asneeds_approval+blocked. Without either, it still ends ascompleted.list_tasks status: "terminal"now also includes parked tasks, since no worker is running.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 includestest/handoff.test.mjs(derivation for each verdict, parsing, failing-test extraction) andtest/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).