forgedock 1.1.7 → 1.1.8

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 (37) hide show
  1. package/bin/engine/invariants.mjs +361 -0
  2. package/bin/hooks/pre-tool-use.mjs +182 -0
  3. package/bin/labels.json +10 -0
  4. package/bin/recall.mjs +540 -0
  5. package/commands/autopilot.md +957 -76
  6. package/commands/cleanup.md +52 -3
  7. package/commands/optimize.md +585 -0
  8. package/commands/orchestrate/phase-1-resolve.md +35 -25
  9. package/commands/orchestrate/phase-3-dependency.md +465 -34
  10. package/commands/orchestrate/phase-4-execution.md +447 -28
  11. package/commands/orchestrate/phase-6-report.md +198 -9
  12. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  13. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  14. package/commands/quality-gate.md +417 -0
  15. package/commands/review-pr.md +391 -13
  16. package/commands/spec-doctor.md +429 -0
  17. package/commands/validate.md +52 -0
  18. package/commands/work-on/build/architect.md +77 -0
  19. package/commands/work-on/build/context.md +357 -22
  20. package/commands/work-on/build/implement.md +2 -0
  21. package/commands/work-on/build/validate.md +28 -0
  22. package/commands/work-on/build.md +44 -0
  23. package/commands/work-on/close.md +621 -8
  24. package/commands/work-on/investigate.md +221 -0
  25. package/commands/work-on.md +15 -14
  26. package/docs/FORGE-PROTOCOL.md +101 -0
  27. package/docs/spec/forge-protocol-v1.md +97 -5
  28. package/docs/spec/label-state-machine.md +6 -1
  29. package/package.json +2 -2
  30. package/packages/protocol/src/cli.js +374 -84
  31. package/packages/protocol/src/types.js +75 -1
  32. package/scripts/build-knowledge-index.mjs +1761 -0
  33. package/scripts/calibration.mjs +996 -0
  34. package/scripts/danger-zones.mjs +782 -0
  35. package/scripts/doctor-pipeline-state.sh +88 -2
  36. package/scripts/select-fix-targets.sh +234 -0
  37. package/scripts/transition-label.sh +157 -6
