forgedock 1.16.2 → 1.18.0

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 (114) hide show
  1. package/bin/engine/outcome-codes.mjs +114 -0
  2. package/bin/engine/phases.mjs +211 -100
  3. package/bin/engine/projector.mjs +48 -1
  4. package/bin/engine/runlog.mjs +20 -5
  5. package/bin/engine/telemetry-seam.mjs +62 -0
  6. package/bin/engine/telemetry-seam.test.mjs +60 -0
  7. package/bin/engine/telemetry.mjs +148 -0
  8. package/bin/engine/telemetry.test.mjs +120 -0
  9. package/bin/engine-cli.mjs +17 -1
  10. package/bin/engine.mjs +227 -48
  11. package/bin/forgedock.mjs +8 -2
  12. package/bin/hooks/interactive-engine.mjs +10 -0
  13. package/bin/hooks/pre-tool-use.mjs +220 -0
  14. package/bin/runner.mjs +70 -4
  15. package/bin/stats/aggregate.mjs +231 -0
  16. package/bin/stats/cache.mjs +40 -0
  17. package/bin/stats/github-source.mjs +211 -0
  18. package/bin/stats/index.mjs +154 -0
  19. package/bin/stats/local-source.mjs +79 -0
  20. package/bin/stats/package.json +6 -0
  21. package/bin/stats/render.mjs +82 -0
  22. package/bin/stats/stats.test.mjs +312 -0
  23. package/commands/cleanup.md +90 -2
  24. package/commands/deploy-info.md +8 -4
  25. package/commands/deploy-pr.md +64 -0
  26. package/commands/fix-ci.md +20 -6
  27. package/commands/milestone.md +34 -17
  28. package/commands/orchestrate/phase-1-resolve.md +4 -1
  29. package/commands/orchestrate/phase-4-execution.md +28 -6
  30. package/commands/orchestrate/phase-5-cleanup.md +18 -2
  31. package/commands/pipeline-health/phase-1-context.md +2 -0
  32. package/commands/pipeline-health/phase-2-metrics.md +12 -5
  33. package/commands/pipeline-status.md +21 -1
  34. package/commands/review-pr-agents/protocols.md +1 -0
  35. package/commands/review-pr-agents.md +10 -2
  36. package/commands/review-pr-staging.md +196 -496
  37. package/commands/review-pr.md +512 -3134
  38. package/commands/test-gate.md +25 -10
  39. package/commands/work-on/build/architect.md +13 -1
  40. package/commands/work-on/build/implement.md +28 -6
  41. package/commands/work-on/build/validate.md +5 -2
  42. package/commands/work-on/build.md +197 -145
  43. package/commands/work-on/close.md +66 -20
  44. package/commands/work-on/decompose.md +113 -34
  45. package/commands/work-on/investigate.md +15 -14
  46. package/commands/work-on/remediate.md +49 -19
  47. package/commands/work-on/review.md +7 -5
  48. package/commands/work-on.md +1 -2
  49. package/docs/CONFIG.md +76 -24
  50. package/docs/OUTCOME-CODES.md +53 -0
  51. package/docs/RELEASE-NOTES.md +24 -0
  52. package/docs/STATS.md +63 -0
  53. package/docs/TELEMETRY.md +65 -0
  54. package/docs/WORK-ON-RUNTIME.md +19 -0
  55. package/docs/site/command-reference.md +1 -1
  56. package/docs/spec/forge-protocol-v1.md +2 -2
  57. package/docs/spec/label-state-machine.md +1 -1
  58. package/package.json +3 -2
  59. package/packages/protocol/src/phases.js +5 -5
  60. package/scripts/builder-resume-match.test.sh +58 -0
  61. package/scripts/cap-findings.sh +110 -0
  62. package/scripts/cap-findings.test.sh +130 -0
  63. package/scripts/check-spec-bash.sh +5 -1
  64. package/scripts/check-spec-bash.test.sh +20 -0
  65. package/scripts/check-spec-markers.sh +2 -0
  66. package/scripts/classify-finding.sh +65 -16
  67. package/scripts/classify-finding.test.sh +43 -1
  68. package/scripts/classify-grep-exit.sh +20 -0
  69. package/scripts/classify-grep-exit.test.sh +46 -0
  70. package/scripts/close-parent-c3.test.sh +123 -0
  71. package/scripts/cost-report.sh +146 -0
  72. package/scripts/cost-report.test.sh +80 -0
  73. package/scripts/diff-size.sh +97 -3
  74. package/scripts/diff-size.test.sh +59 -2
  75. package/scripts/doctor-pipeline-state.sh +43 -0
  76. package/scripts/file-deferred-followups.sh +159 -0
  77. package/scripts/file-deferred-followups.test.sh +106 -0
  78. package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
  79. package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
  80. package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
  81. package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
  82. package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
  83. package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
  84. package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
  85. package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
  86. package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
  87. package/scripts/forge-root.test.sh +18 -11
  88. package/scripts/lib/fence-bootstrap.sh +4 -3
  89. package/scripts/lib/pr-issue-number.sh +18 -0
  90. package/scripts/lib/review-template-resolve.sh +41 -0
  91. package/scripts/lib/telemetry.sh +168 -0
  92. package/scripts/publish-staging-sync.test.sh +48 -0
  93. package/scripts/remediate-liveness.sh +42 -6
  94. package/scripts/resolve-deploy-gate.sh +32 -6
  95. package/scripts/resolve-deploy-gate.test.sh +42 -1
  96. package/scripts/review-acceptance-gate.sh +16 -7
  97. package/scripts/review-acceptance-gate.test.sh +13 -0
  98. package/scripts/review-finalize.sh +1427 -0
  99. package/scripts/review-finalize.test.sh +833 -0
  100. package/scripts/review-pr-delta-wiring.test.sh +19 -72
  101. package/scripts/review-pr-domain-map.test.sh +23 -13
  102. package/scripts/review-pr-gate2.test.sh +24 -65
  103. package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
  104. package/scripts/review-pr-staging-scope.test.sh +56 -80
  105. package/scripts/review-prepare.sh +553 -0
  106. package/scripts/review-prepare.test.sh +288 -0
  107. package/scripts/review-roster.sh +186 -0
  108. package/scripts/review-roster.test.sh +185 -0
  109. package/scripts/run-shell-tests.sh +3 -0
  110. package/scripts/telemetry.test.sh +106 -0
  111. package/scripts/trusted-comments.test.sh +9 -2
  112. package/scripts/wait-ci-green.sh +8 -1
  113. package/scripts/wait-ci-green.test.sh +3 -3
  114. package/templates/forge.yaml.minimal +9 -0
