forgedock 1.0.16 → 1.0.17

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 (71) hide show
  1. package/README.md +21 -3
  2. package/bin/forgedock.mjs +1992 -540
  3. package/bin/github-app-manifest.json.license +2 -0
  4. package/bin/hooks/session-start.mjs +4 -5
  5. package/bin/registry.mjs +20 -0
  6. package/bin/tui.mjs +172 -96
  7. package/commands/adopt.md +433 -0
  8. package/commands/analytics.md +2 -0
  9. package/commands/audit-agents.md +2 -0
  10. package/commands/audit.md +2 -0
  11. package/commands/changelog.md +354 -0
  12. package/commands/cleanup.md +2 -0
  13. package/commands/compat-audit.md +330 -0
  14. package/commands/deploy-info.md +6 -5
  15. package/commands/diagnose.md +483 -0
  16. package/commands/explain.md +349 -0
  17. package/commands/forge-stats.md +2 -0
  18. package/commands/forgedock-init.md +2 -0
  19. package/commands/geo-audit.md +2 -0
  20. package/commands/incident-response.md +2 -0
  21. package/commands/issue.md +2 -0
  22. package/commands/milestone.md +2 -0
  23. package/commands/optimize.md +411 -0
  24. package/commands/orchestrate.md +118 -0
  25. package/commands/pipeline-health.md +60 -1
  26. package/commands/qa-sweep.md +2 -0
  27. package/commands/quality-gate.md +37 -0
  28. package/commands/replay.md +263 -0
  29. package/commands/resume.md +264 -0
  30. package/commands/review-pr-agents.md +5 -1
  31. package/commands/review-pr-staging.md +2 -0
  32. package/commands/review-pr.md +85 -3
  33. package/commands/rollback.md +2 -0
  34. package/commands/scope.md +280 -0
  35. package/commands/security-audit.md +2 -0
  36. package/commands/status.md +262 -0
  37. package/commands/sync-ecosystem.md +2 -0
  38. package/commands/validate.md +2 -0
  39. package/commands/work-on/build/architect.md +128 -18
  40. package/commands/work-on/build/context.md +130 -17
  41. package/commands/work-on/build/implement.md +2 -0
  42. package/commands/work-on/build/validate.md +2 -0
  43. package/commands/work-on/build.md +32 -7
  44. package/commands/work-on/close.md +85 -0
  45. package/commands/work-on/decompose.md +2 -0
  46. package/commands/work-on/investigate.md +30 -3
  47. package/commands/work-on/review.md +13 -1
  48. package/commands/work-on-monolithic.md +2 -0
  49. package/commands/work-on.md +624 -36
  50. package/docs/BRAND.md +51 -0
  51. package/docs/CONFIG.md +94 -0
  52. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +174 -0
  53. package/docs/claude-breakpoints.json +47 -0
  54. package/docs/site/getting-started.md +21 -0
  55. package/package.json +3 -3
  56. package/scripts/classify-lane.sh +145 -0
  57. package/scripts/transition-label.sh +161 -0
  58. package/scripts/validate-pr-target.sh +30 -0
  59. package/templates/devdocs/README.md +72 -0
  60. package/templates/devdocs/agent/using-forgedock.md +3 -0
  61. package/templates/devdocs/agent/using-github.md +3 -0
  62. package/templates/devdocs/index.yaml +162 -0
  63. package/templates/devdocs/project/architecture.md +3 -0
  64. package/templates/devdocs/project/conventions.md +3 -0
  65. package/templates/devdocs/project/custom-instructions.md +3 -0
  66. package/templates/devdocs/project/glossary.md +3 -0
  67. package/templates/devdocs/project/stack.md +3 -0
  68. package/templates/devdocs/templates/commit-convention.md +3 -0
  69. package/templates/devdocs/templates/issue.md +3 -0
  70. package/templates/devdocs/templates/pull-request.md +3 -0
  71. package/templates/devdocs/templates/review-finding.md +3 -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,20 +99,158 @@ 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="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
