forgedock 1.12.0 → 1.13.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.
- package/bin/engine/phases-args.test.mjs +2 -1
- package/bin/engine/phases.mjs +349 -90
- package/bin/engine/runlog.mjs +25 -2
- package/bin/engine-cli.mjs +4 -1
- package/bin/engine.mjs +72 -19
- package/bin/hooks/interactive-engine.mjs +135 -209
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +10 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +258 -23
- package/bin/watch-utils.mjs +1 -0
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +8 -5
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +14 -19
- package/commands/orchestrate/phase-4-execution.md +80 -53
- package/commands/orchestrate/phase-6-report.md +6 -0
- package/commands/orchestrate/safety.md +1 -1
- package/commands/pipeline-health/phase-2-metrics.md +39 -0
- package/commands/pipeline-health/phase-5-report.md +1 -0
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +2 -2
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +3 -3
- package/commands/review-pr-agents/protocols.md +15 -6
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +12 -12
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +394 -53
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +106 -7
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +137 -25
- package/commands/work-on/review.md +25 -9
- package/commands/work-on.md +15 -15
- package/docs/CONFIG.md +29 -0
- package/docs/RELEASE-NOTES.md +45 -0
- package/docs/spec/forge-protocol-v1.md +22 -1
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +20 -5
- package/package.json +2 -1
- package/packages/protocol/src/phases.js +21 -0
- package/scripts/check-blast-radius.sh +164 -0
- package/scripts/check-blast-radius.test.sh +183 -0
- package/scripts/check-contract-scope.sh +137 -0
- package/scripts/check-contract-scope.test.sh +83 -0
- package/scripts/check-spec-bash.sh +60 -3
- package/scripts/check-spec-bash.test.sh +56 -0
- package/scripts/check-spec-markers.sh +29 -0
- package/scripts/check-test-wiring.sh +57 -0
- package/scripts/check-test-wiring.test.sh +70 -0
- package/scripts/classify-finding.sh +72 -8
- package/scripts/classify-finding.test.sh +77 -8
- package/scripts/close-dossier-no-main-commit.test.sh +12 -12
- package/scripts/diff-size.test.sh +3 -2
- package/scripts/doctor-pipeline-state.sh +35 -0
- package/scripts/forge-annotation.sh +3 -0
- package/scripts/forge-root.test.sh +103 -13
- package/scripts/forge-run.sh +2 -2
- package/scripts/recovery-claim-live.test.sh +10 -7
- package/scripts/remediate-basesync.test.sh +103 -0
- package/scripts/review-delta.sh +206 -0
- package/scripts/review-delta.test.sh +213 -0
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/review-pr-gate2.test.sh +84 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
- package/scripts/run-shell-tests.sh +115 -0
- package/scripts/select-fix-targets.sh +17 -5
- package/scripts/transition-label.sh +3 -1
|
@@ -229,20 +229,110 @@ if command -v node >/dev/null 2>&1; then
|
|
|
229
229
|
done
|
|
230
230
|
fi
|
|
231
231
|
|
|
232
|
-
# Guard (#3400): gate-helper (trusted-comments.sh)
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
232
|
+
# Guard (#3400, #3454): every gate-helper (trusted-comments.sh) resolver block under commands/ must be
|
|
233
|
+
# byte-identical, must scan the plugin cache (the plugin-root placeholder is not always substituted in
|
|
234
|
+
# forked runs, so without the scan a cache-only install never resolves the script), and must never list the
|
|
235
|
+
# working directory (the repo under review is author-controlled). Blocks are discovered, not hard-coded, so a
|
|
236
|
+
# new copy cannot drift unnoticed. A block runs from its '# TRUSTED_SCRIPT resolver' header to 'done <<< "$_tc"'.
|
|
237
|
+
: > "$T/tc_all"; TC_BLOCKS=0; TC_STRAY=0
|
|
238
|
+
while IFS= read -r f; do
|
|
239
|
+
awk -v out="$T/tc_all" '
|
|
240
|
+
/^[ ]*# TRUSTED_SCRIPT resolver/ {inb=1; line=""}
|
|
241
|
+
inb { sub(/^[ ]+/, ""); line = line $0 "\\n" }
|
|
242
|
+
inb && /^done <<< "\$_tc"$/ { print line >> out; inb=0; n++ }
|
|
243
|
+
END { print n+0 }' "$f" > "$T/tc_n"
|
|
244
|
+
TC_BLOCKS=$((TC_BLOCKS + $(cat "$T/tc_n")))
|
|
245
|
+
# every trusted-comments.sh resolution case-arm must belong to a block (no stray short copies)
|
|
246
|
+
arms=$(grep -c 'trusted-comments.sh" \]' "$f" || true)
|
|
247
|
+
[ "$arms" = "$(cat "$T/tc_n")" ] || { TC_STRAY=$((TC_STRAY + 1)); echo " stray resolver in $f: $arms arms vs $(cat "$T/tc_n") blocks"; }
|
|
248
|
+
done < <(grep -rl 'trusted-comments.sh" \]' "$ROOT/commands")
|
|
249
|
+
expect "trusted-comments.sh resolver blocks discovered (review-pr 8 + review + remediate + phase-4 + review-pr-staging)" 12 "$TC_BLOCKS"
|
|
250
|
+
expect "trusted-comments.sh resolvers without a canonical block" 0 "$TC_STRAY"
|
|
251
|
+
expect "trusted-comments.sh resolvers byte-identical" 1 "$(sort -u "$T/tc_all" | wc -l | tr -d ' ')"
|
|
252
|
+
grep -qF 'plugins/cache/forgedock/forgedock' "$T/tc_all" && ok || bad 'trusted-comments.sh resolver lacks the plugin-cache scan'
|
|
253
|
+
grep -qF 'CLAUDE_CONFIG_DIR' "$T/tc_all" && ok || bad 'trusted-comments.sh resolver lacks CLAUDE_CONFIG_DIR'
|
|
254
|
+
grep -qF '"$PWD"' "$T/tc_all" && bad 'trusted-comments.sh resolver lists "$PWD"' || ok
|
|
255
|
+
|
|
256
|
+
# Behavior (#3454): a cache-only install with an unsubstituted plugin-root placeholder must resolve the script.
|
|
257
|
+
# Run the real block from review.md against a fake HOME that has only plugins/cache/forgedock/forgedock/<semver>.
|
|
258
|
+
awk '/^[ ]*# TRUSTED_SCRIPT resolver/ {inb=1} inb {sub(/^[ ]+/, ""); print} inb && /^done <<< "\$_tc"$/ {exit}' "$ROOT/commands/work-on/review.md" > "$T/tc_block.sh"
|
|
259
|
+
mkdir -p "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.9.0/scripts" "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts" "$T/tcwork"
|
|
260
|
+
: > "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.9.0/scripts/trusted-comments.sh"
|
|
261
|
+
: > "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/trusted-comments.sh"
|
|
262
|
+
mkdir -p "$T/tcwork/scripts"; : > "$T/tcwork/scripts/trusted-comments.sh" # author-controlled cwd copy must NOT win
|
|
263
|
+
out=$(cd "$T/tcwork" && env -i PATH="$PATH" HOME="$T/tchome" bash -c 'source "$1"; printf %s "$TRUSTED_SCRIPT"' _ "$T/tc_block.sh" 2>&1)
|
|
264
|
+
expect "cache-only install resolves newest cached trusted-comments.sh" "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/trusted-comments.sh" "$out"
|
|
265
|
+
out=$(cd "$T/tcwork" && env -i PATH="$PATH" HOME="$T/nohome" bash -c 'source "$1"; printf %s "$TRUSTED_SCRIPT"' _ "$T/tc_block.sh" 2>&1)
|
|
266
|
+
expect "no install resolves empty (cwd copy never used)" "" "$out"
|
|
267
|
+
|
|
268
|
+
# Guard (#3483): the other executable-helper resolvers (classify-finding.sh, amplification-breaker.sh,
|
|
269
|
+
# extract-affected-files.sh) must use the same trusted-install tiers: no "$PWD", no "$REPO_PATH", plugin-cache
|
|
270
|
+
# scan present. Blocks are extracted from the real specs (header '# HELPER_SCRIPT resolver' to 'done <<< "$_tc"',
|
|
271
|
+
# or the resolve_extract_affected_files function) so a drifting copy cannot pass on a hand-written stand-in.
|
|
272
|
+
extract_helper_block() { # file var -> block on stdout
|
|
273
|
+
awk -v var="$2" '/^[ ]*# HELPER_SCRIPT resolver \(/ && index($0, "(" var ";") {inb=1} inb {sub(/^[ ]+/, ""); print} inb && /^done <<< "\$_tc"$/ {exit}' "$1"
|
|
274
|
+
}
|
|
275
|
+
extract_helper_fn() { # file -> function on stdout
|
|
276
|
+
awk '/^resolve_extract_affected_files\(\) \{/ {inb=1} inb {print} inb && /^\}$/ {exit}' "$1"
|
|
277
|
+
}
|
|
278
|
+
HR_CLASSIFY="$ROOT/commands/review-pr.md"
|
|
279
|
+
HR_AMP="$ROOT/commands/orchestrate/phase-4-execution.md"
|
|
280
|
+
HR_EX3="$ROOT/commands/orchestrate/phase-3-dependency.md"
|
|
281
|
+
extract_helper_block "$HR_CLASSIFY" CLASSIFY_SCRIPT > "$T/hr_classify.sh"
|
|
282
|
+
extract_helper_block "$HR_AMP" AMP_SCRIPT > "$T/hr_amp.sh"
|
|
283
|
+
extract_helper_fn "$HR_AMP" > "$T/hr_ex4.sh"
|
|
284
|
+
extract_helper_fn "$HR_EX3" > "$T/hr_ex3.sh"
|
|
285
|
+
expect "CLASSIFY_SCRIPT resolver block found" 1 "$(grep -c '^CLASSIFY_SCRIPT=""' "$T/hr_classify.sh")"
|
|
286
|
+
expect "AMP_SCRIPT resolver block found" 1 "$(grep -c '^AMP_SCRIPT=""' "$T/hr_amp.sh")"
|
|
287
|
+
expect "extract-affected-files resolver found in phase-4" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex4.sh")"
|
|
288
|
+
expect "extract-affected-files resolver found in phase-3" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex3.sh")"
|
|
289
|
+
cmp -s "$T/hr_ex3.sh" "$T/hr_ex4.sh" && ok || bad 'resolve_extract_affected_files copies differ between phase-3 and phase-4'
|
|
290
|
+
for hr in classify amp ex3 ex4; do
|
|
291
|
+
grep -qF 'plugins/cache/forgedock/forgedock' "$T/hr_$hr.sh" && ok || bad "$hr helper resolver lacks the plugin-cache scan"
|
|
292
|
+
grep -qF 'CLAUDE_CONFIG_DIR' "$T/hr_$hr.sh" && ok || bad "$hr helper resolver lacks CLAUDE_CONFIG_DIR"
|
|
293
|
+
grep -qE '\$PWD|\$REPO_PATH|\$\{PWD|\$\{REPO_PATH' "$T/hr_$hr.sh" && bad "$hr helper resolver lists the working directory or repo path" || ok
|
|
242
294
|
done
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
295
|
+
# no leftover cwd-derived helper tier anywhere under commands/
|
|
296
|
+
if grep -rnE 'PWD/scripts/|^\$PWD$|REPO_PATH/scripts/extract' "$ROOT/commands" >/dev/null 2>&1; then bad 'commands/ still lists a working-directory helper tier'; else ok; fi
|
|
297
|
+
|
|
298
|
+
# Behavior (#3483): a planted scripts/<helper>.sh in the cwd must never be selected; a cache-only install must resolve.
|
|
299
|
+
mkdir -p "$T/hrwork/scripts" "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts"
|
|
300
|
+
for h in classify-finding amplification-breaker extract-affected-files; do
|
|
301
|
+
: > "$T/hrwork/scripts/$h.sh"; : > "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/$h.sh"
|
|
302
|
+
done
|
|
303
|
+
CACHE_SCRIPTS="$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts"
|
|
304
|
+
hr_run() { # home blockfile expr
|
|
305
|
+
(cd "$T/hrwork" && env -i PATH="$PATH" HOME="$1" REPO_PATH="$T/hrwork" bash -c 'source "$1"; '"$3" _ "$2" 2>/dev/null)
|
|
306
|
+
}
|
|
307
|
+
expect "CLASSIFY_SCRIPT: cache-only install resolves (cwd copy ignored)" "$CACHE_SCRIPTS/classify-finding.sh" "$(hr_run "$T/hrhome" "$T/hr_classify.sh" 'printf %s "$CLASSIFY_SCRIPT"')"
|
|
308
|
+
expect "CLASSIFY_SCRIPT: no install resolves empty (cwd copy ignored)" "" "$(hr_run "$T/nohome" "$T/hr_classify.sh" 'printf %s "$CLASSIFY_SCRIPT"')"
|
|
309
|
+
expect "AMP_SCRIPT: cache-only install resolves (cwd copy ignored)" "$CACHE_SCRIPTS/amplification-breaker.sh" "$(hr_run "$T/hrhome" "$T/hr_amp.sh" 'printf %s "$AMP_SCRIPT"')"
|
|
310
|
+
expect "AMP_SCRIPT: no install resolves empty (cwd copy ignored)" "" "$(hr_run "$T/nohome" "$T/hr_amp.sh" 'printf %s "$AMP_SCRIPT"')"
|
|
311
|
+
expect "extract-affected-files: cache-only install resolves (cwd and REPO_PATH copies ignored)" "$CACHE_SCRIPTS/extract-affected-files.sh" "$(hr_run "$T/hrhome" "$T/hr_ex4.sh" 'resolve_extract_affected_files')"
|
|
312
|
+
expect "extract-affected-files: no install resolves nothing (cwd and REPO_PATH copies ignored)" "" "$(hr_run "$T/nohome" "$T/hr_ex4.sh" 'resolve_extract_affected_files')"
|
|
313
|
+
|
|
314
|
+
# Guard (#3636): review-delta.sh takes its trust script ONLY from an explicit absolute --trusted-script. Any
|
|
315
|
+
# commands/ block that resolves review-delta.sh (added by the specs that consume it) must use the trusted-install
|
|
316
|
+
# tiers: plugin-cache scan and CLAUDE_CONFIG_DIR present, no "$PWD"/"$REPO_PATH" tier. Passes with 0 blocks today.
|
|
317
|
+
RD_BLOCKS=0
|
|
318
|
+
while IFS= read -r f; do
|
|
319
|
+
awk '/^[ ]*# HELPER_SCRIPT resolver \(/ && index($0, "(REVIEW_DELTA_SCRIPT;") {inb=1} inb {sub(/^[ ]+/, ""); print} inb && /^done <<< "\$_tc"$/ {exit}' "$f" > "$T/rd_block.sh"
|
|
320
|
+
[ -s "$T/rd_block.sh" ] || { bad "review-delta.sh referenced in $f without a canonical REVIEW_DELTA_SCRIPT resolver block"; continue; }
|
|
321
|
+
RD_BLOCKS=$((RD_BLOCKS + 1))
|
|
322
|
+
grep -qF 'plugins/cache/forgedock/forgedock' "$T/rd_block.sh" && ok || bad "review-delta resolver in $f lacks the plugin-cache scan"
|
|
323
|
+
grep -qF 'CLAUDE_CONFIG_DIR' "$T/rd_block.sh" && ok || bad "review-delta resolver in $f lacks CLAUDE_CONFIG_DIR"
|
|
324
|
+
grep -qE '\$PWD|\$REPO_PATH|\$\{PWD|\$\{REPO_PATH' "$T/rd_block.sh" && bad "review-delta resolver in $f lists the working directory or repo path" || ok
|
|
325
|
+
done < <(grep -rl 'review-delta\.sh' "$ROOT/commands" 2>/dev/null)
|
|
326
|
+
|
|
327
|
+
# Behavior (#3636): a planted scripts/trusted-comments.sh in the cwd must never be used. Without --trusted-script the
|
|
328
|
+
# resolver fails closed (FULL); a stub that would trust everything cannot change that.
|
|
329
|
+
mkdir -p "$T/rdwork/scripts"
|
|
330
|
+
printf '#!/usr/bin/env bash\ncat >/dev/null\nprintf "%%s\\n" "\\"<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->\\""\n' > "$T/rdwork/scripts/trusted-comments.sh"
|
|
331
|
+
echo '[]' > "$T/rdwork/comments.json"
|
|
332
|
+
out=$(cd "$T/rdwork" && bash "$ROOT/scripts/review-delta.sh" --pr 1 --head 0123456789abcdef0123456789abcdef01234567 --base main --comments-file comments.json --labels "" 2>&1 | sed -n 1p)
|
|
333
|
+
expect "review-delta.sh without --trusted-script prints FULL (cwd trust script ignored)" "FULL" "$out"
|
|
334
|
+
out=$(cd "$T/rdwork" && bash "$ROOT/scripts/review-delta.sh" --pr 1 --head 0123456789abcdef0123456789abcdef01234567 --base main --trusted-script scripts/trusted-comments.sh --comments-file comments.json --labels "" 2>&1 | sed -n 1p)
|
|
335
|
+
expect "review-delta.sh with a relative cwd --trusted-script prints FULL" "FULL" "$out"
|
|
246
336
|
|
|
247
337
|
echo "forge-root tests: pass=$PASS fail=$FAILN skipped=$SKIPPED"
|
|
248
338
|
[ "$FAILN" -eq 0 ]
|
package/scripts/forge-run.sh
CHANGED
|
@@ -285,7 +285,7 @@ fi
|
|
|
285
285
|
|
|
286
286
|
# Read current workflow labels
|
|
287
287
|
WORKFLOW_LABEL=""
|
|
288
|
-
for label in "workflow:in-review" "workflow:building" "workflow:ready-to-build" "workflow:investigating"; do
|
|
288
|
+
for label in "workflow:in-review" "workflow:remediating" "workflow:building" "workflow:ready-to-build" "workflow:investigating"; do
|
|
289
289
|
if printf '%s' "$LABELS_JSON" | jq -e --arg l "$label" 'contains([$l])' > /dev/null 2>&1; then
|
|
290
290
|
WORKFLOW_LABEL="$label"
|
|
291
291
|
break
|
|
@@ -299,7 +299,7 @@ ACTION=""
|
|
|
299
299
|
if [ "$REVIEW_STARTED" = "true" ]; then
|
|
300
300
|
PHASE="in-review"
|
|
301
301
|
ACTION="Review in progress — /review-pr handles merge. Check PR state."
|
|
302
|
-
elif [ "$BUILDER_COMPLETE" = "true" ] && [ "$WORKFLOW_LABEL" != "workflow:in-review" ]; then
|
|
302
|
+
elif [ "$BUILDER_COMPLETE" = "true" ] && [ "$WORKFLOW_LABEL" != "workflow:in-review" ] && [ "$WORKFLOW_LABEL" != "workflow:remediating" ]; then
|
|
303
303
|
PHASE="awaiting-pr"
|
|
304
304
|
ACTION="Builder complete. Create PR targeting the classified lane branch."
|
|
305
305
|
elif [ "$BUILDER_PRESENT" = "true" ] && [ "$BUILDER_COMPLETE" = "false" ]; then
|
|
@@ -32,10 +32,13 @@ old_iso() { date -u -d '2 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u
|
|
|
32
32
|
fx() { echo "$2" > "$TMP_FX/$1.json"; echo "$TMP_FX/$1.json"; }
|
|
33
33
|
claim() { jq -cn --arg id "$1" --arg t "$2" '{body:("<!-- FORGE:RECOVERY_CLAIM -->\n**Sweep: "+$id+"**\nHolder: /recover-orphans"), updated_at:$t}'; }
|
|
34
34
|
release() { jq -cn --arg id "$1" --arg t "$2" '{body:("<!-- FORGE:RECOVERY_CLAIM_RELEASED -->\n**Sweep: "+$id+"**\nReleased"), updated_at:$t}'; }
|
|
35
|
+
# comment <marker> <second-line> <updated_at>: built by jq, never hand-escaped. macOS /bin/bash 3.2 mis-parses
|
|
36
|
+
# escaped double quotes inside "$(fx name "...")", which turned literal JSON fixtures into invalid JSON there.
|
|
37
|
+
comment() { jq -cn --arg m "$1" --arg l "$2" --arg t "$3" '{body:("<!-- "+$m+" -->\n"+$l), updated_at:$t}'; }
|
|
35
38
|
|
|
36
39
|
run() { local f="$1"; shift; OUT=$(MOCK_GH_JSON="$f" bash "$CHECK" 7 -R o/r "$@" 2>/dev/null); RC=$?; }
|
|
37
40
|
expect() { # name rc needle
|
|
38
|
-
[ "$RC" -eq "$2" ] &&
|
|
41
|
+
[ "$RC" -eq "$2" ] && grep -q "$3" <<< "$OUT" && ok "$1" || bad "$1 (rc=$RC out=$OUT)"
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
N=$(now_iso); O=$(old_iso)
|
|
@@ -44,16 +47,16 @@ run "$(fx live "[$(claim sweep-A "$N")]")"; expect "fresh c
|
|
|
44
47
|
run "$(fx exp "[$(claim sweep-A "$O")]")"; expect "expired claim is FREE" 0 "CLAIM: FREE"
|
|
45
48
|
run "$(fx rel "[$(claim sweep-A "$N"), $(release sweep-A "$N")]")"; expect "released claim is FREE" 0 "CLAIM: FREE"
|
|
46
49
|
run "$(fx relother "[$(claim sweep-A "$N"), $(release sweep-B "$N")]")"; expect "release of another sweep does not free the claim" 1 "CLAIM: LIVE sweep-A"
|
|
47
|
-
run "$(fx unkrel "[
|
|
50
|
+
run "$(fx unkrel "[$(comment FORGE:RECOVERY_CLAIM "no sweep line" "$N"), $(comment FORGE:RECOVERY_CLAIM_RELEASED "no sweep line" "$N")]")"; expect "release with no sweep id does not free an id-less claim" 1 "CLAIM: LIVE unknown"
|
|
48
51
|
run "$(fx own "[$(claim sweep-A "$N")]")" --exempt-sweep sweep-A; expect "own sweep id is exempt" 0 "CLAIM: FREE"
|
|
49
52
|
run "$(fx other "[$(claim sweep-A "$N")]")" --exempt-sweep sweep-B; expect "other sweep id is not exempt" 1 "CLAIM: LIVE sweep-A"
|
|
50
53
|
run "$(fx two "[$(claim sweep-A "$N"), $(claim sweep-B "$N")]")" --exempt-sweep sweep-A; expect "exempting A still sees live B" 1 "CLAIM: LIVE sweep-B"
|
|
51
|
-
run "$(fx unk "[
|
|
52
|
-
run "$(fx unk2 "[
|
|
54
|
+
run "$(fx unk "[$(comment FORGE:RECOVERY_CLAIM "no sweep line" "$N")]")" --exempt-sweep sweep-A; expect "claim with no sweep id is never exempt" 1 "CLAIM: LIVE"
|
|
55
|
+
run "$(fx unk2 "[$(comment FORGE:RECOVERY_CLAIM "no sweep line" "$N")]")" --exempt-sweep unknown; expect "--exempt-sweep unknown is rejected" 2 "CLAIM: ERROR"
|
|
53
56
|
run "$(fx none '[]')"; expect "no comments is FREE" 0 "CLAIM: FREE"
|
|
54
|
-
run "$(fx hb "[
|
|
55
|
-
run "$(fx bad "[
|
|
56
|
-
run "$(fx nodate "[
|
|
57
|
+
run "$(fx hb "[$(comment FORGE:HEARTBEAT "Phase 3" "$N")]")"; expect "heartbeat alone is not a recovery claim" 0 "CLAIM: FREE"
|
|
58
|
+
run "$(fx bad "[$(comment FORGE:RECOVERY_CLAIM "no sweep line" "$N")]")"; expect "malformed claim fails closed (LIVE)" 1 "CLAIM: LIVE"
|
|
59
|
+
run "$(fx nodate "[$(comment FORGE:RECOVERY_CLAIM "**Sweep: sweep-A**" garbage)]")"; expect "unparsable updated_at fails closed (LIVE)" 1 "CLAIM: LIVE"
|
|
57
60
|
|
|
58
61
|
OUT=$(MOCK_GH_FAIL=1 MOCK_GH_JSON=/dev/null bash "$CHECK" 7 -R o/r 2>/dev/null); RC=$?
|
|
59
62
|
expect "gh outage is ERROR (exit 2)" 2 "CLAIM: ERROR"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# remediate-basesync.test.sh — spec guard for the Phase M3 base-sync block in commands/work-on/remediate.md (forge#3515)
|
|
3
|
+
# Asserts rc capture, MERGE_HEAD classification/guard, distinct refused blocker, sync-before-edits ordering,
|
|
4
|
+
# clean-tree precondition, and a scratch-repo check of the git behaviour the spec relies on. No network.
|
|
5
|
+
# Usage: bash scripts/remediate-basesync.test.sh
|
|
6
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
7
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
SPEC="$ROOT/commands/work-on/remediate.md"
|
|
13
|
+
|
|
14
|
+
PASS=0; FAILN=0
|
|
15
|
+
ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
16
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
17
|
+
eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
18
|
+
has() { if grep -qE -- "$2" "$SPEC"; then ok "$1"; else bad "$1"; fi; }
|
|
19
|
+
|
|
20
|
+
has "merge rc captured into MERGE_RC" 'MERGE_RC=\$\?'
|
|
21
|
+
has "MERGE_HEAD guard present" 'git rev-parse -q --verify MERGE_HEAD'
|
|
22
|
+
has "distinct refused-merge blocker" 'base-sync: merge refused'
|
|
23
|
+
has "ordering: sync before any other FIXABLE edit" 'before any (other )?(FIXABLE|fix) edit'
|
|
24
|
+
has "clean-tree precondition" 'git diff --quiet'
|
|
25
|
+
has "empty CONFLICT_FILES fallback" 'CONFLICT_FILES="\(no conflicted files'
|
|
26
|
+
has "unresolvable blocker unchanged" 'base-sync: unresolvable conflicts'
|
|
27
|
+
has "FORGE:BASESYNC_FAILED marker kept" 'FORGE:BASESYNC_FAILED'
|
|
28
|
+
if grep -nE '^[[:space:]]*git merge --abort' "$SPEC" >/dev/null; then bad "no bare git merge --abort line"; else ok "no bare git merge --abort line"; fi
|
|
29
|
+
if grep -nE 'git merge --abort' "$SPEC" | grep -vq 'MERGE_HEAD'; then bad "every git merge --abort is MERGE_HEAD-guarded"; else ok "every git merge --abort is MERGE_HEAD-guarded"; fi
|
|
30
|
+
# Unannotated side-effect lines would trip the #3541 gate
|
|
31
|
+
if grep -E 'git merge (origin|--abort)' "$SPEC" | grep -v 'allowlist:check-command-side-effects' | grep -vq '^[[:space:]]*$'; then
|
|
32
|
+
grep -E 'git merge (origin|--abort)' "$SPEC" | grep -v 'allowlist:check-command-side-effects' | grep -qE '^[[:space:]]*(MERGE_OUT=|git merge|if git rev-parse)' && bad "side-effect lines annotated" || ok "side-effect lines annotated"
|
|
33
|
+
else ok "side-effect lines annotated"; fi
|
|
34
|
+
|
|
35
|
+
has "M3 records merged base head" 'BASESYNC_BASE_SHA=\$\(git rev-parse origin/'
|
|
36
|
+
has "re-sync rounds hard-capped" 'BASESYNC_MAX_ROUNDS'
|
|
37
|
+
has "M6 distinguishes advanced base" 'Base advanced'
|
|
38
|
+
has "M8 trail persists base sha" 'Base sync\*\*: ran \(base='
|
|
39
|
+
has "rounds derived from pushed sync merges" 'BASESYNC_ROUNDS=\$\(git rev-list --first-parent --merges --count origin/\{PR_BASE\}\.\.origin/\{HEAD_BRANCH\}'
|
|
40
|
+
has "unreadable round count treated as the cap" '\|\| BASESYNC_ROUNDS=\$BASESYNC_MAX_ROUNDS'
|
|
41
|
+
if grep -qF 'BASESYNC_ROUNDS=$(( ${BASESYNC_ROUNDS:-0}' "$SPEC"; then bad "no shell-state round counter"; else ok "no shell-state round counter"; fi
|
|
42
|
+
if grep -qF '[ -n "${BASESYNC_BASE_SHA:-}" ]' "$SPEC"; then bad "freshness pass not gated on a shell var"; else ok "freshness pass not gated on a shell var"; fi
|
|
43
|
+
has "freshness push rc checked" 'if git push origin HEAD:\{HEAD_BRANCH\}; then'
|
|
44
|
+
has "freshness merge re-gated before push" 'Run the Phase M4 pre-push ancestry guard'
|
|
45
|
+
if grep -qF 'do NOT sync a second time' "$SPEC"; then bad "unconditional second-sync refusal removed"; else ok "unconditional second-sync refusal removed"; fi
|
|
46
|
+
|
|
47
|
+
# Behavioural: dirty tree + base change -> refused (rc!=0, no MERGE_HEAD, empty U-list); real conflict -> MERGE_HEAD present.
|
|
48
|
+
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
|
49
|
+
(
|
|
50
|
+
cd "$T" && git init -q -b main . && git config user.email t@t && git config user.name t
|
|
51
|
+
echo a > f && git add f && git commit -qm base
|
|
52
|
+
git checkout -qb feat && git checkout -q main && echo b > f && git commit -qam basechange
|
|
53
|
+
git checkout -q feat && echo local > f
|
|
54
|
+
out=$(git merge main --no-edit 2>&1); echo "$? $(git rev-parse -q --verify MERGE_HEAD >/dev/null && echo head || echo nohead) [$(git diff --name-only --diff-filter=U)]" > "$T/refused.out"
|
|
55
|
+
git checkout -q -- f && echo c > f && git commit -qam featchange
|
|
56
|
+
git merge main --no-edit >/dev/null 2>&1; echo "$? $(git rev-parse -q --verify MERGE_HEAD >/dev/null && echo head || echo nohead)" > "$T/conflict.out"
|
|
57
|
+
)
|
|
58
|
+
eq "refused merge: rc=1, no MERGE_HEAD, empty conflict list" "$(cat "$T/refused.out")" "1 nohead []"
|
|
59
|
+
eq "real conflict: rc=1 with MERGE_HEAD" "$(cat "$T/conflict.out")" "1 head"
|
|
60
|
+
|
|
61
|
+
# Advanced base: merge main at T0, main advances, head differs and a second merge is clean; unchanged base keeps the same SHA.
|
|
62
|
+
(
|
|
63
|
+
R="$T/adv"; mkdir "$R" && cd "$R" && git init -q -b main . && git config user.email t@t && git config user.name t
|
|
64
|
+
echo a > f && git add f && git commit -qm base
|
|
65
|
+
git checkout -qb feat && echo x > g && git add g && git commit -qm feat
|
|
66
|
+
git checkout -q main && echo b > f && git commit -qam m1 && git checkout -q feat
|
|
67
|
+
git update-ref refs/remotes/origin/main main
|
|
68
|
+
sha0=$(git rev-parse origin/main); git merge origin/main --no-edit >/dev/null 2>&1
|
|
69
|
+
same=$(git rev-parse origin/main)
|
|
70
|
+
git checkout -q main && echo c > h && git add h && git commit -qm sibling && git checkout -q feat
|
|
71
|
+
git update-ref refs/remotes/origin/main main
|
|
72
|
+
sha1=$(git rev-parse origin/main)
|
|
73
|
+
git merge origin/main --no-edit >/dev/null 2>&1; rc=$?
|
|
74
|
+
echo "$([ "$sha0" = "$same" ] && echo unchanged-same || echo unchanged-differs) $([ "$sha0" != "$sha1" ] && echo advanced || echo notadvanced) $rc" > "$T/adv.out"
|
|
75
|
+
)
|
|
76
|
+
eq "advanced base: SHA differs, unchanged base same, second merge clean" "$(cat "$T/adv.out")" "unchanged-same advanced 0"
|
|
77
|
+
|
|
78
|
+
# Durable derivation (forge#3517): the M3/M6 lines, run in a fresh shell against a scratch repo, recover rounds and the
|
|
79
|
+
# recorded base head from the pushed branch; an unpushed merge does not count.
|
|
80
|
+
DERIVE="$(grep -m1 -E '^BASESYNC_ROUNDS=\$\(git rev-list' "$SPEC")"$'\n'"$(grep -m1 -E '^_lm=\$\(git rev-list' "$SPEC")"$'\n'"$(grep -m1 -E '^\[ -n "\$_lm" \]' "$SPEC")"
|
|
81
|
+
DERIVE="${DERIVE//\{PR_BASE\}/main}"; DERIVE="${DERIVE//\{HEAD_BRANCH\}/feat}"
|
|
82
|
+
(
|
|
83
|
+
R="$T/derive"; mkdir "$R" && cd "$R" && git init -q -b main . && git config user.email t@t && git config user.name t
|
|
84
|
+
derive() { BASESYNC_MAX_ROUNDS=2; eval "$DERIVE"; echo "$BASESYNC_ROUNDS ${_p2:-none}"; }
|
|
85
|
+
echo a > f && git add f && git commit -qm base
|
|
86
|
+
git checkout -qb feat && echo x > g && git add g && git commit -qm feat
|
|
87
|
+
git update-ref refs/remotes/origin/main main; git update-ref refs/remotes/origin/feat feat
|
|
88
|
+
r0=$(derive)
|
|
89
|
+
git checkout -q main && echo b > f && git commit -qam m1 && git checkout -q feat
|
|
90
|
+
git update-ref refs/remotes/origin/main main; b1=$(git rev-parse main)
|
|
91
|
+
git merge origin/main --no-edit >/dev/null 2>&1
|
|
92
|
+
r_unpushed=$(derive)
|
|
93
|
+
git update-ref refs/remotes/origin/feat feat
|
|
94
|
+
r1=$(derive)
|
|
95
|
+
echo "$r0|$r_unpushed|$r1|$b1" > "$T/derive.out"
|
|
96
|
+
)
|
|
97
|
+
IFS='|' read -r D0 DU D1 B1 < "$T/derive.out"
|
|
98
|
+
eq "derive: no sync merge -> 0 rounds, no recorded head" "$D0" "0 none"
|
|
99
|
+
eq "derive: unpushed merge does not count" "$DU" "0 none"
|
|
100
|
+
eq "derive: pushed sync merge -> 1 round, recorded head = merged base" "$D1" "1 $B1"
|
|
101
|
+
|
|
102
|
+
echo "passed=$PASS failed=$FAILN"
|
|
103
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-delta.sh — fail-closed reviewed-SHA delta resolver for /review-pr.
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Classifies how the PR head moved since the last FULLY reviewed head, so callers can skip or shrink the
|
|
7
|
+
# domain panel. Pure bash + git + jq, no LLM. Standalone so the engine and the specs can both call it.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# review-delta.sh --pr <n> --head <40|64-hex> --base <branch> --trusted-script <abs path>
|
|
11
|
+
# [--comments-file <json>] [--repo <owner/repo>] [--labels <csv>] [--repo-path <dir>]
|
|
12
|
+
#
|
|
13
|
+
# --trusted-script REQUIRED absolute path to an existing trusted-comments.sh. The caller passes the path it
|
|
14
|
+
# resolved from the trusted install; this script never derives it from the working
|
|
15
|
+
# directory, the repo under review or its own location (a PR-controlled copy would let the
|
|
16
|
+
# PR author decide which comments count).
|
|
17
|
+
# --comments-file JSON of `gh api --paginate repos/<repo>/issues/<pr>/comments` (one array or several
|
|
18
|
+
# concatenated arrays). When absent the comments are fetched (needs --repo).
|
|
19
|
+
# --labels comma-separated PR labels. When absent they are fetched (needs --repo). A `review-degraded`
|
|
20
|
+
# label always yields FULL. `--labels ""` means "no labels" and is allowed.
|
|
21
|
+
# --repo-path git checkout to inspect (default: the current directory). Needs origin/<base> fetched.
|
|
22
|
+
#
|
|
23
|
+
# Output (always exit 0; two lines):
|
|
24
|
+
# line 1 FULL | BASE_SYNC_ONLY <reviewed> | DELTA <reviewed>..<head>
|
|
25
|
+
# line 2 FULL_ROUNDS=<n> number of distinct trusted full-panel SHAs (0 when none or on error)
|
|
26
|
+
#
|
|
27
|
+
# Contract notes for callers (#3637/#3638):
|
|
28
|
+
# - Every error path prints FULL. FULL is always safe (a full review), never a skipped one.
|
|
29
|
+
# - head == reviewed prints `DELTA <sha>..<sha>` (empty delta). The caller's same-head re-entry handles that
|
|
30
|
+
# case before calling; this classifier does not special-case it.
|
|
31
|
+
# - Checkpoint = newest full 40/64-hex `Reviewed-SHA:` on a trusted, complete-shape FORGE:REVIEW-AGENT body that
|
|
32
|
+
# also has a post-panel-guard proof (a synthesized body naming the SHA, or a `# PR Review Summary` whose
|
|
33
|
+
# **Reviewed commit** is the SHA and whose **Agents** count equals the distinct domains that posted). The
|
|
34
|
+
# FORGE:REVIEW_ROUTE sha= value is 7 chars and is never a checkpoint. A trusted review-panel-integrity gate
|
|
35
|
+
# failure / REVIEW_DEGRADED / REVIEW_BLOCKED marker posted after a SHA's first agent body disqualifies it.
|
|
36
|
+
# - FULL also when forge.yaml, the review specs, this script or the trust script changed in reviewed..head.
|
|
37
|
+
# - BASE_SYNC_ONLY also needs the per-path diff shape (modes, status, binary blob ids) of both 3-dot diffs
|
|
38
|
+
# to match: the +/- line comparison cannot see binary content or mode changes.
|
|
39
|
+
#
|
|
40
|
+
# bash 3.2 portable. No `set -e`: every read is captured and status-tested, never behind a pipe.
|
|
41
|
+
|
|
42
|
+
set -uo pipefail
|
|
43
|
+
|
|
44
|
+
ROUNDS=0
|
|
45
|
+
full() { echo "FULL"; echo "FULL_ROUNDS=${ROUNDS}"; exit 0; }
|
|
46
|
+
|
|
47
|
+
PR=""; HEAD_SHA=""; BASE=""; TRUSTED=""; COMMENTS_FILE=""; REPO=""; LABELS=""; LABELS_SET=0; REPO_PATH="."
|
|
48
|
+
while [ $# -gt 0 ]; do
|
|
49
|
+
case "$1" in
|
|
50
|
+
--pr) [ $# -ge 2 ] || full; PR="$2"; shift 2 ;;
|
|
51
|
+
--head) [ $# -ge 2 ] || full; HEAD_SHA="$2"; shift 2 ;;
|
|
52
|
+
--base) [ $# -ge 2 ] || full; BASE="$2"; shift 2 ;;
|
|
53
|
+
--trusted-script) [ $# -ge 2 ] || full; TRUSTED="$2"; shift 2 ;;
|
|
54
|
+
--comments-file) [ $# -ge 2 ] || full; COMMENTS_FILE="$2"; shift 2 ;;
|
|
55
|
+
--repo) [ $# -ge 2 ] || full; REPO="$2"; shift 2 ;;
|
|
56
|
+
--labels) [ $# -ge 2 ] || full; LABELS="$2"; LABELS_SET=1; shift 2 ;;
|
|
57
|
+
--repo-path) [ $# -ge 2 ] || full; REPO_PATH="$2"; shift 2 ;;
|
|
58
|
+
*) full ;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
valid_sha() { # 40 or 64 lowercase hex
|
|
63
|
+
case "$1" in *[!0-9a-f]*|"") return 1 ;; esac
|
|
64
|
+
[ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case "$PR" in ''|*[!0-9]*) full ;; esac
|
|
68
|
+
valid_sha "$HEAD_SHA" || full
|
|
69
|
+
case "$BASE" in ''|-*|*[!A-Za-z0-9._/-]*|*..*) full ;; esac
|
|
70
|
+
case "$TRUSTED" in /*) ;; *) full ;; esac
|
|
71
|
+
[ -f "$TRUSTED" ] || full
|
|
72
|
+
case "$REPO" in *[!A-Za-z0-9._/-]*) full ;; esac
|
|
73
|
+
[ -d "$REPO_PATH" ] || full
|
|
74
|
+
command -v jq >/dev/null 2>&1 || full
|
|
75
|
+
|
|
76
|
+
G() { git -C "$REPO_PATH" -c core.quotepath=off "$@"; }
|
|
77
|
+
|
|
78
|
+
# ---- inputs: comments + labels --------------------------------------------------------------------------------
|
|
79
|
+
if [ -n "$COMMENTS_FILE" ]; then
|
|
80
|
+
[ -f "$COMMENTS_FILE" ] && [ -r "$COMMENTS_FILE" ] || full
|
|
81
|
+
COMMENTS_JSON=$(cat "$COMMENTS_FILE" 2>/dev/null) || full
|
|
82
|
+
else
|
|
83
|
+
[ -n "$REPO" ] || full
|
|
84
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR}/comments" 2>/dev/null) || full
|
|
85
|
+
fi
|
|
86
|
+
[ -n "$COMMENTS_JSON" ] || full
|
|
87
|
+
|
|
88
|
+
if [ "$LABELS_SET" -eq 0 ]; then
|
|
89
|
+
[ -n "$REPO" ] || full
|
|
90
|
+
LABELS=$(gh pr view "$PR" -R "$REPO" --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null) || full
|
|
91
|
+
fi
|
|
92
|
+
case ",${LABELS}," in *,review-degraded,*) full ;; esac
|
|
93
|
+
|
|
94
|
+
# ---- checkpoint: newest SHA with a trusted, complete, proven panel ---------------------------------------------
|
|
95
|
+
RE='^(?=[\s\S]*<!-- REVIEW-FINDINGS-START -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->|^<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->|# PR Review Summary|<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity|<!-- FORGE:REVIEW_DEGRADED|<!-- FORGE:REVIEW_BLOCKED'
|
|
96
|
+
BODIES=$(bash "$TRUSTED" bodies "$RE" 2>/dev/null <<< "$COMMENTS_JSON"); rc=$?
|
|
97
|
+
[ "$rc" -eq 0 ] || full
|
|
98
|
+
|
|
99
|
+
# Bodies arrive in comment order, one JSON string per line. Everything is decided in one jq pass over that order
|
|
100
|
+
# (the degraded markers carry no SHA and no timestamp, so relative order is the only signal).
|
|
101
|
+
JQ_PROG='
|
|
102
|
+
def shas($t; $re): [ $t | scan($re) | .[0] ];
|
|
103
|
+
def one($a): if ($a | length) == 1 then $a[0] else null end;
|
|
104
|
+
to_entries as $E
|
|
105
|
+
| ($E | map(select(.value | test("<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity|<!-- FORGE:REVIEW_DEGRADED|<!-- FORGE:REVIEW_BLOCKED")) | .key)) as $DEG
|
|
106
|
+
| ([ $E[] | .key as $i | .value as $t
|
|
107
|
+
| select($t | test("^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->"))
|
|
108
|
+
| one(shas($t; "(?:^|\\n)Reviewed-SHA: ([0-9a-f]{40}|[0-9a-f]{64})\\r?(?:\\n|$)")) as $s
|
|
109
|
+
| select($s != null)
|
|
110
|
+
| {i: $i, sha: $s, d: ($t | capture("^<!-- FORGE:REVIEW-AGENT:(?<d>[a-z-]+) -->").d)} ]) as $AG
|
|
111
|
+
| ([ $E[] | .key as $i | .value as $t
|
|
112
|
+
| select($t | startswith("<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->"))
|
|
113
|
+
| one(shas($t; "(?:^|\\n)Reviewed-SHA: ([0-9a-f]{40}|[0-9a-f]{64})\\r?(?:\\n|$)")) as $s
|
|
114
|
+
| select($s != null)
|
|
115
|
+
| {i: $i, sha: $s} ]) as $SY
|
|
116
|
+
| ([ $E[] | .key as $i | .value as $t
|
|
117
|
+
| select($t | test("# PR Review Summary"))
|
|
118
|
+
| one(shas($t; "\\*\\*Reviewed commit\\*\\*: `?([0-9a-f]{40}|[0-9a-f]{64})`?(?:[ |\\r\\n]|$)")) as $s
|
|
119
|
+
| one(shas($t; "\\*\\*Agents\\*\\*: ([0-9]+)")) as $n
|
|
120
|
+
| select($s != null and $n != null)
|
|
121
|
+
| {i: $i, sha: $s, n: ($n | tonumber)} ]) as $SU
|
|
122
|
+
| ([ $AG | group_by(.sha)[]
|
|
123
|
+
| {sha: .[0].sha, first: (map(.i) | min), domains: (map(.d) | unique | length)}
|
|
124
|
+
| . as $g
|
|
125
|
+
| select(
|
|
126
|
+
(($SY | map(select(.sha == $g.sha)) | length) > 0
|
|
127
|
+
or ($SU | map(select(.sha == $g.sha and .n == $g.domains)) | length) > 0)
|
|
128
|
+
and (($DEG | map(select(. > $g.first)) | length) == 0))
|
|
129
|
+
| . + {last: ([ ($AG[] | select(.sha == $g.sha) | .i), ($SY[] | select(.sha == $g.sha) | .i),
|
|
130
|
+
($SU[] | select(.sha == $g.sha) | .i) ] | max)} ]) as $OK
|
|
131
|
+
| (if ($OK | length) == 0 then "-" else ($OK | max_by(.last) | .sha) end), ($OK | length)'
|
|
132
|
+
PARSED=$(jq -s -r "$JQ_PROG" 2>/dev/null <<< "$BODIES"); rc=$?
|
|
133
|
+
[ "$rc" -eq 0 ] || full
|
|
134
|
+
REVIEWED=$(printf '%s\n' "$PARSED" | sed -n '1p'); N=$(printf '%s\n' "$PARSED" | sed -n '2p')
|
|
135
|
+
case "$N" in ''|*[!0-9]*) full ;; esac
|
|
136
|
+
ROUNDS="$N"
|
|
137
|
+
[ "$REVIEWED" = "-" ] && full
|
|
138
|
+
valid_sha "$REVIEWED" || full
|
|
139
|
+
|
|
140
|
+
# ---- classification --------------------------------------------------------------------------------------------
|
|
141
|
+
G cat-file -e "${HEAD_SHA}^{commit}" 2>/dev/null || full
|
|
142
|
+
G cat-file -e "${REVIEWED}^{commit}" 2>/dev/null || full
|
|
143
|
+
G merge-base --is-ancestor "$REVIEWED" "$HEAD_SHA" 2>/dev/null; rc=$?
|
|
144
|
+
[ "$rc" -eq 0 ] || full # 1 = force-push / rebase, >1 = git error: both FULL
|
|
145
|
+
|
|
146
|
+
NAMES=$(G diff --name-only --no-renames "$REVIEWED" "$HEAD_SHA" 2>/dev/null); rc=$?
|
|
147
|
+
[ "$rc" -eq 0 ] || full
|
|
148
|
+
while IFS= read -r f; do
|
|
149
|
+
case "$f" in
|
|
150
|
+
forge.yaml|commands/review-pr*.md|commands/review-pr-agents/*|scripts/review-delta.sh|scripts/trusted-comments.sh) full ;;
|
|
151
|
+
esac
|
|
152
|
+
done <<< "$NAMES"
|
|
153
|
+
|
|
154
|
+
DELTA_OUT="DELTA ${REVIEWED}..${HEAD_SHA}"
|
|
155
|
+
|
|
156
|
+
G rev-parse --verify --quiet "origin/${BASE}^{commit}" >/dev/null 2>&1 || full
|
|
157
|
+
CHAIN=$(G rev-list --first-parent "${REVIEWED}..${HEAD_SHA}" 2>/dev/null); rc=$?
|
|
158
|
+
[ "$rc" -eq 0 ] || full
|
|
159
|
+
if [ -z "$CHAIN" ]; then echo "$DELTA_OUT"; echo "FULL_ROUNDS=${ROUNDS}"; exit 0; fi
|
|
160
|
+
|
|
161
|
+
# BASE_SYNC_ONLY needs every first-parent commit to be a clean merge of origin/<base>
|
|
162
|
+
SYNC=1
|
|
163
|
+
while IFS= read -r c; do
|
|
164
|
+
[ -n "$c" ] || continue
|
|
165
|
+
PARENTS=$(G rev-list --parents -n 1 "$c" 2>/dev/null); rc=$?
|
|
166
|
+
[ "$rc" -eq 0 ] || full
|
|
167
|
+
set -- $PARENTS
|
|
168
|
+
if [ $# -ne 3 ]; then SYNC=0; break; fi # not a plain two-parent merge
|
|
169
|
+
G merge-base --is-ancestor "$3" "origin/${BASE}" 2>/dev/null; rc=$?
|
|
170
|
+
[ "$rc" -le 1 ] || full
|
|
171
|
+
if [ "$rc" -ne 0 ]; then SYNC=0; break; fi # second parent is not base history
|
|
172
|
+
CC=$(G diff-tree --cc --no-commit-id -r "$c" 2>/dev/null); rc=$?
|
|
173
|
+
[ "$rc" -eq 0 ] || full
|
|
174
|
+
if [ -n "$CC" ]; then SYNC=0; break; fi # conflict-resolution content in the merge
|
|
175
|
+
done <<< "$CHAIN"
|
|
176
|
+
|
|
177
|
+
# per-path shape of a diff: modes, status and path for every file, plus both blob ids for binary files
|
|
178
|
+
# (whose content the +/- line comparison cannot see). Text blob ids are left out: they shift when base moves.
|
|
179
|
+
diff_shape() {
|
|
180
|
+
local raw bin
|
|
181
|
+
raw=$(G diff --raw --no-abbrev --no-renames "$1" 2>/dev/null) || return 1
|
|
182
|
+
bin=$(G diff --numstat --no-renames "$1" 2>/dev/null) || return 1
|
|
183
|
+
{ printf '%s\n' "$bin"; printf '%s\n' '--RAW--'; printf '%s\n' "$raw"; } | awk -F'\t' '
|
|
184
|
+
$0 == "--RAW--" { r = 1; next }
|
|
185
|
+
!r { if ($1 == "-" && $2 == "-") b[$3] = 1; next }
|
|
186
|
+
$0 == "" { next }
|
|
187
|
+
{ split($1, m, " "); line = m[1] " " m[2] " " m[5] "\t" $2; if ($2 in b) line = line " " m[3] " " m[4]; print line }'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if [ "$SYNC" -eq 1 ]; then
|
|
191
|
+
# the PR's own change must be unchanged: compare the +/- lines of both 3-dot diffs (index/hunk numbers shift
|
|
192
|
+
# when base moves, content lines do not)
|
|
193
|
+
D_OLD=$(G diff --no-color --no-ext-diff --no-renames -U0 "origin/${BASE}...${REVIEWED}" 2>/dev/null); rc=$?
|
|
194
|
+
[ "$rc" -eq 0 ] || full
|
|
195
|
+
D_NEW=$(G diff --no-color --no-ext-diff --no-renames -U0 "origin/${BASE}...${HEAD_SHA}" 2>/dev/null); rc=$?
|
|
196
|
+
[ "$rc" -eq 0 ] || full
|
|
197
|
+
L_OLD=$(printf '%s\n' "$D_OLD" | sed -n '/^[-+]/p'); L_NEW=$(printf '%s\n' "$D_NEW" | sed -n '/^[-+]/p')
|
|
198
|
+
# +/- lines miss binary content and mode changes, so the per-path shape must match too
|
|
199
|
+
S_OLD=$(diff_shape "origin/${BASE}...${REVIEWED}") || full
|
|
200
|
+
S_NEW=$(diff_shape "origin/${BASE}...${HEAD_SHA}") || full
|
|
201
|
+
if [ "$L_OLD" = "$L_NEW" ] && [ "$S_OLD" = "$S_NEW" ]; then
|
|
202
|
+
echo "BASE_SYNC_ONLY ${REVIEWED}"; echo "FULL_ROUNDS=${ROUNDS}"; exit 0
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
echo "$DELTA_OUT"; echo "FULL_ROUNDS=${ROUNDS}"; exit 0
|