Files
reliquary/plugins/spire/skills/implementation/references/spawning-claude-code.md
T

135 lines
19 KiB
Markdown

# Spawning agents in Claude Code
Harness mechanics for dispatching, resuming and forking agents in Claude Code, and for the headless sessions (`claude -p`) that `spire:gremlins` drives. Other harnesses get their own `spawning-<harness>.md` when needed.
Each claim carries a status and a date:
- **verified**: read from the tool schema or the Claude Code docs, or reproduced in a local test, on that date; the row names which. Not tested at runtime unless the row says so.
- **observed**: seen in real runs; the mechanism may be assumed. Source given.
- **assumed**: inferred, not checked. Treat as a hypothesis.
- **unknown**: open; check before relying on it.
The harness changes. When a claim fails, fix it here with a new date.
## Rules of thumb
- **Foreground for sequential work.** Pass `run_in_background: false` when the next step depends on the result. Background only for independent leaves whose result can go to the session root.
- **Resume only from the session root.** Igor may `SendMessage` a finished agent (the Athanor between phases, an adept). A subagent should not resume its own children; it dispatches fresh foreground agents instead.
- **Pinned agents: omit `model`.** The per-call alias overrides the agent's frontmatter pin. Pass `model` to a pinned agent only when you mean to override it, and say so in the journal or run log.
- **Unpinned agents: pass `model`.** Otherwise they inherit the caller's model, which may be Fable.
- **Stamp the model id.** Every agent reads its exact id from its system prompt and writes it into its output.
- **Complex blocks go to gremlins via `spire:gremlins`; never resume or fork a headless session by hand.** The helper owns the flags, the cache bookkeeping and the usage ledger (see "Headless sessions" below).
- **Leaf briefs say "do this yourself; do not dispatch further agents."** Only orchestrators (the Athanor) are meant to fan out.
- **Notifications report a turn ending, not a job finishing.** Read the result text before concluding anything. Check `ListAgents` before resuming.
## Dispatch and notification
| Claim | Status |
|---|---|
| `Agent` calls run in the background by default; `run_in_background: false` runs them in the foreground and returns the result to the caller. | verified 2026-10-05 (Agent tool schema) |
| Background children of a subagent notify the **session root**, not their parent. The parent sees nothing and stalls until the root feeds it the result. | observed 2026-09-24 and 2026-09-28 (spacecraft journals, "athanor stalled twice … nudged with results"); dirigent memory, settled 2026-09-29. Mechanism assumed. |
| An agent that hands work to a background child and then yields notifies `completed` at once, before any deliverable exists. Expect one more notification per background grandchild, each with its own `task-id`. | observed 2026-09-22 (Spire harness notes) |
| The same `task-id` may notify more than once (one per turn). Track the deliverable, not the count. | observed 2026-09-22 |
| Background grandchildren show up in `ListAgents` as flat, top-level agents of the session. If one is still writing your output path, do not resume its parent: that starts a second writer. `TaskStop` the child first if you mean to abandon it. | observed 2026-09-22 |
| `TaskList` is the todo list, unrelated to agents. `TaskOutput` on an agent dumps its whole transcript into your context; do not use it for agents. | observed 2026-09-22 |
| A `general-purpose` agent has the Agent tool and will delegate if its brief reads like an orchestration brief. | observed 2026-09-22 |
## Resume
| Claim | Status |
|---|---|
| `SendMessage` to a finished agent's name or id resumes it with its context intact. A new `Agent` call starts fresh. | verified 2026-10-05 (Agent tool schema) |
| A resume issued from inside a subagent runs in the background; its completion goes to the root and the subagent stalls. From the root (Igor) it is safe, because the root is where the notification lands. | observed once 2026-10-05 (spacecraft journal: orchestrator resumed its implementer via SendMessage, went background, yielded). Mechanism assumed from the routing above. |
## Models
| Claim | Status |
|---|---|
| The per-call `model` parameter accepts aliases only (`opus`, `sonnet`, `haiku`, `fable`) and takes precedence over the agent definition's `model` frontmatter. | verified 2026-10-05 (Agent tool schema) |
| With `model` omitted, a pinned agent runs on its pin; an unpinned agent inherits the caller's model unless a default subagent model is configured. | verified 2026-10-05 (Agent tool schema) |
| A full model id (e.g. `claude-opus-5-5`) can only be set in the agent definition's frontmatter, so pinning by full id means a pinned agent dispatched without `model`. | verified 2026-10-05 (follows from the alias-only parameter) |
| The `opus` alias overrode 4.6 pins in 184 of 184 spawns (they ran on 5.5). | observed 2026-09-27 (spacecraft MVP retrospective) |
| A 4.6 Igor passing `model: "opus"` to the then 4.6-pinned Athanor got a 4.6 run (the dirigent end review that passed with zero findings). | observed 2026-10-05 (user's attribution; the dirigent report recorded the model as unknown). Consistent with both the alias following the caller and the pin holding, so it is not evidence for alias-over-pin. The 184/184 row and the schema are. |
| What the `opus` alias resolves to (the caller's model, a session default, or the newest Opus). | unknown. Check the stamped id of every plan, attack and review instead of relying on it. |
| Every agent sees its exact model id in its system prompt. | observed 2026-10-05 for general-purpose agents (prompt shows `claude-opus-5-5`); not checked for plugin agents with their own bodies |
| Headless `--model` takes full ids (and the `[1m]` suffix), so gremlins pin by full id; the per-call alias limit above is an Agent-tool limit. | observed 2026-10-16 (preflight report, P0.5) |
| Context window of the bare `claude-opus-5-5` pin versus a `[1m]` suffix (other Spire pins carry `[1m]`). | resolved 2026-10-05: all Opus-pinned Spire agents (igor, adept, athanor, zathras) pinned to `claude-opus-5-5[1m]` (user decision; Pinky, when it exists, stays 4.6); the ~600k guesstimate in `models.md` assumes the larger window |
## Fork (Agent tool)
Spire no longer uses Agent-tool forks (goal decision 2026-10-16): complex blocks go to gremlins, headless forks of a trained base (see "Headless sessions" below). The rows stay as a record of what the Agent tool offers.
| Claim | Status |
|---|---|
| The Agent tool schema documents `subagent_type: "fork"`: a fork always inherits the caller's model; the `model` parameter is ignored. | verified 2026-10-05 (schema text) |
| `fork` was not offered as an agent type on 2026-10-05 or on 2026-10-15. | observed 2026-10-15 (agent-type lists of those sessions; design 2026-10-16-01 §9) |
| A fork inherits the current conversation only; no parameter names another agent to fork. | verified 2026-10-15 (docs, sub-agents) |
| A fork has the same system prompt, tools, model and history as the forking session. | verified 2026-10-15 (docs, sub-agents) |
| An Agent-tool fork cannot fork again: "A fork can't spawn further forks." A headless `--fork-session` of a fork works. | verified 2026-10-15 (docs, sub-agents) for the Agent tool; verified 2026-10-16 (research `2d`, local test T6) for headless |
| Fork mode is on by default in interactive sessions (v2.1.232 and later), off under `-p` and the SDK unless `CLAUDE_CODE_FORK_SUBAGENT=1`. With it on, all subagents run in the background and `run_in_background` is removed. | verified 2026-10-15 (docs, sub-agents) |
| Agent-tool forks and subagents use the 5 min cache bucket by default; the main conversation (including `-p` runs) gets 1 h on a Claude subscription within plan usage. | verified 2026-10-16 (research `F4a`; docs, prompt caching) |
| Whether a foreground Agent-tool fork returns its result to the caller, and whether it sees the caller's preloaded skills. | unknown 2026-10-16 (never tested; Spire does not use Agent-tool forks) |
## Headless sessions (`claude -p`)
`spire:gremlins` (command `spawn`) is the only supported way to start, resume or fork one; these rows say why its command line is what it is. Tested on Windows with `claude` 2.1.283 and 2.1.299. Research ids (`1a` to `F5b`) refer to `docs/workpad/research/2026-10-16-00-claude-headless-fork-sessions.md`; `P0.x` ids to `docs/workpad/reports/2026-10-16-09-headless-forks-preflight.md`.
| Claim | Status |
|---|---|
| `--output-format json` prints one result object: `session_id`, `result`, `usage` (with the cache fields), `modelUsage`, `total_cost_usd`, `permission_denials`, `subagent_stats`. With `stream-json --verbose` the first event is `system/init` with `session_id`, `model`, `tools`, `agents`, `skills`, `plugins`, `permissionMode`. | verified 2026-10-16 (research `1a`; docs, headless; local tests T1, T6) |
| `--resume <id>` finds the session by id in any project on the machine; without `--fork-session` it appends to the same id. | verified 2026-10-16 (research `1b`; docs; local test T5) |
| `--session-id <uuid>` lets the caller choose the id up front, so no output parsing is needed. | verified 2026-10-16 (research `1d`; docs, `--help`) |
| `--continue` means the most recent conversation in the cwd, `-p` sessions included. Do not use it for fan-out; pass explicit ids. | verified 2026-10-16 (research `1c`; docs) |
| `--fork-session` gives the run a new session id and a copy of the base history. | verified 2026-10-16 (research `2a`; `--help` 2.1.283; docs) |
| The base stays unchanged on disk and in later resumes; forks do not see each other. | verified 2026-10-16 (research `2b`; docs, sessions; local tests T2 to T5) |
| Parallel forks of one base work and each gets its own id and transcript. Resuming the same id in two processes without forking interleaves both into one transcript. | verified 2026-10-16 (research `2c`; local tests T2 and T3 in parallel; docs) |
| A fork of a fork works. | verified 2026-10-16 (research `2d`; local test T6) |
| Forks hit the cache on the shared prefix (T2 and T3 each read 14,650 tokens against the base's 8,661 created). | verified 2026-10-16 (research `3a`; local test) |
| A cache hit needs an identical tool set, model and system prompt. A fork with different flags read 0 and re-created 17,474; one that changed only the model read 4,293 and created 7,213. | verified 2026-10-16 (research `3b`; local tests T6, T7) |
| A fork does not inherit the base's permission mode, `--allowedTools` or `--tools` (T6 came up in `default` with all 43 tools). Pass them again, identically, on every fork. | verified 2026-10-16 (research `4b`; local test T6; docs, sessions) |
| Also not restored on resume: `--mcp-config`, `--settings`, `--plugin-dir`, `--fallback-model`, `--add-dir`. The system prompt (including `--append-system-prompt`) is recorded at the first request and reused on resume and fork until compaction; later flag text is ignored (`--system-prompt-snapshot`, `--help` 2.1.283). | verified 2026-10-16 (research `4c`; docs, sessions and CLI reference) |
| The model is restored on resume; `--model` on the fork overrides it (a haiku fork of a sonnet base ran on haiku). | verified 2026-10-16 (research `4d`; docs; local test T7) |
| `total_cost_usd` and `modelUsage` on a resumed or forked run are cumulative for the conversation, base spend included (v2.1.277 and later). Per-call cost is a difference against the base's total; never sum results. `--max-budget-usd` counts only the call's own spend. | verified 2026-10-16 (research `3d`, `F4e`; docs, headless, cost tracking) |
| The 1 h cache TTL is the default only for the main-conversation bucket (interactive turns and `-p` runs) on a Claude subscription within plan usage; usage credits, API key and cloud providers default to 5 min, as does the main conversation over the plan limit. | verified 2026-10-16 (research `F4a`, `3c`; docs, prompt caching; local `ephemeral_1h` writes in T1 to T7) |
| The TTL is configurable: `CLAUDE_CODE_PROMPT_CACHE_TTL` or `promptCacheTtl` (main bucket), `CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL` or `subagentPromptCacheTtl` (everything else), each `5m` or `1h`, v2.1.242 and later. `ENABLE_PROMPT_CACHING_1H=1` sets both; `FORCE_PROMPT_CACHING_5M=1` overrides everything. | verified 2026-10-16 (research `F4b`; docs, prompt caching) |
| A cache hit resets the TTL timer, so forks spaced under the TTL keep the base prefix warm. Parallel sessions in the same directory share a prefix; a different cwd or git-status snapshot changes it, so run every fork from the base's cwd. | verified 2026-10-16 (research `F4c`, `F4d`; docs, prompt caching) |
| Under `-p` nothing can prompt: without a host or `--permission-prompt-tool`, anything that would prompt is denied, listed in `permission_denials`, and the run continues. `dontAsk` denies anything not pre-allowed. | verified 2026-10-16 (research `4a`; docs, headless and permission modes) |
| `system/init.model` is not the model that ran: read `modelUsage` or `assistant.message.model`. | verified 2026-10-16 (research `5d`, `F2a`, `F2c`; local tests M1, M2) |
| `--permission-mode plan` upgrades a Haiku session to Sonnet. The docs imply it (availableModels section) and do not state it as a rule. | observed 2026-10-16 (research `5d`, `F2a`, `F2b`; local tests T1 to T5, M1, M2; docs only indirect) |
| Aliases resolve by CLI version: `haiku` is Haiku 4.5 on 2.1.283 and Haiku 5.5 on 2.1.299 (Haiku 5.5 needs v2.1.293 or later, Sonnet 5.5 v2.1.284 or later). | verified 2026-10-16 (research `F2c`; local tests; docs, model config) |
| Without `--bare`, `-p` loads installed plugins, skills, agents, hooks, MCP and CLAUDE.md like an interactive session. | verified 2026-10-16 (research `5a`; docs; local test T6) |
| `--bare` skips all of that discovery and refuses OAuth (`Not logged in`); it needs `ANTHROPIC_API_KEY`. | verified 2026-10-16 (research `5b`, `F3d`; docs; local test E2) |
| `--model` (alias or full id), `--agents`, `--agent`, `--append-system-prompt[-file]`, `--settings`, `--max-budget-usd`, `--max-turns` and `--json-schema` exist in 2.1.283. | verified 2026-10-16 (research `5c`; `--help`; docs) |
| A nested `claude -p` inherits `CLAUDECODE=1` and `CLAUDE_CODE_CHILD_SESSION=1`; no recursion guard blocks it, and the session is still persisted. | verified 2026-10-16 (research `6a`; docs, env vars; local env listing) |
| Agent depth is counted per process from its main conversation; the parent's depth is not passed down. A nested `claude -p` can use the Agent tool, and a second level nests too. At the depth limit the Agent tool is simply absent: no error. | verified 2026-10-16 (research `F1a` to `F1d`; local tests D1 to D3; docs, env vars) |
| A nested process does not count against the parent's Agent depth or concurrency caps. | assumed 2026-10-16 (research `6b`; inferred from per-process `subagent_stats`, not tested with a deep parent) |
| `--bg` does not combine with `-p`. | verified 2026-10-16 (research `6c`; docs, headless) |
| A headless run is invisible to the parent's task notifications, `ListAgents` and `TaskStop`: the orchestrator must block on Bash or poll. | assumed 2026-10-16 (research `6c`; not a tracked Agent, never tested from a subagent) |
| Transcripts land in `~/.claude/projects/<cwd with every non-alphanumeric character as '-'>/<session-id>.jsonl`, one file per fork, each holding the copied base history under its own `sessionId`; 30-day retention. | verified 2026-10-16 (research `6d`; docs, sessions; local listing) |
| The nested process inherits the Desktop host's env: `CLAUDE_CODE_MESSAGING_SOCKET`, `CLAUDE_CODE_ENTRYPOINT=claude-desktop` and the OAuth-refresh variables, among others. Removing twelve of those vars (`F3a`) with `env -u` drops the Artifact tools (35 to 32 built-ins); auth still works. | observed 2026-10-16 (research `6e` as corrected by `F3a`, `F3b`; local tests T6, E1) |
| The `messaging_socket_path` in `system/init` is the nested process's own pipe, different on every run and never the host's. This corrects the first reading of `6e`. | verified 2026-10-16 (research `F3c`; local tests T6, E1) |
| Whether the nested process's pipe can reach the host app, and which single env var gates the Desktop Artifact tools. | unknown 2026-10-16 (research `F3c`; not isolated) |
| The PATH `claude` (2.1.283, native installer) and the Desktop host's bundled `claude.exe` (2.1.299, reachable as `$CLAUDE_CODE_EXECPATH`) are separate installs. | verified 2026-10-16 (research `F5a`; local) |
| A fork made by another binary or on another model misses the base's cache: a 2.1.299 fork of a 2.1.283 base read 0 and created 15,020. Pin one binary and a full model id for the base and all its forks. | verified 2026-10-16 (research `F5b`; local test V1; docs, prompt caching) |
Observed in the Phase 0 preflight on this Windows host (2026-10-16):
| Claim | Status |
|---|---|
| `uv run` adds `VIRTUAL_ENV` and a venv `Scripts` entry at the head of PATH, and sets no `UV_*` vars; stdout stays clean (one JSON line). | observed 2026-10-16 (preflight report, P0.0) |
| No job objects on this host (`IsProcessInJob` false for the `uv run` launcher and every supervisor). | observed 2026-10-16 (preflight report, P0.1) |
| The Bash tool's `TaskStop` kills by process tree (parent links). A supervisor created through CIM `Win32_Process.Create` (parent `WmiPrvSE.exe`) survived the stopped Bash call; rungs that start it as a child of the launcher died with it. | observed 2026-10-16 (preflight report, P0.1) |
| A supervisor surviving the end of the agent's turn (the hand-back) was not tested in Phase 0; the parentage under `WmiPrvSE.exe` makes it expected. | unknown 2026-10-16 (preflight report, "Not tested / open"; Phase 4 smoke) |
| An unhidden CIM spawn opens a visible Windows Terminal window. The fix is `Win32_ProcessStartup.ShowWindow=0` plus `CREATE_NO_WINDOW` on every child; no window was seen in 26 checks, including a `claude` to `bash` to `PING` tree. | observed 2026-10-16 (preflight report, P0.1) |
| A CIM-created process gets the registry default user env, not the caller's (58 of the Bash env's variables missing, among them every `CLAUDE*` var and `CLAUDE_CODE_EXECPATH`), so the launcher must pass the full scrubbed env to the supervisor itself. | observed 2026-10-16 (preflight report, P0.1) |
| The Bash tool backgrounds a call that passes its `timeout`; it does not kill it. | observed 2026-10-16 (preflight report, P0.1(d)) |
| An unlisted model (not admitted by `availableModels`) is substituted silently: the run uses a permitted model and emits a `system/informational` warning ("Model … is restricted by your organization's settings. Using … instead."). | observed 2026-10-16 (preflight report, P0.5) |
| An `!` injection runs in an Agent-tool subagent's skill preload, but the skill's `allowed-tools` did not pre-approve the subagent's later `uv run` call (denied: "This command requires approval"). The probe dir was an untrusted workspace; Phase 4 re-checks in a trusted project. | observed 2026-10-16 (preflight report, P0.8) |
## Agent types
| Claim | Status |
|---|---|
| Plugin agent types (`athanor`, `adept`, `researcher`, …) can be missing in remote sessions, where `/reload-plugins` is not available. Fallback: `general-purpose` with an explicit `model`, and the agent file's body pasted into the brief. | observed 2026-10-05 (reliquary Igor journal, 21:00) |
| `Explore` and `Plan` are read-only (no Edit/Write). Do not use them for tasks that must write a file. | observed 2026-09-22 (Spire model tiers) |