166
+ ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
167
+ UNIVERSAL_DIR="${FORGEDOCK_HOME:-$(dirname "$(which classify-lane.sh 2>/dev/null || echo 'scripts')")}/scripts"
168
+
169
+ resolve_script() {
170
+ local operation="$1"
171
+ # Tier 2: per-repo adaptive (skip if disabled)
172
+ if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
173
+ echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
174
+ return
175
+ fi
176
+ # Tier 3: universal script
177
+ if [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
178
+ echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
179
+ return
180
+ fi
181
+ # Tier 4: prose fallback
182
+ echo "prose:"
183
+ }
184
+
185
+ # Canonical tier-dispatch usage pattern — inline at every resolve_script() call site:
186
+ #
187
+ # There is no centralised run_script() function. The pattern below is inlined
188
+ # directly at each call site because each operation has a different prose
189
+ # fallback. Copy and adapt this block wherever resolve_script() is called.
190
+ #
191
+ # Usage pattern at each call site:
192
+ # RESOLUTION=$(resolve_script 'op')
193
+ # TIER="${RESOLUTION%%:*}"
194
+ # SCRIPT_PATH="${RESOLUTION#*:}"
195
+ # case "$TIER" in
196
+ # adaptive|universal) bash "$SCRIPT_PATH" ARGS ;;
197
+ # prose) # inline fallback here ;;
198
+ # esac
199
+ #
200
+ # The case pattern is inlined at every call site (rather than centralised here)
201
+ # because each operation has a different prose fallback — transition-label falls
202
+ # back to inline gh issue edit; classify-lane has no valid prose fallback and
203
+ # must exit 1; validate-pr-target emits a WARNING and continues (the PR review
204
+ # step catches any mismatch before merge). <!-- Added: forge#822 -->
205
+ ```
206
+
207
+ 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 -->
208
+
209
+ ### 0B.1: Apply learned overrides (MANDATORY — run after 0B, before any routing)
210
+
211
+ 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.
212
+
213
+ ```bash
214
+ # Read learned section — all reads use // "" fallback so absent keys are silent no-ops
215
+ LEARNED_STAGING=$(yq '.learned.branch_targets.staging // ""' forge.yaml 2>/dev/null || echo '')
216
+ LEARNED_TEST_COMMANDS=$(yq '.learned.test_commands // []' forge.yaml 2>/dev/null || echo '[]')
217
+ LEARNED_LABEL_MAP=$(yq '.learned.label_map // {}' forge.yaml 2>/dev/null || echo '{}')
218
+ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' forge.yaml 2>/dev/null || echo '')
219
+ ```
220
+
221
+ **Apply overrides**:
222
+
223
+ 1. **Branch target override** — If `LEARNED_STAGING` is non-empty, replace `STAGING_BRANCH` with its value:
224
+ ```bash
225
+ [ -n "$LEARNED_STAGING" ] && STAGING_BRANCH="$LEARNED_STAGING" && \
226
+ echo "Learned override: STAGING_BRANCH → $STAGING_BRANCH (from learned.branch_targets.staging)"
227
+ ```
228
+
229
+ 2. **Test commands** — Store `LEARNED_TEST_COMMANDS` for use in Phase 3H (validate). These are appended to the `verification.commands` runs, not replaced:
230
+ ```bash
231
+ # Pass LEARNED_TEST_COMMANDS to Phase 3H as additional commands to run after verification.commands
232
+ # (consumed in 3H — store as env var or carry forward in context)
233
+ echo "Learned test commands: $LEARNED_TEST_COMMANDS"
234
+ ```
235
+
236
+ 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.
237
+ ```bash
238
+ # Export as FORGE_LABEL_MAP so child processes (resolve_script 'transition-label') can read it.
239
+ # All 8 resolve_script 'transition-label' call sites in this command inherit this env var automatically.
240
+ # The script substitutes the canonical workflow:* label with the mapped value when found.
241
+ export FORGE_LABEL_MAP="$LEARNED_LABEL_MAP"
242
+ [ -n "$LEARNED_LABEL_MAP" ] && [ "$LEARNED_LABEL_MAP" != "{}" ] && \
243
+ echo "Learned override: FORGE_LABEL_MAP active — label_map will be applied by resolve_script 'transition-label'"
244
+ ```
245
+
246
+ 4. **Commit style** — If `LEARNED_COMMIT_STYLE` is non-empty, use it in Phase 3M:
247
+ ```bash
248
+ [ -n "$LEARNED_COMMIT_STYLE" ] && COMMIT_STYLE="$LEARNED_COMMIT_STYLE" && \
249
+ echo "Learned override: COMMIT_STYLE → $COMMIT_STYLE"
250
+ ```
251
+
252
+ <!-- Added: forge#667 — learned section reader -->
253
+
112
254
  ### 0C: Sync to Project board
113
255
  Add issue to project, set Status=In Progress, Lane, Component, Priority, Workflow=Investigating.
114
256
 
@@ -127,8 +269,24 @@ gh api repos/{GH_REPO}/issues/comments/$COMMENT_ID -X DELETE
127
269
 
128
270
  ### 1A: Set label
129
271
  ```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
272
+ RESOLUTION=$(resolve_script 'transition-label')
273
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
274
+ case "$TIER" in
275
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
276
+ prose)
277
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" \
278
+ --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
279
+ ;;
280
+ esac
281
+ ```
282
+
283
+ **Post Phase 1 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
284
+ ```bash
285
+ PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
286
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
287
+ **Phase**: Phase 1 — Investigation
288
+ **Timestamp**: ${PHASE_START_TIMESTAMP}
289
+ **Issue**: #{NUMBER}"
132
290
  ```
133
291
 
134
292
  ### 1A.5: Normalize Issue Body (MANDATORY)
@@ -222,7 +380,7 @@ Use `forge.yaml → review.tech_stack` and the issue domain labels to identify e
222
380
  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
381
  5. Domain context discovery (narrow scope only, 1–5 files):
224
382
  ```bash
225
- git log --oneline --all -30 -- {affected_files} | grep -oP '#\d+' | sort -u
383
+ git log --oneline --all -30 -- {affected_files} | grep -oE '#[0-9]+' | sort -u
226
384
  gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
227
385
  ```
228
386
  Keep only file/function-level overlap. Max 5 related issues.
@@ -282,28 +440,149 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
282
440
  <!-- INVESTIGATION:COMPLETE -->"
283
441
  ```
284
442
 