@@ -439,23 +439,38 @@ BASELINE_RESULTS=""
439
439
 
440
440
  # Run the same tests against the base branch state to establish baseline
441
441
  # If base-branch tests also fail, those are pre-existing failures — not batch-introduced
442
- git stash 2>/dev/null || true
443
- git checkout "origin/${BASE_BRANCH}" -- . 2>/dev/null || true
442
+ # Use a detached worktree at the base branch — never stash/checkout in the main checkout
443
+ BASE_WT=$(mktemp -d)
444
+ git -C "$REPO_PATH" worktree add --detach "$BASE_WT" "origin/${BASE_BRANCH}" 2>/dev/null \
445
+ || { echo "WARNING: could not create baseline worktree — baseline unavailable (failures will count as batch-introduced)"; rmdir "$BASE_WT" 2>/dev/null; BASE_WT=""; }
446
+ # A fresh worktree has no installed deps or local env files: link the main checkout's (read-only use), at the repo
447
+ # root and in every cluster working_dir, so a baseline cluster fails only for code reasons, never for a missing install.
448
+ if [ -n "$BASE_WT" ]; then
449
+ { echo .; yq '.verification.integration_tests[].working_dir // "."' "$CONFIG_FILE" 2>/dev/null; } | sort -u | while IFS= read -r _wd; do
450
+ for _dep in node_modules .venv venv .env .env.local .env.test; do
451
+ [ -e "${REPO_PATH}/${_wd}/${_dep}" ] && [ ! -e "${BASE_WT}/${_wd}/${_dep}" ] && [ -d "${BASE_WT}/${_wd}" ] \
452
+ && ln -s "${REPO_PATH}/${_wd}/${_dep}" "${BASE_WT}/${_wd}/${_dep}"
453
+ done
454
+ done # one working_dir per line: a path with spaces is never word-split
455
+ fi
444
456
 
