forgedock 1.0.21 → 1.0.22

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 (44) hide show
  1. package/README.md +134 -80
  2. package/bin/demo.mjs +386 -0
  3. package/bin/forgedock.mjs +357 -30
  4. package/bin/init-detect.mjs +95 -0
  5. package/bin/runner.mjs +780 -0
  6. package/commands/cleanup.md +85 -39
  7. package/commands/deploy-info.md +6 -2
  8. package/commands/forge-stats.md +1 -1
  9. package/commands/optimize.md +1 -1
  10. package/commands/orchestrate.md +320 -26
  11. package/commands/quality-gate.md +91 -49
  12. package/commands/review-pr-agents.md +62 -38
  13. package/commands/review-pr.md +33 -2
  14. package/commands/scope.md +11 -3
  15. package/commands/security-audit.md +24 -16
  16. package/commands/sync-ecosystem.md +2 -2
  17. package/commands/work-on/build/architect.md +47 -3
  18. package/commands/work-on/build/context.md +36 -5
  19. package/commands/work-on/build/validate.md +16 -7
  20. package/commands/work-on/close.md +129 -1
  21. package/commands/work-on/investigate.md +24 -1
  22. package/commands/work-on.md +226 -8
  23. package/docs/site/annotations-explained.md +55 -0
  24. package/docs/site/command-learning-path.md +104 -0
  25. package/docs/site/command-reference.md +3 -0
  26. package/docs/site/getting-started.md +36 -14
  27. package/docs/site/troubleshooting.md +208 -0
  28. package/docs/spec/forge-protocol-v1.md +768 -0
  29. package/examples/forgedock-demo/README.md +113 -0
  30. package/examples/forgedock-demo/bootstrap.sh +125 -0
  31. package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
  32. package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
  33. package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
  34. package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
  35. package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
  36. package/examples/forgedock-demo/labels.json +21 -0
  37. package/examples/forgedock-demo/package.json +16 -0
  38. package/examples/forgedock-demo/scripts/smoke.js +61 -0
  39. package/examples/forgedock-demo/src/auth.js +28 -0
  40. package/examples/forgedock-demo/src/db.js +66 -0
  41. package/examples/forgedock-demo/src/routes/notes.js +64 -0
  42. package/examples/forgedock-demo/src/server.js +79 -0
  43. package/package.json +5 -1
  44. package/templates/forge.yaml.minimal +44 -0
@@ -125,6 +125,11 @@ if [ -n "$DEVDOCS_PATH" ] && [ -f "$INDEX_PATH" ]; then
125
125
  ISSUE_LABELS=$(gh issue view {NUMBER} -R {GH_REPO} --json labels \
126
126
  --jq '[.labels[].name] | join(" ")' 2>/dev/null || echo "")
127
127
  echo "Issue labels: ${ISSUE_LABELS:-none}"
128
+ # Separate newline-joined list for the loop below — GitHub label names CAN
129
+ # contain spaces (e.g. "good first issue"), so the space-joined $ISSUE_LABELS
130
+ # above is only safe for display, never for iteration.
131
+ ISSUE_LABELS_LIST=$(gh issue view {NUMBER} -R {GH_REPO} --json labels \
132
+ --jq '.labels[].name' 2>/dev/null || echo "")
128
133
 
129
134
  # Read index content
130
135
  INDEX_CONTENT=$(cat "$INDEX_PATH" 2>/dev/null || echo "")
@@ -139,7 +144,12 @@ if [ -n "$DEVDOCS_PATH" ] && [ -f "$INDEX_PATH" ]; then
139
144
 
140
145
  # Extract domain blocks matching any issue label keyword
141
146
  DOMAIN_PATHS=""
142
- for label in $ISSUE_LABELS; do
147
+ # $ISSUE_LABELS_LIST is one label per line — herestring (not a piped
148
+ # `| while read`, which would run the loop body in a subshell and discard
149
+ # DOMAIN_PATHS once the loop exits) preserves newline-safety for labels
150
+ # containing spaces.
151
+ while IFS= read -r label; do
152
+ [ -z "$label" ] && continue
143
153
  # Strip workflow:, priority:, review-finding prefixes — use bare keyword
144
154
  KEYWORD=$(echo "$label" | sed 's/^workflow://; s/^priority://; s/^review-finding$//')
145
155
  [ -z "$KEYWORD" ] && continue
@@ -158,7 +168,7 @@ if [ -n "$DEVDOCS_PATH" ] && [ -f "$INDEX_PATH" ]; then
158
168
  echo "Domain '${SAFE_KEYWORD}' matched — adding docs: $(echo "$BLOCK_PATHS" | tr '\n' ' ')"
159
169
  DOMAIN_PATHS="${DOMAIN_PATHS}${BLOCK_PATHS}"$'\n'
160
170
  fi
161
- done
171
+ done <<< "$ISSUE_LABELS_LIST"
162
172
 
163
173
  # Combine always_load + domain paths (deduplicate)