443
+ ### 1D: Correction capture (MANDATORY — run before label update)
444
+
445
+ 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.
446
+
447
+ **Scan for correction signals**:
448
+ ```bash
449
+ # Get repo owner login for filtering.
450
+ # Tiered resolution — necessary because project.owner is the GitHub org/user NAME,
451
+ # but comment .user.login is always a personal account login. For org-owned repos
452
+ # these are structurally different (e.g. org="RapierCraftStudios", commenter="mrdubey"),
453
+ # so using project.owner directly silently disables correction capture for all org repos.
454
+ #
455
+ # Resolution order:
456
+ # 1. project.owner_login (explicit override — required for org repos where owner ≠ personal login)
457
+ # 2. gh api repos/{GH_REPO} --jq '.owner.login' (auto-resolves correctly for personal repos)
458
+ # 3. project.owner (backward-compat fallback — still broken for org repos, but avoids hard failure)
459
+ REPO_OWNER=$(yq '.project.owner_login // ""' forge.yaml 2>/dev/null || echo '')
460
+ if [ -z "$REPO_OWNER" ]; then
461
+ REPO_OWNER=$(gh api repos/{GH_REPO} --jq '.owner.login' 2>/dev/null || echo '')
462
+ fi
463
+ if [ -z "$REPO_OWNER" ]; then
464
+ REPO_OWNER=$(yq '.project.owner' forge.yaml 2>/dev/null || echo '')
465
+ fi
466
+
467
+ # Fetch all comments, filter to owner-only, look for correction signals
468
+ CORRECTIONS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
469
+ | jq -r --arg owner "$REPO_OWNER" \
470
+ '.[] | select(.user.login == $owner) | select(
471
+ (.body | test("no,? use|actually use|use .+ instead|not .+, use|wrong branch"; "i"))
472
+ ) | .body' 2>/dev/null || echo '')
473
+ ```
474
+
475
+ **If correction signals found** — extract and write each correction:
476
+
477
+ ```bash
478
+ # Example: extract branch target correction "use develop not staging"
479
+ # Adjust regex to the correction pattern detected
480
+
481
+ if [ -n "$CORRECTIONS" ]; then
482
+ echo "Correction signals detected — writing to forge.yaml → learned:"
483
+ echo "$CORRECTIONS"
484
+
485
+ # Write to forge.yaml using yq in-place merge (idempotent — yq merge overwrites existing keys)
486
+ # Always use env variable injection to avoid YAML injection from comment content
487
+ # Example for branch target correction:
488
+ # BRANCH_VALUE="develop"
489
+ # yq eval '.learned.branch_targets.staging = env(BRANCH_VALUE)' -i forge.yaml
490
+
491
+ # After writing, emit FORGE:LEARNED annotation
492
+ LEARNED_KEYS="branch_targets.staging" # replace with actual extracted keys
493
+ CAPTURED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
494
+
495
+ # Update captured_at and captured_by metadata
496
+ CAPTURED_AT_VAL="$CAPTURED_AT" yq eval '.learned.captured_at = env(CAPTURED_AT_VAL)' -i forge.yaml
497
+ CAPTURED_BY_VAL="work-on/{NUMBER}" yq eval '.learned.captured_by = env(CAPTURED_BY_VAL)' -i forge.yaml
498
+
499
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:LEARNED -->
500
+ ## Learned Pattern Captured
501
+
502
+ **Source**: Owner correction in comment on issue #{NUMBER}
503
+ **Captured at**: $CAPTURED_AT
504
+ **Keys written**: \`$LEARNED_KEYS\`
505
+
506
+ 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).
507
+
508
+ \`\`\`yaml
509
+ # Written to forge.yaml
510
+ learned:
511
+ # {key}: {value}
512
+ \`\`\`
513
+
514
+ **Idempotency**: yq merge-write — re-running will not duplicate entries."
515
+
516
+ echo "FORGE:LEARNED annotation posted."
517
+ fi
518
+ ```
519
+
520
+ **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 -->
521
+
285
522
  ### 1D: Update labels & route
286
523
 
287
524
  **CONFIRMED or PARTIAL with decompose: NO**:
288
525
  ```bash
289
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating"
526
+ RESOLUTION=$(resolve_script 'transition-label')
527
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
528
+ case "$TIER" in
529
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
530
+ prose)
531
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" \
532
+ --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
533
+ ;;
534
+ esac
535
+ ```
536
+
537
+ Write machine-readable phase checkpoint (MUST execute immediately after label transition, before continuing):
538
+ ```bash
539
+ CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
540
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
541
+ \`\`\`json
542
+ {\"phase\": \"INVESTIGATION\", \"status\": \"COMPLETE\", \"next_phase\": \"BUILD\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
543
+ \`\`\`"
290
544
  ```
291
545
  <!-- FORGE:PHASE_COMPLETE — Investigation routed to build. See Universal Phase Dispatcher: next phase is Phase 3. Not terminal — continue immediately. -->
292
546
  → Continue to Phase 3.
293
547
 
294
548
  **CONFIRMED or PARTIAL with decompose: YES**:
295
549
  ```bash
296
- gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:investigating"
550
+ RESOLUTION=$(resolve_script 'transition-label')
551
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
552
+ case "$TIER" in
553
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
554
+ prose)
555
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
556
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid" 2>/dev/null || true
557
+ ;;
558
+ esac
559
+ ```
560
+
561
+ Write machine-readable phase checkpoint (MUST execute immediately after label transition, before continuing):
562
+ ```bash
563
+ CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
564
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
565
+ \`\`\`json
566
+ {\"phase\": \"INVESTIGATION\", \"status\": \"COMPLETE\", \"next_phase\": \"DECOMPOSE\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
567
+ \`\`\`"
297
568
  ```
298
569
  <!-- FORGE:PHASE_COMPLETE — Investigation routed to decomposition. See Universal Phase Dispatcher: next phase is Phase 2. Not terminal — continue immediately. -->
299
570
  → Continue to Phase 2 (Decomposition).
300
571
 
301
572
  **INVALID**:
302
573
  ```bash
303
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --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} invalid ;;
578
+ prose)
579
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" \
580
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:decomposed" 2>/dev/null || true
581
+ ;;
582
+ esac
304
583
  gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}"
305
584
  ```
306
- → STOP.
585
+ → STOP. No checkpoint written — INVALID is terminal.
307
586
 
308
587
  ---
309
588
 
@@ -398,9 +677,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSED -->
398
677
 
399
678
  ### 2F: Update labels
400
679
  ```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
680
+ RESOLUTION=$(resolve_script 'transition-label')
681
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
682
+ case "$TIER" in
683
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
684
+ prose)
685
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
686
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid" 2>/dev/null || true
687
+ ;;
688
+ esac
404
689
  ```
405
690
 
406
691
  → STOP. Each sub-issue runs its own `/work-on`.
@@ -413,9 +698,19 @@ gh issue edit {NUMBER} {GH_FLAG} \
413
698
 
414
699
  **Skip if**: `<!-- FORGE:BUILDER -->` exists.
415
700
 
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.**
701
+ **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
702
 
418
703
  ### 3A: Re-read state from GitHub (MANDATORY)
704
+
705
+ **Post Phase 3 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
706
+ ```bash
707
+ PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
708
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
709
+ **Phase**: Phase 3 — Build
710
+ **Timestamp**: ${PHASE_START_TIMESTAMP}
711
+ **Issue**: #{NUMBER}"
712
+ ```
713
+
419
714
  ```bash
420
715
  gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
421
716
 
@@ -426,17 +721,23 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
426
721
  # Check if build already completed
427
722
  gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
428
723
  --jq '.[] | select(.body | contains("FORGE:BUILDER")) | .body'
724
+
725
+ # Check for existing COMPLEXITY_BAND from a prior run (resume path)
726
+ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
727
+ --jq '.[] | select(.body | contains("FORGE:FAST_PATH")) | .body' 2>/dev/null | head -1)
429
728
  ```
430
729
 
431
730
  If no investigation comment with `<!-- INVESTIGATION:COMPLETE -->` → STOP (investigation not complete).
432
731
 
433
732
  Extract from investigation: affected files, root cause, recommendation, task type.
434
733
 
435
- ### 3B: Classify task type
734
+ ### 3B: Classify task type and complexity
735
+
736
+ **Step 1 — Task type classification:**
436
737
 
437
738
  | Signal | Type | Approach |
438
739
  |--------|------|----------|
439
- | Title: "Investigate"/"Audit"/"Research" | Investigation | Produce issues as deliverables |
740
+ | Title starts with "Investigate:"/"Audit:"/"Research:" | Investigation | Produce issues as deliverables |
440
741
  | UI/UX, feature + web/ files | UI/UX | `frontend-design` skill |
441
742
  | Feature + services/ | Backend Feature | Implement directly |
442
743
  | Feature + both | Full-Stack | Backend first, then frontend-design |
@@ -444,7 +745,49 @@ Extract from investigation: affected files, root cause, recommendation, task typ
444
745
  | Bug + services/ | Backend Fix | Direct |
445
746
  | Refactor/docs | Maintenance | Direct |
446
747
 
