forgedock 1.1.6 → 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 (43) hide show
  1. package/README.md +3 -1
  2. package/bin/engine/invariants.mjs +361 -0
  3. package/bin/hooks/pre-tool-use.mjs +182 -0
  4. package/bin/journey.mjs +77 -7
  5. package/bin/labels.json +10 -0
  6. package/bin/recall.mjs +540 -0
  7. package/commands/autopilot.md +959 -78
  8. package/commands/cleanup.md +52 -3
  9. package/commands/optimize.md +585 -0
  10. package/commands/orchestrate/phase-1-resolve.md +35 -25
  11. package/commands/orchestrate/phase-3-dependency.md +465 -34
  12. package/commands/orchestrate/phase-4-execution.md +448 -29
  13. package/commands/orchestrate/phase-6-report.md +198 -9
  14. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  15. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  16. package/commands/quality-gate.md +417 -0
  17. package/commands/review-pr.md +391 -13
  18. package/commands/spec-doctor.md +429 -0
  19. package/commands/validate.md +52 -0
  20. package/commands/work-on/build/architect.md +77 -0
  21. package/commands/work-on/build/context.md +357 -22
  22. package/commands/work-on/build/implement.md +2 -0
  23. package/commands/work-on/build/validate.md +28 -0
  24. package/commands/work-on/build.md +44 -0
  25. package/commands/work-on/close.md +621 -8
  26. package/commands/work-on/investigate.md +221 -0
  27. package/commands/work-on.md +15 -14
  28. package/docs/FORGE-PROTOCOL.md +101 -0
  29. package/docs/design/awesome-list-submissions.md +105 -0
  30. package/docs/design/f5bot-monitoring-setup.md +43 -0
  31. package/docs/design/growth-strategy-2026.md +579 -0
  32. package/docs/spec/forge-protocol-v1.md +97 -5
  33. package/docs/spec/label-state-machine.md +6 -1
  34. package/package.json +2 -2
  35. package/packages/protocol/src/cli.js +374 -84
  36. package/packages/protocol/src/types.js +75 -1
  37. package/scripts/build-knowledge-index.mjs +1761 -0
  38. package/scripts/calibration.mjs +996 -0
  39. package/scripts/danger-zones.mjs +782 -0
  40. package/scripts/doctor-pipeline-state.sh +88 -2
  41. package/scripts/refresh-bot-token.sh +97 -0
  42. package/scripts/select-fix-targets.sh +234 -0
  43. package/scripts/transition-label.sh +157 -6
@@ -202,6 +202,50 @@ if [ -n "$DEVDOCS_PATH" ] && [ -f "$INDEX_PATH" ]; then
202
202
 
203
203
  DEVDOCS_APPLICABLE=$(printf "%s" "$DEVDOCS_APPLICABLE" | sort | cut -d'|' -f2-)
204
204
 
205
+ # --- Module dossier glob pass (index-first path only) ---
206
+ # When AFFECTED_FILES are known (passed via --repo-path or from the contract),
207
+ # match each file against every modules[].glob in index.yaml. Inject matched
208
+ # dossier files into DEVDOCS_APPLICABLE as authority:required entries.
209
+ #
210
+ # Skip if: AFFECTED_FILES is empty, modules section absent, or DEVDOCS_PATH unset.
211
+ #
212
+ # Glob matching uses bash `case` (fnmatch semantics): supports * ? and [class].
213
+ # Operators must use patterns that match the BASENAME of the affected file
214
+ # (e.g. "runner*" matches "bin/runner.mjs" via basename extraction below)
215
+ # OR a relative-path prefix pattern (e.g. "bin/runner*").
216
+ # Both forms are tried; first match wins for each module entry.
217
+ if [ -n "${AFFECTED_FILES:-}" ] && [ -n "$DEVDOCS_PATH" ] && [ -f "$INDEX_PATH" ]; then
218
+ # Extract modules[] entries from index.yaml
219
+ # Format per entry: "name|glob|path"
220
+ MODULE_ENTRIES=$(yq '.modules[]? | .name + "|" + .glob + "|" + .path' "$INDEX_PATH" 2>/dev/null || echo "")
221
+
222
+ if [ -n "$MODULE_ENTRIES" ]; then
223
+ IFS=' ' read -ra AFFECTED_FILES_GLOB_ARR <<< "${AFFECTED_FILES}"
224
+ while IFS='|' read -r MOD_NAME MOD_GLOB MOD_PATH; do
225
+ [ -z "$MOD_GLOB" ] || [ -z "$MOD_PATH" ] && continue
226
+ DOSSIER_ABS="${DEVDOCS_PATH}/${MOD_PATH}"
227
+ [ -f "$DOSSIER_ABS" ] || { echo "WARN: modules[${MOD_NAME}] references missing dossier '${MOD_PATH}' — skipping"; continue; }
228
+ MATCHED=0
229
+ for af in "${AFFECTED_FILES_GLOB_ARR[@]}"; do
230
+ [ -z "$af" ] && continue
231
+ AF_BASENAME=$(basename "$af")
232
+ # Try basename match first, then relative-path match
233
+ case "$AF_BASENAME" in
234
+ $MOD_GLOB) MATCHED=1; break ;;
235
+ esac
236
+ case "$af" in
237
+ $MOD_GLOB) MATCHED=1; break ;;
238
+ esac
239
+ done
240
+ if [ "$MATCHED" -eq 1 ]; then
241
+ echo "Module dossier matched: '${MOD_GLOB}' → ${MOD_PATH} (module: ${MOD_NAME})"
242
+ # Add as sort key "0" (higher priority than required=1) so dossier appears first
243
+ DEVDOCS_APPLICABLE="0|${DOSSIER_ABS}"$'\n'"${DEVDOCS_APPLICABLE}"
244
+ fi
245
+ done <<< "$MODULE_ENTRIES"
246
+ fi
247
+ fi
248
+
205
249
  elif [ -n "$DEVDOCS_PATH" ]; then
206
250
  # --- Fallback: O(N) enumerate (backward compatible — no index.yaml present) ---
207
251
  echo "No index.yaml found at ${INDEX_PATH} — falling back to full enumerate (backward compatible)"
@@ -266,6 +310,229 @@ done <<< "$DEVDOCS_APPLICABLE"
266
310
 
267
311
  `DEVDOCS_CONTENT` is used in the `### Authoritative Devdocs` section of the FORGE:CONTEXT comment output. If empty (path absent or no applicable files), the section is replaced with a skip note.
268
312
 
313
+ **Module dossier injection**: When the glob pass (Step 1, index-first path) matched one or more dossiers, they appear first in `DEVDOCS_APPLICABLE` (sort key `0`) and are rendered as `#### Module Dossier: {name}` sub-sections within `### Authoritative Devdocs`. The 200-line/file cap applies to each dossier individually — the same cap used for all other devdocs files. This ensures dossier injection is bounded by the existing token budget even as dossiers grow. <!-- Added: forge#1733 -->
314
+
315
+ ---
316
+
317
+ ## Phase C-0.5: Active Peer Claims Reader (conditional — when running under orchestration batch) <!-- Added: forge#1736 -->
318
+
319
+ **Skip if**: `FORGE_COORD_ISSUE` is not set. This phase is a no-op when the agent is not running under an `/orchestrate` batch — no error, no output.
320
+
321
+ **Purpose**: Before writing any code, check whether peer agents in the same orchestration batch have posted `FORGE:CLAIM` annotations on the coordination issue that overlap with this agent's planned files. If overlapping claims exist, inject them into the builder's mental model as explicit constraints so the builder avoids modifying interfaces the peer has reserved.
322
+
323
+ **Time budget**: 20 seconds. If exceeded, log a warning and continue without peer-claim constraints.
324
+
325
+ ```bash
326
+ if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
327
+ COORD_NUM=$(echo "$FORGE_COORD_ISSUE" | grep -oE '[0-9]+$')
328
+ if [ -n "$COORD_NUM" ]; then
329
+ echo "Phase C-0.5: reading active peer claims from coordination issue #${COORD_NUM}"
330
+
331
+ # Fetch all comments from coordination issue
332
+ COORD_COMMENTS=$(gh api repos/{GH_REPO}/issues/${COORD_NUM}/comments \
333
+ --jq 'map(select(.body | contains("<!-- FORGE:CLAIM -->")))' 2>/dev/null || echo '[]')
334
+
335
+ # Extract active claims: FORGE:CLAIM comments that are NOT followed by FORGE:CLAIM_RELEASED
336
+ # from the same Holder. We identify active claims by the presence of CLAIM:COMPLETE
337
+ # and the absence of a subsequent CLAIM_RELEASED referencing the same Holder.
338
+ ACTIVE_PEER_CLAIMS=$(echo "$COORD_COMMENTS" | jq -r '.[] |
339
+ select(.body | contains("<!-- CLAIM:COMPLETE -->")) |
340
+ select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not) |
341
+ "Holder: " + (.body | capture("\\*\\*Holder\\*\\*: (?P<h>[^\n]+)").h // "unknown") +
342
+ "\nFiles: " + (.body | capture("\\*\\*Files\\*\\*: (?P<f>[^\n]+)").f // "none")
343
+ ' 2>/dev/null | grep -v "^Holder: #${NUMBER}" || echo "")
344
+ # Note: grep -v filters out this agent's own claim (same issue number) — peers only
345
+
346
+ if [ -n "$ACTIVE_PEER_CLAIMS" ]; then
347
+ echo "Active peer claims found:"
348
+ echo "$ACTIVE_PEER_CLAIMS"
349
+
350
+ # Extract claimed file paths from peer claims
351
+ PEER_CLAIMED_FILES=$(echo "$COORD_COMMENTS" | jq -r '.[] |
352
+ select(.body | contains("<!-- CLAIM:COMPLETE -->")) |
353
+ select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not) |
354
+ .body' 2>/dev/null \
355
+ | grep -v "^#${NUMBER}" \
356
+ | awk '/\*\*Files\*\*:/{found=1; next} /\*\*Interfaces\*\*:/{found=0} found{print}' \
357
+ | grep -oP '[a-zA-Z0-9._/-]+\.(py|tsx?|jsx?|sql|json|ya?ml|md|mjs|sh)' \
358
+ | sort -u || echo "")
359
+
360
+ # Check overlap with this agent's planned files (from FORGE:CONTRACT)
361
+ OWN_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
362
+ --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | last | .body' 2>/dev/null \
363
+ | grep -oP '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|md|mjs|sh)`' \
364
+ | tr -d '`' | sort -u || echo "")
365
+
366
+ OVERLAP=$(comm -12 <(echo "$PEER_CLAIMED_FILES") <(echo "$OWN_FILES") 2>/dev/null || echo "")
367
+
368
+ if [ -n "$OVERLAP" ]; then
369
+ PEER_CLAIMS_CONSTRAINT="
370
+ ⚠ CLAIMS BOARD CONSTRAINT: The following files are claimed by peer agents in this orchestration batch.
371
+ You MUST NOT modify the public interfaces of these files without first checking the peer's FORGE:CLAIM
372
+ on coordination issue #${COORD_NUM} to understand what interfaces they have reserved.
373
+
374
+ Overlapping claimed files:
375
+ $(echo "$OVERLAP" | sed 's/^/ - /')
376
+
377
+ Peer claims:
378
+ $(echo "$ACTIVE_PEER_CLAIMS" | sed 's/^/ /')
379
+
380
+ If you need to change an interface claimed by a peer, post a comment on coordination issue #${COORD_NUM}
381
+ describing the conflict — this routes to Phase 2.5 arbitration."
382
+ else
383
+ PEER_CLAIMS_CONSTRAINT="
384
+ ℹ CLAIMS BOARD: Peer claims exist but no file overlap with this agent's planned files.
385
+ Peer agents are working on independent file sets — no interface constraint applies.
386
+ Peer claims (for reference): $(echo "$ACTIVE_PEER_CLAIMS" | head -5)"
387
+ fi
388
+
389
+ # Store for injection into the FORGE:CONTEXT comment (Phase C4)
390
+ export PEER_CLAIMS_CONSTRAINT
391
+ else
392
+ echo "Phase C-0.5: no active peer claims found on coordination issue #${COORD_NUM}"
393
+ PEER_CLAIMS_CONSTRAINT=""
394
+ export PEER_CLAIMS_CONSTRAINT
395
+ fi
396
+ fi
397
+ fi
398
+ ```
399
+
400
+ **Constraint injection into FORGE:CONTEXT**: When `PEER_CLAIMS_CONSTRAINT` is set, append it to the `### Known Pitfalls for This Area` section of the FORGE:CONTEXT comment (Phase C4). This ensures the builder sees peer claims in the same context block as historical review findings — a single consolidated constraint surface. If `PEER_CLAIMS_CONSTRAINT` is empty, the section is omitted.
401
+
402
+ ---
403
+
404
+ ## Phase C0.5: Danger-Zone Rule Cards (fixed 400-token slot) <!-- Added: forge#1744 -->
405
+
406
+ Surface the highest-value risk knowledge for exactly the files this build will touch. Reads the persisted danger-zones index produced by `scripts/danger-zones.mjs`, filters to files that overlap the Builder Contract's deliverables table, ranks by risk score, and emits one-line rule cards cut at a hard 400-token ceiling. The slot is constant by construction — never more, never less — so risk injection adds zero variance to the builder's token budget.
407
+
408
+ **Time budget**: 10 seconds. If exceeded, log a warning and continue without danger-zone cards.
409
+
410
+ **Skip if**: `~/.forge/index/danger-zones.json` is absent (cold start — no index built yet). Zero cards → section omitted from FORGE:CONTEXT (no empty scaffolding). <!-- Cold-start safety: required — the index only exists after danger-zones.mjs has run at least once -->
411
+
412
+ ### Step 0: Locate danger-zones.json
413
+
414
+ ```bash
415
+ # Resolve index directory — forge.yaml may override ~/.forge/index
416
+ FORGE_INDEX_DIR="${HOME}/.forge/index"
417
+ if [ -f "{REPO_PATH}/forge.yaml" ]; then
418
+ FORGE_INDEX_OVERRIDE=$(yq '.forge_index.directory // ""' "{REPO_PATH}/forge.yaml" 2>/dev/null || echo '')
419
+ [ -n "$FORGE_INDEX_OVERRIDE" ] && FORGE_INDEX_DIR="$FORGE_INDEX_OVERRIDE"
420
+ fi
421
+
422
+ DANGER_ZONES_PATH="${FORGE_INDEX_DIR}/danger-zones.json"
423
+
424
+ if [ ! -f "$DANGER_ZONES_PATH" ]; then
425
+ echo "Phase C0.5: danger-zones.json absent at ${DANGER_ZONES_PATH} — skipping (cold start)"
426
+ DANGER_ZONE_CARDS=""
427
+ # → Continue to Phase C0 without emitting any cards
428
+ fi
429
+ ```
430
+
431
+ ### Step 1: Extract contract-overlapping files
432
+
433
+ Read the Builder Contract from the FORGE:CONTRACT comment on the issue. Extract the file paths listed in the Deliverables table:
434
+
435
+ ```bash
436
+ CONTRACT_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
437
+ --jq '[.[] | select(.body | contains("FORGE:CONTRACT")) | .body] | last // ""' 2>/dev/null \
438
+ | grep -oE '`[^`]+\.(py|ts|tsx|js|mjs|md|sh|yaml|yml|json)[^`]*`' \
439
+ | tr -d '`' \
440
+ | sort -u)
441
+
442
+ if [ -z "$CONTRACT_FILES" ]; then
443
+ # Fallback: use AFFECTED_FILES from arguments
444
+ CONTRACT_FILES=$(echo "{AFFECTED_FILES}" | tr ' ' '\n' | grep -v '^$' | sort -u)
445
+ fi
446
+ echo "Phase C0.5: contract files: $(echo "$CONTRACT_FILES" | tr '\n' ' ')"
447
+ ```
448
+
449
+ ### Step 2: Filter danger-zones.json to overlapping files
450
+
451
+ Read the danger-zones index and select only entries whose `file` key matches (by basename or full path) any file in the contract:
452
+
453
+ ```bash
454
+ DANGER_ZONE_CARDS=""
455
+ TOKEN_BUDGET=400 # hard cap (tokens)
456
+ CHAR_BUDGET=$((TOKEN_BUDGET * 4)) # proxy: 1 token ≈ 4 chars → 1600 chars
457
+ CHARS_USED=0
458
+
459
+ # Read and filter danger-zones.json entries (files key is a dict keyed by file path)
460
+ # For each contract file: look up its entry in danger-zones.json by basename or full path
461
+ while IFS= read -r contract_file; do
462
+ [ -z "$contract_file" ] && continue
463
+ BASENAME=$(basename "$contract_file")
464
+
465
+ # Look up by exact path first, then by basename match
466
+ DZ_ENTRY=$(python3 -c "
467
+ import sys, json, os
468
+
469
+ dz = json.load(open('${DANGER_ZONES_PATH}'))
470
+ files = dz.get('files', {})
471
+
472
+ # Try exact match first
473
+ target = '${contract_file}'
474
+ if target in files:
475
+ entry = files[target]
476
+ entry['file'] = target
477
+ print(json.dumps(entry))
478
+ sys.exit(0)
479
+
480
+ # Basename fallback: match any file whose basename equals the target basename
481
+ basename = os.path.basename(target)
482
+ for path, entry in files.items():
483
+ if os.path.basename(path) == basename:
484
+ entry['file'] = path
485
+ print(json.dumps(entry))
486
+ sys.exit(0)
487
+ " 2>/dev/null || echo '')
488
+
489
+ [ -z "$DZ_ENTRY" ] && continue
490
+
491
+ # Extract fields for the card
492
+ FILE_PATH=$(echo "$DZ_ENTRY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('file',''))" 2>/dev/null || echo '')
493
+ FINDINGS=$(echo "$DZ_ENTRY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('findingCount90d',0))" 2>/dev/null || echo '0')
494
+ TOP_PATTERN=$(echo "$DZ_ENTRY" | python3 -c "import sys,json; d=json.load(sys.stdin); p=d.get('topPatterns',[]); print(p[0] if p else '')" 2>/dev/null || echo '')
495
+ TOP_ISSUE=$(echo "$DZ_ENTRY" | python3 -c "import sys,json; d=json.load(sys.stdin); ci=d.get('citedIssues',[]); print('#'+str(ci[0]) if ci else '')" 2>/dev/null || echo '')
496
+ RISK_SCORE=$(echo "$DZ_ENTRY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('riskScore',0))" 2>/dev/null || echo '0')
497
+
498
+ # Skip files with zero findings (only in danger-zones due to co-change, not findings)
499
+ [ "$FINDINGS" -eq 0 ] && continue
500
+
501
+ # Build one-line card in issue-spec format:
502
+ # {file} — {N} findings/90d — recurring: {pattern} (#{issue}); rule: {prevention}
503
+ if [ -n "$TOP_PATTERN" ] && [ -n "$TOP_ISSUE" ]; then
504
+ CARD="${FILE_PATH} — ${FINDINGS} findings/90d — recurring: ${TOP_PATTERN} (${TOP_ISSUE}); rule: do not repeat this pattern"
505
+ elif [ -n "$TOP_PATTERN" ]; then
506
+ CARD="${FILE_PATH} — ${FINDINGS} findings/90d — recurring: ${TOP_PATTERN}; rule: do not repeat this pattern"
507
+ else
508
+ CARD="${FILE_PATH} — ${FINDINGS} findings/90d; rule: review before modifying"
509
+ fi
510
+
511
+ # Enforce 400-token cap (1600 char proxy)
512
+ CARD_LEN=${#CARD}
513
+ if [ $((CHARS_USED + CARD_LEN + 2)) -gt $CHAR_BUDGET ]; then
514
+ echo "Phase C0.5: token cap reached at ${CHARS_USED} chars (≈$((CHARS_USED / 4)) tokens) — truncating card list"
515
+ break
516
+ fi
517
+
518
+ DANGER_ZONE_CARDS="${DANGER_ZONE_CARDS}
519
+ - ${CARD}"
520
+ CHARS_USED=$((CHARS_USED + CARD_LEN + 2))
521
+
522
+ done < <(echo "$CONTRACT_FILES")
523
+
524
+ CARDS_TOKEN_COUNT=$((CHARS_USED / 4))
525
+ if [ -n "$DANGER_ZONE_CARDS" ]; then
526
+ echo "Phase C0.5: emitting ${CARDS_TOKEN_COUNT} tokens of danger-zone cards (cap: ${TOKEN_BUDGET})"
527
+ else
528
+ echo "Phase C0.5: no danger-zone entries for contract files — skipping section"
529
+ fi
530
+ ```
531
+
532
+ ### Step 3: Store for output
533
+
534
+ `DANGER_ZONE_CARDS` and `CARDS_TOKEN_COUNT` are used in the `### Danger-Zone Rule Cards` section of the FORGE:CONTEXT comment output. If `DANGER_ZONE_CARDS` is empty (no overlapping files with findings, or index absent), the section is **omitted entirely** — no empty scaffolding.
535
+
269
536
  ---
270
537
 
271
538
  ## Phase C0: Prior Investigation Findings (from Gists)
@@ -406,30 +673,73 @@ If `GIST_SUMMARIES` is non-empty, it will be included in the `### Prior Investig
406
673
 
407
674
  ## Phase C1: Past Review Findings on These Files
408
675
 
676
+ **Primary path — Forge Ledger** (O(1) local index lookup, zero API calls): <!-- Added: forge#1732 -->
677
+
678
+ Check for a local knowledge index before making any GitHub API calls. If the index exists, use
679
+ `forge recall` for exact file-path lookups. Fall back to live `gh issue list --search` only when
680
+ the index is absent or returns no results for a given file.
681
+
682
+ ```bash
683
+ # Resolve recall CLI path relative to repository root
684
+ RECALL_PATH="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}/bin/recall.mjs"
685
+ LEDGER_AVAILABLE=0
686
+
687
+ if [ -f "$RECALL_PATH" ]; then
688
+ # Quick probe: does the index exist and have cards?
689
+ PROBE=$(node "$RECALL_PATH" --doctor 2>/dev/null | grep "^Total cards:" | grep -v "^Total cards: 0" || true)
690
+ [ -n "$PROBE" ] && LEDGER_AVAILABLE=1
691
+ fi
692
+
693
+ LEDGER_FINDINGS=""
694
+
695
+ if [ "$LEDGER_AVAILABLE" -eq 1 ]; then
696
+ echo "[context:C1] Using Forge Ledger for file-path recall (zero API calls)"
697
+ IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
698
+ for file in "${AFFECTED_FILES_ARR[@]}"; do
699
+ FILE_CARDS=$(node "$RECALL_PATH" --file "$file" --k 5 --json 2>/dev/null || echo "[]")
700
+ if [ "$FILE_CARDS" != "[]" ] && [ -n "$FILE_CARDS" ]; then
701
+ LEDGER_FINDINGS="${LEDGER_FINDINGS}
702
+ ### Ledger findings for \`${file}\`
703
+ \`\`\`json
704
+ ${FILE_CARDS}
705
+ \`\`\`"
706
+ fi
707
+ done
708
+ fi
709
+ ```
710
+
711
+ **Extract from ledger results** (when `LEDGER_AVAILABLE=1` and `LEDGER_FINDINGS` is non-empty):
712
+ Parse the JSON card array. For each card: extract `kind`, `rootCause`, `pattern`, `prevention`,
713
+ `paths`, `symbols`, `issue` (citation). Include in the FORGE:CONTEXT output under
714
+ **### Known Pitfalls for This Area** (pattern/stale cards) and **### Historical Findings on These
715
+ Files** (investigation cards). Cards with `status: "stale"` are noted as such but still included
716
+ — they may describe a bug class that has since moved files.
717
+
718
+ **Fallback path — live GitHub search** (when `LEDGER_AVAILABLE=0` or `LEDGER_FINDINGS` is empty):
719
+
409
720
  Query closed issues with `review-finding` label, searching by filename:
410
721
 
411
722
  ```bash
412
- # {AFFECTED_FILES} is a space-separated file path list (see contract note above)
413
- # — split explicitly on IFS=' ' into an array instead of a bare
414
- # `for file in {AFFECTED_FILES}`, which word-splits on the shell's default IFS
415
- # (space, tab, AND newline) and would corrupt any path containing a space.
416
- IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
417
- for file in "${AFFECTED_FILES_ARR[@]}"; do
418
- basename=$(basename "$file" .py)
419
- gh issue list -R {GH_REPO} \
420
- --state closed \
421
- --label "review-finding" \
422
- --search "$basename" \
423
- --limit 10 \
424
- --json number,title,body \
425
- --jq '.[] | {
426
- number,
427
- title,
428
- pattern: (.body | capture("\\*\\*Pattern\\*\\*: *(?<p>[^\\n]+)").p // null),
429
- prevention: (.body | capture("\\*\\*Prevention\\*\\*: *(?<v>[^\\n]+)").v // null),
430
- root_cause: (.body | capture("\\*\\*Root cause\\*\\*: *(?<rc>[^\\n]+)").rc // (.body | capture("Root Cause[^\\n]*\\n(?<rc>[^\\n]+)").rc // "see body"))
431
- }'
432
- done
723
+ if [ "$LEDGER_AVAILABLE" -eq 0 ] || [ -z "$LEDGER_FINDINGS" ]; then
724
+ echo "[context:C1] Forge Ledger unavailable or empty — falling back to live gh search"
725
+ IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
726
+ for file in "${AFFECTED_FILES_ARR[@]}"; do
727
+ basename=$(basename "$file" .py)
728
+ gh issue list -R {GH_REPO} \
729
+ --state closed \
730
+ --label "review-finding" \
731
+ --search "$basename" \
732
+ --limit 10 \
733
+ --json number,title,body \
734
+ --jq '.[] | {
735
+ number,
736
+ title,
737
+ pattern: (.body | capture("\\*\\*Pattern\\*\\*: *(?<p>[^\\n]+)").p // null),
738
+ prevention: (.body | capture("\\*\\*Prevention\\*\\*: *(?<v>[^\\n]+)").v // null),
739
+ root_cause: (.body | capture("\\*\\*Root cause\\*\\*: *(?<rc>[^\\n]+)").rc // (.body | capture("Root Cause[^\\n]*\\n(?<rc>[^\\n]+)").rc // "see body"))
740
+ }'
741
+ done
742
+ fi
433
743
  ```
434
744
 
435
745
  Keep findings where the filename or function name appears in the title or body. Discard false matches (same word, different module).
@@ -526,6 +836,15 @@ Use 2-3 keywords from the issue title. If no results, skip this phase — do not
526
836
 
527
837
  ## Output Format
528
838
 
839
+ **CODEC PATH (forge#1727)**: Post the `<!-- FORGE:CONTEXT -->` comment via the protocol codec — do NOT hand-roll the opening tag or completion sentinel. Use `forge-annotation.sh write CONTEXT` or `node packages/protocol/src/cli.js emit CONTEXT` to produce the tag and sentinel (`<!-- FORGE:CONTEXT:COMPLETE -->`). The codec handles completion sentinel emission automatically.
840
+
841
+ ```bash
842
+ # Codec produces the opening tag and completion sentinel
843
+ CONTEXT_BODY=$(node packages/protocol/src/cli.js emit CONTEXT)
844
+ # $CONTEXT_BODY = "<!-- FORGE:CONTEXT -->\n<!-- FORGE:CONTEXT:COMPLETE -->"
845
+ # Insert the Markdown sections between the opening tag line and the sentinel.
846
+ ```
847
+
529
848
  Post the following as a GitHub comment on `{NUMBER}`:
530
849
 
531
850
  ```bash
@@ -540,12 +859,26 @@ gh issue comment {NUMBER} -R {GH_REPO} --body "<!-- FORGE:CONTEXT -->
540
859
  'No devdocs found at {DEVDOCS_PATH} — skipping. Run `npx forgedock docs init` to scaffold.' -->
541
860
  {DEVDOCS_CONTENT}
542
861
 
862
+ ### Danger-Zone Rule Cards
863
+ <!-- Ranked risk cards for contract-overlapping files from the Forge Ledger danger-zones index (Phase C0.5).
864
+ Token budget: {CARDS_TOKEN_COUNT} / 400 tokens used.
865
+ Each card: {file} — {N} findings/90d — recurring: {pattern} (#{issue}); rule: {prevention}
866
+ BINDING CONSTRAINT: treat each card as a must-not-violate rule before committing.
867
+ If danger-zones.json was absent or no contract files had findings — omit this section entirely (no empty scaffolding). -->
868
+ {DANGER_ZONE_CARDS}
869
+
543
870
  ### Prior Investigation Findings
544
871
  <!-- Summarized Knowledge Gist content from upstream investigations (Phase C0).
545
872
  If no FORGE:PRIOR_GIST annotations were found in the issue body: omit this section entirely.
546
873
  If Gist fetches failed: include the failure note so the builder knows context was attempted. -->
547
874
  {GIST_SUMMARIES}
548
875
 
876
+ ### Claims Board Constraints
877
+ <!-- Active peer claims from the orchestration coordination issue (Phase C-0.5).
878
+ If FORGE_COORD_ISSUE is not set (not running under /orchestrate): omit this section entirely.
879
+ If no peer claims overlap this agent's planned files: write 'No overlapping peer claims.' -->
880
+ {PEER_CLAIMS_CONSTRAINT}
881
+
549
882
  ### Known Pitfalls for This Area
550
883
  <!-- Structured prevention rules extracted from past review-finding issues (Pattern Metadata section).
551
884
  If a finding has a Prevention field, list it here. Builder MUST read these before writing code.
@@ -607,7 +940,9 @@ This module runs at **Step 3C.5** — after Builder Contract is posted, before I
607
940
  ```
608
941
  3C → Builder Contract posted
609
942
  3C.5 → [THIS MODULE] Context gathering (max 2 min)
610
- Phase C-1: Authoritative Devdocs (project-resident knowledge — highest precedence)
943
+ Phase C-1: Authoritative Devdocs (project-resident knowledge — highest precedence)
944
+ Phase C-0.5: Active Peer Claims Reader (conditional — orchestration only)
945
+ Phase C0.5: Danger-Zone Rule Cards (fixed 400-token slot — forge#1744)
611
946
  Phase C0: Prior Investigation Findings (from Gists)
612
947
  Phase C1: Past Review Findings on These Files
613
948
  Phase C2: Past Bugs in the Same Module
@@ -87,6 +87,8 @@ Extract from contract:
87
87
  - Deliverables table (file, change, why)
88
88
  - Acceptance criteria
89
89
 
90
+ **Danger-Zone Rule Cards (BINDING CONSTRAINTS)**: When the FORGE:CONTEXT comment contains a `### Danger-Zone Rule Cards` section, each card is a **binding must-not-violate constraint** with the same authority as devdocs custom instructions. Before writing any code, read all injected cards and internalize them as explicit prohibitions. If the implementation plan requires touching a file listed in a card, the card's rule overrides the plan — choose an implementation path that satisfies the rule, or post a `needs-human` comment explaining the conflict. Do NOT silently proceed past a rule card that the implementation would violate. The quality gate (check 2G.9) will flag violations as `known-pattern-recurrence` (highest embarrassment class). <!-- Added: forge#1744 -->
91
+
90
92
  ---
91
93
 
92
94
  ## Phase I2: Route by Task Type
@@ -39,6 +39,34 @@ In all other cases, the gate MUST run.
39
39
 
40
40
  ---
41
41
 
42
+ ## Phase V1: Builder Self-Check — Wire-Through Proof (MANDATORY, run BEFORE quality gate loop)
43
+
44
+ <!-- Added: forge#1731 -->
45
+
46
+ Before invoking the quality gate, the builder MUST perform a self-check on newly added conditional paths. This mirrors the quality gate's 2G.8 check and allows the builder to resolve gaps before the gate invocation rather than after.
47
+
48
+ **Self-check protocol**:
49
+
50
+ 1. Scan the staged diff for newly added conditional lines:
51
+ ```bash
52
+ git diff HEAD -- {CHANGED_FILES} | grep -E '^\+' | grep -v '^+++' \
53
+ | grep -E '\bif\b|\belif\b|\belse\b|guard|feature.?flag|FEATURE_FLAG|ENABLE_|DISABLE_'
54
+ ```
55
+
56
+ 2. For each new conditional found, confirm at least ONE of:
57
+ - **Test in diff**: A test function or assertion in the diff exercises this conditional branch (e.g., calls the function with parameters that trigger the `if`, asserts the guarded output, or triggers the error path deliberately)
58
+ - **WIRE:PROVEN annotation**: Add `# WIRE:PROVEN — <method>` immediately before or after the new conditional, describing how you verified it fires (e.g., `# WIRE:PROVEN — gate logic: condition is checked before every call; unreachable path would raise ValueError visible in tests`)
59
+ - **Trivial re-guard** (auto-exempt): The conditional is a null/length/type check whose body is `return`/`continue`/`break`/`pass` or a single-line assignment — defensive re-guards with no new behavior
60
+
61
+ 3. If none of the above is true for a new conditional, either:
62
+ - Add a test or trace that exercises the path before staging
63
+ - Add a `# WIRE:PROVEN — <method>` annotation explaining how you verified reachability
64
+ - Confirm it qualifies as a trivial re-guard
65
+
66
+ **Why this matters**: Guards, flags, and validators that are never exercised are functionally dead code. This class has cost multiple sprint cycles in this pipeline (#1230, #1244, #1522, #1580). The self-check catches gaps before the quality gate fires, reducing iteration count. <!-- Added: forge#1731 -->
67
+
68
+ ---
69
+
42
70
  ## Phase V1: Quality Gate Loop
43
71
 
44
72
  **Loop protocol** — the gate MUST pass or exhaust iterations before returning:
@@ -185,6 +185,50 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
185
185
 
186
186
  Contract must be grounded in the investigation report. Every deliverable file must appear in the affected files list from the investigator. Adversarially validate the proposed fix against adjacent system layers before posting.
187
187
 
188
+ ### B2.1: Post FORGE:CLAIM on coordination issue (conditional — when running under orchestration batch) <!-- Added: forge#1736 -->
189
+
190
+ **Skip if**: `FORGE_COORD_ISSUE` is not set (agent is not running under an orchestration batch). This step is a no-op outside of `/orchestrate` dispatch — no error, no output.
191
+
192
+ **When `FORGE_COORD_ISSUE` is set**: Post a `FORGE:CLAIM` annotation on the coordination issue to advertise this agent's active resource reservation to the orchestrator and peer agents. This enables the claims-board Layer-2/4 relaxation sweep (orchestrate Step 4B) to identify issue-pairs with disjoint file sets and downgrade unnecessary serialization edges.
193
+
194
+ ```bash
195
+ if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
196
+ COORD_NUM=$(echo "$FORGE_COORD_ISSUE" | grep -oE '[0-9]+$')
197
+ if [ -n "$COORD_NUM" ]; then
198
+ # Extract file paths from the just-posted FORGE:CONTRACT deliverables table
199
+ CLAIMED_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
200
+ --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | last | .body' 2>/dev/null \
201
+ | awk '/^### Deliverables/{p=1; next} /^### /{p=0} p' \
202
+ | grep -oP '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|md|mjs|sh)`' \
203
+ | tr -d '`' | sort -u | tr '\n' '\n' | head -20)
204
+ CLAIMED_FILES="${CLAIMED_FILES:-"(files listed in FORGE:CONTRACT deliverables table)"}"
205
+
206
+ # Extract preserved interfaces from the FORGE:ARCHITECT affected paths table (if present)
207
+ CLAIMED_INTERFACES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
208
+ --jq '[.[] | select(.body | contains("FORGE:ARCHITECT"))] | last | .body' 2>/dev/null \
209
+ | awk '/^### Affected Paths/{p=1; next} /^### /{p=0} p' \
210
+ | grep -oP 'Function/Class.*\|.*\|' | head -10 || echo "(see FORGE:ARCHITECT for interface details)")
211
+ CLAIMED_INTERFACES="${CLAIMED_INTERFACES:-"(see FORGE:ARCHITECT comment for interface details)"}"
212
+
213
+ CLAIM_HOLDER="#${NUMBER} / $(date -u +%Y%m%dT%H%M%S)"
214
+ CLAIM_TTL="terminal state of Holder issue #${NUMBER}"
215
+
216
+ gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM -->
217
+ ## Resource Claim
218
+
219
+ **Holder**: ${CLAIM_HOLDER}
220
+ **Files**: ${CLAIMED_FILES}
221
+ **Interfaces**: ${CLAIMED_INTERFACES}
222
+ **TTL**: ${CLAIM_TTL}
223
+
224
+ <!-- CLAIM:COMPLETE -->" 2>/dev/null || true
225
+ echo "FORGE:CLAIM posted on coordination issue #${COORD_NUM} for #${NUMBER}"
226
+ fi
227
+ fi
228
+ ```
229
+
230
+ **After posting**: Continue to Phase B2.5. The claim is now visible to the orchestrator and peer agents. The orchestrator's claims-board relaxation sweep (orchestrate Step 4B) will read this claim when determining whether serialized peers can be unblocked.
231
+
188
232
  ---
189
233
 
190
234
  ## Phase B2.5: Extract FUNCTION_NAMES from Contract