Skip to content

feat(specialist): zeromaxing posture and orchestrate plan execution - #829

Draft
gnanam1990 wants to merge 128 commits into
Gitlawb:mainfrom
gnanam1990:feat/zeromaxing
Draft

feat(specialist): zeromaxing posture and orchestrate plan execution#829
gnanam1990 wants to merge 128 commits into
Gitlawb:mainfrom
gnanam1990:feat/zeromaxing

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

How to review this PR

It is one PR by necessity — the areas share core files (plan_tool.go,
plan_exec.go, app.go are each touched by many commits), so they cannot be
cleanly split into separate PRs without breaking builds. But the changes are
independent by concern. Review area by area, top to bottom; each is
self-contained and these are the seams a split would follow.

# Area Start here
1 Posture + orchestrate tool — the zeromaxing rung and the DAG tool, validated by one constructor plan.go, plan_tool.go, plan_gate.go, plan_keyword.go
2 Executor + durability — one scheduler; plan state as a reduction over five events plan_schedule.go, plan_exec.go, plan_events.go
3 Identity-aware resume — a completed task's output survives; an edited task (and its downstream) re-runs instead of replaying stale plan_identity.go, plan_resume.go, orchestrate_saved.go
4 Read-grant propagation (sandbox) — sub-agents inherit the parent's request_permissions READ grants via a new --add-read-dir flag, applied read-only (never writable) sandbox/scope.go, cli/exec.go, cli/exec_parse.go, plan_runner.go
5 Size-aware model routing — free providers tier by model size (not alphabet); a min_size decency floor; the router is shown size labels; providers models displays them plan_model_size.go, plan_model_assign.go, plan_model_router.go, config/types.go
6 TUI plan surface + observability — right-column plan panel, per-agent rows, per-worker tokens/model tui/orchestrate_*.go, sidebar*.go, specialist_card.go, worker_view.go

Every production change has a regression test beside it (~61% of the diff is
tests); the tree builds and go test ./... passes; with the posture off the
orchestrate tool is unadvertised and no plan machinery runs (see Verification
for the precise additivity claim and its one documented exception).


Summary

Adds zeromaxing — an explicit, opt-in posture that lets a turn spend more to
get a more exhaustive answer — and the plan orchestration it exists to drive.

The posture is a rung above high on the existing effort ladder, reached by
/effort zeromaxing, /profile zeromaxing, or --exec-profile zeromaxing. It
raises the turn budget, widens the sub-agent allowance, and advertises one new
tool: orchestrate, which takes a declared DAG of tasks and runs them as child
agents, in parallel where the dependency graph allows.

The whole feature is additive. With the posture off, nothing here executes.
That is not an aspiration — it is the property every commit on this branch was
checked against, and the check is in "Verification" below.

What landed

Area Change
Posture New zeromaxing rung; lifecycle reminders injected below the cache breakpoint
orchestrate tool Declared DAG of tasks, validated by one constructor there is no way to bypass
Execution One scheduler; max_workers 1–16 walks the validated topological order. Measured 40.0s → 27.0s at 4 workers
Plan size small/medium/large/unrestricted tiers; project config may only tighten what user config sets
Durability Plan state is a reduction over five ordinary session events — no new store
Resume /plans verbs: list, save, run, resume, restart, stop, pause
Write-capable plans Gated behind an approval prompt and run in a detached git worktree; the tree is left for review rather than deleted
Stall handling A watchdog retries a child that has gone silent; retries are counted and reported
TUI Plan lives in the right column — progress bar, task list, live task detail, per-agent rows

Design notes worth review

No new store. Plan state is derived from five session events beside the
existing specialist ones. Resume is a reduction over them, which is why a plan
recorded by the TUI and one recorded by zero exec resume identically.

One scheduler, not two. The sequential path is the concurrent one with a
single worker. Two executors would have been easier to write and impossible to
keep in step — a duplicated rule drifts, and a duplicated executor drifts faster.

Optional interfaces over name switches. Control, per-task progress, isolation
and concurrency each arrive through a type-asserted optional half, so a recorder
that only records is unaffected and no existing signature changed.

One plan per surface. The panel holds one plan and the card table is keyed by
task id — unique within a plan, not between two. PlanSurfaceBusy enforces
that at the tool, on the path the model drives, matching the guard /plans restart already had on the path a user drives.

Verification

Additivity, the load-bearing claim. With the posture off, the orchestrate
tool is unadvertised and no plan machinery runs; for a write-capable run the first
HTTP request body is byte-identical to an origin/main binary. One documented
exception, orthogonal to the posture:
a run holding no mutating tools omits the
~5 KB confirmation-policy block (runCanMutate), so a read-only run's prompt is
smaller than a pre-feature build — deliberate, fail-closed, and independent of
whether the posture is on. Re-proven after
every commit; both binaries built fresh, baseline from a clean origin/main
worktree:

auto-low      IDENTICAL (33,549 bytes)
auto-medium   IDENTICAL (37,918 bytes)
auto-high     IDENTICAL (51,038 bytes)
auto-member   IDENTICAL (38,500 bytes)
use-spec      IDENTICAL (16,179 bytes)

Gauntlet. go build ./..., go vet ./..., go test ./... all pass. gofmt
clean. Concurrency-sensitive packages run under -race with repeat counts.

Mutation checking. Every fix on this branch was verified by reverting the
production change and confirming the test fails. Several tests passed initially
for the wrong reason and were rebuilt until they bit — the misses are recorded in
the commit messages rather than quietly fixed.

Fan-out measured end to end, not asserted: max_workers=1 → 40.0s,
max_workers=4 → 27.0s on the same plan.

Known gaps

  • Remote/cloud sessions do not inherit the posture.
  • No keyword trigger; activation is explicit only.
  • The repo-wide exec-hardening sweep is out of scope here. The worktree git path
    is hardened; the remaining ~60 call sites are their own campaign.

Notes for the reviewer

This is a draft, and deliberately so: it is large. Per CONTRIBUTING.md a PR
needs an approved parent issue and one PR should carry one change — this carries
a feature. Happy to split it along the seams above (posture rung / orchestrate
tool + executor / durability + resume / TUI surface), each of which builds and
tests independently, if that is the preferred shape.

UI changes are best seen running; screenshots can be added on request.

Summary by CodeRabbit

  • New Features
    • Added the Zeromaxing execution posture with CLI and TUI controls.
    • Introduced /plans orchestration with saved plans, pause/stop/resume, background execution, workspace isolation, retries, model selection, and live progress.
    • Added configurable plan-size limits, bundled plans, model discovery, and verification.
    • Added clearer permission details, adaptive confirmation prompts, and richer usage reporting.
  • Bug Fixes
    • Improved progress streaming, plan resumption, credential-store concurrency, sandbox grants, and Git cancellation.
  • Tests
    • Expanded coverage across orchestration, concurrency, durability, configuration, usage tracking, and TUI interactions.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds a "zeromaxing" execution posture spanning the agent core, execprofile, config, CLI, and TUI layers. It introduces a specialist plan orchestration engine (parsing, scheduling, watchdog, worktree isolation, saved plans, background execution) with an OrchestrateTool. It adds tool interfaces for progress streaming, persistent permission refusal, and permanent denial, with confirmation-policy gating. It builds TUI orchestrate panel, sidebar, and plan-progress UI. It also hardens sandbox temporary grants, credstore file locking, and git subprocess execution in worktrees.

Changes

Zeromaxing Execution Posture

