forgedock 1.0.5 → 1.0.7

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 (42) hide show
  1. package/README.md +7 -1
  2. package/bin/forgedock.mjs +496 -364
  3. package/bin/labels.json +152 -0
  4. package/commands/audit-agents.md +4 -2
  5. package/commands/autopilot.md +19 -11
  6. package/commands/cleanup.md +27 -12
  7. package/commands/deploy-info.md +36 -17
  8. package/commands/incident-response.md +15 -10
  9. package/commands/issue.md +8 -6
  10. package/commands/milestone.md +97 -48
  11. package/commands/orchestrate.md +109 -65
  12. package/commands/pipeline-health.md +8 -1
  13. package/commands/qa-sweep.md +14 -3
  14. package/commands/review-pr-agents.md +1 -1
  15. package/commands/review-pr-staging.md +129 -23
  16. package/commands/review-pr.md +237 -90
  17. package/commands/rollback.md +26 -7
  18. package/commands/security-audit.md +27 -25
  19. package/commands/sync-ecosystem.md +2 -2
  20. package/commands/work-on/build/architect.md +154 -2
  21. package/commands/work-on/build/context.md +131 -3
  22. package/commands/work-on/build/validate.md +32 -13
  23. package/commands/work-on/decompose.md +2 -0
  24. package/commands/work-on-monolithic.md +9 -5
  25. package/commands/work-on.md +67 -23
  26. package/docs/CONFIG.md +117 -0
  27. package/package.json +2 -1
  28. package/scripts/verify-route-registration.sh +36 -15
  29. package/scripts/verify-sops-chain.sh +18 -0
  30. package/templates/devdocs/README.md +96 -0
  31. package/templates/devdocs/agent/using-forgedock.md +245 -0
  32. package/templates/devdocs/agent/using-github.md +325 -0
  33. package/templates/devdocs/project/architecture.md +122 -0
  34. package/templates/devdocs/project/conventions.md +158 -0
  35. package/templates/devdocs/project/custom-instructions.md +73 -0
  36. package/templates/devdocs/project/glossary.md +84 -0
  37. package/templates/devdocs/project/stack.md +80 -0
  38. package/templates/devdocs/templates/commit-convention.md +147 -0
  39. package/templates/devdocs/templates/issue.md +141 -0
  40. package/templates/devdocs/templates/pull-request.md +115 -0
  41. package/templates/devdocs/templates/review-finding.md +147 -0
  42. package/bin/github-app-manifest.json +0 -24
@@ -156,27 +156,78 @@ Record: services touched, domains detected, PR scope (1-2 sentences), change cat
156
156
  ## Phase 2: Automated Checks (Run ALL in Parallel)
157
157
 
158
158
  ### 2A: Python Linting (if Python changed)
159
+
160
+ Read `forge.yaml → verification.commands.python` for project-specific tool commands:
161
+
159
162
  ```bash
160
- cd services/api && poetry run black --check app/
161
- cd services/api && poetry run isort --check app/
162
- cd services/worker && poetry run black --check worker/
163
- cd services/worker && poetry run isort --check worker/
164
- python3 -m py_compile <each-changed-python-file>
163
+ # Read toolchain commands from forge.yaml
164
+ PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
165
+ PYTHON_LINT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'lint:' | head -1 | sed "s/.*lint: *['\"]//;s/['\"].*//")
166
+ PYTHON_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
167
+
168
+ # Run format check
169
+ if [ -n "$PYTHON_FORMAT" ]; then
170
+ eval "$PYTHON_FORMAT" 2>&1 | head -30
171
+ else
172
+ echo "SKIPPED — python.format not configured in verification.commands"
173
+ fi
174
+
175
+ # Run lint
176
+ if [ -n "$PYTHON_LINT" ]; then
177
+ eval "$PYTHON_LINT" 2>&1 | head -30
178
+ else
179
+ echo "SKIPPED — python.lint not configured in verification.commands"
180
+ fi
181
+
182
+ # Run typecheck
183
+ if [ -n "$PYTHON_TYPECHECK" ]; then
184
+ eval "$PYTHON_TYPECHECK" 2>&1 | head -50
185
+ else
186
+ echo "SKIPPED — python.typecheck not configured in verification.commands"
187
+ fi
188
+
189
+ # Always run compile check on changed Python files (fast, language-universal)
190
+ for f in $(gh pr diff $ARGUMENTS --name-only | grep '\.py$'); do
191
+ python3 -m py_compile "$f" 2>&1
192
+ done
165
193
  ```
166
- **IMPORTANT**: Always `cd` into service dir and `poetry run black`. Never `pipx run black`.
167
194
 
168
- ### 2B: TypeScript/JS (if web/ changed)
195
+ ### 2B: TypeScript/JS (if TypeScript/JS changed)
196
+
197
+ Read `forge.yaml → verification.commands.typescript` for project-specific tool commands:
198
+
169
199
  ```bash
170
- cd web && npx prettier --check "src/**/*.{ts,tsx}"
171
- cd web && npx tsc --noEmit
172
- cd web && npx next lint
200
+ TS_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
201
+ TS_LINT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'lint:' | head -1 | sed "s/.*lint: *['\"]//;s/['\"].*//")
202
+ TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
203
+
204
+ # Run format check
205
+ if [ -n "$TS_FORMAT" ]; then
206
+ eval "$TS_FORMAT" 2>&1 | head -30
207
+ else
208
+ echo "SKIPPED — typescript.format not configured in verification.commands"
209
+ fi
210
+
211
+ # Run lint
212
+ if [ -n "$TS_LINT" ]; then
213
+ eval "$TS_LINT" 2>&1 | head -30
214
+ else
215
+ echo "SKIPPED — typescript.lint not configured in verification.commands"
216
+ fi
217
+
218
+ # Run typecheck
219
+ if [ -n "$TS_TYPECHECK" ]; then
220
+ eval "$TS_TYPECHECK" 2>&1 | head -50
221
+ TS_TYPECHECK_EXIT=$?
222
+ else
223
+ echo "SKIPPED — typescript.typecheck not configured in verification.commands"
224
+ TS_TYPECHECK_EXIT=0
225
+ fi
173
226
  ```
174
227
 
175
228
  ### 2C: Static Type Checking (if Python changed)
176
- ```bash
177
- cd services/api && poetry run mypy app/ --ignore-missing-imports --no-error-summary 2>&1 | head -50
178
- cd services/api && poetry run ruff check app/ --select=E,W,F,B,S --ignore=E501 2>&1 | head -30
179
- ```
229
+
230
+ Covered by `PYTHON_TYPECHECK` command in 2A above. If `verification.commands.python.typecheck` is unset, this step is explicitly skipped with a log line — it does not silently pass.
180
231
 
181
232
  ### 2D: Environment Variable Audit
182
233
  ```bash
@@ -206,8 +257,27 @@ git diff origin/main...HEAD -- "**/package.json" | grep -E "^\+" | grep -v "^\+\
206
257
  ```
207
258
 
208
259
  ### 2H: Tests
260
+
261
+ Read `forge.yaml → verification.commands.{lang}.test` for the project's test command:
262
+
209
263
  ```bash
210
- cd services/api && poetry run pytest tests/ -x -q --tb=short 2>&1 | tail -30
264
+ # Check each configured language for a test command
265
+ for lang in python typescript go rust; do
266
+ TEST_CMD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk "/^ ${lang}:/{f=1;next} f && /^ [^ \t]/{exit} f" | grep 'test:' | head -1 | sed "s/.*test: *['\"]//;s/['\"].*//")
267
+ if [ -n "$TEST_CMD" ]; then
268
+ echo "=== Running tests (${lang}): ${TEST_CMD} ==="
269
+ eval "$TEST_CMD" 2>&1 | tail -30
270
+ TEST_EXIT=$?
271
+ [ "$TEST_EXIT" -ne 0 ] && echo "BLOCKING: ${lang} tests failed (exit $TEST_EXIT)"
272
+ fi
273
+ done
274
+
275
+ # If no test commands were configured, log explicitly
276
+ PYTHON_TEST=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'test:' | head -1 | sed "s/.*test: *['\"]//;s/['\"].*//")
277
+ TS_TEST=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'test:' | head -1 | sed "s/.*test: *['\"]//;s/['\"].*//")
278
+ if [ -z "$PYTHON_TEST" ] && [ -z "$TS_TEST" ]; then
279
+ echo "SKIPPED — no test commands configured in verification.commands"
280
+ fi
211
281
  ```
212
282
  **BLOCKING if tests fail.**
213
283
 
@@ -217,36 +287,67 @@ cd services/api && poetry run pytest tests/ -x -q --tb=short 2>&1 | tail -30
217
287
  CHANGED_FILES=$(gh pr diff $ARGUMENTS --name-only)
218
288
  HAS_TS=$(echo "$CHANGED_FILES" | grep -E '\.(tsx?|jsx?)$' | head -1)
219
289
  HAS_PY=$(echo "$CHANGED_FILES" | grep -E '\.py$' | head -1)
220
- IS_STAGING_TO_MAIN=$([[ "$HEAD" == "staging" && "$BASE" == "main" ]] && echo "true" || echo "false")
221
- IS_MILESTONE_TO_STAGING=$([[ "$HEAD" =~ ^milestone/ && "$BASE" == "staging" ]] && echo "true" || echo "false")
222
- REQUIRES_FULL_BUILD=$([[ "$IS_STAGING_TO_MAIN" == "true" || "$IS_MILESTONE_TO_STAGING" == "true" ]] && echo "true" || echo "false")
290
+ # Use POSIX-portable if/else (avoid bash-only [[ ]])
291
+ IS_STAGING_TO_MAIN="false"
292
+ if [ "$HEAD" = "staging" ] && [ "$BASE" = "main" ]; then IS_STAGING_TO_MAIN="true"; fi
293
+ IS_MILESTONE_TO_STAGING="false"
294
+ case "$HEAD" in milestone/*) if [ "$BASE" = "staging" ]; then IS_MILESTONE_TO_STAGING="true"; fi ;; esac
295
+ REQUIRES_FULL_BUILD="false"
296
+ if [ "$IS_STAGING_TO_MAIN" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; then REQUIRES_FULL_BUILD="true"; fi
223
297
  ```
224
298
 
225
299
  **TypeScript files changed:**
300
+
301
+ Read `forge.yaml → verification.commands.typescript.typecheck` and `.build`:
302
+
226
303
  ```bash
227
304
  gh pr checkout $ARGUMENTS --detach 2>/dev/null
228
- cd web && npx tsc --noEmit 2>&1
229
- TSC_EXIT=$?
230
- if [ "$REQUIRES_FULL_BUILD" = "true" ] || [ "$TSC_EXIT" -eq 0 ]; then
231
- npx next build 2>&1 | tail -30
305
+
306
+ TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
307
+ TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
308
+
309
+ if [ -n "$TS_TYPECHECK" ]; then
310
+ eval "$TS_TYPECHECK" 2>&1
311
+ TSC_EXIT=$?
312
+ else
313
+ echo "SKIPPED — typescript.typecheck not configured in verification.commands"
314
+ TSC_EXIT=0
315
+ fi
316
+
317
+ if [ -n "$TS_BUILD" ] && { [ "$REQUIRES_FULL_BUILD" = "true" ] || [ "$TSC_EXIT" -eq 0 ]; }; then
318
+ eval "$TS_BUILD" 2>&1 | tail -30
232
319
  BUILD_EXIT=$?
320
+ elif [ -z "$TS_BUILD" ]; then
321
+ echo "SKIPPED — typescript.build not configured in verification.commands"
233
322
  fi
234
- cd .. && git checkout - 2>/dev/null
323
+
324
+ git checkout - 2>/dev/null
235
325
  ```
236
326
 
237
327
  If `TSC_EXIT != 0`: **CONFIRMED blocking** — type errors.
238
- If `BUILD_EXIT != 0`: **CONFIRMED blocking** — SSG prerender failure.
328
+ If `BUILD_EXIT != 0`: **CONFIRMED blocking** — build/prerender failure.
239
329
 
240
- **CRITICAL**: `tsc --noEmit` alone is NOT sufficient for staging→main or milestone→staging. PR #2565 passed tsc but failed next build. PR #11637 (Session Intelligence milestone) was APPROVED without any build check because milestone→staging was excluded — 4 build errors shipped to staging.
330
+ **CRITICAL**: typecheck alone is NOT sufficient for staging→main or milestone→staging — configure `typescript.build` in `verification.commands` to catch SSG/prerender failures that typecheck misses.
241
331
 
242
332
  **Python files changed:**
333
+
334
+ Read `forge.yaml → verification.commands.python.format` and `.build`:
335
+
243
336
  ```bash
244
337
  gh pr checkout $ARGUMENTS --detach 2>/dev/null
338
+
339
+ # Compile-check all changed Python files (language-universal — no config needed)
245
340
  for f in $(echo "$CHANGED_FILES" | grep '\.py$'); do python3 -m py_compile "$f" 2>&1; done
341
+
246
342
  if [ "$REQUIRES_FULL_BUILD" = "true" ]; then
247
- cd services/api && poetry run black --check app/ 2>&1
248
- cd ../.. && cd services/worker && poetry run black --check worker/ 2>&1
343
+ PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
344
+ if [ -n "$PYTHON_FORMAT" ]; then
345
+ eval "$PYTHON_FORMAT" 2>&1
346
+ else
347
+ echo "SKIPPED — python.format not configured in verification.commands (full-build format check skipped)"
348
+ fi
249
349
  fi
350
+
250
351
  git checkout - 2>/dev/null
251
352
  ```
252
353
 
@@ -258,9 +359,9 @@ Check whether the PR's actual changes match what the builder committed to in its
258
359
 
259
360
  ```bash
260
361
  # Find the contract comment on the linked issue
261
- ISSUE_NUM=$(gh pr view $ARGUMENTS --json body --jq '.body' | grep -oP 'Closes #\K\d+|#\K\d+' | head -1)
362
+ ISSUE_NUM=$(gh pr view $ARGUMENTS --json body --jq '.body | gsub("(?s).*?(?:Closes #|#)(?<n>[0-9]+).*"; "\(.n)") // empty' 2>/dev/null | head -1)
262
363
  if [ -n "$ISSUE_NUM" ]; then
263
- CONTRACT_FILES=$(gh api repos/${REPO}/issues/${ISSUE_NUM}/comments --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | .[0].body' 2>/dev/null | grep -oP '`[^`]+\.(py|tsx?|sql|sh|yml|yaml|json)`' | tr -d '`' | sort -u)
364
+ CONTRACT_FILES=$(gh api repos/${REPO}/issues/${ISSUE_NUM}/comments --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | .[0].body' 2>/dev/null | grep -E '`[^`]+\.(py|tsx?|sql|sh|yml|yaml|json)`' | grep -oE '`[^`]+\.(py|tsx?|sql|sh|yml|yaml|json)`' | tr -d '`' | sort -u)
264
365
  PR_FILES=$(gh pr diff $ARGUMENTS --name-only | sort -u)
265
366
 
266
367
  # Files in PR but NOT in contract
@@ -289,17 +390,28 @@ For each changed file, identify its **activation path** — how does execution r
289
390
 
290
391
  ### Step 2.5A: Identify File Types and Their Registration Points
291
392
 
292
- Map each changed file to its activation requirements:
393
+ Map each changed file to its activation requirements.
394
+
395
+ **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:
396
+
397
+ | `forge.yaml` key | Default | Used in table as |
398
+ |-----------------|---------|-----------------|
399
+ | `review.layout.pages` | `web/src/app` | `{PAGES_ROOT}` |
400
+ | `review.layout.api_routers` | `services/api/app/routers` | `{API_ROUTERS}` |
401
+ | `review.layout.api_main` | `services/api/app/main.py` | `{API_MAIN}` |
402
+ | `review.layout.api_middleware` | `services/api/app/middleware` | `{API_MIDDLEWARE}` |
403
+ | `review.layout.migrations` | `infra/migrations` | `{MIGRATIONS}` |
404
+ | `review.layout.worker` | `services/worker` | `{WORKER}` |
293
405
 
294
406
  | Changed File Pattern | Assumption | Verification Target |
295
407
  |---------------------|------------|---------------------|
296
- | `web/src/app/api/**/*.ts` (Route Handler) | Requests reach this handler | Check `web/next.config.js` rewrites don't shadow it; check `infra/nginx/nginx.conf` routes path to Next.js |
297
- | `services/api/app/routers/*.py` (API Router) | Router is registered in app | Check `services/api/app/main.py` includes this router |
298
- | `services/api/app/middleware/*.py` | Middleware is in the stack | Check `services/api/app/main.py` middleware registration order |
299
- | `infra/migrations/*.sql` | Migration runs on current schema | Check previous migration's end state matches assumptions |
408
+ | `{PAGES_ROOT}/api/**/*.ts` (Route Handler) | Requests reach this handler | Check `web/next.config.js` rewrites don't shadow it; check `infra/nginx/nginx.conf` routes path to Next.js |
409
+ | `{API_ROUTERS}/*.py` (API Router) | Router is registered in app | Check `{API_MAIN}` includes this router |
410
+ | `{API_MIDDLEWARE}/*.py` | Middleware is in the stack | Check `{API_MAIN}` middleware registration order |
411
+ | `{MIGRATIONS}/*.sql` | Migration runs on current schema | Check previous migration's end state matches assumptions |
300
412
  | `shared/**/*.py` | Imported by consumer services | Verify import paths exist in api/worker; check Docker volume mounts |
301
- | `services/worker/worker/*.py` (Consumer) | Queue consumer is registered | Check consumer registration in worker startup |
302
- | Any file using `os.getenv("NEW_VAR")` | Env var is set at runtime | Check `docker-compose.yml`, `.env.example`, `services/api/app/core/env_validation.py` |
413
+ | `{WORKER}/**/*.py` (Consumer) | Queue consumer is registered | Check consumer registration in worker startup |
414
+ | Any file using `os.getenv("NEW_VAR")` | Env var is set at runtime | Check `docker-compose.yml`, `.env.example`, `{API_MAIN}` env validation module |
303
415
  | `scripts/decrypt-secrets.sh` (ENV_MAPPING) | Secret reaches running container | Trace full chain: SOPS key → ENV_MAPPING → deploy workflow SCP target → merge script path → `docker-compose.prod.yml` env_file. See Step 2.5B SOPS deploy chain check. |
304
416
  | `.secrets/prod.enc.yaml` | SOPS key maps to ENV_MAPPING | Verify key path in YAML matches the tuple in `decrypt-secrets.sh` ENV_MAPPING |
305
417
  | `.github/workflows/deploy-production.yml` | Deploy paths are consistent | Verify SCP target + merge script `PROJECT` var resolve to same dir as `docker-compose.prod.yml` env_file |
@@ -317,48 +429,66 @@ Map each changed file to its activation requirements:
317
429
 
318
430
  For each changed file, execute the relevant checks using the standalone verification scripts in `$FORGE_HOME/scripts/`. These scripts can also be run independently outside the review context (e.g., from `/quality-gate` or `/work-on` builder steps).
319
431
 
432
+ **Platform note**: The verify-*.sh scripts require bash and standard POSIX tools. On Windows without bash (Git Bash / WSL / MSYS2), these checks are skipped with an explicit message — the review continues without them.
433
+
320
434
  ```bash
