forgedock 1.1.1 → 1.1.3

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 (174) hide show
  1. package/README.md +53 -8
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/demo.mjs +92 -7
  4. package/bin/engine/phases.mjs +24 -2
  5. package/bin/engine/state.mjs +8 -3
  6. package/bin/engine-cli.mjs +177 -5
  7. package/bin/forgedock.mjs +409 -26
  8. package/bin/hooks/interactive-engine.mjs +509 -0
  9. package/bin/hooks/pre-tool-use.mjs +498 -0
  10. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  11. package/bin/journey.mjs +72 -14
  12. package/bin/registry.mjs +139 -0
  13. package/bin/report.mjs +447 -0
  14. package/bin/runner.mjs +70 -20
  15. package/bin/settings-hook.mjs +235 -1
  16. package/commands/adopt.md +4 -1
  17. package/commands/analytics.md +4 -1
  18. package/commands/audit-agents.md +110 -27
  19. package/commands/audit.md +20 -1
  20. package/commands/autopilot.md +518 -361
  21. package/commands/changelog.md +2 -1
  22. package/commands/ci-audit.md +2 -1
  23. package/commands/cleanup.md +161 -1
  24. package/commands/compat-audit.md +2 -1
  25. package/commands/deploy-info.md +58 -1
  26. package/commands/deploy-pr.md +485 -0
  27. package/commands/design-bench.md +5 -1
  28. package/commands/design-render-critique-loop.md +1 -1
  29. package/commands/diagnose.md +2 -1
  30. package/commands/explain.md +2 -1
  31. package/commands/fix-ci.md +577 -0
  32. package/commands/forge-stats.md +121 -2
  33. package/commands/forgedock-init.md +1 -1
  34. package/commands/geo-audit.md +3 -0
  35. package/commands/incident-response.md +2 -1
  36. package/commands/issue.md +23 -2
  37. package/commands/milestone.md +77 -12
  38. package/commands/optimize.md +2 -1
  39. package/commands/orchestrate/config.md +67 -0
  40. package/commands/orchestrate/phase-1-resolve.md +159 -0
  41. package/commands/orchestrate/phase-2-triage.md +126 -0
  42. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  43. package/commands/orchestrate/phase-3-dependency.md +591 -0
  44. package/commands/orchestrate/phase-4-execution.md +962 -0
  45. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  46. package/commands/orchestrate/phase-6-report.md +198 -0
  47. package/commands/orchestrate/safety.md +62 -0
  48. package/commands/orchestrate.md +25 -2045
  49. package/commands/pipeline-health/config.md +34 -0
  50. package/commands/pipeline-health/phase-1-context.md +180 -0
  51. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  52. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  53. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  54. package/commands/pipeline-health/phase-5-report.md +284 -0
  55. package/commands/pipeline-health/phase-6-summary.md +52 -0
  56. package/commands/pipeline-health.md +25 -2401
  57. package/commands/pipeline-resume.md +24 -1
  58. package/commands/pipeline-status.md +65 -0
  59. package/commands/qa-sweep.md +4 -1
  60. package/commands/quality-gate.md +248 -10
  61. package/commands/recover-orphans.md +576 -0
  62. package/commands/replay.md +2 -1
  63. package/commands/review-pr-agents/api.md +135 -0
  64. package/commands/review-pr-agents/auth.md +137 -0
  65. package/commands/review-pr-agents/billing.md +101 -0
  66. package/commands/review-pr-agents/concurrency.md +145 -0
  67. package/commands/review-pr-agents/database.md +103 -0
  68. package/commands/review-pr-agents/frontend.md +89 -0
  69. package/commands/review-pr-agents/infra.md +385 -0
  70. package/commands/review-pr-agents/protocols.md +185 -0
  71. package/commands/review-pr-agents/scraper.md +185 -0
  72. package/commands/review-pr-agents/security.md +406 -0
  73. package/commands/review-pr-agents.md +45 -1722
  74. package/commands/review-pr-staging.md +196 -40
  75. package/commands/review-pr.md +591 -52
  76. package/commands/rollback.md +2 -1
  77. package/commands/scope.md +2 -1
  78. package/commands/security-audit.md +2 -1
  79. package/commands/signal-planner.md +623 -0
  80. package/commands/sync-ecosystem.md +1 -0
  81. package/commands/test-gate.md +17 -1
  82. package/commands/validate.md +2 -1
  83. package/commands/work-on/build/architect.md +28 -0
  84. package/commands/work-on/build/context.md +2 -0
  85. package/commands/work-on/build/implement.md +3 -1
  86. package/commands/work-on/build/validate.md +26 -7
  87. package/commands/work-on/build.md +163 -24
  88. package/commands/work-on/close.md +130 -3
  89. package/commands/work-on/decompose.md +19 -1
  90. package/commands/work-on/investigate.md +117 -2
  91. package/commands/work-on/review.md +24 -4
  92. package/commands/work-on-monolithic.md +7 -2
  93. package/commands/work-on.md +202 -4
  94. package/docs/BRAND.md +14 -7
  95. package/docs/CI.md +16 -2
  96. package/docs/CODEX.md +36 -1
  97. package/docs/CONFIG.md +5 -8
  98. package/docs/FORGE-PROTOCOL.md +15 -1064
  99. package/docs/anthropic-devrel-outreach.md +2 -2
  100. package/docs/articles/command-decomposition-token-savings.md +214 -0
  101. package/docs/articles/model-release-playbook.md +131 -0
  102. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  103. package/docs/claude-breakpoints.json +75 -5
  104. package/docs/design/abc-benchmark.md +1 -1
  105. package/docs/design/design-spec-schema.md +1 -1
  106. package/docs/design/divergent-generation.md +1 -1
  107. package/docs/design/eval-gate-runbook.md +178 -0
  108. package/docs/design/show-hn-draft.md +26 -1
  109. package/docs/design/token-cost-baseline-1298.md +59 -0
  110. package/docs/eval/README.md +120 -0
  111. package/docs/eval/corpus/seed-v1.json +6 -0
  112. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  113. package/docs/site/annotations-explained.md +24 -0
  114. package/docs/site/command-learning-path.md +51 -14
  115. package/docs/site/command-reference.md +386 -111
  116. package/docs/site/for-companies.md +95 -0
  117. package/docs/site/forge-annotation-protocol.md +12 -256
  118. package/docs/site/getting-started.md +43 -4
  119. package/docs/site/github-is-the-memory.md +113 -0
  120. package/docs/site/how-it-works.md +3 -13
  121. package/docs/site/index.md +9 -1
  122. package/docs/spec/eval-run-result.md +147 -0
  123. package/docs/spec/forge-protocol-v1.md +441 -18
  124. package/docs/spec/label-state-machine.md +55 -0
  125. package/docs/spec/review-protocol.md +158 -0
  126. package/docs/spec-graph-schema.md +28 -0
  127. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  128. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  129. package/examples/forgedock-demo/README.md +41 -22
  130. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  131. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  132. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  133. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  134. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  135. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  136. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  137. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  138. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  139. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  140. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  141. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  142. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  143. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  144. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  145. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  146. package/examples/forgedock-demo/src/db.js +36 -11
  147. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  148. package/examples/forgedock-demo/src/server.js +4 -1
  149. package/package.json +3 -1
  150. package/packages/protocol/LICENSE +21 -0
  151. package/packages/protocol/src/cli.js +135 -0
  152. package/packages/protocol/src/emit.js +146 -0
  153. package/packages/protocol/src/index.js +43 -0
  154. package/packages/protocol/src/parse.js +254 -0
  155. package/packages/protocol/src/types.js +171 -0
  156. package/packages/protocol/src/validate.js +122 -0
  157. package/scripts/bench-topology-cost.mjs +372 -0
  158. package/scripts/check-command-docs-drift.sh +198 -0
  159. package/scripts/check-protocol-restatements.sh +176 -0
  160. package/scripts/check-registry/README.md +72 -0
  161. package/scripts/check-registry/manifest.json +6 -0
  162. package/scripts/code-index.sh +454 -0
  163. package/scripts/conformance-check.mjs +168 -0
  164. package/scripts/doctor-pipeline-state.sh +484 -0
  165. package/scripts/eval-gate-baseline.json +17 -0
  166. package/scripts/eval-gate-scorecard.mjs +242 -0
  167. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  168. package/scripts/flaky-quarantine.sh +241 -0
  169. package/scripts/forge-annotation.sh +567 -0
  170. package/scripts/issue-dedup.sh +236 -0
  171. package/scripts/validate-annotation-node.mjs +233 -0
  172. package/scripts/worktree-lifecycle.sh +3 -3
  173. package/templates/devdocs/agent/using-forgedock.md +14 -0
  174. package/docs/site/.vitepress/config.mjs +0 -85
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Recover full pipeline context after compaction — find the last active issue, reconstruct FORGE annotations, and resume work
3
3
  argument-hint: [issue number (optional — auto-detects if omitted)]