Layer / File(s) Summary
Agent state machine and reminders
internal/agent/zeromaxing.go, internal/agent/loop.go, internal/agent/types.go, internal/providers/openai/provider_test.go, tests
Adds the Zeromaxing enum, reminder text, and per-turn reminder injection. Agent Options gain Zeromaxing, PlanCompletions, and OrchestrateAvailable fields. Provider tests confirm cacheable prefixes hold and that the provider never receives "zeromaxing" as a wire effort value.
execprofile posture
internal/execprofile/profile.go, internal/execprofile/*_test.go
Adds a Zeromaxing profile (320 turns, high effort, self-correct armed) with delta text rendering and selection-refusal logic.
Config plan-size and profiles
internal/config/plan_size.go, internal/config/resolver.go, internal/config/types.go, tests
Adds PlanSize tiers and ProfilesConfig (DisableZeromaxing, PlanSize) with tighten-only project-config merge rules.
CLI exec wiring
internal/cli/exec.go, internal/cli/exec_parse.go, internal/cli/exec_zeromaxing_test.go, internal/cli/exec_plan_size_test.go
Maps --reasoning-effort zeromaxing to the exec profile. Blocks the CLI from forwarding the posture name to providers. Prints turn, effort, and self-correct deltas.
TUI controls and glow chip
internal/tui/session_controls.go, internal/tui/picker.go, internal/tui/commands.go, internal/tui/mouse.go, internal/tui/zeromaxing_glow.go, tests
Adds posture handling and catalog-aware settable-efforts logic to /effort and /profile. Adds an animated footer chip with hover and click support.

Estimated code review effort: 5 (Critical) | ~240 minutes

Tool Capability Declarations and Confirmation Policy

Layer / File(s) Summary
New tool interfaces
internal/tools/registry.go, internal/tools/types.go, internal/specialist/task_tool.go
Adds ChildProgressStreamer, PersistentPermissionRefuser, and PermanentDenier interfaces.
Loop wiring and tests
internal/agent/loop.go, internal/agent/*_test.go
Changes progress-callback wiring and persistent-permission decisions from name-based checks to declaration-based checks.
Confirmation policy gating
internal/agent/system_prompt.go, internal/agent/*_test.go, internal/agent/posture_off_identity_test.go
Adds runCanMutate, which fails closed and strips the confirmation policy when a run cannot mutate. Posture-off additivity tests pin byte-identical behavior after registering the orchestrate tool.

Specialist Plan Orchestration Engine

Layer / File(s) Summary
Plan model and events
internal/specialist/plan.go, plan_events.go, manifest.go, sessions/store.go, streamjson/streamjson.go, streamer.go
Adds Plan/Task/Budget/Limits validation, event payload builders, new EventType constants, Manifest.ToolsResolved, and cache/reasoning token fields in the streamed event shape.
Execution engine
internal/specialist/plan_exec.go, plan_schedule.go, plan_watchdog.go, plan_gate.go, plan_runner.go, plan_worktree.go
Implements ExecutePlan concurrency, a stall watchdog, a posture gate, and worktree isolation resolution.
Orchestrate tool and storage
internal/specialist/plan_tool.go, plan_store.go, plan_builtin.go, plan_resume.go, plan_model.go, plan_model_assign.go, plan_model_router.go, plan_model_probe.go, task_role.go, plans/research.json
Adds OrchestrateTool with posture gating and background execution. Adds saved-plan storage, bundled plans, per-task model auto-assignment/routing/probing, task-role classification, and event-log resumption.
Exec resume wiring
internal/specialist/exec.go (resume paths), accounting.go, child_scope_test.go, resume_manifest_test.go, resume_model_test.go, router_manifest_test.go
Ensures resume does not widen tool grants and carries parent model/effort correctly. Router calls get a dedicated JSON-only system prompt.
Test suite
internal/specialist/*_test.go, plan_budget_negative_test.go
Covers concurrency, retries, watchdog behavior, grant narrowing, worktree isolation, negative budget validation, task-role classification, model fallback/pricing, and saved-plan behavior.

CLI Plan/Orchestrate Registration and Background Launcher

Layer / File(s) Summary
App/exec wiring
internal/cli/app.go, internal/cli/exec.go, shutdown_order_test.go
Wires orchestrate/specialist tools with a posture gate, plan progress bridge, and OrchestrateAvailable. Enforces shutdown ordering.
Background launcher and recorder
internal/cli/plan_background.go, plan_recorder.go, plan_isolate.go
Adds a single-slot background plan launcher, a session event recorder, and a worktree isolator.
Model discovery/probing and pricing fields
internal/cli/plan_model_probe.go, provider_models.go, plan_model_prefs_carry_test.go, exec_writer.go, exec_usage_cache_test.go
Adds live provider re-resolution, model probing for --verify, model preference carry-over, and cache/reasoning token fields on the usage stream.
Tests
internal/cli/plan_background_test.go, plan_grant_test.go, plan_recorder_test.go, orchestrate_notice_test.go, plan_live_provider_test.go, child_exit_code_agreement_test.go, sandbox_test.go
Covers launcher lifecycle, grant wiring, recorder payloads, live provider discovery, exit-code agreement, and tool advertisement.

TUI Orchestrate Panel, Sidebar, and Plan Progress UI

Layer / File(s) Summary
Model wiring
internal/tui/model.go, mouse.go, hover.go, options.go, keybinding_help.go
Adds orchestrate state, Ctrl+G handling, and hover/mouse targets.
Plan progress bridge
internal/tui/plan_progress.go, plan_messages.go, plan_card_regression_test.go, plan_summary_sanitize_test.go, plan_fallback_display_test.go, tests
Connects plan lifecycle to Bubble Tea messages and session recording. Adds durability, fallback-display, regression, and sanitization tests.
Panel rendering and control
internal/tui/orchestrate_panel.go, orchestrate_window.go, orchestrate_control.go, orchestrate_background_survival_test.go, orchestrate_resume_identity_test.go, tests
Renders the panel in the footer and sidebar with windowed scrolling and /plans stop/pause/resume. Adds regression tests for background survival and resume identity across plan collisions.
Saved plans and permission card
internal/tui/orchestrate_saved.go, orchestrate_saved_write_test.go, permission_detail.go, rendering.go, tests
Adds /plans save/list/show/run/restart/resume and a plan permission-detail confirmation card.
Sidebar integration
internal/tui/sidebar*.go, specialist_card.go, render_cache.go, theme.go, transcript_selection.go, files_panel.go, plan_step_detail.go, tests
Shows orchestrate tasks in the sidebar PLAN section. Adds agent row expansion, rename, and done-toggle behavior.

Sandbox Temporary Grant Reference Counting

Layer / File(s) Summary
Scope refcounting
internal/sandbox/scope.go, scope_temporary_test.go, scope_temp_refcount_test.go
Adds per-root reference counts so one temporary grant's cleanup does not revoke another concurrent holder's access.

Credstore File Locking

Layer / File(s) Summary
File lock wiring
internal/credstore/credstore.go, filelock_unix.go, filelock_windows.go, concurrency_test.go
Adds advisory file locks that serialize Set/Delete read-modify-write operations across processes and goroutines.

Worktrees Hardened Git Subprocess

Layer / File(s) Summary
Hardened constructor
internal/worktrees/worktrees.go, run_git_unix.go, run_git_windows.go, tests
Adds a single hardened git subprocess constructor that applies WaitDelay and process-group cancellation. Adds a "single construction site" test to enforce this.

Sequence Diagram(s)

sequenceDiagram
  participant TUIModel
  participant AgentRun
  participant SystemPrompt
  participant PostureGate

  TUIModel->>AgentRun: Options.Zeromaxing = Entering
  AgentRun->>AgentRun: zeromaxingReminders(posture, turn, orchestrateAvailable)
  AgentRun->>AgentRun: append reminder messages to conversation
  AgentRun->>SystemPrompt: runCanMutate(options)
  SystemPrompt-->>AgentRun: policy included or stripped
  AgentRun->>PostureGate: PostureActive()
  PostureGate-->>AgentRun: true or false
  AgentRun-->>TUIModel: turn response with reminders
  TUIModel->>TUIModel: advanceZeromaxing() on completion or cancel
Loading

Suggested reviewers: jatmn, vasanthdev2004

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 85.12% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary additions: the zeromaxing posture and orchestrate plan execution.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/specialist/exec.go (1)

608-608: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

TotalTokens is silently dropped on a post-start child failure.

In the error branch (Lines 626-635), summary := SummarizeStream(run.Events, exitCode) is computed — meaning any tokens the child reported before crashing are known — but the returned ExecResult{SessionID: built.SessionID} omits TotalTokens, unlike the success path at Line 642. Per the comment on TotalTokens (Lines 220-226), the plan executor meters its budget from this field; a task that burns real tokens and then errors out will report 0 tokens, letting the budget meter under-count actual spend for every failed/crashed task — exactly the invisible-to-unit-tests defect class this field was added to fix.

🐛 Proposed fix
 	if err != nil {
 		exitCode := run.exitCodeOr(-1)
 		summary := SummarizeStream(run.Events, exitCode)
 		executor.recordSpecialistStop(accounting, summary, "error", summary.ExitCode, err, false)
 		// Carry the child session id even on a post-start failure so a caller (the
 		// swarm launcher -> FailWithSession) can still make the failed member
 		// drillable; the session exists once the child has started.
-		return ExecResult{SessionID: built.SessionID}, err
+		return ExecResult{SessionID: built.SessionID, TotalTokens: summary.Usage.EffectiveTotalTokens()}, err
 	}

Also applies to: 626-643

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/exec.go` at line 608, Update the error return in
runBuiltArgs to include the TotalTokens value from the already-computed summary,
matching the success path’s token accounting. Preserve the existing SessionID
and error behavior while ensuring failed or crashed child executions report
tokens consumed before failure.
🟡 Minor comments (25)
internal/tui/model.go-2449-2452 (1)

2449-2452: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't freeze orchestrate for background plans
m.orchestrate.frozenAt = m.now() makes a background plan that keeps sending planTaskStartMsg/planTaskProgressMsg after the run ends look frozen forever, because nothing clears frozenAt again. Gate this on foreground-only runs or reset frozenAt when background task activity resumes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/model.go` around lines 2449 - 2452, Update the orchestrate
freeze handling around m.orchestrate.frozenAt so background plans are not
permanently frozen when they continue emitting planTaskStartMsg or
planTaskProgressMsg after the run ends. Apply the freeze only to foreground
runs, or clear frozenAt when background task activity resumes, while preserving
the existing behavior for foreground plans.
internal/tui/mouse.go-90-106 (1)

90-106: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Block the footer chip while an overlay is active.
m.zeromaxingChipAtMouse has no setup/wizard/mcpManager/picker/suggestions guard, so a click on the posture chip can preempt an open overlay and replace it with the effort picker. Match the early-return used by orchestrateTaskAtMouse before this branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/mouse.go` around lines 90 - 106, Guard the posture-chip click
branch identified by zeromaxingChipAtMouse with the same setup, wizard,
mcpManager, picker, and suggestions overlay checks used by
orchestrateTaskAtMouse. Return without opening newEffortPicker when any overlay
is active, preserving the existing pending-turn behavior otherwise.
internal/tui/sidebar_plan_detail_test.go-219-231 (1)

219-231: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The 30-task rounding case may not actually have 30 tasks.

string(rune('a' + index%26)) repeats ids ad after 26, so if admit keys tasks by id the panel ends up with 26 rows while taskCount says 30 — the test would still pass, but not on the shape it claims to exercise. Give each task a unique id.

♻️ Unique ids
-	for index := 0; index < 30; index++ {
-		msg.tasks = append(msg.tasks, planGraphTask{id: string(rune('a' + index%26))})
-	}
+	for index := 0; index < 30; index++ {
+		msg.tasks = append(msg.tasks, planGraphTask{id: fmt.Sprintf("t%02d", index)})
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/sidebar_plan_detail_test.go` around lines 219 - 231, The 30-task
rounding test creates duplicate task IDs after 26 iterations, so admission may
retain fewer than 30 tasks. Update the task construction in the big.orchestrate
test setup to generate a unique id for every index while preserving
msg.taskCount and the existing failure/progress-bar assertions.
internal/tui/orchestrate_panel.go-534-544 (1)

534-544: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Uncapped indent in the /plans rows. Unlike renderOrchestrateTaskLine, this uses raw task.depth, so a 40-link chain indents 80 columns and wraps into unreadable text. Reuse orchestrateMaxIndentDepth here for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/orchestrate_panel.go` around lines 534 - 544, Update
orchestratePlainTaskLine to cap task.depth with orchestrateMaxIndentDepth before
calculating the repeated indentation, matching renderOrchestrateTaskLine while
preserving the rest of the row formatting.
internal/tui/plan_progress.go-309-325 (1)

309-325: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

RunningPlanName can report the previous plan's name.

lastPlanName is never cleared in PlanCompleted, so a newly launched-but-not-yet-admitted plan (background flag set, nothing admitted yet) is refused under the last plan's name instead of the honest "a plan" fallback. Clearing the name when the plan ends keeps the refusal truthful while lastPlan (what /plans save needs) stays intact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/plan_progress.go` around lines 309 - 325, The PlanProgressBridge
completion flow must clear lastPlanName when the current plan ends so
RunningPlanName cannot reuse a previous plan’s name for a newly launched,
unadmitted plan. Update PlanCompleted to reset lastPlanName while preserving
lastPlan for /plans save and keeping RunningPlanName’s “a plan” fallback
unchanged.
internal/tui/specialist_card.go-28-33 (1)

28-33: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The new specialistCancelled value isn't reflected in two render paths.

specialistStatusString maps it, but:

  • the header switch in renderSpecialistCard (Lines 319-329) has no specialistCancelled arm, so a stopped/skipped task falls into default and renders with the accent — the same treatment as pending/running, i.e. a finished task that looks live;
  • renderSpecialistSummary (Lines 504-517) counts cancelled cards in len(specialists) but in neither running nor completed, so "3 specialists · 0 running · 1 done" silently loses two of them.

Everything else here (token/result setters, exit-code guard, zero-token omission) looks right.

Also applies to: 50-53, 103-129, 200-208, 334-340, 524-529

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/specialist_card.go` around lines 28 - 33, Update the
specialistCancelled handling in renderSpecialistCard so cancelled tasks use the
finished/non-live header rendering instead of the default accent bullet. Update
renderSpecialistSummary to count cancelled specialists in the appropriate
completed/done total while leaving running counts unchanged, so every card in
len(specialists) is represented in the summary.
internal/tui/orchestrate_panel.go-347-353 (1)

347-353: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale keybinding in the comment. The comment says "Ctrl+O expands" while the header text and the actual binding are ctrl+g.

📝 Proposed fix
-	// away. Ctrl+O expands.
+	// away. Ctrl+G expands.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/orchestrate_panel.go` around lines 347 - 353, Update the
explanatory comment above the !state.expanded check to say that Ctrl+G expands
the details, matching the header text and actual keybinding; leave the collapse
behavior unchanged.
internal/tui/orchestrate_control_test.go-374-383 (1)

374-383: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This assertion can't fail.

m.orchestrate.admit(...) installs one task, so isEmpty() is already false; the && makes the check vacuous whether or not the terminal message survived the guard. Assert the observable effect of complete instead — the status landing on the panel.

💚 Proposed fix
 	updated, _ := m.Update(done)
-	if updated.(model).orchestrate.frozenAt.IsZero() && updated.(model).orchestrate.isEmpty() {
-		t.Fatal("the terminal message was dropped by the stale-run guard")
-	}
+	if got := updated.(model).orchestrate.status; got != string(specialist.PlanCompleted) {
+		t.Fatalf("panel status = %q; the terminal message was dropped by the stale-run guard", got)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/orchestrate_control_test.go` around lines 374 - 383, Update the
assertion in the test around model.Update(done) to verify the observable
completion effect: assert that the completed task’s status is reflected on the
panel. Do not use the current frozenAt/isEmpty conjunction, since admit installs
a task and makes isEmpty() false regardless of whether the terminal message is
processed.
internal/tui/sidebar_plan_detail.go-193-213 (1)

193-213: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Untrusted text reaches the column unsanitised here. sidebarAgentExpansion in internal/tui/sidebar.go runs child output through sanitizeCardText precisely because "a child's answer is untrusted text and an ANSI escape in it would repaint the column". This path renders info.errorMsg (via firstLineOf), info.currentTool/currentDetail and the model-supplied task.summary with only truncation, so the same escape repaints the sidebar from here.

🛡️ Proposed fix
-func firstLineOf(text string) string {
-	if index := strings.IndexAny(text, "\r\n"); index >= 0 {
-		return strings.TrimSpace(text[:index])
-	}
-	return strings.TrimSpace(text)
-}
+// Sanitised, not merely first-lined: this text came from a child agent or the
+// model, and an ANSI escape in it repaints the column.
+func firstLineOf(text string) string {
+	return sanitizeCardText(text)
+}

and wrap activity and task.summary in sanitizeCardText before truncateStep.

Also applies to: 231-256

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/sidebar_plan_detail.go` around lines 193 - 213, Sanitize all
untrusted sidebar text before rendering in the plan-detail flow: apply
sanitizeCardText to activity assembled from info.currentTool/currentDetail,
task.summary, and the outcome text produced by orchestrateOutcomeLine (including
info.errorMsg via firstLineOf). Keep truncation and existing layout behavior
unchanged, using the sanitized values as input to truncateStep.
internal/tui/orchestrate_panel_test.go-474-477 (1)

474-477: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Comment names the wrong key. The test drives ctrl+g (and is named for it); the doc comment says Ctrl+O.

📝 Proposed fix
-// Ctrl+O toggles it through the real key handler, and does nothing when there
+// Ctrl+G toggles it through the real key handler, and does nothing when there
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/orchestrate_panel_test.go` around lines 474 - 477, Correct the
doc comment above TestCtrlGTogglesTheOrchestratePanel to refer to Ctrl+G instead
of Ctrl+O, keeping the test name and key-handler behavior unchanged.
internal/specialist/plan.go-483-494 (1)

483-494: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

max_wall_seconds is the one budget field with no validation at all.

A negative value is silently discarded (unlike max_tokens, which is rejected), and there is no floor (unlike max_stall_seconds, which has one for exactly the "random task-killer" reason). "max_wall_seconds": 1 admits cleanly and then kills the plan before the first child says anything. Rejecting negatives and applying a small floor keeps the budget validation story consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan.go` around lines 483 - 494, The plan parser’s
max_wall_seconds handling lacks negative-value rejection and a minimum timeout.
Update the validation around planInt and the budget.MaxWall assignment to reject
negative values and enforce the appropriate small wall-time floor, matching the
existing max_tokens and max_stall_seconds validation patterns while preserving
valid-budget assignment.
internal/specialist/plan.go-409-417 (1)

409-417: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The remedy can suggest the tier the user is already on.

planTooLargeError always names config.PlanSizeLarge, so a run already at the large tier gets "raise it with planSize: large" — advice that changes nothing, from a message whose whole stated purpose is being actionable. Consider only appending the raise-it clause when limits.MaxTasks < config.PlanSizeLarge.MaxTasks(), and otherwise just telling the user to split the plan.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan.go` around lines 409 - 417, Update planTooLargeError
so the raise-it configuration guidance is appended only when limits.MaxTasks is
below config.PlanSizeLarge.MaxTasks(); for runs already at the large tier,
return an error that advises splitting the plan without suggesting the unchanged
large setting. Preserve the existing source-specific context and task-count
details.
internal/specialist/plan_worktree.go-8-17 (1)

8-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc comments across the plan engine still describe the pre-write-tool, sequential design. These files carry the "Phase 2 tasks are read-only and run one at a time" narrative that this PR's own code and tests contradict — write tools are admitted, isolation is reachable, and max_workers goes up to maxPlanWorkers. One sweep, three sites:

  • internal/specialist/plan_worktree.go#L8-L17: drop "Nothing requires isolation today" and the step-3-is-future framing (repeat at L56-L61); validateTaskTools now admits planWriteTools and plan_test.go asserts RequiresIsolation() is true.
  • internal/specialist/plan.go#L13-L22: replace "executed SEQUENTIALLY" with the actual worker-slot scheduling, and fix the writeToolMarkers name at L114-L116 to planReadOnlyTools.
  • internal/specialist/plan_test.go#L306-L306: delete "(n) A write tool is rejected — Phase 2 tasks are read-only", which sits directly above TestAWriteToolIsPermittedOnlyWhenTheParentHoldsIt.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_worktree.go` around lines 8 - 17, Update the
plan-engine documentation across internal/specialist/plan_worktree.go lines 8-17
and 56-61 to remove the obsolete “nothing requires isolation” and future step-3
framing; revise internal/specialist/plan.go lines 13-22 to describe worker-slot
scheduling instead of sequential execution and rename writeToolMarkers to
planReadOnlyTools at lines 114-116; delete the outdated write-tool rejection
text above TestAWriteToolIsPermittedOnlyWhenTheParentHoldsIt in
internal/specialist/plan_test.go line 306.
internal/specialist/plan_store_test.go-550-556 (1)

550-556: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

A failed Run turns this test into a nil-func panic instead of a readable failure.

tool.Run's result is discarded, so if the plan is ever refused before Launch fires, launched stays nil and Line 555 panics. Assert the launch happened first — same guard the sibling test at Line 477 already has.

💚 Fail with a reason
-	tool.Run(t.Context(), map[string]any{
+	result := tool.Run(t.Context(), map[string]any{
 		"tasks":      []any{task("a", "x"), task("b", "y", "a")},
 		"budget":     map[string]any{"max_workers": float64(1)},
 		"background": true,
 	})
+	if launched == nil {
+		t.Fatalf("nothing was handed to the launcher: %+v", result)
+	}
 	launched(context.Background())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_store_test.go` around lines 550 - 556, Update the
test around tool.Run and launched to assert that Run succeeds and launched is
non-nil before invoking it, matching the guard used by the sibling test.
Preserve the existing launch invocation after this assertion so a refused plan
produces a readable test failure instead of a nil-function panic.
internal/specialist/plan_store.go-188-193 (1)

188-193: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Only a missing directory should be silent.

The comment names the ordinary case but the code swallows every ReadDir failure, so an unreadable .zero/plans (bad perms, I/O error) reports "no saved plans" — the silent skip this file's header explicitly rules out.

🛡️ Report anything that is not "not exist"
 	entries, err := os.ReadDir(dir)
 	if err != nil {
-		// A missing directory is the ordinary case, not a problem worth naming.
-		return nil, nil
+		// A missing directory is the ordinary case; anything else is reported.
+		if os.IsNotExist(err) {
+			return nil, nil
+		}
+		return nil, []string{fmt.Sprintf("%s: %v", dir, err)}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_store.go` around lines 188 - 193, Update loadPlanDir
so only an os.ReadDir error indicating the directory does not exist returns nil,
nil; propagate or record all other failures in the problems result, including
permission and I/O errors, while preserving normal directory loading.
internal/specialist/plan_retry_test.go-329-340 (1)

329-340: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

t.Fatal inside a plan runner callback runs off the test goroutine. ExecutePlanIn dispatches every task through go func(...), so any FailNow in a runner closure exits only that goroutine — the completion is never harvested, the plan keeps going, and the failure surfaces as a timeout or a confusing report instead of the intended message.

  • internal/specialist/plan_retry_test.go#L329-L340: record the missing-cancel condition in a variable and assert it after ExecutePlan returns.
  • internal/specialist/plan_worktree_test.go#L171-L186: the RunTask closure's t.Fatal is latent today (the closure is never invoked); make it set a flag the test checks, or remove the unused fixture.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_retry_test.go` around lines 329 - 340, Replace the
callback t.Fatal in internal/specialist/plan_retry_test.go:329-340 with a
recorded missing-cancel condition, then assert that condition after ExecutePlan
returns; update the RunTask closure in
internal/specialist/plan_worktree_test.go:171-186 to set a flag checked by the
test, or remove the unused fixture.
internal/specialist/plan_tool.go-225-252 (1)

225-252: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The PermissionForArgs doc comment is attached to RefusesPersistentPermission.

The block starting at Line 225 ("PermissionForArgs is what makes a WRITE-CAPABLE plan ask…") runs straight into the RefusesPersistentPermission paragraph with no separator, so godoc hangs all of it on Line 250's method while PermissionForArgs (Line 252) ends up undocumented. Split the two comments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_tool.go` around lines 225 - 252, Separate the
documentation comments for RefusesPersistentPermission and PermissionForArgs in
OrchestrateTool. Keep the existing RefusesPersistentPermission explanation
attached directly to that method, then add a distinct comment immediately before
PermissionForArgs describing its write-capable-plan prompting behavior.
internal/specialist/plan_tool.go-298-315 (1)

298-315: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Dangling edit left in the doc comment.

Lines 312-315 contain a half-deleted sentence: "Previously this read: MaxWorkers is validated to be 1, … which is no longer true." followed by the orphan fragment "moment two tasks can run at once — see the note on the TUI plan recorder." Trim it to the claim that is still true.

✏️ Suggested trim
-// worker a consumer CANNOT attribute an event to a task, and the TUI stops
-// trying rather than attributing every child to whichever task started last.
-// Threading the child's identity through the loop's callback is what would fix
-// it properly. Previously this read: MaxWorkers is validated to be 1,
-// so exactly one task is in flight at any moment and the consumer can attribute
-// events to the task it last saw dispatched — which is no longer true.
-// moment two tasks can run at once — see the note on the TUI plan recorder.
+// worker a consumer CANNOT attribute an event to a task, and the TUI stops
+// trying rather than attributing every child to whichever task started last.
+// Threading the child's identity through the loop's callback is what would fix
+// it properly — see the note on the TUI plan recorder.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_tool.go` around lines 298 - 315, Clean up the doc
comment above runnerForCall by removing the dangling, half-deleted historical
sentence and orphan fragment about MaxWorkers validation and concurrent tasks.
Preserve only the still-valid explanation that the shared callback cannot
identify individual tasks and that threading child identity through the loop
callback would fix it.
internal/specialist/plan_exec.go-627-635 (1)

627-635: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Summary() never reports the effective worker count, but the tool description promises it does.

plan_tool.go line 153 tells the model "The machine's own capacity may be lower and the report says which number applied", yet neither Summary() nor the Meta map surfaces Workers/WorkersRequested. A plan that asked for 16 and ran 6 reads identically to one that got 16 — the exact "fiction" the report fields were added to prevent.

🔧 Surface the pair when they differ
 	fmt.Fprintf(&b, "sequential total: %s · critical path: %s · max_speedup: %.2fx\n",
 		report.SequentialTotal.Round(time.Millisecond), report.CriticalPath.Round(time.Millisecond), report.MaxSpeedup)
+	if report.WorkersRequested > report.Workers {
+		fmt.Fprintf(&b, "workers: %d of %d requested (the machine could not carry more)\n",
+			report.Workers, report.WorkersRequested)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_exec.go` around lines 627 - 635, Update
PlanReport.Summary to report the requested and effective worker counts when they
differ, using the existing WorkersRequested and Workers fields. Keep the current
summary unchanged when the values match, and ensure the output clearly
distinguishes requested capacity from the number actually applied.
internal/execprofile/zeromaxing_test.go-116-123 (1)

116-123: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Two clause-slicing sites index Delta's output without checking strings.Index. If either literal ever leaves the rendered delta, -1 flows into a slice expression and the test panics instead of reporting a readable failure.

  • internal/execprofile/zeromaxing_test.go#L116-L123: capture strings.Index(got, ", and that budget"), t.Fatalf when it is negative, then slice.
  • internal/execprofile/zeromaxing_test.go#L180-L189: do the same for strings.Index(got, "self-correct:") before slicing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/execprofile/zeromaxing_test.go` around lines 116 - 123, Guard both
Delta clause-slicing sites against missing delimiters: at
internal/execprofile/zeromaxing_test.go lines 116-123, capture the index of ",
and that budget", fail with a readable t.Fatalf when it is negative, then slice;
apply the same pattern at lines 180-189 for "self-correct:".
internal/tui/zeromaxing_test.go-726-742 (1)

726-742: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This "third consumer" is a copy, so headless drift can't be detected here.

The doc comment claims TestEffortSettabilityAgreesAcrossAllThreeConsumers pins this against the real forwardedReasoningEffort — it doesn't. That test compares this local re-implementation against the TUI, so if internal/cli's rule changes, both this helper and the test stay green while the surfaces diverge again. Either lift the rule into a shared package both sides call, or correct the comment so the gap is on the record rather than papered over.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/zeromaxing_test.go` around lines 726 - 742, Correct the
misleading comment above forwardedEffortForTest:
TestEffortSettabilityAgreesAcrossAllThreeConsumers does not compare this helper
with internal/cli’s forwardedReasoningEffort, so it cannot detect headless
drift. Document that this is a duplicated local implementation and explicitly
note that divergence from the CLI rule is not covered, without changing the
helper behavior.
internal/tui/zeromaxing_glow_test.go-12-21 (1)

12-21: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the named enum values instead of raw 2/0.

m.zeromaxing = 2 // ZeromaxingActive (and m.zeromaxing = 0 at line 472) hard-codes the ordinal of agent.Zeromaxing. Reorder that enum and this fixture silently exercises a different state — the zeromaxingActive() guard on line 17 wouldn't catch it, since it's also true for Entering.

♻️ Name the state
+	"github.com/Gitlawb/zero/internal/agent"
-	m.zeromaxing = 2 // ZeromaxingActive
+	m.zeromaxing = agent.ZeromaxingActive
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/zeromaxing_glow_test.go` around lines 12 - 21, Update the
zeromaxing test fixtures in glowModel and the later setup assigning zero to use
the named agent.Zeromaxing enum values instead of raw ordinals, preserving the
intended active and inactive states without relying on numeric ordering.
internal/tui/session_controls.go-316-320 (1)

316-320: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This arm can claim "NOT raised" when the posture never wanted to fill.

!supported && !filled also covers the case where the session already holds an effort the user didn't set through /effort (e.g. Options.ReasoningEffort, so execProfileEffortTouched is false). The profile's fill was skipped because the slot was occupied, not because the model refused — but recording want here makes effortTransition() return EffortNotSupported, and the status card then tells the user the model rejected a level it was never asked for.

🐛 Only record a refusal when the fill was actually attempted
-	case !supported && !filled:
+	case !supported && !filled && m.reasoningEffort == "":
 		// Never filled and still unsupported: keep the reason fresh for the
 		// destination model rather than leaving a stale one from the source.
 		m.execProfileEffortUnraised = want
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/session_controls.go` around lines 316 - 320, Update the
!supported && !filled arm in the effort transition logic to record unraised only
when the profile fill was actually attempted, using the existing
effort-touch/occupancy state to distinguish skipped fills from model refusals.
Leave the refusal status unset when an existing effort such as
Options.ReasoningEffort occupied the slot without an /effort request, so
effortTransition() does not report EffortNotSupported.
internal/sandbox/scope_temporary_test.go-17-35 (1)

17-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a Linux-safe outside-root probe

go test ./... runs on ubuntu-latest, but this helper only probes /Users/Shared and /var/empty, so the file can skip on Linux CI. Reuse the existing tempDirOutsideDefaultTemp pattern or add a GOOS-aware candidate list here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/sandbox/scope_temporary_test.go` around lines 17 - 35, Update
scopeOutsideRoots to use the existing tempDirOutsideDefaultTemp pattern or a
GOOS-aware list of writable directories, including a Linux-safe candidate
outside the default temporary root. Preserve the current fallback behavior,
cleanup, workspace/outside directory creation, and skip only when no suitable
candidate is available.
internal/cli/plan_background.go-72-84 (1)

72-84: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

A panicking background plan vanishes silently — no failure surfaces to the bridge/panel.

defer func() { _ = recover() }() (Line 75) correctly stops a panic from taking down the session, but the recovered value is discarded with no report to launcher.bridge or any log. Combined with the "slot frees" cleanup on Lines 76-82, the plan simply disappears from the user's perspective — TestAPanickingBackgroundPlanIsContainedAndFreesTheSlot only asserts the slot frees, not that the panel/user is told anything failed. A user watching the orchestrate panel has no way to distinguish "plan finished" from "plan crashed."

Consider reporting the recovered panic through the bridge (e.g., a synthetic PlanCompleted/failure event) so the panel reflects the true outcome instead of going stale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/plan_background.go` around lines 72 - 84, Update the panic
recovery defer in the background plan goroutine to capture the recovered value
and report the plan failure through launcher.bridge using the existing
completion or failure event mechanism. Preserve the current panic containment
and cleanup behavior in the adjacent defer, ensuring the panel receives a
failure outcome before the running slot is released.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/app.go`:
- Around line 723-726: Reorder the shutdown defers in the app setup around
newPlanLauncher and closeSpecialistRuntime so planLaunch.Close executes first
and fully drains any background plan before the specialist runtime is closed.
Since defers run LIFO, register the runtime cleanup before defer
planLaunch.Close, preserving the existing cleanup behavior otherwise.

In `@internal/sandbox/scope.go`:
- Around line 199-242: Update releaseTemporaryRead and releaseTemporaryWrite to
decrement the reference count and remove the corresponding root while holding
the same s.mu critical section, avoiding unlock-then-relock through
removeReadRoot/removeWriteRoot; preserve the existing helpers for other callers.
Add a targeted interleaving regression test, extending
TestConcurrentHoldersOfOneRoot or equivalent, that starts
AddTemporaryRead/AddTemporaryWrite during the final release and verifies the new
holder retains the root and receives a functional undo, then run the affected
test under the race detector.

In `@internal/specialist/plan_concurrent_test.go`:
- Around line 87-117: Update TestIndependentTasksRunConcurrently to compare the
peak-concurrency expectation against the effective worker count returned by the
plan’s worker-sizing logic, rather than the requested four tasks. Preserve the
existing release-channel closure and report assertions, including the timeout
behavior when the effective concurrency is not reached.

In `@internal/specialist/plan_resume.go`:
- Around line 54-58: Update PlanProgress terminal-state reduction so each task
has one deduplicated outcome: when a later success is recorded, remove that task
from Failed, and ensure duplicate entries do not remain in either list. Then
derive Done from the same remaining-work state used by Remaining, preserving
consistent no-op behavior when every task has reached a terminal outcome.

In `@internal/specialist/plan_runner.go`:
- Around line 150-193: Update planTaskManifest to detect whether grantedTools
intersects planWriteTools, and make both Manifest.Metadata.Description and
SystemPrompt reflect that capability. Write-capable tasks must not be labeled
read-only or instructed not to modify files, while tasks without write tools
must retain the existing read-only restrictions. Update the nearby “Phase 2
tasks are read-only” comment to match the conditional behavior.

In `@internal/specialist/plan_store.go`:
- Around line 127-136: Replace the fixed `path + ".tmp"` write in the plan-save
function with a uniquely named temporary file created via `os.CreateTemp` in the
same directory, preserving restrictive permissions. Write and close the returned
file before renaming it to `path`, remove the temporary file on any failure, and
retain the existing `refuseSymlink` protections and error propagation.

In `@internal/specialist/plan_tool.go`:
- Around line 424-432: Update resolveSavedPlan and its caller so caller-supplied
control flags, especially background, survive replacement with stored.Args.
Preserve the stored plan arguments while explicitly carrying the invocation’s
allowed control flags into the resolved map, so planBool in ParsePlan observes
background=true for saved plans.

In `@internal/specialist/plan_watchdog_test.go`:
- Around line 299-347: Make TestAChattyChildOutlivesItsStallTimeout exercise at
least one watchdog poll by extending the simulated child runtime beyond
watch()’s one-second minimum interval while continuing to emit progress, or add
a configurable poll interval through PlanTaskRequest/PlanRunner for
millisecond-scale testing. Preserve the test’s assertion that regular progress
prevents the 60ms stall timeout from terminating the child.

In `@internal/specialist/plan.go`:
- Around line 155-167: Update Plan.Tasks to deep-copy each Task’s slice fields,
including DependsOn and Tools, rather than only copying the Task structs;
preserve the existing copy isolation for scalar fields and ensure mutations to
returned nested slices cannot alter the validated plan.
- Around line 593-607: Update planStrings to return an error alongside the
parsed strings, rejecting non-string entries and empty or whitespace-only
strings instead of silently skipping them. Propagate that error through planTask
so malformed depends_on or tools values fail closed and no truncated Task is
constructed.

In `@internal/tui/mouse.go`:
- Around line 185-190: Sequence the hover update and spinner scheduling in the
surrounding mouse-update flow instead of returning hovered.ensureSpinnerTick()
as a combined expression. Assign the result of updateHoverTarget to hovered,
call ensureSpinnerTick on that value, then return the updated model so its
spinnerTicking bookkeeping is preserved reliably.

In `@internal/tui/orchestrate_panel.go`:
- Around line 449-471: Update the row-width calculation around the styled glyph
and head so remaining is based on terminal display width rather than rune counts
that include ANSI escape sequences. Measure the unstyled head (or use
lipgloss.Width consistently) while preserving the existing summary truncation
and rendering behavior in the task row formatting logic.

In `@internal/tui/render_cache.go`:
- Around line 176-202: Update specialistCacheFingerprint to include
specialistInfo.tokenCount and specialistInfo.result, ensuring mutations from
setTokens and setResult invalidate cached renders. Extend
TestSpecialistCacheKeyCoversEveryVaryingField in the plan progress tests with
distinct tokenCount and result cases.

In `@internal/tui/zeromaxing_glow.go`:
- Around line 212-225: Update zeromaxingChipSpan to convert the label’s byte
offset from strings.Index into a rune/column offset before subtracting the badge
prefix. Return span coordinates in the same mouseX(msg) column units so
multi-byte glyphs such as ●, ◎, and ↻ do not shift the clickable chip area.

---

Outside diff comments:
In `@internal/specialist/exec.go`:
- Line 608: Update the error return in runBuiltArgs to include the TotalTokens
value from the already-computed summary, matching the success path’s token
accounting. Preserve the existing SessionID and error behavior while ensuring
failed or crashed child executions report tokens consumed before failure.

---

Minor comments:
In `@internal/cli/plan_background.go`:
- Around line 72-84: Update the panic recovery defer in the background plan
goroutine to capture the recovered value and report the plan failure through
launcher.bridge using the existing completion or failure event mechanism.
Preserve the current panic containment and cleanup behavior in the adjacent
defer, ensuring the panel receives a failure outcome before the running slot is
released.

In `@internal/execprofile/zeromaxing_test.go`:
- Around line 116-123: Guard both Delta clause-slicing sites against missing
delimiters: at internal/execprofile/zeromaxing_test.go lines 116-123, capture
the index of ", and that budget", fail with a readable t.Fatalf when it is
negative, then slice; apply the same pattern at lines 180-189 for
"self-correct:".

In `@internal/sandbox/scope_temporary_test.go`:
- Around line 17-35: Update scopeOutsideRoots to use the existing
tempDirOutsideDefaultTemp pattern or a GOOS-aware list of writable directories,
including a Linux-safe candidate outside the default temporary root. Preserve
the current fallback behavior, cleanup, workspace/outside directory creation,
and skip only when no suitable candidate is available.

In `@internal/specialist/plan_exec.go`:
- Around line 627-635: Update PlanReport.Summary to report the requested and
effective worker counts when they differ, using the existing WorkersRequested
and Workers fields. Keep the current summary unchanged when the values match,
and ensure the output clearly distinguishes requested capacity from the number
actually applied.

In `@internal/specialist/plan_retry_test.go`:
- Around line 329-340: Replace the callback t.Fatal in
internal/specialist/plan_retry_test.go:329-340 with a recorded missing-cancel
condition, then assert that condition after ExecutePlan returns; update the
RunTask closure in internal/specialist/plan_worktree_test.go:171-186 to set a
flag checked by the test, or remove the unused fixture.

In `@internal/specialist/plan_store_test.go`:
- Around line 550-556: Update the test around tool.Run and launched to assert
that Run succeeds and launched is non-nil before invoking it, matching the guard
used by the sibling test. Preserve the existing launch invocation after this
assertion so a refused plan produces a readable test failure instead of a
nil-function panic.

In `@internal/specialist/plan_store.go`:
- Around line 188-193: Update loadPlanDir so only an os.ReadDir error indicating
the directory does not exist returns nil, nil; propagate or record all other
failures in the problems result, including permission and I/O errors, while
preserving normal directory loading.

In `@internal/specialist/plan_tool.go`:
- Around line 225-252: Separate the documentation comments for
RefusesPersistentPermission and PermissionForArgs in OrchestrateTool. Keep the
existing RefusesPersistentPermission explanation attached directly to that
method, then add a distinct comment immediately before PermissionForArgs
describing its write-capable-plan prompting behavior.
- Around line 298-315: Clean up the doc comment above runnerForCall by removing
the dangling, half-deleted historical sentence and orphan fragment about
MaxWorkers validation and concurrent tasks. Preserve only the still-valid
explanation that the shared callback cannot identify individual tasks and that
threading child identity through the loop callback would fix it.

In `@internal/specialist/plan_worktree.go`:
- Around line 8-17: Update the plan-engine documentation across
internal/specialist/plan_worktree.go lines 8-17 and 56-61 to remove the obsolete
“nothing requires isolation” and future step-3 framing; revise
internal/specialist/plan.go lines 13-22 to describe worker-slot scheduling
instead of sequential execution and rename writeToolMarkers to planReadOnlyTools
at lines 114-116; delete the outdated write-tool rejection text above
TestAWriteToolIsPermittedOnlyWhenTheParentHoldsIt in
internal/specialist/plan_test.go line 306.

In `@internal/specialist/plan.go`:
- Around line 483-494: The plan parser’s max_wall_seconds handling lacks
negative-value rejection and a minimum timeout. Update the validation around
planInt and the budget.MaxWall assignment to reject negative values and enforce
the appropriate small wall-time floor, matching the existing max_tokens and
max_stall_seconds validation patterns while preserving valid-budget assignment.
- Around line 409-417: Update planTooLargeError so the raise-it configuration
guidance is appended only when limits.MaxTasks is below
config.PlanSizeLarge.MaxTasks(); for runs already at the large tier, return an
error that advises splitting the plan without suggesting the unchanged large
setting. Preserve the existing source-specific context and task-count details.

In `@internal/tui/model.go`:
- Around line 2449-2452: Update the orchestrate freeze handling around
m.orchestrate.frozenAt so background plans are not permanently frozen when they
continue emitting planTaskStartMsg or planTaskProgressMsg after the run ends.
Apply the freeze only to foreground runs, or clear frozenAt when background task
activity resumes, while preserving the existing behavior for foreground plans.

In `@internal/tui/mouse.go`:
- Around line 90-106: Guard the posture-chip click branch identified by
zeromaxingChipAtMouse with the same setup, wizard, mcpManager, picker, and
suggestions overlay checks used by orchestrateTaskAtMouse. Return without
opening newEffortPicker when any overlay is active, preserving the existing
pending-turn behavior otherwise.

In `@internal/tui/orchestrate_control_test.go`:
- Around line 374-383: Update the assertion in the test around
model.Update(done) to verify the observable completion effect: assert that the
completed task’s status is reflected on the panel. Do not use the current
frozenAt/isEmpty conjunction, since admit installs a task and makes isEmpty()
false regardless of whether the terminal message is processed.

In `@internal/tui/orchestrate_panel_test.go`:
- Around line 474-477: Correct the doc comment above
TestCtrlGTogglesTheOrchestratePanel to refer to Ctrl+G instead of Ctrl+O,
keeping the test name and key-handler behavior unchanged.

In `@internal/tui/orchestrate_panel.go`:
- Around line 534-544: Update orchestratePlainTaskLine to cap task.depth with
orchestrateMaxIndentDepth before calculating the repeated indentation, matching
renderOrchestrateTaskLine while preserving the rest of the row formatting.
- Around line 347-353: Update the explanatory comment above the !state.expanded
check to say that Ctrl+G expands the details, matching the header text and
actual keybinding; leave the collapse behavior unchanged.

In `@internal/tui/plan_progress.go`:
- Around line 309-325: The PlanProgressBridge completion flow must clear
lastPlanName when the current plan ends so RunningPlanName cannot reuse a
previous plan’s name for a newly launched, unadmitted plan. Update PlanCompleted
to reset lastPlanName while preserving lastPlan for /plans save and keeping
RunningPlanName’s “a plan” fallback unchanged.

In `@internal/tui/session_controls.go`:
- Around line 316-320: Update the !supported && !filled arm in the effort
transition logic to record unraised only when the profile fill was actually
attempted, using the existing effort-touch/occupancy state to distinguish
skipped fills from model refusals. Leave the refusal status unset when an
existing effort such as Options.ReasoningEffort occupied the slot without an
/effort request, so effortTransition() does not report EffortNotSupported.

In `@internal/tui/sidebar_plan_detail_test.go`:
- Around line 219-231: The 30-task rounding test creates duplicate task IDs
after 26 iterations, so admission may retain fewer than 30 tasks. Update the
task construction in the big.orchestrate test setup to generate a unique id for
every index while preserving msg.taskCount and the existing failure/progress-bar
assertions.

In `@internal/tui/sidebar_plan_detail.go`:
- Around line 193-213: Sanitize all untrusted sidebar text before rendering in
the plan-detail flow: apply sanitizeCardText to activity assembled from
info.currentTool/currentDetail, task.summary, and the outcome text produced by
orchestrateOutcomeLine (including info.errorMsg via firstLineOf). Keep
truncation and existing layout behavior unchanged, using the sanitized values as
input to truncateStep.

In `@internal/tui/specialist_card.go`:
- Around line 28-33: Update the specialistCancelled handling in
renderSpecialistCard so cancelled tasks use the finished/non-live header
rendering instead of the default accent bullet. Update renderSpecialistSummary
to count cancelled specialists in the appropriate completed/done total while
leaving running counts unchanged, so every card in len(specialists) is
represented in the summary.

In `@internal/tui/zeromaxing_glow_test.go`:
- Around line 12-21: Update the zeromaxing test fixtures in glowModel and the
later setup assigning zero to use the named agent.Zeromaxing enum values instead
of raw ordinals, preserving the intended active and inactive states without
relying on numeric ordering.

In `@internal/tui/zeromaxing_test.go`:
- Around line 726-742: Correct the misleading comment above
forwardedEffortForTest: TestEffortSettabilityAgreesAcrossAllThreeConsumers does
not compare this helper with internal/cli’s forwardedReasoningEffort, so it
cannot detect headless drift. Document that this is a duplicated local
implementation and explicitly note that divergence from the CLI rule is not
covered, without changing the helper behavior.

---

Nitpick comments:
In `@internal/cli/exec_zeromaxing_test.go`:
- Around line 38-63: Update TestZeromaxingDoesNotOverrideModeFillsCLI to assert
that the fast mode fixture actually sets non-empty reasoningEffort and positive
maxTurns immediately after applyExecMode; then perform the precedence checks
unconditionally, preserving the existing expected-value comparisons and failure
messages.

In `@internal/cli/plan_isolate.go`:
- Around line 21-26: Provide an operator-facing cleanup path for worktrees
retained by Release, such as documenting periodic removal or adding a `zero
worktrees` cleanup command. Ensure the guidance or command identifies the
workspace worktree store and allows accumulated, no-longer-needed plan worktrees
to be reclaimed without changing the safety behavior that preserves newly
written work.

In `@internal/sandbox/scope_temporary_test.go`:
- Around line 139-172: Add a write-side nested-grant test alongside
TestATemporaryWriteSurvivesASiblingsCleanup, mirroring
TestANarrowerRequestHoldsTheCoveringGrant: create a broader temporary write
grant, request a narrower covered path, clean up the broader grant, and verify
the narrower grant still preserves coverage until its own cleanup. Use
AddTemporaryWrite and the existing scope.Roots coverage checks to exercise map
iteration order.
- Around line 178-228: Extend TestConcurrentHoldersOfOneRoot with a coordinated
iteration that starts a new AddTemporaryRead(outside) while another holder’s
undo() is in progress, using synchronization to overlap the operations within
the releaseTemporaryRead window. Assert the new grant remains visible throughout
its hold and that the root is removed only after all holders, including the
newly added one, release it.

In `@internal/specialist/exec.go`:
- Around line 415-438: The resumed inline manifest is not validated against the
session’s specialist identity. In runResume, after resolveManifest returns,
validate that a supplied manifest’s Metadata.Name matches specialistName (or
enforce this within resolveManifest using the resumed expected name), regardless
of whether params.Name was provided; reject mismatches before execution. Also
inspect the production plan_runner.go caller to confirm whether
TaskParameters.Name always matches the inline manifest Metadata.Name, and
preserve the guard even if callers currently do.

In `@internal/specialist/plan_concurrent_test.go`:
- Around line 46-54: Update fanOutPlan’s task ID generation to use a formatted,
explicitly valid identifier for every index instead of converting 'a'+i to a
rune. Preserve uniqueness across all n values and keep the existing mustPlan
setup unchanged.
- Around line 335-342: Update goroutineLabel to bound the stack-header slice by
the number of bytes actually written, returning up to 20 bytes without panicking
when runtime.Stack produces a shorter result. Preserve the existing 20-character
label behavior when sufficient data is available.

In `@internal/specialist/plan_grant_test.go`:
- Around line 248-275: The grant-plan table test should explicitly encode
whether each case is expected to refuse dispatch, rather than skipping all
converse checks on any grant error. Add a per-case wantGrantErr expectation,
assert grantErr matches it, and ensure only the intentionally ungrantable “no
request, parent holds only mutators” case permits refusal while admission
succeeds; keep the existing granted-tool validation for successful cases.

In `@internal/specialist/plan_progress_test.go`:
- Around line 156-176: Extend the test around runnerForCall to assert that the
child arguments include the parent reasoning effort supplied through
tools.RunOptions.ReasoningEffort: "high". Keep the existing model and session
assertions unchanged, and verify the corresponding high reasoning-effort
argument so regressions dropping ParentReasoningEffort are detected.

In `@internal/specialist/plan_resume_test.go`:
- Around line 76-83: Update the test around ReducePlanEvents and
progress.Remaining() to first verify the returned slice contains an element,
failing with a descriptive message that includes the actual slice when it is
empty; only index got[0] after that guard, while preserving the existing
expectation that the first remaining task is "a".

In `@internal/specialist/plan_test.go`:
- Around line 485-486: Remove the import-keepalive declarations for errors.New
and time.Second in plan_test.go, since errors is already used by the test and
time is unnecessary. Delete the corresponding time import while preserving the
existing errors usage.

In `@internal/specialist/plan_tool.go`:
- Around line 398-411: Remove the unused options parameter from
OrchestrateTool.limits and update every call site to invoke it without
arguments. Keep the existing tool.Size and tool.Depth-based limit calculation
unchanged.
- Around line 490-505: Merge the consecutive !launched checks in the plan-launch
flow around tool.Launch: release workspace within the single refusal branch
before returning the error result. Preserve the successful launch path and
existing shutdown/already-running error response.

In `@internal/specialist/plan_worktree_test.go`:
- Around line 171-186: Update TestTheToolRefusesAnUnisolatableWritePlan to
exercise the foreground dispatch through RunWithOptions using the configured
tool and write plan, ensuring the test reaches PostureActive and RunTask;
alternatively, remove the unused fixture fields and rename the test to reflect
that it only validates resolvePlanWorkspace with a nil isolator.

In `@internal/specialist/plan.go`:
- Around line 13-22: Update the stale comments in the plan implementation:
revise the package-level ZeroMaxing Phase 2 description to match the concurrent
scheduler and max_workers behavior allowed by planBudget, and correct the
comment above writeToolMarkers to document planReadOnlyTools. Remove outdated
references to sequential execution and obsolete phase hooks while preserving the
current authority rules.

In `@internal/tui/permission_detail.go`:
- Around line 166-188: Update sanitizeCardText to remove
bidirectional-formatting and zero-width/invisible Unicode runes, including
U+202E and U+200B-class characters, in addition to the existing
control-character filtering. Preserve printable text, tab-to-space conversion,
line truncation, and final trimming.

In `@internal/tui/plan_durability_test.go`:
- Around line 247-269: Throttle the concurrent loops in the durability test
around bridge.TaskProgress by adding a small scheduler yield or equivalent
bounded coordination inside the loop. Preserve overlapping progress calls with
later TaskDispatched operations, while avoiding the unbounded default-branch
busy-wait that creates excessive contention under the race detector.

In `@internal/tui/plan_messages.go`:
- Around line 118-120: Update the doc comment immediately above planAdmittedLine
so it starts with the declared function name, replacing the incorrect
planNoticeLine reference while preserving the existing description.

In `@internal/tui/session_controls.go`:
- Around line 162-168: Update effortText and its effort-resolution flow to
resolve the available reasoning-effort ring once per card render, then reuse
that result for the efforts list, known-state check, and settableEfforts
behavior. Refactor availableReasoningEfforts, availableReasoningEffortsKnown,
and settableEfforts as needed so they share the resolved registry/ring instead
of independently calling modelregistry.DefaultRegistry().

In `@internal/tui/sidebar_plan_test.go`:
- Around line 24-44: The test TestFileClickOffsetsSurviveThePlanSection
currently checks only sidebarPlanLines output and never verifies file click
offsets. Update it to call sidebarFileSelectables for models with and without
the plan, then assert the resulting file hits shift by exactly
len(withPlan.sidebarPlanLines(34)), while preserving the existing empty-plan
validation.

In `@internal/tui/sidebar_test.go`:
- Around line 822-829: Strengthen the cancelled-task test around
sidebarAgentExpansion by first validating that m.sidebarSpecialists() is
non-empty before indexing element zero, reporting a test failure instead of
panicking. Extract the cancelled row and assert it contains no zeroTheme.red SGR
escape sequence, rather than checking only for the exact styled word, while
preserving the existing “cancelled” text assertion.

In `@internal/tui/sidebar.go`:
- Around line 205-243: Extract the shared filtering logic into helpers matching
the proposed hiddenNotFoundAgent and m.agentPastLinger predicates, then use
those helpers in both sidebarSpecialists and doneAgentCount. Preserve the
existing not-found exclusion and linger-expiry behavior so the displayed rows
and done count remain synchronized.

In `@internal/tui/zeromaxing_glow_test.go`:
- Around line 315-317: Update the underline assertion in the hovered-chip test
to remove the broad "4;" substring check, which can match unrelated truecolor
values. Validate that the hovered output contains an actual SGR underline
parameter alongside the existing relevant checks, so chips without underline
styling fail reliably.

In `@internal/worktrees/run_git_unix.go`:
- Line 16: Move the shared worktreeWaitDelay declaration and its documentation
out of run_git_unix.go and run_git_windows.go into a build-tag-free shared file
such as worktrees.go. Remove both platform-specific copies while leaving the
platform-specific hardenWorktreeGit implementations unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread internal/cli/app.go Outdated
Comment thread internal/sandbox/scope.go Outdated
Comment thread internal/specialist/plan_concurrent_test.go
Comment thread internal/specialist/plan_resume.go Outdated
Comment thread internal/specialist/plan_runner.go
Comment thread internal/specialist/plan.go
Comment thread internal/tui/mouse.go Outdated
Comment thread internal/tui/orchestrate_panel.go
Comment thread internal/tui/render_cache.go
Comment thread internal/tui/zeromaxing_glow.go
@gnanam1990
gnanam1990 marked this pull request as ready for review July 30, 2026 11:36
@gnanam1990
gnanam1990 marked this pull request as draft July 30, 2026 11:37
@kevincodex1

Copy link
Copy Markdown
Contributor

wow this is great feature

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through the whole branch properly — posture machinery, the DAG engine, durability, the TUI surface. Nice work, Kratos. The one-scheduler design and the event-reduction durability model are the right shape, and the mutation-tested tests show.

A handful of things to look at before this comes out of draft. The big one is the first inline below — the /plans resume narrowing bug silently drops real work, I'd fix that before anything else. The unsanitized plan strings and the background-plan panel wipe are next. The rest are smaller correctness nits and a couple of stale strings.

On the split you offered at the end of the description: the seams you listed look right to me, happy to re-review per slice if you go that way.

if err != nil {
return "", planControlNotice("warning", "Could not read this session's events: "+err.Error()), false
}
progress, found := specialist.ReducePlanEvents(events)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the one I'd fix first. ReducePlanEvents keeps only the last admitted plan's progress, but here we narrow whichever saved plan the user named by that progress — without checking the two are the same plan. Run saved plan A, then plan B, then /plans resume A: every A task whose id happens to sit in B's succeeded set (ids like "tests"/"lint" collide across plans all the time) gets silently dropped and never runs — and the notice then reports B's counts against A's name. progress.Name/order are reduced and right there; can we match them against the plan being resumed and refuse on mismatch?

if index := strings.IndexAny(summary, "\r\n"); index >= 0 {
summary = summary[:index]
}
return truncateRunes(summary, planTaskSummaryWidth)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

planTaskSummary cuts at the first newline but leaves every other control byte in, and these strings are model-authored — so ESC/OSC sequences survive into the inline panel, the sidebar rows, and the detail pane. The realistic path is indirect prompt injection (poisoned file/web content echoed into orchestrate args) repainting someone's terminal. We added sanitizeCardText in permission_detail.go for exactly this data class in this same PR, and sidebar.go already scrubs child output — these plan surfaces need the same treatment. planString (plan name) has the same hole, and so do the first-lines in /plans list and /plans show.

Comment thread internal/tui/model.go Outdated
// previous turn don't bleed into the new one.
m.specialists.clear()
m.plan.clear()
m.orchestrate.clear()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beginRun clears the panel unconditionally, but background plans are built to outlive the run that launched them — every bridge message carries the background flag precisely to survive the stale-run guard. After this clear, that plan's start/done messages pass the guard but no-op against an empty byID, planAdmitted never re-fires, and the PLAN surface silently vanishes for the rest of the plan's life while it keeps running and spending. (The adjacent specialists.clear drops its AGENTS rows mid-flight too.) Can we skip the clear — or re-admit from the bridge — when a background plan is in flight?

// This delegates to handleProfileCommand rather than re-applying the knobs,
// so "/effort zeromaxing and /profile zeromaxing resolve identically" is
// true by construction instead of by two implementations a test hopes agree.
if args == execprofile.Name {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/effort zeromaxing goes straight through with no m.pending check, but it routes into handleProfileCommand — which mutates the turn budget, self-correct, and the shared orchestrate gate. That's the same mutation /profile refuses mid-run ("Finish or stop the current run before switching the execution profile"), and the /turns comment above explains why: the budget propagates to sub-agents spawned later in the same run via ZERO_MAX_TURNS, so a mid-run switch makes a live run inconsistent — here it also arms the orchestrate tool for a run that started without the posture. Same guard here?

Comment thread internal/sandbox/scope.go Outdated
Comment on lines +217 to +219
delete(s.tempReads, root)
s.mu.Unlock()
s.removeReadRoot(root)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refcount delete and the slice removal happen under different lock holds — delete(s.tempReads, root), unlock, then removeReadRoot re-acquires. A concurrent AddTemporaryRead for the same root landing in that window sees the root still present in readRoots but no longer tracked, misclassifies it as permanent, and returns a no-op undo — then removeReadRoot strips the root and the new holder's already-approved tool call gets sandbox-denied. That's exactly the parallel-batch shape this refcounting exists to fix (batched reads granted the same outside-workspace dir). Same issue in releaseTemporaryWrite below. Can we do the removal under the same lock hold?

Comment thread internal/specialist/plans/research.json Outdated
{
"id": "by_name",
"phase": "search",
"prompt": "Find where THE SUBJECT is DEFINED. Search by identifier: type names, function names, constants, struct fields. Report every definition site as file:line with a one-line description. If you find nothing, say so plainly — a wrong guess is worse than an empty result."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two problems with shipping this as runnable: "THE SUBJECT" is never substituted (resolveSavedPlan refuses args alongside saved), so /plans run research spends five child agents searching for a placeholder. And synthesise/refute assume upstream outputs reach dependents — "Using the three searches above" — but NewPlanRunner passes task.Prompt verbatim, so a dependent child never sees earlier results and has to redo or hallucinate the synthesis. Either make the example concrete and self-contained, or stop advertising it in /plans run.

Comment thread internal/specialist/plan_resume.go Outdated
}

// Done reports whether there is anything left to run.
func (progress PlanProgress) Done() bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done and Remaining look like dead API — only the tests call them; the actual resume path narrows via RemainingPlan and never touches either. Wire them in (Done to short-circuit a fully-succeeded resume?) or drop them.

sections = append(sections, style)
}
policy := strings.TrimSpace(confirmationPolicy)
if !runCanMutate(options) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up on the headline invariant: runCanMutate is evaluated unconditionally, so any all-read-only run (e.g. zero exec --enabled-tools read_file,grep, or a read-only specialist child) now gets a system prompt ~5KB smaller than a build without the feature — posture off. The drop itself is deliberate and fails closed, that's fine — but "byte-identical with the posture off" is false as written, and posture_off_identity_test can't see this delta (it only proves registering the tool changes nothing). Either narrow the claim or make the drop opt-in.


// orchestrateHeaderAtMouse reports a click on the sidebar's PLAN header, which
// collapses and expands the whole section.
func (m model) orchestrateHeaderAtMouse(msg tea.MouseMsg) bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hit-tester is missing the modal guard its siblings all carry — sidebarAvailable deliberately excludes suggestionsActive, with the in-code note that each sidebar hit-tester "carries its own suggestionsActive() guard", but this one refuses nothing. Clicking the PLAN header while the / palette is open toggles sidebarCollapsed behind the overlay.

Comment thread internal/tui/mouse.go
// Clicking the posture chip opens /effort, where it can be turned off or
// changed. A chip that highlights under the cursor and then does nothing
// when pressed is worse than one that never highlighted.
if mouseLeftPress(msg) && m.zeromaxingChipAtMouse(msg) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch runs before the modal switch and only checks m.pending — the hit-testers added right beside it guard m.picker, the wizards, and suggestionsActive, but this one doesn't. With the /model picker or a provider wizard open, clicking the chip silently swaps in a fresh effort picker and discards the open picker's loaded/typed state. Same guard here?

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@gnanam1990 took the top three from my review and pushed them as a commit you can cherry-pick — 5e9a8a79 on fix/829-resume-identity-and-panel in the main repo. Your branch is on your fork so I couldn't open a PR against it:

git fetch https://github.com/Gitlawb/zero.git fix/829-resume-identity-and-panel && git cherry-pick 5e9a8a79

Merge it, take pieces, or ignore it entirely and write it your way — no attachment to the implementation. internal/tui +212/-5, three production changes and three test files.

The resume bug is the one that mattered, and it's worse than I first described. ReducePlanEvents keeps only the last admitted plan's state and records whose it is in PlanProgress.Name; resumeSavedPlan never checked that against the plan being resumed. Run plan A, run plan B, /plans resume A — every A task whose id sits in B's succeeded set is dropped and never runs. It fails silently: the remainder validates, saves, and runs, just missing work.

One detail worth your eye. The check compares plan.Name(), not stored.Nameplan_admitted records the plan's own name, which is independent of the name it was saved under (samplePlan is named durable, saved as sweep). I mutated it to stored.Name and TestResumeStagesOnlyTheWorkThatDidNotFinish fails, so the wrong choice is pinned too, not just the missing one.

Task summaries go through sanitizeCardText now instead of only cutting at the first newline — the same helper permission_detail.go already applies to this class of data. Test covers ANSI colour, OSC title-set, bare ESC, CR overwrite, backspace and bell, plus one asserting ordinary text is untouched.

BackgroundPlanLive gates the panel clear in beginRun. It reads background alongside cancelPlan for exactly the reason RunningPlanName documents — the launcher sets one synchronously and the goroutine sets the other later, so either alone leaves a window. Your comment there is what told me how to write it. Both directions are tested: background survives, foreground leftovers still clear.

Three mutations, all killed. internal/tui and internal/specialist green apart from TestAltScreenTranscriptScrollKeepsFooterFixed, which fails identically on clean main on my Windows box.

Untouched from the review, in case you'd rather split them: the /effort zeromaxing mid-run guard, the scope.go refcount race, both glow hit-testers, max_wall_seconds accepting negatives, the stale "read-only" strings on the approval card, research.json's unsubstituted placeholder, Done/Remaining as dead API, the posture_off_identical claim, and the two missing modal guards. Say the word and I'll send another slice.

Vasanthdev2004 added a commit to gnanam1990/zero that referenced this pull request Jul 31, 2026
…panels

Three from the review on Gitlawb#829.

RESUME NARROWED BY THE WRONG PLAN'S PROGRESS. ReducePlanEvents keeps only
the LAST admitted plan's state and records whose it is in PlanProgress.Name,
but resumeSavedPlan narrowed whichever plan the user named by that progress
without checking the two match. Run plan A, run plan B, then /plans resume A:
every A task whose id sits in B's succeeded set is dropped from the remainder
and never runs. Ids like "tests" or "lint" collide across plans constantly, so
this is the ordinary case rather than a contrived one, and it fails silently —
the remainder validates and runs, just without the work.

The comparison is against plan.Name(), not stored.Name: plan_admitted records
the plan's own name, which is independent of the name it was saved under. A
test asserting the saved name would have passed while breaking every
legitimate resume, so both directions are pinned.

TASK SUMMARIES CARRIED CONTROL BYTES. planTaskSummary cut at the first
newline and left every other control byte intact, and these strings are
model-authored — the realistic path is indirect prompt injection, poisoned
file or web content echoed into orchestrate args and painted into the inline
panel, the sidebar rows and the detail pane. It now goes through
sanitizeCardText, which permission_detail.go already applies to exactly this
class of data.

BACKGROUND PLANS LOST THEIR PANEL. beginRun cleared the orchestrate panel
unconditionally, but background plans are built to outlive the run that
launched them and every message they post carries the background flag
precisely to pass the stale-run guard. After the clear those messages passed
the guard and no-oped against an empty byID, so the PLAN surface vanished for
the rest of the plan's life while it kept running. BackgroundPlanLive gates
the clear, consulting background alongside cancelPlan for the same reason
RunningPlanName does — the launcher sets one synchronously and the goroutine
sets the other later, so reading either alone leaves a window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Pushed that commit straight onto your branch rather than leaving it for you to cherry-pick — maintainerCanModify is on and it was a clean fast-forward from 72425c58. Head is 5e9a8a79 now. Shout if you'd rather I didn't touch the branch and I'll go back to sending patches.

Carrying on with the rest of the review list; I'll push them as separate commits so any one is easy to drop.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

9b6ea932 — the scope.go refcount race, pushed onto your branch.

It's real, and I want to show the reproduction rather than assert it, because it took me several wrong turns to pin down. Driving the two mutations apart and stepping a second caller through the gap:

after add:   readRoots=[.../shared]  tempReads=map[.../shared:1]
in window:   second caller tracked=false      <- a NO-OP undo
after release: validateRead -> "requires access outside the workspace"

AddTemporaryRead reads exactly that pair to decide what a caller holds. In the window it finds the root covered, finds no refcount, concludes the cover is permanent, and hands back a no-op undo — then the release strips the root and that caller has silently lost access it believes it holds. Both mutations happen under one hold now, and releaseTemporaryWrite gets the same treatment. removeReadRoot/removeWriteRoot existed only to re-acquire the lock, so they're gone.

The tests are weaker than they look and I'd rather say so than let them read as a guarantee. Neither reproduces the window on demand. The concurrent one can't hit a gap that narrow reliably — it survived the unfixed code even with an injected runtime.Gosched() — and the sequential one holds two references, so the refcount never reaches zero and the delete never runs. They assert the invariant the bug broke and are worth keeping, but the evidence is the reproduction above, not them.

Worth passing on, since it'll bite anyone testing this area: my first two attempts used t.TempDir() with NewScope, and NewScope adds the system temp directory as a write root — so AddTemporaryRead returned early via writeRootCoversLocked and never touched the refcount. Both tests passed against the buggy code while exercising none of it. The scope is built directly in these tests for that reason.

That's the two correctness items from the review done. Remaining, if you want another slice: the /effort zeromaxing mid-run guard, the two glow hit-testers, max_wall_seconds accepting negatives, the stale "read-only" strings, research.json's placeholder, Done/Remaining as dead API, the posture_off_identical claim, and the two missing modal guards.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/sandbox/scope_temp_refcount_test.go (1)

67-102: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the split-lock regression deterministic.

This loop increases contention but does not force AddTemporaryRead into the former gap between removing tempReads[root] and removing readRoots. The previous implementation can pass depending on scheduling.

Add a package-private test synchronization hook around that transition. Pause release at the hook, start a second AddTemporaryRead, then assert that it cannot return until the root-list update completes. Run go test -race ./internal/sandbox after adding the test.

As per coding guidelines, “Run affected concurrent code under the race detector.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/sandbox/scope_temp_refcount_test.go` around lines 67 - 102, Make
TestTemporaryReadGrantsSurviveConcurrentReleases deterministic by adding a
package-private synchronization hook around the temporary-read release
transition that updates tempReads and readRoots. Pause the first undo at this
hook, start a second AddTemporaryRead, assert it remains blocked until the
root-list update completes, then resume and verify the grant succeeds; run go
test -race ./internal/sandbox.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/sandbox/scope_temp_refcount_test.go`:
- Around line 67-102: Make TestTemporaryReadGrantsSurviveConcurrentReleases
deterministic by adding a package-private synchronization hook around the
temporary-read release transition that updates tempReads and readRoots. Pause
the first undo at this hook, start a second AddTemporaryRead, assert it remains
blocked until the root-list update completes, then resume and verify the grant
succeeds; run go test -race ./internal/sandbox.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 657e9d30-988e-47dd-8dfd-e7cfc5634ac7

📥 Commits

Reviewing files that changed from the base of the PR and between 5e9a8a7 and 9b6ea93.

📒 Files selected for processing (2)
  • internal/sandbox/scope.go
  • internal/sandbox/scope_temp_refcount_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/sandbox/scope.go

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Two more commits, 4c31a747 and cc3e9f99. That's eight of the ten review items done; the last two I've deliberately left for you.

4c31a747 — three missing guards.

/effort zeromaxing delegated to handleProfileCommand with no idle check, so it reached the mutation /profile refuses mid-run. Idle behaviour is unchanged and tested, so the guard can't quietly disable the feature.

orchestrateHeaderAtMouse had no modal guard while orchestrateTaskAtMouse directly above it does. Worth flagging how my first test got this wrong: I used a picker, it passed against the unguarded code, and the mutation survived — because sidebarAvailable already suppresses the sidebar for pickers. The actual gap is the / palette, which sidebarAvailable deliberately does not suppress (your comment in sidebar.go says exactly why). Retested with the palette and the mutation fails properly.

The posture chip in mouse.go is checked before the surface switch, so the guards in those cases don't cover it — with the /model picker open, clicking it discarded that picker's loaded state.

cc3e9f99 — one validation bug and two false claims.

planInt returns 0 for both absent and negative, so seconds > 0 read a max_wall_seconds of -60 as "unset" and ran the plan unbounded. Absent and explicit zero keep their meanings, tested, so the stricter parse can't turn "unset" into an error.

The approval card said "read-only specialist sub-agents", but PermissionForArgs prompts only when argsCanWrite — so every card a user actually sees is asking about a write-capable plan. It was inverted on the one screen where it matters.

And the posture-off test claimed byte-identical-to-a-build-without-the-feature. It isn't: system_prompt.go drops the ~5 KB confirmation policy for any run that can't mutate, and runCanMutate doesn't consult the posture, so an all-read-only run gets a smaller prompt regardless. The drop is deliberate and fails closed — the claim was just wider than the proof, and the comparison can't catch it since both registries take the same branch. Comment now states what's actually proved.

Left for you, on purpose — these are design calls, not defects, and I didn't want to make them unilaterally on your branch:

  • PlanProgress.Done/Remaining have only test callers. Wire in or drop, your read.
  • research.json ships with THE SUBJECT never substituted, and synthesise/refute say "using the three searches above" while NewPlanRunner passes task.Prompt verbatim so a dependent never sees upstream output. That needs either argument substitution or unshipping the plan — a feature decision either way.

Everything green apart from TestAltScreenTranscriptScrollKeepsFooterFixed, which fails identically on clean main on my Windows box. Shout if you'd rather I stop pushing to the branch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/specialist/plan_tool.go (1)

212-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the stale safety rationale.

The new Reason correctly states that tasks may receive write tools. However, the surrounding Safety() comments at Lines 197-206 still state that tasks are read-only and that write approval is future work. PermissionForArgs now handles write-capable plans. Update those comments to describe the current approval flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_tool.go` around lines 212 - 216, Update the Safety()
comments near the specialist plan approval logic to remove the outdated
read-only and future-write-approval claims. Describe that PermissionForArgs
evaluates plan arguments and prompts for approval when tasks may use
write-capable tools, consistent with the current behavior and the Reason text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/specialist/plan_budget_negative_test.go`:
- Around line 34-48: Update TestBudgetStillAcceptsAbsentAndZeroTimeouts to
retain the plan returned by ParsePlan and assert that both Budget.MaxWall and
Budget.MaxStall remain zero for the absent and explicit-zero inputs, while
preserving the existing no-error assertion.

In `@internal/specialist/plan.go`:
- Around line 490-510: Validate max_wall_seconds in the budget parsing flow
before converting seconds to time.Duration, rejecting values that exceed the
maximum representable positive duration. Preserve acceptance of 9223372036 and
reject 9223372037, and add focused tests covering both cases near the existing
max_stall_seconds validation.

In `@internal/tui/zeromaxing_guards_test.go`:
- Around line 58-61: Update the sanity check around orchestrateHeaderAtMouse in
the test to call t.Fatal instead of t.Skip when the calculated click misses the
PLAN header, ensuring fixture or layout errors fail the test and the modal guard
remains covered.

---

Nitpick comments:
In `@internal/specialist/plan_tool.go`:
- Around line 212-216: Update the Safety() comments near the specialist plan
approval logic to remove the outdated read-only and future-write-approval
claims. Describe that PermissionForArgs evaluates plan arguments and prompts for
approval when tasks may use write-capable tools, consistent with the current
behavior and the Reason text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6790f934-b9d9-4d3a-91ca-e5a31a1052e3

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6ea93 and cc3e9f9.

📒 Files selected for processing (8)
  • internal/agent/posture_off_identity_test.go
  • internal/specialist/plan.go
  • internal/specialist/plan_budget_negative_test.go
  • internal/specialist/plan_tool.go
  • internal/tui/mouse.go
  • internal/tui/session_controls.go
  • internal/tui/sidebar_plan_detail.go
  • internal/tui/zeromaxing_guards_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/tui/mouse.go
  • internal/tui/sidebar_plan_detail.go
  • internal/agent/posture_off_identity_test.go
  • internal/tui/session_controls.go

Comment thread internal/specialist/plan_budget_negative_test.go Outdated
Comment thread internal/specialist/plan.go
Comment thread internal/tui/zeromaxing_guards_test.go
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Ran a design-level audit on top of the earlier line review — six dimensions, every candidate sent to a refuter that had to reproduce it. 64 candidates, 30 survived, and they collapse into two roots. Two commits: 72d4a316, 3b9f7b29.

max_wall_seconds didn't bound a concurrent plan

The deadline was read only before dispatching, and the walk only reaches that check when it needs a free slot. A plan whose ready set fits the worker pool dispatches in one wave and is never checked again. Measured, four 2s tasks under a 1s wall:

max_workers=4  ->  2.0s elapsed, status=completed, 4 succeeded, 0 skipped
max_workers=1  ->        status=partial,   1 succeeded, 3 skipped

Asking for parallelism deleted the bound, and nothing cancelled an in-flight child — the plan context was WithCancel and the only deadline reads were the two dispatch gates. It now carries the wall as a real deadline, reaching children through the machinery your user-stop path already proves works.

This is the case you removed max_tokens' cap for, writing that a number which neither bounds spend nor lets a plan finish "is worse than no number, because it reads like a guarantee." The wall was the worse version — the token check at least fires between tasks; the wall fired zero times in a fan-out. And the approval card shows 300s wall while never showing max_workers, the parameter that disabled it.

Both cancellation paths now say which reason stopped a task. A wall expiry is the plan spending what it was allowed; a cancel is a person deciding. Both read "the run was stopped".

The write-capable path was half-wired

ParsePlan permits a task to name a write tool — deliberate and recent. That grant is what makes PermissionForArgs prompt and RequiresIsolation prepare a worktree. Two layers downstream still assumed every task is read-only:

  • The child's system prompt was one literal telling every task "You have read-only tools" and "do not attempt to modify anything" — including one granted write_file or bash. A child told not to modify anything doesn't use the tool it holds, so the approval the user gave and the worktree prepared for it bought nothing. Now built from the grant, using the same allow-list ParsePlan validates against. Read-only wording unchanged.
  • savedPlanLimits granted only read-only names, so a plan that parses at run time failed on save, show, run, restart and resume. The durability surface excluded exactly the plans whose work is most worth keeping. Now PlanGrantableToolNames — no authority widened, the grant is intersected again at run time, and an ungrantable tool is still refused (both directions tested).

One thing I deliberately did not touch, because it's yours to decide. Plan children still run at --auto low, where plan_runner.go says "Explicitly NOT MemberAutonomy: Phase 2 tasks are read-only, and granting it here would be the authority widening that was ruled out." That comment and ParsePlan's "A TASK MAY NOW NAME A WRITE TOOL" contradict each other — only one can stand. Either children get the autonomy rung so a granted write tool works end to end, or ParsePlan stops accepting write tools until they do. Widening child autonomy isn't a call to make inside a review fix.

Also still open from the audit, smaller: PlanWorkspace.Describe has one write site and zero read sites, so nobody is ever told where a write-capable plan's work went; the worktree is keyed on the optional plan name, so unrelated plans share one tree; and a user stop reaches the model as tools.StatusError, which isRetriableToolError counts into the repeated-failure streak — so a deliberate stop can earn a "fix the arguments and try once more" hint.

The audit also confirmed a lot as sound and I'd rather say so: read-only default per task, the write allow-list, fail-closed isolation, only stalls retried, cancelled never retried, and nesting genuinely prevented across three layers. 34 candidates were refuted, most because you'd already handled them somewhere the reviewer hadn't looked.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@gnanam1990 six commits from me are on your branch now, head 3b9f7b29. When you have a stretch, could you review them as commits rather than as a diff — each one stands alone and explains its reasoning, and any of them can be dropped without disturbing the others.

5e9a8a79  resume the named plan, sanitize task text, keep background panels
9b6ea932  release a temporary root under one lock hold
4c31a747  guard the zeromaxing effort switch and two mouse hit-testers
cc3e9f99  reject negative plan timeouts, correct two false claims
72d4a316  make max_wall_seconds bound a concurrent plan
3b9f7b29  let a task granted write tools actually use them

The two I'd most want your eyes on, because they touch decisions you reasoned about explicitly:

72d4a316 puts the wall budget on the plan context as a deadline. Your comment at the dispatch gate — "a budget-exhausted or timed-out plan SKIPS the rest rather than aborting" — was written when the executor was sequential and one task was ever in flight, which is why it read as a bound. I kept the skip behaviour for undispatched tasks and only added cancellation for in-flight ones, but you may have wanted the softer semantics on purpose.

3b9f7b29 makes the child prompt reflect the grant. It does not touch autonomy — plan children still run at --auto low per your "Explicitly NOT MemberAutonomy" note. That leaves the contradiction open: ParsePlan says a task may name a write tool, plan_runner says Phase 2 tasks are read-only. One of those has to give and it's your call which.

Two things I got wrong along the way, in case they save you time: NewScope adds the system temp dir as a write root, so a t.TempDir() target never reaches the temp-read refcount path — two of my scope tests passed against the bug while exercising none of it. And a test of mine waited on a bare <-ctx.Done(), so when the wall deadline was mutated out it hung a full run for ten minutes instead of failing.

No rush, and genuinely fine to drop any of them — it's your PR and your design. @kevin-hnnh no action needed here, just so you know the branch moved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
internal/specialist/plan_runner.go (1)

153-186: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Description still says "Read-only plan task." for write-capable grants.

planTaskSystemPrompt (Line 176) now correctly tells a write-capable task to make changes, but Metadata.Description (Line 160) is still unconditionally "Read-only plan task.". A task granted edit_file or bash now carries a manifest whose SystemPrompt says "you have been granted tools that CHANGE things" while its Description says the opposite. This is the same "read-only" mislabeling raised in the earlier review comment on this function; only the SystemPrompt half of that fix was applied, not the Description field.

Branch Description the same way planTaskSystemPrompt branches, using the same grantsPlanWriteTool(grantedTools) check, so the two fields never disagree.

🔧 Proposed fix: branch Description on the granted set
 func planTaskManifest(name string, grantedTools []string) Manifest {
 	if strings.TrimSpace(name) == "" {
 		name = "explorer"
 	}
+	description := "Read-only plan task."
+	if grantsPlanWriteTool(grantedTools) {
+		description = "Write-capable plan task, running in the plan's isolated worktree."
+	}
 	return Manifest{
 		Metadata: Metadata{
 			Name:        name,
-			Description: "Read-only plan task.",
+			Description: description,
 			Tools:       grantedTools,
 		},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_runner.go` around lines 153 - 186, Update
planTaskManifest so Metadata.Description branches on
grantsPlanWriteTool(grantedTools), matching the branching behavior of
planTaskSystemPrompt: use a write-capable description when the granted set
includes write tools and retain the read-only description otherwise, ensuring
both manifest fields consistently reflect the same grants.
🧹 Nitpick comments (1)
internal/specialist/zzhol829_probe_test.go (1)

12-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Convert these probes into real assertions before merge.

TestZZProbeConcurrentWritersShareOneTree (Lines 12-53) and TestZZProbeHeadOfLineBlocking (Lines 58-108) exercise exactly the concurrent-writer and scheduling behaviors this PR cohort is meant to lock in, but both only t.Logf their observations (Lines 49-52, 98-107). Neither test can fail regardless of what ExecutePlanIn actually does with worktree sharing or head-of-line ordering.

The file name (zzhol829_probe_test.go) and the "PROBE" comments (Lines 11, 55) suggest this started as exploration rather than a maintained regression test. If the intent is to keep documenting scheduling characteristics for future readers, that's fine, but pair it with real assertions once the expected behavior is decided (for example, bound peak concurrent writers in Probe 1, or assert an ordering/timing expectation in Probe 2). Otherwise, consider removing this file before merge so it doesn't linger as a log-only artifact in the permanent test suite.

"add a regression test for behavior changes" for **/*_test.go files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/zzhol829_probe_test.go` around lines 12 - 108, Convert
TestZZProbeConcurrentWritersShareOneTree and TestZZProbeHeadOfLineBlocking from
log-only probes into regression tests with deterministic assertions for the
intended worktree-sharing and scheduling behavior. Assert the expected peak
writer concurrency and relevant task ordering or timing, remove probe-only
logging/comments as appropriate, and ensure failures occur when ExecutePlanIn or
ExecutePlan changes those behaviors.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/specialist/plan_exec.go`:
- Around line 196-211: Update the MaxWall deadline setup around the plan
execution context so elapsed paused time in WaitWhilePaused does not consume the
plan’s wall budget. Rework the timeout handling to suspend and resume remaining
MaxWall accounting across pauses while preserving cancellation and
undispatched-task skip behavior, and add a regression test covering a pause
longer than the remaining wall budget followed by successful resumption.

