Spawning a coding agent is one line of shell. Spawning one that knows what to do is the whole job.
You can shell out to claude. Or codex. Or gemini. The process starts in whatever directory the calling shell happens to be in. It signs commits with whatever git config it inherits. It reads whatever CLAUDE.md is closest on disk. It picks an issue to work on by — well, no one told it which one, so it picks. Sometimes the same one twice. Sometimes one that doesn't exist anymore. Sometimes it edits .github/workflows/ because no one told it not to.
By the time the envelope is sealed, the issue inside it has already been through a pipeline that operates on the BEADS graph — not on source files. None of these earlier plays ship a PR. Skip them, and the implementer ends up burning tokens guessing at scope on an ambient-from-GitHub issue with no acceptance criteria, no parent epic, and no dependency edges.
When the RL Agent picks a play and the parameter resolver picks the LLM Agent and the issue, this is the shape of what gets pushed across the subprocess boundary. Nothing here is typed by hand at dispatch time. Each piece has a source the rest of the system is already maintaining.
The biggest piece in the envelope — the rendered SKILL.md — is natural language. But the LLM Agent also reads a structured JSON snapshot of the world AgentShore thinks it's stepping into. The path is injected via --append-system-prompt "Context file: …", and the rendered prompt instructs the LLM Agent to open it first. The file is written through a temp-file + os.replace(), so a half-written payload can never be observed by a racing reader.
// written atomically before subprocess spawn · session-scoped path · per-play file { "schema_version": 1, "session_id": "s-2026-05-21-a3f7", "mode": "agent", // solo · agent "current_play": "code_review", "skill_name": "agentshore-code-review", "play_id": 218, // int; per-play file is play-218.json "assigned_github_identity": "reviewer-bot", "target_branch": "review/gh-112", "context_file": ".agentshore/contexts/s-…/play-218.json", "params": { // resolved by the param resolver, not by the model "agent_id": "claude-reviewer-2", "issue_number": 112, "pr_number": 112, "branch": "feat/dispatch-onepager", "num_commits": 4, "url": "https://github.com/swink/agentshore/pull/112" }, "open_issues": [ // snapshot · so the model can sequence its work { "issue_number": 113, "title": "…", "state": "open", "priority": "P2" }, … 18 more ], "pull_requests": [ { "pr_number": 112, "github_author": "implementer-bot", // ← differs from assigned_github_identity "head_sha": "a3f9d1e…", "mergeable": true, "review_decision": null, "last_reviewed_sha": null, "status_check_summary": "ci ✓ · ruff ✓ · mypy ✓" } ], "budget": { // session-scoped, not global "enabled": true, "total": 25.00, "spent": 17.83, "remaining": 7.17 }, "learnings_count": 3, "learnings": [ // pulled from session_learnings + agent_handoffs { "play": "code_review", "note": "watch for CI-workflow edits" }, … 2 more ], "project_path": "/projects/example-app" }
The trick isn't building any one of these pieces. It's keeping their sources separated so each one can be authoritative for its own slice. BEADS owns the task graph. GitHub owns the human conversation. agentshore.yaml owns identity. AgentShore's SQLite owns the RL state and the per-session memory.
Every play ends with a post-hoc issue-inflation check via validate_scope(), a reward computed against the BEADS alignment delta, and a row appended to dispatch_replay so the dispatch can be reconstructed later. The same machinery that built the envelope reads it back — which is the only reason the next dispatch knows anything about this one.
The architecture is mostly about not letting one LLM Agent's ambient state become the next LLM Agent's bug. Naming the working directory. Naming the identity. Naming the issue. Naming the scope. Writing it all down so the next loop iteration can read it back.