opencode-sub-worker
v0.4.0
Published
Turn a markdown task checklist into a dependency- and file-conflict-aware schedule, run ready tasks in parallel as isolated child sessions in their own git worktrees, coordinate through an orchestrator-mediated mailbox, and gate every task with a verifica
Maintainers
Readme
opencode-sub-worker
Turn a markdown task checklist into a dependency- and file-conflict-aware schedule, run the ready tasks in parallel as isolated child sessions, each in its own git worktree, coordinate through an orchestrator-mediated mailbox, and gate every task with a verification command.
Built for OpenCode V2.
- [ ] Add cache to the auth module
- files: src/auth/cache.ts, src/auth/cache.test.ts
- verify: npm test -- auth
- [ ] Update the docs
- files: docs/auth.md
- verify: false
- [ ] Wire the cache into the router
- files: src/router.ts
- deps: 1, 2/tasks plan TASKS.md # preview the schedule, run nothing
/tasks run TASKS.md # start it
/tasks status # follow alongWhy this exists
Most agent orchestrators ask the model to plan and delegate in prose. That is flexible but hard to reproduce and hard to trust. This plugin moves the loop into deterministic code:
- The plugin owns scheduling, locking, retries, merging and gating.
- The model is only a worker: it receives one task, works in one checkout, and reports back.
- The schedule is driven by declared files, so two workers never edit the same file at the same time.
How it works
- Parse — a
checklistline (- [ ]) or aheadingstask (### N. [ ]), depending ontaskFormat; a custom parser can be plugged in. - Graph —
deps:become ordering edges; tasks that share a file (or declare no files at all) become mutually exclusive. - Schedule — ready, non-conflicting tasks are launched up to
maxConcurrency; the rest wait. - Isolate — each worker runs in its own
git worktreeand gets only its declared write scope in the prompt. - Wait & read — the orchestrator waits for a worker to go idle and reads
its final message for a
[task:done]/[task:blocked]marker. - Gate — the task's
verify:command is run in the worker's worktree. A failing gate is a failing task. - Integrate — a passing task's worktree changes are committed on its branch and merged back; the worktree is removed and dependents unlock.
- Report — status, usage and a durable ledger are kept in plugin storage.
If a task exhausts its retries, or reports blocked, or its gate keeps failing,
the run pauses and the user is notified. /tasks resume gives it another
shot.
Task formats
Two formats are built in; pick one with taskFormat.
checklist (default)
A task is a checklist line. Metadata goes on the lines below it, indented at least as far as the task, with or without a bullet.
| Key | Meaning |
|---|---|
| id | Stable id. Defaults to t1, t2, … (by position). |
| files | Comma-separated write scope. Empty ⇒ never parallel. |
| locks | Comma-separated exclusive named resources (e.g. db-migration, staging). |
| priority | Higher runs first among ready tasks. Default 0. |
| verify | Shell command that must pass. false/no/none/- disables the gate. |
| deps | Comma-separated dependencies — a 1-based item number or an id. |
| agent | Worker agent id. Overrides the plugin default. |
| model | provider/model[#variant] for this task. Overrides the role map and default. |
| risk | Free text; a value listed in approvalRisks holds the task for approval. |
| timeout | Per-task timeout (30s, 5m, 600000). |
Already-checked (- [x]) items are treated as done and satisfy dependents.
Example: examples/tasks.example.md.
headings (PRD / tasks.md style)
A task is a ### N. [ ] heading; fields are bold labels below it; the order in
the file is the dependency chain. [design] marks a visual subtask.
### 1. [ ] Add the cache layer
- **Stories:** 1, 3
- **TDD:** yes
- **Risk:** 🟠
- **Files:** `src/auth/cache.ts`, `src/auth/cache.test.ts`
- **Spec & identifiers in scope:** none
- **What to build:** a small cache keyed by token.
- **How verified (end-to-end):** `npm test -- auth`
- **Acceptance criteria:**
- [ ] a hit avoids the slow path
- [ ] a miss re-fetches- The task id is the heading number, so
/tasks diff 1targets it. - Read labels:
Files,Risk,Stories,TDD,Locks,Priority,Agent,Model,Out of scope,Spec & identifiers in scope,What to build,Acceptance criteria.How verifiedis prose and does not become a gate; add a**Verify:**line with a command to gate the task. sequentialDeps: trueturns file order into explicit dependencies.Risk: 🔴(or any value inapprovalRisks) holds the task for/tasks approve.- A task whose body matches
holdPattern(default[NEEDS DECISION) is held and the run pauses with the reason. - Field labels are configurable with
fields; a custom parser can be plugged withparserModule.
Example: examples/tasks.headings.example.md.
Running a PRD / tasks.md workflow
If your tasks come from a PRD broken into a tasks.md, run in assist mode so
the plugin implements and verifies but leaves the tree dirty for you to review
and commit:
{
"plugins": [
{
"package": "opencode-sub-worker",
"options": {
"taskFormat": "headings",
"sequentialDeps": true,
"mode": "assist",
"detectCompletion": "checkboxes",
"flipCheckboxes": true,
"progressFile": "features/<feature>/progress.md",
"approvalRisks": ["🔴"],
"workerInstructionsFile": ".opencode/sub-worker-worker.md"
}
}
]
}mode: "assist"forcesautoMerge,commitBeforeMergeandcleanupWorktreesoff.workerInstructionsFileappends your project rules (no commits, tracker discipline, and so on) to every worker prompt;workerSkillsandworkerInstructionsdo the same inline.flipCheckboxeswrites### N. [x]back into the source list;progressFileappends a per-task block. The orchestrator is the only writer, so parallel workers never race on the file.detectCompletion: "checkboxes"treats a flipped box as the done signal instead of the worker's report.
Command and tools
| Surface | Description |
|---|---|
| /tasks plan <file\|markdown> | Parse and preview the dependency/conflict schedule. No execution. |
| /tasks run <file\|markdown> | Start the run. |
| /tasks add <file\|markdown> | Add tasks to the active run. |
| /tasks status | Task statuses and recent activity. |
| /tasks list | One-line summary. |
| /tasks diff <taskID> | Show a task's worktree diff. |
| /tasks logs <taskID> | Show a task's status, branch, gate output and history. |
| /tasks approve <taskID> / reject <taskID> <reason> | Resolve a task held at the approval gate. |
| /tasks answer <taskID> <text> | Answer a blocked worker and continue it. |
| /tasks broadcast <text> | Message every running worker. |
| /tasks resume / abort | Resume a paused run / stop the run. |
| taskforce_plan / taskforce_run / taskforce_add / taskforce_status / taskforce_resume / taskforce_abort / taskforce_answer / taskforce_approve / taskforce_reject / taskforce_diff / taskforce_logs / taskforce_send / taskforce_broadcast | The same actions as model tools. |
| worker_report / worker_inbox | Worker-side tools for the mailbox. |
Options
Plugin-level options (V2 object form):
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
{
"package": "opencode-sub-worker",
"options": {
"taskFormat": "checklist",
"sequentialDeps": false,
"parserModule": "",
"maxConcurrency": 3,
"taskTimeoutMs": 1200000,
"verifyTimeoutMs": 600000,
"maxAttempts": 2,
"freshWorktreeOnRetry": false,
"continueOnError": false,
"repairOnFailure": true,
"workerAgent": "subworker",
"installWorkerAgent": true,
"workerModel": "anthropic/claude-sonnet-4-5",
"roleModels": { "reviewer": "anthropic/claude-sonnet-4-5" },
"fallbackModels": ["openai/gpt-4o-mini"],
"useWorktrees": true,
"requireWorktrees": true,
"autoMerge": true,
"commitBeforeMerge": true,
"cleanupWorktrees": true,
"enforceWriteScope": true,
"enforceDiffScope": true,
"approvalRisks": ["high"],
"coChangeLearning": false,
"coChangeMinCount": 2,
"mirrorSessionTitles": false,
"holdPattern": ["\\[NEEDS DECISION"],
"detectCompletion": "report",
"progressFile": "",
"flipCheckboxes": false,
"mode": "integrate",
"workerInstructions": "",
"workerInstructionsFile": "",
"workerSkills": [],
"workerContract": "generic",
"verifierEnabled": false,
"verifierAgent": "explore",
"verifierPrompt": "",
"askUserOnBlocked": false,
"maxCostUsd": 0,
"maxTokens": 0,
"persistState": true
}
}
]
}| Option | Default | Meaning |
|---|---|---|
| commandName | tasks | Slash command name. |
| registerCommand / registerTools | true | Toggle the command / tools. |
| taskFormat | checklist | checklist or headings (PRD/tasks.md style). |
| fields | (built-in) | Field-label mapping for the headings format. |
| sequentialDeps | false | Treat file order as the dependency chain. |
| parserModule | "" | Path or package name of a custom parser module (parse(source) => { tasks, errors }). |
| maxConcurrency | 3 | Max workers running at once. |
| taskTimeoutMs | 1200000 | Per-task wall-clock timeout. |
| verifyTimeoutMs | 600000 | Gate command timeout. |
| maxAttempts | 2 | Attempts per task before the run pauses. |
| freshWorktreeOnRetry | false | Start a retry from a clean worktree instead of continuing in place. |
| continueOnError | false | A failed/blocked task pauses the whole run; set true to let independent work continue. |
| repairOnFailure | true | On a failed gate, steer the failure back to the same worker to repair it before counting an attempt. |
| workerInstructions | "" | Extra instructions appended to every worker prompt. |
| workerInstructionsFile | "" | A file whose contents are appended to every worker prompt. |
| workerSkills | [] | Skills the worker should follow (appended to the prompt). |
| workerContract | generic | Built-in worker report contract; none omits it. |
| workerAgent | subworker | Worker agent id. |
| installWorkerAgent | true | Write .opencode/agents/<workerAgent>.md on setup. |
| workerModel | "" | provider/model[#variant] for workers. Empty = inherit. |
| roleModels | {} | Per-agent model overrides keyed by the task's agent: value. |
| fallbackModels | [] | Ordered model references tried on retry after a failure. |
| useWorktrees | true | One git worktree per worker. |
| requireWorktrees | true | If a worktree cannot be created, fail the task instead of running in place. |
| autoMerge | true | Merge each finished worker's branch back. |
| commitBeforeMerge | true | Commit the worker's worktree changes so the branch can be merged. |
| cleanupWorktrees | true | Remove worktrees after integration. |
| enforceWriteScope | true | Deny edits outside files: at the session-permission level. |
| enforceDiffScope | true | After a worker finishes, fail it if it touched undeclared files. |
| approvalRisks | [] | Risk labels (case-insensitive) that hold a task for /tasks approve/reject before integrating. |
| coChangeLearning | false | Learn file pairs that change together and treat them as conflicts. |
| coChangeMinCount | 2 | Co-change count before a learned pair conflicts. |
| mirrorSessionTitles | false | Mirror run status into the orchestrator session title. |
| holdPattern | ["\\[NEEDS DECISION"] | Regexes; a matching task is held and the run pauses. |
| detectCompletion | report | report (worker report) or checkboxes (a flipped box in the source). |
| progressFile | "" | Append a per-task block here (relative to the project root). |
| flipCheckboxes | false | Write [x] back to the source list when a task is done. |
| mode | integrate | integrate (merge) or assist (leave the tree dirty; forces merge behaviours off). |
| verifierEnabled | false | Run an independent read-only verifier session after the gate. |
| verifierAgent | explore | Agent id used for the verifier. |
| verifierPrompt | "" | Extra verifier instructions appended to the verifier prompt. |
| askUserOnBlocked | false | Keep a blocked worker alive and wait for /tasks answer. |
| maxCostUsd | 0 | Pause the run when accumulated cost reaches this. 0 = off. |
| maxTokens | 0 | Pause the run when accumulated tokens reach this. 0 = off. |
| persistState | true | Durable run snapshot in plugin storage. |
Safety and quality
Four mechanisms turn the declared write scope from advice into an invariant:
- Session permissions (
enforceWriteScope) — the worker child session is created with aneditruleset that denies*and allows only the declared files, so an out-of-scope edit is refused by the host, not just discouraged. - Diff-scope check (
enforceDiffScope) — after a worker finishes, its changed files are compared against the (glob-aware) declared scope; a violation fails the task. - Independent verifier (
verifierEnabled) — an optional read-only session inspects the result and can reject it, independent of the gate command. - Fail-closed isolation (
requireWorktrees) — if a worktree cannot be created, the task fails instead of silently running in place.
Scopes support globs (*, ?, **), so src/auth/** conflicts with
src/auth/cache.ts and covers it in the diff-scope check. Conflict detection is
conservative: when two scopes might intersect, tasks are serialized.
autoMerge commits a worker's worktree changes on its branch and merges that
branch. If your Git worktree strategy does not create a branch, merging
fails and the run pauses — see Limitations.
Budgets and model routing
maxCostUsd/maxTokenspause the run when the accumulated worker usage crosses the limit. Per-task usage is shown in/tasks status.- Model selection: task
model:→roleModels[agent]→workerModel→ inherit. So areviewertask can run on a different model than acoder.
Human-in-the-loop
With askUserOnBlocked: true, a worker that reports blocked is kept alive
and the run pauses with awaiting answer. Reply with:
/tasks answer t1 use the test key from .env.testThe answer is steered into the same worker, which resumes and finishes its task.
Repair loop
By default (repairOnFailure: true), when a task's verification command fails the
failure output is steered back into the same worker, which keeps its context
and fixes the problem in place. Only if it still fails after maxAttempts is the
task marked failed and the run paused. Set it to false for the old
fail-and-retry-from-scratch behavior.
Approval gate
List risky labels in approvalRisks (for example ["high"]). A task whose
risk: matches still runs and is gated, but before its branch is integrated the
run pauses with the task in review:
/tasks approve t3 # integrate it
/tasks reject t3 not safe # block itNamed locks, priority and co-change learning
locks:declares exclusive named resources beyond files (a database, an environment, a port). Tasks sharing a lock are serialized.priority:(higher first) orders ready tasks.- With
coChangeLearning: true, the plugin records which files changed together across completed tasks and treats a learned pair (>=coChangeMinCount) as a conflict, so files that historically move together are not edited in parallel.
RPC and the CLI plugin
The package exposes an RPC contract at opencode-sub-worker/rpc with a status
method and progress events (emitted on every task/run transition), so other
plugins and clients can observe a run over HTTP.
It also exports a CLI plugin at opencode-sub-worker/tui:
- a run panel in the
session.panelslot — a live task table with per-task status colors, counts, cost and tokens. Open it with/tasks-panelor the "Sub-worker run panel" palette command;[f]toggles fullscreen and[esc]closes it; - a "Task force status" palette /
/taskscommand that shows a compact summary; - attention notifications when a task or run finishes or fails.
The panel reads the run over RPC and refreshes on progress events plus a 2s poll.
It renders with @opentui/solid; @opentui/core, @opentui/solid and
solid-js are declared as optional peers (the terminal provides them).
{ "plugins": ["opencode-sub-worker"] }Worker agent
A plugin cannot register a new agent through agent.transform (there is no
add). So, when installWorkerAgent is on, the plugin writes
.opencode/agents/<workerAgent>.md (mode all) with the worker contract. Set it
to false and point workerAgent at an agent you already have.
Mailbox
Coordination is orchestrator-mediated. Two directions are supported:
- Up: a worker calls
worker_report(or ends its message with a marker). Reports land in the run's durable state. - Down: the orchestrator calls
taskforce_send; the message is queued and steered into the running worker, which can read it withworker_inbox.
Persistence and recovery
Runs are snapshotted to plugin storage on every transition. A run that was
active when the server restarted loads paused (so unattended work never
resumes blindly) and can be restarted with /tasks resume.
Limitations (read these)
- V2 only. There is no V1 adapter.
- The report, not a tool, is the durable contract. A worker runs in its own
worktree location; tools registered for the orchestrator's location may not
be visible there. So completion is read from a fenced JSON report at the end of
the worker's final message (
[task:done]/[task:blocked]markers are the fallback);worker_reportis a best-effort enhancement. - The gate, the verifier, the diff and
gitrun in the server process viachild_process, not through OpenCode's permission system. Plugins are trusted code; keep that in mind. repairOnFailurereuses the same worktree and session, so a repair keeps the worker's context — and any partial edits — rather than starting clean.enforceWriteScopeis strict by design. A worker that needs to touch an undeclared file (say, a lockfile) will be refused and should report blocked. Declare the file or disable the option.- The diff-scope check trusts
git status. Untracked files and modified tracked files are seen; changes a worker makes outside the checkout, or files excluded by.gitignore, are not. - Merging assumes Git and a branch per worktree.
createWorktreerequires a Git repository. If your default worktree strategy does not create a branch,mergeWorktreereports a failure and the run pauses. WithautoMerge: falsethe worktree is kept for review instead (and recorded in the run history). - Co-change learning is a heuristic. It only ever adds conflicts, so it can over-serialize; the learned pairs persist in plugin storage.
continueOnErrorstill marks dependents of a failed task as skipped.askUserOnBlockedkeeps a session alive while the run is paused; it is disposed when the run is aborted or resumed.- The bundled CLI plugin renders a live panel in the TUI; it requires the
optional
@opentui/core,@opentui/solidandsolid-jspeers that the OpenCode terminal provides, and is only active in the TUI. - RPC status is per orchestrator session and returns JSON; progress events are live only (disconnected subscribers miss them).
- In
assistmode the plugin is the only writer of the source list and the progress file. It flips checkboxes and appends progress after each task, so parallel workers never race ontasks.md; a worker that also writes the file would race it. headings"How verified" is prose, not a gate. Only an explicit**Verify:**line gates the task; otherwise rely on the verifier and the acceptance criteria.detectCompletion: "checkboxes"andflipCheckboxesare alternatives. The first trusts the worker to flip the box; the second has the plugin flip it (usereportdetection then).- Not a sandbox. Workers inherit the session's permissions except for the write-scope rules the plugin adds.
Development
npm install
npm run typecheck
npm test # core parser/graph/scheduler/engine + the V2 adapter
npm run buildTests import the TypeScript sources directly, so they need Node 22.18+ (type stripping on by default). The published package is plain JavaScript.
To dogfood locally, build and run OpenCode from this repo root (the bundled
loader at .opencode/plugins/opencode-sub-worker.ts re-exports dist/):
npm run build
opencode
# then: /tasks plan examples/tasks.example.mdDesign
The core (src/core/) is platform-independent and talks to the host through
the HostPort interface (src/ports.ts). The OpenCode V2 adapter
(src/v2/) duck-types the plugin context so the package does not pull the
Effect-based type tree into consumers. This mirrors the architecture of
opencode-goal-loop.
Releasing
Releases are automated with GitHub Actions, mirroring
opencode-goal-loop.
ci.ymlrunstypecheck,test,build, andpack:checkon every push tomainand every pull request, across Node 22, 24, and 26.release.ymlruns release-please. It reads Conventional Commits and opens a "release PR" that bumps the version and updatesCHANGELOG.md. Merging that PR creates the git tag and GitHub Release, and the same workflow then publishes to npm with OIDC trusted publishing — no tokens stored, provenance attached.
So a release is just: merge a feat:/fix: PR, then merge the release PR that
release-please opens. For a one-off manual release, use npm version <patch|minor|major>
and push the tag (the trusted publisher must allow direct npm publish).
One-time npm setup
Add a Trusted Publisher for the package on npmjs.com (Package → Settings → Trusted publishing) with:
| Field | Value |
|---|---|
| Organization or user | omerdduran |
| Repository | opencode-sub-worker |
| Workflow filename | release.yml |
| Allowed actions | enable npm publish (Allow npm publish) |
The workflow filename must match exactly (case-sensitive, .yml included) and
the first publish must succeed within 2 days, otherwise the configuration expires.
Or configure it from the CLI (after the first publish):
npm trust github opencode-sub-worker \
--file release.yml \
--repository omerdduran/opencode-sub-worker \
--allow-publish -y
npm trust list opencode-sub-workerDevelopment requirements
The published package is plain JavaScript, but the test suite imports the TypeScript sources directly, so running this repo's tests requires Node 22.18+ (type stripping on by default).
License
MIT
