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
@@ -13,7 +13,10 @@ You are a quality gate that runs AFTER implementation but BEFORE commit. Your jo
13
13
 
14
14
  You do NOT post to GitHub. You do NOT create issues. You return findings directly to the builder agent that spawned you.
15
15
 
16
- **Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`.
16
+ **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
17
+ **NEVER use plan mode (EnterPlanMode).**
18
+
19
+ <!-- FORGE:SPEC_LOADED — quality-gate.md loaded and active. Agent is bound by this spec. -->
17
20
 
18
21
  ---
19
22
 
@@ -39,16 +42,17 @@ This quality gate uses **domain detection** to adapt to your project's actual te
39
42
  | FRONTEND | `.tsx`/`.ts` files in client component paths |
40
43
  | PROXY | Client components using `fetch`/`useSWR`/`apiFetch` |
41
44
  | SHELL | `.sh` files or scripts using `curl`/`wget` |
42
- | STRING_SETS | Python files in anti-detection or browser automation paths |
45
+ | STRING_SETS | *(scraping pack — opt-in)* Python files in anti-detection or browser automation paths. Enable via `forge.yaml → quality_gate.optional_domains: [STRING_SETS]` |
43
46
  | CONCURRENCY | Python files using `asyncio.shield`, `asyncio.wait_for`, or `Task.cancel` |
44
47
  | STATE | Python files with new module-level mutable variable assignments |
45
- | CAPACITY | Worker/infra Python files with new size/limit/threshold constants |
48
+ | CAPACITY | *(scraping pack — opt-in)* Worker/infra Python files with new size/limit/threshold constants. Enable via `forge.yaml → quality_gate.optional_domains: [CAPACITY]` |
46
49
  | WORKFLOW | `.yml` files in `.github/workflows/` |
47
50
  | IMPORT_RESOLUTION | Python files with new `from app.*` imports added outside `try:` blocks |
48
51
  | INFRA | Dockerfiles or entrypoints with runtime UID changes |
49
52
  | ROUTER_BUG | Router Python files where gate conditions are narrowed |
50
53
  | FORGE_GRAPH | `commands/*.md` specs or `scripts/*` files change (ForgeDock self-consistency) |
51
54
  | CONFIG_SCHEMA | Config files for external tools (`traefik/`, `infra/nginx/`, `k8s/`, `terraform/`, `*.conf`, `*.toml` in infra paths, or `docker-compose*.yml` with service definition changes) |
55
+ | BILLING | Files under `billing/`, `credits/`, `subscription`, `ledger`, or `payment` paths |
52
56
 
53
57
  **Stack-agnostic coverage**: The example domains above reflect common patterns caught in production; they are not requirements. A Go or Ruby project benefits from SECURITY checks. A Node.js project benefits from FRONTEND, PROXY, and DEPLOY checks. Any project benefits from DATABASE and WORKFLOW checks when those file types are present. The stack-specific examples (Python routers, SOPS secrets chain, FastAPI layouts, appleboy SSH deploys) are illustrative — the domain detection system applies the applicable subset to any codebase.
54
58
 
@@ -88,14 +92,15 @@ Before running checks, classify the changed files into domains. This avoids runn
88
92
  | `*.py` in `routers/` or `route.ts` handlers | AUTH |
89
93
  | `*.py`, `*.ts`, `*.tsx`, `*.sh` (any code file) | SECURITY (always) |
90
94
  | Files containing `os.getenv` or `process.env` | DEPLOY |
91
- | `*.sql` or files in `migrations/` | DATABASE |
95
+ | `*.sql`, files in `migrations/`, or `*migrations/*.py` (Alembic) | DATABASE |
96
+ | Files under `billing/`, `credits/`, `subscription`, `ledger`, or `payment` paths | BILLING |
92
97
  | `*.tsx`, `*.ts` in `web/src/` (excluding `route.ts`) | FRONTEND |
93
98
  | `*.tsx`, `*.ts` client components with fetch/useSWR | PROXY |
94
99
  | `*.sh` files or scripts with `curl`/`wget` | SHELL |
95
- | `*.py` in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` | STRING_SETS |
100
+ | `*.py` in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` | STRING_SETS *(scraping pack — opt-in, see 2J)* |
96
101
  | `*.py` containing `asyncio.shield`, `asyncio.wait_for`, or `Task.cancel` | CONCURRENCY |
97
102
  | `*.py` files with new module-level `dict`, `set`, `Counter`, `Lock`, or `defaultdict` assignments | STATE |
98
- | `*.py` in `services/worker/`, `infra/`, or `browser/` with new assignments to variables containing `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT` | CAPACITY |
103
+ | `*.py` in `services/worker/`, `infra/`, or `browser/` with new assignments to variables containing `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT` | CAPACITY *(scraping pack — opt-in, see 2K)* |
99
104
  | `*.yml` in `.github/workflows/` | WORKFLOW |
100
105
  | `*.py` with new `from app.` import statements added outside `try:` blocks | IMPORT_RESOLUTION |
101
106
  | `Dockerfile*` or `entrypoint*.sh` with added/changed `USER`, `su-exec`, `gosu`, or `setuid` | INFRA |
@@ -116,7 +121,10 @@ DOMAINS=""
116
121
  IFS=' ' read -ra CHANGED_FILES_ARR <<< "{CHANGED_FILES}"
117
122
  for f in "${CHANGED_FILES_ARR[@]}"; do
118
123
  case "$f" in
119
- *.sql|*migrations/*) DOMAINS="$DOMAINS DATABASE" ;;
124
+ *.sql|*migrations/*|*migrations/*.py) DOMAINS="$DOMAINS DATABASE" ;;
125
+ esac
126
+ case "$f" in
127
+ *billing/*|*credits/*|*subscription*|*ledger*|*payment*) DOMAINS="$DOMAINS BILLING" ;;
120
128
  esac
121
129
  case "$f" in
122
130
  *router*|*route.ts) DOMAINS="$DOMAINS AUTH" ;;
@@ -131,7 +139,12 @@ for f in "${CHANGED_FILES_ARR[@]}"; do
131
139
  esac
132
140
  case "$f" in
133
141
  *anti_detection/*|*consumers/*|*browser/*|*shared/detection/*)
134
- echo "$f" | grep -qE '\.py$' && DOMAINS="$DOMAINS STRING_SETS"
142
+ # STRING_SETS is a scraping-pack domain — opt-in via forge.yaml → quality_gate.optional_domains
143
+ # <!-- Updated: forge#1349 — not enabled by default; scraping-product-specific -->
144
+ if echo "$f" | grep -qE '\.py$'; then
145
+ _OPT_DOMAINS=$(grep -A5 'optional_domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null | grep -oE 'STRING_SETS' | head -1 || true)
146
+ [ -n "$_OPT_DOMAINS" ] && DOMAINS="$DOMAINS STRING_SETS"
147
+ fi
135
148
  ;;
136
149
  esac
137
150
  case "$f" in
@@ -149,10 +162,15 @@ grep -lE "asyncio\.shield|asyncio\.wait_for|Task\.cancel" {CHANGED_FILES} 2>/dev
149
162
  grep -lE "^\w+ = (\{\}|\[\]|set\(\)|Lock\(\)|defaultdict|Counter)" {CHANGED_FILES} 2>/dev/null | grep -qE '\.py$' && DOMAINS="$DOMAINS STATE"
150
163
 
151
164
  # Check for capacity constants in worker/infra/browser Python files
152
- while IFS= read -r f; do
153
- [ -z "$f" ] && continue
154
- git diff HEAD -- "$f" 2>/dev/null | grep -qE '^\+[A-Za-z_]\w*(MB|SIZE|MAX|LIMIT|THRESHOLD|TIMEOUT)\w*\s*=' && DOMAINS="$DOMAINS CAPACITY" && break
155
- done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'services/worker/|infra/|browser/')
165
+ # CAPACITY is a scraping-pack domain — opt-in via forge.yaml → quality_gate.optional_domains
166
+ # <!-- Updated: forge#1349 — not enabled by default; scraping-product-specific -->
167
+ _OPT_CAPACITY=$(grep -A5 'optional_domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null | grep -oE 'CAPACITY' | head -1 || true)
168
+ if [ -n "$_OPT_CAPACITY" ]; then
169
+ while IFS= read -r f; do
170
+ [ -z "$f" ] && continue
171
+ git diff HEAD -- "$f" 2>/dev/null | grep -qE '^\+[A-Za-z_]\w*(MB|SIZE|MAX|LIMIT|THRESHOLD|TIMEOUT)\w*\s*=' && DOMAINS="$DOMAINS CAPACITY" && break
172
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'services/worker/|infra/|browser/')
173
+ fi
156
174
 
157
175
  # Check for env var usage in changed files
158
176
  grep -lE "os\.getenv|process\.env" {CHANGED_FILES} 2>/dev/null && DOMAINS="$DOMAINS DEPLOY"
@@ -226,6 +244,9 @@ Run ONLY the checks whose domain was identified in Step 1.5. Skip checks for dom
226
244
  - **2N (Runtime UID × filesystem write)**: Run if `INFRA` in DOMAINS
227
245
  - **2O (Residual pattern check)**: Run if `ROUTER_BUG` in DOMAINS
228
246
  - **2P (External tool config schema)**: Run if `CONFIG_SCHEMA` in DOMAINS
247
+ - **2Q (Billing)**: Run if `BILLING` in DOMAINS
248
+ - **2R (Registry checks)**: ALWAYS run — executes all promoted checks from `scripts/check-registry/manifest.json` <!-- Added: forge#1331 -->
249
+ - **2S (Test failure classification)**: Run if any Step 2 check invoked a test suite and it failed — classifies the failure as PRE_BROKEN, FLAKY, or REAL using `scripts/flaky-quarantine.sh` <!-- Added: forge#1336 -->
229
250
 
230
251
  ### 2A: Security (ALL files)
231
252
 
@@ -266,14 +287,26 @@ done
266
287
  ### 2B: Auth conventions (Python routers, TypeScript route handlers)
267
288
 
268
289
  For new/modified endpoints:
269
- 1. Which auth dependency is used — `SessionUser` or `CurrentUser`?
270
- 2. Does the route path match? Dashboard routes (`/api/dashboard/*`) must use `SessionUser`. Public API routes (`/api/v1/*`) must use `CurrentUser`.
271
- 3. Does the endpoint check resource ownership (`user_id`) before returning data?
290
+ 1. Which auth dependency is used? Read `forge.yaml → review.domains.auth` for the configured dependency name(s). If absent, skip the naming check and fall back to generic resource-ownership verification only.
291
+ 2. Does the endpoint check resource ownership (`user_id` or equivalent) before returning data?
292
+
293
+ Route-path-to-dependency convention (e.g. "dashboard routes must use X, public routes must use Y") is project-specific. Configure it in `forge.yaml → review.domains.auth` if your project uses one. Do not enforce a hard-coded convention universally. <!-- Updated: forge#1349 — removed hardcoded SessionUser/CurrentUser rule -->
272
294
 
273
295
  ```bash
296
+ # Read configured auth dependency names from forge.yaml (if present)
297
+ AUTH_DEPS=""
298
+ if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
299
+ AUTH_DEPS=$(grep -A5 'domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
300
+ | grep -A3 'auth:' | grep -oE '[A-Za-z_]+User|[A-Za-z_]+Auth|get_[a-z_]+' | sort -u | tr '\n' '|' | sed 's/|$//')
301
+ fi
302
+
274
303
  while IFS= read -r f; do
275
304
  [ -z "$f" ] && continue
276
- grep -nE "CurrentUser|SessionUser|Depends\(get_" "$f" 2>/dev/null
305
+ if [ -n "$AUTH_DEPS" ]; then
306
+ grep -nE "($AUTH_DEPS)|Depends\(get_" "$f" 2>/dev/null
307
+ else
308
+ grep -nE "Depends\(get_" "$f" 2>/dev/null
309
+ fi
277
310
  grep -nE "@router\.(get|post|put|delete)" "$f" 2>/dev/null
278
311
  done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E 'router|route')
279
312
  ```
@@ -286,20 +319,26 @@ if [ -n "$NEW_ENVS" ]; then
286
319
  for var in $NEW_ENVS; do
287
320
  # Check if it exists in .env.example
288
321
  grep -q "$var" .env.example 2>/dev/null || echo "DEPLOY: $var not in .env.example"
289
- # Check if it's in decrypt-secrets.sh ENV_MAPPING
290
- grep -q "$var" scripts/decrypt-secrets.sh 2>/dev/null || echo "DEPLOY: $var not in decrypt-secrets.sh ENV_MAPPING"
322
+ # Check decrypt-secrets.sh ENV_MAPPING only when the SOPS mapping script exists.
323
+ # Emitting a finding for a file that doesn't exist in the repo produces false BLOCKINGs
324
+ # on non-SOPS repos. Use verify-sops-chain.sh for the full SOPS chain check instead.
325
+ # <!-- Updated: forge#1349 — gate on file presence -->
326
+ if [ -f "{WORKTREE_PATH}/scripts/decrypt-secrets.sh" ]; then
327
+ grep -q "$var" "{WORKTREE_PATH}/scripts/decrypt-secrets.sh" 2>/dev/null || echo "DEPLOY: $var not in decrypt-secrets.sh ENV_MAPPING"
328
+ fi
291
329
  done
292
330
  fi
293
331
  ```
294
332
 
295
- ### 2D: Database quality (SQL migration files)
333
+ ### 2D: Database quality (SQL migration files and Alembic Python migrations)
296
334
 
297
- For each `.sql` file:
335
+ For each `.sql` file or Alembic migration `.py` file:
298
336
  1. **NOT NULL without DEFAULT**: `ALTER TABLE ... ADD COLUMN ... NOT NULL` without `DEFAULT` locks table and fails on existing rows
299
337
  2. **DROP without IF EXISTS**: Fails on fresh databases
300
338
  3. **Missing indexes**: New columns used in WHERE/JOIN without indexes
301
339
  4. **Unbounded queries**: SELECT without LIMIT or time-bound WHERE clause
302
340
  5. **Migration number collision**: Check against origin branch
341
+ 6. **IF NOT EXISTS guard**: `CREATE TABLE`/`CREATE INDEX` without `IF NOT EXISTS` guard fails on re-run
303
342
 
304
343
  ```bash
305
344
  while IFS= read -r f; do
@@ -307,30 +346,39 @@ while IFS= read -r f; do
307
346
  grep -nE "ADD COLUMN.*NOT NULL" "$f" | grep -v "DEFAULT" && echo "DB: NOT NULL without DEFAULT in $f"
308
347
  grep -nE "DROP (TABLE|COLUMN|INDEX)" "$f" | grep -v "IF EXISTS" && echo "DB: DROP without IF EXISTS in $f"
309
348
  grep -nE "SELECT.*FROM" "$f" | grep -v "LIMIT" | grep -v "WHERE.*created_at" && echo "DB: possibly unbounded query in $f"
310
- done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep '\.sql$')
349
+ # IF NOT EXISTS guard check — CREATE TABLE/INDEX without guard fails on replay or repeated apply
350
+ grep -nE "^\s*(CREATE TABLE|CREATE INDEX|CREATE UNIQUE INDEX)\b" "$f" | grep -iv "IF NOT EXISTS" && \
351
+ echo "DB-GUARD | MEDIUM | $f | CREATE TABLE/INDEX without IF NOT EXISTS guard — statement will fail on re-run or fresh database replay. Add IF NOT EXISTS."
352
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.sql$')
353
+
354
+ # Also run structural checks on Alembic Python migration files
355
+ while IFS= read -r f; do
356
+ [ -z "$f" ] && continue
357
+ # Check for op.create_table / op.create_index without if_not_exists or checkfirst guard
358
+ grep -nE "\bop\.create_table\b|\bop\.create_index\b" "$f" | grep -iv "if_not_exists\|checkfirst" && \
359
+ echo "DB-GUARD | MEDIUM | $f | op.create_table/op.create_index without if_not_exists/checkfirst guard — will fail on replay. Add if_not_exists=True or checkfirst=True."
360
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E 'migrations/.*\.py$')
311
361
 
312
362
  # Migration prefix collision scan (Deploy Gate — HIGH)
313
- # Runs whenever any SQL file changed. Scans the full infra/migrations/ tree for duplicate
314
- # numeric prefixes not in GRANDFATHERED_DUPLICATES. Duplicates not in the allowlist WILL
315
- # hard-fail deploy-production.yml's validate-migration-order.sh gate.
363
+ # Runs whenever any SQL or Alembic Python migration file changed. Scans the configured
364
+ # migrations directory for duplicate numeric prefixes (both .sql and .py files).
365
+ # Duplicate prefixes can cause ordering conflicts during deploy.
366
+ # <!-- Updated: forge#1349 — removed stale validate-migration-order.sh reference (script does not exist) -->
367
+ # <!-- Updated: forge#1329 — extended collision scan to cover .py Alembic migration files -->
316
368
  MIGRATIONS_DIR="{WORKTREE_PATH}/infra/migrations"
369
+ # Support configured migration directory from forge.yaml (if set)
370
+ if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
371
+ _CONFIGURED_MIGRATIONS=$(grep -E '^\s*migrations_dir:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
372
+ | head -1 | sed 's/.*migrations_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
373
+ [ -n "$_CONFIGURED_MIGRATIONS" ] && MIGRATIONS_DIR="{WORKTREE_PATH}/${_CONFIGURED_MIGRATIONS}"
374
+ fi
317
375
  if [ -d "$MIGRATIONS_DIR" ]; then
318
- # Extract grandfathered prefixes from validate-migration-order.sh if it exists
319
- GRANDFATHER_SCRIPT="{WORKTREE_PATH}/scripts/validate-migration-order.sh"
320
- GRANDFATHERED=""
321
- if [ -f "$GRANDFATHER_SCRIPT" ]; then
322
- GRANDFATHERED=$(grep -oE '[0-9]{4}' "$GRANDFATHER_SCRIPT" | sort -u | tr '\n' ' ')
323
- fi
324
- # Find duplicate numeric prefixes across all migration files
325
- DUPLICATE_PREFIXES=$(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null \
376
+ # Find duplicate numeric prefixes across all migration files (.sql and .py)
377
+ DUPLICATE_PREFIXES=$( { ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null; ls "$MIGRATIONS_DIR"/*.py 2>/dev/null; } \
326
378
  | sed 's|.*/\([0-9]*\)_.*|\1|' \
327
379
  | sort | uniq -d)
328
380
  for prefix in $DUPLICATE_PREFIXES; do
329
- if echo "$GRANDFATHERED" | grep -qw "$prefix"; then
330
- echo "DB: migration prefix $prefix is duplicated but grandfathered (INFO)"
331
- else
332
- echo "DB-COLLISION | HIGH | infra/migrations/ | Duplicate migration prefix $prefix is NOT grandfathered — will hard-fail deploy-production.yml validate-migration-order.sh gate. Classify as CRITICAL BLOCKER."
333
- fi
381
+ echo "DB-COLLISION | HIGH | $MIGRATIONS_DIR | Duplicate migration prefix $prefix — ordering conflict will occur during deploy. Classify as CRITICAL BLOCKER."
334
382
  done
335
383
  fi
336
384
  ```
@@ -393,10 +441,29 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.tsx?$' | grep -v 'route
393
441
 
394
442
  ### 2F: Frontend proxy wiring (client-side fetch calls)
395
443
 
444
+ The direct-backend path prefix to flag is read from `forge.yaml → review.layout.direct_api_prefix`.
445
+ If absent, defaults to `/api/v1/` (backward compatible). This prevents false findings on projects
446
+ that use a different API path scheme. <!-- Updated: forge#1349 — gate on forge.yaml, not hardcoded prefix -->
447
+
396
448
  ```bash
