fix(tasks): plan panel showed only the latest step, not the full plan

Root cause: proposePlan unconditionally deleted and replaced the whole
session_plan_steps list on every call. The model isn't strictly held to
"call propose_plan once with the full list" — nothing stopped it (and
production evidence + live testing showed it happening) from calling
propose_plan once per step as it worked. Each such call wiped every
already-completed step, so the operator only ever saw the model's latest
single step ("1/1") instead of the real, growing plan.

Fix, two layers:
- store.go: proposePlan now only does a destructive replace when no step
  has left 'pending' yet (a genuine pre-execution revision). Once any step
  has started, a new call APPENDS after the current max seq instead of
  wiping — so the panel accumulates the full history regardless of how the
  model chooses to call the tool. plan.proposed now carries `appended` so
  the frontend knows whether to replace or append.
- workspace.ts: plan.proposed handler respects `appended` (update vs set).
- tasks.go / SOUL.md: strengthened the propose_plan description and task-
  loop guidance to call it ONCE with the complete step list end-to-end,
  using update_plan_step (not re-calling propose_plan) to advance — fixing
  the root behavioral cause, with the store-side append as a safety net
  that holds even if the model still calls it incrementally.

Verified: forced the exact incremental-call pattern (propose_plan with 1
step, mark it running, propose_plan again with 1 more step) — the second
call appended at seq 2 instead of erasing seq 1, and its plan.proposed
event carried appended=true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 14:07:10 +02:00
parent 991e7d0900
commit 5384499903
4 changed files with 63 additions and 26 deletions

View File

@@ -57,12 +57,18 @@ Each conversation is a **task**: a goal the operator wants achieved, from
approach, or a failure to avoid. This is how tasks compound: each one's
recorded outcome becomes the next one's prior. Don't skip it and rediscover a
known problem.
2. **Plan, then execute.** Gather what you need, propose a plan, get the single
approval, and carry it out end-to-end (see the plan/approval sections below).
If you hit a genuine decision only the operator can make — an ambiguous
target, a trade-off, missing information — call `ask_operator` with the
options and the entities involved, then STOP and wait; their answer resumes
you. Don't ask about things you can settle yourself with tools.
2. **Plan, then execute.** Gather what you need, then call `propose_plan` ONCE
with the COMPLETE ordered list of every step end-to-end — not one call per
step. The operator watches this list in the context panel; if you call
`propose_plan` again for each step as you go, each call replaces what they
see with just that one step, and the plan looks like it's stuck at "1/1"
forever instead of showing real progress. Get the single approval, then
carry the whole plan out end-to-end, advancing steps with
`update_plan_step` (see the plan/approval sections below). If you hit a
genuine decision only the operator can make — an ambiguous target, a
trade-off, missing information — call `ask_operator` with the options and
the entities involved, then STOP and wait; their answer resumes you. Don't
ask about things you can settle yourself with tools.
3. **Finish explicitly with `complete_task`.** When the goal is verified done —
or you've genuinely failed or only partially succeeded — call `complete_task`
with the `outcome` (success/failure/partial) and a one-line `summary`. This