You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(specialist): zeromaxing posture and orchestrate plan execution (supersedes #829) - #1205
Supersedes #829 by @gnanam1990: the zeromaxing posture and orchestrate plan execution. It carries all 195 of that PR's commits unchanged, with authorship preserved, brings them onto current main, and fixes the review findings that were still open on #829's last heads.
The feature itself, its design notes and its review guide are as described in #829. This PR changes only three things: the integration with main, the copies of the two dependency packages, and the review fixes listed below.
How this branch is built
c4aa2192 is a merge of feat(specialist): zeromaxing posture and orchestrate plan execution #829's head (6916f853) into main (e034a938). The 195 original commits are its second parent, byte-identical (git range-diff is the identity). I tried a linear replay first and dropped it: 189 of the commits conflict, and the automated resolutions silently reverted main's code. All conflict resolution is therefore in this one merge commit, and its message lists every non-trivial combination. Highlights:
the Gitlawb/zero imports are rewritten to Twigpine/zero;
main's config-file locks, maxTurnsSet, specialist lifecycle hooks, tty-gated interactive guard, file-view refresh and locked model selection are kept alongside the PR's equivalents;
Plan scratchpad write access: handled as @Vasanthdev2004 reframed it on 6916f853, not by making the grant read-only. Record now creates its files with O_EXCL, and the false "exactly one writer" invariant comment is corrected. (7cb90565)
Wall budget while paused: the watcher is suspended while dispatch is held, synchronised with the pause transition. Only idle time is credited, so a pause no longer refunds time a task spent running. (4f8156b6)
Present non-array or null depends_on / tools: now refused, both inline and from saved plans. (a2e55a12)
Read grants: these now reach ordinary Task launches and resumes, always as --add-read-dir. (30769f72)
Fractional budget values: refused instead of truncated. (ec68fa65)
Restart/resume staging: it never replaces a user plan of the same name, and the staged copy is removed once the plan is admitted. (86a314cd)
Completed-session records: they expire with their own retention, and a stale expiry never removes a newer record. (0fd0d14c)
Budget numbers sent as strings: all six integer budget fields go through one exact decoder. It accepts whole numbers and numeric strings, like intArg, and a present key is never read as absent. (ec68fa65)
Write roots near the parent tree: an isolated child no longer receives a write root that contains, or sits inside, the parent workspace. (5e48344b)
Turn-scoped grants: these reach only children that finish within the turn. (500f250f)
Per-task token cap:max_tokens_per_task now bounds the task across its retries, not each attempt. (d1903065)
Live meter: it counts the event that trips the per-task cap, and every event after it. (83325fc6)
Pause longer than the remaining wall budget: fixed by 4f8156b6, above.
Case-variant task ids: refused at admission. (c6cc4efe)
Links at a scratchpad destination: closed by O_EXCL. (7cb90565)
Smaller items:
failed task output stays off the shared pad (2f39c31a);
the per-task cap overflow check no longer fails open (cb4bc4de);
non-stall retries no longer spend the stall budget (1851b611);
genuine failures are reported as failed, not cancelled (f9136002);
the dead budgetLeft store is removed (1c1f8e60).
Every behaviour fix has a regression test, and each test was confirmed to fail with its production change reverted.
The older unresolved inline threads from 31 July are already handled on this head: control-byte sanitising, /effort zeromaxing pending guard, chip hit-testing by column, and modal guards on the hit-testers.
Behaviour changes worth knowing
Case-variant task ids: a saved plan containing two task ids that differ only in case now fails to load, with an error naming both ids.
Staged plan names: a staged restart/resume plan may be named last_run_2 (and so on) when the plain name belongs to a user plan. The notice always shows the actual name.
Background and plan children: background Tasks, plan tasks and swarm members no longer inherit the current turn's temporary write grants.
Still open, for maintainers
Scope: this is still one large PR (≈273 files). @jatmn's proposed seam-plus-leaves split remains the alternative to landing it whole.
a read-only plan fan-out can be reached from repository content once the posture is on;
how the write-plan prompt interacts with auto permission modes.
Found while fixing, not fixed here:
zero exec --resume refuses child sessions (IsResumableKind), so a specialist Task resume appears to fail at the child on current main as well.
The orchestrate tool's own read-root snapshot still forwards turn-scoped read grants, read-only, to background plan tasks.
TestRunCommandsDoNotLaunchTUI (internal/cli, on main since feat: add Go worktree and verification backend #70) runs the real worktrees prepare and leaves a locked worktree under ~/.local/state/zero/worktrees. It needs its own PR.
Follow-ups from review of this PR
Rerunning a write-capable plan: it was refused because a PID-less lease outlived each run. Each execution now gets its own tree, the lease names this process, and Release drops it while keeping the tree. (76649ea1)
Resume identity: it now covers depends_on in declared order, because the briefing spends its budget in that order. (bc56e1e6)
Session budget: only admitted sub-agents are counted; refusals no longer inflate the count. (6c680474)
Background TaskOutput: it reports tool calls, not distinct tool names. (781737f4)
Approval card: it decodes the budget with the same decoder as admission, so numeric strings are no longer shown as "no token limit", and it now says when a plan runs in the background. (be207465)
Windows CI fixes:
the cli test binary routes the sandbox self-dispatch subcommands, so the startup shell probe no longer runs the whole suite as a "helper" (0dd689f9);
the probe now releases its plan's runtime lease (acfc011d);
the stall clock starts at child launch, not before the parent's preparation (ce4d3a26).
Weak tests: strengthened to go through the real paths, namely the TaskOutput meta, config persistence and the completion-model tests. (781737f4, 4d7bd8b9)
Validation
All of these ran on 4d7bd8b9, with TZ=UTC, a short TMPDIR, and HOME/XDG_* pointed at a scratch folder:
make fmt-check, go vet ./...: pass
make test (go test ./... -race -count=1): pass, 91 packages
go run ./cmd/zero-release build and smoke: pass
make vulncheck: no vulnerabilities
git diff main --check: clean
make lint-static: 4 staticcheck quick-fixes (QF1001/QF1008), all in files this PR does not touch (internal/installtest/workflow_permissions_test.go, internal/proxydial/proxydial.go, internal/tools/web_fetch.go). They are already on main and need their own PR.
zeromaxing is thorough with double the turn budget again: 320 tool-turns, effort
high, self-correction armed. It fills only what the caller left unset, exactly
as the other profiles do, so an explicit flag and a --mode preset both still win
— precedence stays enforced by ordering, not by a new check.
ONE NAME. /effort zeromaxing, /profile zeromaxing, --exec-profile zeromaxing and
--reasoning-effort zeromaxing all name the same thing, and nothing else does.
No aliases: "max", "deep" and "deepmode" resolve to nothing, asserted by test,
because a second spelling is a second thing to keep in step.
"max" is RESERVED and untouched. ValidReasoningEffort still accepts
ReasoningEffortMax, so /effort max and --reasoning-effort max behave exactly as
they did — parsed as a raw provider level, rejected by the flag parser, and
unsupported by every current model. That spelling stays free for a real provider
rung; claiming it for a Zero posture would burn it.
HONEST DELTA, stated to the user rather than left in a commit message: the turn
budget is the ONLY mechanical change over thorough. Effort is already at the
ceiling and self-correction is already on. execprofile.Delta carries that
sentence to /effort, /profile and the exec selection notice.
ReasoningEffort is "high", pinned by test rather than by comment.
TestZeromaxingReasoningEffortStaysHigh explains why: every level above "high"
falls through the providers' effort mappers into their default arm — anthropic
and gemini both compute a thinking budget of 0 (extended thinking DISABLED) and
the openai mapper drops the field entirely. Raising it would silently turn
reasoning off while the UI claimed a higher posture. Raising the real ceiling
means teaching the providers those tiers first.
The raised budget propagates to spawned sub-agents. Deliberate — this is a
maximal posture — so it is asserted by test and stated in Delta rather than left
to be discovered.
SelectionRefusal is the single authority both selection paths consult. The
headless exec path and the TUI paths already apply a profile's knobs through
different code with different state; letting each decide SELECTION independently
is how a rule gets applied to one call path and omitted from its sibling. Config
gating follows mergeProjectConfig's tighten-only template: a project
.zero/config.json may set profiles.disableZeromaxing (DISABLE) but can never
clear it, so a cloned repo cannot switch a cost multiplier on for whoever opens
it — an attempted enable is dropped silently, exactly like an ignored network
"allow".
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
Four model-facing notices tell the model when its own posture flips: enter
(once, on the first turn after activation), a budget guideline beside it,
still-on (every continuing turn), and exit (once, after deactivation).
WHERE THEY GO IS THE WHOLE DESIGN. Every notice is appended to the CONVERSATION
tail as a user-role message, on the same channel as the existing diagnostics
nudge and the failure/plan hints. None of it reaches buildSystemPromptParts.
The system prompt and tool definitions are the provider's cached prefix — the
anthropic mapper puts its cache_control breakpoint on the last system block and
the last tool — and #760 made that prefix build once per run precisely so it
stays byte-identical across turns. Per-turn text above that breakpoint
invalidates the cache every turn, roughly doubling input cost, and nothing in
the system reports it.
So ZeromaxingStillOnNotice is a fixed literal: no turn counter, no remaining
budget, no timestamp. A test asserts it carries no digits at all, because the
hazard is not today's placement but a future edit that adds a varying part and
then moves the text into prompt assembly.
The reminders also promise NO orchestration — no workers, no fan-out, no
workflow tool. Phase 1 ships none of that, and a prompt advertising capabilities
the run does not have is a prompt-level lie the model will try to act on. A test
enforces the vocabulary.
The state machine is one pure function of (posture, turn) with no memory, so
"enter exactly once" and "still-on never on the first turn" are assertions about
a table rather than about observed side effects.
Proof, both shapes of the hazard, each caught by a DIFFERENT assertion:
- per-turn text written into messages[0] -> the prefix-extension assertion
- static text baked into the system prompt -> the explicit leak assertion
TestRunPreservesRequestPrefixAcrossTurnsUnderZeromaxing is the zeromaxing
variant of the existing prefix test; the openai one is its wire-level sibling,
proving the mapper serializes an append-only conversation into an append-only
request body. A third openai test pins that the posture name can never appear in
a serialized request at all.
Options.Zeromaxing is separate from Options.Profile because the posture arms no
escalation triggers, so Profile.Policy() returns nil for it and could not carry
this. Its zero value is Off, leaving every existing caller byte-identical.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…paths
/effort zeromaxing is the primary way in; /profile zeromaxing is its alias. It
is handled beside "auto" with an EARLY RETURN, before the ReasoningEffort
conversion and before ValidReasoningEffort — it is a posture name, not a
provider level, and must never become one.
It DELEGATES to handleProfileCommand rather than re-applying the knobs, so
"both entry points resolve identically" is true by construction instead of by
two implementations a test hopes agree. The CLI mirrors it:
normalizeZeromaxingEffort folds --reasoning-effort zeromaxing into the
equivalent --exec-profile selection before mode/profile expansion, leaving the
documented precedence ordering untouched. A conflicting --exec-profile is a
usage error rather than a silent winner.
THE LOAD-BEARING GUARD: forwardedReasoningEffort refuses to forward the posture
name for any model, including catalog-unknown ones where unrecognized values
otherwise pass straight through. Normalization already prevents it reaching
there; the guard is what makes a regression upstream fail loudly instead of
sending a provider a parameter value no provider defines.
"max" stays RESERVED and unchanged through all of this: the flag parser still
rejects it, the normalizer passes it through untouched, and /effort max still
reports it unsupported exactly as before.
Driving the real binary is what found the one real gap here: the flag parser
rejected "zeromaxing" outright, so the entire --reasoning-effort entry point was
dead at the user surface while every unit test passed, because they called the
normalizer directly. TestReasoningEffortFlagAcceptsZeromaxingButNotMax is that
regression, and it fails in both directions.
Degrade honestly — this closes a real asymmetry. exec already told the user via
reasoningEffortNotice when a model could not take the requested effort; the TUI
skipped the fill in SILENCE. Same rule, one of two call paths. Both status
surfaces now render ONE shared resolved-state line
("effort: high · profile: zeromaxing · turns: 320") plus the real delta, so a
user sees what actually reached the provider rather than just a posture name.
reconcileProfileAfterModelSwitch is the sibling re-derive site and records the
same reason, so a switch that drops the effort cannot leave the status implying
a raise the run is not making.
revertExecProfile gains a FOURTH knob. It is knob-by-knob, which is exactly
where a new one gets forgotten, so the posture's restore is asserted alongside
budget, effort and self-correct — and gated on zeromaxing specifically, so
/effort auto under fast/thorough keeps its existing meaning of "clear the
effort" instead of dropping the whole profile.
The footer carries a ZEROMAXING chip beside the effort chip while the posture is
on, and drops it while Exiting — by then the posture is already off and only its
announcement is pending.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…wn state
Two corrections to what zeromaxing tells the user, both cases of documentation
describing behaviour that is not happening.
1. --spec-reasoning-effort zeromaxing stays rejected — it is a run posture and
has no meaning for a spec draft — but the generic "Expected low, medium, or
high" read like a bug to anyone who had just learned the name works on
--reasoning-effort. It now says why it does not apply and names both flags
that do what the user was reaching for. A genuinely unknown value keeps the
plain message: the explanation is specific to the posture name, not a new
blanket wording.
2. Delta's self-correct clause described the change from THOROUGH, not from the
caller. Telling a user sitting on the LSP-only default that self-correction
is "already armed" while silently moving them to the full project test plan
is exactly the failure this posture's honesty rules exist to prevent. Delta
is now a function of the caller's actual state and renders one of three
transitions, in /selfcorrect's own vocabulary:
self-correct: lsp -> tests (the common case)
self-correct: unchanged (tests) (genuinely unchanged)
self-correct: lsp (your /selfcorrect choice ...) (user override wins)
The third exists because the TUI lets a user turn it back off after selecting
the posture; reporting a raise there would be a third way to describe
behaviour that is not happening. The budget and effort clauses stay fixed —
effort genuinely is caller-independent, since "high" is the ceiling every
provider accepts.
The headless path captures self-correct BEFORE applyExecProfile, which arms it
as a side effect; reading it afterwards would make every run report "unchanged".
The TUI reads its LIVE state rather than selection-time state, so a later
/selfcorrect off stops the status claiming a raise the session no longer has.
A unit test on the transition helper cannot catch the capture being read at the
wrong moment, because it never runs the code that captures — so the ordering is
pinned by a test that drives the real exec path and reads stderr. That gap was
found by mutation, not by review.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…t vouch for
Three defects found by running the real binary, all in what the posture reports
or applies.
1. THE FILL SILENTLY DID NOT HAPPEN on a custom endpoint. /effort zeromaxing
left the effort at "auto" while --exec-profile zeromaxing on the SAME model
sent reasoning_effort:"high" on the wire. Same posture, same model, two
answers — the fifteenth instance of one rule applied to one of two call
paths.
The fill site used reasoningEffortAllowed, which returns false both for a
model KNOWN to lack the level and for a model the catalog has never heard of.
The headless path already distinguishes them (an unknown model forwards the
requested effort "since no support claim can be made for it"), and so does
the TUI's own model-SWITCH site, which takes a ringKnown flag for exactly
this reason. Only the profile-fill site conflated them.
profileEffortApplies now fills when the model lists the level OR when no
catalog entry exists, and declines only when an authoritative empty ring says
the model has no reasoning controls. A model name that is empty is a third
case and still declines — there is nothing to make a support claim about.
That last distinction was caught by the pre-existing fast-posture test, which
the first version of this fix broke.
THE HOLE THAT HID IT: every test asserted a surface against its OWN
expectation — the TUI tests against the TUI's rule, the CLI tests against the
CLI's. Both suites were green while the two paths disagreed, because nothing
compared them against each other for the same model. A unit test on either
helper could not have caught it.
TestProfileEffortFillAgreesWithTheHeadlessPath is that missing comparison,
run across a catalog reasoning model, an inferred one, a catalog model with
an authoritative empty ring, and an unknown endpoint.
2. THE OUTPUT CONTRADICTED ITSELF: "reasoning effort: unchanged — already at
the highest level" and "NOT raised to high: the active model does not support
that level", in the same card. Both cannot be true. The fixed clause lived in
Delta while the refusal lived in a separate line, so they were only ever
consistent by coincidence.
Delta now renders every clause from one DeltaState, with the effort clause a
single switch over EffortTransition. A contradiction is unrepresentable
rather than merely unlikely, and the separate line is gone.
3. THE BUDGET CLAUSE compared against thorough — "(thorough uses 160)" — which
is information about two profiles, not about the user. It now states the
caller's own transition, "turn budget: 80 → 320", with distinct renderings
for an already-at-320 session and an unknown current budget.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…ing controls
PRE-EXISTING defect, surfaced while investigating the zeromaxing posture and
verified on origin/main. It is not something the posture introduced.
availableReasoningEfforts() returns an EMPTY ring for two very different
situations: a catalog model that genuinely has no reasoning controls (gpt-4.1),
and a model with no catalog entry at all (the reporter's glm-5.2 on
ollama-cloud, any custom openai-compatible endpoint). Every TUI consumer treated
the second as if it were the first.
That single fact produced two visible failures at once:
- /effort listed no levels and reported "no reasoning controls on this model"
- /effort high was REFUSED outright
- and it is why a profile's effort fill was declined with "the model does not
support that level"
Meanwhile the headless path has always disagreed. On origin/main, for the same
glm-5.2: forwardedReasoningEffort returns "high" and emits no notice, because an
unknown model "forwards the requested value as-is, since no support claim can be
made for it". So the CLI forwarded the level while the TUI refused to let the
user set it — one rule, two answers, predating this feature.
There are THREE consumers of "does this model take this level?": a manual
/effort, a profile's fill, and the headless forwarding decision. They now share
one rule — a catalog entry is authoritative and refuses; no entry means Zero
cannot vouch either way and does not block. TestEffortSettabilityAgreesAcross-
AllThreeConsumers pins that they agree, per model, including the reporter's.
/effort also distinguishes the two states in its output, because "this model has
no reasoning controls" and "Zero has no entry for this model" are different
facts and rendered identically before. Setting a level on an unlisted model now
says plainly that support is unconfirmed and an unsupported value is ignored by
the provider.
The regression tests drive the real /effort path rather than the helper. A green
helper test alongside an empty user surface is how this feature has now produced
four defects: the flag-parser hole, the self-correct capture point, the identity
test passing through the wrong gate, and this.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
ZeroMaxing Phase 2: an `orchestrate` tool that accepts a plan as typed tool
arguments, validates it, records it as session events, and runs it SEQUENTIALLY
through the same specialist path a Task call uses. The deliverable is DATA — is
fan-out worth building at all — not concurrency.
ADDITIVE. With the posture off, the advertised tool set, the tool-definition
bytes and the assembled prompt are byte-identical to a build without this
feature, proved by TestPostureOffPrefixUnchangedByRegisteringTheTool: two
registries differing only in whether the tool is registered, asserted equal
under BOTH auto and unsafe. The enforcing mechanism is Safety() returning
PermissionDeny, not Deferred() — deferral only hides anything when it is ACTIVE,
and an unsafe session with deferral inactive would otherwise have advertised the
tool. Deferred() stays as a second layer and is now asserted rather than assumed.
VALIDATION CANNOT BE SKIPPED. Plan's fields are unexported and ParsePlan is the
only constructor, so there is no path from tool arguments to an executable plan
that bypasses it — the prototype's validator was reachable, correct, and never
called. Rejected by default: ids by ALLOW-LIST charset, unique; every depends_on
resolves (unknown edges rejected, never skipped); the graph is acyclic by Kahn's
with the involved ids NAMED (audit U24 — nothing in this tree checked, and a
cyclic graph hangs forever); task count capped; tools read-only and within the
parent's grant; a budget with max_tokens required and max_workers exactly 1,
REJECTED rather than coerced so the field stays meaningful for Phase 3; and
depth checked at admission with the remaining headroom named, rather than an
opaque failure mid-plan.
ONE counting function — a length, not a text scan. The prototype counted source
text, so `agent ("x")` with one space counted as zero and executed anyway.
BUDGET ENFORCED AT DISPATCH, not merely validated. Under this posture every
child inherits a 320-turn ceiling, so a twenty-task plan authorises 6,400 child
turns from one tool call. The grant is intersected again at dispatch too, so a
validator bug cannot widen a task's authority.
FAILURE IS NOT SUCCESS. A failed task skips its transitive dependents, each
RECORDED with the dependency that blocked it; independent siblings still run;
the plan runs to exhaustion. Partial is its own terminal status and the tool
returns StatusError for it, so nineteen of twenty failing can never surface as a
clean result (audit RC-F).
THE METRIC is value, not safety: max_speedup = sequential_total / critical_path,
computed from recorded per-task durations over the declared DAG and surfaced in
the summary and in Meta. Kill criterion: median >= 2.0x across >= 20 real plans,
or Phase 3 is not built. Independence-violation would answer safety, but
read-only tasks are always safe to parallelise, so it would read 0 and decide
nothing.
PermissionAllow when the posture is on, deliberately, with the reasoning in the
code so Phase 3 does not inherit it blind: the approval surface renders only the
tool name and a static sentence — PermissionRequest carries Args but no renderer
reads it — so a prompt could not show the plan it was gating and would train
click-through. What bounds it instead is enforced, not advisory: read-only at
validation and dispatch, a required budget enforced at dispatch, and the posture
itself as explicit consent. When tasks can WRITE, an approval gate becomes
necessary and needs a real renderer first.
Plan state is recorded as five ordinary session events beside the specialist
ones; no new store. Recording is best-effort and never fails the run.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…it 4
Wires Phase 2 into the real binary and maps a plan that did not fully complete
onto the existing incomplete exit path.
POSTURE CALLBACK — a POINTER, not a closure. The TUI model is a VALUE type
(every handler takes and returns `m model`), so a func() bool closing over it
would capture a copy frozen at registration and report the posture as it was
when the session started, forever. Re-registration fails for the reason
decision 2 rejected it earlier and worse: the TUI clones the registry per run
and the clone copies tool POINTERS, so a replacement registered into the session
registry would never reach a run already holding a clone. specialist.PostureGate
is one shared atomic flag the tool holds for the process's life; every clone
sees the same one and a posture flip needs no re-registration.
PLAN RUNNER — captures only run-INVARIANT state (executor, workspace, parent
identity and policy) and NO context. The ctx it uses is the one ExecutePlan
hands it per task, so a cancelled run cancels the task in flight; it also checks
ctx.Err() before launching, so a cancelled plan does not spend another task's
budget. Capturing a context at construction is precisely how the prototype's
background goroutine kept running after cancellation. The runner is synchronous
and holds no goroutine, so it cannot outlive its plan.
PLAN RECORDER — preserves execSessionRecorder's best-effort contract exactly.
Every bridge method returns nothing, so a recording failure has no path back
into ExecutePlan and cannot abort a plan mid-flight; nil is a no-op at every
level. The bridge is built before the session exists and its inner recorder
attached afterwards, so events before that point are simply not recorded rather
than fatal.
PARTIAL -> EXIT 4, folded into the EXISTING result.Incomplete path rather than a
second exit route, so a partial plan and a stalled loop report the same way. It
never overrides an incompleteness the loop already found.
A DEFECT FOUND BY DRIVING THE BINARY, not by any test: the budget was enforced
at dispatch against a counter that never moved. NewPlanRunner never populated
TaskResult.Tokens, so a plan with max_tokens:1 ran all three tasks. The unit
test passed because its fake runner fabricated its own token counts — the same
"test the helper, miss the wiring" pattern that produced the flag-parser hole,
the self-correct capture point, and the identity test's wrong gate.
ExecResult gains an additive TotalTokens, populated from the stream summary the
accounting path already computes.
Verified at the real surface: orchestrate absent with the posture off and
present as [shell/allow] with it on in the same auto-mode session; a 4-task
diamond running in topological order with results verbatim and max_speedup
1.33x (hand-checked: four equal tasks over three hops = 4/3); cyclic, write-tool,
max_workers and missing-budget plans each rejected with the offending ids or
values named; and budget exhaustion reporting partial with `echo $?` showing 4.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
Closes the two gaps left by the registration step.
TUI GATE. handleProfileCommand and revertExecProfile write the shared
PostureGate on every transition, threaded through tui.Options in the same shape
as ZeromaxingDisabled. What is proved without a TTY:
- the gate is written on posture ON and OFF through the REAL handlers, for
BOTH entry points (/effort and /profile) — not through a helper, which is
what missed the wiring four times in this feature
- a REFUSED selection (disabled workspace) leaves the gate off
- a nil gate is safe, so a caller that never wires one simply has no tool
- THE cloneToolRegistry HAZARD, proved rather than assumed: a tool reached
from a CLONE taken BEFORE a flip still observes the flip. That is the exact
ordering a captured copy or a closure over this value-typed model would get
wrong, and it is why the gate is a pointer
- concurrent Set/read, for -race: the TUI writes from its update loop while a
run's tool dispatch reads from the agent goroutine
FAILING-TASK VERIFICATION. The stub gained a prompt sentinel that makes one
specific child fail with a real provider-stream error, so a dependency failure
runs end to end rather than being asserted only at the executor. A 4-task
diamond where b fails now reports, verbatim:
Plan partial: 2 succeeded, 1 failed, 1 skipped max_speedup: 1.49x
- a [succeeded] full result verbatim
- b [failed] provider error: sentinel failure
- c [succeeded] independent sibling unaffected
- d [dependency_failed] skipped: dependency "b" did not succeed
Run incomplete (not reported as success) EXIT=4
Every level of the RC-F seam is now covered by something that can fail:
executor status, tool status, recorder, and the process exit code.
THE BUDGET DEFECT GETS ITS OWN GUARD. TestRealRunnerFeedsTheBudgetMeter drives
NewPlanRunner over an Executor whose child reports usage, and asserts a
max_tokens=1 plan launches exactly one child. A fake runner that fabricates its
own token counts cannot catch a producer that never populates them — which is
precisely how the defect survived every executor test. A second test pins that
the runner honours the per-call context rather than a captured one.
registerSpecialistTools regains its original name and takes the wiring
argument directly; the transitional wrapper is gone now that both call sites
pass wiring, so no dead indirection ships.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…n empty
The rule "a plan task may narrow the parent's grant, never widen it" was
declared, documented and validated against -- and both production call sites
left ParentTools nil, so neither the validation check nor the dispatch-time
intersection ever ran. A run restricted to grep handed a plan task read_file.
The empty case was worse. planToolGrant returned an empty slice when a task
declared no tools and the parent grant was empty; resolvedToolAllowlist read an
empty tool list as UNSPECIFIED and expanded it to the default read-only
category. The narrower the parent, the wider the child -- on the default path,
where a task declares no tools at all.
Three changes:
- The grant is now DERIVED inside registerSpecialistTools from specialist's
own exported read-only list, narrowed by the registry and the run's
operator filters. The filters are explicit parameters rather than wiring
fields, so a call site cannot omit them without failing to compile.
- Both intersections are unconditional. The "skip the check when the parent
list is empty" escape hatch is what made the rule inert; an unsupplied
grant is now a wiring bug that fails closed.
- Manifest gains ToolsResolved, so a resolved tool list can say
"deliberately nothing". A bare []string cannot: absence and emptiness are
both len()==0, and omitempty erases an empty slice entirely, so a nil check
would not survive a round trip. Same shape as audit finding M24. The flag's
meaningful value is true, so omitempty only drops the meaningless false.
planToolGrant now refuses an empty result rather than returning one, and
ExecutePlan records that as a task failure with a reason without dispatching
it, so an ungrantable plan reports partial/failed and exits 4.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
reconcileProfileAfterModelSwitch called reasoningEffortAllowed directly while
the fill site called profileEffortApplies, added in 009f12b precisely to stop
treating an uncatalogued model's empty ring as a refusal. The two disagreed on
exactly that case, which is both of this user's configured models (glm-5.2 and
grok-4.5 are catalogued=false with an empty ring): selecting the posture on one
of them filled high, and switching to one with the posture already active
dropped the effort to auto and reported it as a level the model would not take.
e782470 was meant to fix this class at the cause across all consumers. It
updated one door and not the other -- the seventeenth instance of the pattern,
introduced by the fix for the sixteenth.
The rule is now ONE function, profileEffortAppliesOn, that both doors call, and
the ring's authority is passed through rather than inferred from its emptiness.
reconcileProfileAfterModelSwitch takes ringKnown, which its caller already had
and did not forward.
The comment at command_center.go described the old conservatism as deliberate
("only ever applies where support is known"). That is now the bug's
description, so it is replaced rather than left for the next reader to restore.
TestProfileEffortDoorsAgree asserts the doors against EACH OTHER across
catalogued / catalogued-with-no-ring / uncatalogued / no-model. Both doors had
tests before and both passed; nothing compared them, which is why the
disagreement shipped.
Three existing tests used kimi-k2.7-code:cloud as their "unsupported"
destination -- an UNCATALOGUED model, so they were asserting the defect. They
now use gpt-4o, which the catalog vouches has no reasoning controls, so the
genuine drop case stays covered.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
task_completed carried session_id and tokens; task_failed carried neither. So
the one task a user needs to open was the one task the event log could not
point them at -- and the per-task token records did not add up to the plan's
own total.
Executor.Run already goes out of its way to preserve the child session id on a
post-start failure, with a comment saying it exists so a failed child stays
drillable. The recorder discarded it one layer up.
A dependency or budget skip records an empty session id rather than omitting
the field: that task never started a child, and present-and-empty says so,
where an absent field would look like the same drop being fixed here.
The regression test compares the two events against EACH OTHER. Both already
had tests and both passed; nothing compared them, which is how the asymmetry
survived.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
PlanContext.Depth was unset at both call sites, so Limits.CurrentDepth and the
executor's own nesting check both measured a constant zero. The admission
headroom check could never fire. It was only harmless because plan tasks run at
--auto low, where specialist tools do not register, so nothing could re-enter
-- an inert guard that reads like a live one, which is the shape someone later
trusts.
Wired rather than deleted: the value exists (--depth), it costs two lines, and
the check it feeds is the one thing standing between a nested run and
maxSpecialistDepth if a future phase ever lets a plan task spawn.
The TUI passes an explicit 0 with a comment: it is always a root session, so
zero is the measured value there, not an unset one.
Proven at the process boundary rather than by a unit test, since the defect was
the call site and not the helper: before, `exec --depth 7 --exec-profile
zeromaxing` ran a four-task plan; after, it refuses with "a plan's tasks would
run at depth 8, which reaches the maximum nesting depth of 8", while --depth 6
still runs.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…ch other
RULES.md §3: for every rule you touch, find its sibling and prove by test that
both agree -- one test comparing the paths on the same input, not two tests
each asserting their own expectation. The authority rule has two enforcement
points and Workstream A changed both; two sites fixed together is exactly the
shape that drifts apart later.
The relationship asserted is SUBSET, not equality, and the direction is the
point. Admission is stricter: it rejects a task naming anything outside the
grant. Dispatch narrows: it drops such a tool and grants the rest. Requiring
them to refuse identically would assert a symmetry the design does not want --
dispatch only runs on an already-admitted task, so when it disagrees its job is
to hand over less, never more.
Mutation-checked: dropping the parent check from the dispatch intersection is
caught with "the second layer widened authority", which is the defect the
"BELT AND BRACES" comment promises cannot happen.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
A plan ran silently: sequential, synchronous, no output until it finished. Four
things stood between the existing live-progress machinery and the plan tool,
all of them wiring rather than missing capability.
loop.go gated the progress callback on `call.Name == "Task"`, so the second
sub-agent-spawning tool got nil. Keyed now on the TOOL'S OWN DECLARATION
(tools.ChildProgressStreamer) rather than its name: `|| call.Name ==
"orchestrate"` would have been the same defect one name later, which is this
codebase's most repeated shape. Every tool that does not declare keeps the
nil callback it has today -- swarm tools included, deliberately.
NewPlanRunner built TaskRunOptions without Progress and RunWithOptions never
read options.Progress. Either half alone leaves a plan's children streaming to
nobody, so they are ONE change with one end-to-end test. This is finding 7's
class again: a second construction path that does not carry what the first one
did. PlanRunner now takes a PlanTaskRequest struct rather than a widening
parameter list, because each new positional argument is another chance for
that omission.
app.go never set Recorder, so the TUI recorded NONE of the five plan
lifecycle events (finding 9). The recorder is now a required POSITIONAL
parameter of registerSpecialistTools, like the operator filters: forgetting it
is a compile error rather than a silent nil.
TaskCancelled is a distinct outcome. Cancellation marked every remaining task
failed with "context canceled", so a deliberate Ctrl-C on a twenty-task plan
read as nineteen defects. PlanCancelled joins it as a terminal status, and
terminalStatus counts it -- without that a plan with two successes and two
cancellations reported "completed", which is RC-F exactly.
Also fixes a live render-cache collision this change would have made certain.
A rowSpecialist row carries no id and no text; every distinguishing field lives
behind row.specialistInfo, and none of it was in the cache key. Two specialist
cards in one run hashed identically, so a failed sub-agent rendered as a
successful one. Rare with Task (one child per run is typical), guaranteed with a
plan. Proven by test before fixing, then asserted with four cards including a
failure and a cancellation.
Additivity re-proven: posture off, all five permission modes byte-identical
against an origin/main binary with DeferThreshold unset, hashes unchanged.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
Gate 1a made a plan's tasks visible as cards. Cards cannot show the one thing
that makes a plan a plan: its shape. This adds the panel.
The shape is carried on planAdmittedMsg, not assembled as tasks finish.
ParsePlan already proved the graph acyclic and computed the topological order,
so the panel is complete before the first task starts rather than growing a
node at a time. Each task is ranked by dependency depth -- one more than its
deepest dependency -- and indented by it, so a diamond reads as one node, two
beside each other, one node. A chain steps in once per link; independent tasks
share a rung. All three are asserted.
Mounted above the pinned update_plan panel, since it describes work running
right now. It renders NOTHING when no plan has been admitted -- not an empty
box, not a blank line -- which is what keeps a posture-off session unchanged:
the panel never runs, rather than running and producing nothing.
Named orchestrate throughout. /plan (update_plan's TODO list) and /plans (this)
are genuinely different things with near-identical names, so every type, field
and function here says orchestrate and only the user-facing command says plans.
The command lists every task including any the pinned panel had to drop, and
names each task's dependencies explicitly, so the shape survives copied text and
narrow terminals where indentation does not.
Bounded at 12 rows and it states what it hid: a silently truncated list reads as
a complete one. Summaries cut on rune boundaries. The full result stays in the
tool output -- the panel summarises, and a display formatter on a data path is
how a 583-rune work product became 200 mangled runes.
The header clock stops when the plan ends, and when the run ends without a
terminal plan event. It previously kept counting while the turn that produced
the plan carried on, so a 20-second plan read as minutes.
TestSuggestionOverlayCapsRowsWithoutMoreText pinned the first and last entries
of the command palette's first window; inserting /plans pushed /ps out of it.
The window's start and size are unchanged, so the tail anchor moves to
/permissions and the comment says why it is incidental.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
The struct comment always claimed a plan task "inherits exactly the parent's
policy". It did not: both production call sites left PlanTaskContext.ParentModel
empty, so appendModelArgs received no parent model and passed no --model, and
the child fell back to whatever its own config resolved. After a /model switch
or a --model flag, plan tasks ran on a different model than the run that
launched them.
Decided: the documentation states the safer behaviour, so the code moves to
match the doc. Explicitly NOT a router decision -- choosing a cheaper model per
task is a real future feature and would arrive as an explicit plan field on top
of correct inheritance. Silently failing to propagate is not that feature.
The fix is not "populate the field at registration", which would have been
wrong for the TUI: its registry is built once per session while /model changes
the model between runs, so a captured value is stale by design. That is why the
fields were empty rather than merely unset -- they were in the wrong home.
ParentSessionID/ParentModel/ParentReasoningEffort now travel per call on
PlanTaskRequest, read from the same tools.RunOptions fields the Task tool reads,
and are gone from PlanTaskContext so there is no second source to drift from.
Before: parent ran --model parent-chose-this, child reported stub-model.
After: child reports parent-chose-this. Asserted per call, so a second
orchestrate call with a different model launches on that model.
The parent session id travels with it, which is what links a plan task back to
the run that spawned it rather than leaving it an orphan.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
Launching a specialist and resuming one are two doors onto the same question --
which model does this specialist run on -- and they answered it differently.
BuildResumeArgsInput carried no model fields at all and BuildResumeArgs never
called appendModelArgs, so a resumed child was launched with no --model and fell
back to whatever its own config resolved. The same specialist could change model
between its first turn and its second.
Verified by building both argument sets side by side rather than by reading:
FRESH args: exec --init-session-id … --model parent-chose-this …
RESUME args: exec --resume … (no --model)
The fix calls the SHARED helper rather than repeating its rules, so the
manifest-pins-its-own-model precedence is applied once and cannot drift between
the two paths.
The relationship the regression test asserts is EQUALITY (RULES.md §3): fresh
and resumed launches of the same specialist must produce the same model and the
same reasoning effort. A second test drives Executor.Run with Resume set, since
the builder having the fields proves nothing if the call site never fills them
-- which is exactly how the two paths diverged.
PRE-EXISTING main defect, not ZeroMaxing: it affects every resumed specialist.
Separate commit so it can carry its own issue.
Note for Stage 2c: runResume also resolves the manifest by session.AgentName and
ignores params.Manifest, which is why inline-manifest children are unresumable.
Same function, still open, deliberately not fixed here.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
planReadOnlyTools bounds what a plan task may hold; readOnlySpecialistTools
decides whether a specialist counts as read-only for the Task tool's permission
gate. Different questions, same answer -- and they disagreed: lsp_navigate was
in the first and not the second, so a manifest holding exactly the plan grant
failed the read-only check.
The omission was chronology rather than a decision. readOnlySpecialistTools was
written in #243 (2026-06-18); lsp_navigate arrived in #276 two days later and
nobody went back. The tool declares EffectReadOnly with the safety reason "reads
files, modifies nothing" and resolves its path through the same scoped
confinement its read-only siblings use, so it belongs in the set.
Pinned as a SUBSET, not equality, and the direction is the point: anything a
plan task may hold must be something the wider gate also calls read-only. The
reverse is not required -- update_plan is read-only for a specialist and
deliberately not a plan tool.
NOT derived from the tools package's declared capabilities, which would be the
real class fix: update_plan is EffectInteractive, so deriving the set would
silently drop it and change the Task tool's permission gate. That is its own
change with its own reasoning, not a drive-by.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
A read-only run carried ~5 KB of confirmation policy governing actions it could
not take. Measured on a real plan-task child: 8,365 bytes of the 17,903-byte
system prompt -- 47% of it -- were policy for capabilities the child provably
lacked (no write, no edit, no shell, no ask_user), and the system prompt is 71%
of that request. Multiplied by every task in a plan and every specialist
sub-agent.
Keyed on the RESOLVED TOOL SET, never a name or a manifest flag, and fail closed
at every step: a nil registry, a run holding nothing, and any tool whose effect
is not positively read-only all keep the policy. EffectUnknown -- MCP tools,
plugin tools, the specialist tools -- keeps it, which is why an ordinary session
is untouched.
Two things this got wrong first, both caught rather than reasoned away:
It used ToolVisible, which applies the permission mode's ADVERTISING rules on
top of the operator filters. A write tool auto mode does not advertise is
still held and still callable once approved, so a run launched with
--enabled-tools read_file,grep,glob,write_file counted as read-only and lost
its policy -- a fail-OPEN exactly inverse to the purpose. Found by driving the
binary; every unit test passed at the time. It now asks ToolAllowedByFilters,
which answers "does this run hold the tool".
Reading the registered tool set made registering a posture-gated tool change
the prompt with the posture OFF, breaking the additive-only guarantee.
TestPostureOffPrefixUnchangedByRegisteringTheTool caught it, which is what
that test exists for. A tool the run can never call now says nothing about
what the run can do -- and a tool that varies its permission by argument is
never ruled out from its static safety, however that reads.
Verified on the wire: --enabled-tools read_file,grep,glob drops it (12,310-byte
prompt); adding write_file or bash keeps it (18,789). Additivity re-proven
byte-identical across all five permission modes.
Only the confirmation policy is gated. The remaining ~3.3 KB (Editing
discipline, Testing gate, Permission and safety) lives inside system_prompt.md
and needs that file split, which touches the prompt every run shares. Separate
change.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
Two changes from driving a real six-task chain.
THE TOKEN BUDGET IS NOW OPTIONAL, AND OMITTED MEANS UNBOUNDED.
max_tokens was required and capped at 200_000, and a chain asking for exactly
that spent 469,555 -- 2.3x over -- and still lost its last task. The check runs
only BETWEEN tasks: a task is dispatched whenever any budget remains and then
spends whatever it spends, so the cap bounded nothing while stopping real work
from finishing. A number that neither bounds spend nor lets a heavy plan
complete is worse than no number, because it reads like a guarantee.
Spend is still METERED and reported everywhere it was -- PlanReport.TokensUsed,
the plan_completed event, the panel footer -- so an unbounded plan is not an
unmeasured one. A caller that wants a bound still sets max_tokens and gets the
same dispatch-time behaviour as before, asserted by test in both directions.
This removes the only ceiling on what one orchestrate call can spend. Under the
zeromaxing posture a twenty-task plan authorises 6,400 child turns. That is the
user's decision, taken after seeing the cap fail to do the job it existed for.
THE PANEL COLLAPSES.
A six-task chain took seven footer lines for the whole run, pushing the
conversation up for detail that is one keypress away. The header alone is the
state; the tasks are detail. Click the header line to open it -- the interaction
that was actually asked for -- or Ctrl+G.
NOT Ctrl+O, which already toggles the detailed transcript view: the first
attempt bound it there and the existing handler silently won. Not Ctrl+P
either, which belongs to the update_plan panel.
The dependency indent is capped at five levels. A twenty-task chain adds a rung
per link, which would indent forty columns and run off a narrow terminal; the
depths still reflect the real graph, only the drawing stops. Past the cap every
task sits at one depth, which is honest for a chain.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
TWO SEPARATE THINGS FROM DRIVING A REAL PLAN.
Finished tasks now drop out of the panel after a 5s linger, so it tracks live
work instead of accumulating a transcript of it. Long enough to see a task land,
short enough that a six-task chain does not fill the footer with history.
Matches how the AGENTS sidebar already retires finished agents.
A faded task is hidden, not forgotten: the header still counts it, and /plans
still lists the whole plan. That matters, because the dependency shape fades
with the tasks -- a diamond stops looking like a diamond once its first task
goes. /plans is where the shape stays readable for the whole run, and it names
each task's dependencies explicitly rather than relying on indentation.
/effort NOW SAYS WHAT IT ACCEPTS.
On glm-5.2 the card reported "available: not listed - model is not in Zero's
catalog" and stopped. Nothing typeable, on the model this user actually runs.
And on a catalogued model it listed low/medium/high while omitting zeromaxing --
which is selected through this very command, and which the actions line already
offered.
"available" (what the CATALOG vouches for) and "you can set" (what the command
accepts) are different questions and were conflated into one line. They are now
two fields.
The distinction is enforced, not decorative:
- catalogued with a ring -> exactly that ring
- catalogued with NO ring -> nothing but the posture. gpt-4o genuinely has no
reasoning controls and the command refuses every
level there, so offering them would advertise
what the command rejects
- not catalogued -> low/medium/high, because Zero cannot vouch
either way and the headless path forwards them
- posture disabled -> the posture is dropped from BOTH the list and
the actions line, since suggesting a command the
run will refuse is worse than saying nothing
The card and the command are two doors onto "which efforts can I set", so the
parity test feeds every advertised value back through the real command rather
than comparing two lists -- a list comparison would pass while the command
rejected all of them.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
The popup that /effort opens showed a single "auto" row on glm-5.2. Reported
from use, with a screenshot; I had just told the user no such picker existed,
which was wrong -- newEffortPicker has been there all along.
It read availableReasoningEfforts(), the CATALOG's answer. A model with no
catalog entry has an empty ring, so the picker offered nothing settable -- on
the model this user runs. And it never offered the zeromaxing posture on ANY
model, even though picking one routes straight into handleEffortCommand, which
accepts it.
The picker now shares settableEfforts() with the /effort card, so the three
surfaces that answer "which efforts can I set" -- card, picker, command --
cannot disagree. Catalog authority is preserved: gpt-4o, which the catalog
vouches has no reasoning controls, still offers only auto and the posture.
It also preselects the active posture. Under zeromaxing m.reasoningEffort holds
"high" -- the level the posture FILLED -- so preselecting from that alone would
highlight the wrong row.
The parity test now feeds every value from BOTH surfaces back through the real
command, and asserts the two offer the same set. The picker is the surface that
actually failed in use while the card's own test passed, which is the argument
for testing them against each other rather than each against its own
expectation.
Two existing tests encoded the old behaviour. One asserted "[auto] as the only
effort option on an unsupported model" -- the reported bug, written down as an
expectation; an uncatalogued model is not an unsupported one, and its levels are
forwarded. The other pressed enter expecting to land on auto, which only held
while auto was the sole row; it now selects auto deliberately and asserts the
preselection instead.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
All five from one screenshot of a six-task chain, all in what the run REPORTED
rather than in what it did.
"error (exit code 0)" on a failed task, directly above a body reading
"Subagent failed (exit 4)" -- the card contradicting its own detail. A plan
task's failure carries no exit code, and rendering the zero value claimed one
it did not have. Only shown when non-zero now.
Dependency-skipped tasks rendered "error". specialistCancelled landed in
specialistStatusString's default arm, so a plan with ONE real failure showed
three errors: the failure plus every task skipped behind it. The default stays
"error" so an unmapped status still fails closed.
"0 tokens" in the specialists rollup while the plan reported 130,135. Nothing
has ever populated specialistInfo.tokenCount -- not for plan tasks, not for
Task sub-agents -- so the total was always zero. The segment is now omitted at
zero, matching the per-card rule (M18): a number that looks measured and is
not is worse than no number.
A plan task's spend now reaches its card, so the rollup adds up to what the
plan reports. Task sub-agents still bridge no usage; that gap is unchanged and
now shows as an absent segment rather than a false zero.
The sidebar said "no active plan" while the panel two lines below showed one
mid-flight. Its PLAN section reads the update_plan TODO list and knew nothing
about orchestrate plans. It now shows a one-line summary -- name, done/total,
failures, the task in flight -- when an orchestrate plan is running. EXACTLY
one line, because the FILES section below computes its click offsets from the
lines above it.
The sidebar test drives renderContextSidebar rather than the line helper: the
first version called the helper directly, and the mutation that removes the
section from the sidebar entirely still passed.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
The footer read "budget 0/200000 tokens" for an entire run while the cards
directly above it showed 84,953 spent. tokensUsed was assigned in exactly one
place -- from plan_completed, which arrives when the whole plan ends -- so the
one number a user watches that line for was zero for as long as it mattered.
Spend now accumulates as each task finishes. The executor's own total still
wins at the end, since it counts every task including any whose message the
panel dropped as stale, but a plan that reports no total no longer erases what
was counted.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
Pressing the PLAN header opens a two-pane drill-in: every phase down the left,
the selected phase's live agent state on the right — status, elapsed,
dependencies, tool count, spend, its prompt, what it is doing right now, and
where it ended up. Up/down moves, enter drills into the child session, esc
closes.
An OVERLAY, in the same slot the help and picker overlays use, so it composites
over the chat rather than replacing it. Nothing about the transcript, footer or
composer changes, and it renders nothing when closed or when no plan has been
admitted — which is what keeps a posture-off session unchanged. No separate
posture gate: a plan only exists under the posture, so without one there is no
PLAN line to press.
The panel's own expand/collapse stays on ctrl+g, for a glance without leaving
the conversation.
Opening lands on the task worth reading: the one running, else the first that
failed, else the first task. The selection clamps rather than wraps.
Enter is offered ONLY when the selected task has a child session to open. A
running task is still keyed by a temporary id, and a hint for a key that does
nothing trains the user to ignore hints.
The phase list marks the task in flight. Running and pending both fell into the
default glyph, so the list could not show what was actually happening.
Prompts are SUMMARISED and say when they were cut; the full text stays in the
tool output. A task that never started shows what it is waiting on rather than
a row of zeroes.
Eight mutations, all caught. Two needed the tests fixed first: the running-task
assertion compared whole rows, which differ by the selection marker anyway, so
removing the in-flight glyph passed; and nothing drove the click path at all.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
The right-hand column showed one grey line — "trace-chain 3/6 · helpers" — for
a plan the panel below it was rendering in full. It now carries the plan
properly: a progress count in the header and one coloured line per task, in the
same shape the update_plan steps use so the section reads consistently whichever
plan is in it.
green ✓ done · red ✗ failed · accent • in flight · faint ○ pending
faint ⊘ skipped or cancelled — deliberately NOT red, since neither is a defect
The header count turns red on a failure and green when everything is done, so
the one thing worth noticing carries from across the screen.
THE LINES LIVE INSIDE sidebarPlanLines, and that is not a detail.
sidebarFileSelectables computes the FILES section's click offsets from
len(sidebarPlanLines); the previous one-liner sat beside the placeholder and was
safe only because it was exactly one line. Rendering a whole section there would
have misdirected every file click by the number of lines added, silently. Inside
the function the arithmetic stays correct by construction.
Live tasks first, so a long plan shows what is happening rather than what
already happened, bounded at six lines with the remainder stated — the column is
26 to 40 cells wide and shares its height with AGENTS, FILES and ACTIVITY. The
panel and the detail view are where a whole plan is read.
update_plan's own steps still win the section when it has any: the two are
different things and the TODO list is the one the model is actively editing.
The plan's NAME is not in the sidebar. At this width it would cost a task row,
and it is on the panel and in the detail view.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…live detail
The overlay was wrong and the screenshot showed why: composited into the
transcript column, a twelve-task plan drew its phase list straight through the
user's own prompt. It is gone.
The right column already owns "what is happening" — agents, files, activity —
and had half a screen padded with blank lines down to the token floor. The plan
lives there now:
PLAN 1/8
█████████░░░░░░░░░░░░░░░░░ 1/8
✓ t-sandbox x
✗ t-agent y
• t-tools Read the Go package at…
○ t-redaction
+2 more
TASK t-tools
running
52 tool calls · 21,400 tok
blocks pair-a
↳ read_file internal/tools/registry.go
Read the Go package at internal/tools
A progress bar that keeps done, failed, skipped and running in their own
colours inside one span, sized to the column rather than to a fixed width, and
never rounding a segment away — one failure in thirty still gets a cell,
because a failure the bar hides is a failure it does not report.
TASK fills what was dead space, and yields first when the column is short: the
list above it is what the section is for. It shows the REVERSE dependency edge
too — what a task blocks — because a failure matters in proportion to what
waits on it and the forward edge alone does not say.
Click a task to select it, click PLAN to collapse the section, ctrl+g to cycle
the selection from the keyboard. The sidebar is not focusable, so a mouse-only
affordance would not be one.
THE OFFSET ARITHMETIC IS THE RISK HERE, not the drawing. The progress bar sits
between the header and the first task, so every clickable row below it moves
down one — in the plan's own hit table AND in the FILES section beneath. Both
are adjusted, and both are asserted against where the row actually RENDERS
rather than against a re-derivation of the sum: the first version of the FILES
test recomputed the arithmetic and would have passed with the bar unaccounted
for on both sides.
Nine mutations, all caught. Two needed the tests rewritten first — the FILES
one asserted nothing, and the bar one used numbers that never round to zero.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…s offered
The posture raises a cost multiplier — 320 turns per run, inherited by every
sub-agent — and it was a word in amber, in the same weight as everything beside
it. "Is it on?" should be answerable from across the room.
The footer chip is now a FILLED badge: the label sits on the amber rather than
in it, which is what reads as lit rather than as another label in the row. Off
still means ABSENT, not dim — the footer is byte-identical without the posture.
It breathes while a turn is in flight, ◦ • ● ◉ ● •, on a 1.4s cycle. Slow on
purpose: this marks a standing state, not activity, and a fast blink beside a
working spinner reads as a second thing happening.
THE PULSE IS FREE, and that constraint shaped it. It advances on the spinner
tick that is ALREADY running during a turn and holds a steady full glyph
otherwise. ensureSpinnerTick deliberately schedules no timer on an idle session
— "an idle plain session schedules no timer" — and a chip is not a reason to
break that. Steady rather than frozen mid-cycle, because a half-lit chip on an
idle session would read as a rendering fault. Reduced motion pins it, like
every other animation here.
The /effort picker marks the posture row too, so what you are about to turn on
looks like what you will see once it is on. Marked with a glyph rather than
merely coloured: the selected row already owns its background, so colour alone
would be invisible exactly when you are looking at it. The marker is display
only — the VALUE handed to the command stays the bare name, or selecting it
would be refused as an unknown effort.
Seven mutations, all caught. Two needed the tests fixed first: the chip test
called the helper rather than the footer, so reverting the footer to plain amber
text passed it; and the value test located the row BY value, so the moment the
marker leaked into the value the row stopped matching and the test passed by
finding nothing.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
…is clickable
The chip is now clickable — pressing it opens /effort, where the posture can be
turned off or changed — so it highlights under the cursor. A clickable chip that
looks identical to a label never teaches anyone it can be pressed.
Not while a turn is in flight: /effort refuses mid-run, and opening a dialog
that cannot be acted on is worse than opening none.
Its hit region is measured from the RENDERED footer rather than assumed. The
chips before it — permission mode, effort — vary in width with the session, so
a fixed offset would drift the moment either changed.
The sidebar's plan rows highlight too, joining the agent, plan-step and file
rows that already did. The hover is held by TASK ID, not row index, for the
reason the others hold identities: the rendered row set changes as tasks finish
and fade, with no mouse motion in between to re-resolve it, and a cached offset
would light up whatever slid into the slot.
The /effort picker already highlighted its rows on hover — selectGenericPicker-
AtMouse live-previews on motion — so the zeromaxing row needed nothing.
Six mutations, five caught. The sixth is belt-and-braces rather than a gap and
the comment now says so: the posture-off guard in the chip's hit test is a fast
path, not the enforcement — with the posture off the footer carries no chip, so
the span lookup fails anyway.
Origin-Session: local-de382f | Claude Code | 3 prompts
Origin-Snapshot: c28c274718fa
req.Spend.add ran as a side effect inside the second switch case's condition,
and Go stops at the first true case, so the event that tripped
max_tokens_per_task and every event after it never reached the plan meter.
Downstream dispatch, gated on limit - upstream, then ran on an undercount. The
add is hoisted above the switch so both limits judge a meter that saw it all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runTaskWithRetries passed the full cap on every attempt and NewPlanRunner
started each attempt's meter at zero, so a task that stalled after spending
most of its cap could spend (max_retries+1) times it. Each attempt now carries
TaskTokensSpent, the runner seeds its meter with it, and a retry whose task has
already spent the whole cap is not launched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runTaskWithRetries bounded stall retries with the attempt number, so the
one-shot decline, provider and model-fallback retries each consumed a stall
retry: a decline followed by a stall got zero of the documented one. Stall
retries are now counted on their own, and the exhaustion message reports the
number of stalled attempts rather than every attempt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
terminalStatus returned PlanCancelled whenever nothing succeeded and anything
was cancelled, so a real task failure followed by a user stop surfaced as a
deliberate stop in Meta["plan_status"] and the durable plan event. Cancelled
now also requires that no task failed; such a plan reports PlanFailed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
budgetLeft was decremented on every harvest and never read after its initial
"bounded" check; dispatch is decided by the per-pool harvested spend and the
report by TokensUsed. Three comments still called it the authoritative number
that decides dispatch. The variable is removed and the comments now name what
actually enforces the budget. No behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
removeCompletedLater waited CompletedRetention and then called Remove,
which only drops the live process entry; the immutable record in the
completed store survived until 64 newer completions evicted it. remember
now schedules each record's own expiry when it publishes it, stamped with
a generation so a timer only removes the publication it belongs to, and
expiry also frees the record's slot in the FIFO order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restart, bare resume and named resume staged generated plans under fixed
names in the user's plan storage. Exclusive publication stopped them
replacing a user's plan, but refused instead, and Zero's own staged file
then blocked every later restart or resume in the project. Staging now
publishes under the preferred name or the first free variant, tracks
the bytes it wrote, and removes that file once its plan is admitted
unless someone edited it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m isolated children
appendExtraWriteRootArgs skipped a root only when it was byte-equal to the
child's --cwd, so a parent write root that is an ancestor of the parent
workspace (--add-dir ~/dev, config additionalWriteRoots) or lies inside it
followed a write-capable plan task into its isolated worktree and let it write
into the parent checkout. The executor now knows the parent workspace, and a
child whose --cwd lies outside it gets no write root that contains or is
contained in that workspace (canonical paths, separator-bounded, case-folded on
Windows and macOS); unrelated roots are still forwarded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…utlive the turn
Scope.ExtraRoots returned AddTemporaryWrite roots like permanent ones, so a
turn-scoped request_permissions grant left in a child's argv as --add-dir and
stayed writable for the child's whole life after the parent released it.
Semantics now: a child inherits a turn-scoped grant only when it is certain to
exit inside the turn, i.e. a foreground Task or resume that the tool call waits
on (TaskRunOptions.WithinTurn). Background Tasks, plan tasks (a plan may run in
the background) and swarm members get only grants that outlive the turn, via
the new Scope.SessionExtraRoots. The zero value is the conservative one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and resume
The live read-root supplier was wired into the orchestrate tool only: a fresh
ordinary Task got no read-only roots, and BuildResumeArgs emitted only write
roots, so a child could read a granted external path inside a plan but not when
launched normally or resumed. Executor now has an ExtraReadRoots supplier like
ExtraWriteRoots, wired from the CLI for both TUI and headless runs, and both
argument builders emit it only as --add-read-dir (skipping roots already passed
as write roots), under the same within-turn lifetime rule as write grants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Unreleased worktree locks block repeat plan runs, while dependency-order-insensitive fingerprints can incorrectly reuse prior results when the execution inputs change.
Adds the opt-in zeromaxing posture and multi-agent orchestrate plan execution, integrating #829 with current main across the CLI, TUI, specialist runtime, sessions, and sandbox.
Changes:
Adds posture controls, plan lifecycle commands, and per-agent progress displays.
Extends plan identity, resume handling, workspace isolation, and child permission propagation.
Expands usage accounting, model metadata, configuration wiring, and regression coverage.
This pull request requires changes.
Critical findings affect repeated write-plan execution because retained worktrees remain locked, and resume correctness because dependency ordering can change execution without changing its identity. Moderate findings concern rejected admissions inflating session counts, distinct tool names being reported as tool-call counts, and numeric-string budgets being misrepresented before approval. Remaining nits concern tests that do not verify output metadata propagation, preference persistence, or completion-handler model updates and empty-model guards.
File
Description
internal/tui/zeromaxing_guards_test.go
Tests mid-run posture and modal guards.
internal/tui/zeromaxing_chip_span_test.go
Tests column-based chip geometry.
internal/tui/zeromaxing_chip_row_test.go
Tests status-row chip targeting.
internal/tui/view.go
Adds posture styling and a status chip.
internal/tui/theme.go
Adds theme-derived spectrum styles.
internal/tui/theme_palettes.go
Adjusts Dracula status colors.
internal/tui/spinner_tick_order_test.go
Checks spinner mutation sequencing.
internal/tui/specialist_spend_line_test.go
Tests agent token and model displays.
internal/tui/specialist_restore_test.go
Tests restored timestamps and durations.
internal/tui/specialist_model_test.go
Adds model-tracking tests; completion-handler coverage has gaps.
@Vasanthdev2004 this is really unreasonable to try and merge a pr this large.. it needs to land as smaller pr's.. coderrabit wont even review it its so big.
The isolator prepared a fixed plan-<name> worktree under a PID-less lease
and its Release did nothing, so the lock outlived the run and every rerun
of the plan was refused as "locked by another active run". Each execution
now gets its own tree (timestamp + random suffix), the lease names this
process like `zero exec --worktree`, and Release drops the lease while
keeping the tree: Clean never removes an unlocked tree with uncommitted
work, so the unreviewed output is retained without a lock nobody clears.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
taskIdentity sorted depends_on as if it were a set, but the briefing
walks it in declared order and spends one shared budget, so reordering
decides which dependency outputs a task receives. A reordered task kept
its old fingerprint and resume replayed a result built from a different
briefing. The identity now hashes depends_on in declared order (tools
stay sorted: the grant is a set). A plan recorded before this change
re-runs its multi-dependency tasks once on resume, which is the safe side.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SessionBudget.admit incremented before judging, so each refused spawn
was counted as a start: Started() over-reported and every later refusal
quoted a larger "has started" figure. Admission is now one
compare-and-swap that counts only a start it admits, so the limit also
holds exactly under concurrent spawns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names
TaskOutput published len(summary.Tools) as the child's tool count, but
Tools holds distinct names while the AGENTS panel counts every call live,
so a background agent that ran read_file forty times showed "1 tool". The
stream summary now counts calls (StreamResult.ToolCalls), TaskOutput
publishes that as Meta["tool_calls"], and the TUI reads that key. The
meta test now drives TaskOutput itself instead of only the summary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oval card
The card re-decoded the raw orchestrate arguments with its own decoder,
which accepted only JSON numbers, while admission also accepts numeric
strings: a plan asking for "500000" tokens was approved as "no token
limit". It also looked for background inside the budget object, where no
call puts it, so a background plan (inline or saved) was never flagged.
The card now uses specialist.DecodePlanBudget and PlanRunsInBackground,
the same decoders the tool runs, and shows a budget admission will refuse
as invalid with its reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a standalone helper, the sandbox launches os.Executable() with a
hidden __windows-command-runner / __windows-sandbox-setup /
__sandbox-helper subcommand. Tests that run the cli test binary as the
zero CLI (TestLocalAdoptionSavesAndReloadsEligibleModel) therefore made
the test binary the helper, and it ran the whole package suite instead.
With #829's sandboxed shell probe at prompt build, every `zero exec` in
that child hit it: two 15s probe timeouts, then the 30s test kill on
Windows. TestMain now routes the hidden subcommands exactly as
cmd/zero/main.go and runWithDeps do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shellStartsUnderEngine discarded the CommandPlan it built, so the shared
lease that planning takes on the workspace's sandbox runtime directory
was never released: each probe left an open, locked file for the rest of
the process, keeping the runtime root "in use" to other Zero processes'
cleanup and holding a handle under the cache directory on Windows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stall watchdog was armed when the plan runner began, before the
executor resolved the manifest, built the arguments and prompt file and
recorded start accounting, so the parent's own preparation was charged as
the child's silence and a slow launch could be killed as "stalled" before
the child existed. That is how TestAChattyChildOutlivesItsStallTimeout
failed on a loaded Windows runner with the child never called. The
watchdog now holds until the executor's new TaskRunOptions.OnLaunch hook
fires immediately before launch; a child that launches and never speaks
is still caught.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… paths
TestTogglingKeepFinishedAgentsPersists "reloaded" by calling the setter
again, which sets the value on its returned config, so it passed even if
nothing reached disk; it now decodes the file after each toggle. The two
completion-model tests called setModel directly, so deleting the
completion handler's model update or its non-empty guard left them green;
they now send specialistCompleteMsg through Update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up on the review summary and CI. The six inline findings are answered in their threads; these are the items that had no thread:
Rejected admissions inflate the session count: fixed in 6c68047. SessionBudget.admit incremented before judging, so every refusal was counted as a start: Started() over-reported, and each later refusal quoted a larger "has started" figure. Admission is now a single compare-and-swap that counts only a start it admits. Covered by TestARefusedSpawnIsNotCountedAsStarted and TestConcurrentSpawnsAreAdmittedExactlyUpToTheLimit.
Distinct tool names reported as tool-call counts: fixed in 781737f. See the output_tool_test.go thread.
Numeric-string budgets misrepresented before approval: fixed in be20746. The approval card had its own decoder that accepted only JSON numbers, while admission also accepts numeric strings, so a "500000" token cap was shown as "no token limit". The same card also looked for background inside budget, but it's a top-level argument, so background plans (inline or saved) were never flagged. The card now uses specialist.DecodePlanBudget and PlanRunsInBackground, the decoders the tool itself runs, and shows a budget that admission will refuse as invalid.
Windows smoke, TestLocalAdoptionSavesAndReloadsEligibleModel: fixed in 0dd689f and acfc011. With no standalone helper present, the Windows sandbox self-dispatches by re-running os.Executable() with __windows-command-runner. In this test the zero CLI is the cli test binary, which ignored the hidden subcommand and ran its whole suite as the "helper". This PR's sandboxed shell probe at prompt build made every zero exec in that child hit it: two 15s probe timeouts, then the 30s kill. The cli package now has a TestMain that routes the hidden subcommands as cmd/zero/main.go and runWithDeps do (TestTheTestBinaryRoutesSandboxSelfDispatch). Tracing this also found that the probe discarded its CommandPlan, which leaked the sandbox runtime lease for the rest of the process. It now calls plan.Cleanup() (TestTheSandboxedShellProbeReleasesItsPlan).
Windows smoke, TestAChattyChildOutlivesItsStallTimeout (failed once, passed on rerun): fixed in ce4d3a2. The stall watchdog was armed before the executor resolved the manifest, built the arguments and recorded accounting, so the parent's own preparation counted as the child's silence. On a loaded runner, the 60ms window expired before the child was launched. The watchdog now holds until a new TaskRunOptions.OnLaunch hook fires immediately before launch (TestASlowLaunchIsNotChargedAsTheChildsSilence).
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Unresolved findings identify incomplete modal guards, background-task usage priced with the wrong model, and a read-directory test that fails to verify the read grant.
No open issues 🎉
2 previously missed issues
In code that hasn't changed since last review
Assert that --add-read-dir grants reads in the CLI fixture
internal/cli/exec_scope_test.go:129
This exercises only the denied-write half of --add-read-dir: removing the execScope.AddRead loop still leaves the write denied and this test passing, although the read grant no longer works. Extend the CLI fixture to assert that read_file succeeds with the flag and is denied without it, while retaining the write-denial assertion.
Preserve background child models in terminal usage accounting
internal/specialist/accounting.go:279
Background Tasks lose their model because terminal accounting rebuilds specialistAccountingInput without Model. When a background child uses a different model, its usage rollup omits model and usage.BuildReport prices its tokens using the parent's model. Recover the child's recorded model when the input model is absent, and cover a background model override in a regression test.
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
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.
Summary
Supersedes #829 by @gnanam1990: the zeromaxing posture and
orchestrateplan execution. It carries all 195 of that PR's commits unchanged, with authorship preserved, brings them onto currentmain, and fixes the review findings that were still open on #829's last heads.The feature itself, its design notes and its review guide are as described in #829. This PR changes only three things: the integration with
main, the copies of the two dependency packages, and the review fixes listed below.How this branch is built
c4aa2192is a merge of feat(specialist): zeromaxing posture and orchestrate plan execution #829's head (6916f853) intomain(e034a938). The 195 original commits are its second parent, byte-identical (git range-diffis the identity). I tried a linear replay first and dropped it: 189 of the commits conflict, and the automated resolutions silently reverted main's code. All conflict resolution is therefore in this one merge commit, and its message lists every non-trivial combination. Highlights:Gitlawb/zeroimports are rewritten toTwigpine/zero;maxTurnsSet, specialist lifecycle hooks, tty-gated interactive guard, file-view refresh and locked model selection are kept alongside the PR's equivalents;--model, so the flag is now emitted once.git show --remerge-diff c4aa2192.11b1c20aresyncsinternal/memoryandinternal/measurementswith the current heads of feat(memory): durable note store and memory tool #897 (3eae423c) and feat(measurements): check a report's timings against the run's own #909 (8fd02228). The resolution on feat(specialist): zeromaxing posture and orchestrate plan execution #829 was to keep these copies only while they stayed byte-identical to their owning PRs, and both PRs had moved on since. Apart from the module path, they are now identical again. Each old copy was an earlier commit of its dependency PR, so nothing specific to feat(specialist): zeromaxing posture and orchestrate plan execution #829 is lost.38c906ffadapts three measurement fixtures to feat(measurements): check a report's timings against the run's own #909's newer rule that only the runner's package-level timing counts as evidence.Review findings addressed
From @jatmn's review on
b6b38238:6916f853, not by making the grant read-only.Recordnow creates its files withO_EXCL, and the false "exactly one writer" invariant comment is corrected. (7cb90565)4f8156b6)depends_on/tools: now refused, both inline and from saved plans. (a2e55a12)Tasklaunches and resumes, always as--add-read-dir. (30769f72)ec68fa65)86a314cd)0fd0d14c)From @Vasanthdev2004's review on
6916f853:intArg, and a present key is never read as absent. (ec68fa65)5e48344b)500f250f)max_tokens_per_tasknow bounds the task across its retries, not each attempt. (d1903065)83325fc6)4f8156b6, above.c6cc4efe)O_EXCL. (7cb90565)2f39c31a);cb4bc4de);1851b611);failed, notcancelled(f9136002);budgetLeftstore is removed (1c1f8e60).Every behaviour fix has a regression test, and each test was confirmed to fail with its production change reverted.
The older unresolved inline threads from 31 July are already handled on this head: control-byte sanitising,
/effort zeromaxingpending guard, chip hit-testing by column, and modal guards on the hit-testers.Behaviour changes worth knowing
last_run_2(and so on) when the plain name belongs to a user plan. The notice always shows the actual name.Still open, for maintainers
zero exec --resumerefuses child sessions (IsResumableKind), so a specialistTaskresume appears to fail at the child on currentmainas well.TestRunCommandsDoNotLaunchTUI(internal/cli, onmainsince feat: add Go worktree and verification backend #70) runs the realworktrees prepareand leaves a locked worktree under~/.local/state/zero/worktrees. It needs its own PR.Follow-ups from review of this PR
Releasedrops it while keeping the tree. (76649ea1)depends_onin declared order, because the briefing spends its budget in that order. (bc56e1e6)6c680474)TaskOutput: it reports tool calls, not distinct tool names. (781737f4)be207465)clitest binary routes the sandbox self-dispatch subcommands, so the startup shell probe no longer runs the whole suite as a "helper" (0dd689f9);acfc011d);ce4d3a26).781737f4,4d7bd8b9)Validation
All of these ran on
4d7bd8b9, withTZ=UTC, a shortTMPDIR, andHOME/XDG_*pointed at a scratch folder:make fmt-check,go vet ./...: passmake test(go test ./... -race -count=1): pass, 91 packagesgo run ./cmd/zero-release buildandsmoke: passmake vulncheck: no vulnerabilitiesgit diff main --check: cleanmake lint-static: 4 staticcheck quick-fixes (QF1001/QF1008), all in files this PR does not touch (internal/installtest/workflow_permissions_test.go,internal/proxydial/proxydial.go,internal/tools/web_fetch.go). They are already onmainand need their own PR.🤖 Generated with Claude Code