4
+ install: extras
4
5
  ---
5
6
 
6
7
  # /pipeline-resume — Context Recovery After Compaction
@@ -9,8 +10,11 @@ argument-hint: [issue number (optional — auto-detects if omitted)]
9
10
 
10
11
  You are the context-recovery agent. When a session ends or context is compressed, all working state is lost. Your job is to restore it instantly — find the most recently active issue, read all FORGE annotations, reconstruct the current pipeline phase, and re-enter the pipeline exactly where it left off.
11
12
 
12
- **Agent model policy**: Default `model: "sonnet"`. Fallback: `model: "opus"` if Sonnet is rate-limited.
13
+ **Agent model policy**: `model: "haiku"`, `effort: low` (mechanical tier — context recovery, annotation reading, state reconstruction). Fallback: `model: "sonnet"` if rate-limited. Feature gate: pass `effort` only on Claude Code >= 2.1.154.
13
14
  **NEVER use plan mode (EnterPlanMode).**
15
+ **NEVER use the Agent tool** — pipeline-resume re-enters the pipeline via `Skill(skill="work-on", ...)` only. Using the Agent tool would restart the pipeline in an untracked subprocess rather than resuming the existing tracked session.
16
+
17
+ <!-- FORGE:SPEC_LOADED — pipeline-resume.md loaded and active. Agent is bound by this spec. -->
14
18
 
