forgedock 1.8.1 → 1.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) 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/design-bench.md +1 -0
  11. package/commands/design-render-critique-loop.md +1 -0
  12. package/commands/design.md +1 -0
  13. package/commands/forge-stats.md +1 -0
  14. package/commands/geo-audit.md +3 -1
  15. package/commands/incident-response.md +3 -1
  16. package/commands/issue.md +4 -2
  17. package/commands/milestone.md +8 -6
  18. package/commands/orchestrate/config.md +3 -2
  19. package/commands/orchestrate/phase-1-resolve.md +57 -12
  20. package/commands/orchestrate/phase-2-triage.md +1 -0
  21. package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
  22. package/commands/orchestrate/phase-3-dependency.md +17 -5
  23. package/commands/orchestrate/phase-4-execution.md +625 -78
  24. package/commands/orchestrate/phase-5-cleanup.md +5 -2
  25. package/commands/orchestrate/phase-6-report.md +8 -3
  26. package/commands/orchestrate/safety.md +1 -0
  27. package/commands/orchestrate.md +13 -9
  28. package/commands/pipeline-health/config.md +1 -0
  29. package/commands/pipeline-health/phase-1-context.md +1 -0
  30. package/commands/pipeline-health/phase-2-metrics.md +1 -0
  31. package/commands/pipeline-health/phase-3-analyze.md +1 -0
  32. package/commands/pipeline-health/phase-4-proposals.md +7 -4
  33. package/commands/pipeline-health/phase-5-report.md +5 -2
  34. package/commands/pipeline-health/phase-6-summary.md +1 -0
  35. package/commands/pipeline-health.md +9 -7
  36. package/commands/pipeline-resume.md +4 -2
  37. package/commands/qa-sweep.md +3 -1
  38. package/commands/quality-gate.md +4 -3
  39. package/commands/recover-orphans.md +219 -13
  40. package/commands/review-pr-agents/api.md +1 -0
  41. package/commands/review-pr-agents/auth.md +1 -0
  42. package/commands/review-pr-agents/billing.md +1 -0
  43. package/commands/review-pr-agents/concurrency.md +1 -0
  44. package/commands/review-pr-agents/database.md +1 -0
  45. package/commands/review-pr-agents/frontend.md +1 -0
  46. package/commands/review-pr-agents/infra.md +1 -0
  47. package/commands/review-pr-agents/protocols.md +1 -0
  48. package/commands/review-pr-agents/scraper.md +1 -0
  49. package/commands/review-pr-agents/security.md +1 -0
  50. package/commands/review-pr-agents/spec-cli.md +1 -0
  51. package/commands/review-pr-agents.md +10 -4
  52. package/commands/review-pr-staging.md +14 -5
  53. package/commands/review-pr.md +187 -19
  54. package/commands/rollback.md +3 -1
  55. package/commands/security-audit.md +3 -1
  56. package/commands/signal-planner.md +6 -4
  57. package/commands/sync-ecosystem.md +4 -2
  58. package/commands/test-gate.md +4 -2
  59. package/commands/upgrade-deps.md +3 -1
  60. package/commands/validate.md +3 -1
  61. package/commands/work-on/build/architect.md +82 -19
  62. package/commands/work-on/build/context.md +62 -18
  63. package/commands/work-on/build/implement.md +63 -18
  64. package/commands/work-on/build/validate.md +241 -60
  65. package/commands/work-on/build.md +345 -146
  66. package/commands/work-on/close.md +618 -246
  67. package/commands/work-on/decompose.md +240 -41
  68. package/commands/work-on/investigate.md +378 -61
  69. package/commands/work-on/remediate.md +43 -9
  70. package/commands/work-on/review.md +241 -29
  71. package/commands/work-on.md +180 -2014
  72. package/docs/CODEX.md +2 -0
  73. package/docs/CONFIG.md +11 -0
  74. package/package.json +1 -1
  75. package/scripts/build-spec-graph.mjs +11 -4
  76. package/scripts/check-spec-markers.sh +21 -1
  77. package/scripts/checkbox-sections.test.sh +8 -23
  78. package/scripts/extract-affected-files.sh +5 -2
  79. package/scripts/extract-affected-files.test.sh +27 -0
  80. package/scripts/forge-root.test.sh +176 -0
  81. package/scripts/is-docs-only.sh +31 -0
  82. package/scripts/is-docs-only.test.sh +64 -0
  83. package/scripts/lint-dispatch-prompt.sh +217 -0
  84. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  85. package/scripts/validate-spec-graph.sh +3 -2
  86. package/scripts/verify-phase-trail.sh +282 -0
  87. package/scripts/verify-phase-trail.test.sh +298 -0
  88. package/scripts/wait-ci-green.sh +98 -0
  89. package/scripts/wait-ci-green.test.sh +99 -0
  90. package/commands/work-on-monolithic.md +0 -326
@@ -1,16 +1,22 @@
1
1
  ---
2
+ user-invocable: false
2
3
  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...]"
4
+ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE} --branch {BRANCH} --base {PR_BASE} --files \"<changed files>\""
5
+ context: fork
4
6
  ---
5
7
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
8
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
7
9
 
8
10
  # work-on/build/validate — Validation Subcommand
9
11
 
12
+ > **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>".
13
+
10
14
  **Input**: $ARGUMENTS
11
15
 
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`.
16
+ **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.
17
+ **Output**: End with exactly one `VALIDATE_RESULT:` block as the final reply. On failure after max iterations, post comment and set `needs-human`.
18
+
19
+ **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
20
 
15
21
  **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
22
  **NEVER use plan mode (EnterPlanMode).**
@@ -25,25 +31,29 @@ Parse from $ARGUMENTS:
25
31
  - `{NUMBER}` — issue number (required)
26
32
  - `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}` — resolved from `forge.yaml → project`)
27
33
  - `--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)
34
+ - `--worktree {WORKTREE_PATH}` — absolute path to the git worktree (required)
35
+ - `--branch {BRANCH}` — feature branch name (required)
36
+ - `--base {PR_BASE}` — PR base branch name without the `origin/` prefix (required; used by the V5 ancestry audit)
37
+ - `--files {CHANGED_FILES}` — space-separated list of changed files (required; from the implement result)
38
+
39
+ **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
40
 
31
41
  ---
32
42
 
33
43
  ## Skip Conditions
34
44
 
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`)
45
+ 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:
46
+ - 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
47
 
38
48
  In all other cases, the gate MUST run.
39
49
 
40
50
  ---
41
51
 
42
- ## Phase V1: Builder Self-Check — Wire-Through Proof (MANDATORY, run BEFORE quality gate loop)
52
+ ## Phase V0: Builder Self-Check — Wire-Through Proof (MANDATORY, run BEFORE the Phase V1 quality gate loop)
43
53
 
44
54
  <!-- Added: forge#1731 -->
45
55
 
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.
56
+ 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
57
 
48
58
  **Self-check protocol**:
49
59
 
@@ -69,6 +79,8 @@ Before invoking the quality gate, the builder MUST perform a self-check on newly
69
79
 
70
80
  ## Phase V1: Quality Gate Loop
71
81
 
82
+ 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 -->
83
+
72
84
  **Loop protocol** — the gate MUST pass or exhaust iterations before returning:
73
85
 
74
86
  ```
@@ -101,21 +113,22 @@ if iteration == max_iterations AND result != PASS AND blocker_findings not empty
101
113
  GATE_PASSED = false
102
114
  → post comment (see V1-FAIL below)
103
115
  → add label needs-human
104
- → return GATE_PASSED: false to caller, STOP
116
+ → print VALIDATE_RESULT with gate_passed: false (blocker = remaining findings), STOP
105
117
  ```
106
118
 
107
119
  **Quality gate invocation**:
108
120
  ```
109
- Skill("quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
121
+ Skill("{FORGE_SKILL_PREFIX}quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
110
122
  ```
111
123
 
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.
124
+ **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
125
 
114
126
  **Rules**:
115
127
  - Re-run after EVERY fix pass — never trust that fixes resolved findings without verification
116
128
  - Each iteration re-scans ALL changed files — fixes can introduce new issues
117
129
  - Only HIGH and MEDIUM findings must be fixed; LOW findings are advisory only
118
130
  - `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
131
+ - 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
132
 
120
133
  **V1-FAIL comment** (post when gate never passes):
121
134
  ```bash
@@ -129,6 +142,7 @@ Quality gate findings persist after 3 fix passes. Flagging for human review.
129
142
  Needs human review before proceeding to commit.
130
143
 
131
144
  <!-- FORGE:GATE_FAILED -->"
145
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
132
146
  ```
133
147
 
134
148
  ---
@@ -207,12 +221,82 @@ fi
207
221
  ```
208
222
  Typecheck or build failures are BLOCKING — fix type errors before continuing.
209
223
 
210
- **Shell scripts**: Verify service interactions — read target middleware files, document what was verified in V4 summary.
224
+ **Shell scripts**: Verify service interactions — read target middleware files, document what was verified in the V5 summary.
211
225
 
212
226
  **Markdown / config files**: No format step required.
213
227
 
214
228
  If no files match a language category, skip that language's step.
215
229
 
230
+ ### Known-slow test gate <!-- Added: forge#1861 -->
231
+
232
+ 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.
233
+
234
+ ```bash
235
+ cd {WORKTREE_PATH}
236
+ # Read directly from forge.yaml (static, operator-declared config).
237
+ KNOWN_SLOW_TESTS=$(yq -o=json -I=0 '.verification.known_slow_tests // []' forge.yaml 2>/dev/null || echo '[]')
238
+
239
+ # apply_known_slow_filter <cmd> — echoes the command to actually run, or "" to
240
+ # skip it entirely. Matching is substring match of `pattern` against the full
241
+ # command text. Exactly one of skip/subset is expected per matched entry.
242
+ apply_known_slow_filter() {
243
+ local cmd="$1"
244
+ local out="$cmd"
245
+ if [ -n "$KNOWN_SLOW_TESTS" ] && [ "$KNOWN_SLOW_TESTS" != "[]" ] && [ "$KNOWN_SLOW_TESTS" != "null" ]; then
246
+ while IFS= read -r entry; do
247
+ [ -z "$entry" ] && continue
248
+ pattern=$(echo "$entry" | yq '.pattern // ""')
249
+ skip=$(echo "$entry" | yq '.skip // false')
250
+ subset=$(echo "$entry" | yq '.subset // ""')
251
+ reason=$(echo "$entry" | yq '.reason // "no reason given"')
252
+ [ -z "$pattern" ] && continue
253
+ case "$cmd" in
254
+ *"$pattern"*)
255
+ if [ "$skip" = "true" ]; then
256
+ echo "SKIPPED — known-slow test matched pattern '$pattern' ($reason)" >&2
257
+ out=""
258
+ elif [ -n "$subset" ]; then
259
+ echo "SUBSTITUTED — known-slow test matched pattern '$pattern' ($reason); running safe subset instead" >&2
260
+ out="$subset"
261
+ fi
262
+ ;;
263
+ esac
264
+ done < <(echo "$KNOWN_SLOW_TESTS" | yq -o=json -I=0 '.[]' 2>/dev/null)
265
+ fi
266
+ echo "$out"
267
+ }
268
+ ```
269
+
270
+ ### Learned test commands <!-- Added: forge#667, forge#1861 -->
271
+
272
+ 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.
273
+
274
+ ```bash
275
+ cd {WORKTREE_PATH}
276
+ LEARNED_TEST_COMMANDS=$(yq -o=json -I=0 '.learned.test_commands // []' forge.yaml 2>/dev/null || echo '[]')
277
+ LEARNED_FAILED=0
278
+ if [ -n "$LEARNED_TEST_COMMANDS" ] && [ "$LEARNED_TEST_COMMANDS" != "[]" ] && [ "$LEARNED_TEST_COMMANDS" != "null" ]; then
279
+ echo "Running learned test commands..."
280
+ while IFS= read -r cmd; do
281
+ [ -z "$cmd" ] && continue
282
+ FILTERED_CMD=$(apply_known_slow_filter "$cmd")
283
+ [ -z "$FILTERED_CMD" ] && continue
284
+ echo "Running learned command: $FILTERED_CMD"
285
+ CMD_OUTPUT=$(run_verification_command "learned.test_commands" "$FILTERED_CMD")
286
+ CMD_EXIT=$?
287
+ echo "$CMD_OUTPUT" | tail -30
288
+ if [ "$CMD_EXIT" -ne 0 ]; then
289
+ echo "FAILED (exit $CMD_EXIT): $FILTERED_CMD"
290
+ LEARNED_FAILED=1
291
+ fi
292
+ done < <(echo "$LEARNED_TEST_COMMANDS" | yq -r '.[]' 2>/dev/null)
293
+ else
294
+ echo "No learned test commands configured — skipping"
295
+ fi
296
+ ```
297
+
298
+ 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.
299
+
216
300
  ---
217
301
 
218
302
  ## Phase V3: Frontend Proxy Wiring Check
@@ -231,7 +315,7 @@ grep -n "127.0.0.1" {CHANGED_TS_FILES}
231
315
 
232
316
  If violations found:
233
317
  1. Fix them in `{WORKTREE_PATH}`
234
- 2. Document fixes in V4 summary
318
+ 2. Document fixes in the V5 summary
235
319
 
236
320
  ---
237
321
 
@@ -338,12 +422,29 @@ Classify:
338
422
 
339
423
  **Skip if**: No new environment variables were introduced in the changed files.
340
424
 
341
- Detect new env vars:
425
+ 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
426
  ```bash
343
427
  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
428
+ ADDED_LINES=$(git diff HEAD -- {CHANGED_FILES} | grep -E '^\+' | grep -v '^+++')
429
+ {
430
+ # Python: os.environ["VAR"], os.getenv("VAR")
431
+ echo "$ADDED_LINES" | grep -oE 'os\.environ\["[^"]+' | sed 's/^os\.environ\["//'
432
+ echo "$ADDED_LINES" | grep -oE "os\.getenv\([\"']?[A-Z_]+" | sed -E "s/^os\.getenv\([\"']?//"
433
+ # TypeScript: process.env.VAR
434
+ echo "$ADDED_LINES" | grep -oE 'process\.env\.[A-Z_]+' | sed 's/^process\.env\.//'
435
+ } | sort -u
436
+ ```
437
+
438
+ **Config variables used by this phase** (set in `forge.yaml`):
439
+ - `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.
440
+ - `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.
441
+
442
+ ```bash
443
+ cd {WORKTREE_PATH}
444
+ SECRETS_BACKEND=$(yq '.deploy.secrets_backend // ""' forge.yaml 2>/dev/null || echo '')
445
+ if [ "$SECRETS_BACKEND" != "sops" ]; then
446
+ echo 'SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.'
447
+ fi
347
448
  ```
348
449
 
349
450
  For each new env var found, verify it is present in ALL required locations:
@@ -351,53 +452,116 @@ For each new env var found, verify it is present in ALL required locations:
351
452
  | Location | Required for |
352
453
  |----------|-------------|
353
454
  | `.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 |
455
+ | Secrets backend (see `deploy.secrets_backend`) | Secret vars — skip if backend is `none` or unset |
456
+ | `app/env_validation.py` | API service vars (if project has one) |
457
+ | `docker-compose.prod.yml` | Vars needing explicit injection (if project uses Docker Compose) |
358
458
 
359
- **Deploy chain**: SOPS → `decrypt-secrets.sh` (ENV_MAPPING) → `.env.secrets` → `merge-env-secrets.sh` → `.env.production` → docker-compose `env_file`.
459
+ **Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*: verify the new var is present in all SOPS chain locations:
460
+ - `infra/secrets/prod.enc.yaml` — SOPS-encrypted secret store
461
+ - `infra/decrypt-secrets.sh` ENV_MAPPING — maps SOPS key to env var name
462
+ - Deploy chain: SOPS → `decrypt-secrets.sh` (ENV_MAPPING) → `.env.secrets` → `merge-env-secrets.sh` → `.env.production` → docker-compose `env_file`
360
463
 
361
464
  If any required location is missing the var:
362
465
  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
466
+ 2. Document the addition in the V5 summary
467
+ 3. These additions are NOT new commits — they are absorbed into the single V5 commit
468
+
469
+ **Operator-set var classification** *(trigger: new env var is NOT in the configured secrets backend)*: <!-- Added: forge#380 -->
470
+
471
+ 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).
472
+
473
+ Resolve the container name for the verification command:
474
+ 1. Look up the service in `forge.yaml → verification.services[]` by name — use the `container` field if present.
475
+ 2. If no matching entry, fall back to the bare service name: `{service}` (no suffix).
476
+
477
+ ```
478
+ - [ ] HARD BLOCKER: Add {VAR_NAME} to the runtime environment on the production server.
479
+ This var is operator-set — it does NOT flow through the automated secrets chain.
480
+ It must be added manually before or after deploy.
481
+ Verify with: docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
482
+ (CONTAINER_NAME resolved from verification.services[{service}].container in forge.yaml,
483
+ or bare service name if not configured)
484
+ ```
485
+
486
+ **`env_file` re-read warning** *(trigger: any new env var added to `.env.production` path)*:
487
+
488
+ > **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.
489
+
490
+ Add this warning to the Testing Checklist whenever a new env var is introduced (whether secret or operator-set).
491
+
492
+ **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.
493
+
494
+ ```bash
495
+ # Verify env var reached the running container (run post-deploy)
496
+ docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
497
+ # Expected: {VAR_NAME}={value}
498
+ # If blank: container was not recreated — run: docker compose up --no-deps --force-recreate {service}
499
+ ```
365
500
 
366
501
  ---
367
502
 
368
- ## Phase V5: Commit (always — after GATE_PASSED=true)
503
+ ## Phase V5: Marker, Commit, Audit, Complete (always — after GATE_PASSED=true)
504
+
505
+ 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.
506
+
507
+ ### V5 Step 1: Post FORGE:QUALITY_GATE Marker (MANDATORY, before the commit) <!-- Added: forge#3061 -->
369
508
 
370
- After the gate passes, commit all staged changes in a single commit. This includes:
509
+ 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.
510
+
511
+ **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).
512
+
513
+ ```bash
514
+ GATE_RESULT=$([ "$GATE_PASSED" = "true" ] && echo PASS || echo FAIL)
515
+ # Bind the PASS to what was actually gated: the staged tree is exactly the tree the V5 commit will have.
516
+ # scripts/verify-phase-trail.sh --head-tree (work-on/review.md R1.5) rejects a PASS recorded for a different tree. <!-- Added: forge#3149 -->
517
+ git -C {WORKTREE_PATH} add -u # same staging as the commit below; valid only because V2-V4 are done
518
+ GATE_TREE=$(git -C {WORKTREE_PATH} write-tree)
519
+ QG_BODY="<!-- FORGE:QUALITY_GATE -->
520
+ ## Quality Gate Result
521
+
522
+ **Result**: ${GATE_RESULT}
523
+ **Tree**: ${GATE_TREE}
524
+ **Iterations**: {N}
525
+ **Commands run**: {quality-gate invocation, format/verify commands, test commands actually executed}
526
+ **Findings remaining**: {none | summary}"
527
+ gh issue comment {NUMBER} {GH_FLAG} --body "$QG_BODY" # allowlist:check-command-side-effects
528
+ ```
529
+
530
+ ### V5 Step 2: Commit
531
+
532
+ After the marker is posted, commit all staged changes in a single commit. This includes:
371
533
  - The implementation changes staged by `implement.md` Phase I4
372
534
  - Any format, proxy, or deploy fixes applied by phases V2–V4
373
535
 
374
536
  ```bash
375
537
  cd {WORKTREE_PATH}
376
538
  git add -u
377
- git commit -s -m "fix({SCOPE}): {description} (#NUMBER)"
539
+ git commit -s -m "fix({SCOPE}): {description} (#{NUMBER})"
378
540
  ```
379
541
 
380
542
  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
543
  - Bug Fix → `fix(`
382
544
  - Feature → `feat(`
383
545
  - Refactor → `refactor(`
546
+ - Docs-only → `docs(`
384
547
 
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.
548
+ 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
549
 
387
550
  **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
551
 
389
- ### V5 Post-Commit Ancestry Audit (MANDATORY)
552
+ ### V5 Step 3: Post-Commit Ancestry Audit (MANDATORY)
390
553
 
391
- After committing, run the ancestry audit to detect merge commits from unrelated branches before the branch is pushed:
554
+ 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
555
 
393
556
  ```bash
394
557
  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
558
+ if git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1; then
559
+ git fetch origin {PR_BASE} >/dev/null 2>&1 || true
560
+ MERGE_COMMITS=$(git log --merges HEAD ^origin/{PR_BASE} 2>/dev/null)
561
+ if [ -n "$MERGE_COMMITS" ]; then
562
+ echo "ANCESTRY AUDIT FAILED: merge commits from unrelated branches detected on this branch:"
563
+ echo "$MERGE_COMMITS"
564
+ ANCESTRY_BODY="## Ancestry Audit Failed
401
565
 
402
566
  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
567
 
@@ -409,34 +573,52 @@ ${MERGE_COMMITS}
409
573
  Human review required before this branch can be pushed.
410
574
 
411
575
  <!-- 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
576
+ gh issue comment {NUMBER} {GH_FLAG} --body "$ANCESTRY_BODY" # allowlist:check-command-side-effects
577
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
578
+ echo "ANCESTRY_FAILED=1"
579
+ fi
580
+ else
581
+ echo "PR_BASE not on origin — skipping ancestry audit"
415
582
  fi
416
583
  ```
417
584
 
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
- ```
585
+ 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.
586
+
587
+ ### V5 Step 4: Mark Build Complete and Record Verification Status (MANDATORY)
588
+
589
+ 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
590
 
423
- ### V5 Post-Commit: Mark Build Complete (MANDATORY)
591
+ `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
592
 
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 -->
593
+ **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
594
 
427
595
  ```bash
596
+ # SKIPPED_CHECKS: set from the V2 notes, e.g. SKIPPED_CHECKS="python.format, typescript.typecheck/build"
597
+ SKIPPED_CHECKS="${SKIPPED_CHECKS:-}"
598
+ if [ -z "$SKIPPED_CHECKS" ]; then
599
+ VERIFICATION_STATUS="✅ All configured verification commands passed"
600
+ else
601
+ VERIFICATION_STATUS="⚠ Verification NOT run: ${SKIPPED_CHECKS} — verification.commands not configured for these checks"
602
+ fi
603
+
428
604
  # Find the FORGE:BUILDER comment posted by implement.md Phase I6
429
605
  BUILDER_COMMENT_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
430
606
  --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
431
607
 
432
608
  if [ -n "$BUILDER_COMMENT_ID" ]; then
433
- # Fetch current body and append the completion marker plus best-effort cost signal
434
609
  CURRENT_BODY=$(gh api repos/{GH_REPO}/issues/comments/$BUILDER_COMMENT_ID --jq '.body')
610
+ # Insert the Verification Status line after the "**Files changed**" line (idempotent).
611
+ if printf '%s' "$CURRENT_BODY" | grep -q '^\*\*Verification Status\*\*'; then
612
+ PATCHED_BODY="$CURRENT_BODY"
613
+ else
614
+ PATCHED_BODY=$(printf '%s\n' "$CURRENT_BODY" | awk -v vs="**Verification Status**: ${VERIFICATION_STATUS}" \
615
+ '{print} /^\*\*Files changed\*\*/ && !done {print vs; done=1}')
616
+ fi
435
617
  # Best-effort cost append: only include if session telemetry provides a value; never block
436
618
  PHASE_COST_LINE=""
437
619
  [ -n "${PHASE_COST_USD:-}" ] && PHASE_COST_LINE="
438
620
  cost_usd: ${PHASE_COST_USD}"
439
- UPDATED_BODY="${CURRENT_BODY}${PHASE_COST_LINE}
621
+ UPDATED_BODY="${PATCHED_BODY}${PHASE_COST_LINE}
440
622
 
441
623
  <!-- FORGE:BUILDER:COMPLETE -->"
442
624
  gh api repos/{GH_REPO}/issues/comments/$BUILDER_COMMENT_ID \
@@ -448,13 +630,13 @@ else
448
630
  fi
449
631
  ```
450
632
 
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`.
633
+ **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
634
 
453
635
  ---
454
636
 
455
637
  ## Output
456
638
 
457
- Return structured output to the caller:
639
+ The final reply is exactly one `VALIDATE_RESULT:` block — on every exit path (success, skip path, and every failure):
458
640
 
459
641
  ```
460
642
  VALIDATE_RESULT:
@@ -472,18 +654,17 @@ VALIDATE_RESULT:
472
654
 
473
655
  ---
474
656
 
475
- ## Integration Point in work-on.md
657
+ ## Integration Point
476
658
 
477
- This module runs at **Step 3F.5** — after implement, before commit (3J) and PR creation (Phase 4):
659
+ This module is invoked by `work-on:build` after implement and before review:
478
660
 
479
661
  ```
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)
662
+ implement (work-on:build:implement) — code written, staged (not committed), FORGE:BUILDER posted
663
+ → [THIS MODULE] validate
664
+ V0 self-check → V1 quality-gate loop (Skill quality-gate, forked) → V2 format/verify + known-slow/learned tests
665
+ → V3 proxy wiring → V3.5 DB advisory → V3.6 browser signals → V4 deploy completeness
666
+ → V5 marker → commit → ancestry audit → BUILDER:COMPLETE
667
+ review (work-on:review) — push, PR, merge
487
668
  ```
488
669
 
489
- If `VALIDATE_RESULT: gate_passed: false`, the router adds `needs-human` label and stops — no PR is created.
670
+ If `VALIDATE_RESULT: gate_passed: false`, the build skill reports BLOCKED (`needs-human` is set) and no PR is created.