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
@@ -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,232 @@ 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
+ # Self-exclusion is done via jq select() against the **Holder**: #{NUMBER} field —
339
+ # the actual encoding of the owning issue number — not by grep-matching flattened text
340
+ # (claim comment lines never start with a bare #N, so a line-prefix grep can never match).
341
+ ACTIVE_PEER_CLAIMS=$(echo "$COORD_COMMENTS" | jq -r --arg num "$NUMBER" '.[] |
342
+ select(.body | contains("<!-- CLAIM:COMPLETE -->")) |
343
+ select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not) |
344
+ select((.body | capture("\\*\\*Holder\\*\\*: #(?<n>[0-9]+)").n // "") != $num) |
345
+ "Holder: " + (.body | capture("\\*\\*Holder\\*\\*: (?P<h>[^\n]+)").h // "unknown") +
346
+ "\nFiles: " + (.body | capture("\\*\\*Files\\*\\*: (?P<f>[^\n]+)").f // "none")
347
+ ' 2>/dev/null || echo "")
348
+
349
+ if [ -n "$ACTIVE_PEER_CLAIMS" ]; then
350
+ echo "Active peer claims found:"
351
+ echo "$ACTIVE_PEER_CLAIMS"
352
+
353
+ # Extract claimed file paths from peer claims (self already excluded above via jq select)
354
+ PEER_CLAIMED_FILES=$(echo "$COORD_COMMENTS" | jq -r --arg num "$NUMBER" '.[] |
355
+ select(.body | contains("<!-- CLAIM:COMPLETE -->")) |
356
+ select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not) |
357
+ select((.body | capture("\\*\\*Holder\\*\\*: #(?<n>[0-9]+)").n // "") != $num) |
358
+ .body' 2>/dev/null \
359
+ | awk '/\*\*Files\*\*:/{found=1; next} /\*\*Interfaces\*\*:/{found=0} found{print}' \
360
+ | grep -oP '[a-zA-Z0-9._/-]+\.(py|tsx?|jsx?|sql|json|ya?ml|md|mjs|sh)' \
361
+ | sort -u || echo "")
362
+
363
+ # Check overlap with this agent's planned files (from FORGE:CONTRACT)
364
+ OWN_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
365
+ --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | last | .body' 2>/dev/null \
366
+ | grep -oP '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|md|mjs|sh)`' \
367
+ | tr -d '`' | sort -u || echo "")
368
+
369
+ OVERLAP=$(comm -12 <(echo "$PEER_CLAIMED_FILES") <(echo "$OWN_FILES") 2>/dev/null || echo "")
370
+
371
+ if [ -n "$OVERLAP" ]; then
372
+ PEER_CLAIMS_CONSTRAINT="
373
+ ⚠ CLAIMS BOARD CONSTRAINT: The following files are claimed by peer agents in this orchestration batch.
374
+ You MUST NOT modify the public interfaces of these files without first checking the peer's FORGE:CLAIM
375
+ on coordination issue #${COORD_NUM} to understand what interfaces they have reserved.
376
+
377
+ Overlapping claimed files:
378
+ $(echo "$OVERLAP" | sed 's/^/ - /')
379
+
380
+ Peer claims:
381
+ $(echo "$ACTIVE_PEER_CLAIMS" | sed 's/^/ /')
382
+
383
+ If you need to change an interface claimed by a peer, post a comment on coordination issue #${COORD_NUM}
384
+ describing the conflict — this routes to Phase 2.5 arbitration."
385
+ else
386
+ PEER_CLAIMS_CONSTRAINT="
387
+ ℹ CLAIMS BOARD: Peer claims exist but no file overlap with this agent's planned files.
388
+ Peer agents are working on independent file sets — no interface constraint applies.
389
+ Peer claims (for reference): $(echo "$ACTIVE_PEER_CLAIMS" | head -5)"
390
+ fi
391
+
392
+ # Store for injection into the FORGE:CONTEXT comment (Phase C4)
393
+ export PEER_CLAIMS_CONSTRAINT
394
+ else
395
+ echo "Phase C-0.5: no active peer claims found on coordination issue #${COORD_NUM}"
396
+ PEER_CLAIMS_CONSTRAINT=""
397
+ export PEER_CLAIMS_CONSTRAINT
398
+ fi
399
+ fi
400
+ fi
401
+ ```
402
+
403
+ **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.
404
+
405
+ ---
406
+
407
+ ## Phase C0.5: Danger-Zone Rule Cards (fixed 400-token slot) <!-- Added: forge#1744 -->
408
+
409
+ 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.
410
+
411
+ **Time budget**: 10 seconds. If exceeded, log a warning and continue without danger-zone cards.
412
+
413
+ **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 -->
414
+
415
+ ### Step 0: Locate danger-zones.json
416
+
417
+ ```bash
418
+ # Resolve index directory — forge.yaml may override ~/.forge/index
419
+ FORGE_INDEX_DIR="${HOME}/.forge/index"
420
+ if [ -f "{REPO_PATH}/forge.yaml" ]; then
421
+ FORGE_INDEX_OVERRIDE=$(yq '.forge_index.directory // ""' "{REPO_PATH}/forge.yaml" 2>/dev/null || echo '')
422
+ [ -n "$FORGE_INDEX_OVERRIDE" ] && FORGE_INDEX_DIR="$FORGE_INDEX_OVERRIDE"
423
+ fi
424
+
425
+ DANGER_ZONES_PATH="${FORGE_INDEX_DIR}/danger-zones.json"
426
+
427
+ if [ ! -f "$DANGER_ZONES_PATH" ]; then
428
+ echo "Phase C0.5: danger-zones.json absent at ${DANGER_ZONES_PATH} — skipping (cold start)"
429
+ DANGER_ZONE_CARDS=""
430
+ # → Continue to Phase C0 without emitting any cards
431
+ fi
432
+ ```
433
+
434
+ ### Step 1: Extract contract-overlapping files
435
+
436
+ Read the Builder Contract from the FORGE:CONTRACT comment on the issue. Extract the file paths listed in the Deliverables table:
437
+
438
+ ```bash
439
+ CONTRACT_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
440
+ --jq '[.[] | select(.body | contains("FORGE:CONTRACT")) | .body] | last // ""' 2>/dev/null \
441
+ | grep -oE '`[^`]+\.(py|ts|tsx|js|mjs|md|sh|yaml|yml|json)[^`]*`' \
442
+ | tr -d '`' \
443
+ | sort -u)
444
+
445
+ if [ -z "$CONTRACT_FILES" ]; then
446
+ # Fallback: use AFFECTED_FILES from arguments
447
+ CONTRACT_FILES=$(echo "{AFFECTED_FILES}" | tr ' ' '\n' | grep -v '^$' | sort -u)
448
+ fi
449
+ echo "Phase C0.5: contract files: $(echo "$CONTRACT_FILES" | tr '\n' ' ')"
450
+ ```
451
+
452
+ ### Step 2: Filter danger-zones.json to overlapping files
453
+
454
+ Read the danger-zones index and select only entries whose `file` key matches (by basename or full path) any file in the contract:
455
+
456
+ ```bash
457
+ DANGER_ZONE_CARDS=""
458
+ TOKEN_BUDGET=400 # hard cap (tokens)
459
+ CHAR_BUDGET=$((TOKEN_BUDGET * 4)) # proxy: 1 token ≈ 4 chars → 1600 chars
460
+ CHARS_USED=0
461
+
462
+ # Read and filter danger-zones.json entries (files key is a dict keyed by file path)
463
+ # For each contract file: look up its entry in danger-zones.json by basename or full path
464
+ while IFS= read -r contract_file; do
465
+ [ -z "$contract_file" ] && continue
466
+ BASENAME=$(basename "$contract_file")
467
+
468
+ # Look up by exact path first, then by basename match
469
+ DZ_ENTRY=$(python3 -c "
470
+ import sys, json, os
471
+
472
+ dz = json.load(open('${DANGER_ZONES_PATH}'))
473
+ files = dz.get('files', {})
474
+
475
+ # Try exact match first
476
+ target = '${contract_file}'
477
+ if target in files:
478
+ entry = files[target]
479
+ entry['file'] = target
480
+ print(json.dumps(entry))
481
+ sys.exit(0)
482
+
483
+ # Basename fallback: match any file whose basename equals the target basename
484
+ basename = os.path.basename(target)
485
+ for path, entry in files.items():
486
+ if os.path.basename(path) == basename:
487
+ entry['file'] = path
488
+ print(json.dumps(entry))
489
+ sys.exit(0)
490
+ " 2>/dev/null || echo '')
491
+
492
+ [ -z "$DZ_ENTRY" ] && continue
493
+
494
+ # Extract fields for the card
495
+ FILE_PATH=$(echo "$DZ_ENTRY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('file',''))" 2>/dev/null || echo '')
496
+ FINDINGS=$(echo "$DZ_ENTRY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('findingCount90d',0))" 2>/dev/null || echo '0')
497
+ 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 '')
498
+ 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 '')
499
+ 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')
500
+
501
+ # Skip files with zero findings (only in danger-zones due to co-change, not findings)
502
+ [ "$FINDINGS" -eq 0 ] && continue
503
+
504
+ # Build one-line card in issue-spec format:
505
+ # {file} — {N} findings/90d — recurring: {pattern} (#{issue}); rule: {prevention}
506
+ if [ -n "$TOP_PATTERN" ] && [ -n "$TOP_ISSUE" ]; then
507
+ CARD="${FILE_PATH} — ${FINDINGS} findings/90d — recurring: ${TOP_PATTERN} (${TOP_ISSUE}); rule: do not repeat this pattern"
508
+ elif [ -n "$TOP_PATTERN" ]; then
509
+ CARD="${FILE_PATH} — ${FINDINGS} findings/90d — recurring: ${TOP_PATTERN}; rule: do not repeat this pattern"
510
+ else
511
+ CARD="${FILE_PATH} — ${FINDINGS} findings/90d; rule: review before modifying"
512
+ fi
513
+
514
+ # Enforce 400-token cap (1600 char proxy)
515
+ CARD_LEN=${#CARD}
516
+ if [ $((CHARS_USED + CARD_LEN + 2)) -gt $CHAR_BUDGET ]; then
517
+ echo "Phase C0.5: token cap reached at ${CHARS_USED} chars (≈$((CHARS_USED / 4)) tokens) — truncating card list"
518
+ break
519
+ fi
520
+
521
+ DANGER_ZONE_CARDS="${DANGER_ZONE_CARDS}
522
+ - ${CARD}"
523
+ CHARS_USED=$((CHARS_USED + CARD_LEN + 2))
524
+
525
+ done < <(echo "$CONTRACT_FILES")
526
+
527
+ CARDS_TOKEN_COUNT=$((CHARS_USED / 4))
528
+ if [ -n "$DANGER_ZONE_CARDS" ]; then
529
+ echo "Phase C0.5: emitting ${CARDS_TOKEN_COUNT} tokens of danger-zone cards (cap: ${TOKEN_BUDGET})"
530
+ else
531
+ echo "Phase C0.5: no danger-zone entries for contract files — skipping section"
532
+ fi
533
+ ```
534
+
535
+ ### Step 3: Store for output
536
+
537
+ `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.
538
+
269
539
  ---
270
540
 
271
541
  ## Phase C0: Prior Investigation Findings (from Gists)
@@ -406,30 +676,73 @@ If `GIST_SUMMARIES` is non-empty, it will be included in the `### Prior Investig
406
676
 
407
677
  ## Phase C1: Past Review Findings on These Files
408
678
 
679
+ **Primary path — Forge Ledger** (O(1) local index lookup, zero API calls): <!-- Added: forge#1732 -->
680
+
681
+ Check for a local knowledge index before making any GitHub API calls. If the index exists, use
682
+ `forge recall` for exact file-path lookups. Fall back to live `gh issue list --search` only when
683
+ the index is absent or returns no results for a given file.
684
+
685
+ ```bash
686
+ # Resolve recall CLI path relative to repository root
687
+ RECALL_PATH="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}/bin/recall.mjs"
688
+ LEDGER_AVAILABLE=0
689
+
690
+ if [ -f "$RECALL_PATH" ]; then
691
+ # Quick probe: does the index exist and have cards?
692
+ PROBE=$(node "$RECALL_PATH" --doctor 2>/dev/null | grep "^Total cards:" | grep -v "^Total cards: 0" || true)
693
+ [ -n "$PROBE" ] && LEDGER_AVAILABLE=1
694
+ fi
695
+
696
+ LEDGER_FINDINGS=""
697
+
698
+ if [ "$LEDGER_AVAILABLE" -eq 1 ]; then
699
+ echo "[context:C1] Using Forge Ledger for file-path recall (zero API calls)"
700
+ IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
701
+ for file in "${AFFECTED_FILES_ARR[@]}"; do
702
+ FILE_CARDS=$(node "$RECALL_PATH" --file "$file" --k 5 --json 2>/dev/null || echo "[]")
703
+ if [ "$FILE_CARDS" != "[]" ] && [ -n "$FILE_CARDS" ]; then
704
+ LEDGER_FINDINGS="${LEDGER_FINDINGS}
705
+ ### Ledger findings for \`${file}\`
706
+ \`\`\`json
707
+ ${FILE_CARDS}
708
+ \`\`\`"
709
+ fi
710
+ done
711
+ fi
712
+ ```
713
+
714
+ **Extract from ledger results** (when `LEDGER_AVAILABLE=1` and `LEDGER_FINDINGS` is non-empty):
715
+ Parse the JSON card array. For each card: extract `kind`, `rootCause`, `pattern`, `prevention`,
716
+ `paths`, `symbols`, `issue` (citation). Include in the FORGE:CONTEXT output under
717
+ **### Known Pitfalls for This Area** (pattern/stale cards) and **### Historical Findings on These
718
+ Files** (investigation cards). Cards with `status: "stale"` are noted as such but still included
719
+ — they may describe a bug class that has since moved files.
720
+
721
+ **Fallback path — live GitHub search** (when `LEDGER_AVAILABLE=0` or `LEDGER_FINDINGS` is empty):
722
+
409
723
  Query closed issues with `review-finding` label, searching by filename:
410
724
 
411
725
  ```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
726
+ if [ "$LEDGER_AVAILABLE" -eq 0 ] || [ -z "$LEDGER_FINDINGS" ]; then
727
+ echo "[context:C1] Forge Ledger unavailable or empty — falling back to live gh search"
728
+ IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
729
+ for file in "${AFFECTED_FILES_ARR[@]}"; do
730
+ basename=$(basename "$file" .py)
731
+ gh issue list -R {GH_REPO} \
732
+ --state closed \
733
+ --label "review-finding" \
734
+ --search "$basename" \
735
+ --limit 10 \
736
+ --json number,title,body \
737
+ --jq '.[] | {
738
+ number,
739
+ title,
740
+ pattern: (.body | capture("\\*\\*Pattern\\*\\*: *(?<p>[^\\n]+)").p // null),
741
+ prevention: (.body | capture("\\*\\*Prevention\\*\\*: *(?<v>[^\\n]+)").v // null),
742
+ root_cause: (.body | capture("\\*\\*Root cause\\*\\*: *(?<rc>[^\\n]+)").rc // (.body | capture("Root Cause[^\\n]*\\n(?<rc>[^\\n]+)").rc // "see body"))
743
+ }'
744
+ done
745
+ fi
433
746
  ```
434
747
 
435
748
  Keep findings where the filename or function name appears in the title or body. Discard false matches (same word, different module).
@@ -526,6 +839,15 @@ Use 2-3 keywords from the issue title. If no results, skip this phase — do not
526
839
 
527
840
  ## Output Format
528
841
 
842
+ **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.
843
+
844
+ ```bash
845
+ # Codec produces the opening tag and completion sentinel
846
+ CONTEXT_BODY=$(node packages/protocol/src/cli.js emit CONTEXT)
847
+ # $CONTEXT_BODY = "<!-- FORGE:CONTEXT -->\n<!-- FORGE:CONTEXT:COMPLETE -->"
848
+ # Insert the Markdown sections between the opening tag line and the sentinel.
849
+ ```
850
+
529
851
  Post the following as a GitHub comment on `{NUMBER}`:
530
852
 
531
853
  ```bash
@@ -540,12 +862,26 @@ gh issue comment {NUMBER} -R {GH_REPO} --body "<!-- FORGE:CONTEXT -->
540
862
  'No devdocs found at {DEVDOCS_PATH} — skipping. Run `npx forgedock docs init` to scaffold.' -->
541
863
  {DEVDOCS_CONTENT}
542
864
 
865
+ ### Danger-Zone Rule Cards
866
+ <!-- Ranked risk cards for contract-overlapping files from the Forge Ledger danger-zones index (Phase C0.5).
867
+ Token budget: {CARDS_TOKEN_COUNT} / 400 tokens used.
868
+ Each card: {file} — {N} findings/90d — recurring: {pattern} (#{issue}); rule: {prevention}
869
+ BINDING CONSTRAINT: treat each card as a must-not-violate rule before committing.
870
+ If danger-zones.json was absent or no contract files had findings — omit this section entirely (no empty scaffolding). -->
871
+ {DANGER_ZONE_CARDS}
872
+
543
873
  ### Prior Investigation Findings
544
874
  <!-- Summarized Knowledge Gist content from upstream investigations (Phase C0).
545
875
  If no FORGE:PRIOR_GIST annotations were found in the issue body: omit this section entirely.
546
876
  If Gist fetches failed: include the failure note so the builder knows context was attempted. -->
547
877
  {GIST_SUMMARIES}
548
878
 
879
+ ### Claims Board Constraints
880
+ <!-- Active peer claims from the orchestration coordination issue (Phase C-0.5).
881
+ If FORGE_COORD_ISSUE is not set (not running under /orchestrate): omit this section entirely.
882
+ If no peer claims overlap this agent's planned files: write 'No overlapping peer claims.' -->
883
+ {PEER_CLAIMS_CONSTRAINT}
884
+
549
885
  ### Known Pitfalls for This Area
550
886
  <!-- Structured prevention rules extracted from past review-finding issues (Pattern Metadata section).
551
887
  If a finding has a Prevention field, list it here. Builder MUST read these before writing code.
@@ -607,7 +943,9 @@ This module runs at **Step 3C.5** — after Builder Contract is posted, before I
607
943
  ```
608
944
  3C → Builder Contract posted
609
945
  3C.5 → [THIS MODULE] Context gathering (max 2 min)
610
- Phase C-1: Authoritative Devdocs (project-resident knowledge — highest precedence)
946
+ Phase C-1: Authoritative Devdocs (project-resident knowledge — highest precedence)
947
+ Phase C-0.5: Active Peer Claims Reader (conditional — orchestration only)
948
+ Phase C0.5: Danger-Zone Rule Cards (fixed 400-token slot — forge#1744)
611
949
  Phase C0: Prior Investigation Findings (from Gists)
612
950
  Phase C1: Past Review Findings on These Files
613
951
  Phase C2: Past Bugs in the Same Module
@@ -12,7 +12,7 @@ argument-hint: [issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--worktree P
12
12
  **Invoked by**: `work-on.md` Step 3F, after worktree is created and context is gathered.
13
13
  **Output**: Write code, commit(s), post `<!-- FORGE:BUILDER -->` comment, return result to caller.
14
14
 
15
- **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
15
+ **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
16
16
  **NEVER use plan mode (EnterPlanMode).**
17
17
 
18
18
  <!-- FORGE:SPEC_LOADED — work-on/build/implement.md loaded and active. Agent is bound by this spec. -->
@@ -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
@@ -12,7 +12,7 @@ argument-hint: [issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--worktree P
12
12
  **Invoked by**: `work-on.md` Step 3F.5, after `implement.md` has written and staged code (not committed).
13
13
  **Output**: Return `GATE_PASSED: true/false` to caller. On failure after max iterations, post comment and set `needs-human`.
14
14
 
15
- **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
15
+ **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
16
16
  **NEVER use plan mode (EnterPlanMode).**
17
17
 
18
18
  <!-- FORGE:SPEC_LOADED — work-on/build/validate.md loaded and active. Agent is bound by this spec. -->
@@ -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:
@@ -12,7 +12,7 @@ argument-hint: [issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BA
12
12
  **Invoked by**: `work-on.md` Phase 3 — entered when the issue carries label `workflow:ready-to-build` or `workflow:building` (see Universal Phase Dispatcher in work-on.md).
13
13
  **Output**: Create worktree, post contract, run build phases, return result to work-on.md.
14
14
 
15
- **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
15
+ **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (3B classification, 3D label transitions) stay at this tier because they're interleaved with the reasoning-heavy build steps (3C.5/3C.6/3F) in the same `Skill()` invocation — see `work-on.md` section "Model and Effort Tiering — What Actually Applies". <!-- Added: forge#1827 -->
16
16
  **NEVER use plan mode (EnterPlanMode).**
17
17
 
18
18
  **CRITICAL: You MUST execute ALL phases B0–B6 in order. Phases B3 (context) and B4 (architect) are skipped ONLY when COMPLEXITY_BAND: TRIVIAL (read from FORGE:FAST_PATH comment in Phase B0). For STANDARD and COMPLEX tasks they are NOT optional — skipping them degrades build quality.**
@@ -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
@@ -329,17 +373,42 @@ The acceptance spec contains only a skip sentinel (\`type=skipped\`). No automat
329
373
  <!-- FORGE:ACCEPTANCE_GATE:PASSED -->"
330
374
  ```
331
375
 
332
- **Otherwise — execute each check**:
376
+ **Otherwise — execute each check**: <!-- Added: forge#1829 -->
333
377
 
334
378
  ```bash
335
379
  GATE_PASS=true
336
380
  FAILED_CHECKS=""
337
381
 
338
382
  while IFS= read -r check_line; do
339
- ID=$(echo "$check_line" | sed -n 's/.*id=\([^ ]*\).*/\1/p')
340
- TYPE=$(echo "$check_line" | sed -n 's/.*type=\([^ ]*\).*/\1/p')
341
- TARGET=$(echo "$check_line"| sed -n 's/.*target=\([^ ]*\).*/\1/p')
342
- MATCHER=$(echo "$check_line"| sed -n 's/.*matcher=\([^ ]*\).*/\1/p')
383
+ # Fields are extracted by anchoring each sed pattern to `^ACCEPTANCE_CHECK:` and walking
384
+ # through the fixed field order from investigate.md (id= type= target="..." matcher="..."
385
+ # description=<free text to EOL>) instead of truncating the line before extraction. This
386
+ # is deliberate: description= is unconstrained free text and can legitimately contain
387
+ # key="value"-shaped substrings (e.g. `description=works when target="prod"`), while
388
+ # target=/matcher= are themselves free-form shell commands/regexes and can legitimately
389
+ # contain the literal substring "description=" (e.g. `target="grep -c description= file"`).
390
+ # A prior fix truncated the line at the first description= to keep free text from being
391
+ # mistaken for a real field — but that truncation then broke whenever a *real* target=/
392
+ # matcher= value contained "description=" text, cutting mid-quote. Anchoring from the start
393
+ # of the line through each preceding field in order avoids truncation entirely: every
394
+ # anchored pattern matches only the one fixed position where that field can occur, so
395
+ # neither direction of collision (fake fields inside description=, or description=-like
396
+ # text inside target=/matcher=) can hijack extraction. Falls back to the full line
397
+ # unchanged if a field is absent (no-op, safe for malformed lines).
398
+ ID=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=\([^ ]*\) type=.*/\1/p')
399
+ TYPE=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=\([^ ]*\) target=.*/\1/p')
400
+ # target=/matcher= are quoted (target="..." matcher="...") per the investigate.md wire format —
401
+ # quoting is required so multi-word/piped shell-command values (e.g. `target="grep -qE '...' file"`)
402
+ # survive extraction instead of being truncated at the first space. The quote-bounded pattern
403
+ # (`"\([^"]*\)"`) captures everything up to the next literal quote verbatim, including a literal
404
+ # "description=" substring inside the value. Fall back to the legacy unquoted [^ ]* extraction
405
+ # only for older ACCEPTANCE_CHECK comments emitted before this fix (still correct for
406
+ # single-token exists/contains targets; multi-word legacy targets remain truncated until the
407
+ # issue's investigation is re-run to emit the quoted format).
408
+ TARGET=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target="\([^"]*\)".*/\1/p')
409
+ [ -z "$TARGET" ] && TARGET=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=\([^ ]*\).*/\1/p')
410
+ MATCHER=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target="[^"]*" matcher="\([^"]*\)".*/\1/p')
411
+ [ -z "$MATCHER" ] && MATCHER=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=[^ ]* matcher=\([^ ]*\).*/\1/p')
343
412
  DESC=$(echo "$check_line" | sed -n 's/.*description=\(.*\)/\1/p')
344
413
 
345
414
  [ "$TYPE" = "skipped" ] && continue