forgedock 1.0.21 → 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 (76) hide show
  1. package/README.md +227 -174
  2. package/bin/cinema.mjs +320 -0
  3. package/bin/demo.mjs +386 -0
  4. package/bin/engine/phases.mjs +148 -0
  5. package/bin/engine/projector.mjs +32 -0
  6. package/bin/engine/reconcile.mjs +11 -0
  7. package/bin/engine/runlog.mjs +79 -0
  8. package/bin/engine/state.mjs +27 -0
  9. package/bin/engine-cli.mjs +50 -0
  10. package/bin/engine.mjs +138 -0
  11. package/bin/forgedock.mjs +693 -1393
  12. package/bin/init-detect.mjs +95 -0
  13. package/bin/journey.mjs +783 -0
  14. package/bin/runner.mjs +847 -0
  15. package/bin/settings-hook.mjs +112 -0
  16. package/bin/tui.mjs +6 -3
  17. package/commands/autopilot.md +4 -4
  18. package/commands/cleanup.md +85 -39
  19. package/commands/deploy-info.md +11 -5
  20. package/commands/design-bench.md +1 -0
  21. package/commands/design-render-critique-loop.md +1 -0
  22. package/commands/design.md +1 -0
  23. package/commands/forge-stats.md +2 -1
  24. package/commands/issue.md +1 -1
  25. package/commands/optimize.md +1 -1
  26. package/commands/orchestrate.md +346 -34
  27. package/commands/quality-gate.md +168 -80
  28. package/commands/review-pr-agents.md +69 -45
  29. package/commands/review-pr.md +54 -6
  30. package/commands/scope.md +11 -3
  31. package/commands/security-audit.md +24 -16
  32. package/commands/sync-ecosystem.md +2 -2
  33. package/commands/work-on/build/architect.md +47 -3
  34. package/commands/work-on/build/context.md +36 -5
  35. package/commands/work-on/build/implement.md +16 -4
  36. package/commands/work-on/build/validate.md +54 -8
  37. package/commands/work-on/build.md +27 -9
  38. package/commands/work-on/close.md +131 -3
  39. package/commands/work-on/investigate.md +24 -1
  40. package/commands/work-on/review.md +24 -0
  41. package/commands/work-on-monolithic.md +1 -0
  42. package/commands/work-on.md +271 -11
  43. package/docs/BRAND.md +6 -2
  44. package/docs/CONFIG.md +39 -34
  45. package/docs/site/.vitepress/config.mjs +85 -0
  46. package/docs/site/annotations-explained.md +55 -0
  47. package/docs/site/command-learning-path.md +104 -0
  48. package/docs/site/command-reference.md +3 -0
  49. package/docs/site/getting-started.md +42 -13
  50. package/docs/site/index.md +1 -1
  51. package/docs/site/troubleshooting.md +208 -0
  52. package/docs/spec/forge-protocol-v1.md +768 -0
  53. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  54. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  55. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  56. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  57. package/examples/forgedock-demo/README.md +113 -0
  58. package/examples/forgedock-demo/bootstrap.sh +125 -0
  59. package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
  60. package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
  61. package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
  62. package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
  63. package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
  64. package/examples/forgedock-demo/labels.json +21 -0
  65. package/examples/forgedock-demo/package.json +16 -0
  66. package/examples/forgedock-demo/scripts/smoke.js +61 -0
  67. package/examples/forgedock-demo/src/auth.js +28 -0
  68. package/examples/forgedock-demo/src/db.js +66 -0
  69. package/examples/forgedock-demo/src/routes/notes.js +64 -0
  70. package/examples/forgedock-demo/src/server.js +79 -0
  71. package/package.json +13 -3
  72. package/scripts/verify-env-vars.sh +18 -2
  73. package/scripts/verify-host-headers.sh +4 -4
  74. package/scripts/verify-route-registration.sh +2 -2
  75. package/scripts/worktree-lifecycle.sh +202 -0
  76. package/templates/forge.yaml.minimal +44 -0
@@ -31,6 +31,11 @@ gh pr diff [PR_NUMBER] --name-only
31
31
  gh pr diff [PR_NUMBER]
32
32
  ```
33
33
 
34
+ **Hot-spot prior**:
35
+ [CHURN_CONTEXT]
36
+
37
+ If a file you are reviewing is listed above as a hot-spot, apply deeper scrutiny to it — high historical churn correlates with defect density. Prefer tracing that file's full code paths (Evidence-Based Review Protocol §2) over a quick pattern scan, and weight ambiguous findings in hot-spot files toward LIKELY rather than POSSIBLE.
38
+
34
39
  ### 2. Dynamic Exploration
35
40
  - From each changed file, follow imports and function calls
36
41
  - Trace data flows across service boundaries (API → Redis → Worker)
@@ -201,9 +206,10 @@ For each changed file, check:
201
206
 
202
207
  FastAPI's default validation error response exposes internal field names, types, and constraints. Attackers can enumerate the complete request schema by sending malformed payloads.
203
208
  ```bash
204
- for f in $MODEL_FILES; do
209
+ while IFS= read -r f; do
210
+ [ -z "$f" ] && continue
205
211
  grep -rn "RequestValidationError\|validation_exception_handler\|@app.exception_handler" services/api/app/main.py services/api/app/ 2>/dev/null | head -5
206
- done
212
+ done <<< "$MODEL_FILES"
207
213
  # If no custom handler found AND the PR adds new Pydantic models or FastAPI endpoints:
208
214
  # LIKELY MEDIUM — FastAPI default exposes internal field names/types/constraints to any caller
209
215
  # Safe pattern: catch RequestValidationError and return only {"detail": "Invalid request"} or a sanitized message
@@ -216,12 +222,13 @@ For each changed file, check:
216
222
 
217
223
  Unauthenticated health endpoints are expected to be public — the Auth agent's auth-dependency check does not apply. The relevant question is whether they reveal more than `{"status": "ok"}`.
218
224
  ```bash
219
- for f in $HEALTH_FILES; do
225
+ while IFS= read -r f; do
226
+ [ -z "$f" ] && continue
220
227
  # Find unauthenticated endpoints (no Depends() in signature) and check return content
221
228
  grep -A25 "@router\.\(get\|head\)\|async def health\|async def ping\|async def live\|async def ready\|async def status" "$f" | \
222
229
  grep -iE "version|host|port|redis|postgres|db_url|tesseract|pillow|PIL\.__version__|software|engine|server" && \
223
230
  echo "SEC: health endpoint in $f returns operational details. Reveals software versions (enables CVE targeting) or connectivity state (enables DoS confirmation). Safe pattern: return only {\"status\": \"ok\"} or {\"status\": \"degraded\"} — no version strings, no host/port, no connectivity details."
224
- done
231
+ done <<< "$HEALTH_FILES"
225
232
  ```
226
233
  **Confidence**: `LIKELY` — version strings may be intentionally scoped to internal-only endpoints; verify auth status of the endpoint.
227
234
  **Severity**: MEDIUM for software version strings (direct CVE targeting input); LOW for DB/Redis connectivity state (useful for DoS confirmation, not exploit).
@@ -231,10 +238,11 @@ For each changed file, check:
231
238
 
232
239
  Git SHAs and deployment topology exposed via response headers enable targeted CVE research and attack planning.
233
240
  ```bash
234
- for f in $MIDDLEWARE_FILES; do
241
+ while IFS= read -r f; do
242
+ [ -z "$f" ] && continue
235
243
  grep -nE "X-App-Version|X-Deployment-Color|X-.*-SHA|X-.*-Build|X-.*-Commit|Server:|X-Powered-By" "$f" 2>/dev/null && \
236
244
  echo "SEC: response header in $f leaks infrastructure details. Git SHA enables targeted CVE diffing against public commits. Deployment color reveals blue/green topology. Safe pattern: omit these headers or gate them behind an internal-only middleware."
237
- done
245
+ done <<< "$MIDDLEWARE_FILES"
238
246
  ```
239
247
  **Confidence**: `LIKELY` — header may be intentional for internal observability tooling behind an auth gate; verify it applies to all responses.
240
248
  **Severity**: LOW — information disclosure accelerates other attacks but is not directly exploitable without an additional vulnerability.
@@ -259,11 +267,12 @@ CONFIG_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "(traefik/|infra/ngi
259
267
 
260
268
  **A. Port bind address audit** (when `docker-compose*.yml` is in diff — trigger: `$INFRA_FILES` non-empty)
261
269
  ```bash
262
- for f in $INFRA_FILES; do
270
+ while IFS= read -r f; do
271
+ [ -z "$f" ] && continue
263
272
  # Flag non-web ports bound to 0.0.0.0 (no explicit host binding = binds to 0.0.0.0 by default)
264
273
  grep -nE "^\s+- \"[0-9]+:[0-9]+\"" "$f" | grep -v "127\.0\.0\.1\|::1" | grep -vE "\"(80|443):" && \
265
274
  echo "SEC: port published on 0.0.0.0 in $f — verify UFW/firewall is the ONLY protection. Docker daemon restart can reset iptables and expose the port directly."
266
- done
275
+ done <<< "$INFRA_FILES"
267
276
  ```
268
277
  **Confidence**: `LIKELY` — a firewall rule may exist that provides protection; flag for verification rather than hard-block.
269
278
  **Severity**: HIGH for database/internal service ports (5432, 6432, 6379); MEDIUM for other non-web ports.
@@ -271,10 +280,11 @@ done
271
280
 
272
281
  **B. Missing USER directive** (when any `Dockerfile` is in diff — trigger: `$DOCKERFILE_FILES` non-empty)
273
282
  ```bash
274
- for f in $DOCKERFILE_FILES; do
283
+ while IFS= read -r f; do
284
+ [ -z "$f" ] && continue
275
285
  grep -q "^USER " "$f" || \
276
286
  echo "SEC: CONFIRMED — no USER directive in $f. Container runs as UID 0. For services that process user-supplied URLs or untrusted input (worker, Playwright), running as root significantly lowers the bar for container escape."
277
- done
287
+ done <<< "$DOCKERFILE_FILES"
278
288
  ```
279
289
  **Confidence**: `CONFIRMED` — absence of USER directive is an objective, verifiable fact.
280
290
  **Severity**: HIGH for services processing untrusted input; MEDIUM for internal-only services.
@@ -282,10 +292,11 @@ done
282
292
 
283
293
  **C. Response header information disclosure** (when Python middleware/main.py adds response headers — trigger: `$HEADER_FILES` non-empty)
284
294
  ```bash
285
- for f in $HEADER_FILES; do
295
+ while IFS= read -r f; do
296
+ [ -z "$f" ] && continue
286
297
  grep -nE "X-App-Version|X-Deployment-Color|Server:|X-Powered-By|X-.*-SHA|X-.*-Build" "$f" 2>/dev/null && \
287
298
  echo "SEC: response header in $f leaks infrastructure details. Git SHA enables targeted CVE diffing against public commits. Deployment color reveals blue/green topology to attackers."
288
- done
299
+ done <<< "$HEADER_FILES"
289
300
  ```
290
301
  **Confidence**: `CONFIRMED` — header name reveals infra details by definition.
291
302
  **Severity**: MEDIUM — information disclosure accelerates other attacks but is not directly exploitable.
@@ -293,11 +304,12 @@ done
293
304
 
294
305
  **D. CI security scan gating** (when `.github/workflows/*.yml` is in diff — trigger: `$WORKFLOW_FILES` non-empty)
295
306
  ```bash
296
- for f in $WORKFLOW_FILES; do
307
+ while IFS= read -r f; do
308
+ [ -z "$f" ] && continue
297
309
  # Find security scanner steps with continue-on-error: true
298
310
  grep -B5 "continue-on-error: true" "$f" | grep -iE "trivy|snyk|security|cve|scan|grype|dockle" && \
299
311
  echo "SEC: security scanner in $f has continue-on-error: true — CVEs will be logged but will NEVER block a deploy. Remove continue-on-error or add a severity threshold."
300
- done
312
+ done <<< "$WORKFLOW_FILES"
301
313
  ```
302
314
  **Confidence**: `CONFIRMED` — `continue-on-error: true` on a security scanner step is objectively a non-blocking scan.
303
315
  **Severity**: HIGH — security scanner provides zero protection when it cannot block.
@@ -305,10 +317,11 @@ done
305
317
 
306
318
  **E. In-memory rate limiter** (when TypeScript files are in diff — trigger: `$TS_FILES` non-empty)
307
319
  ```bash
308
- for f in $TS_FILES; do
320
+ while IFS= read -r f; do
321
+ [ -z "$f" ] && continue
309
322
  grep -nE "new Map\(\)|=\s*\{\}|= new Map" "$f" 2>/dev/null | grep -iE "rate|limit|attempt|count|throttle|window" && \
310
323
  echo "SEC: in-memory rate limiter detected in $f — resets on every container restart, not shared across blue/green replicas. Use Redis (via Upstash, ioredis, or the existing Redis client) for distributed rate limiting."
311
- done
324
+ done <<< "$TS_FILES"
312
325
  ```
313
326
  **Confidence**: `LIKELY` — in-memory store may be intentional for non-critical paths; flag for verification.
314
327
  **Severity**: MEDIUM — bypass requires restarting the container or hitting a different replica; not trivially exploitable but provides no protection in blue/green deploys.
@@ -316,10 +329,11 @@ done
316
329
 
317
330
  **F. Cookie security flags** (when TypeScript files are in diff — trigger: `$TS_FILES` non-empty)
318
331
  ```bash
319
- for f in $TS_FILES; do
332
+ while IFS= read -r f; do
333
+ [ -z "$f" ] && continue
320
334
  grep -nE "httpOnly:\s*false|secure:\s*false|sameSite.*['\"]none['\"]" "$f" 2>/dev/null && \
321
335
  echo "SEC: cookie in $f has weakened security flag. httpOnly: false exposes cookie to JS (XSS risk). secure: false allows transmission over HTTP. sameSite: none without Secure enables CSRF."
322
- done
336
+ done <<< "$TS_FILES"
323
337
  ```
324
338
  **Confidence**: `CONFIRMED` — explicit `false`/`none` values are objective findings.
325
339
  **Severity**: HIGH for `httpOnly: false` (direct XSS vector for session cookies); MEDIUM for `secure: false` or `sameSite: none`.
@@ -327,33 +341,36 @@ done
327
341
 
328
342
  **G. Fallback credential scan in config files** (when config files are in diff — trigger: `$CONFIG_FILES` non-empty)
329
343
  ```bash
330
- for f in $CONFIG_FILES; do
344
+ while IFS= read -r f; do
345
+ [ -z "$f" ] && continue
331
346
  # Shell-style fallback: ${VAR:-default} — flag non-trivial fallback values that may be credentials
332
347
  grep -nE "\$\{[A-Z_]+:-[^}]{4,}\}" "$f" | grep -ivE "(localhost|127\.0\.0\.1|0\.0\.0\.0|true|false|^[0-9]+$|/tmp|/var|/etc)" && \
333
348
  echo "SEC: potential credential fallback in $f — verify fallback value is safe if env var is absent"
334
- done
349
+ done <<< "$CONFIG_FILES"
335
350
  ```
336
351
  For each hit: read the fallback value.
337
352
  - Contains `$apr1$`, `$2b$`, `$bcrypt$`, `$argon2`, or hash-like long alphanumeric with `$` separators: **CONFIRMED HIGH** — htpasswd/bcrypt placeholder; active if env var is absent.
338
353
  - Is `admin`, `password`, `changeme`, `secret`, `test`, or similar weak string: **CONFIRMED HIGH** — default credential.
339
354
  - Is empty string (`${VAR:-}` or `${VAR:-""}`): **CONFIRMED HIGH** if field controls authentication.
340
355
 
341
- **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.
342
357
  **Confidence**: `CONFIRMED` — fallback value is objectively verifiable.
343
358
  **Severity**: HIGH — insecure fallback credential is equivalent to a hardcoded credential for any environment missing the env var.
344
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.
345
360
 
346
361
  **H. Runtime UID × volume ownership** (when any Dockerfile is in diff AND diff contains `USER`, `su-exec`, `gosu`, or `setuid` — trigger: `$DOCKERFILE_FILES` non-empty AND UID change detected)
347
362
  ```bash
348
- UID_CHANGE=$(for f in $DOCKERFILE_FILES; do
363
+ UID_CHANGE=$(while IFS= read -r f; do
364
+ [ -z "$f" ] && continue
349
365
  gh pr diff [PR_NUMBER] -- "$f" 2>/dev/null | grep -E '^\+.*(USER\s+[^0]|su-exec|gosu|setuid)' | grep -v '^+++' && break
350
- done)
366
+ done <<< "$DOCKERFILE_FILES")
351
367
  if [ -n "$UID_CHANGE" ]; then
352
368
  echo "UID change detected — cross-referencing named volume mounts..."
353
369
  COMPOSE_FILES=$(find . -maxdepth 2 -name "docker-compose*.yml" 2>/dev/null | sort)
354
- for cf in $COMPOSE_FILES; do
370
+ while IFS= read -r cf; do
371
+ [ -z "$cf" ] && continue
355
372
  grep -A2 "volumes:" "$cf" | grep -E "^\s+\w.*:" | grep -v "^--$" && echo " ^ in $cf"
356
- done
373
+ done <<< "$COMPOSE_FILES"
357
374
  fi
358
375
  ```
359
376
  For each named volume found: determine its container mount point. Named volumes are created as **root-owned** by Docker at first use — a privilege drop (root → UID N) without a `chown` before the drop will silently break any service path that calls `mkdir`, `write_bytes`, `open(... 'w')`, or `os.makedirs` under that mount point.
@@ -784,14 +801,14 @@ CRITICAL: This is a billing system. Double-spend = revenue loss.
784
801
  ```
785
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.
786
803
 
787
- 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.
788
805
 
789
806
  Search for discount flags passed through Redis/job payloads:
790
807
  ```bash
791
- 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
792
809
  # If the flag flows through a job payload and is not re-validated at billing: CONFIRMED HIGH
793
810
  ```
794
- 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.
795
812
 
796
813
  ## Safe Patterns in This Codebase
797
814
  ```bash
@@ -1300,8 +1317,8 @@ You are auditing PR #[PR_NUMBER] for database changes in [PROJECT_NAME].
1300
1317
  a. List ALL `*.sql` files in `infra/migrations/` on the PR's target branch (use `git ls-tree` or `ls`)
1301
1318
  b. Extract the 4-digit numeric prefix from each filename (the leading digits before the first `_`)
1302
1319
  c. Identify any prefix that appears more than once
1303
- d. Cross-reference against the `GRANDFATHERED_DUPLICATES` list in `scripts/validate-migration-order.sh` — these are known-safe legacy duplicates
1304
- 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.
1305
1322
  f. Additionally: if the PR adds new migration files, verify their prefixes don't collide with existing files in the directory
1306
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.
1307
1324
  10. **asyncpg gotchas**:
@@ -1414,17 +1431,19 @@ If no infra context is configured above, derive the deployment model from the ch
1414
1431
  OLD_VALUES=$(gh pr diff [PR_NUMBER] | grep "^\-" | grep -oP '"status"\s*:\s*"\K[^"]+' | sort -u)
1415
1432
 
1416
1433
  # Search all non-application consumer locations for those old values
1417
- for val in $OLD_VALUES; do
1434
+ while IFS= read -r val; do
1435
+ [ -z "$val" ] && continue
1418
1436
  echo "=== Searching for old response value: $val ==="
1419
1437
  grep -rn "$val" scripts/ infra/ .github/ traefik/ 2>/dev/null | grep -v "^Binary"
1420
1438
  grep -rn "$val" docker-compose*.yml 2>/dev/null
1421
- done
1439
+ done <<< "$OLD_VALUES"
1422
1440
 
1423
1441
  # Also search for endpoint path references in consumer locations
1424
1442
  HEALTH_PATHS=$(gh pr diff [PR_NUMBER] | grep "^\+" | grep -oP '@\w+\.(?:get|post)\("\K[^"]+(?:health|status|ping|ready|live|readiness|liveness)[^"]*')
1425
- for path in $HEALTH_PATHS; do
1443
+ while IFS= read -r path; do
1444
+ [ -z "$path" ] && continue
1426
1445
  grep -rn "$path" scripts/ infra/ .github/ traefik/ docker-compose*.yml 2>/dev/null | grep -v "^Binary"
1427
- done
1446
+ done <<< "$HEALTH_PATHS"
1428
1447
  ```
1429
1448
 
1430
1449
  **For each hit, classify:**
@@ -1522,13 +1541,14 @@ If no infra context is configured above, derive the deployment model from the ch
1522
1541
 
1523
1542
  **Scan ALL `{{` occurrences in every `script:` block of every `appleboy/ssh-action` step in changed files:**
1524
1543
  ```bash
1525
- for f in $(gh pr diff [PR_NUMBER] --name-only | grep -E '\.github/workflows/.*\.yml$'); do
1544
+ while IFS= read -r f; do
1545
+ [ -z "$f" ] && continue
1526
1546
  if grep -q "appleboy/ssh-action" "$f"; then
1527
1547
  echo "=== appleboy/ssh-action found in $f ==="
1528
1548
  # Show all {{ occurrences with line numbers
1529
1549
  grep -n "{{" "$f"
1530
1550
  fi
1531
- done
1551
+ done < <(gh pr diff [PR_NUMBER] --name-only | grep -E '\.github/workflows/.*\.yml$')
1532
1552
  ```
1533
1553
 
1534
1554
  **For every `{{` found, classify and flag:**
@@ -1549,10 +1569,11 @@ If no infra context is configured above, derive the deployment model from the ch
1549
1569
  **Scan changed config files for credential fallbacks:**
1550
1570
  ```bash
1551
1571
  CONFIG_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "(traefik/|infra/nginx/|infra/|\.github/workflows/).*(\.ya?ml|\.toml|\.json|\.conf|\.ini)$" | head -20)
1552
- for f in $CONFIG_FILES; do
1572
+ while IFS= read -r f; do
1573
+ [ -z "$f" ] && continue
1553
1574
  # Shell-style fallback: ${VAR:-default} — filter out safe non-credential defaults
1554
1575
  grep -nE "\$\{[A-Z_]+:-[^}]{4,}\}" "$f" | grep -ivE "(localhost|127\.0\.0\.1|0\.0\.0\.0|true|false|^[0-9]+$|/tmp|/var|/etc)" | head -20
1555
- done
1576
+ done <<< "$CONFIG_FILES"
1556
1577
  ```
1557
1578
 
1558
1579
  **For each hit, classify the fallback value:**
@@ -1567,7 +1588,7 @@ If no infra context is configured above, derive the deployment model from the ch
1567
1588
 
1568
1589
  **Cross-check when a credential fallback is found:**
1569
1590
  1. Is the corresponding env var required (not optional) in `app/env_validation.py` (or equivalent startup validation)?
1570
- 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`)?
1571
1592
  3. Is the env var documented with a "REQUIRED" note in `.env.example`?
1572
1593
 
1573
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.
@@ -1588,9 +1609,10 @@ If no infra context is configured above, derive the deployment model from the ch
1588
1609
  **Scan for host-side database tool invocations:**
1589
1610
  ```bash
1590
1611
  DEPLOY_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "(\.github/workflows/.*\.yml$|scripts/deploy.*\.sh$|docker-entrypoint.*\.sh$)")
1591
- for f in $DEPLOY_FILES; do
1612
+ while IFS= read -r f; do
1613
+ [ -z "$f" ] && continue
1592
1614
  grep -n "\bpsql\b\|\bcreatedb\b\|\bpg_dump\b\|\bpg_restore\b" "$f" | grep -v "docker exec" | head -20
1593
- done
1615
+ done <<< "$DEPLOY_FILES"
1594
1616
  ```
1595
1617
 
1596
1618
  **For each match, classify:**
@@ -1633,15 +1655,17 @@ If no infra context is configured above, derive the deployment model from the ch
1633
1655
  COMPOSE_INFRA_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "docker-compose.*\.yml$")
1634
1656
 
1635
1657
  # For each config file: read the full file (not just the diff) to understand the structure
1636
- for f in $CONFIG_TOOL_FILES; do
1658
+ while IFS= read -r f; do
1659
+ [ -z "$f" ] && continue
1637
1660
  echo "=== Reading full config structure: $f ==="
1638
1661
  cat "$f"
1639
- done
1662
+ done <<< "$CONFIG_TOOL_FILES"
1640
1663
 
1641
1664
  # Identify tool version from docker-compose (for version-aware schema reasoning)
1642
- for f in $COMPOSE_INFRA_FILES; do
1665
+ while IFS= read -r f; do
1666
+ [ -z "$f" ] && continue
1643
1667
  grep -E "image:\s*(traefik|nginx|haproxy|envoy|caddy|terraform):" "$f" 2>/dev/null && echo " ^ in $f"
1644
- done
1668
+ done <<< "$COMPOSE_INFRA_FILES"
1645
1669
  ```
1646
1670
 
1647
1671
  **Severity classification**:
@@ -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
@@ -624,6 +641,36 @@ echo "$DIFF" | grep -cE "subprocess|os\.system|eval\(|exec\(|pickle|yaml\.load[^
624
641
  echo "$FILES" | grep -cE "^sdk/|openapi.*\.json$|openapi-versions/" && echo " SDK_OPENAPI" || true
625
642
  ```
626
643
 
644
+ **Churn (hot-spot) signal**: The signals above are all derived from the current diff content — none of them measure historical change frequency, which is one of the strongest empirical predictors of defect density. Compute a bounded per-file churn tier for the PR's changed files only (never repo-wide) and carry it forward as `CHURN_CONTEXT` for Phase 3C:
645
+
646
+ ```bash
647
+ CHURN_WINDOW="90 days ago" # named constant — must match the same window used in architect.md Phase A5
648
+ CHURN_CONTEXT=""
649
+ # Herestring (not a piped `| while read`) — a pipe would run the loop body in a
650
+ # subshell in bash, silently discarding CHURN_CONTEXT once the loop exits. $FILES
651
+ # is one path per line (gh pr diff --name-only), so `read -r` per line is safe
652
+ # even when a path contains embedded spaces.
653
+ while IFS= read -r FILE; do
654
+ [ -z "$FILE" ] && continue
655
+ COMMITS=$(git log --oneline --since="$CHURN_WINDOW" -- "$FILE" 2>/dev/null | wc -l)
656
+ if [ "$COMMITS" -ge 15 ]; then
657
+ # Real newline appended via $'\n' (ANSI-C quoting), not a literal backslash-n.
658
+ # A literal "\n" would only be interpreted by `echo -e` — it survives
659
+ # unprocessed into Phase 3C's raw template substitution ([CHURN_CONTEXT] ->
660
+ # $CHURN_CONTEXT), which does not reprocess escapes, so with 2+ HOT files
661
+ # multi-hotspot PRs would render squished in the agent prompt. A real
662
+ # newline here makes the variable correct for every consumer.
663
+ CHURN_CONTEXT="${CHURN_CONTEXT}${FILE} (${COMMITS} commits in last 90 days — HOT)"$'\n'
664
+ fi
665
+ done <<< "$FILES"
666
+ if [ -z "$CHURN_CONTEXT" ]; then
667
+ CHURN_CONTEXT="No hot-spot files detected (all changed files under 15 commits in the last 90 days)."
668
+ fi
669
+ echo "$CHURN_CONTEXT"
670
+ ```
671
+
672
+ Tiers (fixed thresholds, identical to `architect.md` Phase A5): **HOT** = 15+ commits / 90 days, **MEDIUM** = 5–14, **LOW** = 0–4. Only HOT-tier files are listed in `CHURN_CONTEXT` — this keeps the agent prompt signal short and high-value rather than dumping a churn count for every file. This does not change agent selection (3B) — it is a scrutiny prior surfaced to whichever agents are already selected, substituted as `[CHURN_CONTEXT]` in Phase 3C.
673
+
627
674
  ### 3B: Select Agents
628
675
 
629
676
  | Risk Signal | Required Agents |
@@ -687,8 +734,9 @@ This file contains the Evidence-Based Review Protocol, Structured Findings Proto
687
734
  2. Substitute: `[PR_NUMBER]`, `[REVIEW_SHA]`, `[REVIEW_SHA_SHORT]`, `[TITLE]`, relevant files list
688
735
  3. Substitute: `[PROJECT_NAME]` → `$PROJECT_NAME`, `[PROJECT_CONTEXT]` → `$PROJECT_CONTEXT`, `[TECH_STACK]` → `$TECH_STACK`
689
736
  4. Substitute per-agent domain context: `[DOMAIN_CONTEXT]` → the agent's matching key from `forge.yaml → review.domains` (e.g., `$DOMAIN_CONTEXT_AUTH` for the auth agent, `$DOMAIN_CONTEXT_BILLING` for the billing agent)
690
- 5. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
691
- 6. Launch via `Task` tool with the resolved model (default `"sonnet"`, fallback `"opus"` if rate-limited)
737
+ 5. Substitute the shared hot-spot prior: `[CHURN_CONTEXT]` → `$CHURN_CONTEXT` (computed once in Phase 3A, same value for every agent — this is a PR-level fact, not a per-domain config value, so it is NOT read from `forge.yaml`)
738
+ 6. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
739
+ 7. Launch via `Task` tool with the resolved model (default `"sonnet"`, fallback `"opus"` if rate-limited)
692
740
 
693
741
  **CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple Task tool calls. Each agent posts findings directly to the PR via `gh pr comment`.
694
742
 
@@ -1062,7 +1110,7 @@ else
1062
1110
  fi
1063
1111
  ```
1064
1112
 
1065
- **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.
1066
1114
 
1067
1115
  ### 8B: Post-Merge Review Finding Demilestoning (Milestone PRs Only)
1068
1116
 
package/commands/scope.md CHANGED
@@ -79,13 +79,21 @@ Limit `DOMAIN_TERMS` to the 10 most specific (skip generic words: "fix", "bug",
79
79
  Search `REPO_PATH` for each keyword to find candidate files:
80
80
 
81
81
  ```bash
82
+ # {TARGET_FILES} and {DOMAIN_TERMS} are space-separated arguments (same contract
83
+ # as {AFFECTED_FILES} elsewhere) — split explicitly on IFS=' ' into arrays instead
84
+ # of a bare `for x in {PLACEHOLDER}`, which word-splits on the shell's default IFS
85
+ # (space, tab, AND newline) and would corrupt any file path or compound domain
86
+ # term containing a space.
87
+ IFS=' ' read -ra TARGET_FILES_ARR <<< "{TARGET_FILES}"
88
+ IFS=' ' read -ra DOMAIN_TERMS_ARR <<< "{DOMAIN_TERMS}"
89
+
82
90
  # For each file explicitly named in the issue
83
- for file in {TARGET_FILES}; do
91
+ for file in "${TARGET_FILES_ARR[@]}"; do
84
92
  find "$REPO_PATH" -name "$file" -not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null
85
93
  done
86
94
 
87
95
  # For each domain term: grep for references
88
- for term in {DOMAIN_TERMS}; do
96
+ for term in "${DOMAIN_TERMS_ARR[@]}"; do
89
97
  grep -rl "$term" "$REPO_PATH" \
90
98
  --include="*.py" --include="*.ts" --include="*.tsx" \
91
99
  --include="*.md" --include="*.yml" --include="*.yaml" \
@@ -96,7 +104,7 @@ done
96
104
 
97
105
  # Git log — files recently changed alongside the named files
98
106
  if [ -n "{TARGET_FILES}" ]; then
99
- for file in {TARGET_FILES}; do
107
+ for file in "${TARGET_FILES_ARR[@]}"; do
100
108
  git -C "$REPO_PATH" log --oneline -20 -- "$file" \
101
109
  --name-only --format="" 2>/dev/null | grep -v "^$" | sort -u | head -10
102
110
  done
@@ -110,13 +110,14 @@ fi
110
110
  **Check**:
111
111
  ```bash
112
112
  DOCKER_COMPOSE_FILES=$(find "$REPO_PATH" -name "docker-compose*.yml" -not -path "*/.git/*" 2>/dev/null)
113
- for f in $DOCKER_COMPOSE_FILES; do
113
+ while IFS= read -r f; do
114
+ [ -z "$f" ] && continue
114
115
  HITS=$(grep -n '0\.0\.0\.0:[0-9]' "$f" 2>/dev/null | grep -v '^\s*#' || true)
115
116
  if [ -n "$HITS" ]; then
116
117
  echo "FINDING [1A] $f:"
117
118
  echo "$HITS"
118
119
  fi
119
- done
120
+ done <<< "$DOCKER_COMPOSE_FILES"
120
121
  ```
121
122
 
122
123
  **Confirm**: For each hit, verify the port is NOT a web-facing port (80, 443, 8080, 3000). Ports like 6432 (PgBouncer), 6379 (Redis), 5432 (Postgres), 27017 (MongoDB) bound to `0.0.0.0` are confirmed findings.
@@ -134,12 +135,13 @@ done
134
135
  **Check**:
135
136
  ```bash
136
137
  DOCKERFILES=$(find "$REPO_PATH" -name "Dockerfile*" -not -path "*/.git/*" 2>/dev/null)
137
- for f in $DOCKERFILES; do
138
+ while IFS= read -r f; do
139
+ [ -z "$f" ] && continue
138
140
  # Skip if USER directive is present (anywhere in the file, case-insensitive)
139
141
  if ! grep -iq '^\s*USER\s' "$f" 2>/dev/null; then
140
142
  echo "FINDING [1B] $f: no USER directive — container runs as root"
141
143
  fi
142
- done
144
+ done <<< "$DOCKERFILES"
143
145
  ```
144
146
 
145
147
  **Confirm**: Check if the Dockerfile is for a long-running service (not a one-shot build image). If service container with no USER → confirmed finding.
@@ -157,14 +159,15 @@ done
157
159
  **Check**:
158
160
  ```bash
159
161
  DOCKER_COMPOSE_FILES=$(find "$REPO_PATH" -name "docker-compose*.yml" -not -path "*/.git/*" 2>/dev/null)
160
- for f in $DOCKER_COMPOSE_FILES; do
162
+ while IFS= read -r f; do
163
+ [ -z "$f" ] && continue
161
164
  # Redis without requirepass
162
165
  REDIS_NO_AUTH=$(grep -A5 'image:.*redis' "$f" 2>/dev/null | grep -v 'requirepass\|password' | grep -c 'redis' || true)
163
166
  # Postgres without POSTGRES_PASSWORD
164
167
  PG_NO_AUTH=$(awk '/image:.*postgres/{found=1} found && /POSTGRES_PASSWORD/{found=0} found && /^[[:space:]]*-[[:space:]]*[A-Z]/{print}' "$f" 2>/dev/null | grep -v 'POSTGRES_PASSWORD' | head -3 || true)
165
168
 
166
169
  [ -n "$PG_NO_AUTH" ] && echo "FINDING [1C] $f: Postgres service may lack POSTGRES_PASSWORD"
167
- done
170
+ done <<< "$DOCKER_COMPOSE_FILES"
168
171
  ```
169
172
 
170
173
  **Manual review**: Check for `POSTGRES_PASSWORD` and `requirepass` in compose environment sections. Flag any service using a blank/trivial password as a documented value.
@@ -184,7 +187,8 @@ done
184
187
  **Check**:
185
188
  ```bash
186
189
  WORKFLOW_FILES=$(find "$REPO_PATH/.github/workflows" -name "*.yml" 2>/dev/null)
187
- for f in $WORKFLOW_FILES; do
190
+ while IFS= read -r f; do
191
+ [ -z "$f" ] && continue
188
192
  # Find security scanner steps then check if continue-on-error follows within 5 lines
189
193
  awk '
190
194
  /trivy|snyk|owasp|dependency.check|gitleaks|semgrep|gosec|bandit/ {
@@ -195,7 +199,7 @@ for f in $WORKFLOW_FILES; do
195
199
  print FILENAME ":" NR ": continue-on-error: true near security scanner at line " scanner_line
196
200
  }
197
201
  ' FILENAME="$f" "$f" 2>/dev/null || true
198
- done
202
+ done <<< "$WORKFLOW_FILES"
199
203
  ```
200
204
 
201
205
  **Confirm**: Verify the flagged step is indeed a security scanner step (not a notification or deploy step). Confirmed if scanner produces failure exit codes that are being swallowed.
@@ -213,10 +217,11 @@ done
213
217
  **Check**:
214
218
  ```bash
215
219
  CONFIG_FILES=$(find "$REPO_PATH/infra" "$REPO_PATH/services" -name "*.conf" -o -name "*.yml" -o -name "nginx.conf" 2>/dev/null | grep -v '.git')
216
- for f in $CONFIG_FILES; do
220
+ while IFS= read -r f; do
221
+ [ -z "$f" ] && continue
217
222
  HITS=$(grep -n '^\s*#.*\(deny\|allow\s\|auth_basic\|auth_request\|satisfy all\|limit_req\|access_by_lua\)' "$f" 2>/dev/null || true)
218
223
  [ -n "$HITS" ] && echo "FINDING [2B] $f:" && echo "$HITS"
219
- done
224
+ done <<< "$CONFIG_FILES"
220
225
  ```
221
226
 
222
227
  **Confirm**: Distinguish intentional comments (documentation) from disabled access control rules. Confirmed if the commented line is a functional directive that would restrict access.
@@ -236,10 +241,11 @@ done
236
241
  **Check**:
237
242
  ```bash
238
243
  CSP_FILES=$(grep -rl "Content-Security-Policy\|script-src" "$REPO_PATH" --include="*.js" --include="*.ts" --include="*.json" --include="*.conf" --include="*.yml" 2>/dev/null | grep -v '.git' | grep -v node_modules)
239
- for f in $CSP_FILES; do
244
+ while IFS= read -r f; do
245
+ [ -z "$f" ] && continue
240
246
  HITS=$(grep -n "script-src.*unsafe-inline\|unsafe-inline.*script-src" "$f" 2>/dev/null || true)
241
247
  [ -n "$HITS" ] && echo "FINDING [3A] $f:" && echo "$HITS"
242
- done
248
+ done <<< "$CSP_FILES"
243
249
  ```
244
250
 
245
251
  **Confirm**: Verify the CSP header applies to the production site (not just dev/storybook). Check if a nonce-based approach is already in use (nonce + unsafe-inline is still a finding but lower severity).
@@ -257,10 +263,11 @@ done
257
263
  **Check**:
258
264
  ```bash
259
265
  HEADER_FILES=$(grep -rl "X-App-Version\|X-Deployment-Color\|X-Git-Sha\|X-Build-Id\|Server:" "$REPO_PATH" --include="*.py" --include="*.ts" --include="*.js" --include="*.conf" 2>/dev/null | grep -v '.git' | grep -v node_modules)
260
- for f in $HEADER_FILES; do
266
+ while IFS= read -r f; do
267
+ [ -z "$f" ] && continue
261
268
  HITS=$(grep -n '"X-App-Version"\|"X-Deployment-Color"\|"X-Git-Sha"\|"X-Build-Id"\|"Server:' "$f" 2>/dev/null || true)
262
269
  [ -n "$HITS" ] && echo "FINDING [3B] $f:" && echo "$HITS"
263
- done
270
+ done <<< "$HEADER_FILES"
264
271
  ```
265
272
 
266
273
  **Confirm**: Check if the header is set in a response (not just read). If set in outbound responses → confirmed finding. Internal-only APIs (not publicly routed) are `priority:P3`; public APIs are `priority:P2`.
@@ -435,14 +442,15 @@ payment_intent.payment_failed
435
442
  **Check**:
436
443
  ```bash
437
444
  WEBHOOK_FILES=$(grep -rl "stripe\|webhook" "$REPO_PATH/services" --include="*.py" 2>/dev/null | grep -v '.git' | head -10)
438
- for f in $WEBHOOK_FILES; do
445
+ while IFS= read -r f; do
446
+ [ -z "$f" ] && continue
439
447
  REQUIRED_EVENTS="customer.subscription.deleted invoice.payment_failed charge.refunded charge.dispute.created"
440
448
  for event in $REQUIRED_EVENTS; do
441
449
  if ! grep -q "\"$event\"\|'$event'" "$f" 2>/dev/null; then
442
450
  echo "FINDING [4A] $f: Stripe event '$event' not handled"
443
451
  fi
444
452
  done
445
- done
453
+ done <<< "$WEBHOOK_FILES"
446
454
  ```
447
455
 
448
456
  **Confirm**: Verify the event type appears in a handler dispatch (not just a comment). Confirmed if the event would change user entitlements or financial state but has no handler.
@@ -444,7 +444,7 @@ npm version {minor|patch|major} --no-git-tag-version
444
444
  # 3. Commit the version bump
445
445
  NEW_VERSION=$(grep '"version"' package.json | grep -oP '\d+\.\d+\.\d+')
446
446
  git add package.json package-lock.json
447
- git commit -m "chore: bump version to ${NEW_VERSION}"
447
+ git commit -s -m "chore: bump version to ${NEW_VERSION}"
448
448
  git push origin main
449
449
 
450
450
  # 4. Create GitHub Release (triggers {PUBLISH_WORKFLOW} automatically)
@@ -495,7 +495,7 @@ cd {SUBPATH} && npm version {minor|patch} --no-git-tag-version && cd -
495
495
 
496
496
  # Commit and push
497
497
  git add {SUBPATH}/pyproject.toml {SUBPATH}/package.json {SUBPATH}/package-lock.json
498
- git commit -m "chore({COMPONENT}): bump version to {NEW_VERSION}"
498
+ git commit -s -m "chore({COMPONENT}): bump version to {NEW_VERSION}"
499
499
  git push origin fix/{COMPONENT}-version-bump
500
500
 
501
501
  # Create PR to {STAGING_BRANCH}, merge, then trigger publish from main