445
457
  for i in $(seq 0 $((CLUSTER_COUNT - 1))); do
446
458
  CLUSTER=$(yq ".verification.integration_tests[${i}].cluster" "$CONFIG_FILE")
447
459
  CMD=$(yq ".verification.integration_tests[${i}].command" "$CONFIG_FILE")
448
460
  WORKING_DIR=$(yq ".verification.integration_tests[${i}].working_dir // \".\"" "$CONFIG_FILE")
449
461
 
450
- if (cd "${REPO_PATH}/${WORKING_DIR}" && eval "$CMD") 2>&1; then
462
+ if [ -z "$BASE_WT" ]; then
463
+ # No baseline: never claim pre-existing. A bundle failure in this cluster counts as batch-introduced (fail closed).
464
+ BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: UNAVAILABLE"
465
+ elif (cd "${BASE_WT}/${WORKING_DIR}" && eval "$CMD") 2>&1; then
451
466
  BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: PASS"
452
467
  else
453
468
  BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: FAIL (pre-existing)"
454
469
  fi
455
470
  done
456
471
 
457
- # Restore working state
458
- git checkout HEAD -- . 2>/dev/null || true
472
+ # Remove the baseline worktree (the main checkout was never touched)
473
+ [ -n "$BASE_WT" ] && git -C "$REPO_PATH" worktree remove --force "$BASE_WT" 2>/dev/null || true
459
474
 
460
475
  echo "Baseline results:"
461
476
  echo -e "$BASELINE_RESULTS"
@@ -475,10 +490,10 @@ while IFS= read -r result_line; do
475
490
  if echo "$STATUS" | grep -q "^FAIL"; then
476
491
  # Check if this cluster also failed in baseline
477
492
  BASELINE_STATUS=$(echo -e "$BASELINE_RESULTS" | grep "^- ${CLUSTER}:" | grep -oP ':\s+\K.+' || echo "PASS")
478
- if echo "$BASELINE_STATUS" | grep -q "^PASS"; then
479
- # This cluster passed in baseline but fails in the bundle — batch-introduced failure
493
+ if ! echo "$BASELINE_STATUS" | grep -q "^FAIL"; then
494
+ # Baseline PASS (or UNAVAILABLE: no proof it was pre-existing) but fails in the bundle — batch-introduced failure
480
495
  BATCH_FAILURES="${BATCH_FAILURES}\n${CLUSTER}"
481
- echo "BATCH-INTRODUCED FAILURE: cluster '${CLUSTER}' (baseline: PASS → bundle: FAIL)"
496
+ echo "BATCH-INTRODUCED FAILURE: cluster '${CLUSTER}' (baseline: ${BASELINE_STATUS} → bundle: FAIL)"
482
497
  else
483
498
  echo "PRE-EXISTING FAILURE: cluster '${CLUSTER}' (baseline: FAIL → bundle: FAIL — not this batch)"
484
499
  fi
@@ -534,7 +549,7 @@ else
534
549
  cat <<ISSUE_EOF > "$TESTGATE_FAIL_BODY_FILE"
535
550
  ## Problem
536
551
 
537
- The \`/test-gate\` command detected a runtime failure in cluster \`${cluster}\` that was introduced by the staging→${DEFAULT_BRANCH} bundle. The failure did not exist on \`${BASE_BRANCH}\` (baseline PASS), confirming it is batch-introduced, not pre-existing.
552
+ The \`/test-gate\` command detected a runtime failure in cluster \`${cluster}\` that was introduced by the staging→${DEFAULT_BRANCH} bundle. It was not observed failing on \`${BASE_BRANCH}\` (baseline: PASS, or UNAVAILABLE when the baseline could not run — then treated as batch-introduced, fail closed).
538
553
 
539
554
  ## Root Cause (if known)
540
555
 
@@ -574,7 +589,7 @@ $(echo -e "$SOURCE_ISSUES_LIST")
574
589
  \`\`\`
575
590
  3. Observe failure. Compare against \`${BASE_BRANCH}\` baseline:
576
591
  \`\`\`bash
577
- git checkout origin/${BASE_BRANCH} -- .
592
+ git worktree add --detach "$BASE_WT" origin/${BASE_BRANCH} # run the command inside $BASE_WT
578
593
  $(yq ".verification.integration_tests[] | select(.cluster == \"${cluster}\") | .command" "$CONFIG_FILE")
579
594
  \`\`\`
580
595
  ISSUE_EOF
@@ -719,6 +719,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
719
719
  |---|------|----------------|-----------------|-----|
720
720
  | 1 | {FILE} | {FUNCTION} | {CHANGE} | {REASON} |
721
721
 
722
+ ### Scope Baseline
723
+ <!-- Machine-readable scope baseline read by build B5.5's post-build scope check (forge#3862). Every
724
+ path in ### Affected Paths (including tests, docs and config the plan changes), one per line as
725
+ - path (repo-relative; a trailing slash means every file under that directory). The keys sit alone
726
+ on their lines. ESTIMATED_DIFF_LINES is the plan's own estimate (code plus tests, a whole number). -->
727
+ PLANNED_FILES:
728
+ - {FILE}
729
+ ESTIMATED_DIFF_LINES: {N}
730
+
722
731
  ### Pattern Sweep
723
732
  <!-- Pass-through from the FORGE:INVESTIGATOR '### Pattern Sweep' table (Phase A2.7).
724
733
  Omit this section when the investigation report had none. -->
@@ -799,13 +808,16 @@ SKIP_BODY=$(cat <<SKIP_EOF
799
808
 
800
809
  **Skipped**: {SKIP_REASON}. No cross-path consistency risk — the builder proceeds with the investigation report + context briefing.
801
810
 
811
+ ### Scope Baseline
812
+ PLANNED_FILES:
813
+
802
814
  <!-- FORGE:ARCHITECT:COMPLETE -->
803
815
  SKIP_EOF
804
816
  )
805
817
  gh issue comment {NUMBER} {GH_FLAG} --body "$SKIP_BODY" # <!-- allowlist:check-command-side-effects -->
806
818
  ```
807
819
 
808
- The empty-plan-with-marker changes no downstream behavior — the builder already falls back to the investigation report + contract when the plan is empty (see Integration Point below). The marker only makes the legitimate skip **observable** to the marker-only engine gate. This intentionally does **not** weaken crash detection: a genuinely interrupted architect run still posts no marker at all, so `detectOutcome` still fails-and-escalates it correctly.
820
+ The empty `PLANNED_FILES:` list means "no plan baseline": build B5.5 then skips the out-of-plan check (proceed, never block) and falls back to the investigation's `ESTIMATED_DIFF_LINES`. The empty-plan-with-marker changes no downstream behavior — the builder already falls back to the investigation report + contract when the plan is empty (see Integration Point below). The marker only makes the legitimate skip **observable** to the marker-only engine gate. This intentionally does **not** weaken crash detection: a genuinely interrupted architect run still posts no marker at all, so `detectOutcome` still fails-and-escalates it correctly.
809
821
 
810
822
  ---
811
823
 
@@ -69,15 +69,36 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
69
69
  --jq '.[] | select(.body | contains("FORGE:CONTEXT")) | .body'
70
70
  ```
71
71
 
72
- **Resume check**:
73
- - If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → implementation is done — EXIT and print `IMPLEMENT_RESULT: status: ALREADY_DONE` as the final reply.
74
- - If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → implementation was interrupted after the comment was posted but before the commit (validate.md V5). Delete the partial comment and restart from Phase I2:
72
+ **Resume check** (current-cycle slice only, same as build.md B0 <!-- Added: forge#3666 -->): every read below runs on the comments AFTER the newest trusted `<!-- FORGE:CYCLE: n -->` marker, so a cycle-1 `BUILDER:COMPLETE` never yields `ALREADY_DONE` in cycle 2 (the build would then proceed on an empty branch). `cycle_slice` is declared verbatim from build.md B0:
75
73
  ```bash
76
- PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
77
- --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
74
+ cycle_slice() {
75
+ jq -s --arg assoc "${FORGE_TRAIL_TRUSTED_ASSOCIATIONS-OWNER,MEMBER,COLLABORATOR}" --arg logins "${FORGE_TRAIL_TRUSTED_LOGINS-}" '[.[][]] as $c
76
+ | ($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
77
+ | ($logins | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $L
78
+ | ([$c | to_entries[]
79
+ | select(((.value.author_association // "") as $x | $A | index($x) != null) or ((.value.user.type // "") == "Bot") or ((.value.user.login // "") as $x | $L | index($x) != null))
80
+ | select((.value.body // "") | test("\\A\\s*<!--\\s*FORGE:CYCLE:") and test("(?m)^[ \\t]*<!--[ \\t]*FORGE:CYCLE:[ \\t]*[0-9]+[ \\t]*-->[ \\t]*$"))
81
+ | .key] | last) as $i
82
+ | if $i == null then $c else $c[($i + 1):] end'
83
+ }
84
+ # Capture the read first (as build.md B0): piped straight into jq -s, a failed gh call still yields `[]` and exit 0.
85
+ COMMENTS_JSON=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments) \
86
+ && CYCLE_SLICE=$(printf '%s' "$COMMENTS_JSON" | cycle_slice) && [ -n "$CYCLE_SLICE" ] \
87
+ || { echo "IMPLEMENT_RESULT: status: BLOCKED (cycle-lookup-unavailable: could not read FORGE:CYCLE)"; exit 1; } # a failed read is NEVER "no marker"
88
+ BUILDER_DONE=$(printf '%s' "$CYCLE_SLICE" | jq '[.[] | select((.body // "") | startswith("<!-- FORGE:BUILDER")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | select(.body | contains("<!-- FORGE:BUILDER:COMPLETE -->"))] | length')
89
+ ```
90
+ - If `BUILDER_DONE` > 0 (a `<!-- FORGE:BUILDER:COMPLETE -->` BUILDER comment in the current-cycle slice) → implementation is done — EXIT and print `IMPLEMENT_RESULT: status: ALREADY_DONE` as the final reply.
91
+ - If `<!-- FORGE:BUILDER -->` exists in the current-cycle slice BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → implementation was interrupted after the comment was posted but before the commit (validate.md V5). Delete the partial comment and restart from Phase I2:
92
+ Select with ONE anchored, trusted selector (body must start with the marker; author must be a Bot or OWNER/MEMBER/COLLABORATOR; mirrors `scripts/trusted-comments.sh`). A comment that merely mentions the marker (INVESTIGATOR, CONTEXT, ARCHITECT) never matches. Delete ONLY when exactly one non-complete candidate exists and it carries no other FORGE phase marker; unparsable, zero, or multiple matches delete nothing (fail closed). <!-- Added: forge#3668 -->
93
+ ```bash
94
+ # only a current-cycle partial is ever deleted; prior-cycle comments are never touched
95
+ BLD=$(printf '%s' "$CYCLE_SLICE" | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|ARCHITECT)"))}]') || BLD=""
96
+ PARTIAL_ID=$(printf '%s' "$BLD" | jq -r '[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end' 2>/dev/null)
78
97
  if [ -n "$PARTIAL_ID" ]; then
79
98
  gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
80
99
  echo "Deleted partial FORGE:BUILDER comment (no FORGE:BUILDER:COMPLETE) — restarting implementation"
100
+ else
101
+ echo "builder resume: no single safe partial BUILDER comment — deleting nothing"
81
102
  fi
82
103
  ```
83
104
 
@@ -144,6 +165,7 @@ Work in `{WORKTREE_PATH}`. Follow the contract deliverables table exactly — im
144
165
  - For each acceptance criterion in the contract: implement it, then mentally verify it's met
145
166
  - Do NOT add unrequested scope — contract out-of-scope items stay out of scope
146
167
  - **Library callback verification**: When writing a lambda or callable that will be passed to a library/framework parameter (e.g., `prepared_statement_name_func=lambda: ""`, `key=lambda x: ...`), you MUST verify the expected calling convention BEFORE writing it. Check the library's default value for that parameter, its documentation, or its source code. A lambda with wrong arity causes `TypeError` at runtime — this is invisible to static analysis and linting. The P0 incident from PR #14391 was caused by `lambda _: ""` (1 arg) passed where SQLAlchemy expects 0 args.
168
+ - **Scratch placement (venvs, caches)**: create virtualenvs, package caches and other large scratch data under `"$TMPDIR/forge-venv-{NUMBER}"` (the engine exports a per-run, disk-backed `TMPDIR`) or inside the worktree. Never use a literal `/tmp/<name>` path: a size-limited `/tmp` fails concurrent lanes with EDQUOT/ENOSPC. Remove what you created when the run finishes. <!-- Added: forge#3572 -->
147
169
  - **Worktree-aware path derivation**: When writing shell code that derives repository paths, ALWAYS use `git rev-parse --show-toplevel` for the repo root in regular checkouts, or `git rev-parse --git-common-dir` (then `dirname`) to get the shared `.git` directory when the context may be a linked worktree. For worktree cleanup code specifically, ALWAYS use `--git-common-dir` — `--show-toplevel` returns the worktree path itself, NOT the main repo root. NEVER use `pwd`, relative paths, or `dirname` chains on `--show-toplevel` output for repo root derivation. Test path logic for both regular checkouts and linked worktrees. (Ref: review-findings #104, #105 — 4 PATH_DERIVATION defects, 15% of all review findings)
148
170
  - **State machine completeness verification**: When implementing routing logic, state transition tables, or phase dispatch code (e.g., adding a `Skill()` call in a routing loop, adding a new phase to a state machine, creating a new subcommand file), you MUST run three checks BEFORE staging: (1) **Routing target existence** — for each `Skill("subcommand", ...)` call or file reference in routing logic, verify the target file exists at `commands/{subcommand-path}.md`; (2) **State reachability** — for each declared state or phase, verify at least one prior state or entry condition in the router routes to it; (3) **Subcommand invocation wiring** — for each `commands/work-on/*.md` file that declares its invocation condition (e.g., `Invoked by work-on.md routing loop, when X`), verify that condition is actually handled in the router. A missing target file, an unreachable state, or a declared-but-unwired subcommand will not surface until review. (Ref: review-findings #85, #116, #137 — 4 ROUTING defects, 15% of all review findings)
149
171
  - **Migration safety checklist** *(trigger: diff includes `*.sql` files or files under a `migrations/` path)*: When any migration file is in the diff, verify ALL of the following BEFORE staging. Fix each violation inline — do not defer to review or the quality gate: (a) **Rollback file**: a corresponding down/rollback migration file exists (e.g. `0042_down_*.sql`, `rollback_*.sql`) or the migration is explicitly self-reversing (DROP of a previously-added column); (b) **NOT NULL safety**: any `ADD COLUMN ... NOT NULL` either includes a `DEFAULT` clause or is preceded by a backfill step — a NOT NULL column without DEFAULT locks the table and fails on existing rows; (c) **Constraint name consistency**: constraint names in the migration match ORM model declarations — a mismatch causes `alembic stamp` and FK introspection to silently diverge; (d) **CREATE TRIGGER idempotency**: every `CREATE TRIGGER` uses `CREATE OR REPLACE TRIGGER` or is preceded by `DROP TRIGGER IF EXISTS` — a bare `CREATE TRIGGER` fails on re-run in test and CI environments; (e) **Migration prefix uniqueness**: confirm the new file's numeric prefix does not already exist in the `infra/migrations/` tree (`ls infra/migrations/*.sql | xargs -n1 basename | grep -oE '^[0-9]+' | sort | uniq -d` prints duplicates) — a duplicate prefix hard-fails the deploy gate regardless of file content. <!-- Added: forge#373 -->
@@ -284,7 +306,7 @@ git add {CHANGED_FILES}
284
306
 
285
307
  Do NOT run `git commit` here. The commit (with conventional commit message and issue reference) is made by validate.md Phase V5 after `GATE_PASSED=true`.
286
308
 
287
- **Informational diff size** <!-- Added: forge#3450 -->: after staging, read the changed-line count so the builder comment and result carry it. This is a hint only. Implement never gates or decides: the diff-size gate lives in build B5.5, which re-measures authoritatively. Leave `DIFF_LINES` empty when the script is unavailable or fails.
309
+ **Informational diff size** <!-- Added: forge#3450 -->: after staging, read the changed-line count so the builder comment and result carry it. This is a hint only. Implement never gates or decides: the scope check lives in build B5.5, which re-measures authoritatively against the plan (it needs only the `diff_lines=` key here). Leave `DIFF_LINES` empty when the script is unavailable or fails.
288
310
 
289
311
  ```bash
290
312
  # <Script resolution block from work-on/build.md, verbatim>
@@ -153,6 +153,8 @@ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-com
153
153
 
154
154
  Run after quality gate passes. All tool commands are read from `forge.yaml → verification.commands`. `forge.yaml` is usually gitignored, so it is absent from the worktree: every block below resolves `FORGE_CFG` to the worktree copy if present, else the main checkout's `forge.yaml` (via `git rev-parse --git-common-dir`). Before this, every verification command, learned test command and the SOPS chain check were silently skipped in worktree builds; each step logs `SKIPPED — not configured` when the corresponding key is absent rather than silently passing.
155
155
 
156
+ **Scratch placement (venvs, caches)** <!-- Added: forge#3572 -->: any virtualenv, package cache or other large scratch data a verification command needs goes under `"$TMPDIR/forge-venv-{NUMBER}"` (the engine exports a per-run, disk-backed `TMPDIR`) or inside the worktree, never a literal `/tmp/<name>` path. A size-limited `/tmp` fails concurrent lanes with EDQUOT/ENOSPC. Remove what you created when validation finishes.
157
+
156
158
  **Track skipped checks** — initialize before any check runs:
157
159
  ```bash
158
160
  SKIPPED_CHECKS=""
@@ -710,8 +712,9 @@ else
710
712
  fi
711
713
 
712
714
  # Find the FORGE:BUILDER comment posted by implement.md Phase I6
713
- BUILDER_COMMENT_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
714
- --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
715
+ # Anchored, trusted, single-match selector (forge#3668): zero/many/unparsable → empty → append skipped.
716
+ BLD=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|ARCHITECT)"))}]') || BLD=""
717
+ BUILDER_COMMENT_ID=$(printf '%s' "$BLD" | jq -r '[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end' 2>/dev/null)
715
718
 
716
719
  if [ -n "$BUILDER_COMMENT_ID" ]; then
717
720
  CURRENT_BODY=$(gh api repos/{GH_REPO}/issues/comments/$BUILDER_COMMENT_ID --jq '.body')