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.
- package/README.md +10 -33
- package/bin/cinema.mjs +23 -5
- package/bin/engine/invariants.mjs +361 -0
- package/bin/engine-cli.mjs +137 -11
- package/bin/env-detect.mjs +249 -0
- package/bin/forge-utils.mjs +364 -4
- package/bin/forgedock.mjs +1064 -135
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/hooks/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +917 -16
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +557 -0
- package/bin/registry.mjs +91 -3
- package/bin/runner.mjs +38 -2
- package/bin/tui.mjs +49 -3
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +8 -8
- package/commands/audit.md +1 -1
- package/commands/autopilot.md +958 -77
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +139 -55
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/explain.md +1 -1
- package/commands/fix-ci.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/incident-response.md +3 -3
- package/commands/issue.md +1 -1
- package/commands/optimize.md +586 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +36 -26
- package/commands/orchestrate/phase-2-triage.md +1 -1
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +587 -48
- package/commands/orchestrate/phase-4-execution.md +1031 -57
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +248 -11
- package/commands/orchestrate/safety.md +2 -2
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +418 -1
- package/commands/recover-orphans.md +5 -5
- package/commands/replay.md +1 -1
- package/commands/review-pr-agents/api.md +2 -2
- package/commands/review-pr-agents/auth.md +2 -2
- package/commands/review-pr-agents/billing.md +2 -2
- package/commands/review-pr-agents/concurrency.md +2 -2
- package/commands/review-pr-agents/database.md +2 -2
- package/commands/review-pr-agents/frontend.md +2 -2
- package/commands/review-pr-agents/infra.md +2 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents/scraper.md +2 -2
- package/commands/review-pr-agents/security.md +2 -2
- package/commands/review-pr-staging.md +41 -3
- package/commands/review-pr.md +462 -19
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +429 -0
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +53 -1
- package/commands/work-on/build/architect.md +78 -1
- package/commands/work-on/build/context.md +360 -22
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +29 -1
- package/commands/work-on/build.md +75 -6
- package/commands/work-on/close.md +624 -10
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +235 -5
- package/commands/work-on/remediate.md +331 -0
- package/commands/work-on/review.md +1 -1
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +205 -66
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -0
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/site/command-reference.md +31 -1
- package/docs/site/getting-started.md +9 -34
- package/docs/site/index.md +1 -1
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +3 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +789 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +245 -6
- 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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|