447
- **Investigation tasks**: Research deeply, create GitHub issues for each finding, post deliverables comment, close issue, skip to Phase 7.
748
+ **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.
749
+
750
+ ```bash
751
+ # Post fast-path comment for investigation tasks
752
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
753
+ ## Fast-Path Classification
754
+
755
+ **COMPLEXITY_BAND**: INVESTIGATION
756
+ **Task type**: Investigation
757
+ **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).
758
+ **Phases skipped**: 3C, 3C.5, 3C.6"
759
+ ```
760
+
761
+ → Jump to Phase 3F immediately. Do not continue to Phase 3C.
762
+
763
+ **Step 2 — Complexity classification (for non-Investigation tasks):**
764
+
765
+ Classify COMPLEXITY_BAND based on affected file count and task nature:
766
+
767
+ | Condition | COMPLEXITY_BAND |
768
+ |-----------|-----------------|
769
+ | Single file, doc/config/markdown only, no logic changes expected | TRIVIAL |
770
+ | 1–5 files, existing patterns, no cross-service impact | STANDARD |
771
+ | 6+ files, new abstractions, cross-service, migration, schema changes | COMPLEX |
772
+
773
+ Post `<!-- FORGE:FAST_PATH -->` comment immediately after classification:
774
+
775
+ ```bash
776
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
777
+ ## Fast-Path Classification
778
+
779
+ **COMPLEXITY_BAND**: {TRIVIAL|STANDARD|COMPLEX}
780
+ **Task type**: {TASK_TYPE}
781
+ **Affected file count**: {N}
782
+ **Rationale**: {one-sentence explanation of classification decision}
783
+ **Phases skipped**: {list phases skipped, or 'none — full pipeline' for STANDARD/COMPLEX}"
784
+ ```
785
+
786
+ **Resume path**: If `EXISTING_FAST_PATH` was read in Phase 3A, extract COMPLEXITY_BAND from it and skip re-classification.
787
+
788
+ **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`.
789
+
790
+ **STANDARD and COMPLEX tasks**: Run full pipeline — 3C → 3C.5 → 3C.6 → 3D onward. No phases skipped.
448
791
 
449
792
  ### 3C: Builder Contract (MANDATORY)
450
793
 
@@ -476,9 +819,11 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
476
819
 
477
820
  Contract must be grounded in the investigation report. Adversarially validate proposed fixes against adjacent system layers.
478
821
 
479
- ### 3C.5: Context Gathering (MANDATORY — max 2 minutes)
822
+ ### 3C.5: Context Gathering (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
480
823
 
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.
824
+ **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.
825
+
826
+ **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
827
 
483
828
  Surface institutional memory before writing code. Extract function names from the contract deliverables table:
484
829
 
@@ -486,7 +831,7 @@ Surface institutional memory before writing code. Extract function names from th
486
831
  FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
487
832
  --jq '.[] | select(.body | contains("FORGE:CONTRACT")) | .body' \
488
833
  | awk '/^### Deliverables/{p=1; next} /^### /{p=0} p' \
489
- | grep -oP '`[A-Za-z_][A-Za-z0-9_]*`' \
834
+ | grep -oE '`[A-Za-z_][A-Za-z0-9_]*`' \
490
835
  | tr -d '`' | sort -u | tr '\n' ' ' | xargs)
491
836
  ```
492
837
 
@@ -511,7 +856,7 @@ done
511
856
 
512
857
  **C2: Past Bugs in the Same Module**
513
858
  ```bash
514
- git log --oneline -30 -- {AFFECTED_FILES} | grep -oP '#\d+' | sort -u | head -8
859
+ git log --oneline -30 -- {AFFECTED_FILES} | grep -oE '#[0-9]+' | sort -u | head -8
515
860
  # For each issue: fetch title + root cause, keep only bug/fix/review-finding labeled. Max 5.
516
861
  ```
517
862
 
@@ -557,13 +902,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTEXT -->
557
902
 
558
903
  If total time exceeds 2 minutes, post partial results with `<!-- FORGE:CONTEXT:PARTIAL -->`.
559
904
 
560
- ### 3C.6: Architecture Plan (MANDATORY)
905
+ ### 3C.6: Architecture Plan (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
906
+
907
+ **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
908
 
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.
909
+ **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
910
 
564
911
  Trace ALL affected code paths before writing code.
565
912
 
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:".
913
+ **Additional skip condition** (STANDARD tasks only): Issue creates ONLY new files with no existing callers AND title starts with "docs:" or "chore:".
567
914
 
568
915
  **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
916
 
@@ -633,25 +980,57 @@ If budget exceeded (3 min), use `<!-- FORGE:ARCHITECT:PARTIAL -->`.
633
980
 
634
981
  ### 3D: Set building label
635
982
  ```bash
636
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" --remove-label "workflow:ready-to-build,workflow:investigating"
983
+ RESOLUTION=$(resolve_script 'transition-label')
984
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
985
+ case "$TIER" in
986
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} building ;;
987
+ prose)
988
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
989
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
990
+ ;;
991
+ esac
637
992
  ```
638
993
 
639
994
  ### 3E: Create worktree
640
995
 
641
996
  Branch slug from title (lowercase, hyphenated, max 40 chars). Prefix: `fix/` (bugs) or `feat/` (features).
642
997
 
998
+ **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 -->
999
+
1000
+ ```bash
1001
+ # Compute PR_BASE deterministically from issue milestone — no LLM interpretation
1002
+ RESOLUTION=$(resolve_script 'classify-lane')
1003
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1004
+ case "$TIER" in
1005
+ adaptive|universal)
1006
+ if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
1007
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to compute PR target — see script error above. Adding needs-human."
1008
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1009
+ exit 1
1010
+ fi
1011
+ ;;
1012
+ prose)
1013
+ # classify-lane has no valid prose fallback — the script output is authoritative.
1014
+ # Without it, PR target cannot be determined safely. Add needs-human and stop.
1015
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot compute PR target deterministically. Adding needs-human."
1016
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1017
+ exit 1
1018
+ ;;
1019
+ esac
1020
+ ```
1021
+
643
1022
  **Determine source branch**:
644
1023
  - Review-finding → parse `**Code branch**: \`{branch}\`` from issue body; branch from `origin/{branch}`
