forgedock 1.0.22 → 1.1.1

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 (46) hide show
  1. package/README.md +217 -218
  2. package/bin/cinema.mjs +320 -0
  3. package/bin/engine/phases.mjs +148 -0
  4. package/bin/engine/projector.mjs +32 -0
  5. package/bin/engine/reconcile.mjs +11 -0
  6. package/bin/engine/runlog.mjs +79 -0
  7. package/bin/engine/state.mjs +27 -0
  8. package/bin/engine-cli.mjs +50 -0
  9. package/bin/engine.mjs +138 -0
  10. package/bin/forgedock.mjs +464 -1491
  11. package/bin/journey.mjs +783 -0
  12. package/bin/runner.mjs +80 -13
  13. package/bin/settings-hook.mjs +112 -0
  14. package/bin/tui.mjs +6 -3
  15. package/commands/autopilot.md +4 -4
  16. package/commands/deploy-info.md +5 -3
  17. package/commands/design-bench.md +1 -0
  18. package/commands/design-render-critique-loop.md +1 -0
  19. package/commands/design.md +1 -0
  20. package/commands/forge-stats.md +1 -0
  21. package/commands/issue.md +1 -1
  22. package/commands/orchestrate.md +27 -9
  23. package/commands/quality-gate.md +80 -34
  24. package/commands/review-pr-agents.md +7 -7
  25. package/commands/review-pr.md +21 -4
  26. package/commands/work-on/build/implement.md +16 -4
  27. package/commands/work-on/build/validate.md +38 -1
  28. package/commands/work-on/build.md +27 -9
  29. package/commands/work-on/close.md +2 -2
  30. package/commands/work-on/review.md +24 -0
  31. package/commands/work-on-monolithic.md +1 -0
  32. package/commands/work-on.md +45 -3
  33. package/docs/BRAND.md +6 -2
  34. package/docs/CONFIG.md +39 -34
  35. package/docs/site/.vitepress/config.mjs +85 -0
  36. package/docs/site/getting-started.md +15 -8
  37. package/docs/site/index.md +1 -1
  38. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  39. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  40. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  41. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  42. package/package.json +10 -4
  43. package/scripts/verify-env-vars.sh +18 -2
  44. package/scripts/verify-host-headers.sh +4 -4
  45. package/scripts/verify-route-registration.sh +2 -2
  46. package/scripts/worktree-lifecycle.sh +202 -0
@@ -39,10 +39,10 @@ This quality gate uses **domain detection** to adapt to your project's actual te
39
39
  | FRONTEND | `.tsx`/`.ts` files in client component paths |
40
40
  | PROXY | Client components using `fetch`/`useSWR`/`apiFetch` |
41
41
  | SHELL | `.sh` files or scripts using `curl`/`wget` |
42
- | STRING_SETS | Python files in anti-detection or browser automation paths |
42
+ | 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
43
  | CONCURRENCY | Python files using `asyncio.shield`, `asyncio.wait_for`, or `Task.cancel` |
44
44
  | STATE | Python files with new module-level mutable variable assignments |
45
- | CAPACITY | Worker/infra Python files with new size/limit/threshold constants |
45
+ | 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
46
  | WORKFLOW | `.yml` files in `.github/workflows/` |
47
47
  | IMPORT_RESOLUTION | Python files with new `from app.*` imports added outside `try:` blocks |
48
48
  | INFRA | Dockerfiles or entrypoints with runtime UID changes |
@@ -92,10 +92,10 @@ Before running checks, classify the changed files into domains. This avoids runn
92
92
  | `*.tsx`, `*.ts` in `web/src/` (excluding `route.ts`) | FRONTEND |
93
93
  | `*.tsx`, `*.ts` client components with fetch/useSWR | PROXY |
94
94
  | `*.sh` files or scripts with `curl`/`wget` | SHELL |
95
- | `*.py` in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` | STRING_SETS |
95
+ | `*.py` in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` | STRING_SETS *(scraping pack — opt-in, see 2J)* |
96
96
  | `*.py` containing `asyncio.shield`, `asyncio.wait_for`, or `Task.cancel` | CONCURRENCY |
97
97
  | `*.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 |
98
+ | `*.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
99
  | `*.yml` in `.github/workflows/` | WORKFLOW |
100
100
  | `*.py` with new `from app.` import statements added outside `try:` blocks | IMPORT_RESOLUTION |
101
101
  | `Dockerfile*` or `entrypoint*.sh` with added/changed `USER`, `su-exec`, `gosu`, or `setuid` | INFRA |
@@ -131,7 +131,12 @@ for f in "${CHANGED_FILES_ARR[@]}"; do
131
131
  esac
132
132
  case "$f" in
133
133
  *anti_detection/*|*consumers/*|*browser/*|*shared/detection/*)
134
- echo "$f" | grep -qE '\.py$' && DOMAINS="$DOMAINS STRING_SETS"
134
+ # STRING_SETS is a scraping-pack domain — opt-in via forge.yaml → quality_gate.optional_domains
135
+ # <!-- Updated: forge#1349 — not enabled by default; scraping-product-specific -->
136
+ if echo "$f" | grep -qE '\.py$'; then
137
+ _OPT_DOMAINS=$(grep -A5 'optional_domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null | grep -oE 'STRING_SETS' | head -1 || true)
138
+ [ -n "$_OPT_DOMAINS" ] && DOMAINS="$DOMAINS STRING_SETS"
139
+ fi
135
140
  ;;
136
141
  esac
137
142
  case "$f" in
@@ -149,10 +154,15 @@ grep -lE "asyncio\.shield|asyncio\.wait_for|Task\.cancel" {CHANGED_FILES} 2>/dev
149
154
  grep -lE "^\w+ = (\{\}|\[\]|set\(\)|Lock\(\)|defaultdict|Counter)" {CHANGED_FILES} 2>/dev/null | grep -qE '\.py$' && DOMAINS="$DOMAINS STATE"
150
155
 
151
156
  # 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/')
157
+ # CAPACITY is a scraping-pack domain — opt-in via forge.yaml → quality_gate.optional_domains
158
+ # <!-- Updated: forge#1349 — not enabled by default; scraping-product-specific -->
159
+ _OPT_CAPACITY=$(grep -A5 'optional_domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null | grep -oE 'CAPACITY' | head -1 || true)
160
+ if [ -n "$_OPT_CAPACITY" ]; then
161
+ while IFS= read -r f; do
162
+ [ -z "$f" ] && continue
163
+ 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
164
+ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'services/worker/|infra/|browser/')
165
+ fi
156
166
 
157
167
  # Check for env var usage in changed files
158
168
  grep -lE "os\.getenv|process\.env" {CHANGED_FILES} 2>/dev/null && DOMAINS="$DOMAINS DEPLOY"
@@ -266,14 +276,26 @@ done
266
276
  ### 2B: Auth conventions (Python routers, TypeScript route handlers)
267
277
 
268
278
  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?
279
+ 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.
280
+ 2. Does the endpoint check resource ownership (`user_id` or equivalent) before returning data?
281
+
282
+ 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
283
 
273
284
  ```bash
285
+ # Read configured auth dependency names from forge.yaml (if present)
286
+ AUTH_DEPS=""
287
+ if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
288
+ AUTH_DEPS=$(grep -A5 'domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
289
+ | grep -A3 'auth:' | grep -oE '[A-Za-z_]+User|[A-Za-z_]+Auth|get_[a-z_]+' | sort -u | tr '\n' '|' | sed 's/|$//')
290
+ fi
291
+
274
292
  while IFS= read -r f; do
275
293
  [ -z "$f" ] && continue
276
- grep -nE "CurrentUser|SessionUser|Depends\(get_" "$f" 2>/dev/null
294
+ if [ -n "$AUTH_DEPS" ]; then
295
+ grep -nE "($AUTH_DEPS)|Depends\(get_" "$f" 2>/dev/null
296
+ else
297
+ grep -nE "Depends\(get_" "$f" 2>/dev/null
298
+ fi
277
299
  grep -nE "@router\.(get|post|put|delete)" "$f" 2>/dev/null
278
300
  done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E 'router|route')
279
301
  ```
@@ -286,8 +308,13 @@ if [ -n "$NEW_ENVS" ]; then
286
308
  for var in $NEW_ENVS; do
287
309
  # Check if it exists in .env.example
288
310
  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"
311
+ # Check decrypt-secrets.sh ENV_MAPPING only when the SOPS mapping script exists.
312
+ # Emitting a finding for a file that doesn't exist in the repo produces false BLOCKINGs
313
+ # on non-SOPS repos. Use verify-sops-chain.sh for the full SOPS chain check instead.
314
+ # <!-- Updated: forge#1349 — gate on file presence -->
315
+ if [ -f "{WORKTREE_PATH}/scripts/decrypt-secrets.sh" ]; then
316
+ grep -q "$var" "{WORKTREE_PATH}/scripts/decrypt-secrets.sh" 2>/dev/null || echo "DEPLOY: $var not in decrypt-secrets.sh ENV_MAPPING"
317
+ fi
291
318
  done
292
319
  fi
293
320
  ```
@@ -310,27 +337,23 @@ while IFS= read -r f; do
310
337
  done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep '\.sql$')
311
338
 
312
339
  # 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.
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.
342
+ # <!-- Updated: forge#1349 — removed stale validate-migration-order.sh reference (script does not exist) -->
316
343
  MIGRATIONS_DIR="{WORKTREE_PATH}/infra/migrations"
344
+ # Support configured migration directory from forge.yaml (if set)
345
+ if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
346
+ _CONFIGURED_MIGRATIONS=$(grep -E '^\s*migrations_dir:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
347
+ | head -1 | sed 's/.*migrations_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
348
+ [ -n "$_CONFIGURED_MIGRATIONS" ] && MIGRATIONS_DIR="{WORKTREE_PATH}/${_CONFIGURED_MIGRATIONS}"
349
+ fi
317
350
  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
351
  # Find duplicate numeric prefixes across all migration files
325
352
  DUPLICATE_PREFIXES=$(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null \
326
353
  | sed 's|.*/\([0-9]*\)_.*|\1|' \
327
354
  | sort | uniq -d)
328
355
  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
