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
|
@@ -23,7 +23,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BAS
|
|
|
23
23
|
**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. This file's mechanical bits (classification, label transitions) stay at this tier because they are interleaved with the reasoning-heavy child phases in the same run. <!-- Added: forge#1827 -->
|
|
24
24
|
**NEVER use plan mode (EnterPlanMode).**
|
|
25
25
|
|
|
26
|
-
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order (the one exception is the B0 gate-resume branch, which enters at B6.5 with its state verified). Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
26
|
+
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5 and B5.6) in order (the one exception is the B0 gate-resume branch, which enters at B6.5 with its state verified). Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
27
27
|
|
|
28
28
|
**Synchronous child consumption**: each child `Skill(...)` call (B3-B6) runs to completion in this phase's own turn, and you consume its result in that same turn. Never end or yield your turn to wait for a child, and never wait for a completion notification: notifications for forked children go to the root session, not to this phase, so a turn that yields is never resumed. A child return with no `*_RESULT:` block (running, backgrounded, empty) is not a result — re-read the GitHub markers for that child (`FORGE:CONTEXT`, `FORGE:ARCHITECT`, `FORGE:BUILDER`, `FORGE:VALIDATE`/`FORGE:QUALITY_GATE`) and apply the **bounded re-invoke rule**: re-invoke the same child with the same args at most 2 times per child on running/backgrounded/empty returns (the counter is per child and is not reset by a re-invoke). Before re-invoking, check state: if the child's marker is present, consume it instead of re-invoking; for implement and validate, also check the worktree (`git status --short` and `git log origin/{PR_BASE}..HEAD` in `{WORKTREE_PATH}`) — uncommitted in-flight edits or a fresh commit mean the original child may still be live, so wait for it by re-reading state rather than launching a second writer on the same worktree, and count that as a re-invoke attempt. After the cap is exhausted, take the Blocked exit (see Result emission and Blocked exit) with `blocker: child-stalled: <child>` (child = context|architect|implement|validate); it is terminal and is not re-invoked by the router.
|
|
29
29
|
|
|
@@ -179,12 +179,12 @@ EXISTING_FAST_PATH=$(printf '%s' "$CYCLE_SLICE" | bash "$TRUSTED_SCRIPT" bodies
|
|
|
179
179
|
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment of the current-cycle slice → the builder finished, but COMPLETE is posted at validate V5, BEFORE the B6.5 acceptance gate, so it is not proof the build passed. **Gate resume** (checked first; first match wins) <!-- Added: forge#3725 -->: read the newest trusted, comment of the current-cycle slice that starts with `FORGE:ACCEPTANCE_GATE` or `FORGE:BUILD_BLOCKED`, through the same anchored, trust-filtered lookup as the FAST_PATH read above (script resolution block verbatim; pipe `cycle_slice` into `bodies '^<!-- FORGE:(ACCEPTANCE_GATE|BUILD_BLOCKED)'`, and compare by comment order, never by presence):
|
|
180
180
|
- Lookup failed (`trusted-comments.sh` unresolved or `gh` error) → Blocked exit with blocker `gate-lookup-unavailable: could not read FORGE:ACCEPTANCE_GATE` (a transient GitHub failure uses the `github-unavailable:` prefix). Never read a failed lookup as "no gate marker".
|
|
181
181
|
- Newest is `FORGE:ACCEPTANCE_GATE:FAILED`, `FORGE:ACCEPTANCE_GATE:BLOCKED`, or a `FORGE:BUILD_BLOCKED` comment → the gate is blocked and the engine fails closed on it, so returning ALREADY_DONE would deadlock every re-run. Derive BRANCH/WORKTREE_PATH/PR_BASE as in B1A/B1C (without creating anything), verify the branch and worktree exist and that `git -C "$WORKTREE_PATH" branch --show-current` equals BRANCH (missing or mismatched → Blocked exit `worktree-missing: cannot re-enter B6.5`; never recreate silently), recover `COMPLEXITY_BAND` from `EXISTING_FAST_PATH` with the B0.5 resume extraction, then skip B0.5-B6 and re-enter at **Phase B6.5**, which posts a fresh PASSED/FAILED/BLOCKED verdict (the one-repair `FORGE:ACCEPTANCE_REPAIR` bound still applies).
|
|
182
|
-
- Newest is `FORGE:ACCEPTANCE_GATE:PASSED
|
|
182
|
+
- Newest is `FORGE:ACCEPTANCE_GATE:PASSED` or `FORGE:ACCEPTANCE_GATE:DEGRADED` (pass-class: the build completed, review gates the merge on the degraded paths), or no gate marker exists → build already complete. Derive BRANCH/WORKTREE_PATH as in B1A/B1C (without creating anything), then print `BUILD_RESULT: status: ALREADY_DONE`.
|
|
183
183
|
- If `<!-- FORGE:BUILDER -->` exists in the current-cycle slice BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate V5). Delete the partial comment and restart from Phase B2 (contract): <!-- Added: forge#1305 -->
|
|
184
184
|
Select with ONE anchored, trusted selector (body must start with the marker; author must be a Bot or OWNER/MEMBER/COLLABORATOR). Delete ONLY when exactly one non-complete candidate exists and it carries no other FORGE phase marker; unparsable, zero, or multiple matches delete nothing (fail closed). <!-- Added: forge#3668 -->
|
|
185
185
|
```bash
|
|
186
186
|
# <cycle_slice function from B0, verbatim> — only a current-cycle partial is ever deleted; prior-cycle comments are never touched
|
|
187
|
-
BLD=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | cycle_slice | 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("
|
|
187
|
+
BLD=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | cycle_slice | 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=""
|
|
188
188
|
PARTIAL_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)
|
|
189
189
|
if [ -n "$PARTIAL_ID" ]; then
|
|
190
190
|
gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
|
|
@@ -376,7 +376,7 @@ Every item in the contract's `### Out of Scope` section must carry exactly one d
|
|
|
376
376
|
- **Known-flaw rule**: a contract that keeps a known-flawed design (a design the investigation or architect plan identified as flawed, a fail-open caller, an unsound threshold) must state the flaw and list it as a `deferred → #N` item with a linked follow-up. It may not hide behind `accepted-risk` or free text.
|
|
377
377
|
- Free-text exclusions without a disposition are invalid. `->` is accepted for `→`.
|
|
378
378
|
|
|
379
|
-
**Fold, then batch** <!-- Added: forge#3843 -->: before filing anything, fold small siblings into scope. A candidate deferred item that is the same mechanical change at a sibling path and fits the remaining scope budget (a small fraction of the plan's own estimate: at most 20% of the current `ESTIMATED_DIFF_LINES`, or 200 lines when no estimate exists) goes into the contract's Deliverables, not Out of Scope; count it as `folded`. Defer only genuinely separate work (different module, design or owner). All remaining deferred items of this build go into ONE follow-up issue with a checklist; `scripts/file-deferred-followups.sh` appends to an
|
|
379
|
+
**Fold, then batch** <!-- Added: forge#3843 -->: before filing anything, fold small siblings into scope. A candidate deferred item that is the same mechanical change at a sibling path and fits the remaining scope budget (a small fraction of the plan's own estimate: at most 20% of the current `ESTIMATED_DIFF_LINES`, or 200 lines when no estimate exists) goes into the contract's Deliverables, not Out of Scope; count it as `folded`. Defer only genuinely separate work (different module, design or owner). All remaining deferred items of this build go into ONE follow-up issue with a checklist; `scripts/file-deferred-followups.sh` appends to an existing trusted follow-up for the same parent (exact path match; a closed one is reopened) or creates one, then records a `<!-- FORGE:DEFERRED_ISSUE: #M -->` claim marker on the parent.
|
|
380
380
|
|
|
381
381
|
**File the `deferred` follow-ups** (skip the filing on resume when the contract already exists):
|
|
382
382
|
|
|
@@ -392,10 +392,11 @@ esac
|
|
|
392
392
|
|
|
393
393
|
- Output is one `path<TAB>issue` row per item (rows share the batched issue number: use it as the `#N` in each `deferred → #N` bullet) plus a `DEFERRED_TELEMETRY deferred_items=N folded=N batched_into=#M filed=N` line (forwarded to the #3834 emitter when present).
|
|
394
394
|
- A known-flawed design is recorded as a `known-flaw` checklist item in that batched issue (pass the flag in the TSV); it is never dropped or hidden behind `accepted-risk`.
|
|
395
|
-
- Only follow-ups by a trusted author (OWNER/MEMBER/COLLABORATOR or Bot; `FORGE_TRAIL_TRUSTED_*` honoured) are reused; an untrusted match is ignored.
|
|
395
|
+
- Only follow-ups by a trusted author (OWNER/MEMBER/COLLABORATOR or Bot; `FORGE_TRAIL_TRUSTED_*` honoured) are reused; an untrusted match, claim comment or issue is ignored.
|
|
396
|
+
- Lookup order: the `FORGE:DEFERRED_ISSUE` claim markers on the parent's REST comments (read-after-write consistent), then a search over all issue states (the search index lags, so it is only a fallback); each candidate is verified by number and the lowest number is canonical. After creating, the script posts the claim and re-checks once: a lower-numbered trusted follow-up from a concurrent build wins, the items move to it and the newer issue is closed as a duplicate. <!-- Added: forge#3606 -->
|
|
396
397
|
- `DEFERRED_LOOKUP=FAILED` or `DEFERRED_FILE=FAILED` (non-zero `DEFERRED_RC`) → Blocked exit with blocker `github-unavailable: could not file or look up deferred follow-ups` (a transient failure follows the retry rule above; no `needs-human` for `github-unavailable:`).
|
|
397
|
-
- Prose tier (script absent): create one issue by hand whose body has the exact `<!-- FORGE:DEFERRED_FROM: #{NUMBER} -->` marker and a `- [ ] \`path\` — why` checklist line per path, after a trusted-author
|
|
398
|
-
- On resume the lookup reuses the
|
|
398
|
+
- Prose tier (script absent): create one issue by hand whose body has the exact `<!-- FORGE:DEFERRED_FROM: #{NUMBER} -->` marker and a `- [ ] \`path\` — why` checklist line per path, after a trusted-author lookup over all issue states (not only open; read the parent's comments for an existing `FORGE:DEFERRED_ISSUE` claim first); post `<!-- FORGE:DEFERRED_ISSUE: #M -->` on the parent after creating; a failed lookup is a Blocked exit.
|
|
399
|
+
- On resume the lookup reuses the issue named by the `FORGE:DEFERRED_ISSUE` claim (or carrying the marker, open or closed, reopening a closed one) and appends only missing paths, so a retried build never files a duplicate.
|
|
399
400
|
|
|
400
401
|
**Validate the composed section** with `scripts/check-contract-scope.sh validate` (the exact text that will replace `{OUT_OF_SCOPE_ITEMS}`):
|
|
401
402
|
|
|
@@ -736,7 +737,115 @@ else run gh issue comment {NUMBER} {GH_FLAG} --body "$BODY"; fi
|
|
|
736
737
|
`SCOPE_NOTE` carries, one per line, `out_of_plan: <path>` for each out-of-plan file (up to 50), `overrun: <ws_lines> vs estimate <N> (>{OVERRUN_PCT}%)` when `overrun=true`, the `trimmed:` line after a trim, the override justification for `OVERRIDDEN`, and `review-note: <what reviewers should check>` (for example, check the out-of-plan files first). `DEPRECATION_NOTE` is `deprecated_config: build.diff_size.*` plus a newline when an alias supplied a value. A `FORGE:DIFF_SIZE` comment never contains a `### Split Proposal` heading and never routes anywhere: no engine path reads it as a decompose signal.
|
|
737
738
|
|
|
738
739
|
**Resume**: B5.5 is idempotent. A re-invoked build re-runs B5.5 only through the normal path (B0 resumes at B5/B6 by BUILDER/COMPLETE state); a repeated measure edits the same `FORGE:DIFF_SIZE` comment, and a repeated trim finds the out-of-plan files already gone from the diff.
|
|
739
|
-
# MUST CONTINUE to Phase B6 — the scope check result is intermediate, NOT terminal (validation still required), except the Blocked exits above.
|
|
740
|
+
# MUST CONTINUE to Phase B5.6 (self-review, then B6) — the scope check result is intermediate, NOT terminal (validation still required), except the Blocked exits above.
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
## Phase B5.6: Self-Review (same roster, warm context; never the official review) <!-- Added: forge#3971 -->
|
|
745
|
+
|
|
746
|
+
**Goal**: move adversarial-completeness defects (trust-boundary and bypass forms, domain semantics) to the cheapest point. The risk-tier roster's domain agents review the **staged diff** with the same personas and protocols as `/review-pr` (`commands/review-pr-agents/protocols.md` § SELF-REVIEW MODE), the builder fixes CONFIRMED/LIKELY findings at MEDIUM or above in place (one round), and declined findings are recorded for the official review. This is **not** the official review: it posts no review marker, no verdict, no label, and `FORGE:SELFREVIEW-AGENT:*` never satisfies `review-finalize.sh` or `verify-phase-trail.sh`. It runs before B6, so validate's gate covers the change and any fix together. build is a dispatching phase (router-only, `docs/WORK-ON-RUNTIME.md` R1); its children stay non-dispatching.
|
|
747
|
+
|
|
748
|
+
**Degrade, never block**: every failure here (plan error, no `Agent` tool, no reviewer comment in time, repair trouble) records a skip or missing-domain reason in `build_selfreview` and continues to B6. Skip this phase entirely for Investigation (B5 already exited).
|
|
749
|
+
|
|
750
|
+
**Step 1 — plan.** Paste the Script resolution block and the `cycle_slice` function from B0 first (shell state does not persist). `AGENT_AVAILABLE` is `false` when you have no `Agent` tool or `spawn-depth-check.sh --router-layer 1` exits 1 (conservative: build's own layer is unknown), else `true`. The plan script wraps `scripts/review-roster.sh` (the same roster `/review-pr` picks for this diff) and reads the staged diff only (no network), so `DONE_TREES` comes from the trusted `FORGE:SELFREVIEW` summaries of the current cycle (their `Tree:` and `Final-Tree:` values):
|
|
751
|
+
|
|
752
|
+
```bash
|
|
753
|
+
# <Script resolution block from above goes here, verbatim>
|
|
754
|
+
# <cycle_slice function from B0, verbatim>
|
|
755
|
+
WT="{WORKTREE_PATH}"; AGENT_AVAILABLE=true # set false when no Agent tool, or the depth check below fails
|
|
756
|
+
SD=$(forge_resolve spawn-depth-check.sh); [ -n "$SD" ] && { bash "$SD" --router-layer 1 >/dev/null 2>&1; [ $? -eq 1 ] && AGENT_AVAILABLE=false; }
|
|
757
|
+
TREE=$(git -C "$WT" write-tree 2>/dev/null) || TREE=""
|
|
758
|
+
FILES_FILE=$(mktemp "${TMPDIR:-/tmp}/forge-sr-{NUMBER}.XXXXXX")
|
|
759
|
+
git -C "$WT" diff --cached --name-only "origin/{PR_BASE}" > "$FILES_FILE" 2>/dev/null
|
|
760
|
+
NLINES=$(git -C "$WT" diff --cached --numstat "origin/{PR_BASE}" 2>/dev/null | awk '{ a += $(1); d += $(2) } END { print a + d + 0 }')
|
|
761
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
762
|
+
SLICE=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | cycle_slice) || SLICE="[]"
|
|
763
|
+
CFLAGS=""; CONTRACT=$(printf '%s' "$SLICE" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:CONTRACT' 2>/dev/null | jq -r 'select(type == "string")' 2>/dev/null)
|
|
764
|
+
[ -n "$TRUSTED_SCRIPT" ] || CFLAGS="HIGH_RISK" # cannot read the contract trust-filtered: widen, never narrow
|
|
765
|
+
printf '%s\n' "$CONTRACT" | grep -qiE 'Risk.*(HIGH|CRITICAL)' && CFLAGS="HIGH_RISK"
|
|
766
|
+
printf '%s\n' "$CONTRACT" | grep -qiE 'auth|session|jwt' && CFLAGS="$CFLAGS CONTRACT_AUTH"
|
|
767
|
+
DEPTH=STANDARD; printf '%s' "$SLICE" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:DIFF_SIZE' 2>/dev/null | jq -r 'select(type == "string")' | grep -qE '^review_depth: *EXTENDED' && DEPTH=EXTENDED
|
|
768
|
+
SPEC_CLI=false; [ -f "$WT/commands/review-pr-agents/spec-cli.md" ] && SPEC_CLI=true
|
|
769
|
+
DONE_TREES=$(printf '%s' "$SLICE" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:SELFREVIEW -->' 2>/dev/null | jq -r 'select(type == "string")' | sed -n 's/^\(Final-\)\{0,1\}Tree: *//p' | tr '\n' ' ')
|
|
770
|
+
PLAN_SCRIPT=$(forge_resolve selfreview-plan.sh)
|
|
771
|
+
if [ -n "$PLAN_SCRIPT" ]; then
|
|
772
|
+
bash "$PLAN_SCRIPT" --files-file "$FILES_FILE" --changed-lines "$NLINES" --contract-flags "$CFLAGS" --spec-cli "$SPEC_CLI" --review-depth "$DEPTH" --agent-available "$AGENT_AVAILABLE" --tree "$TREE" --done-trees "$DONE_TREES"
|
|
773
|
+
else echo "SELFREVIEW=skip"; echo "SKIP_REASON=plan-unavailable"; fi
|
|
774
|
+
echo "TREE=$TREE"
|
|
775
|
+
rm -f "$FILES_FILE"
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
Keep the printed `TREE=`, `TIER=`, `SELECTED_AGENTS=` and `SKIP_REASON=` values: shell state does not persist, so Steps 2, 3 and 5 take them as the literal `{TREE}`, `{TIER}`, `{SELECTED_AGENTS}` and `{SKIP_REASON}` values. <!-- Added: forge#4068 review CQ-1 -->
|
|
779
|
+
|
|
780
|
+
`SELFREVIEW=skip` (any `SKIP_REASON`, including a missing plan script or a plan call that fails) → go to Step 5 with `skipped=<reason>`. A skip is never an error. `FORGE_SELFREVIEW=off` is the operator off switch.
|
|
781
|
+
|
|
782
|
+
**Step 2 — dispatch** (`SELFREVIEW=run`). In ONE message, spawn one reviewer per `SELECTED_AGENTS` domain with the `Agent` tool (`model: "{DEFAULT_MODEL}"`; the effort gate applies). Each prompt is the domain persona file `commands/review-pr-agents/{domain}.md` plus `protocols.md`, with `MODE: SELF-REVIEW`, `[AGENT_DOMAIN]`, `[ISSUE_NUMBER]={NUMBER}`, `[GH_REPO]`, `[WORKTREE_PATH]`, `[PR_BASE]`, `[TREE_HASH]=$TREE`, and `[DOMAIN_DIFF_SLICE]` cut from `git -C "{WORKTREE_PATH}" diff --cached "origin/{PR_BASE}"` per the protocol's input scoping (Security gets the full diff). Resolve the persona files from the install root (`$FORGE_ROOT/commands/review-pr-agents/`), never by `find`. Reviewers are read-only. Their completion notifications go to the root session (`docs/WORK-ON-RUNTIME.md` F6), so never wait on one: use Step 3.
|
|
783
|
+
|
|
784
|
+
**Step 3 — bounded wait, one poll pass per Bash call.** A reviewer is done when a trusted comment starting `<!-- FORGE:SELFREVIEW-AGENT:{domain} -->` carries `Tree: ${TREE}` and a findings block. Re-run the pass while it prints `SELFREVIEW_WAIT: WAITING`; paste the Script resolution block first. The first pass records an epoch deadline (`SELFREVIEW_WAIT_SECS`, default 600, max 1800) in a private file keyed by issue and tree; `DONE` and `TIMEOUT` remove it. A domain with no matching comment at the deadline, or only a stale-`Tree:` one, is **missing**: record it and continue (never re-review inline):
|
|
785
|
+
|
|
786
|
+
```bash
|
|
787
|
+
# <Script resolution block from above goes here, verbatim>
|
|
788
|
+
ISSUE={NUMBER}; TREE="{TREE}"; SELECTED="{SELECTED_AGENTS}"
|
|
789
|
+
case "$TREE" in *[!0-9a-f]*|"") echo "SELFREVIEW_WAIT: DONE missing=all (bad tree)"; exit 0 ;; esac
|
|
790
|
+
WAIT_DIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/forge-selfreview-wait-$(id -u)"; mkdir -m 700 -p "$WAIT_DIR" 2>/dev/null
|
|
791
|
+
{ [ -d "$WAIT_DIR" ] && [ -O "$WAIT_DIR" ] && [ ! -L "$WAIT_DIR" ]; } || { echo "SELFREVIEW_WAIT: DONE missing=all (no private wait dir)"; exit 0; }
|
|
792
|
+
DL="${WAIT_DIR}/${ISSUE}_${TREE}"; W="${SELFREVIEW_WAIT_SECS:-600}"; case "$W" in ''|*[!0-9]*) W=600 ;; esac; [ "$W" -gt 1800 ] && W=1800
|
|
793
|
+
NOW=$(date +%s); [ -s "$DL" ] || echo $(( NOW + W )) > "$DL"; DEADLINE=$(cat "$DL" 2>/dev/null); case "$DEADLINE" in ''|*[!0-9]*) DEADLINE=$NOW ;; esac
|
|
794
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh); CJ=$(gh api --paginate repos/{GH_REPO}/issues/${ISSUE}/comments 2>/dev/null || echo "")
|
|
795
|
+
MISSING=""; GOT=0; OUTF=$(mktemp "${TMPDIR:-/tmp}/forge-sr-find.XXXXXX")
|
|
796
|
+
for D in $SELECTED; do
|
|
797
|
+
B=""; [ -n "$TRUSTED_SCRIPT" ] && [ -n "$CJ" ] && B=$(printf '%s' "$CJ" | bash "$TRUSTED_SCRIPT" bodies "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:SELFREVIEW-AGENT:${D} -->[\\s\\S]*(^|\\n)Tree: ${TREE}(\\r?\\n|$)" 2>/dev/null | jq -r 'select(type == "string")' 2>/dev/null)
|
|
798
|
+
if [ -n "$B" ]; then GOT=$((GOT+1)); printf '%s\n' "$B" | sed -n 's/^<!-- FINDING:\(.*\) -->$/SR_FINDING: '"$D"'|\1/p' >> "$OUTF"; else MISSING="$MISSING $D"; fi
|
|
799
|
+
done
|
|
800
|
+
N=$(printf '%s' "$SELECTED" | tr ' ' '\n' | grep -c . || true)
|
|
801
|
+
if [ -z "$MISSING" ]; then rm -f "$DL"; cat "$OUTF"; echo "SELFREVIEW_WAIT: DONE ${GOT}/${N}"
|
|
802
|
+
elif [ "$(date +%s)" -ge "$DEADLINE" ]; then rm -f "$DL"; cat "$OUTF"; echo "SELFREVIEW_WAIT: TIMEOUT ${GOT}/${N} missing:${MISSING}"
|
|
803
|
+
else echo "SELFREVIEW_WAIT: WAITING ${GOT}/${N} missing:${MISSING}"; sleep 30; fi
|
|
804
|
+
rm -f "$OUTF"
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
`DONE` and `TIMEOUT` print one `SR_FINDING: {domain}|{id}|{CONFIDENCE}|{SEVERITY}|{file:line}|{summary}` line per finding of the reviewers that did deliver. Dedupe findings that name the same `file:line` and defect across domains.
|
|
808
|
+
|
|
809
|
+
**Step 4 — one repair round.** The actionable set is every finding with confidence CONFIRMED or LIKELY **and** severity MEDIUM, HIGH or CRITICAL. POSSIBLE and LOW findings are never fixed here; they go into the summary comment as `noted:`. Skip the repair when the set is empty. Otherwise run exactly one round guarded by a `<!-- FORGE:SELFREVIEW_REPAIR: issue={NUMBER} -->` marker (count trusted markers in the current-cycle slice first; if one exists the round is spent, so go to Step 5 with `declined` = the whole set, reason `repair round already spent`):
|
|
810
|
+
1. Post the marker.
|
|
811
|
+
2. `Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-selfreview \"<one SR-<n> line per finding>\"")`. Apply the bounded re-invoke rule if it returns running/empty. `BLOCKED` or a skill-not-found result does not block the build: record `skipped=repair-failed` and continue.
|
|
812
|
+
3. Re-derive `{CHANGED_FILES}` with the B6 command (never reuse the pre-repair value), read `fixed` and `declined` from the `### Self-review` section of the FORGE:BUILDER comment, and re-stage (`git add`). Do not re-run the reviewers: validate gates the result.
|
|
813
|
+
|
|
814
|
+
**Step 5 — record.** Post (or edit, on resume, the latest trusted) one `<!-- FORGE:SELFREVIEW -->` summary comment, then emit the event. `Final-Tree` is `git write-tree` after the repair (equal to `Tree` when none ran); both feed `DONE_TREES` so a re-run never dispatches again:
|
|
815
|
+
|
|
816
|
+
```bash
|
|
817
|
+
# <Script resolution block from above goes here, verbatim>
|
|
818
|
+
# Inline the values from Steps 1-4 (shell state does not persist); counts are integers, 0 when none.
|
|
819
|
+
TREE="{TREE}"; TIER="{TIER}"; SELECTED_AGENTS="{SELECTED_AGENTS}"; SKIP_REASON="{SKIP_REASON}"; MISSING="{MISSING}"
|
|
820
|
+
FINDINGS="{FINDINGS}"; FIXED="{FIXED}"; DECLINED="{DECLINED}"; AGENT_COUNT="{AGENT_COUNT}"
|
|
821
|
+
for _v in FINDINGS FIXED DECLINED AGENT_COUNT; do eval "_x=\${$_v}"; case "$_x" in ''|*[!0-9]*) eval "$_v=0" ;; esac; done
|
|
822
|
+
case "$TREE" in *[!0-9a-f]*) TREE="" ;; esac
|
|
823
|
+
NOTED_LINES=$(cat <<'NOTED_EOF'
|
|
824
|
+
{NOTED_LINES}
|
|
825
|
+
NOTED_EOF
|
|
826
|
+
)
|
|
827
|
+
FINAL_TREE=$(git -C "{WORKTREE_PATH}" write-tree 2>/dev/null || echo "")
|
|
828
|
+
BODY="<!-- FORGE:SELFREVIEW -->
|
|
829
|
+
## Self-review (build-time; not the official review)
|
|
830
|
+
|
|
831
|
+
Tree: ${TREE}
|
|
832
|
+
Final-Tree: ${FINAL_TREE:-$TREE}
|
|
833
|
+
Tier: ${TIER:-none}
|
|
834
|
+
Agents: ${SELECTED_AGENTS:-none}
|
|
835
|
+
Missing: ${MISSING:-none}
|
|
836
|
+
Skipped: ${SKIP_REASON:-none}
|
|
837
|
+
Findings: ${FINDINGS:-0} (fixed ${FIXED:-0}, declined ${DECLINED:-0})
|
|
838
|
+
${NOTED_LINES:-}"
|
|
839
|
+
CID=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" --jq '[.[] | select(.body | startswith("<!-- FORGE:SELFREVIEW -->")) | select((.author_association | IN("OWNER","MEMBER","COLLABORATOR")) or (.user.type == "Bot"))] | last | .id // ""' 2>/dev/null || true)
|
|
840
|
+
if [ -n "$CID" ]; then gh api "repos/{GH_REPO}/issues/comments/${CID}" -X PATCH -f body="$BODY" >/dev/null 2>&1 || gh issue comment {NUMBER} {GH_FLAG} --body "$BODY" # allowlist:check-command-side-effects
|
|
841
|
+
else gh issue comment {NUMBER} {GH_FLAG} --body "$BODY"; fi # allowlist:check-command-side-effects
|
|
842
|
+
_t=$(forge_resolve lib/telemetry.sh); FORGE_REPO="{GH_REPO}" FORGE_ISSUE="{NUMBER}" FORGE_PHASE=build bash "${_t:-/dev/null}" build_selfreview tier="${TIER:-none}" agents="${AGENT_COUNT:-0}" findings="${FINDINGS:-0}" fixed="${FIXED:-0}" declined="${DECLINED:-0}" skipped="${SKIP_REASON:-}" >/dev/null 2>&1 || true # forge#3971 telemetry (ids and counts only; compare with review-found in-PR fixes)
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
Emit on **every** outcome, including skips (`skipped=<reason>`, counts 0). Fill `{NOTED_LINES}` (the heredoc body) with one `noted: <id> <file:line> <summary>` line per sub-threshold finding and one `missing: <domain>` per reviewer that never delivered; the official `/review-pr` reads this comment as known context, never as review evidence. If the repair changed the tree, the next B6 `Skill(...)` call (with the re-derived `{CHANGED_FILES}`) gates it.
|
|
846
|
+
|
|
847
|
+
**Resume**: B5.6 is idempotent. A re-invoked build finds the trusted `FORGE:SELFREVIEW` summary, the plan script reports `already-done` for its `Tree:`/`Final-Tree:`, and the spent `FORGE:SELFREVIEW_REPAIR` marker forbids a second repair round.
|
|
848
|
+
# MUST CONTINUE to Phase B6 — the self-review result is intermediate, NOT terminal (validation still required). Any failure here is a recorded skip, never a Blocked exit.
|
|
740
849
|
|
|
741
850
|
---
|
|
742
851
|
|
|
@@ -755,8 +864,13 @@ CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HE
|
|
|
755
864
|
```
|
|
756
865
|
|
|
757
866
|
**After subcommand returns** (read its `VALIDATE_RESULT:` block):
|
|
758
|
-
- `gate_passed: true` →
|
|
759
|
-
- `gate_passed: false` → the
|
|
867
|
+
- `gate_passed: true` → record `gate_status` (`PASS` or `DEGRADED`) and `degraded_checks` from the `VALIDATE_RESULT:` block for B6.5 (the FORGE:QUALITY_GATE `**Degraded**:` line is the source of truth); a DEGRADED gate never blocks the build. Verify the `FORGE:QUALITY_GATE` marker exists on the issue (posted by validate V5; docs-only changes exempt). If absent, re-derive `{CHANGED_FILES}` with the B6 command above and re-invoke validate once with the full B6 `Skill(...)` call, including `--files "{CHANGED_FILES}"` (never omit `--files`; an empty list falls back to the git diff); a missing marker is not a pass. Read the job ids under `### Deferred to CI` in the trusted `FORGE:BUILDER` comment (jobs the local CI gate could not prove; remote CI at merge must) for `ci_deferred` in the result. Then continue to Phase B6.5 (acceptance gate)
|
|
868
|
+
- `gate_passed: false` with a `blocker` starting `consumer_unaddressed` → the consumer sweep failed in `blocking` mode. This is a repair, never BLOCKED and never `needs-human` <!-- Added: forge#4017 -->: remove `needs-human` if one is present, then run **one** repair round guarded by the `<!-- FORGE:CONSUMER_REPAIR: issue={NUMBER} -->` marker (count the markers first; if one already exists the round is spent, so go to step 3):
|
|
869
|
+
1. Post the marker with the `UNADDRESSED:` site list from the blocker.
|
|
870
|
+
2. Invoke `Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-consumers \"<UNADDRESSED lines>\"")`, then re-derive `{CHANGED_FILES}` with the B6 command above (never reuse the pre-repair value).
|
|
871
|
+
3. Re-invoke validate with the full B6 `Skill(...)` call plus `--consumers-final`. That run sweeps in `advisory` mode, so sites that are still unaddressed are recorded in the FORGE:BUILDER comment as `CONSUMERS: unresolved ...` and travel to review as known risk; the gate then passes or fails on its other checks only. Handle its `VALIDATE_RESULT` with the other rules in this list (a `consumer_unaddressed` blocker cannot recur, because `--consumers-final` never fails the sweep).
|
|
872
|
+
- `gate_passed: false` with a `blocker` starting `ci-local:` → the subcommand has already posted its comment and added `needs-human`; print `BUILD_RESULT: status: BLOCKED` with the child's `blocker` (run the Blocked exit if no comment was posted). This blocker (validate V4.8, the local CI gate) is final: validate already spent its 2 fix iterations in the same warm context, so do not re-invoke validate or repair it here <!-- Added: forge#3973 -->
|
|
873
|
+
- `gate_passed: false` (any other blocker) → the subcommand has already posted its comment and added `needs-human`; print `BUILD_RESULT: status: BLOCKED` with the child's `blocker` (run the Blocked exit if no comment was posted)
|
|
760
874
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:validate"
|
|
761
875
|
- Returned running/backgrounded/empty (no `VALIDATE_RESULT:`) → do not end the turn to wait; re-read the `FORGE:QUALITY_GATE` marker (consume it if present) and the worktree state before re-invoking, then re-invoke the same child under the bounded re-invoke rule (at most 2, then `child-stalled: validate`); this cap is separate from the single marker-absent re-invoke above
|
|
762
876
|
|
|
@@ -818,6 +932,9 @@ The acceptance spec contains only a skip sentinel (\`type=skipped\`). No automat
|
|
|
818
932
|
```bash
|
|
819
933
|
# <Script resolution block, verbatim>
|
|
820
934
|
GREP_CLS=$(forge_resolve classify-grep-exit.sh) # empty/unresolved = every grep exit>=2 is a real FAIL (fail closed)
|
|
935
|
+
RG_SCRIPT=$(forge_resolve acceptance-red-green.sh) # empty/unresolved = every test= check is DEGRADED, never PASS <!-- Added: forge#3630 -->
|
|
936
|
+
RG_ROWS="" # per-AC red-green record rows for `test=` checks: | AC | test | base | head |
|
|
937
|
+
RG_DEGRADED_LIST="" # test= checks whose base run cannot execute; merged into DEGRADED_LIST after the gate-status block
|
|
821
938
|
GATE_PASS=true
|
|
822
939
|
CORRECTED=""
|
|
823
940
|
FAILED_CHECKS=""
|
|
@@ -865,6 +982,8 @@ while IFS= read -r check_line; do
|
|
|
865
982
|
# Anchored after the matcher field so `phase=` text inside description/target/matcher cannot hijack it; any other value or an absent token is `build` (fail closed).
|
|
866
983
|
PHASE=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=("(([^"\\]|\\.)*)"|[^ ]*) matcher=("(([^"\\]|\\.)*)"|[^ ]*) phase=(build|review)( .*)?$/\7/p')
|
|
867
984
|
[ "$PHASE" = "review" ] || PHASE=build
|
|
985
|
+
# test= (optional, after phase=, before description=): the runnable test that proves the AC, red-green checked below (forge#3630). Absent = no change.
|
|
986
|
+
TEST_RG=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=("(([^"\\]|\\.)*)"|[^ ]*) matcher=("(([^"\\]|\\.)*)"|[^ ]*) phase=(build|review) test="(([^"\\]|\\.)*)"( description=.*)?$/\8/p' | sed 's/\\"/"/g')
|
|
868
987
|
|
|
869
988
|
[ "$TYPE" = "skipped" ] && { MANUAL_CHECKS="${MANUAL_CHECKS}\n- [ ] **$ID** ($DESC)"; continue; }
|
|
870
989
|
|
|
@@ -876,6 +995,8 @@ while IFS= read -r check_line; do
|
|
|
876
995
|
MALFORMED=""
|
|
877
996
|
{ [ "$TYPE" = "command" ] || [ "$TYPE" = "behavior" ]; } && { case "$TARGET" in *'\\\\'*) MALFORMED="run of 4+ backslashes (over-escaped target)";; esac; [ -n "$MALFORMED" ] || bash -n -c "$TARGET" 2>/dev/null || MALFORMED="shell syntax error (unbalanced quotes)"; }
|
|
878
997
|
{ [ "$TYPE" = "command" ] || [ "$TYPE" = "behavior" ]; } && [ -z "$MALFORMED" ] && case "$(printf '%s' "$TARGET" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" in none) MALFORMED='target "none" is not a command (a manual criterion must be type=skipped)';; esac
|
|
998
|
+
# A test= value that would break description= extraction or fails the same static parse is a check defect (never executed).
|
|
999
|
+
if [ -n "$TEST_RG" ]; then case "$TEST_RG" in *description=*) MALFORMED="test= contains description= (ambiguous field boundary)";; esac; [ -n "$MALFORMED" ] || bash -n -c "$TEST_RG" 2>/dev/null || MALFORMED="test= shell syntax error (unbalanced quotes)"; fi
|
|
879
1000
|
if [ -n "$MALFORMED" ]; then MALFORMED_CHECKS="${MALFORMED_CHECKS}\n- **$ID** ($DESC): $MALFORMED"; continue; fi
|
|
880
1001
|
|
|
881
1002
|
RESULT="PASS"
|
|
@@ -900,12 +1021,12 @@ while IFS= read -r check_line; do
|
|
|
900
1021
|
# grep exit >= 2 is a usage/syntax error (e.g. a pattern starting with `-`), never a verdict on the code; exit 1 stays FAIL.
|
|
901
1022
|
# Malformed only when stderr positively names a usage error (classify-grep-exit.sh); empty stderr (`2>/dev/null`, `-s`) or any operand error is a REAL failure.
|
|
902
1023
|
[ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
|
|
903
|
-
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -
|
|
1024
|
+
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && { FIRST=${TARGET#"${TARGET%%[![:space:]]*}"}; FIRST=${FIRST%%[[:space:]]*}; [ -n "$FIRST" ] && [ "${FIRST#*/}" = "$FIRST" ] && ! command -v -- "$FIRST" >/dev/null 2>&1 && printf '%s\n' "$ERR" | grep -qE "(^|: )$(printf '%s' "$FIRST" | sed 's/[][\.*^$(){}?+|/]/\\&/g'):"; } && MALFORMED="command not found (rc 127): the target is not an executable command"
|
|
904
1025
|
[ "$RC" -eq 0 ] || [ -n "$MALFORMED" ] || { RESULT="FAIL"; DETAIL="command exited non-zero: $TARGET"; }
|
|
905
1026
|
else
|
|
906
1027
|
ERRF=$(mktemp); OUTPUT=$(eval "$TARGET" 2>"$ERRF"); RC=$?; ERR=$(cat "$ERRF"); rm -f "$ERRF"; [ -z "$ERR" ] || OUTPUT="${OUTPUT}${OUTPUT:+$'\n'}${ERR}" # classify on stderr only, match on both
|
|
907
1028
|
[ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
|
|
908
|
-
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -
|
|
1029
|
+
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && { FIRST=${TARGET#"${TARGET%%[![:space:]]*}"}; FIRST=${FIRST%%[[:space:]]*}; [ -n "$FIRST" ] && [ "${FIRST#*/}" = "$FIRST" ] && ! command -v -- "$FIRST" >/dev/null 2>&1 && printf '%s\n' "$ERR" | grep -qE "(^|: )$(printf '%s' "$FIRST" | sed 's/[][\.*^$(){}?+|/]/\\&/g'):"; } && MALFORMED="command not found (rc 127): the target is not an executable command"
|
|
909
1030
|
if [ -n "$MALFORMED" ]; then :
|
|
910
1031
|
elif ! printf '%s\n' "$OUTPUT" | grep -qE "$MATCHER"; then
|
|
911
1032
|
if printf '%s\n' "$OUTPUT" | grep -qF -- "$MATCHER"; then CORRECTED="${CORRECTED}\n- **$ID**: output matched as a literal string"
|
|
@@ -925,6 +1046,24 @@ while IFS= read -r check_line; do
|
|
|
925
1046
|
case "$TARGET" in *'gh pr'*) DEFERRED_COUNT=$((DEFERRED_COUNT+1)); DEFERRED_CHECKS="${DEFERRED_CHECKS}\n- **$ID** ($DESC): DEFERRED (review) (no pull request exists yet)"; continue;; esac
|
|
926
1047
|
fi
|
|
927
1048
|
|
|
1049
|
+
# Red-green proof <!-- Added: forge#3630 -->: only for a build-phase check with test= whose own head result is PASS.
|
|
1050
|
+
# The helper runs test= on head, then (head RAN only) on a detached base worktree (`git worktree add --detach` at the merge-base) with head's changed test files overlaid;
|
|
1051
|
+
# the worktree is removed on every exit path (trap + explicit `git worktree remove`). Output: `BASE=<..> HEAD=<..>`.
|
|
1052
|
+
if [ "$RESULT" = "PASS" ] && [ -n "$TEST_RG" ]; then
|
|
1053
|
+
RG_OUT=""; [ -n "$RG_SCRIPT" ] && RG_OUT=$(bash "$RG_SCRIPT" --repo-path "{WORKTREE_PATH}" --base-ref "origin/{PR_BASE}" --test-cmd "$TEST_RG" 2>/dev/null)
|
|
1054
|
+
RG_BASE=$(printf '%s\n' "$RG_OUT" | sed -n 's/^BASE=\([A-Z_]*\) HEAD=\([A-Z_]*\)$/\1/p' | head -n 1)
|
|
1055
|
+
RG_HEAD=$(printf '%s\n' "$RG_OUT" | sed -n 's/^BASE=\([A-Z_]*\) HEAD=\([A-Z_]*\)$/\2/p' | head -n 1)
|
|
1056
|
+
[ -n "$RG_BASE" ] && [ -n "$RG_HEAD" ] || { RG_BASE=UNRUNNABLE; RG_HEAD=UNRUNNABLE; } # helper missing or unparsable: DEGRADED, never PASS
|
|
1057
|
+
RG_ID=$(printf '%s' "$TEST_RG" | tr '\n\r|`\\' ' ' | cut -c1-80) # table-safe: no pipe, backtick, backslash or newline; truncated
|
|
1058
|
+
RG_ROWS="${RG_ROWS}${RG_ROWS:+$'\n'}| $ID | $RG_ID | $RG_BASE | $RG_HEAD |"
|
|
1059
|
+
case "$RG_HEAD:$RG_BASE" in
|
|
1060
|
+
RAN:FAILED) : ;; # red on base, green on head: the test proves the AC
|
|
1061
|
+
RAN:RAN) RESULT="FAIL"; DETAIL="test passes on base (it never exercised the defect): rewrite the test so it fails on the base commit and passes on head" ;;
|
|
1062
|
+
FAILED:*) RESULT="FAIL"; DETAIL="test= command fails on head: $RG_ID" ;;
|
|
1063
|
+
*) RG_DEGRADED_LIST="${RG_DEGRADED_LIST}${RG_DEGRADED_LIST:+; }$ID — base run cannot execute (base=$RG_BASE head=$RG_HEAD), so the test is unproven" ;; # a base run that cannot execute is DEGRADED, never PASS
|
|
1064
|
+
esac
|
|
1065
|
+
fi
|
|
1066
|
+
|
|
928
1067
|
VERIFIED_COUNT=$((VERIFIED_COUNT+1))
|
|
929
1068
|
if [ "$RESULT" = "FAIL" ]; then
|
|
930
1069
|
GATE_PASS=false
|
|
@@ -941,6 +1080,26 @@ if [ "$VERIFIED_COUNT" -eq 0 ] && [ "$DEFERRED_COUNT" -eq 0 ]; then
|
|
|
941
1080
|
fi
|
|
942
1081
|
```
|
|
943
1082
|
|
|
1083
|
+
**Gate status (DEGRADED)** <!-- Added: forge#3628 -->: `GATE_STATUS` and `DEGRADED_LIST` come from the B6 `VALIDATE_RESULT` (`gate_status`, `degraded_checks`). When B6.5 is entered without one (B0 gate resume), read them from the `**Degraded**:` line of the newest trusted `FORGE:QUALITY_GATE` comment; an unreadable lookup is DEGRADED, never a pass. A DEGRADED gate means a test run exercised nothing, a known-slow test was skipped, or the blast-radius check did not complete. It does not fail the build: when every check passes, the gate closes with `<!-- FORGE:ACCEPTANCE_GATE:DEGRADED -->` instead of `:PASSED` and lists the degraded checks. Review must cover each one before merge (`scripts/review-finalize.sh`, `degraded-gate-unreviewed`).
|
|
1084
|
+
|
|
1085
|
+
```bash
|
|
1086
|
+
# <Script resolution block, verbatim>
|
|
1087
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
1088
|
+
if [ -z "${GATE_STATUS:-}" ]; then
|
|
1089
|
+
GATE_STATUS=PASS; DEGRADED_LIST=""
|
|
1090
|
+
if [ -z "$TRUSTED_SCRIPT" ] || ! QG_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments 2>/dev/null) \
|
|
1091
|
+
|| ! QG_BODIES=$(printf '%s' "$QG_COMMENTS" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:QUALITY_GATE -->'); then
|
|
1092
|
+
GATE_STATUS=DEGRADED; DEGRADED_LIST="quality-gate — degraded state unreadable"
|
|
1093
|
+
else
|
|
1094
|
+
DEGRADED_LIST=$(printf '%s\n' "$QG_BODIES" | tail -n 1 | jq -r 'select(type == "string")' | sed -n 's/^\*\*Degraded\*\*: //p' | head -n 1)
|
|
1095
|
+
case "$DEGRADED_LIST" in ''|none) DEGRADED_LIST="" ;; *) GATE_STATUS=DEGRADED ;; esac
|
|
1096
|
+
fi
|
|
1097
|
+
fi
|
|
1098
|
+
# test= checks whose base run cannot execute (forge#3630): merged after the lookup above so it cannot wipe them
|
|
1099
|
+
[ -n "$RG_DEGRADED_LIST" ] && { GATE_STATUS=DEGRADED; DEGRADED_LIST="${DEGRADED_LIST:+$DEGRADED_LIST; }$RG_DEGRADED_LIST"; }
|
|
1100
|
+
[ "$GATE_STATUS" = "DEGRADED" ] && [ -z "${DEGRADED_LIST:-}" ] && DEGRADED_LIST="unspecified (see FORGE:QUALITY_GATE)"
|
|
1101
|
+
```
|
|
1102
|
+
|
|
944
1103
|
**Post gate result comment**:
|
|
945
1104
|
|
|
946
1105
|
```bash
|
|
@@ -961,16 +1120,19 @@ $( [ -n "$MANUAL_CHECKS" ] && printf '\n**Manual follow-up (type=skipped; not ve
|
|
|
961
1120
|
<!-- FORGE:ACCEPTANCE_GATE:BLOCKED -->"
|
|
962
1121
|
run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
963
1122
|
elif [ "$GATE_PASS" = "true" ]; then
|
|
1123
|
+
GATE_MARKER=PASSED; [ "${GATE_STATUS:-PASS}" = "DEGRADED" ] && GATE_MARKER=DEGRADED
|
|
964
1124
|
run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
|
|
965
|
-
## Acceptance Gate —
|
|
1125
|
+
## Acceptance Gate — ${GATE_MARKER}
|
|
966
1126
|
|
|
967
1127
|
All machine-checkable build-phase acceptance criteria verified against real behavior.
|
|
1128
|
+
$( [ "$GATE_MARKER" = "DEGRADED" ] && printf '\n**Degraded checks (verified nothing; the review must name each one under `### Degraded Paths Reviewed` before merge)**: %s\n' "$DEGRADED_LIST" )
|
|
1129
|
+
$( [ -n "$RG_ROWS" ] && printf '\n**Red-green proof (test= checks: must FAIL on base, PASS on head)**:\n\n| AC | test | base | head |\n|----|------|------|------|\n%s\n' "$RG_ROWS" )
|
|
968
1130
|
$( [ -n "$DEFERRED_CHECKS" ] && printf '\n**Deferred to review (phase=review; evaluated against the PR before merge)**:%b\n' "$DEFERRED_CHECKS" )
|
|
969
1131
|
$( [ -n "$MANUAL_CHECKS" ] && printf '\n**Manual follow-up (type=skipped; not verified at build time, confirm after merge/deploy)**:%b\n' "$MANUAL_CHECKS" )
|
|
970
1132
|
$( [ -n "$CORRECTED" ] && printf '\n**Checks corrected (malformed matcher, literal text present)**:%b\n' "$CORRECTED" )
|
|
971
1133
|
$( [ -n "$MALFORMED_CHECKS" ] && printf '\n**malformed-check (not counted as failures; regenerate the acceptance spec by re-running investigate)**:%b\n' "$MALFORMED_CHECKS" )
|
|
972
1134
|
|
|
973
|
-
<!-- FORGE:ACCEPTANCE_GATE
|
|
1135
|
+
<!-- FORGE:ACCEPTANCE_GATE:${GATE_MARKER} -->"
|
|
974
1136
|
else
|
|
975
1137
|
run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
|
|
976
1138
|
## Acceptance Gate — FAILED
|
|
@@ -978,6 +1140,7 @@ else
|
|
|
978
1140
|
The following acceptance checks did not pass:
|
|
979
1141
|
|
|
980
1142
|
$(echo -e "$FAILED_CHECKS")
|
|
1143
|
+
$( [ -n "$RG_ROWS" ] && printf '\n**Red-green proof (test= checks: must FAIL on base, PASS on head)**:\n\n| AC | test | base | head |\n|----|------|------|------|\n%s\n' "$RG_ROWS" )
|
|
981
1144
|
$( [ -n "$MALFORMED_CHECKS" ] && printf '\n**malformed-check (not counted as failures; regenerate the acceptance spec by re-running investigate)**:%b\n' "$MALFORMED_CHECKS" )
|
|
982
1145
|
|
|
983
1146
|
Merge is blocked. Fix the failing criteria and re-run the validate phase.
|
|
@@ -989,7 +1152,7 @@ fi
|
|
|
989
1152
|
|
|
990
1153
|
A `malformed-check` (a command/behavior target with a run of 4+ backslashes or a `bash -n` syntax error, or a grep target exiting >= 2 for a reason other than a missing/unreadable operand file, which stays a real FAIL) is reported in the comment and never sets `GATE_PASS=false` by itself, never enters `FAILED_CHECKS`, is never passed to `--fix-acceptance`, and never adds `needs-human` by itself; the gate still posts exactly one of PASSED/FAILED. The gate never posts PASSED with zero verified checks: if every check was malformed (`VERIFIED_COUNT=0` and `DEFERRED_COUNT=0`) it is a spec-defect: it posts `FORGE:ACCEPTANCE_GATE:BLOCKED`, adds `needs-human`, prints `BUILD_RESULT: status: BLOCKED` (blocker: "regenerate acceptance spec via investigate") and skips the `--fix-acceptance` repair entirely, because implement cannot fix a spec defect. <!-- Added: forge#3694, forge#3825 -->
|
|
991
1154
|
|
|
992
|
-
Also `malformed-check` <!-- Added: forge#3825 -->: a `command`/`behavior` target that is literally `none` (any case), or
|
|
1155
|
+
Also `malformed-check` <!-- Added: forge#3825 -->: a `command`/`behavior` target that is literally `none` (any case), or whose own first word is not a command (exit 127 with `<first word>:` leading the stderr message and the word a bare name (no `/`) not resolvable via `command -v`; the target runs in its native locale, so verdict-bearing output is never altered and the check does not depend on the English message text). Classification keys on that stderr text, never on the code alone, and only on the target's first word: a script whose inner command is missing (`bash x.sh` exiting 127 on a tool inside it) stays a real FAIL, as does a leading env assignment or compound (`FOO=1 cmd`, `a && missing`). A missing script (a `bash` run of a nonexistent script path, with `No such file or directory`) also exits 127 and stays a real FAIL, so the missing-operand class is not reopened. A per-check `type=skipped` is never executed, never counted, and is listed under "Manual follow-up" as unchecked items in the PASSED comment.
|
|
993
1156
|
|
|
994
1157
|
A `phase=review` check (or, for legacy specs, tagged or not, a `command`/`behavior` check on a `gh pr` target whose output contains `no pull requests found`) is recorded as `DEFERRED (review)` in the PASSED comment: it is never executed here, never counted in `VERIFIED_COUNT`, never sets `GATE_PASS=false`, never enters `FAILED_CHECKS` or `--fix-acceptance`, and never spends the repair. A spec whose checks are all deferred posts PASSED (it is not "No Spec Found"). Review R2.5 evaluates the deferred checks against the real PR and blocks merge on failure. <!-- Added: forge#3793 -->
|
|
995
1158
|
|
|
@@ -1010,6 +1173,8 @@ If `GATE_PASS = false` and `SPEC_DEFECT` is not `true`, the build repairs itself
|
|
|
1010
1173
|
3. Re-run this whole B6.5 gate. PASS → continue to the checkpoint.
|
|
1011
1174
|
4. Still failing (or already repaired once) → leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "Acceptance gate failed after one repair — see FORGE:ACCEPTANCE_GATE comment".
|
|
1012
1175
|
|
|
1176
|
+
**Red-green proof (`test=`)** <!-- Added: forge#3630 -->: a `phase=build` check carrying `test="<cmd>"` whose own head result is PASS also runs `scripts/acceptance-red-green.sh`: the test must FAIL on the merge-base commit (head's changed test files overlaid, so a new test file is not a false red) and PASS on head. Every such check gets a row in the gate comment's `| AC | test | base | head |` table. A test that passes on base FAILS the gate (`passes on base`) and enters the one-repair `--fix-acceptance` path with the instruction to rewrite the test so it fails on base. A base run that cannot execute (worktree or overlay failure, timeout, rc 126/127, a runner that exercised nothing, or an unresolved helper) is DEGRADED, never PASS: it is appended to `DEGRADED_LIST` and the gate closes `FORGE:ACCEPTANCE_GATE:DEGRADED` (#3628). A `test=` that contains `description=` or fails `bash -n` is a `malformed-check`. Checks without `test=` evaluate exactly as before.
|
|
1177
|
+
|
|
1013
1178
|
If `GATE_PASS = true`: continue to write the phase checkpoint below.
|
|
1014
1179
|
|
|
1015
1180
|
**When the gate passed — write machine-readable phase checkpoint before returning (MANDATORY)**:
|
|
@@ -1033,12 +1198,14 @@ BUILD_RESULT:
|
|
|
1033
1198
|
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | BLOCKED
|
|
1034
1199
|
branch: {BRANCH}
|
|
1035
1200
|
worktree: {WORKTREE_PATH}
|
|
1201
|
+
ci_deferred: [{job id}, ...] # deferred-to-ci jobs from FORGE:BUILDER; empty when none
|
|
1036
1202
|
blocker: {description if status=BLOCKED}
|
|
1037
1203
|
```
|
|
1038
1204
|
|
|
1039
|
-
- `COMPLETE` — B0–B6.5 all passed; `FORGE:BUILDER:COMPLETE` is on the issue.
|
|
1205
|
+
- `COMPLETE` — B0–B6.5 all passed (a DEGRADED acceptance gate, `FORGE:ACCEPTANCE_GATE:DEGRADED`, counts as passed here); `FORGE:BUILDER:COMPLETE` is on the issue.
|
|
1040
1206
|
- `ALREADY_DONE` — B0 found `FORGE:BUILDER:COMPLETE`.
|
|
1041
1207
|
- `INVESTIGATION_COMPLETE` — the implement child created the deliverable issues (B5); no review follows.
|
|
1208
|
+
- B5.6 never produces a `BUILD_RESULT` status of its own either: every outcome (ran, skipped, degraded) is recorded in `FORGE:SELFREVIEW` and the `build_selfreview` event, then the build continues to B6. <!-- Added: forge#3971 -->
|
|
1042
1209
|
- B5.5 never produces a `BUILD_RESULT` status of its own: its scope-check outcome (`OK`, `EXTENDED_REVIEW`, `SCOPE_FLAGGED`, `OVERRIDDEN`) is recorded in `FORGE:DIFF_SIZE`, then the build continues to B6 and ends `COMPLETE`. A finished build is never routed to decompose or discarded. <!-- Added: forge#3862 -->
|
|
1043
1210
|
- `BLOCKED` — any guard or failure; `needs-human` is set and the blocker is posted.
|
|
1044
1211
|
|
|
@@ -1049,7 +1216,7 @@ BUILD_RESULT:
|
|
|
1049
1216
|
Position in the work-on pipeline (label `workflow:ready-to-build` or `workflow:building`):
|
|
1050
1217
|
|
|
1051
1218
|
```
|
|
1052
|
-
investigate → [THIS MODULE] worktree + classify + contract + context + architect + implement + validate + acceptance-gate
|
|
1219
|
+
investigate → [THIS MODULE] worktree + classify + contract + context + architect + implement + self-review + validate + acceptance-gate
|
|
1053
1220
|
→ posts FORGE:FAST_PATH, FORGE:CONTRACT, FORGE:BUILDER(:COMPLETE), FORGE:ACCEPTANCE_GATE; writes FORGE:CHECKPOINT next_phase=REVIEW
|
|
1054
1221
|
→ work-on:review (push branch, create PR, review, merge)
|
|
1055
1222
|
→ work-on:close
|
|
@@ -1256,7 +1256,7 @@ Where:
|
|
|
1256
1256
|
|
|
1257
1257
|
**Goal**: Re-index this issue's knowledge cards and regenerate cost-priors.json so that economic scheduling (orchestrate Step 3E.5) has up-to-date data for future runs. The actual-vs-prior delta recorded in the TRAJECTORY above is the write side of the self-correction loop — this step performs the read/recompute.
|
|
1258
1258
|
|
|
1259
|
-
**This phase is non-blocking** — if the indexer fails, log the reason and continue to Phase C5.2. Never stall close for the cost-prior update.
|
|
1259
|
+
**This phase is non-blocking** — if the indexer fails, log the reason (last 20 lines of its output) and always continue to Phase C5.2–C5.4. Never stall close for the cost-prior update.
|
|
1260
1260
|
|
|
1261
1261
|
**Skip if**: Terminal state is `INVALID` (no useful cost data from invalid issues).
|
|
1262
1262
|
|
|
@@ -1267,9 +1267,14 @@ INDEXER_PATH=""
|
|
|
1267
1267
|
[ -n "${FORGE_ROOT:-}" ] && INDEXER_PATH="$FORGE_ROOT/scripts/build-knowledge-index.mjs"
|
|
1268
1268
|
if node --version >/dev/null 2>&1 && [ -f "$INDEXER_PATH" ]; then
|
|
1269
1269
|
echo "[cost-prior] Re-indexing issue #${NUMBER} and regenerating cost priors..."
|
|
1270
|
-
node
|
|
1271
|
-
|
|
1272
|
-
|
|
1270
|
+
# Capture output and take the status from node itself (a pipe to tail would mask it); --repo targets the repo being closed
|
|
1271
|
+
INDEXER_OUT=$(node "$INDEXER_PATH" --repo {GH_REPO} --issue {NUMBER} --no-mirror 2>&1) && INDEXER_RC=0 || INDEXER_RC=$?
|
|
1272
|
+
if [ "$INDEXER_RC" -eq 0 ]; then
|
|
1273
|
+
echo "[cost-prior] Cost priors updated"
|
|
1274
|
+
else
|
|
1275
|
+
echo "WARNING: Cost prior update failed (exit $INDEXER_RC) — continuing (non-blocking). Last output:"
|
|
1276
|
+
printf '%s\n' "$INDEXER_OUT" | tail -20
|
|
1277
|
+
fi
|
|
1273
1278
|
else
|
|
1274
1279
|
echo "[cost-prior] Indexer not available — skipping cost prior update (non-blocking)"
|
|
1275
1280
|
fi
|
|
@@ -715,6 +715,13 @@ ACCEPTANCE_CHECK: id=ac-7 type=command target="gh pr view --json body --jq .body
|
|
|
715
715
|
|
|
716
716
|
**Phase tag (MANDATORY)** <!-- Added: forge#3793 -->: `phase=` is an optional token placed after `matcher="..."` and before `description=`; allowed values are `build` (default when absent) and `review`. `build` checks run in the build acceptance gate, against the worktree. `review` checks are deferred by the build gate and evaluated by review after the PR exists. Any check whose target reads a PR artifact (`gh pr view`, `gh pr checks`, `gh pr diff`, review comments) MUST be `phase=review`: the PR does not exist until the review phase opens it, so such a check can never pass at build time. Do not put `phase=` inside `target`, `matcher` or `description` text.
|
|
717
717
|
|
|
718
|
+
**Test tag (optional)** <!-- Added: forge#3630 -->: `test="<cmd>"` is an optional token placed after `phase=` and before `description=`. It names the runnable test that proves the criterion (for example `test="node --test bin/tests/foo.test.mjs"`). The build gate runs it on the head worktree and on the merge-base commit: a real proof FAILS on base and PASSES on head, a test that also passes on base fails the gate (it never touched the defect), and a base run that cannot execute makes the gate DEGRADED. Quote it like `target=`, escape an embedded `"` as `\"`, and never write the text `description=` inside it. Emit it only on `phase=build` checks whose test is runnable by a single command; it is forbidden on `phase=review` and `type=skipped` checks (never executed). Without `test=` the check evaluates exactly as before.
|
|
719
|
+
|
|
720
|
+
Example:
|
|
721
|
+
```
|
|
722
|
+
ACCEPTANCE_CHECK: id=ac-3 type=command target="node --test bin/tests/foo.test.mjs" matcher="exit_0" phase=build test="node --test bin/tests/foo.test.mjs" description=Rejects an empty input
|
|
723
|
+
```
|
|
724
|
+
|
|
718
725
|
**Check types**:
|
|
719
726
|
- `exists` — assert a file or directory exists (`target` = path, `matcher` = ignored)
|
|
720
727
|
- `contains` — assert a file contains a string or regex (`target` = file path, `matcher` = string/regex). **Prefer a literal string** for code: the matcher is applied as an extended regex first, so `$`, `(`, `[`, `.`, `*`, `+`, `?`, `|` must be escaped if you mean them literally (e.g. `return \"\$HELD\"`). The acceptance gate falls back to a literal match, but a correct matcher avoids a needless repair round. For a `command` check that looks for code, use `grep -qF '<literal>' <file>`, not `grep -qE`.
|