@@ -90,6 +90,171 @@ Print these blocks to stdout before Phase 1A begins. During Phase 1B (step 3 —
90
90
 
91
91
  ---
92
92
 
93
+ ## Phase 0.6: Forge Ledger Pre-Recall <!-- Added: forge#1740 -->
94
+
95
+ **Goal**: Query the Forge Ledger knowledge index for prior cards matching this issue's title terms, affected files, and symbols — **before reading any code**. Inject above-threshold results with explicit delta-verification framing so the investigator builds on prior knowledge rather than re-deriving it.
96
+
97
+ **This phase is non-blocking** — if the index is absent, empty, or returns no match above threshold, log a single line and proceed to Phase 1A. Never stall the pipeline for recall.
98
+
99
+ **Relationship to Phase 0.5**: Phase 0.5 retrieves structured priors from the Gist-based FORGE:MEMORY_INDEX (coarse keyword scoring over MEMORY_ENTRY lines). Phase 0.6 queries the Forge Ledger index (BM25 over structured knowledge cards, indexed by `build-knowledge-index.mjs`). Both are complementary and both run — neither replaces the other.
100
+
101
+ ### Step 1: Probe for index
102
+
103
+ ```bash
104
+ RECALL_PATH="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}/bin/recall.mjs"
105
+ LEDGER_AVAILABLE=0
106
+
107
+ if [ -f "$RECALL_PATH" ]; then
108
+ # Quick probe: does the index exist and have at least one card?
109
+ PROBE=$(node "$RECALL_PATH" --doctor 2>/dev/null | grep "^Total cards:" | grep -v "^Total cards: 0" || true)
110
+ [ -n "$PROBE" ] && LEDGER_AVAILABLE=1
111
+ fi
112
+
113
+ if [ "$LEDGER_AVAILABLE" -eq 0 ]; then
114
+ echo "[recall] Forge Ledger index absent or empty — skipping Phase 0.6, proceeding to Phase 1A"
115
+ # → Continue to Phase 1A
116
+ fi
117
+ ```
118
+
119
+ ### Step 2: Build combined query (title terms + symbols + affected files)
120
+
121
+ Extract query components from the issue body and title. The recall CLI accepts free text (BM25 ranked) plus repeated `--file` flags (exact-match boost):
122
+
123
+ ```bash
124
+ # Extract title keywords: lowercase, remove stop words, keep noun/verb tokens (≥4 chars)
125
+ ISSUE_TITLE=$(gh issue view {NUMBER} {GH_FLAG} --json title --jq '.title' 2>/dev/null || echo '')
126
+ TITLE_TERMS=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' \
127
+ | sed 's/[^a-z0-9 ]/ /g' \
128
+ | tr ' ' '\n' \
129
+ | grep -E '^[a-z]{4,}$' \
130
+ | grep -vE '^(with|that|this|from|into|have|will|when|then|than|them|they|been|were|also|only|does|some|each|more|over|such|both|most|other|many|after|about|should|would|could|their|these|those|which|where|there|being|while|using|since|until|before|under|above)$' \
131
+ | sort -u | head -10 | tr '\n' ' ' | xargs)
132
+
133
+ # Extract affected files from the issue body's ## Affected Files section
134
+ ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' 2>/dev/null || echo '')
135
+ AFFECTED_FILES_RAW=$(echo "$ISSUE_BODY" \
136
+ | sed -n '/^## Affected Files/,/^## /p' \
137
+ | grep -oE '`[^`]+\.(md|mjs|js|ts|py|sh|yaml|yml|json)`' \
138
+ | tr -d '`' | sort -u | head -5)
139
+
140
+ # Extract symbols: backtick-quoted identifiers from the issue body (≥4 chars, camelCase or snake_case)
141
+ SYMBOLS_RAW=$(echo "$ISSUE_BODY" \
142
+ | grep -oE '`[A-Za-z_][A-Za-z0-9_]{3,}`' \
143
+ | tr -d '`' | sort -u | head -5)
144
+
145
+ # Build --file flags (one per affected file)
146
+ FILE_FLAGS=""
147
+ while IFS= read -r f; do
148
+ [ -n "$f" ] && FILE_FLAGS="$FILE_FLAGS --file $f"
149
+ done <<< "$AFFECTED_FILES_RAW"
150
+
151
+ # Build --symbol flag (first symbol only — recall supports one --symbol)
152
+ SYMBOL_FLAG=""
153
+ FIRST_SYMBOL=$(echo "$SYMBOLS_RAW" | head -1)
154
+ [ -n "$FIRST_SYMBOL" ] && SYMBOL_FLAG="--symbol $FIRST_SYMBOL"
155
+ ```
156
+
157
+ ### Step 3: Run recall query and capture results
158
+
159
+ ```bash
160
+ RECALL_RESULTS=""
161
+ RECALL_ISSUE_CITATIONS=""
162
+
163
+ if [ "$LEDGER_AVAILABLE" -eq 1 ] && [ -n "$TITLE_TERMS$FILE_FLAGS$SYMBOL_FLAG" ]; then
164
+ echo "[recall] Querying Forge Ledger: terms='${TITLE_TERMS}' files='${AFFECTED_FILES_RAW}' symbol='${FIRST_SYMBOL}'"
165
+
166
+ # Run combined query: title terms (free text) + file flags + symbol + min-score threshold
167
+ # --json for machine-readable output; --min-score 0.3 filters weak matches (noise gate)
168
+ RECALL_JSON=$(node "$RECALL_PATH" \
169
+ $TITLE_TERMS \
170
+ $FILE_FLAGS \
171
+ $SYMBOL_FLAG \
172
+ --k 5 \
173
+ --min-score 0.3 \
174
+ --json \
175
+ 2>/dev/null || echo '[]')
176
+
177
+ # Count results
178
+ RECALL_COUNT=$(echo "$RECALL_JSON" | node -e "
179
+ let d = ''; process.stdin.on('data', c => d += c);
180
+ process.stdin.on('end', () => {
181
+ try { console.log(JSON.parse(d).length); } catch { console.log(0); }
182
+ });
183
+ " 2>/dev/null || echo "0")
184
+
185
+ if [ "${RECALL_COUNT:-0}" -gt 0 ]; then
186
+ echo "[recall] Found ${RECALL_COUNT} prior card(s) above threshold — injecting with delta-verification framing"
187
+
188
+ # Extract issue citations for the Prior Investigations field in the FORGE:INVESTIGATOR comment
189
+ RECALL_ISSUE_CITATIONS=$(echo "$RECALL_JSON" | node -e "
190
+ let d = ''; process.stdin.on('data', c => d += c);
191
+ process.stdin.on('end', () => {
192
+ try {
193
+ const cards = JSON.parse(d);
194
+ const seen = new Set();
195
+ const cites = cards
196
+ .filter(c => { if (seen.has(c.issue)) return false; seen.add(c.issue); return true; })
197
+ .map(c => '#' + c.issue)
198
+ .join(', ');
199
+ console.log(cites);
200
+ } catch { console.log(''); }
201
+ });
202
+ " 2>/dev/null || echo '')
203
+
204
+ # Format cards as human-readable text for injection (not raw JSON)
205
+ RECALL_FORMATTED=$(echo "$RECALL_JSON" | node -e "
206
+ let d = ''; process.stdin.on('data', c => d += c);
207
+ process.stdin.on('end', () => {
208
+ try {
209
+ const cards = JSON.parse(d);
210
+ const lines = [];
211
+ for (const c of cards) {
212
+ lines.push('── ' + c.kind.toUpperCase() + ' from #' + c.issue + ' (score: ' + c.score.toFixed(2) + ') ──');
213
+ if (c.rootCause) lines.push('Root Cause: ' + c.rootCause);
214
+ if (c.prevention) lines.push('Fix/Prevention: ' + c.prevention);
215
+ if (c.pattern) lines.push('Pattern: ' + c.pattern);
216
+ if (c.verdict) lines.push('Verdict: ' + c.verdict + ' (' + (c.confidence || '?') + ')');
217
+ if (c.paths && c.paths.length) lines.push('Files: ' + c.paths.slice(0,3).join(', '));
218
+ }
219
+ console.log(lines.join('\n'));
220
+ } catch (e) { console.log(''); }
221
+ });
222
+ " 2>/dev/null || echo '')
223
+
224
+ # Cap at ≤ 2K chars (same budget as context C0 Gist summaries)
225
+ RECALL_RESULTS=$(echo "$RECALL_FORMATTED" | head -c 2048)
226
+ else
227
+ echo "[recall] No prior cards above threshold (min-score 0.3) — investigating from scratch"
228
+ fi
229
+ fi
230
+ ```
231
+
232
+ ### Step 4: Inject recall results into investigation context
233
+
234
+ If `RECALL_RESULTS` is non-empty, print the following block to stdout **before Phase 1A begins**. The investigation steps in Phase 1B MUST treat this as starting context — verify deltas against current code rather than re-deriving the prior model.
235
+
236
+ **If `RECALL_RESULTS` is empty**: skip this step entirely and proceed to Phase 1A.
237
+
238
+ ```
239
+ [FORGE:RECALL_PRIOR — Forge Ledger pre-recall results]
240
+ Prior cards matched this issue above threshold (min-score 0.3).
241
+
242
+ CRITICAL FRAMING: These cards describe prior findings on the same files/symbols.
243
+ → Verify the deltas against current code — do NOT re-derive the prior model.
244
+ → Stale cards (status: stale) are excluded by default — the query runs without
245
+ --include-stale. Do not treat any card as authoritative without confirming
246
+ the finding still exists in the current code.
247
+ → Cite confirmed priors in the FORGE:INVESTIGATOR "Prior Investigations" field.
248
+
249
+ ${RECALL_RESULTS}
250
+
251
+ [END FORGE:RECALL_PRIOR]
252
+ ```
253
+
254
+ Store `RECALL_ISSUE_CITATIONS` in a variable — it is used in Phase 1C to populate the `**Prior Investigations (via recall)**` field in the FORGE:INVESTIGATOR comment.
255
+
256
+ ---
257
+
93
258
  ## Phase 1A: Load Issue & Check Resume State
94
259
 
95
260
  ```bash
@@ -192,6 +357,20 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
192
357
 
193
358
  The comment MUST include `<!-- INVESTIGATION:COMPLETE -->` at the very end, AFTER all required sections are present. This marker signals the investigation finished successfully.
194
359
 
360
+ **CODEC PATH (forge#1727)**: Construct the annotation body via the protocol codec — do NOT hand-roll the `<!-- FORGE:INVESTIGATOR -->` header. Use `forge-annotation.sh write INVESTIGATOR --field ...` or `node packages/protocol/src/cli.js emit INVESTIGATOR --field ...` to produce the opening tag and completion sentinel. Fill in the Markdown body sections below. The full pattern:
361
+
362
+ ```bash
363
+ # Build the annotation body via codec (escaping and sentinel handled by codec)
364
+ ANNOTATION_BODY=$(node packages/protocol/src/cli.js emit INVESTIGATOR \
365
+ --field "Verdict={VERDICT}" \
366
+ --field "Confidence={CONFIDENCE}" \
367
+ --field "Severity={SEVERITY}" \
368
+ --field "Task Type={TASK_TYPE}" \
369
+ --field "Decomposition Assessment={YES|NO} — {reason}")
370
+ # ANNOTATION_BODY now has opening tag + required fields + INVESTIGATION:COMPLETE sentinel.
371
+ # Append the Markdown body sections to it before posting.
372
+ ```
373
+
195
374
  Before posting, resolve the attribution annotation link from `forge.yaml`:
196
375
 
197
376
  ```bash
@@ -234,6 +413,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
234
413
  **Last touched**: {hash — author — date — subject}
235
414
  **Pickaxe hits (prior fixes / regressions)**: {commit(s) found via \`git log -S\`/\`-G\`, or 'None found' — max 5}
236
415
  {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.'}
416
+ **Prior Investigations (via recall)**: {Comma-separated issue citations from \`RECALL_ISSUE_CITATIONS\` (e.g. '#1172, #1243 — building on, not repeating'), or 'None — no Forge Ledger match above threshold' if \`RECALL_RESULTS\` was empty. Use the \`RECALL_ISSUE_CITATIONS\` variable populated in Phase 0.6.}
237
417
 
238
418
  ### Recommendation
239
419
  {what to build/fix, concrete and actionable}
@@ -541,6 +721,47 @@ fi
541
721
 
542
722
  ## Phase 1D: Update Labels & Return Verdict
543
723
 
724
+ ### 1D.0: Finding Lifecycle Label Transition (MANDATORY — run before workflow label update)
725
+
726
+ **Purpose**: Wire the investigation verdict into the finding-validation lifecycle. If the issue under investigation is a review-finding (carries `needs-validation`), translate the verdict into `validated` or `false-positive` using `transition-label.sh --validate`. This is the primary mechanism that resolves the `needs-validation → validated/false-positive` lifecycle gap. <!-- Added: forge#1730 -->
727
+
728
+ **Run this block regardless of verdict (CONFIRMED, PARTIAL, INVALID) — the verdict-to-label mapping handles all cases:**
729
+
730
+ ```bash
731
+ # Check if this issue is a finding awaiting validation
732
+ ISSUE_LABELS=$(gh issue view {NUMBER} {GH_FLAG} --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
733
+
734
+ if echo "$ISSUE_LABELS" | grep -q "needs-validation"; then
735
+ echo "Issue #{NUMBER} has needs-validation — applying verdict label transition..."
736
+ RESOLUTION=$(resolve_script 'transition-label')
737
+ TIER="${RESOLUTION%%:*}"
738
+ SCRIPT_PATH="${RESOLUTION#*:}"
739
+ case "$TIER" in
740
+ adaptive|universal)
741
+ bash "$SCRIPT_PATH" --validate {VERDICT} {NUMBER} {GH_FLAG} || true
742
+ ;;
743
+ prose)
744
+ # Prose fallback: apply label transition directly via gh
745
+ if [ "{VERDICT}" = "CONFIRMED" ]; then
746
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "validated" --remove-label "needs-validation" 2>/dev/null || true
747
+ echo "Applied: needs-validation → validated (verdict: CONFIRMED)"
748
+ else
749
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "false-positive" --remove-label "needs-validation" 2>/dev/null || true
750
+ echo "Applied: needs-validation → false-positive (verdict: {VERDICT})"
751
+ fi
752
+ ;;
753
+ esac
754
+ else
755
+ echo "Issue #{NUMBER} does not have needs-validation — no finding lifecycle transition needed"
756
+ fi
757
+ ```
758
+
759
+ **Verdict → label mapping**: `CONFIRMED` → `validated`; `PARTIAL`, `NOT-CONFIRMED`, `INVALID` → `false-positive`.
760
+
761
+ **Idempotency**: `transition-label.sh --validate` is a no-op if the issue already has `validated` or `false-positive`, or if it lacks `needs-validation`. Safe to call on any issue.
762
+
763
+ ---
764
+
544
765
  **CONFIRMED or PARTIAL with decompose: NO**:
545
766
  ```bash
546
767
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating"
@@ -62,6 +62,7 @@ Orchestrator for the full issue lifecycle: investigate → decompose (if needed)
62
62
  - `workflow:merged` label is set
63
63
  - `workflow:invalid` label is set
64
64
  - `needs-human` label is set
65
+ - `workflow:awaiting-merge` label is set (remediated + re-reviewed, awaiting a human merge decision — see #1810) <!-- Added: forge#1810 -->
65
66
  - `workflow:decomposed` label is set (sub-issues spawned)
66
67
  - Issue state is CLOSED with terminal label
67
68
 
@@ -216,7 +217,7 @@ fi
216
217
  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`.
217
218
 
218
219
  ### 0A: Parse input
219
- Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human` and `workflow:decomposed`, pick highest priority.
220
+ Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human`, `workflow:decomposed`, and `workflow:awaiting-merge`, pick highest priority.
220
221
 
221
222
  **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.
222
223
 
@@ -234,7 +235,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
234
235
 
235
236
  **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 -->
236
237
 
237
- **Skip if**: Issue already has a terminal label (`workflow:merged`, `workflow:invalid`, `needs-human`) — no heartbeat needed on a completed issue.
238
+ **Skip if**: Issue already has a terminal label (`workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`) — no heartbeat needed on a completed issue.
238
239
 
239
240
  ### 0B: Load issue + existing context
240
241
  ```bash
@@ -242,7 +243,7 @@ gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,comments,
242
243
  gh api repos/{GH_REPO}/issues/{NUMBER}/comments --jq '.[] | {id: .id, author: .user.login, body: .body}'
243
244
  ```
244
245
 
245
- **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.
246
+ **Check**: state (closed → STOP), terminal labels (`workflow:merged`/`workflow:invalid`/`workflow:awaiting-merge` → STOP), existing agent comments (`FORGE:INVESTIGATOR`, `FORGE:DECOMPOSED`, `FORGE:CONTRACT`, `FORGE:BUILDER`, `FORGE:TRAJECTORY`, `FORGE:DECISION_RECORD`), parent tracker status, sub-issue status.
246
247
 
247
248
  **Determine resume point**: No comments → Phase 1. Investigation exists + ready-to-build → Phase 3. Builder:COMPLETE + no PR → Phase 4. Builder without :COMPLETE (partial/interrupted build) + no PR → Phase 3 (partial-build cleanup). Builder + PR open → Phase 5. PR merged + issue open → Phase 6.
248
249
 
@@ -471,12 +472,12 @@ case "$TIER" in
471
472
  adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
472
473
  prose)
473
474
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" \
474
- --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
475
+ --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
475
476
  ;;
476
477
  esac
477
478
  ```
478
479
 
479
- **Post Phase 1 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
480
+ **Post Phase 1 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
480
481
  ```bash
481
482
  PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
482
483
  gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
@@ -748,7 +749,7 @@ case "$TIER" in
748
749
  adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
749
750
  prose)
750
751
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" \
751
- --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
752
+ --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
752
753
  ;;
753
754
  esac
754
755
  ```
@@ -797,7 +798,7 @@ case "$TIER" in
797
798
  adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
798
799
  prose)
799
800
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
800
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid" 2>/dev/null || true
801
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
801
802
  ;;
802
803
  esac
803
804
  ```
@@ -821,7 +822,7 @@ case "$TIER" in
821
822
  adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
822
823
  prose)
823
824
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" \
824
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:decomposed" 2>/dev/null || true
825
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true
825
826
  ;;
826
827
  esac
827
828
  gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}"
@@ -927,7 +928,7 @@ case "$TIER" in
927
928
  adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
928
929
  prose)
929
930
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
930
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid" 2>/dev/null || true
931
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
931
932
  ;;
932
933
  esac
933
934
  ```
@@ -958,7 +959,7 @@ fi
958
959
 
959
960
  ### 3A: Re-read state from GitHub (MANDATORY)
960
961
 
961
- **Post Phase 3 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
962
+ **Post Phase 3 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
962
963
  ```bash
963
964
  PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
964
965
  gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
@@ -1262,7 +1263,7 @@ case "$TIER" in
1262
1263
  adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} building ;;
1263
1264
  prose)
1264
1265
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
1265
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1266
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1266
1267
  ;;
1267
1268
  esac
1268
1269
  ```
@@ -1768,7 +1769,7 @@ case "$TIER" in
1768
1769
  adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
1769
1770
  prose)
1770
1771
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" \
1771
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1772
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1772
1773
  ;;
1773
1774
  esac
1774
1775
  ```
@@ -1779,7 +1780,7 @@ esac
1779
1780
 
1780
1781
  ### 5A: Re-read state from GitHub (MANDATORY)
1781
1782
 
1782
- **Post Phase 5 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`):
1783
+ **Post Phase 5 heartbeat** (skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
1783
1784
  ```bash
1784
1785
  PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
1785
1786
  gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
@@ -1924,7 +1925,7 @@ case "$TIER" in
1924
1925
  adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} merged ;;
1925
1926
  prose)
1926
1927
  gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:merged" \
1927
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1928
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:invalid,workflow:decomposed" 2>/dev/null || true
1928
1929
  ;;
1929
1930
  esac
1930
1931
  ```
@@ -13,3 +13,104 @@
13
13
  > and usable in any product with attribution. This older draft was mistakenly
14
14
  > marked AGPL-3.0; the protocol description itself has always been intended for
15
15
  > open, license-compatible adoption.
16
+
17
+ ---
18
+
19
+ ## Forge Ledger — Derived Artifact of the Protocol
20
+
21
+ The **Forge Ledger** is the persistent, file/symbol-keyed knowledge index built from FORGE
22
+ annotations. It is a _derived artifact_ of the protocol: the ledger is rebuilt by re-parsing
23
+ the annotation corpus on GitHub (source of truth), not by maintaining separate authoritative
24
+ state.
25
+
26
+ ### Architecture
27
+
28
+ ```
29
+ GitHub Issues/PRs (annotation corpus, source of truth)
30
+ │
31
+ ▼
32
+ scripts/build-knowledge-index.mjs ← incremental indexer (watermarked by issue.updated_at)
33
+ │ uses packages/protocol/src/parse.js (FORGE annotation parser)
34
+ ▼
35
+ ~/.forge/index/
36
+ knowledge.jsonl ← card records (one JSON per line, keyed by issue/kind/seq)
37
+ postings.json ← BM25-lite inverted index (term → card IDs with per-field tf)
38
+ manifest.json ← issue → content-hash map + watermark + card count
39
+ renames.jsonl ← git rename log (old path → new path for query-time resolution)
40
+ │
41
+ ▼ (mirrored to orphan branch)
42
+ forge-knowledge branch ← versioned, diffable, hydratable by any clone or CI
43
+ ```
44
+
45
+ ### Card Schema
46
+
47
+ Cards are keyed by provenance (`{issue}/{kind}/{seq}`), never by file path. Paths are
48
+ attributes maintained by the indexer; renames are metadata updates, not re-keys.
49
+
50
+ ```json
51
+ {
52
+ "schemaVersion": 1,
53
+ "id": "1370/pattern/0",
54
+ "kind": "pattern",
55
+ "issue": 1370,
56
+ "pattern": "...",
57
+ "rootCause": "...",
58
+ "prevention": "...",
59
+ "paths": ["bin/runner.mjs"],
60
+ "symbols": ["scrubEnv"],
61
+ "anchor": { "path": "bin/runner.mjs", "symbol": "scrubEnv" },
62
+ "status": "fresh",
63
+ "createdAt": "2026-07-04"
64
+ }
65
+ ```
66
+
67
+ ### CLI
68
+
69
+ ```
70
+ # Query by file path (exact lookup):
71
+ forge recall --file bin/runner.mjs --json
72
+
73
+ # Query by symbol:
74
+ forge recall --symbol scrubEnv --json
75
+
76
+ # Free-text BM25 search:
77
+ forge recall "env scrub proc environ" --k 5
78
+
79
+ # Index health:
80
+ forge recall --doctor
81
+
82
+ # Rebuild index from scratch:
83
+ node scripts/build-knowledge-index.mjs --full-rebuild
84
+
85
+ # Incremental update (used by close phase):
86
+ node scripts/build-knowledge-index.mjs --issue {NUMBER}
87
+ ```
88
+
89
+ ### Invariants
90
+
91
+ - **Full rebuild = incremental parity**: running a full rebuild produces byte-identical
92
+ card content to a sequence of incremental runs over the same issue range.
93
+ - **Idempotency**: re-indexing the same issue twice produces the same card content (card IDs
94
+ are deterministic; content is a pure function of current annotation state).
95
+ - **GitHub wins**: the index is always rebuildable from the annotation corpus alone. Deleting
96
+ `~/.forge/index/` loses nothing — a rebuild restores the same state.
97
+ - **Non-blocking**: ledger indexing at close time never stalls the pipeline. Failure is logged
98
+ and the close phase continues.
99
+
100
+ ### Staleness Detection (tiered, cheapest first)
101
+
102
+ 1. Current blob sha of `anchor.path` matches stored blob sha → **FRESH**
103
+ 2. `anchor.symbol` is still defined in the current file → **FRESH**
104
+ 3. Rolling-hash scan finds `snippetHash` elsewhere in file → **MOVED** (re-anchor)
105
+ 4. `snippetHash` found in a file touched by rename log → **MOVED** (re-anchor path)
106
+ 5. Otherwise → **STALE** (excluded from binding injection; recallable with `--include-stale`)
107
+
108
+ ### Integration Points
109
+
110
+ - **`commands/work-on/build/context.md` Phase C1**: uses `forge recall --file` for exact
111
+ file-path lookup; falls back to live `gh issue list --search` when index is absent.
112
+ - **`commands/work-on/close.md` Phase C5.3**: calls `build-knowledge-index.mjs --issue`
113
+ after merge to index the just-closed issue; advances watermark.
114
+
115
+ See issue [#1732](https://github.com/RapierCraftStudios/ForgeDock/issues/1732) for the
116
+ implementation history and design decisions.
@@ -449,6 +449,43 @@ artifact so a sub-issue builder can fetch upstream context directly.
449
449
  <!-- FORGE:PRIOR_GIST: {https://example.com/artifact} -->
450
450
  ```
451
451
 
452
+ #### Security: Gist Visibility — Normative Requirement <!-- Added: forge#1729 -->
453
+
454
+ **All FORGE gist types (`FORGE:KNOWLEDGE_GIST`, `FORGE:MILESTONE_INDEX`, `FORGE:PRIOR_GIST`,
455
+ and the memory-index Gist referenced by `FORGE:MEMORY_INDEX` in `close.md`) MUST be created
456
+ secret. Publishing them as public Gists is a protocol violation.**
457
+
458
+ Rationale: these artifacts embed investigation findings — root causes, file paths, affected
459
+ service names, and security details. A public Gist exposes this content to a world-readable
460
+ URL with no access control. `gh gist create` is secret by default; the requirement is that
461
+ no pipeline spec or operator script must pass `--public` when creating any of these artifacts.
462
+
463
+ **Enforcement**: The `pre-tool-use.mjs` hook (Rule 3) hard-blocks `gh gist create --public`
464
+ in pipeline sessions. The `doctor-pipeline-state.sh` check I8 audits existing annotation-referenced
465
+ gists and reports a CRITICAL finding if any are public.
466
+
467
+ **Migration** (if a public gist is found): GitHub does not support changing a Gist's visibility
468
+ in place. To replace a public pipeline gist with a secret one:
469
+
470
+ ```bash
471
+ # 1. Save current content
472
+ gh gist view {GIST_ID} > /tmp/gist_migrate.md
473
+ [ -s /tmp/gist_migrate.md ] || { echo "ABORT: empty save"; exit 1; }
474
+
475
+ # 2. Delete the public gist
476
+ gh gist delete {GIST_ID} --yes
477
+
478
+ # 3. Recreate secret (no --public) with same description
479
+ gh gist create /tmp/gist_migrate.md --desc "{ORIGINAL_DESCRIPTION}"
480
+ # gh gist create without --public creates a secret gist.
481
+
482
+ # 4. Update the FORGE annotation on the issue to point to the new URL
483
+ ```
484
+
485
+ **Override**: set `FORGE_ALLOW_PUBLIC_GIST=1` in the operator's shell environment before
486
+ starting Claude Code only if a public gist is genuinely required (e.g., sharing a demo
487
+ artifact). This must not be used for pipeline memory gists.
488
+
452
489
  ### 4.3 Control and error markers
453
490
 
454
491
  These markers carry no Markdown body; their presence is the signal.
@@ -1143,16 +1180,71 @@ separate, MIT-licensed npm package:
1143
1180
  **`@forgedock/protocol`** — available at `packages/protocol/` in the ForgeDock
1144
1181
  repository.
1145
1182
 
1146
- It provides:
1183
+ ### 13.1 Normative codec path
1184
+
1185
+ **`packages/protocol/` is the normative write and read path for all FORGE
1186
+ annotations.** Hand-rolled annotation construction (embedding raw
1187
+ `<!-- FORGE:TYPE -->` strings in inline heredocs or `--body` blocks) is
1188
+ **non-conformant** because it bypasses the codec's escaping and round-trip
1189
+ guarantees. Every producer MUST route annotation construction through one of:
1190
+
1191
+ 1. **`forge-annotation.sh write <TYPE> --field KEY=VALUE ...`** (AGPL-licensed,
1192
+ ships with the npm package at `scripts/forge-annotation.sh`), or
1193
+ 2. **`node packages/protocol/src/cli.js emit <TYPE> --field KEY=VALUE ...`**
1194
+ (MIT-licensed, part of the `@forgedock/protocol` package)
1195
+
1196
+ Both paths delegate to `emit()` from `packages/protocol/src/emit.js`, which
1197
+ applies the complete escaping and sanitization policy (§3.5 injection hardening).
1198
+
1199
+ ### 13.2 JavaScript API
1200
+
1201
+ ```js
1202
+ import { parse, validate, emit, emitPartial } from '@forgedock/protocol';
1203
+ ```
1147
1204
 
1148
1205
  - `parse(commentBody)` — extracts all annotations from a comment body string
1149
1206
  - `validate(annotation)` — checks conformance per §3–4; returns `{ valid, errors, warnings }`
1150
- - `emit(type, fields)` — produces well-formed annotation strings
1207
+ - `emit(type, fields)` — produces a well-formed annotation string; applies all §3.5 escaping
1151
1208
  - `emitPartial(type)` — produces the partial sentinel for a type (§3.3)
1152
1209
 
1153
- A conformance suite in `packages/protocol/fixtures/` covers all 13 reserved
1154
- types with valid and invalid examples. The CLI runner can be invoked against any
1155
- fixtures directory:
1210
+ ### 13.3 CLI subcommands
1211
+
1212
+ The CLI at `packages/protocol/src/cli.js` exposes three modes:
1213
+
1214
+ ```bash
1215
+ # Emit — print a well-formed annotation body to stdout
1216
+ node packages/protocol/src/cli.js emit INVESTIGATOR \
1217
+ --field Verdict=CONFIRMED --field Confidence=HIGH \
1218
+ --field Severity=MEDIUM --field "Task Type=Bug Fix" \
1219
+ --field "Decomposition Assessment=NO — single fix."
1220
+
1221
+ # Parse — read a comment body from stdin, extract a field value
1222
+ echo '<!-- FORGE:INVESTIGATOR -->...' | \
1223
+ node packages/protocol/src/cli.js parse --type INVESTIGATOR --field Verdict
1224
+ # → CONFIRMED
1225
+
1226
+ # Conformance runner (legacy — no subcommand keyword)
1227
+ node packages/protocol/src/cli.js fixtures/
1228
+ ```
1229
+
1230
+ The `emit` subcommand also supports the Base64url machine-surface form for
1231
+ `FORGE:CARD` annotations (design decision 2026-07-08: encoding beats escaping):
1232
+
1233
+ ```bash
1234
+ node packages/protocol/src/cli.js emit CARD \
1235
+ --field issue=1370 --field status=merged --b64
1236
+ # → <!-- FORGE:CARD: v1 sha:7234c2d8 b64:eyJ... -->
1237
+ ```
1238
+
1239
+ The Base64url alphabet `[A-Za-z0-9_-]` cannot contain `>`, `<`, or `!`, making
1240
+ `-->`, `--!>`, and `<!--` structurally unrepresentable in the machine surface.
1241
+
1242
+ ### 13.4 Conformance suite
1243
+
1244
+ A conformance suite in `packages/protocol/fixtures/` covers all reserved types
1245
+ with valid and invalid examples, including adversarial payloads that reproduce
1246
+ historical escaping vulnerabilities. The suite can be run against any fixtures
1247
+ directory:
1156
1248
 
1157
1249
  ```bash
1158
1250
  npx @forgedock/protocol fixtures/
@@ -14,11 +14,14 @@ See also: [FORGE Annotation Protocol §6](forge-protocol-v1.md#6-label-state-mac
14
14
  | `workflow:ready-to-build` | Investigation complete, build not started | Investigator agent |
15
15
  | `workflow:building` | Build phase active | Builder agent |
16
16
  | `workflow:in-review` | PR created, review active | Orchestrator agent |
17
+ | `workflow:awaiting-merge` | Remediated + re-reviewed, awaiting a human merge decision | Review-pr Phase 8 (auto-merge guard) |
17
18
  | `workflow:merged` | PR merged, issue closed | Close phase agent |
18
19
  | `workflow:invalid` | Issue closed as invalid | Investigator agent |
19
20
  | `workflow:decomposed` | Issue decomposed into sub-issues | Decomposer agent |
20
21
  | `needs-human` | Pipeline blocked, human intervention required | Any agent on error |
21
22
 
23
+ `workflow:awaiting-merge` vs `needs-human`: both are terminal (the pipeline stops advancing the issue automatically), but they mean different things. `needs-human` means the pipeline hit a condition it cannot resolve on its own (genuinely blocked — conflicting PR, failed verdict, calibration/trust escalation, etc.) and a human must diagnose and act. `workflow:awaiting-merge` means the opposite: the PR was previously escalated to `needs-human`, has since been remediated and re-reviewed to a clean `APPROVED` verdict with no mergeability blockers, but does not yet meet the automated auto-land bar (see forge#1809 Q1) — a human only needs to click merge, not diagnose a problem. `scripts/transition-label.sh` clears `needs-human` (best-effort) on every transition, so moving an issue onto `workflow:awaiting-merge` also clears any stale `needs-human` label.
24
+
22
25
  ## Transitions
23
26
 
24
27
  ```
@@ -27,6 +30,7 @@ See also: [FORGE Annotation Protocol §6](forge-protocol-v1.md#6-label-state-mac
27
30
  → workflow:ready-to-build [Phase 2: Investigation complete]
28
31
  → workflow:building [Phase 3: Build started]
29
32
  → workflow:in-review [Phase 4: PR created]
33
+ → workflow:awaiting-merge [TERMINAL] [Phase 5/review-pr: re-reviewed after needs-human, awaiting human merge]
30
34
  → workflow:merged [TERMINAL] [Phase 5: PR merged]
31
35
  → workflow:invalid [TERMINAL] [Any phase: closed as invalid]
32
36
  → workflow:decomposed [TERMINAL] [Phase 2: split into sub-issues]
@@ -40,6 +44,7 @@ Processing stops when any of these labels is set:
40
44
  - `workflow:merged`
41
45
  - `workflow:invalid`
42
46
  - `workflow:decomposed`
47
+ - `workflow:awaiting-merge`
43
48
  - `needs-human`
44
49
 
45
50
  ## Label Exclusivity
@@ -49,7 +54,7 @@ At most one `workflow:*` label should be active on an issue at any time. When tr
49
54
  ```bash
50
55
  gh issue edit {NUMBER} -R {REPO} \
51
56
  --add-label "workflow:building" \
52
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed"
57
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed"
53
58
  ```
54
59
 
55
60
  This is enforced by `scripts/transition-label.sh` — use `resolve_script 'transition-label'` rather than calling `gh issue edit` directly when possible.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Autonomous dev pipeline for Claude Code. GitHub as institutional memory — every issue, PR, and annotation survives context resets and makes the next agent smarter.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -45,7 +45,7 @@
45
45
  "README.md"
46
46
  ],
47
47
  "scripts": {
48
- "test": "node --test \"bin/tests/**/*.test.mjs\"",
48
+ "test": "node --test \"bin/tests/**/*.test.mjs\" \"packages/protocol/test/**/*.test.mjs\"",
49
49
  "docs:dev": "vitepress dev docs/site",
50
50
  "docs:build": "vitepress build docs/site",
51
51
  "docs:preview": "vitepress preview docs/site"