forgedock 1.1.7 → 1.1.9

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 (97) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine/invariants.mjs +361 -0
  4. package/bin/engine-cli.mjs +137 -11
  5. package/bin/env-detect.mjs +249 -0
  6. package/bin/forge-utils.mjs +364 -4
  7. package/bin/forgedock.mjs +1064 -135
  8. package/bin/hooks/pre-tool-use.mjs +182 -0
  9. package/bin/hooks/session-start.mjs +68 -3
  10. package/bin/init-detect.mjs +101 -12
  11. package/bin/journey.mjs +917 -16
  12. package/bin/labels.json +10 -0
  13. package/bin/recall.mjs +557 -0
  14. package/bin/registry.mjs +91 -3
  15. package/bin/runner.mjs +38 -2
  16. package/bin/tui.mjs +49 -3
  17. package/commands/adopt.md +1 -1
  18. package/commands/analytics.md +8 -8
  19. package/commands/audit.md +1 -1
  20. package/commands/autopilot.md +958 -77
  21. package/commands/ci-audit.md +1 -1
  22. package/commands/cleanup.md +139 -55
  23. package/commands/compat-audit.md +1 -1
  24. package/commands/deploy-info.md +1 -1
  25. package/commands/deploy-pr.md +1 -1
  26. package/commands/diagnose.md +1 -1
  27. package/commands/explain.md +1 -1
  28. package/commands/fix-ci.md +1 -1
  29. package/commands/forgedock-init.md +1 -1
  30. package/commands/incident-response.md +3 -3
  31. package/commands/issue.md +1 -1
  32. package/commands/optimize.md +586 -1
  33. package/commands/orchestrate/config.md +13 -3
  34. package/commands/orchestrate/phase-1-resolve.md +36 -26
  35. package/commands/orchestrate/phase-2-triage.md +1 -1
  36. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  37. package/commands/orchestrate/phase-3-dependency.md +587 -48
  38. package/commands/orchestrate/phase-4-execution.md +1031 -57
  39. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  40. package/commands/orchestrate/phase-6-report.md +248 -11
  41. package/commands/orchestrate/safety.md +2 -2
  42. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  43. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  44. package/commands/qa-sweep.md +1 -1
  45. package/commands/quality-gate.md +418 -1
  46. package/commands/recover-orphans.md +5 -5
  47. package/commands/replay.md +1 -1
  48. package/commands/review-pr-agents/api.md +2 -2
  49. package/commands/review-pr-agents/auth.md +2 -2
  50. package/commands/review-pr-agents/billing.md +2 -2
  51. package/commands/review-pr-agents/concurrency.md +2 -2
  52. package/commands/review-pr-agents/database.md +2 -2
  53. package/commands/review-pr-agents/frontend.md +2 -2
  54. package/commands/review-pr-agents/infra.md +2 -2
  55. package/commands/review-pr-agents/protocols.md +1 -1
  56. package/commands/review-pr-agents/scraper.md +2 -2
  57. package/commands/review-pr-agents/security.md +2 -2
  58. package/commands/review-pr-staging.md +41 -3
  59. package/commands/review-pr.md +462 -19
  60. package/commands/rollback.md +1 -1
  61. package/commands/scope.md +1 -1
  62. package/commands/security-audit.md +1 -1
  63. package/commands/spec-doctor.md +429 -0
  64. package/commands/test-gate.md +1 -1
  65. package/commands/validate.md +53 -1
  66. package/commands/work-on/build/architect.md +78 -1
  67. package/commands/work-on/build/context.md +360 -22
  68. package/commands/work-on/build/implement.md +3 -1
  69. package/commands/work-on/build/validate.md +29 -1
  70. package/commands/work-on/build.md +75 -6
  71. package/commands/work-on/close.md +624 -10
  72. package/commands/work-on/decompose.md +1 -1
  73. package/commands/work-on/investigate.md +235 -5
  74. package/commands/work-on/remediate.md +331 -0
  75. package/commands/work-on/review.md +1 -1
  76. package/commands/work-on-monolithic.md +1 -1
  77. package/commands/work-on.md +205 -66
  78. package/docs/CI.md +2 -2
  79. package/docs/CODEX.md +1 -1
  80. package/docs/CONFIG.md +186 -0
  81. package/docs/FORGE-PROTOCOL.md +101 -0
  82. package/docs/site/command-reference.md +31 -1
  83. package/docs/site/getting-started.md +9 -34
  84. package/docs/site/index.md +1 -1
  85. package/docs/spec/forge-protocol-v1.md +97 -5
  86. package/docs/spec/label-state-machine.md +6 -1
  87. package/package.json +3 -2
  88. package/packages/protocol/src/cli.js +374 -84
  89. package/packages/protocol/src/types.js +75 -1
  90. package/scripts/build-knowledge-index.mjs +1761 -0
  91. package/scripts/calibration.mjs +996 -0
  92. package/scripts/check-command-docs-drift.sh +4 -2
  93. package/scripts/danger-zones.mjs +789 -0
  94. package/scripts/doctor-pipeline-state.sh +88 -2
  95. package/scripts/select-fix-targets.sh +234 -0
  96. package/scripts/transition-label.sh +245 -6
  97. package/scripts/refresh-bot-token.sh +0 -97
