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.
Files changed (60) hide show
  1. package/bin/engine/outcome-codes.mjs +24 -2
  2. package/bin/engine/phases.mjs +153 -102
  3. package/bin/engine/runlog.mjs +7 -0
  4. package/bin/engine-cli.mjs +5 -0
  5. package/bin/engine.mjs +84 -10
  6. package/bin/forgedock.mjs +1 -1
  7. package/bin/hooks/pre-tool-use.mjs +65 -32
  8. package/bin/runner.mjs +63 -4
  9. package/bin/stats/github-source.mjs +89 -18
  10. package/bin/stats/stats.test.mjs +89 -1
  11. package/commands/cleanup.md +10 -3
  12. package/commands/deploy-pr.md +64 -0
  13. package/commands/fix-ci.md +20 -6
  14. package/commands/milestone.md +34 -17
  15. package/commands/orchestrate/phase-4-execution.md +11 -4
  16. package/commands/review-pr-staging.md +142 -519
  17. package/commands/review-pr.md +490 -3128
  18. package/commands/test-gate.md +25 -10
  19. package/commands/work-on/build/architect.md +13 -1
  20. package/commands/work-on/build/implement.md +2 -1
  21. package/commands/work-on/build/validate.md +2 -0
  22. package/commands/work-on/build.md +107 -86
  23. package/commands/work-on/close.md +50 -15
  24. package/commands/work-on/decompose.md +72 -34
  25. package/commands/work-on/investigate.md +9 -12
  26. package/commands/work-on/remediate.md +27 -13
  27. package/commands/work-on/review.md +5 -3
  28. package/commands/work-on.md +1 -2
  29. package/docs/CONFIG.md +44 -25
  30. package/docs/OUTCOME-CODES.md +7 -2
  31. package/docs/RELEASE-NOTES.md +12 -0
  32. package/docs/STATS.md +2 -1
  33. package/docs/TELEMETRY.md +2 -2
  34. package/docs/WORK-ON-RUNTIME.md +6 -0
  35. package/docs/spec/forge-protocol-v1.md +2 -2
  36. package/package.json +1 -1
  37. package/packages/protocol/src/phases.js +5 -5
  38. package/scripts/cap-findings.sh +1 -1
  39. package/scripts/cap-findings.test.sh +1 -1
  40. package/scripts/check-spec-bash.sh +5 -1
  41. package/scripts/check-spec-bash.test.sh +20 -0
  42. package/scripts/close-parent-c3.test.sh +69 -0
  43. package/scripts/diff-size.sh +93 -4
  44. package/scripts/diff-size.test.sh +59 -2
  45. package/scripts/forge-root.test.sh +13 -11
  46. package/scripts/publish-staging-sync.test.sh +48 -0
  47. package/scripts/resolve-deploy-gate.sh +32 -6
  48. package/scripts/resolve-deploy-gate.test.sh +42 -1
  49. package/scripts/review-finalize.sh +1427 -0
  50. package/scripts/review-finalize.test.sh +833 -0
  51. package/scripts/review-pr-delta-wiring.test.sh +19 -72
  52. package/scripts/review-pr-domain-map.test.sh +18 -13
  53. package/scripts/review-pr-gate2.test.sh +9 -65
  54. package/scripts/review-pr-staging-scope.test.sh +14 -98
  55. package/scripts/review-prepare.sh +48 -12
  56. package/scripts/review-prepare.test.sh +19 -1
  57. package/scripts/review-roster.sh +16 -2
  58. package/scripts/review-roster.test.sh +22 -0
  59. package/scripts/trusted-comments.test.sh +9 -2
  60. 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) || block "gh pr diff --name-only failed"
122
- DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO" 2>/dev/null) || block "gh pr diff failed"
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 [ -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
205
- 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
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" ] && block "no review-pr-agents template source resolved: run \`npx forgedock update\`; NEVER review inline"
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
@@ -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 2); do add_agent "$d"; done ;;
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:([^>]+) -->") | .[0]')" "real-1"
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:([^>]+) -->") | .[0]' | sort -u)" "real-2"
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/../commands/review-pr.md" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: review-pr Phase 8 lacks reviewed-head guard"; }
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