321
435
  CHANGED_FILES=$(gh pr diff $ARGUMENTS --name-only)
322
436
  REPO_ROOT="." # Assumes cwd is the repo root
323
437
 
324
- # Write changed files and diff to temp files for script consumption
325
- CHANGED_FILES_TMP=$(mktemp)
326
- DIFF_TMP=$(mktemp)
327
- echo "$CHANGED_FILES" > "$CHANGED_FILES_TMP"
328
- gh pr diff $ARGUMENTS > "$DIFF_TMP"
329
-
330
- # --- Script-based checks (reusable, testable, deterministic) ---
331
- # Each script exits 0 (pass), 1 (blocking findings), or 2 (warnings only).
332
- # Output is structured: "BLOCKING: ...", "WARNING: ...", "OK: ..." per line.
333
-
334
- # 1. Route/router/middleware/shared-module/component registration
335
- echo "=== Running: verify-route-registration.sh ==="
336
- $FORGE_HOME/scripts/verify-route-registration.sh "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
337
-
338
- # 2. Environment variable wiring (checks .env.example, docker-compose, env_validation, SOPS mapping)
339
- echo "=== Running: verify-env-vars.sh ==="
340
- $FORGE_HOME/scripts/verify-env-vars.sh "$DIFF_TMP" "$REPO_ROOT" || true
341
-
342
- # 3. Host headers in shell scripts + client-side proxy bypass check
343
- # Read project-specific internal service patterns from forge.yaml (if present)
344
- FORGE_INTERNAL_PATTERNS=""
345
- if [ -f "$REPO_ROOT/forge.yaml" ]; then
346
- FORGE_INTERNAL_PATTERNS=$(grep -A 999 'internal_service_patterns:' "$REPO_ROOT/forge.yaml" \
347
- | grep -E '^\s*-\s+' \
348
- | sed 's/^\s*-\s*//' \
349
- | tr -d '"'"'" \
350
- | paste -sd '|' -)
438
+ # --- Platform / bash capability guard ---
439
+ # The verify-*.sh scripts require bash. Detect availability before invoking.
440
+ # On Windows without Git Bash/WSL, skip gracefully rather than crash.
441
+ BASH_AVAILABLE=false
442
+ if command -v bash >/dev/null 2>&1 && bash -c 'echo ok' >/dev/null 2>&1; then
443
+ BASH_AVAILABLE=true
351
444
  fi
352
- export FORGE_INTERNAL_PATTERNS
353
- echo "=== Running: verify-host-headers.sh ==="
354
- $FORGE_HOME/scripts/verify-host-headers.sh "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
355
445
 
356
- # 4. SOPS deploy chain (ENV_MAPPING consistency, deploy path drift, hotfix sync)
357
- echo "=== Running: verify-sops-chain.sh ==="
358
- $FORGE_HOME/scripts/verify-sops-chain.sh "$DIFF_TMP" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
446
+ if [ "$BASH_AVAILABLE" = "true" ]; then
447
+ # Write changed files and diff to temp files for script consumption.
448
+ # Use PID-based names instead of mktemp for cross-platform compatibility.
449
+ CHANGED_FILES_TMP="/tmp/forge-review-changed-$$.tmp"
450
+ DIFF_TMP="/tmp/forge-review-diff-$$.tmp"
451
+ echo "$CHANGED_FILES" > "$CHANGED_FILES_TMP"
452
+ gh pr diff $ARGUMENTS > "$DIFF_TMP"
453
+
454
+ # --- Script-based checks (reusable, testable, deterministic) ---
455
+ # Each script exits 0 (pass), 1 (blocking findings), or 2 (warnings only).
456
+ # Output is structured: "BLOCKING: ...", "WARNING: ...", "OK: ..." per line.
457
+
458
+ # 1. Route/router/middleware/shared-module/component registration
459
+ echo "=== Running: verify-route-registration.sh ==="
460
+ bash "$FORGE_HOME/scripts/verify-route-registration.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
461
+
462
+ # 2. Environment variable wiring (checks .env.example, docker-compose, env_validation, SOPS mapping)
463
+ echo "=== Running: verify-env-vars.sh ==="
464
+ bash "$FORGE_HOME/scripts/verify-env-vars.sh" "$DIFF_TMP" "$REPO_ROOT" || true
465
+
466
+ # 3. Host headers in shell scripts + client-side proxy bypass check
467
+ # Read project-specific internal service patterns from forge.yaml (if present)
468
+ FORGE_INTERNAL_PATTERNS=""
469
+ if [ -f "$REPO_ROOT/forge.yaml" ]; then
470
+ FORGE_INTERNAL_PATTERNS=$(grep -A 999 'internal_service_patterns:' "$REPO_ROOT/forge.yaml" \
471
+ | grep -E '^\s*-\s+' \
472
+ | sed 's/^\s*-\s*//' \
473
+ | tr -d '"'"'" \
474
+ | awk 'NR>1{printf "|"}{printf $0}END{print ""}')
475
+ fi
476
+ export FORGE_INTERNAL_PATTERNS
477
+ echo "=== Running: verify-host-headers.sh ==="
478
+ bash "$FORGE_HOME/scripts/verify-host-headers.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
479
+
480
+ # 4. SOPS deploy chain (ENV_MAPPING consistency, deploy path drift, hotfix sync)
481
+ echo "=== Running: verify-sops-chain.sh ==="
482
+ bash "$FORGE_HOME/scripts/verify-sops-chain.sh" "$DIFF_TMP" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
359
483
 
360
- # Cleanup temp files
361
- rm -f "$CHANGED_FILES_TMP" "$DIFF_TMP"
484
+ # Cleanup temp files
485
+ rm -f "$CHANGED_FILES_TMP" "$DIFF_TMP"
486
+ else
487
+ echo "=== Phase 2.5B: verify-*.sh skipped — bash not available on this platform ==="
488
+ echo " The verify-*.sh scripts require bash (POSIX shell)."
489
+ echo " Install Git Bash (Windows) or WSL to enable these checks."
490
+ echo " The review continues — integration assumptions should be verified manually."
491
+ fi
362
492
 
363
493
  # --- Inline checks (not yet extracted to scripts) ---
364
494
 
@@ -396,7 +526,9 @@ done
396
526
  # diff. Pre-existing drift is the most dangerous kind — it lurks until
397
527
  # staging→main and then blocks the deploy.
398
528
  WORKFLOW_FILES=$(echo "$CHANGED_FILES" | grep -E "^\.github/workflows/.*\.yml$" || true)
399
- IS_STAGING_PR=$([[ "$HEAD" == "staging" && "$BASE" == "main" ]] && echo "true" || echo "false")
529
+ # Use POSIX-portable conditional (avoid bash-only [[ ]])
530
+ IS_STAGING_PR="false"
531
+ if [ "$HEAD" = "staging" ] && [ "$BASE" = "main" ]; then IS_STAGING_PR="true"; fi
400
532
 
401
533
  if [ -n "$WORKFLOW_FILES" ] || [ "$IS_STAGING_PR" = "true" ]; then
402
534
  echo "=== Sibling Workflow Drift Check (MANDATORY for staging→main) ==="
@@ -415,9 +547,14 @@ if [ -n "$WORKFLOW_FILES" ] || [ "$IS_STAGING_PR" = "true" ]; then
415
547
 
416
548
  echo "--- Comparing '$JOB' job between ci.yml and deploy-production.yml ---"
417
549
 
418
- # Extract env vars from ALL steps in the job (not just pytest)
419
- CI_ENVS=$(sed -n "/^ ${JOB}:/,/^ [a-z]/p" "$CI_WF" 2>/dev/null | grep -E "PYTHONPATH|DATABASE_URL|REDIS_URL|TESTING" | sed 's/^ *//' | sort)
420
- DEPLOY_ENVS=$(sed -n "/^ ${JOB}:/,/^ [a-z]/p" "$DEPLOY_WF" 2>/dev/null | grep -E "PYTHONPATH|DATABASE_URL|REDIS_URL|TESTING" | sed 's/^ *//' | sort)
550
+ # Extract env vars from ALL steps in the job (not just pytest).
551
+ # Flag-based awk avoids the range-collapse bug: /pat1/,/pat2/ collapses
552
+ # to a single line when the header (e.g. " test-api:") matches both
553
+ # patterns simultaneously. The flag form sets p=1 on the header line,
554
+ # prints body lines while p=1, and clears p when the next sibling job
555
+ # header (same indentation, lowercase start) is seen. <!-- Added: forge#310 -->
556
+ CI_ENVS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$CI_WF" 2>/dev/null | grep -E "PYTHONPATH|DATABASE_URL|REDIS_URL|TESTING" | sed 's/^ *//' | sort)
557
+ DEPLOY_ENVS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$DEPLOY_WF" 2>/dev/null | grep -E "PYTHONPATH|DATABASE_URL|REDIS_URL|TESTING" | sed 's/^ *//' | sort)
421
558
 
422
559
  # Check for PYTHONPATH specifically — the exact var that caused the #11356 failure
423
560
  CI_PYPATH=$(echo "$CI_ENVS" | grep "PYTHONPATH" || echo "(not set)")
@@ -430,16 +567,16 @@ if [ -n "$WORKFLOW_FILES" ] || [ "$IS_STAGING_PR" = "true" ]; then
430
567
  fi
431
568
 
432
569
  # Check for dependency installation steps that exist in one but not the other
433
- CI_INSTALLS=$(sed -n "/^ ${JOB}:/,/^ [a-z]/p" "$CI_WF" 2>/dev/null | grep -c "poetry install\|pip install\|npm install" || echo 0)
434
- DEPLOY_INSTALLS=$(sed -n "/^ ${JOB}:/,/^ [a-z]/p" "$DEPLOY_WF" 2>/dev/null | grep -c "poetry install\|pip install\|npm install" || echo 0)
570
+ CI_INSTALLS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$CI_WF" 2>/dev/null | grep -c "poetry install\|pip install\|npm install" || echo 0)
571
+ DEPLOY_INSTALLS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$DEPLOY_WF" 2>/dev/null | grep -c "poetry install\|pip install\|npm install" || echo 0)
435
572
  if [ "$CI_INSTALLS" != "$DEPLOY_INSTALLS" ]; then
436
573
  echo " WARNING: Different number of dependency install steps in '$JOB' — ci.yml has $CI_INSTALLS, deploy has $DEPLOY_INSTALLS"
437
574
  echo " ACTION: Read both files and verify all dependencies needed by tests are installed in both workflows."
438
575
  fi
439
576
 
440
577
  # Check step names — if CI has a step that deploy doesn't, flag it
441
- CI_STEPS=$(sed -n "/^ ${JOB}:/,/^ [a-z]/p" "$CI_WF" 2>/dev/null | grep "- name:" | sed 's/.*- name: //' | sort)
442
- DEPLOY_STEPS=$(sed -n "/^ ${JOB}:/,/^ [a-z]/p" "$DEPLOY_WF" 2>/dev/null | grep "- name:" | sed 's/.*- name: //' | sort)
578
+ CI_STEPS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$CI_WF" 2>/dev/null | grep "- name:" | sed 's/.*- name: //' | sort)
579
+ DEPLOY_STEPS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$DEPLOY_WF" 2>/dev/null | grep "- name:" | sed 's/.*- name: //' | sort)
443
580
  MISSING_IN_DEPLOY=$(comm -23 <(echo "$CI_STEPS") <(echo "$DEPLOY_STEPS") 2>/dev/null || true)
444
581
  if [ -n "$MISSING_IN_DEPLOY" ]; then
445
582
  echo " WARNING: Steps in ci.yml '$JOB' missing from deploy-production.yml:"
@@ -568,7 +705,10 @@ gh api repos/{owner}/{repo}/issues/$ARGUMENTS/comments --jq '.[-10:] | .[].body[
568
705
  **Skip if**: Only 1 agent OR total findings ≤ 3.
569
706
 
570
707
  ```bash
571
- ALL_FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '.[].body' | grep -oP '(?<=<!-- FINDING:).*?(?= -->)')
708
+ # Extract structured finding IDs from FINDING HTML comments.
709
+ # Uses jq's scan() (POSIX-portable, no grep -oP required).
710
+ ALL_FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
711
+ --jq '[.[].body | scan("<!-- FINDING:([^>]+) -->") | .[0]] | join("\n")')
572
712
  AGENT_COUNT=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '[.[] | select(.body | test("REVIEW-FINDINGS-START"))] | length')
573
713
  FINDING_COUNT=$(echo "$ALL_FINDINGS" | grep -c '.' || echo 0)
574
714
  ```
@@ -595,9 +735,13 @@ REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
595
735
  HAS_SYNTHESIS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '.[].body' | grep -c 'REVIEW-FINDINGS-SYNTHESIZED-START' || echo 0)
596
736
 
597
737
  if [ "$HAS_SYNTHESIS" -gt 0 ]; then
598
- FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '.[] | select(.body | test("REVIEW-FINDINGS-SYNTHESIZED-START")) | .body' | grep -oP '(?<=<!-- FINDING:).*?(?= -->)')
738
+ # Extract finding IDs from synthesized block using jq scan() — no grep -oP needed
739
+ FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
740
+ --jq '[.[] | select(.body | test("REVIEW-FINDINGS-SYNTHESIZED-START")) | .body | scan("<!-- FINDING:([^>]+) -->") | .[0]] | join("\n")')
599
741
  else
600
- FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '.[].body' | grep -oP '(?<=<!-- FINDING:).*?(?= -->)')
742
+ # Extract finding IDs from all agent comments using jq scan() — portable, no grep -oP
743
+ FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
744
+ --jq '[.[].body | scan("<!-- FINDING:([^>]+) -->") | .[0]] | join("\n")')
601
745
  fi
602
746
  ```
603
747
 
@@ -616,10 +760,12 @@ If still 0: review is clean — skip to Phase 7.
616
760
  ### 6C: Create Issues
617
761
 
618
762
  ```bash
619
- gh label create "review-finding" --color "D93F0B" --force 2>/dev/null
620
- gh label create "needs-validation" --color "FBCA04" --force 2>/dev/null
621
- gh label create "validated" --color "0E8A16" --force 2>/dev/null
622
- gh label create "false-positive" --color "CCCCCC" --force 2>/dev/null
763
+ # Colors match the canonical ForgeDock label manifest (bin/labels.json).
764
+ # Run `npx forgedock labels setup` to bootstrap all managed labels at once.
765
+ gh label create "review-finding" --color "D93F0B" --description "Defect or improvement found during automated PR review. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
766
+ gh label create "needs-validation" --color "FBCA04" --description "Review finding awaiting human validation. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
767
+ gh label create "validated" --color "0E8A16" --description "Review finding confirmed as a real issue. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
768
+ gh label create "false-positive" --color "CCCCCC" --description "Review finding dismissed as a false positive. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
623
769
  ```
624
770
 
625
771
  **Milestone detection:**
@@ -693,7 +839,7 @@ else
693
839
  ' 2>/dev/null | head -1)
694
840
  if [ -n "$REGRESSION" ]; then
695
841
  echo "REGRESSION: Previously fixed in #${REGRESSION} — elevating priority"
696
- # Create with regression warning and P1 priority
842
+ # Create with regression warning and priority:P1 label
697
843
  fi
698
844
  fi
699
845
  ```
@@ -701,7 +847,7 @@ fi
701
847
  **Rules:**
702
848
  - Open `review-finding` issue at same file within ±5 lines → **skip** (do not create duplicate)
703
849
  - Open `review-finding` issue at same file with similar title (3+ shared keywords) → **skip** (likely same finding despite line drift)
704
- - Closed `review-finding` at same file within ±5 lines → create with regression warning, elevate to P1
850
+ - Closed `review-finding` at same file within ±5 lines → create with regression warning, elevate to `priority:P1`
705
851
 
706
852
  **For each finding** (that passes dedup), create issue:
707
853
  ```bash
@@ -757,7 +903,7 @@ ISSUE_EOF
757
903
  )" --json number --jq '.number')
758
904
  ```
759
905
 
760
- Labels: `review-finding` + `needs-validation` + priority (`P1` CONFIRMED, `P2` LIKELY, `P3` POSSIBLE).
906
+ Labels: `review-finding` + `needs-validation` + priority (`priority:P1` CONFIRMED, `priority:P2` LIKELY, `priority:P3` POSSIBLE).
761
907
 
762
908
  **Add to project board:**
763
909
  ```bash
@@ -922,7 +1068,8 @@ fi
922
1068
 
923
1069
  ```bash
924
1070
  CURRENT_SHA=$(gh pr view $ARGUMENTS --json headRefOid --jq '.headRefOid')
925
- REVIEW_IS_STALE=$([[ "$CURRENT_SHA" != "$REVIEW_SHA" ]] && echo "true" || echo "false")
1071
+ REVIEW_IS_STALE="false"
1072
+ if [ "$CURRENT_SHA" != "$REVIEW_SHA" ]; then REVIEW_IS_STALE="true"; fi
926
1073
  ```
927
1074
 
928
1075
  ```bash
@@ -9,6 +9,7 @@ argument-hint: [PR number to revert, or "last" for most recent deploy]
9
9
 
10
10
  **Config variables used by this command** (set in `forge.yaml`):
11
11
  - `{REPO_PATH}` ← `paths.root` — project repository root
12
+ - `{DEPLOY_WORKFLOW}` ← `deploy.workflow` (optional) — GitHub Actions workflow filename for fast-track deploys (e.g., `hotfix-deploy.yml`). When absent or empty, the workflow-trigger suggestion is omitted.
12
13
 
13
14
  You are the pipeline's emergency rollback system. When a shipped feature or fix causes production issues, this command creates a revert PR and fast-tracks it through the pipeline.
14
15
 
@@ -112,12 +113,29 @@ If conflicts cannot be auto-resolved, report to user and STOP.
112
113
 
113
114
  ### Step 2C: Verify the revert compiles
114
115
 
116
+ Read `forge.yaml → verification.commands` for project-specific tool commands:
117
+
115
118
  ```bash
116
- # Python files
117
- poetry -C services/api run python -m py_compile {reverted_python_files} 2>&1
119
+ # Python compile check (always runs — no config needed)
120
+ for f in {reverted_python_files}; do
121
+ python3 -m py_compile "$f" 2>&1
122
+ done
123
+
124
+ # Python format check (config-driven)
125
+ PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
126
+ if [ -n "$PYTHON_FORMAT" ]; then
127
+ eval "$PYTHON_FORMAT" 2>&1 | head -20
128
+ else
129
+ echo "SKIPPED — python.format not configured in verification.commands"
130
+ fi
118
131
 
119
- # TypeScript files (if any)
120
- cd web && npx tsc --noEmit 2>&1 | head -20
132
+ # TypeScript typecheck (config-driven)
133
+ TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
134
+ if [ -n "$TS_TYPECHECK" ]; then
135
+ eval "$TS_TYPECHECK" 2>&1 | head -20
136
+ else
137
+ echo "SKIPPED — typescript.typecheck not configured in verification.commands"
138
+ fi
121
139
  ```
122
140
 
123
141
  ---
@@ -160,10 +178,11 @@ PR_EOF
160
178
  ```bash
161
179
  gh pr merge {REVERT_PR} --merge
162
180
  ```
163
- - Then trigger hotfix deploy:
181
+ - Then trigger hotfix deploy (requires `deploy.workflow` set in `forge.yaml`):
164
182
  ```bash
165
183
  echo "Revert merged to main. CI/CD will deploy automatically."
166
- echo "For faster deploy, trigger hotfix: gh workflow run hotfix-deploy.yml --ref main -f services={affected} -f reason=\"Rollback PR #{PR_NUMBER}\""
184
+ echo "For faster deploy, trigger deploy workflow: gh workflow run {deploy.workflow} --ref main -f {deploy.workflow_inputs.services}={affected} -f {deploy.workflow_inputs.reason}=\"Rollback PR #{PR_NUMBER}\""
185
+ # If deploy.workflow is not configured in forge.yaml, CI/CD will deploy automatically on the next push to main.
167
186
  ```
168
187
 
169
188
  **If not urgent**:
@@ -192,7 +211,7 @@ fi
192
211
 
193
212
  ```bash
194
213
  gh issue create --title "fix: {original_title} (reverted — needs new approach)" \
195
- --label "bug,P1" \
214
+ --label "bug,priority:P1" \
196
215
  --body "$(cat <<'BODY_EOF'
197
216
  ## Problem
198
217