diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 01c95a997..dff3b6165 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -148,7 +148,7 @@ A worker deny-on-ask registers a harness-owned denied-call envelope (`src/permis Two directors, selected by role: -- **ChatDirector** (interactive, `src/agent/director.ts`) — Extends `DefaultDirector` with task list tracking, workflow nudges, LSP auto-activation, and multi-turn chat semantics. It never terminates the session: operator declines are surfaced as replies and the reactor stays alive for the next message. Yielding while a live fleet is running is allowed (idle-with-fleet); the open-task nudge does not rewrite that wait/reply, and the workflow idle rail does not declare the parent stuck while helpers run. The TUI idle-with-fleet seed alone is not occupancy: an empty fleet still auto-continues slash workflows. Occupancy, mailbox, and operator inbounds reset the workflow idle counter. When a worker finishes or fails while the parent is idle (including idle-with-fleet), occupancy delivers mailbox mail as system inbound so Skywalker starts a new turn without polling `wait_agents`. Mailbox mail is a Summary + Blockers digest (Findings when Summary is empty) with a blob pointer named for `read_file`, not the full report JSON. A failed blob write inlines a truncated report with an honest not-retrievable notice. When the fleet goes dry with tasks still todo/doing, the TUI runtime re-enters the parent with the same digest for newly collected worker reports rather than settling idle. Already-collected IDs in that continuation are id/status/description only. +- **ChatDirector** (interactive, `src/agent/director.ts`) — Extends `DefaultDirector` with task list tracking, workflow nudges, LSP auto-activation, and multi-turn chat semantics. It never terminates the session: operator declines are surfaced as replies and the reactor stays alive for the next message. Yielding while a live fleet is running is allowed (idle-with-fleet); the open-task nudge does not rewrite that wait/reply, and the workflow idle rail does not declare the parent stuck while helpers run. The TUI idle-with-fleet seed alone is not occupancy: an empty fleet still auto-continues slash workflows. Occupancy, mailbox, and operator inbounds reset the workflow idle counter. When a worker finishes or fails while the parent is idle (including idle-with-fleet), occupancy delivers mailbox mail as system inbound so dispatch starts a new turn without polling `wait_agents`. Mailbox mail is a Summary + Blockers digest (Findings when Summary is empty) with a blob pointer named for `read_file`, not the full report JSON. A failed blob write inlines a truncated report with an honest not-retrievable notice. When the fleet goes dry with tasks still todo/doing, the TUI runtime re-enters the parent with the same digest for newly collected worker reports rather than settling idle. Already-collected IDs in that continuation are id/status/description only. Auto mode is toggled by CLI flags (`--auto` / `--no-auto`); there is currently no in-session key to toggle it (default on; constrained envelope — workspace writes and unconstrained shell auto-allow; installs, recursive rm, force/uncontained worktree changes, sensitive-path and opaque-wrapper shell still ask; contained non-force `git worktree add`/`remove`/`prune` and `list` auto-allow; shell file-mutation denied). It is not a separate edit/plan mode. @@ -249,7 +249,7 @@ Three distinct concepts (do not conflate them): | **Task** | A checklist item owned by _one_ agent via `manage_tasks` | Local work plan — not a spawn | | **Fleet agent** | A short-lived worker for one focused, self-contained job | Spawned with **`spawn_agent`**; mailbox mail arrives as inbound on TUI and nested runs | -The **`spawn_agent`** tool starts a fleet agent on a separate inference source (tier/profile resolved from settings) and returns immediately with an `agent_id`. Each spawned worker gets one focused task; fan-out width follows independent lanes (one lane per PR/path/ownership). Keep the dispatch brief contract (`intent`, `success_criteria`, `do_not`, `report_focus`) tight. On the TUI primary, Skywalker idles after spawn and occupancy delivers mailbox mail as system inbound when a worker finishes or fails (including while siblings still run). Nested orchestrators collect through mailbox mail the same way. Declared fan-out is unlimited: excess dispatches enqueue rather than fail. `run()` is admitted by `src/subagent/admission.ts` (default burst window of 8 is race-avoidance so a 429 freeze can fire before a herd — not a declared-spawn cap). Occupancy is the whole first `run()`, including a mounted `wait_agents` (exec primary). Nested children of an already-admitted parent bypass **capacity** so a nested orchestrator cannot deadlock while holding a slot; they still wait on a provider 429 pause. Drain is FIFO among currently admissible jobs (a paused provider is skipped, not head-of-line for every provider). Resume and followup inference re-enter the same queue. Queued workers report wait/list status `queued` (live, not failed). Lowering capacity never cancels in-flight work. Retryable provider 429s freeze new admits via the shared retry remapper in `createCorbitsRetryPolicy`; `quota_exhausted` does not freeze. `list_agents` remains mailbox-scoped. The dispatch brief separates durable `context`, actionable `prompt`, and optional `goals` (checklist seeds for the _child's_ own `manage_tasks` list). Implement/review dispatches (and their default directors) fail closed without non-empty `success_criteria`. The child returns a structured report (`Summary` / `Findings` / `Blockers` / `Paths`) plus a tools-used footer. Parent and child never share a `manage_tasks` list. +The **`spawn_agent`** tool starts a fleet agent on a separate inference source (tier/profile resolved from settings) and returns immediately with an `agent_id`. Each spawned worker gets one focused task; fan-out width follows independent lanes (one lane per PR/path/ownership). Keep the dispatch brief contract (`intent`, `success_criteria`, `do_not`, `report_focus`) tight. On the TUI primary, dispatch idles after spawn and occupancy delivers mailbox mail as system inbound when a worker finishes or fails (including while siblings still run). Nested orchestrators collect through mailbox mail the same way. Declared fan-out is unlimited: excess dispatches enqueue rather than fail. `run()` is admitted by `src/subagent/admission.ts` (default burst window of 8 is race-avoidance so a 429 freeze can fire before a herd — not a declared-spawn cap). Occupancy is the whole first `run()`, including a mounted `wait_agents` (exec primary). Nested children of an already-admitted parent bypass **capacity** so a nested orchestrator cannot deadlock while holding a slot; they still wait on a provider 429 pause. Drain is FIFO among currently admissible jobs (a paused provider is skipped, not head-of-line for every provider). Resume and followup inference re-enter the same queue. Queued workers report wait/list status `queued` (live, not failed). Lowering capacity never cancels in-flight work. Retryable provider 429s freeze new admits via the shared retry remapper in `createCorbitsRetryPolicy`; `quota_exhausted` does not freeze. `list_agents` remains mailbox-scoped. The dispatch brief separates durable `context`, actionable `prompt`, and optional `goals` (checklist seeds for the _child's_ own `manage_tasks` list). Implement/review dispatches (and their default directors) fail closed without non-empty `success_criteria`. The child returns a structured report (`Summary` / `Findings` / `Blockers` / `Paths`) plus a tools-used footer. Parent and child never share a `manage_tasks` list. Workers ask the spawning parent with **`ask_director`** (not the human). That parks a question while the worker stays `running`. On the TUI primary that arrives as an idle-send wake. On a nested orchestrator the question arrives as mailbox mail with an `awaiting_director` status — that is not terminal. Once a parked ask is surfaced (TUI wake, nested mailbox mail, or a successful `list_agents`), further `list_agents` calls fail closed until **`send_input`** answers or the ask is dropped — `list_agents` is not a poll. The parent answers with **`send_input`**, then continues. Escalate to the human with **`ask_operator`** only when the parent cannot resolve it. @@ -278,7 +278,7 @@ Enforcement is runtime code at the existing tool-mount point, not prompt wording #### Closed director fleet (`src/agent/directors/`) -Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`, fleet authority `tier`) registered in a **closed** set of 20 ids. There is no catch-all worker: `spawn_agent` without `agent` or non-general `intent`, and `spawn_agent(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `spawn_agent` dispatch time (not prompt-only). Skywalker is the primary session identity: `spawn_agent(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog. +Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`, fleet authority `tier`) registered in a **closed** set of 10 ids. There is no catch-all worker: `spawn_agent` without `agent` or non-general `intent`, and `spawn_agent(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `spawn_agent` dispatch time (not prompt-only). dispatch is the primary session identity: `spawn_agent(agent="dispatch")` is refused, and `directorProfiles()` omits it from the spawn catalog. **Primary** @@ -333,12 +333,12 @@ Data-only agent plugins (`src/plugins/data-only-agent.ts`) synthesize `agentPlug ### System Prompt (`src/agent/prompts.ts`) -The primary session identity is **Skywalker** (`buildChatRole` → `createSkywalkerSystemPrompt`). Product name remains Corbits Code; when asked its name, the primary answers Skywalker. Role: dispatcher — classify, DIY tiny/single-file/one-route product edits , spawn named closed directors via `spawn_agent` for substantial work, track the fleet, synthesize. The Skywalker card is a ~3–5k operator-surface prompt; idle, mailbox delivery, and poll-avoidance live in the harness (tool descriptions and occupancy), not the card. Family residuals append via `@corbits/prompt-variance` at assembly (model-family policy) — do not inline family text in the card. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary session (CORE and `SKYWALKER_TOOLS`) so Skywalker can DIY bounded edits; spawn remains the default for substantial, multi-file, parallel, or specialist work. Shell file-writes stay denied by auto-shell policy. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-worker write-path lock; concurrent lanes sharing a cwd are instead flagged (not blocked) as a `conflict` intervention. A frontier model already knows how to code; the static prompt carries harness-specific facts and the closed-fleet routing policy. The base is three individually-exported sections: +The primary director is **dispatch** (`buildChatRole` → `createDispatchSystemPrompt`). Role: dispatcher — classify, DIY tiny/single-file/one-route product edits , spawn named closed directors via `spawn_agent` for substantial work, track the fleet, synthesize. The dispatch card is a ~3–5k operator-surface prompt; idle, mailbox delivery, and poll-avoidance live in the harness (tool descriptions and occupancy), not the card. Family residuals append via `@corbits/prompt-variance` at assembly (model-family policy) — do not inline family text in the card. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary session (CORE and `DISPATCH_TOOLS`) so dispatch can DIY bounded edits; spawn remains the default for substantial, multi-file, parallel, or specialist work. Shell file-writes stay denied by auto-shell policy. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-worker write-path lock; concurrent lanes sharing a cwd are instead flagged (not blocked) as a `conflict` intervention. A frontier model already knows how to code; the static prompt carries harness-specific facts and the closed-fleet routing policy. The base is three individually-exported sections: -- `buildChatRole` — Skywalker dispatcher card (operator surface; classify; DIY tiny/bounded product edits; spawn named specialists). +- `buildChatRole` — dispatch card (operator surface; classify; DIY tiny/bounded product edits; spawn named specialists). - `buildHarnessFacts` — the non-derivable rules: shell file-writes are blocked, path tools are the DIY surface on primary (spawn coder/shakespeare directors for substantial work), dependency installs and off-limits paths need approval, images are native multimodal input, core tools plus the advertised catalog (including `skill_search`) are resident (MCP and other unadvertised tools load via `tool_search`; use `search_agents` before dispatching specialists), workflows run only from slash-command steps, and session memory lives at `.corbits/MEMORY.md`. - `buildGuidelines` — be concise, prefer `spawn_agent` (mailbox mail on TUI; exec-primary `wait_agents`) for substantial product work with one focused task per worker and fan-out by independent lanes, DIY tiny/bounded edits on the parent, answer questions and diagnose visual/product feedback before editing, work autonomously for explicit coding tasks, use `lsp` for symbol work, and require implementation agents to run the repository-defined typecheck, relevant tests, and every defined full verification command. Agents report exact commands, outcomes, and exit statuses; a repository with no typecheck command produces an explicit Blocker backed by project-configuration evidence rather than an invented command or silent skip. -- `buildPromptDisciplineBlock` — a shared, prohibition-form section appended exactly once to every built prompt (chat and sub-agent, every provider family). Primary vs worker wording differs for product writes: workers are told to use `read_file`/`edit_file`/`write_file`; Skywalker is told to DIY tiny/bounded edits with those path tools and spawn directors for substantial work. Shared rules: never `cat`/`sed`/heredoc/`echo` for file work, no setting or exporting environment variables (recurring needs belong in project settings), `web_fetch`/`web_search` instead of `curl`/`wget`/hand-rolled queries, one operation per `run_shell` call, turn semantics (a tool-less reply is the final answer, no repeat searches, stop and change approach after three failed attempts, batch independent reads in parallel), and TTY output rules (short bold headers, one-line bullets, backticks for paths/commands, no wide tables). +- `buildPromptDisciplineBlock` — a shared, prohibition-form section appended exactly once to every built prompt (chat and sub-agent, every provider family). Primary vs worker wording differs for product writes: workers are told to use `read_file`/`edit_file`/`write_file`; dispatch is told to DIY tiny/bounded edits with those path tools and spawn directors for substantial work. Shared rules: never `cat`/`sed`/heredoc/`echo` for file work, no setting or exporting environment variables (recurring needs belong in project settings), `web_fetch`/`web_search` instead of `curl`/`wget`/hand-rolled queries, one operation per `run_shell` call, turn semantics (a tool-less reply is the final answer, no repeat searches, stop and change approach after three failed attempts, batch independent reads in parallel), and TTY output rules (short bold headers, one-line bullets, backticks for paths/commands, no wide tables). **Provider-conditional residuals.** Per-family additions layer on top of the shared block via the same `ModelFamilyPolicy` mechanism the directors use (`src/subagent/provider-family.ts`, `src/agent/model-family-policy.ts`) — additive lines, never prompt forks. **Grok** leaves get `buildGrokLeafAntiThrashNote` (gated by `shouldApplyGrokAntiThrash` / `applyGrokFinishBias`, withheld from orchestrators): a compact finish-bias reinforcement plus a one-line reminder to route file/web work through the dedicated tools rather than `run_shell`, motivated by observed tool-routing thrash on the same harness. **Kimi** intentionally has no residual yet — `detectModelFamily` already resolves the family so callers can branch on it, but the prompt seam is left unfilled pending eval characterization of Kimi's behavior, mirroring the provisional (permissive-default) policy in `model-family-policy.ts`. @@ -346,7 +346,7 @@ The primary session identity is **Skywalker** (`buildChatRole` → `createSkywal **Overrides.** `loadSystemPromptOverrides` (`src/agent/context-extensions.ts`) resolves a project `SYSTEM.md` (repo root, then `.corbits/`) that **replaces** the static base block, and an `APPEND_SYSTEM.md` that is **appended** as an extension. These compose with `config.systemPromptExtensions` (profile config) and the auto-discovered `AGENTS.md`, all of which attach as appended sections after the base. -**Grok prefix.** The provider cache prefix is the advertised tools array plus the system prompt. Grok does **not** get a trimmed fork of that prefix. `AGENTS.md` (capped at 32,000 bytes, framed as reference) and the full CORE+CATALOG schemas stay on every Grok primary prefix, same as every other family. Family residuals are additive lines only (`buildGrokLeafAntiThrashNote` on leaves); stripping project guidance or core schemas for Grok would be a prompt fork and would force `tool_search` round-trips — the thrash Grok is already sensitive to. The cheaper split already exists: workers use `buildSubAgentSystemPrompt` (trimmed director prompt, no `AGENTS.md`, mounted-tool schemas only). Skywalker's infer envelope is the primary chat prompt, not a spawned skywalker package. Prefix bytes are measured in `src/agent/prompt-sizes.ts` (`assembleSkywalkerInferEnvelope` vs `assembleDirectorPrompt("skywalker", "grok")`). `present` stays off the advertised prefix on every family. +**Grok prefix.** The provider cache prefix is the advertised tools array plus the system prompt. Grok does **not** get a trimmed fork of that prefix. `AGENTS.md` (capped at 32,000 bytes, framed as reference) and the full CORE+CATALOG schemas stay on every Grok primary prefix, same as every other family. Family residuals are additive lines only (`buildGrokLeafAntiThrashNote` on leaves); stripping project guidance or core schemas for Grok would be a prompt fork and would force `tool_search` round-trips — the thrash Grok is already sensitive to. The cheaper split already exists: workers use `buildSubAgentSystemPrompt` (trimmed director prompt, no `AGENTS.md`, mounted-tool schemas only). The dispatch card is the primary chat prompt and is never spawned as a worker. Prefix bytes are measured in `src/agent/prompt-sizes.ts` (`assembleDirectorPrompt`). `present` stays off the advertised prefix on every family. ### State Persistence (`src/session/state.ts`) @@ -481,7 +481,7 @@ There is no skill `type` field required for model invocation — a skill body is `buildSkillsSection` lists discovered skill names in the system prompt (no descriptions). Details come from `skill_search`; the full instructions enter context in two ways: 1. **Model** — `skill_search` for descriptions, then `use_skill` (`src/agent/use-skill.ts`) with a skill name. The handler refuses names already attached at spawn or already loaded this session (short “already in context”; it does not dump the body again). Otherwise it calls `resolveSkillBody`, strips the frontmatter, and returns the body as the tool result. -2. **Operator** — `/` from `loadSkillCommands` sends the same SKILL.md body (plus typed args) to the primary as a user turn. Skills with `user-invocable: false` are omitted from the slash registry and remain `use_skill` only. Skywalker then follows the recipe. +2. **Operator** — `/` from `loadSkillCommands` sends the same SKILL.md body (plus typed args) to the primary as a user turn. Skills with `user-invocable: false` are omitted from the slash registry and remain `use_skill` only. Dispatch then follows the recipe. Which plugin skill directories are in scope is decided in `runner.ts` / `skillDirsFromEnabledPlugins`, which passes the enabled plugins' dirs to both `discoverSkills` (for the listing) and the `use_skill` tool (for resolution). Project-local `.agents`/`.claude`/`.codex/skills` are always searched. Slash-command registration is first-wins (built-ins, then plugins in discovery order), so a first-party `/implement` stays first-party if a marketplace plugin of the same slash name is also enabled. diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index bf4b05af9..b0ef73387 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -65,7 +65,7 @@ src/ index.ts CLI entry: verbs, dispatch, help agent/ director.ts ChatDirector; director-layer tool defs - prompts.ts System prompt builders; buildChatRole → Skywalker + prompts.ts System prompt builders; buildChatRole → dispatch card tools.ts Agent tool registration helpers agent-search.ts search_agents tool + profile lexical index default-agents.ts Built-in profiles = directorProfiles() spawn catalog @@ -159,18 +159,18 @@ Twenty packages under `src/agent/directors//` register in `DIRECTOR_REGISTRY 1. `spawn_agent(agent=…)` / `spawn_agent(intent=…)` → `resolveDirector` in `agent-fleet.ts` before tools and system prompt are built. Bare `spawn_agent` (neither field) and `intent=general` fail closed. After director resolution, `createSpawnAgentTool` fail-closes implement/review (and their default directors) without non-empty `success_criteria`. 2. `packageToProfile` maps envelope (`tools.allow`/`deny`) to `AgentProfile.capabilities` and `spawn.maySpawn` → `orchestrator`. System prompts are prefixed with a stable identity block (`formatDirectorSystemPrompt`: agent id, model role, optional skills). -3. Nested spawn: packages with `spawn.allowlist` forward that list into nested `spawn_agent` (`spawnAllowlist` on nestedDispatch). Off-list `agent` is refused. `spawn_agent(agent=skywalker)` is refused (primary is not a spawned worker). Primary omits the list so plugin profiles stay reachable. -4. `directorProfiles()` is the spawn catalog (`default-agents.ts`) — closed set minus skywalker. Plugin and local `.agents/agents/` profiles still load, but closed `DIRECTOR_IDS` cannot be overridden or aliased. -5. Primary chat role is Skywalker: `buildChatRole()` → `createSkywalkerSystemPrompt()`. The Skywalker card is a ~3–5k dispatcher (classify, tiny DIY, spawn named specialists); idle/mailbox/poll live in the harness, and family residuals come from `@corbits/prompt-variance`. Product mutation tools (`write_file` / `edit_file` / `delete_file`) live in CORE (and `SKYWALKER_TOOLS`) so they are advertised on the primary without a `tool_search` round-trip. DIY tiny/bounded edits on the parent; spawn builder/docs directors for substantial work — a prompt judgment call, not a toolset strip. `PRIMARY_DENIED_PRODUCT_TOOLS` is gone. Shell file-writes stay denied; MCP tools are not re-filtered by a product-write deny list. There is no static per-profile write-path lock (CL-6952). +3. Nested spawn: packages with `spawn.allowlist` forward that list into nested `spawn_agent` (`spawnAllowlist` on nestedDispatch). Off-list `agent` is refused. `spawn_agent(agent=dispatch)` is refused (primary is not a spawned worker). Primary omits the list so plugin profiles stay reachable. +4. `directorProfiles()` is the spawn catalog (`default-agents.ts`) — closed set minus dispatch. Plugin and local `.agents/agents/` profiles still load, but closed `DIRECTOR_IDS` cannot be overridden or aliased. +5. Primary chat role is dispatch: `buildChatRole()` → `createDispatchSystemPrompt()`. The dispatch card is a ~3–5k dispatcher (classify, tiny DIY, spawn named specialists); idle/mailbox/poll live in the harness, and family residuals come from `@corbits/prompt-variance`. Product mutation tools (`write_file` / `edit_file` / `delete_file`) live in CORE (and `DISPATCH_TOOLS`) so they are advertised on the primary without a `tool_search` round-trip. DIY tiny/bounded edits on the parent; spawn coder/shakespeare directors for substantial work — a prompt judgment call, not a toolset strip. `PRIMARY_DENIED_PRODUCT_TOOLS` is gone. Shell file-writes stay denied; MCP tools are not re-filtered by a product-write deny list. There is no static per-profile write-path lock (CL-6952). - **Grok infer envelope.** `loadSessionChatPrompt` always appends `loadAgentContextExtensions` (`AGENTS.md`, capped at `MAX_AGENTS_MD_BYTES`) and advertises CORE+CATALOG full schemas via `advertisedToolNamesForSessionMode`. That assembly is family-agnostic — Grok does not substitute the trimmed director prompt (`buildSubAgentSystemPrompt` + `formatDirectorSystemPrompt`). Workers already use that trimmed path (no `AGENTS.md`, mounted-tool schemas only). Keep the infer envelope on Grok; do not strip `AGENTS.md` or core schemas. Measure in `src/agent/prompt-sizes.ts` (`assembleSkywalkerInferEnvelope` vs `assembleDirectorPrompt("skywalker", "grok")`). + **Grok infer envelope.** `loadSessionChatPrompt` always appends `loadAgentContextExtensions` (`AGENTS.md`, capped at `MAX_AGENTS_MD_BYTES`) and advertises CORE+CATALOG full schemas via `advertisedToolNamesForSessionMode`. That assembly is family-agnostic — Grok does not substitute the trimmed director prompt (`buildSubAgentSystemPrompt` + `formatDirectorSystemPrompt`). Workers already use that trimmed path (no `AGENTS.md`, mounted-tool schemas only). Keep the infer envelope on Grok; do not strip `AGENTS.md` or core schemas. Measure in `src/agent/prompt-sizes.ts` (`assembleDirectorPrompt`). **One advertised posix set (CL-8400).** Registry engines stay posix-named (`read_file`, `write_file`, `edit_file`, `delete_file`, `run_shell`, `search_files`, `grep`). Advertise is a 1:1 projection onto wire names (`read`, `write`, `edit`, `delete`, `bash`, `glob`, `grep`) plus the unchanged control-plane. Incoming aliases (wire names, old posix ids, Codex `shell` → `run_shell`, `update_plan` → `manage_tasks`) canonicalize onto the engine id for dispatch and grants. `apply_patch` is neither advertised nor dispatched (not an alias of `edit`). `list_dir` stays mounted and unadvertised. Director `tools.allow` stays engine names. Codex does not dual-publish `shell`+`run_shell`. Hidden `shell` coerces Codex `command` (string or `["bash","-lc",script]` argv), `workdir`, and `timeout_ms` onto `run_shell`. Hidden `update_plan` maps `plan: [{step, status}]` onto `manage_tasks(action: "create")` (`pending`/`in_progress`/`completed` → `todo`/`doing`/`done`). 6. There is no static write-path declaration on packages or profiles (CL-6952 removed it — no shipped director ever set one). Instead, `agent-fleet.ts` tracks each running dispatch by cwd; a new mutating dispatch that lands on the same cwd as a live mutating peer (`pending_init`/`running`, and not a declared read-only `modelRole` of `explore`/`plan`/`review`/`test`) records at most one `concurrent-lane-overlap` entry per cwd wave in `intervention-log.ts` (class `conflict`). The wave flag clears when no live mutating writer remains for that cwd. Terminal-but-unsettled lanes (for example cancelled with `finishedAt` set while the run promise has not reached `finally`) are pruned from the map and do not warn. This is advisory only — it never blocks the spawn, since cwd overlap does not prove the two lanes touch the same files. -7. Spawn effort: pin > package `modelRole` default (`defaultEffortForDirector`; intern=low; plan/review/orchestrator=high; implement/explore/docs/test=medium) > orchestrator/worker binary > parent inheritance. Attached skills (style + philosophy on directors that listed both; never intern or Skywalker primary) are injected into the worker system prompt at spawn from plugin skill dirs only (no project-local `.agents`/`.claude`/`.codex` fallback). Optional skills are listed in the identity header for awareness; workers mount `skill_search` + `use_skill` on every family, scoped to the union of `attachedSkills` and `optionalSkills`. `use_skill` refuses names already attached or already loaded this session and does not return the body again. Primary mounts `use_skill` for its own skill list (same in-session refuse; no attached set). +7. Spawn effort: pin > package `modelRole` default (`defaultEffortForDirector`; plan/review/orchestrator=high; implement/explore/docs/test=medium) > orchestrator/worker binary > parent inheritance. Attached skills (none on the shipped directors today; never the primary) are injected into the worker system prompt at spawn from plugin skill dirs only (no project-local `.agents`/`.claude`/`.codex` fallback). Optional skills are listed in the identity header for awareness; workers mount `skill_search` + `use_skill` on every family, scoped to the union of `attachedSkills` and `optionalSkills`. `use_skill` refuses names already attached or already loaded this session and does not return the body again. Primary mounts `use_skill` for its own skill list (same in-session refuse; no attached set). -Intent defaults: `intent=implement` → director `builder`; `explore` → `explorer`; `plan` → `counsel`; `review` → `critic`; general → error. Spawn: skywalker full fleet; all other directors, including greybeard, mount no fleet tools. Skywalker assigns one focused task per worker; fan-out width follows independent lanes (one lane per PR/path/ownership). Live `` injects cwd, platform, arch, runtime, date, and git status on every chat and worker prompt. +Intent defaults: `intent=implement` → director `coder`; `explore` → `explorer`; `plan` → `planner`; `review` → `reviewer`; general → error. Spawn: dispatch full fleet; all other directors mount no fleet tools. Dispatch assigns one focused task per worker; fan-out width follows independent lanes (one lane per PR/path/ownership). Live `` injects cwd, platform, arch, runtime, date, and git status on every chat and worker prompt. ### Auto Mode @@ -204,7 +204,7 @@ Listings are list-free, dumps are dump-locked: a bounded `ls`/`tree` prints name `ChatInputProps` carries `isProcessing?: boolean` and `onInterrupt?: (message: string) => void`. When `isProcessing` is true, drain timing is **parent-idle** vs **session-idle**: -- **Enter** soft-steers while the parent is busy — enqueues kind `"steer"` and delivers at the next **parent** `tool.boundary` (the parent tool finishing, not a child). Does not interrupt. **Parent-idle** is when the primary Skywalker turn is not inside an in-flight parent tool; a long parent **foreground** `run_shell` is parent-busy and holds steers. A `run_shell` started with `background: true` returns at once and releases the boundary; its completion is delivered as a system message (`buildShellBackgroundMessage`, mailbox `system`, no operator-originated flag — it re-enters the reactor without counting as operator input) on a later turn. +- **Enter** soft-steers while the parent is busy — enqueues kind `"steer"` and delivers at the next **parent** `tool.boundary` (the parent tool finishing, not a child). Does not interrupt. **Parent-idle** is when the primary dispatch turn is not inside an in-flight parent tool; a long parent **foreground** `run_shell` is parent-busy and holds steers. A `run_shell` started with `background: true` returns at once and releases the boundary; its completion is delivered as a system message (`buildShellBackgroundMessage`, mailbox `system`, no operator-originated flag — it re-enters the reactor without counting as operator input) on a later turn. #### Background shell mode diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 6e12b7162..3650c8727 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -41,7 +41,7 @@ The evidence is in how the product fails today: the personas already produce exc 5. **Resume capability** — Runs persist to a git-backed store and resume from the last point after interruption. 6. **Legible loop** — A live event log, working-tree diff panel, plan tracker, and real-time cost meter show what happened, when, and why. 7. **Operator-in-the-loop** — The agent can call `ask_operator` to pause and ask a clarifying question; the operator answers from a modal (TUI). Headless `corbits exec` unmounts `ask_operator` when stdin/stdout are not TTYs. TTY exec still reads a single line from stdin. -8. **Mid-run steering** — Two modes while the agent is running, keyed to **whose** idle. **Parent-idle** is when the primary Skywalker turn is not inside an in-flight parent tool; **session-idle** is parent-idle **and** no live fleet lanes. **Enter** soft-steers while the parent is busy — delivers at the next **parent** `tool.boundary` without stopping the current run; a long parent `run_shell` is parent-busy, so Enter is a queued steer, not a new turn. A queued steer delivers at the next parent `tool.boundary` so occupancy can pick it up. Idle-with-fleet is shipped: after a non-blocking `spawn_agent` dispatch the parent goes idle while workers keep running, mailbox mail arrives as inbound when a worker finishes or fails, and mid-hold Enter starts a new primary turn instead of queueing a steer. **Alt+Enter** queues a follow-up delivered only on session-idle (`run` goes idle; does not interrupt). Session-idle Alt+Enter is a no-op. **Ctrl+C** stops the run outright. The notice row shows distinct `steer N` / `follow-up M` badges, and held messages list in a pending column stacked on the prompt box — `↑`/`↓` select, `Enter` force-pushes one now, `Ctrl+X` drops it — instead of echoing labelled transcript rows; when steers are pending and a parent tool has been in flight a few seconds, the notice names that command. Shortcuts are listed in `/help` (`Enter` soft-steer · `Alt+Enter` follow-up · `Ctrl+C` stop). +8. **Mid-run steering** — Two modes while the agent is running, keyed to **whose** idle. **Parent-idle** is when the primary dispatch turn is not inside an in-flight parent tool; **session-idle** is parent-idle **and** no live fleet lanes. **Enter** soft-steers while the parent is busy — delivers at the next **parent** `tool.boundary` without stopping the current run; a long parent `run_shell` is parent-busy, so Enter is a queued steer, not a new turn. A queued steer delivers at the next parent `tool.boundary` so occupancy can pick it up. Idle-with-fleet is shipped: after a non-blocking `spawn_agent` dispatch the parent goes idle while workers keep running, mailbox mail arrives as inbound when a worker finishes or fails, and mid-hold Enter starts a new primary turn instead of queueing a steer. **Alt+Enter** queues a follow-up delivered only on session-idle (`run` goes idle; does not interrupt). Session-idle Alt+Enter is a no-op. **Ctrl+C** stops the run outright. The notice row shows distinct `steer N` / `follow-up M` badges, and held messages list in a pending column stacked on the prompt box — `↑`/`↓` select, `Enter` force-pushes one now, `Ctrl+X` drops it — instead of echoing labelled transcript rows; when steers are pending and a parent tool has been in flight a few seconds, the notice names that command. Shortcuts are listed in `/help` (`Enter` soft-steer · `Alt+Enter` follow-up · `Ctrl+C` stop). 9. **Orchestrator-only (TUI + exec)** — The primary session is always the orchestrator: it can act directly and delegates via `spawn_agent` (then idle; mailbox mail inbound) / `search_agents`. Nested orchestrators collect through mailbox mail the same way. Long jobs belong on workers — a parent that runs them itself stays parent-busy and holds Enter steers. Single-agent session mode, the first-run mode picker, and Settings → Session are gone (CL-5814). Legacy `sessionMode` values on disk are ignored. ## User Experience @@ -155,7 +155,7 @@ Capabilities beyond the core toolset are opt-in plugins, enabled per workspace t ## Multi-agent (fleet agents) -The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 10 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `SKYWALKER_TOOLS`) — path tools are the DIY surface; spawn remains the default for substantial, multi-file, parallel, or specialist work. Shell file-writes stay denied. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one). A concurrent dispatch landing on the same working directory as another still-running lane is recorded as a `conflict` intervention, not blocked. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`) tell Skywalker which directors to spawn for substantial work; tiny/bounded edits may run on the primary. +The primary session is always **orchestrator** (single-agent mode is gone). Its director is **dispatch**: classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 10 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `DISPATCH_TOOLS`) — path tools are the DIY surface; spawn remains the default for substantial, multi-file, parallel, or specialist work. Shell file-writes stay denied. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one). A concurrent dispatch landing on the same working directory as another still-running lane is recorded as a `conflict` intervention, not blocked. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`) tell dispatch which directors to spawn for substantial work; tiny/bounded edits may run on the primary. | Role | Directors | | ---------- | --------------------------------------------------------------------------------- | diff --git a/e2e/reactor-permission-multi-turn.test.ts b/e2e/reactor-permission-multi-turn.test.ts index 781a9ad09..51e027a25 100644 --- a/e2e/reactor-permission-multi-turn.test.ts +++ b/e2e/reactor-permission-multi-turn.test.ts @@ -86,7 +86,7 @@ describe("integration — reactor permission + multi-turn", () => { }); try { - // Primary Skywalker does not mount write_file; permission multi-turn + // Primary dispatch does not mount write_file; permission multi-turn // still covers a consequential tool that remains on the primary surface. session.harness.scenario.replyOnce("anthropic", { toolCalls: [ diff --git a/scripts/eval-capability.ts b/scripts/eval-capability.ts index b648cb234..0f349d722 100755 --- a/scripts/eval-capability.ts +++ b/scripts/eval-capability.ts @@ -102,7 +102,7 @@ interface CliOptions { allowProviderFallback: boolean; /** * Exec overlay: run the product path as this closed-fleet director. - * Eval/CI override, not single-agent mode. Omitted = skywalker default. + * Eval/CI override, not single-agent mode. Omitted = dispatch default. */ director?: string; } @@ -130,7 +130,7 @@ function printUsage(): void { --dry-run List cases × variants only (still requires --provider/--model or --matrix) --allow-provider-fallback Allow resolved provider/model to differ from what was requested (default: hard-fail) - --director Exec overlay: run as this director (default: skywalker). + --director Exec overlay: run as this director (default: dispatch). Eval/CI override, not single-agent mode -h, --help Show help `); diff --git a/src/agent/directors/tool-sets.test.ts b/src/agent/directors/tool-sets.test.ts index a158dc1af..b15c59130 100644 --- a/src/agent/directors/tool-sets.test.ts +++ b/src/agent/directors/tool-sets.test.ts @@ -8,7 +8,6 @@ import { REVIEW_TOOLS, SKILL_TOOLS, DISPATCH_TOOLS, - SKYWALKER_TOOLS, } from "./tool-sets.js"; describe("PRODUCT_WRITE_TOOLS", () => { @@ -66,23 +65,19 @@ describe("DOCS_TOOLS", () => { }); }); -describe("DISPATCH_TOOLS / SKYWALKER_TOOLS / ORCHESTRATOR_TOOLS", () => { +describe("DISPATCH_TOOLS / ORCHESTRATOR_TOOLS", () => { test("both mount product writes and split fleet tools", () => { for (const name of PRODUCT_WRITE_TOOLS) { expect(DISPATCH_TOOLS as readonly string[]).toContain(name); - expect(SKYWALKER_TOOLS as readonly string[]).toContain(name); + expect(DISPATCH_TOOLS as readonly string[]).toContain(name); expect(ORCHESTRATOR_TOOLS as readonly string[]).toContain(name); } for (const name of ["spawn_agent"] as const) { expect(DISPATCH_TOOLS as readonly string[]).toContain(name); - expect(SKYWALKER_TOOLS as readonly string[]).toContain(name); + expect(DISPATCH_TOOLS as readonly string[]).toContain(name); expect(ORCHESTRATOR_TOOLS as readonly string[]).toContain(name); } - for (const surface of [ - DISPATCH_TOOLS, - SKYWALKER_TOOLS, - ORCHESTRATOR_TOOLS, - ] as const) { + for (const surface of [DISPATCH_TOOLS, ORCHESTRATOR_TOOLS] as const) { expect(surface as readonly string[]).not.toContain("wait_agents"); expect(surface as readonly string[]).not.toContain("task"); } @@ -91,7 +86,7 @@ describe("DISPATCH_TOOLS / SKYWALKER_TOOLS / ORCHESTRATOR_TOOLS", () => { // CL-7051: fleet discovery is Tier-1 only. test("search_agents is on Dispatch only, not the nested orchestrator surface", () => { expect(DISPATCH_TOOLS as readonly string[]).toContain("search_agents"); - expect(SKYWALKER_TOOLS as readonly string[]).toContain("search_agents"); + expect(DISPATCH_TOOLS as readonly string[]).toContain("search_agents"); expect(ORCHESTRATOR_TOOLS as readonly string[]).not.toContain( "search_agents", ); @@ -106,7 +101,6 @@ describe("DISPATCH_TOOLS / SKYWALKER_TOOLS / ORCHESTRATOR_TOOLS", () => { REVIEW_TOOLS, ORCHESTRATOR_TOOLS, DISPATCH_TOOLS, - SKYWALKER_TOOLS, ] as const) { expect(surface as readonly string[]).toContain("skill_search"); expect(surface as readonly string[]).toContain("use_skill"); @@ -122,7 +116,6 @@ describe("DISPATCH_TOOLS / SKYWALKER_TOOLS / ORCHESTRATOR_TOOLS", () => { REVIEW_TOOLS, ORCHESTRATOR_TOOLS, DISPATCH_TOOLS, - SKYWALKER_TOOLS, ] as const) { const names = surface as readonly string[]; expect(new Set(names).size).toBe(names.length); diff --git a/src/agent/directors/tool-sets.ts b/src/agent/directors/tool-sets.ts index 205655788..3e11efed8 100644 --- a/src/agent/directors/tool-sets.ts +++ b/src/agent/directors/tool-sets.ts @@ -76,6 +76,3 @@ export const ORCHESTRATOR_TOOLS = [ /** Dispatch primary: orchestrator surface plus fleet discovery (Tier-1 only). */ export const DISPATCH_TOOLS = [...ORCHESTRATOR_TOOLS, "search_agents"] as const; - -/** Legacy alias for backwards compatibility during migration. */ -export const SKYWALKER_TOOLS = DISPATCH_TOOLS; diff --git a/src/agent/skill-search.ts b/src/agent/skill-search.ts index c2d0d7852..ac6528a0b 100644 --- a/src/agent/skill-search.ts +++ b/src/agent/skill-search.ts @@ -14,7 +14,7 @@ import { // Catalog lookup for skills. Names live in the system prompt; this tool returns // matching name + description so the model can choose. Bodies load via use_skill. // Directly callable and advertised on primary — do not send the model through -// tool_search to find it. Primary copy is on-demand catalog (Skywalker has no +// tool_search to find it. Primary copy is on-demand catalog (dispatch has no // attached skills). Workers mount workerSkillSearchDefinition so they skip // search when attached bodies already cover the job. const SKILL_SEARCH_INPUT_SCHEMA = { diff --git a/src/agent/tool-search.ts b/src/agent/tool-search.ts index 75a7b5b68..ddc6e2fc8 100644 --- a/src/agent/tool-search.ts +++ b/src/agent/tool-search.ts @@ -30,7 +30,7 @@ import { canonicalToolName } from "./canonical-tool-name.js"; // actually needs it. // // Product mutation tools (write / edit / delete) sit in CORE so -// the primary Skywalker session can DIY tiny/bounded edits without a +// the primary dispatch session can DIY tiny/bounded edits without a // tool_search round-trip. Substantial work still spawns build / docs // directors — that is a prompt judgment call, not a toolset strip. // Codex natives (apply_patch / shell / update_plan) are not advertised. @@ -51,7 +51,7 @@ export const CORE_TOOL_NAMES: readonly string[] = [ // tool_search round-trip. // Fleet verbs (non-blocking spawn + lifecycle). Mounted on primary when // subAgent is wired; advertised here so the model does not tool_search for - // them. Package allowlists (ORCHESTRATOR_TOOLS / SKYWALKER_TOOLS) are a + // them. Package allowlists (ORCHESTRATOR_TOOLS / DISPATCH_TOOLS) are a // separate, deferred change. "spawn_agent", "wait_agents", diff --git a/src/agent/tools.test.ts b/src/agent/tools.test.ts index f61c99628..14db22cb4 100644 --- a/src/agent/tools.test.ts +++ b/src/agent/tools.test.ts @@ -244,7 +244,7 @@ test("dynamicRunner contains posix tool names plus ask_operator", async () => { const names = toolset.dynamicRunner.currentDefinitions().map((d) => d.name); expect(names).toContain("read_file"); expect(names).toContain("ask_operator"); - // Primary Skywalker mounts product mutation tools for DIY tiny/bounded edits. + // Primary dispatch mounts product mutation tools for DIY tiny/bounded edits. expect(names).toContain("write_file"); expect(names).toContain("edit_file"); expect(names).toContain("delete_file"); diff --git a/src/agent/use-skill.ts b/src/agent/use-skill.ts index 9d7951e3a..edaac0892 100644 --- a/src/agent/use-skill.ts +++ b/src/agent/use-skill.ts @@ -11,7 +11,7 @@ import { captureSkillUsed } from "../telemetry/product-events.js"; // skill_search; this tool pulls the full instructions into context when the // model decides one applies. There is no operator invocation — discovery and // loading are entirely model-driven. Primary copy is on-demand catalog -// (Skywalker has no attached skills). Workers mount workerUseSkillDefinition +// (dispatch has no attached skills). Workers mount workerUseSkillDefinition // so they do not reload bodies already injected as attached. The handler // refuses attached names and names already loaded this session so the body // is never dumped twice. diff --git a/src/config/index.ts b/src/config/index.ts index 3444b9bf4..738d11ccc 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -607,7 +607,7 @@ export interface Config { skipPermissionsFromSettings: boolean; auto: boolean; /** - * Exec-only chosen primary director. Omitted = Skywalker (product default). + * Exec-only chosen primary director. Omitted = dispatch (product default). * `--director` is rejected in TUI mode. */ director?: DirectorId; diff --git a/src/subagent/index.test.ts b/src/subagent/index.test.ts index 675219466..c7e2db57f 100644 --- a/src/subagent/index.test.ts +++ b/src/subagent/index.test.ts @@ -187,7 +187,7 @@ describe("sub-agent stop helpers", () => { const SUMMARY_ONLY_NARRATION = [ "## Summary", - "Checking whether Skywalker write-tool unmount is tested...", + "Checking whether dispatch write-tool unmount is tested...", "Checking those next.", ].join("\n"); diff --git a/src/subagent/mailbox-mail-drive.ts b/src/subagent/mailbox-mail-drive.ts index 2c9af6215..7efa4cc49 100644 --- a/src/subagent/mailbox-mail-drive.ts +++ b/src/subagent/mailbox-mail-drive.ts @@ -1,6 +1,6 @@ /** * Drive the parent back into a turn when uncollected mailbox terminals exist - * while Skywalker is idle. Pure: occupancy decides when to call; this module + * while dispatch is idle. Pure: occupancy decides when to call; this module * decides whether to drive and what to send. Sibling of fleet-dry-drive — * per-item, not last-lane + open-tasks. */ diff --git a/src/subagent/nudge-director.test.ts b/src/subagent/nudge-director.test.ts index dd10960c2..b260396a7 100644 --- a/src/subagent/nudge-director.test.ts +++ b/src/subagent/nudge-director.test.ts @@ -808,7 +808,7 @@ describe("SubAgentDirector incomplete-report wiring", () => { inferenceDoneText( [ "## Summary", - "Checking whether Skywalker write-tool unmount is tested...", + "Checking whether dispatch write-tool unmount is tested...", "Checking those next.", ].join("\n"), ),