forgedock 1.29.1 → 1.31.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 (166) hide show
  1. package/bin/engine/outcome-codes.mjs +5 -2
  2. package/bin/engine/phases.mjs +20 -3
  3. package/bin/engine/runlog.mjs +82 -12
  4. package/bin/engine/telemetry-seam.mjs +11 -4
  5. package/bin/engine/telemetry-seam.test.mjs +18 -0
  6. package/bin/engine-cli.mjs +453 -66
  7. package/bin/engine.mjs +50 -38
  8. package/bin/forgedock.mjs +35 -3
  9. package/bin/gc.mjs +50 -17
  10. package/bin/labels.json +5 -0
  11. package/bin/lean/apishape.mjs +170 -0
  12. package/bin/lean/apishape.test.mjs +187 -0
  13. package/bin/lean/batch.mjs +32 -0
  14. package/bin/lean/batch.test.mjs +356 -2
  15. package/bin/lean/budget.mjs +89 -0
  16. package/bin/lean/budget.test.mjs +106 -0
  17. package/bin/lean/checks.mjs +32 -9
  18. package/bin/lean/checks.test.mjs +78 -1
  19. package/bin/lean/conformance.e2e.test.mjs +27 -5
  20. package/bin/lean/conformance.mjs +278 -22
  21. package/bin/lean/conformance.test.mjs +251 -5
  22. package/bin/lean/context.mjs +133 -55
  23. package/bin/lean/context.test.mjs +175 -9
  24. package/bin/lean/decompose.mjs +90 -0
  25. package/bin/lean/decompose.test.mjs +105 -0
  26. package/bin/lean/depcache.mjs +11 -7
  27. package/bin/lean/depcache.test.mjs +81 -0
  28. package/bin/lean/e2e.mjs +102 -0
  29. package/bin/lean/e2e.test.mjs +86 -0
  30. package/bin/lean/effects.mjs +55 -19
  31. package/bin/lean/enumerating.mjs +189 -0
  32. package/bin/lean/enumerating.test.mjs +97 -0
  33. package/bin/lean/explore.mjs +47 -0
  34. package/bin/lean/explore.test.mjs +61 -0
  35. package/bin/lean/facts.mjs +84 -0
  36. package/bin/lean/facts.test.mjs +66 -0
  37. package/bin/lean/fixround.mjs +90 -0
  38. package/bin/lean/fixround.test.mjs +117 -0
  39. package/bin/lean/heavy.mjs +35 -2
  40. package/bin/lean/heavy.test.mjs +32 -2
  41. package/bin/lean/lean.mjs +799 -127
  42. package/bin/lean/lean.test.mjs +1749 -37
  43. package/bin/lean/ledger.mjs +5 -3
  44. package/bin/lean/ledger.test.mjs +18 -0
  45. package/bin/lean/light.mjs +135 -0
  46. package/bin/lean/light.test.mjs +173 -0
  47. package/bin/lean/limits.test.mjs +36 -1
  48. package/bin/lean/orchestrate.mjs +341 -99
  49. package/bin/lean/pitfalls.mjs +136 -0
  50. package/bin/lean/pitfalls.test.mjs +117 -0
  51. package/bin/lean/postmerge.mjs +85 -0
  52. package/bin/lean/postmerge.test.mjs +79 -0
  53. package/bin/lean/precheck.mjs +180 -0
  54. package/bin/lean/precheck.test.mjs +205 -0
  55. package/bin/lean/prefilter.mjs +180 -9
  56. package/bin/lean/prefilter.test.mjs +175 -3
  57. package/bin/lean/preflight.mjs +238 -0
  58. package/bin/lean/preflight.test.mjs +108 -0
  59. package/bin/lean/probe.mjs +297 -0
  60. package/bin/lean/probe.test.mjs +210 -0
  61. package/bin/lean/prompts.mjs +134 -12
  62. package/bin/lean/recovery.test.mjs +75 -0
  63. package/bin/lean/render.mjs +7 -3
  64. package/bin/lean/resume.mjs +67 -0
  65. package/bin/lean/resume.test.mjs +84 -0
  66. package/bin/lean/review-trigger.mjs +12 -0
  67. package/bin/lean/review-trigger.test.mjs +17 -1
  68. package/bin/lean/routing.mjs +74 -0
  69. package/bin/lean/routing.test.mjs +67 -0
  70. package/bin/lean/sched.test.mjs +127 -1
  71. package/bin/lean/schedd.mjs +55 -9
  72. package/bin/lean/schedd.test.mjs +149 -3
  73. package/bin/lean/selfreview.mjs +37 -0
  74. package/bin/lean/selfreview.test.mjs +51 -0
  75. package/bin/lean/seqlease.mjs +40 -0
  76. package/bin/lean/seqlease.test.mjs +101 -0
  77. package/bin/lean/sequence.mjs +385 -0
  78. package/bin/lean/sequence.test.mjs +373 -0
  79. package/bin/lean/testmap.mjs +43 -6
  80. package/bin/lean/testmap.test.mjs +38 -1
  81. package/bin/lean/toolchain.mjs +60 -15
  82. package/bin/lean/toolchain.test.mjs +120 -3
  83. package/bin/lean/toolstats.mjs +72 -0
  84. package/bin/lean/toolstats.test.mjs +47 -0
  85. package/bin/lean/verifier.mjs +232 -0
  86. package/bin/lean/verifier.test.mjs +163 -0
  87. package/bin/lean/verify.mjs +77 -10
  88. package/bin/lean/verify.test.mjs +74 -3
  89. package/bin/review-staging.mjs +533 -0
  90. package/bin/runner.mjs +82 -13
  91. package/bin/stats/aggregate.mjs +87 -2
  92. package/bin/stats/efficiency.mjs +319 -0
  93. package/bin/stats/efficiency.test.mjs +233 -0
  94. package/bin/stats/github-source.mjs +8 -3
  95. package/bin/stats/index.mjs +69 -6
  96. package/bin/stats/local-source.mjs +86 -3
  97. package/bin/stats/query.mjs +48 -0
  98. package/bin/stats/render.mjs +22 -0
  99. package/bin/stats/stats.test.mjs +224 -5
  100. package/commands/orchestrate.md +6 -2
  101. package/commands/review-pr-agents/protocols.md +1 -1
  102. package/commands/review-pr-staging.md +84 -831
  103. package/commands/review-pr.md +1 -1
  104. package/commands/work-on/build/implement.md +1 -0
  105. package/commands/work-on/build/validate.md +11 -1
  106. package/docs/CONFIG.md +88 -6
  107. package/docs/OUTCOME-CODES.md +7 -4
  108. package/docs/RELEASE-NOTES.md +10 -0
  109. package/docs/STATS.md +53 -1
  110. package/docs/TELEMETRY.md +1 -1
  111. package/docs/spec/review-protocol.md +1 -1
  112. package/package.json +1 -1
  113. package/scripts/acceptance-red-green.sh +13 -4
  114. package/scripts/acceptance-red-green.test.sh +14 -0
  115. package/scripts/ci-local/lib.mjs +20 -0
  116. package/scripts/ci-local/plan.mjs +2 -12
  117. package/scripts/ci-local/scoping.mjs +10 -1
  118. package/scripts/ci-local.test.sh +2 -0
  119. package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
  120. package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
  121. package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
  122. package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
  123. package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
  124. package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
  125. package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
  126. package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
  127. package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
  128. package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
  129. package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
  130. package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
  131. package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
  132. package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
  133. package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
  134. package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
  135. package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
  136. package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
  137. package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
  138. package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
  139. package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
  140. package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
  141. package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
  142. package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
  143. package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
  144. package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
  145. package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
  146. package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
  147. package/scripts/forge-root.test.sh +2 -2
  148. package/scripts/publish-staging-sync.test.sh +62 -4
  149. package/scripts/review-finalize.sh +17 -6
  150. package/scripts/review-finalize.test.sh +47 -0
  151. package/scripts/review-pr-arguments.test.sh +13 -1
  152. package/scripts/review-pr-delta-wiring.test.sh +2 -2
  153. package/scripts/review-pr-gate2.test.sh +6 -14
  154. package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
  155. package/scripts/review-pr-staging-scope.test.sh +39 -33
  156. package/scripts/review-prepare.sh +73 -6
  157. package/scripts/review-prepare.test.sh +85 -0
  158. package/scripts/review-staging-post.sh +293 -0
  159. package/scripts/review-staging-post.test.sh +143 -0
  160. package/scripts/review-staging-pre.sh +401 -0
  161. package/scripts/review-staging-pre.test.sh +150 -0
  162. package/scripts/symbol-index.mjs +26 -7
  163. package/scripts/symbol-index.py +55 -9
  164. package/scripts/test-map.sh +63 -17
  165. package/scripts/test-map.test.sh +55 -0
  166. package/scripts/trusted-comments.test.sh +1 -1