In `@internal/specialist/zzhol829_probe_test.go`:
- Around line 136-172: The test TestZZProbeRealFailureRelabelledCancelled
reproduces a relabeling behavior where tasks marked TaskFailed are retroactively
changed to TaskCancelled after context cancellation, but only logs the results
without asserting on them, providing no regression protection. Replace the
t.Logf statements at the end of the test with explicit assertions that verify
the intended outcome: either assert that the "boom" task keeps its TaskFailed
outcome with the original error "compile error in main.go" (if the harvest logic
in plan_exec.go is fixed to prevent relabeling of already-failed tasks), or
assert that relabeling to TaskCancelled occurs and add a comment documenting why
this behavior is intentional. Ensure the test fails if the relabeling behavior
changes unexpectedly.

---

Duplicate comments:
In `@internal/specialist/plan_runner.go`:
- Around line 153-186: Update planTaskManifest so Metadata.Description branches
on grantsPlanWriteTool(grantedTools), matching the branching behavior of
planTaskSystemPrompt: use a write-capable description when the granted set
includes write tools and retain the read-only description otherwise, ensuring
both manifest fields consistently reflect the same grants.

---

Nitpick comments:
In `@internal/specialist/zzhol829_probe_test.go`:
- Around line 12-108: Convert TestZZProbeConcurrentWritersShareOneTree and
TestZZProbeHeadOfLineBlocking from log-only probes into regression tests with
deterministic assertions for the intended worktree-sharing and scheduling
behavior. Assert the expected peak writer concurrency and relevant task ordering
or timing, remove probe-only logging/comments as appropriate, and ensure
failures occur when ExecutePlanIn or ExecutePlan changes those behaviors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: acda29ee-1b70-440a-9478-ac7086b4badb

