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.
- package/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +1 -0
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +3 -2
- package/commands/orchestrate/phase-1-resolve.md +57 -12
- package/commands/orchestrate/phase-2-triage.md +1 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
- package/commands/orchestrate/phase-3-dependency.md +17 -5
- package/commands/orchestrate/phase-4-execution.md +625 -78
- package/commands/orchestrate/phase-5-cleanup.md +5 -2
- package/commands/orchestrate/phase-6-report.md +8 -3
- package/commands/orchestrate/safety.md +1 -0
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/config.md +1 -0
- package/commands/pipeline-health/phase-1-context.md +1 -0
- package/commands/pipeline-health/phase-2-metrics.md +1 -0
- package/commands/pipeline-health/phase-3-analyze.md +1 -0
- package/commands/pipeline-health/phase-4-proposals.md +7 -4
- package/commands/pipeline-health/phase-5-report.md +5 -2
- package/commands/pipeline-health/phase-6-summary.md +1 -0
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +219 -13
- package/commands/review-pr-agents/api.md +1 -0
- package/commands/review-pr-agents/auth.md +1 -0
- package/commands/review-pr-agents/billing.md +1 -0
- package/commands/review-pr-agents/concurrency.md +1 -0
- package/commands/review-pr-agents/database.md +1 -0
- package/commands/review-pr-agents/frontend.md +1 -0
- package/commands/review-pr-agents/infra.md +1 -0
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents/scraper.md +1 -0
- package/commands/review-pr-agents/security.md +1 -0
- package/commands/review-pr-agents/spec-cli.md +1 -0
- package/commands/review-pr-agents.md +10 -4
- package/commands/review-pr-staging.md +14 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +3 -1
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +82 -19
- package/commands/work-on/build/context.md +62 -18
- package/commands/work-on/build/implement.md +63 -18
- package/commands/work-on/build/validate.md +241 -60
- package/commands/work-on/build.md +345 -146
- package/commands/work-on/close.md +618 -246
- package/commands/work-on/decompose.md +240 -41
- package/commands/work-on/investigate.md +378 -61
- package/commands/work-on/remediate.md +43 -9
- package/commands/work-on/review.md +241 -29
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +21 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
- 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: "
|
|
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
|
|
13
|
-
**Output**:
|
|
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
|
-
- `--
|
|
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
|
|
36
|
-
- Only 1 file was changed AND it
|
|
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
|
|
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
|
-
→
|
|
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`,
|
|
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
|
|
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
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
| `
|
|
355
|
-
| `
|
|
356
|
-
| `
|
|
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
|
-
**
|
|
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
|
|
364
|
-
3. These additions are NOT new commits — they
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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 `
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="${
|
|
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
|
|
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
|
-
|
|
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
|
|
657
|
+
## Integration Point
|
|
476
658
|
|
|
477
|
-
This module
|
|
659
|
+
This module is invoked by `work-on:build` after implement and before review:
|
|
478
660
|
|
|
479
661
|
```
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
670
|
+
If `VALIDATE_RESULT: gate_passed: false`, the build skill reports BLOCKED (`needs-human` is set) and no PR is created.
|