@@ -8,6 +8,12 @@
8
8
  set -uo pipefail
9
9
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
10
10
  SPEC="$ROOT/commands/review-pr-staging.md"
11
+ # Since #4359 the spec calls `forgedock review-staging`: the 0A/0B.1 blocks live in review-staging-pre.sh, the panel guard,
12
+ # 6.75 delivery and 6.5 verdict handling in review-staging-post.sh, and the 7A-7C wiring in bin/review-staging.mjs.
13
+ PRE="$ROOT/scripts/review-staging-pre.sh"
14
+ POST="$ROOT/scripts/review-staging-post.sh"
15
+ CLI="$ROOT/bin/review-staging.mjs"
16
+ blk() { awk -v n="$2" '$0 == "# BEGIN " n {f=1; next} $0 == "# END " n {f=0} f' "$1"; }
11
17
  TC="$ROOT/scripts/trusted-comments.sh"
12
18
  PASS=0; FAILN=0
13
19
  ok() { PASS=$((PASS+1)); }
@@ -24,10 +30,10 @@ chk HIGH 0; chk high 0; chk " High " 0; chk CRITICAL 0; chk critical 0
24
30
  chk MEDIUM 1; chk medium 1; chk LOW 1; chk INFO 1
25
31
  chk "" 0; chk "SEVERE" 0; chk "HIGH/MEDIUM" 0
26
32
 
27
- # (2) full-sha reviewed test: the _AGENT regex from the spec, run through the real trusted-comments predicate
28
- LINE=$(grep -E '^[[:space:]]*_AGENT=' "$SPEC" | head -1)
33
+ # (2) full-sha reviewed test: the _AGENT regex from the 0B.1 scope loop, run through the real trusted-comments predicate
34
+ LINE=$(grep -E '^[[:space:]]*_AGENT=' "$PRE" | head -1)
29
35
  RE=$(printf '%s' "$LINE" | sed -n 's/.*count "\(.*\)") ||.*/\1/p')
30
- if [ -n "$RE" ]; then ok; else bad "_AGENT regex not found in review-pr-staging.md"; fi
36
+ if [ -n "$RE" ]; then ok; else bad "_AGENT regex not found in review-staging-pre.sh"; fi
31
37
  FULL=0123456789abcdef0123456789abcdef01234567; SHORT=${FULL:0:7}
32
38
  RE=${RE//\\\\/\\}; RE=${RE//\$\{_HEAD\}/$FULL}
33
39
  FB=$'<!-- REVIEW-FINDINGS-START -->\nNo findings.\n<!-- REVIEW-FINDINGS-END -->'
@@ -41,7 +47,7 @@ cnt() { bash "$TC" count "$RE"; }
41
47
  [ "$(mk $'<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: '"$FULL"$'\n\nno findings block here' Bot | cnt)" = 0 ] && ok || bad "comment without a findings block must not count (panel integrity)"
42
48
 
43
49
  # (3) _ROUTE regex: real mode on the final head counts, spec-evolution-blocked and a stale sha do not
44
- RLINE=$(grep -E '^[[:space:]]*_ROUTE=' "$SPEC" | head -1)
50
+ RLINE=$(grep -E '^[[:space:]]*_ROUTE=' "$PRE" | head -1)
45
51
  RRE=$(printf '%s' "$RLINE" | sed -n 's/.*count "\(.*\)") ||.*/\1/p'); RRE=${RRE//\\\\/\\}; RRE=${RRE//\$\{_HEAD7\}/$SHORT}
46
52
  rcnt() { bash "$TC" count "$RRE"; }
47
53
  [ "$(mk '<!-- FORGE:REVIEW_ROUTE mode=single-pr spec=review-pr.md sha='"$SHORT"' -->' Bot | rcnt)" = 1 ] && ok || bad "route marker on the final head should count"
@@ -50,14 +56,14 @@ rcnt() { bash "$TC" count "$RRE"; }
50
56
 
51
57
  # (4) spec text guards: agents may not be told to suppress findings; no $PWD tier in the classifier resolver; git reads are status-checked
52
58
  if grep -q 'MUST NOT report a defect' "$SPEC"; then bad "Phase 2 must not tell agents to suppress findings (7B owns demotion)"; else ok; fi
53
- if grep -qE "^DEPLOY_WIRING_FILES=.*git diff" "$SPEC"; then bad "DEPLOY_WIRING_FILES must not pipe git diff directly"; else ok; fi
54
- if grep -q 'git diff-tree .*|| { SCOPE_MODE=full' "$SPEC"; then ok; else bad "git diff-tree reads must fall back to full scope"; fi
59
+ if grep -qE "^DEPLOY_WIRING_FILES=.*git diff" "$SPEC" "$PRE"; then bad "DEPLOY_WIRING_FILES must not pipe git diff directly"; else ok; fi
60
+ if grep -q 'git diff-tree .*|| { SCOPE_MODE=full' "$PRE"; then ok; else bad "git diff-tree reads must fall back to full scope"; fi
55
61
  # the staging triage is the shared script: scoped and prior-PR arguments reach `classify`, the scope file and member PR list are built from the persisted 0B.1 state, no classifier logic is copied into the spec
56
- if grep -qF -- '--scope-files "$SCOPE_FILE"' "$SPEC" && grep -qF -- '--prior-prs "$PRIOR_PRS"' "$SPEC" && grep -q '_scope.env' "$SPEC" && grep -q 'review-finalize.sh' "$SPEC"; then ok; else bad "7A/7C must call review-finalize.sh classify with --scope-files and --prior-prs from the persisted 0B.1 scope"; fi
62
+ if grep -qF '"--scope-files", scopeFile' "$CLI" && grep -qF '"--prior-prs", priorPrs' "$CLI" && grep -qF '_scope.json' "$CLI" && grep -qF '"review-finalize.sh"' "$CLI"; then ok; else bad "the staging CLI must call review-finalize.sh classify with --scope-files and --prior-prs from the persisted 0B.1 scope"; fi
57
63
  if grep -qE "^(classify_one|cap_pass|prior_findings_tsv)\(\) \{" "$SPEC"; then bad "review-pr-staging.md must not carry a copy of the classifier, cap or re-flag logic"; else ok; fi
58
64
  if grep -qF 'FINDING_TEXT_FILE="$WORKDIR/finding-text.$I"; jq -r' "$ROOT/scripts/review-finalize.sh" && grep -qF -- '--text-file "$FINDING_TEXT_FILE"' "$ROOT/scripts/review-finalize.sh"; then ok; else bad "FINDING_TEXT_FILE must be written before the classifier call in review-finalize.sh"; fi
59
65
  grep -q "skip-dedup" "$ROOT/scripts/review-finalize.sh" && ok || bad "review-finalize.sh must skip a deduped finding"
60
- grep -q "review-prepare.sh" "$SPEC" && grep -q -- "--no-delta" "$SPEC" && ok || bad "staging must call review-prepare.sh --no-delta in 0C"
66
+ grep -q "review-prepare.sh" "$CLI" && grep -q -- "--no-delta" "$CLI" && ok || bad "staging must call review-prepare.sh --no-delta in 0C"
61
67
  # (7) fail-closed guard: BLOCKED when the classifier is unresolvable, no manual classifier mode, no classifier-unavailable fallback
62
68
  SPEC_FIN="$ROOT/scripts/review-finalize.sh" # review-pr.md delegates classification to review-finalize.sh, which owns the fail-closed BLOCKED line (#3841)
63
69
  if grep -qE 'classifier=manual|echo script \|\| echo manual|classifier-unavailable' "$SPEC"; then bad "review-pr-staging.md must carry no manual classifier mode"; else ok; fi
@@ -68,7 +74,7 @@ done
68
74
  # (forge#3778) the spec carries a byte-identical stub per fence; the body lives in scripts/lib/fence-bootstrap.sh, which is SOURCED
69
75
  # in a fresh shell (the stub's locator + source line is executed for real, so the stub itself is covered too).
70
76
  [ "$(grep '# FENCE_BOOTSTRAP$' "$SPEC" | sort -u | wc -l | tr -d ' ')" -eq 1 ] && ok || bad "FENCE_BOOTSTRAP stubs must be byte-identical"
71
- [ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC")" -ge 3 ] && ok || bad "bootstrap must be carried by 0B.1 and both 7B.5 fences"
77
+ [ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC")" -ge 3 ] && ok || bad "bootstrap must be carried by the 7A, 7C and Phase 8 fences"
72
78
  grep '^type forge_resolve >/dev/null' "$SPEC" | sed 1q > "$TMP/boot.sh"
73
79
  grep '# FENCE_BOOTSTRAP$' "$SPEC" | sed 1q >> "$TMP/boot.sh"
74
80
  [ "$(wc -l < "$TMP/boot.sh")" -eq 2 ] && ok || bad "bootstrap stub (locator + source line) not found"
@@ -81,17 +87,17 @@ UNCOVERED=$(awk '
81
87
  infence { body = body "\n" $0 }' "$SPEC")
82
88
  [ -z "$UNCOVERED" ] && ok || bad "bash fences starting at line(s) ${UNCOVERED}consume cross-fence state without the FENCE_BOOTSTRAP block"
83
89
  # the persisted Phase 6.5 / 6.75 values: producers write them, consumers reload them validated
84
- grep -q 'test_gate_verdict' "$SPEC" && grep -q 'launched_review_agents' "$SPEC" && ok || bad "TEST_GATE_VERDICT and LAUNCHED_REVIEW_AGENTS must be persisted to the scratchpad"
90
+ grep -q 'test_gate_verdict' "$POST" && grep -q 'launched_review_agents' "$POST" && ok || bad "TEST_GATE_VERDICT and LAUNCHED_REVIEW_AGENTS must be persisted to the scratchpad"
85
91
  # PR-keyed (concurrent runs in one cwd must not share state) and reset by an executable command, not prose
86
- if grep -qE '\$_SD/(test_gate_verdict|test_gate_reason|launched_review_agents)|\.forge-scratch\}/launched_review_agents' "$SPEC"; then bad "gate/launched-agent scratch files must be keyed by \${PR_NUMBER}_"; else ok; fi
87
- grep -qF ': > "$_SD/${PR_NUMBER}_launched_review_agents"' "$SPEC" && grep -qF 'rm -f "$_SD/${PR_NUMBER}_test_gate_verdict" "$_SD/${PR_NUMBER}_test_gate_reason"' "$SPEC" && ok || bad "Phase 2 must truncate and Phase 6.5 must rm -f the PR-keyed scratch files in executable commands"
92
+ if grep -qE '\$_SD/(test_gate_verdict|test_gate_reason|launched_review_agents)|\.forge-scratch\}/launched_review_agents' "$SPEC" "$POST"; then bad "gate/launched-agent scratch files must be keyed by \${PR_NUMBER}_"; else ok; fi
93
+ grep -qF 'io.writeFile(launchedFile, "")' "$CLI" && grep -qF 'rm -f "${_SD:?}/${PR_NUMBER:?}_test_gate_verdict" "${_SD:?}/${PR_NUMBER:?}_test_gate_reason"' "$POST" && ok || bad "the pre-dispatch must truncate the launched-agent file and the test gate must rm -f the PR-keyed verdict files"
88
94
  if awk '/^### 7E:/,/^### 7F:/' "$SPEC" | grep -qF '{GH_REPO}'; then bad "7E must not use the {GH_REPO} template token"; else ok; fi
89
95
  # (8b) forge#3733: Phase 6.75 fails closed on an empty/absent launched-agent list or empty PR_NUMBER; Phase 6 fails closed on a bad SELECTED_AGENT_COUNT
90
- F675=$(awk '/^### Phase 6.75/,/^### 7A/' "$SPEC" | awk '/^```bash/{f=1;next} /^```/{if(f)exit} f')
96
+ F675=$(blk "$POST" delivery-check)
91
97
  GUARD_LN=$(printf '%s\n' "$F675" | grep -n 'launched-agent list is empty or unreadable' | head -1 | cut -d: -f1)
92
98
  LOOP_LN=$(printf '%s\n' "$F675" | grep -n '^for AGENT_DOMAIN in' | head -1 | cut -d: -f1)
93
99
  [ -n "$GUARD_LN" ] && [ -n "$LOOP_LN" ] && [ "$GUARD_LN" -lt "$LOOP_LN" ] && ok || bad "6.75 empty-list guard must precede the for AGENT_DOMAIN loop"
94
- printf '%s\n' "$F675" | sed -n "1,$((LOOP_LN-1))p" | grep -v '^[[:space:]]*gh ' > "$TMP/f675.sh"
100
+ { echo '_SD="$FORGE_SCRATCHPAD"'; printf '%s\n' "$F675" | sed -n "1,$((LOOP_LN-1))p" | grep -v '^[[:space:]]*gh '; } > "$TMP/f675.sh"
95
101
  mkdir -p "$TMP/sd9/empty" "$TMP/sd9/full" "$TMP/bin9"
96
102
  printf '#!/bin/sh\necho stub-value\n' > "$TMP/bin9/yq"; printf '#!/bin/sh\nexit 0\n' > "$TMP/bin9/gh"; chmod +x "$TMP/bin9/yq" "$TMP/bin9/gh"
97
103
  : > "$TMP/sd9/empty/42_launched_review_agents"
@@ -104,7 +110,7 @@ done
104
110
  OUT9=$(run675 "$TMP/sd9/full" 42)
105
111
  case "$OUT9" in *"launched-agent list is empty"*) bad "6.75 guard must not fire for a non-empty list (got '$OUT9')" ;; *) ok ;; esac
106
112
  # Phase 6 SELECTED_AGENT_COUNT validation: static order + behaviour of the reload/validate lines
107
- F6=$(awk '/Selected isolated reviewers/{exit} {print}' "$SPEC" | awk '/^```bash/{b=""; f=1; next} /^```/{f=0; next} f{b=b $0 "\n"; last=b} END{printf "%s", last}')
113
+ F6=$(blk "$POST" panel-guard)
108
114
  printf '%s' "$F6" | grep -qF 'SELECTED_COUNT_INVALID' && ok || bad "Phase 6 fence must validate SELECTED_AGENT_COUNT (SELECTED_COUNT_INVALID)"
109
115
  printf '%s' "$F6" | grep -F 'SELECTED_AGENT_COUNT=' | grep -F 'launched_review_agents' > "$TMP/sel.sh"
110
116
  printf '%s\n' 'SELECTED_COUNT_INVALID=false' "$(printf '%s' "$F6" | grep -F 'case "${SELECTED_AGENT_COUNT:-}"')" > "$TMP/selcase.sh"
@@ -140,20 +146,20 @@ case "$OUT8" in *"PR=|"*) ok ;; *) bad "non-numeric staging_pr_number must be re
140
146
  OUT8=$( ( unset ALL_PR_NUMBERS; PR_NUMBER=99; export PATH="$TMP/bin:$PATH" FORGE_SCRATCHPAD="$TMP/sd8/full" FORGEDOCK_HOME="$ROOT"; . "$TMP/boot.sh" >/dev/null 2>&1; echo "PR=$PR_NUMBER|ALL=${ALL_PR_NUMBERS:-}" ) )
141
147
  [ "$OUT8" = "PR=99|ALL=" ] && ok || bad "in-shell PR_NUMBER must not be overridden by the scratch file (got '$OUT8')"
142
148
  # producers persist, and 0B.1 / 7B.5 no longer use the unsubstituted-template repo token or a bare unquoted PR number in gh pr comment
143
- grep -q 'staging_pr_number' "$SPEC" && grep -q '_bundle-prs.txt' "$SPEC" && ok || bad "Phase -1 / 0A must persist staging_pr_number and _bundle-prs.txt"
144
- if awk '/^### 0B.1:/,/^## Phase 1:/' "$SPEC" | grep -q '{GH_REPO}'; then bad "0B.1 must not use the {GH_REPO} template token"; else ok; fi
149
+ grep -q 'staging_pr_number' "$CLI" && grep -q '_bundle-prs.txt' "$PRE" && ok || bad "Phase -1 / 0A must persist staging_pr_number and _bundle-prs.txt"
150
+ if grep -v '^[[:space:]]*#' "$PRE" "$POST" | grep -qE '(^|[^$]){GH_REPO}'; then bad "the 0A/0B.1 and post-dispatch scripts must not use the {GH_REPO} template token"; else ok; fi
145
151
  if awk '/^### 7A:/,/^### 7D:/' "$SPEC" | grep -qE '\{GH_REPO\}|gh pr comment \$\{PR_NUMBER\}'; then bad "7A-7C must use quoted \$GH_REPO / \$PR_NUMBER"; else ok; fi
146
152
  # (7) reviewer templates bind the full head SHA via the dispatch placeholder, not an orchestrator-only shell var
147
153
  if grep -qE '^Reviewed-SHA: \$\{REVIEW_SHA_FULL\}$' "$SPEC"; then bad "reviewer template still uses \${REVIEW_SHA_FULL} (unbound in a fresh shell)"; else ok; fi
148
154
  [ "$(grep -cE '^Reviewed-SHA: \[REVIEW_SHA\]$' "$SPEC")" -ge 5 ] && ok || bad "all five reviewer templates must use Reviewed-SHA: [REVIEW_SHA]"
149
155
  if grep -E '^Reviewed-SHA:' "$SPEC" | grep -qvE '^Reviewed-SHA: \[REVIEW_SHA\]$'; then bad "every Reviewed-SHA template line must be [REVIEW_SHA]"; else ok; fi
150
- grep -F 'substitute `[REVIEW_SHA]`' "$SPEC" | grep -qF '[REVIEW_SHA_SHORT]' || grep -qF 'Also substitute `[REVIEW_SHA]`' "$SPEC" && ok || bad "dispatch text must bind [REVIEW_SHA]"
156
+ grep -qiF 'substitute `[REVIEW_SHA]`' "$SPEC" && ok || bad "dispatch text must bind [REVIEW_SHA]"
151
157
  # (8) cross-PR re-flag (forge#3641) and the per-review cap (forge#3642) now run inside review-finalize.sh classify: scripts/review-finalize.test.sh covers them (bundle re-flag, scope demotion, cap)
152
158
  # (9) stale-head delta credit (forge#3638): the spec's bundle_delta_credit runs against real git fixtures and the REAL review-delta.sh / trusted-comments.sh.
153
159
  # Fail-closed cases first: every FULL / error / untrusted path must leave the PR's commits UNREVIEWED (empty credit).
154
160
  if command -v git >/dev/null 2>&1; then
155
- awk '/^# BEGIN bundle-delta-credit$/{f=1;next} /^# END bundle-delta-credit$/{f=0} f' "$SPEC" > "$TMP/delta-fn.sh"
156
- [ -s "$TMP/delta-fn.sh" ] && ok || bad "bundle-delta-credit block not found in the spec"
161
+ awk '/^# BEGIN bundle-delta-credit$/{f=1;next} /^# END bundle-delta-credit$/{f=0} f' "$PRE" > "$TMP/delta-fn.sh"
162
+ [ -s "$TMP/delta-fn.sh" ] && ok || bad "bundle-delta-credit block not found in review-staging-pre.sh"
157
163
  RDS="$ROOT/scripts/review-delta.sh"
158
164
  export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_NOSYSTEM=1
159
165
  export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
@@ -227,25 +233,25 @@ mkrepo9; commit9 pr.txt pr1b pr1b; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"
227
233
  OUT9=$(credit9 "$H"); N9=$(printf '%s' "${OUT9#*|}" | wc -w)
228
234
  [ "${OUT9%%|*}" = 0 ] && [ "$N9" -eq 2 ] && case "$OUT9" in *"$(short9 HEAD)"*|*"$(short9 HEAD~1)"*) false ;; *) true ;; esac && ok || bad "partial review must credit exactly the 2 reviewed commits (got '$OUT9')"
229
235
  # spec text guards: the resolver is reused, never copied; carried record accepted; the unchanged cross-PR and merge-resolution paths are still present
230
- grep -q 'review-delta.sh' "$SPEC" && grep -q 'REVIEW-CARRIED' "$SPEC" && ok || bad "spec must call review-delta.sh and accept the REVIEW-CARRIED record"
231
- if grep -qE 'merge-base --is-ancestor.*REVIEWED_SHA_STALE|diff_shape\(\)' "$SPEC"; then bad "spec must not carry a second copy of the resolver logic"; else ok; fi
232
- grep -qF 'printf '"'"'%s\n'"'"' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"' "$SPEC" && grep -qF 'git diff-tree --no-commit-id --name-only -r --cc "$c"' "$SPEC" && ok || bad "CROSS_PR_FILES accumulation and --cc merge scoping must stay intact"
236
+ grep -q 'review-delta.sh' "$PRE" && grep -q 'REVIEW-CARRIED' "$PRE" && ok || bad "review-staging-pre.sh must call review-delta.sh and accept the REVIEW-CARRIED record"
237
+ if grep -qE 'merge-base --is-ancestor.*REVIEWED_SHA_STALE|diff_shape\(\)' "$SPEC" "$PRE"; then bad "spec must not carry a second copy of the resolver logic"; else ok; fi
238
+ grep -qF 'printf '"'"'%s\n'"'"' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"' "$PRE" && grep -qF 'git diff-tree --no-commit-id --name-only -r --cc "$c"' "$PRE" && ok || bad "CROSS_PR_FILES accumulation and --cc merge scoping must stay intact"
233
239
  [ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC" | head -1)" -ge 3 ] && ok || bad "FENCE_BOOTSTRAP stubs must remain"
234
240
  # an unresolvable helper fails the fence closed (exit 1), never an unsourced bootstrap
235
241
  OUT8=$( ( unset FORGEDOCK_HOME FORGE_HOME FORGE_ROOT CLAUDE_PLUGIN_ROOT CLAUDE_CONFIG_DIR CODEX_HOME FORGE_SOURCED_ROOT FORGEDOCK_MARKETPLACE; export HOME="$TMP/nohome"; cd "$TMP" && bash -c "$(cat "$TMP/boot.sh"); echo reached" ) 2>&1 )
236
242
  case "$OUT8" in *BLOCKED*) case "$OUT8" in *reached*) bad "unresolved bootstrap helper must stop the fence" ;; *) ok ;; esac ;; *) bad "unresolved bootstrap helper must print BLOCKED (got '$OUT8')" ;; esac
237
243
  # a stale-head PR must not credit its own merge commit (it contains the post-review commits): the DELTA branch only appends DELTA_COMMITS
238
- if awk '/^bundle_delta_credit\(\)/,/^\}/' "$SPEC" | grep -q 'mergeCommit'; then bad "the DELTA path must not credit the PR merge commit"; else ok; fi
244
+ if awk '/^bundle_delta_credit\(\)/,/^\}/' "$PRE" | grep -q 'mergeCommit'; then bad "the DELTA path must not credit the PR merge commit"; else ok; fi
239
245
  # (10) fail-closed gate verdict (forge#3734): an empty/missing/invalid persisted verdict must never become SKIP
240
- awk '/^\*\*Verdict handling\*\*:/{f=1;next} f&&/^```bash/{g=1;next} g&&/^```/{exit} g' "$SPEC" | grep -v '# FENCE_BOOTSTRAP$' > "$TMP/vh.sh"
246
+ blk "$POST" test-gate-verdict > "$TMP/vh.sh"
241
247
  [ -s "$TMP/vh.sh" ] && ok || bad "could not extract the Phase 6.5 verdict-handling fence"
242
- if awk '/^ \*\)/,/;;/' "$SPEC" | grep -q 'TEST_GATE_VERDICT="SKIP"'; then bad "catch-all arm must not set TEST_GATE_VERDICT=SKIP"; else ok; fi
243
- grep -qF 'missing/invalid' "$SPEC" && ok || bad "spec must document missing/invalid gate-verdict handling"
244
- if grep -F 'FORGE:TEST_GATE:RESULT=' "$SPEC" | grep -q 'tail -1 || echo "SKIP"'; then bad "producer must not coerce an absent marker to SKIP"; else ok; fi
248
+ if awk '/^ \*\)/,/;;/' "$POST" | grep -q 'TEST_GATE_VERDICT="SKIP"'; then bad "catch-all arm must not set TEST_GATE_VERDICT=SKIP"; else ok; fi
249
+ grep -qF 'missing/invalid' "$SPEC" && grep -qF 'missing/invalid' "$POST" && ok || bad "spec must document missing/invalid gate-verdict handling"
250
+ if grep -F 'FORGE:TEST_GATE:RESULT=' "$POST" | grep -q 'tail -1 || echo "SKIP"'; then bad "producer must not coerce an absent marker to SKIP"; else ok; fi
245
251
  vh10() { # $1 posture, $2 persisted verdict content ('-' = no file), $3 env verdict (optional)
246
252
  local sd="$TMP/vh10.$RANDOM"; mkdir -p "$sd"
247
253
  [ "$2" = "-" ] || printf '%s\n' "$2" > "$sd/5_test_gate_verdict"
248
- ( export _SD="$sd" PR_NUMBER=5 GH_REPO=o/r DEFAULT_BRANCH=main ALL_PR_NUMBERS="5" CONFIG_FILE=/dev/null P10="$1"
254
+ ( export _SD="$sd" PR_NUMBER=5 GH_REPO=o/r DEFAULT_BRANCH=main ALL_PR_NUMBERS="5" CONFIG_FILE=/dev/null P10="$1" GATE_POSTURE="$1" OVERRIDE_PHRASE="OVERRIDE: x" GH_FLAG="-R o/r"
249
255
  [ -z "${3:-}" ] || export TEST_GATE_VERDICT="$3"
250
256
  forge_resolve() { :; }; yq() { case "$*" in *posture*) echo "$P10" ;; *) echo "OVERRIDE: x" ;; esac; }; gh() { return 0; }
251
257
  . "$TMP/vh.sh" >"$sd/out" 2>&1; echo "rc=0" >> "$sd/out" ) ; local rc=$?
@@ -280,12 +286,12 @@ gh11() {
280
286
  *) return 1 ;;
281
287
  esac
282
288
  }
283
- [ "$(grep -c '^# BEGIN bundle-pr-kind$' "$SPEC")" -eq 2 ] && [ "$(awk '/^# BEGIN bundle-pr-kind$/{f=1;n++} f{print > ("'"$TMP"'/kind" n ".sh")} /^# END bundle-pr-kind$/{f=0}' "$SPEC"; cmp -s "$TMP/kind1.sh" "$TMP/kind2.sh" && echo same)" = same ] && ok || bad "0A and 0B.1 must carry identical bundle_pr_kind blocks"
284
- awk '/^# BEGIN bundle-pr-set$/{f=1;next} /^# END bundle-pr-set$/{f=0} f' "$SPEC" | sed 's/{GH_REPO}/o\/r/g' > "$TMP/prset.sh"
289
+ [ "$(grep -c '^# BEGIN bundle-pr-kind$' "$PRE")" -eq 2 ] && [ "$(awk '/^# BEGIN bundle-pr-kind$/{f=1;n++} f{print > ("'"$TMP"'/kind" n ".sh")} /^# END bundle-pr-kind$/{f=0}' "$PRE"; cmp -s "$TMP/kind1.sh" "$TMP/kind2.sh" && echo same)" = same ] && ok || bad "0A and 0B.1 must carry identical bundle_pr_kind blocks"
290
+ awk '/^# BEGIN bundle-pr-set$/{f=1;next} /^# END bundle-pr-set$/{f=0} f' "$PRE" | sed 's/{GH_REPO}/o\/r/g' > "$TMP/prset.sh"
285
291
  [ -s "$TMP/prset.sh" ] && ok || bad "bundle-pr-set block not found in Phase 0A"
286
- OUT11=$( gh() { gh11 "$@"; }; BUNDLE_PRS="5 6 28668 77 90"; MERGE_PRS="90"; . "$TMP/prset.sh"; echo $ALL_PR_NUMBERS "|$SKIPPED_ISSUES" )
292
+ OUT11=$( gh() { gh11 "$@"; }; GH_REPO=o/r; BUNDLE_PRS="5 6 28668 77 90"; MERGE_PRS="90"; . "$TMP/prset.sh"; echo $ALL_PR_NUMBERS "|$SKIPPED_ISSUES" )
287
293
  [ "$OUT11" = "5 6 77 90 | 28668" ] && ok || bad "0A must drop the confirmed issue and keep PRs, merge PRs and unclassifiable numbers (got '$OUT11')"
288
- awk '/^# BEGIN bundle-scope-loop$/{f=1;next} /^# END bundle-scope-loop$/{f=0} f' "$SPEC" > "$TMP/loop.sh"
294
+ awk '/^# BEGIN bundle-scope-loop$/{f=1;next} /^# END bundle-scope-loop$/{f=0} f' "$PRE" > "$TMP/loop.sh"
289
295
  [ -s "$TMP/loop.sh" ] && ok || bad "bundle-scope-loop block not found in 0B.1"
