| name | dispatch-opencode |
| description | Dispatch subagent tasks through opencode (https://opencode.ai) using the async .subagents/ lock-watch protocol. The agent writes a plan YAML, calls run-plan.sh to validate and dispatch, polls lockfiles on its own interval, reads FINAL_OUTPUT.md on completion, and calls subagent-cleanup.sh or subagent-abandon.sh to tear down. Use when you want to hand work off to a background subagent instead of doing it yourself — for example, dispatching a subagent into a worktree, parallelizing independent tasks, or running long-lived investigations. TRIGGER when: the agent would otherwise block on a subagent that could run in the background, or when worktree-isolated subagent dispatch is needed.
|
| license | MIT |
| compatibility | Requires opencode CLI (https://opencode.ai), git, and a running `opencode serve` daemon for --attach mode (optional, falls back to local). |
| metadata | {"version":"2.0.0","status":"active","author":"cristoslc","spokes":[{"path":"references/troubleshooting.md","trigger":"session not found, blank FINAL_OUTPUT.md, dispatch skipped, timeout, stuck, exit code 3, exit code 2, branch not found, orphaned symlink, worktree symlink, prompt file resolves"}]} |
dispatch-opencode
Routes subagent dispatch through opencode using async file-based signaling.
The skill does NOT implement ACP (Agent Client Protocol) or HTTP serve mode —
those were dropped per ADR-001. Instead, it uses CLI opencode run as the
invocation transport, wrapped in a .subagents/ lock-watch protocol.
For previous versions of this skill (ACP/CLI/HTTP), see ADR-001.
When to use
Primary pattern: two-call dispatch
Use the two-call pattern when the agent is actively running and can poll:
- The task takes long enough that the agent should not block (refactors,
dependency upgrades, research sprints).
- The agent wants to parallelize N independent tasks with per-task worktree
isolation.
- The agent wants per-task model selection (e.g., cheap model for fixes,
capable model for design).
- The operator wants to attach to a running subagent from another terminal.
- The agent wants the dispatch artifact (prompt, event log, lock file) on
disk for replay or audit.
- You are the orchestrator in a sashay: the branch, worktree, and draft PR
already exist on the forge. Use this skill to dispatch a subagent into
the worktree with sashay chronicle instructions in the prompt. The
subagent will commit, push, and post PR comments throughout its work.
Secondary pattern: background dispatch (watcher daemon)
Use the watcher daemon when the agent will not be around to poll:
- The agent wants to fire-and-forget a long-running task and continue
working.
- The operator wants to background work from a terminal session and close
it, letting the watcher daemon handle the lifecycle.
When NOT to use
- The task is trivial and finishes in seconds — the lock-watch overhead
exceeds any benefit. Use
opencode run directly.
- The host runtime forbids spawning background subprocesses.
Four design constraints (non-negotiable)
-
Every dispatch takes an explicit absolute path; verification fails
closed. No defaults, no inference. If the path is wrong, the
script exits non-zero before anything runs.
-
Every handoff is an on-disk artifact. Every dispatch writes the
prompt, start script, event log, and lock file under
.subagents/<task-id>/. The task directory is the source of truth for
replay and audit.
-
One template per dispatch kind. Templates are typed by what the
dispatch is for (e.g., single-file-fix, headless-spike), not
parameterized into a single megatemplate.
-
Smart orchestrator, dumb subagents. The agent decides what is
ready and fires only ready tasks. The skill dispatches what it is
given. Subagents are single-responsibility and should work on the
cheapest model that can do the job. The agent continues running,
polls, and kicks off the next wave. Subagents never coordinate with
each other or trigger subsequent work.
Agent workflows
Workflow 1: Single task (two-call pattern)
This is the primary dispatch pattern. The agent calls run-plan.sh to
dispatch, then calls poll-subagent.sh later to check in. Between the
two calls, the agent can do other work.
Call 1 — dispatch (returns immediately):
run-plan.sh --plan plan.yaml
Returns JSON with lockfile path and PID. The subagent starts in the
background. The agent is free to do other work — read files, write the
next plan, investigate results from a previous task.
Call 2 — poll (called later):
poll-subagent.sh --task-id <id> --root <path>
Polls the lockfile and events log. Logs event line count each iteration,
detects stuck tasks (exit 2), and times out (exit 3).
After poll completes:
- Exit 0 (completed): read
FINAL_OUTPUT.md, merge work, call
subagent-cleanup.sh.
- Exit 2/3 (stuck/timeout): call
subagent-abandon.sh.
Workflow 2: Parallelize N tasks (dispatch-all-then-poll)
Dispatch all N tasks first, then poll each one. This lets N subagents
run concurrently while the agent does other work between polls.
- Write an N-task plan YAML (only tasks whose dependencies are
satisfied).
- Call
run-plan.sh --plan plan.yaml once — returns immediately with
N lockfile paths and PIDs.
- For each task, call
poll-subagent.sh --task-id <id> --root <path>
when ready to check in. The agent can do other work between polls.
- Per completed task: read
FINAL_OUTPUT.md, merge work, call
subagent-cleanup.sh.
- Per failed or stuck task: call
subagent-abandon.sh.
Example — dispatch 3 tasks in parallel, then check in on each:
# Phase 1: dispatch all three
run-plan.sh --plan plan.yaml
# → returns 3 lockfile paths, 3 PIDs
# Phase 2: do other work (read files, write prompts, etc.)
# Phase 3: check in on each task
poll-subagent.sh --task-id task-a --root /path
poll-subagent.sh --task-id task-b --root /path
poll-subagent.sh --task-id task-c --root /path
Workflow 3: Stale resource recovery
Call cleanup-stale.sh [--abandon] after a crash or long idle period.
- Without
--abandon: reports stale locks and orphaned worktrees.
- With
--abandon: calls subagent-abandon.sh for each.
Workflow 4: Background dispatch (fire-and-forget) — optional secondary pattern
Use this when the agent will not be around to poll. Requires the watcher
daemon to be running.
- Ensure the watcher daemon is running (
watcher.sh status).
- Write a plan YAML with
mode: background (and optionally ttl_sec).
- Write the plan to
.subagents/watch/<plan-name>.yaml.
- Continue working — the watcher picks it up, dispatches, polls, and
cleans up.
- Check results later at
.subagents/watch/results/<task-id>.md.
Script inventory
| Script | Agent-facing | Purpose |
|---|
run-plan.sh | yes | Validate plan, prepare worktrees, dispatch, return lockfile list as JSON |
poll-subagent.sh | yes | Poll subagent lockfile until completion, stuck, or timeout |
subagent-cleanup.sh | yes | Remove completed task's artifacts + worktree |
subagent-abandon.sh | yes | Kill PID, force-remove failed task + worktree |
cleanup-stale.sh | yes | Scan for stale locks and orphaned worktrees |
dispatch.sh | no (internal) | Single-task prepare, spawn, confirm .lock appeared |
verify-cwd.sh | no (internal) | Fail-closed CWD verification |
validate-run.sh | no (internal) | Post-hoc event stream validation |
poll-all.sh | yes | Poll all active tasks in a project root, return summary JSON |
watcher.sh | yes | Daemon entry point — start/stop/status |
watcher-process.sh | no (internal) | Process a single plan from the watch directory |
Watcher daemon
The watcher daemon (watcher.sh) is a persistent background process that
monitors a directory for plan YAMLs and processes them autonomously.
watcher.sh start [--watch-dir <path>] [--interval <sec>]
watcher.sh stop
watcher.sh status
start — launches daemon, writes PID to .subagents/watcher.pid,
logs to .subagents/watcher.log
stop — terminates daemon gracefully
status — returns JSON with running status, PID, active task count
The daemon must be running for background dispatch. If it is not running,
write plans to .subagents/watch/ anyway — they will be processed when
the daemon starts.
Plan schema
dangerously_write_trunk: false
tasks:
- id: fix-auth
kind: single-file-fix
model: ollama-cloud/glm-5.1
agent: build
prompt: prompts/fix-auth.md
target: src/auth.py
worktree: fix-auth-branch
- id: refactor-api
kind: multi-file-fix
model: ollama-cloud/deepseek-v4-flash:cloud
agent: build
prompt: prompts/refactor-api.md
worktree: refactor-api-branch
Fields: id (required), kind (required), model (required),
prompt (required, path to prompt file), target (required for
single-file-fix, path inside cwd; not used for multi-file-fix),
worktree (optional, branch name), agent (optional, defaults per kind),
mode (optional, foreground or background, default foreground),
ttl_sec (optional, wall-clock deadline in seconds for background tasks,
default 1800).
Top-level field dangerously_write_trunk (optional, default false):
when false, dispatch to a trunk branch (main/master) is rejected with
a hint to set this flag. Set to true to allow dispatching directly to
the project root — use only when you are certain the subagent should
write to trunk.
Store prompt files in prompts/<task-id>.md at the project root.
For example, a plan referencing prompt: prompts/fix-auth.md has the
prompt file at <project>/prompts/fix-auth.md and the task directory
at <project>/.subagents/fix-auth/.
No depends field. The agent writes only tasks that are ready to run
right now.
Structured output from run-plan.sh
run-plan.sh returns JSON on stdout (all other output goes to stderr):
{
"plan_id": "20260528T151600Z",
"tasks": [
{
"id": "fix-auth",
"lockfile": "/abs/path/.subagents/fix-auth/.lock",
"task_dir": "/abs/path/.subagents/fix-auth",
"pid": 48912,
"worktree": "/abs/path/.worktrees/fix-auth",
"status": "dispatched"
},
{
"id": "fix-api",
"status": "skipped",
"reason": "worktree creation failed: branch already exists"
}
]
}
Directory structure
<project-root>/
.subagents/
watcher.pid ← PID file for watcher daemon
watcher.log ← watcher daemon log
watch/ ← agent drops background plans here
plan-001.yaml
processing/ ← watcher moves plans here while working
completed/ ← watcher moves plans here when done
failed/ ← watcher moves plans here on failure
results/ ← watcher writes result summaries here
fix-auth.md
<task-id>/
prompt.md
start-subagent.sh
.lock ← exists while subagent runs
events.jsonl
FINAL_OUTPUT.md
worktree/ ← real git worktree (if task declared one)
.worktrees/
<task-id> ← symlink → ../.subagents/<task-id>/worktree/
.subagents/ is gitignored. The real worktree lives inside the task
directory so its lifecycle is bound to the task. The symlink in
.worktrees/ lets other tooling discover active worktrees.
poll-subagent.sh
poll-subagent.sh --task-id <id> --root <project-root> \
[--interval <sec>] [--max-polls <n>] [--stale-threshold <sec>]
Monitors a dispatched subagent by polling its lockfile and events.jsonl.
Each iteration logs the event line count and mtime. Exits with:
- 0 — task completed (lockfile gone)
- 2 — stuck (events line count unchanged and mtime stale past threshold)
- 3 — timeout (max polls reached, lockfile still present)
- 1 — error (bad args, missing task dir)
Defaults: --interval 15, --max-polls 12 (up to 180s),
--stale-threshold 60. Use --max-polls 16 for complex tasks (up to 240s).
poll-all.sh
poll-all.sh --root <project-root> [--interval <sec>] [--max-polls <n>]
Polls all active tasks in a project root and returns a summary JSON.
Scans .subagents/*/.lock for active tasks, checks events line count
and mtime for each, and returns a consolidated report.
Exits with:
- 0 — all tasks completed (no locks found)
- 1 — one or more tasks still active
Output (stdout):
{
"tasks": [
{"id": "fix-auth", "lines": 42, "mtime": 1717000000, "age_sec": 15},
{"id": "refactor-api", "lines": 18, "mtime": 1717000010, "age_sec": 5}
],
"active": 2
}
subagent-cleanup.sh
subagent-cleanup.sh --task-id <id> --root <project-root>
Removes .lock, removes .worktrees/ symlink, git worktree removes the
real tree (no force — should be clean after merge), removes task dir.
subagent-abandon.sh
subagent-abandon.sh --task-id <id> --root <project-root>
Kills PID (TERM then KILL), removes .lock, removes .worktrees/ symlink,
force-removes worktree + deletes branch, removes task dir.
Permission model
The async mode does not use ACP permission relay. Permission policy is
enforced by:
- Prompt design — the agent writes a tightly-scoped prompt that
constrains the subagent's behaviour.
- Config gating — the consumer project's
opencode.json can set
per-command rules.
- Post-hoc validation —
scripts/validate-run.sh checks
events.jsonl for unexpected tool calls.
Default failure-mode mitigations
OPENCODE_DISABLE_AUTOCOMPACT=true set in the start script.
OPENCODE_DISABLE_AUTOUPDATE=true set in the start script.
- Auto-detects server mode. When
OPENCODE_SERVER_URL is set, the
template uses --attach. When unset, falls back to local --dir
mode. Avoids session-in-session env-var leak (issue #24747).
cleanup-stale.sh — cleans up stale .lock files and orphaned
worktrees whose PIDs are dead.
Dispatch kinds
| Kind | Status | Use for |
|---|
single-file-fix | available | One agent edits one file from a focused prompt. Required: target. |
multi-file-fix | available | Full-directory fix/refactor with no single-file target. Works on the entire CWD. No target needed. |
headless-spike | available | Read-only investigation; agent writes a report file but does not edit source. Required: target (report path). Defaults to --agent explore (opencode's read-only built-in). |
Sashay dispatch pattern
In a sashay, the calling agent has already created the branch, worktree, and draft PR. The calling agent then dispatches a subagent into the existing worktree using this skill. The calling agent includes chronicle instructions (commit, push, post PR comments) directly in the prompt file — the subagent follows them.
The subagent's --cwd must point to the worktree directory, not the project root. Use multi-file-fix with worktree pointing to an existing branch name. The skill creates a worktree from that branch and sets CWD to it automatically.
Example plan YAML from the calling agent:
tasks:
- id: implement-fix
kind: multi-file-fix
model: ollama-cloud/deepseek-v4-flash:cloud
agent: build
prompt: prompts/implement-fix.md
worktree: fix-branch-name
The prompt file (prompts/implement-fix.md) includes the sashay chronicle instructions:
You are working in a PR-tracked worktree. The draft PR URL is
https://github.com/org/repo/pull/123.
Chronicle rules:
1. Commit and push your changes regularly.
2. After each checkpoint, add a PR comment via the forge CLI.
3. When done, ensure all tests pass and signal completion.
Add a kind by:
- Drop a
<kind>.sh.j2 in templates/cli/.
- Add a row to the table above.
- Add an example invocation to
references/examples.md.
What this skill does NOT do
- Run inside an editor as an ACP agent. Editor flows should call
opencode acp directly.
- Expose opencode via MCP. opencode is an MCP client only.
- Manage opencode authentication. Run
opencode auth login separately.
- Coordinate between parallel agents beyond per-task isolation and
shared
.subagents/ directory. The agent owns all coordination.
- Merge worktree results. The agent decides merge semantics (commit, PR,
squash, etc.).
References
- ADR-001: async
.subagents/ lock-watch as primary dispatch mode.
- Trove:
async-subagent-dispatch@5ca7b44 — async dispatch patterns.
- Trove:
opencode-runtime-integration@d9bad44 — failure-mode catalogue.
- SPIKE-001:
--attach session visibility on serve daemon.
- Examples:
references/examples.md.
opencode CLI syntax reference
Commands and flags relevant to dispatch-opencode. Full docs at
https://opencode.ai/docs/cli/.
Global flags
| Flag | Description |
|---|
--help / -h | Display help |
--version / -v | Print version number |
--print-logs | Print logs to stderr |
--log-level | DEBUG, INFO, WARN, ERROR |
--pure | Run without external plugins |
opencode run
Non-interactive prompt execution — the core invocation for subagent
dispatch. Pass a prompt via stdin or as arguments.
opencode run [message..]
opencode run < prompt.md
Flags
| Flag | Description |
|---|
--model / -m | provider/model — e.g. ollama-cloud/deepseek-v4-flash:cloud |
--agent | Agent to use: build (default), explore (read-only), or a custom agent |
--attach | Attach to a running server — e.g. --attach http://localhost:4096 |
--password / -p | Basic auth password (defaults to OPENCODE_SERVER_PASSWORD) |
--username / -u | Basic auth username (defaults to OPENCODE_SERVER_USERNAME or opencode) |
--dir | Working directory (or path on remote server when attaching) |
--file / -f | Attach file(s) to the prompt |
--format | default (formatted) or json (raw JSON events) |
--thinking | Show thinking blocks |
--continue / -c | Continue the last session |
--session / -s | Resume a specific session by ID |
--fork | Fork session when continuing (use with --continue or --session) |
--dangerously-skip-permissions | Auto-approve permissions not explicitly denied |
--title | Title for the session |
opencode serve
Headless HTTP server. Required for --attach mode. Set
OPENCODE_SERVER_PASSWORD to enable basic auth.
opencode serve [--port <n>] [--hostname <host>] [--mdns]
opencode models
List available models from configured providers. Format:
provider/model.
opencode models [provider]
--refresh updates the cached model list. --verbose includes
cost metadata.
opencode auth login
Authenticate with an LLM provider. Credentials stored at
~/.local/share/opencode/auth.json.
opencode auth login [--provider <id>] [--method <label>]
Provider env vars (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
also work without running auth login.
opencode attach
Attach TUI to a running server.
opencode attach <url> [--dir <path>] [--continue]
Relevant environment variables
Dispatched subagents set these automatically (templates):
| Variable | Purpose |
|---|
OPENCODE_DISABLE_AUTOCOMPACT=true | Prevent context compaction in subagent |
OPENCODE_DISABLE_AUTOUPDATE=true | Prevent update checks in subagent |
Server-attach variables (set by the operator before running
opencode serve):
| Variable | Purpose |
|---|
OPENCODE_SERVER_URL | URL of the headless server for --attach mode |
OPENCODE_SERVER_PASSWORD | Basic auth password |
OPENCODE_SERVER_USERNAME | Basic auth username (default opencode) |
Troubleshooting
See references/troubleshooting.md for detailed
solutions to common issues. Trigger keywords: session not found,
blank FINAL_OUTPUT.md, dispatch skipped, timeout, stuck,
exit code 3, exit code 2, branch not found, orphaned symlink,
worktree symlink, prompt file resolves.