@@ -12,7 +12,7 @@ argument-hint: [issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--pr PR_NUMB
12
12
  **Invoked by**: `work-on.md` Phase 6–7, after `review.md` returns `REVIEW_RESULT: status: COMPLETE`.
13
13
  **Output**: Update project board, close issue, update parent tracker, post trajectory log. Return final summary.
14
14
 
15
- **Agent model policy**: `model: "haiku"`, `effort: low` (mechanical tier — label transitions, annotation posting, board updates). Fallback: `model: "sonnet"` if rate-limited. Feature gate: pass `effort` only on Claude Code >= 2.1.154.
15
+ **Agent model policy**: `effort: low` (mechanical tier — label transitions, annotation posting, board updates; this file is mechanical end-to-end, so a low effort level is safe here). Fallback: `model: "sonnet"` if rate-limited. Feature gate: pass `effort` only on Claude Code >= 2.1.154. **Note**: this file is dispatched via `Skill("work-on:close", ...)`, which does not support a `model` override — see `work-on.md` section "Model and Effort Tiering — What Actually Applies" for why a `model: "haiku"` claim here would not take effect. <!-- Corrected: forge#1827 -->
16
16
  **NEVER use plan mode (EnterPlanMode).**
17
17
 
18
18
  <!-- FORGE:SPEC_LOADED — work-on/close.md loaded and active. Agent is bound by this spec. -->
@@ -59,6 +59,77 @@ Extract from agent comments:
59
59
 
60
60
  ---
61
61
 
62
+ ## Phase C0.5: Close-Scope Invariant Assertions (MANDATORY)
63
+
64
+ Run before any close actions. Evaluates `close`-scope invariants declared in
65
+ `forge-invariants.yaml` via `bin/engine/invariants.mjs`. A failed assertion
66
+ logs the violated proposition by name and flags the anomaly in the trajectory
67
+ log — it does NOT abort the close phase (advisory enforcement: flag, then continue).
68
+
69
+ **Skip if**: `forge-invariants.yaml` is absent or `bin/engine/invariants.mjs`
70
+ is unavailable (e.g. fresh install before this file ships). Fail-open.
71
+
72
+ ```bash
73
+ # Read local run-log for this issue (absolute path matches engine run-log dir)
74
+ RUN_LOG_DIR="${HOME}/.forge/runs"
75
+ RUN_LOG_FILE="${RUN_LOG_DIR}/{NUMBER}.jsonl"
76
+
77
+ INVARIANT_ANOMALIES=""
78
+
79
+ if [ -f "${RUN_LOG_FILE}" ] && [ -f "$(dirname "$(which node)")/node" ] 2>/dev/null; then
80
+ # Check close-scope invariants via the evaluator
81
+ INVARIANT_RESULT=$(node -e "
82
+ import(new URL('file://$(pwd)/bin/engine/invariants.mjs'))
83
+ .then(m => {
84
+ const decls = m.loadInvariants('$(pwd)/forge-invariants.yaml');
85
+ const fs = require('fs');
86
+ let events = [];
87
+ try {
88
+ const lines = fs.readFileSync('${RUN_LOG_FILE}', 'utf-8').split('\n').filter(Boolean);
89
+ events = lines.flatMap(l => { try { return [JSON.parse(l)]; } catch { return []; } });
90
+ } catch {}
91
+ const results = m.assertCloseInvariants(decls, events);
92
+ const failed = results.filter(r => !r.ok);
93
+ if (failed.length) {
94
+ failed.forEach(r => process.stderr.write(m.formatViolation(r) + '\n'));
95
+ process.exit(1);
96
+ }
97
+ })
98
+ .catch(() => process.exit(0)); // fail-open on any error
99
+ " 2>&1) || INVARIANT_ANOMALIES="${INVARIANT_RESULT}"
100
+
101
+ if [ -n "$INVARIANT_ANOMALIES" ]; then
102
+ echo "CLOSE-SCOPE INVARIANT ANOMALY (flagging — close continues):"
103
+ echo "$INVARIANT_ANOMALIES"
104
+ # The anomaly will be recorded in the trajectory log Anomalies field.
105
+ # It does NOT block the close phase.
106
+ fi
107
+ fi
108
+
109
+ # Also check: issue must be in CLOSED state after close attempt.
110
+ # This check runs AFTER Phase C2 (ensure issue is closed). Set a sentinel
111
+ # here to be evaluated post-C2:
112
+ CLOSE_INVARIANT_ISSUE_CHECK=true
113
+ ```
114
+
115
+ **Post-C2 check** (evaluate after Phase C2 runs the `gh issue close` command):
116
+
117
+ ```bash
118
+ if [ "${CLOSE_INVARIANT_ISSUE_CHECK:-false}" = "true" ]; then
119
+ ISSUE_STATE=$(gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state' 2>/dev/null || echo "UNKNOWN")
120
+ if [ "$ISSUE_STATE" != "CLOSED" ]; then
121
+ INVARIANT_ANOMALIES="${INVARIANT_ANOMALIES:+$INVARIANT_ANOMALIES; }issue_closed_at_terminal: issue state is ${ISSUE_STATE} (not CLOSED) after close attempt"
122
+ echo "INVARIANT ANOMALY: issue_closed_at_terminal — issue is ${ISSUE_STATE}, not CLOSED"
123
+ # Flag but continue — trajectory Anomalies field will surface this.
124
+ fi
125
+ fi
126
+ ```
127
+
128
+ The `INVARIANT_ANOMALIES` variable is read in Phase C4.5 (trajectory post) and written to the **Anomalies** field.
129
+
130
+ ---
131
+
132
+
62
133
  ## Phase C1: Final Issue Body Update
63
134
 
64
135
  **Multi-phase guard**: Before checking off items, detect whether the issue has multiple phases. Only check off items belonging to the current completed phase — not all remaining items across future phases.
@@ -106,6 +177,194 @@ The `REMAINING_AFTER` variable is passed to Phase C2 to decide whether to close.
106
177
 
107
178
  ---
108
179
 
180
+ ## Phase C1.7: Module Dossier Append (MANDATORY when PR exists) <!-- Added: forge#1733 -->
181
+
182
+ **Goal**: After each merge that touches a module covered by `devdocs/modules/`, append a dated entry so future agents working on that module receive current institutional knowledge through the binding devdocs channel.
183
+
184
+ **This phase is non-blocking** — if the dossier write fails, log the reason and continue to Phase C1.5. Never stall close for dossier maintenance.
185
+
186
+ **Skip if**: `{PR_NUMBER}` is empty (investigation-only tasks) OR `{REPO_PATH}` is unset OR `devdocs/index.yaml` does not contain a `modules:` section OR no PR files match any module glob.
187
+
188
+ ### Step 1: Resolve affected files from FORGE:BUILDER comment
189
+
190
+ ```bash
191
+ # Read FORGE:BUILDER comment to get the list of changed files
192
+ BUILDER_COMMENT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
193
+ --jq '[.[] | select(.body | contains("FORGE:BUILDER"))] | last | .body // ""' 2>/dev/null || echo "")
194
+
195
+ # Extract file paths from the Changes section (lines starting with `- \`filepath\``)
196
+ CHANGED_FILES_RAW=$(echo "$BUILDER_COMMENT" \
197
+ | sed -n '/^### Changes/,/^###/p' \
198
+ | grep -oE '`[^`]+`' \
199
+ | tr -d '`' \
200
+ | grep -E '\.' \
201
+ | head -20)
202
+
203
+ # Fallback: try the FORGE:INVESTIGATOR affected files list
204
+ if [ -z "$CHANGED_FILES_RAW" ]; then
205
+ CHANGED_FILES_RAW=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
206
+ --jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR"))] | last | .body // ""' 2>/dev/null \
207
+ | sed -n '/### Affected Files/,/###/p' \
208
+ | grep -oE '`[^`]+`' \
209
+ | tr -d '`' \
210
+ | grep -E '\.' \
211
+ | head -20)
212
+ fi
213
+
214
+ if [ -z "$CHANGED_FILES_RAW" ]; then
215
+ echo "Phase C1.7: No changed files found from FORGE:BUILDER or FORGE:INVESTIGATOR — skipping dossier append"
216
+ # → continue to Phase C1.5
217
+ fi
218
+ ```
219
+
220
+ ### Step 2: Match against module globs and append entries
221
+
222
+ ```bash
223
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
224
+ DEVDOCS_REL=$(yq '.devdocs.path // "devdocs"' "$CONFIG_FILE" 2>/dev/null || echo "devdocs")
225
+ DEVDOCS_PATH="${REPO_PATH:-{REPO_PATH}}/${DEVDOCS_REL}"
226
+ INDEX_PATH="${DEVDOCS_PATH}/index.yaml"
227
+
228
+ if [ ! -f "$INDEX_PATH" ]; then
229
+ echo "Phase C1.7: ${INDEX_PATH} not found — skipping dossier append"
230
+ # → continue to Phase C1.5
231
+ fi
232
+
233
+ # Extract modules entries: "name|glob|path"
234
+ MODULE_ENTRIES=$(yq '.modules[]? | .name + "|" + .glob + "|" + .path' "$INDEX_PATH" 2>/dev/null || echo "")
235
+
236
+ if [ -z "$MODULE_ENTRIES" ]; then
237
+ echo "Phase C1.7: No modules[] section in index.yaml — skipping dossier append"
238
+ # → continue to Phase C1.5
239
+ fi
240
+
241
+ DOSSIER_TIMESTAMP=$(date -u +"%Y-%m-%d")
242
+ DOSSIER_UPDATED_MODULES=""
243
+
244
+ # Iterate module entries; for each: check if any changed file matches the glob
245
+ while IFS='|' read -r MOD_NAME MOD_GLOB MOD_PATH; do
246
+ [ -z "$MOD_GLOB" ] || [ -z "$MOD_PATH" ] && continue
247
+ DOSSIER_ABS="${DEVDOCS_PATH}/${MOD_PATH}"
248
+
249
+ MATCHED=0
250
+ # Iterate changed files using while read — not bare for-in (IFS word-split guard per c39758d)
251
+ while IFS= read -r af; do
252
+ [ -z "$af" ] && continue
253
+ AF_BASENAME=$(basename "$af")
254
+ case "$AF_BASENAME" in
255
+ $MOD_GLOB) MATCHED=1; break ;;
256
+ esac
257
+ case "$af" in
258
+ $MOD_GLOB) MATCHED=1; break ;;
259
+ esac
260
+ done <<< "$CHANGED_FILES_RAW"
261
+
262
+ if [ "$MATCHED" -eq 0 ]; then
263
+ continue
264
+ fi
265
+
266
+ echo "Phase C1.7: Module '${MOD_NAME}' matched (glob '${MOD_GLOB}') — appending entry to ${MOD_PATH}"
267
+
268
+ # Build entry text
269
+ # One-line summary from the PR title + issue number
270
+ PR_TITLE=$(gh pr view {PR_NUMBER} {GH_FLAG} --json title --jq '.title' 2>/dev/null || echo "untitled")
271
+ DOSSIER_ENTRY="## Entry ${DOSSIER_TIMESTAMP} — ${PR_TITLE} (#{NUMBER})
272
+
273
+ PR #{PR_NUMBER} touched \`${MOD_NAME}\`. See FORGE:BUILDER comment on issue #{NUMBER} for full change list.
274
+ Key gotcha recorded: (update this entry by editing \`${MOD_PATH}\` in a follow-up PR if the change revealed a new failure mode).
275
+ Cite: #${NUMBER} / PR #{PR_NUMBER}."
276
+
277
+ # Ensure dossier file exists (create skeleton if missing — allows operator to create
278
+ # a new module entry in index.yaml before the dossier file is seeded)
279
+ if [ ! -f "$DOSSIER_ABS" ]; then
280
+ mkdir -p "$(dirname "$DOSSIER_ABS")"
281
+ cat > "$DOSSIER_ABS" <<DOSSIER_INIT_EOF
282
+ ---
283
+ module: ${MOD_NAME}
284
+ glob: "${MOD_GLOB}"
285
+ authority: required
286
+ token_cost: 200
287
+ last_compacted: "${DOSSIER_TIMESTAMP}"
288
+ ---
289
+
290
+ # Module Dossier: ${MOD_NAME}
291
+
292
+ Rolling per-module knowledge log. Each entry is 3–5 lines with a citation.
293
+ Hard cap: 150 lines. Entries are appended by close.md Phase C1.7 after each
294
+ PR that touches this module. When the file exceeds 150 lines, oldest entries
295
+ are compacted into the Summary block (LLM compaction, in-run).
296
+
297
+ ## Summary
298
+
299
+ _No compacted history yet. Dossier was auto-created on ${DOSSIER_TIMESTAMP} by close.md Phase C1.7._
300
+ DOSSIER_INIT_EOF
301
+ echo "Phase C1.7: Created new dossier skeleton at ${DOSSIER_ABS}"
302
+ fi
303
+
304
+ # Append entry (avoid subshell — use file redirect directly)
305
+ printf '\n%s\n' "$DOSSIER_ENTRY" >> "$DOSSIER_ABS"
306
+
307
+ # Compact if over 150 lines
308
+ DOSSIER_LINE_COUNT=$(wc -l < "$DOSSIER_ABS" 2>/dev/null || echo 0)
309
+ if [ "$DOSSIER_LINE_COUNT" -gt 150 ]; then
310
+ echo "Phase C1.7: Dossier ${MOD_PATH} has ${DOSSIER_LINE_COUNT} lines (>150) — compacting oldest entries"
311
+ # LLM compaction: read the dossier, summarize oldest Entry blocks into the ## Summary
312
+ # section, keeping the most recent 3 entries intact.
313
+ # Implementation note: this is prose-instruction compaction (LLM reads the file and
314
+ # rewrites it). The compacted file must preserve the frontmatter and ## Summary block;
315
+ # it may replace old ## Entry blocks with a single "## Archived Summary (compacted)"
316
+ # block. After compaction, update frontmatter last_compacted to today's date.
317
+ # The compacted file MUST be ≤ 150 lines. If compaction fails (e.g. LLM context
318
+ # overflow), log a warning and leave the file as-is — never delete entries silently.
319
+ echo "COMPACT INSTRUCTION: Read ${DOSSIER_ABS}. Keep the frontmatter (lines between ---), keep the ## Summary section, keep the 3 most recent ## Entry blocks, and replace all older ## Entry blocks with a single '## Archived Summary (compacted — ${DOSSIER_TIMESTAMP})' block containing a 5–8 line distillation of the key failure modes, gotchas, and citations from the archived entries. Write the result back to ${DOSSIER_ABS}. The output MUST be ≤ 150 lines. Update frontmatter last_compacted to ${DOSSIER_TIMESTAMP}."
320
+ fi
321
+
322
+ DOSSIER_UPDATED_MODULES="${DOSSIER_UPDATED_MODULES} ${MOD_NAME}"
323
+
324
+ done <<< "$MODULE_ENTRIES"
325
+ ```
326
+
327
+ ### Step 3: Commit dossier changes and post annotation
328
+
329
+ ```bash
330
+ if [ -n "$DOSSIER_UPDATED_MODULES" ]; then
331
+ # Commit the updated dossier files
332
+ cd "{REPO_PATH}"
333
+ CHANGED_DOSSIER_FILES=$(echo "$DOSSIER_UPDATED_MODULES" | tr ' ' '\n' | while IFS= read -r mod; do
334
+ yq ".modules[]? | select(.name == \"${mod}\") | \"${DEVDOCS_REL}/\" + .path" "$INDEX_PATH" 2>/dev/null
335
+ done | grep -v '^$')
336
+
337
+ if [ -n "$CHANGED_DOSSIER_FILES" ]; then
338
+ # CHANGED_DOSSIER_FILES is newline-separated — iterate so paths containing
339
+ # spaces are staged individually rather than word-split by the shell.
340
+ while IFS= read -r dossier_file; do
341
+ [ -n "$dossier_file" ] || continue
342
+ git -C "{REPO_PATH}" add "$dossier_file" 2>/dev/null || true
343
+ done <<< "$CHANGED_DOSSIER_FILES"
344
+ # Only commit if there are staged changes (new or modified dossier files)
345
+ if ! git -C "{REPO_PATH}" diff --cached --quiet 2>/dev/null; then
346
+ git -C "{REPO_PATH}" commit -s -m "docs(dossier): append entry for PR #{PR_NUMBER} (#${NUMBER})" 2>/dev/null || true
347
+ echo "Phase C1.7: Dossier commit created for modules:${DOSSIER_UPDATED_MODULES}"
348
+ else
349
+ echo "Phase C1.7: No staged dossier changes — skipping commit"
350
+ fi
351
+ fi
352
+
353
+ # Post annotation on the issue
354
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DOSSIER_UPDATED -->
355
+ Module dossier(s) updated:${DOSSIER_UPDATED_MODULES}
356
+
357
+ Entries appended to \`devdocs/modules/\` after PR #{PR_NUMBER} merged. Future agents working
358
+ on these modules will receive the updated knowledge through the devdocs channel (context.md Phase C-1).
359
+
360
+ <!-- FORGE:DOSSIER_UPDATED:COMPLETE -->" 2>/dev/null || true
361
+ else
362
+ echo "Phase C1.7: No module dossiers matched changed files — skipping"
363
+ fi
364
+ ```
365
+
366
+ ---
367
+
109
368
  ## Phase C1.5: Project Board Update (Status=Done, Workflow=Merged)
110
369
 
111
370
  Update the project board to reflect the merged state. This replaces the old Phase 5E project board update that existed before the modular refactor.
@@ -217,9 +476,10 @@ gh issue edit {NUMBER} {GH_FLAG} \
217
476
 
218
477
  **Skip if**: Issue body does NOT contain a parent issue reference (e.g. `Part of #NNN`) or the issue has no parent in its milestone tracker.
219
478
 
220
- Detect parent reference:
479
+ Detect parent reference. Markdown emphasis markers (`**bold**`, `__bold__`, `*italic*`) are stripped before matching, since sub-issue bodies commonly render the label as `**Parent**: #NNN` and the bare label alternation below would otherwise fail to match past the emphasis characters:
221
480
  ```bash
222
481
  PARENT_REF=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' \
482
+ | sed -E 's/[*_]+//g' \
223
483
  | grep -oP '(?i)(part of|spawned from|sub-issue of|parent issue[:]?|parent[:])\s*#\K\d+' \
224
484
  | head -1)
225
485
  ```
@@ -404,7 +664,58 @@ CARD_JSON=$(jq -nc \
404
664
 
405
665
  ## Phase C5: Trajectory Log (MANDATORY)
406
666
 
407
- Post the `<!-- FORGE:TRAJECTORY -->` comment as the final pipeline record:
667
+ **CODEC PATH (forge#1727)**: Post the `<!-- FORGE:TRAJECTORY -->` comment via the protocol codec — do NOT hand-roll the opening tag. Use `forge-annotation.sh write TRAJECTORY --field ...` or `node packages/protocol/src/cli.js emit TRAJECTORY` to produce the opening tag. The codec handles any field escaping automatically.
668
+
669
+ ```bash
670
+ # Codec produces the opening <!-- FORGE:TRAJECTORY --> tag
671
+ TRAJECTORY_HEADER=$(node packages/protocol/src/cli.js emit TRAJECTORY)
672
+ # $TRAJECTORY_HEADER = "<!-- FORGE:TRAJECTORY -->"
673
+ # Append the Markdown body sections, then post via gh issue comment.
674
+ ```
675
+
676
+ Post the `<!-- FORGE:TRAJECTORY -->` comment as the final pipeline record.
677
+
678
+ **Prior delta computation** — read cost-prior for this issue's task_type × module before posting (forge#1743):
679
+
680
+ ```bash
681
+ # Compute actual vs prior cost delta for self-correction of cost priors
682
+ COST_PRIORS_PATH="${HOME}/.forge/index/cost-priors.json"
683
+ ACTUAL_TOTAL_USD=""
684
+ PRIOR_EST_USD=""
685
+ COST_DELTA_NOTE=""
686
+
687
+ # Read actual spend from FORGE:BUILDER / FORGE:TRAJECTORY best-effort telemetry
688
+ # (same extraction used in work-on.md Phase 7C DECISION_RECORD cost block)
689
+ ACTUAL_TOTAL_USD=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
690
+ --jq '[.[] | select(.body | contains("FORGE:BUILDER")) | .body] | last // ""' 2>/dev/null \
691
+ | grep -oP '(?<=cost_usd: )\S+' | head -1 || echo "")
692
+
693
+ if [ -n "$ACTUAL_TOTAL_USD" ] && [ -f "$COST_PRIORS_PATH" ]; then
694
+ # Derive task_type:module key (same logic as Step 3E.5)
695
+ TASK_TYPE=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
696
+ --jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body] | last // ""' 2>/dev/null \
697
+ | grep -oP '(?<=\*\*Task Type\*\*: )\S+' | head -1 | tr '[:upper:]' '[:lower:]' | tr ' ' '-' || echo 'unknown')
698
+ PRIMARY_FILE=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
699
+ --jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body] | last // ""' 2>/dev/null \
700
+ | grep -oP '`[^`]+\.(py|mjs|ts|md|sh|yaml|yml)`' | tr -d '`' | head -1 || echo '')
701
+ MODULE=$(basename "${PRIMARY_FILE:-_unknown}" | sed 's/\.[^.]*$//' | tr '[:upper:]' '[:lower:]')
702
+ [ -z "$MODULE" ] && MODULE="_unknown"
703
+ PRIOR_KEY="${TASK_TYPE}:${MODULE}"
704
+
705
+ PRIOR_EST_USD=$(jq -r --arg k "$PRIOR_KEY" '.priors[$k].mean // empty' "$COST_PRIORS_PATH" 2>/dev/null || echo '')
706
+
707
+ if [ -n "$PRIOR_EST_USD" ]; then
708
+ DELTA=$(echo "scale=4; $ACTUAL_TOTAL_USD - $PRIOR_EST_USD" | bc 2>/dev/null || echo "?")
709
+ COST_DELTA_NOTE="actual=\$${ACTUAL_TOTAL_USD} prior=\$${PRIOR_EST_USD} delta=${DELTA} key=${PRIOR_KEY}"
710
+ else
711
+ COST_DELTA_NOTE="actual=\$${ACTUAL_TOTAL_USD} prior=absent (no prior for key ${PRIOR_KEY})"
712
+ fi
713
+ elif [ -n "$ACTUAL_TOTAL_USD" ]; then
714
+ COST_DELTA_NOTE="actual=\$${ACTUAL_TOTAL_USD} prior=index-absent"
715
+ else
716
+ COST_DELTA_NOTE="no-telemetry"
717
+ fi
718
+ ```
408
719
 
409
720
  ```bash
410
721
  gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
@@ -426,19 +737,39 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
426
737
  - Decomposition skipped: {DECOMPOSE_REASON}
427
738
  - PR merged to: \`{PR_BASE}\` (feature lane, milestone branch)
428
739
 
740
+ **Cost (economic scheduling)**: ${COST_DELTA_NOTE}
741
+
429
742
  **Anomalies**: None
430
743
 
431
744
  **Pipeline completed**: {TIMESTAMP}
432
745
 
433
- <!-- FORGE:CARD ${CARD_JSON} -->"
746
+ $(node packages/protocol/src/cli.js emit CARD --b64 \
747
+ --field issue={NUMBER} \
748
+ --field status={CARD_STATUS} \
749
+ --field pipeline="${PIPELINE_LINE}" \
750
+ --field pr={PR_NUMBER} \
751
+ --field pr_target="${PR_TARGET}" \
752
+ --field commits="${COMMITS}" \
753
+ --field additions="${ADDITIONS}" \
754
+ --field deletions="${DELETIONS}" \
755
+ --field review="${REVIEW_SUMMARY}" \
756
+ --field elapsed="${ELAPSED_SECS:-0}")"
434
757
  ```
435
758
 
436
- The `<!-- FORGE:CARD {...} -->` block carries the machine-readable summary computed in
437
- Phase C4.5c. It is wrapped in an HTML comment so it stays hidden in GitHub's rendered view
438
- (keeping the trajectory comment clean) while remaining greppable in the raw body for platform
439
- consumption — `/orchestrate` reads it to build per-issue cards. This block is **additive**:
440
- all existing `FORGE:TRAJECTORY` consumers select via `contains("FORGE:TRAJECTORY")` and parse
441
- the markdown table, so the embedded JSON does not affect them.
759
+ The `<!-- FORGE:CARD: v1 sha:... b64:... -->` line carries the machine-readable summary computed in
760
+ Phase C4.5c, encoded as Base64url (design decision 2026-07-08: encoding beats escaping — the
761
+ Base64url alphabet cannot contain HTML comment delimiters by construction). It is wrapped in
762
+ the inline-value annotation form `<!-- FORGE:CARD: ... -->` so parse() extracts the encoded
763
+ payload. Platform consumers (e.g., `/orchestrate`) decode via `node packages/protocol/src/cli.js
764
+ parse --type CARD --field status`. This block is **additive**: all existing `FORGE:TRAJECTORY`
765
+ consumers select via `contains("FORGE:TRAJECTORY")` and parse the markdown table, so the
766
+ embedded CARD line does not affect them.
767
+
768
+ **CODEC PATH (forge#1727)**: The `$(node packages/protocol/src/cli.js emit CARD --b64 ...)` call
769
+ above replaces the previous `<!-- FORGE:CARD ${CARD_JSON} -->` inline-JSON form. The Base64url
770
+ form is safe against all HTML comment injection vectors and includes a sha8 integrity prefix for
771
+ truncation detection. Consumers that parsed the old inline-JSON form must migrate to the codec
772
+ parse path: `echo '...' | node packages/protocol/src/cli.js parse --type CARD --field <key>`.
442
773
 
443
774
  Where:
444
775
  - `{PARENT_STATUS}` = `⏭ Skipped` (if no parent) or `✅ Complete` (if parent updated)
@@ -448,6 +779,29 @@ Where:
448
779
 
449
780
  ---
450
781
 
782
+ ## Phase C5.1: Knowledge Index + Cost Prior Update (forge#1743) <!-- Added: forge#1743 -->
783
+
784
+ **Goal**: Re-index this issue's knowledge cards and regenerate cost-priors.json so that economic scheduling (orchestrate Step 3E.5) has up-to-date data for future runs. The actual-vs-prior delta recorded in the TRAJECTORY above is the write side of the self-correction loop — this step performs the read/recompute.
785
+
786
+ **This phase is non-blocking** — if the indexer fails, log the reason and continue to Phase C5.2. Never stall close for the cost-prior update.
787
+
788
+ **Skip if**: Terminal state is `INVALID` (no useful cost data from invalid issues).
789
+
790
+ ```bash
791
+ # Re-index this issue and regenerate cost priors — non-blocking
792
+ INDEXER_PATH=$(dirname "$(realpath "$0" 2>/dev/null || echo '.')")/../../scripts/build-knowledge-index.mjs
793
+ if node --version >/dev/null 2>&1 && [ -f "$INDEXER_PATH" ]; then
794
+ echo "[cost-prior] Re-indexing issue #${NUMBER} and regenerating cost priors..."
795
+ node "$INDEXER_PATH" --issue {NUMBER} --no-mirror 2>&1 | tail -5 \
796
+ && echo "[cost-prior] Cost priors updated" \
797
+ || echo "WARNING: Cost prior update failed — continuing (non-blocking)"
798
+ else
799
+ echo "[cost-prior] Indexer not available — skipping cost prior update (non-blocking)"
800
+ fi
801
+ ```
802
+
803
+ ---
804
+
451
805
  ## Phase C5.2: Memory Index Update <!-- Added: forge#1316 -->
452
806
 
453
807
  **Goal**: Append this run's learnings to the per-repo memory index so future `investigate` runs can retrieve relevant priors. This is the write side of the compounding intelligence loop.
@@ -571,6 +925,266 @@ fi
571
925
 
572
926
  ---
573
927
 
928
+ ## Phase C5.3: Knowledge Ledger Index <!-- Added: forge#1732 -->
929
+
930
+ **Goal**: Index the just-closed issue into the Forge Ledger so future context phases can retrieve
931
+ its knowledge cards by file path or symbol without making live GitHub API calls.
932
+
933
+ **This phase is non-blocking** — if the indexer fails, log the reason and continue to Phase C5.5.
934
+ Never stall close for ledger indexing.
935
+
936
+ **Skip if**: Terminal state is `INVALID` (no confirmed findings to index) OR a `<!-- FORGE:LEDGER_INDEXED -->` comment already exists on the issue (idempotency guard).
937
+
938
+ **Requires**: `scripts/build-knowledge-index.mjs` present in the repository root. If absent, skip
939
+ with a warning — the feature may not be installed on this version.
940
+
941
+ ### Step 1: Idempotency check
942
+
943
+ ```bash
944
+ LEDGER_INDEXED=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
945
+ --jq '[.[] | select(.body | contains("FORGE:LEDGER_INDEXED"))] | length > 0' 2>/dev/null || echo "false")
946
+ ```
947
+
948
+ **Skip to Phase C5.5 if `$LEDGER_INDEXED == "true"`**.
949
+
950
+ ### Step 2: Run incremental indexer
951
+
952
+ Resolve the indexer script path relative to the repository root:
953
+
954
+ ```bash
955
+ INDEXER_PATH="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}/scripts/build-knowledge-index.mjs"
956
+
957
+ if [ ! -f "$INDEXER_PATH" ]; then
958
+ echo "[LEDGER] scripts/build-knowledge-index.mjs not found — skipping Phase C5.3"
959
+ echo "[LEDGER] Install: update ForgeDock to a version that ships this file"
960
+ else
961
+ # Run incremental indexer for this issue only — ~2 API calls
962
+ LEDGER_EXIT=0
963
+ LEDGER_OUTPUT=$(node "$INDEXER_PATH" \
964
+ --issue {NUMBER} \
965
+ --repo {GH_REPO} \
966
+ --no-mirror \
967
+ 2>&1) || LEDGER_EXIT=$?
968
+
969
+ if [ $LEDGER_EXIT -eq 0 ]; then
970
+ echo "[LEDGER] Issue #{NUMBER} indexed into Forge Ledger"
971
+
972
+ # Mirror update: run separately after single-issue index so mirror has up-to-date postings
973
+ node "$INDEXER_PATH" --issue {NUMBER} --repo {GH_REPO} \
974
+ 2>/dev/null || true # Non-blocking: mirror failure does not affect local index
975
+
976
+ # Post audit annotation (idempotency guard for future close runs)
977
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:LEDGER_INDEXED -->
978
+ Issue #{NUMBER} has been indexed into the Forge Ledger.
979
+
980
+ Knowledge cards extracted from FORGE:INVESTIGATOR and FORGE:TRAJECTORY annotations are now
981
+ queryable via \`forge recall\` (exact file/symbol lookup or free-text BM25 search).
982
+
983
+ \`\`\`
984
+ forge recall --file {PRIMARY_AFFECTED_FILE} --json
985
+ \`\`\`
986
+
987
+ <!-- FORGE:LEDGER_INDEXED:COMPLETE -->" 2>/dev/null || true
988
+
989
+ else
990
+ echo "[LEDGER] WARNING: Indexer exited with code ${LEDGER_EXIT} — continuing"
991
+ echo "[LEDGER] Output: ${LEDGER_OUTPUT}"
992
+ fi
993
+ fi
994
+ ```
995
+
996
+ ### Watermark semantics
997
+
998
+ The indexer's watermark is `max(issue.updated_at)` across all indexed issues. Indexing a
999
+ single issue via `--issue` advances the watermark only if this issue's `updated_at` is newer
1000
+ than the stored watermark — ensuring the next full incremental run starts from the right
1001
+ position and does not re-scan already-indexed history.
1002
+
1003
+ ---
1004
+
1005
+ ## Phase C5.4: Auto-ADR Extraction from TRAJECTORY Decisions <!-- Added: forge#1737 -->
1006
+
1007
+ **Goal**: Promote tradeoff-shaped Decisions bullets from the FORGE:TRAJECTORY comment into
1008
+ human-readable, git-tracked ADR markdown files at `devdocs/decisions/NNN-{slug}.md`. Architect
1009
+ plans on future runs will load matching ADRs as constraints before writing any code.
1010
+
1011
+ **This phase is non-blocking** — if ADR extraction, file write, or commit fails at any step,
1012
+ log the reason and continue to Phase C5.5. Never stall close for ADR generation.
1013
+
1014
+ **Skip if**: Terminal state is `INVALID` (no useful decisions to record) OR a
1015
+ `<!-- FORGE:ADR_EXTRACTED -->` comment already exists on the issue (idempotency guard) OR
1016
+ `devdocs/decisions/` directory does not exist in the repository root (feature not installed).
1017
+
1018
+ ### What is a "tradeoff-shaped" decision?
1019
+
1020
+ A Decisions bullet is extractable if it contains BOTH:
1021
+ 1. A **choice indicator**: words like `chose`, `use`, `prefer`, `process substitution`, `over`,
1022
+ `instead`, `rather than`, `not X`
1023
+ 2. A **rationale connector**: `because`, `since`, `so`, `to avoid`, `prevents`, `due to`
1024
+
1025
+ Generic bullets like `- Decomposition skipped: single-concern change` do NOT match and are ignored.
1026
+
1027
+ ### Step 1: Idempotency check
1028
+
1029
+ ```bash
1030
+ ADR_EXTRACTED=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1031
+ --jq '[.[] | select(.body | contains("FORGE:ADR_EXTRACTED"))] | length > 0' 2>/dev/null || echo "false")
1032
+ ```
1033
+
1034
+ **Skip to Phase C5.5 if `$ADR_EXTRACTED == "true"`**.
1035
+
1036
+ ### Step 2: Extract TRAJECTORY Decisions
1037
+
1038
+ ```bash
1039
+ # Read the TRAJECTORY comment posted in Phase C5
1040
+ TRAJECTORY_BODY=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1041
+ --jq '[.[] | select(.body | contains("FORGE:TRAJECTORY"))] | last | .body // ""' 2>/dev/null || echo '')
1042
+
1043
+ # Extract the Decisions section (lines between **Decisions**: and **Anomalies**:)
1044
+ DECISIONS_RAW=$(echo "$TRAJECTORY_BODY" \
1045
+ | awk '/^\*\*Decisions\*\*:/{found=1; next} /^\*\*Anomalies\*\*:/{found=0} found{print}' \
1046
+ | grep -v '^\s*$' \
1047
+ | head -20) # Cap at 20 bullets to prevent runaway parsing
1048
+
1049
+ if [ -z "$DECISIONS_RAW" ] || echo "$DECISIONS_RAW" | grep -qi "^- None$\|^None$"; then
1050
+ echo "[ADR] No Decisions section found in TRAJECTORY — skipping ADR extraction"
1051
+ ADR_FILES_WRITTEN=0
1052
+ fi
1053
+ ```
1054
+
1055
+ ### Step 3: Parse and filter for tradeoff shape
1056
+
1057
+ ```bash
1058
+ ADR_FILES_WRITTEN=0
1059
+ DECISIONS_DIR="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}/devdocs/decisions"
1060
+
1061
+ if [ -z "$DECISIONS_RAW" ] || ! [ -d "$DECISIONS_DIR" ]; then
1062
+ echo "[ADR] Skipping: no decisions or devdocs/decisions/ absent"
1063
+ else
1064
+ # Get commit SHA for anchor citation
1065
+ COMMIT_SHA=$(git -C "${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}" \
1066
+ rev-parse HEAD 2>/dev/null | head -c 12 || echo "unknown")
1067
+
1068
+ while IFS= read -r bullet; do
1069
+ # Strip leading "- " or "* "
1070
+ text=$(echo "$bullet" | sed 's/^[-*]\s*//')
1071
+ [ -z "$text" ] && continue
1072
+
1073
+ # Tradeoff shape filter: must have a choice indicator + rationale connector
1074
+ CHOICE_MATCH=$(echo "$text" | grep -iE 'chose|use |prefer|instead|rather than|over |not [a-z]|process substitution|branched from|avoided' || true)
1075
+ RATIONALE_MATCH=$(echo "$text" | grep -iE 'because|since[[:space:]]|so[[:space:]]|to avoid|prevents|due to' || true)
1076
+
1077
+ if [ -z "$CHOICE_MATCH" ] || [ -z "$RATIONALE_MATCH" ]; then
1078
+ echo "[ADR] Skipped (not a tradeoff): $text"
1079
+ continue
1080
+ fi
1081
+
1082
+ # Extract anchor: first backtick-quoted path-like string in the decision text
1083
+ ANCHOR_PATH=$(echo "$text" | grep -oE '`[a-zA-Z][^`]*/[^`]+`' | head -1 | tr -d '`' || true)
1084
+
1085
+ # Build slug from first 6 words of decision (lowercase, hyphenated)
1086
+ SLUG=$(echo "$text" | tr '[:upper:]' '[:lower:]' | \
1087
+ sed 's/[^a-z0-9 ]/ /g' | tr -s ' ' '-' | \
1088
+ cut -c1-40 | sed 's/-$//')
1089
+ ADR_FILENAME="${NUMBER}-${SLUG}.md"
1090
+ ADR_PATH="$DECISIONS_DIR/$ADR_FILENAME"
1091
+
1092
+ # Idempotency: skip if file already exists
1093
+ if [ -f "$ADR_PATH" ]; then
1094
+ echo "[ADR] Already exists — skipping: $ADR_FILENAME"
1095
+ ADR_FILES_WRITTEN=$((ADR_FILES_WRITTEN + 1))
1096
+ continue
1097
+ fi
1098
+
1099
+ # Write ADR file
1100
+ PR_REF="${PR_NUMBER:-unknown}"
1101
+ cat > "$ADR_PATH" <<ADR_EOF
1102
+ ---
1103
+ issue: {NUMBER}
1104
+ pr: ${PR_REF}
1105
+ commit: ${COMMIT_SHA}
1106
+ status: fresh
1107
+ anchor: ${ANCHOR_PATH:-unknown}
1108
+ created: $(date -u +%Y-%m-%d)
1109
+ ---
1110
+
1111
+ # ADR — ${text}
1112
+
1113
+ ## Decision
1114
+
1115
+ ${text}
1116
+
1117
+ ## Context
1118
+
1119
+ Auto-extracted from FORGE:TRAJECTORY Decisions section on issue #{NUMBER}.
1120
+
1121
+ **Citations**:
1122
+ - Issue: https://github.com/{GH_REPO}/issues/{NUMBER}
1123
+ - PR: https://github.com/{GH_REPO}/pull/${PR_REF}
1124
+ - Commit: ${COMMIT_SHA}
1125
+ - Anchor: \`${ANCHOR_PATH:-no file anchor found}\`
1126
+
1127
+ ## Status
1128
+
1129
+ \`fresh\` — anchor is active. Architect plans on future runs will inject this ADR as a constraint
1130
+ when the anchor path overlaps the contract files.
1131
+
1132
+ Set \`status: needs-review\` manually (or the staleness pass in \`build-knowledge-index.mjs\` will
1133
+ flip it automatically) when the anchored code region no longer exists.
1134
+ ADR_EOF
1135
+
1136
+ echo "[ADR] Written: $ADR_FILENAME"
1137
+ ADR_FILES_WRITTEN=$((ADR_FILES_WRITTEN + 1))
1138
+ done <<< "$DECISIONS_RAW"
1139
+ fi
1140
+ ```
1141
+
1142
+ ### Step 4: Commit and push ADR files (non-blocking)
1143
+
1144
+ ```bash
1145
+ if [ "$ADR_FILES_WRITTEN" -gt 0 ] && [ -n "{WORKTREE_PATH}" ] && [ -d "{WORKTREE_PATH}" ]; then
1146
+ # Commit ADR files in the worktree so they ride the existing PR
1147
+ (
1148
+ cd "{WORKTREE_PATH}"
1149
+ # Stage only the new/updated ADR files (not the whole tree)
1150
+ git add devdocs/decisions/*.md 2>/dev/null || true
1151
+ # Check if there's anything to commit
1152
+ if ! git diff --cached --quiet 2>/dev/null; then
1153
+ git commit -s -m "docs(decisions): auto-ADRs from TRAJECTORY decisions (#{NUMBER})" \
1154
+ --no-verify 2>/dev/null && echo "[ADR] Committed ${ADR_FILES_WRITTEN} ADR file(s)" || \
1155
+ echo "[ADR] WARNING: commit failed — ADR files on disk but not in git"
1156
+ else
1157
+ echo "[ADR] No staged changes — ADR files may already be committed"
1158
+ fi
1159
+ ) || echo "[ADR] WARNING: worktree operations failed — ADR files written but not committed"
1160
+ else
1161
+ if [ "$ADR_FILES_WRITTEN" -gt 0 ]; then
1162
+ echo "[ADR] No worktree available — ADR files written to repo root devdocs/decisions/ only"
1163
+ echo "[ADR] Manually commit devdocs/decisions/*.md if needed"
1164
+ fi
1165
+ fi
1166
+ ```
1167
+
1168
+ ### Step 5: Post audit annotation
1169
+
1170
+ ```bash
1171
+ if [ "${ADR_FILES_WRITTEN:-0}" -gt 0 ]; then
1172
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ADR_EXTRACTED -->
1173
+ ${ADR_FILES_WRITTEN} ADR file(s) auto-extracted from TRAJECTORY decisions and written to \`devdocs/decisions/\`.
1174
+
1175
+ Future architect runs will load matching ADRs as constraints when anchor paths overlap contract files.
1176
+
1177
+ ADRs are human-editable — update or remove them as the codebase evolves. The staleness pass in
1178
+ \`build-knowledge-index.mjs\` automatically flips \`status: needs-review\` when an anchor is dead.
1179
+
1180
+ <!-- FORGE:ADR_EXTRACTED:COMPLETE -->" 2>/dev/null || true
1181
+ else
1182
+ echo "[ADR] No tradeoff-shaped decisions found — no ADR files written"
1183
+ fi
1184
+ ```
1185
+
1186
+ ---
1187
+
574
1188
  ## Phase C5.5: Graph Decision Record (MANDATORY when PR exists)
575
1189
 
576
1190
  **Skip if**: `{PR_NUMBER}` is empty OR `<!-- FORGE:DECISION_RECORD -->` already posted on the PR.