164
174
  ALL_PATHS=$(printf "%s\n%s" "$ALWAYS_LOAD_PATHS" "$DOMAIN_PATHS" \
@@ -397,7 +407,12 @@ If `GIST_SUMMARIES` is non-empty, it will be included in the `### Prior Investig
397
407
  Query closed issues with `review-finding` label, searching by filename:
398
408
 
399
409
  ```bash
400
- for file in {AFFECTED_FILES}; do
410
+ # {AFFECTED_FILES} is a space-separated file path list (see contract note above)
411
+ # — split explicitly on IFS=' ' into an array instead of a bare
412
+ # `for file in {AFFECTED_FILES}`, which word-splits on the shell's default IFS
413
+ # (space, tab, AND newline) and would corrupt any path containing a space.
414
+ IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
415
+ for file in "${AFFECTED_FILES_ARR[@]}"; do
401
416
  basename=$(basename "$file" .py)
402
417
  gh issue list -R {GH_REPO} \
403
418
  --state closed \
@@ -425,7 +440,7 @@ Keep findings where the filename or function name appears in the title or body.
425
440
 
426
441
  ## Phase C2: Past Bugs in the Same Module
427
442
 
428
- Mine git log for commit messages referencing issues, then fetch those issues:
443
+ Mine git log for commit messages referencing issues, then fetch those issues. Also read commit bodies/diffs directly — local git history is near-free relative to `gh api` round-trips, and commit bodies often explain the "why" without needing to fetch the linked issue at all.
429
444
 
430
445
  ```bash
431
446
  # Step 1: find issue numbers from git history on affected files
@@ -444,6 +459,20 @@ Filter: keep only `bug`, `fix`, or `review-finding` labeled issues. Skip feature
444
459
 
445
460
  **Max results**: 5 issues.
446
461
 
462
+ **Direct commit-body read (bounded, prefer over `gh api` when it already answers "why")**: For the top 5 commits touching `{AFFECTED_FILES}`, read the commit subject + body directly instead of round-tripping to `gh issue view` when the body already explains the change:
463
+ ```bash
464
+ git log -5 --format='%h %ad %s%n%b' --date=short -- {AFFECTED_FILES}
465
+ ```
466
+ If a commit body fully explains the prior bug/fix (common for squashed or fix-up commits with no `#NNN` reference), use it directly as a "Past Bug in This Module" entry — do not require a linked GitHub issue to exist.
467
+
468
+ **Pickaxe pass (has this exact area been fixed before?)** — one bounded pass, capped at 5 hits, keyed on the suspected symbol/string from the Builder Contract or investigation report:
469
+ ```bash
470
+ git log -S"{suspected_symbol_or_string}" --oneline -- {AFFECTED_FILES} | head -5
471
+ # Use -G instead of -S for regex patterns
472
+ git log -G"{pattern}" --oneline -- {AFFECTED_FILES} | head -5
473
+ ```
474
+ Any hit is a candidate prior fix or reintroduced defect for this exact code area — read `git show {hash}` to confirm scope before including it in the output. This catches regressions the issue-number harvest above misses (e.g. a defect fixed via a squashed commit with no `#NNN` reference).
475
+
447
476
  ---
448
477
 
449
478
  ## Phase C3: Related Code Paths
@@ -526,8 +555,10 @@ gh issue comment {NUMBER} -R {GH_REPO} --body "<!-- FORGE:CONTEXT -->
526
555
  - #{NUM}: \"{TITLE}\" — root cause: {ROOT_CAUSE}
527
556
 
528
557
  ### Past Bugs in This Module
529
- <!-- List of closed bug issues from git log mining. If none: 'No prior bugs found in git history.' -->
558
+ <!-- List of closed bug issues from git log mining, PLUS pickaxe-derived findings (commits with no linked issue, or
559
+ commit bodies read directly per the C2 direct-commit-body step). If none: 'No prior bugs found in git history.' -->
530
560
  - #{NUM}: \"{TITLE}\" — root cause: {SNIPPET}
561
+ - {COMMIT_HASH} (no linked issue): {COMMIT_SUBJECT} — {WHY_RELEVANT, from commit body or pickaxe hit}
531
562
 
532
563
  ### Related Code Paths (must stay consistent)
533
564
  <!-- Files that import or call the changed functions. Builder must read and validate these. -->
@@ -172,21 +172,30 @@ When changed files touch database engine configuration, flag for manual connecti
172
172
  ```bash
173
173
  cd {WORKTREE_PATH}
174
174
  DB_CONFIG_FILES=""
175
- for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$'); do
175
+ # Process substitution (< <(...)), NOT a piped `| while read`, so DB_CONFIG_FILES
176
+ # set inside the loop body survives past the loop (a piped while-read would run
177
+ # in a subshell and silently discard the accumulator).
178
+ while IFS= read -r f; do
179
+ [ -z "$f" ] && continue
176
180
  grep -qE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" "$f" 2>/dev/null && \
177
- DB_CONFIG_FILES="$DB_CONFIG_FILES $f"
178
- done
181
+ DB_CONFIG_FILES="${DB_CONFIG_FILES}${f}"$'\n'
182
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
179
183
 
180
184
  if [ -n "$DB_CONFIG_FILES" ]; then
181
- echo "DB CONFIG CHANGE DETECTED in:$DB_CONFIG_FILES"
185
+ echo "DB CONFIG CHANGE DETECTED in:"
186
+ echo "$DB_CONFIG_FILES"
182
187
  echo "ACTION: Verify database connectivity after deploy — changes to engine config, connect_args, or session factories can cause silent runtime failures."
183
188
  echo "RECOMMENDED: Run a minimal connectivity test (e.g., SELECT 1) through the modified session/engine path."
184
189
 
185
190
  # Check for lambda/callable in connect_args — the exact bug class from PR #14391
186
- for f in $DB_CONFIG_FILES; do
191
+ # $DB_CONFIG_FILES is one path per line (built above) — herestring, not a
192
+ # piped `| while read`, so behavior stays consistent with the other fixes
193
+ # in this sweep even though no accumulator is set inside this particular loop.
194
+ while IFS= read -r f; do
195
+ [ -z "$f" ] && continue
187
196
  grep -nE "lambda.*:.*['\"]|=lambda" "$f" 2>/dev/null | grep -iE "connect_args|prepared_statement|pool|engine" && \
188
197
  echo "WARNING: Lambda/callable in database configuration in $f — verify callback signature matches library's expected calling convention"
189
- done
198
+ done <<< "$DB_CONFIG_FILES"
190
199
  fi
191
200
  ```
192
201
 
@@ -234,7 +243,7 @@ After the gate passes, commit all staged changes in a single commit. This includ
234
243
  ```bash
235
244
  cd {WORKTREE_PATH}
236
245
  git add -u
237
- git commit -m "fix({SCOPE}): {description} (#NUMBER)"
246
+ git commit -s -m "fix({SCOPE}): {description} (#NUMBER)"
238
247
  ```
239
248
 
240
249
  Where `{SCOPE}` is the command or module scope from the contract (e.g. `work-on`, `quality-gate`), and `{description}` summarises the implementation. Use the commit convention from the contract:
@@ -279,6 +279,125 @@ Output to stdout (returned to calling agent):
279
279
 
280
280
  ---
281
281
 
282
+ ## Phase C4.5: Pipeline Summary Card (MANDATORY)
283
+
284
+ The shareable artifact. After the close completes, render a box-drawing summary card to
285
+ stdout (terminal screenshot moment) AND compute a machine-readable twin that Phase C5
286
+ embeds in the `FORGE:TRAJECTORY` comment for platform consumption.
287
+
288
+ **All stats are real — pulled from `gh`/`git`. Every lookup degrades gracefully: a missing
289
+ value renders as `—` and NEVER aborts the card. Do NOT fabricate stats.**
290
+
291
+ ### C4.5a: Gather real stats
292
+
293
+ ```bash
294
+ # Commit / diff stats from the merged PR (single API call). Fallbacks to "—" if absent.
295
+ PR_STATS=$(gh pr view {PR_NUMBER} {GH_FLAG} --json commits,additions,deletions,baseRefName,isDraft 2>/dev/null)
296
+ COMMITS=$(echo "$PR_STATS" | jq -r '(.commits | length) // empty' 2>/dev/null); COMMITS=${COMMITS:-—}
297
+ ADDITIONS=$(echo "$PR_STATS" | jq -r '.additions // empty' 2>/dev/null); ADDITIONS=${ADDITIONS:-—}
298
+ DELETIONS=$(echo "$PR_STATS" | jq -r '.deletions // empty' 2>/dev/null); DELETIONS=${DELETIONS:-—}
299
+ PR_TARGET=$(echo "$PR_STATS" | jq -r '.baseRefName // empty' 2>/dev/null); PR_TARGET=${PR_TARGET:-{PR_BASE}}
300
+ IS_DRAFT=$(echo "$PR_STATS" | jq -r '.isDraft // false' 2>/dev/null)
301
+
302
+ # Review summary — count domain-agent verdicts posted by /review-pr on the PR.
303
+ REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
304
+ --jq '[.reviews[].body // ""] + [.comments[].body // ""] | .[]' 2>/dev/null)
305
+ # NOTE: `grep -c` already prints `0` on no match (and exits non-zero) — do NOT add
306
+ # `|| echo 0`, which would append a second line ("0\n0") and break the arithmetic
307
+ # and `--argjson` below. Swallow the non-zero exit with `|| true`, then default.
308
+ APPROVED=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED=${APPROVED:-0}
309
+ CHANGES=$(echo "$REVIEW_BODIES" | grep -cE 'CHANGES REQUESTED:' 2>/dev/null || true); CHANGES=${CHANGES:-0}
310
+ TOTAL_AGENTS=$((APPROVED + CHANGES))
311
+ # Blockers = review-finding issues created by this PR that are still open (best-effort).
312
+ BLOCKERS=$(echo "$REVIEW_BODIES" | grep -ciE 'blocker|merge.?block' 2>/dev/null || true); BLOCKERS=${BLOCKERS:-0}
313
+ if [ "$TOTAL_AGENTS" -gt 0 ]; then
314
+ REVIEW_SUMMARY="${APPROVED}/${TOTAL_AGENTS} agents passed, ${BLOCKERS} blockers"
315
+ else
316
+ REVIEW_SUMMARY="—" # review data unavailable (e.g. review skipped)
317
+ fi
318
+
319
+ # Elapsed wall-clock: first FORGE agent comment → now.
320
+ FIRST_TS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
321
+ --jq '[.[] | select(.body | contains("FORGE:")) | .created_at] | sort | .[0] // empty' 2>/dev/null)
322
+ if [ -n "$FIRST_TS" ]; then
323
+ START_EPOCH=$(date -u -d "$FIRST_TS" +%s 2>/dev/null \
324
+ || python3 -c "import sys,datetime; ts=sys.argv[1].rstrip('Z'); print(int(datetime.datetime.fromisoformat(ts+'+00:00').timestamp()))" "$FIRST_TS" 2>/dev/null \
325
+ || echo "")
326
+ NOW_EPOCH=$(date -u +%s)
327
+ if [ -n "$START_EPOCH" ]; then
328
+ ELAPSED_SECS=$((NOW_EPOCH - START_EPOCH))
329
+ ELAPSED=$(printf '%dm %02ds' $((ELAPSED_SECS / 60)) $((ELAPSED_SECS % 60)))
330
+ else ELAPSED="—"; ELAPSED_SECS=0; fi
331
+ else ELAPSED="—"; ELAPSED_SECS=0; fi
332
+
333
+ # Pipeline line + status — reflect the ACTUAL terminal state.
334
+ # merged → investigate → architect → build → review → merge ✓
335
+ # decomposed→ investigate → decompose ⏹ (sub-issues spawned)
336
+ # invalid → investigate → invalid ✗
337
+ # blocked → investigate → … → blocked ⚠ (needs-human)
338
+ # draft PR → append "(draft)" to the merge segment
339
+ case "{TERMINAL_STATE}" in
340
+ decomposed) PIPELINE_LINE="investigate → decompose ⏹"; CARD_STATUS="decomposed" ;;
341
+ invalid) PIPELINE_LINE="investigate → invalid ✗"; CARD_STATUS="invalid" ;;
342
+ blocked) PIPELINE_LINE="investigate → build → blocked ⚠"; CARD_STATUS="blocked" ;;
343
+ *) PIPELINE_LINE="investigate → architect → build → review → merge ✓"; CARD_STATUS="merged" ;;
344
+ esac
345
+ [ "$IS_DRAFT" = "true" ] && PIPELINE_LINE="${PIPELINE_LINE} (draft)"
346
+ ```
347
+
348
+ ### C4.5b: Render the card to stdout
349
+
350
+ Print the card to stdout (the calling agent surfaces it in the terminal). Card inner
351
+ width is **51** columns. Truncate the title with an ellipsis (`…`) if `#{NUMBER} — {TITLE}`
352
+ exceeds the field; pad shorter lines with spaces so the right border `║` stays aligned.
353
+
354
+ ```
355
+ ╔═══════════════════════════════════════════════════╗
356
+ ║ ForgeDock Pipeline Complete ║
357
+ ╠═══════════════════════════════════════════════════╣
358
+ ║ ║
359
+ ║ Issue: #{NUMBER} — {TITLE} ║
360
+ ║ Pipeline: {PIPELINE_LINE} ║
361
+ ║ Commits: {COMMITS} ({ADDITIONS} additions, {DELETIONS} deletions) ║
362
+ ║ PR: #{PR_NUMBER} (merged to {PR_TARGET}) ║
363
+ ║ Review: {REVIEW_SUMMARY} ║
364
+ ║ Time: {ELAPSED} ║
365
+ ║ ║
366
+ ╚═══════════════════════════════════════════════════╝
367
+ ```
368
+
369
+ **Edge-case rendering**:
370
+ - Decomposed: title line stays; `Pipeline:` shows `investigate → decompose ⏹`; `PR:`, `Review:`, `Commits:` render `—`; the header reads `ForgeDock Pipeline — Decomposed`.
371
+ - Invalid: header `ForgeDock Pipeline — Closed (invalid)`; `Pipeline:` shows `investigate → invalid ✗`; downstream stats `—`.
372
+ - Blocked / needs-human: header `ForgeDock Pipeline — Blocked`; `Review:`/`PR:` reflect last known state; remaining stats `—`.
373
+ - Draft PR: `PR:` line appends `(draft)` and the merge segment is not marked `✓`.
374
+
375
+ ### C4.5c: Build the machine-readable twin
376
+
377
+ Assemble the JSON object below (used verbatim by Phase C5). Numeric stats that were `—`
378
+ become `null` in JSON; never emit `"—"` as a number.
379
+
380
+ ```bash
381
+ CARD_JSON=$(jq -nc \
382
+ --argjson issue {NUMBER} \
383
+ --arg title "{TITLE}" \
384
+ --arg status "$CARD_STATUS" \
385
+ --arg pipeline "$PIPELINE_LINE" \
386
+ --arg pr "{PR_NUMBER}" \
387
+ --arg target "$PR_TARGET" \
388
+ --arg commits "$COMMITS" --arg adds "$ADDITIONS" --arg dels "$DELETIONS" \
389
+ --arg review "$REVIEW_SUMMARY" --argjson blockers "${BLOCKERS:-0}" \
390
+ --argjson elapsed "${ELAPSED_SECS:-0}" \
391
+ '{issue:$issue, title:$title, status:$status, pipeline:$pipeline,
392
+ pr:($pr|tonumber? // null), pr_target:$target,
393
+ commits:($commits|tonumber? // null),
394
+ additions:($adds|tonumber? // null),
395
+ deletions:($dels|tonumber? // null),
396
+ review:$review, blockers:$blockers, elapsed_seconds:$elapsed}')
397
+ ```
398
+
399
+ ---
400
+
282
401
  ## Phase C5: Trajectory Log (MANDATORY)
283
402
 
284
403
  Post the `<!-- FORGE:TRAJECTORY -->` comment as the final pipeline record:
@@ -305,9 +424,18 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
305
424
 
306
425
  **Anomalies**: None
307
426
 
308
- **Pipeline completed**: {TIMESTAMP}"
427
+ **Pipeline completed**: {TIMESTAMP}
428
+
429
+ <!-- FORGE:CARD ${CARD_JSON} -->"
309
430
  ```
310
431
 
432
+ The `<!-- FORGE:CARD {...} -->` block carries the machine-readable summary computed in
433
+ Phase C4.5c. It is wrapped in an HTML comment so it stays hidden in GitHub's rendered view
434
+ (keeping the trajectory comment clean) while remaining greppable in the raw body for platform
435
+ consumption — `/orchestrate` reads it to build per-issue cards. This block is **additive**:
436
+ all existing `FORGE:TRAJECTORY` consumers select via `contains("FORGE:TRAJECTORY")` and parse
437
+ the markdown table, so the embedded JSON does not affect them.
438
+
311
439
  Where:
312
440
  - `{PARENT_STATUS}` = `⏭ Skipped` (if no parent) or `✅ Complete` (if parent updated)
313
441
  - `{PARENT_NOTES}` = `No parent tracker` or `Checked off in #{PARENT_REF}`
@@ -72,13 +72,30 @@ If the issue specifies a **Code branch** (`**Code branch**: \`{branch}\``), chec
72
72
  1. **Check the right branch** — read from the branch specified in the issue body (`**Code branch**: \`{branch}\``) if present
73
73
  2. **Read domain files** — start with the key files for the affected domain
74
74
  3. **Verify claims** — does the code actually have the problem described?
75
- 4. **Git blame** — trace when/why the relevant code was written
75
+ 4. **Git blame** — trace when/why the relevant code was written. Run bounded, local commands (no network round-trip):
76
+ ```bash
77
+ # Introducing commit for each affected file (first commit that added it)
78
+ git log --reverse --format='%h %an %ad %s' --date=short -- {affected_file} | head -1
79
+ # Last-touch commit (most recent change)
80
+ git log -1 --format='%h %an %ad %s' --date=short -- {affected_file}
81
+ # Line-level blame for a specific suspect hunk, if the issue names one
82
+ git blame -L {start},{end} -- {affected_file}
83
+ ```
84
+ Record the introducing commit and last-touch commit for each primary affected file — this feeds the mandatory **History findings** field in Phase 1C.
76
85
  5. **Domain context discovery** (narrow scope only, 1–5 files):
77
86
  ```bash
78
87
  git log --oneline --all -30 -- {affected_files} | grep -oP '#\d+' | sort -u
79
88
  gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
80
89
  ```
81
90
  Keep only file/function-level overlap. Max 5 related issues. Everything is a hint to verify, not a fact.
91
+
92
+ **Pickaxe pass (prior fix / regression detection)** — bounded to one pass, capped at 5 hits: search for prior additions/removals of the suspected symbol or literal string named in the issue (a function name, error string, or config key), independent of whether that fix was ever linked to a filed issue:
93
+ ```bash
94
+ git log -S"{suspected_symbol_or_string}" --oneline -- {affected_files} | head -5
95
+ # Use -G instead of -S when the target is a regex pattern rather than a literal string
96
+ git log -G"{pattern}" --oneline -- {affected_files} | head -5
97
+ ```
98
+ Any hit here is a candidate prior fix or reintroduced defect — read the commit body (`git show {hash}`) to confirm before citing it. Feed confirmed hits into the History findings field and let them inform the verdict (e.g. a defect being reintroduced raises severity).
82
99
  6. **Determine root cause** — what's actually broken or missing?
83
100
  7. **Identify affected files** — full list of files that need changes
84
101
  8. **Fix-approach validation** — if the issue proposes a fix, don't adopt it as spec. Trace through the target system's middleware, auth, routing, config. Cross-domain: if fix in domain A interacts with domain B, read domain B's files too.
@@ -113,6 +130,12 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
113
130
  ### Evidence
114
131
  {specific findings — function names, line numbers, behavior observed}
115
132
 
133
+ ### History Findings
134
+ **Introducing commit**: {hash — author — date — subject, per primary affected file}
135
+ **Last touched**: {hash — author — date — subject}
136
+ **Pickaxe hits (prior fixes / regressions)**: {commit(s) found via \`git log -S\`/\`-G\`, or 'None found' — max 5}
137
+ {This field is MANDATORY — populate from the git blame + pickaxe commands in step 4/5. If a file is newly created (no history), write 'New file — no history.'}
138
+
116
139
  ### Recommendation
117
140
  {what to build/fix, concrete and actionable}
118
141
 
@@ -96,6 +96,74 @@ Satellite repos (those without a `staging` branch) receive fast-lane PRs directl
96
96
 
97
97
  ## Phase 0: Resolve Issue & Load Context
98
98
 
99
+ ### 0.0: Pre-Flight Checks (MANDATORY — run before any other Phase 0 step)
100
+
101
+ Validate the environment before the pipeline spends tokens. Each check fails fast with an actionable error and a pointer to the troubleshooting guide (`docs/site/troubleshooting.md`). Run all checks; report every failure, then STOP if any HARD check fails. <!-- Added: forge#1149 -->
102
+
103
+ ```bash
104
+ PREFLIGHT_FAILED=0
105
+
106
+ # Check 1 — forge.yaml present (HARD)
107
+ if [ ! -f forge.yaml ]; then
108
+ echo "ERROR: forge.yaml not found in the repository root."
109
+ echo " Fix: run \`npx forgedock init\` to generate one, or copy forge.yaml.example."
110
+ echo " See: docs/site/troubleshooting.md#1-forgeyaml-not-found"
111
+ PREFLIGHT_FAILED=1
112
+ fi
113
+
114
+ # Check 2 — yq installed; forge.yaml is valid YAML (HARD, only if present)
115
+ if [ -f forge.yaml ]; then
116
+ if ! command -v yq >/dev/null 2>&1; then
117
+ echo "ERROR: yq is not installed. The pipeline requires yq to parse forge.yaml."
118
+ echo " Fix: install yq — https://github.com/mikefarah/yq#install"
119
+ echo " See: docs/site/troubleshooting.md#2-forgeyaml-has-a-syntax-error"
120
+ PREFLIGHT_FAILED=1
121
+ elif ! yq '.' forge.yaml >/dev/null 2>&1; then
122
+ echo "ERROR: forge.yaml has a YAML syntax error."
123
+ echo " Fix: run \`yq '.' forge.yaml\` to locate the offending line, then correct the indentation/quoting."
124
+ echo " See: docs/site/troubleshooting.md#2-forgeyaml-has-a-syntax-error"
125
+ PREFLIGHT_FAILED=1
126
+ fi
127
+ fi
128
+
129
+ # Check 3 — gh CLI authenticated (HARD)
130
+ if ! gh auth status >/dev/null 2>&1; then
131
+ echo "ERROR: gh CLI is not authenticated. The pipeline cannot read or write GitHub state."
132
+ echo " Fix: run \`gh auth login\` (ensure repo scope), then \`gh auth status\` to confirm."
133
+ echo " See: docs/site/troubleshooting.md#3-gh-cli-not-authenticated"
134
+ PREFLIGHT_FAILED=1
135
+ fi
136
+
137
+ # Check 4 — workflow labels exist on the repo (SOFT — warn, auto-recoverable)
138
+ if [ -f forge.yaml ] && gh auth status >/dev/null 2>&1; then
139
+ GH_REPO_PF="$(yq -r '.project.owner + "/" + .project.repo' forge.yaml 2>/dev/null)"
140
+ if [ -n "$GH_REPO_PF" ] && ! gh label list -R "$GH_REPO_PF" --search "workflow:" 2>/dev/null | grep -q "workflow:"; then
141
+ echo "WARNING: ForgeDock workflow:* labels not found on $GH_REPO_PF."
142
+ echo " Fix: run \`npx forgedock labels setup\` (or \`--repo $GH_REPO_PF\`) to bootstrap them."
143
+ echo " See: docs/site/troubleshooting.md#9-missing-workflow-labels"
144
+ fi
145
+ fi
146
+
147
+ # Check 5 — GitHub API rate limit headroom (SOFT — warn)
148
+ if gh auth status >/dev/null 2>&1; then
149
+ RL_REMAINING="$(gh api rate_limit --jq '.resources.core.remaining' 2>/dev/null || echo '')"
150
+ if [ -n "$RL_REMAINING" ] && [ "$RL_REMAINING" -lt 100 ] 2>/dev/null; then
151
+ RL_RESET="$(gh api rate_limit --jq '.resources.core.reset' 2>/dev/null)"
152
+ echo "WARNING: GitHub API rate limit low ($RL_REMAINING remaining; resets at epoch $RL_RESET)."
153
+ echo " Fix: wait for the reset, reduce orchestration parallelism, or use a higher-limit PAT."
154
+ echo " See: docs/site/troubleshooting.md#10-github-api-rate-limit-exceeded"
155
+ fi
156
+ fi
157
+
158
+ if [ "$PREFLIGHT_FAILED" -eq 1 ]; then
159
+ echo "Pre-flight checks failed. Resolve the errors above and re-run /work-on {NUMBER}."
160
+ echo "Full recovery guide: docs/site/troubleshooting.md"
161
+ exit 1
162
+ fi
163
+ ```
164
+
165
+ Worktree/branch-already-exists and stale-label conditions are surfaced later (Phase 3E worktree creation and the `## Error Handling` section) with their own recovery guidance in `docs/site/troubleshooting.md`.
166
+
99
167
  ### 0A: Parse input
100
168
  Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human` and `workflow:decomposed`, pick highest priority.
101
169
 
@@ -424,7 +492,16 @@ Use `forge.yaml → review.tech_stack` and the issue domain labels to identify e
424
492
  grep -rn '\.field_name\s*=' services/ # Attribute assignments
425
493
  ```
426
494
  If the field is written with different types in different code paths (e.g. dict in the standard path, string in the auth-gated path), document ALL variants. The fix must handle every variant — not just the one on the primary investigated code path. A type guard like `or {}` only protects against falsy values; a non-empty string is truthy and bypasses it.
427
- 4. Git blame — trace when/why the relevant code was written
495
+ 4. Git blame — trace when/why the relevant code was written. Run bounded, local commands (no network round-trip):
496
+ ```bash
497
+ # Introducing commit for each affected file (first commit that added it)
498
+ git log --reverse --format='%h %an %ad %s' --date=short -- {affected_file} | head -1
499
+ # Last-touch commit (most recent change)
500
+ git log -1 --format='%h %an %ad %s' --date=short -- {affected_file}
501
+ # Line-level blame for a specific suspect hunk, if the issue names one
502
+ git blame -L {start},{end} -- {affected_file}
503
+ ```
504
+ Record the introducing commit and last-touch commit for each primary affected file — this feeds the mandatory **History findings** field in Phase 1C.
428
505
  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)
429
506
  5. Domain context discovery (narrow scope only, 1–5 files):
430
507
  ```bash
@@ -432,6 +509,14 @@ Use `forge.yaml → review.tech_stack` and the issue domain labels to identify e
432
509
  gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
433
510
  ```
434
511
  Keep only file/function-level overlap. Max 5 related issues.
512
+
513
+ **Pickaxe pass (prior fix / regression detection)** — bounded to one pass, capped at 5 hits: search for prior additions/removals of the suspected symbol or literal string named in the issue, independent of whether that fix was ever linked to a filed issue:
514
+ ```bash
515
+ git log -S"{suspected_symbol_or_string}" --oneline -- {affected_files} | head -5
516
+ # Use -G instead of -S when the target is a regex pattern rather than a literal string
517
+ git log -G"{pattern}" --oneline -- {affected_files} | head -5
518
+ ```
519
+ Any hit here is a candidate prior fix or reintroduced defect — read the commit body (`git show {hash}`) to confirm before citing it. Feed confirmed hits into the History findings field and let them inform the verdict (e.g. a defect being reintroduced raises severity).
435
520
  6. Determine root cause
436
521
  7. Identify affected files — full list of files that need changes
437
522
  7.5. **Sibling Pattern Sweep** *(conditional — when the bug is a condition, gated function call, or field presence check)*: After identifying the affected files, grep for the same pattern in sibling files within the same directory. The issue spec may name only the file where the error was first observed — but the same commit or PR that introduced the bug often applied it uniformly across related handlers.
@@ -475,6 +560,12 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
475
560
  ### Evidence
476
561
  {specific findings — function names, line numbers, behavior observed}
477
562
 
563
+ ### History Findings
564
+ **Introducing commit**: {hash — author — date — subject, per primary affected file}
565
+ **Last touched**: {hash — author — date — subject}
566
+ **Pickaxe hits (prior fixes / regressions)**: {commit(s) found via \`git log -S\`/\`-G\`, or 'None found' — max 5}
567
+ {This field is MANDATORY — populate from the git blame + pickaxe commands in step 4/5. If a file is newly created (no history), write 'New file — no history.'}
568
+
478
569
  ### Recommendation
479
570
  {what to build/fix, concrete and actionable}
480
571
 
@@ -889,7 +980,12 @@ Run these queries (20s timeout each, 2 min total budget):
889
980
 
890
981
  **C1: Past Review Findings on These Files**
891
982
  ```bash
892
- for file in {AFFECTED_FILES}; do
983
+ # {AFFECTED_FILES} is a space-separated argument (see --files contract) — split
984
+ # explicitly on IFS=' ' into an array instead of a bare `for file in {AFFECTED_FILES}`,
985
+ # which word-splits on the shell's default IFS (space, tab, AND newline) and
986
+ # would corrupt any path containing a space.
987
+ IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
988
+ for file in "${AFFECTED_FILES_ARR[@]}"; do
893
989
  basename=$(basename "$file" .py)
894
990
  gh issue list -R {GH_REPO} --state closed --label "review-finding" \
895
991
  --search "$basename" --limit 10 \
@@ -908,6 +1004,20 @@ git log --oneline -30 -- {AFFECTED_FILES} | grep -oE '#[0-9]+' | sort -u | head
908
1004
  # For each issue: fetch title + root cause, keep only bug/fix/review-finding labeled. Max 5.
909
1005
  ```
910
1006
 
1007
+ **Direct commit-body read (bounded, prefer over `gh api` when it already answers "why")**: read the top 5 commit subjects+bodies on the affected files directly — local git is near-free relative to `gh api` round-trips, and commit bodies often explain the "why" without needing to fetch a linked issue at all:
1008
+ ```bash
1009
+ git log -5 --format='%h %ad %s%n%b' --date=short -- {AFFECTED_FILES}
1010
+ ```
1011
+ If a commit body fully explains a prior bug/fix (common for squashed or fix-up commits with no `#NNN` reference), use it directly as a "Past Bug in This Module" entry — do not require a linked GitHub issue to exist.
1012
+
1013
+ **Pickaxe pass (has this exact area been fixed before?)** — one bounded pass, capped at 5 hits, keyed on the suspected symbol/string from the Builder Contract or investigation report:
1014
+ ```bash
1015
+ git log -S"{suspected_symbol_or_string}" --oneline -- {AFFECTED_FILES} | head -5
1016
+ # Use -G instead of -S for regex patterns
1017
+ git log -G"{pattern}" --oneline -- {AFFECTED_FILES} | head -5
1018
+ ```
1019
+ Any hit is a candidate prior fix or reintroduced defect for this exact code area — read `git show {hash}` to confirm scope before including it in the output. This catches regressions the issue-number harvest above misses (e.g. a defect fixed via a squashed commit with no `#NNN` reference).
1020
+
911
1021
  **C3: Related Code Paths** (callers/importers of FUNCTION_NAMES)
912
1022
  ```bash
913
1023
  for fn in {FUNCTION_NAMES}; do
@@ -934,7 +1044,8 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTEXT -->
934
1044
  {past review-finding issues}
935
1045
 
936
1046
  ### Past Bugs in This Module
937
- {closed bug issues from git log mining}
1047
+ {closed bug issues from git log mining, PLUS pickaxe-derived findings (commits with no linked issue, or commit
1048
+ bodies read directly per the C2 direct-commit-body step)}
938
1049
 
939
1050
  ### Related Code Paths (must stay consistent)
940
1051
  {files that import/call changed functions}
@@ -1257,10 +1368,11 @@ Skip if no changed Python files contain DB engine/session/pool patterns.
1257
1368
 
1258
1369
  ```bash
1259
1370
  cd {WORKTREE_PATH}
1260
- for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$'); do
1371
+ while IFS= read -r f; do
1372
+ [ -z "$f" ] && continue
1261
1373
  grep -qE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" "$f" 2>/dev/null && \
1262
1374
  echo "DB CONFIG CHANGE DETECTED in: $f"
1263
- done
1375
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
1264
1376
  ```
1265
1377
 
1266
1378
  Advisory only — does not block build. Check for lambda/callable in connect_args (the exact bug class from PR #14391).
@@ -1335,7 +1447,7 @@ Stage all changes and commit:
1335
1447
  ```bash
1336
1448
  cd {WORKTREE_PATH}
1337
1449
  git add -u
1338
- git commit -m "fix({SCOPE}): {description} (#{NUMBER})"
1450
+ git commit -s -m "fix({SCOPE}): {description} (#{NUMBER})"
1339
1451
  ```
1340
1452
 
1341
1453
  Conventional prefix: `fix`/`feat`/`refactor`/`docs`. Reference `#{NUMBER}` in message.
@@ -1649,7 +1761,14 @@ fi
1649
1761
 
1650
1762
  ## Phase 7: Summary & Trajectory
1651
1763
 
1652
- ### 7A: Report
1764
+ ### 7A: Report + Pipeline Summary Card
1765
+
1766
+ Output the terse report, then render the shareable **Pipeline Summary Card** — the shareable
1767
+ moment a developer screenshots. Gather real stats (commits, additions/deletions, PR target,
1768
+ review summary, elapsed time) and render exactly as specified in `work-on/close.md` Phase C4.5
1769
+ (`C4.5a` stats gathering → `C4.5b` box-drawing card to stdout → `C4.5c` machine-readable twin).
1770
+ This inline path and the delegated `close.md` path MUST produce an identical card.
1771
+
1653
1772
  ```
1654
1773
  ## Done: #{NUMBER} — {TITLE}
1655
1774
  - Investigation: {VERDICT} ({CONFIDENCE})
@@ -1658,6 +1777,98 @@ fi
1658
1777
  - Files changed: {COUNT}
1659
1778
  ```
1660
1779
 
1780
+ Then print the card to stdout (inner width 51; truncate long titles with `…`; missing stats
1781
+ render `—`; pipeline line reflects the actual terminal state — merged / decomposed / invalid /
1782
+ blocked; draft PRs append `(draft)`):
1783
+
1784
+ ```
1785
+ ╔═══════════════════════════════════════════════════╗
1786
+ ║ ForgeDock Pipeline Complete ║
1787
+ ╠═══════════════════════════════════════════════════╣
1788
+ ║ ║
1789
+ ║ Issue: #{NUMBER} — {TITLE} ║
1790
+ ║ Pipeline: investigate → architect → build → ║
1791
+ ║ review → merge ✓ ║
1792
+ ║ Commits: {COMMITS} ({ADDITIONS} additions, {DELETIONS} deletions) ║
1793
+ ║ PR: #{PR_NUMBER} (merged to {PR_BASE}) ║
1794
+ ║ Review: {REVIEW_SUMMARY} ║
1795
+ ║ Time: {ELAPSED} ║
1796
+ ║ ║
1797
+ ╚═══════════════════════════════════════════════════╝
1798
+ ```
1799
+
1800
+ **Gather real stats** (C4.5a — this block MUST run on the inline path to populate card variables;
1801
+ do NOT rely on the cross-reference to `close.md` alone):
1802
+
1803
+ ```bash
1804
+ PR_STATS=$(gh pr view {PR_NUMBER} {GH_FLAG} --json commits,additions,deletions,baseRefName,isDraft 2>/dev/null)
1805
+ COMMITS=$(echo "$PR_STATS" | jq -r '(.commits | length) // empty' 2>/dev/null); COMMITS=${COMMITS:-—}
1806
+ ADDITIONS=$(echo "$PR_STATS" | jq -r '.additions // empty' 2>/dev/null); ADDITIONS=${ADDITIONS:-—}
1807
+ DELETIONS=$(echo "$PR_STATS" | jq -r '.deletions // empty' 2>/dev/null); DELETIONS=${DELETIONS:-—}
1808
+ PR_TARGET=$(echo "$PR_STATS" | jq -r '.baseRefName // empty' 2>/dev/null); PR_TARGET=${PR_TARGET:-{PR_BASE}}
1809
+ IS_DRAFT=$(echo "$PR_STATS" | jq -r '.isDraft // false' 2>/dev/null)
1810
+
1811
+ REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
1812
+ --jq '[.reviews[].body // ""] + [.comments[].body // ""] | .[]' 2>/dev/null)
1813
+ # NOTE: `grep -c` already prints `0` on no match (and exits non-zero) — do NOT add
1814
+ # `|| echo 0`, which would append a second line ("0\n0") and break the arithmetic
1815
+ # and `--argjson` below. Swallow the non-zero exit with `|| true`, then default.
1816
+ APPROVED=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED=${APPROVED:-0}
1817
+ CHANGES=$(echo "$REVIEW_BODIES" | grep -cE 'CHANGES REQUESTED:' 2>/dev/null || true); CHANGES=${CHANGES:-0}
1818
+ TOTAL_AGENTS=$((APPROVED + CHANGES))
1819
+ BLOCKERS=$(echo "$REVIEW_BODIES" | grep -ciE 'blocker|merge.?block' 2>/dev/null || true); BLOCKERS=${BLOCKERS:-0}
1820
+ if [ "$TOTAL_AGENTS" -gt 0 ]; then
1821
+ REVIEW_SUMMARY="${APPROVED}/${TOTAL_AGENTS} agents passed, ${BLOCKERS} blockers"
1822
+ else
1823
+ REVIEW_SUMMARY="—" # review data unavailable (e.g. review skipped)
1824
+ fi
1825
+
1826
+ FIRST_TS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1827
+ --jq '[.[] | select(.body | contains("FORGE:")) | .created_at] | sort | .[0] // empty' 2>/dev/null)
1828
+ if [ -n "$FIRST_TS" ]; then
1829
+ START_EPOCH=$(date -u -d "$FIRST_TS" +%s 2>/dev/null \
1830
+ || python3 -c "import sys,datetime; ts=sys.argv[1].rstrip('Z'); print(int(datetime.datetime.fromisoformat(ts+'+00:00').timestamp()))" "$FIRST_TS" 2>/dev/null \
1831
+ || echo "")
1832
+ NOW_EPOCH=$(date -u +%s)
1833
+ if [ -n "$START_EPOCH" ]; then
1834
+ ELAPSED_SECS=$((NOW_EPOCH - START_EPOCH))
1835
+ ELAPSED=$(printf '%dm %02ds' $((ELAPSED_SECS / 60)) $((ELAPSED_SECS % 60)))
1836
+ else ELAPSED="—"; ELAPSED_SECS=0; fi
1837
+ else ELAPSED="—"; ELAPSED_SECS=0; fi
1838
+
1839
+ case "{TERMINAL_STATE}" in
1840
+ decomposed) PIPELINE_LINE="investigate → decompose ⏹"; CARD_STATUS="decomposed" ;;
1841
+ invalid) PIPELINE_LINE="investigate → invalid ✗"; CARD_STATUS="invalid" ;;
1842
+ blocked) PIPELINE_LINE="investigate → build → blocked ⚠"; CARD_STATUS="blocked" ;;
1843
+ *) PIPELINE_LINE="investigate → architect → build → review → merge ✓"; CARD_STATUS="merged" ;;
1844
+ esac
1845
+ [ "$IS_DRAFT" = "true" ] && PIPELINE_LINE="${PIPELINE_LINE} (draft)"
1846
+ ```
1847
+
1848
+ **Build the machine-readable twin** (C4.5c — MUST run this block to assign `CARD_JSON` before
1849
+ Phase 7B embeds it; the cross-reference to `close.md` above is insufficient on the inline path): <!-- forge#1178 -->
1850
+
1851
+ ```bash
1852
+ CARD_JSON=$(jq -nc \
1853
+ --argjson issue {NUMBER} \
1854
+ --arg title "{TITLE}" \
1855
+ --arg status "$CARD_STATUS" \
1856
+ --arg pipeline "$PIPELINE_LINE" \
1857
+ --arg pr "{PR_NUMBER}" \
1858
+ --arg target "$PR_TARGET" \
1859
+ --arg commits "$COMMITS" --arg adds "$ADDITIONS" --arg dels "$DELETIONS" \
1860
+ --arg review "$REVIEW_SUMMARY" --argjson blockers "${BLOCKERS:-0}" \
1861
+ --argjson elapsed "${ELAPSED_SECS:-0}" \
1862
+ '{issue:$issue, title:$title, status:$status, pipeline:$pipeline,
1863
+ pr:($pr|tonumber? // null), pr_target:$target,
1864
+ commits:($commits|tonumber? // null),
1865
+ additions:($adds|tonumber? // null),
1866
+ deletions:($dels|tonumber? // null),
1867
+ review:$review, blockers:$blockers, elapsed_seconds:$elapsed}')
1868
+ ```
1869
+
1870
+ `CARD_JSON` is now set and embedded in the trajectory comment by 7B.
1871
+
1661
1872
  ### 7B: Trajectory Log (MANDATORY)
1662
1873
 
1663
1874
  **Review-presence check** (run before filling in Phase 4-5 row): <!-- Added: forge#381 -->
@@ -1691,9 +1902,16 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
1691
1902
 
1692
1903
  **Decisions**: {key decisions}
1693
1904
  **Anomalies**: {anomalies or None}
1694
- **Pipeline completed**: {TIMESTAMP}"
1905
+ **Pipeline completed**: {TIMESTAMP}
1906
+
1907
+ <!-- FORGE:CARD ${CARD_JSON} -->"
1695
1908
  ```
1696
1909
 
1910
+ Append the `<!-- FORGE:CARD {...} -->` block (machine-readable twin from 7A / close.md C4.5c)
1911
+ as the last line of the trajectory comment. It is HTML-comment-wrapped so it stays hidden in
1912
+ the rendered view but greppable for platform consumption (`/orchestrate` Phase 6 reads it for
1913
+ per-issue cards). Additive — does not affect existing `FORGE:TRAJECTORY` table consumers.
1914
+
1697
1915
  ### 7C: Graph Decision Record (MANDATORY when PR exists)
1698
1916
 
1699
1917
  **Skip if**: `{PR_NUMBER}` is empty (investigation-only tasks with no PR) OR `<!-- FORGE:DECISION_RECORD -->` already posted on the PR.