290
296
  scope11() { # $1 = ALL_PR_NUMBERS; prints "<SCOPE_MODE> <REVIEWED_PR_COUNT>" then the loop output
291
297
  ( gh() { gh11 "$@"; }; bundle_delta_credit() { DELTA_COMMITS=""; DELTA_FULL=0; }; . "$TMP/kind1.sh"
@@ -9,7 +9,7 @@
9
9
  # the PR checkout, a SPEC_CLI persona file, forge.yaml paths.root) as data.
10
10
  #
11
11
  # Usage: review-prepare.sh --pr N --repo owner/name [--merge-issue N] [--thorough true|false] [--out FILE] [--out-dir DIR]
12
- # [--checkout-pr] [--no-delta] [--build-gate block|advisory]
12
+ # [--checkout-pr] [--no-delta] [--delta-from SHA] [--build-gate block|advisory]
13
13
  # --pr PR number (numeric, required) --repo owner/name (required)
14
14
  # --merge-issue issue the PR closes, from --issue (numeric; non-numeric is ignored)
15
15
  # --thorough true restores the full-union roster --out write the context JSON here (default: stdout)
@@ -23,6 +23,11 @@
23
23
  # --no-delta skip the FULL/DELTA/BASE_SYNC_ONLY resolver: the review mode is always FULL (a staging bundle review always reviews the
24
24
  # whole bundle), without widening the roster to the full union the way --thorough does; it also enables the pinned
25
25
  # local-git diff fallback when `gh pr diff` fails (e.g. HTTP 406 over 20,000 lines), which a milestone->staging PR gets without it
26
+ # --delta-from explicit DELTA from a caller-supplied earlier reviewed head (forge#4360, `forgedock review-staging --delta`): skips the
27
+ # resolver (even with --no-delta), narrows diff, slices and roster to SHA..review sha (the roster is the domains the delta
28
+ # files touch). BLOCKED when SHA is not a full commit id in the checkout, equals or is not an ancestor of the review sha,
29
+ # or has no trusted FORGE:REVIEW-AGENT body with `Reviewed-SHA: SHA` (no earlier pass to credit). An empty file delta
30
+ # is FULL. Not combinable with --thorough true (exit 2).
26
31
  # Env: FORGE_CONFIG (forge.yaml path; default: the main checkout's forge.yaml, then the git top-level one),
27
32
  # REVIEW_PREPARE_SLEEP (seconds between mergeability retries, default 5),
28
33
  # REVIEW_PREPARE_INDEX_TIMEOUT (seconds the code-index build may take, default 120; on timeout or failure the slices say
@@ -61,10 +66,10 @@
61
66
 
62
67
  set -u
63
68
 
64
- PR_NUMBER=""; REPO=""; MERGE_ISSUE=""; THOROUGH="false"; OUT=""; OUT_DIR=""; CHECKOUT_PR="false"; NO_DELTA="false"; BUILD_GATE="block"
69
+ PR_NUMBER=""; REPO=""; MERGE_ISSUE=""; THOROUGH="false"; OUT=""; OUT_DIR=""; CHECKOUT_PR="false"; NO_DELTA="false"; BUILD_GATE="block"; DELTA_FROM_OPT=""
65
70
  while [ $# -gt 0 ]; do
66
71
  case "$1" in
67
- --pr|--repo|--merge-issue|--thorough|--out|--out-dir)
72
+ --pr|--repo|--merge-issue|--thorough|--out|--out-dir|--delta-from)
68
73
  [ $# -ge 2 ] || { echo "review-prepare: $1 requires a value" >&2; exit 2; } ;;
69
74
  esac
70
75
  case "$1" in
@@ -76,6 +81,7 @@ while [ $# -gt 0 ]; do
76
81
  --out-dir) OUT_DIR="${2:-}"; shift 2 ;;
77
82
  --checkout-pr) CHECKOUT_PR="true"; shift ;;
78
83
  --no-delta) NO_DELTA="true"; shift ;;
84
+ --delta-from) DELTA_FROM_OPT="${2:-}"; [ -n "$DELTA_FROM_OPT" ] || DELTA_FROM_OPT="-"; shift 2 ;;
79
85
  --build-gate) case "${2:-}" in block|advisory) BUILD_GATE="$2" ;; *) echo "review-prepare: --build-gate must be block or advisory" >&2; exit 2 ;; esac; shift 2 ;;
80
86
  *) echo "review-prepare: unknown argument: $1" >&2; exit 2 ;;
81
87
  esac
@@ -85,6 +91,7 @@ case "$REPO" in */*) ;; *) echo "review-prepare: --repo must be owner/name" >&2;
85
91
  case "$REPO" in *[!A-Za-z0-9._/-]*|*/*/*|/*|*/) echo "review-prepare: invalid --repo" >&2; exit 2 ;; esac
86
92
  case "$MERGE_ISSUE" in *[!0-9]*) MERGE_ISSUE="" ;; esac
87
93
  [ "$THOROUGH" = "true" ] || THOROUGH="false"
94
+ if [ -n "$DELTA_FROM_OPT" ] && [ "$THOROUGH" = "true" ]; then echo "review-prepare: --delta-from cannot be combined with --thorough true" >&2; exit 2; fi
88
95
 
89
96
  # --- trusted helper resolution (install roots only; never cwd) ---
90
97
  # shellcheck source=/dev/null # the resolver lib is found at runtime in the trusted install root
@@ -125,7 +132,7 @@ cleanup() { restore_ref; rm -rf "$TMP_DIR"; }
125
132
  trap cleanup EXIT
126
133
 
127
134
  # --- Phase 1A: fetch ---
128
- PR_INFO=$(gh pr view "$PR_NUMBER" -R "$REPO" --json number,title,author,baseRefName,headRefName,headRefOid,isCrossRepository,additions,deletions,changedFiles 2>/dev/null) || PR_INFO=""
135
+ PR_INFO=$(gh pr view "$PR_NUMBER" -R "$REPO" --json number,title,author,baseRefName,headRefName,headRefOid,isCrossRepository,additions,deletions,changedFiles,labels 2>/dev/null) || PR_INFO=""
129
136
  printf '%s' "$PR_INFO" | jq -e . >/dev/null 2>&1 || PR_INFO=""
130
137
  if [ -z "$PR_INFO" ]; then block "gh pr view failed for #${PR_NUMBER}"; fi
131
138
  pj() { printf '%s' "$PR_INFO" | jq -r "$1 // empty" 2>/dev/null; }
@@ -452,7 +459,53 @@ delta_roster() {
452
459
  printf '%s' "$out"
453
460
  }
454
461
  REVIEW_MODE="FULL"; DELTA_FROM=""; CARRIED_FROM=""; FULL_ROUNDS=0; DELTA_FILES=""