645
1024
  - **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}`
1025
+ - Feature lane (has milestone) → branch from `origin/{PR_BASE}` (PR_BASE now set above)
1026
+ - Fast lane (no milestone) → branch from `origin/{PR_BASE}` (PR_BASE = `{STAGING_BRANCH}`)
648
1027
 
649
1028
  ```bash
650
1029
  cd {REPO_PATH}
651
1030
  git fetch origin
652
1031
  BRANCH="fix/{slug}-{NUMBER}"
653
1032
  WORKTREE_PATH="{REPO_PATH}/.claude/worktrees/{BRANCH_SLUG}"
654
- git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{SOURCE_BRANCH}
1033
+ git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{PR_BASE}
655
1034
  ```
656
1035
 
657
1036
  If worktree already exists: verify correct branch, reuse or remove and recreate.
@@ -794,6 +1173,30 @@ fi
794
1173
  ```
795
1174
  Typecheck or build failures are BLOCKING.
796
1175
 
1176
+ **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):
1177
+
1178
+ ```bash
1179
+ # LEARNED_TEST_COMMANDS was set in Phase 0B.1 from forge.yaml → learned.test_commands
1180
+ # If empty/null, this block is a no-op
1181
+ if [ -n "$LEARNED_TEST_COMMANDS" ] && [ "$LEARNED_TEST_COMMANDS" != "[]" ]; then
1182
+ echo "Running learned test commands..."
1183
+ # yq outputs each entry on its own line with -r flag
1184
+ echo "$LEARNED_TEST_COMMANDS" | yq '.[]' | while IFS= read -r cmd; do
1185
+ [ -z "$cmd" ] && continue
1186
+ echo "Running learned command: $cmd"
1187
+ eval "$cmd" 2>&1 | tail -30
1188
+ CMD_EXIT=$?
1189
+ if [ $CMD_EXIT -ne 0 ]; then
1190
+ echo "FAILED (exit $CMD_EXIT): $cmd"
1191
+ exit $CMD_EXIT
1192
+ fi
1193
+ done
1194
+ else
1195
+ echo "No learned test commands configured — skipping"
1196
+ fi
1197
+ ```
1198
+ Learned test command failures are BLOCKING (same as verification.commands failures). <!-- Added: forge#667 -->
1199
+
797
1200
  ### 3I: Frontend proxy wiring check (MANDATORY)
798
1201
 
799
1202
  Skip if no TS/TSX files changed.
@@ -928,6 +1331,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
928
1331
  <!-- FORGE:BUILDER:COMPLETE -->"
929
1332
  ```
930
1333
 
1334
+ Write machine-readable phase checkpoint (MUST execute immediately after FORGE:BUILDER comment is posted, before Phase 4):
1335
+ ```bash
1336
+ CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1337
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
1338
+ \`\`\`json
1339
+ {\"phase\": \"BUILD\", \"status\": \"COMPLETE\", \"next_phase\": \"REVIEW\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
1340
+ \`\`\`"
1341
+ ```
1342
+
931
1343
  ---
932
1344
 
933
1345
  ## Phase 4: PR Creation
@@ -951,7 +1363,50 @@ cd {WORKTREE_PATH} && git push -u origin {BRANCH}
951
1363
  If fails: try `--force-with-lease`. If still fails: post comment, add `needs-human`, STOP.
952
1364
 
953
1365
  ### 4C: Determine PR target
954
- No milestone → `staging`. Has milestone → `milestone/{slug}`. NEVER `main`.
1366
+ `PR_BASE` was computed in Phase 3E. If somehow unset (e.g., resumed session after compaction), recompute:
1367
+ ```bash
1368
+ RESOLUTION=$(resolve_script 'classify-lane')
1369
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1370
+ case "$TIER" in
1371
+ adaptive|universal)
1372
+ if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
1373
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to recompute PR target — see script error above. Adding needs-human."
1374
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1375
+ exit 1
1376
+ fi
1377
+ ;;
1378
+ prose)
1379
+ # No valid prose fallback — see Phase 3E note.
1380
+ gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot recompute PR target. Adding needs-human."
1381
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1382
+ exit 1
1383
+ ;;
1384
+ esac
1385
+ ```
1386
+ 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 -->
1387
+
1388
+ ### 4C.5: Validate PR target against classified lane
1389
+ ```bash
1390
+ RESOLUTION=$(resolve_script 'validate-pr-target')
1391
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1392
+ case "$TIER" in
1393
+ adaptive|universal)
1394
+ bash "$SCRIPT_PATH" {PR_BASE} {CLASSIFIED_LANE}
1395
+ ;;
1396
+ prose)
1397
+ # validate-pr-target has no safe prose fallback — silently skipping validation risks
1398
+ # merging to the wrong branch. Log a warning but do NOT block the pipeline; the PR
1399
+ # review step will catch a mismatched target before merge.
1400
+ echo "WARNING: validate-pr-target.sh not installed (prose tier) — skipping lane validation. Confirm PR base manually." >&2
1401
+ ;;
1402
+ esac
1403
+ ```
1404
+ `{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):
1405
+ ```bash
1406
+ 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."
1407
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
1408
+ ```
1409
+ → STOP. Do NOT proceed to Phase 4D. <!-- Added: forge#671 -->
955
1410
 
956
1411
  ### 4D: Create PR
957
1412
  ```bash
