forgedock 1.19.0 → 1.21.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/README.md +2 -0
- package/bin/engine/phases.mjs +4 -2
- package/bin/engine/telemetry-seam.mjs +1 -1
- package/bin/engine-cli.mjs +40 -1
- package/bin/engine.mjs +48 -3
- package/bin/forgedock.mjs +1 -1
- package/bin/lean/effects.mjs +51 -0
- package/bin/lean/lean.mjs +352 -0
- package/bin/lean/lean.test.mjs +525 -0
- package/bin/lean/prompts.mjs +62 -0
- package/bin/lean/risk.mjs +49 -0
- package/bin/runner.mjs +99 -6
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr.md +7 -6
- package/commands/work-on/build/architect.md +1 -1
- package/commands/work-on/build/implement.md +9 -1
- package/commands/work-on/build/validate.md +14 -7
- package/commands/work-on/build.md +118 -8
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/remediate.md +11 -4
- package/commands/work-on/review.md +12 -4
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +8 -1
- package/docs/RELEASE-NOTES.md +21 -0
- package/docs/TELEMETRY.md +2 -2
- package/docs/WORK-ON-RUNTIME.md +11 -3
- package/package.json +2 -2
- package/scripts/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/ci-local/lib.mjs +11 -3
- package/scripts/ci-local/run.mjs +155 -49
- package/scripts/ci-local/scoping.mjs +27 -4
- package/scripts/ci-local.sh +3 -2
- package/scripts/ci-local.test.sh +189 -0
- package/scripts/classify-finding.sh +1 -1
- package/scripts/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- package/scripts/lib/telemetry.sh +2 -0
- package/scripts/review-finalize.sh +101 -12
- package/scripts/review-finalize.test.sh +136 -5
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +8 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/verify-phase-trail.test.sh +6 -0
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
# Phase 7-9 subcommands: --context FILE is optional; without it pass --review-sha SHA, --review-mode FULL|DELTA|BASE_SYNC_ONLY, --issue N, --base B.
|
|
51
51
|
# verdict: --merge-health S --merge-health-state S [--base B] [--head-branch H] [--is-milestone true|false] [--has-purpose-regression true|false]
|
|
52
52
|
# [--purpose-findings-file F | --purpose-findings TEXT] [--result stale|clean|blocking] [--agents-file F | --agents TEXT] [--findings-count N] [--blocking-count N]
|
|
53
|
-
# [--intensity-tier T] [--table-cell-key K] [--trust-shadow-mode B] [--dry-run]
|
|
53
|
+
# [--intensity-tier T] [--table-cell-key K] [--trust-shadow-mode B] [--other-blocking true|false] [--out-dir D] [--dry-run]
|
|
54
|
+
# (--other-blocking: the reviewer's criterion 1/2/3/5 result; resolved and persisted to OUT_DIR/other-blocking.txt, which merge-gate ORs in)
|
|
54
55
|
# calibrate: [--intensity-tier T] [--table-cell-key K] [--trust-shadow-mode B] [--trust-needs-human B] [--dry-run]; the issue is context contract.issue or --issue N
|
|
55
56
|
# merge-gate: --verdict "APPROVED|CHANGES REQUESTED" --has-purpose-regression B --calibration-needs-human B --trust-needs-human B
|
|
56
57
|
# --base-conflict B --other-blocking B [--calibration-note-file F | --calibration-note T] [--table-cell-key K] [--base B] [--issue N]
|
|
@@ -407,13 +408,15 @@ EOF
|
|
|
407
408
|
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate > "$FILES_JSON" 2>/dev/null || : > "$FILES_JSON"
|
|
408
409
|
# "path:line" for every added line; an empty file means the diff was unreadable (gates then keep every finding).
|
|
409
410
|
ADDED_LINES_FILE="$WORKDIR/added-lines"
|
|
410
|
-
|
|
411
|
+
# Second stream from the same pass: "path<TAB>text" per added line, for the Gate 2 symbol fallback.
|
|
412
|
+
ADDED_TEXT_FILE="$WORKDIR/added-text"; : > "$ADDED_TEXT_FILE"
|
|
413
|
+
jq -r '.[] | select(.patch != null) | "FILE\t\(.filename)", .patch' "$FILES_JSON" 2>/dev/null | awk -v tf="$ADDED_TEXT_FILE" '
|
|
411
414
|
/^FILE\t/{f=substr($(0),6);next}
|
|
412
415
|
/^@@/{s=$(0);sub(/^@@ -[0-9,]+ \+/,"",s);sub(/[ ,].*/,"",s);n=s+0;next}
|
|
413
416
|
/^\\/{next}
|
|
414
|
-
/^\+/{print f":"n;n++;next}
|
|
417
|
+
/^\+/{print f":"n; print f "\t" substr($(0),2) > tf; n++;next}
|
|
415
418
|
/^-/{next}
|
|
416
|
-
{n++}' > "$ADDED_LINES_FILE" || : > "$ADDED_LINES_FILE"
|
|
419
|
+
{n++}' > "$ADDED_LINES_FILE" || { : > "$ADDED_LINES_FILE"; : > "$ADDED_TEXT_FILE"; }
|
|
417
420
|
# Files the PR deleted or renamed (the old name too): findings on them always intersect.
|
|
418
421
|
GONE_FILES_FILE="$WORKDIR/gone-files"
|
|
419
422
|
jq -r '.[] | select(.status=="removed" or .status=="renamed") | .filename, (.previous_filename // empty)' "$FILES_JSON" > "$GONE_FILES_FILE" 2>/dev/null || : > "$GONE_FILES_FILE"
|
|
@@ -435,6 +438,52 @@ EOF
|
|
|
435
438
|
if gh api -H "Accept: application/vnd.github.raw" "repos/${REPO}/contents/${f}?ref=${REVIEW_SHA}" > "$t" 2>/dev/null; then awk 'END{print NR}' "$t"; else echo UNKNOWN; fi
|
|
436
439
|
}
|
|
437
440
|
|
|
441
|
+
# Gate 2 symbol fallback: 0 when an identifier the finding names occurs (whole word, fixed string) in the added lines of FILE
|
|
442
|
+
# and nowhere in the base version of FILE (a symbol the PR introduced).
|
|
443
|
+
# Candidates: backticked tokens, plus identifier-shaped tokens on "Root cause" lines; each must match an anchored identifier
|
|
444
|
+
# regex of 4+ chars and not be a common word. Reviewer text is never used in arithmetic, a glob or a regex. Any failure returns 1.
|
|
445
|
+
# A symbol counts only when it is NEW: present in the added lines AND absent from the base version of the file (#4068 review
|
|
446
|
+
# DSEC-1/DBH-1: `self`, `return`, `result` in an added line must not re-scope a pre-existing finding). The base text comes from
|
|
447
|
+
# `git show <base>:<file>` (local commit) or the raw contents API; a file the PR adds has an empty base; an unreadable base
|
|
448
|
+
# disables the fallback (the line-window result stands).
|
|
449
|
+
SYM_BASE_SHA=""
|
|
450
|
+
base_text_for() { # base_text_for <file> <out>; 0 = out holds the base text (possibly empty), 1 = unknown
|
|
451
|
+
local f="$1" o="$2"
|
|
452
|
+
if jq -e --arg f "$f" 'any(.[]; .filename == $f and .status == "added")' "$FILES_JSON" >/dev/null 2>&1; then : > "$o"; return 0; fi
|
|
453
|
+
[ -n "$SYM_BASE_SHA" ] || SYM_BASE_SHA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq .base.sha 2>/dev/null || true)
|
|
454
|
+
case "$SYM_BASE_SHA" in *[!0-9a-f]*|"") SYM_BASE_SHA="-"; return 1 ;; -) return 1 ;; esac
|
|
455
|
+
if git rev-parse -q --verify "${SYM_BASE_SHA}^{commit}" >/dev/null 2>&1; then
|
|
456
|
+
git show "${SYM_BASE_SHA}:${f}" > "$o" 2>/dev/null && return 0
|
|
457
|
+
git cat-file -e "${SYM_BASE_SHA}:${f}" 2>/dev/null && return 1
|
|
458
|
+
: > "$o"; return 0
|
|
459
|
+
fi
|
|
460
|
+
case "$f" in *[!A-Za-z0-9._/@+-]*|"") return 1 ;; esac
|
|
461
|
+
gh api -H "Accept: application/vnd.github.raw" "repos/${REPO}/contents/${f}?ref=${SYM_BASE_SHA}" > "$o" 2>/dev/null && return 0
|
|
462
|
+
return 1
|
|
463
|
+
}
|
|
464
|
+
symbol_in_added() {
|
|
465
|
+
local f="$1" text="$2" t="$WORKDIR/sym-added" b="$WORKDIR/sym-base" c n=0
|
|
466
|
+
[ -s "$ADDED_TEXT_FILE" ] || return 1
|
|
467
|
+
awk -v f="$f" 'index($0, f "\t") == 1 { print substr($0, length(f) + 2) }' "$ADDED_TEXT_FILE" > "$t" 2>/dev/null || return 1
|
|
468
|
+
[ -s "$t" ] || return 1
|
|
469
|
+
base_text_for "$f" "$b" || return 1
|
|
470
|
+
while IFS= read -r c; do
|
|
471
|
+
case "$c" in ""|[!A-Za-z_]*) continue ;; esac
|
|
472
|
+
[ "${#c}" -ge 4 ] || continue
|
|
473
|
+
case "$(printf '%s' "$c" | tr '[:upper:]' '[:lower:]')" in
|
|
474
|
+
this|that|with|from|true|false|null|none|file|line|code|test|case|value|root|cause|when|then|which|where|there|their|error|data|name|path|type|call|only|also|into|have|does|used|uses) continue ;;
|
|
475
|
+
esac
|
|
476
|
+
n=$((n + 1)); [ "$n" -le 8 ] || break
|
|
477
|
+
if grep -qwF -- "$c" "$t" 2>/dev/null && ! grep -qwF -- "$c" "$b" 2>/dev/null; then return 0; fi
|
|
478
|
+
done <<SYMEOF
|
|
479
|
+
$({
|
|
480
|
+
printf '%s\n' "$text" | grep -oE '`[^`]+`' 2>/dev/null | tr -c 'A-Za-z0-9_\n' '\n'
|
|
481
|
+
printf '%s\n' "$text" | grep -iE 'root cause' 2>/dev/null | grep -oE '[A-Za-z_][A-Za-z0-9_]*' | grep -E '_|[a-z][A-Z]|[0-9]'
|
|
482
|
+
} 2>/dev/null | grep -E '^[A-Za-z_][A-Za-z0-9_]{3,}$' | awk '!seen[$0]++')
|
|
483
|
+
SYMEOF
|
|
484
|
+
return 1
|
|
485
|
+
}
|
|
486
|
+
|
|
438
487
|
FINAL="$WORKDIR/final.jsonl"; : > "$FINAL"
|
|
439
488
|
N_STALE=0; N_PREEXISTING=0; N_NO_SCENARIO=0; N_INTRODUCED=0; N_KEPT=0
|
|
440
489
|
I=0; TOTAL=$(jq 'length' "$DEDUPED")
|
|
@@ -472,6 +521,8 @@ EOF
|
|
|
472
521
|
if grep -qxF -- "${FILE}:${L}" "$ADDED_LINES_FILE"; then INTRODUCED=1; break; fi
|
|
473
522
|
L=$((L + 1))
|
|
474
523
|
done
|
|
524
|
+
# The cited line is the reviewer's estimate (stale or at the call site): symbol fallback before routing pre-existing.
|
|
525
|
+
if [ "$INTRODUCED" -eq 0 ] && symbol_in_added "$FILE" "$(printf '%s' "$F" | jq -r '.text // ""')"; then INTRODUCED=1; fi
|
|
475
526
|
if [ "$INTRODUCED" -eq 1 ]; then SCOPE="introduced"; else
|
|
476
527
|
SCOPE="pre-existing"
|
|
477
528
|
case "$SEV" in MEDIUM|LOW|INFO) ROUTE="pre-existing" ;; esac
|
|
@@ -561,7 +612,7 @@ do_classify() {
|
|
|
561
612
|
&& [ -s "$CONTRACT_SCOPE_FILE" ] \
|
|
562
613
|
&& gh pr diff "$PR_NUMBER" -R "$REPO" --name-only > "$PR_FILES_FILE" 2>/dev/null; then
|
|
563
614
|
# A deferred row counts only if issue N is OPEN, is an issue (not a PR), is not MERGE_ISSUE, and its body
|
|
564
|
-
# carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` plus the row's path, and is authored by a trusted author
|
|
615
|
+
# carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` plus a checklist line for the row's exact path (the list script strips a trailing slash, so `p` and `p/` both match; `- [ ] \`path\``, [xX] accepted, the producer format of file-deferred-followups.sh; a body that only mentions the path does not count), and is authored by a trusted author
|
|
565
616
|
# (OWNER/MEMBER/COLLABORATOR or Bot; same predicate as trusted-comments.sh; missing fields fail closed).
|
|
566
617
|
# Unverified rows are dropped.
|
|
567
618
|
_verified="$_scratch/contract-verified"; : > "$_verified"
|
|
@@ -571,7 +622,7 @@ do_classify() {
|
|
|
571
622
|
[ "$_n" = "${MERGE_ISSUE#\#}" ] && continue
|
|
572
623
|
gh api "repos/${REPO}/issues/$_n" 2>/dev/null \
|
|
573
624
|
| jq -e --arg m "FORGE:DEFERRED_FROM: #${MERGE_ISSUE#\#} -->" --arg p "$_ipath" \
|
|
574
|
-
'.state == "open" and .pull_request == null and ((.body // "") | contains($m) and
|
|
625
|
+
'.state == "open" and .pull_request == null and ((.body // "") | (contains($m) and (split("\n") | any(rtrimstr("\r") as $l | ("- [ ] `","- [x] `","- [X] `") as $pre | ($p, $p + "/") as $q | ($pre + $q + "`") as $w | $l == $w or ($l | startswith($w + " ")))))) and (((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR")) or ((.user.type // "") == "Bot"))' >/dev/null 2>&1 \
|
|
575
626
|
|| continue
|
|
576
627
|
grep -Fxq -- "$_n" "$CONTRACT_OPEN_FILE" || echo "$_n" >> "$CONTRACT_OPEN_FILE"
|
|
577
628
|
fi
|
|
@@ -696,11 +747,32 @@ do_classify() {
|
|
|
696
747
|
fi
|
|
697
748
|
case "$DISPOSITION" in
|
|
698
749
|
"NOTE contract-deferred #"*)
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
750
|
+
# The demotion counts only after the comment on #N succeeded and #N is still OPEN; any failure files the finding (fails toward filing).
|
|
751
|
+
_dn="${DISPOSITION##*#}"; _dok=1
|
|
752
|
+
case "$_dn" in ''|*[!0-9]*) _dok=0 ;; *)
|
|
753
|
+
if [ "$DRY_RUN" != "true" ]; then
|
|
754
|
+
_dst=$(gh issue view "$_dn" -R "$REPO" --json state --jq .state 2>/dev/null) || _dst=""
|
|
755
|
+
if [ "$_dst" != "OPEN" ]; then _dok=0
|
|
756
|
+
elif ! gh issue comment "$_dn" -R "$REPO" --body "Review of #${PR_NUMBER} also found this on a path declared deferred here: ${F_LOC} ${F_TITLE}" >/dev/null 2>&1; then _dok=0 # allowlist:check-command-side-effects
|
|
757
|
+
fi
|
|
758
|
+
fi
|
|
702
759
|
;; esac
|
|
703
|
-
CONTRACT_DEMOTED=$((CONTRACT_DEMOTED + 1))
|
|
760
|
+
if [ "$_dok" = "1" ]; then CONTRACT_DEMOTED=$((CONTRACT_DEMOTED + 1)); else
|
|
761
|
+
# Fall back to what the finding would have been absent the contract (a LOW/POSSIBLE or lineage-damped finding stays a NOTE),
|
|
762
|
+
# not a blanket ISSUE: re-run the classifier without the contract flags. An INPR_FIX or unusable result files (fails toward filing).
|
|
763
|
+
DISPOSITION="ISSUE contract-comment-failed"
|
|
764
|
+
_n=$#; _skip=0
|
|
765
|
+
while [ "$_n" -gt 0 ]; do
|
|
766
|
+
_a="$1"; shift; _n=$((_n - 1))
|
|
767
|
+
if [ "$_skip" = "1" ]; then _skip=0; continue; fi
|
|
768
|
+
case "$_a" in --contract-scope|--contract-open|--pr-files) _skip=1 ;; *) set -- "$@" "$_a" ;; esac
|
|
769
|
+
done
|
|
770
|
+
_fb=$(bash "$CLASSIFY_SCRIPT" "$@" 2>/dev/null | head -n 1) || _fb=""
|
|
771
|
+
case "$_fb" in
|
|
772
|
+
"NOTE reflag"*) DISPOSITION="$_fb"; NOTES_REFLAG=$((NOTES_REFLAG + 1)) ;;
|
|
773
|
+
NOTE*|ISSUE*) DISPOSITION="$_fb" ;;
|
|
774
|
+
esac
|
|
775
|
+
fi ;;
|
|
704
776
|
"NOTE contract-accepted-risk"*) CONTRACT_DEMOTED=$((CONTRACT_DEMOTED + 1)) ;;
|
|
705
777
|
"NOTE reflag-"*) NOTES_REFLAG=$((NOTES_REFLAG + 1)) ;;
|
|
706
778
|
INPR_FIX*)
|
|
@@ -1021,6 +1093,20 @@ do_verdict() {
|
|
|
1021
1093
|
if [ "$RESULT" = "clean" ] && { [ "$BASE_CONFLICT" = "true" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ]; }; then
|
|
1022
1094
|
RESULT="blocking"; VERDICT_COERCED=true
|
|
1023
1095
|
fi
|
|
1096
|
+
# OTHER_BLOCKING (criteria 1, 2, 3 and 5) is reviewer-judged, so it is resolved here and persisted for merge-gate to OR in: a blocking
|
|
1097
|
+
# result that BASE_CONFLICT and HAS_PURPOSE_REGRESSION do not explain was caused by another criterion, whatever the reviewer passed.
|
|
1098
|
+
OTHER_BLOCKING_RESOLVED=""
|
|
1099
|
+
case "$OTHER_BLOCKING" in true|false) OTHER_BLOCKING_RESOLVED="$OTHER_BLOCKING" ;; esac
|
|
1100
|
+
if [ "$RESULT" = "blocking" ] && [ "$BASE_CONFLICT" != "true" ] && [ "$HAS_PURPOSE_REGRESSION" != "true" ]; then
|
|
1101
|
+
OTHER_BLOCKING_RESOLVED=true
|
|
1102
|
+
elif [ "$RESULT" = "clean" ]; then
|
|
1103
|
+
OTHER_BLOCKING_RESOLVED=false
|
|
1104
|
+
fi
|
|
1105
|
+
# Rewritten (or removed) on every run so a stale value from a prior head can never survive a re-review.
|
|
1106
|
+
rm -f "$OUT_DIR/other-blocking.txt" 2>/dev/null || true
|
|
1107
|
+
if { [ "$RESULT" = "clean" ] || [ "$RESULT" = "blocking" ]; } && [ -n "$OTHER_BLOCKING_RESOLVED" ] && [ "$DRY_RUN" != "true" ]; then
|
|
1108
|
+
printf '%s\n' "$OTHER_BLOCKING_RESOLVED" > "$OUT_DIR/other-blocking.txt" 2>/dev/null || true
|
|
1109
|
+
fi
|
|
1024
1110
|
VERDICT_OUT=""
|
|
1025
1111
|
case "$RESULT" in
|
|
1026
1112
|
stale) VERDICT_OUT="STALE" ;;
|
|
@@ -1055,6 +1141,7 @@ ${TRUST_ANNOTATION}${CONFLICT_LINE}${PROTECTION_LINE}${PURPOSE_LINE}${ATTRIBUTIO
|
|
|
1055
1141
|
echo "HAS_PURPOSE_REGRESSION=${HAS_PURPOSE_REGRESSION}"
|
|
1056
1142
|
echo "HAS_MERGE_CONFLICT=${HAS_MERGE_CONFLICT}"
|
|
1057
1143
|
echo "BASE_CONFLICT=${BASE_CONFLICT}"
|
|
1144
|
+
echo "OTHER_BLOCKING=${OTHER_BLOCKING_RESOLVED}"
|
|
1058
1145
|
echo "MERGE_CONFLICT_MSG=${MERGE_CONFLICT_MSG}"
|
|
1059
1146
|
echo "MERGE_PROTECTION_WARNING=${MERGE_PROTECTION_WARNING}"
|
|
1060
1147
|
exit 0
|
|
@@ -1347,6 +1434,8 @@ The review must add a \`### Degraded Paths Reviewed\` section that names each de
|
|
|
1347
1434
|
gh issue edit "$MERGE_ISSUE" -R "$REPO" --add-label "needs-human" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
1348
1435
|
blocked "merge guard state unbound"
|
|
1349
1436
|
fi
|
|
1437
|
+
# OR in the value `verdict` persisted: a passed `false` never overrides a persisted `true`. Anything else in the file is ignored.
|
|
1438
|
+
if [ "$(head -n 1 "$OUT_DIR/other-blocking.txt" 2>/dev/null | tr -d '[:space:]')" = "true" ]; then OTHER_BLOCKING=true; fi
|
|
1350
1439
|
# ONLY_BASE_CONFLICT: the base conflict is the ONLY reason for CHANGES REQUESTED, evaluated after every human gate is set.
|
|
1351
1440
|
ONLY_BASE_CONFLICT=false
|
|
1352
1441
|
if [ "$BASE_CONFLICT" = "true" ] && [ "$OTHER_BLOCKING" != "true" ] && [ "$HAS_PURPOSE_REGRESSION" != "true" ] && [ "$CALIBRATION_NEEDS_HUMAN" != "true" ] && [ "$TRUST_NEEDS_HUMAN" != "true" ]; then
|
|
@@ -1354,7 +1443,7 @@ The review must add a \`### Degraded Paths Reviewed\` section that names each de
|
|
|
1354
1443
|
fi
|
|
1355
1444
|
if [ "$ONLY_BASE_CONFLICT" = "true" ] && [ "$VERDICT" = "CHANGES REQUESTED" ]; then
|
|
1356
1445
|
# Pure base conflict: route to the base-sync path, NOT needs-human. A standalone /review-pr caller just sees the blocker.
|
|
1357
|
-
gh issue comment "$MERGE_ISSUE" -R "$REPO" --body "⛔ Auto-merge aborted for PR #${PR_NUMBER}: base conflict with \`${BASE}\` (the only blocking reason).
|
|
1446
|
+
gh issue comment "$MERGE_ISSUE" -R "$REPO" --body "⛔ Auto-merge aborted for PR #${PR_NUMBER}: base conflict with \`${BASE}\` (the only blocking reason). Base sync needed: the work-on pipeline performs the base sync (merging \`origin/${BASE}\` into the PR branch, merge only, no rebase, no force-push); a standalone \`/review-pr --auto-merge\` caller with no work-on router must run \`/work-on ${MERGE_ISSUE} --remediate\` to do it. The sync re-runs the quality gate and then re-reviews the new head. A human is pulled in only if the conflict cannot be resolved automatically." # <!-- allowlist:check-command-side-effects -->
|
|
1358
1447
|
echo "MERGE_OUTCOME=base-conflict"
|
|
1359
1448
|
blocked "base-conflict"
|
|
1360
1449
|
elif [ "$VERDICT" = "CHANGES REQUESTED" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ] || [ "$CALIBRATION_NEEDS_HUMAN" = "true" ] || [ "$TRUST_NEEDS_HUMAN" = "true" ]; then
|
|
@@ -1382,7 +1471,7 @@ The review must add a \`### Degraded Paths Reviewed\` section that names each de
|
|
|
1382
1471
|
fi
|
|
1383
1472
|
if [ "$PRE_MERGE_BASE_CONFLICT" = "true" ]; then
|
|
1384
1473
|
# Base moved during review: route to the base-sync path, NOT needs-human.
|
|
1385
|
-
gh issue comment "$MERGE_ISSUE" -R "$REPO" --body "⛔ Auto-merge aborted for PR #${PR_NUMBER}: base conflict (\`mergeable=${PRE_MERGE_HEALTH}\`, \`mergeStateStatus=${PRE_MERGE_HEALTH_STATE}\`).
|
|
1474
|
+
gh issue comment "$MERGE_ISSUE" -R "$REPO" --body "⛔ Auto-merge aborted for PR #${PR_NUMBER}: base conflict (\`mergeable=${PRE_MERGE_HEALTH}\`, \`mergeStateStatus=${PRE_MERGE_HEALTH_STATE}\`). Base sync needed: the work-on pipeline performs the base sync (merging \`origin/${BASE}\` into the PR branch, merge only, no rebase, no force-push); a standalone \`/review-pr --auto-merge\` caller with no work-on router must run \`/work-on ${MERGE_ISSUE} --remediate\` to do it. The sync is followed by a re-review. A human is pulled in only if the conflict cannot be resolved automatically." # <!-- allowlist:check-command-side-effects -->
|
|
1386
1475
|
echo "MERGE_OUTCOME=base-conflict"
|
|
1387
1476
|
blocked "base-conflict"
|
|
1388
1477
|
elif [ "$PRE_MERGE_HEALTH_STATE" = "BLOCKED" ]; then
|
|
@@ -39,13 +39,15 @@ case "$a" in
|
|
|
39
39
|
"api --paginate repos/o/r/pulls/"*"/files"*) cat "$FIX/pr-files.txt" 2>/dev/null || exit 1 ;;
|
|
40
40
|
"api repos/o/r/pulls/"*"--jq .changed_files") cat "$FIX/changed-n" 2>/dev/null || exit 1 ;;
|
|
41
41
|
"api repos/o/r/pulls/"*"--jq .head.sha") cat "$FIX/head" ;;
|
|
42
|
+
"api repos/o/r/pulls/"*"--jq .base.sha") cat "$FIX/base-sha" 2>/dev/null || exit 1 ;;
|
|
42
43
|
"api repos/o/r/branches/"*"/protection"*) cat "$FIX/protection" 2>/dev/null || exit 1 ;;
|
|
43
44
|
"api repos/o/r/issues/"*"/comments --paginate --jq"*) cat "$FIX/route-bodies" 2>/dev/null; exit 0 ;;
|
|
44
45
|
"api repos/o/r/issues/"*"/comments --jq"*) cat "$FIX/investigator" 2>/dev/null; exit 0 ;;
|
|
45
46
|
"api repos/o/r/commits/"*) cat "$FIX/commit-sha" 2>/dev/null || exit 1 ;;
|
|
46
47
|
"api repos/o/r/compare/"*) cat "$FIX/compare-files" 2>/dev/null || exit 1 ;;
|
|
47
48
|
"pr comment"*) { printf '%s\n' '=====COMMENT====='; for x in "$@"; do last="$x"; done; printf '%s\n' "$last"; } >> "$FIX/posted.log"; exit 0 ;;
|
|
48
|
-
"issue
|
|
49
|
+
"issue view"*"--json state"*) [ -f "$FIX/issue-closed" ] && echo CLOSED || echo OPEN; exit 0 ;;
|
|
50
|
+
"issue comment"*) [ -f "$FIX/issue-comment-fail" ] && exit 1; printf 'ISSUE_COMMENT %s\n' "$3" >> "$FIX/issue-comments.log"; { for x in "$@"; do last="$x"; done; printf '%s\n' "$last"; } >> "$FIX/issue-bodies.log"; exit 0 ;;
|
|
49
51
|
"label create"*) exit 0 ;;
|
|
50
52
|
"pr view"*"headRefOid"*) cat "$FIX/head" ;;
|
|
51
53
|
"pr view"*"baseRefName"*) echo main ;;
|
|
@@ -317,6 +319,55 @@ for pair in G-1:introduced G-2:pre-existing G-3:pre-existing G-4:unknown G-7:pre
|
|
|
317
319
|
done
|
|
318
320
|
want "gate2 table: a file absent from the PR and from the head is stale (gate 1), not pre-existing" '[.findings[].id] | contains(["G-14"]) | not' findings.json "true"
|
|
319
321
|
|
|
322
|
+
# symbol fallback (#4061): a stale or call-site cited line still scopes introduced when a named identifier is in the added lines
|
|
323
|
+
reset
|
|
324
|
+
printf '%s\n' '[{"filename":"s.sh","status":"modified","patch":"@@ -9,2 +9,3 @@\n ctx baseOnlyName\n+stepKey=1 stepKeyed=2 a*b $(x) self return result computeLaneBudget\n ctx"}]' > "$FIX/pr-files.json"
|
|
325
|
+
# The base version of s.sh (a dangling commit, HEAD untouched): base-present words never count as new symbols.
|
|
326
|
+
_bb=$(printf 'ctx baseOnlyName\nctx\nself return result\n' | git -C "$REPO_DIR" hash-object -w --stdin)
|
|
327
|
+
_bt=$(printf '100644 blob %s\ts.sh\n' "$_bb" | git -C "$REPO_DIR" mktree)
|
|
328
|
+
git -C "$REPO_DIR" commit-tree "$_bt" -m base-s > "$FIX/base-sha"
|
|
329
|
+
SY_PROSE=""; SY_MARKS=""; SY_N=0
|
|
330
|
+
sy() { # sy <id> <title> <extra prose line>
|
|
331
|
+
SY_PROSE="${SY_PROSE}### $1 finding
|
|
332
|
+
**Failure scenario**: s
|
|
333
|
+
$3
|
|
334
|
+
"
|
|
335
|
+
SY_N=$((SY_N + 1))
|
|
336
|
+
SY_MARKS="${SY_MARKS}<!-- FINDING:$1|LIKELY|MEDIUM|s.sh:$((300 + SY_N * 20))|$2 -->
|
|
337
|
+
"
|
|
338
|
+
}
|
|
339
|
+
sy Y-1 "alpha stale" 'The dedup key `stepKey` is wrong'
|
|
340
|
+
sy Y-2 "bravo base" 'Calls `baseOnlyName` badly'
|
|
341
|
+
sy Y-3 "charlie generic" 'Uses `this` and `file` and `abc` here'
|
|
342
|
+
sy Y-4 "delta substring" 'Wrong `stepKe` or `step` usage'
|
|
343
|
+
sy Y-5 "echo plain" 'No backticks and no cause line at all'
|
|
344
|
+
sy Y-6 "foxtrot meta" 'Odd `a*b` and `$(x)` and `[` tokens'
|
|
345
|
+
sy Y-7 "golf rootcause" 'Root cause: stepKey is built too late'
|
|
346
|
+
sy Y-8 "hotel self" 'The `self` handle leaks'
|
|
347
|
+
sy Y-9 "india return" 'Missing `return` on the error path'
|
|
348
|
+
sy Y-10 "juliet result" 'The `result` is discarded'
|
|
349
|
+
sy Y-11 "kilo newfn" 'The new `computeLaneBudget` overflows'
|
|
350
|
+
cm MEMBER User "$(agent_body security "$SHA" "$SY_PROSE" "$SY_MARKS")"; flush
|
|
351
|
+
run "$GOOD" extract; rc_is "symbol fallback" "$RC" 0
|
|
352
|
+
# stale cited line (outside every added hunk) naming an added identifier: Y-1, Y-7 introduced
|
|
353
|
+
# generic words (this, file) and identifiers under 4 characters (abc): Y-3 never triggers the fallback
|
|
354
|
+
for pair in Y-1:introduced Y-2:pre-existing Y-3:pre-existing Y-4:pre-existing Y-5:pre-existing Y-6:pre-existing Y-7:introduced Y-8:pre-existing Y-9:pre-existing Y-10:pre-existing Y-11:introduced; do
|
|
355
|
+
want "symbol fallback: ${pair%%:*}" ".findings[] | select(.id == \"${pair%%:*}\") | .scope" findings.json "${pair#*:}"
|
|
356
|
+
done
|
|
357
|
+
want "symbol fallback: stale cited line naming an added identifier is introduced" '[.findings[] | select(.id == "Y-1" or .id == "Y-7") | .scope] | join(",")' findings.json "introduced,introduced"
|
|
358
|
+
want "symbol fallback: generic words and short identifiers never trigger the fallback" '.findings[] | select(.id == "Y-3") | .scope' findings.json "pre-existing"
|
|
359
|
+
want "symbol fallback: introduced routes file" '.findings[] | select(.id == "Y-1") | .route' findings.json "file"
|
|
360
|
+
want "symbol fallback: base-only stays pre-existing route" '.findings[] | select(.id == "Y-2") | .route' findings.json "pre-existing"
|
|
361
|
+
want "symbol fallback: self/return/result present in base stay pre-existing (DSEC-1/DBH-1)" '[.findings[] | select(.id == "Y-8" or .id == "Y-9" or .id == "Y-10") | .scope] | join(",")' findings.json "pre-existing,pre-existing,pre-existing"
|
|
362
|
+
want "symbol fallback: a brand-new function name is introduced" '.findings[] | select(.id == "Y-11") | .scope' findings.json "introduced"
|
|
363
|
+
# unreadable added text (extraction failure) keeps the line-window result
|
|
364
|
+
reset; echo '[]' > "$FIX/pr-files.json"
|
|
365
|
+
cm MEMBER User "$(agent_body security "$SHA" '### Y-1 f
|
|
366
|
+
**Failure scenario**: s
|
|
367
|
+
`stepKey`
|
|
368
|
+
' '<!-- FINDING:Y-1|LIKELY|MEDIUM|s.sh:300|alpha stale -->')"; flush
|
|
369
|
+
run "$GOOD" extract; want "symbol fallback: no diff keeps" '.findings[0].scope' findings.json "unknown"
|
|
370
|
+
|
|
320
371
|
# DELTA: the earlier reviewed head's findings are read as well
|
|
321
372
|
reset; mkctx 1 DELTA "$OLD_SHA"
|
|
322
373
|
cm MEMBER User "$(agent_body security "$SHA" '### D-1 new
|
|
@@ -462,16 +513,49 @@ run "$GOOD" classify --auto-merge true; has "untrusted marker" "INPR_FIX=1"
|
|
|
462
513
|
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
463
514
|
CONTRACT_BODY=$(printf '<!-- FORGE:CONTRACT -->\n### Out of Scope\n\n- `docs/` — deferred → #30: tracked there\n')
|
|
464
515
|
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
465
|
-
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12
|
|
516
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/` — why","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
466
517
|
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
467
518
|
run "$GOOD" classify; has "contract" "CONTRACT_DEMOTED=1"
|
|
468
519
|
want "contract-deferred" '.findings[0].disposition' classified.json "NOTE contract-deferred #30"
|
|
469
520
|
[ "$(cat "$FIX/issue-comments.log" 2>/dev/null)" = "ISSUE_COMMENT 30" ] && ok || bad "deferred issue comment not posted"
|
|
470
521
|
posted_has "contract list line" "tracked on #30"
|
|
522
|
+
# substring-only / longer-path / legacy / empty bodies must not verify (e1, e3, e4); [x] and regex-metachar paths do (e2, e3)
|
|
523
|
+
for B in 'x <!-- FORGE:DEFERRED_FROM: #12 --> docs/' 'x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/guide.md.bak` — why' 'x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/x`' 'x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/`x' ''; do
|
|
524
|
+
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
525
|
+
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
526
|
+
echo "{\"state\":\"open\",\"body\":\"$B\",\"author_association\":\"MEMBER\",\"user\":{\"type\":\"User\"}}" > "$FIX/issue-30.json"
|
|
527
|
+
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
528
|
+
run "$GOOD" classify; has "unverified-body '$B'" "CONTRACT_DEMOTED=0"
|
|
529
|
+
done
|
|
530
|
+
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
531
|
+
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
532
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [x] `docs/`","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
533
|
+
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
534
|
+
run "$GOOD" classify; has "checked-item-verifies" "CONTRACT_DEMOTED=1"
|
|
535
|
+
# a failing comment on the deferred issue, or a no-longer-open one, files the finding instead of demoting it
|
|
536
|
+
for _mode in issue-comment-fail issue-closed; do
|
|
537
|
+
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
538
|
+
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
539
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/` — why","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
540
|
+
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
541
|
+
: > "$FIX/$_mode"
|
|
542
|
+
run "$GOOD" classify; has "$_mode not counted" "CONTRACT_DEMOTED=0"
|
|
543
|
+
want "$_mode falls back to the no-contract disposition" '.findings[0].disposition' classified.json "ISSUE MEDIUM-LIKELY"
|
|
544
|
+
rm -f "$FIX/$_mode"
|
|
545
|
+
done
|
|
546
|
+
# a failing comment on a LOW/POSSIBLE finding falls back to its no-contract disposition (a NOTE), not a blanket ISSUE (DB-1)
|
|
547
|
+
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
548
|
+
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
549
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/` — why","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
550
|
+
mkfind K-1 POSSIBLE LOW docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
551
|
+
: > "$FIX/issue-comment-fail"
|
|
552
|
+
run "$GOOD" classify; has "low-fallback not counted" "CONTRACT_DEMOTED=0"
|
|
553
|
+
want "low finding falls back to its NOTE" '.findings[0].class' classified.json "NOTE"
|
|
554
|
+
rm -f "$FIX/issue-comment-fail"
|
|
471
555
|
# an untrusted contract demotes nothing
|
|
472
556
|
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
473
557
|
cm NONE User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
474
|
-
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12
|
|
558
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/` — why","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
475
559
|
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
476
560
|
run "$GOOD" classify; has "untrusted contract" "CONTRACT_DEMOTED=0"
|
|
477
561
|
|
|
@@ -809,9 +893,25 @@ rc_is "guard: non-boolean" "$RC" 3; has "guard: non-boolean" "merge guard state
|
|
|
809
893
|
# --- verdict / calibration / trust / base-conflict routing ---
|
|
810
894
|
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true
|
|
811
895
|
rc_is "route: only base conflict" "$RC" 3; has "route: only base conflict" "blocker: base-conflict"; has "route: only base conflict" "MERGE_OUTCOME=base-conflict"
|
|
812
|
-
logged "route: base-sync comment" issue-bodies.log "the pipeline
|
|
896
|
+
logged "route: base-sync comment" issue-bodies.log "the work-on pipeline performs the base sync"; logged "route: standalone remediate action" issue-bodies.log "--remediate"; not_logged "route: base conflict is not needs-human" edits.log "needs-human"; not_merged "route: base conflict"
|
|
813
897
|
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true --other-blocking true
|
|
814
898
|
rc_is "route: base conflict plus blocker" "$RC" 3; has "route: base conflict plus blocker" "MERGE_OUTCOME=needs-human"; logged "route: needs-human" edits.log "needs-human"
|
|
899
|
+
# other-blocking-derived: a persisted true (written by verdict) is ORed in; a passed false never lowers it
|
|
900
|
+
ob() { mkdir -p "$WORK/out"; printf '%s\n' "$1" > "$WORK/out/other-blocking.txt"; }
|
|
901
|
+
gate_reset; ob true; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true --other-blocking false
|
|
902
|
+
rc_is "other-blocking-derived: persisted true beats passed false" "$RC" 3; has "other-blocking-derived: persisted true beats passed false" "MERGE_OUTCOME=needs-human"
|
|
903
|
+
not_has() { case "$OUT" in *"$2"*) bad "$1: output has '$2'" ;; *) ok ;; esac; }
|
|
904
|
+
not_has "other-blocking-derived: not base-conflict" "MERGE_OUTCOME=base-conflict"; logged "other-blocking-derived: needs-human" edits.log "needs-human"
|
|
905
|
+
gate_reset; ob false; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true
|
|
906
|
+
rc_is "other-blocking-derived: persisted false keeps base-conflict route" "$RC" 3; has "other-blocking-derived: persisted false" "MERGE_OUTCOME=base-conflict"
|
|
907
|
+
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true
|
|
908
|
+
has "other-blocking-derived: missing file uses passed flag" "MERGE_OUTCOME=base-conflict"
|
|
909
|
+
for junk in "" "maybe" "TRUE" "yes"; do
|
|
910
|
+
gate_reset; ob "$junk"; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true
|
|
911
|
+
has "other-blocking-derived: garbage file '$junk' ignored" "MERGE_OUTCOME=base-conflict"
|
|
912
|
+
done
|
|
913
|
+
gate_reset; ob true; mg "$GATE" --other-blocking ""
|
|
914
|
+
rc_is "other-blocking-derived: unbound passed flag still fails closed" "$RC" 3; has "other-blocking-derived: unbound still fails closed" "merge guard state unbound"
|
|
815
915
|
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true --trust-needs-human true --table-cell-key "feature x HIGH"
|
|
816
916
|
rc_is "route: base conflict plus trust" "$RC" 3; logged "route: trust reason" issue-bodies.log "provenance trust: NOVEL_NEEDS_HUMAN tier for cell \`feature x HIGH\`"; logged "route: trust needs-human" edits.log "needs-human"
|
|
817
917
|
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true --calibration-needs-human true --calibration-note "overconfident cell"
|
|
@@ -834,7 +934,7 @@ rc_is "route: CHANGES REQUESTED" "$RC" 3; logged "route: verdict reason" issue-b
|
|
|
834
934
|
|
|
835
935
|
# --- pre-merge mergeability guard ---
|
|
836
936
|
gate_reset; echo "CONFLICTING|DIRTY" > "$FIX/premerge"; mg "$GATE"
|
|
837
|
-
rc_is "premerge: conflicting" "$RC" 3; has "premerge: conflicting" "blocker: base-conflict"; not_logged "premerge: conflict is not needs-human" edits.log "needs-human"; not_merged "premerge: conflicting"
|
|
937
|
+
rc_is "premerge: conflicting" "$RC" 3; has "premerge: conflicting" "blocker: base-conflict"; not_logged "premerge: conflict is not needs-human" edits.log "needs-human"; logged "premerge: standalone base-sync wording" issue-bodies.log "/work-on 42 --remediate"; not_merged "premerge: conflicting"
|
|
838
938
|
gate_reset; echo "MERGEABLE|BEHIND" > "$FIX/premerge"; echo true > "$FIX/protection"; mg "$GATE"
|
|
839
939
|
rc_is "premerge: behind under strict protection" "$RC" 3; has "premerge: behind under strict protection" "blocker: base-conflict"
|
|
840
940
|
gate_reset; echo "MERGEABLE|BEHIND" > "$FIX/premerge"; echo false > "$FIX/protection"; mg "$GATE"
|
|
@@ -896,6 +996,22 @@ reset; MH=MERGEABLE; MHS=CLEAN; vd --result clean --intensity-tier FAST --table-
|
|
|
896
996
|
logged "verdict: trust annotation" reviews.log '**Intensity tier**: FAST | **Cell**: `bug HIGH` | **Shadow mode**: false'
|
|
897
997
|
reset; MH=CONFLICTING; MHS=DIRTY; vd --result blocking --blocking-count 1
|
|
898
998
|
has "verdict: blocking" "VERDICT=CHANGES REQUESTED"; logged "verdict: blocking body" reviews.log "CHANGES REQUESTED: commit"; logged "verdict: base conflict line" reviews.log "🔴 Base Conflict:"
|
|
999
|
+
# other-blocking-derived: verdict resolves and persists OTHER_BLOCKING
|
|
1000
|
+
OBF="$WORK/out/other-blocking.txt"
|
|
1001
|
+
reset; MH=MERGEABLE; MHS=CLEAN; vd --result blocking --blocking-count 1 --other-blocking false
|
|
1002
|
+
has "other-blocking-derived: unexplained blocking forces true" "OTHER_BLOCKING=true"; [ "$(cat "$OBF" 2>/dev/null)" = "true" ] && ok || bad "other-blocking-derived: file lacks true"
|
|
1003
|
+
reset; MH=MERGEABLE; MHS=CLEAN; vd --result blocking --blocking-count 1
|
|
1004
|
+
has "other-blocking-derived: no flag still derives true" "OTHER_BLOCKING=true"
|
|
1005
|
+
reset; MH=CONFLICTING; MHS=DIRTY; vd --result blocking --blocking-count 1 --other-blocking false
|
|
1006
|
+
has "other-blocking-derived: base conflict explains the block" "OTHER_BLOCKING=false"; [ "$(cat "$OBF" 2>/dev/null)" = "false" ] && ok || bad "other-blocking-derived: file lacks false"
|
|
1007
|
+
reset; MH=CONFLICTING; MHS=DIRTY; vd --result blocking --blocking-count 1 --other-blocking true
|
|
1008
|
+
has "other-blocking-derived: reviewer true is kept" "OTHER_BLOCKING=true"
|
|
1009
|
+
reset; MH=MERGEABLE; MHS=CLEAN; mkdir -p "$WORK/out"; echo true > "$OBF"; vd --result clean --other-blocking true
|
|
1010
|
+
has "other-blocking-derived: clean result resets to false" "OTHER_BLOCKING=false"; [ "$(cat "$OBF" 2>/dev/null)" = "false" ] && ok || bad "other-blocking-derived: stale true survived a clean re-review"
|
|
1011
|
+
reset; MH=MERGEABLE; MHS=CLEAN; mkdir -p "$WORK/out"; echo true > "$OBF"; vd --result stale
|
|
1012
|
+
[ -e "$OBF" ] && bad "other-blocking-derived: stale result must remove the file" || ok
|
|
1013
|
+
reset; MH=MERGEABLE; MHS=CLEAN; vd --result blocking --other-blocking maybe --blocking-count 1
|
|
1014
|
+
has "other-blocking-derived: garbage flag, unexplained block" "OTHER_BLOCKING=true"
|
|
899
1015
|
reset; MH=CONFLICTING; MHS=DIRTY; vd --result clean
|
|
900
1016
|
has "verdict: clean coerced over a base conflict" "VERDICT_COERCED=true"; has "verdict: coerced verdict" "VERDICT=CHANGES REQUESTED"; logged "verdict: coerced body" reviews.log "CHANGES REQUESTED: commit"
|
|
901
1017
|
reset; MH=MERGEABLE; MHS=CLEAN; vd --result clean --has-purpose-regression true --purpose-findings "F-1"
|
|
@@ -1007,6 +1123,21 @@ reset; rm -f "$FIX/comments.json"; ig; has "integrity: unreadable comments count
|
|
|
1007
1123
|
reset; mkctx 1 BASE_SYNC_ONLY "$OLD_SHA" 42; cm MEMBER User "$(agent_body security "$SHA" 'a' '')"; flush; ig
|
|
1008
1124
|
has "integrity: unverified carry is not reported as carried" "CARRIED_FROM: "; case "$OUT" in *"carried from"*) bad "integrity: carried requires a verified record" ;; *) ok ;; esac
|
|
1009
1125
|
|
|
1126
|
+
# forge#3971: a build-time FORGE:SELFREVIEW-AGENT comment (even a trusted one pinned to the current head) is never a panel member
|
|
1127
|
+
sr_body() { printf '<!-- FORGE:SELFREVIEW-AGENT:%s -->\n%s\n<!-- REVIEW-FINDINGS-START -->\n%s\n<!-- REVIEW-FINDINGS-END -->\nReviewed-SHA: %s\nTree: abc123\n' "$1" "$3" "$4" "$2"; }
|
|
1128
|
+
reset
|
|
1129
|
+
cm MEMBER User "$(sr_body security "$SHA" 'self' '<!-- FINDING:SEC-9|CONFIRMED|HIGH|a.py:9|Self-review finding -->')"; cm MEMBER Bot "<!-- FORGE:SELFREVIEW -->
|
|
1130
|
+
Tree: abc123"; flush
|
|
1131
|
+
ig; has "integrity: self-review comments never count as panel agents" "ACTUAL_AGENT_COUNT=0"
|
|
1132
|
+
case "$OUT" in *security*) bad "integrity: SELFREVIEW-AGENT domain leaked into the panel" ;; *) ok ;; esac
|
|
1133
|
+
reset
|
|
1134
|
+
cm MEMBER User "$(sr_body security "$SHA" 'self' '<!-- FINDING:SEC-9|CONFIRMED|HIGH|a.py:9|Self-review finding -->')"; flush
|
|
1135
|
+
run "$GOOD" extract
|
|
1136
|
+
has "extract: a self-review comment alone is no review (fail closed)" "no trusted reviewer comments"
|
|
1137
|
+
reset
|
|
1138
|
+
cm MEMBER User "$(sr_body security "$SHA" 'self' '')"; cm MEMBER User "$(agent_body api "$SHA" 'real' '')"; flush
|
|
1139
|
+
ig; has "integrity: only the REVIEW-AGENT counts beside a self-review" "ACTUAL_AGENT_COUNT=1"; has "integrity: only the real domain" "ACTUAL_AGENT_DOMAINS=api"
|
|
1140
|
+
|
|
1010
1141
|
# --- Phase 7-9 subcommands run without a review-prepare context (flags only) ---
|
|
1011
1142
|
nctx() { local sub="$1"; shift; OUT=$(cd "$REPO_DIR" && FORGEDOCK_HOME="$GATE" bash "$ROOT/scripts/review-finalize.sh" "$sub" --pr 7 --repo o/r "$@" 2>"$WORK/stderr"); RC=$?; }
|
|
1012
1143
|
gate_reset; nctx merge-gate --issue 42 --base main --verdict APPROVED --has-purpose-regression false --calibration-needs-human false --trust-needs-human false --base-conflict false --other-blocking false
|
package/scripts/review-roster.sh
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
#
|
|
18
18
|
# Tiers, evaluated in this order:
|
|
19
19
|
# full THOROUGH or milestone->staging: Security plus every domain with a score > 0.
|
|
20
|
-
# fix-pr the PR closes a `review-finding` issue
|
|
20
|
+
# fix-pr the PR closes a `review-finding` issue AND has <= 150 changed lines with a known size: Security, plus the
|
|
21
|
+
# source finding's domain when it was HIGH+. Larger or unknown-size fix PRs fall to the size ladder below.
|
|
21
22
|
# small <= 60 changed lines, or one file of <= 150 lines: Security only.
|
|
22
23
|
# medium 61-150 lines: Security plus the score-escalation table (no top-2 baseline).
|
|
23
24
|
# standard > 150 lines, or an unknown size (fail toward more agents): top-2 domains by score plus the table.
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
# exact value EXTENDED is STANDARD. An extended roster prints an extra DEPTH=extended line.
|
|
28
29
|
# Path triggers escalate in EVERY tier except full (which already runs the union): CONTRACT HIGH_RISK, cross-critical
|
|
29
30
|
# pairs, churn hot-spot, danger zone, bin/engine* (Concurrency + spec-cli), bin/ (spec-cli), auth/session/trust-filter
|
|
30
|
-
# files (Auth), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API).
|
|
31
|
+
# files (Auth: auth*, not author*; oauth, jwt, login, password, permission, rbac, acl, token(s), api_key(s), credential, deps.py), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API).
|
|
31
32
|
# Auth and Infra are path-gated: a diff-text score alone never selects them. Security is a hard floor in every tier.
|
|
32
33
|
# spec-cli is added only with --spec-cli true (the persona file exists in the repo under review); in the standard tier it
|
|
33
34
|
# also covers commands/**. Bash 3.2 portable (no mapfile, associative arrays, grep -P).
|
|
@@ -114,7 +115,10 @@ fi
|
|
|
114
115
|
HIGH_RISK=false
|
|
115
116
|
case " $CFLAGS " in *" HIGH_RISK "*) HIGH_RISK=true ;; esac
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
# The fix-pr tier is author-controlled (Closes #N), so it is capped by size: a larger or unknown-size fix PR follows the size ladder.
|
|
119
|
+
FIX_PR_MAX_LINES=150
|
|
120
|
+
FIX_PR_CAPPED=false
|
|
121
|
+
if [ "$FIX_PR" = "true" ] && [ "$LINES_KNOWN" = "true" ] && [ "$LINES" -le "$FIX_PR_MAX_LINES" ]; then
|
|
118
122
|
TIER=fix-pr; REASON="fix PR for a review-finding issue"
|
|
119
123
|
elif [ "$LINES_KNOWN" = "true" ] && { [ "$LINES" -le 60 ] || { [ "$NFILES" -eq 1 ] && [ "$LINES" -le 150 ]; }; }; then
|
|
120
124
|
TIER=small; REASON="lines=$LINES files=$NFILES"
|
|
@@ -124,6 +128,9 @@ else
|
|
|
124
128
|
TIER=standard
|
|
125
129
|
if [ "$LINES_KNOWN" = "true" ]; then REASON="lines=$LINES"; else REASON="size unknown"; fi
|
|
126
130
|
fi
|
|
131
|
+
if [ "$FIX_PR" = "true" ] && [ "$TIER" != "fix-pr" ]; then
|
|
132
|
+
FIX_PR_CAPPED=true; REASON="$REASON; fix-pr tier capped by size (max $FIX_PR_MAX_LINES lines, known size)"
|
|
133
|
+
fi
|
|
127
134
|
# CONTRACT HIGH_RISK is never narrowed: a small, medium or fix-PR roster is lifted to the standard roster.
|
|
128
135
|
BASE_TIER="$TIER"
|
|
129
136
|
if [ "$HIGH_RISK" = "true" ] && [ "$TIER" != "standard" ]; then TIER=standard; REASON="$REASON; CONTRACT HIGH_RISK lifts $BASE_TIER to standard"; fi
|
|
@@ -147,6 +154,12 @@ case "$TIER" in
|
|
|
147
154
|
esac ;;
|
|
148
155
|
standard) for d in $(top_domains "$TOP_N"); do add_agent "$d"; done ;;
|
|
149
156
|
esac
|
|
157
|
+
# A size-capped fix PR keeps the source finding's domain (size only ever ADDS depth).
|
|
158
|
+
if [ "$FIX_PR_CAPPED" = "true" ] && [ -n "$SRC_DOM" ]; then
|
|
159
|
+
case "$(printf '%s' "$SRC_SEV" | tr '[:lower:]' '[:upper:]')" in
|
|
160
|
+
HIGH|CRITICAL|BLOCKER) add_agent "$SRC_DOM"; REASON="$REASON; source finding ${SRC_SEV} adds its domain" ;;
|
|
161
|
+
esac
|
|
162
|
+
fi
|
|
150
163
|
|
|
151
164
|
# Score escalation table (medium and standard only; diff-text signals, so never in the small or fix-pr tiers)
|
|
152
165
|
if [ "$TIER" = "medium" ] || [ "$TIER" = "standard" ]; then
|
|
@@ -170,8 +183,8 @@ if [ "$CHURN" = "true" ] || [ "$DANGER" = "true" ]; then
|
|
|
170
183
|
for d in $(top_domains 1); do add_agent "$d"; trig churn-or-danger-zone; done
|
|
171
184
|
fi
|
|
172
185
|
hit() { printf '%s\n' "$FILES" | grep -qiE "$1"; }
|
|
173
|
-
hit '(^|[/_.-])(auth|
|
|
174
|
-
hit '^\.github/|(^|/)Dockerfile|docker-compose|(^|/)(deploy|infra|traefik|nginx|k8s|helm|terraform)/|\.tf$' && { add_agent Infra; trig infra-path; }
|
|
186
|
+
hit '(^|[/_.-])(auth([^o]|o[^r]|$)|authori[sz]|oauth|session|jwt|login|logout|passw(or)?d|permission|rbac|acl([/_.-]|$)|tokens?([/_.-]|$)|api[-_]?keys?|credential|trust[-_]?filter|trusted[-_a-z]*)|(^|/)deps\.py$' && { add_agent Auth; trig auth-path; }
|
|
187
|
+
hit '^\.github/|(^|/)Dockerfile|docker-compose|(^|/)(docker-)?compose[^/]*\.ya?ml$|(^|/)deploy[^/]*\.sh$|(^|/)docker-entrypoint[^/]*$|(^|/)(deploy|infra|traefik|nginx|k8s|helm|terraform)/|\.tf$' && { add_agent Infra; trig infra-path; }
|
|
175
188
|
hit 'migration|\.sql$' && { add_agent Database; trig migration-path; }
|
|
176
189
|
hit '^sdk/|openapi.*\.json$|openapi-versions/' && { add_agent API; trig sdk-path; }
|
|
177
190
|
hit '^bin/engine' && { add_agent Concurrency; trig bin-engine; }
|
|
@@ -58,9 +58,26 @@ expect_has "consumer repo bin/engine" concurrency; expect_not "consumer repo bin
|
|
|
58
58
|
|
|
59
59
|
# Case 3: a fix PR gives Security only
|
|
60
60
|
run "commands/orchestrate/phase-4-execution.md
|
|
61
|
-
commands/work-on.md" --scores "$SC_NOISY" --changed-lines
|
|
61
|
+
commands/work-on.md" --scores "$SC_NOISY" --changed-lines 120 --changed-files 2 --fix-pr true --source-severity MEDIUM
|
|
62
62
|
expect_sel "fix PR" "security"; expect_tier "fix PR" fix-pr
|
|
63
63
|
|
|
64
|
+
# Case 3b (forge#3848): the fix-pr tier is capped at 150 known lines; larger or unknown-size fix PRs follow the size ladder
|
|
65
|
+
SC_MULTI="AUTH=0 BILLING=5 CONCURRENCY=0 DATABASE=4 INFRA=0 FRONTEND=3 API=0"
|
|
66
|
+
run "svc/x.py" --scores "$SC_MULTI" --changed-lines 900 --changed-files 30 --fix-pr true --source-severity MEDIUM
|
|
67
|
+
expect_tier "fix PR 900 lines" standard; expect_has "fix PR 900 lines" billing; expect_has "fix PR 900 lines" database
|
|
68
|
+
run "svc/x.py" --scores "$SC_QUIET" --changed-lines 150 --changed-files 2 --fix-pr true
|
|
69
|
+
expect_tier "fix-PR-at-cap" fix-pr
|
|
70
|
+
run "svc/x.py" --scores "$SC_QUIET" --changed-lines 151 --changed-files 2 --fix-pr true
|
|
71
|
+
expect_tier "fix PR over cap" standard
|
|
72
|
+
run "svc/x.py" --scores "$SC_MULTI" --fix-pr true
|
|
73
|
+
expect_tier "fix-PR-unknown-size" standard
|
|
74
|
+
run "svc/x.py" --scores "$SC_MULTI" --changed-lines abc --fix-pr true
|
|
75
|
+
expect_tier "fix PR non-numeric size" standard
|
|
76
|
+
run "svc/x.py" --scores "$SC_QUIET" --changed-lines 400 --changed-files 5 --fix-pr true --source-severity HIGH --source-domain Concurrency
|
|
77
|
+
expect_has "capped-fix-PR,-HIGH-source" concurrency; expect_tier "capped-fix-PR,-HIGH-source" standard
|
|
78
|
+
run "svc/x.py" --scores "$SC_QUIET" --changed-lines 400 --changed-files 5 --fix-pr true --contract-flags "HIGH_RISK" --review-depth EXTENDED
|
|
79
|
+
expect_tier "capped-fix-PR-with-HIGH_RISK-and-EXTENDED" standard
|
|
80
|
+
|
|
64
81
|
# Case 4: a fix PR whose source finding was HIGH adds the domain agent
|
|
65
82
|
run "commands/work-on.md" --scores "AUTH=0 BILLING=0 CONCURRENCY=2 DATABASE=0 INFRA=0 FRONTEND=0 API=0" --changed-lines 40 --fix-pr true --source-severity HIGH --source-domain Concurrency
|
|
66
83
|
expect_sel "fix PR, HIGH source finding" "security concurrency"
|
|
@@ -89,12 +106,51 @@ run "svc/app/routers/session.py" --scores "$SC_QUIET" --changed-lines 20 --chang
|
|
|
89
106
|
expect_has "auth path" auth
|
|
90
107
|
run "scripts/trusted-comments.sh" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
91
108
|
expect_has "trust-filter path" auth
|
|
109
|
+
run "svc/authorization.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
110
|
+
expect_has "auth path authorization.py" auth
|
|
111
|
+
run "svc/authMiddleware.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
112
|
+
expect_has "auth-path-authMiddleware.ts" auth
|
|
113
|
+
run "svc/oauth2.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
114
|
+
expect_has "auth path oauth2.ts" auth
|
|
115
|
+
run "svc/permissions.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
116
|
+
expect_has "auth path permissions.py" auth
|
|
117
|
+
run "svc/rbac.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
118
|
+
expect_has "auth path rbac.py" auth
|
|
119
|
+
run "svc/deps.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
120
|
+
expect_has "auth path deps.py" auth
|
|
121
|
+
run "svc/tokens.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
122
|
+
expect_has "auth path tokens.py" auth
|
|
123
|
+
run "svc/api_keys.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
124
|
+
expect_has "auth path api_keys.py" auth
|
|
125
|
+
run "svc/auth_utils.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
126
|
+
expect_has "auth-path-auth_utils.py" auth
|
|
127
|
+
run "AUTHORS.md" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
128
|
+
expect_not "no-auth-path-AUTHORS.md" auth
|
|
129
|
+
run "docs/author.md" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
130
|
+
expect_not "no auth path docs/author.md" auth
|
|
131
|
+
run "svc/tokenizer.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
132
|
+
expect_not "no-auth-path-svc/tokenizer.py" auth
|
|
133
|
+
run "commands/review-pr.md" --scores "AUTH=3 INFRA=0 DATABASE=0 BILLING=0 CONCURRENCY=0" --changed-lines 20 --changed-files 1
|
|
134
|
+
expect_not "auth-from-prose-score-alone" auth
|
|
92
135
|
run "commands/review-pr.md" --scores "AUTH=3 INFRA=3 DATABASE=0 BILLING=0 CONCURRENCY=0" --changed-lines 120
|
|
93
136
|
expect_not "auth from prose score alone" auth; expect_not "infra from prose score alone" infra
|
|
94
137
|
run ".github/workflows/ci.yml" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
95
138
|
expect_has "infra path" infra
|
|
96
139
|
run "docker-compose.prod.yml" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
97
140
|
expect_has "docker path" infra
|
|
141
|
+
# Infra path gate covers deploy scripts, container entrypoints and Compose v2 names (basename-anchored)
|
|
142
|
+
for f in scripts/deploy.sh scripts/deploy-prod.sh docker-entrypoint.sh svc/docker-entrypoint.sh compose.yaml compose.yml; do
|
|
143
|
+
run "$f" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
144
|
+
expect_has "infra path: $f" infra
|
|
145
|
+
done
|
|
146
|
+
for f in scripts/resolve-deploy-gate.sh commands/deploy-pr.md; do
|
|
147
|
+
run "$f" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
148
|
+
expect_not "no-infra-path-$f" infra
|
|
149
|
+
done
|
|
150
|
+
run "$(printf 'README.md\nscripts/deploy.sh')" --scores "$SC_QUIET" --changed-lines 20 --changed-files 2
|
|
151
|
+
expect_has "infra-path-multi-file-list" infra
|
|
152
|
+
run "Scripts/Deploy.SH" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
153
|
+
expect_has "infra-path-case-insensitive" infra
|
|
98
154
|
run "db/migrations/0042_add.sql" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
99
155
|
expect_has "migration path" database
|
|
100
156
|
run "sdk/node/src/index.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|