feat(opencode): write plan and task specs to .workflow/run-<id>/ on disk

Plans and task specs were previously re-emitted as inline prompt text on
every dispatch. That meant @check and @simplify might receive paraphrased
versions of the same plan, mid-loop revisions could leak as "actually let
me reconsider" passes, and the same content rode through orchestrator
context many times across review/test/make dispatches.

The orchestrator now writes finalized artifacts to a per-run directory:

  .workflow/run-<ISSUE-ID>/
    plan.md         # Phase 3 output
    task-1.md       # Phase 5 output, one file per task
    task-2.md
    summary.md      # Phase 9 output (was .workflow/workflow-summary.md)

Subagents read these by absolute path; the dispatch prompt body shrinks
to agent role, artifact path, and short per-dispatch context. Mid-loop
revisions (Phase 4 review cycles, etc.) edit the file in place so every
subsequent dispatch sees the same byte-for-byte source of truth — the
Finalized-Text Rule has a physical anchor.

Phase 1 captures WORKTREE_PATH, ISSUE_ID, and RUN_DIR. Phase 3 mkdirs
the run directory and writes plan.md. Phase 4 dispatches reviewers
against plan.md by path. Phase 5 writes one task-N.md per task. Phase
6/7 dispatch @test/@make against task-N.md by path; the @test→@make
TDD handoff stays inline. Phase 8 reviewers re-read plan.md from disk.
Phase 9 renames "Local Summary" to "Run Summary" and writes to
$RUN_DIR/summary.md. The staging exclusion broadens from a single
file to the whole .workflow/ tree, and Failure Handling follows suit.
This commit is contained in:
Harald Hoyer 2026-05-07 14:44:08 +02:00
parent 4dc3cffba6
commit 25f4c6f179

View file