455
- if [ "$THOROUGH" != "true" ] && [ "$IS_MILESTONE_TO_STAGING" != "true" ] && [ "$NO_DELTA" != "true" ]; then
462
+ if [ -n "$DELTA_FROM_OPT" ]; then
463
+ # forge#4360: explicit delta. Every check fails closed (BLOCKED); only an empty file delta widens to FULL (below).
464
+ _dtop="${TOP:-.}"
465
+ case "$DELTA_FROM_OPT" in *[!0-9a-f]*|"") _dok=false ;; *) { [ "${#DELTA_FROM_OPT}" -eq 40 ] || [ "${#DELTA_FROM_OPT}" -eq 64 ]; } && _dok=true || _dok=false ;; esac
466
+ if [ "$_dok" != "true" ]; then block "--delta-from is not a full commit id: $(printf '%s' "$DELTA_FROM_OPT" | tr -cd '0-9A-Za-z' | cut -c1-64)"
467
+ elif [ "$DELTA_FROM_OPT" = "$REVIEW_SHA" ]; then block "--delta-from ${DELTA_FROM_OPT} is the review head: nothing to review"
468
+ elif ! git -C "$_dtop" cat-file -e "${DELTA_FROM_OPT}^{commit}" 2>/dev/null; then block "--delta-from ${DELTA_FROM_OPT} is not a commit in this checkout (fetch it first)"
469
+ elif ! git -C "$_dtop" merge-base --is-ancestor "$DELTA_FROM_OPT" "$REVIEW_SHA" 2>/dev/null; then block "--delta-from ${DELTA_FROM_OPT} is not an ancestor of ${REVIEW_SHA} (force-push or rebase): run a full review"
470
+ elif printf '%s' "$PR_INFO" | jq -e '[.labels[]?.name] | index("review-degraded") != null' >/dev/null 2>&1; then block "--delta-from ${DELTA_FROM_OPT}: the PR is labelled review-degraded (an earlier panel was incomplete): run a full review"
471
+ else
472
+ # forge#4471: the earlier pass must be complete and non-degraded, as scripts/review-delta.sh requires of a checkpoint:
473
+ # an agent body with a findings block for <from>, a post-panel-guard proof naming <from> (staging summary, synthesized
474
+ # block, or a # PR Review Summary whose Agents count matches) and no degraded marker after its first agent body.
475
+ _dv="error"
476
+ if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$COMMENTS_JSON" ]; then
477
+ _db=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->|^<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->|# PR Review Summary|^<!-- FORGE:GATE_(PASS|FAILURE) -->|<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity|<!-- FORGE:REVIEW_DEGRADED|<!-- FORGE:REVIEW_BLOCKED' 2>/dev/null) \
478
+ && _dv=$(printf '%s\n' "$_db" | jq -s -r --arg f "$DELTA_FROM_OPT" '
479
+ def shas($t; $re): [ $t | scan($re) | .[0] ];
480
+ def one($a): if ($a | length) == 1 then $a[0] else null end;
481
+ def rsha: "(?:^|\\n)Reviewed-SHA: ([0-9a-f]{40}|[0-9a-f]{64})\\r?(?:\\n|$)";
482
+ to_entries as $E
483
+ | [ $E[] | select((.value | test("\\A<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->")) and (.value | test("<!-- REVIEW-FINDINGS-START -->")))
484
+ | select(one(shas(.value; rsha)) == $f)
485
+ | {i: .key, d: (.value | capture("\\A<!-- FORGE:REVIEW-AGENT:(?<d>[a-z0-9-]+) -->").d)} ] as $AG
486
+ | if ($AG | length) == 0 then "no-agent" else
487
+ ($AG | map(.i) | min) as $first | ($AG | map(.d) | unique | length) as $nd
488
+ | ([ $E[] | .value as $t | select(
489
+ (($t | test("\\A<!-- FORGE:GATE_(?:PASS|FAILURE) -->\\r?\\n## Staging Review:"))
490
+ and one(shas($t; "\\*\\*Head\\*\\*: `([0-9a-f]{40}|[0-9a-f]{64})`")) == $f)
491
+ or (($t | startswith("<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->")) and one(shas($t; rsha)) == $f)
492
+ or (($t | test("# PR Review Summary"))
493
+ and one(shas($t; "\\*\\*Reviewed commit\\*\\*: `?([0-9a-f]{40}|[0-9a-f]{64})`?(?:[ |\\r\\n]|$)")) == $f
494
+ and one(shas($t; "\\*\\*Agents\\*\\*: ([0-9]+)")) == ($nd | tostring))) ] | length) as $proof
495
+ | ([ $E[] | select(.key > $first)
496
+ | select(.value | test("<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity|<!-- FORGE:REVIEW_DEGRADED|<!-- FORGE:REVIEW_BLOCKED")) ] | length) as $deg
497
+ | if $deg > 0 then "degraded" elif $proof == 0 then "no-proof" else "ok" end
498
+ end' 2>/dev/null) || _dv="error"
499
+ fi
500
+ case "$_dv" in
501
+ ok) REVIEW_MODE="DELTA"; DELTA_FROM="$DELTA_FROM_OPT" ;;
502
+ no-agent) block "--delta-from ${DELTA_FROM_OPT}: no trusted FORGE:REVIEW-AGENT comment with a findings block for that head, no earlier pass to credit: run a full review" ;;
503
+ no-proof) block "--delta-from ${DELTA_FROM_OPT}: no trusted proof that the earlier panel completed (staging summary, synthesized block or PR Review Summary for that head): run a full review" ;;
504
+ degraded) block "--delta-from ${DELTA_FROM_OPT}: the earlier pass is degraded (review-panel-integrity / REVIEW_DEGRADED / REVIEW_BLOCKED after its agents): run a full review" ;;
505
+ *) block "--delta-from ${DELTA_FROM_OPT}: could not read the trusted review comments, no earlier pass to credit: run a full review" ;;
506
+ esac
507
+ fi
508
+ elif [ "$THOROUGH" != "true" ] && [ "$IS_MILESTONE_TO_STAGING" != "true" ] && [ "$NO_DELTA" != "true" ]; then
456
509
  read -r RD_KIND RD_ARG RD_ROUNDS <<< "$(review_delta_mode)"
457
510
  FULL_ROUNDS="${RD_ROUNDS:-0}"
458
511
  case "$RD_KIND" in
@@ -463,7 +516,21 @@ fi
463
516
  if [ "$REVIEW_MODE" = "DELTA" ]; then
464
517
  DELTA_FILES=$(git -C "${TOP:-.}" -c core.quotepath=off diff --name-only --no-renames "${DELTA_FROM}" "${REVIEW_SHA}" 2>/dev/null) || DELTA_FILES=""
465
518
  if [ -z "$DELTA_FILES" ]; then REVIEW_MODE="FULL"; DELTA_FROM=""