15
19
  ---
16
20
 
@@ -36,6 +40,25 @@ If `forge.yaml` is missing: stop and tell the user to run `npx forgedock init` t
36
40
 
37
41
  Check `$ARGUMENTS`:
38
42
 
43
+ - **`--all-stalled`** (fleet recovery mode): shell out to `npx forgedock resume-stalled` to scan all in-flight issues for expired leases and re-dispatch them. This delegates to the durable engine's `scanStalls` for ground-truth stall detection. Add `--dry-run` if only a report is needed. Then STOP — fleet recovery is complete.
44
+
45
+ ```bash
46
+ # Fleet recovery mode — delegate to the engine's resume-stalled CLI
47
+ if echo "$ARGUMENTS" | grep -q -- "--all-stalled"; then
48
+ DRY_RUN_FLAG=""
49
+ if echo "$ARGUMENTS" | grep -q -- "--dry-run"; then
50
+ DRY_RUN_FLAG="--dry-run"
51
+ fi
52
+ echo "Fleet stall recovery: scanning all in-flight issues for expired leases…"
53
+ npx forgedock resume-stalled $DRY_RUN_FLAG
54
+ echo ""
55
+ echo "Fleet recovery complete. To resume a single issue: /pipeline-resume <issue-number>"
56
+ # STOP — fleet recovery does not fall through to single-issue resume.
57
+ fi
58
+ ```
59
+
60
+ If `--all-stalled` was not specified, continue with single-issue resume:
61
+
39
62
  - **Explicit issue number** (e.g., `610`, `#610`): strip `#`, set `TARGET_NUMBER={number}`, skip to Phase 2.
40
63
  - **Empty**: proceed with auto-detection below.
41
64
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Pipeline-wide situational awareness — groups open issues by workflow state, shows active PRs, flags stale items, and reports milestone progress
3
3
  argument-hint: [--repo <owner/repo> | --stale-days <N>]
4
+ install: extras
4
5
  ---
5
6
 
6
7
  # /pipeline-status — Pipeline Status
@@ -184,6 +185,70 @@ echo ""
184
185
 
185
186
  ---
186
187
 
