forgedock 1.0.22 → 1.1.2

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 (184) hide show
  1. package/README.md +255 -211
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/cinema.mjs +320 -0
  4. package/bin/demo.mjs +92 -7
  5. package/bin/engine/phases.mjs +168 -0
  6. package/bin/engine/projector.mjs +32 -0
  7. package/bin/engine/reconcile.mjs +11 -0
  8. package/bin/engine/runlog.mjs +79 -0
  9. package/bin/engine/state.mjs +32 -0
  10. package/bin/engine-cli.mjs +222 -0
  11. package/bin/engine.mjs +138 -0
  12. package/bin/forgedock.mjs +896 -1540
  13. package/bin/hooks/interactive-engine.mjs +509 -0
  14. package/bin/hooks/pre-tool-use.mjs +498 -0
  15. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  16. package/bin/journey.mjs +841 -0
  17. package/bin/registry.mjs +139 -0
  18. package/bin/report.mjs +447 -0
  19. package/bin/runner.mjs +136 -20
  20. package/bin/settings-hook.mjs +346 -0
  21. package/bin/tui.mjs +6 -3
  22. package/commands/adopt.md +4 -1
  23. package/commands/analytics.md +4 -1
  24. package/commands/audit-agents.md +110 -27
  25. package/commands/audit.md +20 -1
  26. package/commands/autopilot.md +84 -12
  27. package/commands/changelog.md +2 -1
  28. package/commands/ci-audit.md +2 -1
  29. package/commands/cleanup.md +161 -1
  30. package/commands/compat-audit.md +2 -1
  31. package/commands/deploy-info.md +63 -4
  32. package/commands/design-bench.md +6 -1
  33. package/commands/design-render-critique-loop.md +2 -1
  34. package/commands/design.md +1 -0
  35. package/commands/diagnose.md +2 -1
  36. package/commands/explain.md +2 -1
  37. package/commands/forge-stats.md +122 -2
  38. package/commands/forgedock-init.md +1 -1
  39. package/commands/geo-audit.md +3 -0
  40. package/commands/incident-response.md +2 -1
  41. package/commands/issue.md +24 -3
  42. package/commands/milestone.md +77 -12
  43. package/commands/optimize.md +2 -1
  44. package/commands/orchestrate/config.md +67 -0
  45. package/commands/orchestrate/phase-1-resolve.md +159 -0
  46. package/commands/orchestrate/phase-2-triage.md +126 -0
  47. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  48. package/commands/orchestrate/phase-3-dependency.md +591 -0
  49. package/commands/orchestrate/phase-4-execution.md +962 -0
  50. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  51. package/commands/orchestrate/phase-6-report.md +198 -0
  52. package/commands/orchestrate/safety.md +62 -0
  53. package/commands/orchestrate.md +25 -2027
  54. package/commands/pipeline-health/config.md +34 -0
  55. package/commands/pipeline-health/phase-1-context.md +180 -0
  56. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  57. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  58. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  59. package/commands/pipeline-health/phase-5-report.md +284 -0
  60. package/commands/pipeline-health/phase-6-summary.md +52 -0
  61. package/commands/pipeline-health.md +25 -2401
  62. package/commands/pipeline-resume.md +24 -1
  63. package/commands/pipeline-status.md +65 -0
  64. package/commands/qa-sweep.md +4 -1
  65. package/commands/quality-gate.md +326 -42
  66. package/commands/replay.md +2 -1
  67. package/commands/review-pr-agents/api.md +135 -0
  68. package/commands/review-pr-agents/auth.md +137 -0
  69. package/commands/review-pr-agents/billing.md +101 -0
  70. package/commands/review-pr-agents/concurrency.md +145 -0
  71. package/commands/review-pr-agents/database.md +103 -0
  72. package/commands/review-pr-agents/frontend.md +89 -0
  73. package/commands/review-pr-agents/infra.md +385 -0
  74. package/commands/review-pr-agents/protocols.md +185 -0
  75. package/commands/review-pr-agents/scraper.md +185 -0
  76. package/commands/review-pr-agents/security.md +406 -0
  77. package/commands/review-pr-agents.md +45 -1722
  78. package/commands/review-pr-staging.md +196 -40
  79. package/commands/review-pr.md +612 -56
  80. package/commands/rollback.md +2 -1
  81. package/commands/scope.md +2 -1
  82. package/commands/security-audit.md +2 -1
  83. package/commands/signal-planner.md +623 -0
  84. package/commands/sync-ecosystem.md +1 -0
  85. package/commands/test-gate.md +17 -1
  86. package/commands/validate.md +2 -1
  87. package/commands/work-on/build/architect.md +28 -0
  88. package/commands/work-on/build/context.md +2 -0
  89. package/commands/work-on/build/implement.md +19 -5
  90. package/commands/work-on/build/validate.md +62 -6
  91. package/commands/work-on/build.md +187 -30
  92. package/commands/work-on/close.md +132 -5
  93. package/commands/work-on/decompose.md +19 -1
  94. package/commands/work-on/investigate.md +117 -2
  95. package/commands/work-on/review.md +48 -4
  96. package/commands/work-on-monolithic.md +8 -2
  97. package/commands/work-on.md +247 -7
  98. package/docs/BRAND.md +16 -5
  99. package/docs/CI.md +2 -2
  100. package/docs/CODEX.md +36 -1
  101. package/docs/CONFIG.md +44 -42
  102. package/docs/FORGE-PROTOCOL.md +15 -1064
  103. package/docs/anthropic-devrel-outreach.md +2 -2
  104. package/docs/articles/command-decomposition-token-savings.md +214 -0
  105. package/docs/articles/model-release-playbook.md +131 -0
  106. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  107. package/docs/claude-breakpoints.json +75 -5
  108. package/docs/design/abc-benchmark.md +1 -1
  109. package/docs/design/design-spec-schema.md +1 -1
  110. package/docs/design/divergent-generation.md +1 -1
  111. package/docs/design/eval-gate-runbook.md +178 -0
  112. package/docs/design/show-hn-draft.md +26 -1
  113. package/docs/design/token-cost-baseline-1298.md +59 -0
  114. package/docs/eval/README.md +117 -0
  115. package/docs/eval/corpus/seed-v1.json +6 -0
  116. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  117. package/docs/site/annotations-explained.md +24 -0
  118. package/docs/site/command-learning-path.md +51 -14
  119. package/docs/site/command-reference.md +339 -113
  120. package/docs/site/for-companies.md +95 -0
  121. package/docs/site/forge-annotation-protocol.md +12 -256
  122. package/docs/site/getting-started.md +58 -12
  123. package/docs/site/github-is-the-memory.md +113 -0
  124. package/docs/site/how-it-works.md +3 -13
  125. package/docs/site/index.md +10 -2
  126. package/docs/spec/eval-run-result.md +147 -0
  127. package/docs/spec/forge-protocol-v1.md +441 -18
  128. package/docs/spec/label-state-machine.md +55 -0
  129. package/docs/spec/review-protocol.md +158 -0
  130. package/docs/spec-graph-schema.md +28 -0
  131. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  132. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  133. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  134. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  135. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  136. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  137. package/examples/forgedock-demo/README.md +41 -22
  138. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  139. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  140. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  141. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  142. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  143. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  144. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  145. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  146. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  147. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  148. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  149. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  150. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  151. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  152. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  153. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  154. package/examples/forgedock-demo/src/db.js +36 -11
  155. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  156. package/examples/forgedock-demo/src/server.js +4 -1
  157. package/package.json +12 -4
  158. package/packages/protocol/LICENSE +21 -0
  159. package/packages/protocol/src/cli.js +135 -0
  160. package/packages/protocol/src/emit.js +146 -0
  161. package/packages/protocol/src/index.js +43 -0
  162. package/packages/protocol/src/parse.js +229 -0
  163. package/packages/protocol/src/types.js +171 -0
  164. package/packages/protocol/src/validate.js +122 -0
  165. package/scripts/bench-topology-cost.mjs +372 -0
  166. package/scripts/check-command-docs-drift.sh +198 -0
  167. package/scripts/check-protocol-restatements.sh +176 -0
  168. package/scripts/check-registry/README.md +72 -0
  169. package/scripts/check-registry/manifest.json +6 -0
  170. package/scripts/code-index.sh +454 -0
  171. package/scripts/conformance-check.mjs +168 -0
  172. package/scripts/doctor-pipeline-state.sh +484 -0
  173. package/scripts/eval-gate-baseline.json +17 -0
  174. package/scripts/eval-gate-scorecard.mjs +242 -0
  175. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  176. package/scripts/flaky-quarantine.sh +241 -0
  177. package/scripts/forge-annotation.sh +567 -0
  178. package/scripts/issue-dedup.sh +236 -0
  179. package/scripts/validate-annotation-node.mjs +233 -0
  180. package/scripts/verify-env-vars.sh +18 -2
  181. package/scripts/verify-host-headers.sh +4 -4
  182. package/scripts/verify-route-registration.sh +2 -2
  183. package/scripts/worktree-lifecycle.sh +202 -0
  184. package/templates/devdocs/agent/using-forgedock.md +14 -0
@@ -11,8 +11,32 @@ allowed-tools: Task, Bash, Read, Grep, Glob, WebFetch, Skill
11
11
  **Input**: $ARGUMENTS
12
12
 
13
13
  **NEVER use plan mode (EnterPlanMode)** during review — it breaks execution context.
14
+ **NEVER use the Agent tool** — review-pr dispatches domain agents via `Task` tool only. `Agent` spawns opaque subprocesses that bypass the allowed-tools constraint and cannot post structured findings to the PR. Always use `Task(...)` for review agent launch (Phase 3C).
14
15
 
15
- **Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`. User can override with `--model <name>`. Pass the resolved model in every `Task` tool call.
16
+ **Agent model policy**: `model: "sonnet"` (standard tier); the General Security & Quality reviewer spawned as always-runs Task uses `effort: xhigh` (deep tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Pass model and effort explicitly in every `Task` tool call. Feature gate: pass `effort` only on Claude Code >= 2.1.154.
17
+
18
+ <!-- FORGE:SPEC_LOADED — review-pr.md loaded and active. Agent is bound by this spec. -->
19
+
20
+ ## HARD RULES — READ BEFORE ANYTHING ELSE
21
+
22
+ 1. **Use `Task(...)` for ALL domain agent launches.** Never substitute `Agent(...)`. Task agents run in a constrained context, post findings to the PR via `gh pr comment`, and their output is structured. Agent spawns opaque subprocesses outside allowed-tools.
23
+
24
+ 2. **Post the FORGE:REVIEW verdict regardless of finding severity.** A review that completes but posts no `<!-- FORGE:REVIEW -->` comment is invisible to the pipeline. Even a PASS verdict must be posted.
25
+
26
+ 3. **Review findings do NOT block merge UNLESS they meet the Blocking Criteria in §7B** (a CONFIRMED HIGH/CRITICAL finding, a purpose regression, a merge conflict, or a build/type/test failure). File every finding as a GitHub issue with the `review-finding` label regardless of severity. Minor/style findings never block; §7B's blocking conditions always do — including under `--auto-merge`.
27
+
28
+ 4. **Route correctly at Phase 0.** If the input is "staging" or the PR targets `main`, invoke `Skill("review-pr-staging", ...)` — do NOT run the standard PR review pipeline against a staging→main PR.
29
+
30
+ ## Forbidden Tools Self-Check
31
+
32
+ **Before executing any phase**, verify you are NOT using any of these tools:
33
+
34
+ | Tool | Status | Reason |
35
+ |------|--------|--------|
36
+ | `Agent` | **FORBIDDEN** | Spawns opaque subprocesses outside allowed-tools; bypasses spec workflow; cannot post structured findings |
37
+ | `EnterPlanMode` | **FORBIDDEN** | Breaks execution context; must run phases, not plan them |
38
+
39
+ If you find yourself about to call `Agent(...)`, stop and use `Task(...)` instead. If you find yourself about to use `EnterPlanMode`, stop and execute the next phase directly.
16
40
 
17
41
  ## Architecture — How This Command Works
18
42
 
@@ -22,7 +46,8 @@ This is the **orchestrator**. It routes to the right review mode, runs automated
22
46
 
23
47
  | File | What | How to invoke |
24
48
  |------|------|---------------|
25
- | `$FORGE_HOME/commands/review-pr-agents.md` | Agent prompt templates (9 agents + protocols) | `Read` tool during Phase 3C |
49
+ | `$FORGE_HOME/commands/review-pr-agents/protocols.md` | Shared review protocols (Evidence-Based + Structured Findings + Input Scoping) | `Read` tool during Phase 3C (always) |
50
+ | `$FORGE_HOME/commands/review-pr-agents/<persona>.md` | Per-persona agent prompt templates (9 files) | `Read` tool during Phase 3C (selected agents only) |
26
51
  | `$FORGE_HOME/commands/review-pr-staging.md` | Full staging→main review pipeline | `Skill("review-pr-staging", ...)` during Phase 0 |
27
52
 
28
53
  `$FORGE_HOME` defaults to `~/.claude` (the directory where `npx forgedock` symlinks commands). Override by setting `FORGE_HOME` in your environment.
@@ -49,6 +74,55 @@ Extract: `PR_NUMBER`, `AUTO_MERGE=true`, `MERGE_ISSUE`, `MERGE_BASE`, `MERGE_GH_
49
74
 
50
75
  If `--auto-merge` is NOT present, `AUTO_MERGE=false` — Phase 8 (Auto-Merge) will be skipped.
51
76
 
77
+ ### Thoroughness Flag
78
+
79
+ If `$ARGUMENTS` contains `--thorough`, set `THOROUGH=true`. This restores full union-dispatch (all matched domain agents run) for release-critical PRs. Default is `THOROUGH=false` — risk-scaled dispatch (2-3 agents).
80
+
81
+ ```bash
82
+ THOROUGH=false
83
+ echo "$ARGUMENTS" | grep -q "\-\-thorough" && THOROUGH=true
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Phase -1: Route Assertion
89
+
90
+ **This phase is MANDATORY and must execute BEFORE Phase 0. No phase may be skipped.**
91
+
92
+ Resolve the PR number and post a routing marker immediately. This creates an audit trail — if a PR has no `FORGE:REVIEW_ROUTE` comment after a review command was run, the review was bypassed or never started.
93
+
94
+ ```bash
95
+ # Determine REVIEW_MODE from $ARGUMENTS before any routing decision
96
+ REVIEW_MODE_RAW="$ARGUMENTS"
97
+ if echo "$ARGUMENTS" | grep -qE '^(staging|feature|staging:feature)$'; then
98
+ REVIEW_MODE="staging-keyword"
99
+ ROUTE_PR_NUMBER="(resolved by staging sub-command)"
100
+ elif echo "$ARGUMENTS" | grep -qE '^(open|all)$'; then
101
+ REVIEW_MODE="multi-pr"
102
+ ROUTE_PR_NUMBER="(list mode)"
103
+ else
104
+ # Single PR number or URL — resolve HEAD/BASE now
105
+ PR_ROUTE_INFO=$(gh pr view $ARGUMENTS --json number,baseRefName,headRefName --jq '{number:.number,base:.baseRefName,head:.headRefName}')
106
+ ROUTE_PR_NUMBER=$(echo "$PR_ROUTE_INFO" | jq -r '.number')
107
+ ROUTE_HEAD=$(echo "$PR_ROUTE_INFO" | jq -r '.head')
108
+ ROUTE_BASE=$(echo "$PR_ROUTE_INFO" | jq -r '.base')
109
+ if [ "$ROUTE_HEAD" = "staging" ] && [ "$ROUTE_BASE" = "main" ] || [ "$ROUTE_HEAD" = "feature" ] && [ "$ROUTE_BASE" = "main" ]; then
110
+ REVIEW_MODE="staging-auto"
111
+ else
112
+ REVIEW_MODE="single-pr"
113
+ fi
114
+ fi
115
+
116
+ REVIEW_SHA_ROUTE=$(gh pr view ${ROUTE_PR_NUMBER:-$ARGUMENTS} --json headRefOid --jq '.headRefOid' 2>/dev/null | cut -c1-7 || echo "n/a")
117
+
118
+ # Post the routing assertion marker to the PR (skip for list/keyword modes where no PR# is known yet)
119
+ if [ "$REVIEW_MODE" != "staging-keyword" ] && [ "$REVIEW_MODE" != "multi-pr" ]; then
120
+ gh pr comment "$ROUTE_PR_NUMBER" --body "<!-- FORGE:REVIEW_ROUTE mode=${REVIEW_MODE} spec=review-pr.md sha=${REVIEW_SHA_ROUTE} -->"
121
+ fi
122
+ ```
123
+
124
+ **Invariant**: After this phase, `REVIEW_MODE` and (where applicable) `ROUTE_PR_NUMBER` are set. Any sub-invocation of `Skill("review-pr-staging", ...)` should immediately post its own `FORGE:REVIEW_ROUTE` marker scoped to the PR it resolves.
125
+
52
126
  ---
53
127
 
54
128
  ## Phase 0: Route to Review Mode
@@ -137,17 +211,19 @@ FILES=$(gh pr diff $ARGUMENTS --name-only)
137
211
  DIFF=$(gh pr diff $ARGUMENTS)
138
212
 
139
213
  echo "=== SERVICES ==="
140
- echo "$FILES" | grep -c "^services/api/" && echo "API" || true
141
- echo "$FILES" | grep -c "^services/worker/" && echo "WORKER" || true
142
- echo "$FILES" | grep -c "^web/" && echo "WEB" || true
143
- echo "$FILES" | grep -c "^shared/" && echo "SHARED" || true
144
- echo "$FILES" | grep -cE "^(docker|infra/|\.github|Makefile|traefik)" && echo "INFRA" || true
214
+ # Service detection is path-agnostic — matches project-specific and generic structures
215
+ echo "$FILES" | grep -cE "(^services/api/|/api/|/backend/)" && echo "API" || true
216
+ echo "$FILES" | grep -cE "(^services/worker/|/worker/|/jobs/|/tasks/)" && echo "WORKER" || true
217
+ echo "$FILES" | grep -cE "(^web/|/frontend/|/app/|\.tsx?$|\.jsx?$)" && echo "WEB" || true
218
+ echo "$FILES" | grep -cE "(^shared/|/shared/|/common/|/lib/)" && echo "SHARED" || true
219
+ echo "$FILES" | grep -cE "^(docker|infra/|\.github|Makefile|traefik|k8s/|terraform/)" && echo "INFRA" || true
145
220
 
146
221
  echo "=== DOMAINS ==="
147
- echo "$DIFF" | grep -cE "SessionUser|CurrentUser|get_current_user|jwt|oauth|login|logout|Depends\(get_|x.forwarded.for|x_forwarded_for|forwarded_for|rate.limit.*ip|ip.*rate.limit|algorithm.*HS256|algorithm.*RS256|NEXTAUTH_SECRET|JWT_SECRET|admin.proxy" && echo "AUTH" || true
148
- echo "$DIFF" | grep -cE "credit|balance|debit|reconcil|tier_cost|pricing|charge|refund|stripe|subscription" && echo "BILLING" || true
149
- echo "$DIFF" | grep -cE "scrape|tier.*escalat|proxy|anti_bot|stealth|playwright|playbook" && echo "SCRAPING" || true
150
- echo "$DIFF" | grep -cE "FOR UPDATE|atomic|transaction|pipeline|MULTI|distributed_lock|acquire_lock|reserved_by|promo.*claim|voucher.*redeem|is_byop|byop_discount" && echo "CONCURRENCY" || true
222
+ echo "$DIFF" | grep -cE "get_current_user|jwt|oauth|login|logout|require_auth|authenticated_user|x.forwarded.for|x_forwarded_for|forwarded_for|rate.limit.*ip|ip.*rate.limit|algorithm.*HS256|algorithm.*RS256|NEXTAUTH_SECRET|JWT_SECRET" && echo "AUTH" || true
223
+ echo "$DIFF" | grep -cE "credit|balance|debit|reconcil|pricing|charge|refund|stripe|subscription|payment" && echo "BILLING" || true
224
+ # SCRAPING: only match browser-automation/scraping keywords, NOT bare "playwright" (avoids E2E test repos)
225
+ echo "$DIFF" | grep -cE "scrape|tier.*escalat|anti_bot|stealth|playwright.*scrape|scrape.*playwright|playbook_min_tier|browser.*pool|proxy.*scrape|web.*scrape" && echo "SCRAPING" || true
226
+ echo "$DIFF" | grep -cE "FOR UPDATE|atomic|transaction|pipeline|MULTI|distributed_lock|acquire_lock|reserved_by|promo.*claim|voucher.*redeem" && echo "CONCURRENCY" || true
151
227
  echo "$FILES" | grep -cE "migration|\.sql$" && echo "DATABASE" || true
152
228
  echo "$DIFF" | grep -cE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" && echo "DB_CONFIG" || true
153
229
  echo "$FILES" | grep -cE "router|routes" && echo "API_DESIGN" || true
@@ -263,7 +339,21 @@ git diff origin/main...HEAD -- "**/package.json" | grep -E "^\+" | grep -v "^\+\
263
339
 
264
340
  ### 2H: Tests
265
341
 
266
- Read `forge.yaml → verification.commands.{lang}.test` for the project's test command:
342
+ Read `forge.yaml → verification.commands.{lang}.test` for the project's test command.
343
+
344
+ **Before running tests**: check the quarantine manifest for known pre-broken and flaky tests:
345
+
346
+ ```bash
347
+ # Load quarantine manifest if present — used below to suppress known-bad tests from blocking.
348
+ QUARANTINE_MANIFEST=".forgedock/quarantine.jsonl"
349
+ QUARANTINED_TESTS=""
350
+ if [ -f "$QUARANTINE_MANIFEST" ]; then
351
+ QUARANTINED_TESTS=$(grep -oP '"test":"[^"]*"' "$QUARANTINE_MANIFEST" 2>/dev/null | sed 's/"test":"//;s/"//' | sort -u)
352
+ QUARANTINE_COUNT=$(echo "$QUARANTINED_TESTS" | grep -c . || echo 0)
353
+ echo "Quarantine manifest: ${QUARANTINE_COUNT} known-bad test(s) — these will not block the review if they fail."
354
+ echo "$QUARANTINED_TESTS" | sed 's/^/ - /'
355
+ fi
356
+ ```
267
357
 
268
358
  ```bash
269
359
  # Check each configured language for a test command
@@ -273,7 +363,29 @@ for lang in python typescript go rust; do
273
363
  echo "=== Running tests (${lang}): ${TEST_CMD} ==="
274
364
  eval "$TEST_CMD" 2>&1 | tail -30
275
365
  TEST_EXIT=$?
276
- [ "$TEST_EXIT" -ne 0 ] && echo "BLOCKING: ${lang} tests failed (exit $TEST_EXIT)"
366
+ if [ "$TEST_EXIT" -ne 0 ]; then
367
+ # Attempt to cross-reference with quarantine manifest.
368
+ # If ALL failing test names appear in $QUARANTINED_TESTS, suppress the block.
369
+ # Without per-test granularity from the test runner, fall back to treating
370
+ # any failure as blocking unless the classifier script is available.
371
+ CLASSIFIER="${FORGEDOCK_SCRIPTS:-scripts}/flaky-quarantine.sh"
372
+ [ -f "$CLASSIFIER" ] || CLASSIFIER="scripts/flaky-quarantine.sh"
373
+ if [ -f "$CLASSIFIER" ]; then
374
+ CL_RESULT=$(bash "$CLASSIFIER" \
375
+ --test "$TEST_CMD" \
376
+ --base "$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null | sed 's|origin/||' || echo main)" \
377
+ --retries 3 2>&1)
378
+ echo "$CL_RESULT"
379
+ CL=$(echo "$CL_RESULT" | grep '^CLASSIFICATION:' | awk '{print $2}')
380
+ if [ "$CL" = "PRE_BROKEN" ] || [ "$CL" = "FLAKY" ]; then
381
+ echo "ADVISORY (not blocking): ${lang} tests classified ${CL} — quarantined, does not block this review"
382
+ else
383
+ echo "BLOCKING: ${lang} tests failed (exit $TEST_EXIT) — classified REAL"
384
+ fi
385
+ else
386
+ echo "BLOCKING: ${lang} tests failed (exit $TEST_EXIT)"
387
+ fi
388
+ fi
277
389
  fi
278
390
  done
279
391
 
@@ -284,7 +396,7 @@ if [ -z "$PYTHON_TEST" ] && [ -z "$TS_TEST" ]; then
284
396
  echo "SKIPPED — no test commands configured in verification.commands"
285
397
  fi
286
398
  ```
287
- **BLOCKING if tests fail.**
399
+ **BLOCKING only for REAL test failures** (deterministically caused by this PR). Pre-broken and flaky failures are surfaced as advisories and do not block approval.
288
400
 
289
401
  ### 2I: Build Verification (MANDATORY for staging→main AND milestone→staging)
290
402
 
@@ -397,7 +509,7 @@ For each changed file, identify its **activation path** — how does execution r
397
509
 
398
510
  Map each changed file to its activation requirements.
399
511
 
400
- **Layout path resolution** — Before applying the table below, read your project's layout from `forge.yaml → review.layout` and substitute the values into the pattern column. Defaults (used when the key is absent) match the AlterLab monorepo layout:
512
+ **Layout path resolution** — Before applying the table below, read your project's layout from `forge.yaml → review.layout` and substitute the values into the pattern column. Defaults (used when the key is absent) are the ForgeDock install defaults:
401
513
 
402
514
  | `forge.yaml` key | Default | Used in table as |
403
515
  |-----------------|---------|-----------------|
@@ -461,6 +573,23 @@ if [ "$BASH_AVAILABLE" = "true" ]; then
461
573
  # Output is structured: "BLOCKING: ...", "WARNING: ...", "OK: ..." per line.
462
574
 
463
575
  # 1. Route/router/middleware/shared-module/component registration
576
+ # Export forge.yaml layout overrides so verify-route-registration.sh uses project-configured
577
+ # paths instead of project defaults. The script supports these env vars (lines 36-44 of
578
+ # verify-route-registration.sh) but requires the caller to set them. <!-- Added: forge#1349 -->
579
+ if [ -f "$REPO_ROOT/forge.yaml" ]; then
580
+ _PAGES_ROOT=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
581
+ | grep -E '^\s*pages:' | head -1 | sed 's/.*pages:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
582
+ _API_ROUTERS=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
583
+ | grep -E '^\s*api_routers_dir:' | head -1 | sed 's/.*api_routers_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
584
+ _API_MAIN=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
585
+ | grep -E '^\s*api_main:' | head -1 | sed 's/.*api_main:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
586
+ _API_MIDDLEWARE=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
587
+ | grep -E '^\s*api_middleware_dir:' | head -1 | sed 's/.*api_middleware_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
588
+ [ -n "$_PAGES_ROOT" ] && export FORGE_PAGES_ROOT="$_PAGES_ROOT"
589
+ [ -n "$_API_ROUTERS" ] && export FORGE_API_ROUTERS_DIR="$_API_ROUTERS"
590
+ [ -n "$_API_MAIN" ] && export FORGE_API_MAIN="$_API_MAIN"
591
+ [ -n "$_API_MIDDLEWARE" ] && export FORGE_API_MIDDLEWARE_DIR="$_API_MIDDLEWARE"
592
+ fi
464
593
  echo "=== Running: verify-route-registration.sh ==="
465
594
  bash "$FORGE_HOME/scripts/verify-route-registration.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
466
595
 
@@ -612,10 +741,10 @@ Format: `INTEG-N|CONFIRMED|HIGH|file:line|Changed code may be unreachable: {reas
612
741
  DIFF=$(gh pr diff $ARGUMENTS)
613
742
  FILES=$(gh pr diff $ARGUMENTS --name-only)
614
743
  echo "=== RISK SIGNALS ==="
615
- echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc|EMEMO_EOF|LLM_|llm_" && echo " UNTRUSTED_INPUT_PROCESSING" || true
744
+ echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc" && echo " UNTRUSTED_INPUT_PROCESSING" || true
616
745
  echo "$DIFF" | grep -cE "\.sh$|bash|shell|cron" && echo " SHELL_SCRIPT" || true
617
746
  echo "$DIFF" | grep -cE "credit|balance|debit|charge|refund|stripe" && echo " FINANCIAL" || true
618
- echo "$DIFF" | grep -cE "SessionUser|CurrentUser|jwt|oauth|password|token|secret|x.forwarded.for|x_forwarded_for|forwarded_for|rate.limit.*ip|ip.*rate.limit|algorithm.*HS256|algorithm.*RS256|NEXTAUTH_SECRET|JWT_SECRET|admin.proxy" && echo " AUTH_SENSITIVE" || true
747
+ echo "$DIFF" | grep -cE "jwt|oauth|password|token|secret|x.forwarded.for|x_forwarded_for|forwarded_for|rate.limit.*ip|ip.*rate.limit|algorithm.*HS256|algorithm.*RS256|NEXTAUTH_SECRET|JWT_SECRET|get_current_user|require_auth" && echo " AUTH_SENSITIVE" || true
619
748
  echo "$DIFF" | grep -cE "\.sql$|migration|DROP|ALTER|DELETE FROM" && echo " DATABASE_MUTATION" || true
620
749
  echo "$DIFF" | grep -cE "docker|deploy|traefik|nginx|\.yml.*service" && echo " INFRASTRUCTURE" || true
621
750
  echo "$DIFF" | grep -cE "docker-compose.*postgres|docker-compose.*redis|postgres.*command:|redis.*command:|image:.*postgres|image:.*redis" && echo " DATABASE_CONTAINER" || true
@@ -654,43 +783,232 @@ echo "$CHURN_CONTEXT"
654
783
 
655
784
  Tiers (fixed thresholds, identical to `architect.md` Phase A5): **HOT** = 15+ commits / 90 days, **MEDIUM** = 5–14, **LOW** = 0–4. Only HOT-tier files are listed in `CHURN_CONTEXT` — this keeps the agent prompt signal short and high-value rather than dumping a churn count for every file. This does not change agent selection (3B) — it is a scrutiny prior surfaced to whichever agents are already selected, substituted as `[CHURN_CONTEXT]` in Phase 3C.
656
785
 
657
- ### 3B: Select Agents
786
+ ### 3B: Select Agents — Risk-Scaled Dispatch
658
787
 
659
- | Risk Signal | Required Agents |
660
- |-------------|----------------|
661
- | UNTRUSTED_INPUT_PROCESSING | Security (deep) + relevant domain |
662
- | SHELL_SCRIPT | Security (deep) + Infrastructure |
663
- | FINANCIAL | Security + Billing + Concurrency |
664
- | AUTH_SENSITIVE | Security + Auth Conventions |
665
- | DATABASE_MUTATION | Security + Database |
666
- | DB_CONFIG | Security + Database + Infrastructure |
667
- | INFRASTRUCTURE | Security + Infrastructure |
668
- | DATABASE_CONTAINER | Security + Infrastructure (escalate to HIGH risk — stateful container restart) |
669
- | CODE_EXECUTION | Security (deep) |
670
- | SDK_OPENAPI | Security + API Design & Consistency (runs cross-PR schema check #10) |
671
- | None | Security + all matching domain agents |
788
+ **Default: 2-3 agents.** General Security always runs. Select the top 1-2 domain agents by relevance score, then apply escalation triggers to add more only when signals warrant it. Use `--thorough` to restore full union dispatch for release-critical PRs.
672
789
 
673
- **General Security agent ALWAYS runs.** Domain agents selected by classification, not PR size. If BILLING detected, always also spawn Concurrency agent. If SHARED touched, spawn agents for all importing services.
790
+ #### Step 1: Score Each Domain by Signal Strength
674
791
 
675
- **Domain-overlap dispatch (MANDATORY for multi-signal PRs):**
792
+ Assign a relevance score to each domain based on signals detected in Phase 3A. Higher score = higher priority for inclusion.
676
793
 
677
- 1. **Union semantics**: If multiple risk signals are detected, spawn the UNION of all agents from ALL matched rows. A PR triggering both `AUTH_SENSITIVE` and `FINANCIAL` spawns Security + Auth + Billing + Concurrency — not just one row's agents.
794
+ ```bash
795
+ # Relevance scoring — accumulate points per domain from Phase 3A signals
796
+ SCORE_AUTH=0
797
+ SCORE_BILLING=0
798
+ SCORE_CONCURRENCY=0
799
+ SCORE_DATABASE=0
800
+ SCORE_INFRA=0
801
+ SCORE_SECURITY=0
802
+ SCORE_SCRAPING=0
803
+ SCORE_FRONTEND=0
804
+ SCORE_API=0
805
+
806
+ # AUTH domain signals
807
+ echo "$DIFF" | grep -qE "jwt|oauth|login|logout|password|NEXTAUTH_SECRET|JWT_SECRET|auth_token|access_token|refresh_token" && SCORE_AUTH=$((SCORE_AUTH + 3))
808
+ echo "$DIFF" | grep -qE "get_current_user|Depends\(get_|x.forwarded.for|forwarded_for|rate.limit.*ip|ip.*rate.limit" && SCORE_AUTH=$((SCORE_AUTH + 2))
809
+ echo "$DIFF" | grep -qE "algorithm.*HS256|algorithm.*RS256|admin.proxy" && SCORE_AUTH=$((SCORE_AUTH + 2))
810
+
811
+ # BILLING domain signals
812
+ echo "$DIFF" | grep -qE "credit|balance|debit|charge|refund|stripe|subscription" && SCORE_BILLING=$((SCORE_BILLING + 3))
813
+ echo "$DIFF" | grep -qE "pricing|tier_cost|reconcil" && SCORE_BILLING=$((SCORE_BILLING + 2))
814
+
815
+ # CONCURRENCY domain signals
816
+ echo "$DIFF" | grep -qE "FOR UPDATE|atomic|transaction|pipeline|MULTI|distributed_lock|acquire_lock" && SCORE_CONCURRENCY=$((SCORE_CONCURRENCY + 3))
817
+ echo "$DIFF" | grep -qE "reserved_by|promo.*claim|voucher.*redeem" && SCORE_CONCURRENCY=$((SCORE_CONCURRENCY + 2))
818
+
819
+ # DATABASE domain signals
820
+ echo "$FILES" | grep -qE "migration|\.sql$" && SCORE_DATABASE=$((SCORE_DATABASE + 3))
821
+ echo "$DIFF" | grep -qE "DROP|ALTER|DELETE FROM" && SCORE_DATABASE=$((SCORE_DATABASE + 2))
822
+ echo "$DIFF" | grep -qE "create_async_engine|AsyncSession|pool_size|prepared_statement|sessionmaker" && SCORE_DATABASE=$((SCORE_DATABASE + 2))
823
+
824
+ # INFRASTRUCTURE domain signals
825
+ echo "$DIFF" | grep -qE "docker-compose.*postgres|docker-compose.*redis|postgres.*command:|redis.*command:" && SCORE_INFRA=$((SCORE_INFRA + 3))
826
+ echo "$DIFF" | grep -qE "docker|deploy|traefik|nginx" && SCORE_INFRA=$((SCORE_INFRA + 1))
827
+ echo "$FILES" | grep -qE "^\.github/workflows/" && SCORE_INFRA=$((SCORE_INFRA + 2))
828
+ echo "$FILES" | grep -qE "Dockerfile|docker-compose" && SCORE_INFRA=$((SCORE_INFRA + 2))
829
+
830
+ # SECURITY domain signals (always baseline)
831
+ SCORE_SECURITY=1
832
+ echo "$DIFF" | grep -qE "subprocess|exec|eval|system\(|popen" && SCORE_SECURITY=$((SCORE_SECURITY + 3))
833
+ echo "$DIFF" | grep -qE "os\.system|eval\(|exec\(|pickle|yaml\.load[^_]" && SCORE_SECURITY=$((SCORE_SECURITY + 3))
834
+ echo "$DIFF" | grep -qE "\.sh$|bash|shell|cron" && SCORE_SECURITY=$((SCORE_SECURITY + 2))
835
+
836
+ # SCRAPING domain signals — only match browser-automation/scraping context, NOT bare "playwright"
837
+ echo "$DIFF" | grep -qE "scrape|tier.*escalat|anti_bot|stealth|playwright.*scrape|scrape.*playwright|playbook_min_tier|browser.*pool|web.*scrape" && SCORE_SCRAPING=$((SCORE_SCRAPING + 3))
838
+
839
+ # FRONTEND domain signals
840
+ echo "$FILES" | grep -qE "^web/src/" && SCORE_FRONTEND=$((SCORE_FRONTEND + 1))
841
+ echo "$FILES" | grep -qcE "^web/src/app/|^web/src/components/" | grep -qv "^0$" && SCORE_FRONTEND=$((SCORE_FRONTEND + 2))
842
+
843
+ # API domain signals
844
+ echo "$FILES" | grep -qE "router|routes" && SCORE_API=$((SCORE_API + 2))
845
+ echo "$FILES" | grep -qE "^sdk/|openapi.*\.json$|openapi-versions/" && SCORE_API=$((SCORE_API + 3))
846
+
847
+ echo "=== DOMAIN SCORES ==="
848
+ echo "AUTH=$SCORE_AUTH BILLING=$SCORE_BILLING CONCURRENCY=$SCORE_CONCURRENCY DATABASE=$SCORE_DATABASE INFRA=$SCORE_INFRA SECURITY=$SCORE_SECURITY SCRAPING=$SCORE_SCRAPING FRONTEND=$SCORE_FRONTEND API=$SCORE_API"
849
+ ```
678
850
 
679
- 2. **Critical domain override**: If 2+ of these critical domains are detected in the same PR, spawn agents for ALL affected domains regardless of file count or line changes:
680
- - AUTH + BILLING → Security (deep) + Auth + Billing + Concurrency
681
- - AUTH + DATABASE_MUTATION → Security (deep) + Auth + Database
682
- - FINANCIAL + CONCURRENCY → Security (deep) + Billing + Concurrency + Database
683
- - AUTH + CODE_EXECUTION → Security (deep) + Auth
851
+ #### Step 2: Read Architect CONTRACT for Risk Flags (if PR is from /work-on)
684
852
 
685
- 3. **Why this exists**: A 2-file PR touching both `services/api/app/core/auth.py` and `services/api/app/routers/billing.py` is MORE dangerous than a 20-file refactor in one domain. Small cross-domain PRs create interaction bugs that single-domain reviewers cannot catch. Never reduce agent count based on file count when multiple critical domains are involved.
853
+ ```bash
854
+ CONTRACT_RISK_FLAGS=""
855
+ ISSUE_NUM=$(gh pr view $ARGUMENTS --json body --jq '.body | gsub("(?s).*?(?:Closes #|#)(?<n>[0-9]+).*"; "\(.n)") // empty' 2>/dev/null | head -1)
856
+ if [ -n "$ISSUE_NUM" ]; then
857
+ CONTRACT_BODY=$(gh api repos/${REPO}/issues/${ISSUE_NUM}/comments \
858
+ --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | .[0].body' 2>/dev/null || echo "")
859
+ # Extract risk level from CONTRACT — look for "HIGH" or "CRITICAL" risk markers
860
+ if echo "$CONTRACT_BODY" | grep -qiE "\*\*Risk\*\*.*HIGH|\*\*Risk\*\*.*CRITICAL|Risk.*:\s*(HIGH|CRITICAL)"; then
861
+ CONTRACT_RISK_FLAGS="HIGH_RISK"
862
+ fi
863
+ # Extract cross-domain touches flagged in CONTRACT
864
+ echo "$CONTRACT_BODY" | grep -qiE "auth|session|jwt" && CONTRACT_RISK_FLAGS="${CONTRACT_RISK_FLAGS} CONTRACT_AUTH"
865
+ echo "$CONTRACT_BODY" | grep -qiE "billing|credit|payment" && CONTRACT_RISK_FLAGS="${CONTRACT_RISK_FLAGS} CONTRACT_BILLING"
866
+ echo "$CONTRACT_BODY" | grep -qiE "migration|schema|database" && CONTRACT_RISK_FLAGS="${CONTRACT_RISK_FLAGS} CONTRACT_DATABASE"
867
+ fi
868
+ echo "=== CONTRACT FLAGS: ${CONTRACT_RISK_FLAGS:-none} ==="
869
+ ```
870
+
871
+ #### Step 3: Compute CHURN_ESCALATION
872
+
873
+ Check whether churn hot-spots exist (computed in Phase 3A `$CHURN_CONTEXT`):
874
+
875
+ ```bash
876
+ CHURN_ESCALATION=false
877
+ echo "$CHURN_CONTEXT" | grep -q "HOT" && CHURN_ESCALATION=true
878
+ echo "=== CHURN_ESCALATION: $CHURN_ESCALATION ==="
879
+ ```
880
+
881
+ #### Step 4: Build Selected Agent Roster
882
+
883
+ **If `THOROUGH=true`** (user passed `--thorough`, or `IS_MILESTONE_TO_STAGING=true`): run full union dispatch — all agents matching any signal. Skip to "Full union dispatch" below.
884
+
885
+ **Default (THOROUGH=false):**
886
+
887
+ Start with the baseline roster: `SELECTED_AGENTS="Security"` (General Security always runs).
888
+
889
+ Pick the top 1-2 domain agents by score:
890
+
891
+ ```bash
892
+ # Build sorted list: "score:domain"
893
+ DOMAIN_SCORES="$SCORE_AUTH:AUTH $SCORE_BILLING:BILLING $SCORE_CONCURRENCY:CONCURRENCY $SCORE_DATABASE:DATABASE $SCORE_INFRA:INFRA $SCORE_SCRAPING:SCRAPING $SCORE_FRONTEND:FRONTEND $SCORE_API:API"
894
+
895
+ # Sort descending by score, pick top 2 with score > 0
896
+ TOP_DOMAINS=$(echo "$DOMAIN_SCORES" | tr ' ' '\n' | sort -t: -k1 -rn | head -2 | awk -F: '$1 > 0 {print $2}' | tr '\n' ' ')
897
+ for DOMAIN in $TOP_DOMAINS; do
898
+ SELECTED_AGENTS="$SELECTED_AGENTS $DOMAIN"
899
+ done
900
+ echo "=== BASELINE ROSTER (top domains): $SELECTED_AGENTS ==="
901
+ ```
902
+
903
+ **Apply escalation triggers** — each adds agents if not already included:
904
+
905
+ | Trigger | Condition | Added Agents |
906
+ |---------|-----------|--------------|
907
+ | Critical auth path | `SCORE_AUTH >= 3` | Auth (if not already selected) |
908
+ | Critical billing path | `SCORE_BILLING >= 3` | Billing + Concurrency |
909
+ | Migration/schema change | `SCORE_DATABASE >= 3` | Database |
910
+ | Stateful container change | `SCORE_INFRA >= 3` AND `DATABASE_CONTAINER` signal | Infrastructure |
911
+ | Code execution risk | `SCORE_SECURITY >= 4` (deep scan signals) | Security runs in deep mode (already selected) |
912
+ | Churn hot-spot | `CHURN_ESCALATION=true` AND top-scoring domain | Top domain gets added if not already in roster |
913
+ | CONTRACT high-risk | `CONTRACT_RISK_FLAGS` contains `HIGH_RISK` | All CONTRACT-flagged domain agents |
914
+ | First-pass finding severity | Phase 3 re-entry after a CONFIRMED/HIGH finding posted to PR | Add all agents for the domain of the finding |
915
+ | Scraping domain | `SCORE_SCRAPING >= 3` AND `review.domains.scraping` is set in forge.yaml | Scraping (optional domain pack — never in default catalog) |
916
+ | Cross-critical domains | `SCORE_AUTH >= 2` AND `SCORE_BILLING >= 2` | Auth + Billing + Concurrency |
917
+ | Cross-critical domains | `SCORE_AUTH >= 2` AND `SCORE_DATABASE >= 3` | Auth + Database |
918
+ | Cross-critical domains | `SCORE_BILLING >= 2` AND `SCORE_CONCURRENCY >= 2` | Billing + Concurrency + Database |
919
+
920
+ ```bash
921
+ # Apply escalation triggers
922
+ add_agent() {
923
+ local AGENT="$1"
924
+ echo "$SELECTED_AGENTS" | grep -qw "$AGENT" || SELECTED_AGENTS="$SELECTED_AGENTS $AGENT"
925
+ }
926
+
927
+ [ "$SCORE_AUTH" -ge 3 ] && add_agent "Auth"
928
+ [ "$SCORE_BILLING" -ge 3 ] && { add_agent "Billing"; add_agent "Concurrency"; }
929
+ [ "$SCORE_DATABASE" -ge 3 ] && add_agent "Database"
930
+ [ "$SCORE_INFRA" -ge 3 ] && echo "$DIFF" | grep -qE "docker-compose.*postgres|docker-compose.*redis|postgres.*command:|redis.*command:" && add_agent "Infrastructure"
931
+ # Scraping agent is opt-in: only spawns when review.domains.scraping is configured in forge.yaml
932
+ [ "$SCORE_SCRAPING" -ge 3 ] && [ -n "$DOMAIN_CONTEXT_SCRAPING" ] && add_agent "Scraping"
933
+ [ "$CHURN_ESCALATION" = "true" ] && {
934
+ # Add the top-scoring domain for deeper churn scrutiny if not already selected
935
+ TOP_CHURN_DOMAIN=$(echo "$DOMAIN_SCORES" | tr ' ' '\n' | sort -t: -k1 -rn | head -1 | awk -F: '{print $2}')
936
+ [ -n "$TOP_CHURN_DOMAIN" ] && add_agent "$TOP_CHURN_DOMAIN"
937
+ }
938
+ echo "$CONTRACT_RISK_FLAGS" | grep -q "HIGH_RISK" && {
939
+ echo "$CONTRACT_RISK_FLAGS" | grep -q "CONTRACT_AUTH" && add_agent "Auth"
940
+ echo "$CONTRACT_RISK_FLAGS" | grep -q "CONTRACT_BILLING" && { add_agent "Billing"; add_agent "Concurrency"; }
941
+ echo "$CONTRACT_RISK_FLAGS" | grep -q "CONTRACT_DATABASE" && add_agent "Database"
942
+ }
943
+ # Cross-critical domain escalation
944
+ { [ "$SCORE_AUTH" -ge 2 ] && [ "$SCORE_BILLING" -ge 2 ]; } && { add_agent "Auth"; add_agent "Billing"; add_agent "Concurrency"; }
945
+ { [ "$SCORE_AUTH" -ge 2 ] && [ "$SCORE_DATABASE" -ge 3 ]; } && { add_agent "Auth"; add_agent "Database"; }
946
+ { [ "$SCORE_BILLING" -ge 2 ] && [ "$SCORE_CONCURRENCY" -ge 2 ]; } && { add_agent "Billing"; add_agent "Concurrency"; add_agent "Database"; }
947
+ # SDK/OpenAPI always adds API agent
948
+ echo "$FILES" | grep -qE "^sdk/|openapi.*\.json$|openapi-versions/" && add_agent "API"
949
+
950
+ echo "=== FINAL ROSTER (after escalation): $SELECTED_AGENTS ==="
951
+ AGENT_COUNT=$(echo "$SELECTED_AGENTS" | wc -w)
952
+ echo "=== AGENT COUNT: $AGENT_COUNT ==="
953
+ ```
954
+
955
+ **Full union dispatch (THOROUGH=true or IS_MILESTONE_TO_STAGING=true):**
956
+
957
+ ```bash
958
+ if [ "$THOROUGH" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; then
959
+ SELECTED_AGENTS="Security"
960
+ [ "$SCORE_AUTH" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Auth"
961
+ [ "$SCORE_BILLING" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Billing Concurrency"
962
+ [ "$SCORE_DATABASE" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Database"
963
+ [ "$SCORE_INFRA" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Infrastructure"
964
+ # Scraping agent only added in thorough mode when review.domains.scraping is configured
965
+ SCRAPING_ENABLED=$(yq '.review.domains.scraping' "$FORGE_YAML" 2>/dev/null || echo "")
966
+ [ "$SCORE_SCRAPING" -gt 0 ] && [ -n "$SCRAPING_ENABLED" ] && SELECTED_AGENTS="$SELECTED_AGENTS Scraping"
967
+ [ "$SCORE_FRONTEND" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Frontend"
968
+ [ "$SCORE_API" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS API"
969
+ # Deduplicate
970
+ SELECTED_AGENTS=$(echo "$SELECTED_AGENTS" | tr ' ' '\n' | sort -u | tr '\n' ' ')
971
+ echo "=== THOROUGH mode: FULL UNION DISPATCH — $SELECTED_AGENTS ==="
972
+ fi
973
+ ```
974
+
975
+ **Why cross-critical domain pairs always escalate**: A 2-file PR touching both `services/api/app/core/auth.py` and `services/api/app/routers/billing.py` creates interaction bugs that single-domain reviewers cannot catch. Never rely on a single agent for multi-domain risk.
976
+
977
+ **General Security agent ALWAYS runs.** If BILLING is selected, Concurrency is always added. If SHARED module is touched, add agents for all importing services.
978
+
979
+ #### Domain-to-File Mapping (for diff slicing in Phase 3C)
980
+
981
+ Each agent receives only its domain-relevant diff slice rather than the full changeset. Compute per-domain file filters:
982
+
983
+ ```bash
984
+ # Domain file patterns — used in Phase 3C to scope each agent's input
985
+ DOMAIN_FILES_AUTH=$(echo "$FILES" | grep -iE "auth|session|login|logout|jwt|oauth|token|user|permission|middleware" || echo "")
986
+ DOMAIN_FILES_BILLING=$(echo "$FILES" | grep -iE "billing|credit|payment|charge|refund|subscription|pricing|tier" || echo "")
987
+ DOMAIN_FILES_CONCURRENCY=$(echo "$FILES" | grep -iE "lock|queue|atomic|transaction|worker|job|task|celery|redis" || echo "")
988
+ DOMAIN_FILES_DATABASE=$(echo "$FILES" | grep -iE "migration|\.sql$|model|schema|db|database|alembic" || echo "")
989
+ DOMAIN_FILES_INFRA=$(echo "$FILES" | grep -iE "docker|nginx|traefik|\.github|Makefile|deploy|infra" || echo "")
990
+ DOMAIN_FILES_SCRAPING=$(echo "$FILES" | grep -iE "scrape|crawler|stealth|browser.*pool|headless|anti.bot|detection.*keyword|captcha" || echo "")
991
+ DOMAIN_FILES_FRONTEND=$(echo "$FILES" | grep -iE "^web/|\.tsx?$|\.jsx?$|component|page|layout|style|css" || echo "")
992
+ DOMAIN_FILES_API=$(echo "$FILES" | grep -iE "router|route|endpoint|openapi|sdk|api" || echo "")
993
+
994
+ # Fallback: if a domain has no specific files matched, give it the full file list
995
+ # (happens for Security — which reviews everything — and for edge cases)
996
+ [ -z "$DOMAIN_FILES_AUTH" ] && DOMAIN_FILES_AUTH="$FILES"
997
+ [ -z "$DOMAIN_FILES_BILLING" ] && DOMAIN_FILES_BILLING="$FILES"
998
+ [ -z "$DOMAIN_FILES_DATABASE" ] && DOMAIN_FILES_DATABASE="$FILES"
999
+ ```
686
1000
 
687
1001
  ### 3C: Load Agent Templates & Launch
688
1002
 
689
- **>>> INVOCATION: Read the agent catalog file:**
1003
+ **>>> INVOCATION: Read shared protocols + selected persona files:**
690
1004
  ```
691
- Read the file: $FORGE_HOME/commands/review-pr-agents.md
1005
+ Read: $FORGE_HOME/commands/review-pr-agents/protocols.md
1006
+ Read: $FORGE_HOME/commands/review-pr-agents/<persona>.md (one per selected agent from Phase 3B)
692
1007
  ```
693
1008
 
1009
+ Persona file names: `security.md` (always), `auth.md`, `billing.md`, `concurrency.md`, `scraper.md`, `frontend.md`, `api.md`, `database.md`, `infra.md`.
1010
+ See `review-pr-agents.md` for the full routing table mapping domains → persona files.
1011
+
694
1012
  (`$FORGE_HOME` defaults to `~/.claude`)
695
1013
 
696
1014
  **>>> LOAD CONFIG: Read forge.yaml for project context:**
@@ -699,30 +1017,193 @@ Read the file: $FORGE_HOME/commands/review-pr-agents.md
699
1017
  FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
700
1018
  PROJECT_NAME=$(yq '.project.name' "$FORGE_YAML" 2>/dev/null || echo "this project")
701
1019
  PROJECT_CONTEXT=$(yq '.review.context' "$FORGE_YAML" 2>/dev/null || echo "")
702
- TECH_STACK=$(yq '.review.tech_stack' "$FORGE_YAML" 2>/dev/null || echo "")
703
1020
  # Domain-specific context (keyed by agent name)
704
1021
  DOMAIN_CONTEXT_AUTH=$(yq '.review.domains.auth' "$FORGE_YAML" 2>/dev/null || echo "")
705
1022
  DOMAIN_CONTEXT_BILLING=$(yq '.review.domains.billing' "$FORGE_YAML" 2>/dev/null || echo "")
1023
+ DOMAIN_CONTEXT_CONCURRENCY=$(yq '.review.domains.concurrency' "$FORGE_YAML" 2>/dev/null || echo "")
706
1024
  DOMAIN_CONTEXT_INFRA=$(yq '.review.domains.infra' "$FORGE_YAML" 2>/dev/null || echo "")
707
1025
  DOMAIN_CONTEXT_DATABASE=$(yq '.review.domains.database' "$FORGE_YAML" 2>/dev/null || echo "")
708
1026
  DOMAIN_CONTEXT_FRONTEND=$(yq '.review.domains.frontend' "$FORGE_YAML" 2>/dev/null || echo "")
709
1027
  DOMAIN_CONTEXT_SECURITY=$(yq '.review.domains.security' "$FORGE_YAML" 2>/dev/null || echo "")
710
1028
  DOMAIN_CONTEXT_API=$(yq '.review.domains.api' "$FORGE_YAML" 2>/dev/null || echo "")
1029
+ # Scraping domain context also gates spawning: agent only runs when this key is set
1030
+ DOMAIN_CONTEXT_SCRAPING=$(yq '.review.domains.scraping' "$FORGE_YAML" 2>/dev/null || echo "")
711
1031
  ```
712
1032
 
713
1033
  If `forge.yaml` is absent or a field is empty/null, agents fall back to generic checks (no project-specific context injected — agents still function correctly, just without project conventions).
714
1034
 
715
- This file contains the Evidence-Based Review Protocol, Structured Findings Protocol, and all 9 agent prompt templates. For each selected agent:
716
- 1. Extract its template from the catalog
1035
+ **Code Index Slice (inject per agent — replaces full-repo search space with domain-scoped facts):**
1036
+
1037
+ Before launching agents, query the code index to build a domain-scoped file list and symbol map for each agent's diff domain. This replaces full-repo search with deterministic pre-computed data:
1038
+
1039
+ ```bash
1040
+ REPO_PATH=$(yq '.paths.root' "$FORGE_YAML" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
1041
+ CODE_INDEX_SCRIPT="${REPO_PATH}/scripts/code-index.sh"
1042
+
1043
+ # Ensure index is current (cache-hit on unchanged HEAD — instant if already built)
1044
+ if [[ -x "$CODE_INDEX_SCRIPT" ]]; then
1045
+ bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" 2>/dev/null || true
1046
+ fi
1047
+
1048
+ # Build domain-scoped index slices for each selected agent's domain
1049
+ # Replace {DOMAIN} with the agent's domain label (auth, billing, database, api, frontend, etc.)
1050
+ build_index_slice() {
1051
+ local domain="$1"
1052
+ if [[ -x "$CODE_INDEX_SCRIPT" ]]; then
1053
+ local files
1054
+ files=$(bash "$CODE_INDEX_SCRIPT" query --domain "$domain" --repo-path "$REPO_PATH" 2>/dev/null | awk -F'\t' '{print $1}' | head -30 | tr '\n' ' ')
1055
+ echo "Domain files (${domain}): ${files:-none}"
1056
+ else
1057
+ echo "Code index not available — agent will use grep exploration"
1058
+ fi
1059
+ }
1060
+
1061
+ # Compute slices for selected agents (only the domains that are actually running)
1062
+ INDEX_SLICE_AUTH=$(build_index_slice "auth")
1063
+ INDEX_SLICE_BILLING=$(build_index_slice "billing")
1064
+ INDEX_SLICE_DATABASE=$(build_index_slice "database")
1065
+ INDEX_SLICE_API=$(build_index_slice "api")
1066
+ INDEX_SLICE_FRONTEND=$(build_index_slice "frontend")
1067
+ INDEX_SLICE_INFRA=$(build_index_slice "infrastructure")
1068
+ ```
1069
+
1070
+ **Absence is not an error**: If `scripts/code-index.sh` is absent, `INDEX_SLICE_*` variables will contain the fallback message and agents proceed with their standard grep-based exploration.
1071
+
1072
+ **>>> COMPUTE: Per-domain diff slices (input-scoping — do this BEFORE launching agents):**
1073
+
1074
+ Each domain agent receives only the diff slice relevant to its domain, not the full PR changeset. This caps per-child input cost on large PRs. Compute slices once here; substitute `[DOMAIN_DIFF_SLICE]` per agent below.
1075
+
1076
+ ```bash
1077
+ # Full diff fetched once — agents do NOT re-fetch it
1078
+ FULL_DIFF=$(gh pr diff $ARGUMENTS)
1079
+ FULL_FILES=$(gh pr diff $ARGUMENTS --name-only)
1080
+
1081
+ # --- Security agent: always receives the full diff (cross-cutting domain) ---
1082
+ DIFF_SLICE_SECURITY="$FULL_DIFF"
1083
+
1084
+ # --- Auth agent: auth, session, jwt, oauth, middleware, permission files ---
1085
+ DIFF_SLICE_AUTH=$(echo "$FULL_DIFF" | awk '
1086
+ /^diff --git/ { in_block=0 }
1087
+ /^diff --git.*\/(auth|session|jwt|oauth|permission|middleware|login|token)/ { in_block=1 }
1088
+ in_block { print }
1089
+ ')
1090
+ [ -z "$DIFF_SLICE_AUTH" ] && DIFF_SLICE_AUTH="$FULL_DIFF"
1091
+
1092
+ # --- Billing agent: billing, payment, credit, pricing, stripe, subscription files ---
1093
+ DIFF_SLICE_BILLING=$(echo "$FULL_DIFF" | awk '
1094
+ /^diff --git/ { in_block=0 }
1095
+ /^diff --git.*\/(billing|payment|credit|pricing|stripe|subscription|invoice|charge|refund)/ { in_block=1 }
1096
+ in_block { print }
1097
+ ')
1098
+ [ -z "$DIFF_SLICE_BILLING" ] && DIFF_SLICE_BILLING="$FULL_DIFF"
1099
+
1100
+ # --- Concurrency agent: transaction, lock, queue, async, worker, race-condition files ---
1101
+ DIFF_SLICE_CONCURRENCY=$(echo "$FULL_DIFF" | awk '
1102
+ /^diff --git/ { in_block=0 }
1103
+ /^diff --git.*\/(worker|queue|task|async|lock|transaction|pipeline|job)/ { in_block=1 }
1104
+ in_block { print }
1105
+ ')
1106
+ [ -z "$DIFF_SLICE_CONCURRENCY" ] && DIFF_SLICE_CONCURRENCY="$FULL_DIFF"
1107
+
1108
+ # --- Database agent: migration, model, schema, ORM, SQL files ---
1109
+ DIFF_SLICE_DATABASE=$(echo "$FULL_DIFF" | awk '
1110
+ /^diff --git/ { in_block=0 }
1111
+ /^diff --git.*\/(migration|model|schema|\.sql$|orm|database|db)/ { in_block=1 }
1112
+ in_block { print }
1113
+ ')
1114
+ [ -z "$DIFF_SLICE_DATABASE" ] && DIFF_SLICE_DATABASE="$FULL_DIFF"
1115
+
1116
+ # --- Frontend agent: web/src, components, pages, styles, tsx/jsx files ---
1117
+ DIFF_SLICE_FRONTEND=$(echo "$FULL_DIFF" | awk '
1118
+ /^diff --git/ { in_block=0 }
1119
+ /^diff --git.*(web\/src|components|pages|styles|\.tsx|\.jsx|\.css|\.scss)/ { in_block=1 }
1120
+ in_block { print }
1121
+ ')
1122
+ [ -z "$DIFF_SLICE_FRONTEND" ] && DIFF_SLICE_FRONTEND="$FULL_DIFF"
1123
+
1124
+ # --- API Design agent: router, endpoint, openapi, schema, serializer files ---
1125
+ DIFF_SLICE_API=$(echo "$FULL_DIFF" | awk '
1126
+ /^diff --git/ { in_block=0 }
1127
+ /^diff --git.*\/(router|route|endpoint|openapi|serializer|schema|api)/ { in_block=1 }
1128
+ in_block { print }
1129
+ ')
1130
+ [ -z "$DIFF_SLICE_API" ] && DIFF_SLICE_API="$FULL_DIFF"
1131
+
1132
+ # --- Infrastructure agent: docker, nginx, ci, deploy, infra, workflow files ---
1133
+ DIFF_SLICE_INFRA=$(echo "$FULL_DIFF" | awk '
1134
+ /^diff --git/ { in_block=0 }
1135
+ /^diff --git.*\/(docker|nginx|infra|\.github|deploy|ci|traefik|k8s|helm)/ { in_block=1 }
1136
+ in_block { print }
1137
+ ')
1138
+ [ -z "$DIFF_SLICE_INFRA" ] && DIFF_SLICE_INFRA="$FULL_DIFF"
1139
+
1140
+ # --- Domain Logic agent: scraping/browser-automation files (does not match bare playwright/E2E test files) ---
1141
+ DIFF_SLICE_SCRAPER=$(echo "$FULL_DIFF" | awk '
1142
+ /^diff --git/ { in_block=0 }
1143
+ /^diff --git.*\/(scrape|scraper|stealth|browser_pool|headless|anti_bot|captcha|crawl)/ { in_block=1 }
1144
+ in_block { print }
1145
+ ')
1146
+ [ -z "$DIFF_SLICE_SCRAPER" ] && DIFF_SLICE_SCRAPER="$FULL_DIFF"
1147
+ ```
1148
+
1149
+ **Tool-result truncation discipline**: All `gh pr diff` and file-read outputs piped to agents MUST be capped at ~100K characters. This mirrors the runner's built-in 100K-char tool-result cap (`bin/runner.mjs`). Any diff slice exceeding 100K chars must be truncated before substitution — agents that receive oversized context perform worse, not better.
1150
+
1151
+ ```bash
1152
+ # Truncate any slice exceeding 100K chars before passing to agent
1153
+ truncate_slice() {
1154
+ local slice="$1"
1155
+ local limit=102400
1156
+ if [ "${#slice}" -gt "$limit" ]; then
1157
+ echo "${slice:0:$limit}"
1158
+ echo "... [TRUNCATED — diff exceeded 100K chars; focus on the files listed above]"
1159
+ else
1160
+ echo "$slice"
1161
+ fi
1162
+ }
1163
+ DIFF_SLICE_SECURITY=$(truncate_slice "$DIFF_SLICE_SECURITY")
1164
+ DIFF_SLICE_AUTH=$(truncate_slice "$DIFF_SLICE_AUTH")
1165
+ DIFF_SLICE_BILLING=$(truncate_slice "$DIFF_SLICE_BILLING")
1166
+ DIFF_SLICE_CONCURRENCY=$(truncate_slice "$DIFF_SLICE_CONCURRENCY")
1167
+ DIFF_SLICE_DATABASE=$(truncate_slice "$DIFF_SLICE_DATABASE")
1168
+ DIFF_SLICE_FRONTEND=$(truncate_slice "$DIFF_SLICE_FRONTEND")
1169
+ DIFF_SLICE_API=$(truncate_slice "$DIFF_SLICE_API")
1170
+ DIFF_SLICE_INFRA=$(truncate_slice "$DIFF_SLICE_INFRA")
1171
+ DIFF_SLICE_SCRAPER=$(truncate_slice "$DIFF_SLICE_SCRAPER")
1172
+ ```
1173
+
1174
+ The `protocols.md` file contains the Evidence-Based Review Protocol, Structured Findings Protocol, Per-Agent Input Scoping rules, and Tool-Result Truncation Discipline. Each persona file contains that agent's full prompt template. For each agent in `$SELECTED_AGENTS` (computed in Phase 3B):
1175
+ 1. Extract its template from the persona file (`review-pr-agents/<persona>.md`)
717
1176
  2. Substitute: `[PR_NUMBER]`, `[REVIEW_SHA]`, `[REVIEW_SHA_SHORT]`, `[TITLE]`, relevant files list
718
- 3. Substitute: `[PROJECT_NAME]` → `$PROJECT_NAME`, `[PROJECT_CONTEXT]` → `$PROJECT_CONTEXT`, `[TECH_STACK]` → `$TECH_STACK`
719
- 4. Substitute per-agent domain context: `[DOMAIN_CONTEXT]` → the agent's matching key from `forge.yaml → review.domains` (e.g., `$DOMAIN_CONTEXT_AUTH` for the auth agent, `$DOMAIN_CONTEXT_BILLING` for the billing agent)
1177
+ 3. Substitute: `[PROJECT_NAME]` → `$PROJECT_NAME`, `[PROJECT_CONTEXT]` → `$PROJECT_CONTEXT`
1178
+ 4. Substitute per-agent domain context: `[DOMAIN_CONTEXT]` → the agent's matching key from `forge.yaml → review.domains` (e.g., `$DOMAIN_CONTEXT_AUTH` for the auth agent, `$DOMAIN_CONTEXT_BILLING` for the billing agent, `$DOMAIN_CONTEXT_CONCURRENCY` for the concurrency agent, `$DOMAIN_CONTEXT_SCRAPING` for the scraping agent)
720
1179
  5. Substitute the shared hot-spot prior: `[CHURN_CONTEXT]` → `$CHURN_CONTEXT` (computed once in Phase 3A, same value for every agent — this is a PR-level fact, not a per-domain config value, so it is NOT read from `forge.yaml`)
721
- 6. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
722
- 7. Launch via `Task` tool with the resolved model (default `"sonnet"`, fallback `"opus"` if rate-limited)
1180
+ 6. Substitute code index slice: `[INDEX_SLICE]` → the matching `$INDEX_SLICE_{DOMAIN}` variable for this agent (e.g., `$INDEX_SLICE_AUTH` for the auth agent). Agents MUST query index data first; fall back to grep only when index slice is empty or unavailable.
1181
+ 7. Substitute per-agent diff slice: `[DOMAIN_DIFF_SLICE]` → the matching `$DIFF_SLICE_*` variable (e.g., `$DIFF_SLICE_AUTH` for the auth agent, `$DIFF_SLICE_SECURITY` for the security agent). This replaces any `gh pr diff [PR_NUMBER]` call inside the agent template — the agent works from the pre-computed slice, not the full changeset.
1182
+ 8. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
1183
+ 9. Launch via `Task` tool with the resolved model (default `"sonnet"`, fallback `"opus"` if rate-limited)
723
1184
 
724
1185
  **CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple Task tool calls. Each agent posts findings directly to the PR via `gh pr comment`.
725
1186
 
1187
+ #### Domain Diff Slicing
1188
+
1189
+ Each agent's prompt receives `[FILE_LIST]` scoped to its domain-relevant files (computed in Phase 3B as `DOMAIN_FILES_*`). This reduces per-agent token cost — each agent reads only the diff slice relevant to its domain, not the full changeset.
1190
+
1191
+ - **Security agent**: receives the full file list and full diff (it must see everything)
1192
+ - **Domain agents**: receive `DOMAIN_FILES_<DOMAIN>` as `[FILE_LIST]`; if a domain's file list is empty (fallback), pass the full list
1193
+
1194
+ When substituting `[FILE_LIST]` in each agent's template:
1195
+ - Auth agent → `$DOMAIN_FILES_AUTH`
1196
+ - Billing agent → `$DOMAIN_FILES_BILLING`
1197
+ - Concurrency agent → `$DOMAIN_FILES_CONCURRENCY`
1198
+ - Database agent → `$DOMAIN_FILES_DATABASE`
1199
+ - Infrastructure agent → `$DOMAIN_FILES_INFRA`
1200
+ - Scraping agent → `$DOMAIN_FILES_SCRAPING`
1201
+ - Frontend agent → `$DOMAIN_FILES_FRONTEND`
1202
+ - API agent → `$DOMAIN_FILES_API`
1203
+ - Security agent → `$FILES` (full list)
1204
+
1205
+ **Note**: Domain agents still run `gh pr diff $PR_NUMBER` inside their execution context. The scoped `[FILE_LIST]` tells each agent which files are most relevant to its domain — it is a focused starting point, not a hard restriction. Agents should follow code paths beyond their slice if those paths are needed to complete a trace.
1206
+
726
1207
  ---
727
1208
 
728
1209
  ## Phase 4: Wait for Agents
@@ -829,8 +1310,26 @@ fi
829
1310
 
830
1311
  **Dedup against existing issues (MANDATORY before creating):**
831
1312
 
1313
+ Run the deterministic dedup script first, then fall through to the line-range check:
1314
+
1315
+ ```bash
1316
+ # Step 0: Deterministic title dedup — catches near-duplicates before line-range check
1317
+ # See scripts/issue-dedup.sh for the token-overlap algorithm. <!-- Added: forge#1335 -->
1318
+ FINDING_TITLE_DEDUP="fix: brief description of finding (review finding — PR #${PR_NUMBER})"
1319
+ DEDUP_RESULT=$(scripts/issue-dedup.sh "$FINDING_TITLE_DEDUP" "$GH_FLAG" 2>&1)
1320
+ DEDUP_EXIT=$?
1321
+ if [ "$DEDUP_EXIT" -eq 1 ]; then
1322
+ echo "DEDUP: Skipping — $DEDUP_RESULT"
1323
+ # Skip this finding — do NOT create a duplicate issue
1324
+ # Add a comment on the existing issue referencing this recurrence in PR #${PR_NUMBER}
1325
+ elif [ "$DEDUP_EXIT" -eq 2 ]; then
1326
+ echo "DEDUP: Usage error — $DEDUP_RESULT"
1327
+ # Skip this finding — do NOT fall through to gh issue create on a usage error
1328
+ fi
1329
+ ```
1330
+
832
1331
  ```bash
833
- # For each finding, check if an open issue already exists at the same file within ±5 lines
1332
+ # For each finding (that passes the title dedup above), check line-range overlap
834
1333
  FINDING_FILE="path/to/file.py"
835
1334
  FINDING_LINE="123"
836
1335
  FINDING_TITLE="fix: brief description of finding (review finding — PR #${PR_NUMBER})"
@@ -908,6 +1407,11 @@ ISSUE_NUM=$(gh issue create \
908
1407
  **Root cause**: [one sentence — why the bug occurs mechanically]
909
1408
  **Prevention**: [one sentence — what the builder must do to avoid this class of bug]
910
1409
 
1410
+ <!-- FORGE:PATTERN: [pattern-slug] -->
1411
+ <!-- This machine-readable tag is used by pipeline-health Phase 4A to count pattern recurrences.
1412
+ When this slug appears on 3+ findings, a check-promotion issue is automatically filed.
1413
+ Keep the slug consistent across all findings for the same defect class. --> <!-- Added: forge#1331 -->
1414
+
911
1415
  ## Affected Files
912
1416
 
913
1417
  Files that need changes:
@@ -934,12 +1438,39 @@ Files that need changes:
934
1438
  - [ ] Trace code path to verify issue
935
1439
  - [ ] Check existing mitigations
936
1440
  - [ ] Reproduce or construct proof-of-concept
1441
+ [BATCHABLE_ANNOTATION]
937
1442
  ISSUE_EOF
938
1443
  )" --json number --jq '.number')
939
1444
  ```
940
1445
 
941
1446
  Labels: `review-finding` + `needs-validation` + priority (`priority:P1` CONFIRMED, `priority:P2` LIKELY, `priority:P3` POSSIBLE).
942
1447
 
1448
+ **P3 batchable annotation** — <!-- Added: forge#1333 --> When priority resolves to `priority:P3` (POSSIBLE confidence), check whether the finding qualifies for batching. If the affected file does NOT touch a security or billing path, substitute `[BATCHABLE_ANNOTATION]` with the `<!-- FORGE:BATCHABLE -->` marker. This signals the orchestrator's Phase 1 batching rule that this finding can be grouped with other P3s in the same domain:
1449
+
1450
+ ```bash
1451
+ # Determine batchable eligibility for this finding
1452
+ FINDING_PRIORITY="{priority}" # priority:P1 / priority:P2 / priority:P3
1453
+ BATCHABLE_ANNOTATION=""
1454
+
1455
+ if [ "$FINDING_PRIORITY" = "priority:P3" ]; then
1456
+ # Check for security/billing exclusions
1457
+ IS_SECURITY_PATH=0
1458
+ IS_BILLING_PATH=0
1459
+ # Test affected file path
1460
+ echo "$FINDING_FILE" | grep -qiE 'security|billing|payment|stripe|charge|invoice' && IS_SECURITY_PATH=1
1461
+ echo "$FINDING_TITLE" | grep -qiE 'security|billing|payment|stripe|charge|invoice' && IS_BILLING_PATH=1
1462
+
1463
+ if [ "$IS_SECURITY_PATH" -eq 0 ] && [ "$IS_BILLING_PATH" -eq 0 ]; then
1464
+ BATCHABLE_ANNOTATION="<!-- FORGE:BATCHABLE -->"
1465
+ echo "INFO: P3 finding eligible for batching — appending FORGE:BATCHABLE annotation"
1466
+ else
1467
+ BATCHABLE_ANNOTATION=""
1468
+ echo "INFO: P3 finding touches security/billing path — NOT batchable, keeping individual pipeline"
1469
+ fi
1470
+ fi
1471
+ # When priority is P1 or P2: BATCHABLE_ANNOTATION stays empty (never batched)
1472
+ ```
1473
+
943
1474
  **Add to project board:**
944
1475
  ```bash
945
1476
  for FINDING_NUM in {numbers}; do
@@ -1046,20 +1577,29 @@ if [ "$MERGE_HEALTH" = "CONFLICTING" ] || [ "$MERGE_HEALTH_STATE" = "DIRTY" ] ||
1046
1577
  MERGE_CONFLICT_MSG="Merge conflict with \`${BASE}\`. Rebase \`${HEAD}\` onto \`origin/${BASE}\`, resolve the conflicting files, then re-run /review-pr."
1047
1578
  fi
1048
1579
 
1580
+ # Resolve attribution footer (forge.yaml → attribution.pr_footer)
1581
+ ATTRIBUTION_PR_FOOTER=$(grep -A5 "^attribution:" forge.yaml 2>/dev/null | grep "pr_footer:" | awk '{print $2}' | tr -d '"' || echo "false")
1582
+ ATTRIBUTION_FOOTER_LINE=""
1583
+ if [ "$ATTRIBUTION_PR_FOOTER" = "true" ]; then
1584
+ ATTRIBUTION_FOOTER_LINE="
1585
+ ---
1586
+ > Orchestrated with [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock) — state, scheduling, review, and memory on GitHub."
1587
+ fi
1588
+
1049
1589
  # Stale review:
1050
1590
  gh pr review $ARGUMENTS --comment --body "Review of commit $REVIEW_SHA_SHORT is stale — PR HEAD changed. Re-run /review-pr."
1051
1591
 
1052
1592
  # Clean (no blocking issues):
1053
1593
  gh pr review $ARGUMENTS --comment --body "APPROVED: commit $REVIEW_SHA_SHORT after context-aware review ([N] agents: [names]). [M] findings created as issues. Safe to merge.
1054
1594
  $([ "$MERGE_HEALTH" = "UNKNOWN" ] && echo "
1055
- ⚠ Mergeability: GitHub is still computing merge state (UNKNOWN after retries). Verify manually before merging.")"
1595
+ ⚠ Mergeability: GitHub is still computing merge state (UNKNOWN after retries). Verify manually before merging.")${ATTRIBUTION_FOOTER_LINE}"
1056
1596
 
1057
1597
  # Blocking issues (including merge conflicts and purpose regressions):
1058
1598
  gh pr review $ARGUMENTS --comment --body "CHANGES REQUESTED: commit $REVIEW_SHA_SHORT — [N] blocking issues found. See GitHub issues.
1059
1599
  $([ "$HAS_MERGE_CONFLICT" = "true" ] && echo "
1060
1600
  🔴 Merge Conflict: ${MERGE_CONFLICT_MSG}")
1061
1601
  $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
1062
- ⚠ Purpose Regression: [N] finding(s) contradict the milestone's stated goal and are automatically blocking regardless of runtime impact. See: ${PURPOSE_REGRESSION_FINDINGS[@]}")"
1602
+ ⚠ Purpose Regression: [N] finding(s) contradict the milestone's stated goal and are automatically blocking regardless of runtime impact. See: ${PURPOSE_REGRESSION_FINDINGS[@]}")${ATTRIBUTION_FOOTER_LINE}"
1063
1603
  ```
1064
1604
 
1065
1605
  ---
@@ -1069,6 +1609,20 @@ $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
1069
1609
  **Skip if** `AUTO_MERGE=false`.
1070
1610
 
1071
1611
  ```bash
1612
+ # §7B verdict + purpose-regression guard — check before any merge attempt <!-- Added: forge#1601 -->
1613
+ # HARD RULE 3 requires that VERDICT=CHANGES REQUESTED and HAS_PURPOSE_REGRESSION=true
1614
+ # always block merge, including under --auto-merge. These vars are set in Phase 7A/7B
1615
+ # earlier in the same agent session. An unset/empty VERDICT is safe — it evaluates to
1616
+ # "" which does not equal "CHANGES REQUESTED", so fast-lane PRs (no 7A run) are unaffected.
1617
+ if [ "$VERDICT" = "CHANGES REQUESTED" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ]; then
1618
+ BLOCK_REASON=""
1619
+ [ "$VERDICT" = "CHANGES REQUESTED" ] && BLOCK_REASON="review verdict is CHANGES REQUESTED (blocking finding confirmed by Phase 7B)"
1620
+ [ "$HAS_PURPOSE_REGRESSION" = "true" ] && BLOCK_REASON="${BLOCK_REASON:+${BLOCK_REASON}; }purpose regression detected by Phase 7A (\`HAS_PURPOSE_REGRESSION=true\`)"
1621
+ gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: ${BLOCK_REASON}. Manual review required before merging."
1622
+ gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "needs-human" 2>/dev/null || true
1623
+ # STOP — do not attempt gh pr merge when §7B blocking conditions are active
1624
+ else
1625
+
1072
1626
  # Pre-merge mergeability guard — re-fetch fresh state before attempting merge <!-- Added: forge#194 -->
1073
1627
  # A PR that was MERGEABLE at Phase 1A may have become CONFLICTING if the base branch
1074
1628
  # received commits while the review was running. Re-check before every auto-merge.
@@ -1082,7 +1636,7 @@ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIR
1082
1636
  # STOP — do not attempt gh pr merge on a CONFLICTING/DIRTY PR
1083
1637
  else
1084
1638
  # Checkpoint comment on issue
1085
- gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: {VERDICT}. Proceeding to merge."
1639
+ gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: ${VERDICT:-APPROVED}. Proceeding to merge."
1086
1640
 
1087
1641
  # Merge
1088
1642
  gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge
@@ -1091,9 +1645,10 @@ else
1091
1645
  MERGE_STATE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json state --jq '.state')
1092
1646
  [ "$MERGE_STATE" != "MERGED" ] && gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "PR #{PR_NUMBER} merge failed. State: $MERGE_STATE."
1093
1647
  fi
1648
+ fi
1094
1649
  ```
1095
1650
 
1096
- **Important**: Phase 8 ONLY merges the PR. It does NOT close the issue, update labels, or clean up worktrees. When invoked via `/work-on`, those responsibilities belong to `work-on/close.md` — which runs after the router detects state 4 (MERGE_COMPLETE: PR merged + issue open). Doing them here would cause the router to hit TERMINAL_MERGED (state 1) and skip the close phase entirely.
1651
+ **Important**: Phase 8 ONLY merges the PR. It does NOT close the issue, update labels, or clean up worktrees. When invoked via `/work-on`, those responsibilities belong to `work-on/close.md` (work-on.md Phase 6 — triggered when PR is merged and issue is still open). Doing them here would cause the issue to be closed with `workflow:merged` before Phase 6 runs, skipping the close phase entirely.
1097
1652
 
1098
1653
  ### 8B: Post-Merge Review Finding Demilestoning (Milestone PRs Only)
1099
1654
 
@@ -1194,7 +1749,7 @@ gh pr comment $ARGUMENTS --body "$(cat <<'EOF'
1194
1749
  ## Verdict: [APPROVE / CHANGES REQUESTED / NEEDS RE-REVIEW]
1195
1750
 
1196
1751
  ## Context-Aware Review
1197
- **Domains**: [list] | **Agents**: [N] ([names])
1752
+ **Domains**: [list] | **Agents**: [N] ([names]) | **Dispatch mode**: [risk-scaled (default) / thorough (--thorough flag) / full-union (milestone PR)]
1198
1753
 
1199
1754
  ## Integration Checks (Phase 2.5)
1200
1755
  **Code registration**: [pass / N broken activation paths found]
@@ -1226,9 +1781,10 @@ Notify user:
1226
1781
  ```
1227
1782
  Review complete for PR #X. Verdict: [VERDICT].
1228
1783
  - Domains: [list]
1229
- - Agents: [N] ([names])
1784
+ - Agents: [N] ([names]) — [dispatch mode: risk-scaled / thorough / full-union]
1230
1785
  - Integration checks: [pass/N broken paths found]
1231
1786
  - Issues created: [M]
1232
1787
  {IF AUTO_MERGE: "PR merged and issue closed." / "Merge FAILED — see issue."}
1233
1788
  {IF IS_MILESTONE_TO_STAGING: "Review findings demilestoned: [N] issues moved to fast lane." / ""}
1789
+ {Tip if risk-scaled: "Run with --thorough to enable full union dispatch for release-critical reviews."}
1234
1790
  ```