466
- else SELECTED_AGENTS=$(canonical_roster "$(delta_roster "$FULL_ROUNDS" "$SELECTED_AGENTS" "$DELTA_FILES")"); fi
519
+ else
520
+ # an explicit --delta-from always narrows the roster to the domains the delta touches (no full-rounds gate)
521
+ _drounds="$FULL_ROUNDS"
522
+ # forge#4471: an explicit delta that changes the review specs/config cannot credit the earlier pass (same list as
523
+ # scripts/review-delta.sh, which answers FULL there; review-prepare.test.sh checks the two lists match)
524
+ if [ -n "$DELTA_FROM_OPT" ]; then
525
+ while IFS= read -r _df; do
526
+ case "$_df" in
527
+ forge.yaml|commands/review-pr*.md|commands/review-pr-agents/*|scripts/review-delta.sh|scripts/trusted-comments.sh|scripts/verify-phase-trail.sh|docs/spec/review-protocol.md|scripts/classify-finding.sh) block "--delta-from ${DELTA_FROM}: the delta changes ${_df} (review spec/config), the earlier pass cannot be credited: run a full review"; break ;;
528
+ esac
529
+ done <<< "$DELTA_FILES"
530
+ fi
531
+ [ -n "$DELTA_FROM_OPT" ] && { _drounds=2; ROSTER_REASON="delta ${DELTA_FROM:0:7}..${REVIEW_SHA:0:7}: domains the delta touches"; }
532
+ SELECTED_AGENTS=$(canonical_roster "$(delta_roster "$_drounds" "$SELECTED_AGENTS" "$DELTA_FILES")")
533
+ fi
467
534
  fi
468
535
  [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ] && SELECTED_AGENTS=""
469
536
 
@@ -199,6 +199,91 @@ DELTA_KIND=DELTA prep "$WORK/root-delta" --thorough true; eq "thorough never goe
199
199
  setup feat/x staging "$FIXFILES" "$DIFF_AUTH_BILL" "notasha"
200
200
  DELTA_KIND=DELTA prep "$WORK/root-delta"; eq "unvalidated sha is FULL" .delta.mode FULL
201
201
 
202
+ # --- 5b. explicit --delta-from (forge#4360: forgedock review-staging --delta): no resolver involved ---
203
+ cmt() { # cmt <body> [association]: one comment object
204
+ jq -n --arg b "$1" --arg a "${2:-OWNER}" '{body: $b, author_association: $a, user: {login: "r", type: "User"}}'
205
+ }
206
+ agent_body() { printf '<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: %s\n<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->' "$1"; }
207
+ # shellcheck disable=SC2016 # the backticks are literal markdown
208
+ staging_summary() { printf '<!-- FORGE:GATE_FAILURE -->\n## Staging Review: NEEDS FIXES FIRST\n\n**Head**: `%s`\n\n<!-- FORGE:GATE_FAILURE:TYPE=staging-review|BUNDLE= -->' "$1"; }
209
+ comments_of() { jq -s . > "$FIX/comments.json"; } # comments_of < <cmt ...>...
210
+ earlier_pass() { # earlier_pass <sha> [association]: a complete earlier pass (agent body + post-guard staging summary)
211
+ { cmt "$(agent_body "$1")" "${2:-OWNER}"; cmt "$(staging_summary "$1")" "${2:-OWNER}"; } | comments_of
212
+ }
213
+ git -C "$REPO_DIR" checkout -q -b pr-empty pr-branch; git -C "$REPO_DIR" commit -q --allow-empty -m C; # own branch: pr-branch stays at B (pinned-fallback tests below)
214
+ SHA_C=$(git -C "$REPO_DIR" rev-parse HEAD)
215
+ git -C "$REPO_DIR" checkout -q -b side main; echo d > "$REPO_DIR/d.txt"; git -C "$REPO_DIR" add -A; git -C "$REPO_DIR" commit -qm D
216
+ SHA_D=$(git -C "$REPO_DIR" rev-parse HEAD); git -C "$REPO_DIR" checkout -q main
217
+ setup staging main "$FIXFILES" "$DIFF_AUTH_BILL"; earlier_pass "$SHA_A"
218
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"
219
+ rc_is "--delta-from ok" 0; eq "--delta-from mode" .delta.mode DELTA; eq "--delta-from from" .delta.from "$SHA_A"
220
+ eq "--delta-from files" '.delta.files | join(" ")' "services/api/auth.py"
221
+ eq "--delta-from roster is the touched domains, even before 2 full rounds" '.roster.agents | join(" ")' "security auth api"
222
+ eq "--delta-from reason names the delta" '.roster.reason | startswith("delta ")' true
223
+ eq "--delta-from slices narrow to the delta" '.slices.security.paths | join(" ")' "services/api/auth.py"
224
+ eq "--delta-from full_files stays whole" '.full_files | length' 2
225
+ prep "$WORK/root-nodelta" --no-delta; eq "--no-delta without --delta-from stays FULL" .delta.mode FULL
226
+ earlier_pass "$SHA_A" NONE
227
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "untrusted earlier pass" 3
228
+ eq "untrusted earlier pass blocker" '.blockers | join(" ") | test("no earlier pass to credit")' true
229
+ earlier_pass "$SHA_B"
230
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "no earlier pass at the from sha" 3
231
+ earlier_pass "$SHA_D"
232
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_D"; rc_is "non-ancestor from sha" 3
233
+ eq "non-ancestor blocker" '.blockers | join(" ") | test("not an ancestor")' true
234
+ earlier_pass "$SHA_B"
235
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "from sha == head" 3
236
+ eq "head blocker" '.blockers | join(" ") | test("is the review head")' true
237
+ prep "$WORK/root-nodelta" --no-delta --delta-from "0123456789012345678901234567890123456789"; rc_is "unknown commit" 3
238
+ prep "$WORK/root-nodelta" --no-delta --delta-from "abc123"; rc_is "short sha" 3
239
+ setup staging main "$FIXFILES" "$DIFF_AUTH_BILL" "$SHA_C"; earlier_pass "$SHA_B"
240
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "empty delta" 0; eq "empty delta is FULL" .delta.mode FULL
241
+ prep "$WORK/root-nodelta" --delta-from "$SHA_B" --thorough true; rc_is "--delta-from with --thorough" 2
242
+ # forge#4471: the earlier pass must be complete and non-degraded, and the delta must not change review specs/config
243
+ setup staging main "$FIXFILES" "$DIFF_AUTH_BILL"
244
+ { cmt "$(agent_body "$SHA_A")"; cmt "<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->
245
+ panel degraded: 1 of 4 reviewers completed"; } | comments_of
246
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "degraded earlier pass blocks --delta-from" 3
247
+ eq "degraded earlier pass blocker" '.blockers | join(" ") | test("earlier pass is degraded")' true
248
+ { cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")"; cmt "<!-- FORGE:REVIEW_DEGRADED -->"; } | comments_of
249
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "REVIEW_DEGRADED after a summarised pass blocks" 3
250
+ { cmt "<!-- FORGE:REVIEW_DEGRADED -->"; cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")"; } | comments_of
251
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "degraded marker before the earlier pass does not disqualify it" 0
252
+ { cmt "$(agent_body "$SHA_A")"; } | comments_of
253
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "partial earlier pass (no post-guard proof) blocks" 3
254
+ eq "partial earlier pass blocker" '.blockers | join(" ") | test("no trusted proof that the earlier panel completed")' true
255
+ { cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")" NONE; } | comments_of
256
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "untrusted staging summary is no proof" 3
257
+ { cmt "$(staging_summary "$SHA_B")"; cmt "$(agent_body "$SHA_A")"; } | comments_of
258
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "summary for another head is no proof" 3
259
+ { cmt "<!-- FORGE:REVIEW-AGENT:security -->
260
+ Reviewed-SHA: $SHA_A
261
+
262
+ no findings block"; cmt "$(staging_summary "$SHA_A")"; } | comments_of
263
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "agent body without a findings block blocks" 3
264
+ { cmt "$(agent_body "$SHA_A")"; cmt "<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->
265
+ Reviewed-SHA: $SHA_A
266
+ <!-- REVIEW-FINDINGS-SYNTHESIZED-END -->"; } | comments_of
267
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "synthesized block is a post-guard proof" 0
268
+ { cmt "$(agent_body "$SHA_A")"; cmt "# PR Review Summary
269
+ **Reviewed commit**: \`$SHA_A\`
270
+ **Agents**: 2"; } | comments_of
271
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "PR Review Summary with an Agents count above the domains blocks" 3
272
+ earlier_pass "$SHA_A"
273
+ sed 's/"changedFiles":1}/"changedFiles":1,"labels":[{"name":"review-degraded"}]}/' "$FIX/prinfo.json" > "$FIX/prinfo.tmp" && mv "$FIX/prinfo.tmp" "$FIX/prinfo.json"
274
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "review-degraded label blocks --delta-from" 3
275
+ eq "review-degraded label blocker" '.blockers | join(" ") | test("labelled review-degraded")' true
276
+ for _sf in forge.yaml scripts/trusted-comments.sh; do
277
+ git -C "$REPO_DIR" checkout -q -b "pr-spec-$(basename "$_sf")" pr-branch; mkdir -p "$REPO_DIR/$(dirname "$_sf")"; echo x > "$REPO_DIR/$_sf"
278
+ git -C "$REPO_DIR" add -A; git -C "$REPO_DIR" commit -qm "touch $_sf"; _sh=$(git -C "$REPO_DIR" rev-parse HEAD); git -C "$REPO_DIR" checkout -q main
279
+ setup staging main "$FIXFILES" "$DIFF_AUTH_BILL" "$_sh"; earlier_pass "$SHA_B"
280
+ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "delta touching $_sf blocks --delta-from" 3
281
+ eq "delta touching $_sf blocker" '.blockers | join(" ") | test("review spec/config")' true
282
+ done
283
+ # the spec/config list must stay identical to the resolver's FULL list
284
+ _pl() { grep -oE 'forge\.yaml\|commands/review-pr[^)]*\)' "$1" | head -n 1; }
285
+ if [ -n "$(_pl "$ROOT/scripts/review-delta.sh")" ] && [ "$(_pl "$ROOT/scripts/review-prepare.sh")" = "$(_pl "$ROOT/scripts/review-delta.sh")" ]; then ok; else bad "review-prepare.sh spec/config list drifted from scripts/review-delta.sh"; fi
286
+
202
287
  # --- 6. full-build lane: empty trusted config is BLOCKED, never a silent skip ---
203
288
  TS_DIFF='diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
204
289
  +x'