forgedock 1.19.0 → 1.20.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.mjs +48 -3
- package/bin/runner.mjs +20 -4
- 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 +1 -1
- 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 +11 -3
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/TELEMETRY.md +2 -2
- package/docs/WORK-ON-RUNTIME.md +11 -3
- package/package.json +1 -1
- 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
|
@@ -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
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# selfreview-plan.sh — build-time self-review plan: run or skip, and which roster (forge#3971).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Pure decision script: flags in, key=value lines out. No network, no git, no eval; every flag value is data.
|
|
7
|
+
# It calls scripts/review-roster.sh (resolved through forge_resolve, never from the repo under build) on the staged
|
|
8
|
+
# diff's file list so the self-review roster equals the roster /review-pr would pick for the same diff.
|
|
9
|
+
#
|
|
10
|
+
# Usage: selfreview-plan.sh --files-file PATH|- [--changed-lines N] [--changed-files N] [--contract-flags "HIGH_RISK ..."]
|
|
11
|
+
# [--spec-cli true|false] [--review-depth STANDARD|EXTENDED] [--scores "AUTH=n ..."]
|
|
12
|
+
# [--agent-available true|false] [--tree HASH] [--done-trees "HASH HASH ..."]
|
|
13
|
+
# Env: FORGE_SELFREVIEW=off|0|false|disabled operator off switch
|
|
14
|
+
# Output (stdout, always all four lines):
|
|
15
|
+
# SELFREVIEW=run|skip SKIP_REASON=<reason or empty> TIER=<roster tier or empty> SELECTED_AGENTS=<domains>
|
|
16
|
+
# Skip reasons (first match wins): disabled, empty-diff, docs-only, no-agent-tool, already-done, roster-unavailable,
|
|
17
|
+
# small-security-only (small tier, Security-only roster, no path or contract trigger fired).
|
|
18
|
+
# Exit: 0 for every parseable call, including every degrade-to-skip path (self-review never blocks build); 2 on an unknown flag.
|
|
19
|
+
# Bash 3.2 portable (no mapfile, associative arrays, grep -P).
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
FILES_FILE=""; LINES=""; NFILES=""; CFLAGS=""; SPEC_CLI=false; DEPTH=STANDARD; SCORES=""
|
|
23
|
+
AGENT_AVAILABLE=true; TREE=""; DONE_TREES=""
|
|
24
|
+
|
|
25
|
+
while [ $# -gt 0 ]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--files-file) FILES_FILE="${2:-}"; shift 2 ;;
|
|
28
|
+
--changed-lines) LINES="${2:-}"; shift 2 ;;
|
|
29
|
+
--changed-files) NFILES="${2:-}"; shift 2 ;;
|
|
30
|
+
--contract-flags) CFLAGS="${2:-}"; shift 2 ;;
|
|
31
|
+
--spec-cli) SPEC_CLI="${2:-false}"; shift 2 ;;
|
|
32
|
+
--review-depth) DEPTH="${2:-STANDARD}"; shift 2 ;;
|
|
33
|
+
--scores) SCORES="${2:-}"; shift 2 ;;
|
|
34
|
+
--agent-available) AGENT_AVAILABLE="${2:-true}"; shift 2 ;;
|
|
35
|
+
--tree) TREE="${2:-}"; shift 2 ;;
|
|
36
|
+
--done-trees) DONE_TREES="${2:-}"; shift 2 ;;
|
|
37
|
+
*) echo "selfreview-plan: unknown argument: $1" >&2; exit 2 ;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
emit() { # <run|skip> <skip reason> <tier> <agents>
|
|
42
|
+
printf 'SELFREVIEW=%s\nSKIP_REASON=%s\nTIER=%s\nSELECTED_AGENTS=%s\n' "$1" "$2" "$3" "$4"
|
|
43
|
+
exit 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case "$(printf '%s' "${FORGE_SELFREVIEW:-}" | tr '[:upper:]' '[:lower:]')" in
|
|
47
|
+
off|0|false|disabled|no) emit skip disabled "" "" ;;
|
|
48
|
+
esac
|
|
49
|
+
|
|
50
|
+
FILES=""
|
|
51
|
+
if [ "$FILES_FILE" = "-" ]; then FILES=$(cat)
|
|
52
|
+
elif [ -n "$FILES_FILE" ] && [ -f "$FILES_FILE" ]; then FILES=$(cat "$FILES_FILE"); fi
|
|
53
|
+
FILES=$(printf '%s\n' "$FILES" | grep -v '^[[:space:]]*$' || true)
|
|
54
|
+
|
|
55
|
+
[ -n "$FILES" ] || emit skip empty-diff "" ""
|
|
56
|
+
|
|
57
|
+
# Docs-only: every file is documentation. commands/**, .agents/** and skills/** are executable spec, never docs.
|
|
58
|
+
CODE_FILES=$(printf '%s\n' "$FILES" | grep -vE '^(docs/|README|CHANGELOG|LICENSE|NOTICE)|\.(md|txt|rst)$' || true)
|
|
59
|
+
SPEC_FILES=$(printf '%s\n' "$FILES" | grep -E '^(commands|\.agents|skills)/.*\.md$' || true)
|
|
60
|
+
[ -n "$CODE_FILES" ] || [ -n "$SPEC_FILES" ] || emit skip docs-only "" ""
|
|
61
|
+
|
|
62
|
+
[ "$AGENT_AVAILABLE" = "true" ] || emit skip no-agent-tool "" ""
|
|
63
|
+
|
|
64
|
+
if [ -n "$TREE" ]; then
|
|
65
|
+
for _t in $DONE_TREES; do
|
|
66
|
+
[ "$_t" = "$TREE" ] && emit skip already-done "" ""
|
|
67
|
+
done
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; _SELF_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd -P)"; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" "${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/* "$_SELF_ROOT"; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; } # allowlist:portability — canonical resolver block; readlink -f is guarded (2>/dev/null) and optional
|
|
71
|
+
ROSTER_SCRIPT=$(forge_resolve review-roster.sh)
|
|
72
|
+
[ -n "$ROSTER_SCRIPT" ] && [ -f "$ROSTER_SCRIPT" ] || emit skip roster-unavailable "" ""
|
|
73
|
+
|
|
74
|
+
set -- --files-file - --contract-flags "$CFLAGS" --spec-cli "$SPEC_CLI" --review-depth "$DEPTH"
|
|
75
|
+
[ -n "$LINES" ] && set -- "$@" --changed-lines "$LINES"
|
|
76
|
+
[ -n "$NFILES" ] && set -- "$@" --changed-files "$NFILES"
|
|
77
|
+
[ -n "$SCORES" ] && set -- "$@" --scores "$SCORES"
|
|
78
|
+
OUT=$(printf '%s\n' "$FILES" | bash "$ROSTER_SCRIPT" "$@" 2>/dev/null) || emit skip roster-unavailable "" ""
|
|
79
|
+
|
|
80
|
+
SEL=$(printf '%s\n' "$OUT" | sed -n 's/^SELECTED_AGENTS=//p' | head -n 1)
|
|
81
|
+
TIER=$(printf '%s\n' "$OUT" | sed -n 's/^TIER=//p' | head -n 1)
|
|
82
|
+
[ -n "$SEL" ] && [ -n "$TIER" ] || emit skip roster-unavailable "" ""
|
|
83
|
+
|
|
84
|
+
# Cost guard: the small tier runs only when a path or contract trigger widened the roster past Security.
|
|
85
|
+
if [ "$TIER" = "small" ] && [ "$SEL" = "security" ]; then emit skip small-security-only "$TIER" "$SEL"; fi
|
|
86
|
+
emit run "" "$TIER" "$SEL"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Tests for scripts/selfreview-plan.sh (build-time self-review plan, forge#3971). Run: bash scripts/selfreview-plan.test.sh
|
|
6
|
+
# (bash 3.2 compatible)
|
|
7
|
+
set -u
|
|
8
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
9
|
+
ROOT=$(cd "$DIR/.." && pwd)
|
|
10
|
+
SCRIPT="$DIR/selfreview-plan.sh"
|
|
11
|
+
pass=0; fail=0
|
|
12
|
+
ok() { pass=$((pass+1)); }
|
|
13
|
+
bad() { fail=$((fail+1)); echo "FAIL: $1"; }
|
|
14
|
+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/selfreview-plan.XXXXXX")
|
|
15
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
16
|
+
export FORGEDOCK_HOME="$ROOT" # the resolver must not pick up an ambient install
|
|
17
|
+
unset FORGE_SELFREVIEW
|
|
18
|
+
|
|
19
|
+
PLAN=""; RC=0
|
|
20
|
+
# run <files, newline separated> <flags...>
|
|
21
|
+
run() {
|
|
22
|
+
printf '%s\n' "$1" > "$WORK/files"; shift
|
|
23
|
+
PLAN=$(bash "$SCRIPT" --files-file "$WORK/files" "$@" 2>/dev/null); RC=$?
|
|
24
|
+
}
|
|
25
|
+
field() { printf '%s\n' "$PLAN" | sed -n "s/^$1=//p"; }
|
|
26
|
+
expect() { # name key want
|
|
27
|
+
local got; got=$(field "$2")
|
|
28
|
+
if [ "$got" = "$3" ]; then ok; else bad "$1: $2='$got', want '$3'"; fi
|
|
29
|
+
}
|
|
30
|
+
has_agent() { case " $(field SELECTED_AGENTS) " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
|
31
|
+
|
|
32
|
+
# e1: docs-only and empty diffs skip with a recorded reason
|
|
33
|
+
run "docs/GUIDE.md
|
|
34
|
+
README.md" --changed-lines 40
|
|
35
|
+
expect "docs-only skips" SELFREVIEW skip; expect "docs-only reason" SKIP_REASON docs-only
|
|
36
|
+
run "" --changed-lines 0
|
|
37
|
+
expect "empty diff skips" SELFREVIEW skip; expect "empty reason" SKIP_REASON empty-diff
|
|
38
|
+
run "commands/work-on/build.md" --changed-lines 40 --changed-files 1
|
|
39
|
+
expect "spec markdown is not docs" SELFREVIEW skip
|
|
40
|
+
if [ "$(field SKIP_REASON)" = "docs-only" ]; then bad "commands/*.md treated as docs"; else ok; fi
|
|
41
|
+
|
|
42
|
+
# e2: small tier, Security-only roster, no trigger: skipped; the same size with an auth path runs
|
|
43
|
+
run "scripts/foo.sh" --changed-lines 30 --changed-files 1
|
|
44
|
+
expect "small security-only skips" SELFREVIEW skip; expect "small reason" SKIP_REASON small-security-only; expect "small tier" TIER small
|
|
45
|
+
run "src/auth/session.py
|
|
46
|
+
src/util.py" --changed-lines 30 --changed-files 2
|
|
47
|
+
expect "small + auth path runs" SELFREVIEW run; expect "small + auth tier" TIER small
|
|
48
|
+
has_agent auth && ok || bad "auth path did not add auth"
|
|
49
|
+
has_agent security && ok || bad "security floor missing"
|
|
50
|
+
run "scripts/foo.sh
|
|
51
|
+
scripts/bar.sh" --changed-lines 30 --changed-files 2 --contract-flags "HIGH_RISK"
|
|
52
|
+
expect "small + HIGH_RISK runs" SELFREVIEW run; expect "HIGH_RISK lifts tier" TIER standard
|
|
53
|
+
|
|
54
|
+
# standard tier runs
|
|
55
|
+
run "scripts/foo.sh
|
|
56
|
+
scripts/bar.sh" --changed-lines 400 --changed-files 2
|
|
57
|
+
expect "standard runs" SELFREVIEW run; expect "standard tier" TIER standard; expect "standard no reason" SKIP_REASON ""
|
|
58
|
+
has_agent security && ok || bad "standard roster lacks security"
|
|
59
|
+
|
|
60
|
+
# EXTENDED depth lifts a small roster
|
|
61
|
+
run "scripts/foo.sh" --changed-lines 20 --changed-files 1 --review-depth EXTENDED
|
|
62
|
+
expect "extended depth runs" SELFREVIEW run; expect "extended tier" TIER standard
|
|
63
|
+
|
|
64
|
+
# e3: off switch, no Agent tool, unresolvable roster degrade to skip and exit 0
|
|
65
|
+
FORGE_SELFREVIEW=off run "scripts/a.sh
|
|
66
|
+
scripts/b.sh" --changed-lines 400
|
|
67
|
+
[ "$RC" = 0 ] && ok || bad "off exit code $RC"
|
|
68
|
+
expect "off skips" SELFREVIEW skip; expect "off reason" SKIP_REASON disabled
|
|
69
|
+
run "scripts/a.sh
|
|
70
|
+
scripts/b.sh" --changed-lines 400 --agent-available false
|
|
71
|
+
[ "$RC" = 0 ] && ok || bad "no-agent exit code $RC"
|
|
72
|
+
expect "no agent skips" SELFREVIEW skip; expect "no agent reason" SKIP_REASON no-agent-tool
|
|
73
|
+
FORGEDOCK_HOME=/nonexistent-forge-home run "scripts/a.sh
|
|
74
|
+
scripts/b.sh" --changed-lines 400
|
|
75
|
+
[ "$RC" = 0 ] && ok || bad "unresolvable exit code $RC"
|
|
76
|
+
expect "unresolvable roster skips" SELFREVIEW skip; expect "unresolvable reason" SKIP_REASON roster-unavailable
|
|
77
|
+
|
|
78
|
+
# e4: a tree that already has a completed self-review is not dispatched again
|
|
79
|
+
run "scripts/a.sh
|
|
80
|
+
scripts/b.sh" --changed-lines 400 --tree aaa --done-trees "bbb aaa"
|
|
81
|
+
expect "same tree skips" SELFREVIEW skip; expect "same tree reason" SKIP_REASON already-done
|
|
82
|
+
run "scripts/a.sh
|
|
83
|
+
scripts/b.sh" --changed-lines 400 --tree ccc --done-trees "bbb aaa"
|
|
84
|
+
expect "new tree runs" SELFREVIEW run
|
|
85
|
+
|
|
86
|
+
# usage: unknown flag exits 2
|
|
87
|
+
bash "$SCRIPT" --nope >/dev/null 2>&1; [ "$?" = 2 ] && ok || bad "unknown flag should exit 2"
|
|
88
|
+
|
|
89
|
+
echo "selfreview-plan: ${pass} passed, ${fail} failed"
|
|
90
|
+
[ "$fail" -eq 0 ]
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# spawn-depth-check.sh — will a /work-on dispatching phase (review, remediate) have the Agent tool?
|
|
2
|
+
# spawn-depth-check.sh — will a /work-on dispatching phase (build self-review, review, remediate) have the Agent tool?
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
4
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
#
|
|
6
6
|
# Claude Code grants the Agent tool to sub-agents down to CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH layers
|
|
7
7
|
# below the main conversation (default 3 since 2.1.219; 5 for 2.1.172-2.1.216; 1 for 2.1.217-2.1.218).
|
|
8
|
-
# A sub-agent at layer L can spawn only when L < depth. The router invokes review/remediate at
|
|
8
|
+
# A sub-agent at layer L can spawn only when L < depth. The router invokes build/review/remediate at
|
|
9
9
|
# router layer + 1, so they can spawn reviewers only when router layer + 2 <= depth (forge#3398,
|
|
10
|
-
# docs/WORK-ON-RUNTIME.md).
|
|
10
|
+
# docs/WORK-ON-RUNTIME.md). build (B5.6 self-review, forge#3971) uses the same arithmetic but degrades to a recorded
|
|
11
|
+
# skip on FAIL instead of stopping the run.
|
|
11
12
|
#
|
|
12
13
|
# Usage: spawn-depth-check.sh [--router-layer N] (N = 0 solo, 1 under /orchestrate; default 0)
|
|
13
14
|
# Env: CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH explicit limit (wins over the version default)
|
|
@@ -69,5 +70,5 @@ if [ "$DEPTH" -ge "$REQUIRED" ]; then
|
|
|
69
70
|
echo "SPAWN_DEPTH: OK depth=${DEPTH} required=${REQUIRED} (${SOURCE})"
|
|
70
71
|
exit 0
|
|
71
72
|
fi
|
|
72
|
-
echo "SPAWN_DEPTH: FAIL depth=${DEPTH} required=${REQUIRED} (${SOURCE}) — review/remediate would run without the Agent tool and could not spawn /review-pr reviewers. Set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH>=${REQUIRED}."
|
|
73
|
+
echo "SPAWN_DEPTH: FAIL depth=${DEPTH} required=${REQUIRED} (${SOURCE}) — build/review/remediate would run without the Agent tool and could not spawn self-review or /review-pr reviewers. Set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH>=${REQUIRED}."
|
|
73
74
|
exit 1
|
|
@@ -43,5 +43,8 @@ t "env depth 09 decimal" 0 "SPAWN_DEPTH: OK" CLAUDE_CODE_MAX_SUBA
|
|
|
43
43
|
# Usage errors.
|
|
44
44
|
t "bad router layer" 2 "spawn-depth-check:" -- --router-layer x
|
|
45
45
|
t "unknown flag" 2 "usage:" -- --nope
|
|
46
|
+
# forge#3971: build is a dispatching phase too, and the FAIL text names it (exit codes unchanged)
|
|
47
|
+
out=$(env -u CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH -u FORGE_RUNTIME FORGE_CLAUDE_VERSION=2.1.217 bash "$S" 2>&1)
|
|
48
|
+
case "$out" in *build/review/remediate*) PASS=$((PASS+1)) ;; *) FAILN=$((FAILN+1)); echo "FAIL: FAIL text does not name build ($out)" ;; esac
|
|
46
49
|
echo "spawn-depth-check: ${PASS} passed, ${FAILN} failed"
|
|
47
50
|
[ "$FAILN" -eq 0 ]
|
|
@@ -61,6 +61,14 @@ check "finding_disposition carries scope" "$(grep '^{' "$TD/posted" | jq -r '.sc
|
|
|
61
61
|
reset; FORGE_PR=9 forge_emit review_panel mode=FULL; grep -q 'issues/9/comments' "$TD/calls" && ok || bad "review_panel targets PR"
|
|
62
62
|
reset; FORGE_PR=9 forge_emit diff_size lines=1; grep -q 'issues/5/comments' "$TD/calls" && ok || bad "diff_size targets issue"
|
|
63
63
|
|
|
64
|
+
# build_selfreview (forge#3971) is a build-side event: posted to the issue even when a PR number is set; ids and counts only
|
|
65
|
+
reset; FORGE_PR=9 forge_emit build_selfreview tier=standard agents=3 findings=4 fixed=2 declined=1 skipped=
|
|
66
|
+
grep -q 'issues/5/comments' "$TD/calls" && ok || bad "build_selfreview targets issue"
|
|
67
|
+
REC=$(grep '^{' "$TD/posted")
|
|
68
|
+
check "build_selfreview event" "$(printf '%s' "$REC" | jq -r '.event')" "build_selfreview"
|
|
69
|
+
check "build_selfreview counts typed" "$(printf '%s' "$REC" | jq -r '.agents + .findings + .fixed + .declined')" "10"
|
|
70
|
+
check "build_selfreview skip reason" "$(reset; forge_emit build_selfreview skipped=small-security-only tier=small; grep '^{' "$TD/posted" | jq -r '.skipped')" "small-security-only"
|
|
71
|
+
|
|
64
72
|
# 2. Sanitising: reserved keys, bad keys, backticks and newlines never reach the record.
|
|
65
73
|
reset
|
|
66
74
|
forge_emit x repo=evil 'a b=1' ok='a`b
|