449
+ # Read configured direct API prefix from forge.yaml, fall back to /api/v1/
450
+ DIRECT_API_PREFIX="/api/v1/"
451
+ if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
452
+ _CONFIGURED_PREFIX=$(grep -E '^\s*direct_api_prefix:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
453
+ | head -1 | sed 's/.*direct_api_prefix:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
454
+ # Also check under review.layout section
455
+ if [ -z "$_CONFIGURED_PREFIX" ]; then
456
+ _CONFIGURED_PREFIX=$(grep -A 10 'layout:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
457
+ | grep -E '^\s*direct_api_prefix:' \
458
+ | head -1 | sed 's/.*direct_api_prefix:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
459
+ fi
460
+ [ -n "$_CONFIGURED_PREFIX" ] && DIRECT_API_PREFIX="$_CONFIGURED_PREFIX"
461
+ fi
462
+ ESCAPED_PREFIX=$(printf '%s' "$DIRECT_API_PREFIX" | sed 's|/|\\/|g')
463
+
397
464
  while IFS= read -r f; do
398
465
  [ -z "$f" ] && continue
399
- grep -nE '(fetch|useSWR|apiFetch)\s*[(<]\s*[`"'"'"']/api/v1/' "$f" 2>/dev/null && echo "PROXY: direct /api/v1/ call in $f — must use /api/ proxy"
466
+ grep -nE "(fetch|useSWR|apiFetch)\\s*[(<]\\s*[\`\"']${ESCAPED_PREFIX}" "$f" 2>/dev/null && echo "PROXY: direct ${DIRECT_API_PREFIX} call in $f — must use /api/ proxy (configure review.layout.direct_api_prefix in forge.yaml to change the flagged prefix)"
400
467
  done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.(tsx?|jsx?)$' | grep -v 'route\.ts$')
401
468
  ```
402
469
 
@@ -578,9 +645,11 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
578
645
 
579
646
  ---
580
647
 
581
- ### 2J: String literal consistency (anti-detection/scraping code)
648
+ ### 2J: String literal consistency (anti-detection/scraping code) *(Scraping Pack — opt-in)*
582
649
 
583
- **Triggered when**: changed files include Python files in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` directories.
650
+ > **This domain is opt-in and disabled by default.** It is specific to scraping/anti-detection products and produces false findings on general repos. Enable it by adding `STRING_SETS` to `forge.yaml → quality_gate.optional_domains`. <!-- Added: forge#1349 -->
651
+
652
+ **Triggered when**: changed files include Python files in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` directories AND `STRING_SETS` is in `forge.yaml → quality_gate.optional_domains`.
584
653
 
585
654
  **Why this matters**: Detection keyword sets (e.g., `CHALLENGE_KEYWORDS`, `COMMON_COOKIES`) across related modules share a domain truth — if Akamai adds a new cookie marker, it must appear in ALL related sets. A typo or omission in one set silently breaks detection for that service path. This check catches both inter-file inconsistencies and intra-file comment/code drift.
586
655
 
@@ -670,9 +739,11 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_de
670
739
 
671
740
  ---
672
741
 
673
- ### 2K: Capacity constant validation (worker/infra code)
742
+ ### 2K: Capacity constant validation (worker/infra code) *(Scraping Pack — opt-in)*
743
+
744
+ > **This domain is opt-in and disabled by default.** It is specific to scraping/browser-pool products and produces false findings on general repos. Enable it by adding `CAPACITY` to `forge.yaml → quality_gate.optional_domains`. <!-- Added: forge#1349 -->
674
745
 
675
- **Triggered when**: changed Python files in `services/worker/`, `infra/`, or `browser/` introduce new assignments to variables whose names contain `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT`.
746
+ **Triggered when**: changed Python files in `services/worker/`, `infra/`, or `browser/` introduce new assignments to variables whose names contain `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT` AND `CAPACITY` is in `forge.yaml → quality_gate.optional_domains`.
676
747
 
677
748
  **Why this matters**: Hardcoded capacity constants (e.g., `_BROWSER_ESTIMATED_MB = 200`) are design-time guesses that drift from production reality. When used in guard conditions (e.g., "5 × 200 MB = 1000 MB peak"), a wrong constant directly limits system throughput and may never be caught until a slot fails to spawn. A constant is correctly-typed and correctly-used but factually wrong — logic checks pass, the bug ships.
678
749
 
@@ -956,6 +1027,219 @@ fi
956
1027
 
957
1028
  **Separation of concerns**: ForgeDock reasons about architecture, logic, and security; CI runs deterministic tool validators with zero token cost. The quality gate enforces that the CI layer is complete — it does not replace it.
958
1029
 
1030
+ ### 2Q: Billing domain checks (billing/credits/subscription/ledger/payment code)
1031
+
1032
+ **Triggered when**: BILLING domain is set (changed files are under `billing/`, `credits/`, `subscription`, `ledger`, or `payment` paths).
1033
+
1034
+ **Why this matters**: Billing code has three reliably grep-detectable defect classes that account for ~27% of review findings in the AlterLab health window: (1) `org_id` not propagated to debit/refund ledger write paths (silent data ownership loss — charges appear with wrong tenant); (2) `asyncio.sleep` / blocking sleep called inside an advisory-lock or open-transaction scope (holds DB connection/lock for the full sleep duration, causing connection exhaustion and deadlocks under load); (3) stdlib `logging.*()` calls passed structlog-style keyword arguments (`event=`, named context keys) that are not valid stdlib kwargs — crashes at runtime with `TypeError: unexpected keyword argument`. <!-- Added: forge#1329 --> <!-- allowlist: AlterLab -->
1035
+
1036
+ **2Q-1: org_id propagation in ledger write functions**
1037
+
1038
+ ```bash
1039
+ # Grep billing Python files for ledger write function definitions missing an org_id parameter.
1040
+ # Heuristic: functions named debit/refund/charge/credit/apply_credit/record_usage whose
1041
+ # signature (def line) does not contain org_id are flagged as MEDIUM — reviewer verifies
1042
+ # with full context whether org_id is pulled from a passed object instead.
1043
+ while IFS= read -r f; do
1044
+ [ -z "$f" ] && continue
1045
+ grep -nE "^\s*def (debit|refund|charge|credit|apply_credit|record_usage|create_invoice|issue_credit)\b" "$f" 2>/dev/null | while IFS=: read -r lineno rest; do
1046
+ # Read the full function signature (up to 3 lines to cover multi-line sigs)
1047
+ sig=$(sed -n "${lineno},$((lineno+2))p" "$f" 2>/dev/null)
1048
+ echo "$sig" | grep -qE '\borg_id\b' || \
1049
+ echo "BILLING-1 | MEDIUM | $f:$lineno | Ledger write function '$(echo "$rest" | grep -oE 'def \w+' | head -1)' signature does not include org_id parameter — verify org_id is propagated to all debit/refund paths (AlterLab#25348 pattern)" <!-- allowlist: AlterLab -->
1050
+ done
1051
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
1052
+ ```
1053
+
1054
+ **2Q-2: Blocking sleep inside advisory-lock or open-transaction scope**
1055
+
1056
+ ```bash
1057
+ # Grep billing Python files for asyncio.sleep / time.sleep called inside an advisory-lock
1058
+ # or open-transaction context (within 20 lines after lock acquisition or BEGIN/session open).
1059
+ # Holding a lock or DB connection across a sleep starves the connection pool and causes
1060
+ # deadlocks under concurrent load (AlterLab#25725 pattern). <!-- allowlist: AlterLab -->
1061
+ while IFS= read -r f; do
1062
+ [ -z "$f" ] && continue
1063
+ grep -nE "\basyncio\.sleep\b|\btime\.sleep\b" "$f" 2>/dev/null | while IFS=: read -r lineno rest; do
1064
+ start=$((lineno > 20 ? lineno - 20 : 1))
1065
+ window=$(sed -n "${start},$((lineno-1))p" "$f" 2>/dev/null)
1066
+ echo "$window" | grep -qE 'advisory_lock|BEGIN\b|async with.*session|async with.*conn|async with.*transaction|with.*engine\b' && \
1067
+ echo "BILLING-2 | HIGH | $f:$lineno | asyncio.sleep/time.sleep inside advisory-lock or open-transaction scope — holds DB connection/lock for full sleep duration, causing connection exhaustion and deadlocks under load (AlterLab#25725 pattern). Move sleep outside lock/transaction scope." <!-- allowlist: AlterLab -->
1068
+ done
1069
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
1070
+ ```
1071
+
1072
+ **2Q-3: stdlib logging calls with structlog-style kwargs**
1073
+
1074
+ ```bash
1075
+ # Grep changed Python files for logging.*()/logger.*() calls that pass keyword arguments
1076
+ # that are NOT valid stdlib logging kwargs (exc_info, stack_info, stacklevel, extra).
1077
+ # Structlog-style kwargs (event=, extra_=, named context keys) are NOT accepted by stdlib
1078
+ # Logger methods and crash at runtime with TypeError: unexpected keyword argument.
1079
+ # (AlterLab#25715, #25837, #25819 pattern) <!-- allowlist: AlterLab -->
1080
+ while IFS= read -r f; do
1081
+ [ -z "$f" ] && continue
1082
+ # Match logging.info/warning/error/debug/critical and logger.info/etc. calls with
1083
+ # non-stdlib kwargs. Valid stdlib kwargs: exc_info, stack_info, stacklevel, extra.
1084
+ # Flag any named kwarg that is NOT one of those four.
1085
+ grep -nE "\b(logging|logger)\.(info|warning|error|debug|critical|exception)\s*\(.*\b\w+\s*=" "$f" 2>/dev/null | \
1086
+ grep -vE "\b(exc_info|stack_info|stacklevel|extra)\s*=" | \
1087
+ grep -v "^\s*#" && \
1088
+ echo "BILLING-3 | HIGH | $f | stdlib logging call with non-stdlib keyword argument detected — stdlib Logger.info/warning/error/etc. only accept exc_info, stack_info, stacklevel, extra as kwargs. Structlog-style kwargs (event=, named context keys) cause TypeError at runtime (AlterLab#25715 pattern). Use structlog.get_logger() instead of logging if structlog kwargs are needed." <!-- allowlist: AlterLab -->
1089
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
1090
+ ```
1091
+
1092
+ **Flag as findings**:
1093
+ - Ledger write function missing `org_id` parameter: **MEDIUM** — heuristic; reviewer verifies whether org_id is obtained from a passed object. Silent data ownership loss if omitted.
1094
+ - `asyncio.sleep`/`time.sleep` inside advisory-lock or transaction scope: **HIGH** — guaranteed connection/lock starvation under concurrent load.
1095
+ - stdlib `logging.*()` with non-stdlib kwargs: **HIGH** — guaranteed `TypeError` crash at runtime when the log call is reached.
1096
+
1097
+ **Note**: 2Q-3 (stdlib logging check) fires on ALL changed Python files regardless of path — stdlib logger misuse is not specific to billing paths and is a universal crash class. 2Q-1 and 2Q-2 fire only on billing-domain files.
1098
+
1099
+ ### 2R: Registry checks (ALWAYS — promoted patterns from recurring review findings)
1100
+
1101
+ <!-- Added: forge#1331 -->
1102
+
1103
+ **Purpose**: Run all checks that have been promoted from recurring review findings into the registry. These are deterministic scripts that catch in milliseconds what previously required a full review cycle to detect. Each check was generated once by an LLM after the same defect class appeared 3+ times — thereafter it runs forever at zero LLM cost.
1104
+
1105
+ **Registry location**: `scripts/check-registry/manifest.json` (relative to repo root)
1106
+
1107
+ ```bash
1108
+ REGISTRY_FILE="{WORKTREE_PATH}/scripts/check-registry/manifest.json"
1109
+ REGISTRY_FINDINGS=""
1110
+
1111
+ if [ ! -f "$REGISTRY_FILE" ]; then
1112
+ echo "2R: No check registry found at $REGISTRY_FILE — skipping registry checks."
1113
+ else
1114
+ # Parse manifest and run each registered check
1115
+ CHECK_COUNT=$(jq '.checks | length' "$REGISTRY_FILE" 2>/dev/null || echo 0)
1116
+
1117
+ if [ "$CHECK_COUNT" -eq 0 ]; then
1118
+ echo "2R: Registry is empty — no promoted checks to run."
1119
+ else
1120
+ echo "2R: Running $CHECK_COUNT registered check(s)..."
1121
+ CHECK_INDEX=0
1122
+ while [ "$CHECK_INDEX" -lt "$CHECK_COUNT" ]; do
1123
+ SCRIPT=$(jq -r ".checks[$CHECK_INDEX].script" "$REGISTRY_FILE" 2>/dev/null)
1124
+ SLUG=$(jq -r ".checks[$CHECK_INDEX].slug" "$REGISTRY_FILE" 2>/dev/null)
1125
+ SEVERITY=$(jq -r ".checks[$CHECK_INDEX].severity" "$REGISTRY_FILE" 2>/dev/null)
1126
+
1127
+ if [ -z "$SCRIPT" ] || [ "$SCRIPT" = "null" ]; then
1128
+ CHECK_INDEX=$((CHECK_INDEX + 1))
1129
+ continue
1130
+ fi
1131
+
1132
+ SCRIPT_PATH="{WORKTREE_PATH}/$SCRIPT"
1133
+
1134
+ if [ ! -x "$SCRIPT_PATH" ]; then
1135
+ echo "2R: WARN — registry check $SLUG script not found or not executable at $SCRIPT_PATH"
1136
+ CHECK_INDEX=$((CHECK_INDEX + 1))
1137
+ continue
1138
+ fi
1139
+
1140
+ # Run the check; pass changed files list and worktree path
1141
+ CHECK_OUTPUT=$("$SCRIPT_PATH" "{CHANGED_FILES}" "{WORKTREE_PATH}" 2>/dev/null)
1142
+ CHECK_EXIT=$?
1143
+
1144
+ if [ "$CHECK_EXIT" -eq 1 ]; then
1145
+ # Check fired — append to findings
1146
+ while IFS= read -r line; do
1147
+ [ -z "$line" ] && continue
1148
+ REGISTRY_FINDINGS="${REGISTRY_FINDINGS}
1149
+ ${line}"
1150
+ done <<< "$CHECK_OUTPUT"
1151
+ echo "2R: FAIL — $SLUG matched ($SEVERITY)"
1152
+ elif [ "$CHECK_EXIT" -eq 0 ] || [ "$CHECK_EXIT" -eq 2 ]; then
1153
+ echo "2R: PASS — $SLUG"
1154
+ else
1155
+ echo "2R: WARN — $SLUG exited with unexpected code $CHECK_EXIT"
1156
+ fi
1157
+
1158
+ CHECK_INDEX=$((CHECK_INDEX + 1))
1159
+ done
1160
+ fi
1161
+ fi
1162
+ ```
1163
+
1164
+ If any registry check produces findings, include them in the Step 3 findings list under the check's slug (e.g. `REGISTRY-migration-number-collision | HIGH | ...`). HIGH-severity registry findings are blocking; MEDIUM are advisory.
1165
+
1166
+ ---
1167
+
1168
+ ## Step 2S: Test failure classification (run when tests fail in Step 2)
1169
+
1170
+ <!-- Added: forge#1336 -->
1171
+
1172
+ **Triggered when**: any domain check in Step 2 invokes a test suite command and that command exits non-zero.
1173
+
1174
+ **Why this matters**: Builders and reviewers waste cycles on test failures that pre-exist the PR or are intermittently caused by fixture-state races. A test that fails on both the PR branch and the base branch is *pre-broken* and must not block an unrelated PR. A test that fails then passes on retry is *flaky* and should be quarantined, not "fixed" by the builder.
1175
+
1176
+ ### Classification protocol
1177
+
1178
+ For each failing test ID (or test command) surfaced in Step 2:
1179
+
1180
+ ```bash
1181
+ # PR_BASE is the target branch (e.g. "staging", "main", or "milestone/slug")
1182
+ # FAILING_TEST is the individual test identifier or re-runnable command
1183
+ # GH_REPO is owner/repo from forge.yaml (used to file the quarantine issue)
1184
+ # PR_ISSUE_NUMBER is the issue this quality gate was invoked from (optional)
1185
+
1186
+ CLASSIFIER="${FORGEDOCK_SCRIPTS:-scripts}/flaky-quarantine.sh"
1187
+ [ -f "$CLASSIFIER" ] || CLASSIFIER="{WORKTREE_PATH}/scripts/flaky-quarantine.sh"
1188
+
1189
+ if [ -f "$CLASSIFIER" ]; then
1190
+ RESULT=$(bash "$CLASSIFIER" \
1191
+ --test "{FAILING_TEST}" \
1192
+ --base "{PR_BASE}" \
1193
+ --worktree "{WORKTREE_PATH}" \
1194
+ --repo "{GH_REPO}" \
1195
+ --issue "{PR_ISSUE_NUMBER}" \
1196
+ --retries 3 \
1197
+ 2>&1)
1198
+ echo "$RESULT"
1199
+ CLASSIFICATION=$(echo "$RESULT" | grep '^CLASSIFICATION:' | awk '{print $2}')
1200
+ else
1201
+ # Script not present — fall back to treating all test failures as real.
1202
+ CLASSIFICATION="REAL"
1203
+ echo "NOTE: scripts/flaky-quarantine.sh not found — treating test failure as REAL (no classification)"
1204
+ fi
1205
+ ```
1206
+
1207
+ ### Classification outcomes
1208
+
1209
+ | Classification | Meaning | Gate behaviour |
1210
+ |---|---|---|
1211
+ | `PRE_BROKEN` | Fails on base branch — broken before this PR | **Does not block the gate.** Log as advisory. Quarantine manifest entry written; issue filed once. |
1212
+ | `FLAKY` | Intermittent — fails then passes on retry | **Does not block the gate.** Log as advisory. Quarantine manifest entry written; issue filed once. |
1213
+ | `REAL` | Deterministic on PR, passes on base | **Blocks the gate** — treated as a regression caused by this PR. |
1214
+
1215
+ ### Gate behaviour
1216
+
1217
+ ```
1218
+ if CLASSIFICATION == "PRE_BROKEN" or CLASSIFICATION == "FLAKY":
1219
+ # Test does not block the gate — record in findings as advisory only.
1220
+ # Emit a LOW finding so the builder and reviewer see it in the report.
1221
+ emit: "TEST-QUARANTINE | LOW | {FAILING_TEST} | classified {CLASSIFICATION} — quarantine entry recorded; does not block this PR"
1222
+ # Do NOT re-run the test or count it as a gate failure.
1223
+
1224
+ elif CLASSIFICATION == "REAL":
1225
+ # Test failure is caused by this PR — block the gate as before.
1226
+ emit: "TEST-REAL | HIGH | {FAILING_TEST} | deterministic failure on PR branch; passes on base — this PR introduced the regression"
1227
+ ```
1228
+
1229
+ ### Quarantine manifest
1230
+
1231
+ The manifest lives at `.forgedock/quarantine.jsonl` in the project root (or at the path set in `FORGEDOCK_QUARANTINE_MANIFEST`). Each line is a JSON object:
1232
+
1233
+ ```json
1234
+ {"test":"<id>","classification":"PRE_BROKEN|FLAKY","base":"<branch>","pr_branch":"<branch>","issue":"<num>","repo":"<owner/repo>","first_seen":"<ISO-8601>","runs_pr":3,"failures_pr":3,"runs_base":1,"failures_base":1}
1235
+ ```
1236
+
1237
+ The manifest is append-only. Duplicate entries (same test already quarantined) are skipped. A corresponding GitHub issue is filed automatically when the script has `--repo` and `gh` is authenticated.
1238
+
1239
+ **Reviewers**: when the manifest exists, read it during Phase 2 of `review-pr.md` to avoid re-filing known-quarantined tests as new findings.
1240
+
1241
+ ---
1242
+
959
1243
  ## Step 3: Compile findings
960
1244
 
961
1245
  Format findings as a structured list:
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Read-only playback of a completed pipeline run — shows FORGE annotations phase by phase with timestamps
3
3
  argument-hint: <issue-number>
4
+ install: extras
4
5
  ---
5
6
 
6
7
  # /replay — Pipeline Run Playback
@@ -9,7 +10,7 @@ argument-hint: <issue-number>
9
10
 
10
11
  Replay a completed pipeline run. Fetches all FORGE annotations posted during the pipeline and displays them chronologically — phase headings, timestamps, and key structured fields. Read-only: nothing is written, no labels changed.
11
12
 
12
- **Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`.
13
+ **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
13
14
 
14
15
  ---
15
16