@@ -978,7 +1433,15 @@ If PR already exists for this branch, use the existing PR number.
978
1433
 
979
1434
  ### 4E: Update labels
980
1435
  ```bash
981
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" --remove-label "workflow:building"
1436
+ RESOLUTION=$(resolve_script 'transition-label')
1437
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1438
+ case "$TIER" in
1439
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
1440
+ prose)
1441
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" \
1442
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1443
+ ;;
1444
+ esac
982
1445
  ```
983
1446
 
984
1447
  ---
@@ -986,6 +1449,16 @@ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" --remove-label
986
1449
  ## Phase 5: Auto-Review
987
1450
 
988
1451
  ### 5A: Re-read state from GitHub (MANDATORY)
1452
+
1453
+ **Post Phase 5 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
1454
+ ```bash
1455
+ PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
1456
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
1457
+ **Phase**: Phase 5 — Review
1458
+ **Timestamp**: ${PHASE_START_TIMESTAMP}
1459
+ **Issue**: #{NUMBER}"
1460
+ ```
1461
+
989
1462
  ```bash
990
1463
  gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state
991
1464
  PR_NUMBER=$(gh pr list {GH_FLAG} --head {BRANCH} --json number --jq '.[0].number')
@@ -1029,10 +1502,19 @@ gh pr view {PR_NUMBER} {GH_FLAG} --json state,mergedAt --jq '{state: .state, mer
1029
1502
  gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
1030
1503
  ```
1031
1504
 
1032
- - PR MERGED + issue CLOSED → proceed to Phase 6
1033
- - PR MERGED + issue OPEN → close issue manually
1505
+ - PR MERGED + issue CLOSED → write checkpoint, then proceed to Phase 6
1506
+ - PR MERGED + issue OPEN → close issue manually, write checkpoint, proceed to Phase 6
1034
1507
  - PR NOT MERGED → `gh pr merge {PR_NUMBER} {GH_FLAG} --merge --auto`. If fails → post comment, add `needs-human`, STOP.
1035
1508
 
1509
+ **When PR is MERGED — write machine-readable phase checkpoint (MANDATORY)**:
1510
+ ```bash
1511
+ CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1512
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
1513
+ \`\`\`json
1514
+ {\"phase\": \"REVIEW\", \"status\": \"COMPLETE\", \"next_phase\": \"CLOSE\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
1515
+ \`\`\`"
1516
+ ```
1517
+
1036
1518
  <!-- FORGE:PHASE_COMPLETE — Review done, PR merged. See Universal Phase Dispatcher: next phase is Phase 6 (Close & Cleanup). Not terminal — continue immediately. -->
1037
1519
 
1038
1520
  **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 +1559,15 @@ If all phases complete:
1077
1559
  ```bash
1078
1560
  gh issue close {NUMBER} {GH_FLAG} \
1079
1561
  --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
1562
+ RESOLUTION=$(resolve_script 'transition-label')
1563
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
1564
+ case "$TIER" in
1565
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} merged ;;
1566
+ prose)
1567
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:merged" \
1568
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1569
+ ;;
1570
+ esac
1083
1571
  ```
1084
1572
 
1085
1573
  ### 6D: Parent tracker update (sub-issues only)
@@ -1088,7 +1576,8 @@ gh issue edit {NUMBER} {GH_FLAG} \
1088
1576
 
1089
1577
  ```bash
1090
1578
  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)
1579
+ | grep -iE '(part of|spawned from|sub-issue of|parent issue:?|parent:)\s*#[0-9]+' \
1580
+ | sed -n 's/.*#\([0-9][0-9]*\).*/\1/p' | head -1)
1092
1581
  ```
1093
1582
 
1094
1583
  If parent found: check off this sub-issue in parent body. If ALL sub-issues checked off → close parent with `workflow:merged`.
@@ -1155,6 +1644,105 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
1155
1644
  **Pipeline completed**: {TIMESTAMP}"
1156
1645
  ```
1157
1646
 
1647
+ ### 7C: Graph Decision Record (MANDATORY when PR exists)
1648
+
1649
+ **Skip if**: `{PR_NUMBER}` is empty (investigation-only tasks with no PR) OR `<!-- FORGE:DECISION_RECORD -->` already posted on the PR.
1650
+
1651
+ **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`.
1652
+
1653
+ **Idempotency check**:
1654
+ ```bash
1655
+ GDR_EXISTS=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
1656
+ --jq '[.[] | select(.body | contains("FORGE:DECISION_RECORD"))] | length > 0' 2>/dev/null || echo "false")
1657
+ ```
1658
+
1659
+ **Extract context edge counts** from FORGE:CONTEXT comment (already posted on issue):
1660
+ ```bash
1661
+ CONTEXT_COMMENT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1662
+ --jq '.[] | select(.body | contains("FORGE:CONTEXT")) | .body' 2>/dev/null | head -1)
1663
+
1664
+ # Count historical review-finding issue references (#NNN patterns in Context comment)
1665
+ REVIEW_FINDING_COUNT=$(echo "$CONTEXT_COMMENT" | grep -oE '#[0-9]+' | wc -l | tr -d ' ')
1666
+ REVIEW_FINDING_COUNT=${REVIEW_FINDING_COUNT:-0}
1667
+ ```
1668
+
1669
+ **Extract review verdict and findings count** from PR review summary (Phase 9 of review-pr):
1670
+ ```bash
1671
+ REVIEW_SUMMARY=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
1672
+ --jq '[.[] | select(.body | contains("FORGE:REVIEWER") or (.body | test("APPROVED:|CHANGES REQUESTED:"; "i")))] | last | .body // ""' 2>/dev/null || echo '')
1673
+
1674
+ REVIEW_VERDICT=$(echo "$REVIEW_SUMMARY" | sed -n 's/.*Verdict: \(APPROVED\|CHANGES REQUESTED\).*/\1/p' | head -1 || echo "APPROVED")
1675
+ REVIEW_VERDICT="${REVIEW_VERDICT:-APPROVED}"
1676
+ FINDINGS_COUNT=$(echo "$REVIEW_SUMMARY" | grep -oE '[0-9]+ findings' | grep -oE '[0-9]+' | head -1 || echo "0")
1677
+ FINDINGS_COUNT="${FINDINGS_COUNT:-0}"
1678
+ AGENTS_RUN=$(echo "$REVIEW_SUMMARY" | grep -oE '[0-9]+ agents' | grep -oE '[0-9]+' | head -1 || echo "0")
1679
+ AGENTS_RUN="${AGENTS_RUN:-0}"
1680
+ ```
1681
+
1682
+ **Post GDR to PR** (not to issue — PR comment survives as permanent artifact on the merged diff):
1683
+ ```bash
1684
+ if [ "$GDR_EXISTS" != "true" ] && [ -n "{PR_NUMBER}" ]; then
1685
+ GDR_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1686
+ HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
1687
+ MERGE_COMMIT=$(gh pr view {PR_NUMBER} {GH_FLAG} --json mergeCommit --jq '.mergeCommit.oid // ""' 2>/dev/null || echo "")
1688
+
1689
+ gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:DECISION_RECORD -->
1690
+ ## Graph Decision Record — Issue #${NUMBER} / PR #${PR_NUMBER}
1691
+
1692
+ \`\`\`json
1693
+ {
1694
+ \"schema_version\": \"1\",
1695
+ \"issue\": ${NUMBER},
1696
+ \"pr\": ${PR_NUMBER},
1697
+ \"repo\": \"{GH_REPO}\",
1698
+ \"lane\": \"{lane}\",
1699
+ \"pr_base\": \"{PR_BASE}\",
1700
+ \"branch\": \"{BRANCH}\",
1701
+ \"head_sha\": \"${HEAD_SHA}\",
1702
+ \"merge_commit\": \"${MERGE_COMMIT}\",
1703
+ \"investigation\": {
1704
+ \"verdict\": \"{VERDICT}\",
1705
+ \"confidence\": \"{CONFIDENCE}\",
1706
+ \"task_type\": \"{TASK_TYPE}\"
1707
+ },
1708
+ \"context\": {
1709
+ \"historical_edges_referenced\": ${REVIEW_FINDING_COUNT},
1710
+ \"forge_annotations_read\": [\"FORGE:INVESTIGATOR\", \"FORGE:CONTRACT\", \"FORGE:CONTEXT\", \"FORGE:ARCHITECT\", \"FORGE:BUILDER\"]
1711
+ },
1712
+ \"build\": {
1713
+ \"files_changed\": {FILES_CHANGED},
1714
+ \"quality_gate\": \"{pass|fail}\",
1715
+ \"quality_gate_iterations\": {GATE_ITERATIONS}
1716
+ },
1717
+ \"review\": {
1718
+ \"verdict\": \"${REVIEW_VERDICT:-APPROVED}\",
1719
+ \"findings_created\": ${FINDINGS_COUNT},
1720
+ \"agents_run\": ${AGENTS_RUN}
1721
+ },
1722
+ \"merge\": {
1723
+ \"merged_at\": \"${GDR_TIMESTAMP}\",
1724
+ \"justification\": \"Investigation confirmed ({VERDICT}/{CONFIDENCE}), quality gate passed, review ${REVIEW_VERDICT:-approved}\"
1725
+ }
1726
+ }
1727
+ \`\`\`
1728
+
1729
+ **Queryable**: \`gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments --jq '[.[] | select(.body | contains(\"FORGE:DECISION_RECORD\"))] | .[0].body\`"
1730
+ fi
1731
+ ```
1732
+
1733
+ **Benchmarking**: Query all GDRs for a repo to compute pipeline metrics:
1734
+ ```bash
1735
+ # Fetch all merged PRs and extract their GDR JSON blocks for metric computation
1736
+ # (used by /pipeline-health to measure repeated-mistake rate, stale-edge hit rate, etc.)
1737
+ gh pr list -R {GH_REPO} --state merged --limit 100 --json number \
1738
+ --jq '.[].number' | while read pr; do
1739
+ gh api repos/{GH_REPO}/issues/$pr/comments \
1740
+ --jq '.[] | select(.body | contains("FORGE:DECISION_RECORD")) | .body' 2>/dev/null
1741
+ done
1742
+ ```
1743
+
1744
+ <!-- Added: forge#776 -->
1745
+
1158
1746
  ---
1159
1747
 
1160
1748
  ## Error Handling