📥 Commits

Reviewing files that changed from the base of the PR and between cc3e9f9 and 3b9f7b2.

📒 Files selected for processing (7)
  • internal/specialist/plan_exec.go
  • internal/specialist/plan_runner.go
  • internal/specialist/plan_wall_budget_test.go
  • internal/specialist/plan_write_grant_test.go
  • internal/specialist/zzhol829_probe_test.go
  • internal/tui/orchestrate_saved.go
  • internal/tui/orchestrate_saved_write_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/tui/orchestrate_saved.go

Comment thread internal/specialist/plan_exec.go
Comment thread internal/specialist/zzhol829_probe_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/specialist/plan_tool.go (2)

585-592: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The background path drops the auto-assignment report.

assignNotes is only rendered on the foreground path at Line 611. A background plan can be fully re-modelled, and pay for a router call, and the returned text says nothing about it. autoAssignSummary documents this reporting as mandatory. Append it here too.

🔧 Report the assignment on the background path
 		return tools.Result{
 			Status: tools.StatusOK,
 			Output: fmt.Sprintf(
 				"Plan %q started in the background with %d tasks. It is NOT finished — its result will arrive on a later turn. "+
 					"Carry on with other work; do not wait for it and do not report it as done.",
-				plan.Name(), plan.TaskCount()),
+				plan.Name(), plan.TaskCount()) + autoAssignSummary(assignNotes),
 			Meta: map[string]string{"plan_status": "background"},
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_tool.go` around lines 585 - 592, Update the
background-plan result in the plan execution flow to append the auto-assignment
report from autoAssignSummary, matching the foreground path’s assignNotes
reporting. Keep the existing background status, task count, and deferred-result
messaging intact while ensuring any assignment summary is included in the
returned Output.

506-539: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

A refused plan still pays for discovery and routing.

Line 503 was added so an invalid plan costs no provider call. Two later refusals still run after the spend: the missing-runner check at Line 516 and the one-plan-at-a-time check at Line 531. Both are pure and knowable before Line 506. With a background plan already running, every following orchestrate call lists the provider's models, spends a frontier-model router call, and is then refused.

Move both checks above autoAssignModels.

🔧 Refuse before spending
+	if tool.RunTask == nil {
+		return tools.Result{Status: tools.StatusError, Output: "Error: orchestrate has no task runner wired."}
+	}
+	if running, busy := runningPlanOn(tool.Recorder); busy {
+		return tools.Result{
+			Status: tools.StatusError,
+			Output: fmt.Sprintf(
+				"Error: plan %q is still running, and a session shows one plan at a time. "+
+					"Wait for it to finish — its result arrives on a later turn if it is a background plan — "+
+					"or stop it with /plans stop, then run this one.", running),
+		}
+	}
 	assignNotes, autoErr := tool.autoAssignModels(ctx, args, options)
 	if autoErr != nil {
 		return tools.Result{Status: tools.StatusError, Output: "Error: " + autoErr.Error()}
 	}
@@
 	plan, err := ParsePlan(args, tool.limits(options))
 	if err != nil {
 		return tools.Result{Status: tools.StatusError, Output: "Error: " + err.Error()}
 	}
-	if tool.RunTask == nil {
-		return tools.Result{Status: tools.StatusError, Output: "Error: orchestrate has no task runner wired."}
-	}
-	if running, busy := runningPlanOn(tool.Recorder); busy {
-		return tools.Result{ ... }
-	}

Note that the existing comment at Line 528 justifies checking after parsing. Line 503 already parses, so the reason for the ordering no longer applies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_tool.go` around lines 506 - 539, Move the RunTask
nil check and runningPlanOn(tool.Recorder) admission check ahead of
autoAssignModels in the orchestrate flow, while keeping ParsePlan first so
invalid plans are still reported as validation errors. Update the nearby
ordering comment to reflect that these refusals now occur before discovery and
routing, and preserve the existing error responses.
🧹 Nitpick comments (12)
internal/specialist/router_manifest_test.go (1)

