fix(goal): goal-scoped todo epochs, diff-gated steering, real completion (#318) - #319
Conversation
…#318) /goal reattached every stale todo to each turn and stayed active forever after normal completion. Now: - Goal + TodoItem carry an epoch; todo_write stamps items with the goal that authored them (new src/goal_state.zig, pure + unit-tested). - Replacing a goal parks the superseded checklist and queues a one-shot codex-style supersession note instead of bequeathing the items. - /goal clear closes its checklist; /clear and /new reset steering state. - The steering note no longer embeds the checklist (the model already sees it in todo_write results) and is diff-gated: injected on change, on resume, after compaction, or every 8th turn - not verbatim every turn (compaction was memorizing dead goals from the repeats). - attempt_completion completes the goal (with a double-check when current-epoch items are still open), so a goal ends without needing /loop's controller. - /loop: a turn that uses no tools is a natural finish - a one-turn prompt no longer burns all 25 continuations; the harness note now states the completion contract. - Sessions persist todos with their epoch; resume replaces the list wholesale and reconciles a finished active goal to .complete. Closes #318 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjuHQ7vTXWYnVAfUo5EWqH
A 16-agent adversarial review of the first commit confirmed six defects, all fixed here: - A zero-tool /loop turn was fed to continuationDecision as "checklist complete" and silently flipped an active goal to .complete (including the turn where the harness itself had just refused attempt_completion). The loop still stops on a natural finish, but only real work_done (completed set, or the checklist finished) may complete the goal. - attempt_completion closed a checklist-less goal on the first interim call (guaranteed under --strict). completionGate now requires either a fully-completed checklist or an explicit second confirming call. - /review turns hit the goal gate and could complete the user's standing goal from inside an isolated review; goalActive excludes review_mode. - The steering fingerprint was reset on only one of the history-rewrite paths. It now resets at the source - compact(), emergencyTrim(), /rewind - covering /compact, mid-turn, overflow recovery, and resume. - pending_goal_note and the fingerprint survived loadSession, leaking a stale supersession note into a resumed conversation. - /goal clear with no goal set destroyed the model's unscoped working checklist and steered about a phantom goal; it is now a no-op, and the over-eager resume reconcile (which silently completed phase-style goals whose current checklist happened to be finished) is removed. 392/392 tests pass; completionGate has direct unit coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjuHQ7vTXWYnVAfUo5EWqH
|
Ran a 16-agent adversarial review (3 lenses × per-finding skeptic verification) over the first commit. 15 findings survived verification, collapsing into 6 real defect classes — all fixed in d6876e3:
One finding was refuted (the provider-failover double-check spend — mechanics right, consequence doesn't materialize). 392/392 tests green after the round. |
…tdout Live behavioral testing (6 agents driving the built binary through real sessions) passed 52/53 checks. The one real failure: the completion deferral promises "open items will be parked", but force-closing the goal left the todos in place - /goal status then read the contradictory "Status: complete. Checklist: 1 item(s) open." Completion now closes the goal's epoch checklist (goal_state.closeEpoch). Also, the deferral was invisible on stdout (meta tool results are not echoed), so the user saw the completion text twice with no explanation; the gate now prints "⏸ completion deferred" when it fires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjuHQ7vTXWYnVAfUo5EWqH
|
Live behavioral test round — 6 agents drove the built binary (kimi/k3 turns, isolated workspaces) through the full goal/todo surface, asserting on transcripts + persisted session JSON: 52/53 checks passed, and the one failure is fixed in the commit above.
Design notes surfaced by the testers, left as-is intentionally: the completion gate is a confirm-once speed bump, not a hard wall — a model can close a goal with two consecutive calls (Cline's double-check semantics; a hard wall would recreate the goal-that-never-ends). And the diff-gate fingerprints the note text, not checklist deltas — the checklist already reaches the model as todo_write results. Post-fix re-verification: |
|
Shipped in v0.0.221. This branch grew well past its original scope during review, so it landed through What landed beyond the original commits:
Four adversarial verification rounds ran over this; three of them found real defects in the newest code, including two that recreated #318 through new doors. Details are in the commit bodies. Closing in favor of the release branch. |
Fixes #318 —
/goalreattached unscoped stale todos on every turn and stayed active after normal completion.Informed by a comparative study of how other harnesses handle this (openai/codex source + DeepWiki, cline, opencode, gemini-cli). Every mechanism below has a named precedent.
Root causes fixed
TodoItemwas{content, status};/goal <new>replaced onlyroot.goal, so the new goal inherited every stale item and presented them as its own checklist each turn.todo_writetool results — the re-paste was pure duplication, and compaction summarized the 40 repeated copies into "the user's goal", making stale steering unremovable by/goal clear./loopcontroller ever wrote.complete, so a/goalsteered every future turn forever./loopnever stopped naturally: with no completion signal,continuationDecisionburned all 25 iterations on one-turn prompts./resume, leaking one conversation's checklist into another session's goal.Changes
src/goal_state.zig(new, pure, unit-tested): epochs,parkSuperseded,steeringGate(diff-gating),completionDecision, supersession notes,applyGoalSteering.Goal.epoch+TodoItem.epoch;todo_writestamps items with the authoring goal (codexThreadGoal.goal_id; opencode session-keyedTodoTable; cline task-scoped focus chain)./goal <new>parks the old checklist and queues a one-shot note ported from codex'sobjective_updated.md: "the new objective supersedes… avoid continuing work that only served the previous objective."/goal clearcloses its checklist. A fresh goal adopts an in-flight pre-goal plan.render_diff; clineshouldIncludeFocusChainInstructions). Compaction invalidates the fingerprint so steering is re-stated in full afterwards (codexworld_state_baseline = Noneon history rewrite).attempt_completionsetsgoal.status = .complete(+ trace note), with a Cline-style double-check when current-epoch items are open — first call refused with the open list, an explicit second call closes anyway./loopnatural finish: a turn with zero tool calls ends the loop (codexRegularTasksemantics —attempt_completionis budget-exempt and doesn't count); the harness note now states the completion contract.todos(+goal.epoch); resume replaces the list wholesale and reconciles a restored-active goal with a finished checklist to.complete. Legacy sessions load unchanged (epoch defaults to 0).392/392 tests pass; all modules stay ≤600 lines (new logic lives in
goal_state.zig;renderTodosmoved there fromagent.zig).Follow-ups deliberately not in this PR (from the same review)
.graff/; neither codex nor opencode has this — cline's checkpoint lock is the model.InternalModelContextFragment); diff-gating removes most of the amplification, the transport change is the endgame.rejectToolCallearly-out onself.sub), and Esc doesn't signal or kill background subagents.goal_set/goal_replaced/goal_status_changed/todos_replacedevents (this PR adds tracer notes at goal transitions as a first step).🤖 Generated with Claude Code
https://claude.ai/code/session_01XjuHQ7vTXWYnVAfUo5EWqH