forgedock 1.30.0 → 1.31.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/bin/engine/outcome-codes.mjs +3 -2
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +362 -33
- package/bin/engine.mjs +47 -37
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +294 -1
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +150 -15
- package/bin/lean/conformance.test.mjs +175 -3
- package/bin/lean/context.mjs +116 -43
- package/bin/lean/context.test.mjs +139 -4
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +37 -15
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +650 -124
- package/bin/lean/lean.test.mjs +1450 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +261 -38
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +138 -13
- package/bin/lean/precheck.test.mjs +113 -1
- package/bin/lean/prefilter.mjs +159 -5
- package/bin/lean/prefilter.test.mjs +152 -3
- package/bin/lean/preflight.mjs +108 -3
- package/bin/lean/preflight.test.mjs +48 -1
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +101 -13
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.test.mjs +127 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +149 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +296 -13
- package/bin/lean/sequence.test.mjs +263 -2
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +72 -10
- package/bin/lean/verify.test.mjs +68 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +319 -0
- package/bin/stats/efficiency.test.mjs +233 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +69 -6
- package/bin/stats/local-source.mjs +86 -3
- package/bin/stats/query.mjs +48 -0
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +224 -5
- package/commands/orchestrate.md +3 -1
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +80 -5
- package/docs/OUTCOME-CODES.md +2 -1
- package/docs/STATS.md +53 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/trusted-comments.test.sh +1 -1
|
@@ -67,25 +67,6 @@ If you find yourself about to call `Agent(...)` while `Task` is available, stop
|
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
70
|
-
## Config Resolution
|
|
71
|
-
|
|
72
|
-
Read `forge.yaml` to resolve branch names before running any commands:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
76
|
-
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
|
|
77
|
-
GH_FLAG="-R $GH_REPO"
|
|
78
|
-
DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
|
|
79
|
-
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
|
|
80
|
-
|
|
81
|
-
# Test-gate config (Phase 6.5) — read here so vars are available before first use
|
|
82
|
-
GATE_POSTURE=$(yq '.verification.test_gate.posture // "blocking"' "$CONFIG_FILE" 2>/dev/null || echo "blocking")
|
|
83
|
-
OVERRIDE_PHRASE=$(yq '.verification.test_gate.override_phrase // "OVERRIDE: shipping with test failures —"' "$CONFIG_FILE" 2>/dev/null || echo "OVERRIDE: shipping with test failures —")
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
All `$DEFAULT_BRANCH`, `$STAGING_BRANCH`, `$GATE_POSTURE`, and `$OVERRIDE_PHRASE` references below are populated from `forge.yaml`.
|
|
87
|
-
|
|
88
|
-
---
|
|
89
70
|
|
|
90
71
|
## Review Protocol Reference
|
|
91
72
|
|
|
@@ -104,22 +85,18 @@ The **Evidence-Based Review Protocol** and **Structured Findings Protocol** are
|
|
|
104
85
|
|
|
105
86
|
---
|
|
106
87
|
|
|
107
|
-
##
|
|
88
|
+
## Step 1: Pre-dispatch (`forgedock review-staging`)
|
|
108
89
|
|
|
109
|
-
**This
|
|
90
|
+
**This step is MANDATORY and runs first. No step may be skipped.** One CLI call runs the deterministic pre-dispatch (forge#4358): the `FORGE:REVIEW_ROUTE` marker (route assertion and audit trail), prior-finding awareness on re-entry, the Phase 0A bundle PR set and open review-finding gate (it posts `FORGE:GATE_FAILURE` / `FORGE:GATE_PASS` and stops on a block, including degraded bundle panels), the 0B.1 bundle review scope (`scripts/review-staging-pre.sh`), the 0C prepare (`scripts/review-prepare.sh --no-delta --build-gate advisory`) and the risk-tiered roster (`scripts/review-roster.sh`, inside prepare). It writes one reviewer brief per roster domain and truncates the PR-keyed launched-agent file. All state lives in `$FORGE_SCRATCHPAD` (default `$PWD/.forge-scratch`) under the PR-keyed names the later steps read. Every helper comes from the ForgeDock install, never from the repo under review.
|
|
110
91
|
|
|
111
|
-
**Idempotent re-review (merge-train aware)**: <!-- Added: forge#1328, extended: forge#1332 --> This spec is safe to invoke multiple times on the same PR. Each invocation posts a new `FORGE:REVIEW_ROUTE` marker with the current SHA
|
|
112
|
-
|
|
113
|
-
Resolve the staging→main PR number and post a routing marker immediately. This creates an audit trail — if a staging→main PR has no `FORGE:REVIEW_ROUTE` comment after this command was invoked, the review was bypassed or never started.
|
|
92
|
+
**Idempotent re-review (merge-train aware)**: <!-- Added: forge#1328, extended: forge#1332 --> This spec is safe to invoke multiple times on the same PR. Each invocation posts a new `FORGE:REVIEW_ROUTE` marker with the current SHA, so re-review passes are distinguished by SHA, not by PR number. Prior open findings are carried forward into the briefs (do not re-file them). Prior resolved findings are excluded from the verdict.
|
|
114
93
|
|
|
115
94
|
```bash
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE" 2>/dev/null); STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE" 2>/dev/null)
|
|
119
|
-
# Resolve PR_NUMBER from the argument string (never name the loader placeholder in this block: every
|
|
95
|
+
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() { :; }; }
|
|
96
|
+
# Resolve the PR ref from the argument string (never name the loader placeholder in this block: every
|
|
120
97
|
# occurrence is substituted textually, comments included) <!-- Added: forge#3466 -->
|
|
121
|
-
# The argument string may be: a PR number, "staging", "feature", or "staging:feature"
|
|
122
|
-
# Only the first token is the PR ref; later tokens are flags (--
|
|
98
|
+
# The argument string may be: a PR number or PR URL, "staging", "feature", or "staging:feature" (no number: the open staging->default PR)
|
|
99
|
+
# Only the first token is the PR ref; later tokens are flags (--thorough, ...) <!-- allowlist:check-command-side-effects -->
|
|
123
100
|
# The loader substitutes the text before bash parses it, so read it from a quoted heredoc (never a quoted
|
|
124
101
|
# assignment) and reject any string with a quote, backtick, dollar sign, backslash, newline or tab. <!-- Added: forge#3466 -->
|
|
125
102
|
# NONCE RULE: before running, replace NONCE in BOTH delimiter lines with a fresh random hex string (e.g. `openssl rand -hex 16`) not present in the argument text. <!-- Added: forge#3466 -->
|
|
@@ -135,452 +112,24 @@ case "$STAGING_ARGS_RAW" in
|
|
|
135
112
|
esac
|
|
136
113
|
PR_ARG="${STAGING_ARGS_RAW%% *}"
|
|
137
114
|
PR_ARG_NUM=$(printf '%s' "$PR_ARG" | sed -nE 's#^(https?://[^ ]*/pull/)?([0-9]+)$#\2#p')
|
|
138
|
-
if [
|
|
139
|
-
|
|
140
|
-
elif [ "$STAGING_ARGS_REJECTED" = true ]; then
|
|
141
|
-
# Rejected string: stop. Never fall through to the open staging PR lookup or run later phases with no PR. <!-- Added: forge#3466 -->
|
|
115
|
+
if [ "$STAGING_ARGS_REJECTED" = true ]; then
|
|
116
|
+
# Rejected string: stop. Never fall through to the open staging PR lookup or run later steps with no PR. <!-- Added: forge#3466 -->
|
|
142
117
|
echo "review-pr-staging: rejected argument string - stopping (nothing reviewed, nothing posted)" >&2
|
|
143
118
|
exit 1
|
|
144
|
-
else
|
|
145
|
-
# Find the open staging→main PR
|
|
146
|
-
PR_NUMBER=$(gh pr list ${GH_FLAG} \
|
|
147
|
-
--head "$STAGING_BRANCH" \
|
|
148
|
-
--base "$DEFAULT_BRANCH" \
|
|
149
|
-
--state open \
|
|
150
|
-
--json number \
|
|
151
|
-
--jq '.[0].number' 2>/dev/null || echo "")
|
|
152
119
|
fi
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# Full head SHA (40/64 hex) for the Reviewed-SHA line in reviewer comments and the trusted-comment regexes. Empty or malformed stays empty; every consumer refuses on empty. <!-- Added: forge#3492 -->
|
|
157
|
-
REVIEW_SHA_FULL=""
|
|
158
|
-
[ -n "$PR_NUMBER" ] && REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" ${GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
159
|
-
case "$REVIEW_SHA_FULL" in *[!0-9a-f]*|"") REVIEW_SHA_FULL="" ;; *) [ "${#REVIEW_SHA_FULL}" -eq 40 ] || [ "${#REVIEW_SHA_FULL}" -eq 64 ] || REVIEW_SHA_FULL="" ;; esac
|
|
160
|
-
[ -n "$REVIEW_SHA_FULL" ] || echo "WARNING: could not resolve a full head SHA; reviewer trust checks will refuse (fail closed)." >&2
|
|
161
|
-
# Short SHA for the persona slice path (forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-...): hex-only so a sentinel like n/a or an empty value can never put a / or -- into the path. <!-- Added: forge#3700 -->
|
|
162
|
-
REVIEW_SHA_SHORT=$(printf '%s' "${REVIEW_SHA_FULL:-}" | tr -cd '0-9a-f' | cut -c1-7); [ -n "$REVIEW_SHA_SHORT" ] || REVIEW_SHA_SHORT="unresolved"
|
|
163
|
-
|
|
164
|
-
if [ -n "$PR_NUMBER" ]; then
|
|
165
|
-
gh pr comment "$PR_NUMBER" ${GH_FLAG} --body "<!-- FORGE:REVIEW_ROUTE mode=staging-deploy spec=review-pr-staging.md sha=${REVIEW_SHA_STAGING} -->"
|
|
166
|
-
else
|
|
167
|
-
echo "WARNING: Could not resolve staging→main PR number. FORGE:REVIEW_ROUTE marker not posted."
|
|
168
|
-
fi
|
|
169
|
-
# Persist for later fences (each Bash call is a fresh shell). Remove a stale file from a previous run when nothing resolved.
|
|
170
|
-
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_SD"
|
|
120
|
+
STAGING_THOROUGH=""; case " $STAGING_ARGS_RAW " in *" --thorough "*) STAGING_THOROUGH="--thorough" ;; esac
|
|
121
|
+
STAGING_DELTA=$(printf '%s\n' " $STAGING_ARGS_RAW " | sed -nE 's/.* --delta ([0-9a-f]{40}|[0-9a-f]{64}) .*/\1/p') # forge#4360, hex only
|
|
122
|
+
# The CLI writes the run-keyed PR pointer later fences reload, so it must derive the same key as scripts/lib/fence-bootstrap.sh.
|
|
171
123
|
FORGE_RUN_KEY="${FORGE_RUN_KEY:-${FORGE_RUN_ID:-${CLAUDE_CODE_SESSION_ID:-${CLAUDE_SESSION_ID:-${CODEX_THREAD_ID:-${OPENCODE_SESSION_ID:-$PPID}}}}}}"; FORGE_RUN_KEY=$(printf '%s' "$FORGE_RUN_KEY" | tr -c 'A-Za-z0-9_-' '_')
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
**Prior-finding awareness on re-entry**: On each re-invocation, load findings from prior review passes on the same PR before running any new checks. Previously-resolved findings (issues that are now closed or marked `false-positive`) are excluded from the current verdict to avoid re-surfacing already-fixed issues. Previously-unresolved findings (open issues from a prior pass) are carried forward as context so agents know what was previously flagged:
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
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() { :; }; }
|
|
181
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
182
|
-
GH_FLAG="-R $GH_REPO"
|
|
183
|
-
# Load prior FORGE:REVIEW_ROUTE comments to detect re-entry
|
|
184
|
-
PRIOR_ROUTES=$(gh api repos/${GH_REPO}/issues/${PR_NUMBER}/comments \
|
|
185
|
-
--jq '[.[] | select(.body | test("FORGE:REVIEW_ROUTE")) | .body] | length' 2>/dev/null || echo 0)
|
|
186
|
-
|
|
187
|
-
IS_REENTRY=0
|
|
188
|
-
if [ "$PRIOR_ROUTES" -gt 0 ]; then
|
|
189
|
-
IS_REENTRY=1
|
|
190
|
-
echo "Re-entry detected: $PRIOR_ROUTES prior review pass(es) on PR #${PR_NUMBER}"
|
|
191
|
-
|
|
192
|
-
# Anchored post-filter: keep a search hit only if its title ends `PR #N)` or its body has a
|
|
193
|
-
# `**Source**: PR #N` line. Hidden markers (issue-create-token, etc.) never count as references.
|
|
194
|
-
FINDING_PR_FILTER='[.[] | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
195
|
-
|
|
196
|
-
# Load findings from prior passes that are still open
|
|
197
|
-
PRIOR_OPEN_FINDINGS=$(gh issue list ${GH_FLAG} \
|
|
198
|
-
--label "review-finding" \
|
|
199
|
-
--state open \
|
|
200
|
-
--search "PR #${PR_NUMBER}" \
|
|
201
|
-
--limit 50 \
|
|
202
|
-
--json number,title,labels,body 2>/dev/null \
|
|
203
|
-
| jq -r --arg n "${PR_NUMBER}" "$FINDING_PR_FILTER" 2>/dev/null || true) # uses FINDING_PR_FILTER
|
|
204
|
-
|
|
205
|
-
# Load findings that were resolved (closed) since the last pass
|
|
206
|
-
PRIOR_RESOLVED_FINDINGS=$(gh issue list ${GH_FLAG} \
|
|
207
|
-
--label "review-finding" \
|
|
208
|
-
--state closed \
|
|
209
|
-
--search "PR #${PR_NUMBER}" \
|
|
210
|
-
--limit 50 \
|
|
211
|
-
--json number,title,body 2>/dev/null \
|
|
212
|
-
| jq -r --arg n "${PR_NUMBER}" "$FINDING_PR_FILTER" 2>/dev/null || true) # uses FINDING_PR_FILTER
|
|
213
|
-
|
|
214
|
-
echo "Prior open findings (carry forward): $(echo "$PRIOR_OPEN_FINDINGS" | grep -c '.' || echo 0)"
|
|
215
|
-
echo "Prior resolved findings (exclude from verdict): $(echo "$PRIOR_RESOLVED_FINDINGS" | grep -c '.' || echo 0)"
|
|
216
|
-
fi
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
Agents receive `$IS_REENTRY`, `$PRIOR_OPEN_FINDINGS`, and `$PRIOR_RESOLVED_FINDINGS` as context. They MUST NOT re-file issues that already exist in `$PRIOR_OPEN_FINDINGS` (dedup gate in Phase 7 covers this). Resolved findings do NOT contribute to the BLOCK verdict for the current pass.
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
## Phase 0A: Open Review-Finding Gate (BLOCKING — runs before scope analysis)
|
|
224
|
-
|
|
225
|
-
**Purpose**: Prevent deploying commits that have known, unfixed review findings. The review system catches bugs before merging; this gate ensures the merge path acts on that information.
|
|
226
|
-
|
|
227
|
-
**Why this matters**: Review findings are filed before the originating PR merges to staging. Without this gate, a staging→main bundle can include commits with known unfixed bugs — the review system caught the issue, but the deploy path ignored it. This gate closes the gap between issue discovery and deploy execution. <!-- Added: forge#303 -->
|
|
228
|
-
|
|
229
|
-
```bash
|
|
230
|
-
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() { :; }; }
|
|
231
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
232
|
-
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
233
|
-
|
|
234
|
-
# Step 1: Find all PR numbers in the staging→main bundle
|
|
235
|
-
# These are the PRs whose commits are included in staging but not yet in main
|
|
236
|
-
BUNDLE_PRS=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --oneline \
|
|
237
|
-
| grep -oP '#\d+' \
|
|
238
|
-
| sort -u \
|
|
239
|
-
| tr -d '#')
|
|
240
|
-
|
|
241
|
-
# Also extract PR numbers from merge commit subjects (most reliable)
|
|
242
|
-
MERGE_PRS=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --merges --oneline \
|
|
243
|
-
| grep -oP '(?<=pull request #)\d+' \
|
|
244
|
-
| sort -u)
|
|
245
|
-
|
|
246
|
-
# BEGIN bundle-pr-set
|
|
247
|
-
# Conventional commit subjects cite issues too ("fix(x): ... (#123)"), so a subject-only number is kept only when it is not a confirmed issue.
|
|
248
|
-
# Merge-subject numbers are PRs by construction; an unclassifiable number is kept (fail open: it may be a PR). forge#4127
|
|
249
|
-
# BEGIN bundle-pr-kind
|
|
250
|
-
# Prints "pr" or "issue" for issue number ${2} in repo ${1}; prints nothing and returns 1 when the read fails or is malformed.
|
|
251
|
-
bundle_pr_kind() { local k; k=$(gh api "repos/${1}/issues/${2}" --jq 'if .pull_request then "pr" else "issue" end' 2>/dev/null) || return 1; case "$k" in pr|issue) printf '%s' "$k" ;; *) return 1 ;; esac; }
|
|
252
|
-
# END bundle-pr-kind
|
|
253
|
-
SUBJECT_PRS=""; SKIPPED_ISSUES=""
|
|
254
|
-
for n in $BUNDLE_PRS; do
|
|
255
|
-
case " $(echo $MERGE_PRS) " in *" $n "*) continue ;; esac
|
|
256
|
-
if [ "$(bundle_pr_kind "{GH_REPO}" "$n")" = issue ]; then SKIPPED_ISSUES="$SKIPPED_ISSUES $n"; else SUBJECT_PRS="$SUBJECT_PRS $n"; fi
|
|
257
|
-
done
|
|
258
|
-
ALL_PR_NUMBERS=$(echo "$SUBJECT_PRS $MERGE_PRS" | tr ' ' '\n' | sort -u | grep -E '^[0-9]+$')
|
|
259
|
-
# END bundle-pr-set
|
|
260
|
-
|
|
261
|
-
echo "PRs in staging→main bundle: $(echo $ALL_PR_NUMBERS | tr '\n' ' ')"
|
|
262
|
-
[ -z "$SKIPPED_ISSUES" ] || echo "Issue numbers cited in commit subjects (not bundle PRs, skipped):$SKIPPED_ISSUES"
|
|
263
|
-
# Persist for the 0B.1 fence (a later Bash call is a fresh shell); keyed by the PR number resolved in Phase -1.
|
|
264
|
-
if [ -n "${PR_NUMBER:-}" ]; then
|
|
265
|
-
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_SD"
|
|
266
|
-
printf '%s\n' "$ALL_PR_NUMBERS" > "$_SD/${PR_NUMBER}_bundle-prs.txt"
|
|
267
|
-
fi
|
|
268
|
-
|
|
269
|
-
# Step 2: For each PR in the bundle, check for open review-finding issues and degraded panels
|
|
270
|
-
BLOCKING_FINDINGS=""
|
|
271
|
-
DEGRADED_REVIEWS=""
|
|
272
|
-
for pr_num in $ALL_PR_NUMBERS; do
|
|
273
|
-
IS_REVIEW_DEGRADED=$(gh pr view "$pr_num" -R {GH_REPO} --json labels \
|
|
274
|
-
--jq '[.labels[].name] | any(. == "review-degraded")' 2>/dev/null || echo "false")
|
|
275
|
-
if [ "$IS_REVIEW_DEGRADED" = "true" ]; then
|
|
276
|
-
DEGRADED_REVIEWS="${DEGRADED_REVIEWS}
|
|
277
|
-
**PR #${pr_num}** has a \`review-degraded\` label and requires a full fresh-context re-review."
|
|
278
|
-
fi
|
|
279
|
-
|
|
280
|
-
# Search for open review-finding issues that reference this PR. The search is only a coarse
|
|
281
|
-
# candidate fetch (full-text, tokenized: it also hits hidden markers and longer numbers), so
|
|
282
|
-
# narrow it with the anchored FINDING_PR_FILTER: title suffix `PR #N)` or a `**Source**: PR #N` line.
|
|
283
|
-
FINDING_PR_FILTER='[.[] | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
284
|
-
OPEN_FINDINGS=$(gh issue list -R {GH_REPO} \
|
|
285
|
-
--label "review-finding" \
|
|
286
|
-
--state open \
|
|
287
|
-
--search "PR #${pr_num}" \
|
|
288
|
-
--limit 20 \
|
|
289
|
-
--json number,title,body 2>/dev/null \
|
|
290
|
-
| jq -r --arg n "${pr_num}" "$FINDING_PR_FILTER" 2>/dev/null) # uses FINDING_PR_FILTER
|
|
291
|
-
|
|
292
|
-
if [ -n "$OPEN_FINDINGS" ]; then
|
|
293
|
-
BLOCKING_FINDINGS="${BLOCKING_FINDINGS}
|
|
294
|
-
**PR #${pr_num}** has open review findings:
|
|
295
|
-
${OPEN_FINDINGS}"
|
|
296
|
-
fi
|
|
297
|
-
done
|
|
298
|
-
|
|
299
|
-
# A degraded panel is a review-integrity failure, not a finding that an override may waive.
|
|
300
|
-
if [ -n "$DEGRADED_REVIEWS" ]; then
|
|
301
|
-
echo "⛔ DEPLOY BLOCKED — An included PR has an incomplete isolated review panel."
|
|
302
|
-
echo "$DEGRADED_REVIEWS"
|
|
303
|
-
if [ -n "$PR_NUMBER" ]; then
|
|
304
|
-
gh pr comment "$PR_NUMBER" -R {GH_REPO} --body "<!-- FORGE:GATE_FAILURE -->
|
|
305
|
-
## Deploy Gate: BLOCKED
|
|
306
|
-
|
|
307
|
-
**Gate**: review-panel-integrity
|
|
308
|
-
|
|
309
|
-
### Degraded Review Panels
|
|
310
|
-
|
|
311
|
-
${DEGRADED_REVIEWS}
|
|
312
|
-
|
|
313
|
-
Re-run the complete review panel in a fresh session before deployment.
|
|
314
|
-
|
|
315
|
-
<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->" 2>/dev/null || true
|
|
316
|
-
fi
|
|
317
|
-
exit 1
|
|
318
|
-
fi
|
|
319
|
-
|
|
320
|
-
# Step 3: Block deploy if open findings exist (unless human override present)
|
|
321
|
-
if [ -n "$BLOCKING_FINDINGS" ]; then
|
|
322
|
-
# Check for human override comment on the staging→main PR
|
|
323
|
-
if [ -n "$PR_NUMBER" ]; then
|
|
324
|
-
# Trusted authors only (scripts/trusted-comments.sh: OWNER/MEMBER/COLLABORATOR or a Bot), all comment pages,
|
|
325
|
-
# marker anchored at the start of the body. Fail closed: unresolvable script or unreadable comments = no override (#3492).
|
|
326
|
-
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() { :; }; }
|
|
327
|
-
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
328
|
-
OVR_SCRIPT="$TRUSTED_SCRIPT"
|
|
329
|
-
OVR_COMMENTS=$(gh api --paginate "repos/{GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null) || OVR_COMMENTS=""
|
|
330
|
-
if [ -n "$OVR_SCRIPT" ] && [ -n "$OVR_COMMENTS" ]; then
|
|
331
|
-
OVERRIDE=$(printf '%s' "$OVR_COMMENTS" | bash "$OVR_SCRIPT" count '^OVERRIDE: shipping with open findings' 2>/dev/null || echo 0)
|
|
332
|
-
else
|
|
333
|
-
OVERRIDE=0
|
|
334
|
-
fi
|
|
335
|
-
else
|
|
336
|
-
OVERRIDE=0
|
|
337
|
-
fi
|
|
338
|
-
|
|
339
|
-
if [ "${OVERRIDE:-0}" -eq 0 ]; then
|
|
340
|
-
echo "⛔ DEPLOY BLOCKED — Open review-finding issues exist for PRs in this bundle."
|
|
341
|
-
echo ""
|
|
342
|
-
echo "$BLOCKING_FINDINGS"
|
|
343
|
-
echo ""
|
|
344
|
-
echo "Options:"
|
|
345
|
-
echo " 1. Wait for the open findings to be fixed and merged to staging first."
|
|
346
|
-
echo " 2. Post a comment on this PR starting with \"OVERRIDE: shipping with open findings — <reason>\" to bypass this gate."
|
|
347
|
-
echo ""
|
|
348
|
-
echo "This gate exists to prevent deploying commits with known unfixed review findings."
|
|
349
|
-
echo "RESULT: BLOCK DEPLOY"
|
|
350
|
-
|
|
351
|
-
# Post structured FORGE:GATE_FAILURE comment for pipeline-health tracking
|
|
352
|
-
FINDING_COUNT=$(echo "$BLOCKING_FINDINGS" | grep -c '^\s*- #' || echo "unknown")
|
|
353
|
-
GATE_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
354
|
-
if [ -n "$PR_NUMBER" ]; then
|
|
355
|
-
gh pr comment "$PR_NUMBER" -R {GH_REPO} --body "<!-- FORGE:GATE_FAILURE -->
|
|
356
|
-
## Deploy Gate: BLOCKED
|
|
357
|
-
|
|
358
|
-
**Gate**: open-review-finding
|
|
359
|
-
**Timestamp**: ${GATE_TIMESTAMP}
|
|
360
|
-
**Blocking findings**: ${FINDING_COUNT}
|
|
361
|
-
|
|
362
|
-
### Open Review-Finding Issues
|
|
363
|
-
|
|
364
|
-
${BLOCKING_FINDINGS}
|
|
365
|
-
|
|
366
|
-
### Resolution
|
|
367
|
-
|
|
368
|
-
Fix the open findings above and merge fixes to staging before retrying the staging→main deploy.
|
|
369
|
-
To override (ship known issues with documented reason), post a comment starting with:
|
|
370
|
-
\`OVERRIDE: shipping with open findings — <reason>\`
|
|
371
|
-
|
|
372
|
-
<!-- FORGE:GATE_FAILURE:TYPE=open-review-finding|FINDINGS=${FINDING_COUNT} -->" 2>/dev/null || true
|
|
373
|
-
fi
|
|
374
|
-
exit 1
|
|
375
|
-
else
|
|
376
|
-
echo "⚠️ Open review findings exist but human override detected — proceeding with deploy."
|
|
377
|
-
echo "$BLOCKING_FINDINGS"
|
|
378
|
-
echo "Override comment found on PR #${PR_NUMBER}. Continuing."
|
|
379
|
-
fi
|
|
380
|
-
else
|
|
381
|
-
echo "✅ Open review-finding gate: PASSED — no open findings for PRs in this bundle."
|
|
382
|
-
# Post FORGE:GATE_PASS for symmetric observability — bypass is indistinguishable from clean pass without this
|
|
383
|
-
if [ -n "$PR_NUMBER" ]; then
|
|
384
|
-
gh pr comment "$PR_NUMBER" -R {GH_REPO} --body "<!-- FORGE:GATE_PASS -->
|
|
385
|
-
## Deploy Gate: PASSED
|
|
386
|
-
|
|
387
|
-
**Gate**: open-review-finding check
|
|
388
|
-
**Result**: PASS — no open \`review-finding\` issues exist for PRs in this bundle.
|
|
389
|
-
**Bundle PRs**: ${ALL_PR_NUMBERS}
|
|
390
|
-
**Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
391
|
-
|
|
392
|
-
<!-- FORGE:GATE_PASS:TYPE=open-review-finding -->" 2>/dev/null || true
|
|
393
|
-
fi
|
|
394
|
-
fi
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
If the gate exits with `RESULT: BLOCK DEPLOY` → **STOP**. Do NOT proceed to Phase 0B or any downstream phases. A `<!-- FORGE:GATE_FAILURE -->` structured comment is automatically posted on the staging→main PR (if `$PR_NUMBER` is set) for pipeline-health tracking. Report the blocking finding list.
|
|
398
|
-
|
|
399
|
-
If the gate exits with `RESULT: PASS` → a `<!-- FORGE:GATE_PASS -->` structured comment is posted on the staging→main PR so that `/pipeline-health` can distinguish a clean gate from a silently skipped one. A PR with no gate comment at all indicates a gate bypass — not a clean pass.
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
## Phase 0B: Scope Analysis
|
|
404
|
-
|
|
405
|
-
```bash
|
|
406
|
-
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() { :; }; }
|
|
407
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
408
|
-
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
409
|
-
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --stat | tail -20
|
|
410
|
-
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --numstat | awk '{add+=$(1); del+=$(2)} END {print "Added:", add, "Deleted:", del, "Total:", add+del}'
|
|
411
|
-
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --name-only | sort | uniq
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
Categorize by service (API, Worker, Web, Shared, Infra). Identify high-risk files (billing, credits, pricing, auth, security, migration, scraper).
|
|
415
|
-
|
|
416
|
-
Create review chunks by priority: Billing/Pricing (CRITICAL) → Security/Auth (CRITICAL) → Scraper Core (HIGH) → API Routers (HIGH) → Worker (HIGH) → Web (MEDIUM) → Shared (MEDIUM) → Infra (MEDIUM) → Other (LOW).
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
### 0B.1: Bundle Review Scope (already-reviewed commit set)
|
|
420
|
-
|
|
421
|
-
A bundle PR already got a full line-level review before it merged to staging, so re-reviewing its diff here re-files the same defects. Compute the already-reviewed commit set and scope line-level review to what a feature-PR review could not see. Consumes `ALL_PR_NUMBERS` (Phase 0A). Trust comes from `scripts/trusted-comments.sh` (resolved by the `forge_resolve` stub below, which sources `scripts/lib/forge-resolve.sh`: `FORGEDOCK_HOME` (authoritative), plugin root, `FORGE_ROOT`, `FORGE_HOME`, install symlink, Codex `forge-home`, pinned plugin cache (numeric-newest), marketplaces; never the working directory, which is author-controlled; an unresolved script leaves the variable empty).
|
|
422
|
-
|
|
423
|
-
```bash
|
|
424
|
-
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() { :; }; }
|
|
425
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
426
|
-
SCOPE_MODE=full; REVIEWED_COMMITS=""; REVIEWED_MERGES=""; DEPLOY_WIRING_FILES=""; UNREVIEWED_COMMITS=""; UNREVIEWED_FILES=""; _LOG=""; CROSS_PR_FILES=""; REVIEWED_PR_COUNT=0
|
|
427
|
-
_PF="$_SD/${PR_NUMBER}_bundle-pr-files.txt"; : > "$_PF"
|
|
428
|
-
[ -n "${PR_NUMBER:-}" ] && [ -n "${GH_REPO:-}" ] || echo "SCOPE_MODE=full: scoping skipped (PR_NUMBER or GH_REPO empty after reload)"
|
|
429
|
-
[ -n "${ALL_PR_NUMBERS:-}" ] || echo "SCOPE_MODE=full: scoping skipped (ALL_PR_NUMBERS empty: Phase 0A did not persist a bundle list)"
|
|
430
|
-
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() { :; }; }
|
|
431
|
-
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
432
|
-
[ -n "$TRUSTED_SCRIPT" ] || echo "SCOPE_MODE=full: scoping skipped (trusted-comments.sh not resolvable)"
|
|
433
|
-
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() { :; }; }
|
|
434
|
-
REVIEW_DELTA_SCRIPT=$(forge_resolve review-delta.sh)
|
|
435
|
-
[ -n "$REVIEW_DELTA_SCRIPT" ] || echo "delta credit off: review-delta.sh not resolvable (stale-head PR commits stay UNREVIEWED)"
|
|
436
|
-
# BEGIN bundle-delta-credit
|
|
437
|
-
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
438
|
-
# Partial credit for a PR whose head moved after its last complete review. Only the shared resolver (scripts/review-delta.sh) decides; nothing is re-derived here.
|
|
439
|
-
# Needs pr _HEAD _J _CM _SD GH_REPO TRUSTED_SCRIPT REVIEW_DELTA_SCRIPT in scope. Sets DELTA_COMMITS (space-separated PR commit oids already covered by a trusted complete panel) and DELTA_FULL (1 = whole PR covered by a trusted carried-forward base sync).
|
|
440
|
-
# Every failure (script unresolved, FULL, error, malformed output, force-push, untrusted or absent carried record, git read failure) leaves DELTA_COMMITS empty and DELTA_FULL=0: those commits stay UNREVIEWED.
|
|
441
|
-
bundle_delta_credit() {
|
|
442
|
-
local base labels top cf out l1 kind arg from to oid rc n
|
|
443
|
-
DELTA_COMMITS=""; DELTA_FULL=0
|
|
444
|
-
[ -n "$REVIEW_DELTA_SCRIPT" ] || return 0
|
|
445
|
-
base=$(printf '%s' "$_J" | jq -r '.baseRefName // empty') || return 0
|
|
446
|
-
labels=$(printf '%s' "$_J" | jq -r '[.labels[].name] | join(",")') || return 0
|
|
447
|
-
top=$(git rev-parse --show-toplevel 2>/dev/null) || return 0
|
|
448
|
-
[ -n "$base" ] && [ -n "$top" ] || return 0
|
|
449
|
-
cf=$(mktemp "$_SD/delta-comments.XXXXXX") || return 0
|
|
450
|
-
printf '%s' "$_CM" > "$cf" || { rm -f "$cf"; return 0; }
|
|
451
|
-
out=$(bash "$REVIEW_DELTA_SCRIPT" --pr "$pr" --head "$_HEAD" --base "$base" --trusted-script "$TRUSTED_SCRIPT" --comments-file "$cf" --labels "$labels" --repo "$GH_REPO" --repo-path "$top" 2>/dev/null) || out=""
|
|
452
|
-
rm -f "$cf"
|
|
453
|
-
l1=$(printf '%s\n' "$out" | sed -n 1p)
|
|
454
|
-
kind="${l1%% *}"; arg="${l1#* }"
|
|
455
|
-
case "$kind" in
|
|
456
|
-
DELTA)
|
|
457
|
-
from="${arg%%..*}"; to="${arg##*..}"
|
|
458
|
-
is_sha "$from" && [ "$to" = "$_HEAD" ] && [ "$from" != "$to" ] || return 0
|
|
459
|
-
for oid in $(printf '%s' "$_J" | jq -r '.commits[].oid'); do
|
|
460
|
-
git merge-base --is-ancestor "$oid" "$from" 2>/dev/null; rc=$?
|
|
461
|
-
[ "$rc" -eq 0 ] && DELTA_COMMITS="$DELTA_COMMITS $oid" # rc 1 (not covered) and rc >1 (error) both stay UNREVIEWED
|
|
462
|
-
done ;;
|
|
463
|
-
BASE_SYNC_ONLY)
|
|
464
|
-
is_sha "$arg" && [ "$arg" != "$_HEAD" ] || return 0
|
|
465
|
-
# A marker alone never counts: a TRUSTED FORGE:REVIEW-CARRIED record for this head must name the same reviewed sha (shape owned by review-pr.md carry_verified_from).
|
|
466
|
-
n=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "\\A(?=[\\s\\S]*\\nReviewed-SHA: ${_HEAD}\\r?(\\n|$))(?=[\\s\\S]*\\nCarried-From-SHA: ${arg}\\r?(\\n|$))<!-- FORGE:REVIEW-CARRIED -->" 2>/dev/null) || return 0
|
|
467
|
-
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
468
|
-
[ "$n" -ge 1 ] && DELTA_FULL=1 ;;
|
|
469
|
-
esac
|
|
470
|
-
return 0
|
|
471
|
-
}
|
|
472
|
-
# END bundle-delta-credit
|
|
473
|
-
# BEGIN bundle-pr-kind
|
|
474
|
-
# Prints "pr" or "issue" for issue number ${2} in repo ${1}; prints nothing and returns 1 when the read fails or is malformed.
|
|
475
|
-
bundle_pr_kind() { local k; k=$(gh api "repos/${1}/issues/${2}" --jq 'if .pull_request then "pr" else "issue" end' 2>/dev/null) || return 1; case "$k" in pr|issue) printf '%s' "$k" ;; *) return 1 ;; esac; }
|
|
476
|
-
# END bundle-pr-kind
|
|
477
|
-
# BEGIN bundle-scope-loop
|
|
478
|
-
_WHY=""
|
|
479
|
-
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "${ALL_PR_NUMBERS:-}" ] && [ -n "${PR_NUMBER:-}" ] && [ -n "${GH_REPO:-}" ]; then
|
|
480
|
-
SCOPE_MODE=scoped
|
|
481
|
-
for pr in $ALL_PR_NUMBERS; do
|
|
482
|
-
# A number 0A could not classify may be an issue (subjects cite issues): a confirmed issue is skipped, only a genuine read failure falls open (forge#4127).
|
|
483
|
-
_J=$(gh pr view "$pr" -R $GH_REPO --json headRefOid,mergeCommit,commits,state,baseRefName,labels 2>/dev/null) || {
|
|
484
|
-
if [ "$(bundle_pr_kind "$GH_REPO" "$pr")" = issue ]; then echo "0B.1: #$pr is an issue, not a bundle PR: skipped"; continue; fi
|
|
485
|
-
_WHY="gh pr view failed and #$pr is not a confirmed issue"; SCOPE_MODE=full; break; }
|
|
486
|
-
_HEAD=$(printf '%s' "$_J" | jq -r '.headRefOid')
|
|
487
|
-
case "$_HEAD" in *[!0-9a-f]*|"") SCOPE_MODE=full; break ;; esac # a malformed head id would make the sha match accept any comment (fail open): review everything
|
|
488
|
-
[ "${#_HEAD}" -eq 40 ] || [ "${#_HEAD}" -eq 64 ] || { SCOPE_MODE=full; break; }
|
|
489
|
-
_HEAD7=$(printf '%s' "$_HEAD" | cut -c1-7)
|
|
490
|
-
# Capture each gh read into a variable FIRST and test its own exit status: in a pipeline `||` only sees the last stage (no pipefail),
|
|
491
|
-
# so a failed gh read piped into another command would silently look like "no comments" / "no files".
|
|
492
|
-
_CM=$(gh api --paginate "repos/$GH_REPO/issues/$pr/comments" 2>/dev/null) || { SCOPE_MODE=full; break; }
|
|
493
|
-
_FILES=$(gh pr diff "$pr" -R $GH_REPO --name-only 2>/dev/null) || { SCOPE_MODE=full; break; }
|
|
494
|
-
# Reviewed = trusted FORGE:REVIEW_ROUTE (a real review mode) whose sha is the PR's FINAL head, plus at least one review-agent comment
|
|
495
|
-
# carrying the full-length `Reviewed-SHA: <head>` line and a findings block (same predicate as review-pr.md Phase 5; a 7-char substring or a comment with no findings block never counts), and no degraded/blocked review marker. A PR pushed to after its review has a stale sha: the exact-head gate gives it no credit, and only the delta branch below (shared resolver `scripts/review-delta.sh`) may credit the commits an earlier trusted complete panel covered or a trusted `FORGE:REVIEW-CARRIED` base sync carried; everything else stays UNREVIEWED.
|
|
496
|
-
_ROUTE=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^<!-- FORGE:REVIEW_ROUTE mode=(?!spec-evolution-blocked)[^ ]+ spec=review-pr.md sha=${_HEAD7} -->") || { SCOPE_MODE=full; break; }
|
|
497
|
-
_AGENT=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${_HEAD}(\\r?\\n|$)") || { SCOPE_MODE=full; break; }
|
|
498
|
-
_DEGRADED=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^<!-- FORGE:(REVIEW_DEGRADED|REVIEW_BLOCKED)") || { SCOPE_MODE=full; break; }
|
|
499
|
-
case "$_ROUTE$_AGENT$_DEGRADED" in ''|*[!0-9]*) SCOPE_MODE=full; break ;; esac
|
|
500
|
-
if [ "$_ROUTE" -ge 1 ] && [ "$_AGENT" -ge 1 ] && [ "$_DEGRADED" -eq 0 ] && [ "$(printf '%s' "$_J" | jq '.commits|length')" -lt 100 ]; then
|
|
501
|
-
REVIEWED_PR_COUNT=$((REVIEWED_PR_COUNT+1))
|
|
502
|
-
REVIEWED_COMMITS="$REVIEWED_COMMITS $(printf '%s' "$_J" | jq -r '[.commits[].oid, .mergeCommit.oid // empty] | join(" ")')"
|
|
503
|
-
REVIEWED_MERGES="$REVIEWED_MERGES $(printf '%s' "$_J" | jq -r '.mergeCommit.oid // empty')"
|
|
504
|
-
elif [ "$_DEGRADED" -eq 0 ] && [ "$(printf '%s' "$_J" | jq '.commits|length')" -lt 100 ]; then
|
|
505
|
-
# Stale head: the exact-head gate failed, so ask the shared resolver how much of the PR an earlier trusted complete panel covered. Additive only: it can move commits out of UNREVIEWED, never into it.
|
|
506
|
-
bundle_delta_credit
|
|
507
|
-
if [ "$DELTA_FULL" -eq 1 ]; then
|
|
508
|
-
REVIEWED_PR_COUNT=$((REVIEWED_PR_COUNT+1))
|
|
509
|
-
REVIEWED_COMMITS="$REVIEWED_COMMITS $(printf '%s' "$_J" | jq -r '[.commits[].oid, .mergeCommit.oid // empty] | join(" ")')"
|
|
510
|
-
REVIEWED_MERGES="$REVIEWED_MERGES $(printf '%s' "$_J" | jq -r '.mergeCommit.oid // empty')"
|
|
511
|
-
elif [ -n "$DELTA_COMMITS" ]; then
|
|
512
|
-
REVIEWED_COMMITS="$REVIEWED_COMMITS$DELTA_COMMITS" # the PR's merge commit is not credited: it contains the post-review commits
|
|
513
|
-
fi
|
|
514
|
-
fi
|
|
515
|
-
printf '%s\n' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"
|
|
516
|
-
done
|
|
517
|
-
[ "$SCOPE_MODE" = scoped ] || echo "SCOPE_MODE=full: scoping fell open at bundle PR #$pr (${_WHY:-a gh read or marker count failed or returned malformed data})"
|
|
518
|
-
fi
|
|
519
|
-
# END bundle-scope-loop
|
|
520
|
-
if [ "$SCOPE_MODE" = "scoped" ]; then
|
|
521
|
-
# Cross-PR interactions: files touched by 2+ bundle PRs.
|
|
522
|
-
CROSS_PR_FILES=$(cut -d' ' -f2- "$_PF" | sort | uniq -d)
|
|
523
|
-
# Unreviewed commits: on staging, not in main, and not a commit of any reviewed PR (direct pushes, fast-lane fixes, post-review pushes).
|
|
524
|
-
# Merge commits are included: a merge that is not a reviewed PR's own merge commit may carry conflict-resolution content nobody reviewed.
|
|
525
|
-
_LOG=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --format=%H) || SCOPE_MODE=full
|
|
526
|
-
fi
|
|
527
|
-
if [ "$SCOPE_MODE" = "scoped" ]; then
|
|
528
|
-
for c in $_LOG; do
|
|
529
|
-
case " $REVIEWED_COMMITS " in *" $c "*) continue ;; esac
|
|
530
|
-
UNREVIEWED_COMMITS="$UNREVIEWED_COMMITS $c"
|
|
531
|
-
done
|
|
532
|
-
# Files of every unreviewed commit (--cc limits a merge commit to its conflict-resolution content), so 7B can test file membership.
|
|
533
|
-
# Each git read is captured and its own status tested (a failure here must widen scope, never narrow it).
|
|
534
|
-
UNREVIEWED_FILES=""
|
|
535
|
-
for c in $UNREVIEWED_COMMITS; do
|
|
536
|
-
_DT=$(git diff-tree --no-commit-id --name-only -r "$c") || { SCOPE_MODE=full; break; }
|
|
537
|
-
_DC=$(git diff-tree --no-commit-id --name-only -r --cc "$c") || { SCOPE_MODE=full; break; }
|
|
538
|
-
UNREVIEWED_FILES="$UNREVIEWED_FILES"$'\n'"$_DT"$'\n'"$_DC"
|
|
539
|
-
done
|
|
540
|
-
# A reviewed PR's own merge commit may carry conflict-resolution content its review never saw: count its --cc files as unreviewed too.
|
|
541
|
-
for c in $REVIEWED_MERGES; do
|
|
542
|
-
_DC=$(git diff-tree --no-commit-id --name-only -r --cc "$c") || { SCOPE_MODE=full; break; }
|
|
543
|
-
UNREVIEWED_FILES="$UNREVIEWED_FILES"$'\n'"$_DC"
|
|
544
|
-
done
|
|
545
|
-
UNREVIEWED_FILES=$(printf '%s\n' "$UNREVIEWED_FILES" | sed '/^$/d' | sort -u)
|
|
546
|
-
fi
|
|
547
|
-
# Deploy/runtime readiness files are always in scope: migrations, env, workflows, compose/infra, dependency manifests.
|
|
548
|
-
_DW=$(git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --name-only) || { SCOPE_MODE=full; _DW=""; }
|
|
549
|
-
DEPLOY_WIRING_FILES=$(printf '%s\n' "$_DW" | grep -E '(^|/)(migrations?/|\.github/workflows/|infra/|deploy/|docker-compose[^/]*\.ya?ml$|\.env[^/]*$|Dockerfile[^/]*$|package(-lock)?\.json$|pnpm-lock\.yaml$|yarn\.lock$|requirements[^/]*\.txt$|pyproject\.toml$|poetry\.lock$|go\.(mod|sum)$|Cargo\.(toml|lock)$)' || true)
|
|
550
|
-
# Persist the results for 7A (a later Bash call is a fresh shell). %q keeps multi-line file lists intact.
|
|
551
|
-
printf 'SCOPE_MODE=%q\nCROSS_PR_FILES=%q\nUNREVIEWED_FILES=%q\nDEPLOY_WIRING_FILES=%q\n' "$SCOPE_MODE" "${CROSS_PR_FILES:-}" "${UNREVIEWED_FILES:-}" "${DEPLOY_WIRING_FILES:-}" > "$_SD/${PR_NUMBER}_scope.env"
|
|
552
|
-
echo "SCOPE_MODE=$SCOPE_MODE reviewed_prs=$REVIEWED_PR_COUNT unreviewed_commits=$(echo $UNREVIEWED_COMMITS | wc -w) unreviewed_files=$(echo "$UNREVIEWED_FILES" | grep -c .) cross_pr_files=$(echo "$CROSS_PR_FILES" | grep -c .)"
|
|
124
|
+
export FORGE_RUN_KEY
|
|
125
|
+
FD_ROOT=$(forge_resolve review-staging-post.sh); FD_ROOT="${FD_ROOT%/scripts/review-staging-post.sh}" # install root, never the PR checkout
|
|
126
|
+
[ -n "$FD_ROOT" ] && [ -f "$FD_ROOT/bin/forgedock.mjs" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: the forgedock CLI is not resolvable (set FORGEDOCK_HOME to the ForgeDock install); nothing reviewed (fail closed)"; exit 1; }
|
|
127
|
+
node "$FD_ROOT/bin/forgedock.mjs" review-staging ${PR_ARG_NUM:+"$PR_ARG_NUM"} ${STAGING_THOROUGH:+"$STAGING_THOROUGH"} ${STAGING_DELTA:+--delta "$STAGING_DELTA"}
|
|
553
128
|
```
|
|
554
129
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
**Stale-head delta credit (fail-closed)**: delta credit only ever moves commits out of UNREVIEWED, and only on a positive, trusted resolver answer. A resolver `FULL`, an error, malformed output, a force-push (reviewed sha not an ancestor), an untrusted or absent `FORGE:REVIEW-CARRIED` record, a degraded/blocked PR, 100+ commits or an unresolved `review-delta.sh` all leave the PR's commits UNREVIEWED (today's behaviour) and never force `SCOPE_MODE=full` by themselves. `DELTA <reviewed>..<head>` credits only the PR commits that are ancestors of `<reviewed>` (never its merge commit); `BASE_SYNC_ONLY` credits the whole PR only with a trusted carried record naming this head and that reviewed sha. A bundle that changes the review specs gets `FULL` from the resolver by design. <!-- Added: forge#3638 -->
|
|
558
|
-
|
|
559
|
-
**Scoped line-level review set** = `CROSS_PR_FILES` + `UNREVIEWED_FILES` (the files of `UNREVIEWED_COMMITS`, including merge-commit conflict resolutions) + `DEPLOY_WIRING_FILES`. Files and commits that belong only to an already-reviewed PR (the already-reviewed set) are excluded from line-level review.
|
|
560
|
-
|
|
561
|
-
---
|
|
562
|
-
|
|
563
|
-
### 0C: Prepare (one script call)
|
|
564
|
-
|
|
565
|
-
`scripts/review-prepare.sh` is the same preparation `/review-pr` Phase 1 runs, called with `--no-delta` (a bundle review always reviews the whole bundle: no FULL/DELTA resolver). It resolves the persona template source, the per-domain diff slices (`slices.<domain>.diff_file`, read from `origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH` when the API diff is too large), project config and the head SHA into one context file that Phase 5 and the Phase 7 `review-finalize.sh` calls read. A non-zero exit is `BLOCKED` (for example no template source or no readable trusted forge.yaml): STOP, never skip. An unconfigured `typescript.build` / `python.format` is reported as a SKIPPED Phase 2 finding (`--build-gate advisory`), as the deploy review always did.
|
|
566
|
-
|
|
567
|
-
```bash
|
|
568
|
-
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() { :; }; }
|
|
569
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
570
|
-
[ -n "${PR_NUMBER:-}" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: PR_NUMBER unavailable after reload; nothing reviewed"; exit 1; }
|
|
571
|
-
RP=$(forge_resolve review-prepare.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed)
|
|
572
|
-
[ -n "$RP" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/review-prepare.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install); refusing to continue (fail closed)"; exit 1; }
|
|
573
|
-
CTX="$_SD/review-ctx-${PR_NUMBER}.json"
|
|
574
|
-
bash "$RP" --pr "$PR_NUMBER" --repo "$GH_REPO" --no-delta --build-gate advisory --out "$CTX"; RP_RC=$? # an unconfigured build is reported SKIPPED, never a deploy block
|
|
575
|
-
if [ "$RP_RC" -ne 0 ] || [ ! -s "$CTX" ]; then
|
|
576
|
-
[ -s "$CTX" ] && jq -r '.blockers[]? | "BLOCKER: " + .' "$CTX"
|
|
577
|
-
echo "REVIEW_RESULT: status: BLOCKED, blocker: review-prepare failed (exit ${RP_RC}); nothing reviewed (fail closed)"; exit 1
|
|
578
|
-
fi
|
|
579
|
-
echo "CONTEXT_FILE=$CTX"
|
|
580
|
-
jq -r '"TEMPLATE_SOURCE=\(.templates.source)", "TEMPLATE_BASE=\(.templates.base)", "MONOLITHIC_CATALOG=\(.templates.monolithic_catalog // "")"' "$CTX"
|
|
581
|
-
```
|
|
130
|
+
Exit 0 prints the manifest. `REVIEW_STAGING: PR #N head <sha>` gives the PR number and the full head SHA. `BUNDLE_PRS:` lists the bundle member PRs. `SCOPE_MODE=` gives the 0B.1 mode. `ROSTER:` gives the tier and reason (record them in the final summary). `CONTEXT_FILE=` names the 0C context. There is one `DISPATCH <domain> <brief file>` line per roster domain. Exit 1 is a stop: the gate printed `RESULT: BLOCK DEPLOY` (report the blocking finding list), or a `REVIEW_RESULT: status: BLOCKED` line names the blocker. Report it and do not continue. A PR with no gate comment at all indicates a gate bypass, not a clean pass.
|
|
582
131
|
|
|
583
|
-
|
|
132
|
+
**Delta pass** (forge#4360): after a fix lands on staging mid-review, re-run with `--delta <previous full head SHA>`. Only that range is reviewed, by the domains it touches (a `DELTA:` manifest line), and the earlier pass is credited. Dispatch only the DISPATCH lines.
|
|
584
133
|
|
|
585
134
|
---
|
|
586
135
|
|
|
@@ -687,18 +236,15 @@ For fixable failures: checkout staging, apply fix, verify locally, commit as `fi
|
|
|
687
236
|
|
|
688
237
|
## Phase 2: Material Change Analysis
|
|
689
238
|
|
|
690
|
-
**
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
694
|
-
[ -n "${PR_NUMBER:-}" ] && : > "$_SD/${PR_NUMBER}_launched_review_agents"
|
|
695
|
-
```
|
|
239
|
+
**Launched-agent bookkeeping (MANDATORY)**: Step 1 truncated the PR-keyed launched-agent file. Before launching **every** Phase 2-6 reviewer (the DISPATCH briefs and the spec-owned panel), append its lowercase marker domain (for example `security`, `bug-hunter-api` or `regression-risk`) to that file: `printf '%s\n' "<domain>" >> "${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}/<PR>_launched_review_agents"`. Step 7A checks delivery for exactly these domains.
|
|
240
|
+
|
|
241
|
+
**Head SHA binding**: substitute `[REVIEW_SHA]` in every template below with the literal full head SHA from the manifest's `REVIEW_STAGING:` line before dispatch. The reviewer's post fence runs in a fresh shell, so it must never reference a shell variable and must never invent a value.
|
|
696
242
|
|
|
697
|
-
**Review scope (applies to Phases 2-6, from
|
|
243
|
+
**Review scope (applies to Phases 2-6, from 0B.1)**: when `SCOPE_MODE=scoped`, pass every spec-panel agent `REVIEWED_COMMITS`, `UNREVIEWED_COMMITS`, `CROSS_PR_FILES` and `DEPLOY_WIRING_FILES` in its prompt (the DISPATCH briefs already carry them; the lists are in `$FORGE_SCRATCHPAD/<PR>_scope.json`). Line-level review is limited to `CROSS_PR_FILES`, the diffs of `UNREVIEWED_COMMITS`, and a runtime/deploy readiness checklist (required secrets and env vars present and documented, migration ordering and collisions, deploy wiring and workflow sibling drift). Agents report every finding they see, with its severity and file, whatever file or commit it lives in; a defect that lives only in an already-reviewed PR's files was visible to that PR's own review, so the 7B scope filter may demote it, but only when it is `MEDIUM`, `LOW` or `INFO`: a `CRITICAL` or `HIGH` finding is always reported and kept. Safety-domain agents (Security, Auth, Billing, Concurrency, Database) still read the whole diff of any in-scope file, not just the changed hunks. When `SCOPE_MODE=full`, review the full bundle diff. Report the scope used in the agent comment. <!-- Added: forge#3451 -->
|
|
698
244
|
|
|
699
245
|
Launch agent (model: {SUBAGENT_MODEL}) to analyze all commits since last deploy. Categorize as: NEW FEATURE, ENHANCEMENT, BUG FIX, REFACTOR, SECURITY, PERFORMANCE, INFRASTRUCTURE, DEPENDENCY. Separate user-facing vs internal. Document breaking changes and required pre-deploy actions.
|
|
700
246
|
|
|
701
|
-
Substitute `[REVIEW_SHA]` in the template below with the literal full head SHA
|
|
247
|
+
Substitute `[REVIEW_SHA]` in the template below with the literal full head SHA from the manifest (see **Head SHA binding** above).
|
|
702
248
|
|
|
703
249
|
**MANDATORY — post findings as PR comment**: Before its first post attempt, the agent MUST persist the final report to a uniquely named durable file, then post it with `--body-file`. It MUST also return verdict, finding count, and one line per finding to the orchestrator whether the post succeeds or fails; on failure, return the file path and stop without retrying. After completing analysis, the agent posts its full report directly to the PR:
|
|
704
250
|
```bash
|
|
@@ -772,15 +318,11 @@ Reviewed-SHA: [REVIEW_SHA]
|
|
|
772
318
|
|
|
773
319
|
## Phase 5: Security & Billing Deep Dive
|
|
774
320
|
|
|
775
|
-
**
|
|
321
|
+
**Dispatch the briefs (MANDATORY)**: dispatch one isolated reviewer per `DISPATCH <domain> <brief file>` line of the Step 1 manifest, all in a single message. Each prompt is the brief file's content, verbatim. Step 1 already did the following, through the same template chain `/review-pr` Phase 3C uses:
|
|
322
|
+
- chose the risk-tiered domain panel (`scripts/review-roster.sh`) <!-- Added: forge#3599 -->;
|
|
323
|
+
- rendered each persona (`protocols.md` + persona) with its per-domain diff slice, PR number, short and full head SHA, marker domain, scope lists and prior findings.
|
|
776
324
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
**Risk-tiered domain panel** <!-- Added: forge#3599 -->: choose the domain agents with `scripts/review-roster.sh` (resolve it with `forge_resolve review-roster.sh`, never from `$PWD`) instead of launching every domain with a change. Every PR in the bundle already had its own review, so this panel only needs to catch cross-PR interactions. Pass the bundle's changed lines (`git diff --numstat "origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH"`, added plus deleted), changed-file count and file list (`--files-file -`), optional `--scores` diff-text signals (omit for none), and `--fix-pr false`; a bundle of 60 lines or fewer runs Security only unless a path trigger fires, a larger one follows the medium and standard tiers, and Auth and Infra run only when auth/session/trust-filter or CI/docker/deploy files are in the bundle. Run the full union when `--thorough` was passed. When the script cannot be resolved or fails, launch the full union (fail toward more agents). Record the tier and reason in the final summary.
|
|
780
|
-
|
|
781
|
-
**Per-domain diff slices (MANDATORY)**: `protocols.md` tells agents to use a pre-supplied `[DOMAIN_DIFF_SLICE]` and not to re-fetch the diff, so staging MUST supply one (the staging→main diff is the largest in the pipeline). `review-prepare.sh` (0C) already sliced the uncapped bundle diff by domain and capped each slice at 100K chars: read `slices.<domain>.diff_file` from the context file (`jq -r '.slices["<domain>"].diff_file' "$_SD/review-ctx-${PR_NUMBER}.json"`; `security` and `spec-cli` get the full diff, a domain with no pattern hit the full diff) with the canonical key (`api-design`→`api`, `infrastructure`→`infra`, the others unchanged).
|
|
782
|
-
|
|
783
|
-
For each dispatched persona, substitute `[DOMAIN_DIFF_SLICE]` → the content of its `slices.<domain>.diff_file`. Also substitute `[AGENT_DOMAIN]` → the persona's marker domain (the `{domain}` used in its `FORGE:REVIEW-AGENT` marker), and bind `[PR_NUMBER]` to the staging review's PR number (`staging` when there is none) and `[REVIEW_SHA_SHORT]` to the literal Phase -1 `REVIEW_SHA_SHORT` value (hex, or `unresolved`; never `REVIEW_SHA_STAGING`, whose `n/a` sentinel would put a `/` in the path): personas derive their slice path `forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff` from them. Also substitute `[REVIEW_SHA]` → the literal full head SHA (40 or 64 hex, the orchestrator's Phase -1 `REVIEW_SHA_FULL` value) into the dispatch prompt: a reviewer runs its post fence in a fresh shell, so it MUST NOT reference `${REVIEW_SHA_FULL}` and MUST NOT invent a value; when no full SHA resolved (no PR), omit the `Reviewed-SHA:` line value rather than fabricate one (Phase 7 consumers then fail closed). Agents never run a full `gh pr diff`; only `--name-only` is allowed. Agents: General Security (always), Auth, Billing, Concurrency, Scraper, API Design, Database, Infrastructure.
|
|
325
|
+
A missing persona template BLOCKED Step 1. **NEVER perform the review inline as a substitute.** Agents never run a full `gh pr diff`; only `--name-only` is allowed. Run Step 1 with `--thorough` for the full union of domains.
|
|
784
326
|
|
|
785
327
|
**MANDATORY — each domain agent MUST persist its finalized body before posting its findings directly to the PR immediately upon completion** (not batched by the orchestrator). It MUST return verdict, finding count, and one line per finding to the orchestrator independently of delivery; if posting fails, return the durable file path and stop without retrying:
|
|
786
328
|
```bash
|
|
@@ -825,329 +367,69 @@ Reviewed-SHA: [REVIEW_SHA]
|
|
|
825
367
|
**Database container restart risk (MANDATORY when `docker-compose*.yml` changes touch `postgres` or `redis` service)**: Any change to a stateful container's `command:`, `image:`, `volumes:`, or `environment:` forces container recreation on deploy. Auto-escalate to HIGH risk. Verify: `stop_grace_period` is sufficient (≥30s for PG), `full_page_writes = on`, `fsync = on`, no active long-running transactions will be interrupted. Recommend maintenance window — stateful container restarts must NOT happen as a side effect of routine deploys. A Postgres restart under active write load can corrupt btree indexes and bypass UNIQUE constraints. <!-- Added: forge#146 -->
|
|
826
368
|
|
|
827
369
|
---
|
|
828
|
-
|
|
829
370
|
## Phase 6.5: Runtime Test Gate (BLOCKING — runs after static analysis, before finding triage)
|
|
830
371
|
|
|
831
|
-
**Purpose**: Verify the integrated bundle's acceptance criteria against running code before the deploy verdict. Catches runtime defects (cross-PR interactions, container/startup failures, regression in tested behaviour) that static review cannot surface.
|
|
372
|
+
**Purpose**: Verify the integrated bundle's acceptance criteria against running code before the deploy verdict. Catches runtime defects (cross-PR interactions, container/startup failures, regression in tested behaviour) that static review cannot surface. `/test-gate` files a `test-failure` issue for each failing cluster itself.
|
|
832
373
|
|
|
833
|
-
**
|
|
374
|
+
**Posture**: Controlled by `verification.test_gate.posture` in `forge.yaml`. Default: `blocking`. Set to `advisory` to surface failures without preventing deploy. A BLOCK is downgraded by a trusted PR comment starting with `verification.test_gate.override_phrase` (default `OVERRIDE: shipping with test failures —`). <!-- Added: forge#906 -->
|
|
834
375
|
|
|
835
|
-
|
|
376
|
+
Invoke `/test-gate` with the manifest's `BUNDLE_PRS` and the default branch. Save its **complete output** to `$FORGE_SCRATCHPAD/<PR>_test_gate_output.txt` (default `.forge-scratch/`), using the PR number from the manifest:
|
|
836
377
|
|
|
837
|
-
```bash
|
|
838
|
-
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() { :; }; }
|
|
839
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
840
|
-
GH_FLAG="-R $GH_REPO"
|
|
841
|
-
GATE_POSTURE=$(yq '.verification.test_gate.posture // "blocking"' "$CONFIG_FILE" 2>/dev/null || echo "blocking")
|
|
842
|
-
OVERRIDE_PHRASE=$(yq '.verification.test_gate.override_phrase // "OVERRIDE: shipping with test failures —"' "$CONFIG_FILE" 2>/dev/null || echo "OVERRIDE: shipping with test failures —")
|
|
843
|
-
echo "=== Phase 6.5: Runtime Test Gate ==="
|
|
844
|
-
echo "Bundle PRs: $(echo $ALL_PR_NUMBERS | tr '\n' ' ')"
|
|
845
|
-
echo "Posture: ${GATE_POSTURE}"
|
|
846
|
-
|
|
847
|
-
# Reset any stale gate result from a prior run before the Skill call (files are PR-keyed)
|
|
848
|
-
rm -f "$_SD/${PR_NUMBER}_test_gate_verdict" "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
849
|
-
# Initialize test-gate verdict (default SKIP — safe if Phase 6.5 is bypassed)
|
|
850
|
-
TEST_GATE_VERDICT="SKIP"
|
|
851
|
-
TEST_GATE_REASON="Phase 6.5 not yet run"
|
|
852
|
-
|
|
853
|
-
# Invoke /test-gate with the bundle PRs already computed in Phase 0A
|
|
854
|
-
# ALL_PR_NUMBERS is the de-duplicated union of both scan methods (commit log + merge subjects)
|
|
855
|
-
GATE_OUTPUT=$(Skill("{FORGE_SKILL_PREFIX}test-gate", "--prs \"$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs)\" --base $DEFAULT_BRANCH"))
|
|
856
|
-
|
|
857
|
-
# Extract machine-readable verdict from Skill output. An absent/invalid marker stays EMPTY (never coerced to SKIP):
|
|
858
|
-
# the verdict-handling fence treats an empty verdict as missing/invalid and fails closed under blocking posture.
|
|
859
|
-
TEST_GATE_VERDICT=$(echo "$GATE_OUTPUT" | grep -oE 'FORGE:TEST_GATE:RESULT=(BLOCK|PASS|SKIP)' | sed 's/^FORGE:TEST_GATE:RESULT=//' | tail -1)
|
|
860
|
-
[ -n "$TEST_GATE_VERDICT" ] || TEST_GATE_REASON="gate result missing/invalid (no FORGE:TEST_GATE:RESULT marker in /test-gate output)"
|
|
861
|
-
|
|
862
|
-
echo "Test-gate verdict: ${TEST_GATE_VERDICT}"
|
|
863
|
-
# Persist the gate result: every Bash call is a fresh shell, and the verdict-handling fence and Phase 8 read it back (validated on reload).
|
|
864
|
-
printf '%s\n' "$TEST_GATE_VERDICT" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "$TEST_GATE_REASON" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
865
378
|
```
|
|
866
|
-
|
|
867
|
-
**Verdict handling**:
|
|
868
|
-
|
|
869
|
-
```bash
|
|
870
|
-
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() { :; }; }
|
|
871
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
872
|
-
GH_FLAG="-R $GH_REPO"
|
|
873
|
-
GATE_POSTURE=$(yq '.verification.test_gate.posture // "blocking"' "$CONFIG_FILE" 2>/dev/null || echo "blocking")
|
|
874
|
-
OVERRIDE_PHRASE=$(yq '.verification.test_gate.override_phrase // "OVERRIDE: shipping with test failures —"' "$CONFIG_FILE" 2>/dev/null || echo "OVERRIDE: shipping with test failures —")
|
|
875
|
-
# Reload the Phase 6.5 gate result persisted by its producer (validated: only a known verdict reaches a gate decision).
|
|
876
|
-
[ -n "${TEST_GATE_VERDICT:-}" ] || { [ -r "$_SD/${PR_NUMBER}_test_gate_verdict" ] && TEST_GATE_VERDICT=$(sed -n -E '1{/^(BLOCK|PASS|SKIP)$/p;q}' "$_SD/${PR_NUMBER}_test_gate_verdict"); }
|
|
877
|
-
[ -n "${TEST_GATE_REASON:-}" ] || { [ -r "$_SD/${PR_NUMBER}_test_gate_reason" ] && TEST_GATE_REASON=$(sed -n 1p "$_SD/${PR_NUMBER}_test_gate_reason" | tr -d '\r\000-\037' | cut -c1-400); }
|
|
878
|
-
# Fail closed: a missing, empty, or unrecognised verdict is never a SKIP. Normalize it to BLOCK so the BLOCK arm applies
|
|
879
|
-
# the override check, advisory/blocking posture, and exit 1; GATE_VERDICT_INVALID makes the reasons say missing/invalid.
|
|
880
|
-
GATE_VERDICT_INVALID=0
|
|
881
|
-
case "${TEST_GATE_VERDICT:-}" in
|
|
882
|
-
BLOCK|PASS|SKIP) ;;
|
|
883
|
-
*)
|
|
884
|
-
GATE_VERDICT_INVALID=1
|
|
885
|
-
if [ -r "$_SD/${PR_NUMBER}_test_gate_verdict" ]; then _GV_WHY="persisted verdict file present but empty/invalid"; else _GV_WHY="persisted verdict file missing"; fi
|
|
886
|
-
echo "⚠️ Test gate: gate result missing/invalid (${_GV_WHY}) — treating as BLOCK, not SKIP."
|
|
887
|
-
TEST_GATE_VERDICT="BLOCK"
|
|
888
|
-
;;
|
|
889
|
-
esac
|
|
890
|
-
case "$TEST_GATE_VERDICT" in
|
|
891
|
-
|
|
892
|
-
SKIP)
|
|
893
|
-
echo "ℹ️ Test gate: SKIP — no executable changes or tests not configured."
|
|
894
|
-
echo " This gap will appear in the Phase 8 summary. No deploy impact."
|
|
895
|
-
TEST_GATE_REASON="SKIP — no runtime tests ran (docs-only bundle, no integration tests configured, or manual-only criteria)"
|
|
896
|
-
;;
|
|
897
|
-
|
|
898
|
-
PASS)
|
|
899
|
-
echo "✅ Test gate: PASS — all test clusters passed. Deploy may proceed."
|
|
900
|
-
TEST_GATE_REASON="PASS — all automated test clusters passed"
|
|
901
|
-
# Post FORGE:GATE_PASS for symmetric observability
|
|
902
|
-
if [ -n "$PR_NUMBER" ]; then
|
|
903
|
-
gh pr comment "$PR_NUMBER" ${GH_FLAG} --body "<!-- FORGE:GATE_PASS -->
|
|
904
|
-
## Deploy Gate: PASSED
|
|
905
|
-
|
|
906
|
-
**Gate**: test-gate (runtime acceptance criteria)
|
|
907
|
-
**Result**: PASS — all test clusters passed. Deploy may proceed.
|
|
908
|
-
**Bundle PRs**: ${ALL_PR_NUMBERS}
|
|
909
|
-
**Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
910
|
-
|
|
911
|
-
<!-- FORGE:GATE_PASS:TYPE=test-gate|BUNDLE=$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs) -->" 2>/dev/null || true
|
|
912
|
-
fi
|
|
913
|
-
;;
|
|
914
|
-
|
|
915
|
-
BLOCK)
|
|
916
|
-
# Check for override comment on the staging→main PR (mirrors Phase 0A pattern)
|
|
917
|
-
if [ -n "$PR_NUMBER" ]; then
|
|
918
|
-
# Trusted authors only, all comment pages, anchored; fail closed (no override) if the script or comments are
|
|
919
|
-
# unreadable (#3492). OVERRIDE_PHRASE comes from forge.yaml, so regex-escape it before using it as a pattern.
|
|
920
|
-
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() { :; }; }
|
|
921
|
-
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
922
|
-
TG_SCRIPT="$TRUSTED_SCRIPT"
|
|
923
|
-
TG_COMMENTS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null) || TG_COMMENTS=""
|
|
924
|
-
TG_OVERRIDE=0; OVERRIDE_REASON="(reason not captured)"
|
|
925
|
-
if [ -n "$TG_SCRIPT" ] && [ -n "$TG_COMMENTS" ]; then
|
|
926
|
-
TG_RE="^$(printf '%s' "$OVERRIDE_PHRASE" | sed 's/[][\\.^$*+?(){}|\/]/\\&/g')"
|
|
927
|
-
TG_OVERRIDE=$(printf '%s' "$TG_COMMENTS" | bash "$TG_SCRIPT" count "$TG_RE" 2>/dev/null || echo 0)
|
|
928
|
-
[ "${TG_OVERRIDE:-0}" -gt 0 ] && OVERRIDE_REASON=$(printf '%s' "$TG_COMMENTS" | bash "$TG_SCRIPT" bodies "$TG_RE" 2>/dev/null | tail -1 | jq -r '.' 2>/dev/null || echo "(reason not captured)")
|
|
929
|
-
fi
|
|
930
|
-
else
|
|
931
|
-
TG_OVERRIDE=0
|
|
932
|
-
fi
|
|
933
|
-
|
|
934
|
-
if [ "${TG_OVERRIDE:-0}" -gt 0 ]; then
|
|
935
|
-
echo "⚠️ Test gate: BLOCK — but override comment detected on PR #${PR_NUMBER}."
|
|
936
|
-
echo " Override: ${OVERRIDE_REASON}"
|
|
937
|
-
echo " Proceeding with deploy. Override is logged in Phase 8 summary."
|
|
938
|
-
TEST_GATE_VERDICT="PASS"
|
|
939
|
-
TEST_GATE_REASON="BLOCK downgraded to PASS by override: ${OVERRIDE_REASON}"
|
|
940
|
-
[ "$GATE_VERDICT_INVALID" = 1 ] && TEST_GATE_REASON="BLOCK (gate result missing/invalid) downgraded to PASS by override: ${OVERRIDE_REASON}"
|
|
941
|
-
|
|
942
|
-
elif [ "$GATE_POSTURE" = "advisory" ]; then
|
|
943
|
-
echo "⚠️ Test gate: BLOCK (advisory posture) — runtime failures detected but deploy is NOT prevented."
|
|
944
|
-
echo " Switch verification.test_gate.posture to 'blocking' in forge.yaml to enforce this gate."
|
|
945
|
-
TEST_GATE_REASON="BLOCK (advisory) — runtime failures detected; deploy allowed by advisory posture"
|
|
946
|
-
[ "$GATE_VERDICT_INVALID" = 1 ] && TEST_GATE_REASON="BLOCK (advisory) — gate result missing/invalid; deploy allowed by advisory posture"
|
|
947
|
-
|
|
948
|
-
else
|
|
949
|
-
# blocking posture (default) — STOP
|
|
950
|
-
echo "⛔ DEPLOY BLOCKED — /test-gate returned BLOCK verdict."
|
|
951
|
-
[ "$GATE_VERDICT_INVALID" = 1 ] && echo " (gate result missing/invalid — failing closed; re-run /test-gate or post the override comment)"
|
|
952
|
-
echo ""
|
|
953
|
-
echo "Runtime failures were detected in the staging→${DEFAULT_BRANCH} bundle."
|
|
954
|
-
echo "The failures were batch-introduced (not pre-existing on ${DEFAULT_BRANCH} baseline)."
|
|
955
|
-
echo ""
|
|
956
|
-
echo "Options:"
|
|
957
|
-
echo " 1. Fix the failing tests and merge fixes to staging before retrying the deploy."
|
|
958
|
-
echo " 2. Post a comment on this PR starting with \"${OVERRIDE_PHRASE} <reason>\" to bypass this gate."
|
|
959
|
-
echo " 3. Set verification.test_gate.posture: advisory in forge.yaml to downgrade to a warning."
|
|
960
|
-
echo ""
|
|
961
|
-
echo "RESULT: BLOCK DEPLOY"
|
|
962
|
-
|
|
963
|
-
# Post structured FORGE:GATE_FAILURE comment for pipeline-health tracking
|
|
964
|
-
GATE_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
965
|
-
if [ -n "$PR_NUMBER" ]; then
|
|
966
|
-
gh pr comment "$PR_NUMBER" ${GH_FLAG} --body "<!-- FORGE:GATE_FAILURE -->
|
|
967
|
-
## Deploy Gate: BLOCKED
|
|
968
|
-
|
|
969
|
-
**Gate**: test-gate
|
|
970
|
-
**Timestamp**: ${GATE_TIMESTAMP}
|
|
971
|
-
**Bundle PRs**: $(echo $ALL_PR_NUMBERS | tr '\n' ' ')
|
|
972
|
-
**Posture**: ${GATE_POSTURE}
|
|
973
|
-
|
|
974
|
-
### What Happened
|
|
975
|
-
|
|
976
|
-
\`/test-gate\` detected runtime failures in the staging→\`${DEFAULT_BRANCH}\` bundle that were NOT present on the \`${DEFAULT_BRANCH}\` baseline. These are batch-introduced regressions. (If the gate result was missing/invalid, no verdict could be read and the gate failed closed; re-run \`/test-gate\`.)
|
|
977
|
-
|
|
978
|
-
\`/test-gate\` has filed \`test-failure\` issues for each failing cluster — check the issue tracker for details.
|
|
979
|
-
|
|
980
|
-
### Resolution
|
|
981
|
-
|
|
982
|
-
1. Fix the failing tests and merge fixes to staging.
|
|
983
|
-
2. Retry the staging→\`${DEFAULT_BRANCH}\` deploy.
|
|
984
|
-
|
|
985
|
-
To override (ship known failures with documented reason), post a comment containing:
|
|
986
|
-
\`${OVERRIDE_PHRASE} <reason>\`
|
|
987
|
-
|
|
988
|
-
<!-- FORGE:GATE_FAILURE:TYPE=test-gate|BUNDLE=$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs) -->" 2>/dev/null || true
|
|
989
|
-
fi
|
|
990
|
-
_GV_REASON="BLOCK (blocking posture, no override)"; [ "$GATE_VERDICT_INVALID" = 1 ] && _GV_REASON="BLOCK (gate result missing/invalid; blocking posture, no override)"
|
|
991
|
-
printf '%s\n' "$TEST_GATE_VERDICT" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "$_GV_REASON" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
992
|
-
exit 1
|
|
993
|
-
fi
|
|
994
|
-
;;
|
|
995
|
-
|
|
996
|
-
*)
|
|
997
|
-
# Unreachable (normalized above); fail closed regardless — never SKIP.
|
|
998
|
-
echo "⛔ Test gate: gate result missing/invalid ('${TEST_GATE_VERDICT}') — failing closed."
|
|
999
|
-
printf '%s\n' "BLOCK" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "BLOCK (gate result missing/invalid)" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
1000
|
-
exit 1
|
|
1001
|
-
;;
|
|
1002
|
-
|
|
1003
|
-
esac
|
|
1004
|
-
# Persist the gate result: every Bash call is a fresh shell, and the verdict-handling fence and Phase 8 read it back (validated on reload).
|
|
1005
|
-
printf '%s\n' "$TEST_GATE_VERDICT" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "$TEST_GATE_REASON" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
379
|
+
>>> INVOKE: Skill(skill="{FORGE_SKILL_PREFIX}test-gate", args="--prs \"<BUNDLE_PRS>\" --base <default branch>")
|
|
1006
380
|
```
|
|
1007
381
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
---
|
|
382
|
+
Do not judge the result yourself. Step 7A reads the output's last `FORGE:TEST_GATE:RESULT=` marker through `scripts/review-staging-post.sh test-gate`. A missing or invalid marker (gate result missing/invalid) is BLOCK, never SKIP (fail closed). The step then applies the override and the posture. It posts `FORGE:GATE_PASS` (PASS) or `FORGE:GATE_FAILURE` (blocking BLOCK, with `RESULT: BLOCK DEPLOY`), and persists the verdict for Phase 8.
|
|
1011
383
|
|
|
1012
|
-
**Incomplete-panel guard (MANDATORY):**
|
|
384
|
+
**Incomplete-panel guard (MANDATORY):** Every reviewer selected in Phases 2–6 is appended to the launched-agent file before it is launched. If any dispatch fails, follow step 4 of Sub-Agent Dispatch Tool Resolution (hard stop). Before triage, step 7A counts unique trusted, current-head `FORGE:REVIEW-AGENT` markers. A marker counts only when all of these hold: the author is trusted per `scripts/trusted-comments.sh`, the body starts with the marker, the `Reviewed-SHA:` line matches the full head SHA, and the findings block is present. All comment pages are read. An unresolvable script or unreadable comments count as 0. If the completed count (`ACTUAL_AGENT_COUNT`) is lower than the selected count (`SELECTED_AGENT_COUNT`), the step labels the PR `review-degraded` and `needs-human`. It posts `FORGE:GATE_FAILURE:TYPE=review-panel-integrity` and stops. Do not create a deploy verdict from partial reviewer output.
|
|
1013
385
|
|
|
1014
|
-
|
|
1015
|
-
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() { :; }; }
|
|
1016
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1017
|
-
# Each Bash call is a fresh shell: re-resolve the full head SHA if it is not set, and refuse when it is not a commit id
|
|
1018
|
-
# (an empty SHA would make the Reviewed-SHA match accept any head).
|
|
1019
|
-
[ -n "${REVIEW_SHA_FULL:-}" ] || REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
1020
|
-
case "$REVIEW_SHA_FULL" in *[!0-9a-f]*|"") REVIEW_SHA_FULL="" ;; *) [ "${#REVIEW_SHA_FULL}" -eq 40 ] || [ "${#REVIEW_SHA_FULL}" -eq 64 ] || REVIEW_SHA_FULL="" ;; esac
|
|
1021
|
-
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() { :; }; }
|
|
1022
|
-
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
1023
|
-
ACTUAL_AGENT_COUNT=0
|
|
1024
|
-
if [ -n "$REVIEW_SHA_FULL" ] && [ -n "$TRUSTED_SCRIPT" ]; then
|
|
1025
|
-
# Only TRUSTED, current-head reviewer comments count, over every page of comments. Unique domains, so a duplicate does not inflate the count.
|
|
1026
|
-
ACTUAL_AGENT_COUNT=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
|
|
1027
|
-
| bash "$TRUSTED_SCRIPT" bodies "^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)" 2>/dev/null \
|
|
1028
|
-
| jq -r 'scan("^<!-- FORGE:REVIEW-AGENT:([a-z0-9-]+) -->") | .[0]' 2>/dev/null | sort -u | grep -c . || echo 0)
|
|
1029
|
-
fi
|
|
1030
|
-
case "$ACTUAL_AGENT_COUNT" in ''|*[!0-9]*) ACTUAL_AGENT_COUNT=0 ;; esac # fail closed: unreadable or untrusted counts as not posted
|
|
1031
|
-
# Fresh shell: SELECTED_AGENT_COUNT is not carried over. Reload it from the PR-keyed launched-agent file, then fail closed when it is empty, non-integer or zero (a staging review always selects at least one reviewer).
|
|
1032
|
-
[ -n "${SELECTED_AGENT_COUNT:-}" ] || { [ -n "${PR_NUMBER:-}" ] && [ -r "$_SD/${PR_NUMBER}_launched_review_agents" ] && SELECTED_AGENT_COUNT=$(grep -E '^[a-z0-9-]+$' "$_SD/${PR_NUMBER}_launched_review_agents" | sort -u | grep -c .); }
|
|
1033
|
-
SELECTED_COUNT_INVALID=false
|
|
1034
|
-
case "${SELECTED_AGENT_COUNT:-}" in ''|*[!0-9]*|0) SELECTED_COUNT_INVALID=true; SELECTED_AGENT_COUNT=0 ;; esac
|
|
1035
|
-
if [ "${DISPATCH_FAILED:-}" = "true" ] || [ "$SELECTED_COUNT_INVALID" = "true" ] || [ "$ACTUAL_AGENT_COUNT" -lt "$SELECTED_AGENT_COUNT" ]; then
|
|
1036
|
-
gh label create "review-degraded" --color "E4E669" --description "PR review panel was incomplete; re-review required before deployment. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null || true
|
|
1037
|
-
gh pr edit "$PR_NUMBER" -R "$GH_REPO" --add-label "review-degraded" --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1038
|
-
gh pr comment "$PR_NUMBER" -R "$GH_REPO" --body "<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->
|
|
1039
|
-
## Deploy Review Blocked: Incomplete Isolated Review Panel
|
|
1040
|
-
|
|
1041
|
-
**Selected isolated reviewers**: ${SELECTED_AGENT_COUNT}
|
|
1042
|
-
**Completed isolated reviewers**: ${ACTUAL_AGENT_COUNT}
|
|
1043
|
-
|
|
1044
|
-
Re-run the full staging review in a fresh session before deployment."
|
|
1045
|
-
exit 1
|
|
1046
|
-
fi
|
|
1047
|
-
```
|
|
386
|
+
---
|
|
1048
387
|
|
|
1049
388
|
## Phase 7: Finding Triage & Issue Creation
|
|
1050
389
|
|
|
1051
|
-
###
|
|
1052
|
-
|
|
1053
|
-
Before launching every Phase 2-6 review agent, append its lowercase marker domain (for example, `security`, `bug-hunter-api`, or `regression-risk`) to `LAUNCHED_REVIEW_AGENTS` and persist it with `printf '%s\n' "$AGENT_DOMAIN" >> "${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}/${PR_NUMBER}_launched_review_agents"` (each Bash call is a fresh shell; the fence below reloads the file, accepting only `^[a-z0-9-]+$` lines). The file is truncated by the executable fence at the start of Phase 2. A completed agent is accounted for only when its marker comment is present on the PR from a trusted author, starts with the marker, and carries the `Reviewed-SHA:` line for the current head.
|
|
390
|
+
### 7A: Delivery, test gate, extract, classify and prepare (one CLI call)
|
|
1054
391
|
|
|
1055
392
|
```bash
|
|
1056
393
|
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() { :; }; }
|
|
1057
394
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
echo "REVIEW DELIVERY FAILURE: launched-agent list is empty or unreadable (PR_NUMBER='${PR_NUMBER:-}')"
|
|
1063
|
-
echo "Recover the launched reviewer domains from the PR's FORGE:REVIEW-AGENT comments, re-append them to the launched-agent file, and rerun."
|
|
1064
|
-
exit 1
|
|
1065
|
-
fi
|
|
1066
|
-
MISSING_AGENT_COMMENTS=""
|
|
1067
|
-
# Each Bash call is a fresh shell: re-resolve the full head SHA if it is not set, and refuse when it is not a commit id
|
|
1068
|
-
# (an empty SHA would make the Reviewed-SHA match accept any head).
|
|
1069
|
-
[ -n "${REVIEW_SHA_FULL:-}" ] || REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
1070
|
-
case "$REVIEW_SHA_FULL" in *[!0-9a-f]*|"") REVIEW_SHA_FULL="" ;; *) [ "${#REVIEW_SHA_FULL}" -eq 40 ] || [ "${#REVIEW_SHA_FULL}" -eq 64 ] || REVIEW_SHA_FULL="" ;; esac
|
|
1071
|
-
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() { :; }; }
|
|
1072
|
-
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
1073
|
-
for AGENT_DOMAIN in $LAUNCHED_REVIEW_AGENTS; do
|
|
1074
|
-
AGENT_COMMENT_COUNT=0
|
|
1075
|
-
if [ -n "$REVIEW_SHA_FULL" ] && [ -n "$TRUSTED_SCRIPT" ]; then
|
|
1076
|
-
AGENT_COMMENT_COUNT=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
|
|
1077
|
-
| bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)" 2>/dev/null || echo 0)
|
|
1078
|
-
fi
|
|
1079
|
-
case "$AGENT_COMMENT_COUNT" in ''|*[!0-9]*) AGENT_COMMENT_COUNT=0 ;; esac # fail closed: counts as missing
|
|
1080
|
-
if [ "${AGENT_COMMENT_COUNT:-0}" -lt 1 ]; then
|
|
1081
|
-
MISSING_AGENT_COMMENTS="${MISSING_AGENT_COMMENTS} ${AGENT_DOMAIN}"
|
|
1082
|
-
fi
|
|
1083
|
-
done
|
|
1084
|
-
|
|
1085
|
-
if [ -n "$MISSING_AGENT_COMMENTS" ]; then
|
|
1086
|
-
echo "REVIEW DELIVERY FAILURE: missing findings comment(s) for:${MISSING_AGENT_COMMENTS}"
|
|
1087
|
-
echo "A comment without a REVIEW-FINDINGS-START block counts as missing. Re-dispatch the missing agent once with the note: structured findings block required. A second miss is a degraded panel. Recover each agent's returned verdict, findings, and durable body path before rerunning review."
|
|
1088
|
-
exit 1
|
|
1089
|
-
fi
|
|
395
|
+
[ -n "${PR_NUMBER:-}" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: PR_NUMBER unavailable after reload; nothing triaged (fail closed)"; exit 1; }
|
|
396
|
+
FD_ROOT=$(forge_resolve review-staging-post.sh); FD_ROOT="${FD_ROOT%/scripts/review-staging-post.sh}" # install root, never the PR checkout
|
|
397
|
+
[ -n "$FD_ROOT" ] && [ -f "$FD_ROOT/bin/forgedock.mjs" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: the forgedock CLI is not resolvable (set FORGEDOCK_HOME to the ForgeDock install); nothing triaged (fail closed)"; exit 1; }
|
|
398
|
+
node "$FD_ROOT/bin/forgedock.mjs" review-staging "$PR_NUMBER" --finalize --test-gate-output "$_SD/${PR_NUMBER}_test_gate_output.txt" --notes-dropped "${NOTES_DROPPED:-0}"
|
|
1090
399
|
```
|
|
1091
400
|
|
|
1092
|
-
|
|
401
|
+
The call runs these steps in order:
|
|
402
|
+
1. The incomplete-panel guard and Phase 6.75 delivery verification. A launched reviewer without a trusted current-head comment that carries a findings block is a delivery failure. An empty or unreadable launched-agent list also fails, and is never evidence that nothing needed verifying.
|
|
403
|
+
2. The Phase 6.5 verdict handling.
|
|
404
|
+
3. `scripts/review-finalize.sh`:
|
|
405
|
+
- `extract`: trusted comments only, dedup, and the `commands/review-pr.md` §6B.4 provenance and concreteness gates. **CRITICAL/HIGH are never routed as pre-existing**, whatever their confidence.
|
|
406
|
+
- `classify --dry-run`, then `classify` for real. This passes `--scope-files` (the 0B.1 scoped set when `SCOPE_MODE=scoped`) and `--prior-prs` (the bundle members), and posts `FORGE:NOTE_DISPOSITION`.
|
|
407
|
+
- `file-prepare`.
|
|
1093
408
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
`scripts/review-finalize.sh` is the script `/review-pr` Phase 6 runs; staging adds `--scope-files` (the 0B.1 scoped review set) and `--prior-prs` (the bundle member PRs). It reads reviewer comments only through `scripts/trusted-comments.sh` (trusted author, body starting with the marker, the current-head `Reviewed-SHA` line), fails closed on an unreadable source or a marker extraction mismatch (a marker the parser cannot read never makes the review look clean), dedupes by file + line range ±5 and by title, and applies the `commands/review-pr.md` §6B.4 provenance and concreteness gates (head SHA, introduced-line against the PR diff, failure scenario): a stale finding is dropped, a MEDIUM-or-lower finding outside the diff is `pre-existing`, and **CRITICAL/HIGH are never routed as pre-existing**, whatever their confidence. **Exit 3 means `REVIEW_RESULT: status: BLOCKED` was printed** (unresolvable classifier or cap helper, unreadable comments, extraction mismatch, no trusted reviewer comment for the head): STOP, file nothing, emit no deploy approval and report `RESULT: BLOCK DEPLOY` with the blocker (`exit` ends only that Bash block).
|
|
1097
|
-
|
|
1098
|
-
```bash
|
|
1099
|
-
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() { :; }; }
|
|
1100
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1101
|
-
CTX="$_SD/review-ctx-${PR_NUMBER}.json"; [ -s "$CTX" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: review context $CTX missing (0C did not run); nothing triaged (fail closed)"; exit 1; } # STOP
|
|
1102
|
-
RF=$(forge_resolve review-finalize.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed), never a skipped gate
|
|
1103
|
-
[ -n "$RF" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/review-finalize.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install); refusing to continue (fail closed)"; exit 1; } # STOP
|
|
1104
|
-
OUT_DIR="$_SD/review-finalize-${PR_NUMBER}"
|
|
1105
|
-
# 0B.1 results were written to a scope file by the Phase 0B.1 block; a missing or unreadable file means SCOPE_MODE=full (no demotion, fail open), announced rather than silent.
|
|
1106
|
-
SCOPE_MODE=full; CROSS_PR_FILES=""; UNREVIEWED_FILES=""; DEPLOY_WIRING_FILES=""
|
|
1107
|
-
if [ -r "$_SD/${PR_NUMBER}_scope.env" ]; then . "$_SD/${PR_NUMBER}_scope.env"; else echo "WARNING: $_SD/${PR_NUMBER}_scope.env unreadable: SCOPE_MODE=full (no out-of-scope demotion)"; fi
|
|
1108
|
-
SCOPE_FILE="$_SD/${PR_NUMBER}_scope-files.txt"; rm -f "$SCOPE_FILE"
|
|
1109
|
-
if [ "$SCOPE_MODE" = "scoped" ]; then printf '%s\n%s\n%s\n' "$CROSS_PR_FILES" "$UNREVIEWED_FILES" "$DEPLOY_WIRING_FILES" | sed '/^$/d' | sort -u > "$SCOPE_FILE"; else SCOPE_FILE=""; fi
|
|
1110
|
-
PRIOR_PRS=$(printf '%s ' $ALL_PR_NUMBERS)
|
|
1111
|
-
bash "$RF" extract --pr "$PR_NUMBER" --repo "$GH_REPO" --context "$CTX" --out-dir "$OUT_DIR"; RC=$?
|
|
1112
|
-
[ "$RC" -eq 0 ] || exit 1 # STOP: REVIEW_RESULT: status: BLOCKED was printed
|
|
1113
|
-
# note-disposition dry run (posts nothing): classifies every kept finding and prints the NOTE list
|
|
1114
|
-
bash "$RF" classify --pr "$PR_NUMBER" --repo "$GH_REPO" --context "$CTX" --out-dir "$OUT_DIR" --auto-merge false ${SCOPE_FILE:+--scope-files "$SCOPE_FILE"} --prior-prs "$PRIOR_PRS" --dry-run; RC=$?
|
|
1115
|
-
[ "$RC" -eq 0 ] || exit 1 # STOP
|
|
1116
|
-
cat "$OUT_DIR/note-list.txt" 2>/dev/null
|
|
1117
|
-
```
|
|
409
|
+
Findings come only from `FINDING:` markers, never from free text. **Exit 1 is a stop**: a `REVIEW DELIVERY FAILURE`, `RESULT: BLOCK DEPLOY` or `REVIEW_RESULT: status: BLOCKED` line names the cause. File nothing and emit no deploy approval. On a delivery failure, re-dispatch each missing reviewer once with the note "structured findings block required". A second miss is a degraded panel. Then rerun 7A.
|
|
1118
410
|
|
|
1119
|
-
|
|
411
|
+
Exit 0 prints the following:
|
|
412
|
+
- `TEST_GATE:`, `SCOPE_MODE=`.
|
|
413
|
+
- The counters (`FINDINGS_KEPT`, `FINDINGS_DROPPED_STALE`, `FINDINGS_PREEXISTING`, `NOTES_DEMOTED_NO_SCENARIO`, `FINDINGS_FILED`, `NOTES_LISTED`, `NOTES_CAPPED`, `NOTES_REFLAG`, `OUT_OF_SCOPE`).
|
|
414
|
+
- `CODE_BRANCH=`.
|
|
415
|
+
- One `NOTE` line per NOTE.
|
|
416
|
+
- One `SKIP` line per covered or unfilable worklist item (recorded in the Phase 8 summary).
|
|
417
|
+
- One `FILE <item json>` line per finding to file.
|
|
1120
418
|
|
|
1121
419
|
### 7B: Note disposition (same damper and classifier as `/review-pr` §6B.5)
|
|
1122
420
|
|
|
1123
|
-
`classify` runs `scripts/classify-finding.sh`
|
|
421
|
+
`classify` runs `scripts/classify-finding.sh` and `scripts/cap-findings.sh`. Never hand-classify. HIGH and CRITICAL findings are always ISSUEs. Under `SCOPE_MODE=scoped`, a MEDIUM, LOW or INFO finding whose cited files are all outside the 0B.1 set becomes `NOTE out_of_scope`. A finding a bundle member PR's review already covered becomes `NOTE reflag-*`. At most `review.max_filed_findings` ISSUEs are filed (default 2); the overflow becomes `NOTE capped`. Give each `NOTE` line one disposition: list it in the PR body under `## Non-blocking notes` (edit, never replace), or drop it as a duplicate or nit. NOTEs never reach the filing call.
|
|
1124
422
|
|
|
1125
|
-
### 7C:
|
|
423
|
+
### 7C: Create issues
|
|
1126
424
|
|
|
1127
|
-
`file-prepare`
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
RF=$(forge_resolve review-finalize.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed), never a skipped gate
|
|
1134
|
-
[ -n "$RF" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/review-finalize.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install); refusing to continue (fail closed)"; exit 1; } # STOP
|
|
1135
|
-
OUT_DIR="$_SD/review-finalize-${PR_NUMBER}"
|
|
1136
|
-
SCOPE_FILE="$_SD/${PR_NUMBER}_scope-files.txt"; [ -r "$SCOPE_FILE" ] || SCOPE_FILE="" # written by 7A only for a scoped review
|
|
1137
|
-
PRIOR_PRS=$(printf '%s ' $ALL_PR_NUMBERS)
|
|
1138
|
-
# 7B for real: posts FORGE:NOTE_DISPOSITION with the caller's NOTE counts.
|
|
1139
|
-
bash "$RF" classify --pr "$PR_NUMBER" --repo "$GH_REPO" --context "$CTX" --out-dir "$OUT_DIR" ${SCOPE_FILE:+--scope-files "$SCOPE_FILE"} --prior-prs "$PRIOR_PRS" \
|
|
1140
|
-
--auto-merge false --notes-fixed 0 --notes-dropped "${NOTES_DROPPED:-0}"; RC=$? # never merges here (staging deploy is gated separately) allowlist:check-command-side-effects
|
|
1141
|
-
[ "$RC" -eq 0 ] || exit 1 # STOP: REVIEW_RESULT: status: BLOCKED was printed
|
|
1142
|
-
PATTERNS_FILE="$OUT_DIR/patterns.json"; [ -s "$PATTERNS_FILE" ] || PATTERNS_FILE=""
|
|
1143
|
-
bash "$RF" file-prepare --pr "$PR_NUMBER" --repo "$GH_REPO" --context "$CTX" --out-dir "$OUT_DIR" ${PATTERNS_FILE:+--patterns "$PATTERNS_FILE"}; RC=$?
|
|
1144
|
-
[ "$RC" -eq 0 ] || exit 1 # STOP
|
|
1145
|
-
gh label create "staging-review" --color "1D76DB" --description "Finding from a staging branch review before deploy to main. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null || true
|
|
1146
|
-
echo "CODE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefName --jq '.headRefName')"
|
|
1147
|
-
jq -c '.items[] | select(.action == "file")' "$OUT_DIR/worklist.json"
|
|
1148
|
-
```
|
|
425
|
+
`file-prepare` already did every pre-filing step for each `FILE` item:
|
|
426
|
+
- managed labels and the milestone;
|
|
427
|
+
- title and line-range dedup;
|
|
428
|
+
- regression elevation;
|
|
429
|
+
- `priority:*` from `**Severity**` only;
|
|
430
|
+
- the pattern recurrence check.
|
|
1149
431
|
|
|
1150
|
-
Each
|
|
432
|
+
Each item carries `title`, `labels` (with `priority:*`), `milestone`, `batchable`, `regression_of`, `file`, `line`, `severity`, `agent` and `pattern`. Pattern consolidation follows `commands/review-pr.md` §6C. The code branch is the printed `CODE_BRANCH` (the PR's `headRefName`). **For each `FILE` item**, write the issue body, then create the issue through the `/issue` programmatic invocation contract (`commands/issue.md` § "Programmatic Invocation Contract"), never the raw issue-creation command:
|
|
1151
433
|
|
|
1152
434
|
```bash
|
|
1153
435
|
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() { :; }; }
|
|
@@ -1211,62 +493,33 @@ Add each created issue to the project board (`forge.yaml → project_board`; a m
|
|
|
1211
493
|
|
|
1212
494
|
---
|
|
1213
495
|
|
|
1214
|
-
## Phase 8: Final Summary &
|
|
1215
|
-
|
|
1216
|
-
Post summary with verdict:
|
|
1217
|
-
1. CI failed + autofix failed → BLOCK DEPLOY
|
|
1218
|
-
2. CI failed + autofix succeeded → continue
|
|
1219
|
-
2.5. Test gate returned BLOCK (blocking posture, no override) → BLOCK DEPLOY *(handled in Phase 6.5 — if Phase 8 is reached, BLOCK was either overridden or posture is advisory)*
|
|
1220
|
-
3. CONFIRMED CRITICAL (non-CI) → BLOCK DEPLOY
|
|
1221
|
-
4. CONFIRMED HIGH blocking (crashes, data loss) → NEEDS FIXES FIRST
|
|
1222
|
-
5. All else → APPROVE FOR DEPLOY
|
|
1223
|
-
|
|
1224
|
-
Include in Finding Triage Results: `SCOPE_MODE`, `reviewed_prs`, `unreviewed_commits`, `cross_pr_files` counts (Phase 0B.1), `notes_fixed`/`notes_listed`/`notes_dropped`, `findings_filed`, `out_of_scope`, and the deduped list (the `skip-*` worklist items). **Idempotency**: re-running the staging review on an unchanged bundle files 0 new findings (reviewed-set exclusion plus 7E dedup); a re-run that files anything must cite what changed in the bundle.
|
|
1225
|
-
|
|
1226
|
-
Include: Material Changes Summary, Risk Matrix (CI, Build, Bugs, Security, Billing, Quality, Regression, **Test Gate**), Finding Triage Results, Blocking Issues, Deployment Checklist (pre-deploy, deploy, post-deploy verification, rollback triggers), Stats.
|
|
1227
|
-
|
|
1228
|
-
**Risk Matrix must include a Test Gate row** (use `$TEST_GATE_VERDICT` and `$TEST_GATE_REASON` set in Phase 6.5; both are persisted to `$_SD/${PR_NUMBER}_test_gate_verdict` / `$_SD/${PR_NUMBER}_test_gate_reason` by that phase, so reload them in a fresh shell with the validated `TEST_GATE_VERDICT` / `TEST_GATE_REASON` reload lines carried by the Phase 6.5 verdict-handling fence):
|
|
1229
|
-
|
|
1230
|
-
| Domain | Result | Notes |
|
|
1231
|
-
|--------|--------|-------|
|
|
1232
|
-
| CI | ... | ... |
|
|
1233
|
-
| Build | ... | ... |
|
|
1234
|
-
| Bugs | ... | ... |
|
|
1235
|
-
| Security | ... | ... |
|
|
1236
|
-
| Billing | ... | ... |
|
|
1237
|
-
| Quality | ... | ... |
|
|
1238
|
-
| Regression | ... | ... |
|
|
1239
|
-
| **Test Gate** | `${TEST_GATE_VERDICT:-SKIP}` | `${TEST_GATE_REASON:-Phase 6.5 not run}` |
|
|
1240
|
-
|
|
1241
|
-
If `TEST_GATE_VERDICT` is `SKIP`, surface the gap explicitly in the summary:
|
|
1242
|
-
|
|
1243
|
-
> **Test Gate: SKIP** — No runtime tests ran for this bundle. This means acceptance criteria were NOT verified against running code before deploy. Cause: `${TEST_GATE_REASON}`. To enable runtime testing, configure `verification.integration_tests` in `forge.yaml`.
|
|
1244
|
-
|
|
1245
|
-
If `TEST_GATE_VERDICT` is `PASS`, note it as a positive signal:
|
|
1246
|
-
|
|
1247
|
-
> **Test Gate: PASS** — Acceptance criteria verified against running code. No batch-introduced runtime failures detected.
|
|
1248
|
-
|
|
1249
|
-
If `TEST_GATE_VERDICT` is `BLOCK` and Phase 8 was reached (advisory posture or override active), note it as a risk:
|
|
1250
|
-
|
|
1251
|
-
> **Test Gate: BLOCK (override/advisory)** — Runtime failures were detected but deploy is proceeding. Reason: `${TEST_GATE_REASON}`. Filed `test-failure` issues track the failures.
|
|
1252
|
-
|
|
1253
|
-
**Pipeline cost** (fail-soft, informational; never changes the verdict) <!-- Added: forge#3824 -->: add a "Pipeline cost" table from `scripts/cost-report.sh` (current version vs the previous two). An unresolvable script or an empty runs dir omits the table.
|
|
496
|
+
## Phase 8: Final Summary & Gate Marker (one CLI call)
|
|
1254
497
|
|
|
1255
498
|
```bash
|
|
1256
499
|
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() { :; }; }
|
|
1257
|
-
|
|
1258
|
-
[ -n "$
|
|
500
|
+
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
501
|
+
[ -n "${PR_NUMBER:-}" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: PR_NUMBER unavailable after reload; no verdict (fail closed)"; exit 1; }
|
|
502
|
+
FD_ROOT=$(forge_resolve review-staging-post.sh); FD_ROOT="${FD_ROOT%/scripts/review-staging-post.sh}" # install root, never the PR checkout
|
|
503
|
+
[ -n "$FD_ROOT" ] && [ -f "$FD_ROOT/bin/forgedock.mjs" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: the forgedock CLI is not resolvable (set FORGEDOCK_HOME to the ForgeDock install); no verdict (fail closed)"; exit 1; }
|
|
504
|
+
FILED_ARGS=""; for _n in ${FILED_ISSUES:-}; do case "$_n" in *[!0-9]*|"") ;; *) FILED_ARGS="$FILED_ARGS --filed $_n" ;; esac; done # the ISSUE_NUM values file-verify printed
|
|
505
|
+
# shellcheck disable=SC2086 # FILED_ARGS is a validated list of flags
|
|
506
|
+
node "$FD_ROOT/bin/forgedock.mjs" review-staging "$PR_NUMBER" --summary $FILED_ARGS
|
|
1259
507
|
```
|
|
1260
508
|
|
|
1261
|
-
|
|
509
|
+
Set `FILED_ISSUES` to the space-separated `ISSUE_NUM` values from 7C. The call computes the verdict:
|
|
510
|
+
1. CI failing (the Phase 1E read; a red `forgedock/verify` alone does not count, and an unreadable result counts as failing) → BLOCK DEPLOY. A CI fixed by the 1F autofix is green again by now.
|
|
511
|
+
2. A missing or invalid persisted test-gate verdict → BLOCK DEPLOY. A blocking BLOCK already stopped in 7A, so a BLOCK seen here was either overridden or advisory.
|
|
512
|
+
3. A CONFIRMED CRITICAL → BLOCK DEPLOY.
|
|
513
|
+
4. A CONFIRMED HIGH → NEEDS FIXES FIRST.
|
|
514
|
+
5. All else → APPROVE FOR DEPLOY.
|
|
1262
515
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
516
|
+
It posts one summary comment holding the following:
|
|
517
|
+
- The Risk Matrix. It includes CI, the findings counts and the **Test Gate** row. A SKIP is surfaced explicitly as a gap: acceptance criteria were not verified against running code.
|
|
518
|
+
- The Finding Triage Results: `SCOPE_MODE`, `reviewed_prs`, `unreviewed_commits`, `cross_pr_files`, `notes_listed`/`notes_dropped`/`notes_capped`, `findings_filed` with the filed issues, `out_of_scope`, and the deduped list.
|
|
519
|
+
- The fail-soft pipeline cost (`scripts/cost-report.sh`) and stats (`forgedock stats --compact`) tables. These are informational and never change the verdict.
|
|
520
|
+
- **Exactly one gate marker kind**: `<!-- FORGE:GATE_PASS -->` on APPROVE FOR DEPLOY, `<!-- FORGE:GATE_FAILURE -->` otherwise, each with a `TYPE=staging-review|BUNDLE=...` line.
|
|
521
|
+
|
|
522
|
+
The call exits 0 on APPROVE FOR DEPLOY and 1 otherwise (`RESULT: BLOCK DEPLOY`). A head that moved since 7A is a stop: re-run the review. **Idempotency**: re-running the staging review on an unchanged bundle files 0 new findings (reviewed-set exclusion plus dedup). A re-run that files anything must cite what changed in the bundle. In your final report, add the Material Changes Summary (Phase 2), the per-domain reviewer verdicts, Blocking Issues and the Deployment Checklist: pre-deploy, deploy, post-deploy verification and rollback triggers.
|
|
1270
523
|
|
|
1271
524
|
**CRITICAL**: This review NEVER merges staging → main. User makes deploy decision via GitHub web UI.
|
|
1272
525
|
|