forgedock 1.8.1 → 1.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/bin/engine/admission.mjs +57 -1
  2. package/bin/engine/admission.test.mjs +60 -0
  3. package/commands/analytics.md +3 -1
  4. package/commands/audit-agents.md +3 -1
  5. package/commands/audit.md +10 -8
  6. package/commands/autopilot.md +16 -14
  7. package/commands/ci-audit.md +4 -2
  8. package/commands/cleanup.md +1 -1
  9. package/commands/deploy-pr.md +31 -11
  10. package/commands/design-bench.md +1 -0
  11. package/commands/design-render-critique-loop.md +1 -0
  12. package/commands/design.md +1 -0
  13. package/commands/forge-stats.md +1 -0
  14. package/commands/geo-audit.md +3 -1
  15. package/commands/incident-response.md +3 -1
  16. package/commands/issue.md +4 -2
  17. package/commands/milestone.md +8 -6
  18. package/commands/orchestrate/config.md +3 -2
  19. package/commands/orchestrate/phase-1-resolve.md +57 -12
  20. package/commands/orchestrate/phase-2-triage.md +1 -0
  21. package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
  22. package/commands/orchestrate/phase-3-dependency.md +17 -5
  23. package/commands/orchestrate/phase-4-execution.md +625 -78
  24. package/commands/orchestrate/phase-5-cleanup.md +5 -2
  25. package/commands/orchestrate/phase-6-report.md +8 -3
  26. package/commands/orchestrate/safety.md +1 -0
  27. package/commands/orchestrate.md +13 -9
  28. package/commands/pipeline-health/config.md +1 -0
  29. package/commands/pipeline-health/phase-1-context.md +1 -0
  30. package/commands/pipeline-health/phase-2-metrics.md +1 -0
  31. package/commands/pipeline-health/phase-3-analyze.md +1 -0
  32. package/commands/pipeline-health/phase-4-proposals.md +7 -4
  33. package/commands/pipeline-health/phase-5-report.md +5 -2
  34. package/commands/pipeline-health/phase-6-summary.md +1 -0
  35. package/commands/pipeline-health.md +9 -7
  36. package/commands/pipeline-resume.md +4 -2
  37. package/commands/qa-sweep.md +3 -1
  38. package/commands/quality-gate.md +4 -3
  39. package/commands/recover-orphans.md +219 -13
  40. package/commands/review-pr-agents/api.md +1 -0
  41. package/commands/review-pr-agents/auth.md +1 -0
  42. package/commands/review-pr-agents/billing.md +1 -0
  43. package/commands/review-pr-agents/concurrency.md +1 -0
  44. package/commands/review-pr-agents/database.md +1 -0
  45. package/commands/review-pr-agents/frontend.md +1 -0
  46. package/commands/review-pr-agents/infra.md +1 -0
  47. package/commands/review-pr-agents/protocols.md +1 -0
  48. package/commands/review-pr-agents/scraper.md +1 -0
  49. package/commands/review-pr-agents/security.md +1 -0
  50. package/commands/review-pr-agents/spec-cli.md +1 -0
  51. package/commands/review-pr-agents.md +10 -4
  52. package/commands/review-pr-staging.md +14 -5
  53. package/commands/review-pr.md +187 -19
  54. package/commands/rollback.md +3 -1
  55. package/commands/security-audit.md +3 -1
  56. package/commands/signal-planner.md +6 -4
  57. package/commands/sync-ecosystem.md +4 -2
  58. package/commands/test-gate.md +4 -2
  59. package/commands/upgrade-deps.md +3 -1
  60. package/commands/validate.md +3 -1
  61. package/commands/work-on/build/architect.md +82 -19
  62. package/commands/work-on/build/context.md +62 -18
  63. package/commands/work-on/build/implement.md +63 -18
  64. package/commands/work-on/build/validate.md +241 -60
  65. package/commands/work-on/build.md +345 -146
  66. package/commands/work-on/close.md +618 -246
  67. package/commands/work-on/decompose.md +240 -41
  68. package/commands/work-on/investigate.md +378 -61
  69. package/commands/work-on/remediate.md +43 -9
  70. package/commands/work-on/review.md +241 -29
  71. package/commands/work-on.md +180 -2014
  72. package/docs/CODEX.md +2 -0
  73. package/docs/CONFIG.md +11 -0
  74. package/package.json +1 -1
  75. package/scripts/build-spec-graph.mjs +11 -4
  76. package/scripts/check-spec-markers.sh +21 -1
  77. package/scripts/checkbox-sections.test.sh +8 -23
  78. package/scripts/extract-affected-files.sh +5 -2
  79. package/scripts/extract-affected-files.test.sh +27 -0
  80. package/scripts/forge-root.test.sh +176 -0
  81. package/scripts/is-docs-only.sh +31 -0
  82. package/scripts/is-docs-only.test.sh +64 -0
  83. package/scripts/lint-dispatch-prompt.sh +217 -0
  84. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  85. package/scripts/validate-spec-graph.sh +3 -2
  86. package/scripts/verify-phase-trail.sh +282 -0
  87. package/scripts/verify-phase-trail.test.sh +298 -0
  88. package/scripts/wait-ci-green.sh +98 -0
  89. package/scripts/wait-ci-green.test.sh +99 -0
  90. package/commands/work-on-monolithic.md +0 -326
@@ -19,116 +19,92 @@ Orchestrator for the full issue lifecycle: investigate → decompose (if needed)
19
19
 
20
20
  ## HARD RULES — READ BEFORE ANYTHING ELSE
21
21
 
22
- 1. **Every sub-phase MUST be invoked via `Skill(...)`.** You do NOT implement inline. You invoke `Skill(skill="work-on/investigate", ...)`, `Skill(skill="work-on/build", ...)`, etc. The Skill tool invocation is what triggers label updates, FORGE annotations, and structured output. Without it, the phase has no paper trail.
22
+ 1. **This file is a router. Every phase runs as a forked sub-skill via `Skill(...)`; the router never executes phase logic itself.** Each phase sub-skill (`work-on:investigate`, `work-on:decompose`, `work-on:build`, `work-on:review`, `work-on:close`, `work-on:remediate`) declares `context: fork`, so Claude Code runs it in an isolated sub-agent context that holds only that phase's spec and the args you pass. The phase re-reads everything else from GitHub/git and returns exactly one `*_RESULT:` block — that block is all the router sees. Do not read phase spec files, do not re-implement a phase step here, and do not "help" a phase by posting its markers or labels. If a skill name does not resolve, STOP with "skill not found" (see Skill Name Resolution) — never run a phase inline.
23
23
 
24
- 2. **Write to GitHub after EVERY phase.** Every FORGE annotation (HEARTBEAT, INVESTIGATOR, CONTRACT, BUILDER, etc.) must be posted before the next phase starts. A phase that completes without a GitHub write is effectively invisible to the stall detector and future sessions.
24
+ 2. **Route only on `*_RESULT` blocks and GitHub state.** After each phase returns, apply the routing table for that phase (below). Never infer success from a phase's prose; never continue past a `BLOCKED`/`FAILED` result.
25
25
 
26
- 3. **Follow the Universal Phase Dispatcher.** The phase sequence table is the SINGLE source of truth for transitions. Do NOT skip phases, do NOT reorder phases, do NOT treat intermediate completions as terminal. Only the terminal states listed in the Dispatcher allow stopping.
26
+ 3. **Follow the Phase Dispatcher.** Do not skip, reorder, or treat an intermediate completion as terminal. Only the terminal states listed in the Dispatcher allow stopping. Between phases: no narration, no summary, no end of turn — invoke the next phase immediately.
27
27
 
28
- 4. **PRs NEVER target `main`.** Target `staging` (fast lane) or `milestone/{slug}` (feature lane). A PR to main is a pipeline violation regardless of what the issue description says.
28
+ 4. **PRs NEVER target `main`.** Target `staging` (fast lane) or `milestone/{slug}` (feature lane). The router computes and validates the target (Lane Resolution) and passes it to the phases as `--base`.
29
29
 
30
- ### Compaction Resilience
30
+ 5. **`needs-human` is for genuine decisions and external actions only.** A phase that can fix its own problem (failing checks, review findings, a moved PR head, a missing marker it can produce) does so inside its own fork; the router never adds `needs-human` for a condition a phase reported as fixable.
31
31
 
32
- 1. Write state to GitHub after EVERY significant step
33
- 2. Read full GitHub state (issue body + comments + labels) ONCE, in Phase 0B. Carry those values in-context for the rest of the run — do NOT re-fetch them at later phase boundaries. The only trigger for a fresh re-read of something already read this session is a **detected compaction**: if a later phase needs a value (issue body, a specific FORGE:* comment, labels) that this session does not actually have in its visible context — because compaction dropped the earlier tool output, or this is a brand-new session resuming mid-pipeline — re-fetch ONLY that missing value, not the full state.
34
- 3. After compaction: re-read issue (body + comments + labels) to reconstruct state
35
- 4. Key principle: A NEW session running `/work-on {number}` should pick up where the last left off by reading GitHub state alone
32
+ ### Skill Name Resolution (`{FORGE_SKILL_PREFIX}`)
36
33
 
37
- **Session state cache convention**: Phases 1A.5, 3A, and 5A previously re-ran `gh issue view`/`gh api .../comments` unconditionally "for safety." That default is now inverted: reuse the value if this session already produced it (e.g., you already have `ISSUE_BODY` from Phase 0B, or already read the FORGE:INVESTIGATOR comment during Phase 1). Only issue the `gh` call if the value is genuinely absent from context. This is what makes re-reads free for a normal single-turn run while remaining exactly as resilient across a real compaction or resume. (Phase 6A's body read is a deliberate exception — see its note — because it immediately precedes a body-mutating write and Phase 5 can involve an external `/review-pr` process.)
34
+ ForgeDock skills register under different names per install:
38
35
 
39
- ### Orchestration Flag
36
+ | Runtime / install | `{FORGE_SKILL_PREFIX}` | Nesting separator | `work-on` / `work-on` + `build` |
37
+ |---|---|---|---|
38
+ | Claude Code plugin | `forgedock:` | `:` | `forgedock:work-on` / `forgedock:work-on:build` |
39
+ | Claude Code `install.sh` symlinks (`~/.claude/commands/work-on/build.md`) | empty | `:` | `work-on` / `work-on:build` |
40
+ | Codex (`install-codex.sh`) | `forge-` | `-` | `forge-work-on` / `forge-work-on-build` |
41
+ | OpenCode (`forgedock opencode install`) | empty | `-` | `work-on` / `work-on-build` |
40
42
 
41
- `UNDER_ORCHESTRATION` — resolved once in Phase 0A. Defaults to `false` (solo run). Set to `true` when the invocation args include `--under-orchestration` (this is how `/orchestrate` dispatches `/work-on`; see `commands/orchestrate/phase-4-execution.md`). This flag gates the 4 heartbeat comments below — they exist solely to feed `/orchestrate`'s Step 4B.5 time-based stall detector (which reads the last comment's `updated_at`) and have no consumer in a solo run.
43
+ Every `Skill(skill="...")` call to a ForgeDock skill (any command under `commands/`: `work-on*`, `review-pr*`, `quality-gate`, `issue`, `orchestrate`, `cleanup`, `deploy-pr`, etc.) is written `{FORGE_SKILL_PREFIX}<name>`, with `:` between nesting levels as the canonical spelling. Resolve the prefix AND the nesting separator ONCE per run, before the first Skill dispatch. Substitute the prefix literally; when the separator is `-`, also rewrite every `:` nesting level in the name to `-` (`{FORGE_SKILL_PREFIX}work-on:build` becomes `forge-work-on-build`). Apply the result to every call and every sub-agent prompt:
42
44
 
43
- ### Universal Phase Dispatcher
45
+ 1. If env `FORGE_SKILL_NAMESPACE` is set: `forgedock` → prefix `forgedock:`, separator `:`; `none` → empty prefix, separator `:`; `codex` → prefix `forge-`, separator `-`; `opencode` → empty prefix, separator `-`. Any other value is an error.
46
+ 2. Else if env `FORGE_RUNTIME` is `codex` or `opencode`, use that runtime's row above.
47
+ 3. Else read the available-skills list: if it contains `forgedock:work-on` → `forgedock:`/`:`; else if it contains `forge-work-on` → `forge-`/`-`; else if it contains `work-on:build` → empty/`:`; else if it contains `work-on-build` → empty/`-`; else if it contains `work-on` → empty/`:`.
48
+ 4. If none of these resolves, or a later `Skill(...)` call reports the resolved name unknown, this is a **HARD ERROR**: STOP and report "skill not found: <name>" (post a `needs-human` comment when running against an issue). NEVER fall back to running the phase inline or via the Agent tool — an inline phase has no paper trail.
44
49
 
45
- <!-- FORGE:DISPATCHER — This is the SINGLE source of truth for phase transitions. Every phase boundary references this section. -->
50
+ A sub-agent that receives no resolved value applies the same rule itself.
46
51
 
47
- **Phase sequence** (canonical order):
52
+ ### Compaction Resilience
48
53
 
49
- | Step | Phase | Entry Condition | Terminal? |
50
- |------|-------|-----------------|-----------|
51
- | 1 | Phase 0: Resolve Issue | Always first | No |
52
- | 2 | Phase 1: Investigation | No `INVESTIGATION:COMPLETE` comment | No |
53
- | 3 | Phase 1D: Route | Investigation complete | No |
54
- | 4 | Phase 2: Decomposition | decompose: YES | Yes (spawns sub-issues) |
55
- | 5 | Phase 3: Build (3A–3M) | `workflow:ready-to-build` or `workflow:building` | No |
56
- | 6 | Phase 4: PR Creation | Builder comment posted, no PR exists | No |
57
- | 7 | Phase 5: Auto-Review | PR exists, `workflow:in-review` | No |
58
- | 8 | Phase 6: Close & Cleanup | PR merged | No |
59
- | 9 | Phase 7: Trajectory | Issue closed | Yes |
54
+ 1. Every phase writes its state to GitHub (FORGE annotations + `workflow:*` labels) before returning.
55
+ 2. The router keeps only small routing values in context: `NUMBER`, `GH_REPO`, `GH_FLAG`, `PR_BASE`, `CLASSIFIED_LANE`, `BRANCH`, `WORKTREE_PATH`, `PR_NUMBER`, `UNDER_ORCHESTRATION`. Phase results carry the rest.
56
+ 3. After a compaction (or in a new session), re-run Phase 0: it reconstructs the resume point from GitHub state alone, and every phase is idempotent (each starts with its own resume check).
57
+ 4. **Shell state does not persist between Bash tool calls.** Any bash snippet below that uses `resolve_script`/`FORGE_ROOT` must include the Script resolution block from Phase 0 in the same command; substitute `{PLACEHOLDER}` values literally.
60
58
 
61
- **Phase 3 sub-phase sequence** (execute in order; 3C.5 and 3C.6 are conditional — see Phase 3B):
59
+ ### Orchestration Flag
62
60
 
63
- 3A → 3B → [3C → 3C.5* → 3C.6*] → 3D → 3E → 3F → 3F.5 → 3G → 3H → 3I → 3I.5 → 3J → 3K → 3L → 3M
61
+ `UNDER_ORCHESTRATION` — resolved once in Phase 0A. Defaults to `false` (solo run). Set to `true` when the invocation args include `--under-orchestration` (this is how `/orchestrate` dispatches `/work-on`; see `commands/orchestrate/phase-4-execution.md`). This flag gates the phase-entry heartbeat comments posted by the router (Phases 1, 3 and 5 below, plus Phase 0A.5); solo runs skip them.
64
62
 
65
- *3C.5 and 3C.6 are skipped for TRIVIAL tasks; 3C (Builder Contract) is still required. Investigation tasks exit at 3B before 3C.
63
+ ### Phase Dispatcher
66
64
 
67
- **Universal continuation rule**: After ANY phase or sub-phase completes, check whether a terminal state has been reached. Terminal states are:
68
- - `workflow:merged` label is set
69
- - `workflow:invalid` label is set
70
- - `needs-human` label is set
71
- - `workflow:awaiting-merge` label is set (remediated + re-reviewed, awaiting a human merge decision — see #1810) <!-- Added: forge#1810 -->
72
- - `workflow:decomposed` label is set (sub-issues spawned)
73
- - Issue state is CLOSED with terminal label
65
+ <!-- FORGE:DISPATCHER — This is the SINGLE source of truth for phase transitions. -->
74
66
 
75
- **If the current state is NOT terminal: proceed to the next phase in the sequence immediately. Do NOT stop. Do NOT emit a summary. Do NOT treat any intermediate phase completion as a terminal signal.** Every phase completion — investigation done, quality gate passed, PR merged, review complete — is an intermediate result. Only the terminal states listed above allow stopping.
67
+ | Step | Phase | Skill (forked) | Entry condition |
68
+ |------|-------|----------------|-----------------|
69
+ | 0 | Resolve & resume | — (router) | Always first |
70
+ | 1 | Investigate | `work-on:investigate` | No `FORGE:INVESTIGATOR` comment containing `INVESTIGATION:COMPLETE` or `INVESTIGATION:INVALID` |
71
+ | 2 | Decompose | `work-on:decompose` | `INVESTIGATE_RESULT.decompose: YES` (or resume: investigation says decompose YES and no `FORGE:DECOMPOSED`) |
72
+ | 3 | Build | `work-on:build` | Investigation complete, decompose NO, no `FORGE:BUILDER:COMPLETE` |
73
+ | 4 | Review (push, PR, review, merge) | `work-on:review` | `FORGE:BUILDER:COMPLETE` present and PR not merged |
74
+ | 5 | Close & trajectory | `work-on:close` | PR merged, or a PR-less terminal outcome (investigation deliverables, decomposed, invalid) |
76
75
 
77
- **Adding a new phase**: Insert it into the phase sequence table above and the sub-phase sequence if it belongs to Phase 3. No per-boundary transition code is needed — the universal continuation rule handles all transitions.
76
+ **Terminal states** (only these allow stopping): `workflow:merged` with the issue closed; `workflow:invalid`; `workflow:decomposed` (sub-issues own the work); `needs-human`; `workflow:awaiting-merge` (held for a human merge decision on the deploy gate); a `CLOSE_RESULT` with `status: COMPLETE | ALREADY_DONE`. Anything else → run the next phase immediately.
78
77
 
79
78
  ---
80
79
 
81
80
  ## Spawn-Decision Policy
82
81
 
83
- <!-- FORGE:SPAWN_POLICY — Canonical spawn-decision table. Sibling specs (orchestrate.md, review-pr.md) link to this section. Sub-issues #1276–#1279 reference this table. -->
82
+ <!-- FORGE:SPAWN_POLICY — Canonical spawn-decision table. Sibling specs (orchestrate.md, review-pr.md) link to this section. -->
84
83
 
85
- **Default: run inline.** Every skill, phase, and sub-agent runs inline in the current context unless one of the four criteria below explicitly applies. A sub-agent buys exactly three things — parallelism, context isolation, and (below) prompt-cache preservation. If none is needed, forking is waste.
84
+ **Phases are forked by declaration, not by judgement.** Every `/work-on` phase sub-skill declares `context: fork` in its frontmatter, so each phase (and each build child: context, architect, implement, validate, and the quality gate) runs in its own isolated context with only its own spec loaded. This replaces the former "default inline" rule and its Row (c)/(d) exceptions: phase-at-a-time spec delivery is what keeps every worker's context small and its instructions unambiguous (field evidence: inline delivery put 230k–360k tokens of overlapping spec into each orchestrated worker and produced phase shortcuts).
86
85
 
87
- ### Spawn-Decision Table
86
+ `Agent(...)` sub-agents remain the mechanism for work that is not a phase:
88
87
 
89
- | Row | Criterion | Fork? | Example |
90
- |-----|-----------|-------|---------|
91
- | a | **Parallel fan-out** — two or more independent work units can execute concurrently and the total wall time saving justifies the fork overhead | YES — spawn one sub-agent per work unit | `/orchestrate` dispatching multiple `/work-on` agents; `review-pr` spawning domain-specific reviewers in parallel |
92
- | b | **Fresh-context isolation** — the work unit is a structured review or audit whose value depends on seeing the artefact without the builder's accumulated context bias, AND the review result is load-bearing for the merge decision | YES — spawn a dedicated sub-agent | Phase 5C review-fork when build context is large (see Row c for the quantitative threshold) |
93
- | c | **Parent context near overflow** — the parent agent has made ≥20 Skill invocations OR the build changed ≥10 files, meaning delegating review inline risks a mid-review token overflow | YES — spawn a fresh sub-agent for review | Phase 5C: `Skill(skill="work-on/review", …)` instead of direct `review-pr` invocation |
94
- | d | **Prompt-cache TTL** — the sub-operation (multi-domain review, multi-iteration quality-gate) is expected to run for several minutes, longer than Anthropic's ~5-minute prompt-cache TTL. Leaving it inline lets the parent's already-large accumulated context (investigation/contract/context/architect/implement annotations) sit idle past the TTL; the parent's next turn then re-hydrates that entire context **uncached**, roughly doubling effective token cost for that turn. This is independent of build size — a 1-file fix idles the parent exactly as long as a 20-file one once the sub-operation starts running | YES — spawn a fresh sub-agent, **unconditionally**, regardless of file count or Skill-invocation count | Phase 5C review (always forks — Row d supersedes Row c's threshold as the controlling reason); Phase 3G quality-gate loop (forks under Row d even though it never qualified under Row c) <!-- Added: forge#1825 --> |
95
-
96
- **If none of the four rows match: run inline.** Do not fork for convenience, narrative clarity, or to avoid reading a large file. Context cost of a fork (spawning, context reconstruction, result aggregation) is paid every time, even when parallelism, isolation, or cache preservation adds no value.
88
+ | Row | Criterion | Example |
89
+ |-----|-----------|---------|
90
+ | a | **Parallel fan-out** — independent work units run concurrently | `/orchestrate` dispatching one `/work-on` worker per issue; `review-pr` dispatching domain reviewers |
91
+ | b | **Fresh-context isolation for a load-bearing review** | `review-pr` domain reviewers (they must not see the builder's reasoning) |
97
92
 
98
93
  ### Depth Budget
99
94
 
100
- **Available depth**: 5 levels (since Claude Code v2.1.172).
101
-
102
- **Target depth for a standard run**: ≤ 3.
103
-
104
95
  | Depth | Agent | Notes |
105
96
  |-------|-------|-------|
106
- | 1 | `/orchestrate` | Top-level dispatcher — never implements directly |
107
- | 2 | `/work-on` | Issue pipeline — runs build phases inline |
108
- | 3 | Parallel reviewers (Row a/b/c/d fork); quality-gate loop (Row d fork) | Domain review agents spawned by Phase 5C; quality-gate sub-agent spawned by Phase 3G |
109
-
110
- **Build phases (3A–3M) run inline at depth 2** — they are sequential sub-phases of `/work-on`, not independent agents. Forking the build into a sub-agent (depth 3) is a violation of Row a/b/c/d unless the build itself fans out independently scoped work units, **except** the Phase 3G quality-gate loop, which forks unconditionally under Row d (cache-TTL economics) regardless of build size. <!-- Updated: forge#1825 -->
111
-
112
- **Depth 4–5 are reserved** for exceptional cases (e.g. an orchestrate agent that itself spawns an orchestrate agent for a sub-milestone). Agents that reach depth 4 MUST log a justification comment on the relevant issue.
113
-
114
- ### Phase 5C Cross-Reference
115
-
116
- The Phase 5C review fork is now unconditional, controlled by **Row (d)** (prompt-cache TTL economics — independent of build size). It was previously gated by **Row (c)** alone (≥10 changed files OR ≥20 Skill invocations, i.e. parent-context-overflow risk); that threshold is preserved as a *documented, non-gating* reason the fork is also correct for large builds, but no longer determines *whether* the fork happens — Row (d) means it always does. <!-- Updated: forge#1825 (previously: "quantitative thresholds are not changed") -->
117
-
118
- ### Phase 3G Cross-Reference
119
-
120
- The Phase 3G quality-gate loop forks unconditionally under **Row (d)**: it scans 14+ domains across up to 3 iterations, long enough to idle the parent's accumulated context past the prompt-cache TTL regardless of how many files changed. Unlike Phase 5C, quality-gate never had a Row (c) (file-count) exception — Row (d) is the sole justification for forking it. <!-- Added: forge#1825 -->
97
+ | 1 | `/orchestrate` (or a solo `/work-on` session) | Never implements directly |
98
+ | 2 | `/work-on` router (an orchestrated worker) | Routes only |
99
+ | 3 | Forked phase (`work-on:<phase>`) | One at a time; `review-pr` runs inside `work-on:review`'s fork |
100
+ | 4 | Forked build child (context, architect, implement, validate); domain reviewers dispatched by `review-pr` | |
101
+ | 5 | Quality gate (forked, inside validate) | Deepest level used |
121
102
 
122
103
  ### Model and Effort Tiering — What Actually Applies
123
104
 
124
105
  <!-- FORGE:MODEL_TIER_NOTE — Canonical explanation of the real vs. aspirational tiering mechanism. Every work-on/*.md "Agent model policy" line cross-references this section instead of restating it. -->
125
106
 
126
- Every `work-on/*.md` sub-phase file carries an "Agent model policy" line naming a `model` and an `effort`. These are two different mechanisms with two different scopes, and only one of them changes anything for an in-process `Skill()` call:
127
-
128
- - **`effort` is real and applies per `Skill()` invocation.** It is genuine reasoning-depth tuning on the model already running the session, gated correctly on Claude Code >= 2.1.154. Setting `effort: low` on a sub-phase file that is mechanical end-to-end (deterministic label edits, annotation posting, board sync — no root-cause analysis, no architecture planning) is a real, no-fork-required cost reduction.
129
- - **`model` overrides are NOT functional for `Skill()`-dispatched sub-phases.** The `Skill` tool executes "within the main conversation" — it has no model parameter and does not fork a new agent/session, so a sub-phase file cannot switch the model that's already generating the current run. The only tool with real model-override semantics is `Agent(model=...)` (see `tool_param_value_permission_rules`, Claude Code >= 2.1.178) — and HARD RULE #2 above explicitly forbids using the `Agent` tool for `/work-on` sub-phase dispatch, to keep the FORGE-annotation paper trail and phase protocol intact. In practice, the only place a `/work-on` run's model is genuinely chosen is `/orchestrate`'s single `Agent(model=..., ...)` spawn for the entire run (see `commands/orchestrate/phase-4-execution.md`) — every internal `Skill()` call inside that run inherits that same model, with no per-sub-phase override.
130
-
131
- **What this means concretely**: a sub-phase file's "Agent model policy" line documents *effort* tiering that genuinely takes effect, plus a *model* value that is aspirational/no-op unless that file is one day dispatched via `Agent(...)` instead of `Skill(...)`. Because mechanical bits (label transitions, `FORGE:CHECKPOINT` writes, heartbeat posts, task-type classification) are interleaved with reasoning-heavy content within the same `Skill()`-invoked file in most sub-phases, they cannot be selectively downtiered without either degrading the reasoning phases sharing that file, or extracting them into a brand-new fork — which the Spawn-Decision Policy above correctly discourages for operations this small (a single `gh issue edit` call does not clear Row a/b/c/d). Do not add a `model: "haiku"` claim to a sub-phase file expecting it to have effect; only add `effort: low`, and only when the whole file is mechanical end-to-end. <!-- Added: forge#1827 -->
107
+ Every `work-on/*.md` phase file carries an "Agent model policy" line naming a `model` and an `effort`. Phase files now run with `context: fork` (an isolated sub-agent per phase), so the line documents the intended tier for that phase. `effort` applies per invocation on Claude Code >= 2.1.154. A `model` value only takes effect where the runtime honours it for a forked skill or an `Agent(model=...)` dispatch; otherwise the phase runs on the session's model. Mechanical work (labels, annotations, heartbeats) lives in the same phase file as the reasoning it belongs to, so no separate tiering split is needed.
132
108
 
133
109
  ---
134
110
 
@@ -236,7 +212,7 @@ if [ "$PREFLIGHT_FAILED" -eq 1 ]; then
236
212
  fi
237
213
  ```
238
214
 
239
- Worktree/branch-already-exists and stale-label conditions are surfaced later (Phase 3E worktree creation and the `## Error Handling` section) with their own recovery guidance in `docs/site/troubleshooting.md`.
215
+ Worktree/branch-already-exists and stale-label conditions are surfaced later (by the build phase's worktree step and the `## Error Handling` section) with their own recovery guidance in `docs/site/troubleshooting.md`.
240
216
 
241
217
  ### 0A: Parse input
242
218
  Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human`, `workflow:decomposed`, and `workflow:awaiting-merge`, pick highest priority.
@@ -253,9 +229,9 @@ Extract project prefix and issue number. If `next`/`pick`: list open issues sort
253
229
 
254
230
  ```bash
255
231
  if echo "$ARGUMENTS" | grep -qE -- '--remediate\b'; then
256
- REMEDIATE_PR_NUMBER=$(echo "$ARGUMENTS" | grep -oP '^\s*\K[0-9]+' | head -1)
232
+ REMEDIATE_PR_NUMBER=$(echo "$ARGUMENTS" | sed -n 's/^[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -1)
257
233
  REMEDIATE_ISSUE_FLAG=""
258
- REMEDIATE_ISSUE_NUMBER=$(echo "$ARGUMENTS" | grep -oP -- '--issue\s+\K[0-9]+' | head -1)
234
+ REMEDIATE_ISSUE_NUMBER=$(echo "$ARGUMENTS" | sed -n 's/.*--issue[[:space:]][[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -1)
259
235
  [ -n "$REMEDIATE_ISSUE_NUMBER" ] && REMEDIATE_ISSUE_FLAG="--issue ${REMEDIATE_ISSUE_NUMBER}"
260
236
 
261
237
  if [ -z "$REMEDIATE_PR_NUMBER" ]; then
@@ -270,12 +246,12 @@ fi
270
246
  If detected, dispatch immediately and STOP — do NOT fall through to Phase 0B's normal issue-number resume logic (an issue number is not even known yet; `work-on/remediate.md` Phase M0 resolves it):
271
247
 
272
248
  ```
273
- Skill(skill="work-on/remediate", args="${REMEDIATE_PR_NUMBER} ${REMEDIATE_ISSUE_FLAG} --repo {GH_REPO} --gh-flag {GH_FLAG}")
249
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="${REMEDIATE_PR_NUMBER} ${REMEDIATE_ISSUE_FLAG} --repo {GH_REPO} --gh-flag {GH_FLAG}")
274
250
  ```
275
251
 
276
- **After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
252
+ **After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("{FORGE_SKILL_PREFIX}work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
277
253
 
278
- This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated predecessor's own PR).
254
+ This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated predecessor's own PR).
279
255
 
280
256
  **Skip this entire section if `--remediate` is absent from `$ARGUMENTS`** — proceed to the normal parse below.
281
257
 
@@ -293,7 +269,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
293
269
  **Issue**: #{NUMBER}"
294
270
  ```
295
271
 
296
- **Also post at major phase entry points** (Phases 1, 3, and 5) — replace `Phase 0` with the correct phase name in each case, and same `UNDER_ORCHESTRATION` gate. These mid-pipeline heartbeats ensure the stall detector sees recent activity during long phases (e.g., a build phase running for 20 minutes is not falsely classified as stalled). Inline snippets are embedded at Phase 1A, Phase 3A, and Phase 5A — agents resuming mid-pipeline encounter them without reading this section. <!-- Added: forge#740 -->
272
+ **Also posted at phase entry points** (Phases 1, 3 and 4) by the router — see "Phase Heartbeat (router-owned)" below. Phases never post heartbeats. <!-- Added: forge#740 -->
297
273
 
298
274
  **Skip if**: Issue already has a terminal label (`workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`) — no heartbeat needed on a completed issue. (This is in addition to, not instead of, the `UNDER_ORCHESTRATION` gate above.)
299
275
 
@@ -305,7 +281,9 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments --jq '.[] | {id: .id, author: .u
305
281
 
306
282
  **Check**: state (closed → STOP), terminal labels (`workflow:merged`/`workflow:invalid`/`workflow:awaiting-merge` → STOP), existing agent comments (`FORGE:INVESTIGATOR`, `FORGE:DECOMPOSED`, `FORGE:CONTRACT`, `FORGE:BUILDER`, `FORGE:TRAJECTORY`, `FORGE:DECISION_RECORD`), parent tracker status, sub-issue status.
307
283
 
308
- **Determine resume point**: No comments → Phase 1. Investigation exists + ready-to-build → Phase 3. Builder:COMPLETE + no PR → Phase 4. Builder without :COMPLETE (partial/interrupted build) + no PR → Phase 3 (partial-build cleanup). Builder + PR open → Phase 5. PR merged + issue open → Phase 6.
284
+ **Resume preflight (MANDATORY on any resume past Phase 1)** <!-- Added: forge#3061 -->: before routing to Phase 3 (build), 4 (review) or 5 (close) from existing state, run `bash "$FORGE_ROOT/scripts/verify-phase-trail.sh" {NUMBER} -R {GH_REPO}` (add `--docs-only` for docs-only diffs, or `--code-diff` when the diff has any non-docs file so an INVESTIGATION band cannot waive requirements), with `FORGE_ROOT` resolved by the bootstrap in "Script resolution" below. If `FORGE_ROOT` is empty or the script is missing, treat it as `PHASE_TRAIL: ERROR` (fail closed: stop and add `needs-human`; never skip the preflight). On `PHASE_TRAIL: FAIL`, go BACK and run each missing phase through its `Skill(...)` (investigate, Phase 3B classification, build contract/context/architect, validate) before continuing — never continue forward over a gap, never hand-post a missing marker, and never treat a recovered uncommitted worktree as a substitute for the skipped phases. The same verifier gates PR creation (`work-on/review.md` Phase R1.5) and auto-merge (`review-pr.md` Phase 8).
285
+
286
+ **Determine resume point**: no `FORGE:INVESTIGATOR` → Phase 1 (investigate). Investigation complete with decompose YES and no `FORGE:DECOMPOSED` → Phase 2 (decompose); with `FORGE:DECOMPOSED` → Phase 5 (`--terminal-state decomposed`). Investigation complete (decompose NO) without `FORGE:BUILDER:COMPLETE` → Phase 3 (build; a partial BUILDER comment is cleaned up by the build phase). `FORGE:BUILDER:COMPLETE` and no merged PR → Phase 4 (review). PR merged and issue open → Phase 5 (close). `workflow:invalid` → STOP.
309
287
 
310
288
  ### 0B.5: Read Phase Checkpoint (MANDATORY — executes before any phase-skip decision)
311
289
 
@@ -327,13 +305,13 @@ fi
327
305
 
328
306
  | `next_phase` value | Resume at |
329
307
  |--------------------|-----------|
330
- | `BUILD` | Phase 3 (skip Phase 1 investigation) |
331
- | `DECOMPOSE` | Phase 2 (skip Phase 1 investigation) |
332
- | `REVIEW` | Phase 4 (skip Phase 1–3) |
333
- | `CLOSE` | Phase 6 (skip Phase 1–5) |
308
+ | `BUILD` | Phase 3 — build (skip investigation) |
309
+ | `DECOMPOSE` | Phase 2 — decompose (skip investigation) |
310
+ | `REVIEW` | Phase 4 — review (skip investigation and build) |
311
+ | `CLOSE` | Phase 5 — close (skip everything before it) |
334
312
  | *(absent or unrecognized)* | Fall back to prose heuristics above |
335
313
 
336
- Note: Phase 1D no longer writes `next_phase: BUILD`/`DECOMPOSE` CHECKPOINT comments (removed as redundant with the `workflow:ready-to-build`/`workflow:decomposed` label transition — see Phase 1D). Those two rows remain here only to route older, pre-existing CHECKPOINT comments correctly; new runs land on the prose-heuristic fallback for those two cases instead, which is equally precise. `REVIEW` and `CLOSE` are still written (Phase 3M and Phase 5D) because each covers a real gap before the corresponding label transition.
314
+ Note: the investigate phase no longer writes `next_phase: BUILD`/`DECOMPOSE` CHECKPOINT comments (removed as redundant with the `workflow:ready-to-build`/`workflow:decomposed` label transition — see Phase 1D). Those two rows remain here only to route older, pre-existing CHECKPOINT comments correctly; new runs land on the prose-heuristic fallback for those two cases instead, which is equally precise. `REVIEW` and `CLOSE` are still written (Phase 3M and Phase 5D) because each covers a real gap before the corresponding label transition.
337
315
 
338
316
  **If no checkpoint exists**: fall back to prose resume heuristics in Phase 0B above — treat as fresh start at Phase 1.
339
317
 
@@ -351,7 +329,7 @@ BATCH_MEMBERS_BLOCK=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body'
351
329
  if [ -n "$BATCH_MEMBERS_BLOCK" ]; then
352
330
  IS_BATCH=1
353
331
  # Extract member issue numbers (- [ ] #NNN: title lines)
354
- BATCH_MEMBERS=($(echo "$BATCH_MEMBERS_BLOCK" | grep -oP '(?<=- \[ \] #)\d+' || true))
332
+ BATCH_MEMBERS=($(echo "$BATCH_MEMBERS_BLOCK" | sed -n 's/^.*- \[ \] #\([0-9][0-9]*\).*/\1/p' || true))
355
333
  echo "Batch issue detected — member issues: ${BATCH_MEMBERS[*]}"
356
334
  fi
357
335
  ```
@@ -378,7 +356,7 @@ fi
378
356
  gh issue edit "$MEMBER" {GH_FLAG} --add-label "workflow:merged" 2>/dev/null || true
379
357
  done
380
358
  ```
381
- - Member issues are closed in Phase 6 (after PR merge) — NOT before
359
+ - Member issues are closed by the router after Phase 5 (close) returns COMPLETE — NOT before (see "Batch-member closure").
382
360
 
383
361
  **Source branch for review-findings**: Parse `**Code branch**: \`{branch}\`` from body. Branch from there, not main.
384
362
 
@@ -396,11 +374,29 @@ if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
396
374
  echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
397
375
  ADAPTIVE_ENABLED=false
398
376
  fi
399
- UNIVERSAL_DIR="${FORGEDOCK_HOME:-$REPO_PATH}/scripts"
377
+ # ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
378
+ # below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
379
+ # repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
380
+ # Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
381
+ # ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
382
+ # FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
383
+ FORGE_ROOT=""
384
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
385
+ # Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
386
+ _l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
387
+ # newest cached version first: numeric major.minor.patch of the version dir name only; a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
388
+ _v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ '$(NF-1)=="forgedock"{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5-)"
389
+ _m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
390
+ # '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
391
+ _k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
392
+ while IFS= read -r _c; do
393
+ case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
394
+ done <<< "$_k"
395
+ fi
396
+ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
400
397
  # NOTE: never resolve this via `which` or `find` — universal scripts are
401
- # repo-relative, not installed on $PATH, so a PATH lookup always misses.
402
- # REPO_PATH is already resolved from forge.yaml → paths.root earlier in
403
- # Phase 0, so it is the deterministic fallback when FORGEDOCK_HOME is unset.
398
+ # install-relative, not installed on $PATH, so a PATH lookup always misses.
399
+ # FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
404
400
  # Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
405
401
  # locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
406
402
  # does not exist, resolve_script() falls through to Tier 4 (prose) below,
@@ -415,7 +411,7 @@ resolve_script() {
415
411
  return
416
412
  fi
417
413
  # Tier 3: universal script
418
- if [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
414
+ if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
419
415
  echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
420
416
  return
421
417
  fi
@@ -467,12 +463,7 @@ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' forge.yaml 2>/dev/null |
467
463
  echo "Learned override: STAGING_BRANCH → $STAGING_BRANCH (from learned.branch_targets.staging)"
468
464
  ```
469
465
 
470
- 2. **Test commands** — Store `LEARNED_TEST_COMMANDS` for use in Phase 3H (validate). These are appended to the `verification.commands` runs, not replaced:
471
- ```bash
472
- # Pass LEARNED_TEST_COMMANDS to Phase 3H as additional commands to run after verification.commands
473
- # (consumed in 3H — store as env var or carry forward in context)
474
- echo "Learned test commands: $LEARNED_TEST_COMMANDS"
475
- ```
466
+ 2. **Test commands** — Read by the validate phase itself from `forge.yaml → learned.test_commands` (a forked phase cannot see router variables). Nothing to pass.
476
467
 
477
468
  3. **Label map** — If `LEARNED_LABEL_MAP` is non-empty, export it as `FORGE_LABEL_MAP` so that all subsequent `resolve_script 'transition-label'` invocations (which are child processes) can read it. The script performs the substitution internally: if the canonical label (e.g. `workflow:investigating`) appears as a key in the map, it uses the mapped value instead.
478
469
  ```bash
@@ -484,7 +475,7 @@ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' forge.yaml 2>/dev/null |
484
475
  echo "Learned override: FORGE_LABEL_MAP active — label_map will be applied by resolve_script 'transition-label'"
485
476
  ```
486
477
 
487
- 4. **Commit style** — If `LEARNED_COMMIT_STYLE` is non-empty, use it in Phase 3M:
478
+ 4. **Commit style** — Read by the build children (implement/validate) from `forge.yaml → learned.commit_style`; the router only logs it:
488
479
  ```bash
489
480
  [ -n "$LEARNED_COMMIT_STYLE" ] && COMMIT_STYLE="$LEARNED_COMMIT_STYLE" && \
490
481
  echo "Learned override: COMMIT_STYLE → $COMMIT_STYLE"
@@ -495,1998 +486,173 @@ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' forge.yaml 2>/dev/null |
495
486
  ### 0C: Sync to Project board
496
487
  Add issue to project, set Status=In Progress, Lane, Component, Priority, Workflow=Investigating.
497
488
 
498
- ### 0C.5: Resolve minimal spec set (selective spec loading)
499
-
500
- Rather than loading the full ~27-command corpus (~1.1 MB / ~276K tokens) into
501
- context, resolve the **minimal spec set** this run actually needs from the spec
502
- knowledge graph. Use the universal-tier `graph-query` script via the canonical
503
- `resolve_script` tier-dispatch pattern:
504
-
505
- ```bash
506
- # Forward-transitive reachability: work-on + its reachable sub-phases (CONTAINS)
507
- # + required devdocs (REQUIRES), as repo-relative file paths.
508
- RESOLUTION=$(resolve_script 'graph-query')
509
- TIER="${RESOLUTION%%:*}"
510
- SCRIPT_PATH="${RESOLUTION#*:}"
511
- case "$TIER" in
512
- adaptive|universal)
513
- SPEC_SET=$(bash "$SCRIPT_PATH" load-set work-on 2>/dev/null || echo '[]')
514
- echo "$SPEC_SET" | jq -r '.[]'
515
- ;;
516
- prose)
517
- # Prose fallback: graph-query.sh unavailable — read specs on demand as each
518
- # Skill(...) is invoked. Selective loading is an optimization, not required.
519
- : ;;
520
- esac
521
- ```
522
-
523
- **Read ONLY the files returned by `load-set work-on`** when you need full spec
524
- text during this run — these are the work-on orchestrator plus the sub-phases
525
- and devdocs reachable from it (e.g. `commands/work-on/build/*`, `commands/work-on/review.md`,
526
- `commands/review-pr.md`). Do **not** broadly read unrelated command specs
527
- (`pipeline-health.md`, `audit.md`, `geo-audit.md`, …) that are not in the set.
528
- Sub-phases are still invoked normally via their existing `Skill(...)` calls; this
529
- step only narrows what is *pre-read* into context, it does not remove any phase.
489
+ ### 0C.5: Spec loading
530
490
 
531
- This is the inverse of `graph-query.sh impact` (forward instead of reverse
532
- reachability). It is read-only and auto-builds the graph if the gitignored JSON
533
- is absent — no committed graph is required. The prose tier above handles older
534
- installs without the scripts layer: selective loading is an optimization, never
535
- a hard dependency.
491
+ The router never reads phase spec files. Each phase loads exactly its own spec when it runs, because every phase sub-skill is forked (`context: fork`): the phase's context holds only that phase's instructions plus its args. Do not pre-read `commands/work-on/*.md`, `review-pr.md` or any other command spec into the router's context.
536
492
 
537
493
  ---
538
494
 
539
- ## Phase 1: Investigation
540
-
541
- **Skip if**: `<!-- FORGE:INVESTIGATOR -->` exists with `<!-- INVESTIGATION:COMPLETE -->` OR `<!-- INVESTIGATION:INVALID -->` in the SAME comment. `INVESTIGATION:INVALID` is the terminal sentinel Phase 1C emits for an INVALID verdict (see 1C below) — it signals investigation completion just as much as `INVESTIGATION:COMPLETE` does, not an interrupted state.
542
-
543
- **Partial investigation**: If investigator comment exists BUT NEITHER `<!-- INVESTIGATION:COMPLETE -->` NOR `<!-- INVESTIGATION:INVALID -->` is present → investigation was interrupted. Delete the partial comment and restart:
544
- ```bash
545
- COMMENT_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
546
- --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .id')
547
- gh api repos/{GH_REPO}/issues/comments/$COMMENT_ID -X DELETE
548
- ```
495
+ ## Phase Heartbeat (router-owned)
549
496
 
550
- ### 1A: Set label
551
- ```bash
552
- RESOLUTION=$(resolve_script 'transition-label')
553
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
554
- case "$TIER" in
555
- adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
556
- prose)
557
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" \
558
- --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
559
- ;;
560
- esac
561
- ```
497
+ Posted at the entry of Phases 1, 3 and 4 — **only** when `UNDER_ORCHESTRATION` is `true`, and skipped when the issue already carries a terminal label (`workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`). `/orchestrate`'s stall detector reads these timestamps. Phases never post heartbeats.
562
498
 
563
- **Post Phase 1 heartbeat** (skip unless `UNDER_ORCHESTRATION` is `true`; also skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
564
499
  ```bash
565
- PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
566
500
  gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
567
- **Phase**: Phase 1 — Investigation
568
- **Timestamp**: ${PHASE_START_TIMESTAMP}
569
- **Issue**: #{NUMBER}"
501
+ **Phase**: {PHASE_LABEL}
502
+ **Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
503
+ **Issue**: #{NUMBER}" 2>/dev/null || true # allowlist:check-command-side-effects
570
504
  ```
571
505
 
572
- ### 1A.5: Normalize Issue Body (MANDATORY)
573
-
574
- Before investigation begins, verify the issue body contains the four mandatory pipeline sections. If any are missing, add placeholder content so the investigator has the correct scaffolding.
575
-
576
- **Skip if**: All four sections (`## Problem`, `## Affected Files`, `## Expected Behavior`, `## Acceptance Criteria`) are already present.
577
-
578
- **Reuse `ISSUE_BODY` from Phase 0B** — it was already read in full there. Only re-fetch with the command below if `ISSUE_BODY` is not actually present in this session's context (compaction or a fresh resume).
579
-
580
- ```bash
581
- ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
582
-
583
- MISSING_SECTIONS=""
584
- echo "$ISSUE_BODY" | grep -q "^## Problem" || MISSING_SECTIONS="$MISSING_SECTIONS PROBLEM"
585
- echo "$ISSUE_BODY" | grep -q "^## Affected Files" || MISSING_SECTIONS="$MISSING_SECTIONS AFFECTED_FILES"
586
- echo "$ISSUE_BODY" | grep -q "^## Expected Behavior" || MISSING_SECTIONS="$MISSING_SECTIONS EXPECTED_BEHAVIOR"
587
- echo "$ISSUE_BODY" | grep -q "^## Acceptance Criteria" || MISSING_SECTIONS="$MISSING_SECTIONS ACCEPTANCE_CRITERIA"
506
+ `{PHASE_LABEL}`: `Phase 1 — Investigation`, `Phase 3 — Build`, `Phase 4 — Review`.
588
507
 
589
- if [ -n "$MISSING_SECTIONS" ]; then
590
- echo "Missing sections:$MISSING_SECTIONS — normalizing issue body before investigation"
591
-
592
- APPEND_TEXT=""
593
- echo "$MISSING_SECTIONS" | grep -q "PROBLEM" && APPEND_TEXT="$APPEND_TEXT
594
- ## Problem
595
-
596
- Root cause unknown — investigation needed."
597
-
598
- echo "$MISSING_SECTIONS" | grep -q "AFFECTED_FILES" && APPEND_TEXT="$APPEND_TEXT
599
- ## Affected Files
600
-
601
- Files to be identified during investigation."
602
-
603
- echo "$MISSING_SECTIONS" | grep -q "EXPECTED_BEHAVIOR" && APPEND_TEXT="$APPEND_TEXT
604
- ## Expected Behavior
508
+ ---
605
509
 
606
- Expected behavior to be determined during investigation."
510
+ ## Phase 1: Investigate
607
511
 
608
- echo "$MISSING_SECTIONS" | grep -q "ACCEPTANCE_CRITERIA" && APPEND_TEXT="$APPEND_TEXT
609
- ## Acceptance Criteria
512
+ **Skip if**: a `<!-- FORGE:INVESTIGATOR -->` comment contains `<!-- INVESTIGATION:COMPLETE -->` or `<!-- INVESTIGATION:INVALID -->` (route on the existing verdict as below — the skill's own resume check returns it if invoked).
610
513
 
611
- - [ ] Fix confirmed during investigation."
514
+ Post the Phase 1 heartbeat, then:
612
515
 
613
- # Append missing sections to the existing body (never replace — only extend)
614
- NORMALIZED_BODY="${ISSUE_BODY}${APPEND_TEXT}"
615
- gh issue edit {NUMBER} {GH_FLAG} --body "$NORMALIZED_BODY"
616
- echo "Issue body normalized — added:$MISSING_SECTIONS"
617
- else
618
- echo "Issue body already contains all mandatory sections — skipping normalization"
619
- fi
620
516
  ```
621
-
622
- **Continue to Phase 1B unconditionally.** Normalization is a compensation step — it never blocks investigation.
623
-
624
- ### 1B: Investigate the issue
625
-
626
- Mission: Validate whether the issue is real. Assume description is wrong until proven otherwise.
627
-
628
- **Resolve target repo and branch**:
629
-
630
- Read `forge.yaml → review.tech_stack` and `forge.yaml → review.key_paths` (if present) to identify which files are most relevant for the affected domain. If the `review` section is absent, use the issue labels, title keywords, and the affected files listed in the issue body to determine the domain. Start with the files the issue explicitly names, then expand to callers and related modules.
631
-
632
- **Workflow pipeline issues** (repo is a ForgeDock installation):
633
- - Key files: `commands/work-on.md`, `commands/review-pr.md`, `commands/quality-gate.md`, `commands/orchestrate.md`, `forge.yaml`, `bin/forgedock.mjs`
634
-
635
- **Application issues** (all other repos):
636
-
637
- Use `forge.yaml → review.tech_stack` and the issue domain labels to identify entry points. If `forge.yaml → review.key_paths` lists domain-to-file mappings, use that table directly. Otherwise, infer key files from the issue body's Affected Files section.
638
-
639
- **INFRA domain known footguns** (read before writing any `.github/workflows/*.yml` changes):
640
- - **appleboy/ssh-action Go template preprocessing**: Any `{{` in a `script:` block is interpreted as a Go template directive **before the script reaches SSH**. This means `docker ps --format '{{.Names}}'` and `docker inspect --format '{{index .RepoTags 0}}'` will crash the action with exit 1. Both function calls (`{{index .X Y}}`) AND field accessors (`{{.Names}}`, `{{.Status}}`) fail on the action's empty data context. Shell error handlers (`|| fallback`, `set -e`, `2>/dev/null`) are bypassed because the failure is client-side. Always use `docker inspect IMAGE | jq -r '.[0].RepoTags[0]'` and `docker ps --format json | jq -r '.Names'` patterns in `appleboy/ssh-action` scripts. (Ref: forge#226 — 6-day silent deploy failure masked by `continue-on-error: true`)
641
-
642
- **Steps**:
643
- 1. Check the right branch — read from branch specified in issue body (`**Code branch**: \`{branch}\``) if present
644
- 2. Read domain files — start with key files for the affected domain
645
- 2.5. **Existing system search (conditional)**: If the issue describes a gap in a functional capability — content not being distributed, notifications not sending, jobs not running, data not being synced — MUST search for an existing automated system before proposing a new one. The issue body may name a specific tool or path (e.g., `reddit-bot/`, `marketing/`) — do NOT anchor on that path alone. Expand the search to all service layers:
646
- ```bash
647
- # Check all service layers for the capability (adapt paths to your project structure)
648
- grep -rn "{capability_keyword}" {REPO_PATH}/services/ --include="*.py" -l | head -20
649
- # Look for scheduled jobs, automated runners, existing integrations
650
- grep -rn "scheduler\|celery\|cron\|nightly\|periodic" {REPO_PATH}/services/ --include="*.py" -l | head -10
651
- ```
652
- If an existing system is found that already handles the capability: the fix MUST route through the existing system (fix its config, env var, or gate) — NOT create a new parallel tool. Document the existing system in the investigation report and make it the centerpiece of the recommendation. This check is especially critical when the issue references a standalone tool directory (`reddit-bot/`, `scripts/`, `tools/`) — those directories often duplicate functionality that a service already owns. (Ref: forge#279 — investigator anchored on `reddit-bot/` from issue body, never checked `services/herald/app/scheduler/`, built parallel PRAW integration alongside Herald's existing automated crosspost scheduler)
653
- 3. Verify claims — does the code actually have the problem described?
654
- 3.5. **Type Invariant Verification (MANDATORY)**: Before declaring that a field, key, or parameter has a specific type (e.g. "content is always a dict", "status is always an int"), search for ALL code paths that write to that field across ALL services:
655
- ```bash
656
- grep -rn '"field_name"\s*:' services/ # Python dict key assignments
657
- grep -rn 'result\["field_name"\]\s*=' services/ # Direct assignments
658
- grep -rn '\.field_name\s*=' services/ # Attribute assignments
659
- ```
660
- If the field is written with different types in different code paths (e.g. dict in the standard path, string in the auth-gated path), document ALL variants. The fix must handle every variant — not just the one on the primary investigated code path. A type guard like `or {}` only protects against falsy values; a non-empty string is truthy and bypasses it.
661
- 4. Git blame — trace when/why the relevant code was written. Run bounded, local commands (no network round-trip):
662
- ```bash
663
- # Introducing commit for each affected file (first commit that added it)
664
- git log --reverse --format='%h %an %ad %s' --date=short -- {affected_file} | head -1
665
- # Last-touch commit (most recent change)
666
- git log -1 --format='%h %an %ad %s' --date=short -- {affected_file}
667
- # Line-level blame for a specific suspect hunk, if the issue names one
668
- git blame -L {start},{end} -- {affected_file}
669
- ```
670
- Record the introducing commit and last-touch commit for each primary affected file — this feeds the mandatory **History findings** field in Phase 1C.
671
- 4.5. **Rogue commit pre-state comparison (conditional)**: If the issue body references a specific commit as rogue, bad, or unintended (e.g., "rogue commit `abc1234`", "bad commit", "this was never intended"), MUST run `git show {commit}^:{file}` to see the file before that commit. Compare the pre-commit state against the current file. Any block present in the current file but absent in the pre-commit state was introduced by that commit chain and is a candidate for full reversion — not just partial editing. Report the delta (pre vs. current) in the investigation report. Do NOT assume surrounding code near a named import/bug is correct simply because the issue only named a specific sub-problem. (Ref: forge#278 — investigator confirmed the broken import but never ran `git show 18a3a2cf3^:batch.py`; the surrounding 50-line feature gate was also rogue and was preserved by the fix PR, causing a P1 access regression for all non-Scale users)
672
- 5. Domain context discovery (narrow scope only, 1–5 files):
673
- ```bash
674
- git log --oneline --all -30 -- {affected_files} | grep -oE '#[0-9]+' | sort -u
675
- gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
676
- ```
677
- Keep only file/function-level overlap. Max 5 related issues.
678
-
679
- **Pickaxe pass (prior fix / regression detection)** — bounded to one pass, capped at 5 hits: search for prior additions/removals of the suspected symbol or literal string named in the issue, independent of whether that fix was ever linked to a filed issue:
680
- ```bash
681
- git log -S"{suspected_symbol_or_string}" --oneline -- {affected_files} | head -5
682
- # Use -G instead of -S when the target is a regex pattern rather than a literal string
683
- git log -G"{pattern}" --oneline -- {affected_files} | head -5
684
- ```
685
- Any hit here is a candidate prior fix or reintroduced defect — read the commit body (`git show {hash}`) to confirm before citing it. Feed confirmed hits into the History findings field and let them inform the verdict (e.g. a defect being reintroduced raises severity).
686
- 6. Determine root cause
687
- 7. Identify affected files — full list of files that need changes
688
- 7.5. **Sibling Pattern Sweep** *(conditional — when the bug is a condition, gated function call, or field presence check)*: After identifying the affected files, grep for the same pattern in sibling files within the same directory. The issue spec may name only the file where the error was first observed — but the same commit or PR that introduced the bug often applied it uniformly across related handlers.
689
- ```bash
690
- # Identify the broken condition or gated function call from the issue
691
- # Then search sibling files in the same router/service directory
692
- AFFECTED_DIR=$(dirname {PRIMARY_AFFECTED_FILE})
693
- grep -rn "{broken_pattern}" "$AFFECTED_DIR" --include="*.py" | grep -v "{PRIMARY_AFFECTED_FILE}"
694
- ```
695
- **If identical patterns are found in files NOT listed in the issue spec**, output a scope-gap warning:
696
- > **Scope-Gap Warning**: The issue spec lists `{PRIMARY_FILE}` but the same pattern exists in `{SIBLING_FILE}:{LINE}`. These were likely introduced together. Recommend widening scope to fix all callers in this PR, or creating follow-up issues for the other files before proceeding.
697
-
698
- Do NOT silently exclude sibling matches. The appropriate output when sibling files have the same bug is to flag them explicitly — even if the issue spec's silence appears intentional. The fix-approach validation step (step 8) will confirm whether to widen scope or create follow-ups. <!-- Added: forge#383 -->
699
- 8. Fix-approach validation — if issue proposes a fix, don't adopt as spec. Trace through middleware, auth, routing, config. Cross-domain: if fix in domain A interacts with domain B, read domain B's files too.
700
-
701
- ### 1C: Post investigation comment
702
-
703
- The comment MUST include a terminal sentinel at the very end. **The sentinel is conditional on the resolved Verdict — it is NOT always `<!-- INVESTIGATION:COMPLETE -->`:**
704
-
705
- - **Verdict is INVALID** → close with `<!-- INVESTIGATION:INVALID -->`. This is a distinct, already-wired-up terminal marker: `bin/engine/phases.mjs`'s `detectOutcome` for the `investigate` phase checks for it explicitly (ahead of `INVESTIGATION:COMPLETE`) and routes to `terminalReason: "invalid"`; `bin/hooks/interactive-engine.mjs`'s `PHASE_MARKERS` table also already treats it as terminal. Emitting `INVESTIGATION:COMPLETE` for an INVALID verdict is what previously caused every completed investigation to read as `{verdict: "CONFIRMED"}` regardless of actual outcome — do NOT regress this (forge#2350).
706
- - **Verdict is CONFIRMED or PARTIAL** → close with `<!-- INVESTIGATION:COMPLETE -->` as before (PARTIAL still routes to `ready-to-build` below — only INVALID gets the distinct terminal sentinel).
707
-
708
- ```bash
709
- if [ "{VERDICT}" = "INVALID" ]; then
710
- INVESTIGATION_SENTINEL="<!-- INVESTIGATION:INVALID -->"
711
- else
712
- INVESTIGATION_SENTINEL="<!-- INVESTIGATION:COMPLETE -->"
713
- fi
714
-
715
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
716
- ## Investigation Report
717
-
718
- **Verdict**: {CONFIRMED|PARTIAL|INVALID}
719
- **Confidence**: {HIGH|MEDIUM|LOW}
720
- **Severity**: {CRITICAL|HIGH|MEDIUM|LOW}
721
- **Task Type**: {Bug Fix|Feature|Refactor|Maintenance|Investigation}
722
-
723
- ### What Was Claimed
724
- {summary of what the issue describes}
725
-
726
- ### What We Found
727
- {what the code actually shows}
728
-
729
- ### Root Cause
730
- {specific root cause, with file:line references where applicable}
731
-
732
- ### Affected Files
733
- {numbered list of files that need changes}
734
-
735
- ### Evidence
736
- {specific findings — function names, line numbers, behavior observed}
737
-
738
- ### History Findings
739
- **Introducing commit**: {hash — author — date — subject, per primary affected file}
740
- **Last touched**: {hash — author — date — subject}
741
- **Pickaxe hits (prior fixes / regressions)**: {commit(s) found via \`git log -S\`/\`-G\`, or 'None found' — max 5}
742
- {This field is MANDATORY — populate from the git blame + pickaxe commands in step 4/5. If a file is newly created (no history), write 'New file — no history.'}
743
-
744
- ### Recommendation
745
- {what to build/fix, concrete and actionable}
746
-
747
- ### Related Issues
748
- {if any found via domain context discovery, max 5}
749
-
750
- ### Decomposition Assessment
751
- **{YES|NO}** — {reason}
752
- {if YES: proposed sub-issues with titles and dependencies}
753
-
754
- ${INVESTIGATION_SENTINEL}"
517
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:investigate", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\"")
755
518
  ```
756
519
 
757
- **Do not hardcode `<!-- INVESTIGATION:COMPLETE -->` as the closing line.** The closing line MUST be the `${INVESTIGATION_SENTINEL}` variable computed above — it resolves to `<!-- INVESTIGATION:INVALID -->` for an INVALID verdict and `<!-- INVESTIGATION:COMPLETE -->` otherwise. `INVESTIGATION:COMPLETE` and `INVESTIGATION:INVALID` are mutually exclusive within a single posted comment — never emit both.
520
+ | `INVESTIGATE_RESULT` | Router action |
521
+ |---|---|
522
+ | `status: COMPLETE` or `ALREADY_DONE`, `decompose: NO` | Marker gate (below) → Lane Resolution → Phase 3 |
523
+ | `status: COMPLETE` or `ALREADY_DONE`, `decompose: YES` | Phase 2 |
524
+ | `status: INVALID` | Terminal (the phase labelled `workflow:invalid` and closed the issue) → Phase 5 with `--terminal-state invalid` |
525
+ | `status: BLOCKED` | Terminal: confirm `needs-human` is on the issue (add it with the blocker as a comment if not) and STOP |
758
526
 
759
- ### 1D: Correction capture (MANDATORY — run before label update)
527
+ **Marker gate — Phase 1 exit** (decompose NO path only): the issue must have a `FORGE:INVESTIGATOR` comment containing `INVESTIGATION:COMPLETE`. If absent, invoke the skill once more; if still absent, post `<!-- FORGE:GATE_FAILURE -->` ("investigation marker missing after one re-invoke"), add `needs-human`, STOP. <!-- forge#1419 -->
760
528
 
761
- Before routing, scan all non-agent comments for correction signals from the repository owner. Correction signals are owner comments that contain phrases like "no, use", "actually use", "use X instead", "not X, use Y", or "wrong branch". If found, write the correction to `forge.yaml → learned:` and emit a `FORGE:LEARNED` annotation.
529
+ ---
762
530
 
763
- **Scan for correction signals**:
764
- ```bash
765
- # Get repo owner login for filtering.
766
- # Tiered resolution — necessary because project.owner is the GitHub org/user NAME,
767
- # but comment .user.login is always a personal account login. For org-owned repos
768
- # these are structurally different (e.g. org="RapierCraftStudios", commenter="mrdubey"),
769
- # so using project.owner directly silently disables correction capture for all org repos.
770
- #
771
- # Resolution order:
772
- # 1. project.owner_login (explicit override — required for org repos where owner ≠ personal login)
773
- # 2. gh api repos/{GH_REPO} --jq '.owner.login' (auto-resolves correctly for personal repos)
774
- # 3. project.owner (backward-compat fallback — still broken for org repos, but avoids hard failure)
775
- REPO_OWNER=$(yq '.project.owner_login // ""' forge.yaml 2>/dev/null || echo '')
776
- if [ -z "$REPO_OWNER" ]; then
777
- REPO_OWNER=$(gh api repos/{GH_REPO} --jq '.owner.login' 2>/dev/null || echo '')
778
- fi
779
- if [ -z "$REPO_OWNER" ]; then
780
- REPO_OWNER=$(yq '.project.owner' forge.yaml 2>/dev/null || echo '')
781
- fi
531
+ ## Phase 2: Decompose
782
532
 
783
- # Fetch all comments, filter to owner-only, look for correction signals
784
- CORRECTIONS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
785
- | jq -r --arg owner "$REPO_OWNER" \
786
- '.[] | select(.user.login == $owner) | select(
787
- (.body | test("no,? use|actually use|use .+ instead|not .+, use|wrong branch"; "i"))
788
- ) | .body' 2>/dev/null || echo '')
789
533
  ```
790
-
791
- **If correction signals found** — extract and write each correction:
792
-
793
- ```bash
794
- # Example: extract branch target correction "use develop not staging"
795
- # Adjust regex to the correction pattern detected
796
-
797
- if [ -n "$CORRECTIONS" ]; then
798
- echo "Correction signals detected — writing to forge.yaml → learned:"
799
- echo "$CORRECTIONS"
800
-
801
- # Write to forge.yaml using yq in-place merge (idempotent — yq merge overwrites existing keys)
802
- # Always use env variable injection to avoid YAML injection from comment content
803
- # Example for branch target correction:
804
- # BRANCH_VALUE="develop"
805
- # yq eval '.learned.branch_targets.staging = env(BRANCH_VALUE)' -i forge.yaml
806
-
807
- # After writing, emit FORGE:LEARNED annotation
808
- LEARNED_KEYS="branch_targets.staging" # replace with actual extracted keys
809
- CAPTURED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
810
-
811
- # Update captured_at and captured_by metadata
812
- CAPTURED_AT_VAL="$CAPTURED_AT" yq eval '.learned.captured_at = env(CAPTURED_AT_VAL)' -i forge.yaml
813
- CAPTURED_BY_VAL="work-on/{NUMBER}" yq eval '.learned.captured_by = env(CAPTURED_BY_VAL)' -i forge.yaml
814
-
815
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:LEARNED -->
816
- ## Learned Pattern Captured
817
-
818
- **Source**: Owner correction in comment on issue #{NUMBER}
819
- **Captured at**: $CAPTURED_AT
820
- **Keys written**: \`$LEARNED_KEYS\`
821
-
822
- The following project-specific pattern was detected from owner feedback and written to \`forge.yaml → learned:\`. Future sessions will use this override automatically (read in Phase 0B.1).
823
-
824
- \`\`\`yaml
825
- # Written to forge.yaml
826
- learned:
827
- # {key}: {value}
828
- \`\`\`
829
-
830
- **Idempotency**: yq merge-write — re-running will not duplicate entries."
831
-
832
- echo "FORGE:LEARNED annotation posted."
833
- fi
534
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:decompose", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\"")
834
535
  ```
835
536
 
836
- **Idempotency guarantee**: Use `yq eval '.learned.key = env(VAR)' -i forge.yaml` — yq overwrites existing keys rather than appending. Re-running the capture step on the same comment produces the same forge.yaml state. <!-- Added: forge#667 -->
537
+ | `DECOMPOSE_RESULT` | Router action |
538
+ |---|---|
539
+ | `COMPLETE` / `ALREADY_DONE` | Phase 5 with `--terminal-state decomposed` (the parent stays open as a tracker; each sub-issue runs its own `/work-on`) |
540
+ | `BLOCKED` | Terminal: the phase already added `needs-human` and posted the blocker. STOP. |
837
541
 
838
- ### 1D: Update labels & route
839
-
840
- **CONFIRMED or PARTIAL with decompose: NO**:
841
- ```bash
842
- RESOLUTION=$(resolve_script 'transition-label')
843
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
844
- case "$TIER" in
845
- adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
846
- prose)
847
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" \
848
- --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
849
- ;;
850
- esac
851
- ```
542
+ ---
852
543
 
853
- **Marker gate — Phase 1 exit** (see Marker Gate table in Universal Phase Dispatcher): <!-- forge#1419, forge#1418 -->
854
- ```bash
855
- INV_MARKER=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
856
- --jq '[.[] | select(.body | contains("INVESTIGATION:COMPLETE"))] | length')
857
- if [ "${INV_MARKER:-0}" -eq 0 ]; then
858
- echo "MARKER GATE FAIL: INVESTIGATION:COMPLETE absent — re-invoking work-on/investigate once"
859
- Skill(skill="work-on/investigate", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG}")
860
- INV_MARKER=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
861
- --jq '[.[] | select(.body | contains("INVESTIGATION:COMPLETE"))] | length')
862
- if [ "${INV_MARKER:-0}" -eq 0 ]; then
863
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:GATE_FAILURE -->
864
- ## Marker Gate Failure — Phase 1 (Investigation)
865
-
866
- **Expected marker**: \`INVESTIGATION:COMPLETE\` inside a \`FORGE:INVESTIGATOR\` comment
867
- **Status**: Absent after subcommand re-invocation. Human review required.
868
-
869
- The router re-invoked \`work-on/investigate\` once but the marker was still not posted.
870
- Inspect the subcommand output above for errors. <!-- forge#1418 -->"
871
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" \
872
- --remove-label "workflow:investigating" 2>/dev/null || true
873
- exit 1
874
- fi
875
- fi
876
- ```
544
+ ## Lane Resolution (router, before Phase 3 and again before Phase 4)
877
545
 
878
- **No separate CHECKPOINT write here** — the `workflow:ready-to-build` label set above already fully disambiguates the resume point ("Investigation exists + ready-to-build → Phase 3" per Phase 0B's prose heuristics); a `next_phase: BUILD` CHECKPOINT comment would duplicate that signal with an extra write. <!-- Removed redundant FORGE:CHECKPOINT write: forge#1826 -->
879
- <!-- FORGE:PHASE_COMPLETE — Investigation routed to build. See Universal Phase Dispatcher: next phase is Phase 3. Not terminal — continue immediately. -->
880
- → Continue to Phase 3.
546
+ `PR_BASE` (the branch the PR targets and the worktree is based on) and `CLASSIFIED_LANE` are computed by the router and passed to the phases as `--base`. Include the Phase 0 Script resolution block in the same command.
881
547
 
882
- **CONFIRMED or PARTIAL with decompose: YES**:
883
548
  ```bash
884
- RESOLUTION=$(resolve_script 'transition-label')
549
+ RESOLUTION=$(resolve_script 'classify-lane')
885
550
  TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
886
551
  case "$TIER" in
887
- adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
888
- prose)
889
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
890
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
552
+ adaptive|universal)
553
+ if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
554
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to compute the PR target — see the script error. Adding needs-human." # allowlist:check-command-side-effects
555
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
556
+ exit 1
557
+ fi
891
558
  ;;
892
- esac
893
- ```
894
-
895
- **No separate CHECKPOINT write here** — the `workflow:decomposed` label set above is itself a terminal state for this run; nothing needs to resume past it, so a `next_phase: DECOMPOSE` CHECKPOINT comment adds no information. <!-- Removed redundant FORGE:CHECKPOINT write: forge#1826 -->
896
- <!-- FORGE:PHASE_COMPLETE — Investigation routed to decomposition. See Universal Phase Dispatcher: next phase is Phase 2. Not terminal — continue immediately. -->
897
- → Continue to Phase 2 (Decomposition).
898
-
899
- **INVALID**:
900
- ```bash
901
- RESOLUTION=$(resolve_script 'transition-label')
902
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
903
- case "$TIER" in
904
- adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
905
559
  prose)
906
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" \
907
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true
560
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot compute the PR target. Adding needs-human." # allowlist:check-command-side-effects
561
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
562
+ exit 1
908
563
  ;;
909
564
  esac
910
- gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}"
911
- ```
912
- → STOP. No checkpoint written — INVALID is terminal.
913
-
914
- ---
915
-
916
- ## Phase 2: Decomposition (Conditional)
917
-
918
- **Engine coverage** (forge#2379): this phase's dispatch target (`work-on/decompose`) is now a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`/`bin/engine/phases.mjs`, live-wired so `investigate`'s `DECOMPOSE:YES` outcome hands off to it instead of terminating the run in place. This prose Phase 2 remains the interactive/prose dispatch path; the engine path is the headless equivalent, both invoking the same `commands/work-on/decompose.md` subcommand.
919
-
920
- **Skip if**: Already decomposed, is a sub-issue, or investigation says decompose: NO.
921
-
922
- **Trigger if**: Investigation assessment says YES — 2+ signals match (at least 1 Strong): multiple task types, 3+ service groups, phased requirements, 6+ files across directories.
923
-
924
- ### 2A: Load state
925
- ```bash
926
- gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
927
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
928
- --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
929
- ```
930
-
931
- **MANDATORY — Owner override detection**: After reading the investigation comment, read ALL comments on the issue to check for owner override signals:
932
- ```bash
933
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
934
- --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR") | not) | {author: .user.login, body: .body}'
935
- ```
936
-
937
- Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
938
-
939
- 1. **Document the override**: Note which direction the owner is steering (e.g., "remove the feature" vs. investigation's "keep with warnings")
940
- 2. **Re-derive sub-issue scopes**: Derive sub-issue titles, bodies, and file scope from the override direction — NOT from the original investigation recommendation. The investigation's Decomposition Assessment may list sub-issues that are now stale or contradictory with the override.
941
- 3. **If override makes a sub-issue obsolete**: Skip creating it. Note the skip reason in the decomposition comment.
942
- 4. **If override changes the sequencing dependency**: Revise the execution order so that the override's primary action (e.g., "strip the feature") completes before any downstream doc/SDK sub-issues are built against it.
943
-
944
- **Why this matters**: Sub-issues scoped before an override are built against a stale premise. A docs sub-issue scoped as "neutralize liability language" becomes incorrect if the upstream schema sub-issue will fully remove the feature — the docs sub-issue should instead be "remove all references to the deleted feature." Building both in parallel against the pre-override scope produces contradictory staging state.
945
-
946
- ### 2B: Design sub-issues
947
- From the Decomposition Assessment (adjusted for any owner override detected in 2A), extract sub-issue titles (dependency order — independent first), dependencies, and descriptions.
948
-
949
- For each sub-issue:
950
- - **Title**: from investigation report
951
- - **Body**: brief scope + `**Parent**: #{NUMBER}` + dependency note
952
- - **Labels**: inherit priority label from parent; do NOT copy workflow labels
953
- - **Milestone**: same as parent
954
-
955
- ### 2C: Create sub-issues
956
-
957
- Route through the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") instead of calling the raw issue-creation command directly — this gets dedup (Phase 2D) and body validation (Phase 3F) for free on every decomposition-spawned sub-issue.
958
-
959
- ```bash
960
- SUB_ISSUE_TITLE_FULL="{fix|feat|refactor}: {SUB_ISSUE_TITLE}"
961
- # Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
962
- # an xargs-based tokenizer that never expands backtick/$(...) substitution, so
963
- # this is no longer required for safety — but strip it anyway so the raw title
964
- # stays readable if it round-trips through any other eval-based consumer.
965
- SUB_ISSUE_TITLE_FULL=$(printf '%s' "$SUB_ISSUE_TITLE_FULL" | tr '`' "'" | sed 's/\$(/$ (/g')
966
- SUB_ISSUE_BODY_FILE=$(mktemp)
967
- cat <<'SUB_BODY_EOF' > "$SUB_ISSUE_BODY_FILE"
968
- ## Problem
969
-
970
- {1-3 sentences: what this sub-issue specifically addresses. What's wrong or what needs to be built.}
971
-
972
- ## Root Cause (if known)
973
-
974
- {Specific root cause for this sub-task. Reference the parent investigation findings where applicable. If unknown: "Root cause unknown — investigation needed."}
975
-
976
- ## Affected Files
977
-
978
- Files that need changes:
979
- 1. `{filepath}` — {what needs to change}
980
- 2. `{filepath}` — {what needs to change}
981
-
982
- ## Acceptance Criteria
983
-
984
- - [ ] {Specific, testable criterion}
985
- - [ ] {Specific, testable criterion}
986
- - [ ] No regression in {related feature}
987
-
988
- ## Context
989
-
990
- **Parent**: #{NUMBER}
991
- {If depends on another sub-issue: "**Depends on**: #{SUB_ISSUE_N} — {reason}"}
992
- SUB_BODY_EOF
993
-
994
- # --milestone is only passed when the parent has one (see Phase 2B — "Milestone: same as parent").
995
- MILESTONE_ARG=""
996
- [ -n "{MILESTONE_TITLE}" ] && MILESTONE_ARG="--milestone \"{MILESTONE_TITLE}\""
997
- Skill(skill="issue", args="--title \"$SUB_ISSUE_TITLE_FULL\" --body-file \"$SUB_ISSUE_BODY_FILE\" --label \"{PRIORITY_LABEL}\" ${MILESTONE_ARG}")
998
- rm -f "$SUB_ISSUE_BODY_FILE"
999
-
1000
- # /issue has no machine-readable return contract — resolve the created issue's number by
1001
- # exact-title search immediately after the call (used by Phase 2D's tracker checklist).
1002
- # Retry to absorb GitHub Search API indexing lag.
1003
- SUB_ISSUE_NUM=""
1004
- for _resolve_attempt in 1 2 3; do
1005
- SUB_ISSUE_NUM=$(gh issue list {GH_FLAG} --search "in:title \"${SUB_ISSUE_TITLE_FULL}\"" --state open --limit 1 --json number --jq '.[0].number // empty')
1006
- [ -n "$SUB_ISSUE_NUM" ] && break
1007
- sleep 2
1008
- done
565
+ CLASSIFIED_LANE="$PR_BASE"
566
+ echo "PR_BASE=$PR_BASE"
1009
567
  ```
568
+ Output is authoritative — no prose fallback. A non-zero exit means `needs-human` and STOP. <!-- Added: forge#669, forge#639 -->
1010
569
 
1011
- ### 2D: Update parent issue body
1012
- Add tracker checklist with all sub-issues in dependency order.
1013
-
1014
- ### 2E: Post decomposition comment
1015
- ```bash
1016
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSED -->
1017
- ## Decomposition Complete
1018
-
1019
- ### Sub-Issues Created
1020
- - #{SUB_NUMBER}: {TITLE}
1021
-
1022
- ### Decomposition Rationale
1023
- {brief summary}
1024
-
1025
- <!-- FORGE:DECOMPOSED:COMPLETE -->"
1026
- ```
570
+ **Before Phase 4 only — validate the target against the classified lane** (guards against a resumed run or a hand-edited branch targeting the wrong branch):
1027
571
 
1028
- ### 2F: Update labels
1029
572
  ```bash
1030
- RESOLUTION=$(resolve_script 'transition-label')
573
+ RESOLUTION=$(resolve_script 'validate-pr-target')
1031
574
  TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1032
575
  case "$TIER" in
1033
- adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
576
+ adaptive|universal)
577
+ if ! bash "$SCRIPT_PATH" {PR_BASE} {CLASSIFIED_LANE}; then
578
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKING: validate-pr-target.sh — PR base \`{PR_BASE}\` does not match classified lane \`{CLASSIFIED_LANE}\`. Manual intervention required." # allowlist:check-command-side-effects
579
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
580
+ exit 1
581
+ fi
582
+ ;;
1034
583
  prose)
1035
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
1036
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
584
+ echo "WARNING: validate-pr-target.sh not installed (prose tier) — lane validation skipped; the review phase still refuses to merge into main." >&2
1037
585
  ;;
1038
586
  esac
1039
587
  ```
1040
-
1041
- → STOP. Each sub-issue runs its own `/work-on`.
588
+ On a mismatch → STOP; do not invoke Phase 4. <!-- Added: forge#671 -->
1042
589
 
1043
590
  ---
1044
591
 
1045
592
  ## Phase 3: Build
1046
593
 
1047
- <!-- FORGE:PHASE_COMPLETE — Entering Phase 3 (Build). See Universal Phase Dispatcher: sub-phases 3A–3M execute in sequence. No sub-phase completion is terminal. -->
1048
-
1049
- **Canonical path**: Sub-phases 3A–3M run **inline** in the current context window for STANDARD and fast-lane issues, with one standing exception: 3G (quality gate) always forks to a sub-agent under Row (d). This is the single authoritative build topology. `work-on/build.md` and `work-on-monolithic.md` ([BENCHMARK]) describe the same inline model with different levels of detail; they are not separate competing paths. `Skill()`/`Agent()` sub-agent spawns for build sub-phases are only permitted under a Spawn-Decision Table Row (c) or Row (d) exception — Row (c): ≥20 Skill invocations or ≥10 files changed before the build; Row (d): the sub-phase's own runtime risks exceeding the prompt-cache TTL regardless of build size (3G's standing exception). <!-- Added: forge#1276, updated: forge#1825 -->
1050
-
1051
- **Skip if**: `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment. <!-- Added: forge#1305 — require completion marker, not mere presence of BUILDER annotation -->
1052
-
1053
- **Partial build detection**: If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → the build was interrupted after implement.md Phase I6 (comment posted) but before validate.md Phase V5 (commit). Delete the partial comment and restart Phase 3 from the top: <!-- Added: forge#1305 -->
1054
- ```bash
1055
- PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1056
- --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
1057
- if [ -n "$PARTIAL_ID" ]; then
1058
- gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
1059
- echo "Deleted partial FORGE:BUILDER comment (no FORGE:BUILDER:COMPLETE) — restarting build"
1060
- fi
1061
- ```
1062
-
1063
- **CRITICAL: You MUST execute ALL sub-phases 3A–3M in order. Sub-phases 3C.5 (context) and 3C.6 (architect) are skipped ONLY for TRIVIAL tasks and Investigation tasks — see Phase 3B for classification. For STANDARD and COMPLEX tasks they post mandatory `FORGE:CONTEXT` and `FORGE:ARCHITECT` comments that Phase 3F reads as its primary input. Skipping them without a TRIVIAL/Investigation classification degrades build quality and causes review findings. After each sub-phase, continue to the next — no sub-phase is terminal.**
1064
-
1065
- ### 3A: Confirm state (reuse in-context; re-read only on compaction)
1066
-
1067
- **Post Phase 3 heartbeat** (skip unless `UNDER_ORCHESTRATION` is `true`; also skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
1068
- ```bash
1069
- PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
1070
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
1071
- **Phase**: Phase 3 — Build
1072
- **Timestamp**: ${PHASE_START_TIMESTAMP}
1073
- **Issue**: #{NUMBER}"
1074
- ```
1075
-
1076
- If this is a continuous run (Phase 1 already executed in this same session), you already have the issue view, the FORGE:INVESTIGATOR body, and know whether FORGE:BUILDER:COMPLETE/FORGE:FAST_PATH exist from having posted or checked for them earlier — reuse those values, do not re-fetch. Only run the block below if one of those values is genuinely missing from this session's context (fresh/resumed session after compaction, or checkpoint routing jumped straight here):
1077
-
1078
- ```bash
1079
- gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
1080
-
1081
- # Read investigation report
1082
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1083
- --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
1084
-
1085
- # Check if build already completed (require FORGE:BUILDER:COMPLETE — not just FORGE:BUILDER)
1086
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1087
- --jq '.[] | select(.body | contains("FORGE:BUILDER:COMPLETE")) | .body'
1088
-
1089
- # Check for existing COMPLEXITY_BAND from a prior run (resume path)
1090
- EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1091
- --jq '.[] | select(.body | contains("FORGE:FAST_PATH")) | .body' 2>/dev/null | head -1)
1092
- ```
1093
-
1094
- If no investigation comment with `<!-- INVESTIGATION:COMPLETE -->` → STOP (investigation not complete).
1095
-
1096
- Extract from investigation: affected files, root cause, recommendation, task type.
1097
-
1098
- ### 3B: Classify task type and complexity
1099
-
1100
- **Step 1 — Task type classification:**
594
+ **Skip if**: a `FORGE:BUILDER` comment contains `<!-- FORGE:BUILDER:COMPLETE -->` (go to Phase 4).
1101
595
 
1102
- | Signal | Type | Approach |
1103
- |--------|------|----------|
1104
- | Title starts with "Investigate:"/"Audit:"/"Research:" | Investigation | Produce issues as deliverables |
1105
- | UI/UX, feature + web/ files | UI/UX | `frontend-design` skill |
1106
- | Feature + services/ | Backend Feature | Implement directly |
1107
- | Feature + both | Full-Stack | Backend first, then frontend-design |
1108
- | Bug + web/ | Frontend Fix | Direct |
1109
- | Bug + services/ | Backend Fix | Direct |
1110
- | Refactor/docs | Maintenance | Direct |
596
+ Post the Phase 3 heartbeat, run Lane Resolution, then:
1111
597
 
1112
- **Investigation tasks — early exit (BEFORE Phase 3C):** If task type = Investigation, skip Phases 3C, 3C.5, and 3C.6 entirely. Post `<!-- FORGE:FAST_PATH -->` comment, then jump directly to Phase 3F (implement → issue creation path). Do NOT run the Builder Contract, Context Gathering, or Architecture Plan for investigation tasks.
1113
-
1114
- ```bash
1115
- # Post fast-path comment for investigation tasks
1116
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
1117
- ## Fast-Path Classification
1118
-
1119
- **COMPLEXITY_BAND**: INVESTIGATION
1120
- **Task type**: Investigation
1121
- **Rationale**: Title prefix 'Investigate:' (or task type = Investigation from investigator report) — skipping Builder Contract (3C), Context Gathering (3C.5), and Architecture Plan (3C.6). Jumping directly to Phase 3F (issue creation).
1122
- **Phases skipped**: 3C, 3C.5, 3C.6"
1123
598
  ```
1124
-
1125
- → Jump to Phase 3F immediately. Do not continue to Phase 3C.
1126
-
1127
- **Step 2 — Complexity classification (for non-Investigation tasks):**
1128
-
1129
- Classify COMPLEXITY_BAND based on affected file count and task nature:
1130
-
1131
- | Condition | COMPLEXITY_BAND |
1132
- |-----------|-----------------|
1133
- | Single file, doc/config/markdown only, no logic changes expected | TRIVIAL |
1134
- | 1–5 files, existing patterns, no cross-service impact | STANDARD |
1135
- | 6+ files, new abstractions, cross-service, migration, schema changes | COMPLEX |
1136
-
1137
- Post `<!-- FORGE:FAST_PATH -->` comment immediately after classification:
1138
-
1139
- ```bash
1140
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
1141
- ## Fast-Path Classification
1142
-
1143
- **COMPLEXITY_BAND**: {TRIVIAL|STANDARD|COMPLEX}
1144
- **Task type**: {TASK_TYPE}
1145
- **Affected file count**: {N}
1146
- **Rationale**: {one-sentence explanation of classification decision}
1147
- **Phases skipped**: {list phases skipped, or 'none — full pipeline' for STANDARD/COMPLEX}"
599
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BASE}")
1148
600
  ```
1149
601
 
1150
- **Resume path**: If `EXISTING_FAST_PATH` was read in Phase 3A, extract COMPLEXITY_BAND from it and skip re-classification.
1151
-
1152
- **TRIVIAL tasks**: After posting FORGE:FAST_PATH, skip Phase 3C.5 (Context Gathering) and Phase 3C.6 (Architecture Plan) only. Phase 3C (Builder Contract) is **retained** — it still runs. Continue: 3C (Builder Contract) → 3D → 3E → 3F → 3F.5 → 3G → 3H onward. When filling in **Phases skipped** in the FORGE:FAST_PATH comment, write: `3C.5, 3C.6`.
1153
-
1154
- **STANDARD and COMPLEX tasks**: Run full pipeline — 3C → 3C.5 → 3C.6 → 3D onward. No phases skipped.
1155
-
1156
- ### 3C: Builder Contract (MANDATORY)
1157
-
1158
- Post `<!-- FORGE:CONTRACT -->` comment with: task type, proposed approach, deliverables table (file/change/why), acceptance criteria, quality considerations (auth model, new env vars, SQL safety, security surface), out of scope, alternatives.
1159
-
1160
- ```bash
1161
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
1162
- ## Builder Contract
1163
-
1164
- **Task type**: {TASK_TYPE}
1165
-
1166
- ### Proposed Approach
1167
- {BRIEF_APPROACH_DESCRIPTION}
1168
-
1169
- ### Deliverables
1170
- | File | Change | Why |
1171
- |------|--------|-----|
1172
- {DELIVERABLES_ROWS}
1173
-
1174
- ### Acceptance Criteria
1175
- {ACCEPTANCE_CRITERIA_CHECKLIST}
1176
-
1177
- ### Quality Considerations
1178
- {AUTH_MODEL_NEW_ENV_VARS_SQL_SAFETY_SECURITY_SURFACE}
602
+ The build phase owns classification (`FORGE:FAST_PATH`), the worktree, the `workflow:building` label, the contract, and runs its children — context, architect, implement, validate (with the quality gate) — each as its own forked skill, then the acceptance gate and the build checkpoint.
1179
603
 
1180
- ### Out of Scope
1181
- {OUT_OF_SCOPE_ITEMS}"
1182
- ```
1183
-
1184
- Contract must be grounded in the investigation report. Adversarially validate proposed fixes against adjacent system layers.
604
+ | `BUILD_RESULT` | Router action |
605
+ |---|---|
606
+ | `COMPLETE` | Marker gate (below) → record `BRANCH` and `WORKTREE_PATH` from the result → Phase 4 |
607
+ | `ALREADY_DONE` | Record `BRANCH`/`WORKTREE_PATH` (from the result, or `git worktree list` for the issue branch) → Phase 4 |
608
+ | `INVESTIGATION_COMPLETE` | Investigation-type task: deliverable issues were filed and the original closed by the phase → Phase 5 with `--terminal-state investigation` (no review) |
609
+ | `BLOCKED` | Terminal: confirm `needs-human` is present (the phase adds it with the blocker) and STOP |
1185
610
 
1186
- ### 3C.5: Context Gathering (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
611
+ **Marker gate — Phase 3 exit**: a `FORGE:BUILDER` comment must contain `FORGE:BUILDER:COMPLETE`. If absent, invoke `work-on:build` once more with the same args; if still absent, post `<!-- FORGE:GATE_FAILURE -->` ("builder completion marker missing after one re-invoke"), add `needs-human`, remove `workflow:building`, STOP. <!-- forge#1418 -->
1187
612
 
1188
- **Skip if COMPLEXITY_BAND: TRIVIAL** (classified in Phase 3B) — post nothing, proceed directly to Phase 3C.6. Trivial single-file changes have no institutional memory value to surface.
613
+ ---
1189
614
 
1190
- **For STANDARD and COMPLEX tasks**: This phase is NOT optional. Run it regardless. Do NOT skip it without a TRIVIAL classification from Phase 3B.
615
+ ## Phase 4: Review (push → PR → /review-pr → merge)
1191
616
 
1192
- Surface institutional memory before writing code. Extract function names from the contract deliverables table:
617
+ Post the Phase 4 heartbeat, run Lane Resolution including the lane validation, then:
1193
618
 
1194
- ```bash
1195
- FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1196
- --jq '.[] | select(.body | contains("FORGE:CONTRACT")) | .body' \
1197
- | awk '/^### Deliverables/{p=1; next} /^### /{p=0} p' \
1198
- | grep -oE '`[A-Za-z_][A-Za-z0-9_]*`' \
1199
- | tr -d '`' | sort -u | tr '\n' ' ' | xargs)
1200
619
  ```
1201
-
1202
- **The ONLY acceptable skip conditions** (all must be true): Issue is a 1-file config/docs edit with no code logic AND affected files have zero git history. In all other cases, run context gathering.
1203
-
1204
- **Batch execution (MANDATORY)**: C1, C2 (including the direct commit-body read and the pickaxe pass), C3, and C4 below are mutually independent — none consumes another's output. Issue ALL of their underlying `gh`/`git`/`grep` calls as a single batch of parallel tool calls in one message, not as four sequential steps. This is the same rule as the "independent tool calls → same message" convention used elsewhere in the pipeline; it turns a ~4x serial round-trip chain (each `gh api` call is 0.5-2s) into one wall-clock round-trip. The per-file loop inside C1 and the per-function loop inside C3 are themselves independent across iterations — include every iteration's call in the same batch rather than looping turn-by-turn. Total budget is still 20s per query / 2 min overall; batching only removes serialization, it does not change the timeout.
1205
-
1206
- Queries to batch (20s timeout each, 2 min total budget):
1207
-
1208
- **C1: Past Review Findings on These Files**
1209
- ```bash
1210
- # {AFFECTED_FILES} is a space-separated argument (see --files contract) — split
1211
- # explicitly on IFS=' ' into an array instead of a bare `for file in {AFFECTED_FILES}`,
1212
- # which word-splits on the shell's default IFS (space, tab, AND newline) and
1213
- # would corrupt any path containing a space.
1214
- IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
1215
- for file in "${AFFECTED_FILES_ARR[@]}"; do
1216
- basename=$(basename "$file" .py)
1217
- gh issue list -R {GH_REPO} --state closed --label "review-finding" \
1218
- --search "$basename" --limit 10 \
1219
- --json number,title,body \
1220
- --jq '.[] | {number, title,
1221
- pattern: (.body | capture("\\*\\*Pattern\\*\\*: *(?<p>[^\\n]+)").p // null),
1222
- prevention: (.body | capture("\\*\\*Prevention\\*\\*: *(?<v>[^\\n]+)").v // null),
1223
- root_cause: (.body | capture("\\*\\*Root cause\\*\\*: *(?<rc>[^\\n]+)").rc // (.body | capture("Root Cause[^\\n]*\\n(?<rc>[^\\n]+)").rc // "see body"))
1224
- }'
1225
- done
620
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:review", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE}")
1226
621
  ```
1227
622
 
1228
- **C2: Past Bugs in the Same Module**
1229
- ```bash
1230
- git log --oneline -30 -- {AFFECTED_FILES} | grep -oE '#[0-9]+' | sort -u | head -8
1231
- # For each issue: fetch title + root cause, keep only bug/fix/review-finding labeled. Max 5.
1232
- ```
623
+ The review phase owns: ancestry and empty-branch guards, push, PR creation, `workflow:in-review`, the phase-trail preflight (and the single re-dispatch of any missing phase via `Skill`), `/review-pr --auto-merge` (CI gate and reviewed-head guard included), stale-review re-review, and the REVIEW checkpoint. It never closes the issue.
1233
624
 
1234
- **Direct commit-body read (bounded, prefer over `gh api` when it already answers "why")**: read the top 5 commit subjects+bodies on the affected files directly — local git is near-free relative to `gh api` round-trips, and commit bodies often explain the "why" without needing to fetch a linked issue at all:
1235
- ```bash
1236
- git log -5 --format='%h %ad %s%n%b' --date=short -- {AFFECTED_FILES}
1237
- ```
1238
- If a commit body fully explains a prior bug/fix (common for squashed or fix-up commits with no `#NNN` reference), use it directly as a "Past Bug in This Module" entry — do not require a linked GitHub issue to exist.
625
+ | `REVIEW_RESULT` | Router action |
626
+ |---|---|
627
+ | `COMPLETE` (PR merged) | Record `PR_NUMBER` → Phase 5 with `--terminal-state merged` |
628
+ | `ALREADY_MERGED` | Same as COMPLETE |
629
+ | `BLOCKED` | Terminal: the phase already added `needs-human` (or `workflow:awaiting-merge` for a deploy-gate hold) with the blocker. Do NOT merge, re-run phases, or add labels here. STOP. |
1239
630
 
1240
- **Pickaxe pass (has this exact area been fixed before?)** — one bounded pass, capped at 5 hits, keyed on the suspected symbol/string from the Builder Contract or investigation report:
1241
- ```bash
1242
- git log -S"{suspected_symbol_or_string}" --oneline -- {AFFECTED_FILES} | head -5
1243
- # Use -G instead of -S for regex patterns
1244
- git log -G"{pattern}" --oneline -- {AFFECTED_FILES} | head -5
1245
- ```
1246
- Any hit is a candidate prior fix or reintroduced defect for this exact code area — read `git show {hash}` to confirm scope before including it in the output. This catches regressions the issue-number harvest above misses (e.g. a defect fixed via a squashed commit with no `#NNN` reference).
631
+ ---
1247
632
 
1248
- **C3: Related Code Paths** (callers/importers of FUNCTION_NAMES)
1249
- ```bash
1250
- for fn in {FUNCTION_NAMES}; do
1251
- grep -r "$fn" {WORKTREE_PATH} --include="*.py" -l | grep -v __pycache__ | head -5
1252
- grep -r "$fn" {WORKTREE_PATH}/web/src --include="*.ts" --include="*.tsx" -l 2>/dev/null | head -5
1253
- done
1254
- ```
633
+ ## Phase 5: Close & trajectory
1255
634
 
1256
- **C4: Successful Similar Implementations**
1257
- ```bash
1258
- gh pr list -R {GH_REPO} --state merged --search "{domain_keywords}" --limit 5 \
1259
- --json number,title,files --jq '.[] | {number, title, file_count: (.files | length)}'
1260
635
  ```
1261
-
1262
- Post `<!-- FORGE:CONTEXT -->` comment with findings:
1263
- ```bash
1264
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTEXT -->
1265
- ## Implementation Context for #{NUMBER}
1266
-
1267
- ### Known Pitfalls for This Area
1268
- {prevention rules from past review-findings}
1269
-
1270
- ### Historical Findings on These Files
1271
- {past review-finding issues}
1272
-
1273
- ### Past Bugs in This Module
1274
- {closed bug issues from git log mining, PLUS pickaxe-derived findings (commits with no linked issue, or commit
1275
- bodies read directly per the C2 direct-commit-body step)}
1276
-
1277
- ### Related Code Paths (must stay consistent)
1278
- {files that import/call changed functions}
1279
-
1280
- ### Patterns That Cause Bugs Here
1281
- {recurring bug types synthesized from C1+C2}
1282
-
1283
- ### Successful Similar Implementations
1284
- {positive patterns from C4}
1285
-
1286
- <!-- FORGE:CONTEXT:COMPLETE -->"
636
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:close", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --pr {PR_NUMBER} --base {PR_BASE} --branch {BRANCH} --worktree {WORKTREE_PATH} --terminal-state {merged|investigation|decomposed|invalid}")
1287
637
  ```
638
+ Omit `--pr/--branch/--worktree` when the value is unknown (PR-less terminals).
1288
639
 
1289
- If total time exceeds 2 minutes, post partial results with `<!-- FORGE:CONTEXT:PARTIAL -->`.
640
+ | `CLOSE_RESULT` | Router action |
641
+ |---|---|
642
+ | `COMPLETE` / `ALREADY_DONE` | Pipeline done. If `IS_BATCH` (Phase 0B), run the batch-member closure below. Print the phase's report/card verbatim and stop. |
643
+ | `PHASE_COMPLETE` | Multi-phase issue: this phase merged, more remain. The issue is open again at `workflow:investigating` → re-run Phase 0 and continue from the next phase. |
644
+ | `FAILED` | Invoke `work-on:close` once more with the same args (it is idempotent). If it fails again, post the blocker as a comment, add `needs-human`, STOP. |
1290
645
 
1291
- ### 3C.6: Architecture Plan (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
646
+ **Batch-member closure** (only when `IS_BATCH` and the batch PR merged): for each member in `BATCH_MEMBERS`, re-read its live state and labels; leave open (and report as a split outcome) any member that is closed, `needs-human`, `blocked` or `operator-only`; otherwise close it with "Resolved as part of batch PR #{PR_NUMBER} (#{NUMBER})" and add `workflow:merged`.
1292
647
 
1293
- **Skip if COMPLEXITY_BAND: TRIVIAL** (classified in Phase 3B) — post nothing, proceed directly to Phase 3D. Trivial single-file changes have no multi-path consistency risk.
1294
-
1295
- **For STANDARD and COMPLEX tasks**: This phase is NOT optional. Always run it. Even a 1-file STANDARD fix benefits from cross-path consistency checks. Do NOT skip without a TRIVIAL classification from Phase 3B.
1296
-
1297
- Trace ALL affected code paths before writing code.
1298
-
1299
- **Additional skip condition** (STANDARD tasks only): Issue creates ONLY new files with no existing callers AND title starts with "docs:" or "chore:".
648
+ ---
1300
649
 
1301
- **A1: Read Entry Points** — For each affected file: identify the primary function, all callers (grep), and sibling implementations. Read 3–5 most relevant files, max 8 total.
650
+ ## Remediation entry (`--remediate`)
1302
651
 
1303
- **A1.5: Route-Tree Classification for Shared Components** *(conditional — skip if no files under `components/` are affected, except `components/ui/primitives/`)* — When a change adds a new hook call or context dependency to a shared component, classify ALL call sites found in A1 by route context:
1304
- 1. **Authenticated routes**: Callers under `app/dashboard/`, `app/(authenticated)/`, or any layout that wraps children with an auth provider (e.g., `UserProvider`, `SessionProvider`).
1305
- 2. **Public routes**: Callers under `app/(public)/`, `app/playground/`, `app/(marketing)/`, or any layout without the relevant provider.
1306
- 3. **If both categories have callers AND the new hook throws when its provider is absent**: add an explicit implementation step to either (a) guard the hook call with a null-context check, (b) make the hook return a safe default when called outside its provider, or (c) remove the hook from the shared component and move it to the authenticated-route-only caller. Document the split in the FORGE:ARCHITECT affected paths table. Do NOT leave a shared component that crashes public routes to be discovered by the FE review agent. <!-- Added: forge#381 -->
1307
-
1308
- **A2: Trace Data Flow** — From each entry point: Entry → Transform → Persist/Relay → Exit. Check if the proposed change needs to propagate to each step. **For every field or key read by the changed code, enumerate ALL write paths first** — search across all services for assignments to that field. If multiple code paths write different types to the same field (e.g. dict in the standard path, string in the auth-gated path), the implementation must handle all variants. Do not assume the type you see on the primary code path is the only possible type.
1309
-
1310
- **A2.1: Runtime UID × Volume Ownership Check** *(conditional — skip if no Dockerfile or entrypoint is affected)* — When the PR changes the container's runtime user (Dockerfile `USER` directive, `su-exec`, `gosu`, `setuid`), the architect MUST trace the full write-path chain before writing the implementation plan:
1311
- 1. **Enumerate volume mounts**: Read all `docker-compose*.yml` files for the affected service. List every named volume and its container mount point (e.g. `storage_shared:/app/storage`). Docker named volumes are created as root-owned by default — any UID change without a corresponding ownership fix will silently break writes.
1312
- 2. **Grep for filesystem writes**: Search the affected service's codebase for all filesystem write operations (`mkdir`, `Path.mkdir`, `write_bytes`, `open(`, `os.makedirs`, `shutil.copy`, `shutil.move`). For each write operation, identify the target path.
1313
- 3. **Cross-reference**: For each write path that falls under a named volume mount point, add an explicit implementation step to ensure ownership compatibility before the privilege drop — typically `chown -R <user>:<group> <mount_point>` in the entrypoint script before the `exec su-exec` / `exec gosu` call.
1314
- 4. **Add to FORGE:ARCHITECT deliverables table**: List the entrypoint or docker-compose change as an explicit deliverable. Do NOT leave volume ownership to be discovered by the builder or reviewer. <!-- Added: forge#323 -->
1315
-
1316
- **A2.2: Gate-Condition Caller Sweep** *(conditional — when the fix changes a gate condition that guards a function call or restricts a field)*: Before finalizing the affected-paths table, grep for all callers of the gated function across sibling files in the same service directory. For each caller, verify that the gate condition applied at the call site is semantically correct.
1317
- ```bash
1318
- # Identify the gated function from the issue/contract
1319
- GATED_FUNCTION="{function_being_called_inside_the_gate}"
1320
- SERVICE_DIR=$(dirname {PRIMARY_AFFECTED_FILE})
1321
- # Find all call sites
1322
- grep -rn "$GATED_FUNCTION" "$SERVICE_DIR" --include="*.py" | grep -v "#"
1323
- ```
1324
- For each call site found: read the surrounding gate condition (±10 lines). If the condition includes fields that do NOT require the gated resource (e.g., `extraction_schema` gated behind an LLM key check when `extraction_schema` is processed without an LLM), add that caller file to the FORGE:ARCHITECT affected-paths table with a note explaining the incorrect gate condition.
1325
-
1326
- **Do NOT** omit sibling callers from the affected-paths table simply because they were not listed in the issue spec. The architect's scope is determined by code correctness, not by the issue spec's file list. A gate-condition bug that exists identically in 3 router files must be fixed in all 3 — even if the issue only named 1. <!-- Added: forge#383 -->
1327
-
1328
- **A2.5: Pipeline Phase-Dependency Check** *(Forge pipeline changes only — skip if no `commands/*.md` file is affected)* — Identify artefact contracts the changed file produces (comment markers, structured output blocks, Skill() invocation signatures, label transitions). Find downstream pipeline phases that consume those artefacts. Flag any invocation signature change and verify all callers. Emit a checklist of downstream phases that must be updated — appended to the Consistency Checks block in the FORGE:ARCHITECT comment.
1329
-
1330
- **A3: Consistency Rules** — Identify invariants all paths must satisfy: null handling, validation, logging, error response shape, auth checks.
1331
-
1332
- **A4: Sequence Implementation** — Order: schema/type changes first → core logic → secondary paths → tests → config/env. Files imported by others change before importers. Higher risk first.
1333
-
1334
- **A5: Risk Assessment** — Rate each non-obvious interaction: HIGH/MEDIUM/LOW with mitigation.
1335
-
1336
- Post `<!-- FORGE:ARCHITECT -->` comment:
1337
- ```bash
1338
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
1339
- ## Implementation Plan for #{NUMBER}
1340
-
1341
- ### Affected Paths (ALL must be updated)
1342
- | # | File | Function/Class | Change Required | Why |
1343
- |---|------|----------------|-----------------|-----|
1344
- {rows}
1345
-
1346
- ### Implementation Order
1347
- 1. {FIRST_CHANGE} — {WHY_FIRST}
1348
- 2. {SECOND_CHANGE} — {WHY_SECOND}
1349
-
1350
- ### Consistency Checks
1351
- - [ ] {INVARIANT_1}
1352
- - [ ] {INVARIANT_2}
1353
-
1354
- ### Risk Assessment
1355
- | Risk | Severity | Mitigation |
1356
- |------|----------|------------|
1357
- {rows}
1358
-
1359
- ### Files to Read Before Coding
1360
- - \`{FILE}\` — {WHY_READ_IT}
1361
-
1362
- <!-- FORGE:ARCHITECT:COMPLETE -->"
1363
- ```
1364
-
1365
- If budget exceeded (3 min), use `<!-- FORGE:ARCHITECT:PARTIAL -->`.
1366
-
1367
- ### 3D: Set building label
1368
- ```bash
1369
- RESOLUTION=$(resolve_script 'transition-label')
1370
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1371
- case "$TIER" in
1372
- adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} building ;;
1373
- prose)
1374
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
1375
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1376
- ;;
1377
- esac
1378
- ```
1379
-
1380
- ### 3E: Create worktree
1381
-
1382
- Branch slug from title (lowercase, hyphenated, max 40 chars). Prefix: `fix/` (bugs) or `feat/` (features).
1383
-
1384
- **Compute `PR_BASE` before worktree creation** — the source branch for the worktree MUST match the PR target. Compute `PR_BASE` now using `classify-lane.sh` so both worktree creation and Phase 4C use the same deterministic value. <!-- Added: forge#639 -->
1385
-
1386
- ```bash
1387
- # Compute PR_BASE deterministically from issue milestone — no LLM interpretation
1388
- RESOLUTION=$(resolve_script 'classify-lane')
1389
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1390
- case "$TIER" in
1391
- adaptive|universal)
1392
- if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
1393
- gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to compute PR target — see script error above. Adding needs-human."
1394
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1395
- exit 1
1396
- fi
1397
- ;;
1398
- prose)
1399
- # classify-lane has no valid prose fallback — the script output is authoritative.
1400
- # Without it, PR target cannot be determined safely. Add needs-human and stop.
1401
- gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot compute PR target deterministically. Adding needs-human."
1402
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1403
- exit 1
1404
- ;;
1405
- esac
1406
- ```
1407
-
1408
- **Determine source branch**:
1409
- - Review-finding → parse `**Code branch**: \`{branch}\`` from issue body; branch from `origin/{branch}`
1410
- - **Milestone review-finding hybrid lane** (Code branch matches `milestone/*`): High-risk lane. NEVER use `git merge` to resolve conflicts — use `git rebase` or `git cherry-pick` only. If conflicts can't be resolved without merge, post comment, add `needs-human`, STOP.
1411
- - **Missing ref fallback**: After parsing, verify the Code branch still exists on remote. If not (e.g., the source PR's head branch was deleted post-merge before a corrected stamp took effect), fall back to `PR_BASE` (the lane default) and note the fallback:
1412
- ```bash
1413
- SOURCE_BRANCH="{CODE_BRANCH_FROM_ISSUE_BODY}"
1414
- if ! git ls-remote --exit-code origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
1415
- echo "WARNING: Code branch '$SOURCE_BRANCH' not found on remote — falling back to PR_BASE '$PR_BASE'"
1416
- SOURCE_BRANCH="$PR_BASE"
1417
- fi
1418
- ```
1419
- - Feature lane (has milestone) → branch from `origin/{PR_BASE}` (PR_BASE now set above)
1420
- - Fast lane (no milestone) → branch from `origin/{PR_BASE}` (PR_BASE = `{STAGING_BRANCH}`)
1421
-
1422
- ```bash
1423
- cd {REPO_PATH}
1424
- git fetch origin
1425
- BRANCH="fix/{slug}-{NUMBER}"
1426
- WORKTREE_ROOT="{REPO_PATH}/.claude/worktrees"
1427
- if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
1428
- [ -n "${OPENCODE_SESSION_ID:-}" ] ||
1429
- [ -n "${OPENCODE_PID:-}" ] ||
1430
- [ -n "${OPENCODE:-}" ]; then
1431
- WORKTREE_ROOT="{REPO_PATH}/.opencode/worktrees"
1432
- elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
1433
- WORKTREE_ROOT="{REPO_PATH}/.codex/worktrees"
1434
- fi
1435
- WORKTREE_PATH="${WORKTREE_ROOT}/{BRANCH_SLUG}"
1436
- git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{PR_BASE}
1437
- ```
1438
-
1439
- If worktree already exists: verify correct branch, reuse or remove and recreate.
1440
-
1441
- ### 3F: Implement
1442
-
1443
- **Load context chain** — Read these from GitHub BEFORE writing code:
1444
- 1. `FORGE:ARCHITECT` comment (primary implementation guide — if present, follow its ordered list exactly)
1445
- 2. `FORGE:INVESTIGATOR` comment (root cause, affected files)
1446
- 3. `FORGE:CONTRACT` comment (deliverables, acceptance criteria)
1447
- 4. `FORGE:CONTEXT` comment (pitfalls, related paths, past bugs)
1448
-
1449
- If `FORGE:ARCHITECT` is absent, fall back to investigation report + contract.
1450
-
1451
- **Route by task type**: Bug Fix → implement directly. Feature (backend) → implement directly. Feature (UI/UX) → invoke `frontend-design` skill. Full-Stack → backend first, then frontend-design. Investigation → create issues, skip to Phase 7.
1452
-
1453
- **Implementation rules**:
1454
- - Work in `{WORKTREE_PATH}` — all file reads, writes, git ops happen here
1455
- - Read the current file before modifying it — never assume its state
1456
- - Read related files identified in context briefing before touching changed code
1457
- - Follow architect plan's implementation order exactly (when present)
1458
- - For each acceptance criterion: implement it, then verify it's met
1459
- - Do NOT add unrequested scope — contract out-of-scope stays out
1460
- - **Library callback verification**: When writing a lambda/callable passed to a library parameter, MUST verify expected calling convention BEFORE writing it. Check library's default value, documentation, or source code. Wrong arity causes runtime `TypeError` invisible to static analysis. (Ref: PR #14391 — `lambda _: ""` passed where SQLAlchemy expects 0 args)
1461
- - **Cross-lane import guard**: Before adding any `import` or `from X import Y` statement for a service-internal module (`app.*`), verify the module exists on the PR's base branch — NOT just on your local disk or a milestone branch. Run `git show origin/{base_branch}:{module_path}.py` (replacing dots with slashes) to confirm. If the module only exists on a milestone branch, do NOT import it — find an alternative implementation or make the import conditional with a `try/except ImportError` fallback. A milestone-only import on a fast-lane PR will crash production on every request with `ModuleNotFoundError`. (Ref: forge#277 — builder imported `app.billing.subscriptions` from `milestone/subscription-model`, which doesn't exist on `staging`/`main`, causing P1 production crash for paying customer)
1462
- - **Deliverable-type consistency check**: Before committing, compare the actual output against the CONTRACT's deliverable list. If the CONTRACT explicitly states "no code changes required", "docs only", or "configuration update only" AND the diff introduces new executable files (`.py`, `.js`, `.ts`, `.sh` — not test, config, or documentation files), STOP. Do NOT commit. Re-read the CONTRACT, the investigation recommendation, and the ARCHITECT plan. If all three agree that code is needed, update the CONTRACT comment to reflect the new deliverable type before proceeding. If only the builder decided to add code without contract support, discard the code change and implement the contracted deliverable instead. <!-- Added: forge#279 -->
1463
- - **Pipeline check documentation — generalization rule**: When writing or updating pipeline check documentation (in `commands/*.md`), describe the **bug class**, not a specific incident. Do NOT embed: PR numbers, issue numbers, run IDs, timestamps, function names, dollar amounts, or multi-sentence incident timelines in check prose or `**Evidence**:` blocks. One brief HTML comment `<!-- Added: forge#NNN -->` is acceptable per check for traceability. `**Evidence**:` blocks must describe the vulnerability pattern (what the class of bug looks like, why it's dangerous) — not narrate a single historical occurrence. CHANGELOG entries may reference originating issues; command prompt text must not.
1464
- - **Endpoint response contract consumer tracing**: When changing an endpoint's response body shape or status field values (e.g., changing `"status": "healthy"` to `"status": "ok"`, renaming response keys, removing fields), grep the full repo for ALL consumers of that response body before committing. Consumers are not limited to the service being changed — they include deploy scripts, CI health checks, monitoring configs, docker-compose healthcheck definitions, Traefik probes, and any script that parses or pattern-matches on the response body. Run: `grep -rn "{old_value}\|{endpoint_path}" scripts/ infra/ .github/ docker-compose*.yml traefik/ 2>/dev/null`. All consumers whose behavior depends on the old response format MUST be updated in the same PR — a response contract change that updates only one consumer while leaving others on the old format is a deploy-time breakage. <!-- Added: forge#321 -->
1465
- - If contract is wrong (file doesn't exist, function has different signature): STOP, post comment, add `needs-human`, EXIT
1466
-
1467
- ### 3F.5: Env/Config Completeness Check
1468
-
1469
- Run BEFORE committing. Read-only scan of working changes.
1470
-
1471
- **Trigger**: Run whenever diff introduces env vars, touches infra/deploy configs, or adds literal IPs.
1472
-
1473
- **Check 1 — New env var sync**: Scan for `os.getenv`/`process.env.` references. For each, verify present in `.env.example`, `ENV_VARS.md`, `env_validation.py`. Add if missing.
1474
-
1475
- **Check 2 — Deploy/infra restart risk**: If docker-compose/deploy/infra files changed, scan for restart-inducing changes. Annotate commit with `[restart: <service>]`.
1476
-
1477
- **Check 3 — Hardcoded IPs and credentials**: Scan for bare IPv4 literals and credential-like assignments. HARD BLOCKER — replace with env vars before staging.
1478
-
1479
- **Check 4 — SDK/API Literal sync advisory** (trigger: diff contains `Literal[` in a schema file):
1480
- ```bash
1481
- # Detect Literal type changes in API schema files
1482
- cd {WORKTREE_PATH}
1483
- LITERAL_CHANGES=$(git diff HEAD -- | grep -E '^\+.*Literal\[' | grep -v '^\+\+\+')
1484
- if [ -n "$LITERAL_CHANGES" ]; then
1485
- echo "SDK SYNC ADVISORY: Literal type changed in schema."
1486
- echo "Changed Literal lines:"
1487
- echo "$LITERAL_CHANGES"
1488
- echo ""
1489
- echo "ACTION REQUIRED — verify SDK method/type lists match new API schema:"
1490
- echo " - sdk/python/*/client.py: check _valid_methods or equivalent list"
1491
- echo " - sdk/node/src/index.ts: check JSDoc @param Literal type annotation"
1492
- echo " - web/public/openapi*.json: check enum arrays for affected field"
1493
- echo " - web/public/openapi-versions/*.json: check all versioned specs"
1494
- echo ""
1495
- echo "Inconsistency example: API schema narrows Literal['GET','POST','PUT','PATCH','DELETE']"
1496
- echo "to Literal['GET','POST'] but SDK JSDoc still lists all 5 methods — API returns 422"
1497
- echo "for callers following SDK docs. This produces silent user-facing failures."
1498
- fi
1499
- ```
1500
-
1501
- This advisory is informational — it does NOT block the commit. But the implementer MUST check each listed file and add to the implementation scope if any SDK/spec file still documents the removed/changed Literal values. If SDK files need changes, add them to the current PR rather than leaving the inconsistency for review to catch.
1502
-
1503
- ### 3G: Quality Gate
1504
-
1505
- Skip for 1-file config/docs edits.
1506
-
1507
- **Fork the loop unconditionally under Spawn-Decision Table Row (d)** (see Phase 3G Cross-Reference above): quality-gate scans 14+ domains across up to 3 iterations — long enough to idle the parent's already-large accumulated context past the prompt-cache's ~5-minute TTL, forcing an uncached re-hydration on the parent's next turn. This applies regardless of changed-file count; there is no small-build exception. Dispatch with the `Agent` tool, not a nested `Skill()` call — a nested `Skill()` call still executes inside the parent's own context and would not stop the parent from idling past the cache TTL while the loop runs. The sub-agent needs nothing from the parent's context: it works directly against files on disk at `{WORKTREE_PATH}`. <!-- Added: forge#1825 -->
1508
-
1509
- ```
1510
- Agent(
1511
- subagent_type="general-purpose",
1512
- model="{SUBAGENT_MODEL}",
1513
- description="Quality gate for #{NUMBER}",
1514
- run_in_background=false,
1515
- prompt="Run the quality gate loop for issue #{NUMBER} in worktree {WORKTREE_PATH}.
1516
- NEVER use plan mode (EnterPlanMode).
1517
- Changed files: {CHANGED_FILES}
1518
-
1519
- iteration = 0
1520
- max_iterations = 3
1521
- while iteration < max_iterations:
1522
- iteration += 1
1523
- Skill('quality-gate', args='{CHANGED_FILES} --worktree {WORKTREE_PATH}')
1524
- if result == 'QUALITY GATE: PASS':
1525
- GATE_PASSED = true
1526
- break
1527
- else:
1528
- Fix each HIGH and MEDIUM finding directly in the files at {WORKTREE_PATH}
1529
- Re-stage the fixes (do not commit, do not push)
1530
-
1531
- Do not touch GitHub issue state — the caller handles labels/comments.
1532
- Return exactly one line: 'GATE_RESULT: passed={true|false} iterations={N} summary={one-line summary of remaining findings if failed, else \"clean\"}'"
1533
- )
1534
- ```
1535
-
1536
- Parse the returned `GATE_RESULT` line:
1537
- - `passed=true` → `GATE_PASSED = true` → continue to sub-phase 3H.
1538
- - `passed=false` (loop exhausted 3 iterations) → post "Quality Gate Failed After 3 Iterations" comment using the returned `summary` → add `needs-human` label → STOP.
1539
-
1540
- **Fallback**: if the `Agent` tool is unavailable in the current runtime (partial install), run the loop inline as before —
1541
-
1542
- ```
1543
- iteration = 0
1544
- max_iterations = 3
1545
-
1546
- while iteration < max_iterations:
1547
- iteration += 1
1548
- Skill("quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
1549
- if result == "QUALITY GATE: PASS":
1550
- GATE_PASSED = true
1551
- break
1552
- else:
1553
- Fix each HIGH and MEDIUM finding in {WORKTREE_PATH}
1554
- Re-stage fixes
1555
-
1556
- if iteration == max_iterations AND not PASS:
1557
- Post "Quality Gate Failed After 3 Iterations" comment
1558
- Add needs-human label → STOP
1559
- ```
1560
-
1561
- — and note in the builder comment that context isolation was degraded for this run.
1562
-
1563
- # MUST CONTINUE to sub-phase 3H (Format and verify) — quality gate PASS is intermediate, NOT terminal. <!-- Added: forge#220 -->
1564
-
1565
- **After the sub-agent returns `passed=true`: proceed immediately to sub-phase 3H below. Quality gate is an intermediate check — "PASS" means the code is clean, NOT that the build is done. Do NOT stop.**
1566
-
1567
- **After PASS: Do NOT re-read GitHub state, issue body, labels, or any file beyond what the sub-agent already changed on disk. Do NOT run any gh commands. Do NOT check PR status. Proceed directly to Phase 3H (Format and verify) below.** <!-- Added: forge#93 -->
1568
-
1569
- ### 3H: Format and verify
1570
-
1571
- All tool commands are read from `forge.yaml → verification.commands`. When a key is absent, the step logs `SKIPPED — not configured in verification.commands` and continues rather than silently passing. Before any test command executes (`learned.test_commands`, or a future `verification.commands.*.test` invocation), it is first checked against `verification.known_slow_tests` — a repo-declared list of patterns for suites known to hang or make live network/LLM calls — and skipped or narrowed to a safe subset on a match. When that config is absent, this gate is a no-op and behavior is unchanged. <!-- Added: forge#1861 -->
1572
-
1573
- **Track skipped checks** — initialize before any check runs:
1574
- ```bash
1575
- VERIFICATION_SKIPPED_CHECKS=""
1576
- ```
1577
-
1578
- **Python**:
1579
- ```bash
1580
- cd {WORKTREE_PATH}
1581
-
1582
- PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
1583
- if [ -n "$PYTHON_FORMAT" ]; then
1584
- eval "$PYTHON_FORMAT" 2>&1
1585
- else
1586
- echo "SKIPPED — python.format not configured in verification.commands"
1587
- VERIFICATION_SKIPPED_CHECKS="${VERIFICATION_SKIPPED_CHECKS:+$VERIFICATION_SKIPPED_CHECKS, }python.format"
1588
- fi
1589
-
1590
- # Compile check always runs (no config needed — catches syntax errors)
1591
- python -m py_compile {PYTHON_FILES}
1592
- ```
1593
- `py_compile` failures are BLOCKING.
1594
-
1595
- **TypeScript**:
1596
- ```bash
1597
- cd {WORKTREE_PATH}
1598
-
1599
- TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
1600
- TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
1601
- TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
1602
-
1603
- if [ -n "$TS_FORMAT" ]; then
1604
- eval "$TS_FORMAT" 2>&1
1605
- else
1606
- echo "SKIPPED — typescript.format not configured in verification.commands"
1607
- VERIFICATION_SKIPPED_CHECKS="${VERIFICATION_SKIPPED_CHECKS:+$VERIFICATION_SKIPPED_CHECKS, }typescript.format"
1608
- fi
1609
-
1610
- if [ -n "$TS_TYPECHECK" ]; then
1611
- eval "$TS_TYPECHECK" 2>&1
1612
- TS_EXIT=$?
1613
- elif [ -n "$TS_BUILD" ]; then
1614
- eval "$TS_BUILD" 2>&1 | tail -30
1615
- TS_EXIT=$?
1616
- else
1617
- echo "SKIPPED — typescript.typecheck and typescript.build not configured in verification.commands"
1618
- VERIFICATION_SKIPPED_CHECKS="${VERIFICATION_SKIPPED_CHECKS:+$VERIFICATION_SKIPPED_CHECKS, }typescript.typecheck/build"
1619
- TS_EXIT=0
1620
- fi
1621
- ```
1622
- Typecheck or build failures are BLOCKING.
1623
-
1624
- **Known-slow test gate** — Before running any test command below, check it against `verification.known_slow_tests` (a repo-declared list of test patterns known to hang or make live network/LLM calls). When absent or empty, this is a no-op and behavior is unchanged. <!-- Added: forge#1861 -->
1625
-
1626
- ```bash
1627
- # KNOWN_SLOW_TESTS read directly from verification.known_slow_tests (a static,
1628
- # operator-declared config — unlike learned.test_commands, it is NOT part of the
1629
- # agent-writable `learned:` section, so it is read inline here rather than
1630
- # pre-loaded in Phase 0B.1).
1631
- KNOWN_SLOW_TESTS=$(yq -o=json -I=0 '.verification.known_slow_tests // []' forge.yaml 2>/dev/null || echo '[]')
1632
-
1633
- # apply_known_slow_filter <cmd> — echoes the command to actually run, or "" to
1634
- # skip it entirely. Matching is substring match of `pattern` against the full
1635
- # command text. Exactly one of skip/subset is expected per matched entry.
1636
- apply_known_slow_filter() {
1637
- local cmd="$1"
1638
- local out="$cmd"
1639
- if [ -n "$KNOWN_SLOW_TESTS" ] && [ "$KNOWN_SLOW_TESTS" != "[]" ] && [ "$KNOWN_SLOW_TESTS" != "null" ]; then
1640
- while IFS= read -r entry; do
1641
- [ -z "$entry" ] && continue
1642
- pattern=$(echo "$entry" | yq '.pattern // ""')
1643
- skip=$(echo "$entry" | yq '.skip // false')
1644
- subset=$(echo "$entry" | yq '.subset // ""')
1645
- reason=$(echo "$entry" | yq '.reason // "no reason given"')
1646
- [ -z "$pattern" ] && continue
1647
- case "$cmd" in
1648
- *"$pattern"*)
1649
- if [ "$skip" = "true" ]; then
1650
- echo "SKIPPED — known-slow test matched pattern '$pattern' ($reason)" >&2
1651
- out=""
1652
- elif [ -n "$subset" ]; then
1653
- echo "SUBSTITUTED — known-slow test matched pattern '$pattern' ($reason); running safe subset instead" >&2
1654
- out="$subset"
1655
- fi
1656
- ;;
1657
- esac
1658
- done < <(echo "$KNOWN_SLOW_TESTS" | yq -o=json -I=0 '.[]' 2>/dev/null)
1659
- fi
1660
- echo "$out"
1661
- }
1662
- ```
1663
-
1664
- **Learned test commands** — After all `verification.commands` steps complete, run any commands from `learned.test_commands` (captured from owner corrections in Phase 1D or set manually in forge.yaml), filtered through the known-slow gate above:
1665
-
1666
- ```bash
1667
- # LEARNED_TEST_COMMANDS was set in Phase 0B.1 from forge.yaml → learned.test_commands
1668
- # If empty/null, this block is a no-op
1669
- if [ -n "$LEARNED_TEST_COMMANDS" ] && [ "$LEARNED_TEST_COMMANDS" != "[]" ]; then
1670
- echo "Running learned test commands..."
1671
- # yq outputs each entry on its own line with -r flag
1672
- echo "$LEARNED_TEST_COMMANDS" | yq '.[]' | while IFS= read -r cmd; do
1673
- [ -z "$cmd" ] && continue
1674
- FILTERED_CMD=$(apply_known_slow_filter "$cmd")
1675
- if [ -z "$FILTERED_CMD" ]; then
1676
- continue
1677
- fi
1678
- echo "Running learned command: $FILTERED_CMD"
1679
- eval "$FILTERED_CMD" 2>&1 | tail -30
1680
- CMD_EXIT=$?
1681
- if [ $CMD_EXIT -ne 0 ]; then
1682
- echo "FAILED (exit $CMD_EXIT): $FILTERED_CMD"
1683
- exit $CMD_EXIT
1684
- fi
1685
- done
1686
- else
1687
- echo "No learned test commands configured — skipping"
1688
- fi
1689
- ```
1690
- Learned test command failures are BLOCKING (same as verification.commands failures). A command matched and skipped by the known-slow gate is never executed and never counted as a failure. <!-- Added: forge#667, forge#1861 -->
1691
-
1692
- ### 3I: Frontend proxy wiring check (MANDATORY)
1693
-
1694
- Skip if no TS/TSX files changed.
1695
-
1696
- All client-side `fetch`/`useSWR`/`apiFetch`/`axios` MUST use `/api/...` proxy routes, NEVER `/api/v1/...` or hardcoded host:port. Scan and fix violations.
1697
-
1698
- ### 3I.5: Database Configuration Change Advisory
1699
-
1700
- Skip if no changed Python files contain DB engine/session/pool patterns.
1701
-
1702
- ```bash
1703
- cd {WORKTREE_PATH}
1704
- while IFS= read -r f; do
1705
- [ -z "$f" ] && continue
1706
- grep -qE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" "$f" 2>/dev/null && \
1707
- echo "DB CONFIG CHANGE DETECTED in: $f"
1708
- done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
1709
- ```
1710
-
1711
- Advisory only — does not block build. Check for lambda/callable in connect_args (the exact bug class from PR #14391).
1712
-
1713
- ### 3J: Deployment completeness check (MANDATORY)
1714
-
1715
- Skip if no new env vars introduced.
1716
-
1717
- **Config variables used by this phase** (set in `forge.yaml`):
1718
- - `{deploy.secrets_backend}` — secrets delivery method (`sops`, `aws-sm`, `vault`, `ci-env`, `none`). When absent or not `sops`, SOPS-specific checks below are skipped with an explicit log message.
1719
- - `{verification.services[name].container}` — container name for post-deploy verification. Resolved by matching the service name; falls back to `{service}` (bare name) when not configured.
1720
-
1721
- For each new env var, verify present in ALL required locations:
1722
-
1723
- | Location | Required for |
1724
- |----------|-------------|
1725
- | `.env.example` | All new vars |
1726
- | Secrets backend (see `deploy.secrets_backend`) | Secret vars — skip if backend is `none` or unset |
1727
- | `app/env_validation.py` | API service vars (if project has one) |
1728
- | `docker-compose.prod.yml` | Vars needing explicit injection (if project uses Docker Compose) |
1729
-
1730
- **Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*:
1731
-
1732
- If the project uses SOPS, verify the new var is present in all SOPS chain locations:
1733
- - `infra/secrets/prod.enc.yaml` — SOPS-encrypted secret store
1734
- - `infra/decrypt-secrets.sh` ENV_MAPPING — maps SOPS key to env var name
1735
- - Deploy chain: SOPS → `decrypt-secrets.sh` (ENV_MAPPING) → `.env.secrets` → `merge-env-secrets.sh` → `.env.production` → docker-compose `env_file`
1736
-
1737
- If `deploy.secrets_backend` is absent or not `sops`, skip these checks and log:
1738
- > `SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.`
1739
-
1740
- **Operator-set var classification** *(trigger: new env var is NOT in the configured secrets backend)*: <!-- Added: forge#380 -->
1741
-
1742
- Some env vars are operator-set (non-secret, not sourced from the secrets backend) — they must be manually added to the runtime environment on the production server. When a new env var has no entry in the secrets backend, classify it as operator-set and add a **HARD BLOCKER** item to the Testing Checklist.
1743
-
1744
- Resolve the container name for the verification command:
1745
- 1. Look up the service in `forge.yaml → verification.services[]` by name — use the `container` field if present.
1746
- 2. If no matching entry, fall back to the bare service name: `{service}` (no suffix).
1747
-
1748
- ```
1749
- - [ ] HARD BLOCKER: Add {VAR_NAME} to the runtime environment on the production server.
1750
- This var is operator-set — it does NOT flow through the automated secrets chain.
1751
- It must be added manually before or after deploy.
1752
- Verify with: docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
1753
- (CONTAINER_NAME resolved from verification.services[{service}].container in forge.yaml,
1754
- or bare service name if not configured)
1755
- ```
1756
-
1757
- **`env_file` re-read warning** *(trigger: any new env var added to `.env.production` path)*:
1758
-
1759
- > **Docker `env_file` re-read behavior**: New entries in `.env.production` are only read when a container is **recreated** (e.g., `docker compose up --force-recreate`). A plain `docker restart` restarts the existing container with its frozen env — new `env_file` entries are silently absent. The standard deploy workflow uses `--force-recreate` and handles this correctly. If any out-of-band restart is used, new env vars will not take effect.
1760
-
1761
- Add this warning to the Testing Checklist whenever a new env var is introduced (whether secret or operator-set).
1762
-
1763
- **Post-deploy in-container verification** *(trigger: any new env var)*:
1764
-
1765
- Add the following to the Testing Checklist so the deployer can confirm delivery after deploy.
1766
-
1767
- Resolve `{CONTAINER_NAME}` from `forge.yaml → verification.services[{service}].container`; use `{service}` (bare) if the field is absent.
1768
-
1769
- ```bash
1770
- # Verify env var reached the running container (run post-deploy)
1771
- docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
1772
- # Expected: {VAR_NAME}={value}
1773
- # If blank: container was not recreated — run: docker compose up --no-deps --force-recreate {service}
1774
- ```
1775
-
1776
- ### 3K: Commit
1777
-
1778
- Stage all changes and commit:
1779
-
1780
- ```bash
1781
- cd {WORKTREE_PATH}
1782
- git add -u
1783
- git commit -s -m "fix({SCOPE}): {description} (#{NUMBER})"
1784
- ```
1785
-
1786
- Conventional prefix: `fix`/`feat`/`refactor`/`docs`. Reference `#{NUMBER}` in message.
1787
-
1788
- **Post-commit ancestry audit (MANDATORY)**:
1789
- ```bash
1790
- cd {WORKTREE_PATH}
1791
- if git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1; then
1792
- MERGE_COMMITS=$(git log --merges HEAD ^origin/{PR_BASE} 2>/dev/null)
1793
- if [ -n "$MERGE_COMMITS" ]; then
1794
- # Post ancestry audit failure comment, add needs-human → STOP
1795
- fi
1796
- fi
1797
- ```
1798
-
1799
- ### 3L: Update issue body (MANDATORY)
1800
- Check off completed items, mark phases complete, add PR references.
1801
-
1802
- ### 3M: Post implementation comment
1803
- ```bash
1804
- # Compute verification status from VERIFICATION_SKIPPED_CHECKS (set in Phase 3H)
1805
- if [ -z "$VERIFICATION_SKIPPED_CHECKS" ]; then
1806
- VERIFICATION_STATUS="✅ All configured verification commands passed"
1807
- else
1808
- VERIFICATION_STATUS="⚠ Verification NOT run: ${VERIFICATION_SKIPPED_CHECKS} — verification.commands not configured for these checks"
1809
- fi
1810
-
1811
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
1812
- ## Implementation Complete
1813
-
1814
- **Branch**: \`{BRANCH}\`
1815
- **Commits**: {COMMIT_SHA(S)}
1816
- **Files changed**: {COUNT}
1817
- **Verification Status**: ${VERIFICATION_STATUS}
1818
- **Cost (build phase)**: ${PHASE_COST_USD:-unavailable} (best-effort — session telemetry; omit if unavailable)
1819
-
1820
- ### Approach
1821
- {what was built, key decisions}
1822
-
1823
- ### Changes
1824
- {bulleted list of file changes}
1825
-
1826
- ### Acceptance Criteria Status
1827
- {checklist from contract, marked pass/fail}
1828
-
1829
- ### Testing Checklist
1830
- - [ ] {scenario 1} [type:api]
1831
- - [ ] {scenario 2} [type:unit]
1832
-
1833
- > **Test-type annotation** (optional): Append `[type:api]`, `[type:unit]`, `[type:e2e]`, or `[type:manual]` to each checklist item. The test gate reads this annotation directly and skips regex inference. Omit it to rely on regex classification fallback.
1834
-
1835
- <!-- FORGE:BUILDER:COMPLETE -->"
1836
- ```
1837
-
1838
- Write machine-readable phase checkpoint (MUST execute immediately after FORGE:BUILDER comment is posted, before Phase 4):
1839
- ```bash
1840
- CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1841
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
1842
- \`\`\`json
1843
- {\"phase\": \"BUILD\", \"status\": \"COMPLETE\", \"next_phase\": \"REVIEW\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
1844
- \`\`\`"
1845
- ```
1846
-
1847
- **Marker gate — Phase 3 exit** (see Marker Gate table in Universal Phase Dispatcher): <!-- forge#1419, forge#1418 -->
1848
- ```bash
1849
- BUILD_MARKER=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1850
- --jq '[.[] | select(.body | contains("FORGE:BUILDER:COMPLETE"))] | length')
1851
- if [ "${BUILD_MARKER:-0}" -eq 0 ]; then
1852
- echo "MARKER GATE FAIL: FORGE:BUILDER:COMPLETE absent — re-invoking work-on/build once"
1853
- Skill(skill="work-on/build", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE}")
1854
- BUILD_MARKER=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1855
- --jq '[.[] | select(.body | contains("FORGE:BUILDER:COMPLETE"))] | length')
1856
- if [ "${BUILD_MARKER:-0}" -eq 0 ]; then
1857
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:GATE_FAILURE -->
1858
- ## Marker Gate Failure — Phase 3 (Build)
1859
-
1860
- **Expected marker**: \`FORGE:BUILDER:COMPLETE\` inside a \`FORGE:BUILDER\` comment
1861
- **Status**: Absent after subcommand re-invocation. Human review required.
1862
-
1863
- The router re-invoked \`work-on/build\` once but the marker was still not posted.
1864
- Inspect the subcommand output above for errors. <!-- forge#1418 -->"
1865
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" \
1866
- --remove-label "workflow:building" 2>/dev/null || true
1867
- exit 1
1868
- fi
1869
- fi
1870
- ```
652
+ Handled in Phase 0A.1: `/work-on <pr> --remediate [--issue N]` dispatches `Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", ...)` (forked) and STOPS after its `REMEDIATE_RESULT`.
1871
653
 
1872
654
  ---
1873
655
 
1874
- ## Phase 4: PR Creation
1875
-
1876
- ### 4A: Pre-push ancestry guard
1877
-
1878
- ```bash
1879
- cd {WORKTREE_PATH}
1880
- if git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1; then
1881
- MERGE_COMMITS=$(git log --merges {BRANCH} ^origin/{PR_BASE} 2>/dev/null)
1882
- if [ -n "$MERGE_COMMITS" ]; then
1883
- # Post ancestry guard failure, add needs-human → STOP
1884
- fi
1885
- fi
1886
- ```
1887
-
1888
- ### 4B: Push branch
1889
- ```bash
1890
- cd {WORKTREE_PATH} && git push -u origin {BRANCH}
1891
- ```
1892
- If fails: try `--force-with-lease`. If still fails: post comment, add `needs-human`, STOP.
1893
-
1894
- ### 4C: Determine PR target
1895
- `PR_BASE` was computed in Phase 3E. If somehow unset (e.g., resumed session after compaction), recompute:
1896
- ```bash
1897
- RESOLUTION=$(resolve_script 'classify-lane')
1898
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1899
- case "$TIER" in
1900
- adaptive|universal)
1901
- if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
1902
- gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to recompute PR target — see script error above. Adding needs-human."
1903
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1904
- exit 1
1905
- fi
1906
- ;;
1907
- prose)
1908
- # No valid prose fallback — see Phase 3E note.
1909
- gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot recompute PR target. Adding needs-human."
1910
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1911
- exit 1
1912
- ;;
1913
- esac
1914
- ```
1915
- Output is authoritative — no prose fallback. Script exits 1 on error (invalid issue, `gh` auth failure, or milestone branch absent on remote); treat non-zero exit as `needs-human` and STOP. <!-- Added: forge#669, forge#639 -->
1916
-
1917
- ### 4C.5: Validate PR target against classified lane
1918
- ```bash
1919
- RESOLUTION=$(resolve_script 'validate-pr-target')
1920
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1921
- case "$TIER" in
1922
- adaptive|universal)
1923
- bash "$SCRIPT_PATH" {PR_BASE} {CLASSIFIED_LANE}
1924
- ;;
1925
- prose)
1926
- # validate-pr-target has no safe prose fallback — silently skipping validation risks
1927
- # merging to the wrong branch. Log a warning but do NOT block the pipeline; the PR
1928
- # review step will catch a mismatched target before merge.
1929
- echo "WARNING: validate-pr-target.sh not installed (prose tier) — skipping lane validation. Confirm PR base manually." >&2
1930
- ;;
1931
- esac
1932
- ```
1933
- `{CLASSIFIED_LANE}` is the value returned by `classify-lane.sh` in Phase 4C. `{PR_BASE}` is the branch the PR will target. If exit code is 1 (mismatch):
1934
- ```bash
1935
- gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKING: validate-pr-target.sh — PR base \`{PR_BASE}\` does not match classified lane \`{CLASSIFIED_LANE}\`. Manual intervention required."
1936
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1937
- ```
1938
- → STOP. Do NOT proceed to Phase 4D. <!-- Added: forge#671 -->
1939
-
1940
- ### 4D: Create PR
1941
- ```bash
1942
- PR_URL=$(gh pr create {GH_FLAG} --base {PR_BASE} --head {BRANCH} \
1943
- --title "{Fix|Feat|Refactor}: {description}" \
1944
- --body "## Summary
1945
- {BRIEF_DESCRIPTION}
1946
-
1947
- ## Changes
1948
- {CHANGES_LIST}
1949
-
1950
- ## Testing
1951
- {TESTING_CHECKLIST}
1952
-
1953
- ---
1954
- Closes #{NUMBER}
1955
- **Implementation branch**: \`{BRANCH}\`
1956
- **Base**: \`{PR_BASE}\`")
1957
- PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
1958
- ```
1959
-
1960
- `Closes #{NUMBER}` documents intent but does NOT auto-close for non-default-branch PRs. Capture `PR_NUMBER` here — Phase 5A reuses it instead of re-querying `gh pr list`.
1961
-
1962
- If PR already exists for this branch, use the existing PR number.
1963
-
1964
- ### 4E: Update labels
1965
- ```bash
1966
- RESOLUTION=$(resolve_script 'transition-label')
1967
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1968
- case "$TIER" in
1969
- adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
1970
- prose)
1971
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" \
1972
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1973
- ;;
1974
- esac
1975
- ```
1976
-
1977
- ---
1978
-
1979
- ## Phase 5: Auto-Review
1980
-
1981
- ### 5A: Confirm state (reuse in-context; re-read only on compaction)
1982
-
1983
- **Post Phase 5 heartbeat** (skip unless `UNDER_ORCHESTRATION` is `true`; also skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
1984
- ```bash
1985
- PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
1986
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
1987
- **Phase**: Phase 5 — Review
1988
- **Timestamp**: ${PHASE_START_TIMESTAMP}
1989
- **Issue**: #{NUMBER}"
1990
- ```
1991
-
1992
- `PR_NUMBER` was already captured from `gh pr create`'s output in Phase 4D — reuse it. Only run the lookup below if `PR_NUMBER` is genuinely unset (resumed session after compaction, or checkpoint routing jumped straight to Phase 4/5):
1993
- ```bash
1994
- gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state
1995
- PR_NUMBER=$(gh pr list {GH_FLAG} --head {BRANCH} --json number --jq '.[0].number')
1996
- ```
1997
-
1998
- ### 5B: Post progress comment
1999
- ```bash
2000
- gh issue comment {NUMBER} {GH_FLAG} --body "## Submitting for Review
2001
-
2002
- PR #${PR_NUMBER} created targeting \`{PR_BASE}\`. Invoking /review-pr with --auto-merge.
2003
-
2004
- <!-- FORGE:REVIEW_STARTED -->"
2005
- ```
2006
-
2007
- ### 5C: Invoke /review-pr with --auto-merge
2008
-
2009
- **Always fork — Row (d) supersedes Row (c) as the controlling reason** (run before invoking review-pr): <!-- Updated: forge#1825 (previously threshold-gated by Row (c) alone — forge#93) -->
2010
-
2011
- `/review-pr` spawns domain review agent(s) (observed at `effort: xhigh`) that run for minutes — long enough to idle the parent's accumulated context past the prompt cache's ~5-minute TTL under **Spawn-Decision Policy Row (d)**, regardless of build size — see the [Spawn-Decision Table](#spawn-decision-table). There is no small-build exception: review ALWAYS forks.
2012
-
2013
- - ALWAYS invoke `work-on/review` as a fresh sub-agent (via `Skill(skill="work-on/review", args="...")`) rather than calling review-pr directly. The sub-agent starts with a clean context window and re-reads all needed state from GitHub (Phase R0) — it does not depend on anything the parent accumulated.
2014
- - **Fallback only**: if `work-on/review` is not available (partial install), invoke review-pr directly and add a note in the progress comment that context isolation was degraded for this run.
2015
-
2016
- **Sub-agent invocation** (always, regardless of changed-file count or Skill-invocation count):
2017
-
2018
- Before spawning, build a distilled hot-copy of the key annotations the review sub-agent would otherwise re-fetch from GitHub. This reduces gh round-trips in the child without replacing the durable FORGE annotation record. <!-- Added: forge#1277 -->
2019
-
2020
- ```bash
2021
- # Hot-copy: extract CONTRACT and ARCHITECT annotation bodies for inline injection
2022
- HOT_CONTRACT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
2023
- --jq '[.[] | select(.body | contains("<!-- FORGE:CONTRACT -->"))] | last | .body // ""' 2>/dev/null \
2024
- | head -60) # Scope: first 60 lines — captures Proposed Approach + Deliverables table
2025
-
2026
- HOT_ARCHITECT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
2027
- --jq '[.[] | select(.body | contains("<!-- FORGE:ARCHITECT -->"))] | last | .body // ""' 2>/dev/null \
2028
- | head -40) # Scope: first 40 lines — captures Affected Paths + Implementation Order
2029
-
2030
- # Build inline context block (omit sections where annotation was not found)
2031
- HOT_COPY_BLOCK=""
2032
- if [ -n "$HOT_CONTRACT" ]; then
2033
- HOT_COPY_BLOCK="${HOT_COPY_BLOCK}
2034
- **HOT COPY — FORGE:CONTRACT** (do not re-fetch; durable record is on the issue):
2035
- ${HOT_CONTRACT}"
2036
- fi
2037
- if [ -n "$HOT_ARCHITECT" ]; then
2038
- HOT_COPY_BLOCK="${HOT_COPY_BLOCK}
2039
-
2040
- **HOT COPY — FORGE:ARCHITECT** (do not re-fetch; durable record is on the issue):
2041
- ${HOT_ARCHITECT}"
2042
- fi
2043
- ```
2044
-
2045
- ```
2046
- Skill(skill="work-on/review", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE}", context="{HOT_COPY_BLOCK}")
2047
- ```
2048
-
2049
- The `{HOT_COPY_BLOCK}` is an optimization that avoids the child re-discovering context already held by the parent. The FORGE annotations on GitHub remain the durable, compaction-safe record. If the hot-copy block is empty (annotations not yet posted), the sub-agent falls back to reading them from GitHub as before.
2050
-
2051
- **Fallback invocation** (only when `work-on/review` is unavailable):
2052
- ```
2053
- Skill(skill="review-pr", args="{PR_NUMBER} --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
2054
- ```
2055
-
2056
- Review-pr handles: full domain-agent review → post findings as separate issues → merge PR. It does NOT close the issue or clean up the worktree — those run in Phase 6.
2057
-
2058
- ### 5D: Verify merge and close (recovery)
2059
-
2060
- ```bash
2061
- gh pr view {PR_NUMBER} {GH_FLAG} --json state,mergedAt --jq '{state: .state, mergedAt: .mergedAt}'
2062
- gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
2063
- ```
2064
-
2065
- - PR MERGED + issue CLOSED → write checkpoint, then proceed to Phase 6
2066
- - PR MERGED + issue OPEN → close issue manually, write checkpoint, proceed to Phase 6
2067
- - PR NOT MERGED → `gh pr merge {PR_NUMBER} {GH_FLAG} --merge --auto`. If fails → post comment, add `needs-human`, STOP.
2068
-
2069
- **When PR is MERGED — write machine-readable phase checkpoint (MANDATORY)**:
2070
- ```bash
2071
- CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
2072
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
2073
- \`\`\`json
2074
- {\"phase\": \"REVIEW\", \"status\": \"COMPLETE\", \"next_phase\": \"CLOSE\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
2075
- \`\`\`"
2076
- ```
2077
-
2078
- <!-- FORGE:PHASE_COMPLETE — Review done, PR merged. See Universal Phase Dispatcher: next phase is Phase 6 (Close & Cleanup). Not terminal — continue immediately. -->
2079
-
2080
- **After /review-pr returns and the PR is confirmed merged: immediately proceed to Phase 6 (Close & Cleanup). Do NOT stop here. `REVIEW_RESULT: status: COMPLETE` is an intermediate result — the pipeline is NOT done. Invoke Phase 6 now to close the issue, update labels, post the trajectory log, and clean up the worktree.**
2081
-
2082
- **Do NOT output any text describing this transition. Do NOT write phrases like "returning to work-on", "proceeding to close", "now invoking Phase 6", or any narrative summary of what comes next. Do NOT emit end_turn. Execute Phase 6 code immediately.** <!-- Added: forge#93 -->
2083
-
2084
- ---
2085
-
2086
- ## Phase 6: Close & Cleanup
2087
-
2088
- ### 6A: Final issue body update
2089
-
2090
- **Multi-phase guard**: Detect whether the issue has multiple phases. Only check off items belonging to the current completed phase.
2091
-
2092
- **This read is intentionally NOT covered by the session-state-cache rule** — the body is about to be rewritten below, and Phase 5's `/review-pr` invocation is an external process that can post comments/edits between the last read and here. Writing back a stale cached body would silently revert any concurrent change, so always fetch fresh immediately before a body mutation.
2093
-
2094
- ```bash
2095
- BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
2096
- REMAINING_BEFORE=$(printf '%s\n' "$BODY" | grep -cE '^[-*+] \[ \]' || true)
2097
-
2098
- # Structural test: count heading-delimited sections that contain checkbox items.
2099
- # Multi-phase == 2+ checkbox-bearing sections. A single checkbox group is
2100
- # single-phase regardless of how many prose headings surround it.
2101
- #
2102
- # - Fenced code blocks are stripped first: issue bodies routinely embed fenced
2103
- # blocks whose lines start with '#' or contain a literal '- [ ]'. An
2104
- # unterminated fence keeps the original body so later work is never hidden.
2105
- # - ATX headings and setext underlines both delimit sections. The ATX pattern
2106
- # avoids awk interval-quantifier variance across awk implementations.
2107
- # - grep -E / awk only — no PCRE. '^#+ ' needs none.
2108
- FENCE_COUNT=$(printf '%s\n' "$BODY" | grep -cE '^(```+|~~~+)' || true)
2109
- if [ $(( ${FENCE_COUNT:-0} % 2 )) -ne 0 ]; then
2110
- BODY_STRIPPED="$BODY"
2111
- else
2112
- BODY_STRIPPED=$(printf '%s\n' "$BODY" | awk '/^(```+|~~~+)/{f=!f; next} !f')
2113
- fi
2114
-
2115
- CHECKBOX_SECTIONS=$(printf '%s\n' "$BODY_STRIPPED" | awk '
2116
- /^#+ / { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
2117
- /^(=+|-+)$/ && previous != "" { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
2118
- { if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$0 }
2119
- END { if (in_section && has) n++; print n+0 }
2120
- ')
2121
-
2122
- # Sub-issue-tracker guard: a decompose parent whose only checkbox group is
2123
- # '## Sub-Issue Tracker' counts 1 section. Checking those off would mark open
2124
- # sub-issues done and close the tracker, so any unchecked GFM task item for an
2125
- # issue forces multi-phase.
2126
- SUBISSUE_ITEMS=$(printf '%s\n' "$BODY_STRIPPED" | grep -cE '^[-*+] \[ \] #[0-9]+' || true)
2127
- ```
2128
-
2129
- **Sync invariant:** Keep the structural computation above and its consuming multi-phase guard in `commands/work-on/close.md` Phase C1 synchronized. The guard is `CHECKBOX_SECTIONS >= 2 OR SUBISSUE_ITEMS > 0`; this Phase 6A path is reached only when `REMAINING_BEFORE > 0`. <!-- Fixed: forge#2840, #2874 -->
2130
-
2131
- If multi-phase (`CHECKBOX_SECTIONS >= 2` OR a `- [ ] #NNN` sub-issue item is present): do NOT check off future phase items. Add PR reference only. A heading count is **not** the test — every templated issue carries `## Problem`/`## Evidence`/`## Context`, so it is `> 0` universally.
2132
-
2133
- If single-phase or final phase: check off all `[ ]` items, add PR reference.
2134
-
2135
- ### 6B: Project board update (Status=Done, Workflow=Merged)
2136
-
2137
- Resolve `PROJECT_BOARD_OWNER` and `PROJECT_BOARD_NUMBER` from `forge.yaml → project_board` (fields: `owner`, `project_number`). Fall back to `forge.yaml → project.owner` and project number `1` if `project_board` section is absent.
2138
-
2139
- ```bash
2140
- ISSUE_URL="https://github.com/{GH_REPO}/issues/{NUMBER}"
2141
- ITEM_ID=$(gh project item-list {PROJECT_BOARD_NUMBER} --owner {PROJECT_BOARD_OWNER} --format json --limit 200 \
2142
- --jq ".items[] | select(.content.url == \"$ISSUE_URL\") | .id" 2>/dev/null | head -1)
2143
- ```
2144
-
2145
- If found: set Status=Done, Workflow=Merged using project field IDs from `forge.yaml → project_board.field_ids`.
2146
-
2147
- ### 6C: Ensure issue is closed
2148
-
2149
- **Multi-phase guard**: If `REMAINING_AFTER > 0`, uncompleted phases remain. Post phase-complete comment, leave issue open, EXIT — router picks up next phase on next iteration.
2150
-
2151
- If all phases complete:
2152
- ```bash
2153
- gh issue close {NUMBER} {GH_FLAG} \
2154
- --comment "Closed: PR #{PR_NUMBER} merged to \`{PR_BASE}\`. Closes #{NUMBER}."
2155
- RESOLUTION=$(resolve_script 'transition-label')
2156
- TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
2157
- case "$TIER" in
2158
- adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} merged ;;
2159
- prose)
2160
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:merged" \
2161
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:invalid,workflow:decomposed" 2>/dev/null || true
2162
- ;;
2163
- esac
2164
- ```
2165
-
2166
- ### 6D: Parent tracker update (sub-issues only)
2167
-
2168
- **Skip if**: Not a sub-issue (no parent reference in body).
2169
-
2170
- Markdown emphasis markers (`**bold**`, `__bold__`, `*italic*`) are stripped before matching, since sub-issue bodies commonly render the label as `**Parent**: #NNN` and the bare label alternation below would otherwise fail to match past the emphasis characters:
2171
- ```bash
2172
- PARENT_REF=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' \
2173
- | sed -E 's/[*_]+//g' \
2174
- | grep -iE '(part of|spawned from|sub-issue of|parent issue:?|parent:)\s*#[0-9]+' \
2175
- | sed -n 's/.*#\([0-9][0-9]*\).*/\1/p' | head -1)
2176
- ```
2177
-
2178
- If parent found: check off this sub-issue in parent body. If ALL sub-issues checked off → close parent with `workflow:merged`.
2179
-
2180
- ### 6E: Worktree & branch cleanup
2181
-
2182
- ```bash
2183
- if [ -n "{WORKTREE_PATH}" ] && [ -d "{WORKTREE_PATH}" ]; then
2184
- GIT_COMMON=$(git -C {WORKTREE_PATH} rev-parse --git-common-dir 2>/dev/null)
2185
- REPO_ROOT=$(dirname "$(realpath "$GIT_COMMON" 2>/dev/null || echo "$GIT_COMMON")")
2186
- git -C "$REPO_ROOT" worktree remove {WORKTREE_PATH} --force 2>/dev/null || true
2187
- if [ -n "{BRANCH}" ]; then
2188
- git -C "$REPO_ROOT" branch -D {BRANCH} 2>/dev/null || true
2189
- fi
2190
- fi
2191
- ```
2192
-
2193
- ---
2194
-
2195
- ## Phase 7: Summary & Trajectory
2196
-
2197
- ### 7A: Report + Pipeline Summary Card
2198
-
2199
- Output the terse report, then render the shareable **Pipeline Summary Card** — the shareable
2200
- moment a developer screenshots. Gather real stats (commits, additions/deletions, PR target,
2201
- review summary, elapsed time) and render exactly as specified in `work-on/close.md` Phase C4.5
2202
- (`C4.5a` stats gathering → `C4.5b` box-drawing card to stdout → `C4.5c` machine-readable twin).
2203
- This inline path and the delegated `close.md` path MUST produce an identical card.
2204
-
2205
- ```
2206
- ## Done: #{NUMBER} — {TITLE}
2207
- - Investigation: {VERDICT} ({CONFIDENCE})
2208
- - Lane: {FAST/FEATURE}
2209
- - Fix: {BRANCH} → PR #{PR_NUMBER} → merged to `{PR_BASE}`
2210
- - Files changed: {COUNT}
2211
- ```
2212
-
2213
- Then print the card to stdout (inner width 51; truncate long titles with `…`; missing stats
2214
- render `—`; pipeline line reflects the actual terminal state — merged / decomposed / invalid /
2215
- blocked; draft PRs append `(draft)`):
2216
-
2217
- ```
2218
- ╔═══════════════════════════════════════════════════╗
2219
- ║ ForgeDock Pipeline Complete ║
2220
- ╠═══════════════════════════════════════════════════╣
2221
- ║ ║
2222
- ║ Issue: #{NUMBER} — {TITLE} ║
2223
- ║ Pipeline: investigate → architect → build → ║
2224
- ║ review → merge ✓ ║
2225
- ║ Commits: {COMMITS} ({ADDITIONS} additions, {DELETIONS} deletions) ║
2226
- ║ PR: #{PR_NUMBER} (merged to {PR_BASE}) ║
2227
- ║ Review: {REVIEW_SUMMARY} ║
2228
- ║ Time: {ELAPSED} ║
2229
- ║ ║
2230
- ╚═══════════════════════════════════════════════════╝
2231
- ```
2232
-
2233
- **Gather real stats** (C4.5a — this block MUST run on the inline path to populate card variables;
2234
- do NOT rely on the cross-reference to `close.md` alone):
2235
-
2236
- ```bash
2237
- PR_STATS=$(gh pr view {PR_NUMBER} {GH_FLAG} --json commits,additions,deletions,baseRefName,isDraft 2>/dev/null)
2238
- COMMITS=$(echo "$PR_STATS" | jq -r '(.commits | length) // empty' 2>/dev/null); COMMITS=${COMMITS:-—}
2239
- ADDITIONS=$(echo "$PR_STATS" | jq -r '.additions // empty' 2>/dev/null); ADDITIONS=${ADDITIONS:-—}
2240
- DELETIONS=$(echo "$PR_STATS" | jq -r '.deletions // empty' 2>/dev/null); DELETIONS=${DELETIONS:-—}
2241
- PR_TARGET=$(echo "$PR_STATS" | jq -r '.baseRefName // empty' 2>/dev/null); PR_TARGET=${PR_TARGET:-{PR_BASE}}
2242
- IS_DRAFT=$(echo "$PR_STATS" | jq -r '.isDraft // false' 2>/dev/null)
2243
-
2244
- REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
2245
- --jq '[.reviews[].body // ""] + [.comments[].body // ""] | .[]' 2>/dev/null)
2246
- # NOTE: `grep -c` already prints `0` on no match (and exits non-zero) — do NOT add
2247
- # `|| echo 0`, which would append a second line ("0\n0") and break the arithmetic
2248
- # and `--argjson` below. Swallow the non-zero exit with `|| true`, then default.
2249
- APPROVED=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED=${APPROVED:-0}
2250
- CHANGES=$(echo "$REVIEW_BODIES" | grep -cE 'CHANGES REQUESTED:' 2>/dev/null || true); CHANGES=${CHANGES:-0}
2251
- TOTAL_AGENTS=$((APPROVED + CHANGES))
2252
- BLOCKERS=$(echo "$REVIEW_BODIES" | grep -ciE 'blocker|merge.?block' 2>/dev/null || true); BLOCKERS=${BLOCKERS:-0}
2253
- if [ "$TOTAL_AGENTS" -gt 0 ]; then
2254
- REVIEW_SUMMARY="${APPROVED}/${TOTAL_AGENTS} agents passed, ${BLOCKERS} blockers"
2255
- else
2256
- REVIEW_SUMMARY="—" # review data unavailable (e.g. review skipped)
2257
- fi
2258
-
2259
- FIRST_TS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
2260
- --jq '[.[] | select(.body | contains("FORGE:")) | .created_at] | sort | .[0] // empty' 2>/dev/null)
2261
- if [ -n "$FIRST_TS" ]; then
2262
- START_EPOCH=$(date -u -d "$FIRST_TS" +%s 2>/dev/null \
2263
- || python3 -c "import sys,datetime; ts=sys.argv[1].rstrip('Z'); print(int(datetime.datetime.fromisoformat(ts+'+00:00').timestamp()))" "$FIRST_TS" 2>/dev/null \
2264
- || echo "")
2265
- NOW_EPOCH=$(date -u +%s)
2266
- if [ -n "$START_EPOCH" ]; then
2267
- ELAPSED_SECS=$((NOW_EPOCH - START_EPOCH))
2268
- ELAPSED=$(printf '%dm %02ds' $((ELAPSED_SECS / 60)) $((ELAPSED_SECS % 60)))
2269
- else ELAPSED="—"; ELAPSED_SECS=0; fi
2270
- else ELAPSED="—"; ELAPSED_SECS=0; fi
2271
-
2272
- case "{TERMINAL_STATE}" in
2273
- decomposed) PIPELINE_LINE="investigate → decompose ⏹"; CARD_STATUS="decomposed" ;;
2274
- invalid) PIPELINE_LINE="investigate → invalid ✗"; CARD_STATUS="invalid" ;;
2275
- blocked) PIPELINE_LINE="investigate → build → blocked ⚠"; CARD_STATUS="blocked" ;;
2276
- *) PIPELINE_LINE="investigate → architect → build → review → merge ✓"; CARD_STATUS="merged" ;;
2277
- esac
2278
- [ "$IS_DRAFT" = "true" ] && PIPELINE_LINE="${PIPELINE_LINE} (draft)"
2279
- ```
2280
-
2281
- **Build the machine-readable twin** (C4.5c — MUST run this block to assign `CARD_JSON` before
2282
- Phase 7B embeds it; the cross-reference to `close.md` above is insufficient on the inline path): <!-- forge#1178 -->
2283
-
2284
- ```bash
2285
- CARD_JSON=$(jq -nc \
2286
- --argjson issue {NUMBER} \
2287
- --arg title "{TITLE}" \
2288
- --arg status "$CARD_STATUS" \
2289
- --arg pipeline "$PIPELINE_LINE" \
2290
- --arg pr "{PR_NUMBER}" \
2291
- --arg target "$PR_TARGET" \
2292
- --arg commits "$COMMITS" --arg adds "$ADDITIONS" --arg dels "$DELETIONS" \
2293
- --arg review "$REVIEW_SUMMARY" --argjson blockers "${BLOCKERS:-0}" \
2294
- --argjson elapsed "${ELAPSED_SECS:-0}" \
2295
- '{issue:$issue, title:$title, status:$status, pipeline:$pipeline,
2296
- pr:($pr|tonumber? // null), pr_target:$target,
2297
- commits:($commits|tonumber? // null),
2298
- additions:($adds|tonumber? // null),
2299
- deletions:($dels|tonumber? // null),
2300
- review:$review, blockers:$blockers, elapsed_seconds:$elapsed}')
2301
- ```
2302
-
2303
- `CARD_JSON` is now set and embedded in the trajectory comment by 7B.
2304
-
2305
- ### 7B: Trajectory Log (MANDATORY)
2306
-
2307
- **Review-presence check** (run before filling in Phase 4-5 row): <!-- Added: forge#381 -->
2308
- ```bash
2309
- # Check whether /review-pr was actually invoked — look for review agent comments on the PR
2310
- REVIEW_PRESENT=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
2311
- --jq '([.reviews[].body // ""] + [.comments[].body // ""]) |
2312
- map(select(test("APPROVED:|CHANGES REQUESTED:|FORGE:REVIEWER|review-pr";"i"))) |
2313
- length > 0')
2314
- # Set Phase 4-5 row: ✅ Merged if review present, ⚠ Skipped (no review) if not
2315
- REVIEW_ROW=$([ "$REVIEW_PRESENT" = "true" ] && echo "✅ Merged" || echo "⚠ Skipped (no review)")
2316
- ```
2317
-
2318
- This check is **audit-only** — it annotates the trajectory for visibility. It cannot retroactively block a merged PR. If `⚠ Skipped (no review)` is emitted, log it in the Anomalies field so the skip is surfaced during pipeline health review.
2319
-
2320
- Post `<!-- FORGE:TRAJECTORY -->` comment with phase-by-phase results table:
2321
-
2322
- ```bash
2323
- # Compute verification row from VERIFICATION_SKIPPED_CHECKS (set in Phase 3H)
2324
- if [ -z "$VERIFICATION_SKIPPED_CHECKS" ]; then
2325
- VERIFICATION_ROW="✅ Ran"
2326
- else
2327
- VERIFICATION_ROW="⚠ Skipped — verification.commands not configured for: ${VERIFICATION_SKIPPED_CHECKS}"
2328
- fi
2329
-
2330
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
2331
- ## Pipeline Trajectory — #{NUMBER}
2332
-
2333
- | Phase | Result | Notes |
2334
- |-------|--------|-------|
2335
- | Phase 0: Context Load | ✅ Complete | {lane} → \`{PR_BASE}\` |
2336
- | Phase 1: Investigation | ✅ {VERDICT} ({CONFIDENCE}) | Task type: {TASK_TYPE} |
2337
- | Phase 2: Decomposition | ⏭ Skipped | {reason} |
2338
- | Phase 3: Build | ✅ Complete | Branch: \`{BRANCH}\` |
2339
- | Phase 3G: Quality Gate | ✅ Gate passed | {iterations} iterations |
2340
- | Phase 3H: Verification | ${VERIFICATION_ROW} | |
2341
- | Phase 4–5: Review + PR | {REVIEW_ROW} | PR #{PR_NUMBER} → \`{PR_BASE}\` |
2342
- | Phase 6: Close | ✅ Complete | Issue closed |
2343
-
2344
- **Decisions**: {key decisions}
2345
- **Anomalies**: {anomalies or None}
2346
- **Pipeline completed**: {TIMESTAMP}
2347
-
2348
- <!-- FORGE:CARD ${CARD_JSON} -->"
2349
- ```
2350
-
2351
- Append the `<!-- FORGE:CARD {...} -->` block (machine-readable twin from 7A / close.md C4.5c)
2352
- as the last line of the trajectory comment. It is HTML-comment-wrapped so it stays hidden in
2353
- the rendered view but greppable for platform consumption (`/orchestrate` Phase 6 reads it for
2354
- per-issue cards). Additive — does not affect existing `FORGE:TRAJECTORY` table consumers.
2355
-
2356
- ### 7C: Graph Decision Record (MANDATORY when PR exists)
2357
-
2358
- **Skip if**: `{PR_NUMBER}` is empty (investigation-only tasks with no PR) OR `<!-- FORGE:DECISION_RECORD -->` already posted on the PR.
2359
-
2360
- **Purpose**: Post a single consolidated provenance artifact to the PR that proves the merge was backed by citable evidence. Enables downstream benchmarking queries (repeated-mistake rate, stale-edge hit rate, review escape rate) by making every pipeline run queryable via `gh api`.
2361
-
2362
- **Idempotency check**:
2363
- ```bash
2364
- GDR_EXISTS=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
2365
- --jq '[.[] | select(.body | contains("FORGE:DECISION_RECORD"))] | length > 0' 2>/dev/null || echo "false")
2366
- ```
2367
-
2368
- **Extract context edge counts** from FORGE:CONTEXT comment (already posted on issue):
2369
- ```bash
2370
- CONTEXT_COMMENT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
2371
- --jq '.[] | select(.body | contains("FORGE:CONTEXT")) | .body' 2>/dev/null | head -1)
2372
-
2373
- # Count historical review-finding issue references (#NNN patterns in Context comment)
2374
- REVIEW_FINDING_COUNT=$(echo "$CONTEXT_COMMENT" | grep -oE '#[0-9]+' | wc -l | tr -d ' ')
2375
- REVIEW_FINDING_COUNT=${REVIEW_FINDING_COUNT:-0}
2376
- ```
2377
-
2378
- **Extract review verdict and findings count** from PR review summary (Phase 9 of review-pr):
2379
- ```bash
2380
- REVIEW_SUMMARY=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
2381
- --jq '[.[] | select(.body | contains("FORGE:REVIEWER") or (.body | test("APPROVED:|CHANGES REQUESTED:"; "i")))] | last | .body // ""' 2>/dev/null || echo '')
2382
-
2383
- REVIEW_VERDICT=$(echo "$REVIEW_SUMMARY" | sed -n 's/.*Verdict: \(APPROVED\|CHANGES REQUESTED\).*/\1/p' | head -1 || echo "APPROVED")
2384
- REVIEW_VERDICT="${REVIEW_VERDICT:-APPROVED}"
2385
- FINDINGS_COUNT=$(echo "$REVIEW_SUMMARY" | grep -oE '[0-9]+ findings' | grep -oE '[0-9]+' | head -1 || echo "0")
2386
- FINDINGS_COUNT="${FINDINGS_COUNT:-0}"
2387
- AGENTS_RUN=$(echo "$REVIEW_SUMMARY" | grep -oE '[0-9]+ agents' | grep -oE '[0-9]+' | head -1 || echo "0")
2388
- AGENTS_RUN="${AGENTS_RUN:-0}"
2389
- ```
2390
-
2391
- **Capture best-effort cost signal** from session telemetry before posting GDR. This is best-effort — if the signal is unavailable, the cost block is omitted rather than blocking the pipeline or fabricating a number. Field names align with `bin/runner.mjs` usage accounting from #1295 so downstream tooling shares one schema:
2392
- ```bash
2393
- # Best-effort: read per-stage usage from FORGE:BUILDER/FORGE:CONTEXT/FORGE:ARCHITECT phase annotations
2394
- # Source: session telemetry when available (e.g. OTEL_LOG_TOOL_DETAILS, Claude Code usage reporting).
2395
- # If unavailable, set COST_BLOCK to null — the field is omitted from the GDR rather than fabricated.
2396
- COST_INVESTIGATION=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
2397
- --jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body] | last // ""' 2>/dev/null \
2398
- | grep -oP '(?<=cost_usd: )\S+' | head -1 || echo "")
2399
- COST_BUILD=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
2400
- --jq '[.[] | select(.body | contains("FORGE:BUILDER")) | .body] | last // ""' 2>/dev/null \
2401
- | grep -oP '(?<=cost_usd: )\S+' | head -1 || echo "")
2402
- COST_REVIEW=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
2403
- --jq '[.[] | select(.body | contains("FORGE:REVIEWER")) | .body] | last // ""' 2>/dev/null \
2404
- | grep -oP '(?<=cost_usd: )\S+' | head -1 || echo "")
2405
-
2406
- # Build cost block JSON only if at least one stage value is present; otherwise null
2407
- if [ -n "$COST_INVESTIGATION" ] || [ -n "$COST_BUILD" ] || [ -n "$COST_REVIEW" ]; then
2408
- COST_INV_JSON="${COST_INVESTIGATION:-null}"
2409
- COST_BUILD_JSON="${COST_BUILD:-null}"
2410
- COST_REVIEW_JSON="${COST_REVIEW:-null}"
2411
- COST_BLOCK="\"cost\": {
2412
- \"stages\": {
2413
- \"investigation\": $COST_INV_JSON,
2414
- \"build\": $COST_BUILD_JSON,
2415
- \"review\": $COST_REVIEW_JSON
2416
- },
2417
- \"total_usd\": null,
2418
- \"source\": \"session-telemetry\"
2419
- },"
2420
- else
2421
- COST_BLOCK=""
2422
- fi
2423
- ```
2424
-
2425
- **Post GDR to PR** (not to issue — PR comment survives as permanent artifact on the merged diff):
2426
- ```bash
2427
- if [ "$GDR_EXISTS" != "true" ] && [ -n "{PR_NUMBER}" ]; then
2428
- GDR_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
2429
- HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
2430
- MERGE_COMMIT=$(gh pr view {PR_NUMBER} {GH_FLAG} --json mergeCommit --jq '.mergeCommit.oid // ""' 2>/dev/null || echo "")
2431
-
2432
- gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:DECISION_RECORD -->
2433
- ## Graph Decision Record — Issue #${NUMBER} / PR #${PR_NUMBER}
2434
-
2435
- \`\`\`json
2436
- {
2437
- \"schema_version\": \"1\",
2438
- \"issue\": ${NUMBER},
2439
- \"pr\": ${PR_NUMBER},
2440
- \"repo\": \"{GH_REPO}\",
2441
- \"lane\": \"{lane}\",
2442
- \"pr_base\": \"{PR_BASE}\",
2443
- \"branch\": \"{BRANCH}\",
2444
- \"head_sha\": \"${HEAD_SHA}\",
2445
- \"merge_commit\": \"${MERGE_COMMIT}\",
2446
- \"investigation\": {
2447
- \"verdict\": \"{VERDICT}\",
2448
- \"confidence\": \"{CONFIDENCE}\",
2449
- \"task_type\": \"{TASK_TYPE}\"
2450
- },
2451
- ${COST_BLOCK}
2452
- \"context\": {
2453
- \"historical_edges_referenced\": ${REVIEW_FINDING_COUNT},
2454
- \"forge_annotations_read\": [\"FORGE:INVESTIGATOR\", \"FORGE:CONTRACT\", \"FORGE:CONTEXT\", \"FORGE:ARCHITECT\", \"FORGE:BUILDER\"]
2455
- },
2456
- \"build\": {
2457
- \"files_changed\": {FILES_CHANGED},
2458
- \"quality_gate\": \"{pass|fail}\",
2459
- \"quality_gate_iterations\": {GATE_ITERATIONS}
2460
- },
2461
- \"review\": {
2462
- \"verdict\": \"${REVIEW_VERDICT:-APPROVED}\",
2463
- \"findings_created\": ${FINDINGS_COUNT},
2464
- \"agents_run\": ${AGENTS_RUN}
2465
- },
2466
- \"merge\": {
2467
- \"merged_at\": \"${GDR_TIMESTAMP}\",
2468
- \"justification\": \"Investigation confirmed ({VERDICT}/{CONFIDENCE}), quality gate passed, review ${REVIEW_VERDICT:-approved}\"
2469
- }
2470
- }
2471
- \`\`\`
2472
-
2473
- **Queryable**: \`gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments --jq '[.[] | select(.body | contains(\"FORGE:DECISION_RECORD\"))] | .[0].body\`"
2474
- fi
2475
- ```
2476
-
2477
- **Benchmarking**: Query all GDRs for a repo to compute pipeline metrics:
2478
- ```bash
2479
- # Fetch all merged PRs and extract their GDR JSON blocks for metric computation
2480
- # (used by /pipeline-health to measure repeated-mistake rate, stale-edge hit rate, etc.)
2481
- gh pr list -R {GH_REPO} --state merged --limit 100 --json number \
2482
- --jq '.[].number' | while read pr; do
2483
- gh api repos/{GH_REPO}/issues/$pr/comments \
2484
- --jq '.[] | select(.body | contains("FORGE:DECISION_RECORD")) | .body' 2>/dev/null
2485
- done
2486
- ```
2487
-
2488
- <!-- Added: forge#776 -->
2489
-
2490
656
  ---
2491
657
 
2492
658
  ## Error Handling