21-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Delete the unused runner scaffolding in this test.

The assertions read only seen, which the stub PlanRunner at Lines 40-43 sets. exec, planCtx and run are never exercised: run is discarded by _ = run at Line 46, and the RunChild reassignment at Lines 34-36 mutates a copy after NewPlanRunner already captured planCtx, so it has no effect at all. A future reader will assume this test drives the real executor. It does not.

♻️ Proposed trim
-	var seen string
-	exec := Executor{
-		BinaryPath:   "/bin/true",
-		NewSessionID: func() (string, error) { return "specialist_00000000000000000000000a", nil },
-		Load:         func(LoadOptions) (LoadResult, error) { return LoadResult{}, nil },
-		RunChild: func(context.Context, string, []string, func(streamjson.Event)) (ChildRunResult, error) {
-			return ChildRunResult{Started: true}, nil
-		},
-	}
-	// The runner builds the manifest; capture it by intercepting the load path.
-	planCtx := PlanTaskContext{Executor: exec, Cwd: t.TempDir(), SpecialistName: "explorer"}
-	planCtx.Executor.Load = func(LoadOptions) (LoadResult, error) { return LoadResult{}, nil }
-	run := NewPlanRunner(planCtx)
-	planCtx.Executor.RunChild = func(context.Context, string, []string, func(streamjson.Event)) (ChildRunResult, error) {
-		return ChildRunResult{Started: true}, nil
-	}
-
+	var seen string
 	// Drive the real router entry point so the request it builds is the one tested.
 	_, _, _ = routeTaskModels(context.Background(),
 		func(_ context.Context, req PlanTaskRequest) (TaskResult, error) {
 			seen = req.SystemPrompt
 			return TaskResult{Outcome: TaskSucceeded, Output: `{"assignments":[]}`}, nil
 		},
 		PlanTaskRequest{Tools: []string{"read_file"}}, "m",
 		routerTasks(), routerCandidates(), "")
-	_ = run

The streamjson import then becomes unused in this file only if the second test does not need it; it does, so leave the import list alone.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/router_manifest_test.go` around lines 21 - 46, Remove the
unused Executor, planCtx, NewPlanRunner, RunChild reassignment, and run
scaffolding from the test, leaving only the routeTaskModels setup needed to
capture seen. Do not alter the streamjson import if it remains required by the
other test.
internal/specialist/plan_model_router_test.go (1)

125-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The table does not cover the "no runner" case the comment claims.

Line 125 says "No router model, no candidates, no runner: all skip silently", and the table has only "no model" and "no candidates". The third element of each [3]any holds run and is never read. Either add a nil-runner case or drop the claim and the unused slot.

💚 Proposed addition
-	for name, args := range map[string][3]any{
-		"no model":      {"", routerCandidates(), run},
-		"no candidates": {"qwen3.5:397b", []DiscoveredModel(nil), run},
-	} {
-		called = false
-		model := args[0].(string)
-		cands, _ := args[1].([]DiscoveredModel)
-		if _, _, err := routeTaskModels(context.Background(), run, PlanTaskRequest{}, model,
-			routerTasks(), cands, ""); err != nil {
+	type skipCase struct {
+		model      string
+		candidates []DiscoveredModel
+		runner     PlanRunner
+	}
+	for name, tc := range map[string]skipCase{
+		"no model":      {"", routerCandidates(), run},
+		"no candidates": {"qwen3.5:397b", nil, run},
+		"no runner":     {"qwen3.5:397b", routerCandidates(), nil},
+	} {
+		called = false
+		if _, _, err := routeTaskModels(context.Background(), tc.runner, PlanTaskRequest{}, tc.model,
+			routerTasks(), tc.candidates, ""); err != nil {
 			t.Errorf("%s: expected a silent skip, got %v", name, err)
 		}

If routeTaskModels does not guard a nil runner today, that is the bug this case would find.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_model_router_test.go` around lines 125 - 140, Add a
`"no runner"` table case to the test around routeTaskModels, using a nil runner
while retaining valid router model and candidates, and ensure the invocation
passes each case’s runner value instead of the fixed run variable. Keep the
silent-skip and router-not-called assertions so the test verifies nil-runner
handling.
internal/specialist/plan_runner.go (1)

98-123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename task_ID and stop shadowing task with a token count.

Two readability problems in one closure:

  • task_ID uses an underscore, which is not idiomatic Go for a function name. taskLabel or requestTaskID reads the same and matches the rest of the file.
  • Line 104 binds task := taskTokens.Add(...), an int64, while the enclosing scope already has task bound to the Task at Line 64. A later edit that reaches for task.ID inside this closure compiles into something unexpected or fails confusingly.
♻️ Proposed rename
-				spent := *event.TotalTokens
-				task := taskTokens.Add(int64(spent))
+				spent := *event.TotalTokens
+				spentSoFar := taskTokens.Add(int64(spent))
 				switch {
-				case req.MaxTaskTokens > 0 && task > int64(req.MaxTaskTokens):
+				case req.MaxTaskTokens > 0 && spentSoFar > int64(req.MaxTaskTokens):
 					overspent.Store(fmt.Sprintf(
 						"task %s stopped after %d tokens: budget.max_tokens_per_task is %d",
-						task_ID(req), task, req.MaxTaskTokens))
+						taskLabel(req), spentSoFar, req.MaxTaskTokens))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_runner.go` around lines 98 - 123, Rename the helper
function task_ID to an idiomatic name such as taskLabel, updating all references
in the closure and surrounding plan-runner code. In the counted closure, rename
the local token-count result from task to a distinct name such as taskTotal, and
use that name in the limit comparison and overspent message while leaving the
enclosing Task variable unshadowed.
internal/specialist/plan_model_fallback_test.go (1)

37-54: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Plan-task fixtures mutate plain variables from worker goroutines. RunChild and RunTask callbacks execute on the plan scheduler's worker goroutines, so every counter and slice they touch is shared state. Both sites are safe today only because the fixture sets one worker or one task. Raising either value turns these into data races under go test -race, and the failure will point at the harness rather than the behavior under test. Add a sync.Mutex (or use atomic.Int64 for counters) so the fixtures stay race-free independent of max_workers.

  • internal/specialist/plan_model_fallback_test.go#L37-L54: guard the ran append with a mutex, and apply the same treatment to the attempts counters in the remaining tests of this file.
  • internal/specialist/child_scope_test.go#L194-L214: guard the seen append in RunTask with a mutex.

As per coding guidelines "Run affected concurrent code under the race detector".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_model_fallback_test.go` around lines 37 - 54, Make
the concurrent test fixtures race-free: in
internal/specialist/plan_model_fallback_test.go around lines 37-54, protect the
ran append and every attempts counter in the remaining tests with a sync.Mutex
or atomic.Int64; in internal/specialist/child_scope_test.go around lines
194-214, protect the seen append in RunTask with a mutex. Run the affected tests
with the race detector.

Source: Coding guidelines

internal/specialist/served_forms_test.go (1)

77-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Do not discard the error here.

If autoAssignModels returns an error, notes may be empty and the failure reports "an Ollama list on an xAI session was accepted", which points at the wrong cause. Assert the error explicitly.

♻️ Proposed change
-	notes, _ := tool.autoAssignModels(context.Background(), args, tools.RunOptions{Model: "grok-4.5"})
+	notes, err := tool.autoAssignModels(context.Background(), args, tools.RunOptions{Model: "grok-4.5"})
+	if err != nil {
+		t.Fatalf("autoAssignModels: %v", err)
+	}
 	if !strings.Contains(strings.Join(notes, " "), "different provider") {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/served_forms_test.go` around lines 77 - 80, Update the
autoAssignModels call in the test to retain its returned error and assert that
the error is nil before checking notes, so execution reports an assignment
failure directly instead of misclassifying it as an accepted Ollama list.
internal/specialist/exec.go (1)

362-372: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Compare cleaned paths, not raw strings.

The workspace skip uses exact string equality. A supplier that reports /ws/ or /ws/. while --cwd is /ws re-emits the workspace as --add-dir, which is the exact case the comment above says cannot happen. Duplicate entries also produce duplicate flag pairs. filepath.Clean on both sides plus a seen-set closes both.

♻️ Proposed hardening
 func appendExtraWriteRootArgs(args []string, roots []string, cwd string) []string {
-	workspace := strings.TrimSpace(cwd)
+	workspace := ""
+	if trimmed := strings.TrimSpace(cwd); trimmed != "" {
+		workspace = filepath.Clean(trimmed)
+	}
+	seen := map[string]bool{}
 	for _, root := range roots {
 		root = strings.TrimSpace(root)
-		if root == "" || root == workspace {
+		if root == "" {
+			continue
+		}
+		root = filepath.Clean(root)
+		if root == workspace || seen[root] {
 			continue
 		}
+		seen[root] = true
 		args = append(args, "--add-dir", root)
 	}
 	return args
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/exec.go` around lines 362 - 372, Update
appendExtraWriteRootArgs to normalize both cwd and each root with filepath.Clean
before comparing them, so equivalent paths such as trailing-slash or dot forms
are skipped. Track normalized roots in a seen-set and avoid appending duplicate
--add-dir flag pairs while preserving empty-root filtering and argument order.
internal/specialist/usage_pricing_test.go (1)

139-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test does not prove the property its comment claims.

The notes are hand-written, so the assertion only shows autoAssignSummary passes a substring through. Nothing here shows the router ever produces a note containing its token spend. A change that stops emitting the token count from the routing call keeps this test green. Drive the routing path and assert the note it generates, as TestTheUsageRollupWritesThePricingFields does for the rollup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/usage_pricing_test.go` around lines 139 - 145, Strengthen
TestTheRoutingCallReportsWhatItSpent by exercising the actual routing path
instead of passing hand-written notes directly to autoAssignSummary. Use the
same setup and invocation pattern as TestTheUsageRollupWritesThePricingFields,
then assert that the routing call’s generated note includes its token spend
before validating the summary.
internal/specialist/manifest.go (1)

272-281: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use ResolveWithFallback for manifest model canonicalization.

ResolveID leaves sonnet 4.5 unresolved and preserves deprecated claude-haiku-3.5. Use the same resolver as resolveTaskModel so manifest metadata, --model, and usage accounting use the model that the child executes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/manifest.go` around lines 272 - 281, Update manifest
model canonicalization to call the registry’s ResolveWithFallback, matching the
resolver used by resolveTaskModel. Apply the fallback result to
manifest.Metadata.Model so unresolved aliases such as sonnet 4.5 and deprecated
identifiers such as claude-haiku-3.5 resolve consistently for execution and
accounting.
internal/specialist/plan_model_assign_test.go (1)

26-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

A duplicated assertion probably replaced an intended one.

Lines 26-28 and 29-31 assert the same thing about tiers.strong. The second block likely meant to assert tiers.balanced, which no test in this function checks directly. Replace it or delete it.

🔧 Assert the middle tier instead
-	if tiers.strong != "claude-opus-4.1" {
-		t.Errorf("strong = %q", tiers.strong)
-	}
+	if tiers.balanced != "claude-sonnet-4.5" {
+		t.Errorf("balanced = %q, want the mid-priced tool-calling model", tiers.balanced)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_model_assign_test.go` around lines 26 - 34, Replace
the duplicated second assertion on tiers.strong with an assertion validating
tiers.balanced, preserving the expected middle-tier model value and
error-reporting style used in the test.
internal/specialist/plan_model_router.go (1)

220-240: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Match the router's answer with the same id normalization used elsewhere.

offered holds exact canonical ids, and Line 236 compares by exact string. modelIDForms exists because a model id has several legitimate spellings, including an Ollama :latest tag. If the router replies with glm-5.2 where the list printed glm-5.2:latest, the decision is dropped without a note and the task falls back to the classifier. Reuse the form-aware lookup.

🔧 Resolve the router's choice through the offered forms
-	offered := make(map[string]bool, len(candidates))
-	for _, model := range candidates {
-		if id := strings.TrimSpace(model.ID); id != "" {
-			offered[id] = true
-		}
-	}
+	offered := map[string]string{}
+	for _, model := range candidates {
+		id := strings.TrimSpace(model.ID)
+		if id == "" {
+			continue
+		}
+		for _, form := range modelIDForms(id) {
+			offered[form] = id
+		}
+	}
@@
-		if !offered[chosen] {
+		canonical, ok := offered[chosen]
+		if !ok {
 			continue
 		}
-		out[id] = chosen
+		out[id] = canonical
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_model_router.go` around lines 220 - 240, Update the
router assignment validation in the loop over decoded.Assignments to resolve
chosen model IDs using the existing modelIDForms normalization rather than exact
offered-map lookup. Preserve rejection of unknown models, but accept equivalent
spellings such as an omitted Ollama :latest tag and store the canonical offered
ID in out so downstream dispatch uses the listed model identifier.
internal/specialist/plan_model_assign.go (1)

86-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc comments are stacked on the wrong functions.

Lines 86-101 document buildModelTiers and canonicalisation, but they sit directly above rankedEligibleModels, so go doc attributes all of it to that function. Lines 301-318 contain two separate assignModelsToTaskArgs summaries. Move the tier text above buildModelTiers at Line 159 and merge the duplicate summary.

Also applies to: 301-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/plan_model_assign.go` around lines 86 - 110, Move the
tier-ranking and canonicalisation documentation currently above
rankedEligibleModels so it directly precedes buildModelTiers, preserving the
rankedEligibleModels comment for that function. In the assignModelsToTaskArgs
area, merge the two adjacent duplicate summaries into one accurate doc comment
and remove the redundant block.
internal/cli/provider_models.go (1)

74-103: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound the verify probe concurrency and confirm the race detector passes.

The verify loop launches one goroutine per discovered model with no cap:
"for index, model := range models { wait.Add(1); go func(index int, id string) { defer wait.Done(); results[index] = probe(ctx, id) }(index, model.ID) }"

Each goroutine builds a new provider client and calls discoveryCredentialProfile, which reads config.ProviderKeyStore() (a shared credential store). For a provider that lists dozens of models, this fires that many concurrent network probes and concurrent credential-store reads at once. Add a bounded worker pool (a semaphore or fixed-size channel) so probe concurrency has a ceiling and does not risk provider-side rate limiting or resource exhaustion.

As per coding guidelines, "Run affected concurrent code under the race detector," run this verify path under go test -race (or an equivalent manual -race run) to confirm config.ProviderKeyStore() and the shared results slice/verdicts map access are race-free under concurrent probing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/provider_models.go` around lines 74 - 103, Bound the concurrent
probing in the options.verify block by replacing the unbounded per-model
goroutines with a fixed-size worker pool or semaphore, using an appropriate
concurrency ceiling while preserving each model’s result. Ensure shared results
and verdicts access remains race-free, then run the affected verify path with go
test -race or an equivalent race-enabled run.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/provider_models.go`:
- Around line 293-322: Update planModelDiscoverer to filter discovered models
with providermodelcatalog.ModelIDAllowedForProvider for the active provider
before converting them to specialist.DiscoveredModel; exclude catalog-disallowed
IDs from out, while preserving allowed models and the existing discovery error
propagation.

In `@internal/config/resolver.go`:
- Around line 396-398: Stop merging untrusted project model exclusions in the
resolver’s PlanModels handling; remove or gate the
src.Profiles.PlanModels.Exclude append behind explicit user-approved cost
policy. Update internal/config/plan_size_test.go lines 249-270 to verify project
model preferences cannot alter the user’s candidate set, replacing the existing
safety assertion.

In `@internal/specialist/plan_exec.go`:
- Around line 1050-1063: Update tasksCutForBudget and the task-result flow so
budget termination is identified structurally rather than by matching task.Err
text, including both plan-level and max_tokens_per_task cancellations. Have the
runner mark the relevant TaskResult when a budget stops a task, and select
cancelled results using that signal alongside TaskSkippedBudget; adjust the
generated headline wording so cancelled tasks are not described as having “never
ran.”

In `@internal/specialist/plan_model_probe.go`:
- Around line 145-166: Update proveModels to cap the number of concurrent probe
goroutines and provider requests, using a bounded worker or semaphore pattern
while preserving result indexing, cache handling, panic recovery, and waiting
for all probes to finish. Ensure the bound is applied to every uncached model
and run the affected concurrent code under the race detector.

In `@internal/specialist/plan_provider_mismatch_test.go`:
- Around line 32-49: Update the test’s OrchestrateTool setup around
autoAssignModels to provide a RunTask implementation that sets dispatched when
routing occurs, using the existing context and task inputs as appropriate. Keep
the assertion verifying that a configured default is downgraded without error
and that no dispatch occurs against an incompatible provider, so the check can
fail on regressions.

In `@internal/specialist/plan_runner.go`:
- Around line 107-118: Update the token-limit handling switch in the task
execution flow to call req.Spend.add(spent) before evaluating whether
MaxTaskTokens stopped the task. Preserve the task-cap message and cancellation
behavior, while ensuring the plan-level meter receives the final token event
before either cap decision is applied.

In `@internal/specialist/plan_tool.go`:
- Around line 154-176: The tasks array schema in the plan tool lacks an Items
definition. Update the "tasks" PropertySchema to define each item as an object
with required id and prompt fields, plus optional depends_on, read-only tool
subset, phase label, and model fields, following the existing recursive Items
pattern used by comparable arrays.

---

Outside diff comments:
In `@internal/specialist/plan_tool.go`:
- Around line 585-592: Update the background-plan result in the plan execution
flow to append the auto-assignment report from autoAssignSummary, matching the
foreground path’s assignNotes reporting. Keep the existing background status,
task count, and deferred-result messaging intact while ensuring any assignment
summary is included in the returned Output.
- Around line 506-539: Move the RunTask nil check and
runningPlanOn(tool.Recorder) admission check ahead of autoAssignModels in the
orchestrate flow, while keeping ParsePlan first so invalid plans are still
reported as validation errors. Update the nearby ordering comment to reflect
that these refusals now occur before discovery and routing, and preserve the
existing error responses.

---

Nitpick comments:
In `@internal/cli/provider_models.go`:
- Around line 74-103: Bound the concurrent probing in the options.verify block
by replacing the unbounded per-model goroutines with a fixed-size worker pool or
semaphore, using an appropriate concurrency ceiling while preserving each
model’s result. Ensure shared results and verdicts access remains race-free,
then run the affected verify path with go test -race or an equivalent
race-enabled run.

In `@internal/specialist/exec.go`:
- Around line 362-372: Update appendExtraWriteRootArgs to normalize both cwd and
each root with filepath.Clean before comparing them, so equivalent paths such as
trailing-slash or dot forms are skipped. Track normalized roots in a seen-set
and avoid appending duplicate --add-dir flag pairs while preserving empty-root
filtering and argument order.

In `@internal/specialist/manifest.go`:
- Around line 272-281: Update manifest model canonicalization to call the
registry’s ResolveWithFallback, matching the resolver used by resolveTaskModel.
Apply the fallback result to manifest.Metadata.Model so unresolved aliases such
as sonnet 4.5 and deprecated identifiers such as claude-haiku-3.5 resolve
consistently for execution and accounting.

In `@internal/specialist/plan_model_assign_test.go`:
- Around line 26-34: Replace the duplicated second assertion on tiers.strong
with an assertion validating tiers.balanced, preserving the expected middle-tier
model value and error-reporting style used in the test.

In `@internal/specialist/plan_model_assign.go`:
- Around line 86-110: Move the tier-ranking and canonicalisation documentation
currently above rankedEligibleModels so it directly precedes buildModelTiers,
preserving the rankedEligibleModels comment for that function. In the
assignModelsToTaskArgs area, merge the two adjacent duplicate summaries into one
accurate doc comment and remove the redundant block.

In `@internal/specialist/plan_model_fallback_test.go`:
- Around line 37-54: Make the concurrent test fixtures race-free: in
internal/specialist/plan_model_fallback_test.go around lines 37-54, protect the
ran append and every attempts counter in the remaining tests with a sync.Mutex
or atomic.Int64; in internal/specialist/child_scope_test.go around lines
194-214, protect the seen append in RunTask with a mutex. Run the affected tests
with the race detector.

In `@internal/specialist/plan_model_router_test.go`:
- Around line 125-140: Add a `"no runner"` table case to the test around
routeTaskModels, using a nil runner while retaining valid router model and
candidates, and ensure the invocation passes each case’s runner value instead of
the fixed run variable. Keep the silent-skip and router-not-called assertions so
the test verifies nil-runner handling.

In `@internal/specialist/plan_model_router.go`:
- Around line 220-240: Update the router assignment validation in the loop over
decoded.Assignments to resolve chosen model IDs using the existing modelIDForms
normalization rather than exact offered-map lookup. Preserve rejection of
unknown models, but accept equivalent spellings such as an omitted Ollama
:latest tag and store the canonical offered ID in out so downstream dispatch
uses the listed model identifier.

In `@internal/specialist/plan_runner.go`:
- Around line 98-123: Rename the helper function task_ID to an idiomatic name
such as taskLabel, updating all references in the closure and surrounding
plan-runner code. In the counted closure, rename the local token-count result
from task to a distinct name such as taskTotal, and use that name in the limit
comparison and overspent message while leaving the enclosing Task variable
unshadowed.

In `@internal/specialist/router_manifest_test.go`:
- Around line 21-46: Remove the unused Executor, planCtx, NewPlanRunner,
RunChild reassignment, and run scaffolding from the test, leaving only the
routeTaskModels setup needed to capture seen. Do not alter the streamjson import
if it remains required by the other test.

In `@internal/specialist/served_forms_test.go`:
- Around line 77-80: Update the autoAssignModels call in the test to retain its
returned error and assert that the error is nil before checking notes, so
execution reports an assignment failure directly instead of misclassifying it as
an accepted Ollama list.

In `@internal/specialist/usage_pricing_test.go`:
- Around line 139-145: Strengthen TestTheRoutingCallReportsWhatItSpent by
exercising the actual routing path instead of passing hand-written notes
directly to autoAssignSummary. Use the same setup and invocation pattern as
TestTheUsageRollupWritesThePricingFields, then assert that the routing call’s
generated note includes its token spend before validating the summary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5fed1fd2-99b9-48f9-8a80-319dd8a1576e

📥 Commits

Reviewing files that changed from the base of the PR and between 3b9f7b2 and c80a518.

📒 Files selected for processing (65)
  • internal/cli/app.go
  • internal/cli/child_exit_code_agreement_test.go
  • internal/cli/exec.go
  • internal/cli/exec_usage_cache_test.go
  • internal/cli/exec_writer.go
  • internal/cli/exec_zeromaxing_test.go
  • internal/cli/plan_grant_test.go
  • internal/cli/plan_live_provider_test.go
  • internal/cli/plan_model_prefs_carry_test.go
  • internal/cli/plan_model_probe.go
  • internal/cli/provider_models.go
  • internal/config/plan_size_test.go
  • internal/config/resolver.go
  • internal/config/types.go
  • internal/specialist/accounting.go
  • internal/specialist/budget_enforcement_test.go
  • internal/specialist/child_scope_test.go
  • internal/specialist/dependency_briefing_test.go
  • internal/specialist/exec.go
  • internal/specialist/manifest.go
  • internal/specialist/manifest_test.go
  • internal/specialist/plan.go
  • internal/specialist/plan_exec.go
  • internal/specialist/plan_exec_test.go
  • internal/specialist/plan_grant_test.go
  • internal/specialist/plan_model.go
  • internal/specialist/plan_model_assign.go
  • internal/specialist/plan_model_assign_test.go
  • internal/specialist/plan_model_fallback_test.go
  • internal/specialist/plan_model_probe.go
  • internal/specialist/plan_model_probe_test.go
  • internal/specialist/plan_model_router.go
  • internal/specialist/plan_model_router_test.go
  • internal/specialist/plan_model_test.go
  • internal/specialist/plan_provider_mismatch_test.go
  • internal/specialist/plan_runner.go
  • internal/specialist/plan_store_test.go
  • internal/specialist/plan_test.go
  • internal/specialist/plan_tool.go
  • internal/specialist/plan_worktree_test.go
  • internal/specialist/plan_write_test.go
  • internal/specialist/resume_manifest_test.go
  • internal/specialist/router_manifest_test.go
  • internal/specialist/served_forms_test.go
  • internal/specialist/streamer.go
  • internal/specialist/task_role.go
  • internal/specialist/task_role_test.go
  • internal/specialist/usage_pricing_test.go
  • internal/streamjson/streamjson.go
  • internal/tui/model.go
  • internal/tui/orchestrate_panel.go
  • internal/tui/orchestrate_panel_test.go
  • internal/tui/orchestrate_saved.go
  • internal/tui/orchestrate_window_test.go
  • internal/tui/plan_card_regression_test.go
  • internal/tui/plan_fallback_display_test.go
  • internal/tui/plan_messages.go
  • internal/tui/plan_progress.go
  • internal/tui/plan_progress_test.go
  • internal/tui/sidebar.go
  • internal/tui/sidebar_plan_detail.go
  • internal/tui/sidebar_plan_detail_test.go
  • internal/tui/sidebar_plan_test.go
  • internal/tui/sidebar_test.go
  • internal/tui/specialist_card.go
🚧 Files skipped from review as they are similar to previous changes (20)
  • internal/tui/plan_messages.go
  • internal/tui/orchestrate_saved.go
  • internal/tui/orchestrate_window_test.go
  • internal/specialist/resume_manifest_test.go
  • internal/tui/plan_card_regression_test.go
  • internal/specialist/plan_store_test.go
  • internal/tui/model.go
  • internal/specialist/plan_test.go
  • internal/specialist/plan_grant_test.go
  • internal/cli/exec_zeromaxing_test.go
  • internal/specialist/plan_worktree_test.go
  • internal/tui/sidebar_plan_test.go
  • internal/specialist/plan_exec_test.go
  • internal/tui/plan_progress.go
  • internal/cli/app.go
  • internal/tui/orchestrate_panel_test.go
  • internal/tui/sidebar_plan_detail_test.go
  • internal/tui/orchestrate_panel.go
  • internal/cli/exec.go
  • internal/tui/sidebar.go

Comment thread internal/cli/provider_models.go
Comment thread internal/config/resolver.go Outdated
Comment thread internal/specialist/plan_exec.go Outdated
Comment thread internal/specialist/plan_model_probe.go
Comment thread internal/specialist/plan_provider_mismatch_test.go
Comment thread internal/specialist/plan_runner.go
Comment thread internal/specialist/plan_tool.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/specialist/plan_store_test.go`:
- Around line 590-616: Update SavePlan to create the temporary file atomically
with os.CreateTemp, write through the returned file descriptor, close it, and
then rename it; remove the separate refuseSymlink/temp-path write sequence.
Extend the existing symlink protection coverage with a deterministic regression
test that replaces the temporary path concurrently and verifies the symlink
target is not modified.

In `@internal/specialist/plan_strict_lists_test.go`:
- Around line 154-161: Update TestASavedPlanStillRefusesInlineContent to seed a
valid saved plan named “sweep” in the temporary UserDir before iterating over
inline fields. Use the existing plan-saving setup or helper, then keep the loop
asserting each field causes an error so failures specifically verify the
saved-plan override policy.

In `@internal/specialist/plan.go`:
- Around line 776-782: The planStringsStrict function currently treats present
non-array values as absent, allowing malformed depends_on and tools fields to
bypass validation. Distinguish a missing key from a present value, return an
error when args[key] exists but is not an array (including null), and preserve
the existing nil result only for absent keys. Add regression cases in
plan_strict_lists_test.go covering invalid depends_on and tools values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7a3e9fb9-1221-48d6-9ec8-01ebecd92512

📥 Commits

Reviewing files that changed from the base of the PR and between 0b4ce8b and 1396d9e.

📒 Files selected for processing (7)
  • internal/cli/app.go
  • internal/cli/shutdown_order_test.go
  • internal/specialist/plan.go
  • internal/specialist/plan_store.go
  • internal/specialist/plan_store_test.go
  • internal/specialist/plan_strict_lists_test.go
  • internal/specialist/plan_tool.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/cli/app.go
  • internal/specialist/plan_store.go
  • internal/specialist/plan_tool.go

Comment thread internal/specialist/plan_store_test.go Outdated
Comment thread internal/specialist/plan_strict_lists_test.go
Comment thread internal/specialist/plan.go
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 3, 2026

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this from the TUI as well as reviewing the diff. The core path works: zeromaxing activates, a two-task read-only plan completed correctly in parallel, and the specialist/TUI/CLI tests pass.

I am requesting changes for the remaining user-facing issues:

  1. The bundled research plan still uses the literal "THE SUBJECT". The documented "/plans run research" path cannot provide a subject, so it can spend five child-agent runs researching a placeholder.
  2. The budget schema describes limits but does not declare its minimums and maximums. In the manual run the model emitted max_tokens_per_task: 5000, Zero rejected it, and the model had to retry. The schema should prevent that invalid call.
  3. The zeromaxing chip hit-test still searches every footer row for the label. Typing the same word in the composer can make the composer row become the hover/click target instead of the status chip.
  4. The completed plan UI showed both "AGENTS 2 done" and "no agents spawned", and raw child session_id values were printed inside the plan result.

The size also makes this difficult to review safely: 36,607 additions across 198 files and 96 commits. About 22k lines are tests, which is good, but there are still roughly 14.2k production additions spanning posture, scheduling, persistence/resume, background execution, write isolation, model routing, TUI, sandbox, credential-store, and worktree changes. These are independently reviewable seams and would be safer as focused PRs.

The feature has real value, but I do not think this draft is ready to merge until the concrete issues above are fixed and the scope is reduced or split into reviewable pieces.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read through this properly rather than skimming the diff — 36k lines is a lot, and most of it is solid. The plan/orchestrate model hangs together, the isolation design is the right shape, and the commentary explaining why things are the way they are is genuinely better than most of what lands here.

Two things need fixing before it goes in, and one of them is the reason I'm requesting changes rather than nitting.

1. Isolated plan tasks can write to the real repo — internal/specialist/exec.go:366

The write-capable path promises isolation and doesn't deliver it. Chain:

  • appendExtraWriteRootArgs skips a root only when root == input.Cwd
  • Scope.Roots() returns the parent workspace root first, then extras
  • for an isolated task input.Cwd is the worktree, so the parent repo root isn't skipped — it goes out as --add-dir <parentRepo>
  • the child feeds addDirs into sandbox.NewScope(workspaceRoot, extras), and Add fails with write root %q, so these are write roots, not read

Meanwhile planWorkspaceNote tells the user "nothing was written to the parent tree", and the tool schema says naming a write tool "runs it in an isolated worktree and asks for approval first". So a task the user approved on that promise can write_file/apply_patch/bash anywhere in their actual repo.

I don't think this is carelessness — a worktree's .git file points back at the parent, so some git operations genuinely need write access under <parentRepo>/.git. But that argues for granting that path specifically, not the whole tree. Whatever you land on, the note and the schema text need to describe what's actually enforced.

internal/specialist/child_scope_test.go:36 only covers Cwd == the parent workspace, which is why this passes today — the isolated case, the one that matters, isn't exercised.

2. /effort auto mutates a run in flight — internal/tui/session_controls.go:93

You already wrote the argument for this guard, on the branch right above it:

Same idle-session rule /profile enforces… The budget propagates to sub-agents spawned later in the same run, so changing it mid-run leaves one turn running under two different budgets.

The entering branch checks m.pending. The leaving branch calls revertExecProfile() with no check at all, and /effort auto is reachable mid-run. Sub-agents spawned later in that turn inherit the reverted budget while earlier ones kept the posture's, which is exactly the straddling state /turns and /profile refuse. It also flips the orchestrate gate, so the tool disappears from under a model that's mid-plan.

Same guard, same reason, other direction.

Worth fixing in the same pass

  • internal/cli/plan_model_probe.go:43providers models <name> --verify looks like it probes the active provider rather than the named one, because the prober routes the selected profile through livePlanProvider. If so, --verify reports the wrong thing whenever the named profile isn't the active one.
  • plan_exec.go:720 / plan_runner.go:98budget.max_tokens_per_task is applied per attempt. With retries and fallbacks a task can spend a multiple of its declared cap, which makes the cap advisory rather than a bound.
  • internal/agent/loop.go:595Options.MaxTokens counts only the parent's own provider usage, so orchestrate and sub-agent spend never lands against the posture's spend bound. For a posture whose whole point is running long, that's the number people will assume is holding.
  • internal/cli/provider_models.go:92--verify fans out one goroutine per discovered model, uncapped. The specialist path next door caps at 8 for exactly this reason.
  • plan.go:483 (from my earlier round, still open) — seconds > 0 means a negative max_wall_seconds/max_stall_seconds reads as "unset" and the plan runs unbounded. A negative value should be an error, not an off switch.
  • plan_exec.go:510 — paused time is credited to the wall clock only once the pause ends, while the watchdog keeps counting through it, so a long pause can trip the budget.
  • plan_exec.go:413cutShort keys on a non-nil error the production runner never returns, so a task in flight when a plan is stopped records as TaskFailed rather than TaskCancelled.

Smaller

orchestrate_saved.go:382 — the cross-plan resume guard compares plan names, but names are optional, so two unnamed plans both compare "" and it passes. orchestrate_saved.go:237planTaskSummaryLine cuts only at \r\n and leaves ESC through, unlike the sibling you already fixed; same for the failed-task error text at sidebar.go:668. These strings are model-authored, so escape sequences reach the terminal. zeromaxing_glow.go:208 — chip hit-testing takes the first footer line containing "zeromaxing", which composer text can shadow. plans/research.json:8 still ships literal "THE SUBJECT" with no placeholder declared, so /plans run research dispatches five sub-agents on an unanswerable prompt.

Three tests don't test what they're named for: plan_watchdog_test.go:386 asserts parent.Err() != nil on a context.Background(), which can never be true; plan_grant_test.go:377 puts its only assertion inside if err == nil && len(models) > 0, so the error property it names is never checked; scope_temporary_test.go:19 only tries macOS paths, so all five refcount tests — the only coverage AddTemporaryWrite has — skip on Windows and Linux.

Where that leaves it

Fix 1 and 2 and I'll re-review the whole thing rather than sending you back round again — I know CodeRabbit has already bounced this five times and I'd rather this be the last round from me. The middle group I'd want addressed or argued with; the smaller ones you can take or leave, but the three dead tests I'd fix since they're currently reporting green on properties nobody is checking.

Nothing here is a design objection. The shape is right.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Thanks for the careful reviews. Important context first: the fixes for most of these were committed but unpushed when you reviewed — the fork branch was behind, so the code you saw predates them. Everything is now pushed; here is where each point stands against current HEAD.

@anandh8x — all four addressed

  1. Research plan literal "THE SUBJECT" → now a ${subject} parameter (703061d5).
  2. Budget schema didn't declare min/max → the schema now declares the bounds it enforces (3ab5b032).
  3. Chip hit-test scanned every footer row → restricted to the status line (690f8de8).
  4. "AGENTS done" vs "no agents" + raw session_id in the result → header no longer contradicts itself (f4630181); a task's answer no longer carries the child session id (5400fa46).

@Vasanthdev2004 — addressed

  • Cross-plan resume (narrowing plan A by plan B's progress) → resumeSavedPlan now refuses on progress.Name != plan.Name().
  • planTaskSummary control-byte / ESC-OSC injection → now sanitized like the other model-authored surfaces.
  • beginRun clearing the panel for a live background plan → guarded by BackgroundPlanLive().
  • /effort zeromaxing mid-run → now carries the same m.pending guard as /profile.
  • releaseTemporaryRead/Write refcount TOCTOU → delete + slice removal now under one lock hold.
  • Chip hit-test byte-offset vs cell + composer row → lipgloss.Width and a status-line-only scan.
  • Negative max_wall_seconds/max_stall_seconds silently accepted → now rejected.
  • Approval card "read-only" Reason + Description → reworded (write tools are grantable by name).
  • research.json (subject + dependent briefing) → subject parameterized; dependents receive the dependency briefing.
  • sidebar_plan_detail / mouse.go missing modal guards → guards added.
  • Done/Remaining dead API → dropped (54299ed). Not wired in as suggested: Done() is id-based, so under the identity-aware resume a plan whose ids all succeeded but whose task was edited would report "done" and skip the re-run. The "failed task is remaining" test now asserts through the real gate (RemainingPlan).
  • "byte-identical posture-off" vs runCanMutate → claim narrowed in the description; the read-only confirmation-policy drop is now documented as the one deliberate, fail-closed exception.

go test ./... passes (84 packages); fmt/vet/build clean. A couple of CodeRabbit test-only nitpicks (plan_concurrent_test on ≤5-CPU hosts, a watchdog test) remain and can follow. Re-requesting review — the current diff is the one to look at.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@gnanam1990 sorry for the wait. I went straight at the two blockers on current HEAD rather than re-reading all 49k lines, because you have been sitting on this since this morning.

Both are fixed in one place and missed in a twin. Three one-line misses and then I think this is done.

Isolation. app.go:767 is right, and the comment recording the measured "wrote ten times into the user's real tree" is the best kind of fix note. But internal/cli/exec.go:291 still returns execScope.Roots(), and Roots() is workspaceRoot plus extras where ExtraRoots() is extras only. Plan isolation is reachable on that path too (Isolate: newPlanIsolator(workspaceRoot), exec.go:332), so zero exec still hands an isolated task --add-dir <parent repo>. TestAnIsolatedPlanTaskIsNotHandedTheParentTree cannot catch it because it builds its roots from .ExtraRoots, so it pins the supplier that was already fixed.

/effort auto. The entering branch got the guard, the leaving one did not. session_controls.go:93 still calls revertExecProfile() with no m.pending check, and that restores the turn budget and flips the orchestrate gate mid-run. The leaving direction was the one I meant.

planTaskSummaryLine. planTaskSummary (plan_progress.go:711) is now sanitized through sanitizeCardText, with the right reasoning in the comment. Its near-namesake at orchestrate_saved.go:243 still cuts at \r\n only, and renders the same model-authored task.Prompt.

Negative max_wall_seconds is fixed properly, including the set-versus-unset distinction that made the original seconds > 0 wrong.

Fix those three and I will approve. No more rounds from me after that.

…ents

A bare Task delegation inherited the parent's model unconditionally, so
under zeromaxing every sub-agent ran on the session model no matter what
it was for. A Task that names no model is now routed to the configured
role pin — the plan path's own classifier and pins, the grant outranking
the prose — gated on the posture AND the autoAssign config, so a
posture-off spawn stays byte-identical.

Four defects found and closed while verifying the seam end to end:

- A resumed task drifted back to the parent's model. runResume applied
  neither the call's model nor the assigned one; it now recovers the
  model the session actually ran on from the session store, with an
  explicit model on the resume call winning exactly as on a fresh
  launch.
- A plan task could be second-guessed at dispatch. Plan tasks flow
  through the same executor, and a model the plan tool deliberately left
  empty (a reported decision, or auto_assign:false) would have been
  re-assigned blind — including to a pin the plan level had passed over
  as not served. autoTaskModel now refuses plan-authored manifests,
  keyed on the provenance the manifest already carries.
- Naming an uncurated model killed the spawn. applyTaskModel forwarded
  the posture's raised reasoning effort unconditionally; the child only
  clamps efforts for models the registry knows, so providers that reject
  the parameter refused the whole request — a real orchestrator lost
  three children to it and retreated to inherit-everything. The effort
  now forwards only where the registry can vouch, the same gate the plan
  path already applies.
- A stale pin after a provider switch killed the spawn. Pins named
  Ollama models while the session ran on xai, and three children died
  with not-found. A pin now fires only when the provider's own listing
  carries both the pin and the session's model (the plan path's
  provider-mismatch guard); anything uncertain inherits. One listing is
  cached for two minutes, so a five-agent fan-out costs one probe.

Every load-bearing line is mutation-checked: the pin lookup, the
manifest-model guard, the posture gate, the session-model recovery, the
explicit-resume-model apply, the plan-provenance skip, the effort vouch
gate and the served-set guards each have a test that goes red when the
line is gutted.
…te-aware animation, settled cancels

While the posture is on, the workspace wears it; off, every surface is
byte-identical to before (each gate mutation-checked).

MODELS panel: the sidebar grows a section showing the live mix of models
the fleet runs on — a proportional mix bar plus a row per model with
live counts, colours stable per model name. Absent until an agent runs
on a known model, so a plain session's layout is untouched.

The skin: an electric gradient (theme blue blended into the accent —
deliberately two-tone) paints the always-on surfaces: the composer box
on all four edges, the chat|sidebar divider as a full-height rail, the
sidebar section headers, and the Working ripple. The gradient sweep is
mirrored, so no seam ever reads as "half painted". No new timers:
every walk reads the spinner clock that already runs during a turn, and
reduced motion pins all of it.

State-aware animation: thinking drifts, writing races (4x), and
orchestrating gets a scanner — a bright band sweeping the composer bar
while plan tasks are actually running. The Working ripple's wavelength
follows the same states.

Plan bars join the skin: done work fills with the gradient, the running
head burns accent, failure stays findable in calm dimmed red, and the
update_plan checklist gets a progress bar of its own (the orchestrate
bar's renderer, so the two cannot drift). Click offsets for plan steps
account for the inserted bar row, test-pinned against the rendered
sidebar.

Settled cancels: cancelling the run kills the children but the trackers
were never told — agents kept their spinner, ticking clock and "live"
mark in MODELS beside "Run cancelled." in the transcript. cancelRun now
marks running specialists and orchestrate tasks cancelled (not failed —
stopping a run is not a defect), freezing their clocks and dropping the
live counts.
The stock #50fa7b green and #ff5555 red are the highest-chroma signals
in the whole registry, and in a zeromaxing session — success ticks and
error rows arriving constantly — they read as alarms, not status. The
success and failure roles now wear Dracula's signature cyan and pink,
which sit naturally beside the purple accent and stay clear of amber's
permission meaning. The palette token names stay green/red (every
consumer binds to them as success/failure); diff colours keep their
conventional hues — a diff is a diff on every theme. The registry-wide
contrast floors still hold.
The status card stacked three renderers — a labelled list, the resolved
posture line, and the delta sentence — so the profile name appeared
twice and the effort and turn budget three times each. It is now a
single sentence of key-value clauses: each fact once, the transitions
as clauses on the row they qualify ("was 80", "raised by the posture",
"NOT raised — high is unsupported on this model"), no section header
and no hint advertising the command the user just typed.

Every honesty guarantee the old card's tests enforced survives on the
one line: the refused effort level is named, the self-correct claim
tracks live state ("your /selfcorrect off overrides the posture"),
exactly one effort-transition claim exists, and the escalation row keeps
the headless-only asymmetry. /effort still carries the shared delta
sentence verbatim, so the switch notice and that surface cannot
disagree. Also learned and recorded: compactCommandOutputText collapses
every whitespace run, so padded column alignment can never survive this
renderer.
…roots

The exec wiring handed children execScope.Roots() — the parent workspace
root included — as extra write roots. For a worktree-isolated plan that
defeats the isolation outright: the worktree exists so a write-capable
plan cannot touch the parent tree, and this line was handing the parent
tree back as a writable --add-dir. The TUI has always passed only
scope.ExtraRoots; the two surfaces now agree, through a named helper the
regression test drives with a real sandbox scope.
Entering the posture through /effort zeromaxing is guarded by the
idle-session rule; leaving it through /effort auto reached
revertExecProfile — moving the turn budget, the self-correct setting and
the shared orchestrate gate — with no guard at all, leaving one turn
running under two different budgets. The same rule now guards both
doors. Plain /effort auto outside the posture keeps working mid-run: it
only clears the effort selection, which mutates no shared budget.
A saved plan's prompts are model-authored text read back from disk — the
same trust level as a live task's — and planTaskSummaryLine only cut at
the first newline, so control characters (ESC included, the byte that
starts an ANSI sequence) reached the display. It now routes through
sanitizeCardText, the chokepoint the live path already uses.
absolutePathPattern only matched POSIX-style paths, so on Windows the
reach check silently never fired: prompts naming C:\ paths contributed
no candidates, and four tests showed it. The pattern now takes both
separators and an optional drive letter, plus '~' for 8.3 short names
(RUNNER~1), which real CI paths contain. pathInsideRoot keyed on the
single platform separator, answering false for every /-separated pair on
Windows; both separators are boundaries on every platform now.

Also makes the completed-session retention test cross-platform (cmd /c
on Windows) instead of assuming /bin/sh — the behaviour under test is
OS-agnostic, so it runs everywhere rather than skipping.
make deadcode (-test=false) flags production functions whose only
callers are tests: ExecutePlan, withDependencyBriefing and
autoAssignModels in specialist, markDone and joinColumns in tui. Each
was a deliberate convenience seam with documented rationale; they keep
that rationale and move to export_test.go, the repo's home for test-only
helpers, so the production binary carries none of them. SortedPlanSizeNames
had no callers anywhere and is deleted.

Also clears the three staticcheck findings the advisory lint reported in
PR-owned code: a tagged switch in plan_model_assign, time.Since in a
test, and a self-comparison rewritten as two named renders.
NTFS ignores trailing dots, so Lstat("…/streamer.go.") succeeds for the
file named "…/streamer.go" — the exact-hit branch returned the
sentence's full stop as part of the path, and the reach check compared
the wrong spelling. When the trimmed spelling names the SAME file, the
canonical name wins; a POSIX file genuinely named with a trailing dot
keeps its exact spelling, which the new cases pin (dotted-only, distinct
dotted sibling, and the plain trim), with the unconditional-rewrite
mutation killed by them.
A plan fans out, every task reports, and the report carries what they
said — under zeromaxing that is five or more sub-agents' claims, each
written by a model that cannot see the others' work and will never be
asked about it again. The bundled research plan ends in a refute step
for exactly this reason and planTaskSystemPrompt's evidence rules exist
to make one possible, but both are habits: a plan whose author forgot a
verifier finished green with nothing checked, and a wrong "looks fine"
is indistinguishable from a right one once it reaches the report.

Under the posture, a multi-task plan that names no verification now gets
one appended — a task depending on every other, told to REFUTE and to
default to refuted when uncertain, because a verifier that sets out to
agree always does. It classifies as verify, so the user's verify pin
routes it to their strongest model, and it works on the ARGS before
ParsePlan, so it is validated by the same constructor as a hand-written
task, round-trips into a saved plan, and re-admits on resume.

THE APPEND MUST NEVER BE THE REASON A PLAN IS REFUSED, and two ceilings
can do that. The size tier caps task COUNT. The budget floor is the one
the existing suite caught: refuseImplausibleBudget requires max_tokens
to fund 50,000 per task, so one more task raises the bar by 50,000 and a
plan whose budget exactly funded its own tasks was rejected outright —
for a task its author never wrote. Both have a guard and a test.

It also names NO tools: planToolGrant then resolves the read-only
intersection of the parent's grant, so it can neither modify anything
nor flip the plan into worktree isolation (isolation is plan-level, the
measured failure plan_workspace_reach.go exists to catch). It defers
entirely to a plan that already verifies, moves its id aside on a
collision, leaves a malformed task list to ParsePlan, and reports itself
in the run's output so a task nobody wrote never appears unexplained.

Each guard is mutation-checked: gutting the posture gate, either ceiling,
the already-verifies check or the id probe fails its own test.
…le field

Ranking spanned everything the provider serves, so buildModelTiers read
the cheap tier off the SMALLEST model on the account and the router was
handed every candidate to choose between. On a provider listing twenty
models that is a toy running a scan while nineteen better ones sit
unused — the reason 20b-class models kept turning up on tasks that had
a 120b available.

rankedEligibleModels now keeps the most capable N (default 10) after
exclusions and the size floor, so the tiers span the good models rather
than the full catalogue and the router chooses from a shortlist worth
choosing from. Configurable as planModels.topModels for anyone whose
provider warrants a wider or narrower field.

THE TAIL, NOT THE HEAD: the list is sorted least-to-most capable, so the
shortlist is its end — taking the front would keep exactly the toys this
drops. Fail-open throughout: a provider offering fewer than the cap keeps
all of them, and a zero or negative cap means the default rather than
none, because a zero value must never leave a plan with nothing to
assign.

A PIN IS UNAFFECTED. Pins are validated against what the provider serves,
never against this list, so naming a model outside the top N still routes
to it — an explicit instruction outranks a heuristic. Tested, along with
the fail-open cases; gutting the shortlist, taking the head instead of
the tail, or reading a zero cap as none each fails its own test.
…empt

A measured ten-task run lost one task to a bare "Internal Server Error"
after 6 tool calls and 42,524 tokens, and everything depending on it went
with it. Nothing about the task produced that.

Two layers each declined to retry, and only one of them was right to.
The transport does not replay a 500/502/504 on purpose: unlike 429/503/
529 they do not guarantee the request had no effect, so replaying the
same POST could pay for the same completion twice
(providerio.ShouldRetryStatus). That is correct for a replay. The plan's
own loop then returned on "anything other than a stall is an ANSWER,
including a failure" — true of a task that read the code and concluded
wrongly, which will conclude the same thing, and not true of an HTTP 500,
which is a coin flip. A fresh child is not that replay; it is a new
request the provider is very likely to answer.

So the loop grows a third carve-out beside the two that already exist for
"not the task's fault" (ModelRejected, Declined): one more attempt when
the child died on the provider, recognised from its exit code the way
Declined is — a flag, never a message match.

READ-ONLY TASKS ONLY, and that bound is the whole safety argument. A
write task may have applied part of its change before the provider died
and no exit code says how far it got, so re-running it could apply that
change twice. A read-only task re-reads, which costs tokens and nothing
else — the same asymmetry the no-tool-call check rests on.

Bounded at exactly one by its own flag (a provider failing twice is
having an outage), gated by the same cancellation and wall-clock stops as
every other retry here, and NOT gated by max_retries, which bounds stall
thrashing rather than this. Both attempts' spend is reported.

Five mutations, five tests: removing the branch, dropping the write-task
gate, dropping the one-retry bound, never setting the flag from the exit
code, and drifting the duplicated constant each fail. The flag test is
separate on purpose — gutting the classification left the four executor
tests green, which is the layer-join defect this package keeps
relearning, so one test drives the real runner. The constant is now
pinned on BOTH sides; pinning only cli's left specialist's free to move
silently.
Two defects a high-effort review found in the top-ranked shortlist.

THE SHORTLIST INVERTED COST ON PRICED PROVIDERS. The ranked list is
sorted least-to-most capable, but "capable" is measured by SIZE on a
free provider and by COST on a priced one — so taking the tail meant
"keep the biggest" in the first case and "keep the DEAREST" in the
second. Measured on a 12-model priced fixture: the cheap tier moved from
cost 1 to cost 3, and on a real 30-model account every scan-role task in
every auto-assigned plan silently moved an order of magnitude up in
price, reported only as "scan -> <model>".

A priced catalogue is now never narrowed. The asymmetry is the point: on
a free provider the small models cost nothing and buy nothing, so
dropping them is pure gain; on a priced one the cheap end is exactly what
the cheap tier exists to spend. catalogueIsPriced is extracted as one
authority so the ranking and the shortlist cannot disagree about what a
free provider is.

A TRILLION-PARAMETER MODEL PARSED AS SIZE ZERO. modelSizeToken accepted
only the b/m suffixes, so "kimi-k2:1t" read as unknown and sorted BELOW
gpt-oss:20b on a free provider — the largest model on the account became
the cheap tier every scan task was routed to, and the first thing the
shortlist discarded. The suffix class now covers t.

Both mutation-checked: re-narrowing a priced catalogue, and dropping the
t suffix, each fail their own test.
…or's plan

Two more defects in the auto-appended verification task, both of the same
shape: a task the author never wrote changing the outcome of the plan
they did write.

A THIRD BUDGET CEILING. The headroom check covered
refuseImplausibleBudget (max_tokens vs 50,000 per task) but not
refuseUnreachablePerTaskCap (max_tokens vs max_tokens_per_task x
taskCount), so the extra task raised that bar by one whole per-task cap.
A 4-task plan with max_tokens 400,000 and max_tokens_per_task 100,000
parsed fine and was then refused outright — naming a task count and a
budget its author never chose. Any plan whose per-task cap exceeds the
50,000 floor falls in that window, so the floor check alone never
covered it.

THE VERIFIER'S FAILURE FAILED THE WHOLE PLAN. It runs last, on the
strongest tier, with the largest dependency briefing — the task most
likely to stall or exhaust its provider retry — and terminalStatus
counted it like any other, so three succeeded author tasks plus one
stalled verifier returned StatusError and the orchestrating model re-ran
a plan whose work was already done. When the appended task is the ONLY
thing that did not succeed, the call now stays OK and says plainly that
the claims were not independently verified and that re-running would
repeat completed work. The report stays honest either way: Failed still
counts it and the summary still names it; only the verdict on the
author's plan is left to the author's tasks. appendVerifyStage returns
the id actually used, so a suffixed id cannot match the wrong task.

Both mutation-checked.
CANCEL CLOBBERED A LIVE BACKGROUND PLAN. cancelRun settled the trackers
unconditionally, with none of the BackgroundPlanLive guard beginRun makes
a few lines above — so stopping a foreground turn marked a background
plan's tasks and sub-agents cancelled while they kept spending tokens and
writing files. The mirror image of the defect the background-status tests
exist to prevent. Foreground children still die with the run context and
are still settled.

A RESUME BLIND-PINNED A STALE SESSION MODEL. runResume applied
session.ModelID with no served-check, so a sub-agent recorded on one
provider and resumed after a switch was spawned with a model that
provider has never heard of and died at spawn — the failure autoTaskModel
already guards against. The recorded model is now REJECTED ON EVIDENCE
rather than requiring proof, the opposite direction to autoTaskModel and
deliberately so: this restores a model the session demonstrably ran on,
so no discoverer, a failed listing or an empty one all leave the recovery
in place.

THE PROVIDER RETRY FIRED ON PERMANENT FAILURES. Exit code 3 means "the
provider failed", but internal/cli returns it for every agent-run error
— an expired key, an unknown model, a quota wall — so an expired key cost
a ten-task plan twenty spawns to produce the same ten auth errors. The
budget is now PLAN-WIDE (two, mutex-guarded because tasks dispatch
concurrently) instead of per-task: a genuine blip is still retried, a
systemic failure costs two extra children for the whole plan.

THE SESSION BUDGET CHARGED FOR RESUMES AND REFUSED CALLS. admit() ran
ahead of prompt validation and ahead of the resume branch, so iterating
on one sub-agent's answer thirty times consumed thirty of the session's
slots while spawning nothing. It now runs on the fresh path only, after
the checks that can refuse the call outright and still before every side
effect.

NON-LATIN LABELS WERE REJECTED AS NON-WORDS. hasNameLetter tested ASCII
ranges, so every CJK, Cyrillic, Greek and Arabic token was dropped and
the sidebar row lost its label — a silent degradation for anyone not
writing in English. unicode.IsLetter/IsDigit is the same question asked
correctly.

DRACULA'S SUCCESS COLOUR COLLIDED WITH ITS BLUE. The recolour set green
to #8be9fd, byte-identical to that palette's blue, collapsing two of the
four series colours modelMixPalette cycles through so two models in the
MODELS bar could render indistinguishably. Success moves to a teal in the
same cool family; the registry-wide contrast tests still pass.

Also drops TestZeromaxingSkinPreview, a visual preview with no assertion
that could not fail.

Each fix is mutation-checked: gutting any one fails its own test.
…ot read

The ranking sorts an unparseable id as size 0 — the least-capable end —
so taking a plain tail deleted exactly the models whose names do not
state a parameter count. On a real ollama-cloud account that removed
kimi-k2.6, glm-5.2 and deepseek-v4-flash from routing while gpt-oss:20b
survived, and the operator's own router guidance calls the first two the
strongest reasoners on that machine.

Dropping a model because its name is uninformative is a parsing
accident, not a capability judgement — and applyMinSizeFloor in this same
file already follows exactly that fail-open rule. The cut now applies to
SIZED models only: every unknown-size model is kept, and the top N of the
ones that can actually be compared join them, with order preserved so
cheap/balanced/strong still read off the ends.

On the fourteen-model catalogue this was measured against, the shortlist
now drops only phi4:14b — small AND sized — and the cheap tier becomes
deepseek-v4-flash rather than gpt-oss:20b.
…t a size

Two regressions the review found in the previous two commits, plus the
hole that made one of them reachable.

THE CAP WENT INERT ON ANY PRICED CATALOGUE. Exempting priced catalogues
from the cut fixed the cost inversion by removing the bound entirely, and
catalogueIsPriced is an ANY test — so one priced model among three
hundred disabled the cut for all three hundred, planModels.topModels
contradicted its own documentation, and the router prompt lost its only
bound (measured 2,194 -> 22,985 characters on a 300-model catalogue, paid
once per plan on the strongest model).

The cut now keeps BOTH ENDS of the ranking instead of exempting anything:
a third from the least-capable end, the rest from the most-capable. That
satisfies every reader of the list at once — the cheap tier reads
eligible[0], the strong tier reads the last, and the router gets a
bounded shortlist that still spans the real range. It also preserves the
unsized models without a special case, since an id whose size cannot be
read sorts at the cheap end.

VERSION TEXT WAS READ AS A PARAMETER COUNT. The suffix boundary was
[^a-z], which a DIGIT satisfies, so "deepseek-r1t2-chimera" matched
"1t" followed by "2" and reported a trillion-parameter model — on a
free provider that would have made it the strong tier every judgement
task was routed to. The b/m suffixes always had the same hole
("r1b2-x" read as 1B); adding t is what made it reachable on a real id,
so the boundary excludes digits for all three rather than for the one
that was reported.

Both mutation-checked, and the boundary now has a test naming the exact
reported id — reverting it alone previously failed nothing.
…plan runs

The fix for "cancel must not stop a background plan" over-corrected into
its own opposite. The guard wrapped BOTH cancelRunning calls in
BackgroundPlanLive, and the specialist tracker holds foreground and
background children TOGETHER — so one live background plan left every
FOREGROUND sub-agent spinning forever: still specialistRunning, still
showing its current tool, still counted live in MODELS, over a process
that died with the run context. Nothing else settles it, because a late
completion is dropped by the stale-run guard once cancelRun has zeroed
activeRunID. That is verbatim what TestCancelSettlesEveryRunningAgentAndTask
exists to prevent, and the background test missed it because it had no
foreground child.

Children now carry the distinction themselves. specialistInfo gains a
background marker, set from the two places that can know: a background
plan's task-start message carries the flag, and a specialistRebindMsg is
only ever emitted for a background Task spawn. cancelRunning skips marked
rows and settles the rest. The orchestrate panel keeps its whole-panel
guard, because one plan runs at a time and there is no mixed case there.

The new test is the mixed state neither existing test covered: a
foreground child and a background child under a live background plan,
asserting the first settles and the second does not. The background test
now marks its row the way production does, rather than passing because
nothing settled at all.

specialistInfo's new field is added to specialistCacheFingerprint, as the
render-cache guard test requires — without it a card that changed through
the field would keep serving its previous render.
Rebasing onto main flattened the two merge commits this branch carried,
and in doing so replayed an older value of postureOffDefinitionsFingerprint
over the current one — reverting it to the pre-Gitlawb#838 hash and dropping the
comment recording why it had moved.

The constant is a frozen golden over the tool DEFINITION bytes, and main's
Gitlawb#838 reworded four tool descriptions (glob, grep, list_directory,
read_file), so the correct value on this base is the one the branch
already had. TestPostureOffToolDefinitionsMatchGolden proves it: with the
reverted constant it fails with got 51ffc34f… want 3693b93a…, and the
'got' is what the fixture's tools actually hash to now.

Restored with its rationale intact, since that comment is the thing that
tells the next reader the move was main's schema change rather than a
posture leak from this branch.
Gitlawb#867 landed on main while this branch was being rebased, reshaping
read_file's schema to the canonical path/offset/limit contract and
rewording its description. The golden hashes the tool DEFINITION bytes,
so a description change on main moves it — the same way Gitlawb#838's rewording
did before.

The new value is what the fixture's tools actually hash to on this base,
and it is the value CI computed independently: the macOS and Ubuntu smoke
jobs both reported got ea7b4e64… against the old constant, which is the
figure recorded here now.

This is main's schema change, not a posture leak from this branch. The
byte-identical guarantee is unaffected: what the guard protects is that a
posture-off run's definitions match a binary built from the same main,
and both sides moved together.
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@gnanam1990 separate from the two findings above, and worth doing first: please merge main into this branch.

This branch cannot be hand-tested on Windows right now. Copying any transcript output panics the whole TUI:

syscall: string with NUL passed to StringToUTF16
  clipboard.writeAll(...)
  tui.model.handleTranscriptSelectionMouse.copyTranscriptSelectionCmd.func1()
  transcript_selection.go:1531

That is your own #876, merged to main as cae0269. This branch's last merge from main brought in up to cd0eb19, which is just before it, so the fix is missing here and the row prefixes that trigger it ("\x00command-card\x00") are still very much present.

It hit us twice today trying to exercise the posture by hand. I ended up cherry-picking cae0269 onto 2f399f7 locally just to get a binary that survives a copy, which is when it became obvious this is worth telling you rather than working around.

It matters more than usual for this PR specifically: most of what changed here is TUI, so "run it and look at it" is the main way to review it, and right now that path ends in a panic for anyone on Windows.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Behavioural note from hand-testing this branch. Not a code defect, and not necessarily attributable to anything in the diff. Filing it here because it is evidence about how the posture behaves in practice, which is hard to come by otherwise.

Under /effort zeromaxing (turns 480, effort high, verify lsp to tests), given a task to create four files, the run created them and then reported that it had not:

One honest caveat: the three files and doc.go already existed in the workspace with exactly the content you requested, so no edits were needed. I verified rather than created

The timestamps say otherwise. The workspace was seeded at 21:41:12 with only calc.go and go.mod. The four files it claims pre-existed have mtime 21:46:32, inside that run, and nothing else touched the directory.

Two reasons this is worth having on this PR rather than shrugged off as model noise. The posture's whole pitch is more rigour, so a confidently false completion report is the failure mode it should be least prone to, and a verify stage that checks the work but not the report will never catch it. And the phrasing dresses the confabulation as scrupulousness, "one honest caveat", which is worse than a plainly wrong answer because it reads as trustworthy.

Smaller observation from the same run: the task was written to fan out, three independent files then one depending on all three, and no plan was produced at all. It ran as a single turn of tool calls. Orchestrate never engaged on a task shaped to trigger it. That may well be intended, but it does mean the plan path is harder to exercise by hand than I expected, which is worth knowing given how much of this PR only shows up when a plan runs.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped: 276 files exceed the limit of 150.

detectShellRuntime probes PowerShell by starting it in this process, which is
unrestricted, and the command then runs inside the sandbox, which is not. On
Windows the sandbox wraps commands in a WRITE_RESTRICTED token, and PowerShell
is a .NET program whose crypto initialization fails there:

    System.DllNotFoundException: Unable to load DLL 'BCrypt.dll'
    or one of its dependencies (0x8007045A)

So PowerShell answers "usable" when asked in the parent, is selected, and is
then unable to run anything at all: exec_command and bash could not execute so
much as `echo` under the sandbox. The probe was asking a different environment
than the one that had to answer.

buildBashCommand is the single execution site for both tools, so it now resolves
the shell through the engine that will wrap the command. When PowerShell cannot
start there, detection falls through to cmd.exe, which does survive the token
and was already the documented fallback -- no new fallback path, just an honest
probe reaching the existing one. The sandboxed answer is cached separately from
the unsandboxed one because the two environments genuinely disagree; sharing one
cache would let whichever question was asked first answer both.

This is the same restricted-token incompatibility class already recorded for
Schannel TLS and MSYS2/Cygwin in internal/sandbox/windows_command_runner_windows.go.

Verified by running the feature on all three platforms against the real binary,
same commit, A/B: Windows 6/7 -> 7/7, macOS 7/7 and Linux 7/7 unchanged. The
added test pins the contract the fix relies on -- that a failed probe reaches
the cmd.exe fallback -- but not the wiring itself, which would need lookPath
injected into buildBashCommand; the cross-platform run is what covers that.

Known gap, not addressed here: the bash/exec_command tool descriptions still
advertise PowerShell syntax, because they are built in NewScopedBashTool, which
receives no engine. Sandboxed Windows now runs cmd.exe while the model is told
otherwise. shellGuidanceForRuntime already contains the correct cmd.exe wording;
reaching it means threading the engine through tool construction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants