feat(opencode): hide TODO paths from orchestrator (ADR-22)
In recent runs the orchestrator skipped @pm and edited TODO/ files
itself, despite the workflow.md anti-pattern warning. Root cause: the
workflow doc literally taught the orchestrator the path layout
(`./TODO/<ID>.md`), making self-help a discoverable shortcut.
Fix: remove the recipe. The orchestrator now never constructs or reads
any per-issue TODO path. All TODO operations go through @pm dispatches;
@pm returns the absolute file path of every issue it touches, and the
orchestrator captures and reuses those paths downstream.
- Phase 1 loses the TODO-existence and depends-on checks (former steps
3 and 9 of the recent edit) — Phase 1 is now git/worktree-only.
- Phase 2 expands @pm's existing dispatch into a `Validate run
prerequisites` operation that returns either {ok: true,
issue_file_path, issue: {...}} or {ok: false, error_code, message}
with error_code in {tracker_missing, issue_not_found,
dependency_unmet, dependency_missing}. depends-on enforcement moves
here.
- Phase 7 split_needed exit, Phase 9 TODO Update, Phase 9 Commit TODO
Changes, and Failure Handler all reference @pm-returned paths or use
`git add ./TODO/` blanketly (safe because Phase 1 verified clean tree
and only @pm writes there during a run).
- pm.md gains a path-return rule: every read returns issue_file_path,
every write returns the modified paths. Run-Prerequisite Output
format documented with all four error codes.
- ADR-22 captures the rationale; routing matrix updates Phase 1/2 rows;
pipeline diagram labels updated.
The fix is discoverability-only — no permission deny on TODO/, per
explicit user direction. The schema lives in agents/pm.md, which the
orchestrator does not load.
Refs: config/opencode/workflow-design.md ADR-22
This commit is contained in:
parent
3e515d54eb
commit
a3e0de6d04
3 changed files with 140 additions and 44 deletions
|
|
@ -8,11 +8,11 @@ You are executing the multi-agent workflow inside the worktree this opencode ses
|
|||
**Prerequisites (the user handles before launching opencode):**
|
||||
- A git worktree is checked out for the issue's feature branch
|
||||
- `opencode` was launched from the root of that worktree
|
||||
- A `TODO/` directory is committed to the repo containing per-issue files (`TODO/<ID>.md`) plus `TODO/README.md`
|
||||
- A `TODO/` directory is committed to the repo with a per-issue tracker (schema in `agents/pm.md`) and a `TODO/README.md` index. The orchestrator does not read or construct per-issue paths — `@pm` is the only agent that touches issue files (ADR-22).
|
||||
|
||||
**Task reference:** $ARGUMENTS
|
||||
|
||||
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 already be tracked under `TODO/` (`@pm` validates existence at Phase 2). 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. Store as `ISSUE_ID`.
|
||||
|
||||
|
|
@ -72,38 +72,73 @@ Define `RUN_DIR="$WORKTREE_PATH/.workflow/run-$ISSUE_ID"` once in Phase 1 and re
|
|||
|
||||
## Phase 1: Sanity Check
|
||||
|
||||
This phase covers **only** git/worktree-shaped sanity. **TODO-tracker validation (issue file existence, `depends-on` enforcement) is `@pm`'s job and happens at Phase 2 (ADR-22)**. The orchestrator does not construct or read paths under `TODO/` at any point — it dispatches `@pm` and uses whatever path `@pm` returns.
|
||||
|
||||
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. 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/README.md` must exist. If not, stop: "TODO/README.md not found. Add the category index file before running the workflow."
|
||||
- `./TODO/$ISSUE_ID.md` must exist. If not, stop: "Issue file `./TODO/<ID>.md` not found for ID parsed from `$ARGUMENTS`."
|
||||
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."
|
||||
5. Capture the current branch: `BRANCH_NAME="$(git symbolic-ref --short HEAD)"`.
|
||||
6. Resolve the base branch (`BASE_BRANCH`):
|
||||
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. Capture the current branch: `BRANCH_NAME="$(git symbolic-ref --short HEAD)"`.
|
||||
5. Resolve the base branch (`BASE_BRANCH`):
|
||||
- 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 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>`."
|
||||
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. **Verify the working tree is clean** (ADR-20): `git status --porcelain` must return empty. If not, stop: "Working tree must be clean. Commit or stash uncommitted changes before running the workflow."
|
||||
9. **Check `depends-on:` declarations** (ADR-21): if `./TODO/$ISSUE_ID.md`'s frontmatter contains a `depends-on: [<ID>, ...]` list, verify every listed dependency's status is `Done` (read each `./TODO/<DEP-ID>.md`). If any dependency is not `Done`, stop: "Cannot start `$ISSUE_ID`; it depends on `<DEP-ID>` (status: `<status>`). Complete dependencies first." If a listed dependency file does not exist, stop with: "Cannot start `$ISSUE_ID`; declared dependency `<DEP-ID>` has no issue file." If the field is absent, proceed.
|
||||
10. 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.
|
||||
11. Initialize the run-level rework counter: `PLAN_REWORK_REMAINING=1` (per ADR-13). Decrement on every P5.5-BLOCK→P4, P7-escalation-exhaustion→P3, and P8-plan-level→P3 transition. When the counter is `0` and another such transition fires, abort to the Failure Handler instead of re-entering.
|
||||
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 working tree is clean** (ADR-20): `git status --porcelain` must return empty. If not, stop: "Working tree must be clean. Commit or stash uncommitted changes before running the workflow."
|
||||
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.
|
||||
9. Initialize the run-level rework counter: `PLAN_REWORK_REMAINING=1` (per ADR-13). Decrement on every P5.5-BLOCK→P4, P7-escalation-exhaustion→P3, and P8-plan-level→P3 transition. When the counter is `0` and another such transition fires, abort to the Failure Handler instead of re-entering.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Issue Context
|
||||
|
||||
Dispatch `@pm` against `./TODO/` (pass the absolute `TODO/` directory path) and fetch the issue at `./TODO/<ID>.md`:
|
||||
- Title, description, acceptance criteria (if section present)
|
||||
- Labels and parent
|
||||
- Sub-issues list (if the issue is a parent)
|
||||
- Existing status
|
||||
Dispatch `@pm` with the issue ID `$ISSUE_ID`, `$WORKTREE_PATH`, and `Validate run prerequisites` as the operation. **The orchestrator does not assume any path under the worktree's `TODO/` tree exists** — it asks `@pm` to:
|
||||
|
||||
If the issue file does not exist or `@pm` fails, stop with error.
|
||||
1. Verify the TODO tracker is well-formed in this worktree (directory + index file present).
|
||||
2. Locate the issue file for `$ISSUE_ID`.
|
||||
3. Verify all `depends-on:` entries in the issue's frontmatter resolve to issues with `status: Done` (ADR-21 / ADR-22).
|
||||
4. Return one of two structured responses:
|
||||
|
||||
If the issue's status is `Todo`, ask `@pm` to set it to `In Progress` and propagate the change to the dependent index (`README.md` for top-level issues, the parent's `## Sub-issues` line for sub-issues). The status edit will be staged alongside other TODO updates in Phase 9.
|
||||
**Success:**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"issue_file_path": "<absolute path to the issue file>",
|
||||
"issue": {
|
||||
"id": "...",
|
||||
"title": "...",
|
||||
"status": "Todo | In Progress | Done",
|
||||
"parent": "... | null",
|
||||
"labels": [...],
|
||||
"depends_on": [...],
|
||||
"description": "...",
|
||||
"acceptance_criteria": [{"checked": false, "text": "..."}],
|
||||
"sub_issues": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Failure:**
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error_code": "tracker_missing | issue_not_found | dependency_unmet | dependency_missing",
|
||||
"message": "<human-readable description>"
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**Forbidden in the orchestrator from this point forward:**
|
||||
- Reading any file inside the `TODO/` tree directly.
|
||||
- Constructing a per-issue file path from an issue ID — `@pm` is the only agent that knows the layout.
|
||||
- Editing or writing any file under `TODO/` — every TODO mutation is a `@pm` dispatch that returns the path of what it touched.
|
||||
|
||||
These rules are enforced by *not telling you the path layout*. The schema lives in `agents/pm.md`; the orchestrator never needs it.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -472,7 +507,7 @@ The Failure Handler's recovery procedure (ADR-14: discard worktree, delete branc
|
|||
Concretely on `split_needed`:
|
||||
|
||||
1. Write a Failure Handler summary noting `@check`'s diagnosis verbatim and the Phase 5 split that was attempted.
|
||||
2. Dispatch `@pm` to add a comment on `./TODO/$ISSUE_ID.md`: `- YYYY-MM-DD — split_needed at Phase 7 task-1; <one-line diagnosis>. Re-run after re-creating the worktree.`
|
||||
2. Dispatch `@pm` (operation: `Add comment`, issue ID: `$ISSUE_ID`) with the comment text: `- YYYY-MM-DD — split_needed at Phase 7 task-1; <one-line diagnosis>. Re-run after re-creating the worktree.` `@pm` resolves the issue file path itself; the orchestrator never constructs it.
|
||||
3. Stop execution. Do not commit code, do not file new sub-issues, do not stage anything under `.workflow/`.
|
||||
|
||||
---
|
||||
|
|
@ -516,15 +551,18 @@ The workflow is forge-agnostic. It commits locally and stops. **Do not push, and
|
|||
|
||||
### TODO Update
|
||||
|
||||
Dispatch `@pm` against the absolute `./TODO/` path. Ask it to:
|
||||
Dispatch `@pm` with the issue ID `$ISSUE_ID` and the following operations (a single dispatch can carry all of them — see `agents/pm.md` for the request shape):
|
||||
|
||||
1. **Check off the AC checkboxes that task-1 satisfied.** For each `- [ ]` AC line in `./TODO/$ISSUE_ID.md`'s `## Acceptance criteria` section that the implemented work fulfilled, flip to `- [x]`. The orchestrator decides which AC are satisfied by inspecting task-1's spec and verification output.
|
||||
2. **Set the issue's frontmatter `status` based on AC completion** (ADR-21, AC-driven):
|
||||
- **All AC are now `[x]`** → `status: Done`.
|
||||
- **Some AC remain `[ ]`** → `status: In Progress`. (Sub-issues filed at Phase 5.5 cover the unmet AC; the user runs them in subsequent invocations.)
|
||||
- **No AC section in the file** → `status: Done` (the parent had no testable AC; one task ran end-to-end).
|
||||
3. **Propagate any status flip to the dependent index:** `TODO/README.md` for top-level issues (`parent: null`), or the parent issue file's `## Sub-issues` line for sub-issues.
|
||||
4. **Add a comment** of the form: `- YYYY-MM-DD — Branch \`$BRANCH_NAME\`, commit <SHA> — <one-line summary>` (date from the shell, never fabricated).
|
||||
1. **Check off the AC the run satisfied.** Pass the list of AC indices or texts (from the `acceptance_criteria` array `@pm` returned at Phase 2) that the implemented work fulfilled. The orchestrator decides which AC are satisfied by inspecting task-1's spec and verification output. `@pm` flips the corresponding `- [ ]` to `- [x]`.
|
||||
2. **Set the issue's `status` based on AC completion** (ADR-21, AC-driven):
|
||||
- **All AC are now `[x]`** → `Done`.
|
||||
- **Some AC remain `[ ]`** → `In Progress`. (Sub-issues filed at Phase 5.5 cover the unmet AC; the user runs them in subsequent invocations.)
|
||||
- **No AC section** → `Done` (the parent had no testable AC; one task ran end-to-end).
|
||||
3. **Add a comment** of the form: `- YYYY-MM-DD — Branch \`$BRANCH_NAME\`, commit <SHA> — <one-line summary>` (date from the shell, never fabricated).
|
||||
|
||||
`@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
|
||||
|
||||
|
|
@ -549,9 +587,11 @@ 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, commit everything under `TODO/` in a single atomic commit: `chore(todo): update <issue-id> status, file follow-ups`. Stage the worked issue file, the dependent index (README.md or parent file), and any newly created follow-up issue files.
|
||||
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`.
|
||||
|
||||
If no follow-ups were filed, the commit message simplifies to `chore(todo): update <issue-id> status and progress` and only the TODO Update changes are staged.
|
||||
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 no follow-ups were filed, the commit message simplifies to `chore(todo): update <issue-id> status and progress`.
|
||||
|
||||
### Run Summary
|
||||
- Write `$RUN_DIR/summary.md` with:
|
||||
|
|
@ -575,7 +615,7 @@ At any phase, if an unrecoverable error occurs (or a routing rule explicitly abo
|
|||
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 `.workflow/` and `TODO/`.
|
||||
3. Leave the branch and worktree intact for the user to inspect — do not push, do not delete.
|
||||
4. Dispatch `@pm` against `./TODO/` to add a comment on the issue file (`./TODO/<ID>.md`) 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`).
|
||||
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.
|
||||
|
||||
### Recovery procedure (workflow is non-resumable, ADR-14)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue