forgedock 1.0.16 → 1.0.18

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 (190) hide show
  1. package/README.md +54 -3
  2. package/bin/cross-publish.mjs +344 -0
  3. package/bin/forgedock.mjs +2205 -541
  4. package/bin/github-app-manifest.json.license +2 -0
  5. package/bin/hooks/session-start.mjs +4 -5
  6. package/bin/registry.mjs +20 -0
  7. package/bin/tui.mjs +172 -96
  8. package/commands/adopt.md +433 -0
  9. package/commands/analytics.md +2 -0
  10. package/commands/audit-agents.md +2 -0
  11. package/commands/audit.md +2 -0
  12. package/commands/autopilot.md +77 -0
  13. package/commands/changelog.md +354 -0
  14. package/commands/cleanup.md +2 -0
  15. package/commands/compat-audit.md +330 -0
  16. package/commands/deploy-info.md +6 -5
  17. package/commands/design-bench.md +176 -0
  18. package/commands/design-render-critique-loop.md +103 -0
  19. package/commands/design.md +259 -0
  20. package/commands/diagnose.md +483 -0
  21. package/commands/explain.md +349 -0
  22. package/commands/forge-stats.md +2 -0
  23. package/commands/forgedock-init.md +2 -0
  24. package/commands/geo-audit.md +2 -0
  25. package/commands/incident-response.md +2 -0
  26. package/commands/issue.md +9 -3
  27. package/commands/milestone.md +2 -0
  28. package/commands/optimize.md +411 -0
  29. package/commands/orchestrate.md +239 -0
  30. package/commands/pipeline-health.md +60 -1
  31. package/commands/qa-sweep.md +39 -2
  32. package/commands/quality-gate.md +67 -0
  33. package/commands/replay.md +263 -0
  34. package/commands/resume.md +264 -0
  35. package/commands/review-pr-agents.md +5 -1
  36. package/commands/review-pr-staging.md +160 -2
  37. package/commands/review-pr.md +85 -3
  38. package/commands/rollback.md +2 -0
  39. package/commands/scope.md +280 -0
  40. package/commands/security-audit.md +2 -0
  41. package/commands/status.md +262 -0
  42. package/commands/sync-ecosystem.md +2 -0
  43. package/commands/test-gate.md +929 -0
  44. package/commands/validate.md +2 -0
  45. package/commands/work-on/build/architect.md +128 -18
  46. package/commands/work-on/build/context.md +130 -17
  47. package/commands/work-on/build/implement.md +7 -3
  48. package/commands/work-on/build/validate.md +2 -0
  49. package/commands/work-on/build.md +32 -7
  50. package/commands/work-on/close.md +85 -0
  51. package/commands/work-on/decompose.md +2 -0
  52. package/commands/work-on/investigate.md +30 -3
  53. package/commands/work-on/review.md +13 -1
  54. package/commands/work-on-monolithic.md +2 -0
  55. package/commands/work-on.md +676 -38
  56. package/docs/BRAND.md +51 -0
  57. package/docs/CI.md +177 -0
  58. package/docs/CONFIG.md +94 -0
  59. package/docs/FORGE-PROTOCOL.md +324 -0
  60. package/docs/articles/b-vs-c-baseline.md +210 -0
  61. package/docs/articles/full-abc-benchmark.md +205 -0
  62. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +174 -0
  63. package/docs/articles/selective-spec-loading-token-savings.md +140 -0
  64. package/docs/claude-breakpoints.json +47 -0
  65. package/docs/design/abc-benchmark.md +195 -0
  66. package/docs/design/design-architect-rationale.md +123 -0
  67. package/docs/design/design-memory.md +61 -0
  68. package/docs/design/design-spec-schema.md +248 -0
  69. package/docs/design/design-system-lint.md +150 -0
  70. package/docs/design/divergent-generation.md +106 -0
  71. package/docs/design/effects-appropriateness.md +133 -0
  72. package/docs/design/fixtures/briefs/README.md +47 -0
  73. package/docs/design/fixtures/briefs/cadence.md +39 -0
  74. package/docs/design/fixtures/briefs/plume.md +39 -0
  75. package/docs/design/fixtures/briefs/slipstream.md +39 -0
  76. package/docs/design/fixtures/briefs/tender.md +39 -0
  77. package/docs/design/fixtures/briefs/voltage.md +39 -0
  78. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
  79. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
  80. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
  81. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
  82. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
  83. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
  84. package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
  85. package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
  86. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
  87. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
  88. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
  89. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
  90. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
  91. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
  92. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
  93. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
  94. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
  95. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
  96. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
  97. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
  98. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
  99. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
  100. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
  101. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
  102. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
  103. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
  104. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
  105. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
  106. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
  107. package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
  108. package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
  109. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
  110. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
  111. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
  112. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
  113. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
  114. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
  115. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
  116. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
  117. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
  118. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
  119. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
  120. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
  121. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
  122. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
  123. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
  124. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
  125. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
  126. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
  127. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
  128. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
  129. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
  130. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
  131. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
  132. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
  133. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
  134. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
  135. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
  136. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
  137. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
  138. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
  139. package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
  140. package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
  141. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
  142. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
  143. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
  144. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
  145. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
  146. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
  147. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
  148. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
  149. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
  150. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
  151. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
  152. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
  153. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
  154. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
  155. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
  156. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
  157. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
  158. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
  159. package/docs/design/foundations/brutalist.css +294 -0
  160. package/docs/design/foundations/editorial.css +300 -0
  161. package/docs/design/foundations/minimal-luxury.css +295 -0
  162. package/docs/design/foundations/technical.css +293 -0
  163. package/docs/design/foundations/warm-photo.css +299 -0
  164. package/docs/design/reference-corpus.md +1726 -0
  165. package/docs/design/render-critique-loop.md +57 -0
  166. package/docs/site/getting-started.md +21 -0
  167. package/docs/spec-graph-schema.md +397 -0
  168. package/package.json +3 -3
  169. package/scripts/bench-scorecard.mjs +239 -0
  170. package/scripts/build-spec-graph.mjs +509 -0
  171. package/scripts/classify-lane.sh +138 -0
  172. package/scripts/design-system-lint.mjs +993 -0
  173. package/scripts/graph-query.sh +486 -0
  174. package/scripts/transition-label.sh +169 -0
  175. package/scripts/validate-pr-target.sh +30 -0
  176. package/scripts/validate-spec-graph.sh +423 -0
  177. package/templates/devdocs/README.md +72 -0
  178. package/templates/devdocs/agent/using-forgedock.md +3 -0
  179. package/templates/devdocs/agent/using-github.md +3 -0
  180. package/templates/devdocs/index.yaml +162 -0
  181. package/templates/devdocs/project/architecture.md +3 -0
  182. package/templates/devdocs/project/conventions.md +3 -0
  183. package/templates/devdocs/project/custom-instructions.md +3 -0
  184. package/templates/devdocs/project/glossary.md +3 -0
  185. package/templates/devdocs/project/stack.md +3 -0
  186. package/templates/devdocs/templates/commit-convention.md +3 -0
  187. package/templates/devdocs/templates/issue.md +3 -0
  188. package/templates/devdocs/templates/pull-request.md +3 -0
  189. package/templates/devdocs/templates/review-finding.md +3 -0
  190. package/templates/workflows/forgedock-review.yml +96 -0
@@ -2,6 +2,8 @@
2
2
  description: Pick up a GitHub issue and run the full investigate-build-review-merge pipeline
3
3
  argument-hint: [issue number or "next" to pick highest priority]
4
4
  ---
5
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
5
7
 
6
8
  # /work-on — Full Issue Pipeline
7
9
 
@@ -37,9 +39,11 @@ Orchestrator for the full issue lifecycle: investigate → decompose (if needed)
37
39
  | 8 | Phase 6: Close & Cleanup | PR merged | No |
38
40
  | 9 | Phase 7: Trajectory | Issue closed | Yes |
39
41
 
40
- **Phase 3 sub-phase sequence** (all mandatory, execute in order):
42
+ **Phase 3 sub-phase sequence** (execute in order; 3C.5 and 3C.6 are conditional — see Phase 3B):
41
43
 
42
- 3A → 3B → 3C → 3C.5 → 3C.6 → 3D → 3E → 3F → 3F.5 → 3G → 3H → 3I → 3I.5 → 3J → 3K → 3L → 3M
44
+ 3A → 3B → [3C → 3C.5* → 3C.6*] → 3D → 3E → 3F → 3F.5 → 3G → 3H → 3I → 3I.5 → 3J → 3K → 3L → 3M
45
+
46
+ *3C.5 and 3C.6 are skipped for TRIVIAL tasks; 3C (Builder Contract) is still required. Investigation tasks exit at 3B before 3C.
43
47
 
44
48
  **Universal continuation rule**: After ANY phase or sub-phase completes, check whether a terminal state has been reached. Terminal states are:
45
49
  - `workflow:merged` label is set
@@ -95,23 +99,209 @@ Satellite repos (those without a `staging` branch) receive fast-lane PRs directl
95
99
  ### 0A: Parse input
96
100
  Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human` and `workflow:decomposed`, pick highest priority.
97
101
 
102
+ **Optional pre-flight**: Before committing to the full pipeline, run `/scope {NUMBER}` to get a complexity estimate (affected files, blast radius, risk flags, and decomposition recommendation). Especially useful for large or ambiguous issues.
103
+
104
+ ### 0A.5: Post Heartbeat Annotation
105
+
106
+ Post a lightweight activity signal immediately after resolving the issue number. This gives the stall detector (orchestrate Step 4B.5) a fresh timestamp to compare against `STALL_TIMEOUT`. Without this, the stall detector can only see the last structured comment (INVESTIGATOR, BUILDER, etc.) which may be hours old during a valid long-running phase.
107
+
108
+ ```bash
109
+ PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
110
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
111
+ **Phase**: Phase 0 — starting pipeline
112
+ **Timestamp**: ${PHASE_START_TIMESTAMP}
113
+ **Issue**: #{NUMBER}"
114
+ ```
115
+
116
+ **Also post at major phase entry points** (Phases 1, 3, and 5) — replace `Phase 0` with the correct phase name in each case. 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 -->
117
+
118
+ **Skip if**: Issue already has a terminal label (`workflow:merged`, `workflow:invalid`, `needs-human`) — no heartbeat needed on a completed issue.
119
+
98
120
  ### 0B: Load issue + existing context
99
121
  ```bash
100
122
  gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,comments,milestone
101
123
  gh api repos/{GH_REPO}/issues/{NUMBER}/comments --jq '.[] | {id: .id, author: .user.login, body: .body}'
102
124
  ```
103
125
 
104
- **Check**: state (closed → STOP), terminal labels (`workflow:merged`/`workflow:invalid` → STOP), existing agent comments (`FORGE:INVESTIGATOR`, `FORGE:DECOMPOSED`, `FORGE:CONTRACT`, `FORGE:BUILDER`, `FORGE:TRAJECTORY`), parent tracker status, sub-issue status.
126
+ **Check**: state (closed → STOP), terminal labels (`workflow:merged`/`workflow:invalid` → STOP), existing agent comments (`FORGE:INVESTIGATOR`, `FORGE:DECOMPOSED`, `FORGE:CONTRACT`, `FORGE:BUILDER`, `FORGE:TRAJECTORY`, `FORGE:DECISION_RECORD`), parent tracker status, sub-issue status.
105
127
 
106
128
  **Determine resume point**: No comments → Phase 1. Investigation exists + ready-to-build → Phase 3. Builder + no PR → Phase 4. Builder + PR open → Phase 5. PR merged + issue open → Phase 6.
107
129
 
130
+ ### 0B.5: Read Phase Checkpoint (MANDATORY — executes before any phase-skip decision)
131
+
132
+ Query for the latest `<!-- FORGE:CHECKPOINT -->` comment. This is the machine-readable source of truth for the pipeline's current phase position — it takes priority over all prose-based resume heuristics above.
133
+
134
+ ```bash
135
+ CHECKPOINT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
136
+ --jq '[.[] | select(.body | contains("FORGE:CHECKPOINT"))] | last | .body // ""')
137
+
138
+ if [ -n "$CHECKPOINT" ]; then
139
+ # Extract next_phase from the JSON block inside the comment
140
+ NEXT_PHASE=$(echo "$CHECKPOINT" | grep -A5 '```json' | grep '"next_phase"' \
141
+ | sed -n 's/.*"next_phase": "\([^"]*\)".*/\1/p')
142
+ echo "Checkpoint found: next_phase=${NEXT_PHASE}"
143
+ fi
144
+ ```
145
+
146
+ **Routing from checkpoint** (overrides prose heuristics above when a checkpoint exists):
147
+
148
+ | `next_phase` value | Resume at |
149
+ |--------------------|-----------|
150
+ | `BUILD` | Phase 3 (skip Phase 1 investigation) |
151
+ | `DECOMPOSE` | Phase 2 (skip Phase 1 investigation) |
152
+ | `REVIEW` | Phase 4 (skip Phase 1–3) |
153
+ | `CLOSE` | Phase 6 (skip Phase 1–5) |
154
+ | *(absent or unrecognized)* | Fall back to prose heuristics above |
155
+
156
+ **If no checkpoint exists**: fall back to prose resume heuristics in Phase 0B above — treat as fresh start at Phase 1.
157
+
108
158
  **Classify lane**: Milestone → feature lane (`milestone/{slug}`). No milestone → fast lane (`staging`).
109
159
 
110
160
  **Source branch for review-findings**: Parse `**Code branch**: \`{branch}\`` from body. Branch from there, not main.
111
161
 
162
+ **Script resolution** — Use the following `resolve_script()` function whenever calling a pipeline script. It enforces the 4-level precedence hierarchy (see `devdocs/project/architecture.md → Script Precedence`):
163
+
164
+ ```bash
165
+ ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
166
+ ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
167
+ ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
168
+ # Bounds check: reject adaptive_scripts.directory values that escape the repo root.
169
+ # Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
170
+ # slash in paths.root does not inject a '//' into the glob and trigger a false positive.
171
+ REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
172
+ if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
173
+ echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
174
+ ADAPTIVE_ENABLED=false
175
+ fi
176
+ UNIVERSAL_DIR="${FORGEDOCK_HOME:-$(dirname "$(which classify-lane.sh 2>/dev/null || echo 'scripts')")}/scripts"
177
+
178
+ resolve_script() {
179
+ local operation="$1"
180
+ # Tier 2: per-repo adaptive (skip if disabled)
181
+ if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
182
+ echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
183
+ return
184
+ fi
185
+ # Tier 3: universal script
186
+ if [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
187
+ echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
188
+ return
189
+ fi
190
+ # Tier 4: prose fallback
191
+ echo "prose:"
192
+ }
193
+
194
+ # Canonical tier-dispatch usage pattern — inline at every resolve_script() call site:
195
+ #
196
+ # There is no centralised run_script() function. The pattern below is inlined
197
+ # directly at each call site because each operation has a different prose
198
+ # fallback. Copy and adapt this block wherever resolve_script() is called.
199
+ #
200
+ # Usage pattern at each call site:
201
+ # RESOLUTION=$(resolve_script 'op')
202
+ # TIER="${RESOLUTION%%:*}"
203
+ # SCRIPT_PATH="${RESOLUTION#*:}"
204
+ # case "$TIER" in
205
+ # adaptive|universal) bash "$SCRIPT_PATH" ARGS ;;
206
+ # prose) # inline fallback here ;;
207
+ # esac
208
+ #
209
+ # The case pattern is inlined at every call site (rather than centralised here)
210
+ # because each operation has a different prose fallback — transition-label falls
211
+ # back to inline gh issue edit; classify-lane has no valid prose fallback and
212
+ # must exit 1; validate-pr-target emits a WARNING and continues (the PR review
213
+ # step catches any mismatch before merge). <!-- Added: forge#822 -->
214
+ ```
215
+
216
+ When invoking a resolved script, log the tier in the FORGE annotation: `Script tier: {adaptive|universal|prose} ({path})`. This provides full pipeline observability. <!-- Added: forge#670 -->
217
+
218
+ ### 0B.1: Apply learned overrides (MANDATORY — run after 0B, before any routing)
219
+
220
+ Read `forge.yaml → learned:` and override runtime variables. If the `learned:` key is absent or empty, all steps below are no-ops — continue to 0C.
221
+
222
+ ```bash
223
+ # Read learned section — all reads use // "" fallback so absent keys are silent no-ops
224
+ LEARNED_STAGING=$(yq '.learned.branch_targets.staging // ""' forge.yaml 2>/dev/null || echo '')
225
+ LEARNED_TEST_COMMANDS=$(yq '.learned.test_commands // []' forge.yaml 2>/dev/null || echo '[]')
226
+ LEARNED_LABEL_MAP=$(yq '.learned.label_map // {}' forge.yaml 2>/dev/null || echo '{}')
227
+ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' forge.yaml 2>/dev/null || echo '')
228
+ ```
229
+
230
+ **Apply overrides**:
231
+
232
+ 1. **Branch target override** — If `LEARNED_STAGING` is non-empty, replace `STAGING_BRANCH` with its value:
233
+ ```bash
234
+ [ -n "$LEARNED_STAGING" ] && STAGING_BRANCH="$LEARNED_STAGING" && \
235
+ echo "Learned override: STAGING_BRANCH → $STAGING_BRANCH (from learned.branch_targets.staging)"
236
+ ```
237
+
238
+ 2. **Test commands** — Store `LEARNED_TEST_COMMANDS` for use in Phase 3H (validate). These are appended to the `verification.commands` runs, not replaced:
239
+ ```bash
240
+ # Pass LEARNED_TEST_COMMANDS to Phase 3H as additional commands to run after verification.commands
241
+ # (consumed in 3H — store as env var or carry forward in context)
242
+ echo "Learned test commands: $LEARNED_TEST_COMMANDS"
243
+ ```
244
+
245
+ 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.
246
+ ```bash
247
+ # Export as FORGE_LABEL_MAP so child processes (resolve_script 'transition-label') can read it.
248
+ # All 8 resolve_script 'transition-label' call sites in this command inherit this env var automatically.
249
+ # The script substitutes the canonical workflow:* label with the mapped value when found.
250
+ export FORGE_LABEL_MAP="$LEARNED_LABEL_MAP"
251
+ [ -n "$LEARNED_LABEL_MAP" ] && [ "$LEARNED_LABEL_MAP" != "{}" ] && \
252
+ echo "Learned override: FORGE_LABEL_MAP active — label_map will be applied by resolve_script 'transition-label'"
253
+ ```
254
+
255
+ 4. **Commit style** — If `LEARNED_COMMIT_STYLE` is non-empty, use it in Phase 3M:
256
+ ```bash
257
+ [ -n "$LEARNED_COMMIT_STYLE" ] && COMMIT_STYLE="$LEARNED_COMMIT_STYLE" && \
258
+ echo "Learned override: COMMIT_STYLE → $COMMIT_STYLE"
259
+ ```
260
+
261
+ <!-- Added: forge#667 — learned section reader -->
262
+
112
263
  ### 0C: Sync to Project board
113
264
  Add issue to project, set Status=In Progress, Lane, Component, Priority, Workflow=Investigating.
114
265
 
266
+ ### 0C.5: Resolve minimal spec set (selective spec loading)
267
+
268
+ Rather than loading the full ~27-command corpus (~1.1 MB / ~276K tokens) into
269
+ context, resolve the **minimal spec set** this run actually needs from the spec
270
+ knowledge graph. Use the universal-tier `graph-query` script via the canonical
271
+ `resolve_script` tier-dispatch pattern:
272
+
273
+ ```bash
274
+ # Forward-transitive reachability: work-on + its reachable sub-phases (CONTAINS)
275
+ # + required devdocs (REQUIRES), as repo-relative file paths.
276
+ RESOLUTION=$(resolve_script 'graph-query')
277
+ TIER="${RESOLUTION%%:*}"
278
+ SCRIPT_PATH="${RESOLUTION#*:}"
279
+ case "$TIER" in
280
+ adaptive|universal)
281
+ SPEC_SET=$(bash "$SCRIPT_PATH" load-set work-on 2>/dev/null || echo '[]')
282
+ echo "$SPEC_SET" | jq -r '.[]'
283
+ ;;
284
+ prose)
285
+ # Prose fallback: graph-query.sh unavailable — read specs on demand as each
286
+ # Skill(...) is invoked. Selective loading is an optimization, not required.
287
+ : ;;
288
+ esac
289
+ ```
290
+
291
+ **Read ONLY the files returned by `load-set work-on`** when you need full spec
292
+ text during this run — these are the work-on orchestrator plus the sub-phases
293
+ and devdocs reachable from it (e.g. `commands/work-on/build/*`, `commands/work-on/review.md`,
294
+ `commands/review-pr.md`). Do **not** broadly read unrelated command specs
295
+ (`pipeline-health.md`, `audit.md`, `geo-audit.md`, …) that are not in the set.
296
+ Sub-phases are still invoked normally via their existing `Skill(...)` calls; this
297
+ step only narrows what is *pre-read* into context, it does not remove any phase.
298
+
299
+ This is the inverse of `graph-query.sh impact` (forward instead of reverse
300
+ reachability). It is read-only and auto-builds the graph if the gitignored JSON
301
+ is absent — no committed graph is required. The prose tier above handles older
302
+ installs without the scripts layer: selective loading is an optimization, never
303
+ a hard dependency.
304
+
115
305
  ---
116
306
 
117
307
  ## Phase 1: Investigation
@@ -127,8 +317,24 @@ gh api repos/{GH_REPO}/issues/comments/$COMMENT_ID -X DELETE
127
317
 
128
318
  ### 1A: Set label
129
319
  ```bash
130
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating"
131
- gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review" 2>/dev/null || true
320
+ RESOLUTION=$(resolve_script 'transition-label')
321
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
322
+ case "$TIER" in
323
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
324
+ prose)
325
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" \
326
+ --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
327
+ ;;
328
+ esac
329
+ ```
330
+
331
+ **Post Phase 1 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
332
+ ```bash
333
+ PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
334
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
335
+ **Phase**: Phase 1 — Investigation
336
+ **Timestamp**: ${PHASE_START_TIMESTAMP}
337
+ **Issue**: #{NUMBER}"
132
338
  ```
133
339
 
134
340
  ### 1A.5: Normalize Issue Body (MANDATORY)
@@ -222,7 +428,7 @@ Use `forge.yaml → review.tech_stack` and the issue domain labels to identify e
222
428
  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)
223
429
  5. Domain context discovery (narrow scope only, 1–5 files):
224
430
  ```bash
225
- git log --oneline --all -30 -- {affected_files} | grep -oP '#\d+' | sort -u
431
+ git log --oneline --all -30 -- {affected_files} | grep -oE '#[0-9]+' | sort -u
226
432
  gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
227
433
  ```
228
434
  Keep only file/function-level overlap. Max 5 related issues.
@@ -282,28 +488,149 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
282
488
  <!-- INVESTIGATION:COMPLETE -->"
283
489
  ```
284
490
 
491
+ ### 1D: Correction capture (MANDATORY — run before label update)
492
+
493
+ 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.
494
+
495
+ **Scan for correction signals**:
496
+ ```bash
497
+ # Get repo owner login for filtering.
498
+ # Tiered resolution — necessary because project.owner is the GitHub org/user NAME,
499
+ # but comment .user.login is always a personal account login. For org-owned repos
500
+ # these are structurally different (e.g. org="RapierCraftStudios", commenter="mrdubey"),
501
+ # so using project.owner directly silently disables correction capture for all org repos.
502
+ #
503
+ # Resolution order:
504
+ # 1. project.owner_login (explicit override — required for org repos where owner ≠ personal login)
505
+ # 2. gh api repos/{GH_REPO} --jq '.owner.login' (auto-resolves correctly for personal repos)
506
+ # 3. project.owner (backward-compat fallback — still broken for org repos, but avoids hard failure)
507
+ REPO_OWNER=$(yq '.project.owner_login // ""' forge.yaml 2>/dev/null || echo '')
508
+ if [ -z "$REPO_OWNER" ]; then
509
+ REPO_OWNER=$(gh api repos/{GH_REPO} --jq '.owner.login' 2>/dev/null || echo '')
510
+ fi
511
+ if [ -z "$REPO_OWNER" ]; then
512
+ REPO_OWNER=$(yq '.project.owner' forge.yaml 2>/dev/null || echo '')
513
+ fi
514
+
515
+ # Fetch all comments, filter to owner-only, look for correction signals
516
+ CORRECTIONS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
517
+ | jq -r --arg owner "$REPO_OWNER" \
518
+ '.[] | select(.user.login == $owner) | select(
519
+ (.body | test("no,? use|actually use|use .+ instead|not .+, use|wrong branch"; "i"))
520
+ ) | .body' 2>/dev/null || echo '')
521
+ ```
522
+
523
+ **If correction signals found** — extract and write each correction:
524
+
525
+ ```bash
526
+ # Example: extract branch target correction "use develop not staging"
527
+ # Adjust regex to the correction pattern detected
528
+
529
+ if [ -n "$CORRECTIONS" ]; then
530
+ echo "Correction signals detected — writing to forge.yaml → learned:"
531
+ echo "$CORRECTIONS"
532
+
533
+ # Write to forge.yaml using yq in-place merge (idempotent — yq merge overwrites existing keys)
534
+ # Always use env variable injection to avoid YAML injection from comment content
535
+ # Example for branch target correction:
536
+ # BRANCH_VALUE="develop"
537
+ # yq eval '.learned.branch_targets.staging = env(BRANCH_VALUE)' -i forge.yaml
538
+
539
+ # After writing, emit FORGE:LEARNED annotation
540
+ LEARNED_KEYS="branch_targets.staging" # replace with actual extracted keys
541
+ CAPTURED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
542
+
543
+ # Update captured_at and captured_by metadata
544
+ CAPTURED_AT_VAL="$CAPTURED_AT" yq eval '.learned.captured_at = env(CAPTURED_AT_VAL)' -i forge.yaml
545
+ CAPTURED_BY_VAL="work-on/{NUMBER}" yq eval '.learned.captured_by = env(CAPTURED_BY_VAL)' -i forge.yaml
546
+
547
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:LEARNED -->
548
+ ## Learned Pattern Captured
549
+
550
+ **Source**: Owner correction in comment on issue #{NUMBER}
551
+ **Captured at**: $CAPTURED_AT
552
+ **Keys written**: \`$LEARNED_KEYS\`
553
+
554
+ 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).
555
+
556
+ \`\`\`yaml
557
+ # Written to forge.yaml
558
+ learned:
559
+ # {key}: {value}
560
+ \`\`\`
561
+
562
+ **Idempotency**: yq merge-write — re-running will not duplicate entries."
563
+
564
+ echo "FORGE:LEARNED annotation posted."
565
+ fi
566
+ ```
567
+
568
+ **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 -->
569
+
285
570
  ### 1D: Update labels & route
286
571
 
287
572
  **CONFIRMED or PARTIAL with decompose: NO**:
288
573
  ```bash
289
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating"
574
+ RESOLUTION=$(resolve_script 'transition-label')
575
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
576
+ case "$TIER" in
577
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
578
+ prose)
579
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" \
580
+ --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
581
+ ;;
582
+ esac
583
+ ```
584
+
585
+ Write machine-readable phase checkpoint (MUST execute immediately after label transition, before continuing):
586
+ ```bash
587
+ CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
588
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
589
+ \`\`\`json
590
+ {\"phase\": \"INVESTIGATION\", \"status\": \"COMPLETE\", \"next_phase\": \"BUILD\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
591
+ \`\`\`"
290
592
  ```
291
593
  <!-- FORGE:PHASE_COMPLETE — Investigation routed to build. See Universal Phase Dispatcher: next phase is Phase 3. Not terminal — continue immediately. -->
292
594
  → Continue to Phase 3.
293
595
 
294
596
  **CONFIRMED or PARTIAL with decompose: YES**:
295
597
  ```bash
296
- gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:investigating"
598
+ RESOLUTION=$(resolve_script 'transition-label')
599
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
600
+ case "$TIER" in
601
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
602
+ prose)
603
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
604
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid" 2>/dev/null || true
605
+ ;;
606
+ esac
607
+ ```
608
+
609
+ Write machine-readable phase checkpoint (MUST execute immediately after label transition, before continuing):
610
+ ```bash
611
+ CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
612
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
613
+ \`\`\`json
614
+ {\"phase\": \"INVESTIGATION\", \"status\": \"COMPLETE\", \"next_phase\": \"DECOMPOSE\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
615
+ \`\`\`"
297
616
  ```
298
617
  <!-- FORGE:PHASE_COMPLETE — Investigation routed to decomposition. See Universal Phase Dispatcher: next phase is Phase 2. Not terminal — continue immediately. -->
299
618
  → Continue to Phase 2 (Decomposition).
300
619
 
301
620
  **INVALID**:
302
621
  ```bash
303
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --remove-label "workflow:investigating"
622
+ RESOLUTION=$(resolve_script 'transition-label')
623
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
624
+ case "$TIER" in
625
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
626
+ prose)
627
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" \
628
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:decomposed" 2>/dev/null || true
629
+ ;;
630
+ esac
304
631
  gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}"
305
632
  ```
306
- → STOP.
633
+ → STOP. No checkpoint written — INVALID is terminal.
307
634
 
308
635
  ---
309
636
 
@@ -398,9 +725,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSED -->
398
725
 
399
726
  ### 2F: Update labels
400
727
  ```bash
401
- gh issue edit {NUMBER} {GH_FLAG} \
402
- --add-label "workflow:decomposed" \
403
- --remove-label "workflow:ready-to-build,workflow:building,workflow:investigating" 2>/dev/null || true
728
+ RESOLUTION=$(resolve_script 'transition-label')
729
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
730
+ case "$TIER" in
731
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
732
+ prose)
733
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
734
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid" 2>/dev/null || true
735
+ ;;
736
+ esac
404
737
  ```
405
738
 
406
739
  → STOP. Each sub-issue runs its own `/work-on`.
@@ -413,9 +746,19 @@ gh issue edit {NUMBER} {GH_FLAG} \
413
746
 
414
747
  **Skip if**: `<!-- FORGE:BUILDER -->` exists.
415
748
 
416
- **CRITICAL: You MUST execute ALL sub-phases 3A–3M in order. Do NOT skip phases 3C.5 (context) or 3C.6 (architect) — they post mandatory `FORGE:CONTEXT` and `FORGE:ARCHITECT` comments that Phase 3F reads as its primary input. Skipping them degrades build quality and causes review findings. After each sub-phase, continue to the next — no sub-phase is terminal.**
749
+ **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.**
417
750
 
418
751
  ### 3A: Re-read state from GitHub (MANDATORY)
752
+
753
+ **Post Phase 3 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
754
+ ```bash
755
+ PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
756
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
757
+ **Phase**: Phase 3 — Build
758
+ **Timestamp**: ${PHASE_START_TIMESTAMP}
759
+ **Issue**: #{NUMBER}"
760
+ ```
761
+
419
762
  ```bash
420
763
  gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
421
764
 
@@ -426,17 +769,23 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
426
769
  # Check if build already completed
427
770
  gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
428
771
  --jq '.[] | select(.body | contains("FORGE:BUILDER")) | .body'
772
+
773
+ # Check for existing COMPLEXITY_BAND from a prior run (resume path)
774
+ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
775
+ --jq '.[] | select(.body | contains("FORGE:FAST_PATH")) | .body' 2>/dev/null | head -1)
429
776
  ```
430
777
 
431
778
  If no investigation comment with `<!-- INVESTIGATION:COMPLETE -->` → STOP (investigation not complete).
432
779
 
433
780
  Extract from investigation: affected files, root cause, recommendation, task type.
434
781
 
435
- ### 3B: Classify task type
782
+ ### 3B: Classify task type and complexity
783
+
784
+ **Step 1 — Task type classification:**
436
785
 
437
786
  | Signal | Type | Approach |
438
787
  |--------|------|----------|
439
- | Title: "Investigate"/"Audit"/"Research" | Investigation | Produce issues as deliverables |
788
+ | Title starts with "Investigate:"/"Audit:"/"Research:" | Investigation | Produce issues as deliverables |
440
789
  | UI/UX, feature + web/ files | UI/UX | `frontend-design` skill |
441
790
  | Feature + services/ | Backend Feature | Implement directly |
442
791
  | Feature + both | Full-Stack | Backend first, then frontend-design |
@@ -444,7 +793,49 @@ Extract from investigation: affected files, root cause, recommendation, task typ
444
793
  | Bug + services/ | Backend Fix | Direct |
445
794
  | Refactor/docs | Maintenance | Direct |
446
795
 
447
- **Investigation tasks**: Research deeply, create GitHub issues for each finding, post deliverables comment, close issue, skip to Phase 7.
796
+ **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.
797
+
798
+ ```bash
799
+ # Post fast-path comment for investigation tasks
800
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
801
+ ## Fast-Path Classification
802
+
803
+ **COMPLEXITY_BAND**: INVESTIGATION
804
+ **Task type**: Investigation
805
+ **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).
806
+ **Phases skipped**: 3C, 3C.5, 3C.6"
807
+ ```
808
+
809
+ → Jump to Phase 3F immediately. Do not continue to Phase 3C.
810
+
811
+ **Step 2 — Complexity classification (for non-Investigation tasks):**
812
+
813
+ Classify COMPLEXITY_BAND based on affected file count and task nature:
814
+
815
+ | Condition | COMPLEXITY_BAND |
816
+ |-----------|-----------------|
817
+ | Single file, doc/config/markdown only, no logic changes expected | TRIVIAL |
818
+ | 1–5 files, existing patterns, no cross-service impact | STANDARD |
819
+ | 6+ files, new abstractions, cross-service, migration, schema changes | COMPLEX |
820
+
821
+ Post `<!-- FORGE:FAST_PATH -->` comment immediately after classification:
822
+
823
+ ```bash
824
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
825
+ ## Fast-Path Classification
826
+
827
+ **COMPLEXITY_BAND**: {TRIVIAL|STANDARD|COMPLEX}
828
+ **Task type**: {TASK_TYPE}
829
+ **Affected file count**: {N}
830
+ **Rationale**: {one-sentence explanation of classification decision}
831
+ **Phases skipped**: {list phases skipped, or 'none — full pipeline' for STANDARD/COMPLEX}"
832
+ ```
833
+
834
+ **Resume path**: If `EXISTING_FAST_PATH` was read in Phase 3A, extract COMPLEXITY_BAND from it and skip re-classification.
835
+
836
+ **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`.
837
+
838
+ **STANDARD and COMPLEX tasks**: Run full pipeline — 3C → 3C.5 → 3C.6 → 3D onward. No phases skipped.
448
839
 
449
840
  ### 3C: Builder Contract (MANDATORY)
450
841
 
@@ -476,9 +867,11 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
476
867
 
477
868
  Contract must be grounded in the investigation report. Adversarially validate proposed fixes against adjacent system layers.
478
869
 
479
- ### 3C.5: Context Gathering (MANDATORY — max 2 minutes)
870
+ ### 3C.5: Context Gathering (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
480
871
 
481
- **This phase is NOT optional.** Always run it regardless of issue size or complexity. Do NOT skip it — trivial cases return quickly on their own.
872
+ **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.
873
+
874
+ **For STANDARD and COMPLEX tasks**: This phase is NOT optional. Run it regardless. Do NOT skip it without a TRIVIAL classification from Phase 3B.
482
875
 
483
876
  Surface institutional memory before writing code. Extract function names from the contract deliverables table:
484
877
 
@@ -486,7 +879,7 @@ Surface institutional memory before writing code. Extract function names from th
486
879
  FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
487
880
  --jq '.[] | select(.body | contains("FORGE:CONTRACT")) | .body' \
488
881
  | awk '/^### Deliverables/{p=1; next} /^### /{p=0} p' \
489
- | grep -oP '`[A-Za-z_][A-Za-z0-9_]*`' \
882
+ | grep -oE '`[A-Za-z_][A-Za-z0-9_]*`' \
490
883
  | tr -d '`' | sort -u | tr '\n' ' ' | xargs)
491
884
  ```
492
885
 
@@ -511,7 +904,7 @@ done
511
904
 
512
905
  **C2: Past Bugs in the Same Module**
513
906
  ```bash
514
- git log --oneline -30 -- {AFFECTED_FILES} | grep -oP '#\d+' | sort -u | head -8
907
+ git log --oneline -30 -- {AFFECTED_FILES} | grep -oE '#[0-9]+' | sort -u | head -8
515
908
  # For each issue: fetch title + root cause, keep only bug/fix/review-finding labeled. Max 5.
516
909
  ```
517
910
 
@@ -557,13 +950,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTEXT -->
557
950
 
558
951
  If total time exceeds 2 minutes, post partial results with `<!-- FORGE:CONTEXT:PARTIAL -->`.
559
952
 
560
- ### 3C.6: Architecture Plan (MANDATORY)
953
+ ### 3C.6: Architecture Plan (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
954
+
955
+ **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.
561
956
 
562
- **This phase is NOT optional.** Always run it regardless of issue size or complexity. Even a 1-file fix benefits from cross-path consistency checks. Do NOT invent skip heuristics — the phase handles simple changes gracefully and returns quickly.
957
+ **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.
563
958
 
564
959
  Trace ALL affected code paths before writing code.
565
960
 
566
- **The ONLY acceptable skip conditions** (all must be true): Issue creates ONLY new files with no existing callers AND title starts with "docs:" or "chore:".
961
+ **Additional skip condition** (STANDARD tasks only): Issue creates ONLY new files with no existing callers AND title starts with "docs:" or "chore:".
567
962
 
568
963
  **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.
569
964
 
@@ -633,25 +1028,57 @@ If budget exceeded (3 min), use `<!-- FORGE:ARCHITECT:PARTIAL -->`.
633
1028
 
634
1029
  ### 3D: Set building label
635
1030
  ```bash
636
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" --remove-label "workflow:ready-to-build,workflow:investigating"
1031
+ RESOLUTION=$(resolve_script 'transition-label')
1032
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1033
+ case "$TIER" in
1034
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} building ;;
1035
+ prose)
1036
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
1037
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1038
+ ;;
1039
+ esac
637
1040
  ```
638
1041
 
639
1042
  ### 3E: Create worktree
640
1043
 
641
1044
  Branch slug from title (lowercase, hyphenated, max 40 chars). Prefix: `fix/` (bugs) or `feat/` (features).
642
1045
 
1046
+ **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 -->
1047
+
1048
+ ```bash
1049
+ # Compute PR_BASE deterministically from issue milestone — no LLM interpretation
1050
+ RESOLUTION=$(resolve_script 'classify-lane')
1051
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1052
+ case "$TIER" in
1053
+ adaptive|universal)
1054
+ if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
1055
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to compute PR target — see script error above. Adding needs-human."
1056
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1057
+ exit 1
1058
+ fi
1059
+ ;;
1060
+ prose)
1061
+ # classify-lane has no valid prose fallback — the script output is authoritative.
1062
+ # Without it, PR target cannot be determined safely. Add needs-human and stop.
1063
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot compute PR target deterministically. Adding needs-human."
1064
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1065
+ exit 1
1066
+ ;;
1067
+ esac
1068
+ ```
1069
+
643
1070
  **Determine source branch**:
644
1071
  - Review-finding → parse `**Code branch**: \`{branch}\`` from issue body; branch from `origin/{branch}`
645
1072
  - **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.
646
- - Feature lane (has milestone) → branch from `origin/{PR_BASE}`
647
- - Fast lane (no milestone) → branch from `origin/{STAGING_BRANCH}`
1073
+ - Feature lane (has milestone) → branch from `origin/{PR_BASE}` (PR_BASE now set above)
1074
+ - Fast lane (no milestone) → branch from `origin/{PR_BASE}` (PR_BASE = `{STAGING_BRANCH}`)
648
1075
 
649
1076
  ```bash
650
1077
  cd {REPO_PATH}
651
1078
  git fetch origin
652
1079
  BRANCH="fix/{slug}-{NUMBER}"
653
1080
  WORKTREE_PATH="{REPO_PATH}/.claude/worktrees/{BRANCH_SLUG}"
654
- git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{SOURCE_BRANCH}
1081
+ git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{PR_BASE}
655
1082
  ```
656
1083
 
657
1084
  If worktree already exists: verify correct branch, reuse or remove and recreate.
@@ -794,6 +1221,30 @@ fi
794
1221
  ```
795
1222
  Typecheck or build failures are BLOCKING.
796
1223
 
1224
+ **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):
1225
+
1226
+ ```bash
1227
+ # LEARNED_TEST_COMMANDS was set in Phase 0B.1 from forge.yaml → learned.test_commands
1228
+ # If empty/null, this block is a no-op
1229
+ if [ -n "$LEARNED_TEST_COMMANDS" ] && [ "$LEARNED_TEST_COMMANDS" != "[]" ]; then
1230
+ echo "Running learned test commands..."
1231
+ # yq outputs each entry on its own line with -r flag
1232
+ echo "$LEARNED_TEST_COMMANDS" | yq '.[]' | while IFS= read -r cmd; do
1233
+ [ -z "$cmd" ] && continue
1234
+ echo "Running learned command: $cmd"
1235
+ eval "$cmd" 2>&1 | tail -30
1236
+ CMD_EXIT=$?
1237
+ if [ $CMD_EXIT -ne 0 ]; then
1238
+ echo "FAILED (exit $CMD_EXIT): $cmd"
1239
+ exit $CMD_EXIT
1240
+ fi
1241
+ done
1242
+ else
1243
+ echo "No learned test commands configured — skipping"
1244
+ fi
1245
+ ```
1246
+ Learned test command failures are BLOCKING (same as verification.commands failures). <!-- Added: forge#667 -->
1247
+
797
1248
  ### 3I: Frontend proxy wiring check (MANDATORY)
798
1249
 
799
1250
  Skip if no TS/TSX files changed.
@@ -922,12 +1373,23 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
922
1373
  {checklist from contract, marked pass/fail}
923
1374
 
924
1375
  ### Testing Checklist
925
- - [ ] {scenario 1}
926
- - [ ] {scenario 2}
1376
+ - [ ] {scenario 1} [type:api]
1377
+ - [ ] {scenario 2} [type:unit]
1378
+
1379
+ > **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.
927
1380
 
928
1381
  <!-- FORGE:BUILDER:COMPLETE -->"
929
1382
  ```