@ -14,25 +14,56 @@ You are executing the multi-agent workflow inside the worktree this opencode ses
If `$ARGUMENTS` is empty, stop immediately: "Usage: `/workflow <ISSUE-ID> [base-branch]` (e.g. `/workflow ABC-1`). The ID must exist as `./TODO/<ID>.md`. Base branch defaults to `main` (then `master`)." If `$ARGUMENTS` is empty, stop immediately: "Usage: `/workflow <ISSUE-ID> [base-branch]` (e.g. `/workflow ABC-1`). The ID must exist as `./TODO/<ID>.md`. Base branch defaults to `main` (then `master`)."
Parse `$ARGUMENTS`: the first whitespace-separated token is the issue ID, an optional second token overrides the base branch. Parse `$ARGUMENTS`: the first whitespace-separated token is the issue ID, an optional second token overrides the base branch. Store as `ISSUE_ID`.
---
## Run Artifacts
The orchestrator writes plan and task-spec artifacts to a per-run directory in the worktree. Subagents read these by absolute path rather than from inline prompt text. This keeps dispatch prompts small, eliminates paraphrase drift between dispatches (`@check` and `@simplify` see the same plan byte-for-byte), and gives Dispatch Hygiene's Finalized-Text Rule a physical anchor — the file *is* the final version.
**Directory layout** (relative to `$WORKTREE_PATH`):
```
.workflow/
└── run-<ISSUE-ID>/
├── plan.md # Phase 3 output — finalized
├── task-1.md # Phase 5 output — one file per task
├── task-2.md
└── summary.md # Phase 9 output (the run summary)
```
Define `RUN_DIR="$WORKTREE_PATH/.workflow/run-$ISSUE_ID"` once in Phase 1 and reference it everywhere downstream. Create the directory in Phase 3 (`mkdir -p "$RUN_DIR"`).
**Authoring rules:**
- Files are written by the orchestrator, never by subagents.
- Files are passed to subagents as absolute paths: e.g. *"the plan is at `<RUN_DIR>/plan.md`; read it before responding."* The dispatch prompt body should be short — agent role, artifact path, per-dispatch context (worktree path, branch, base branch). **Do not quote artifact contents inline.**
- Mid-loop revisions (Phase 4 review cycle, Phase 5 task respec, etc.) edit the file in place; every subsequent dispatch reads the new version automatically.
**Lifecycle:**
- Files persist across phases until the run finishes.
- Files are **not committed** (same as `summary.md`). Recommend `.workflow/` in `.gitignore`.
- Multiple runs on the same issue overwrite the prior run's artifacts. Save anything you want to keep before re-running.
--- ---
## Phase 1: Sanity Check ## Phase 1: Sanity Check
1. Verify CWD is a non-bare git worktree: `git rev-parse --is-bare-repository 2>/dev/null` must output `false`. If not, stop: "Workflow must be run from a non-bare worktree (the directory opencode was launched in)." 1. Verify CWD is a non-bare git worktree: `git rev-parse --is-bare-repository 2>/dev/null` must output `false`. If not, stop: "Workflow must be run from a non-bare worktree (the directory opencode was launched in)."
2. Verify the TODO tracker exists: 2. Capture the worktree path: `WORKTREE_PATH="$(pwd)"`.
3. Verify the TODO tracker exists:
- `./TODO/` directory must exist. If not, stop: "TODO/ directory not found in the current worktree. Commit a TODO/ folder with one file per issue plus a README.md index." - `./TODO/` directory must exist. If not, stop: "TODO/ directory not found in the current worktree. Commit a TODO/ folder with one file per issue plus a README.md index."
- `./TODO/README.md` must exist. If not, stop: "TODO/README.md not found. Add the category index file before running the workflow." - `./TODO/README.md` must exist. If not, stop: "TODO/README.md not found. Add the category index file before running the workflow."
- `./TODO/<ARGUMENTS-first-token>.md` must exist. If not, stop: "Issue file `./TODO/<ID>.md` not found for ID parsed from `$ARGUMENTS`." - `./TODO/$ISSUE_ID.md` must exist. If not, stop: "Issue file `./TODO/<ID>.md` not found for ID parsed from `$ARGUMENTS`."
3. Verify HEAD is not detached: `git symbolic-ref --short HEAD` must succeed. If it fails, stop: "Cannot run on a detached HEAD. Check out a feature branch first." 4. Verify HEAD is not detached: `git symbolic-ref --short HEAD` must succeed. If it fails, stop: "Cannot run on a detached HEAD. Check out a feature branch first."
4. Capture the current branch: `BRANCH_NAME="$(git symbolic-ref --short HEAD)"`. 5. Capture the current branch: `BRANCH_NAME="$(git symbolic-ref --short HEAD)"`.
5. Resolve the base branch (`BASE_BRANCH`): 6. Resolve the base branch (`BASE_BRANCH`):
- If `$ARGUMENTS` provided a second token, use it. - If `$ARGUMENTS` provided a second token, use it.
- Else if `git rev-parse --verify --quiet main` succeeds, use `main`. - Else if `git rev-parse --verify --quiet main` succeeds, use `main`.
- Else if `git rev-parse --verify --quiet master` succeeds, use `master`. - Else if `git rev-parse --verify --quiet master` succeeds, use `master`.
- Else stop: "Could not determine base branch (no `main` or `master`). Pass it as the second argument: `/workflow <ISSUE-ID> <base-branch>`." - Else stop: "Could not determine base branch (no `main` or `master`). Pass it as the second argument: `/workflow <ISSUE-ID> <base-branch>`."
6. Verify the current branch is not the base branch: if `BRANCH_NAME == BASE_BRANCH`, stop: "Cannot run workflow on the base branch (`$BASE_BRANCH`). Switch to a feature branch first." 7. Verify the current branch is not the base branch: if `BRANCH_NAME == BASE_BRANCH`, stop: "Cannot run workflow on the base branch (`$BASE_BRANCH`). Switch to a feature branch first."
8. Set the run-artifacts directory: `RUN_DIR="$WORKTREE_PATH/.workflow/run-$ISSUE_ID"`. Phase 3 will `mkdir -p "$RUN_DIR"` before writing the first artifact.
--- ---
@ -52,7 +83,7 @@ If the issue's status is `Todo`, ask `@pm` to set it to `In Progress` and propag
## Phase 3: Plan ## Phase 3: Plan
Analyze the codebase. Create a detailed implementation plan addressing the issue's requirements and acceptance criteria. Analyze the codebase. Create a detailed implementation plan addressing the issue's requirements and acceptance criteria, then write it to `$RUN_DIR/plan.md` (run `mkdir -p "$RUN_DIR"` first if the directory doesn't exist). All Phase 4 reviewer dispatches read this file.
The plan should include: The plan should include:
- Problem summary (from issue context) - Problem summary (from issue context)
@ -70,7 +101,7 @@ The plan should include:
**Skip Test Design for:** Config-only changes, decorator swaps, import reorganization, documentation. **Skip Test Design for:** Config-only changes, decorator swaps, import reorganization, documentation.
When skipped, `@test` derives test cases directly from acceptance criteria. When skipped, `@test` derives test cases directly from acceptance criteria.
After drafting, apply **Dispatch Hygiene** (below) to the plan — it is a dispatch artifact and gets sent to `@check`/`@simplify` in Phase 4. Before saving `plan.md`, apply **Dispatch Hygiene** (below). The file on disk is what reviewers will read in Phase 4 — there is no second chance to revise during dispatch.
--- ---
@ -135,7 +166,7 @@ If any check trips, **do not dispatch.** Fix and re-validate. Repeated trips on
## Phase 4: Review Plan ## Phase 4: Review Plan
Apply **Dispatch Hygiene** to the plan and to each reviewer prompt before sending. Dispatch `@check` and `@simplify` in parallel to review the plan. Dispatch `@check` and `@simplify` in parallel to review `$RUN_DIR/plan.md`. The dispatch prompt is short — agent role, the absolute path to the plan, the worktree path, and any per-dispatch reviewer focus. Tell each reviewer to read the plan from disk; do **not** paste the plan inline. Apply **Dispatch Hygiene** to each dispatch prompt.
Reviewers should evaluate testability: Reviewers should evaluate testability:
- `@check`: Is the design testable? Are the right behaviors identified? (Review Framework §8) - `@check`: Is the design testable? Are the right behaviors identified? (Review Framework §8)
@ -147,10 +178,10 @@ Reviewers should evaluate testability:
- Note conflicts explicitly - Note conflicts explicitly
**Review loop (max 3 cycles):** **Review loop (max 3 cycles):**
1. Send plan to both reviewers 1. Dispatch both reviewers against `$RUN_DIR/plan.md`.
2. Merge findings 2. Merge findings
3. If verdict is ACCEPTABLE from both (or JUSTIFIED COMPLEXITY from `@simplify`): proceed to Phase 5 3. If verdict is ACCEPTABLE from both (or JUSTIFIED COMPLEXITY from `@simplify`): proceed to Phase 5
4. If BLOCK or NEEDS WORK: revise the plan addressing findings, then re-review 4. If BLOCK or NEEDS WORK: edit `$RUN_DIR/plan.md` in place addressing findings (re-apply Dispatch Hygiene to the updated file), then re-review.
5. **Convergence detection:** if reviewers return the same findings as the previous cycle, stop the loop early 5. **Convergence detection:** if reviewers return the same findings as the previous cycle, stop the loop early
6. If still unresolved after 3 cycles: note unresolved blockers and proceed anyway (they will be documented in the workflow summary and commit message) 6. If still unresolved after 3 cycles: note unresolved blockers and proceed anyway (they will be documented in the workflow summary and commit message)
@ -158,7 +189,9 @@ Reviewers should evaluate testability:
## Phase 5: Split into Tasks ## Phase 5: Split into Tasks
Break the approved plan into discrete tasks for `@make`. Each task needs: Break the approved plan into discrete tasks. **Write each task to `$RUN_DIR/task-<N>.md`** (1-indexed: `task-1.md`, `task-2.md`, …). Phase 6 (`@test`) and Phase 7 (`@make`) read these files by absolute path.
Each task file must contain:
| Required | Description | | Required | Description |
|----------|-------------| |----------|-------------|
@ -226,10 +259,13 @@ Apply **Dispatch Hygiene** to each task spec before dispatch in Phase 7.
Apply **Dispatch Hygiene** to each `@test` prompt before sending. Apply **Dispatch Hygiene** to each `@test` prompt before sending.
For each task from Phase 5, dispatch `@test` with: For each task from Phase 5, dispatch `@test` with a short prompt that names:
- The task spec (acceptance criteria, code context, files to modify) - The absolute path to the task spec: `$RUN_DIR/task-<N>.md``@test` reads acceptance criteria, code context, and files-to-modify from there.
- The Test Design section from the plan (if provided) - The absolute path to the plan, if test design context is needed: `$RUN_DIR/plan.md`.
- The test file path to create (following colocated pattern) - The worktree path (so `@test` resolves source files correctly).
- The test file path to create.
Do **not** quote task or plan content inline — `@test` reads from disk.
`@test` writes failing tests and verifies RED with structured failure codes. `@test` writes failing tests and verifies RED with structured failure codes.
@ -255,11 +291,11 @@ To get a clean runtime RED, dispatch a **stub-first `@make` pass** *before* `@te
**Stub pass (split from Phase 7's body pass):** **Stub pass (split from Phase 7's body pass):**
1. Dispatch `@make` in **standard mode** (no tests exist yet) with this exact scope: 1. Dispatch `@make` in **standard mode** (no tests exist yet). The dispatch prompt names `$RUN_DIR/task-<N>.md` as the source spec and adds this stub-pass-specific scope inline:
- **Goal:** add the planned API as `todo!()`-bodied stubs so the test will compile. - **Goal:** add the planned API as `todo!()`-bodied stubs so the test will compile.
- **Files to modify:** the relevant `src/<module>.rs` for module tests, or `src/lib.rs` plus any new `src/<module>.rs` for integration tests (the latter need `pub mod …;` declarations so the test crate can import). - **Files to modify:** the relevant `src/<module>.rs` for module tests, or `src/lib.rs` plus any new `src/<module>.rs` for integration tests (the latter need `pub mod …;` declarations so the test crate can import).
- **Stubs only:** every function body is exactly `todo!()`. Every method body is exactly `todo!()`. Structs may use `pub struct Foo;` or `pub struct Foo { /* fields TBD */ }` — but no logic. - **Stubs only:** every function body is exactly `todo!()`. Every method body is exactly `todo!()`. Structs may use `pub struct Foo;` or `pub struct Foo { /* fields TBD */ }` — but no logic.
- **Signatures must match the planned final API exactly** (return types, lifetimes, generics, visibility). Lift signatures from the Phase 3 plan / Phase 5 task spec. - **Signatures must match the planned final API exactly** (return types, lifetimes, generics, visibility). Lift signatures from the task spec.
- **Acceptance criteria:** `cargo check` (wrapped in `nix develop -c …` if the project has a devshell) passes; no test command is run. - **Acceptance criteria:** `cargo check` (wrapped in `nix develop -c …` if the project has a devshell) passes; no test command is run.
- **Dispatch Hygiene still applies:** the stub pass is small and finalized — no draft bodies, no contradictory signatures. - **Dispatch Hygiene still applies:** the stub pass is small and finalized — no draft bodies, no contradictory signatures.
2. Verify `cargo check` passed in `@make`'s output. If not, fix and re-dispatch the stub pass before continuing. 2. Verify `cargo check` passed in `@make`'s output. If not, fix and re-dispatch the stub pass before continuing.
@ -293,10 +329,12 @@ Apply **Dispatch Hygiene** to each `@make` spec before sending. Repeated trips o
This applies to **all** `@make` invocations: standard mode, TDD mode, stub-pass, body-pass, and integration-fix dispatches. This applies to **all** `@make` invocations: standard mode, TDD mode, stub-pass, body-pass, and integration-fix dispatches.
Execute each task by dispatching `@make` with: Execute each task by dispatching `@make` with a short prompt:
- The task spec (from Phase 5, finalized per Dispatch Hygiene) - The absolute path to the task spec: `$RUN_DIR/task-<N>.md``@make` reads acceptance criteria, code context, and files-to-modify from there.
- Relevant code context (seam-revealing snippets only — see Phase 5 "Code Context — what to include") - The worktree path.
- **Pre-written failing tests and handoff from `@test` (if TESTS_READY)** - **Pre-written failing tests and handoff from `@test` (if TESTS_READY)** — these are short and per-dispatch, so include them inline in the prompt.
Do **not** quote the task spec inline.
`@make` runs in TDD mode when tests are provided: `@make` runs in TDD mode when tests are provided:
1. Entry validation: run tests, verify RED, check failure codes match handoff 1. Entry validation: run tests, verify RED, check failure codes match handoff
@ -326,9 +364,9 @@ After all tasks complete, verify overall integration:
Apply **Dispatch Hygiene** to each reviewer prompt before sending. Dispatch `@check` and `@simplify` in parallel to review the full implementation (all changes across all files). Apply **Dispatch Hygiene** to each reviewer prompt before sending. Dispatch `@check` and `@simplify` in parallel to review the full implementation (all changes across all files).
Provide reviewers with: Provide reviewers with:
- The original plan - The absolute path to `$RUN_DIR/plan.md` (the same file Phase 4 reviewed; mid-loop revisions will have updated it in place)
- The full diff (`git diff "$BASE_BRANCH"...HEAD`) - The full diff (`git diff "$BASE_BRANCH"...HEAD`)
- Any decisions or deviations from the plan - Any decisions or deviations from the plan, captured inline in the dispatch prompt
**Review loop (max 3 cycles):** **Review loop (max 3 cycles):**
1. Send implementation to both reviewers 1. Send implementation to both reviewers
@ -348,7 +386,7 @@ Provide reviewers with:
The workflow is forge-agnostic. It commits locally and stops. **Do not push, and do not open a pull/merge request** — the user chooses their forge and review workflow manually. The workflow is forge-agnostic. It commits locally and stops. **Do not push, and do not open a pull/merge request** — the user chooses their forge and review workflow manually.
### Commit Code Changes ### Commit Code Changes
- Stage code changes only. **Do not stage anything under `TODO/`** (committed separately below) and **do not stage `.opencode/workflow-summary.md`** (intentionally never committed — see Local Summary). - Stage code changes only. **Do not stage anything under `TODO/`** (committed separately below) and **do not stage anything under `.workflow/`** (intentionally never committed — these are per-run artifacts).
- Write a conventional commit message summarizing the implementation. Reference the TODO issue ID in the body (e.g. `Refs: GAL-39`). - Write a conventional commit message summarizing the implementation. Reference the TODO issue ID in the body (e.g. `Refs: GAL-39`).
- If changes are large/varied, use multiple atomic commits (one per logical unit) - If changes are large/varied, use multiple atomic commits (one per logical unit)
@ -360,8 +398,8 @@ The workflow is forge-agnostic. It commits locally and stops. **Do not push, and
- If acceptance-criteria checkboxes were addressed by the implementation, ask `@pm` to check them off (flip `- [ ]` to `- [x]` under `## Acceptance criteria`). - If acceptance-criteria checkboxes were addressed by the implementation, ask `@pm` to check them off (flip `- [ ]` to `- [x]` under `## Acceptance criteria`).
- Commit the TODO/ changes as a separate atomic commit: `chore(todo): update <issue-id> status and progress`. Stage the issue file plus any propagated index file (README.md or parent file). - Commit the TODO/ changes as a separate atomic commit: `chore(todo): update <issue-id> status and progress`. Stage the issue file plus any propagated index file (README.md or parent file).
### Local Summary ### Run Summary
- Write `.opencode/workflow-summary.md` in the worktree with: - Write `$RUN_DIR/summary.md` with:
- **Run timestamp** — capture it from the shell at write time: `date -Iseconds` (e.g. `2026-05-07T11:24:13+02:00`). **Do not** use a placeholder like `???:???:??` or "session date" — if you cannot get a real timestamp, omit the field entirely rather than fabricating one. - **Run timestamp** — capture it from the shell at write time: `date -Iseconds` (e.g. `2026-05-07T11:24:13+02:00`). **Do not** use a placeholder like `???:???:??` or "session date" — if you cannot get a real timestamp, omit the field entirely rather than fabricating one.
- Issue reference and title - Issue reference and title
- Branch name and final commit SHA(s) - Branch name and final commit SHA(s)
@ -370,15 +408,15 @@ The workflow is forge-agnostic. It commits locally and stops. **Do not push, and
- Review outcomes (plan review + final review verdicts) - Review outcomes (plan review + final review verdicts)
- Unresolved items (if any) - Unresolved items (if any)
- Files changed - Files changed
- **Do not commit this file.** It is a per-run, per-branch artifact; committing it would create merge conflicts whenever multiple workflow branches are merged. Leave it untracked. Recommend the user add `.opencode/` to `.gitignore` if not already. - **Do not commit anything under `.workflow/`.** The whole directory is per-run, per-branch state. Recommend the user add `.workflow/` to `.gitignore` if not already.
--- ---
## Failure Handling ## Failure Handling
At any phase, if an unrecoverable error occurs: At any phase, if an unrecoverable error occurs:
1. Write `.opencode/workflow-summary.md` with what was completed and what failed. Do **not** stage or commit this file. 1. Write `$RUN_DIR/summary.md` (creating `$RUN_DIR` first if it doesn't exist) with what was completed and what failed. Do **not** stage or commit anything under `.workflow/`.
2. If any code was written, commit it with message `wip: incomplete workflow run for <issue-id>`. Stage code only — exclude `.opencode/workflow-summary.md`. 2. If any code was written, commit it with message `wip: incomplete workflow run for <issue-id>`. Stage code only — exclude `.workflow/` and `TODO/`.
3. Leave the branch and worktree intact for the user to inspect — do not push, do not delete. 3. Leave the branch and worktree intact for the user to inspect — do not push, do not delete.
4. Dispatch `@pm` against `./TODO/` (live filesystem mode) to add a comment on the issue file (`./TODO/<ID>.md`) summarising what failed. 4. Dispatch `@pm` against `./TODO/` (live filesystem mode) to add a comment on the issue file (`./TODO/<ID>.md`) summarising what failed.
5. Stop execution. 5. Stop execution.