forgedock 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/outcome-codes.mjs +24 -2
- package/bin/engine/phases.mjs +153 -102
- package/bin/engine/runlog.mjs +7 -0
- package/bin/engine-cli.mjs +5 -0
- package/bin/engine.mjs +84 -10
- package/bin/forgedock.mjs +1 -1
- package/bin/hooks/pre-tool-use.mjs +65 -32
- package/bin/runner.mjs +63 -4
- package/bin/stats/github-source.mjs +89 -18
- package/bin/stats/stats.test.mjs +89 -1
- package/commands/cleanup.md +10 -3
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-4-execution.md +11 -4
- package/commands/review-pr-staging.md +142 -519
- package/commands/review-pr.md +490 -3128
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +13 -1
- package/commands/work-on/build/implement.md +2 -1
- package/commands/work-on/build/validate.md +2 -0
- package/commands/work-on/build.md +107 -86
- package/commands/work-on/close.md +50 -15
- package/commands/work-on/decompose.md +72 -34
- package/commands/work-on/investigate.md +9 -12
- package/commands/work-on/remediate.md +27 -13
- package/commands/work-on/review.md +5 -3
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +44 -25
- package/docs/OUTCOME-CODES.md +7 -2
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +2 -1
- package/docs/TELEMETRY.md +2 -2
- package/docs/WORK-ON-RUNTIME.md +6 -0
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +5 -5
- package/scripts/cap-findings.sh +1 -1
- package/scripts/cap-findings.test.sh +1 -1
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/close-parent-c3.test.sh +69 -0
- package/scripts/diff-size.sh +93 -4
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/forge-root.test.sh +13 -11
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-finalize.sh +1427 -0
- package/scripts/review-finalize.test.sh +833 -0
- package/scripts/review-pr-delta-wiring.test.sh +19 -72
- package/scripts/review-pr-domain-map.test.sh +18 -13
- package/scripts/review-pr-gate2.test.sh +9 -65
- package/scripts/review-pr-staging-scope.test.sh +14 -98
- package/scripts/review-prepare.sh +48 -12
- package/scripts/review-prepare.test.sh +19 -1
- package/scripts/review-roster.sh +16 -2
- package/scripts/review-roster.test.sh +22 -0
- package/scripts/trusted-comments.test.sh +9 -2
- package/scripts/wait-ci-green.test.sh +3 -3
|
@@ -9,13 +9,19 @@
|
|
|
9
9
|
# the PR checkout, a SPEC_CLI persona file, forge.yaml paths.root) as data.
|
|
10
10
|
#
|
|
11
11
|
# Usage: review-prepare.sh --pr N --repo owner/name [--merge-issue N] [--thorough true|false] [--out FILE] [--out-dir DIR]
|
|
12
|
-
# [--checkout-pr]
|
|
12
|
+
# [--checkout-pr] [--no-delta] [--build-gate block|advisory]
|
|
13
13
|
# --pr PR number (numeric, required) --repo owner/name (required)
|
|
14
14
|
# --merge-issue issue the PR closes, from --issue (numeric; non-numeric is ignored)
|
|
15
15
|
# --thorough true restores the full-union roster --out write the context JSON here (default: stdout)
|
|
16
16
|
# --out-dir directory for per-domain diff slices (default: ${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}/review-prepare-<pr>)
|
|
17
17
|
# --checkout-pr `git checkout -f --detach <review sha>` in the CURRENT repo (must be a throwaway review checkout, never the
|
|
18
18
|
# main checkout); the recorded original ref is restored by restore_ref on every exit path
|
|
19
|
+
# --build-gate block (default): an empty trusted typescript.build / python.format on a full-build lane BLOCKs.
|
|
20
|
+
# advisory: record it as a SKIPPED Phase 2 finding instead (review-pr-staging: the deploy review only ever
|
|
21
|
+
# reported "SKIPPED — typescript.build not configured", and blocking it would stop every deploy of a repo
|
|
22
|
+
# with plain .js files and no build command, ForgeDock itself included).
|
|
23
|
+
# --no-delta skip the FULL/DELTA/BASE_SYNC_ONLY resolver: the review mode is always FULL (a staging bundle review always reviews the
|
|
24
|
+
# whole bundle), without widening the roster to the full union the way --thorough does
|
|
19
25
|
# Env: FORGE_CONFIG (forge.yaml path; default: the main checkout's forge.yaml, then the git top-level one),
|
|
20
26
|
# REVIEW_PREPARE_SLEEP (seconds between mergeability retries, default 5).
|
|
21
27
|
# Exit: 0 context emitted (status OK) | 3 context emitted with status BLOCKED (the caller must stop, never skip) | 2 usage error
|
|
@@ -52,7 +58,7 @@
|
|
|
52
58
|
|
|
53
59
|
set -u
|
|
54
60
|
|
|
55
|
-
PR_NUMBER=""; REPO=""; MERGE_ISSUE=""; THOROUGH="false"; OUT=""; OUT_DIR=""; CHECKOUT_PR="false"
|
|
61
|
+
PR_NUMBER=""; REPO=""; MERGE_ISSUE=""; THOROUGH="false"; OUT=""; OUT_DIR=""; CHECKOUT_PR="false"; NO_DELTA="false"; BUILD_GATE="block"
|
|
56
62
|
while [ $# -gt 0 ]; do
|
|
57
63
|
case "$1" in
|
|
58
64
|
--pr|--repo|--merge-issue|--thorough|--out|--out-dir)
|
|
@@ -66,6 +72,8 @@ while [ $# -gt 0 ]; do
|
|
|
66
72
|
--out) OUT="${2:-}"; shift 2 ;;
|
|
67
73
|
--out-dir) OUT_DIR="${2:-}"; shift 2 ;;
|
|
68
74
|
--checkout-pr) CHECKOUT_PR="true"; shift ;;
|
|
75
|
+
--no-delta) NO_DELTA="true"; shift ;;
|
|
76
|
+
--build-gate) case "${2:-}" in block|advisory) BUILD_GATE="$2" ;; *) echo "review-prepare: --build-gate must be block or advisory" >&2; exit 2 ;; esac; shift 2 ;;
|
|
69
77
|
*) echo "review-prepare: unknown argument: $1" >&2; exit 2 ;;
|
|
70
78
|
esac
|
|
71
79
|
done
|
|
@@ -104,7 +112,7 @@ cleanup() { restore_ref; rm -rf "$TMP_DIR"; }
|
|
|
104
112
|
trap cleanup EXIT
|
|
105
113
|
|
|
106
114
|
# --- Phase 1A: fetch ---
|
|
107
|
-
PR_INFO=$(gh pr view "$PR_NUMBER" -R "$REPO" --json number,title,author,baseRefName,headRefName,headRefOid,additions,deletions,changedFiles 2>/dev/null) || PR_INFO=""
|
|
115
|
+
PR_INFO=$(gh pr view "$PR_NUMBER" -R "$REPO" --json number,title,author,baseRefName,headRefName,headRefOid,isCrossRepository,additions,deletions,changedFiles 2>/dev/null) || PR_INFO=""
|
|
108
116
|
printf '%s' "$PR_INFO" | jq -e . >/dev/null 2>&1 || PR_INFO=""
|
|
109
117
|
if [ -z "$PR_INFO" ]; then block "gh pr view failed for #${PR_NUMBER}"; fi
|
|
110
118
|
pj() { printf '%s' "$PR_INFO" | jq -r "$1 // empty" 2>/dev/null; }
|
|
@@ -118,8 +126,18 @@ LINES=$((ADDITIONS + DELETIONS))
|
|
|
118
126
|
|
|
119
127
|
FILES=""; DIFF=""
|
|
120
128
|
if [ -z "$BLOCKERS" ]; then
|
|
121
|
-
FILES=$(gh pr diff "$PR_NUMBER" -R "$REPO" --name-only 2>/dev/null) ||
|
|
122
|
-
DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO" 2>/dev/null) ||
|
|
129
|
+
FILES=$(gh pr diff "$PR_NUMBER" -R "$REPO" --name-only 2>/dev/null) || FILES="__FAILED__"
|
|
130
|
+
[ "$FILES" = "__FAILED__" ] || DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO" 2>/dev/null) || FILES="__FAILED__"
|
|
131
|
+
if [ "$FILES" = "__FAILED__" ]; then
|
|
132
|
+
# A staging bundle can exceed the API's diff size limit: read it from the long-lived branches instead (--no-delta only; a per-PR review blocks).
|
|
133
|
+
FILES=""; DIFF=""
|
|
134
|
+
# Pinned (forge#3935): same-repo PRs only (a fork's branch name resolves to origin's own branch), and the fetched tip must equal REVIEW_SHA; the diff is taken against REVIEW_SHA, never the branch name.
|
|
135
|
+
if [ "$NO_DELTA" = "true" ] && [ -n "$BASE" ] && [ -n "$HEAD" ] && [ "$(printf '%s' "$PR_INFO" | jq -r .isCrossRepository 2>/dev/null)" = "false" ] && printf '%s' "$REVIEW_SHA" | grep -qE '^[0-9a-f]{40}$' \
|
|
136
|
+
&& git fetch -q origin "$BASE" "$HEAD" 2>/dev/null && [ "$(git rev-parse -q --verify "refs/remotes/origin/${HEAD}^{commit}" 2>/dev/null)" = "$REVIEW_SHA" ]; then
|
|
137
|
+
FILES=$(git diff --name-only "origin/${BASE}...${REVIEW_SHA}" 2>/dev/null) && DIFF=$(git diff "origin/${BASE}...${REVIEW_SHA}" 2>/dev/null) || { FILES=""; DIFF=""; }
|
|
138
|
+
fi
|
|
139
|
+
[ -n "$FILES" ] || block "gh pr diff failed"
|
|
140
|
+
fi
|
|
123
141
|
[ -n "$FILES" ] || [ -n "$BLOCKERS" ] || block "PR #${PR_NUMBER} has an empty file list"
|
|
124
142
|
fi
|
|
125
143
|
|
|
@@ -201,8 +219,13 @@ if [ "$REQUIRES_FULL_BUILD" = "true" ]; then
|
|
|
201
219
|
if [ "$CONFIG_READABLE" != "true" ]; then
|
|
202
220
|
BUILD_CONFIG_STATUS="blocked"; block "full-build lane but no readable trusted forge.yaml (checked FORGE_CONFIG, the main checkout and the git top-level)"
|
|
203
221
|
else
|
|
204
|
-
if [
|
|
205
|
-
|
|
222
|
+
if [ "$BUILD_GATE" = "advisory" ]; then
|
|
223
|
+
if [ -n "$HAS_TS" ] && [ -z "$TS_BUILD" ]; then BUILD_CONFIG_STATUS="skipped"; p2 "1B: SKIPPED — typescript.build not configured in verification.commands (build not run)"; fi
|
|
224
|
+
if [ -n "$HAS_PY" ] && [ -z "$PY_FORMAT" ]; then BUILD_CONFIG_STATUS="skipped"; p2 "1A: SKIPPED — python.format not configured in verification.commands"; fi
|
|
225
|
+
else
|
|
226
|
+
if [ -n "$HAS_TS" ] && [ -z "$TS_BUILD" ]; then BUILD_CONFIG_STATUS="blocked"; block "BLOCKED: typescript.build is empty in verification.commands on a full-build lane (typecheck alone misses prerender failures)"; fi
|
|
227
|
+
if [ -n "$HAS_PY" ] && [ -z "$PY_FORMAT" ]; then BUILD_CONFIG_STATUS="blocked"; block "BLOCKED: python.format is empty in verification.commands on a full-build lane"; fi
|
|
228
|
+
fi
|
|
206
229
|
fi
|
|
207
230
|
fi
|
|
208
231
|
# 2D env vars, 2E secrets (CRITICAL), 2F SQL, over the diff text
|
|
@@ -346,6 +369,13 @@ CONTRACT_RISK_FLAGS="${CONTRACT_RISK_FLAGS# }"
|
|
|
346
369
|
# --- Phase 3B step 4: roster (review-roster.sh; unresolved or failing means the full union) ---
|
|
347
370
|
agent_domain() { local l; l=$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]'); case "$l" in infrastructure) l=infra ;; scraping) l=scraper ;; web) l=frontend ;; esac; printf '%s' "$l"; }
|
|
348
371
|
FIX_PR=false; SRC_SEV=""; SRC_DOM=""
|
|
372
|
+
# Review depth (forge#3862): latest TRUSTED FORGE:DIFF_SIZE on the merge issue; unreadable or absent stays STANDARD (extended only adds reviewers).
|
|
373
|
+
REVIEW_DEPTH=STANDARD; REVIEW_WEIGHT=""
|
|
374
|
+
if [ -n "$MERGE_ISSUE" ]; then
|
|
375
|
+
_ds=$(gh api --paginate "repos/$REPO/issues/$MERGE_ISSUE/comments" --jq '[.[] | select(.body | startswith("<!-- FORGE:DIFF_SIZE")) | select((.author_association | IN("OWNER","MEMBER","COLLABORATOR")) or (.user.type == "Bot"))] | last | .body // ""' 2>/dev/null) || _ds=""
|
|
376
|
+
printf '%s\n' "$_ds" | grep -qE '^review_depth: *EXTENDED[[:space:]]*$' && REVIEW_DEPTH=EXTENDED
|
|
377
|
+
REVIEW_WEIGHT=$(printf '%s\n' "$_ds" | sed -n 's/^review_weight: *\([0-9][0-9]*\).*/\1/p' | head -n 1)
|
|
378
|
+
fi
|
|
349
379
|
if [ -n "$MERGE_ISSUE" ]; then
|
|
350
380
|
gh issue view "$MERGE_ISSUE" -R "$REPO" --json labels --jq '[.labels[].name] | any(. == "review-finding")' 2>/dev/null | grep -qx true && FIX_PR=true
|
|
351
381
|
if [ "$FIX_PR" = "true" ]; then
|
|
@@ -361,7 +391,7 @@ DBC=false; printf '%s\n' "$DIFF" | grep -qE "docker-compose.*postgres|docker-com
|
|
|
361
391
|
ROSTER_OUT=""
|
|
362
392
|
[ -n "$ROSTER_SCRIPT" ] && ROSTER_OUT=$(printf '%s\n' "$FILES" | bash "$ROSTER_SCRIPT" --files-file - --scores "$SC" --changed-lines "$LINES" --changed-files "$NFILES" \
|
|
363
393
|
--fix-pr "$FIX_PR" --source-severity "$SRC_SEV" --source-domain "$SRC_DOM" --contract-flags "$CONTRACT_RISK_FLAGS" --churn "$CHURN_ESCALATION" --danger-zone "$DANGER" \
|
|
364
|
-
--thorough "$THOROUGH" --milestone "$IS_MILESTONE_TO_STAGING" --scraping-configured "$SCRAPING_ON" --database-container "$DBC" --spec-cli "$SPEC_CLI" 2>/dev/null)
|
|
394
|
+
--thorough "$THOROUGH" --milestone "$IS_MILESTONE_TO_STAGING" --scraping-configured "$SCRAPING_ON" --database-container "$DBC" --spec-cli "$SPEC_CLI" --review-depth "$REVIEW_DEPTH" 2>/dev/null)
|
|
365
395
|
SELECTED_AGENTS=$(printf '%s\n' "$ROSTER_OUT" | sed -n 's/^SELECTED_AGENTS=//p')
|
|
366
396
|
ROSTER_TIER=$(printf '%s\n' "$ROSTER_OUT" | sed -n 's/^TIER=//p')
|
|
367
397
|
ROSTER_REASON=$(printf '%s\n' "$ROSTER_OUT" | sed -n 's/^REASON=//p')
|
|
@@ -405,7 +435,7 @@ delta_roster() {
|
|
|
405
435
|
printf '%s' "$out"
|
|
406
436
|
}
|
|
407
437
|
REVIEW_MODE="FULL"; DELTA_FROM=""; CARRIED_FROM=""; FULL_ROUNDS=0; DELTA_FILES=""
|
|
408
|
-
if [ "$THOROUGH" != "true" ] && [ "$IS_MILESTONE_TO_STAGING" != "true" ]; then
|
|
438
|
+
if [ "$THOROUGH" != "true" ] && [ "$IS_MILESTONE_TO_STAGING" != "true" ] && [ "$NO_DELTA" != "true" ]; then
|
|
409
439
|
read -r RD_KIND RD_ARG RD_ROUNDS <<< "$(review_delta_mode)"
|
|
410
440
|
FULL_ROUNDS="${RD_ROUNDS:-0}"
|
|
411
441
|
case "$RD_KIND" in
|
|
@@ -426,7 +456,13 @@ IDX_AUTH=""; IDX_BILLING=""; IDX_DATABASE=""; IDX_API=""; IDX_FRONTEND=""; IDX_I
|
|
|
426
456
|
SLICES_JSON="{}"
|
|
427
457
|
if [ "$REVIEW_MODE" != "BASE_SYNC_ONLY" ]; then
|
|
428
458
|
review_template_resolve "$REPO_PATH"
|
|
429
|
-
[ "$TEMPLATE_SOURCE" = "none" ]
|
|
459
|
+
if [ "$TEMPLATE_SOURCE" = "none" ]; then
|
|
460
|
+
block "no review-pr-agents template source resolved: run \`npx forgedock update\`; NEVER review inline"
|
|
461
|
+
# forge#3936: keep the setup-error trace and the needs-human alert the old spec bash posted before returning BLOCKED.
|
|
462
|
+
_msg="Review of PR #${PR_NUMBER} could not start: no review-pr-agents template source resolved (broken install). Run \`npx forgedock update\`, then re-run the review. Reviews are never done inline."
|
|
463
|
+
gh pr comment "$PR_NUMBER" -R "$REPO" --body "$_msg" >/dev/null 2>&1 || true # allowlist:check-command-side-effects
|
|
464
|
+
[ -z "$MERGE_ISSUE" ] || gh issue edit "$MERGE_ISSUE" -R "$REPO" --add-label needs-human >/dev/null 2>&1 || true # allowlist:check-command-side-effects
|
|
465
|
+
fi
|
|
430
466
|
if [ -n "$CODE_INDEX_SCRIPT" ]; then bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" >/dev/null 2>&1 || true; fi
|
|
431
467
|
build_index_slice() {
|
|
432
468
|
local domain="${1}" files
|
|
@@ -487,7 +523,7 @@ CTX=$(jq -n \
|
|
|
487
523
|
--argjson sa "$SCORE_AUTH" --argjson sb "$SCORE_BILLING" --argjson sc "$SCORE_CONCURRENCY" --argjson sd "$SCORE_DATABASE" --argjson si "$SCORE_INFRA" \
|
|
488
524
|
--argjson ss "$SCORE_SECURITY" --argjson ssc "$SCORE_SCRAPING" --argjson sf "$SCORE_FRONTEND" --argjson sap "$SCORE_API" \
|
|
489
525
|
--arg cissue "$ISSUE_NUM" --arg cflags "$CONTRACT_RISK_FLAGS" \
|
|
490
|
-
--arg rtier "$ROSTER_TIER" --arg rreason "$ROSTER_REASON" --argjson agents "$(j_words "$SELECTED_AGENTS")" \
|
|
526
|
+
--arg rdepth "$REVIEW_DEPTH" --arg rweight "$REVIEW_WEIGHT" --arg rtier "$ROSTER_TIER" --arg rreason "$ROSTER_REASON" --argjson agents "$(j_words "$SELECTED_AGENTS")" \
|
|
491
527
|
--arg dmode "$REVIEW_MODE" --arg dfrom "$DELTA_FROM" --arg dcarried "$CARRIED_FROM" --argjson drounds "$FULL_ROUNDS" --argjson dfiles "$(j_lines "$DELTA_FILES")" \
|
|
492
528
|
--arg pname "$PROJECT_NAME" --arg pctx "$PROJECT_CONTEXT" --arg scraping_on "$SCRAPING_ON" --arg rpath "$REPO_PATH" \
|
|
493
529
|
--arg d_auth "$DOMAIN_CONTEXT_AUTH" --arg d_billing "$DOMAIN_CONTEXT_BILLING" --arg d_conc "$DOMAIN_CONTEXT_CONCURRENCY" --arg d_infra "$DOMAIN_CONTEXT_INFRA" \
|
|
@@ -505,7 +541,7 @@ CTX=$(jq -n \
|
|
|
505
541
|
broken_assumptions: $asm, risk_signals: $risk, churn_context: $churn, churn_escalation: ($churn_esc == "true"), danger_zone: ($danger == "true"),
|
|
506
542
|
scores: {AUTH: $sa, BILLING: $sb, CONCURRENCY: $sc, DATABASE: $sd, INFRA: $si, SECURITY: $ss, SCRAPING: $ssc, FRONTEND: $sf, API: $sap},
|
|
507
543
|
contract: {issue: ($cissue | tonumber? // null), flags: $cflags},
|
|
508
|
-
roster: {tier: $rtier, reason: $rreason, agents: $agents},
|
|
544
|
+
roster: {tier: $rtier, review_depth: $rdepth, review_weight: $rweight, reason: $rreason, agents: $agents},
|
|
509
545
|
delta: {mode: $dmode, from: $dfrom, carried_from: $dcarried, full_rounds: $drounds, files: $dfiles},
|
|
510
546
|
config: {project_name: $pname, project_context: $pctx, scraping_configured: ($scraping_on == "true"), repo_path: $rpath,
|
|
511
547
|
domains: {auth: $d_auth, billing: $d_billing, concurrency: $d_conc, infra: $d_infra, database: $d_db, frontend: $d_fe, security: $d_sec, api: $d_api, scraping: $d_scr}},
|
|
@@ -68,7 +68,7 @@ printf 'project:\n name: Demo\nverification:\n commands:\n typescript:\n
|
|
|
68
68
|
# setup <head> <base> <files> <diff> [sha]
|
|
69
69
|
setup() {
|
|
70
70
|
rm -f "${FIX:?}"/*; printf 'MERGEABLE|CLEAN\n' > "$FIX/merge.seq"; : > "$FIX/body.txt"
|
|
71
|
-
printf '{"number":7,"title":"t","author":{"login":"dev"},"baseRefName":"%s","headRefName":"%s","headRefOid":"%s","additions":5,"deletions":2,"changedFiles":1}\n' "$2" "$1" "${5:-$SHA_B}" > "$FIX/prinfo.json"
|
|
71
|
+
printf '{"number":7,"title":"t","author":{"login":"dev"},"baseRefName":"%s","headRefName":"%s","headRefOid":"%s","isCrossRepository":%s,"additions":5,"deletions":2,"changedFiles":1}\n' "$2" "$1" "${5:-$SHA_B}" "${CROSS_REPO:-false}" > "$FIX/prinfo.json"
|
|
72
72
|
printf '%s\n' "$3" > "$FIX/files"; printf '%s\n' "$4" > "$FIX/diff"
|
|
73
73
|
}
|
|
74
74
|
CTX=""; RC=0
|
|
@@ -94,6 +94,8 @@ prep "$ROOT"
|
|
|
94
94
|
rc_is "happy path" 0
|
|
95
95
|
eq "schema" .schema_version 1; eq "status" .status OK; eq "pr number" .pr.number 7; eq "review sha" .pr.review_sha "$SHA_B"
|
|
96
96
|
eq "sha7" .pr.review_sha_short "$(printf '%s' "$SHA_B" | cut -c1-7)"
|
|
97
|
+
eq "review depth defaults to STANDARD" .roster.review_depth STANDARD
|
|
98
|
+
eq "review weight key present in context" '.roster | has("review_weight")' true
|
|
97
99
|
eq "small tier" .roster.tier small; eq "small roster" '.roster.agents | join(" ")' security
|
|
98
100
|
eq "lines" '.pr.additions + .pr.deletions' 7
|
|
99
101
|
eq "first-time false" .pr.first_time_contributor false
|
|
@@ -189,6 +191,11 @@ rc_is "empty ts build on a milestone lane" 3; eq "BLOCKED status" .status BLOCKE
|
|
|
189
191
|
eq "blocker names typescript.build" '.blockers | join(" ") | test("typescript.build")' true
|
|
190
192
|
eq "config status blocked" .phase2.build_config.status blocked; eq "lane flag" .lane.requires_full_build true
|
|
191
193
|
prep "$ROOT"; rc_is "configured ts build passes" 0; eq "status OK" .status OK
|
|
194
|
+
# --build-gate advisory (review-pr-staging): an unconfigured build is a SKIPPED Phase 2 finding, never a block (#3952 QA-1)
|
|
195
|
+
CFG_OVERRIDE="$WORK/nobuild.yaml" prep "$ROOT" --build-gate advisory
|
|
196
|
+
rc_is "advisory build gate does not block" 0; eq "advisory status OK" .status OK
|
|
197
|
+
eq "advisory config status skipped" .phase2.build_config.status skipped
|
|
198
|
+
eq "advisory records SKIPPED finding" '.phase2.findings | join(" ") | test("SKIPPED — typescript.build")' true
|
|
192
199
|
PY_DIFF='diff --git a/app/x.py b/app/x.py
|
|
193
200
|
+x'
|
|
194
201
|
printf 'project:\n name: Demo\n' > "$WORK/empty.yaml"
|
|
@@ -253,6 +260,17 @@ if grep -q 'checkout -f "\$ORIG_REF"' "$ROOT/scripts/review-prepare.sh"; then ok
|
|
|
253
260
|
# --- 10. fail closed on gh and bad arguments ---
|
|
254
261
|
setup feat/x staging "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
255
262
|
prep "$ROOT"; rc_is "gh pr diff failure" 3; eq "blocked on gh failure" .status BLOCKED
|
|
263
|
+
# a staging bundle review (--no-delta) reads an API-oversized diff from the long-lived branches; a per-PR review still blocks
|
|
264
|
+
setup pr-branch main "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
265
|
+
prep "$ROOT" --no-delta; rc_is "--no-delta falls back to git when the API diff fails" 0; eq "fallback status" .status OK
|
|
266
|
+
eq "fallback file list comes from git" '.full_files | index("services/api/auth.py") != null' true; eq "--no-delta mode is FULL" .delta.mode FULL
|
|
267
|
+
# forge#3935: the fallback is pinned to the review SHA and refuses fork PRs
|
|
268
|
+
setup pr-branch main "README.md" "$DIFF_PLAIN" "0000000000000000000000000000000000000000"; : > "$FIX/difffail"
|
|
269
|
+
prep "$ROOT" --no-delta; rc_is "--no-delta fallback refuses a branch tip that is not the review SHA" 3
|
|
270
|
+
CROSS_REPO=true setup pr-branch main "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
271
|
+
prep "$ROOT" --no-delta; rc_is "--no-delta fallback refuses a cross-repository PR" 3
|
|
272
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
273
|
+
prep "$ROOT" --no-delta; rc_is "--no-delta with unfetchable branches still fails closed" 3; eq "unfetchable blocked" .status BLOCKED
|
|
256
274
|
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
257
275
|
for bad_args in "--pr abc --repo o/r" "--pr 7 --repo noslash" "--pr 7 --repo o/r;x/y" "--pr 7 --repo o/r --bogus" "--repo o/r --pr"; do
|
|
258
276
|
# shellcheck disable=SC2086
|
package/scripts/review-roster.sh
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# [--source-severity SEV] [--source-domain DOMAIN] [--contract-flags "HIGH_RISK CONTRACT_AUTH ..."]
|
|
12
12
|
# [--churn true|false] [--danger-zone true|false] [--thorough true|false] [--milestone true|false]
|
|
13
13
|
# [--scraping-configured true|false] [--database-container true|false] [--spec-cli true|false]
|
|
14
|
+
# [--review-depth STANDARD|EXTENDED]
|
|
14
15
|
# Output (stdout): SELECTED_AGENTS=<canonical marker domains, space separated> TIER=<tier> REASON=<text>
|
|
15
16
|
# Exit: 0 always for a parseable call; 2 on an unknown flag (callers fall back to the full union, never a narrower roster).
|
|
16
17
|
#
|
|
@@ -20,6 +21,10 @@
|
|
|
20
21
|
# small <= 60 changed lines, or one file of <= 150 lines: Security only.
|
|
21
22
|
# medium 61-150 lines: Security plus the score-escalation table (no top-2 baseline).
|
|
22
23
|
# standard > 150 lines, or an unknown size (fail toward more agents): top-2 domains by score plus the table.
|
|
24
|
+
# --review-depth EXTENDED (forge#3862: the build's FORGE:DIFF_SIZE review_weight or out-of-plan scope flagged extended
|
|
25
|
+
# review) only ever ADDS reviewers: a small, medium or fix-pr roster is lifted to standard and the standard baseline takes
|
|
26
|
+
# the top-3 domains instead of top-2. It never narrows a roster and never touches the Security floor. Anything but the
|
|
27
|
+
# exact value EXTENDED is STANDARD. An extended roster prints an extra DEPTH=extended line.
|
|
23
28
|
# Path triggers escalate in EVERY tier except full (which already runs the union): CONTRACT HIGH_RISK, cross-critical
|
|
24
29
|
# pairs, churn hot-spot, danger zone, bin/engine* (Concurrency + spec-cli), bin/ (spec-cli), auth/session/trust-filter
|
|
25
30
|
# files (Auth), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API).
|
|
@@ -30,7 +35,7 @@ set -u
|
|
|
30
35
|
|
|
31
36
|
S_AUTH=0; S_BILLING=0; S_CONCURRENCY=0; S_DATABASE=0; S_INFRA=0; S_SCRAPING=0; S_FRONTEND=0; S_API=0
|
|
32
37
|
LINES=""; NFILES=""; FILES_FILE=""; FIX_PR=false; SRC_SEV=""; SRC_DOM=""; CFLAGS=""
|
|
33
|
-
CHURN=false; DANGER=false; THOROUGH=false; MILESTONE=false; SCRAPING_ON=false; DB_CONTAINER=false; SPEC_CLI=false
|
|
38
|
+
DEPTH=STANDARD; CHURN=false; DANGER=false; THOROUGH=false; MILESTONE=false; SCRAPING_ON=false; DB_CONTAINER=false; SPEC_CLI=false
|
|
34
39
|
|
|
35
40
|
num() { case "${1:-}" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$1" ;; esac; }
|
|
36
41
|
agent_domain() { local l; l=$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]'); case "$l" in infrastructure) l=infra ;; scraping) l=scraper ;; web) l=frontend ;; esac; printf '%s' "$l"; }
|
|
@@ -67,6 +72,7 @@ while [ $# -gt 0 ]; do
|
|
|
67
72
|
--scraping-configured) SCRAPING_ON="${2:-false}"; shift 2 ;;
|
|
68
73
|
--database-container) DB_CONTAINER="${2:-false}"; shift 2 ;;
|
|
69
74
|
--spec-cli) SPEC_CLI="${2:-false}"; shift 2 ;;
|
|
75
|
+
--review-depth) [ "${2:-}" = "EXTENDED" ] && DEPTH=EXTENDED; shift 2 ;;
|
|
70
76
|
*) echo "review-roster: unknown argument: $1" >&2; exit 2 ;;
|
|
71
77
|
esac
|
|
72
78
|
done
|
|
@@ -121,6 +127,12 @@ fi
|
|
|
121
127
|
# CONTRACT HIGH_RISK is never narrowed: a small, medium or fix-PR roster is lifted to the standard roster.
|
|
122
128
|
BASE_TIER="$TIER"
|
|
123
129
|
if [ "$HIGH_RISK" = "true" ] && [ "$TIER" != "standard" ]; then TIER=standard; REASON="$REASON; CONTRACT HIGH_RISK lifts $BASE_TIER to standard"; fi
|
|
130
|
+
# Extended review depth (forge#3862) is only ever additive: lift to standard, widen the standard baseline to top-3.
|
|
131
|
+
TOP_N=2
|
|
132
|
+
if [ "$DEPTH" = "EXTENDED" ]; then
|
|
133
|
+
TOP_N=3
|
|
134
|
+
if [ "$TIER" != "standard" ]; then REASON="$REASON; extended review depth lifts $TIER to standard"; TIER=standard; else REASON="$REASON; extended review depth"; fi
|
|
135
|
+
fi
|
|
124
136
|
|
|
125
137
|
TRIG=""
|
|
126
138
|
trig() { case " $TRIG " in *" $1 "*) ;; *) TRIG="$TRIG $1" ;; esac; }
|
|
@@ -133,7 +145,7 @@ case "$TIER" in
|
|
|
133
145
|
if [ -n "$SRC_DOM" ]; then add_agent "$SRC_DOM"; else for d in $(top_domains 1); do add_agent "$d"; done; fi
|
|
134
146
|
REASON="$REASON; source finding ${SRC_SEV} adds its domain" ;;
|
|
135
147
|
esac ;;
|
|
136
|
-
standard) for d in $(top_domains
|
|
148
|
+
standard) for d in $(top_domains "$TOP_N"); do add_agent "$d"; done ;;
|
|
137
149
|
esac
|
|
138
150
|
|
|
139
151
|
# Score escalation table (medium and standard only; diff-text signals, so never in the small or fix-pr tiers)
|
|
@@ -170,3 +182,5 @@ fi
|
|
|
170
182
|
|
|
171
183
|
[ -n "$TRIG" ] && REASON="$REASON; triggers:$TRIG"
|
|
172
184
|
printf 'SELECTED_AGENTS=%s\nTIER=%s\nREASON=%s\n' "${SELECTED_AGENTS# }" "$TIER" "$REASON"
|
|
185
|
+
[ "$DEPTH" = "EXTENDED" ] && printf 'DEPTH=extended\n'
|
|
186
|
+
exit 0
|
|
@@ -159,5 +159,27 @@ hold 3520 4977 9 false "concurrency" "concurrency" "bin/engine-cli.mjs bin/engin
|
|
|
159
159
|
hold 3524 156 5 false "spec-cli" "spec-cli" "bin/engine.mjs bin/engine/phases.mjs bin/runner.mjs bin/tests/engine-phases.test.mjs bin/tests/engine.test.mjs"
|
|
160
160
|
hold 3555 137 8 false "security" "" "bin/engine/phases.mjs bin/tests/engine-phases.test.mjs commands/work-on/build.md commands/work-on/build/implement.md commands/work-on/decompose.md commands/work-on/investigate.md packages/protocol/CHANGELOG.md packages/protocol/src/phases.js"
|
|
161
161
|
|
|
162
|
+
# forge#3862: extended review depth (from the build's review_weight / scope flag) only ever adds reviewers
|
|
163
|
+
SC_TOP="AUTH=0 BILLING=1 CONCURRENCY=0 DATABASE=1 INFRA=0 SECURITY=1 SCRAPING=0 FRONTEND=2 API=2"
|
|
164
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 40 --changed-files 1
|
|
165
|
+
expect_tier "no depth flag stays small" small; expect_sel "no depth flag stays security-only" "security"
|
|
166
|
+
case "$out" in *DEPTH=*) bad "standard roster prints no DEPTH line" ;; *) ok ;; esac
|
|
167
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 40 --changed-files 1 --review-depth STANDARD
|
|
168
|
+
expect_tier "STANDARD depth stays small" small
|
|
169
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 40 --changed-files 1 --review-depth extended
|
|
170
|
+
expect_tier "lowercase depth is not EXTENDED" small
|
|
171
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 40 --changed-files 1 --review-depth EXTENDED
|
|
172
|
+
expect_tier "EXTENDED lifts small to standard" standard; expect_has "EXTENDED keeps security" security
|
|
173
|
+
expect_has "EXTENDED small adds top domains" frontend; expect_has "EXTENDED small adds top domains" api
|
|
174
|
+
case "$out" in *DEPTH=extended*) ok ;; *) bad "extended roster prints DEPTH=extended" ;; esac
|
|
175
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 400 --changed-files 6
|
|
176
|
+
expect_not "standard top-2 omits the third domain" database
|
|
177
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 400 --changed-files 6 --review-depth EXTENDED
|
|
178
|
+
expect_has "EXTENDED standard takes a third domain" database; expect_has "EXTENDED standard keeps security" security
|
|
179
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 30 --fix-pr true --review-depth EXTENDED
|
|
180
|
+
expect_tier "EXTENDED lifts fix-pr to standard" standard
|
|
181
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 400 --thorough true --review-depth EXTENDED
|
|
182
|
+
expect_tier "full stays full" full
|
|
183
|
+
|
|
162
184
|
echo "review-roster: $pass passed, $fail failed"
|
|
163
185
|
[ "$fail" -eq 0 ]
|
|
@@ -55,7 +55,7 @@ FAKE_AG="<!-- FORGE:REVIEW-AGENT:security -->${NL}Reviewed-SHA: ${SHA}${NL}<!--
|
|
|
55
55
|
FAKE_SYN="<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->${NL}Reviewed-SHA: ${SHA}${NL}"
|
|
56
56
|
AGJ="[$(c NONE Bot b "$AG_BODY"),$(c NONE User h "$FAKE_AG")]"
|
|
57
57
|
eq "agent regex counts only the trusted body" "$(printf '%s' "$AGJ" | bash "$TC" count "$AGENT_RE")" 1
|
|
58
|
-
eq "forged FINDING id never extracted" "$(printf '%s' "$AGJ" | bash "$TC" bodies "$AGENT_RE" | jq -r 'scan("<!-- FINDING:(
|
|
58
|
+
eq "forged FINDING id never extracted" "$(printf '%s' "$AGJ" | bash "$TC" bodies "$AGENT_RE" | jq -r 'scan("<!-- FINDING:(.+?) -->") | .[0]')" "real-1"
|
|
59
59
|
eq "lookahead agent count (REVIEW-FINDINGS-START inside body)" "$(printf '%s' "$AGJ" | bash "$TC" count "$LOOK_RE")" 1
|
|
60
60
|
eq "lookahead rejects body without REVIEW-FINDINGS-START" "$(printf '%s' "[$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:api -->${NL}Reviewed-SHA: ${SHA}")]" | bash "$TC" count "$LOOK_RE")" 0
|
|
61
61
|
eq "agent regex rejects a different head SHA" "$(printf '%s' "[$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:api -->${NL}Reviewed-SHA: ${OTHER}")]" | bash "$TC" count "$AGENT_RE")" 0
|
|
@@ -74,7 +74,14 @@ STG_FORGED="<!-- FORGE:REVIEW-AGENT:bug-hunter-api -->${NL}Reviewed-SHA: ${SHA}$
|
|
|
74
74
|
STGJ="[$(c NONE Bot b "$STG_TRUE"),$(c NONE Bot b "$STG_TRUE"),$(c NONE User h "$STG_FORGED"),$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:security -->${NL}Reviewed-SHA: ${SHA}"),$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:code-quality -->${NL}Reviewed-SHA: ${OTHER}")]"
|
|
75
75
|
eq "staging per-domain count ignores forged and duplicate-free of untrusted" "$(printf '%s' "$STGJ" | bash "$TC" count "$STG_DOM_RE")" 2
|
|
76
76
|
eq "staging unique trusted current-head domains (hyphen, wrong head excluded)" "$(printf '%s' "$STGJ" | bash "$TC" bodies "$STG_RE" | jq -r 'scan("^<!-- FORGE:REVIEW-AGENT:([a-z0-9-]+) -->") | .[0]' | sort -u | grep -c .)" 2
|
|
77
|
-
eq "staging forged FINDING never extracted" "$(printf '%s' "$STGJ" | bash "$TC" bodies "$STG_RE" | jq -r 'scan("<!-- FINDING:(
|
|
77
|
+
eq "staging forged FINDING never extracted" "$(printf '%s' "$STGJ" | bash "$TC" bodies "$STG_RE" | jq -r 'scan("<!-- FINDING:(.+?) -->") | .[0]' | sort -u)" "real-2"
|
|
78
|
+
# Marker titles containing '>' must be extracted in full (the old [^>]+ capture dropped them); two markers on one line still split
|
|
79
|
+
GT_BODY="<!-- FORGE:REVIEW-AGENT:security -->${NL}Reviewed-SHA: ${SHA}${NL}<!-- FINDING:SEC-9|CONFIRMED|HIGH|a.sh:3|eval of x -> y reaches shell -->${NL}<!-- FINDING:SEC-10|LIKELY|LOW|b.sh:1|a > b --> <!-- FINDING:SEC-11|LIKELY|LOW|c.sh:2|plain -->"
|
|
80
|
+
GTJ="[$(c NONE Bot b "$GT_BODY")]"
|
|
81
|
+
GT_OUT=$(printf '%s' "$GTJ" | bash "$TC" bodies "$AGENT_RE" | jq -r 'scan("<!-- FINDING:(.+?) -->") | .[0]')
|
|
82
|
+
eq "marker with -> in title extracted in full" "$(printf '%s\n' "$GT_OUT" | grep -c 'eval of x -> y reaches shell')" 1
|
|
83
|
+
eq "markers with > and two on one line all extracted" "$(printf '%s\n' "$GT_OUT" | grep -c .)" 3
|
|
84
|
+
eq "raw marker count equals extracted count" "$(printf '%s' "$GTJ" | bash "$TC" bodies "$AGENT_RE" | grep -o '<!-- FINDING:' | wc -l | tr -d ' ')" 3
|
|
78
85
|
eq "staging regex rejects forged-only comment from NONE user" "$(printf '[%s]' "$(c NONE User h "$STG_FORGED")" | bash "$TC" count "$STG_RE")" 0
|
|
79
86
|
|
|
80
87
|
# fail closed
|
|
@@ -68,7 +68,7 @@ expect "every result names the gated head" "CI_GATE_HEAD: abc1234" "$(printf '%s
|
|
|
68
68
|
|
|
69
69
|
# Static guard: every autonomous `gh pr merge` command line in the pipeline specs must be preceded
|
|
70
70
|
# (within 25 lines) by a CI gate result check. Prose/manual instructions are not command lines.
|
|
71
|
-
for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
71
|
+
for spec in commands/review-pr.md scripts/review-finalize.sh commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
72
72
|
bad=$(awk '
|
|
73
73
|
{ line[NR]=$0 }
|
|
74
74
|
/^[[:space:]]*(MERGE_RESULT=\$\()?gh pr merge / {
|
|
@@ -78,7 +78,7 @@ for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/re
|
|
|
78
78
|
expect "every autonomous merge in $spec is CI-gated" "" "$bad"
|
|
79
79
|
done
|
|
80
80
|
# Every autonomous merge pins the gated head (forge#3187): no merge line may omit --match-head-commit.
|
|
81
|
-
for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
81
|
+
for spec in commands/review-pr.md scripts/review-finalize.sh commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
82
82
|
unpinned=$(grep -nE '^[[:space:]]*(MERGE_RESULT=\$\(|MERGE_OUTPUT=\$\()?gh pr merge ' "$HERE/../$spec" | grep -v -- '--match-head-commit')
|
|
83
83
|
expect "every autonomous merge in $spec pins the gated head" "" "$unpinned"
|
|
84
84
|
done
|
|
@@ -90,7 +90,7 @@ if grep -qE '^[[:space:]]*(MERGE_RESULT=\$\()?gh pr merge ' "$HERE/../commands/w
|
|
|
90
90
|
|
|
91
91
|
# Reviewed-head guard: review-pr Phase 8 must refuse to merge a head other than the reviewed commit,
|
|
92
92
|
# and the work-on callers must route "stale review" to a re-review, never a manual merge.
|
|
93
|
-
grep -qF '${GATED_HEAD#"$REVIEWED_SHORT"}' "$HERE/../
|
|
93
|
+
grep -qF '${GATED_HEAD#"$REVIEWED_SHORT"}' "$HERE/../scripts/review-finalize.sh" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: review-pr Phase 8 (scripts/review-finalize.sh merge-gate) lacks reviewed-head guard"; }
|
|
94
94
|
for f in commands/work-on/review.md; do
|
|
95
95
|
grep -q 'stale review' "$HERE/../$f" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: $f does not route stale review"; }
|
|
96
96
|
done
|