188
+ ## Phase 3B: Engine Lease State (FORGE:STATE)
189
+
190
+ For issues managed by the durable engine, read the `FORGE:STATE` block from each issue body and display lease status. This gives ground-truth stall detection beyond `updatedAt` heuristics. Gracefully degrades — issues without a `FORGE:STATE` block are skipped (they use the Phase 3 timestamp-based detection above).
191
+
192
+ ```bash
193
+ echo "=== Engine Lease State ==="
194
+ echo ""
195
+
196
+ # gh --label uses AND filtering, so query each label separately and merge
197
+ ENGINE_CANDIDATES=$(
198
+ for LABEL in "workflow:investigating" "workflow:ready-to-build" "workflow:building" "workflow:in-review"; do
199
+ gh issue list $GH_FLAG \
200
+ --state open \
201
+ --label "$LABEL" \
202
+ --limit 100 \
203
+ --json number,title,body 2>/dev/null || echo "[]"
204
+ done | jq -s 'flatten | unique_by(.number)'
205
+ )
206
+
207
+ NOW_MS=$(date +%s)000
208
+ ENGINE_OUTPUT=""
209
+
210
+ while IFS= read -r ROW; do
211
+ NUM=$(echo "$ROW" | jq -r '.number')
212
+ TITLE=$(echo "$ROW" | jq -r '.title[:55]')
213
+ BODY=$(echo "$ROW" | jq -r '.body // ""')
214
+
215
+ # Extract FORGE:STATE JSON block from issue body
216
+ STATE_JSON=$(echo "$BODY" | sed -n '/<!-- FORGE:STATE -->/,/<!-- \/FORGE:STATE -->/p' \
217
+ | grep -v '<!-- ' | tr -d '\n' 2>/dev/null || echo "")
218
+
219
+ if [ -z "$STATE_JSON" ]; then
220
+ continue # No engine state — skip, Phase 3 covers this issue via updatedAt
221
+ fi
222
+
223
+ TERMINAL=$(echo "$STATE_JSON" | jq -r '.terminal // false' 2>/dev/null || echo "false")
224
+ if [ "$TERMINAL" = "true" ]; then
225
+ continue # Terminal — not interesting for stall detection
226
+ fi
227
+
228
+ LEASE_UNTIL=$(echo "$STATE_JSON" | jq -r '.lease.until // 0' 2>/dev/null || echo "0")
229
+ LEASE_BY=$(echo "$STATE_JSON" | jq -r '.lease.by // "unknown"' 2>/dev/null || echo "unknown")
230
+ PHASE=$(echo "$STATE_JSON" | jq -r '.phase // "unknown"' 2>/dev/null || echo "unknown")
231
+
232
+ if [ "$LEASE_UNTIL" -gt "$NOW_MS" ] 2>/dev/null; then
233
+ REMAINING=$(( (LEASE_UNTIL - NOW_MS) / 60000 ))
234
+ ENGINE_OUTPUT="${ENGINE_OUTPUT} #${NUM} ${TITLE} [${PHASE}] LEASED (${LEASE_BY}, ${REMAINING}m left)\n"
235
+ else
236
+ EXPIRED_AGO=$(( (NOW_MS - LEASE_UNTIL) / 60000 ))
237
+ ENGINE_OUTPUT="${ENGINE_OUTPUT} #${NUM} ${TITLE} [${PHASE}] STALLED (lease expired ${EXPIRED_AGO}m ago)\n"
238
+ fi
239
+ done <<< "$(echo "$ENGINE_CANDIDATES" | jq -c '.[]')"
240
+
241
+ if [ -z "$ENGINE_OUTPUT" ]; then
242
+ echo " No engine-managed issues in flight (or none with FORGE:STATE blocks)."
243
+ else
244
+ echo -e "$ENGINE_OUTPUT"
245
+ fi
246
+
247
+ echo ""
248
+ ```
249
+
250
+ ---
251
+
187
252
  ## Phase 4: Milestone Progress
188
253
 
189
254
  Show open milestones with open/closed issue counts and percentage complete.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Full platform QA sweep — auto-discovers every page, tests every UI element via browser automation, creates GitHub issues for all findings
3
3
  argument-hint: [all | dashboard | marketing | page <route> | journey | visual | functional | a11y]
