feat(opencode): @pm owns the TODO commit (ADR-23)

The orchestrator was running `git add ./TODO/` and `git commit -m
chore(todo): ...` itself in Phase 9, baking filesystem-tracker
specifics into commands/workflow.md. The point of @pm as an
abstraction is that it should be swappable — a Linear-backed @pm or a
Notion-backed @pm should drop in without touching the workflow
command. With API-backed trackers, "commit the TODO updates" is a
no-op and `git add ./TODO/` is wrong.

Push persistence shape behind the @pm boundary:

- New @pm capability `Commit pending changes` accepts a commit message
  and returns {ok, sha, message}. Filesystem @pm runs `git add ./TODO/`
  + `git commit -m <msg>` and returns the SHA. Tracker-backed
  implementations no-op and return sha: null.
- @pm gains tightly-scoped bash access: `git add ./TODO/*`,
  `git commit -m *`, `git status --porcelain ./TODO/*` only. Push,
  reset, rebase, checkout, branch, tag are explicit denies. Everything
  else falls through to the default deny.
- Phase 9 "Commit TODO Changes" replaces orchestrator-side git with a
  @pm dispatch; orchestrator constructs the message from run context
  and captures the returned SHA for the summary.
- Failure Handler gains a step 5 (commit pending after the failure
  comment add). Today the comment is left uncommitted in the working
  tree and gets discarded with the throwaway worktree (ADR-14) —
  forensic loss. With this change the failure note lands as its own
  commit on the failed branch.
- Routing matrix Phase 9 rows updated; ADR-22's superseded wording
  about orchestrator-side staging removed.

Stub-pass / body-pass / wip code commits remain orchestrator-owned —
those are code, not tracker-specific.

Refs: config/opencode/workflow-design.md ADR-23
This commit is contained in:
Harald Hoyer 2026-05-08 14:04:47 +02:00
parent a3e0de6d04
commit 56713cd7b8
3 changed files with 59 additions and 25 deletions

View file

@ -103,7 +103,6 @@ Dispatch `@pm` with the issue ID `$ISSUE_ID`, `$WORKTREE_PATH`, and `Validate ru
```json
{
"ok": true,
"issue_file_path": "<absolute path to the issue file>",
"issue": {
"id": "...",
"title": "...",
@ -129,7 +128,7 @@ Dispatch `@pm` with the issue ID `$ISSUE_ID`, `$WORKTREE_PATH`, and `Validate ru
On failure, stop the workflow with `@pm`'s `message` verbatim. Do **not** attempt to inspect or repair the TODO tree from the orchestrator — that belongs to `@pm`.
On success, capture `ISSUE_FILE_PATH` from the response. **Use this captured path verbatim everywhere downstream** (Phase 9 staging, Failure Handler comments, etc.) — never construct a TODO path from `$ISSUE_ID` directly.
On success, the orchestrator works exclusively from the structured `issue` object. **Every subsequent TODO operation re-dispatches `@pm` by issue ID** — the orchestrator never holds or passes around a file path.
If `issue.status == "Todo"`, dispatch `@pm` again to flip it to `In Progress` (operation: `Update status`, target: the same issue ID; `@pm` propagates to README.md / parent's `## Sub-issues` line). The status edit will be staged alongside other TODO updates in Phase 9.
@ -562,8 +561,6 @@ Dispatch `@pm` with the issue ID `$ISSUE_ID` and the following operations (a sin
`@pm` propagates status flips to the dependent index (the top-level README or the parent's `## Sub-issues` line) on its own — that's its job, not the orchestrator's. The orchestrator passes high-level intent ("set status to Done") and trusts `@pm` to update every dependent file.
`@pm`'s response includes the list of files it modified (absolute paths). Capture this list as `MODIFIED_TODO_PATHS` for the staging step below.
### File Follow-ups
Tracked-worthy unresolved items must become real TODO issues; otherwise they vanish into the per-run `summary.md` and the user (who has walked away) never sees them. Before writing the summary, scan the run for items in these categories and dispatch `@pm` to file each as a **sub-issue of the current issue** (`parent: $ISSUE_ID`).
@ -587,11 +584,14 @@ Tracked-worthy unresolved items must become real TODO issues; otherwise they van
### Commit TODO Changes
After both the TODO Update and File Follow-ups steps, stage every path returned by `@pm` in this run (the union of `MODIFIED_TODO_PATHS` and `NEW_SUBISSUE_PATHS` collected from each `@pm` dispatch). Commit them in a single atomic commit: `chore(todo): update <issue-id> status, file follow-ups`.
After both the TODO Update and File Follow-ups steps, dispatch `@pm` with operation `Commit pending changes` and the commit message constructed from the run context:
Equivalently — and more robustly, since the orchestrator can't have edited TODO files directly (Phase 1 verified the working tree was clean and the orchestrator never writes there) — stage the entire `TODO/` directory: `git add ./TODO/`. Anything staged under `TODO/` came from `@pm` during this run.
- If follow-ups were filed: `chore(todo): update <issue-id> status, file follow-ups`.
- Otherwise: `chore(todo): update <issue-id> status and progress`.
If no follow-ups were filed, the commit message simplifies to `chore(todo): update <issue-id> status and progress`.
`@pm` is responsible for persistence — the orchestrator does **not** run `git add` or `git commit` on TODO changes itself (per ADR-23). For the filesystem-backed `@pm`, the dispatch results in a single atomic commit on the feature branch; for tracker-backed `@pm` implementations (e.g. Linear), the dispatch is a no-op because the API calls already persisted the data.
Capture the returned `sha` (may be `null` for non-filesystem trackers) for the run summary's "final commit SHA(s)" field.
### Run Summary
- Write `$RUN_DIR/summary.md` with:
@ -616,7 +616,8 @@ At any phase, if an unrecoverable error occurs (or a routing rule explicitly abo
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.
4. Dispatch `@pm` (operation: `Add comment`, issue ID: `$ISSUE_ID`) summarising what failed and naming the abort reason if it was a routing-rule abort (e.g. `split_needed at Phase 7 task-1`, `plan_rework_remaining exhausted at Phase 8`). The orchestrator never constructs the issue file path — `@pm` resolves it.
5. Stop execution.
5. Dispatch `@pm` (operation: `Commit pending changes`, message: `chore(todo): record failure on <issue-id>`) so the failure note lands on the branch as a commit (per ADR-23). For tracker-backed `@pm` implementations this is a no-op. For filesystem `@pm`, the failure comment survives on the branch for the user to review before discarding the worktree.
6. Stop execution.
### Recovery procedure (workflow is non-resumable, ADR-14)