356
+ echo "DB-COLLISION | HIGH | $MIGRATIONS_DIR | Duplicate migration prefix $prefix — ordering conflict will occur during deploy. Classify as CRITICAL BLOCKER."
334
357
  done
335
358
  fi
336
359
  ```
@@ -393,10 +416,29 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.tsx?$' | grep -v 'route
393
416
 
394
417
  ### 2F: Frontend proxy wiring (client-side fetch calls)
395
418
 
419
+ The direct-backend path prefix to flag is read from `forge.yaml → review.layout.direct_api_prefix`.
420
+ If absent, defaults to `/api/v1/` (backward compatible). This prevents false findings on projects
421
+ that use a different API path scheme. <!-- Updated: forge#1349 — gate on forge.yaml, not hardcoded prefix -->
422
+
396
423
  ```bash
424
+ # Read configured direct API prefix from forge.yaml, fall back to /api/v1/
425
+ DIRECT_API_PREFIX="/api/v1/"
426
+ if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
427
+ _CONFIGURED_PREFIX=$(grep -E '^\s*direct_api_prefix:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
428
+ | head -1 | sed 's/.*direct_api_prefix:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
429
+ # Also check under review.layout section
430
+ if [ -z "$_CONFIGURED_PREFIX" ]; then
431
+ _CONFIGURED_PREFIX=$(grep -A 10 'layout:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
432
+ | grep -E '^\s*direct_api_prefix:' \
433
+ | head -1 | sed 's/.*direct_api_prefix:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
434
+ fi
435
+ [ -n "$_CONFIGURED_PREFIX" ] && DIRECT_API_PREFIX="$_CONFIGURED_PREFIX"
436
+ fi
437
+ ESCAPED_PREFIX=$(printf '%s' "$DIRECT_API_PREFIX" | sed 's|/|\\/|g')
438
+
397
439
  while IFS= read -r f; do
398
440
  [ -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"
441
+ 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
442
  done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.(tsx?|jsx?)$' | grep -v 'route\.ts$')
401
443
  ```
402
444
 
@@ -578,9 +620,11 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
578
620
 
579
621
  ---
580
622
 
581
- ### 2J: String literal consistency (anti-detection/scraping code)
623
+ ### 2J: String literal consistency (anti-detection/scraping code) *(Scraping Pack — opt-in)*
624
+
625
+ > **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 -->
582
626
 
583
- **Triggered when**: changed files include Python files in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` directories.
627
+ **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
628
 
585
629
  **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
630
 
@@ -670,9 +714,11 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_de
670
714
 
671
715
  ---
672
716
 
673
- ### 2K: Capacity constant validation (worker/infra code)
717
+ ### 2K: Capacity constant validation (worker/infra code) *(Scraping Pack — opt-in)*
718
+
719
+ > **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
720
 
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`.
721
+ **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
722
 
677
723
  **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
724
 
@@ -353,7 +353,7 @@ For each hit: read the fallback value.
353
353
  - Is `admin`, `password`, `changeme`, `secret`, `test`, or similar weak string: **CONFIRMED HIGH** — default credential.
354
354
  - Is empty string (`${VAR:-}` or `${VAR:-""}`): **CONFIRMED HIGH** if field controls authentication.
355
355
 
356
- **Required cross-checks when a credential fallback is found**: (1) Is the env var required in `app/env_validation.py`? (2) Is it in `scripts/decrypt-secrets.sh` ENV_MAPPING? (3) Is it marked required in `.env.example`? If ANY missing: the insecure fallback may be active in some environments.
356
+ **Required cross-checks when a credential fallback is found**: (1) Is the env var required in `app/env_validation.py`? (2) Is it in `scripts/decrypt-secrets.sh` ENV_MAPPING (if configured in your SOPS deploy chain — skip if absent)? (3) Is it marked required in `.env.example`? If ANY missing: the insecure fallback may be active in some environments.
357
357
  **Confidence**: `CONFIRMED` — fallback value is objectively verifiable.
358
358
  **Severity**: HIGH — insecure fallback credential is equivalent to a hardcoded credential for any environment missing the env var.
359
359
  **Evidence**: Config files commonly use `${VAR:-placeholder}` patterns during development, where the placeholder is a sample credential or hash. If the env var is absent in any environment (staging, non-production, first deploy before secrets are injected), the placeholder becomes the active credential. Also covered by INFRA agent item 13 (deployment context + ENV_MAPPING cross-check); this item extends coverage to PRs not classified as INFRA domain.
@@ -801,14 +801,14 @@ CRITICAL: This is a billing system. Double-spend = revenue loss.
801
801
  ```
802
802
  If `WHERE reserved_by IS NULL` appears in a SELECT that is followed by a separate UPDATE (not in the same atomic statement), this is a CONFIRMED HIGH finding — two concurrent sessions can both pass the read check before either writes.
803
803
 
804
- 8. **Cross-service flag staleness**: When a discount or pricing flag (`is_byop`, `has_active_subscription`, `byop_discount`) is set by the API layer at job submission and read by the Worker layer at billing time, verify the flag is re-validated at debit time — not trusted from the queued job payload.
804
+ 8. **Cross-service flag staleness**: When a discount or pricing flag (e.g. `has_active_subscription`, a `discount_type` field) is set by the API layer at job submission and read by the Worker layer at billing time, verify the flag is re-validated at debit time — not trusted from the queued job payload.
805
805
 
806
806
  Search for discount flags passed through Redis/job payloads:
807
807
  ```bash
808
- grep -rn "is_byop\|byop_discount\|discount.*flag\|flag.*discount" services/api/app/ services/worker/worker/ | head -20
808
+ grep -rn "discount.*flag\|flag.*discount\|subscription.*flag\|plan_type\|discount_type" services/api/ services/worker/ | head -20
809
809
  # If the flag flows through a job payload and is not re-validated at billing: CONFIRMED HIGH
810
810
  ```
811
- A race window exists when a flag is checked at submission but consumed at billing: the underlying condition (e.g., proxy existence, subscription status) may have changed between the two operations. The fix must re-validate the flag atomically at the point of debit, not rely on a stale value from the job payload.
811
+ A race window exists when a flag is checked at submission but consumed at billing: the underlying condition (e.g., subscription status, entitlement) may have changed between the two operations. The fix must re-validate the flag atomically at the point of debit, not rely on a stale value from the job payload.
812
812
 
813
813
  ## Safe Patterns in This Codebase
814
814
  ```bash
@@ -1317,8 +1317,8 @@ You are auditing PR #[PR_NUMBER] for database changes in [PROJECT_NAME].
1317
1317
  a. List ALL `*.sql` files in `infra/migrations/` on the PR's target branch (use `git ls-tree` or `ls`)
1318
1318
  b. Extract the 4-digit numeric prefix from each filename (the leading digits before the first `_`)
1319
1319
  c. Identify any prefix that appears more than once
1320
- d. Cross-reference against the `GRANDFATHERED_DUPLICATES` list in `scripts/validate-migration-order.sh` — these are known-safe legacy duplicates
1321
- e. **DEPLOY GATE — CRITICAL**: If ANY non-grandfathered duplicate prefix exists → flag as **CRITICAL BLOCKER** and reject the PR. **Do NOT apply migration runner reasoning here.** The deploy gate (`deploy-production.yml`) runs `validate-migration-order.sh` with no `continue-on-error` — it hard-fails on any non-grandfathered duplicate regardless of whether the migration runner executes files correctly. The runner may handle duplicate filenames fine; the deploy script does not. A PR that passes this reasoning trap ("unique filenames, so the runner is safe") will still halt production deploy and waste a 30-minute CI compute cycle. The only safe classification is CRITICAL BLOCKER.
1320
+ d. Identify if the project maintains a grandfathered-duplicates allowlist (e.g., a config file or comment block listing known-safe legacy duplicate prefixes). If one exists, cross-reference against it. <!-- Updated: forge#1349 — removed stale validate-migration-order.sh reference (script does not exist in ForgeDock) -->
1321
+ e. **DEPLOY GATE — CRITICAL**: If ANY non-allowlisted duplicate prefix exists → flag as **CRITICAL BLOCKER** and reject the PR. **Do NOT apply migration runner reasoning here.** Deploy gates that enforce migration ordering hard-fail on any non-allowed duplicate regardless of whether the migration runner executes files correctly. The runner may handle duplicate filenames fine; the deploy script does not. A PR that passes this reasoning trap ("unique filenames, so the runner is safe") will still halt production deploy. The only safe classification is CRITICAL BLOCKER.
1322
1322
  f. Additionally: if the PR adds new migration files, verify their prefixes don't collide with existing files in the directory
1323
1323
  9. **FK and CHECK constraints**: New tables should have appropriate foreign keys and CHECK constraints. Missing FK allows orphaned rows; missing CHECK allows invalid enum values.
1324
1324
  10. **asyncpg gotchas**:
@@ -1588,7 +1588,7 @@ If no infra context is configured above, derive the deployment model from the ch
1588
1588
 
1589
1589
  **Cross-check when a credential fallback is found:**
1590
1590
  1. Is the corresponding env var required (not optional) in `app/env_validation.py` (or equivalent startup validation)?
1591
- 2. Is the env var in `scripts/decrypt-secrets.sh` ENV_MAPPING (delivered from SOPS to `.env.production`)?
1591
+ 2. Is the env var in `scripts/decrypt-secrets.sh` ENV_MAPPING (if configured in your SOPS deploy chain — skip if absent; delivered from SOPS to `.env.production`)?
1592
1592
  3. Is the env var documented with a "REQUIRED" note in `.env.example`?
1593
1593
 
1594
1594
  If ANY of these three are missing: the env var may legitimately be absent in some environments, making the insecure fallback active. Flag as **CONFIRMED HIGH** with all three cross-check results.
@@ -147,7 +147,7 @@ echo "=== DOMAINS ==="
147
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
148
  echo "$DIFF" | grep -cE "credit|balance|debit|reconcil|tier_cost|pricing|charge|refund|stripe|subscription" && echo "BILLING" || true
149
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
150
+ echo "$DIFF" | grep -cE "FOR UPDATE|atomic|transaction|pipeline|MULTI|distributed_lock|acquire_lock|reserved_by|promo.*claim|voucher.*redeem" && echo "CONCURRENCY" || true
151
151
  echo "$FILES" | grep -cE "migration|\.sql$" && echo "DATABASE" || true
152
152
  echo "$DIFF" | grep -cE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" && echo "DB_CONFIG" || true
153
153
  echo "$FILES" | grep -cE "router|routes" && echo "API_DESIGN" || true
@@ -397,7 +397,7 @@ For each changed file, identify its **activation path** — how does execution r
397
397
 
398
398
  Map each changed file to its activation requirements.
399
399
 
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:
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) are the ForgeDock install defaults:
401
401
 
402
402
  | `forge.yaml` key | Default | Used in table as |
403
403
  |-----------------|---------|-----------------|
@@ -461,6 +461,23 @@ if [ "$BASH_AVAILABLE" = "true" ]; then
461
461
  # Output is structured: "BLOCKING: ...", "WARNING: ...", "OK: ..." per line.
462
462
 
463
463
  # 1. Route/router/middleware/shared-module/component registration
464
+ # Export forge.yaml layout overrides so verify-route-registration.sh uses project-configured
465
+ # paths instead of project defaults. The script supports these env vars (lines 36-44 of
466
+ # verify-route-registration.sh) but requires the caller to set them. <!-- Added: forge#1349 -->
467
+ if [ -f "$REPO_ROOT/forge.yaml" ]; then
468
+ _PAGES_ROOT=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
469
+ | grep -E '^\s*pages:' | head -1 | sed 's/.*pages:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
470
+ _API_ROUTERS=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
471
+ | grep -E '^\s*api_routers_dir:' | head -1 | sed 's/.*api_routers_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
472
+ _API_MAIN=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
473
+ | grep -E '^\s*api_main:' | head -1 | sed 's/.*api_main:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
474
+ _API_MIDDLEWARE=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
475
+ | grep -E '^\s*api_middleware_dir:' | head -1 | sed 's/.*api_middleware_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
476
+ [ -n "$_PAGES_ROOT" ] && export FORGE_PAGES_ROOT="$_PAGES_ROOT"
477
+ [ -n "$_API_ROUTERS" ] && export FORGE_API_ROUTERS_DIR="$_API_ROUTERS"
478
+ [ -n "$_API_MAIN" ] && export FORGE_API_MAIN="$_API_MAIN"
479
+ [ -n "$_API_MIDDLEWARE" ] && export FORGE_API_MIDDLEWARE_DIR="$_API_MIDDLEWARE"
480
+ fi
464
481
  echo "=== Running: verify-route-registration.sh ==="
465
482
  bash "$FORGE_HOME/scripts/verify-route-registration.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
466
483
 
@@ -612,7 +629,7 @@ Format: `INTEG-N|CONFIRMED|HIGH|file:line|Changed code may be unreachable: {reas
612
629
  DIFF=$(gh pr diff $ARGUMENTS)
613
630
  FILES=$(gh pr diff $ARGUMENTS --name-only)
614
631
  echo "=== RISK SIGNALS ==="
615
- echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc|EMEMO_EOF|LLM_|llm_" && echo " UNTRUSTED_INPUT_PROCESSING" || true
632
+ echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc" && echo " UNTRUSTED_INPUT_PROCESSING" || true
616
633
  echo "$DIFF" | grep -cE "\.sh$|bash|shell|cron" && echo " SHELL_SCRIPT" || true
617
634
  echo "$DIFF" | grep -cE "credit|balance|debit|charge|refund|stripe" && echo " FINANCIAL" || true
618
635
  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
@@ -1093,7 +1110,7 @@ else
1093
1110
  fi
1094
1111
  ```
1095
1112
 
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.
1113
+ **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
1114
 
1098
1115
  ### 8B: Post-Merge Review Finding Demilestoning (Milestone PRs Only)
1099
1116
 
@@ -53,7 +53,17 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
53
53
  --jq '.[] | select(.body | contains("FORGE:CONTEXT")) | .body'
54
54
  ```
55
55
 
56
- **Resume check**: If `<!-- FORGE:BUILDER -->` comment already exists, the implementation is done — EXIT and return `IMPLEMENT_RESULT: status: ALREADY_DONE` to caller.
56
+ **Resume check**:
57
+ - If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → implementation is done — EXIT and return `IMPLEMENT_RESULT: status: ALREADY_DONE` to caller.
58
+ - If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → implementation was interrupted after the comment was posted but before the commit (validate.md V5). Delete the partial comment and restart from Phase I2:
59
+ ```bash
60
+ PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
61
+ --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
62
+ if [ -n "$PARTIAL_ID" ]; then
63
+ gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
64
+ echo "Deleted partial FORGE:BUILDER comment (no FORGE:BUILDER:COMPLETE) — restarting implementation"
65
+ fi
66
+ ```
57
67
 
58
68
  **Primary guide**: If `<!-- FORGE:ARCHITECT -->` is present, it is the **primary implementation input**. Follow its ordered implementation list exactly — it defines which files change, in what order, and what consistency checks must pass. The investigation report and contract are secondary context.
59
69
 
@@ -225,6 +235,8 @@ gh issue edit {NUMBER} {GH_FLAG} --body "{UPDATED_BODY}"
225
235
 
226
236
  ## Phase I6: Post FORGE:BUILDER Comment
227
237
 
238
+ Post the implementation summary comment. **Do NOT include `<!-- FORGE:BUILDER:COMPLETE -->` here** — that marker is posted by `validate.md` Phase V5 after the commit succeeds. A crash between I6 and V5 would otherwise leave a completion-marked comment with no commit on the branch.
239
+
228
240
  ```bash
229
241
  gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
230
242
  ## Implementation Complete
@@ -247,11 +259,11 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
247
259
  - [ ] {Test scenario 2} [type:unit]
248
260
  - [ ] {Test scenario 3} [type:e2e]
249
261
 
250
- > **Test-type annotation** (optional): Append `[type:api]`, `[type:unit]`, `[type:e2e]`, or `[type:manual]` to each checklist item. The test gate reads this annotation directly and skips regex inference. Omit it to rely on regex classification fallback.
251
-
252
- <!-- FORGE:BUILDER:COMPLETE -->"
262
+ > **Test-type annotation** (optional): Append `[type:api]`, `[type:unit]`, `[type:e2e]`, or `[type:manual]` to each checklist item. The test gate reads this annotation directly and skips regex inference. Omit it to rely on regex classification fallback."
253
263
  ```
254
264
 
265
+ **Note**: `<!-- FORGE:BUILDER:COMPLETE -->` is intentionally absent from this comment. It is appended to this comment by `validate.md` Phase V5 after the git commit completes — see `validate.md § Phase V5`. This ordering ensures the completion marker only exists when a real commit is present on the branch.
266
+
255
267
  ---
256
268
 
257
269
  ## Output
@@ -90,7 +90,12 @@ Needs human review before proceeding to commit.
90
90
 
91
91
  ## Phase V2: Format and Verify
92
92
 
93
- Run after quality gate passes. All tool commands are read from `forge.yaml → verification.commands`; each step logs `SKIPPED — not configured` when the corresponding key is absent rather than silently passing:
93
+ Run after quality gate passes. All tool commands are read from `forge.yaml → verification.commands`; each step logs `SKIPPED — not configured` when the corresponding key is absent rather than silently passing.
94
+
95
+ **Track skipped checks** — initialize before any check runs:
96
+ ```bash
97
+ SKIPPED_CHECKS=""
98
+ ```
94
99
 
95
100
  **Python**:
96
101
  ```bash
@@ -101,6 +106,7 @@ if [ -n "$PYTHON_FORMAT" ]; then
101
106
  eval "$PYTHON_FORMAT" 2>&1
102
107
  else
103
108
  echo "SKIPPED — python.format not configured in verification.commands"
109
+ SKIPPED_CHECKS="${SKIPPED_CHECKS:+$SKIPPED_CHECKS, }python.format"
104
110
  fi
105
111
 
106
112
  # Compile check always runs for Python files (no config needed — catches syntax errors)
@@ -120,6 +126,7 @@ if [ -n "$TS_FORMAT" ]; then
120
126
  eval "$TS_FORMAT" 2>&1
121
127
  else
122
128
  echo "SKIPPED — typescript.format not configured in verification.commands"
129
+ SKIPPED_CHECKS="${SKIPPED_CHECKS:+$SKIPPED_CHECKS, }typescript.format"
123
130
  fi
124
131
 
125
132
  if [ -n "$TS_TYPECHECK" ]; then
@@ -130,6 +137,7 @@ elif [ -n "$TS_BUILD" ]; then
130
137
  TS_EXIT=$?
131
138
  else
132
139
  echo "SKIPPED — typescript.typecheck and typescript.build not configured in verification.commands"
140
+ SKIPPED_CHECKS="${SKIPPED_CHECKS:+$SKIPPED_CHECKS, }typescript.typecheck/build"
133
141
  TS_EXIT=0
134
142
  fi
135
143
  ```
@@ -287,6 +295,32 @@ If `origin/{PR_BASE}` does not exist yet (new branch), skip this check — no co
287
295
  git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1 || echo "PR_BASE not on origin — skipping ancestry audit"
288
296
  ```
289
297
 
298
+ ### V5 Post-Commit: Mark Build Complete (MANDATORY)
299
+
300
+ After the ancestry audit passes (or is skipped), append `<!-- FORGE:BUILDER:COMPLETE -->` to the existing FORGE:BUILDER comment. This is the **only** place this marker is written — it signals that a real commit exists on the branch and the build is safe to resume-skip. <!-- Added: forge#1305 -->
301
+
302
+ ```bash
303
+ # Find the FORGE:BUILDER comment posted by implement.md Phase I6
304
+ BUILDER_COMMENT_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
305
+ --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
306
+
307
+ if [ -n "$BUILDER_COMMENT_ID" ]; then
308
+ # Fetch current body and append the completion marker
309
+ CURRENT_BODY=$(gh api repos/{GH_REPO}/issues/comments/$BUILDER_COMMENT_ID --jq '.body')
310
+ UPDATED_BODY="${CURRENT_BODY}
311
+
312
+ <!-- FORGE:BUILDER:COMPLETE -->"
313
+ gh api repos/{GH_REPO}/issues/comments/$BUILDER_COMMENT_ID \
314
+ -X PATCH \
315
+ --field body="$UPDATED_BODY"
316
+ echo "FORGE:BUILDER:COMPLETE appended to comment $BUILDER_COMMENT_ID"
317
+ else
318
+ echo "WARNING: FORGE:BUILDER comment not found or already marked complete — skipping BUILDER:COMPLETE append"
319
+ fi
320
+ ```
321
+
322
+ **Why here and not in implement.md**: The commit (`git commit`) runs in this phase (V5). Appending `:COMPLETE` after the commit ensures that a session crash between implement.md I6 (comment posted) and this step (commit) leaves a partial BUILDER comment without `:COMPLETE`. The next resume will detect the partial comment, delete it, and restart the build. See `implement.md § Phase I1 resume check`.
323
+
290
324
  ---
291
325
 
292
326
  ## Output
@@ -302,6 +336,9 @@ VALIDATE_RESULT:
302
336
  deploy_completeness_fixes: [{VAR_NAME: location_added}, ...]
303
337
  commits_added: [{SHA}, ...] # from V5 if any
304
338
  blocker: {description if gate_passed=false}
339
+ verification_skipped: [] # empty when all configured checks ran; list of skipped check names otherwise
340
+ # e.g. ["python.format", "typescript.typecheck/build"]
341
+ # populated from SKIPPED_CHECKS in Phase V2
305
342
  ```
306
343
 
307
344
  ---
@@ -9,8 +9,8 @@ argument-hint: [issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BA
9
9
 
10
10
  **Input**: $ARGUMENTS
11
11
 
12
- **Invoked by**: `work-on.md` state 8 (READY_TO_BUILD).
13
- **Output**: Create worktree, post contract, sequence build subcommands, return result to router.
12
+ **Invoked by**: `work-on.md` Phase 3 — entered when the issue carries label `workflow:ready-to-build` or `workflow:building` (see Universal Phase Dispatcher in work-on.md).
13
+ **Output**: Create worktree, post contract, sequence build subcommands, return result to work-on.md.
14
14
 
15
15
  **Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`.
16
16
  **NEVER use plan mode (EnterPlanMode).**
@@ -46,7 +46,16 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
46
46
  ```
47
47
 
48
48
  **Resume check**:
49
- - If `<!-- FORGE:BUILDER -->` comment exists → build already complete. Return `BUILD_RESULT: status: ALREADY_DONE` to router.
49
+ - If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → build already complete. Return `BUILD_RESULT: status: ALREADY_DONE` to router.
50
+ - If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate.md V5). Delete the partial comment and restart from Phase B2 (contract): <!-- Added: forge#1305 -->
51
+ ```bash
52
+ PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
53
+ --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
54
+ if [ -n "$PARTIAL_ID" ]; then
55
+ gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
56
+ echo "Deleted partial FORGE:BUILDER comment (no FORGE:BUILDER:COMPLETE) — restarting build from Phase B2"
57
+ fi
58
+ ```
50
59
  - If no `<!-- FORGE:INVESTIGATOR -->` comment with `<!-- INVESTIGATION:COMPLETE -->` → EXIT with `BUILD_RESULT: status: BLOCKED`, blocker: "Investigation not complete — run investigate first".
51
60
 
52
61
  Extract from investigation report:
@@ -82,6 +91,14 @@ Append `-{NUMBER}` to ensure uniqueness: e.g. `fix/work-on-build-landing-file-85
82
91
 
83
92
  - Review-finding issue → parse `**Code branch**: \`{branch}\`` from issue body; branch from `origin/{branch}`
84
93
  - **Milestone review-finding hybrid lane** (ONLY when Code branch matches `milestone/*`): This is a high-risk lane. The worktree will carry the full milestone history. The PR target is `staging` (or the base specified). **DANGER: Agents MUST NOT use `git merge` to resolve any conflicts in this lane.** Merge-based conflict resolution will pull the entire milestone commit tree onto staging, contaminating it with unapproved code. Use `git rebase` or `git cherry-pick` only. If conflicts cannot be resolved without a merge, post a comment on the issue, add `needs-human`, and STOP.
94
+ - **Missing ref fallback**: After parsing, verify the Code branch still exists on remote. If not, fall back to the lane default (`staging` for fast lane, `milestone/{slug}` for feature lane) and note the fallback:
95
+ ```bash
96
+ SOURCE_BRANCH="{CODE_BRANCH_FROM_ISSUE_BODY}"
97
+ if ! git ls-remote --exit-code origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
98
+ echo "WARNING: Code branch '$SOURCE_BRANCH' not found on remote — falling back to lane default '$PR_BASE'"
99
+ SOURCE_BRANCH="$PR_BASE"
100
+ fi
101
+ ```
85
102
  - Feature lane (has milestone) → branch from `origin/{PR_BASE}`
86
103
  - Fast lane (no milestone) → branch from `origin/staging`
87
104
 
@@ -275,13 +292,14 @@ BUILD_RESULT:
275
292
 
276
293
  ## Integration Point in work-on.md
277
294
 
278
- This module runs at **state 8 (READY_TO_BUILD)** in the routing loop:
295
+ This module runs during **Phase 3** of the work-on.md pipeline (label: `workflow:ready-to-build` or `workflow:building`). The full sequence is defined by the Universal Phase Dispatcher in work-on.md:
279
296
 
280
297
  ```
281
- state 8 → [THIS MODULE] worktree + contract + context + architect + implement + validate
282
- → BUILD_RESULT: COMPLETE
283
- states 7, 6 → work-on:review (push + PR + /review-pr --auto-merge)
284
- states 5, 4 → work-on:close (trajectory + parent tracker + summary)
298
+ Phase 3 (Build) → [THIS MODULE] worktree + contract + context + architect + implement + validate
299
+ → posts FORGE:BUILDER comment, writes FORGE:CHECKPOINT next_phase=REVIEW
300
+ Phase 4 (PR) → work-on:review — push branch, create PR, set workflow:in-review
301
+ Phase 5 (Review) → work-on:review — invoke /review-pr --auto-merge
302
+ Phase 6 (Close) → work-on:close — trajectory + parent tracker + summary + worktree cleanup
285
303
  ```
286
304
 
287
- The router re-reads GitHub state after this module returns COMPLETE — it will then detect state 6 (IN_REVIEW) or 7 (BUILD_NO_PR) and invoke `work-on:review`.
305
+ After this module posts `FORGE:BUILDER` and returns, work-on.md's Universal continuation rule re-reads the issue labels. Since the label is not yet terminal (`workflow:merged` / `workflow:invalid` / `needs-human`), it proceeds immediately to Phase 4 (PR Creation) and then Phase 5 (Auto-Review).
@@ -179,7 +179,7 @@ ISSUE_STATE=$(gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state')
179
179
 
180
180
  **If `REMAINING_AFTER > 0`** (multi-phase: uncompleted phases remain):
181
181
  ```bash
182
- # Post phase-complete marker — router state 10 (MULTI_PHASE) will re-detect this issue
182
+ # Post phase-complete marker — work-on.md's Universal continuation rule will re-read labels and continue to the next phase
183
183
  gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:PHASE:COMPLETE -->
184
184
  Phase complete. PR #{PR_NUMBER} merged to \`{PR_BASE}\`. ${REMAINING_AFTER} phase item(s) remain — leaving issue open for next pipeline iteration."
185
185
 
@@ -577,7 +577,7 @@ CLOSE_RESULT:
577
577
  parent_closed: {true|false}
578
578
  ```
579
579
 
580
- Where `PHASE_COMPLETE` means the current phase was closed but uncompleted phases remain — the issue is left OPEN for the router to re-evaluate on the next pipeline iteration (state 10, MULTI_PHASE).
580
+ Where `PHASE_COMPLETE` means the current phase was closed but uncompleted phases remain — the issue is left OPEN for work-on.md to re-evaluate on the next invocation (via Universal continuation rule: re-read labels, check terminal state, continue to next phase).
581
581
 
582
582
  ---
583
583
 
@@ -81,6 +81,30 @@ Do NOT push this branch. Human review required to identify the source of the mer
81
81
  fi
82
82
  ```
83
83
 
84
+ ## Phase R1: Non-Empty Commit Guard (MANDATORY — run before push) <!-- Added: forge#1305 -->
85
+
86
+ Before pushing, verify the branch has at least one commit ahead of the PR base. This is the last-line defense against the phantom-commit hazard: a session that resumed from a partial FORGE:BUILDER comment (without `:COMPLETE`) would have skipped the commit step and could otherwise push an empty branch.
87
+
88
+ ```bash
89
+ cd {WORKTREE_PATH}
90
+ # Count commits on this branch that are not reachable from origin/{PR_BASE}
91
+ COMMIT_COUNT=$(git rev-list --count HEAD ^origin/{PR_BASE} 2>/dev/null || echo "0")
92
+ if [ "$COMMIT_COUNT" -eq 0 ]; then
93
+ gh issue comment {NUMBER} {GH_FLAG} --body "## Push Blocked — No Commits Ahead of Base
94
+
95
+ Branch \`{BRANCH}\` has 0 commits ahead of \`origin/{PR_BASE}\`. Pushing this branch would create an empty PR.
96
+
97
+ **Likely cause**: Build was interrupted after the FORGE:BUILDER comment was posted (implement.md Phase I6) but before the commit was created (validate.md Phase V5). The branch was pushed with no implementation on it.
98
+
99
+ **Resolution**: Delete this branch, re-run \`/work-on {NUMBER}\` to restart the build phase. The partial FORGE:BUILDER comment (lacking \`FORGE:BUILDER:COMPLETE\`) will be detected and deleted, and the build will restart cleanly.
100
+
101
+ <!-- FORGE:PUSH_BLOCKED_EMPTY_BRANCH -->"
102
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
103
+ exit 1
104
+ fi
105
+ echo "Commit count ahead of origin/{PR_BASE}: $COMMIT_COUNT — OK to push"
106
+ ```
107
+
84
108
  ## Phase R1: Push Branch
85
109
 
86
110
  ```bash
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: "[BENCHMARK] Monolithic work-on — single-prompt pipeline with no Skill sub-phase boundaries"
3
3
  argument-hint: [issue number]
4
+ install: internal
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->