forgedock 1.18.0 → 1.20.0
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/README.md +2 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +54 -4
- package/bin/engine/runlog.mjs +10 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +32 -2
- package/bin/engine.mjs +187 -10
- package/bin/runner.mjs +89 -13
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +136 -28
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +201 -40
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr-staging.md +4 -4
- package/commands/review-pr.md +31 -20
- package/commands/work-on/build/architect.md +54 -1
- package/commands/work-on/build/implement.md +28 -1
- package/commands/work-on/build/validate.md +321 -29
- package/commands/work-on/build.md +184 -17
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/investigate.md +7 -0
- package/commands/work-on/remediate.md +43 -17
- package/commands/work-on/review.md +43 -22
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +93 -2
- package/docs/OUTCOME-CODES.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/STATS.md +11 -4
- package/docs/TELEMETRY.md +5 -2
- package/docs/WORK-ON-RUNTIME.md +29 -3
- package/package.json +2 -2
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/cap-findings.test.sh +2 -0
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-markers.sh +4 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +379 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +451 -0
- package/scripts/ci-local/scoping.mjs +531 -0
- package/scripts/ci-local.sh +98 -0
- package/scripts/ci-local.test.sh +1108 -0
- package/scripts/classify-finding.sh +33 -7
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +2 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/lib/telemetry.sh +3 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +294 -53
- package/scripts/review-finalize.test.sh +347 -9
- package/scripts/review-pr-delta-wiring.test.sh +13 -0
- package/scripts/review-prepare.sh +5 -3
- package/scripts/review-prepare.test.sh +18 -2
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +10 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/trusted-comments.test.sh +2 -1
- package/scripts/verify-phase-trail.test.sh +14 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +41 -3
- package/scripts/with-run-tmpdir.sh +20 -0
|
@@ -36,6 +36,7 @@ Parse from $ARGUMENTS:
|
|
|
36
36
|
- `--branch {BRANCH}` — feature branch name (required)
|
|
37
37
|
- `--base {PR_BASE}` — PR base branch name without the `origin/` prefix (required; used by the V5 ancestry audit)
|
|
38
38
|
- `--files {CHANGED_FILES}` — space-separated list of changed files (required; from the implement result)
|
|
39
|
+
- `--consumers-final` — optional. Set by build B6 on the re-validation after the one consumer repair round: V4.6 then runs in `advisory` mode whatever forge.yaml says, so remaining unaddressed sites are recorded as `CONSUMERS: unresolved` instead of failing again. <!-- Added: forge#4017 -->
|
|
39
40
|
|
|
40
41
|
**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.
|
|
41
42
|
|
|
@@ -43,7 +44,7 @@ Parse from $ARGUMENTS:
|
|
|
43
44
|
|
|
44
45
|
## Skip Conditions
|
|
45
46
|
|
|
46
|
-
Skip Phases V0–V4.
|
|
47
|
+
Skip Phases V0–V4.8 (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:
|
|
47
48
|
- 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" -->
|
|
48
49
|
|
|
49
50
|
In all other cases, the gate MUST run.
|
|
@@ -155,9 +156,18 @@ Run after quality gate passes. All tool commands are read from `forge.yaml → v
|
|
|
155
156
|
|
|
156
157
|
**Scratch placement (venvs, caches)** <!-- Added: forge#3572 -->: any virtualenv, package cache or other large scratch data a verification command needs goes under `"$TMPDIR/forge-venv-{NUMBER}"` (the engine exports a per-run, disk-backed `TMPDIR`) or inside the worktree, never a literal `/tmp/<name>` path. A size-limited `/tmp` fails concurrent lanes with EDQUOT/ENOSPC. Remove what you created when validation finishes.
|
|
157
158
|
|
|
158
|
-
**Track skipped checks** — initialize before any check runs:
|
|
159
|
+
**Track skipped and degraded checks** — initialize before any check runs. `SKIPPED_CHECKS` is advisory (an unconfigured check). `DEGRADED_CHECKS` is a different state: a check that was configured or required but demonstrably verified nothing (a test run that ran nothing, a known-slow `skip`, V4.5 `DEGRADED`). It is carried to the merge gate (V5, `gate_status`). <!-- Added: forge#3628 -->
|
|
159
160
|
```bash
|
|
160
161
|
SKIPPED_CHECKS=""
|
|
162
|
+
DEGRADED_CHECKS="" # "name — reason; name — reason"; never mixed into SKIPPED_CHECKS
|
|
163
|
+
|
|
164
|
+
# degrade_check <name> <reason> — record a degraded check and print the carry-forward line (each Bash call is a fresh shell).
|
|
165
|
+
degrade_check() {
|
|
166
|
+
local name="${1}" reason
|
|
167
|
+
reason=$(printf '%s' "${2}" | tr -d ';\r\n')
|
|
168
|
+
DEGRADED_CHECKS="${DEGRADED_CHECKS:+$DEGRADED_CHECKS; }${name} — ${reason}"
|
|
169
|
+
echo "DEGRADED_CHECKS+=${name} — ${reason}"
|
|
170
|
+
}
|
|
161
171
|
|
|
162
172
|
# Verification commands are project configuration; bound each one so a stalled
|
|
163
173
|
# formatter, typechecker, or build cannot block validation indefinitely.
|
|
@@ -274,36 +284,82 @@ apply_known_slow_filter() {
|
|
|
274
284
|
}
|
|
275
285
|
```
|
|
276
286
|
|
|
277
|
-
###
|
|
287
|
+
### Test commands: classified runs <!-- Added: forge#667, forge#1861, forge#3628 -->
|
|
288
|
+
|
|
289
|
+
After all format/typecheck steps complete, run every test command: `forge.yaml → verification.commands.<lang>.test` (when configured) and every `forge.yaml → learned.test_commands` entry (captured from owner corrections or set manually), each filtered through the known-slow gate above. This phase reads `learned:` itself, no value is passed in from the caller.
|
|
278
290
|
|
|
279
|
-
|
|
291
|
+
A non-zero exit is a real failure and BLOCKING. A zero exit is not proof the tests ran: every test command's exit code and captured output go through `scripts/classify-test-run.sh <exit_code> <output_file>`, which prints `RAN`, `RAN_NOTHING` or `FAILED`. `RAN_NOTHING` (pytest exit 5, "no tests ran/collected", every collected test skipped, or an unreachable DB/service that turned tests into skips) is NOT a pass: the command is recorded in `DEGRADED_CHECKS` and the build continues as DEGRADED. A command skipped by the known-slow gate (`skip: true`) is also recorded in `DEGRADED_CHECKS`. Only test commands are classified; format, typecheck and build commands never are, so a docs-only repo never gets a false DEGRADED. The classifier resolves from `FORGE_ROOT` (never the worktree); an unresolvable classifier is DEGRADED for that command, never a pass.
|
|
280
292
|
|
|
281
293
|
```bash
|
|
282
294
|
cd {WORKTREE_PATH}
|
|
295
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
296
|
+
CLASSIFY_SCRIPT=$(forge_resolve classify-test-run.sh)
|
|
283
297
|
FORGE_CFG=forge.yaml; [ -f "$FORGE_CFG" ] || { _gcd=$(git rev-parse --git-common-dir 2>/dev/null) && _gcd=$(cd "$_gcd" 2>/dev/null && pwd) && [ -f "${_gcd%/.git}/forge.yaml" ] && FORGE_CFG="${_gcd%/.git}/forge.yaml"; } # gitignored forge.yaml exists only in the main checkout, not in worktrees
|
|
284
|
-
|
|
298
|
+
TEST_FAILED=0
|
|
285
299
|
LEARNED_FAILED=0
|
|
300
|
+
|
|
301
|
+
# run_classified_test <label> <command> — runs one test command, prints its tail, classifies the run.
|
|
302
|
+
# Sets TEST_FAILED=1 on FAILED (or any non-zero exit); records DEGRADED_CHECKS on RAN_NOTHING or an unresolvable classifier.
|
|
303
|
+
run_classified_test() {
|
|
304
|
+
local label="${1}" cmd="${2}" out_file cls
|
|
305
|
+
out_file=$(mktemp "${TMPDIR:-/tmp}/forge-test-run.XXXXXX")
|
|
306
|
+
run_verification_command "$label" "$cmd" > "$out_file" 2>&1
|
|
307
|
+
CMD_EXIT=$?
|
|
308
|
+
tail -30 "$out_file"
|
|
309
|
+
if [ "$CMD_EXIT" -eq 124 ]; then
|
|
310
|
+
# forge#4037: a wall-clock timeout proves nothing about the code: DEGRADED (recorded, review informed), never FAILED / needs-human.
|
|
311
|
+
degrade_check "$label" "test command timed out (exit 124) so the tests are unproven"
|
|
312
|
+
elif [ "$CMD_EXIT" -ne 0 ] && [ "$CMD_EXIT" -ne 5 ]; then
|
|
313
|
+
echo "FAILED (exit $CMD_EXIT): $cmd"
|
|
314
|
+
TEST_FAILED=1
|
|
315
|
+
elif [ -z "$CLASSIFY_SCRIPT" ]; then
|
|
316
|
+
degrade_check "$label" "classify-test-run.sh not found under FORGE_ROOT; could not confirm tests ran"
|
|
317
|
+
else
|
|
318
|
+
cls=$(bash "$CLASSIFY_SCRIPT" "$CMD_EXIT" "$out_file" 2>/dev/null) || cls=""
|
|
319
|
+
case "$cls" in
|
|
320
|
+
RAN) ;;
|
|
321
|
+
RAN_NOTHING) degrade_check "$label" "test run exercised nothing (exit ${CMD_EXIT}: no tests ran, all skipped, or service unavailable)" ;;
|
|
322
|
+
FAILED) echo "FAILED (exit $CMD_EXIT): $cmd"; TEST_FAILED=1 ;;
|
|
323
|
+
*) degrade_check "$label" "classify-test-run.sh returned no classification" ;;
|
|
324
|
+
esac
|
|
325
|
+
fi
|
|
326
|
+
rm -f "$out_file"
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# verification.commands.<lang>.test (when configured). forge#4040: ALWAYS runs, even when the change-scoped local CI gate (V4.8)
|
|
330
|
+
# resolves: ci-local runs the repo's CI-defined jobs and never this configured command, so skipping it here would let a
|
|
331
|
+
# failing suite pass. A timeout (rc 124) stays DEGRADED via run_classified_test.
|
|
332
|
+
_LANGS="python typescript"
|
|
333
|
+
for _lang in $_LANGS; do
|
|
334
|
+
TEST_CMD=$(yq ".verification.commands.${_lang}.test // \"\"" "$FORGE_CFG" 2>/dev/null || echo '')
|
|
335
|
+
[ -n "$TEST_CMD" ] || continue
|
|
336
|
+
FILTERED_CMD=$(apply_known_slow_filter "$TEST_CMD")
|
|
337
|
+
if [ -z "$FILTERED_CMD" ]; then degrade_check "${_lang}.test" "skipped by verification.known_slow_tests"; continue; fi
|
|
338
|
+
echo "Running ${_lang}.test: $FILTERED_CMD"
|
|
339
|
+
run_classified_test "${_lang}.test" "$FILTERED_CMD"
|
|
340
|
+
done
|
|
341
|
+
|
|
342
|
+
# learned.test_commands
|
|
343
|
+
LEARNED_TEST_COMMANDS=$(yq -o=json -I=0 '.learned.test_commands // []' "$FORGE_CFG" 2>/dev/null || echo '[]')
|
|
286
344
|
if [ -n "$LEARNED_TEST_COMMANDS" ] && [ "$LEARNED_TEST_COMMANDS" != "[]" ] && [ "$LEARNED_TEST_COMMANDS" != "null" ]; then
|
|
287
345
|
echo "Running learned test commands..."
|
|
346
|
+
LEARNED_IDX=0
|
|
288
347
|
while IFS= read -r cmd; do
|
|
289
348
|
[ -z "$cmd" ] && continue
|
|
349
|
+
LEARNED_IDX=$((LEARNED_IDX+1))
|
|
290
350
|
FILTERED_CMD=$(apply_known_slow_filter "$cmd")
|
|
291
|
-
[ -z "$FILTERED_CMD" ]
|
|
351
|
+
if [ -z "$FILTERED_CMD" ]; then degrade_check "learned.test_commands[${LEARNED_IDX}]" "skipped by verification.known_slow_tests"; continue; fi
|
|
292
352
|
echo "Running learned command: $FILTERED_CMD"
|
|
293
|
-
|
|
294
|
-
CMD_EXIT=$?
|
|
295
|
-
echo "$CMD_OUTPUT" | tail -30
|
|
296
|
-
if [ "$CMD_EXIT" -ne 0 ]; then
|
|
297
|
-
echo "FAILED (exit $CMD_EXIT): $FILTERED_CMD"
|
|
298
|
-
LEARNED_FAILED=1
|
|
299
|
-
fi
|
|
353
|
+
run_classified_test "learned.test_commands[${LEARNED_IDX}]" "$FILTERED_CMD"
|
|
300
354
|
done < <(echo "$LEARNED_TEST_COMMANDS" | yq -r '.[]' 2>/dev/null)
|
|
301
355
|
else
|
|
302
356
|
echo "No learned test commands configured — skipping"
|
|
303
357
|
fi
|
|
358
|
+
[ "$TEST_FAILED" -eq 1 ] && LEARNED_FAILED=1
|
|
359
|
+
echo "TEST_FAILED=${TEST_FAILED}"
|
|
304
360
|
```
|
|
305
361
|
|
|
306
|
-
|
|
362
|
+
The configured `verification.commands.<lang>.test` always runs here, whether or not V4.8's `ci-local.sh` resolves (forge#4040): the local CI gate never runs that command, so a failing configured suite must fail V2 on its own. Only a wall-clock timeout (rc 124) is DEGRADED. Test command failures (`TEST_FAILED=1`, also reported as `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, but it is DEGRADED, not verified. Each `DEGRADED_CHECKS+=` line printed here is a carry-forward line: record it in your notes, V5 Step 1 and Step 4 rebuild `DEGRADED_CHECKS` from them (a fresh shell does not keep the variable).
|
|
307
363
|
|
|
308
364
|
---
|
|
309
365
|
|
|
@@ -516,8 +572,8 @@ The architect records the caller and sibling sweep as a machine-readable manifes
|
|
|
516
572
|
|
|
517
573
|
- **Trust**: the manifest drives a gate, so it is read only from the newest comment that `trusted-comments.sh` accepts (the same trust predicate as the phase-trail verifier). An untrusted comment is never parsed.
|
|
518
574
|
- **Install root only**: both scripts resolve from `FORGE_ROOT` and never from the worktree, because the branch under audit must not supply its own gate. An unresolvable script is DEGRADED, never a pass.
|
|
519
|
-
- **Exit codes**: `0` covered or no manifest (SKIP); `1` an `UNLISTED:` or `NOT_DONE:` line was printed, so set `GATE_PASSED=false`, fix the code (or add a justified `verified-unaffected` row to the plan by editing the FORGE:ARCHITECT comment; never delete a row), and re-run V1 onward; anything else, a failed fetch, or an unresolvable script is DEGRADED:
|
|
520
|
-
- **Carry-forward**: each Bash call is a fresh shell, so V4.5's variables do not reach V5. The block prints its result as explicit lines: `BLAST_RADIUS_STATE: <PASS|SKIP|FAIL|DEGRADED>`, plus `GATE_PASSED: false` on FAIL and `
|
|
575
|
+
- **Exit codes**: `0` covered or no manifest (SKIP); `1` an `UNLISTED:` or `NOT_DONE:` line was printed, so set `GATE_PASSED=false`, fix the code (or add a justified `verified-unaffected` row to the plan by editing the FORGE:ARCHITECT comment; never delete a row), and re-run V1 onward; anything else, a failed fetch, or an unresolvable script is DEGRADED: record `blast-radius` in `DEGRADED_CHECKS` (not the advisory `SKIPPED_CHECKS`) with the reason, so the merge gate sees it. DEGRADED never passes silently and a failed fetch is never treated as "no manifest".
|
|
576
|
+
- **Carry-forward**: each Bash call is a fresh shell, so V4.5's variables do not reach V5. The block prints its result as explicit lines: `BLAST_RADIUS_STATE: <PASS|SKIP|FAIL|DEGRADED>`, plus `GATE_PASSED: false` on FAIL and `DEGRADED_CHECKS+=blast-radius — <reason>` on DEGRADED. Record those lines in your notes; V5 Step 1 sets `GATE_PASSED` and `DEGRADED_CHECKS` from them, alongside the V2 notes.
|
|
521
577
|
|
|
522
578
|
```bash
|
|
523
579
|
cd {WORKTREE_PATH}
|
|
@@ -564,8 +620,8 @@ echo "BLAST_RADIUS_STATE: ${BR_STATE}"
|
|
|
564
620
|
if [ "$BR_STATE" = "FAIL" ]; then
|
|
565
621
|
echo "GATE_PASSED: false"
|
|
566
622
|
elif [ "$BR_STATE" = "DEGRADED" ]; then
|
|
567
|
-
|
|
568
|
-
echo "
|
|
623
|
+
# self-contained (fresh shell): same line format as V2's degrade_check
|
|
624
|
+
echo "DEGRADED_CHECKS+=blast-radius — $(printf '%s' "${BR_NOTE:-blast-radius check did not complete}" | tr -d ';\r\n')"
|
|
569
625
|
fi
|
|
570
626
|
```
|
|
571
627
|
|
|
@@ -573,6 +629,198 @@ On `FAIL`, the blocker line for `VALIDATE_RESULT` is `blast-radius: <UNLISTED/NO
|
|
|
573
629
|
|
|
574
630
|
---
|
|
575
631
|
|
|
632
|
+
## Phase V4.6: Consumer Sweep *(runs after V4.5, before V5 — skipped only on the docs-only skip path)* <!-- Added: forge#3970 -->
|
|
633
|
+
|
|
634
|
+
A changed or removed identifier (function, class, constant, config or context key, CLI flag, env var, path, literal hash or version) leaves stale references in files the diff never touched. This phase runs `consumer-sweep.sh`, a script and not a prompt: it extracts the identifiers the final diff actually removed, renamed or re-typed (a per-file survival filter: tokens that still appear on the file's added lines, prose words and versions are never swept) and lists every reference outside the change set in ONE bounded `git grep` (generated and vendored paths excluded through the `build.scope.exclude_globs` set, read as in build B5.5). Every listed site must be justified by a `unaffected: <reason>` line in the `CONSUMERS:` block of the FORGE:BUILDER comment (grammar in implement I6); a site in a changed file is already addressed.
|
|
635
|
+
|
|
636
|
+
- **Trust and install root**: the same rules as V4.5. The BUILDER body is read only from the newest comment `trusted-comments.sh` accepts, and both scripts resolve from `FORGE_ROOT`, never from the worktree.
|
|
637
|
+
- **Mode and budget** <!-- Added: forge#4017 -->: `verification.consumer_sweep.mode` in forge.yaml (`advisory` | `blocking`, default `advisory`; see `docs/CONFIG.md`) and `verification.consumer_sweep.budget_seconds` (default 60) are passed as `--mode` and `--budget-seconds`. In `advisory` mode the sweep records sites (`ADVISORY:` lines) and never fails. `BUDGET_EXCEEDED` is always advisory: the sweep stopped early, so the run is a PASS with a note and never a FAIL. A removed function/class whose name is still defined outside the change set is reported `ambiguous` and is not a site.
|
|
638
|
+
- **Exit codes**: `0` all sites addressed or none, advisory mode, or `BUDGET_EXCEEDED` (PASS); `1` (`blocking` mode only) an `UNADDRESSED:` line was printed, so set `GATE_PASSED=false` and print `CONSUMER_SWEEP_STATE: FAIL`: this is a repair, not a block. Validate does NOT add `needs-human` for it and returns `blocker: consumer_unaddressed: ...`; build B6 runs ONE implement repair round (`--fix-consumers`) and re-validates, after which still-unresolved sites are recorded as `CONSUMERS: unresolved ...` in FORGE:BUILDER and passed to review as known risk. Anything else, a failed fetch, or an unresolvable script is DEGRADED: append `consumer-sweep` to `SKIPPED_CHECKS` and print the reason. DEGRADED never passes silently and a failed read is never treated as "no sites". A missing BUILDER comment is not a pass: the sweep still runs with an empty justification set.
|
|
639
|
+
- **Carry-forward**: as V4.5, the block prints `CONSUMER_SWEEP_STATE: <PASS|FAIL|DEGRADED>`, plus `GATE_PASSED: false` on FAIL and `SKIPPED_CHECKS+=consumer-sweep` on DEGRADED, and one `CONSUMERS_UNRESOLVED: - <file:line> unresolved: <identifier>` line per site left unaddressed in advisory mode (including the forced-advisory `--consumers-final` run). Record those lines; V5 Step 1 and Step 4 read them.
|
|
640
|
+
|
|
641
|
+
```bash
|
|
642
|
+
cd {WORKTREE_PATH}
|
|
643
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
644
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
645
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
646
|
+
CS_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/consumer-sweep.sh}"; [ -f "$CS_SCRIPT" ] || CS_SCRIPT=""
|
|
647
|
+
TRUST_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/trusted-comments.sh}"; [ -f "$TRUST_SCRIPT" ] || TRUST_SCRIPT=""
|
|
648
|
+
CS_STATE=""; CS_NOTE=""; CS_FILE=$(mktemp "${TMPDIR:-/tmp}/forge-consumers.XXXXXX")
|
|
649
|
+
# mode and budget from forge.yaml; anything unreadable or invalid falls back to the safe defaults (advisory, 60 s)
|
|
650
|
+
CS_MODE=$(yq '.verification.consumer_sweep.mode // "advisory"' forge.yaml 2>/dev/null || true)
|
|
651
|
+
case "$CS_MODE" in advisory|blocking) ;; *) CS_MODE=advisory ;; esac
|
|
652
|
+
[ "${CONSUMERS_FINAL:-}" != "1" ] || CS_MODE=advisory # set CONSUMERS_FINAL=1 before this block when invoked with --consumers-final
|
|
653
|
+
CS_BUDGET=$(yq '.verification.consumer_sweep.budget_seconds // 60' forge.yaml 2>/dev/null || true)
|
|
654
|
+
case "$CS_BUDGET" in ""|*[!0-9]*) CS_BUDGET=60 ;; esac
|
|
655
|
+
if [ -z "$CS_SCRIPT" ] || [ -z "$TRUST_SCRIPT" ]; then
|
|
656
|
+
CS_STATE="DEGRADED"; CS_NOTE="consumer-sweep.sh or trusted-comments.sh not found under FORGE_ROOT"
|
|
657
|
+
else
|
|
658
|
+
COMMENTS_JSON=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments 2>/dev/null); FETCH_RC=$?
|
|
659
|
+
if [ "$FETCH_RC" -ne 0 ]; then
|
|
660
|
+
CS_STATE="DEGRADED"; CS_NOTE="could not fetch issue comments (rc=${FETCH_RC})"
|
|
661
|
+
else
|
|
662
|
+
BODIES=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUST_SCRIPT" bodies '^<!-- FORGE:BUILDER'); TRUST_RC=$?
|
|
663
|
+
if [ "$TRUST_RC" -ne 0 ]; then
|
|
664
|
+
CS_STATE="DEGRADED"; CS_NOTE="trusted-comments.sh failed (rc=${TRUST_RC})"
|
|
665
|
+
else
|
|
666
|
+
# one JSON string per line, newest last; no trusted BUILDER comment leaves the justification file empty
|
|
667
|
+
if [ -n "$BODIES" ]; then printf '%s\n' "$BODIES" | tail -n 1 | jq -r . > "$CS_FILE"; JQ_RC=$?; else JQ_RC=0; fi
|
|
668
|
+
EXCLUDES=()
|
|
669
|
+
while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
|
|
670
|
+
< <(yq '(.build.scope.exclude_globs // []) + (.build.diff_size.exclude_globs // []) | .[]' forge.yaml 2>/dev/null || true)
|
|
671
|
+
if [ "$JQ_RC" -ne 0 ]; then
|
|
672
|
+
CS_STATE="DEGRADED"; CS_NOTE="could not decode the FORGE:BUILDER comment (rc=${JQ_RC})"
|
|
673
|
+
else
|
|
674
|
+
git fetch origin {PR_BASE} >/dev/null 2>&1 || true
|
|
675
|
+
CS_OUT=$(bash "$CS_SCRIPT" --range "origin/{PR_BASE}..WORKTREE" --repo "{WORKTREE_PATH}" --consumers "$CS_FILE" --mode "$CS_MODE" --budget-seconds "$CS_BUDGET" ${EXCLUDES[@]+"${EXCLUDES[@]}"} 2>&1); CS_RC=$?
|
|
676
|
+
printf '%s\n' "$CS_OUT"
|
|
677
|
+
# carry-forward for V5 Step 4: every unaddressed site becomes a "CONSUMERS: unresolved" line in FORGE:BUILDER
|
|
678
|
+
printf '%s\n' "$CS_OUT" | sed -n 's/^ADVISORY: \([^ ]*\) \(.*\)$/CONSUMERS_UNRESOLVED: - \2 unresolved: \1/p' | head -n 50
|
|
679
|
+
case "$CS_RC" in
|
|
680
|
+
0) CS_STATE="PASS"
|
|
681
|
+
case "$CS_OUT" in *BUDGET_EXCEEDED*) CS_NOTE="budget exceeded, partial sweep (advisory)" ;; *ADVISORY:*) CS_NOTE="advisory sites recorded (mode=advisory)" ;; esac ;;
|
|
682
|
+
1) CS_STATE="FAIL"; GATE_PASSED=false ;;
|
|
683
|
+
*) CS_STATE="DEGRADED"; CS_NOTE="consumer-sweep.sh exited ${CS_RC}" ;;
|
|
684
|
+
esac
|
|
685
|
+
fi
|
|
686
|
+
fi
|
|
687
|
+
fi
|
|
688
|
+
fi
|
|
689
|
+
rm -f "$CS_FILE"
|
|
690
|
+
echo "CONSUMER_SWEEP: ${CS_STATE}${CS_NOTE:+ — $CS_NOTE}"
|
|
691
|
+
# Carry-forward lines for V5 (fresh shell): read them back in V5 Step 1 and Step 4.
|
|
692
|
+
echo "CONSUMER_SWEEP_STATE: ${CS_STATE}"
|
|
693
|
+
if [ "$CS_STATE" = "FAIL" ]; then
|
|
694
|
+
echo "GATE_PASSED: false"
|
|
695
|
+
elif [ "$CS_STATE" = "DEGRADED" ]; then
|
|
696
|
+
SKIPPED_CHECKS="${SKIPPED_CHECKS:+$SKIPPED_CHECKS, }consumer-sweep"
|
|
697
|
+
echo "SKIPPED_CHECKS+=consumer-sweep"
|
|
698
|
+
fi
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
On `FAIL` (`blocking` mode only), the blocker line for `VALIDATE_RESULT` is `consumer_unaddressed: <UNADDRESSED lines>` and no `needs-human` label is added for it. Build B6 runs one repair round: implement updates the listed files or adds a justified `unaffected:` line to the BUILDER `CONSUMERS:` block, never edits the checker; sites still unaddressed after that round are recorded as `CONSUMERS: unresolved` (implement I6) and continue to review as known risk. In `advisory` mode the `ADVISORY:` lines are the record and nothing fails.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## Phase V4.7: Edge-Case Coverage *(runs after V4.6, before V5 — skipped only on the docs-only skip path)* <!-- Added: forge#3631 -->
|
|
706
|
+
|
|
707
|
+
The architect lists error, edge and concurrency cases as `EDGE:` records between `<!-- FORGE:EDGE_CASES:BEGIN -->` and `<!-- FORGE:EDGE_CASES:END -->`; the builder maps each id to an `EDGE_TEST:` line in its FORGE:BUILDER comment (grammar in architect A4.5 and implement I6). This phase runs `check-edge-cases.sh`, a script and not a prompt: an id is covered by a non-empty `untestable=` reason or by a `test=<path>::<name>` whose name occurs in a line the change adds to that file. A test that already existed never satisfies a new case.
|
|
708
|
+
|
|
709
|
+
- **Trust and install root**: as V4.5. Both bodies are read only from the newest comment `trusted-comments.sh` accepts, and both scripts resolve from `FORGE_ROOT`, never from the worktree.
|
|
710
|
+
- **Exit codes**: `0` covered, or an absent or empty block (SKIP/PASS); `1` an `UNMAPPED:`, `TEST_NOT_IN_DIFF:`, `EMPTY_REASON:` or `DUPLICATE_ID:` line was printed, so set `GATE_PASSED=false`, add the test or a real `untestable=` reason (edit the FORGE:BUILDER comment), and re-run V1 onward; anything else, a failed fetch, a missing BUILDER comment or an unresolvable script is DEGRADED: record `edge-cases` in `DEGRADED_CHECKS` with the reason. A covered case that maps to a test is also DEGRADED when the V2 test run printed a `DEGRADED_CHECKS+=` line for tests (the mapped tests could not run; set `TEST_RUNNER_DEGRADED=1` from your V2 notes). DEGRADED never passes silently and a failed read is never treated as "no edge cases".
|
|
711
|
+
- **Carry-forward**: as V4.5, the block prints `EDGE_CASES_STATE: <PASS|SKIP|FAIL|DEGRADED>`, plus `GATE_PASSED: false` on FAIL and `DEGRADED_CHECKS+=edge-cases — <reason>` on DEGRADED. Record those lines; V5 Step 1 reads them.
|
|
712
|
+
|
|
713
|
+
```bash
|
|
714
|
+
cd {WORKTREE_PATH}
|
|
715
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
716
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
717
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
718
|
+
EC_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-edge-cases.sh}"; [ -f "$EC_SCRIPT" ] || EC_SCRIPT=""
|
|
719
|
+
TRUST_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/trusted-comments.sh}"; [ -f "$TRUST_SCRIPT" ] || TRUST_SCRIPT=""
|
|
720
|
+
TEST_RUNNER_DEGRADED="${TEST_RUNNER_DEGRADED:-0}" # set to 1 from the V2 notes when a test run printed DEGRADED_CHECKS+=
|
|
721
|
+
EC_STATE=""; EC_NOTE=""; EC_ARCH=$(mktemp "${TMPDIR:-/tmp}/forge-edge-arch.XXXXXX"); EC_BLD=$(mktemp "${TMPDIR:-/tmp}/forge-edge-bld.XXXXXX")
|
|
722
|
+
if [ -z "$EC_SCRIPT" ] || [ -z "$TRUST_SCRIPT" ]; then
|
|
723
|
+
EC_STATE="DEGRADED"; EC_NOTE="check-edge-cases.sh or trusted-comments.sh not found under FORGE_ROOT"
|
|
724
|
+
else
|
|
725
|
+
COMMENTS_JSON=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments 2>/dev/null); FETCH_RC=$?
|
|
726
|
+
if [ "$FETCH_RC" -ne 0 ]; then
|
|
727
|
+
EC_STATE="DEGRADED"; EC_NOTE="could not fetch issue comments (rc=${FETCH_RC})"
|
|
728
|
+
else
|
|
729
|
+
A_BODIES=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUST_SCRIPT" bodies '^<!-- FORGE:ARCHITECT -->'); A_RC=$?
|
|
730
|
+
B_BODIES=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUST_SCRIPT" bodies '^<!-- FORGE:BUILDER'); B_RC=$?
|
|
731
|
+
if [ "$A_RC" -ne 0 ] || [ "$B_RC" -ne 0 ]; then
|
|
732
|
+
EC_STATE="DEGRADED"; EC_NOTE="trusted-comments.sh failed (rc=${A_RC}/${B_RC})"
|
|
733
|
+
elif [ -z "$A_BODIES" ]; then
|
|
734
|
+
EC_STATE="SKIP"; EC_NOTE="no trusted FORGE:ARCHITECT comment"
|
|
735
|
+
elif [ -z "$B_BODIES" ]; then
|
|
736
|
+
EC_STATE="DEGRADED"; EC_NOTE="no trusted FORGE:BUILDER comment to read the edge-case mapping from"
|
|
737
|
+
else
|
|
738
|
+
# one JSON string per line, newest last
|
|
739
|
+
printf '%s\n' "$A_BODIES" | tail -n 1 | jq -r . > "$EC_ARCH"; JQ_A=$?
|
|
740
|
+
printf '%s\n' "$B_BODIES" | tail -n 1 | jq -r . > "$EC_BLD"; JQ_B=$?
|
|
741
|
+
if [ "$JQ_A" -ne 0 ] || [ "$JQ_B" -ne 0 ]; then
|
|
742
|
+
EC_STATE="DEGRADED"; EC_NOTE="could not decode the FORGE:ARCHITECT or FORGE:BUILDER comment"
|
|
743
|
+
else
|
|
744
|
+
git fetch origin {PR_BASE} >/dev/null 2>&1 || true
|
|
745
|
+
EC_OUT=$(bash "$EC_SCRIPT" --architect "$EC_ARCH" --builder "$EC_BLD" --base "origin/{PR_BASE}" --repo "{WORKTREE_PATH}" 2>&1); EC_RC=$?
|
|
746
|
+
printf '%s\n' "$EC_OUT"
|
|
747
|
+
case "$EC_RC" in
|
|
748
|
+
0) case "$EC_OUT" in SKIP:*) EC_STATE="SKIP" ;; *) EC_STATE="PASS" ;; esac
|
|
749
|
+
case "$EC_OUT" in *"MAPPED_TESTS: 0"*|SKIP:*) ;; *) [ "$TEST_RUNNER_DEGRADED" = "1" ] && { EC_STATE="DEGRADED"; EC_NOTE="mapped tests exist but the test runner did not run"; } ;; esac ;;
|
|
750
|
+
1) EC_STATE="FAIL"; GATE_PASSED=false ;;
|
|
751
|
+
*) EC_STATE="DEGRADED"; EC_NOTE="check-edge-cases.sh exited ${EC_RC}" ;;
|
|
752
|
+
esac
|
|
753
|
+
fi
|
|
754
|
+
fi
|
|
755
|
+
fi
|
|
756
|
+
fi
|
|
757
|
+
rm -f "$EC_ARCH" "$EC_BLD"
|
|
758
|
+
echo "EDGE_CASES: ${EC_STATE}${EC_NOTE:+ — $EC_NOTE}"
|
|
759
|
+
# Carry-forward lines for V5 (fresh shell): read them back in V5 Step 1.
|
|
760
|
+
echo "EDGE_CASES_STATE: ${EC_STATE}"
|
|
761
|
+
if [ "$EC_STATE" = "FAIL" ]; then
|
|
762
|
+
echo "GATE_PASSED: false"
|
|
763
|
+
elif [ "$EC_STATE" = "DEGRADED" ]; then
|
|
764
|
+
echo "DEGRADED_CHECKS+=edge-cases — $(printf '%s' "${EC_NOTE:-edge-case check did not complete}" | tr -d ';\r\n')"
|
|
765
|
+
fi
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
On `FAIL`, the blocker line for `VALIDATE_RESULT` is `edge-cases: <ids>` (the ids from the `UNMAPPED:`, `TEST_NOT_IN_DIFF:`, `EMPTY_REASON:` and `DUPLICATE_ID:` lines); the repair loop (build B6.5) adds the tests or reasons, never edits the checker or removes an `EDGE:` record.
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
## Phase V4.8: Local CI Gate *(runs after V4.7, before V5 — skipped only on the docs-only skip path)* <!-- Added: forge#3973 -->
|
|
773
|
+
|
|
774
|
+
The repo's own CI checks used to run first at review, after the full panel, so deterministic failures (spec-size budget, test wiring, lint, portability) cost a remediation round. `scripts/ci-local.sh run` derives the pull-request checks from the repo's CI definitions (read from the trusted base ref plus the PR's own edits), runs only what this change can affect, locally in `{WORKTREE_PATH}`, and never contacts GitHub or triggers a remote run. Remote CI still runs once, at merge.
|
|
775
|
+
|
|
776
|
+
- **Install root only**: `ci-local.sh` resolves from `FORGE_ROOT`, never from the worktree. An unresolvable script is DEGRADED, never a pass.
|
|
777
|
+
- **Trusted config**: the gate's own config (`exclude_jobs`, `include_jobs`, `extra_commands`, `mode`, ...) is read from the main checkout's `forge.yaml` (or the base ref's copy for a plain checkout), never from the PR's tree, so a PR cannot change which jobs gate it (forge#4040).
|
|
778
|
+
- **Exit codes**: `0` every runnable job passed (jobs it could not prove locally, including a job that failed under a tool-version drift such as `setup-node` 18 vs host 26, are printed as `CI_LOCAL_DEFERRED:` lines); `1` a runnable job really failed; `2` DEGRADED: `CI_LOCAL_RESULT: DEGRADED` (a job timeout, an exhausted or capped budget, a missing script or `yq`). DEGRADED is recorded in `DEGRADED_CHECKS` (`DEGRADED_CHECKS+=ci-local — <reason>`), never in `SKIPPED_CHECKS`: V5 reads `DEGRADED_CHECKS+=` lines (forge#4040).
|
|
779
|
+
- **Cap**: a single invocation is capped at 9 minutes (the Bash tool limit is 10): the runner clamps its budget to 540 s whatever `budget_minutes` says. Past the cap the run ends as a partial DEGRADED result (`reason=budget_capped_at_invocation_limit`); passed steps are cached, so re-running the block resumes where it stopped.
|
|
780
|
+
- **Mode** (`verification.ci_local.mode`, default `advisory`): the fix loop is the same in both modes. On exit `1`, read the `CI_LOCAL_FAIL:` blocks, fix the failure in this same context (the code, never the check or the CI definition), stage it, and re-run this block with `CL_ITER` set to 1, then 2 (at most 2 fix iterations). A fix that touches files beyond the failing check's own inputs re-runs V1 first. Still failing after the second: in `advisory` the gate is DEGRADED (`DEGRADED_CHECKS+=ci-local — unfixable after 2 fix iterations: <job ids>`), the failure is recorded in `FORGE:BUILDER` as `### Local CI unresolved` and review is told to cover it; the build is NOT blocked. In `blocking` (opt-in) it is `GATE_PASSED=false` with blocker `ci-local: <failing job ids>`.
|
|
781
|
+
- **Carry-forward**: each Bash call is a fresh shell, so the block prints `CI_LOCAL_STATE: <PASS|FAIL|DEGRADED>`, plus `GATE_PASSED: false` on FAIL, `DEGRADED_CHECKS+=ci-local — <reason>` on DEGRADED, its `CI_LOCAL_DEFERRED:` lines and `CI_LOCAL_UNRESOLVED:` lines. Record them in your notes; V5 Step 1 and Step 4 read them. Only the FINAL iteration's `CI_LOCAL_STATE` counts: a `FAIL` printed by an earlier iteration is superseded once a later iteration prints `PASS` or `DEGRADED` (forge#3999).
|
|
782
|
+
|
|
783
|
+
```bash
|
|
784
|
+
cd {WORKTREE_PATH}
|
|
785
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
786
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
787
|
+
CL_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/ci-local.sh}"; [ -f "$CL_SCRIPT" ] || CL_SCRIPT=""
|
|
788
|
+
CL_ITER=0 # 0 first run; 1 and 2 after each fix
|
|
789
|
+
CL_STATE=""; CL_OUT=""; CL_REASON=""
|
|
790
|
+
# Trusted config only: the main checkout's forge.yaml, never the worktree's (forge#4040).
|
|
791
|
+
CL_MODE=advisory; _gcd=$(git rev-parse --git-common-dir 2>/dev/null) && _gcd=$(cd "$_gcd" 2>/dev/null && pwd) || _gcd=""
|
|
792
|
+
if [ -n "$_gcd" ] && [ "${_gcd%/.git}" != "$(git rev-parse --show-toplevel 2>/dev/null)" ] && [ -f "${_gcd%/.git}/forge.yaml" ]; then
|
|
793
|
+
[ "$(yq '.verification.ci_local.mode // ""' "${_gcd%/.git}/forge.yaml" 2>/dev/null)" = "blocking" ] && CL_MODE=blocking
|
|
794
|
+
fi
|
|
795
|
+
if [ -z "$CL_SCRIPT" ]; then
|
|
796
|
+
CL_STATE="DEGRADED"; CL_REASON="ci-local.sh not found under FORGE_ROOT"; echo "CI_LOCAL: $CL_REASON"
|
|
797
|
+
else
|
|
798
|
+
git fetch origin {PR_BASE} >/dev/null 2>&1 || true
|
|
799
|
+
CL_OUT=$(FORGE_REPO="{GH_REPO}" FORGE_ISSUE="{NUMBER}" FORGE_PHASE=build bash "$CL_SCRIPT" run --repo "{WORKTREE_PATH}" --base "origin/{PR_BASE}" --base-branch "{PR_BASE}" --head-branch "{BRANCH}" --budget-minutes 9 --iteration "$CL_ITER" 2>&1); CL_RC=$?
|
|
800
|
+
printf '%s\n' "$CL_OUT"
|
|
801
|
+
case "$CL_RC" in 0) CL_STATE="PASS" ;; 1) CL_STATE="FAIL" ;; *) CL_STATE="DEGRADED" ;; esac
|
|
802
|
+
CL_FAILED=$(printf '%s\n' "$CL_OUT" | sed -n 's/^CI_LOCAL_FAIL: \([^ ]*\) .*/\1/p' | paste -sd, -)
|
|
803
|
+
if [ "$CL_STATE" = "DEGRADED" ]; then
|
|
804
|
+
CL_REASON=$(printf '%s\n' "$CL_OUT" | sed -n 's/^CI_LOCAL_RESULT: DEGRADED.*reason=\([^ ]*\).*/\1/p' | tail -n 1)
|
|
805
|
+
[ -n "$CL_REASON" ] || CL_REASON="local CI gate could not complete (exit $CL_RC)"
|
|
806
|
+
elif [ "$CL_STATE" = "FAIL" ] && [ "$CL_MODE" = "advisory" ] && [ "$CL_ITER" -ge 2 ]; then
|
|
807
|
+
# advisory (default): an unfixable failure after the 2 fix iterations is DEGRADED, recorded and surfaced to review, not a block
|
|
808
|
+
CL_STATE="DEGRADED"; CL_REASON="unfixable after 2 fix iterations: ${CL_FAILED:-unknown}"
|
|
809
|
+
printf '%s\n' "$CL_OUT" | grep '^CI_LOCAL_FAIL:' | sed 's/^CI_LOCAL_FAIL: /CI_LOCAL_UNRESOLVED: /' || true
|
|
810
|
+
fi
|
|
811
|
+
fi
|
|
812
|
+
CL_REASON=$(printf '%s' "$CL_REASON" | tr -d ';\r\n' | sed 's/ — / - /g')
|
|
813
|
+
echo "CI_LOCAL_MODE: ${CL_MODE}"
|
|
814
|
+
echo "CI_LOCAL_STATE: ${CL_STATE}"
|
|
815
|
+
if [ "$CL_STATE" = "FAIL" ]; then echo "GATE_PASSED: false"
|
|
816
|
+
elif [ "$CL_STATE" = "DEGRADED" ]; then echo "DEGRADED_CHECKS+=ci-local — ${CL_REASON}"; fi
|
|
817
|
+
printf '%s\n' "$CL_OUT" | grep '^CI_LOCAL_DEFERRED:' || true
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
In `blocking` mode a `FAIL` after the second fix iteration makes the `VALIDATE_RESULT` blocker `ci-local: <failing job ids>`; in the default `advisory` mode it is DEGRADED and the build continues. The `FORGE:BUILDER` comment lists the deferred jobs and the unresolved failures (V5 Step 4), so the reviewer and the merge gate know what local CI could not prove and what remote CI still has to prove.
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
576
824
|
## Phase V5: Marker, Commit, Audit, Complete (always — after GATE_PASSED=true)
|
|
577
825
|
|
|
578
826
|
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.
|
|
@@ -583,15 +831,23 @@ The quality gate must leave a checkable artifact. `scripts/verify-phase-trail.sh
|
|
|
583
831
|
|
|
584
832
|
**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).
|
|
585
833
|
|
|
586
|
-
Set `GATE_PASSED` from the V1 loop result, then apply
|
|
834
|
+
Set `GATE_PASSED` from the V1 loop result, then apply the printed carry-forward lines of V4.5, V4.6, V4.7 and V4.8: if any printed `GATE_PASSED: false` (`BLAST_RADIUS_STATE: FAIL`, `CONSUMER_SWEEP_STATE: FAIL`, `EDGE_CASES_STATE: FAIL`, or the FINAL V4.8 iteration's `CI_LOCAL_STATE: FAIL`, which only `blocking` mode produces after the fix iterations), `GATE_PASSED` is `false` here even if the V1 loop passed. A V4.8 `FAIL` from an earlier iteration that a later iteration superseded with `CI_LOCAL_STATE: PASS` does not count (forge#3999).
|
|
835
|
+
Rebuild `DEGRADED_CHECKS` from every `DEGRADED_CHECKS+=<name> — <reason>` line printed by V2, V4.5, V4.7 and V4.8 (joined with `; `, empty when none). `GATE_STATUS` is `FAIL` when `GATE_PASSED` is false, `DEGRADED` when it is true and `DEGRADED_CHECKS` is non-empty, else `PASS`. The marker keeps `**Result**: PASS` for a DEGRADED gate (the build still completes) and adds the `**Degraded**:` line; the merge gate (`scripts/review-finalize.sh`) reads it. Check names contain no ` — ` and no `;`. <!-- Added: forge#3628 -->
|
|
587
836
|
|
|
588
837
|
```bash
|
|
589
|
-
# GATE_PASSED: set from the V1 loop result, and forced to false when V4.5 printed "GATE_PASSED: false"
|
|
838
|
+
# GATE_PASSED: set from the V1 loop result, and forced to false when V4.5, V4.6, V4.7 or the final V4.8 iteration printed "GATE_PASSED: false"
|
|
590
839
|
GATE_RESULT=$([ "$GATE_PASSED" = "true" ] && echo PASS || echo FAIL)
|
|
840
|
+
# DEGRADED_CHECKS: set from the V2/V4.5 carry-forward lines, e.g. DEGRADED_CHECKS="python.test — test run exercised nothing; blast-radius — check unavailable"
|
|
841
|
+
DEGRADED_CHECKS="${DEGRADED_CHECKS:-}"
|
|
842
|
+
GATE_STATUS=$([ "$GATE_PASSED" != "true" ] && echo FAIL || { [ -n "$DEGRADED_CHECKS" ] && echo DEGRADED || echo PASS; })
|
|
591
843
|
# Bind the PASS to what was actually gated: the staged tree is exactly the tree the V5 commit will have.
|
|
592
844
|
# scripts/verify-phase-trail.sh --head-tree (work-on/review.md R1.5) rejects a PASS recorded for a different tree. <!-- Added: forge#3149 -->
|
|
593
845
|
git -C {WORKTREE_PATH} add -u # same staging as the commit below; valid only because V2-V4 are done
|
|
594
846
|
GATE_TREE=$(git -C {WORKTREE_PATH} write-tree)
|
|
847
|
+
# Marker-only re-gate (invoked by work-on/review.md after a base sync or a stale re-review): HEAD is already committed and
|
|
848
|
+
# the index is clean, so the gated tree is HEAD's tree. Use it instead of write-tree, and post the marker only (no V5 Step 2 commit).
|
|
849
|
+
[ "${REGATE_MERGED_HEAD:-}" = "1" ] && GATE_TREE=$(git -C {WORKTREE_PATH} rev-parse 'HEAD^{tree}')
|
|
850
|
+
[ -n "$GATE_TREE" ] || { echo "QUALITY_GATE: tree unresolved - not posting a marker" >&2; exit 1; }
|
|
595
851
|
QG_BODY="<!-- FORGE:QUALITY_GATE -->
|
|
596
852
|
## Quality Gate Result
|
|
597
853
|
|
|
@@ -599,10 +855,13 @@ QG_BODY="<!-- FORGE:QUALITY_GATE -->
|
|
|
599
855
|
**Tree**: ${GATE_TREE}
|
|
600
856
|
**Iterations**: {N}
|
|
601
857
|
**Commands run**: {quality-gate invocation, format/verify commands, test commands actually executed}
|
|
602
|
-
**Findings remaining**: {none | summary}
|
|
858
|
+
**Findings remaining**: {none | summary}
|
|
859
|
+
**Degraded**: ${DEGRADED_CHECKS:-none}"
|
|
603
860
|
gh issue comment {NUMBER} {GH_FLAG} --body "$QG_BODY" # allowlist:check-command-side-effects
|
|
604
861
|
```
|
|
605
862
|
|
|
863
|
+
**Marker-only re-gate mode** <!-- Added: forge#3983 -->: when `work-on/review.md` re-gates a merged head (Pre-Handoff Base Sync or stale re-review), it runs the scoped V1 gate on the committed head first, then sets `REGATE_MERGED_HEAD=1` and runs only this Step 1 with the real `GATE_RESULT` (FAIL stays FAIL). Steps 2-4 are skipped: there is nothing staged and no commit to make.
|
|
864
|
+
|
|
606
865
|
### V5 Step 2: Commit
|
|
607
866
|
|
|
608
867
|
After the marker is posted, commit all staged changes in a single commit. This includes:
|
|
@@ -698,22 +957,27 @@ If the output contains `ANCESTRY_FAILED=1`: do NOT append `:COMPLETE` and do NOT
|
|
|
698
957
|
|
|
699
958
|
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 -->
|
|
700
959
|
|
|
701
|
-
`SKIPPED_CHECKS` comes from Phase V2 and Phase V4.
|
|
960
|
+
`SKIPPED_CHECKS` comes from Phase V2 (unconfigured checks) and Phase V4.6. 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), append `consumer-sweep` when V4.6 printed `SKIPPED_CHECKS+=consumer-sweep` (`CONSUMER_SWEEP_STATE: DEGRADED`). V4.8's DEGRADED arrives as `DEGRADED_CHECKS+=ci-local — <reason>` (never in `SKIPPED_CHECKS`). `DEGRADED_CHECKS` (V2 test runs that ran nothing, known-slow skips, V4.5 `DEGRADED`) is the same list posted in V5 Step 1; it is never merged into `SKIPPED_CHECKS`. Set `UNRESOLVED_LINES` from the V4.6 `CONSUMERS_UNRESOLVED:` lines (prefix stripped, newline-separated; empty when none). Set `CI_LOCAL_UNRESOLVED_LINES` from V4.8's `CI_LOCAL_UNRESOLVED:` lines (prefix stripped, one `- <job id> | step: ... | exit N` bullet each, empty when none): they become the `### Local CI unresolved` section. Set `CI_LOCAL_DEFERRED_LINES` from V4.8's `CI_LOCAL_DEFERRED:` lines (one `- <job id> | <name> | <reason>` bullet each, empty when none): they become the `### Deferred to CI` section of the `FORGE:BUILDER` comment.
|
|
702
961
|
|
|
703
962
|
**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.
|
|
704
963
|
|
|
705
964
|
```bash
|
|
706
|
-
# SKIPPED_CHECKS: set from the V2 notes plus V4.
|
|
965
|
+
# SKIPPED_CHECKS: set from the V2 notes plus the V4.6 carry-forward lines, e.g. SKIPPED_CHECKS="python.format, consumer-sweep"
|
|
707
966
|
SKIPPED_CHECKS="${SKIPPED_CHECKS:-}"
|
|
967
|
+
DEGRADED_CHECKS="${DEGRADED_CHECKS:-}" # same value as V5 Step 1
|
|
968
|
+
# CI_LOCAL_DEFERRED_LINES: bullets from V4.8's CI_LOCAL_DEFERRED lines (empty when none)
|
|
969
|
+
CI_LOCAL_DEFERRED_LINES="${CI_LOCAL_DEFERRED_LINES:-}"
|
|
970
|
+
CI_LOCAL_UNRESOLVED_LINES="${CI_LOCAL_UNRESOLVED_LINES:-}" # advisory-mode failures left after 2 fix iterations
|
|
708
971
|
if [ -z "$SKIPPED_CHECKS" ]; then
|
|
709
972
|
VERIFICATION_STATUS="✅ All configured verification commands passed"
|
|
710
973
|
else
|
|
711
974
|
VERIFICATION_STATUS="⚠ Verification NOT run: ${SKIPPED_CHECKS} — verification.commands not configured for these checks"
|
|
712
975
|
fi
|
|
976
|
+
[ -z "$DEGRADED_CHECKS" ] || VERIFICATION_STATUS="${VERIFICATION_STATUS}; ⚠ DEGRADED (ran nothing, review must cover these paths): ${DEGRADED_CHECKS}"
|
|
713
977
|
|
|
714
978
|
# Find the FORGE:BUILDER comment posted by implement.md Phase I6
|
|
715
979
|
# Anchored, trusted, single-match selector (forge#3668): zero/many/unparsable → empty → append skipped.
|
|
716
|
-
BLD=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("
|
|
980
|
+
BLD=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("\\n[ \\t]*<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|ARCHITECT)"))}]') || BLD=""
|
|
717
981
|
BUILDER_COMMENT_ID=$(printf '%s' "$BLD" | jq -r '[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end' 2>/dev/null)
|
|
718
982
|
|
|
719
983
|
if [ -n "$BUILDER_COMMENT_ID" ]; then
|
|
@@ -729,7 +993,31 @@ if [ -n "$BUILDER_COMMENT_ID" ]; then
|
|
|
729
993
|
PHASE_COST_LINE=""
|
|
730
994
|
[ -n "${PHASE_COST_USD:-}" ] && PHASE_COST_LINE="
|
|
731
995
|
cost_usd: ${PHASE_COST_USD}"
|
|
732
|
-
|
|
996
|
+
# Unaddressed consumer sites (V4.6 CONSUMERS_UNRESOLVED lines, prefix stripped, one "- <file:line> unresolved: <id>" per line)
|
|
997
|
+
UNRESOLVED_LINES="${UNRESOLVED_LINES:-}"
|
|
998
|
+
if [ -n "$UNRESOLVED_LINES" ] && ! printf '%s' "$PATCHED_BODY" | grep -q '^CONSUMERS: unresolved'; then
|
|
999
|
+
PATCHED_BODY="${PATCHED_BODY}
|
|
1000
|
+
|
|
1001
|
+
CONSUMERS: unresolved
|
|
1002
|
+
${UNRESOLVED_LINES}"
|
|
1003
|
+
fi
|
|
1004
|
+
DEFERRED_SECTION=""
|
|
1005
|
+
if [ -n "$CI_LOCAL_DEFERRED_LINES" ] && ! printf '%s' "$PATCHED_BODY" | grep -q '^### Deferred to CI'; then
|
|
1006
|
+
DEFERRED_SECTION="
|
|
1007
|
+
|
|
1008
|
+
### Deferred to CI
|
|
1009
|
+
Not proven locally; remote CI at merge must prove these:
|
|
1010
|
+
${CI_LOCAL_DEFERRED_LINES}"
|
|
1011
|
+
fi
|
|
1012
|
+
UNRESOLVED_SECTION=""
|
|
1013
|
+
if [ -n "$CI_LOCAL_UNRESOLVED_LINES" ] && ! printf '%s' "$PATCHED_BODY" | grep -q '^### Local CI unresolved'; then
|
|
1014
|
+
UNRESOLVED_SECTION="
|
|
1015
|
+
|
|
1016
|
+
### Local CI unresolved
|
|
1017
|
+
DEGRADED (advisory mode): these local CI jobs still failed after 2 fix iterations; review must cover them:
|
|
1018
|
+
${CI_LOCAL_UNRESOLVED_LINES}"
|
|
1019
|
+
fi
|
|
1020
|
+
UPDATED_BODY="${PATCHED_BODY}${UNRESOLVED_SECTION}${DEFERRED_SECTION}${PHASE_COST_LINE}
|
|
733
1021
|
|
|
734
1022
|
<!-- FORGE:BUILDER:COMPLETE -->"
|
|
735
1023
|
gh api repos/{GH_REPO}/issues/comments/$BUILDER_COMMENT_ID \
|
|
@@ -758,9 +1046,11 @@ VALIDATE_RESULT:
|
|
|
758
1046
|
deploy_completeness_fixes: [{VAR_NAME: location_added}, ...]
|
|
759
1047
|
commits_added: [{SHA}, ...] # from V5 if any
|
|
760
1048
|
blocker: {description if gate_passed=false}
|
|
761
|
-
|
|
1049
|
+
gate_status: PASS | DEGRADED | FAIL # FAIL iff gate_passed=false; DEGRADED when DEGRADED_CHECKS is non-empty (V5 Step 1)
|
|
1050
|
+
degraded_checks: [] # check names from DEGRADED_CHECKS (test run ran nothing, known-slow skip, blast-radius DEGRADED); empty on PASS
|
|
1051
|
+
verification_skipped: [] # empty when all configured checks ran; list of skipped (unconfigured) check names otherwise
|
|
762
1052
|
# e.g. ["python.format", "typescript.typecheck/build"]
|
|
763
|
-
# populated from SKIPPED_CHECKS in Phase V2 and V4.
|
|
1053
|
+
# populated from SKIPPED_CHECKS in Phase V2 and V4.6
|
|
764
1054
|
```
|
|
765
1055
|
|
|
766
1056
|
---
|
|
@@ -772,11 +1062,13 @@ This module is invoked by `work-on:build` after implement and before review:
|
|
|
772
1062
|
```
|
|
773
1063
|
implement (work-on:build:implement) — code written, staged (not committed), FORGE:BUILDER posted
|
|
774
1064
|
→ [THIS MODULE] validate
|
|
775
|
-
V0 self-check → V1 quality-gate loop (Skill quality-gate, forked) → V2 format/verify + known-slow/learned tests
|
|
1065
|
+
V0 self-check → V1 quality-gate loop (Skill quality-gate, forked) → V2 format/verify + classified test runs (classify-test-run.sh) + known-slow/learned tests
|
|
776
1066
|
→ V3 proxy wiring → V3.5 DB advisory → V3.6 browser signals → V4 deploy completeness
|
|
777
1067
|
→ V4.5 blast-radius manifest check (check-blast-radius.sh, install-root only)
|
|
1068
|
+
→ V4.6 consumer sweep (consumer-sweep.sh, install-root only)
|
|
1069
|
+
→ V4.8 local CI gate (ci-local.sh, install-root only; at most 2 fix iterations, then DEGRADED in advisory mode (default) or blocker `ci-local: <jobs>` in blocking mode)
|
|
778
1070
|
→ V5 marker → commit → ancestry audit → BUILDER:COMPLETE
|
|
779
1071
|
review (work-on:review) — push, PR, merge
|
|
780
1072
|
```
|
|
781
1073
|
|
|
782
|
-
If `VALIDATE_RESULT: gate_passed: false`, the build skill reports BLOCKED (`needs-human` is set) and no PR is created.
|
|
1074
|
+
If `VALIDATE_RESULT: gate_passed: false`, the build skill reports BLOCKED (`needs-human` is set) and no PR is created, except for `blocker: consumer_unaddressed: ...`, which carries no `needs-human` and which build B6 routes to one consumer repair round (V4.6).
|