4
+ install: extras
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -11,9 +12,11 @@ argument-hint: [all | dashboard | marketing | page <route> | journey | visual |
11
12
 
12
13
  You are the QA orchestrator. Auto-discover every page across the platform (dashboard, marketing, blog, auth, docs, pricing), then systematically test every UI element, interaction, workflow, and state transition using browser automation. Create GitHub issues for every finding.
13
14
 
14
- **Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`. User can override with `--model <name>`.
15
+ **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
15
16
  **NEVER use plan mode (EnterPlanMode).**
16
17
 
18
+ <!-- FORGE:SPEC_LOADED — qa-sweep.md loaded and active. Agent is bound by this spec. -->
19
+
17
20
  ---
18
21
 
19
22
  ## Playwright MCP Prerequisite (MANDATORY — check before proceeding)
@@ -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
 
@@ -49,6 +52,7 @@ This quality gate uses **domain detection** to adapt to your project's actual te
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,7 +92,8 @@ 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 |
@@ -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" ;;
@@ -236,6 +244,9 @@ Run ONLY the checks whose domain was identified in Step 1.5. Skip checks for dom
236
244
  - **2N (Runtime UID × filesystem write)**: Run if `INFRA` in DOMAINS
237
245
  - **2O (Residual pattern check)**: Run if `ROUTER_BUG` in DOMAINS
238
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 -->
239
250
 
240
251
  ### 2A: Security (ALL files)
241
252
 
@@ -319,14 +330,15 @@ if [ -n "$NEW_ENVS" ]; then
319
330
  fi
320
331
  ```
321
332
 
322
- ### 2D: Database quality (SQL migration files)
333
+ ### 2D: Database quality (SQL migration files and Alembic Python migrations)
323
334
 
324
- For each `.sql` file:
335
+ For each `.sql` file or Alembic migration `.py` file:
325
336
  1. **NOT NULL without DEFAULT**: `ALTER TABLE ... ADD COLUMN ... NOT NULL` without `DEFAULT` locks table and fails on existing rows
326
337
  2. **DROP without IF EXISTS**: Fails on fresh databases
327
338
  3. **Missing indexes**: New columns used in WHERE/JOIN without indexes
328
339
  4. **Unbounded queries**: SELECT without LIMIT or time-bound WHERE clause
329
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
330
342
 
331
343
  ```bash
332
344
  while IFS= read -r f; do
@@ -334,12 +346,25 @@ while IFS= read -r f; do
334
346
  grep -nE "ADD COLUMN.*NOT NULL" "$f" | grep -v "DEFAULT" && echo "DB: NOT NULL without DEFAULT in $f"
335
347
  grep -nE "DROP (TABLE|COLUMN|INDEX)" "$f" | grep -v "IF EXISTS" && echo "DB: DROP without IF EXISTS in $f"
336
348
  grep -nE "SELECT.*FROM" "$f" | grep -v "LIMIT" | grep -v "WHERE.*created_at" && echo "DB: possibly unbounded query in $f"
337
- 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$')
338
361
 
339
362
  # Migration prefix collision scan (Deploy Gate — HIGH)
340
- # Runs whenever any SQL file changed. Scans the configured migrations directory for
341
- # duplicate numeric prefixes. Duplicate prefixes can cause ordering conflicts during deploy.
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.
342
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 -->
343
368
  MIGRATIONS_DIR="{WORKTREE_PATH}/infra/migrations"
344
369
  # Support configured migration directory from forge.yaml (if set)
345
370
  if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
@@ -348,8 +373,8 @@ if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
348
373
  [ -n "$_CONFIGURED_MIGRATIONS" ] && MIGRATIONS_DIR="{WORKTREE_PATH}/${_CONFIGURED_MIGRATIONS}"
349
374
  fi
350
375
  if [ -d "$MIGRATIONS_DIR" ]; then
351
- # Find duplicate numeric prefixes across all migration files
352
- 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; } \
353
378
  | sed 's|.*/\([0-9]*\)_.*|\1|' \
354
379
  | sort | uniq -d)
355
380
  for prefix in $DUPLICATE_PREFIXES; do
@@ -1002,6 +1027,219 @@ fi
1002
1027
 
1003
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.
1004
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
+
1005
1243
  ## Step 3: Compile findings
1006
1244
 
1007
1245
  Format findings as a structured list: