# Phase 4 — Write task.toml, spawn via kei-spawn, emit Agent-tool invocation > Goal: serialize `ROLE / TASK_FULL / WHITELIST / DENYLIST` into a > `task.toml`, call `kei-spawn spawn --format=json`, parse the JSON, and > emit a ready-to-paste Agent-tool invocation. One confirm click before > the spawn call (it writes a ledger row). > **Verify criterion:** `AGENT_ID`, `DNA`, `SUBAGENT_TYPE`, `ISOLATION` > all set; Agent-tool invocation printed. --- ## 4.a — Compute `TASK_TOML` path Generate a short UUID (v4, first 8 hex chars is sufficient — kei-spawn validates uniqueness on write). Path: ``` tasks/.toml ``` Relative to the current repo root. If `tasks/` does not exist, do NOT create it from this skill — `kei-spawn spawn` will fail loudly and the constructive path is to run `mkdir tasks && git add tasks/.gitkeep` in the orchestrator session. --- ## 4.b — Render task.toml Use this exact schema (verify against `_primitives/_rust/kei-spawn/src/task.rs` before any drift; if the CLI schema changes, this phase file updates too): ```toml # Generated by /spawn-agent skill — do not hand-edit. # kei-spawn consumes this file; the spawn ceremony writes a ledger row # (RULE 0.12) and returns agent_id + dna in JSON. role = "" task = """ """ [scope] whitelist = [ "", "", # ... ] denylist = [ # empty if Auto; populated if Explicit; absent if Override (see 4.c) ] ``` TOML rules: - Use triple-quoted `"""..."""` for `task` so newlines in TASK_FULL survive. - Escape any triple-quote inside TASK_FULL as `\"\"\"` (unlikely but validate before write). - Every whitelist entry is a TOML string (double-quoted, 4-space indented, trailing comma). - If `DENYLIST_OVERRIDE = true` from Phase 3.d, OMIT the `denylist` key entirely — `kei-spawn` distinguishes "empty list" (union with defaults) from "absent" (no defaults applied) via the `--no-default-deny` flag passed in 4.d. Write the file via the `Write` tool. --- ## 4.c — Confirm-emit click Send ONE `AskUserQuestion` before calling `kei-spawn`: ```json { "questions": [ { "question": "Spawn agent with this configuration?", "header": "Confirm", "multiSelect": false, "options": [ { "label": "Yes — spawn now", "description": "Writes ledger row, returns agent_id + DNA + subagent_type + isolation. The Agent-tool invocation will be emitted for you to paste." }, { "label": "No — show me the task.toml first", "description": "Print the generated tasks/.toml contents verbatim, then re-ask." }, { "label": "Abort", "description": "Delete the task.toml and exit the skill." } ] } ] } ``` Resolve: - **Yes** → proceed to 4.d. - **No** → Read the file back and print it in a fenced code block; loop the same AskUserQuestion. - **Abort** → delete `tasks/.toml` and emit a short summary: no spawn, no ledger row, no Agent invocation. --- ## 4.d — Run kei-spawn Call exactly one command (no chaining, so errors surface cleanly): ```bash kei-spawn spawn tasks/.toml --format=json ``` If `DENYLIST_OVERRIDE = true`, append `--no-default-deny`. If `kei-spawn` is not on PATH, fall back to `"$KEI_RUNTIME_BIN_DIR/kei-spawn"`. If both fail, surface the SKILL.md runtime-resolution paths (A/B/C) and STOP — do NOT fabricate a response. Parse the stdout JSON. Expected shape (exact field names per `_primitives/_rust/kei-spawn/src/spawn.rs`): ```json { "agent_id": "6a48ca7b-...", "dna": "sha256:b37e2d1e9a...", "subagent_type": "code-implementer", "isolation": "worktree", "worktree_path": ".claude/worktrees/agent-6a48ca7b", "branch": "agent/-" } ``` Store each field as named (`AGENT_ID`, `DNA`, `SUBAGENT_TYPE`, `ISOLATION`, `WORKTREE_PATH`, `BRANCH`). If `isolation == "shared"`, `worktree_path` and `branch` may be absent — handle that gracefully. On non-zero exit from `kei-spawn`: print stderr verbatim, surface the error, offer three constructive paths: - (A) Fix the task.toml and re-spawn (show the diff first). - (B) Fall back to a smaller scope (loop back to Phase 3). - (C) Abort — delete task.toml, no ledger row written. --- ## 4.e — Emit the Agent-tool invocation Print a single fenced code block with the ready-to-paste JSON. Use the Agent tool's exact parameter names: ```json { "subagent_type": "", "description": "", "prompt": "", "isolation": "" } ``` Plus a short annotation under the block: ``` Agent ID: DNA: Branch: Worktree: Paste the JSON above into an Agent tool call. On return, run: kei-spawn verify to check the 6-file artefact bundle (RULE 0.12) and merge-readiness. ``` --- ## 4.f — Final report Emit the SKILL.md-defined `=== /SPAWN-AGENT REPORT ===` block. Populate every field from the variables above. Do NOT invent any field value — if something is absent (e.g. worktree for shared isolation), print `n/a`. --- ## 4.g — Verify criterion - `AGENT_ID` non-empty UUID - `DNA` non-empty sha256 prefix - `SUBAGENT_TYPE ∈ {researcher, code-implementer, ...}` (whatever kei-spawn returns — the skill does not enforce a specific set) - `ISOLATION ∈ {shared, worktree}` - The Agent-tool invocation JSON is printed in a fenced block - `TASK_FULL` in the `prompt` field contains the literal phrase `MUST NOT invoke git` (RULE 0.13 ban-phrase — added in Phase 2.c) If the ban-phrase check fails (should be impossible after Phase 2.c), STOP and surface a bug report — do NOT emit the invocation. --- ## 4.h — Failure paths (NO DOWNGRADE) Covered inline in 4.d. Additionally, if the user tries to paste the emitted JSON but the Agent tool rejects it: - (A) Likely `subagent_type` mismatch — run `kei-spawn roles` to list valid subagent_type values in the current runtime and reconcile. - (B) Likely `isolation` unsupported in the caller's Claude Code version — fall back to `shared` and loop back to Phase 1 to re-pick a read-only / explorer role. - (C) Escalate to `/new-agent` if the user actually needs a new agent manifest rather than a new instance.