forgedock 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/bin/engine/admission.mjs +57 -1
  2. package/bin/engine/admission.test.mjs +60 -0
  3. package/commands/analytics.md +3 -1
  4. package/commands/audit-agents.md +3 -1
  5. package/commands/audit.md +10 -8
  6. package/commands/autopilot.md +16 -14
  7. package/commands/ci-audit.md +4 -2
  8. package/commands/cleanup.md +1 -1
  9. package/commands/deploy-pr.md +31 -11
  10. package/commands/geo-audit.md +3 -1
  11. package/commands/incident-response.md +3 -1
  12. package/commands/issue.md +4 -2
  13. package/commands/milestone.md +8 -6
  14. package/commands/orchestrate/config.md +2 -2
  15. package/commands/orchestrate/phase-1-resolve.md +56 -12
  16. package/commands/orchestrate/phase-3-dependency.md +15 -5
  17. package/commands/orchestrate/phase-4-execution.md +593 -78
  18. package/commands/orchestrate/phase-5-cleanup.md +4 -2
  19. package/commands/orchestrate/phase-6-report.md +7 -3
  20. package/commands/orchestrate.md +13 -9
  21. package/commands/pipeline-health/phase-4-proposals.md +6 -4
  22. package/commands/pipeline-health/phase-5-report.md +4 -2
  23. package/commands/pipeline-health.md +9 -7
  24. package/commands/pipeline-resume.md +4 -2
  25. package/commands/qa-sweep.md +3 -1
  26. package/commands/quality-gate.md +4 -3
  27. package/commands/recover-orphans.md +172 -13
  28. package/commands/review-pr-agents.md +6 -4
  29. package/commands/review-pr-staging.md +13 -5
  30. package/commands/review-pr.md +187 -19
  31. package/commands/rollback.md +3 -1
  32. package/commands/security-audit.md +3 -1
  33. package/commands/signal-planner.md +6 -4
  34. package/commands/sync-ecosystem.md +4 -2
  35. package/commands/test-gate.md +4 -2
  36. package/commands/upgrade-deps.md +3 -1
  37. package/commands/validate.md +3 -1
  38. package/commands/work-on/build/architect.md +81 -19
  39. package/commands/work-on/build/context.md +61 -18
  40. package/commands/work-on/build/implement.md +62 -18
  41. package/commands/work-on/build/validate.md +240 -60
  42. package/commands/work-on/build.md +344 -146
  43. package/commands/work-on/close.md +617 -246
  44. package/commands/work-on/decompose.md +239 -41
  45. package/commands/work-on/investigate.md +377 -61
  46. package/commands/work-on/remediate.md +42 -9
  47. package/commands/work-on/review.md +240 -29
  48. package/commands/work-on-monolithic.md +48 -7
  49. package/commands/work-on.md +180 -2014
  50. package/docs/CODEX.md +2 -0
  51. package/docs/CONFIG.md +11 -0
  52. package/package.json +1 -1
  53. package/scripts/build-spec-graph.mjs +11 -4
  54. package/scripts/check-spec-markers.sh +11 -1
  55. package/scripts/checkbox-sections.test.sh +8 -23
  56. package/scripts/extract-affected-files.sh +5 -2
  57. package/scripts/extract-affected-files.test.sh +27 -0
  58. package/scripts/forge-root.test.sh +176 -0
  59. package/scripts/is-docs-only.sh +31 -0
  60. package/scripts/is-docs-only.test.sh +64 -0
  61. package/scripts/lint-dispatch-prompt.sh +217 -0
  62. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  63. package/scripts/validate-spec-graph.sh +3 -2
  64. package/scripts/verify-phase-trail.sh +282 -0
  65. package/scripts/verify-phase-trail.test.sh +298 -0
  66. package/scripts/wait-ci-green.sh +98 -0
  67. package/scripts/wait-ci-green.test.sh +99 -0
@@ -1,16 +1,21 @@
1
1
  ---
2
2
  description: Validation agent — quality gate loop, format/verify, proxy check, deploy check
3
- argument-hint: "[issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--worktree PATH] [--files FILE1 FILE2...]"
3
+ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE} --branch {BRANCH} --base {PR_BASE} --files \"<changed files>\""
4
+ context: fork
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
7
8
 
8
9
  # work-on/build/validate — Validation Subcommand
9
10
 
11
+ > **Skill Name Resolution (forked phase)**: `{FORGE_SKILL_PREFIX}` is the namespace this skill itself was invoked under — invoked as `forgedock:work-on:…` → `forgedock:` (nesting `:`); as `work-on:…` → empty (`install.sh`); as `forge-work-on-…` → `forge-` (Codex, nesting `-`); OpenCode → empty with `-` nesting. Confirm the target name in the available-skills list before calling it. A forked phase receives no resolved value from its caller; never guess, and if the target skill is not listed return BLOCKED "skill not found: <name>".
12
+
10
13
  **Input**: $ARGUMENTS
11
14
 
12
- **Invoked by**: `work-on.md` Step 3F.5, after `implement.md` has written and staged code (not committed).
13
- **Output**: Return `GATE_PASSED: true/false` to caller. On failure after max iterations, post comment and set `needs-human`.
15
+ **Invoked by**: `work-on:build`, after `implement.md` has written and staged code (not committed). This skill runs in an isolated forked context: it sees only this text and its args, so it re-reads all other state from GitHub/git/forge.yaml.
16
+ **Output**: End with exactly one `VALIDATE_RESULT:` block as the final reply. On failure after max iterations, post comment and set `needs-human`.
17
+
18
+ **Result-on-every-exit rule**: every exit path — success, skip path, quality-gate failure, timeout, ancestry failure, missing arg, skill not found — MUST print the `VALIDATE_RESULT:` block (see Output) as the final reply. Failures use `gate_passed: false` with a `blocker` line. Never exit with a bare `exit 1` or free text.
14
19
 
15
20
  **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
16
21
  **NEVER use plan mode (EnterPlanMode).**
@@ -25,25 +30,29 @@ Parse from $ARGUMENTS:
25
30
  - `{NUMBER}` — issue number (required)
26
31
  - `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}` — resolved from `forge.yaml → project`)
27
32
  - `--gh-flag {GH_FLAG}` — gh CLI repo flag (e.g. `-R {owner}/{repo}`)
28
- - `--worktree {WORKTREE_PATH}` — absolute path to the git worktree
29
- - `--files {CHANGED_FILES}` — space-separated list of changed files (from implement result)
33
+ - `--worktree {WORKTREE_PATH}` — absolute path to the git worktree (required)
34
+ - `--branch {BRANCH}` — feature branch name (required)
35
+ - `--base {PR_BASE}` — PR base branch name without the `origin/` prefix (required; used by the V5 ancestry audit)
36
+ - `--files {CHANGED_FILES}` — space-separated list of changed files (required; from the implement result)
37
+
38
+ **Fail closed**: if `{NUMBER}`, `--repo`, `--worktree`, `--branch`, `--base`, or `--files` is missing or empty, or `{WORKTREE_PATH}` is not an existing directory, print `VALIDATE_RESULT:` with `gate_passed: false` and `blocker: missing or invalid arg: <name>` and STOP. `{GH_FLAG}` defaults to `-R {GH_REPO}` when absent.
30
39
 
31
40
  ---
32
41
 
33
42
  ## Skip Conditions
34
43
 
35
- Skip all phases (return `GATE_PASSED: true` immediately) if:
36
- - Only 1 file was changed AND it is a config or docs file with no code logic (e.g. `.md`, `.yml` with no scripts, `.env.example`)
44
+ Skip Phases V0–V4 (set `GATE_PASSED=true`, iterations 0) and go straight to Phase V5 (which posts the skip-path quality-gate marker, commits, and marks the build complete) if:
45
+ - Only 1 file was changed AND the shared predicate classifies it as documentation: `printf '%s\n' "<that file>" | bash "$FORGE_ROOT/scripts/is-docs-only.sh"` exits 0 (resolve `FORGE_ROOT` with the canonical block; unresolvable → the gate runs). A single file under `commands/`, `.claude/`, `.agents/`, `hooks/`, … or any agent-instruction Markdown is **code** for this purpose (it contains executable shell and agent instructions) and is never skipped. <!-- field test: phase-4-execution.md was skipped as "docs" -->
37
46
 
38
47
  In all other cases, the gate MUST run.
39
48
 
40
49
  ---
41
50
 
42
- ## Phase V1: Builder Self-Check — Wire-Through Proof (MANDATORY, run BEFORE quality gate loop)
51
+ ## Phase V0: Builder Self-Check — Wire-Through Proof (MANDATORY, run BEFORE the Phase V1 quality gate loop)
43
52
 
44
53
  <!-- Added: forge#1731 -->
45
54
 
46
- Before invoking the quality gate, the builder MUST perform a self-check on newly added conditional paths. This mirrors the quality gate's 2G.8 check and allows the builder to resolve gaps before the gate invocation rather than after.
55
+ Before invoking the quality gate (Phase V1), the builder MUST perform a self-check on newly added conditional paths. This mirrors the quality gate's 2G.8 check and allows the builder to resolve gaps before the gate invocation rather than after.
47
56
 
48
57
  **Self-check protocol**:
49
58
 
@@ -69,6 +78,8 @@ Before invoking the quality gate, the builder MUST perform a self-check on newly
69
78
 
70
79
  ## Phase V1: Quality Gate Loop
71
80
 
81
+ The loop runs **in this context** and calls `quality-gate` through the `Skill` tool; `quality-gate` is itself a forked skill (isolated context) that returns only its PASS/FINDINGS result lines, so no `Agent` fork is needed here. <!-- Added: forge#1825 -->
82
+
72
83
  **Loop protocol** — the gate MUST pass or exhaust iterations before returning:
73
84
 
74
85
  ```
@@ -101,21 +112,22 @@ if iteration == max_iterations AND result != PASS AND blocker_findings not empty
101
112
  GATE_PASSED = false
102
113
  → post comment (see V1-FAIL below)
103
114
  → add label needs-human
104
- → return GATE_PASSED: false to caller, STOP
115
+ → print VALIDATE_RESULT with gate_passed: false (blocker = remaining findings), STOP
105
116
  ```
106
117
 
107
118
  **Quality gate invocation**:
108
119
  ```
109
- Skill("quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
120
+ Skill("{FORGE_SKILL_PREFIX}quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
110
121
  ```
111
122
 
112
- **Timeout handling**: The quality-gate's executable checks carry their own explicit timeouts; a `QUALITY-GATE-TIMEOUT` result is a failed gate iteration. Record it as HIGH, do not apply speculative fixes, and proceed to V1-FAIL immediately (post the failure comment, add `needs-human`, return `GATE_PASSED: false`). Do not retry a timed-out check. A wall-time timeout for the in-context `Skill(...)` invocation itself requires native runtime support and is not claimed by this workflow.
123
+ **Timeout handling**: The quality-gate's executable checks carry their own explicit timeouts; a `QUALITY-GATE-TIMEOUT` result is a failed gate iteration. Record it as HIGH, do not apply speculative fixes, and proceed to V1-FAIL immediately (post the failure comment, add `needs-human`, print `VALIDATE_RESULT` with `gate_passed: false`). Do not retry a timed-out check. A wall-time timeout for the in-context `Skill(...)` invocation itself requires native runtime support and is not claimed by this workflow.
113
124
 
114
125
  **Rules**:
115
126
  - Re-run after EVERY fix pass — never trust that fixes resolved findings without verification
116
127
  - Each iteration re-scans ALL changed files — fixes can introduce new issues
117
128
  - Only HIGH and MEDIUM findings must be fixed; LOW findings are advisory only
118
129
  - `TEST-QUARANTINE | LOW` findings (pre-broken or flaky tests classified by Step 2R) do **not** require fixing and do **not** count toward gate failure — include them in the V5 commit comment for reviewer visibility
130
+ - If the `quality-gate` skill is not found under either name, STOP and print `VALIDATE_RESULT` with `gate_passed: false`, `blocker: skill not found: quality-gate` — never run the gate inline
119
131
 
120
132
  **V1-FAIL comment** (post when gate never passes):
121
133
  ```bash
@@ -129,6 +141,7 @@ Quality gate findings persist after 3 fix passes. Flagging for human review.
129
141
  Needs human review before proceeding to commit.
130
142
 
131
143
  <!-- FORGE:GATE_FAILED -->"
144
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
132
145
  ```
133
146
 
134
147
  ---
@@ -207,12 +220,82 @@ fi
207
220
  ```
208
221
  Typecheck or build failures are BLOCKING — fix type errors before continuing.
209
222
 
210
- **Shell scripts**: Verify service interactions — read target middleware files, document what was verified in V4 summary.
223
+ **Shell scripts**: Verify service interactions — read target middleware files, document what was verified in the V5 summary.
211
224
 
212
225
  **Markdown / config files**: No format step required.
213
226
 
214
227
  If no files match a language category, skip that language's step.
215
228
 
229
+ ### Known-slow test gate <!-- Added: forge#1861 -->
230
+
231
+ Before running any test command below, check it against `verification.known_slow_tests` (a repo-declared list of test patterns known to hang or make live network/LLM calls). When absent or empty, this is a no-op and behavior is unchanged.
232
+
233
+ ```bash
234
+ cd {WORKTREE_PATH}
235
+ # Read directly from forge.yaml (static, operator-declared config).
236
+ KNOWN_SLOW_TESTS=$(yq -o=json -I=0 '.verification.known_slow_tests // []' forge.yaml 2>/dev/null || echo '[]')
237
+
238
+ # apply_known_slow_filter <cmd> — echoes the command to actually run, or "" to
239
+ # skip it entirely. Matching is substring match of `pattern` against the full
240
+ # command text. Exactly one of skip/subset is expected per matched entry.
241
+ apply_known_slow_filter() {
242
+ local cmd="$1"
243
+ local out="$cmd"
244
+ if [ -n "$KNOWN_SLOW_TESTS" ] && [ "$KNOWN_SLOW_TESTS" != "[]" ] && [ "$KNOWN_SLOW_TESTS" != "null" ]; then
245
+ while IFS= read -r entry; do
246
+ [ -z "$entry" ] && continue
247
+ pattern=$(echo "$entry" | yq '.pattern // ""')
248
+ skip=$(echo "$entry" | yq '.skip // false')
249
+ subset=$(echo "$entry" | yq '.subset // ""')
250
+ reason=$(echo "$entry" | yq '.reason // "no reason given"')
251
+ [ -z "$pattern" ] && continue
252
+ case "$cmd" in
253
+ *"$pattern"*)
254
+ if [ "$skip" = "true" ]; then
255
+ echo "SKIPPED — known-slow test matched pattern '$pattern' ($reason)" >&2
256
+ out=""
257
+ elif [ -n "$subset" ]; then
258
+ echo "SUBSTITUTED — known-slow test matched pattern '$pattern' ($reason); running safe subset instead" >&2
259
+ out="$subset"
260
+ fi
261
+ ;;
262
+ esac
263
+ done < <(echo "$KNOWN_SLOW_TESTS" | yq -o=json -I=0 '.[]' 2>/dev/null)
264
+ fi
265
+ echo "$out"
266
+ }
267
+ ```
268
+
269
+ ### Learned test commands <!-- Added: forge#667, forge#1861 -->
270
+
271
+ After all `verification.commands` steps complete, run any commands from `forge.yaml → learned.test_commands` (captured from owner corrections or set manually), filtered through the known-slow gate above. This phase reads `learned:` itself — no value is passed in from the caller.
272
+
273
+ ```bash
274
+ cd {WORKTREE_PATH}
275
+ LEARNED_TEST_COMMANDS=$(yq -o=json -I=0 '.learned.test_commands // []' forge.yaml 2>/dev/null || echo '[]')
276
+ LEARNED_FAILED=0
277
+ if [ -n "$LEARNED_TEST_COMMANDS" ] && [ "$LEARNED_TEST_COMMANDS" != "[]" ] && [ "$LEARNED_TEST_COMMANDS" != "null" ]; then
278
+ echo "Running learned test commands..."
279
+ while IFS= read -r cmd; do
280
+ [ -z "$cmd" ] && continue
281
+ FILTERED_CMD=$(apply_known_slow_filter "$cmd")
282
+ [ -z "$FILTERED_CMD" ] && continue
283
+ echo "Running learned command: $FILTERED_CMD"
284
+ CMD_OUTPUT=$(run_verification_command "learned.test_commands" "$FILTERED_CMD")
285
+ CMD_EXIT=$?
286
+ echo "$CMD_OUTPUT" | tail -30
287
+ if [ "$CMD_EXIT" -ne 0 ]; then
288
+ echo "FAILED (exit $CMD_EXIT): $FILTERED_CMD"
289
+ LEARNED_FAILED=1
290
+ fi
291
+ done < <(echo "$LEARNED_TEST_COMMANDS" | yq -r '.[]' 2>/dev/null)
292
+ else
293
+ echo "No learned test commands configured — skipping"
294
+ fi
295
+ ```
296
+
297
+ Learned test command failures (`LEARNED_FAILED=1`) are BLOCKING (same as `verification.commands` failures) — fix and re-run. A command matched and skipped by the known-slow gate is never executed and never counted as a failure.
298
+
216
299
  ---
217
300
 
218
301
  ## Phase V3: Frontend Proxy Wiring Check
@@ -231,7 +314,7 @@ grep -n "127.0.0.1" {CHANGED_TS_FILES}
231
314
 
232
315
  If violations found:
233
316
  1. Fix them in `{WORKTREE_PATH}`
234
- 2. Document fixes in V4 summary
317
+ 2. Document fixes in the V5 summary
235
318
 
236
319
  ---
237
320
 
@@ -338,12 +421,29 @@ Classify:
338
421
 
339
422
  **Skip if**: No new environment variables were introduced in the changed files.
340
423
 
341
- Detect new env vars:
424
+ Detect new env vars (staged + unstaged changes versus `HEAD`; the commit has not happened yet, so the diff base is `HEAD`, not `HEAD~1`):
342
425
  ```bash
343
426
  cd {WORKTREE_PATH}
344
- git diff HEAD~1 -- {CHANGED_FILES} | grep -oP '(?<=os\.environ\[")[^"]+|(?<=os\.getenv\()["\']?[A-Z_]+' | sort -u
345
- # Also check TypeScript:
346
- git diff HEAD~1 -- {CHANGED_FILES} | grep -oP 'process\.env\.\K[A-Z_]+' | sort -u
427
+ ADDED_LINES=$(git diff HEAD -- {CHANGED_FILES} | grep -E '^\+' | grep -v '^+++')
428
+ {
429
+ # Python: os.environ["VAR"], os.getenv("VAR")
430
+ echo "$ADDED_LINES" | grep -oE 'os\.environ\["[^"]+' | sed 's/^os\.environ\["//'
431
+ echo "$ADDED_LINES" | grep -oE "os\.getenv\([\"']?[A-Z_]+" | sed -E "s/^os\.getenv\([\"']?//"
432
+ # TypeScript: process.env.VAR
433
+ echo "$ADDED_LINES" | grep -oE 'process\.env\.[A-Z_]+' | sed 's/^process\.env\.//'
434
+ } | sort -u
435
+ ```
436
+
437
+ **Config variables used by this phase** (set in `forge.yaml`):
438
+ - `deploy.secrets_backend` — secrets delivery method (`sops`, `aws-sm`, `vault`, `ci-env`, `none`). When absent or not `sops`, SOPS-specific checks below are skipped with an explicit log message.
439
+ - `verification.services[name].container` — container name for post-deploy verification. Resolved by matching the service name; falls back to `{service}` (bare name) when not configured.
440
+
441
+ ```bash
442
+ cd {WORKTREE_PATH}
443
+ SECRETS_BACKEND=$(yq '.deploy.secrets_backend // ""' forge.yaml 2>/dev/null || echo '')
444
+ if [ "$SECRETS_BACKEND" != "sops" ]; then
445
+ echo 'SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.'
446
+ fi
347
447
  ```
348
448
 
349
449
  For each new env var found, verify it is present in ALL required locations:
@@ -351,53 +451,116 @@ For each new env var found, verify it is present in ALL required locations:
351
451
  | Location | Required for |
352
452
  |----------|-------------|
353
453
  | `.env.example` | All new vars |
354
- | `infra/secrets/prod.enc.yaml` (SOPS) | Production secrets |
355
- | `infra/decrypt-secrets.sh` ENV_MAPPING | All vars in deploy chain |
356
- | `app/env_validation.py` | API service vars |
357
- | `docker-compose.prod.yml` | Vars needing explicit injection |
454
+ | Secrets backend (see `deploy.secrets_backend`) | Secret vars — skip if backend is `none` or unset |
455
+ | `app/env_validation.py` | API service vars (if project has one) |
456
+ | `docker-compose.prod.yml` | Vars needing explicit injection (if project uses Docker Compose) |
358
457
 
359
- **Deploy chain**: SOPS → `decrypt-secrets.sh` (ENV_MAPPING) → `.env.secrets` → `merge-env-secrets.sh` → `.env.production` → docker-compose `env_file`.
458
+ **Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*: verify the new var is present in all SOPS chain locations:
459
+ - `infra/secrets/prod.enc.yaml` — SOPS-encrypted secret store
460
+ - `infra/decrypt-secrets.sh` ENV_MAPPING — maps SOPS key to env var name
461
+ - Deploy chain: SOPS → `decrypt-secrets.sh` (ENV_MAPPING) → `.env.secrets` → `merge-env-secrets.sh` → `.env.production` → docker-compose `env_file`
360
462
 
361
463
  If any required location is missing the var:
362
464
  1. Add it to the missing location in `{WORKTREE_PATH}`
363
- 2. Document the addition in V4 summary
364
- 3. These additions are NOT new commits — they should be staged and committed together with any other pending fixes, or as a separate commit if clean
465
+ 2. Document the addition in the V5 summary
466
+ 3. These additions are NOT new commits — they are absorbed into the single V5 commit
467
+
468
+ **Operator-set var classification** *(trigger: new env var is NOT in the configured secrets backend)*: <!-- Added: forge#380 -->
469
+
470
+ Some env vars are operator-set (non-secret, not sourced from the secrets backend) — they must be manually added to the runtime environment on the production server. When a new env var has no entry in the secrets backend, classify it as operator-set and add a **HARD BLOCKER** item to the Testing Checklist (in the FORGE:BUILDER comment).
471
+
472
+ Resolve the container name for the verification command:
473
+ 1. Look up the service in `forge.yaml → verification.services[]` by name — use the `container` field if present.
474
+ 2. If no matching entry, fall back to the bare service name: `{service}` (no suffix).
475
+
476
+ ```
477
+ - [ ] HARD BLOCKER: Add {VAR_NAME} to the runtime environment on the production server.
478
+ This var is operator-set — it does NOT flow through the automated secrets chain.
479
+ It must be added manually before or after deploy.
480
+ Verify with: docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
481
+ (CONTAINER_NAME resolved from verification.services[{service}].container in forge.yaml,
482
+ or bare service name if not configured)
483
+ ```
484
+
485
+ **`env_file` re-read warning** *(trigger: any new env var added to `.env.production` path)*:
486
+
487
+ > **Docker `env_file` re-read behavior**: New entries in `.env.production` are only read when a container is **recreated** (e.g., `docker compose up --force-recreate`). A plain `docker restart` restarts the existing container with its frozen env — new `env_file` entries are silently absent. The standard deploy workflow uses `--force-recreate` and handles this correctly. If any out-of-band restart is used, new env vars will not take effect.
488
+
489
+ Add this warning to the Testing Checklist whenever a new env var is introduced (whether secret or operator-set).
490
+
491
+ **Post-deploy in-container verification** *(trigger: any new env var)*: add the following to the Testing Checklist so the deployer can confirm delivery after deploy. Resolve `{CONTAINER_NAME}` from `forge.yaml → verification.services[{service}].container`; use `{service}` (bare) if the field is absent.
492
+
493
+ ```bash
494
+ # Verify env var reached the running container (run post-deploy)
495
+ docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
496
+ # Expected: {VAR_NAME}={value}
497
+ # If blank: container was not recreated — run: docker compose up --no-deps --force-recreate {service}
498
+ ```
365
499
 
366
500
  ---
367
501
 
368
- ## Phase V5: Commit (always — after GATE_PASSED=true)
502
+ ## Phase V5: Marker, Commit, Audit, Complete (always — after GATE_PASSED=true)
503
+
504
+ Phase V5 runs in this exact order: **(1)** post the `FORGE:QUALITY_GATE` marker, **(2)** commit, **(3)** ancestry audit, **(4)** append `FORGE:BUILDER:COMPLETE` and the verification status. Do not reorder.
505
+
506
+ ### V5 Step 1: Post FORGE:QUALITY_GATE Marker (MANDATORY, before the commit) <!-- Added: forge#3061 -->
369
507
 
370
- After the gate passes, commit all staged changes in a single commit. This includes:
508
+ The quality gate must leave a checkable artifact. `scripts/verify-phase-trail.sh` (run before PR creation and before auto-merge) requires a `FORGE:QUALITY_GATE` comment with `**Result**: PASS` for every non-docs-only change. Post it after the V1 loop ends, recording the real commands run and their real results. The marker records the staged tree (`**Tree**`) so a PASS from an earlier build commit cannot satisfy the gate for a later one: any edit after the gate requires re-running validate. Do NOT hand-post this marker without having actually run the gate: a marker with no run behind it is a pipeline bypass.
509
+
510
+ **Skip-path marker**: when the Skip Conditions above set `GATE_PASSED=true` early (single config/docs file), still post the marker with `**Result**: PASS (skipped — single config/docs file)` and `**Iterations**: 0`, so the verifier never has to guess. The verifier's `--docs-only` waiver additionally covers diffs accepted by `scripts/is-docs-only.sh` (allowlisted Markdown only: `docs/**` or root README/CHANGELOG/CONTRIBUTING/SECURITY/GOVERNANCE; nested `AGENTS.md`/`CLAUDE.md`/`SKILL.md`/`GEMINI.md` and the instruction directories `commands/`, `devdocs/`, `templates/`, `skills/`, `agents/`, `hooks/`, `.claude/`, `.claude-plugin/`, `.agents/`, `.codex/`, `.cursor/`, `.github/`, `.opencode/`, `.gemini/`, `.kiro/` excluded; callers feed both sides of renames).
511
+
512
+ ```bash
513
+ GATE_RESULT=$([ "$GATE_PASSED" = "true" ] && echo PASS || echo FAIL)
514
+ # Bind the PASS to what was actually gated: the staged tree is exactly the tree the V5 commit will have.
515
+ # scripts/verify-phase-trail.sh --head-tree (work-on/review.md R1.5) rejects a PASS recorded for a different tree. <!-- Added: forge#3149 -->
516
+ git -C {WORKTREE_PATH} add -u # same staging as the commit below; valid only because V2-V4 are done
517
+ GATE_TREE=$(git -C {WORKTREE_PATH} write-tree)
518
+ QG_BODY="<!-- FORGE:QUALITY_GATE -->
519
+ ## Quality Gate Result
520
+
521
+ **Result**: ${GATE_RESULT}
522
+ **Tree**: ${GATE_TREE}
523
+ **Iterations**: {N}
524
+ **Commands run**: {quality-gate invocation, format/verify commands, test commands actually executed}
525
+ **Findings remaining**: {none | summary}"
526
+ gh issue comment {NUMBER} {GH_FLAG} --body "$QG_BODY" # allowlist:check-command-side-effects
527
+ ```
528
+
529
+ ### V5 Step 2: Commit
530
+
531
+ After the marker is posted, commit all staged changes in a single commit. This includes:
371
532
  - The implementation changes staged by `implement.md` Phase I4
372
533
  - Any format, proxy, or deploy fixes applied by phases V2–V4
373
534
 
374
535
  ```bash
375
536
  cd {WORKTREE_PATH}
376
537
  git add -u
377
- git commit -s -m "fix({SCOPE}): {description} (#NUMBER)"
538
+ git commit -s -m "fix({SCOPE}): {description} (#{NUMBER})"
378
539
  ```
379
540
 
380
541
  Where `{SCOPE}` is the command or module scope from the contract (e.g. `work-on`, `quality-gate`), and `{description}` summarises the implementation. Use the commit convention from the contract:
381
542
  - Bug Fix → `fix(`
382
543
  - Feature → `feat(`
383
544
  - Refactor → `refactor(`
545
+ - Docs-only → `docs(`
384
546
 
385
- This is the **only** commit for this build cycle. It replaces the old `git commit` that was previously in `implement.md` Phase I4. Do NOT create a separate commit for validation fixes — they are absorbed into this single commit.
547
+ Reference `#{NUMBER}` in the message. This is the **only** commit for this build cycle. Do NOT create a separate commit for validation fixes — they are absorbed into this single commit.
386
548
 
387
549
  **Attribution**: The commit message is exactly the conventional-commit line above — nothing more. Do NOT append a `Co-Authored-By: Claude` trailer, a `🤖 Generated with Claude Code` line, or any assistant-tool attribution. Pipeline output is ForgeDock-branded; the assistant signature must never enter the repo's commit history. (A PreToolUse guard hard-blocks it as a backstop — see `bin/hooks/pre-tool-use.mjs` Rule 5.)
388
550
 
389
- ### V5 Post-Commit Ancestry Audit (MANDATORY)
551
+ ### V5 Step 3: Post-Commit Ancestry Audit (MANDATORY)
390
552
 
391
- After committing, run the ancestry audit to detect merge commits from unrelated branches before the branch is pushed:
553
+ After committing, run the ancestry audit to detect merge commits from unrelated branches before the branch is pushed. If `origin/{PR_BASE}` does not exist yet (new branch), skip this check — no contamination is possible from a non-existent base.
392
554
 
393
555
  ```bash
394
556
  cd {WORKTREE_PATH}
395
- MERGE_COMMITS=$(git log --merges HEAD ^origin/{PR_BASE} 2>/dev/null)
396
- if [ -n "$MERGE_COMMITS" ]; then
397
- echo "ANCESTRY AUDIT FAILED: merge commits from unrelated branches detected on this branch:"
398
- echo "$MERGE_COMMITS"
399
- # Block push — do NOT proceed to review.md R1
400
- gh issue comment {NUMBER} {GH_FLAG} --body "## Ancestry Audit Failed
557
+ if git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1; then
558
+ git fetch origin {PR_BASE} >/dev/null 2>&1 || true
559
+ MERGE_COMMITS=$(git log --merges HEAD ^origin/{PR_BASE} 2>/dev/null)
560
+ if [ -n "$MERGE_COMMITS" ]; then
561
+ echo "ANCESTRY AUDIT FAILED: merge commits from unrelated branches detected on this branch:"
562
+ echo "$MERGE_COMMITS"
563
+ ANCESTRY_BODY="## Ancestry Audit Failed
401
564
 
402
565
  Branch \`{BRANCH}\` contains merge commits from branches outside the PR base (\`{PR_BASE}\`). This is a staging contamination risk — these commits may carry code from milestone branches that has not been approved for \`{PR_BASE}\`.
403
566
 
@@ -409,34 +572,52 @@ ${MERGE_COMMITS}
409
572
  Human review required before this branch can be pushed.
410
573
 
411
574
  <!-- FORGE:ANCESTRY_FAILED -->"
412
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
413
- # Return GATE_PASSED: false — do not push
414
- exit 1
575
+ gh issue comment {NUMBER} {GH_FLAG} --body "$ANCESTRY_BODY" # allowlist:check-command-side-effects
576
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
577
+ echo "ANCESTRY_FAILED=1"
578
+ fi
579
+ else
580
+ echo "PR_BASE not on origin — skipping ancestry audit"
415
581
  fi
416
582
  ```
417
583
 
418
- If `origin/{PR_BASE}` does not exist yet (new branch), skip this check — no contamination is possible from a non-existent base. Detect with:
419
- ```bash
420
- git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1 || echo "PR_BASE not on origin — skipping ancestry audit"
421
- ```
584
+ If the output contains `ANCESTRY_FAILED=1`: do NOT append `:COMPLETE` and do NOT push. Print `VALIDATE_RESULT` with `gate_passed: false` and `blocker: ancestry audit failed — merge commits from unrelated branches` as the final reply, and STOP.
585
+
586
+ ### V5 Step 4: Mark Build Complete and Record Verification Status (MANDATORY)
587
+
588
+ After the ancestry audit passes (or is skipped), patch the existing FORGE:BUILDER comment: add the `**Verification Status**` line, the best-effort `cost_usd:` line, and the `<!-- FORGE:BUILDER:COMPLETE -->` marker. This is the **only** place the marker is written — it signals that a real commit exists on the branch and the build is safe to resume-skip. <!-- Added: forge#1305 -->
422
589
 
423
- ### V5 Post-Commit: Mark Build Complete (MANDATORY)
590
+ `SKIPPED_CHECKS` comes from Phase V2. Shell state may not persist between Bash calls, so set it here from your V2 notes before running the block (comma-separated check names, empty when every configured check ran).
424
591
 
425
- 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 -->
592
+ **Cost line reconciliation**: the machine-readable `cost_usd:` line (best-effort; only when `PHASE_COST_USD` is available, never blocking) is the single cost signal. Do not add a separate `**Cost (build phase)**` line.
426
593
 
427
594
  ```bash
595
+ # SKIPPED_CHECKS: set from the V2 notes, e.g. SKIPPED_CHECKS="python.format, typescript.typecheck/build"
596
+ SKIPPED_CHECKS="${SKIPPED_CHECKS:-}"
597
+ if [ -z "$SKIPPED_CHECKS" ]; then
598
+ VERIFICATION_STATUS="✅ All configured verification commands passed"
599
+ else
600
+ VERIFICATION_STATUS="⚠ Verification NOT run: ${SKIPPED_CHECKS} — verification.commands not configured for these checks"
601
+ fi
602
+
428
603
  # Find the FORGE:BUILDER comment posted by implement.md Phase I6
429
604
  BUILDER_COMMENT_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
430
605
  --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
431
606
 
432
607
  if [ -n "$BUILDER_COMMENT_ID" ]; then
433
- # Fetch current body and append the completion marker plus best-effort cost signal
434
608
  CURRENT_BODY=$(gh api repos/{GH_REPO}/issues/comments/$BUILDER_COMMENT_ID --jq '.body')
609
+ # Insert the Verification Status line after the "**Files changed**" line (idempotent).
610
+ if printf '%s' "$CURRENT_BODY" | grep -q '^\*\*Verification Status\*\*'; then
611
+ PATCHED_BODY="$CURRENT_BODY"
612
+ else
613
+ PATCHED_BODY=$(printf '%s\n' "$CURRENT_BODY" | awk -v vs="**Verification Status**: ${VERIFICATION_STATUS}" \
614
+ '{print} /^\*\*Files changed\*\*/ && !done {print vs; done=1}')
615
+ fi
435
616
  # Best-effort cost append: only include if session telemetry provides a value; never block
436
617
  PHASE_COST_LINE=""
437
618
  [ -n "${PHASE_COST_USD:-}" ] && PHASE_COST_LINE="
438
619
  cost_usd: ${PHASE_COST_USD}"
439
- UPDATED_BODY="${CURRENT_BODY}${PHASE_COST_LINE}
620
+ UPDATED_BODY="${PATCHED_BODY}${PHASE_COST_LINE}
440
621
 
441
622
  <!-- FORGE:BUILDER:COMPLETE -->"
442
623
  gh api repos/{GH_REPO}/issues/comments/$BUILDER_COMMENT_ID \
@@ -448,13 +629,13 @@ else
448
629
  fi
449
630
  ```
450
631
 
451
- **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`.
632
+ **Why here and not in implement.md**: The commit (`git commit`) runs in Step 2. Appending `:COMPLETE` after the commit and ancestry audit ensures that a session crash between implement.md I6 (comment posted) and this step 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`.
452
633
 
453
634
  ---
454
635
 
455
636
  ## Output
456
637
 
457
- Return structured output to the caller:
638
+ The final reply is exactly one `VALIDATE_RESULT:` block — on every exit path (success, skip path, and every failure):
458
639
 
459
640
  ```
460
641
  VALIDATE_RESULT:
@@ -472,18 +653,17 @@ VALIDATE_RESULT:
472
653
 
473
654
  ---
474
655
 
475
- ## Integration Point in work-on.md
656
+ ## Integration Point
476
657
 
477
- This module runs at **Step 3F.5** — after implement, before commit (3J) and PR creation (Phase 4):
658
+ This module is invoked by `work-on:build` after implement and before review:
478
659
 
479
660
  ```
480
- 3F → Implement (by implement.md) — code written, staged (not committed)
481
- 3F.5 → [THIS MODULE] Validate — gate loop, format, proxy, browser signals, deploy checks
482
- 3G → (covered by Phase V2 above)
483
- 3H → (covered by Phase V3 above)
484
- 3H.5 → (covered by Phase V3.6 above — browser signal check for UI changes)
485
- 3I → (covered by Phase V4 above)
486
- 3J → V5 commit happens here after GATE_PASSED=true (single commit for implementation + any fixes)
661
+ implement (work-on:build:implement) — code written, staged (not committed), FORGE:BUILDER posted
662
+ → [THIS MODULE] validate
663
+ V0 self-check → V1 quality-gate loop (Skill quality-gate, forked) → V2 format/verify + known-slow/learned tests
664
+ → V3 proxy wiring → V3.5 DB advisory → V3.6 browser signals → V4 deploy completeness
665
+ → V5 marker → commit → ancestry audit → BUILDER:COMPLETE
666
+ review (work-on:review) — push, PR, merge
487
667
  ```
488
668
 
489
- If `VALIDATE_RESULT: gate_passed: false`, the router adds `needs-human` label and stops — no PR is created.
669
+ If `VALIDATE_RESULT: gate_passed: false`, the build skill reports BLOCKED (`needs-human` is set) and no PR is created.