* fix(specialist): cap max specialist nesting depth
A Task specialist can recursively spawn further specialists via --depth.
Without a ceiling, a runaway or malicious chain of Task calls can recurse
indefinitely, exhausting process and memory resources. Reject any run whose
depth exceeds a hard cap of 8.
Related to #470, not closing it: today's normal specialist/swarm path
cannot reach this recursively, since child sessions are launched with
--tag specialist and shouldRegisterExecSpecialistTools suppresses the Task
tool for that tag (see PR description for details). This is a
defense-in-depth guard, not a fix for an active exploit. Split out of #468
at reviewer request.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
* fix(specialist): reject depth cap at the boundary, not past it
Addresses jatmn's review on PR #491:
- Executor.Run checked `CurrentDepth > maxSpecialistDepth`, but this call is
what launches the child at CurrentDepth+1. A parent already AT the cap
passed the check and started a child one level past it, only getting
rejected on that child's own next call. Change to `>=` so the guard fires
before the over-cap child is launched.
- Add TestRunRejectsDepthAtMax to cover the boundary explicitly.
* test(specialist): cover resume boundary in depth-cap guard
The pre-branch depth guard in Run sits before the fresh/resume split, but
the boundary test only exercised the fresh path, leaving a future refactor
free to move the guard without any test catching a resume regression.
---------
Co-authored-by: Claude Sonnet 5 <[email protected]>
* feat(swarm): let swarm members build (write + sandboxed shell)
Swarm subagents were effectively read-only: a member runs headless at
"--auto low" (PermissionModeAuto), and ToolAdvertised strips every
prompt-requiring tool from the model's list — so write_file/edit_file/
apply_patch/bash were never handed to the member. It only saw read/grep/
glob/plan tools, so it could not build and the orchestrator did all the
writes itself.
Add PermissionModeMemberAuto: a headless mode that ADVERTISES the
in-workspace mutators (SideEffectWrite + SideEffectShell) on top of the
Auto set, while behaving exactly like Auto everywhere else. The sandbox
engine still decides at call time — in-workspace writes and sandbox-backed
shell auto-allow; out-of-workspace writes, network, and destructive
commands prompt and so are denied headless. member-auto normalizes to Auto
in the sandbox layer, so no sandbox authority is widened beyond what an
interactive auto agent already has.
Scope: swarm members only. The launcher sets MemberAutonomy; the executor
emits "--auto member" for a non-unsafe member (unsafe still -> high, plain
specialists still -> low). Task-tool specialists are unchanged.
Windows note: shell only auto-runs when the OS sandbox is active; without
`zero sandbox setup` a member writes files but cannot run builds.
* style: gofmt member_auto_test.go
* token efficiency: lossless cost accounting + minified file reads
Two complementary improvements to how Zero handles tokens.
ACCOUNTING — make cost measurement lossless and correct:
- Carry all five token dimensions end-to-end (input, cache-read, cache-write,
output, reasoning) in one Usage; add CacheWriteTokens (cache creation), which
the Anthropic adapter previously parsed then discarded.
- Price cache-writes at their premium rate (Anthropic 1.25x input, derived per
model). Regression-safe: a model without the rate bills cache-write at the
input rate exactly as before.
- Persist cache-read/cache-write/reasoning in EventUsage so `zero usage report`
reconstructs the SAME cost as the live tracker instead of over-pricing
cache-heavy sessions. A single EventUsagePayload writer is paired with the
reader so the JSON keys can't drift.
- Add a live context-window gauge to the status bar (used/window + percent,
graded green/amber/red) using the last request's input tokens.
REDUCTION — read_minified_file, a dense token-cheap file view:
- New internal/minify package + read_minified_file tool returning a comment-free,
line-number-free view: Go via the real go/parser AST; C/Java/CSS/SCSS/LESS/
Python via a string-aware comment lexer (handles strings, char literals,
triple-quotes, Java text blocks, Python f-strings incl. 3.12 same-quote
nesting). Every other language falls back to whitespace-only, so the stripper
can never corrupt content.
- ~32% fewer tokens on a realistic multi-file read; +~102 fixed tokens for the
tool schema, recovered many times over by a single subsystem read.
Tests: cache-write pricing + fallback, lossless persistence round-trip, the
context gauge, comment-stripping golden cases per language (comment-chars in
strings, Java text blocks, Python f-string nesting, CSS has no //), and the
unsupported-language safe fallback. gofmt / vet / build / -race all green.
* specialist: register read_minified_file in the tool registry + read-only sets
Smoke caught TestKnownToolNamesMatchCoreRegistry drifting once read_minified_file
joined the core read-only tools: the specialist tool allowlist (knownToolNames),
the read-only/edit/execute categories, and the read-only auto-approve set did not
list it. It is a read-only companion to read_file, so it belongs alongside
read_file in all of them; updated the default-allowlist test accordingly.
* feat(subagent): proactive specialist delegation + swarm execution fixes
Fixes:
- Task tool dropped the parent's permission mode, so every delegated
sub-agent ran at --auto high regardless of the parent. Forward
PermissionMode so a child never exceeds its parent's authority.
- Swarm members never executed: swarm_spawn accepts agent types
teammate/subagent, but the launcher looked those up as specialist names
(only worker/explorer/code-review exist), so every member failed with
"specialist not found". Run each member from its swarm definition via an
inline manifest on the executor.
Enhancements:
- Auto-delegation: the orchestrator system prompt now lists the available
specialists and when to use each, nudging it to offload read-heavy work to
a sub-agent so large tool output stays out of the main context.
- @-mention: a leading "@<specialist> <task>" in the TUI composer delegates
to that specialist (transcript keeps the verbatim mention); the @ palette
suggests specialists on the first token, file references mid-message.
- swarm.maxTeamSize config caps concurrent members per team (0 => default 8;
negative rejected), honored in user and project config.
- Proactive spawning: read-only specialists (explorer/code-review) auto-
approve via a new args-aware tool permission (tools.ArgsPermissioner), so
the agent delegates exploration off a normal prompt without an approval
prompt; write-capable specialists, resume, and unknown names still prompt.
The exec gate registers specialist tools at medium/high autonomy so a
non-trivial headless run can auto-delegate read-only too.
* fix(specialist): fail-safe child autonomy when PermissionMode is unset
specialistAutonomy mapped an empty PermissionMode to "--auto high" (unsafe),
silently re-introducing the privilege escalation the rest of this PR fixes: an
orchestrator that forgets to wire PermissionMode would get an unsafe child. The
Task tool and swarm both now propagate a resolved mode, so "" only occurs for a
caller that omitted it -- default that case to "low".
Only an explicit unsafe parent now yields --auto high; everything else (incl.
unset/unknown) is fail-safe low. Updates the stale "back-compat" comments and the
tests that documented the old behavior.
Addresses the non-blocking follow-up from PR review.
* feat(swarm): multi-agent swarm over the specialist sub-agent
Add internal/swarm so an orchestrator can spawn and coordinate MULTIPLE
specialist members concurrently under one team, communicate via per-agent
mailboxes, hand work off, and adopt orphaned tasks. Additive and opt-in: the
single Task tool is unchanged; swarm tools only act when invoked.
- Registry/Definition: agent roster (built-in teammate/subagent + user-defined).
- Mailbox: per-team/per-agent JSON inbox, owner-only 0600, file-locked with
stale-break, size-capped, path-confined, and malformed-fails-closed.
- Coordinator: in-memory task registry with terminal-state guards and stable
per-agent colors.
- Team: bounded concurrent members per team with a FIFO overflow queue that
drains one-per-slot.
- Lifecycle: spawn / handoff / orphan-adoption with bounded relaunch on
temporary failures; members run under the Swarm's base context so they
outlive the spawn call.
- Tools: swarm_spawn, swarm_send, swarm_inbox, swarm_status, swarm_handoff,
swarm_collect.
- Wiring: each member is launched through specialist.Executor and inherits the
orchestrator's model + permission mode (never widened) and runs under the same
sandbox/policy. Mailbox state lives under the workspace so writes fall within
the sandbox write rules. Swarm lifetime is paired with the specialist runtime.
Scheduling (cron/wakeup) and daemon-pool-backed members are deferred follow-ups.
* swarm: address CodeRabbit review (7 findings)
- coordinator: SetStatus rejects unknown TaskStatus values (fail closed) so a
task can never enter a state Summarize/lifecycle don't reason about.
- permission inheritance ENFORCED end-to-end (was resolved but discarded):
thread PermissionMode through specialist.TaskRunOptions -> BuildArgs/
BuildResumeArgs. A headless specialist child now runs "--auto high" (unsafe)
ONLY when the parent is unsafe or no mode is given (preserves the Task tool's
behavior); any other parent mode yields non-unsafe "--auto low", so a member
never gains more authority than its orchestrator. Also fixed the swarm
permission vocabulary to the real agent runtime modes (ask/spec-draft/auto/
unsafe) instead of TUI display names, and the launcher substitutes "auto" for
an empty mode so a member can't fall into the empty-means-high path.
- mailbox: symlink-aware confinement (EvalSymlinks) rejects an inbox dir that
resolves outside BaseDir; dirs tightened to 0700 even if pre-existing with
broad perms; lock release is ownership-token-aware so a stale-broken writer
can't delete a newer writer's lock (split-brain).
- member: recover panics inside a member goroutine -> fails that member only,
never crashes the orchestrator.
- tools: collapse() truncates on rune boundaries (no invalid UTF-8).
Tests added for every fix. Existing specialist behavior is unchanged (empty
PermissionMode keeps --auto high). Gates: gofmt/vet/build (host+linux)/test
-race/staticcheck(no new)/govulncheck(0)/deadcode(no new) all pass.
* swarm: address review round 2 (Windows lock + non-blocking notes)
Blocking (Vasanth): mailbox acquireLock now treats Windows ERROR_ACCESS_DENIED
/ ERROR_SHARING_VIOLATION as lock-contended (retry/wait) instead of a fatal
error, via a platform-aware isLockContended (lock_windows.go / lock_other.go).
A contended lock under Windows file-sharing semantics no longer fails a
send/read spuriously. TestMailboxConcurrentSends raised to 200 goroutines with a
10s lock timeout and now asserts zero send failures (regression guard).
Non-blocking notes addressed:
- Removed third-party reference-project names from doc comments (clean-room +
provider-neutral); the Go code is original.
- Documented the inbox-file size bound's ×2 (pretty-print overhead) constant.
- Documented Send's O(N) rewrite cost + when to switch to append-only.
- permissionRank: spec-draft now ranks below ask (it never widens authority).
- Documented watch's per-member-id retry binding (Member.ID == MemberSpec.ID).
- handoff Safety.Reason now also mentions the inbox write.
Gates: gofmt · vet · build (host+linux+windows) · test -race · staticcheck
(no new) · govulncheck (0) · deadcode (no new) all pass.
A specialist run with no description set --session-title to just the
description (empty), leaving AgentName empty on resume and making the
session unresumable. Always prefix the title with the specialist name and
fall back to the bare name when no description is given.