930
1383
 
1384
+ Write machine-readable phase checkpoint (MUST execute immediately after FORGE:BUILDER comment is posted, before Phase 4):
1385
+ ```bash
1386
+ CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1387
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
1388
+ \`\`\`json
1389
+ {\"phase\": \"BUILD\", \"status\": \"COMPLETE\", \"next_phase\": \"REVIEW\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
1390
+ \`\`\`"
1391
+ ```
1392
+
931
1393
  ---
932
1394
 
933
1395
  ## Phase 4: PR Creation
@@ -951,7 +1413,50 @@ cd {WORKTREE_PATH} && git push -u origin {BRANCH}
951
1413
  If fails: try `--force-with-lease`. If still fails: post comment, add `needs-human`, STOP.
952
1414
 
953
1415
  ### 4C: Determine PR target
954
- No milestone → `staging`. Has milestone → `milestone/{slug}`. NEVER `main`.
1416
+ `PR_BASE` was computed in Phase 3E. If somehow unset (e.g., resumed session after compaction), recompute:
1417
+ ```bash
1418
+ RESOLUTION=$(resolve_script 'classify-lane')
1419
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1420
+ case "$TIER" in
1421
+ adaptive|universal)
1422
+ if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
1423
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to recompute PR target — see script error above. Adding needs-human."
1424
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1425
+ exit 1
1426
+ fi
1427
+ ;;
1428
+ prose)
1429
+ # No valid prose fallback — see Phase 3E note.
1430
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot recompute PR target. Adding needs-human."
1431
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1432
+ exit 1
1433
+ ;;
1434
+ esac
1435
+ ```
1436
+ 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 -->
1437
+
1438
+ ### 4C.5: Validate PR target against classified lane
1439
+ ```bash
1440
+ RESOLUTION=$(resolve_script 'validate-pr-target')
1441
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1442
+ case "$TIER" in
1443
+ adaptive|universal)
1444
+ bash "$SCRIPT_PATH" {PR_BASE} {CLASSIFIED_LANE}
1445
+ ;;
1446
+ prose)
1447
+ # validate-pr-target has no safe prose fallback — silently skipping validation risks
1448
+ # merging to the wrong branch. Log a warning but do NOT block the pipeline; the PR
1449
+ # review step will catch a mismatched target before merge.
1450
+ echo "WARNING: validate-pr-target.sh not installed (prose tier) — skipping lane validation. Confirm PR base manually." >&2
1451
+ ;;
1452
+ esac
1453
+ ```
1454
+ `{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):
1455
+ ```bash
1456
+ 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."
1457
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1458
+ ```
1459
+ → STOP. Do NOT proceed to Phase 4D. <!-- Added: forge#671 -->
955
1460
 
956
1461
  ### 4D: Create PR
957
1462
  ```bash
@@ -978,7 +1483,15 @@ If PR already exists for this branch, use the existing PR number.
978
1483
 
979
1484
  ### 4E: Update labels
980
1485
  ```bash
981
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" --remove-label "workflow:building"
1486
+ RESOLUTION=$(resolve_script 'transition-label')
1487
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1488
+ case "$TIER" in
1489
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
1490
+ prose)
1491
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" \
1492
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1493
+ ;;
1494
+ esac
982
1495
  ```
983
1496
 
984
1497
  ---
@@ -986,6 +1499,16 @@ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" --remove-label
986
1499
  ## Phase 5: Auto-Review
987
1500
 
988
1501
  ### 5A: Re-read state from GitHub (MANDATORY)
1502
+
1503
+ **Post Phase 5 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
1504
+ ```bash
1505
+ PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
1506
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
1507
+ **Phase**: Phase 5 — Review
1508
+ **Timestamp**: ${PHASE_START_TIMESTAMP}
1509
+ **Issue**: #{NUMBER}"
1510
+ ```
1511
+
989
1512
  ```bash
990
1513
  gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state
991
1514
  PR_NUMBER=$(gh pr list {GH_FLAG} --head {BRANCH} --json number --jq '.[0].number')
@@ -1029,10 +1552,19 @@ gh pr view {PR_NUMBER} {GH_FLAG} --json state,mergedAt --jq '{state: .state, mer
1029
1552
  gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
1030
1553
  ```
1031
1554
 
1032
- - PR MERGED + issue CLOSED → proceed to Phase 6
1033
- - PR MERGED + issue OPEN → close issue manually
1555
+ - PR MERGED + issue CLOSED → write checkpoint, then proceed to Phase 6
1556
+ - PR MERGED + issue OPEN → close issue manually, write checkpoint, proceed to Phase 6
1034
1557
  - PR NOT MERGED → `gh pr merge {PR_NUMBER} {GH_FLAG} --merge --auto`. If fails → post comment, add `needs-human`, STOP.
1035
1558
 
1559
+ **When PR is MERGED — write machine-readable phase checkpoint (MANDATORY)**:
1560
+ ```bash
1561
+ CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1562
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
1563
+ \`\`\`json
1564
+ {\"phase\": \"REVIEW\", \"status\": \"COMPLETE\", \"next_phase\": \"CLOSE\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
1565
+ \`\`\`"
1566
+ ```
1567
+
1036
1568
  <!-- FORGE:PHASE_COMPLETE — Review done, PR merged. See Universal Phase Dispatcher: next phase is Phase 6 (Close & Cleanup). Not terminal — continue immediately. -->
1037
1569
 
1038
1570
  **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.**
@@ -1077,9 +1609,15 @@ If all phases complete:
1077
1609
  ```bash
1078
1610
  gh issue close {NUMBER} {GH_FLAG} \
1079
1611
  --comment "Closed: PR #{PR_NUMBER} merged to \`{PR_BASE}\`. Closes #{NUMBER}."
1080
- gh issue edit {NUMBER} {GH_FLAG} \
1081
- --add-label "workflow:merged" \
1082
- --remove-label "workflow:in-review,workflow:building,workflow:investigating" 2>/dev/null || true
1612
+ RESOLUTION=$(resolve_script 'transition-label')
1613
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1614
+ case "$TIER" in
1615
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} merged ;;
1616
+ prose)
1617
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:merged" \
1618
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1619
+ ;;
1620
+ esac
1083
1621
  ```
1084
1622
 
1085
1623
  ### 6D: Parent tracker update (sub-issues only)
@@ -1088,7 +1626,8 @@ gh issue edit {NUMBER} {GH_FLAG} \
1088
1626
 
1089
1627
  ```bash
1090
1628
  PARENT_REF=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' \
1091
- | grep -oP '(?i)(part of|spawned from|sub-issue of|parent issue[:]?|parent[:])\s*#\K\d+' | head -1)
1629
+ | grep -iE '(part of|spawned from|sub-issue of|parent issue:?|parent:)\s*#[0-9]+' \
1630
+ | sed -n 's/.*#\([0-9][0-9]*\).*/\1/p' | head -1)
1092
1631
  ```
1093
1632
 
1094
1633
  If parent found: check off this sub-issue in parent body. If ALL sub-issues checked off → close parent with `workflow:merged`.
@@ -1155,6 +1694,105 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
1155
1694
  **Pipeline completed**: {TIMESTAMP}"
1156
1695
  ```
1157
1696
 
1697
+ ### 7C: Graph Decision Record (MANDATORY when PR exists)
1698
+
1699
+ **Skip if**: `{PR_NUMBER}` is empty (investigation-only tasks with no PR) OR `<!-- FORGE:DECISION_RECORD -->` already posted on the PR.
1700
+
1701
+ **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`.
1702
+
1703
+ **Idempotency check**:
1704
+ ```bash
1705
+ GDR_EXISTS=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
1706
+ --jq '[.[] | select(.body | contains("FORGE:DECISION_RECORD"))] | length > 0' 2>/dev/null || echo "false")
1707
+ ```
1708
+
1709
+ **Extract context edge counts** from FORGE:CONTEXT comment (already posted on issue):
1710
+ ```bash
1711
+ CONTEXT_COMMENT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1712
+ --jq '.[] | select(.body | contains("FORGE:CONTEXT")) | .body' 2>/dev/null | head -1)
1713
+
1714
+ # Count historical review-finding issue references (#NNN patterns in Context comment)
1715
+ REVIEW_FINDING_COUNT=$(echo "$CONTEXT_COMMENT" | grep -oE '#[0-9]+' | wc -l | tr -d ' ')
1716
+ REVIEW_FINDING_COUNT=${REVIEW_FINDING_COUNT:-0}
1717
+ ```
1718
+
1719
+ **Extract review verdict and findings count** from PR review summary (Phase 9 of review-pr):
1720
+ ```bash
1721
+ REVIEW_SUMMARY=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
1722
+ --jq '[.[] | select(.body | contains("FORGE:REVIEWER") or (.body | test("APPROVED:|CHANGES REQUESTED:"; "i")))] | last | .body // ""' 2>/dev/null || echo '')
1723
+
1724
+ REVIEW_VERDICT=$(echo "$REVIEW_SUMMARY" | sed -n 's/.*Verdict: \(APPROVED\|CHANGES REQUESTED\).*/\1/p' | head -1 || echo "APPROVED")
1725
+ REVIEW_VERDICT="${REVIEW_VERDICT:-APPROVED}"
1726
+ FINDINGS_COUNT=$(echo "$REVIEW_SUMMARY" | grep -oE '[0-9]+ findings' | grep -oE '[0-9]+' | head -1 || echo "0")
1727
+ FINDINGS_COUNT="${FINDINGS_COUNT:-0}"
1728
+ AGENTS_RUN=$(echo "$REVIEW_SUMMARY" | grep -oE '[0-9]+ agents' | grep -oE '[0-9]+' | head -1 || echo "0")
1729
+ AGENTS_RUN="${AGENTS_RUN:-0}"
1730
+ ```
1731
+
1732
+ **Post GDR to PR** (not to issue — PR comment survives as permanent artifact on the merged diff):
1733
+ ```bash
1734
+ if [ "$GDR_EXISTS" != "true" ] && [ -n "{PR_NUMBER}" ]; then
1735
+ GDR_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1736
+ HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
1737
+ MERGE_COMMIT=$(gh pr view {PR_NUMBER} {GH_FLAG} --json mergeCommit --jq '.mergeCommit.oid // ""' 2>/dev/null || echo "")
1738
+
1739
+ gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:DECISION_RECORD -->
1740
+ ## Graph Decision Record — Issue #${NUMBER} / PR #${PR_NUMBER}
1741
+
1742
+ \`\`\`json
1743
+ {
1744
+ \"schema_version\": \"1\",
1745
+ \"issue\": ${NUMBER},
1746
+ \"pr\": ${PR_NUMBER},
1747
+ \"repo\": \"{GH_REPO}\",
1748
+ \"lane\": \"{lane}\",
1749
+ \"pr_base\": \"{PR_BASE}\",
1750
+ \"branch\": \"{BRANCH}\",
1751
+ \"head_sha\": \"${HEAD_SHA}\",
1752
+ \"merge_commit\": \"${MERGE_COMMIT}\",
1753
+ \"investigation\": {
1754
+ \"verdict\": \"{VERDICT}\",
1755
+ \"confidence\": \"{CONFIDENCE}\",
1756
+ \"task_type\": \"{TASK_TYPE}\"
1757
+ },
1758
+ \"context\": {
1759
+ \"historical_edges_referenced\": ${REVIEW_FINDING_COUNT},
1760
+ \"forge_annotations_read\": [\"FORGE:INVESTIGATOR\", \"FORGE:CONTRACT\", \"FORGE:CONTEXT\", \"FORGE:ARCHITECT\", \"FORGE:BUILDER\"]
1761
+ },
1762
+ \"build\": {
1763
+ \"files_changed\": {FILES_CHANGED},
1764
+ \"quality_gate\": \"{pass|fail}\",
1765
+ \"quality_gate_iterations\": {GATE_ITERATIONS}
1766
+ },
1767
+ \"review\": {
1768
+ \"verdict\": \"${REVIEW_VERDICT:-APPROVED}\",
1769
+ \"findings_created\": ${FINDINGS_COUNT},
1770
+ \"agents_run\": ${AGENTS_RUN}
1771
+ },
1772
+ \"merge\": {
1773
+ \"merged_at\": \"${GDR_TIMESTAMP}\",
1774
+ \"justification\": \"Investigation confirmed ({VERDICT}/{CONFIDENCE}), quality gate passed, review ${REVIEW_VERDICT:-approved}\"
1775
+ }
1776
+ }
1777
+ \`\`\`
1778
+
1779
+ **Queryable**: \`gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments --jq '[.[] | select(.body | contains(\"FORGE:DECISION_RECORD\"))] | .[0].body\`"
1780
+ fi
1781
+ ```
1782
+
1783
+ **Benchmarking**: Query all GDRs for a repo to compute pipeline metrics:
1784
+ ```bash
1785
+ # Fetch all merged PRs and extract their GDR JSON blocks for metric computation
1786
+ # (used by /pipeline-health to measure repeated-mistake rate, stale-edge hit rate, etc.)
1787
+ gh pr list -R {GH_REPO} --state merged --limit 100 --json number \
1788
+ --jq '.[].number' | while read pr; do
1789
+ gh api repos/{GH_REPO}/issues/$pr/comments \
1790
+ --jq '.[] | select(.body | contains("FORGE:DECISION_RECORD")) | .body' 2>/dev/null
1791
+ done
1792
+ ```
1793
+
1794
+ <!-- Added: forge#776 -->
1795
+
1158
1796
  ---
1159
1797
 
1160
1798
  ## Error Handling