forgedock 1.16.2 → 1.18.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 +114 -0
- package/bin/engine/phases.mjs +211 -100
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +20 -5
- package/bin/engine/telemetry-seam.mjs +62 -0
- package/bin/engine/telemetry-seam.test.mjs +60 -0
- package/bin/engine/telemetry.mjs +148 -0
- package/bin/engine/telemetry.test.mjs +120 -0
- package/bin/engine-cli.mjs +17 -1
- package/bin/engine.mjs +227 -48
- package/bin/forgedock.mjs +8 -2
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +220 -0
- package/bin/runner.mjs +70 -4
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +211 -0
- package/bin/stats/index.mjs +154 -0
- package/bin/stats/local-source.mjs +79 -0
- package/bin/stats/package.json +6 -0
- package/bin/stats/render.mjs +82 -0
- package/bin/stats/stats.test.mjs +312 -0
- package/commands/cleanup.md +90 -2
- package/commands/deploy-info.md +8 -4
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-1-resolve.md +4 -1
- package/commands/orchestrate/phase-4-execution.md +28 -6
- package/commands/orchestrate/phase-5-cleanup.md +18 -2
- package/commands/pipeline-health/phase-1-context.md +2 -0
- package/commands/pipeline-health/phase-2-metrics.md +12 -5
- package/commands/pipeline-status.md +21 -1
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents.md +10 -2
- package/commands/review-pr-staging.md +196 -496
- package/commands/review-pr.md +512 -3134
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +13 -1
- package/commands/work-on/build/implement.md +28 -6
- package/commands/work-on/build/validate.md +5 -2
- package/commands/work-on/build.md +197 -145
- package/commands/work-on/close.md +66 -20
- package/commands/work-on/decompose.md +113 -34
- package/commands/work-on/investigate.md +15 -14
- package/commands/work-on/remediate.md +49 -19
- package/commands/work-on/review.md +7 -5
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +76 -24
- package/docs/OUTCOME-CODES.md +53 -0
- package/docs/RELEASE-NOTES.md +24 -0
- package/docs/STATS.md +63 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +19 -0
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- package/packages/protocol/src/phases.js +5 -5
- package/scripts/builder-resume-match.test.sh +58 -0
- package/scripts/cap-findings.sh +110 -0
- package/scripts/cap-findings.test.sh +130 -0
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-finding.sh +65 -16
- package/scripts/classify-finding.test.sh +43 -1
- package/scripts/classify-grep-exit.sh +20 -0
- package/scripts/classify-grep-exit.test.sh +46 -0
- package/scripts/close-parent-c3.test.sh +123 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +97 -3
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/doctor-pipeline-state.sh +43 -0
- package/scripts/file-deferred-followups.sh +159 -0
- package/scripts/file-deferred-followups.test.sh +106 -0
- package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
- package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
- package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
- package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
- package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
- package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
- package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
- package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
- package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
- package/scripts/forge-root.test.sh +18 -11
- package/scripts/lib/fence-bootstrap.sh +4 -3
- package/scripts/lib/pr-issue-number.sh +18 -0
- package/scripts/lib/review-template-resolve.sh +41 -0
- package/scripts/lib/telemetry.sh +168 -0
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/remediate-liveness.sh +42 -6
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-acceptance-gate.sh +16 -7
- package/scripts/review-acceptance-gate.test.sh +13 -0
- package/scripts/review-finalize.sh +1427 -0
- package/scripts/review-finalize.test.sh +833 -0
- package/scripts/review-pr-delta-wiring.test.sh +19 -72
- package/scripts/review-pr-domain-map.test.sh +23 -13
- package/scripts/review-pr-gate2.test.sh +24 -65
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +56 -80
- package/scripts/review-prepare.sh +553 -0
- package/scripts/review-prepare.test.sh +288 -0
- package/scripts/review-roster.sh +186 -0
- package/scripts/review-roster.test.sh +185 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/trusted-comments.test.sh +9 -2
- package/scripts/wait-ci-green.sh +8 -1
- package/scripts/wait-ci-green.test.sh +3 -3
- package/templates/forge.yaml.minimal +9 -0
|
@@ -47,7 +47,7 @@ When `IS_OPENCODE_RUNTIME=true`, lowercase native `task` is the preferred isolat
|
|
|
47
47
|
1. **If `Task` is available in the current environment**: set `{DISPATCH_TOOL} = Task`. This is the preferred tool — tightest `allowed-tools` scoping. (Identical resolution logic to `/review-pr` Phase 3C — do not diverge.)
|
|
48
48
|
2. **Else if `Agent` is available**: set `{DISPATCH_TOOL} = Agent`. This is the documented fallback, not a degraded path — use it exactly as you would `Task`: one call per selected agent (Bug Hunters in Phase 3, Code Quality in Phase 4, domain agents in Phase 5), same prompt template, `subagent_type: "general-purpose"` (or the closest equivalent the environment offers), same requirement that each agent posts its own findings directly to the PR via `gh pr comment`. Isolation and fresh-context review are preserved either way.
|
|
49
49
|
3. **Neither tool is available**: this is a genuine setup defect, not a routing decision — HARD STOP, post a PR/issue comment explaining that no sub-agent dispatch tool is available, add `needs-human`, and exit without posting a verdict. Do NOT fall back to reviewing inline in the orchestrator's own context.
|
|
50
|
-
4. **Dispatch pool exhausted or dispatch call fails**: this is not tool absence. If any Bug Hunter, quality, domain, material-change, or regression reviewer cannot be launched because the sub-agent pool/session limit is exhausted (or any dispatch call fails), HARD STOP. Do not substitute inline review or silently continue with a partial panel. Mark the PR `review-degraded`, post `<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->` with the selected and completed counts, and exit without a deploy verdict. A fresh session must re-run the full panel.
|
|
50
|
+
4. **Dispatch pool exhausted or dispatch call fails**: this is not tool absence. If any Bug Hunter, quality, domain, material-change, or regression reviewer cannot be launched because the sub-agent pool/session limit is exhausted (or any dispatch call fails), HARD STOP. Do not substitute inline review or silently continue with a partial panel. Mark the PR `review-degraded`, post `<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->` with the selected and completed counts, and exit without a deploy verdict. A fresh session must re-run the full panel. **Disk-full carve-out**: if the failure text matches `EDQUOT`, `ENOSPC`, `No space left`, or `Disk quota exceeded` (errno -28/-122), it is a transient infrastructure failure, not a panel integrity failure: do NOT add `needs-human`, do NOT post `review-panel-integrity`, and do NOT mark `review-degraded`; exit with `REVIEW_RESULT: status: BLOCKED, blocker: infra disk full (EDQUOT/ENOSPC)` so the engine ends a retryable `engine-error` that `--retry` resumes once space is freed.
|
|
51
51
|
|
|
52
52
|
**Do not halt to ask the operator which tool to use.** Steps 1–2 are deterministic and fully resolve the common case; only step 3 (both absent) requires a stop, and even then the action is HARD STOP + `needs-human`, not a question back to the operator.
|
|
53
53
|
|
|
@@ -110,46 +110,12 @@ The **Evidence-Based Review Protocol** and **Structured Findings Protocol** are
|
|
|
110
110
|
|
|
111
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 — re-review passes are distinguished by SHA, not by PR number. When a staging→main deploy PR receives blocking findings and the fixes are pushed to the head branch, re-invoke this spec on the same PR number. Do NOT close the PR and create a new one.
|
|
112
112
|
|
|
113
|
-
**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:
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
# Load prior FORGE:REVIEW_ROUTE comments to detect re-entry
|
|
117
|
-
PRIOR_ROUTES=$(gh api repos/${GH_REPO}/issues/${PR_NUMBER}/comments \
|
|
118
|
-
--jq '[.[] | select(.body | test("FORGE:REVIEW_ROUTE")) | .body] | length' 2>/dev/null || echo 0)
|
|
119
|
-
|
|
120
|
-
IS_REENTRY=0
|
|
121
|
-
if [ "$PRIOR_ROUTES" -gt 0 ]; then
|
|
122
|
-
IS_REENTRY=1
|
|
123
|
-
echo "Re-entry detected: $PRIOR_ROUTES prior review pass(es) on PR #${PR_NUMBER}"
|
|
124
|
-
|
|
125
|
-
# Load findings from prior passes that are still open
|
|
126
|
-
PRIOR_OPEN_FINDINGS=$(gh issue list ${GH_FLAG} \
|
|
127
|
-
--label "review-finding" \
|
|
128
|
-
--state open \
|
|
129
|
-
--search "PR #${PR_NUMBER}" \
|
|
130
|
-
--limit 50 \
|
|
131
|
-
--json number,title,labels \
|
|
132
|
-
--jq '.[] | " - #\(.number): \(.title)"' 2>/dev/null || true)
|
|
133
|
-
|
|
134
|
-
# Load findings that were resolved (closed) since the last pass
|
|
135
|
-
PRIOR_RESOLVED_FINDINGS=$(gh issue list ${GH_FLAG} \
|
|
136
|
-
--label "review-finding" \
|
|
137
|
-
--state closed \
|
|
138
|
-
--search "PR #${PR_NUMBER}" \
|
|
139
|
-
--limit 50 \
|
|
140
|
-
--json number,title \
|
|
141
|
-
--jq '.[] | " - #\(.number): \(.title)"' 2>/dev/null || true)
|
|
142
|
-
|
|
143
|
-
echo "Prior open findings (carry forward): $(echo "$PRIOR_OPEN_FINDINGS" | grep -c '.' || echo 0)"
|
|
144
|
-
echo "Prior resolved findings (exclude from verdict): $(echo "$PRIOR_RESOLVED_FINDINGS" | grep -c '.' || echo 0)"
|
|
145
|
-
fi
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
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.
|
|
149
|
-
|
|
150
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.
|
|
151
114
|
|
|
152
115
|
```bash
|
|
116
|
+
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}" # this fence resolves PR_NUMBER itself, so it binds config directly instead of reloading the persisted pointer
|
|
117
|
+
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE" 2>/dev/null); GH_FLAG="-R $GH_REPO"
|
|
118
|
+
DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE" 2>/dev/null); STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE" 2>/dev/null)
|
|
153
119
|
# Resolve PR_NUMBER from the argument string (never name the loader placeholder in this block: every
|
|
154
120
|
# occurrence is substituted textually, comments included) <!-- Added: forge#3466 -->
|
|
155
121
|
# The argument string may be: a PR number, "staging", "feature", or "staging:feature"
|
|
@@ -192,6 +158,8 @@ REVIEW_SHA_FULL=""
|
|
|
192
158
|
[ -n "$PR_NUMBER" ] && REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" ${GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
193
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
|
|
194
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"
|
|
195
163
|
|
|
196
164
|
if [ -n "$PR_NUMBER" ]; then
|
|
197
165
|
gh pr comment "$PR_NUMBER" ${GH_FLAG} --body "<!-- FORGE:REVIEW_ROUTE mode=staging-deploy spec=review-pr-staging.md sha=${REVIEW_SHA_STAGING} -->"
|
|
@@ -200,10 +168,55 @@ else
|
|
|
200
168
|
fi
|
|
201
169
|
# Persist for later fences (each Bash call is a fresh shell). Remove a stale file from a previous run when nothing resolved.
|
|
202
170
|
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_SD"
|
|
203
|
-
|
|
171
|
+
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
|
+
if [ -n "$PR_NUMBER" ]; then printf '%s\n' "$PR_NUMBER" > "$_SD/staging_pr_number.${FORGE_RUN_KEY}"; else rm -f "$_SD/staging_pr_number.${FORGE_RUN_KEY}"; fi
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`$PR_NUMBER` is now set for all downstream phases that conditionally post gate comments to the PR. It is also persisted to the run-keyed pointer `$_SD/staging_pr_number.${FORGE_RUN_KEY}`, because every Bash call is a fresh shell and concurrent runs share `$_SD`. `FORGE_RUN_KEY` is a pre-exported `FORGE_RUN_KEY`, else `FORGE_RUN_ID` or the first available session id (Claude Code, Codex, OpenCode), else `$PPID` (best-effort, may not be stable across calls); `fence-bootstrap.sh` derives it identically and never falls back to an unkeyed pointer. Every fence that consumes cross-fence state (`PR_NUMBER`, `ALL_PR_NUMBERS`, `GH_REPO`, branch names) begins with the `# FENCE_BOOTSTRAP` stub, which sources `scripts/lib/fence-bootstrap.sh` (via `scripts/lib/forge-resolve.sh`) to re-derive it and exits 1 when the helper is unresolvable; `scripts/review-pr-staging-scope.test.sh` fails if a consuming fence omits it. The Phase 6.5 gate result and the Phase 6.75 launched-agent list are persisted to `$_SD` by their producers and reloaded (validated) by their consumers.
|
|
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
|
|
204
217
|
```
|
|
205
218
|
|
|
206
|
-
|
|
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.
|
|
207
220
|
|
|
208
221
|
---
|
|
209
222
|
|
|
@@ -214,6 +227,8 @@ if [ -n "$PR_NUMBER" ]; then printf '%s\n' "$PR_NUMBER" > "$_SD/staging_pr_numbe
|
|
|
214
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 -->
|
|
215
228
|
|
|
216
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
|
|
217
232
|
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
218
233
|
|
|
219
234
|
# Step 1: Find all PR numbers in the staging→main bundle
|
|
@@ -248,14 +263,17 @@ for pr_num in $ALL_PR_NUMBERS; do
|
|
|
248
263
|
**PR #${pr_num}** has a \`review-degraded\` label and requires a full fresh-context re-review."
|
|
249
264
|
fi
|
|
250
265
|
|
|
251
|
-
# Search for open review-finding issues that reference this PR
|
|
266
|
+
# Search for open review-finding issues that reference this PR. The search is only a coarse
|
|
267
|
+
# candidate fetch (full-text, tokenized: it also hits hidden markers and longer numbers), so
|
|
268
|
+
# narrow it with the anchored FINDING_PR_FILTER: title suffix `PR #N)` or a `**Source**: PR #N` line.
|
|
269
|
+
FINDING_PR_FILTER='[.[] | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
252
270
|
OPEN_FINDINGS=$(gh issue list -R {GH_REPO} \
|
|
253
271
|
--label "review-finding" \
|
|
254
272
|
--state open \
|
|
255
273
|
--search "PR #${pr_num}" \
|
|
256
274
|
--limit 20 \
|
|
257
|
-
--json number,title \
|
|
258
|
-
|
|
275
|
+
--json number,title,body 2>/dev/null \
|
|
276
|
+
| jq -r --arg n "${pr_num}" "$FINDING_PR_FILTER" 2>/dev/null) # uses FINDING_PR_FILTER
|
|
259
277
|
|
|
260
278
|
if [ -n "$OPEN_FINDINGS" ]; then
|
|
261
279
|
BLOCKING_FINDINGS="${BLOCKING_FINDINGS}
|
|
@@ -371,6 +389,8 @@ If the gate exits with `RESULT: PASS` → a `<!-- FORGE:GATE_PASS -->` structure
|
|
|
371
389
|
## Phase 0B: Scope Analysis
|
|
372
390
|
|
|
373
391
|
```bash
|
|
392
|
+
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() { :; }; }
|
|
393
|
+
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
374
394
|
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
375
395
|
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --stat | tail -20
|
|
376
396
|
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --numstat | awk '{add+=$(1); del+=$(2)} END {print "Added:", add, "Deleted:", del, "Total:", add+del}'
|
|
@@ -502,7 +522,7 @@ fi
|
|
|
502
522
|
# Deploy/runtime readiness files are always in scope: migrations, env, workflows, compose/infra, dependency manifests.
|
|
503
523
|
_DW=$(git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --name-only) || { SCOPE_MODE=full; _DW=""; }
|
|
504
524
|
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)
|
|
505
|
-
# Persist the results for
|
|
525
|
+
# Persist the results for 7A (a later Bash call is a fresh shell). %q keeps multi-line file lists intact.
|
|
506
526
|
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"
|
|
507
527
|
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 .)"
|
|
508
528
|
```
|
|
@@ -515,6 +535,30 @@ echo "SCOPE_MODE=$SCOPE_MODE reviewed_prs=$REVIEWED_PR_COUNT unreviewed_commits=
|
|
|
515
535
|
|
|
516
536
|
---
|
|
517
537
|
|
|
538
|
+
### 0C: Prepare (one script call)
|
|
539
|
+
|
|
540
|
+
`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.
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
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() { :; }; }
|
|
544
|
+
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
545
|
+
[ -n "${PR_NUMBER:-}" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: PR_NUMBER unavailable after reload; nothing reviewed"; exit 1; }
|
|
546
|
+
RP=$(forge_resolve review-prepare.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed)
|
|
547
|
+
[ -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; }
|
|
548
|
+
CTX="$_SD/review-ctx-${PR_NUMBER}.json"
|
|
549
|
+
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
|
|
550
|
+
if [ "$RP_RC" -ne 0 ] || [ ! -s "$CTX" ]; then
|
|
551
|
+
[ -s "$CTX" ] && jq -r '.blockers[]? | "BLOCKER: " + .' "$CTX"
|
|
552
|
+
echo "REVIEW_RESULT: status: BLOCKED, blocker: review-prepare failed (exit ${RP_RC}); nothing reviewed (fail closed)"; exit 1
|
|
553
|
+
fi
|
|
554
|
+
echo "CONTEXT_FILE=$CTX"
|
|
555
|
+
jq -r '"TEMPLATE_SOURCE=\(.templates.source)", "TEMPLATE_BASE=\(.templates.base)", "MONOLITHIC_CATALOG=\(.templates.monolithic_catalog // "")"' "$CTX"
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
The context file lives at `$_SD/review-ctx-${PR_NUMBER}.json`; later fences recompute that path after their bootstrap line.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
518
562
|
## Phase 1: Automated Checks
|
|
519
563
|
|
|
520
564
|
### 1A: Python Linting
|
|
@@ -698,139 +742,15 @@ Reviewed-SHA: [REVIEW_SHA]
|
|
|
698
742
|
|
|
699
743
|
## Phase 5: Security & Billing Deep Dive
|
|
700
744
|
|
|
701
|
-
**
|
|
702
|
-
|
|
703
|
-
Missing persona templates are a fatal setup error, not permission to skip multi-agent review. Resolve the template source through this ordered chain (identical to the one used by `/review-pr` Phase 3C — do not diverge) and STOP if none resolve — never fall through to reviewing inline in the main context:
|
|
704
|
-
|
|
705
|
-
```bash
|
|
706
|
-
TEMPLATE_BASE=""
|
|
707
|
-
# Tier 0: the running plugin's own root (Claude Code substitutes this exact spelling; elsewhere it stays a
|
|
708
|
-
# literal that the /* check rejects). Wins over an exported FORGE_HOME, which may name an older checkout.
|
|
709
|
-
_PLUGIN_ROOT='${CLAUDE_PLUGIN_ROOT}'; case "$_PLUGIN_ROOT" in /*) ;; *) _PLUGIN_ROOT="" ;; esac
|
|
710
|
-
if [[ -n "$_PLUGIN_ROOT" && -f "$_PLUGIN_ROOT/commands/review-pr-agents/protocols.md" ]]; then
|
|
711
|
-
TEMPLATE_BASE="$_PLUGIN_ROOT/commands/review-pr-agents"
|
|
712
|
-
TEMPLATE_SOURCE="plugin_root"
|
|
713
|
-
elif [[ -f "$FORGE_HOME/commands/review-pr-agents/protocols.md" ]]; then
|
|
714
|
-
TEMPLATE_BASE="$FORGE_HOME/commands/review-pr-agents"
|
|
715
|
-
TEMPLATE_SOURCE="forge_home"
|
|
716
|
-
else
|
|
717
|
-
FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
|
|
718
|
-
REPO_PATH=$(yq '.paths.root' "$FORGE_YAML" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
719
|
-
if [[ -f "$REPO_PATH/commands/review-pr-agents/protocols.md" ]]; then
|
|
720
|
-
TEMPLATE_BASE="$REPO_PATH/commands/review-pr-agents"
|
|
721
|
-
TEMPLATE_SOURCE="repo_path"
|
|
722
|
-
elif [[ -f "$REPO_PATH/commands/review-pr-agents.md" ]] && grep -q "^### Agent:" "$REPO_PATH/commands/review-pr-agents.md" 2>/dev/null; then
|
|
723
|
-
# Content check (not just existence) required: a post-split repo still ships a small
|
|
724
|
-
# router stub at this same path that only points back to the (missing) persona
|
|
725
|
-
# directory — reading it would provide no actual protocol/persona content.
|
|
726
|
-
MONOLITHIC_CATALOG="$REPO_PATH/commands/review-pr-agents.md"
|
|
727
|
-
TEMPLATE_SOURCE="monolithic_catalog"
|
|
728
|
-
else
|
|
729
|
-
TEMPLATE_SOURCE="none"
|
|
730
|
-
fi
|
|
731
|
-
fi
|
|
732
|
-
|
|
733
|
-
if [[ "$TEMPLATE_SOURCE" == "none" ]]; then
|
|
734
|
-
echo "FATAL: no review-pr-agents template source resolved (checked \$FORGE_HOME, repo-path fallback, monolithic catalog)."
|
|
735
|
-
# HARD STOP — post error, add needs-human, do NOT review
|
|
736
|
-
fi
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
**If `TEMPLATE_SOURCE` is `none`**: HARD STOP. Post a PR comment explaining the setup is broken, instructing the user to run `npx forgedock update` to repair the install, add `needs-human`, and exit without posting any findings or a verdict. **NEVER perform the review inline in the main agent context as a substitute.**
|
|
745
|
+
**Templates (resolved by `review-prepare.sh` in 0C, the same ordered chain `/review-pr` Phase 3C uses)**: `TEMPLATE_SOURCE` is `plugin_root`, `forge_home`, `repo_path` or `monolithic_catalog`; a missing source BLOCKED the review in 0C. **NEVER perform the review inline as a substitute.** Read `$TEMPLATE_BASE/protocols.md` and `$TEMPLATE_BASE/<persona>.md` per selected agent; for `monolithic_catalog` read `$MONOLITHIC_CATALOG` and extract the shared protocols plus each persona's section from that one file.
|
|
740
746
|
|
|
741
|
-
|
|
747
|
+
Launch domain-specific agents based on which domains have changes. Substitute PR diff commands with staging diff commands.
|
|
742
748
|
|
|
743
|
-
**
|
|
749
|
+
**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.
|
|
744
750
|
|
|
745
|
-
|
|
751
|
+
**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).
|
|
746
752
|
|
|
747
|
-
|
|
748
|
-
```bash
|
|
749
|
-
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() { :; }; }
|
|
750
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
751
|
-
# Fetched once, UNCAPPED; agents do NOT re-fetch it. Never cap here: slicing must see the whole bundle.
|
|
752
|
-
FULL_DIFF=$(git diff "origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH")
|
|
753
|
-
|
|
754
|
-
# --- Security agent: always receives the full diff (cross-cutting domain) ---
|
|
755
|
-
DIFF_SLICE_SECURITY="$FULL_DIFF"
|
|
756
|
-
|
|
757
|
-
# --- Auth agent: auth, session, jwt, oauth, middleware, permission files ---
|
|
758
|
-
DIFF_SLICE_AUTH=$(echo "$FULL_DIFF" | awk '
|
|
759
|
-
/^diff --git/ { in_block=0 }
|
|
760
|
-
/^diff --git.*\/(auth|session|jwt|oauth|permission|middleware|login|token)/ { in_block=1 }
|
|
761
|
-
in_block { print }
|
|
762
|
-
')
|
|
763
|
-
[ -z "$DIFF_SLICE_AUTH" ] && DIFF_SLICE_AUTH="$FULL_DIFF"
|
|
764
|
-
|
|
765
|
-
# --- Billing agent: billing, payment, credit, pricing, stripe, subscription files ---
|
|
766
|
-
DIFF_SLICE_BILLING=$(echo "$FULL_DIFF" | awk '
|
|
767
|
-
/^diff --git/ { in_block=0 }
|
|
768
|
-
/^diff --git.*\/(billing|payment|credit|pricing|stripe|subscription|invoice|charge|refund)/ { in_block=1 }
|
|
769
|
-
in_block { print }
|
|
770
|
-
')
|
|
771
|
-
[ -z "$DIFF_SLICE_BILLING" ] && DIFF_SLICE_BILLING="$FULL_DIFF"
|
|
772
|
-
|
|
773
|
-
# --- Concurrency agent: transaction, lock, queue, async, worker, race-condition files ---
|
|
774
|
-
DIFF_SLICE_CONCURRENCY=$(echo "$FULL_DIFF" | awk '
|
|
775
|
-
/^diff --git/ { in_block=0 }
|
|
776
|
-
/^diff --git.*\/(worker|queue|task|async|lock|transaction|pipeline|job)/ { in_block=1 }
|
|
777
|
-
in_block { print }
|
|
778
|
-
')
|
|
779
|
-
[ -z "$DIFF_SLICE_CONCURRENCY" ] && DIFF_SLICE_CONCURRENCY="$FULL_DIFF"
|
|
780
|
-
|
|
781
|
-
# --- Database agent: migration, model, schema, ORM, SQL files ---
|
|
782
|
-
DIFF_SLICE_DATABASE=$(echo "$FULL_DIFF" | awk '
|
|
783
|
-
/^diff --git/ { in_block=0 }
|
|
784
|
-
/^diff --git.*\/(migration|model|schema|\.sql$|orm|database|db)/ { in_block=1 }
|
|
785
|
-
in_block { print }
|
|
786
|
-
')
|
|
787
|
-
[ -z "$DIFF_SLICE_DATABASE" ] && DIFF_SLICE_DATABASE="$FULL_DIFF"
|
|
788
|
-
|
|
789
|
-
# --- API Design agent: router, endpoint, openapi, schema, serializer files ---
|
|
790
|
-
DIFF_SLICE_API=$(echo "$FULL_DIFF" | awk '
|
|
791
|
-
/^diff --git/ { in_block=0 }
|
|
792
|
-
/^diff --git.*\/(router|route|endpoint|openapi|serializer|schema|api)/ { in_block=1 }
|
|
793
|
-
in_block { print }
|
|
794
|
-
')
|
|
795
|
-
[ -z "$DIFF_SLICE_API" ] && DIFF_SLICE_API="$FULL_DIFF"
|
|
796
|
-
|
|
797
|
-
# --- Infrastructure agent: docker, nginx, ci, deploy, infra, workflow files ---
|
|
798
|
-
DIFF_SLICE_INFRA=$(echo "$FULL_DIFF" | awk '
|
|
799
|
-
/^diff --git/ { in_block=0 }
|
|
800
|
-
/^diff --git.*\/(docker|nginx|infra|\.github|deploy|ci|traefik|k8s|helm)/ { in_block=1 }
|
|
801
|
-
in_block { print }
|
|
802
|
-
')
|
|
803
|
-
[ -z "$DIFF_SLICE_INFRA" ] && DIFF_SLICE_INFRA="$FULL_DIFF"
|
|
804
|
-
|
|
805
|
-
# --- Domain Logic agent: scraping/browser-automation files (does not match bare playwright/E2E test files) ---
|
|
806
|
-
DIFF_SLICE_SCRAPER=$(echo "$FULL_DIFF" | awk '
|
|
807
|
-
/^diff --git/ { in_block=0 }
|
|
808
|
-
/^diff --git.*\/(scrape|scraper|stealth|browser_pool|headless|anti_bot|captcha|crawl)/ { in_block=1 }
|
|
809
|
-
in_block { print }
|
|
810
|
-
')
|
|
811
|
-
[ -z "$DIFF_SLICE_SCRAPER" ] && DIFF_SLICE_SCRAPER="$FULL_DIFF"
|
|
812
|
-
|
|
813
|
-
# Cap each slice AFTER slicing and after the empty-slice fallback (a fallback slice is the truncated full diff)
|
|
814
|
-
truncate_slice() {
|
|
815
|
-
local slice="${1}"
|
|
816
|
-
local limit=102400
|
|
817
|
-
if [ "${#slice}" -gt "$limit" ]; then
|
|
818
|
-
echo "${slice:0:$limit}"
|
|
819
|
-
echo "... [TRUNCATED — diff exceeded 100K chars; focus on the files listed above]"
|
|
820
|
-
else
|
|
821
|
-
echo "$slice"
|
|
822
|
-
fi
|
|
823
|
-
}
|
|
824
|
-
DIFF_SLICE_SECURITY=$(truncate_slice "$DIFF_SLICE_SECURITY")
|
|
825
|
-
DIFF_SLICE_AUTH=$(truncate_slice "$DIFF_SLICE_AUTH")
|
|
826
|
-
DIFF_SLICE_BILLING=$(truncate_slice "$DIFF_SLICE_BILLING")
|
|
827
|
-
DIFF_SLICE_CONCURRENCY=$(truncate_slice "$DIFF_SLICE_CONCURRENCY")
|
|
828
|
-
DIFF_SLICE_DATABASE=$(truncate_slice "$DIFF_SLICE_DATABASE")
|
|
829
|
-
DIFF_SLICE_API=$(truncate_slice "$DIFF_SLICE_API")
|
|
830
|
-
DIFF_SLICE_INFRA=$(truncate_slice "$DIFF_SLICE_INFRA")
|
|
831
|
-
DIFF_SLICE_SCRAPER=$(truncate_slice "$DIFF_SLICE_SCRAPER")
|
|
832
|
-
```
|
|
833
|
-
For each dispatched persona, substitute `[DOMAIN_DIFF_SLICE]` → its matching `$DIFF_SLICE_*` variable (an empty slice falls back to the full diff, which is then truncated like every other slice). Also substitute `[AGENT_DOMAIN]` → the persona's marker domain (the `{domain}` used in its `FORGE:REVIEW-AGENT` marker), and keep `[PR_NUMBER]` / `[REVIEW_SHA_SHORT]` bound to the staging review's PR number and short head SHA (`staging` as the key when there is no PR number): 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.
|
|
753
|
+
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.
|
|
834
754
|
|
|
835
755
|
**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:
|
|
836
756
|
```bash
|
|
@@ -904,8 +824,10 @@ TEST_GATE_REASON="Phase 6.5 not yet run"
|
|
|
904
824
|
# ALL_PR_NUMBERS is the de-duplicated union of both scan methods (commit log + merge subjects)
|
|
905
825
|
GATE_OUTPUT=$(Skill("{FORGE_SKILL_PREFIX}test-gate", "--prs \"$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs)\" --base $DEFAULT_BRANCH"))
|
|
906
826
|
|
|
907
|
-
# Extract machine-readable verdict from Skill output
|
|
908
|
-
|
|
827
|
+
# Extract machine-readable verdict from Skill output. An absent/invalid marker stays EMPTY (never coerced to SKIP):
|
|
828
|
+
# the verdict-handling fence treats an empty verdict as missing/invalid and fails closed under blocking posture.
|
|
829
|
+
TEST_GATE_VERDICT=$(echo "$GATE_OUTPUT" | grep -oE 'FORGE:TEST_GATE:RESULT=(BLOCK|PASS|SKIP)' | sed 's/^FORGE:TEST_GATE:RESULT=//' | tail -1)
|
|
830
|
+
[ -n "$TEST_GATE_VERDICT" ] || TEST_GATE_REASON="gate result missing/invalid (no FORGE:TEST_GATE:RESULT marker in /test-gate output)"
|
|
909
831
|
|
|
910
832
|
echo "Test-gate verdict: ${TEST_GATE_VERDICT}"
|
|
911
833
|
# 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).
|
|
@@ -923,6 +845,18 @@ OVERRIDE_PHRASE=$(yq '.verification.test_gate.override_phrase // "OVERRIDE: ship
|
|
|
923
845
|
# Reload the Phase 6.5 gate result persisted by its producer (validated: only a known verdict reaches a gate decision).
|
|
924
846
|
[ -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"); }
|
|
925
847
|
[ -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); }
|
|
848
|
+
# Fail closed: a missing, empty, or unrecognised verdict is never a SKIP. Normalize it to BLOCK so the BLOCK arm applies
|
|
849
|
+
# the override check, advisory/blocking posture, and exit 1; GATE_VERDICT_INVALID makes the reasons say missing/invalid.
|
|
850
|
+
GATE_VERDICT_INVALID=0
|
|
851
|
+
case "${TEST_GATE_VERDICT:-}" in
|
|
852
|
+
BLOCK|PASS|SKIP) ;;
|
|
853
|
+
*)
|
|
854
|
+
GATE_VERDICT_INVALID=1
|
|
855
|
+
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
|
|
856
|
+
echo "⚠️ Test gate: gate result missing/invalid (${_GV_WHY}) — treating as BLOCK, not SKIP."
|
|
857
|
+
TEST_GATE_VERDICT="BLOCK"
|
|
858
|
+
;;
|
|
859
|
+
esac
|
|
926
860
|
case "$TEST_GATE_VERDICT" in
|
|
927
861
|
|
|
928
862
|
SKIP)
|
|
@@ -973,15 +907,18 @@ case "$TEST_GATE_VERDICT" in
|
|
|
973
907
|
echo " Proceeding with deploy. Override is logged in Phase 8 summary."
|
|
974
908
|
TEST_GATE_VERDICT="PASS"
|
|
975
909
|
TEST_GATE_REASON="BLOCK downgraded to PASS by override: ${OVERRIDE_REASON}"
|
|
910
|
+
[ "$GATE_VERDICT_INVALID" = 1 ] && TEST_GATE_REASON="BLOCK (gate result missing/invalid) downgraded to PASS by override: ${OVERRIDE_REASON}"
|
|
976
911
|
|
|
977
912
|
elif [ "$GATE_POSTURE" = "advisory" ]; then
|
|
978
913
|
echo "⚠️ Test gate: BLOCK (advisory posture) — runtime failures detected but deploy is NOT prevented."
|
|
979
914
|
echo " Switch verification.test_gate.posture to 'blocking' in forge.yaml to enforce this gate."
|
|
980
915
|
TEST_GATE_REASON="BLOCK (advisory) — runtime failures detected; deploy allowed by advisory posture"
|
|
916
|
+
[ "$GATE_VERDICT_INVALID" = 1 ] && TEST_GATE_REASON="BLOCK (advisory) — gate result missing/invalid; deploy allowed by advisory posture"
|
|
981
917
|
|
|
982
918
|
else
|
|
983
919
|
# blocking posture (default) — STOP
|
|
984
920
|
echo "⛔ DEPLOY BLOCKED — /test-gate returned BLOCK verdict."
|
|
921
|
+
[ "$GATE_VERDICT_INVALID" = 1 ] && echo " (gate result missing/invalid — failing closed; re-run /test-gate or post the override comment)"
|
|
985
922
|
echo ""
|
|
986
923
|
echo "Runtime failures were detected in the staging→${DEFAULT_BRANCH} bundle."
|
|
987
924
|
echo "The failures were batch-introduced (not pre-existing on ${DEFAULT_BRANCH} baseline)."
|
|
@@ -1006,7 +943,7 @@ case "$TEST_GATE_VERDICT" in
|
|
|
1006
943
|
|
|
1007
944
|
### What Happened
|
|
1008
945
|
|
|
1009
|
-
\`/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.
|
|
946
|
+
\`/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\`.)
|
|
1010
947
|
|
|
1011
948
|
\`/test-gate\` has filed \`test-failure\` issues for each failing cluster — check the issue tracker for details.
|
|
1012
949
|
|
|
@@ -1020,15 +957,17 @@ To override (ship known failures with documented reason), post a comment contain
|
|
|
1020
957
|
|
|
1021
958
|
<!-- FORGE:GATE_FAILURE:TYPE=test-gate|BUNDLE=$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs) -->" 2>/dev/null || true
|
|
1022
959
|
fi
|
|
1023
|
-
|
|
960
|
+
_GV_REASON="BLOCK (blocking posture, no override)"; [ "$GATE_VERDICT_INVALID" = 1 ] && _GV_REASON="BLOCK (gate result missing/invalid; blocking posture, no override)"
|
|
961
|
+
printf '%s\n' "$TEST_GATE_VERDICT" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "$_GV_REASON" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
1024
962
|
exit 1
|
|
1025
963
|
fi
|
|
1026
964
|
;;
|
|
1027
965
|
|
|
1028
966
|
*)
|
|
1029
|
-
|
|
1030
|
-
TEST_GATE_VERDICT
|
|
1031
|
-
|
|
967
|
+
# Unreachable (normalized above); fail closed regardless — never SKIP.
|
|
968
|
+
echo "⛔ Test gate: gate result missing/invalid ('${TEST_GATE_VERDICT}') — failing closed."
|
|
969
|
+
printf '%s\n' "BLOCK" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "BLOCK (gate result missing/invalid)" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
970
|
+
exit 1
|
|
1032
971
|
;;
|
|
1033
972
|
|
|
1034
973
|
esac
|
|
@@ -1122,292 +1061,74 @@ fi
|
|
|
1122
1061
|
|
|
1123
1062
|
An empty or unreadable launched-agent list is also a delivery failure (the fence exits 1), never evidence that nothing needed verifying; recover the list from the PR comments and rerun. Missing comments are an explicit failure, never evidence of no findings. Do not triage or synthesize a deploy verdict while any launched agent is unaccounted for.
|
|
1124
1063
|
|
|
1125
|
-
### 7A: Extract
|
|
1126
|
-
Only findings inside trusted, current-head reviewer comments count: the head SHA is public, so an untrusted commenter must never be able to inject `FINDING` markers that get filed as issues. Read every page of comments, fail closed when the trust script or comments are unreadable (never fall through to "no findings").
|
|
1127
|
-
|
|
1128
|
-
```bash
|
|
1129
|
-
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() { :; }; }
|
|
1130
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1131
|
-
# 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
|
|
1132
|
-
# (an empty SHA would make the Reviewed-SHA match accept any head).
|
|
1133
|
-
[ -n "${REVIEW_SHA_FULL:-}" ] || REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
1134
|
-
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
|
|
1135
|
-
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() { :; }; }
|
|
1136
|
-
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
1137
|
-
[ -n "$REVIEW_SHA_FULL" ] || { echo "STAGING_REVIEW: BLOCKED - head SHA is not a commit id; refusing to extract findings (fail closed)"; exit 1; }
|
|
1138
|
-
COMMENTS_JSON=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1139
|
-
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
1140
|
-
echo "STAGING_REVIEW: BLOCKED - scripts/trusted-comments.sh unresolvable or PR comments unreadable; refusing to extract findings (fail closed)"; exit 1
|
|
1141
|
-
fi
|
|
1142
|
-
AGENT_RE="^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)"
|
|
1143
|
-
AGENT_BODIES=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies "$AGENT_RE" 2>/dev/null) \
|
|
1144
|
-
|| { echo "STAGING_REVIEW: BLOCKED - trusted reviewer comments unreadable (fail closed)"; exit 1; }
|
|
1145
|
-
ALL_FINDINGS=$(printf '%s\n' "$AGENT_BODIES" | jq -r 'scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null)
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
Extract structured findings (`<!-- FINDING:... -->`) from `ALL_FINDINGS`. If none found, scan the trusted bodies for unstructured findings. If still 0 → skip to Phase 8.
|
|
1149
|
-
|
|
1150
|
-
### 7B: Filter & Deduplicate
|
|
1151
|
-
Keep ALL findings (CONFIRMED/LIKELY/POSSIBLE) for the 7B.6 provenance gates and then classification (7B.5). Deduplicate within this run by file + pattern + location: same file, location within +-5 lines, and 3+ shared title keywords (a shared explicit finding id is an additional match, never a substitute for the keyword test) are one finding (keep higher confidence). When `SCOPE_MODE=scoped`, a finding is `out_of_scope` only when it cites at least one file and EVERY cited file is outside `CROSS_PR_FILES`, `UNREVIEWED_FILES` and `DEPLOY_WIRING_FILES` (0B.1; a finding with any in-scope file, such as a cross-file interaction, stays in scope), and the filter may only DEMOTE it, never drop it: only a finding whose normalised severity is `MEDIUM`, `LOW` or `INFO` becomes a listed note. `CRITICAL` and `HIGH` (any confidence), and any missing or unrecognised severity, are always kept and go to 7B.5 as usual, so scope can never remove a blocking finding. Every demoted finding is listed (id, file:line, reason `out_of_scope`) in the 7B.5 disposition; none is silently dropped. Findings with no file (readiness-checklist items such as undocumented env vars or a migration-number collision across PRs) are never demoted. The executable rule is the two `SCOPE_RULE` lines inside `classify_one` in 7B.5 (`scripts/review-pr-staging-scope.test.sh` evaluates them).
|
|
1152
|
-
|
|
1153
|
-
Sort: CONFIRMED first, then by severity.
|
|
1154
|
-
|
|
1155
|
-
### 7B.6: Provenance and concreteness gates (forge#3452)
|
|
1156
|
-
|
|
1157
|
-
Before filing, apply the three gates defined in `commands/review-pr.md` §6B.4 (head-SHA, introduced-line, failure scenario), in that order, to every finding — do not duplicate the algorithm here. Use the staging diff (`git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH`, or the reviewed PR's diff) as the introduced-line source and the full 40-hex head SHA (`git rev-parse origin/$STAGING_BRANCH`, never an abbreviated one) as the reviewed SHA. Stale findings are dropped (`findings_dropped_stale`), findings outside the diff are `pre-existing` (`findings_preexisting`: listed in the summary, MEDIUM or lower only, since CRITICAL/HIGH, any confidence, are never routed as pre-existing and a file with no `patch` in the diff API is UNKNOWN and kept; filed only when safety-domain with the `pre-existing` label and never `review-finding`, so they are excluded from findings-per-PR and the amplification ratio), and findings without a `**Failure scenario**` are demoted to notes (`notes_demoted_no_scenario`) unless CRITICAL/HIGH, any confidence. Unreadable inputs keep the finding. Report the three counts in the staging review summary using the same names, and record every demoted or listed note in a `FORGE:NOTE_DISPOSITION` record in the same format as `commands/review-pr.md` §6B.5.
|
|
1064
|
+
### 7A: Extract, dedupe and gate (with the note-disposition dry run)
|
|
1158
1065
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
Classify every deduped finding with `scripts/classify-finding.sh` before filing, exactly as `commands/review-pr.md` §6B.5 does (the rules live in the script, not here: HIGH/CRITICAL always ISSUE, content-based safety exemption where LOW needs a keyword AND CONFIRMED and a domain agent rescues MEDIUM only, otherwise LOW/POSSIBLE become NOTEs). Resolve it with the same `forge_resolve` stub. There is no `$PWD` tier: the repo under review is author-controlled. The staging PR is not a finding fix, so `FINDING_LINEAGE=none`.
|
|
1162
|
-
|
|
1163
|
-
**Cross-PR re-flag** <!-- Added: forge#3641 -->: the 7B.6 gates run first, so re-flag counts cover only findings that survived them. A finding that a bundle member PR's review already posted, or that a member PR's `FORGE:NOTE_DISPOSITION` list recorded, is not filed or noted again. The fence builds a prior-findings TSV (file, line-start, line-end, title, prior-disposition, issue#) from each member PR in `ALL_PR_NUMBERS`, reading every comment through `scripts/trusted-comments.sh` (never an untrusted author), and passes it to the classifier as `--prior-findings`. A finding re-flags a row when it is the same file AND (line IoU >= 0.6 AND >= 1 shared non-stopword title token, OR >= 3 shared title tokens of which >= 2 are non-stopword; generic stopwords never count as topical similarity); the classifier answers `NOTE reflag-filed #N` (the prior issue is still open) or `NOTE reflag-note`, never a new ISSUE. HIGH/CRITICAL are never demoted (they keep blocking via §7B), and a row whose linked issue is closed is ignored so a regressed defect is classified normally. Reviewer-body rows whose issue linkage fails are passed as `unknown` (recorded NOTE_DISPOSITION rows as `note`); an `unknown` row never demotes a MEDIUM CONFIRMED/LIKELY re-flag. Re-flag NOTEs are listed with the other NOTEs and counted in `notes_reflag` only. An unreadable member PR only shrinks the TSV; it never blocks.
|
|
1066
|
+
`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).
|
|
1164
1067
|
|
|
1165
1068
|
```bash
|
|
1166
1069
|
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() { :; }; }
|
|
1167
|
-
CLASSIFY_SCRIPT=$(forge_resolve classify-finding.sh)
|
|
1168
|
-
# Fail closed (#3639): no hand classification and no ISSUE fallback. Kept OUTSIDE classify_one (the scope test sources that function with a stubbed CLASSIFY_SCRIPT).
|
|
1169
|
-
if [ -z "$CLASSIFY_SCRIPT" ]; then
|
|
1170
|
-
echo "REVIEW_RESULT: status: BLOCKED, blocker: classifier unavailable (classify-finding.sh not resolvable through the trusted resolver)"
|
|
1171
|
-
exit 0
|
|
1172
|
-
fi
|
|
1173
|
-
# Per-finding inputs come from each structured block: FINDING_ID, FINDING_SEVERITY, FINDING_CONFIDENCE, FINDING_AGENT, FINDING_TITLE, FINDING_BODY,
|
|
1174
|
-
# FINDING_FILE (primary) and FINDING_PATHS (newline-separated, every cited file). Run THIS WHOLE BLOCK in ONE Bash call, looping over every deduped finding
|
|
1175
|
-
# (init once, then classify_one per finding, then the disposition record), because counters and the note list live in shell state and a fresh shell loses them.
|
|
1176
|
-
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() { :; }; }
|
|
1177
1070
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1071
|
+
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
|
|
1072
|
+
RF=$(forge_resolve review-finalize.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed), never a skipped gate
|
|
1073
|
+
[ -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
|
|
1074
|
+
OUT_DIR="$_SD/review-finalize-${PR_NUMBER}"
|
|
1182
1075
|
# 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.
|
|
1183
1076
|
SCOPE_MODE=full; CROSS_PR_FILES=""; UNREVIEWED_FILES=""; DEPLOY_WIRING_FILES=""
|
|
1184
1077
|
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
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
#
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
# from an earlier run of this same review, must never swallow its findings).
|
|
1195
|
-
pf_split_loc() { # PF_LOC="file[:N[-M]]" -> file<TAB>N<TAB>M (`-` sentinel when a line column is absent: a tab IFS read collapses empty fields)
|
|
1196
|
-
local _loc="$PF_LOC" _file _r _a="" _b=""; _file="$_loc"
|
|
1197
|
-
case "$_loc" in *:[0-9]*) _r="${_loc##*:}"; case "$_r" in *[!0-9-]*|"") ;; *) _file="${_loc%:*}"; _a="${_r%%-*}"; _b="${_r#*-}"; [ "$_b" != "$_r" ] || _b="$_a" ;; esac ;; esac
|
|
1198
|
-
printf '%s\t%s\t%s\n' "$_file" "${_a:--}" "${_b:--}"
|
|
1199
|
-
}
|
|
1200
|
-
prior_findings_tsv() {
|
|
1201
|
-
local pr cj rows issues _id _c _s _loc _t _p _hit _f _a _b _src _dflt
|
|
1202
|
-
[ -n "${TRUSTED_SCRIPT:-}" ] || return 0
|
|
1203
|
-
rows=$(mktemp "${_pscr}/prior-rows.XXXXXX"); issues=$(mktemp "${_pscr}/prior-issues.XXXXXX")
|
|
1204
|
-
for pr in $PF_PRS; do
|
|
1205
|
-
case "$pr" in ''|*[!0-9]*) continue ;; esac
|
|
1206
|
-
cj=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" 2>/dev/null) || continue
|
|
1207
|
-
[ -n "$cj" ] || continue
|
|
1208
|
-
printf '%s' "$cj" | bash "$TRUSTED_SCRIPT" bodies '^<!-- (FORGE:REVIEW-AGENT:[a-z-]+|REVIEW-FINDINGS-SYNTHESIZED-START) -->[\s\S]*(^|\n)Reviewed-SHA: [0-9a-f]{40,64}' 2>/dev/null \
|
|
1209
|
-
| jq -r --arg skip "${PF_SKIP:-}" 'select($skip == "" or (test("(^|\\n)Reviewed-SHA: " + $skip + "\\r?(\\n|$)") | not)) | scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null \
|
|
1210
|
-
| while IFS='|' read -r _id _c _s _loc _t; do
|
|
1211
|
-
[ -n "$_t" ] || continue
|
|
1212
|
-
_p=$(PF_LOC="$_loc" pf_split_loc); printf 'F\t%s\t%s\n' "$_p" "$(printf '%s' "$_t" | tr '\t' ' ')"
|
|
1213
|
-
done >> "$rows"
|
|
1214
|
-
printf '%s' "$cj" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:NOTE_DISPOSITION' 2>/dev/null \
|
|
1215
|
-
| jq -r --arg skip "${PF_SKIP:-}" 'select($skip == "" or ((capture("^<!-- FORGE:NOTE_DISPOSITION:[^>]*reviewed_sha=(?<s>[0-9a-f]{40,64})") | .s) as $s | $s != $skip)) | split("\n")[] | select(startswith("- "))' 2>/dev/null \
|
|
1216
|
-
| sed -n 's/^- [^ ]* \([^ ]*\) \[[A-Za-z?]*\] \(.*\): [^:]*$/\1\t\2/p' \
|
|
1217
|
-
| while IFS="$(printf '\t')" read -r _loc _t; do
|
|
1218
|
-
[ "$_loc" != "?" ] || continue
|
|
1219
|
-
_p=$(PF_LOC="$_loc" pf_split_loc); printf 'N\t%s\t%s\n' "$_p" "$_t"
|
|
1220
|
-
done >> "$rows"
|
|
1221
|
-
gh issue list -R "$REPO" --label review-finding --state all --search "${pr} in:body" --limit 100 --json number,state,title,body 2>/dev/null \
|
|
1222
|
-
| jq -c '.[]' >> "$issues" 2>/dev/null || true
|
|
1223
|
-
done
|
|
1224
|
-
# Linkage: a filed issue whose body names the file and whose title shares >= 3 tokens with the finding (open preferred).
|
|
1225
|
-
# Rows carry a leading source column (F = reviewer FINDING marker, N = NOTE_DISPOSITION record), stripped on output. An unlinked F row may
|
|
1226
|
-
# have been filed under a reworded title, so it is `unknown` (never demotes MEDIUM CONFIRMED/LIKELY); an unlinked N row is a true `note`.
|
|
1227
|
-
sort -u "$rows" | while IFS="$(printf '\t')" read -r _src _f _a _b _t; do
|
|
1228
|
-
[ -n "$_f" ] || continue
|
|
1229
|
-
[ "$_a" != "-" ] || _a=""; [ "$_b" != "-" ] || _b=""
|
|
1230
|
-
case "$_src" in N) _dflt=note ;; *) _dflt=unknown ;; esac
|
|
1231
|
-
_hit=$(jq -rs --arg f "$_f" --arg t "$_t" 'def toks: ascii_downcase | [scan("[a-z0-9]{3,}")] | unique;
|
|
1232
|
-
($t | toks) as $a | [ .[] | select((.body // "") | contains($f))
|
|
1233
|
-
| select((((.title // "") | toks) as $b | [$a[] | select(. as $x | $b | index($x))] | length) >= 3)
|
|
1234
|
-
| {n: .number, s: (.state | ascii_downcase)} ]
|
|
1235
|
-
| (map(select(.s == "open")) + map(select(.s != "open"))) | .[0] // empty | "\(.s)\t\(.n)"' "$issues" 2>/dev/null) || _hit=""
|
|
1236
|
-
case "$_hit" in
|
|
1237
|
-
open*) printf '%s\t%s\t%s\t%s\tfiled\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
1238
|
-
closed*) printf '%s\t%s\t%s\t%s\tfiled-closed\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
1239
|
-
*) printf '%s\t%s\t%s\t%s\t%s\t\n' "$_f" "$_a" "$_b" "$_t" "$_dflt" ;;
|
|
1240
|
-
esac
|
|
1241
|
-
done
|
|
1242
|
-
rm -f "$rows" "$issues"
|
|
1243
|
-
}
|
|
1244
|
-
PRIOR_ROWS=$(PF_SKIP="" PF_PRS="$ALL_PR_NUMBERS" prior_findings_tsv)
|
|
1245
|
-
if [ -n "$PRIOR_ROWS" ]; then PRIOR_FILE="$_SD/${PR_NUMBER}_prior-findings.tsv"; printf '%s\n' "$PRIOR_ROWS" > "$PRIOR_FILE"; fi
|
|
1246
|
-
classify_one() {
|
|
1247
|
-
# FINDING_TEXT_FILE holds the finding text: the classifier's content-based safety exemption reads it, so an empty file silently disables the exemption.
|
|
1248
|
-
local FINDING_TEXT_FILE; FINDING_TEXT_FILE=$(mktemp "$_SD/staging-finding.XXXXXX")
|
|
1249
|
-
printf '%s\n%s\n%s\n' "${FINDING_TITLE:-}" "${FINDING_BODY:-}" "${FINDING_PATHS:-${FINDING_FILE:-}}" > "$FINDING_TEXT_FILE"
|
|
1250
|
-
DISPOSITION=""
|
|
1251
|
-
if [ -n "$CLASSIFY_SCRIPT" ]; then
|
|
1252
|
-
DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
|
|
1253
|
-
--agent "$FINDING_AGENT" --lineage none --text-file "$FINDING_TEXT_FILE" \
|
|
1254
|
-
${PRIOR_FILE:+--prior-findings "$PRIOR_FILE" --title "${FINDING_TITLE:-}" --file "${FINDING_FILE:-}"}) || DISPOSITION=""
|
|
1255
|
-
fi
|
|
1256
|
-
rm -f "$FINDING_TEXT_FILE"
|
|
1257
|
-
# Fail toward filing, never drop: a classifier runtime failure or garbled result on one finding is ISSUE (the classifier's own convention); an unresolvable classifier never reaches here (guard above). INPR_FIX is never emitted (no --inpr-diff on a bundle).
|
|
1258
|
-
case "$DISPOSITION" in ISSUE*|NOTE*) ;; *) DISPOSITION="ISSUE classifier-error" ;; esac
|
|
1259
|
-
# 7B scope rule (the two SCOPE_RULE lines are executed here and evaluated by scripts/review-pr-staging-scope.test.sh):
|
|
1260
|
-
SEV=$(printf '%s' "$FINDING_SEVERITY" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]') # SCOPE_RULE
|
|
1261
|
-
case "$SEV" in MEDIUM|LOW|INFO) OOS_DEMOTE=1 ;; *) OOS_DEMOTE=0 ;; esac # SCOPE_RULE
|
|
1262
|
-
# Demote ONLY when scoped, the rule allows it, the classifier did not apply its safety exemption, and EVERY cited file is out of scope.
|
|
1263
|
-
# A finding with no file, or with any file in scope, is never demoted. The demotion is recorded as a listed note, never dropped.
|
|
1264
|
-
case "$DISPOSITION" in *safety-exemption*|"NOTE reflag"*) OOS_DEMOTE=0 ;; esac # a re-flag is already a recorded NOTE: never double-count it as out_of_scope
|
|
1265
|
-
if [ "$SCOPE_MODE" = "scoped" ] && [ "$OOS_DEMOTE" = "1" ] && [ -n "${FINDING_PATHS:-${FINDING_FILE:-}}" ]; then
|
|
1266
|
-
local _ANY_IN=false _f
|
|
1267
|
-
while IFS= read -r _f; do
|
|
1268
|
-
[ -n "$_f" ] || continue
|
|
1269
|
-
printf '%s\n' "$CROSS_PR_FILES" "$UNREVIEWED_FILES" "$DEPLOY_WIRING_FILES" | grep -qxF -- "$_f" && _ANY_IN=true
|
|
1270
|
-
done <<< "${FINDING_PATHS:-$FINDING_FILE}"
|
|
1271
|
-
if [ "$_ANY_IN" = "false" ]; then DISPOSITION="NOTE out_of_scope"; OUT_OF_SCOPE=$((OUT_OF_SCOPE+1)); fi
|
|
1272
|
-
fi
|
|
1273
|
-
case "$DISPOSITION" in
|
|
1274
|
-
"NOTE reflag"*) NOTES_REFLAG=$((NOTES_REFLAG+1)); printf '%s\n' "- ${FINDING_ID:-?} ${FINDING_FILE:-?} [${SEV:-?}] ${FINDING_TITLE:-}: ${DISPOSITION}" >> "$NOTE_LIST_FILE" ;; # counted in notes_reflag only
|
|
1275
|
-
NOTE*) NOTES_LISTED=$((NOTES_LISTED+1)); printf '%s\n' "- ${FINDING_ID:-?} ${FINDING_FILE:-?} [${SEV:-?}] ${FINDING_TITLE:-}: ${DISPOSITION}" >> "$NOTE_LIST_FILE" ;;
|
|
1276
|
-
*) FINDINGS_FILED=$((FINDINGS_FILED+1)) ;; # classified ISSUE; 7E dedup may still skip creation
|
|
1277
|
-
esac
|
|
1278
|
-
}
|
|
1279
|
-
# for each deduped finding: set the FINDING_* inputs, call classify_one, then use $DISPOSITION (ISSUE continues to 7E/7F).
|
|
1078
|
+
SCOPE_FILE="$_SD/${PR_NUMBER}_scope-files.txt"; rm -f "$SCOPE_FILE"
|
|
1079
|
+
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
|
|
1080
|
+
PRIOR_PRS=$(printf '%s ' $ALL_PR_NUMBERS)
|
|
1081
|
+
bash "$RF" extract --pr "$PR_NUMBER" --repo "$GH_REPO" --context "$CTX" --out-dir "$OUT_DIR"; RC=$?
|
|
1082
|
+
[ "$RC" -eq 0 ] || exit 1 # STOP: REVIEW_RESULT: status: BLOCKED was printed
|
|
1083
|
+
# note-disposition dry run (posts nothing): classifies every kept finding and prints the NOTE list
|
|
1084
|
+
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=$?
|
|
1085
|
+
[ "$RC" -eq 0 ] || exit 1 # STOP
|
|
1086
|
+
cat "$OUT_DIR/note-list.txt" 2>/dev/null
|
|
1280
1087
|
```
|
|
1281
1088
|
|
|
1282
|
-
|
|
1089
|
+
Read `FINDINGS_KEPT`, `FINDINGS_DROPPED_STALE`, `FINDINGS_PREEXISTING` and `NOTES_DEMOTED_NO_SCENARIO` for the summary. With no kept finding and no unstructured finding in the trusted reviewer bodies, skip to Phase 8.
|
|
1283
1090
|
|
|
1284
|
-
|
|
1091
|
+
### 7B: Note disposition (same damper and classifier as `/review-pr` §6B.5)
|
|
1285
1092
|
|
|
1286
|
-
|
|
1287
|
-
CLASSIFIER_MODE=script # the 7B.5 guard blocks the review when the classifier is unresolvable, so this is always script
|
|
1288
|
-
# This is a separate Bash call: shell state is lost. Re-derive PR_NUMBER and GH_REPO; counters the caller did not carry over are recomputed from the note list file (NOTES_LISTED) or flagged.
|
|
1289
|
-
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() { :; }; }
|
|
1290
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1291
|
-
if [ -z "${PR_NUMBER:-}" ]; then
|
|
1292
|
-
echo "REVIEW_RESULT: status: BLOCKED, blocker: PR_NUMBER unavailable; FORGE:NOTE_DISPOSITION not posted (record lost)"
|
|
1293
|
-
else
|
|
1294
|
-
NOTE_LIST_FILE="${NOTE_LIST_FILE:-$_SD/${PR_NUMBER}_notes.md}"
|
|
1295
|
-
[ -n "${NOTES_LISTED:-}" ] || { NOTES_LISTED=$(grep -c '^- ' "$NOTE_LIST_FILE" 2>/dev/null || true); echo "WARNING: classification counters lost (fresh shell): notes_listed recomputed from $NOTE_LIST_FILE; set SCOPE_MODE, FINDINGS_FILED, OUT_OF_SCOPE and NOTES_REFLAG from the 7B.5 block output before posting"; }
|
|
1296
|
-
[ -n "${SCOPE_MODE:-}" ] || { SCOPE_MODE=full; echo "WARNING: SCOPE_MODE lost (fresh shell): recording scope=full"; }
|
|
1297
|
-
_DISP_BODY="$_SD/${PR_NUMBER}_note_disposition.md"
|
|
1298
|
-
{ printf '%s\n' "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=0 notes_listed=${NOTES_LISTED:-0} notes_dropped=${NOTES_DROPPED:-0} findings_filed=${FINDINGS_FILED:-0} out_of_scope=${OUT_OF_SCOPE:-0} notes_reflag=${NOTES_REFLAG:-0} lineage=none classifier=${CLASSIFIER_MODE} scope=${SCOPE_MODE} -->"; cat "$NOTE_LIST_FILE" 2>/dev/null; } > "$_DISP_BODY"
|
|
1299
|
-
gh pr comment "$PR_NUMBER" -R "$GH_REPO" --body-file "$_DISP_BODY" # allowlist:check-command-side-effects
|
|
1300
|
-
fi
|
|
1301
|
-
```
|
|
1302
|
-
|
|
1303
|
-
### 7C: Ensure Labels
|
|
1304
|
-
```bash
|
|
1305
|
-
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() { :; }; }
|
|
1306
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1307
|
-
# Colors match the canonical ForgeDock label manifest (bin/labels.json).
|
|
1308
|
-
# Run `npx forgedock labels setup` to bootstrap all managed labels at once.
|
|
1309
|
-
gh label create "review-finding" --color "D93F0B" --description "Defect or improvement found during automated PR review. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null
|
|
1310
|
-
gh label create "needs-validation" --color "FBCA04" --description "Review finding awaiting human validation. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null
|
|
1311
|
-
gh label create "pre-existing" --color "BFD4F2" --description "Defect noticed during PR review that the PR did not introduce. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null
|
|
1312
|
-
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
|
|
1313
|
-
```
|
|
1314
|
-
|
|
1315
|
-
### 7D: Milestone & Code Branch Detection
|
|
1316
|
-
|
|
1317
|
-
Derive both `CODE_BRANCH` and `MILESTONE_FLAG` from the reviewed PR before creating any findings. Unlike `commands/review-pr.md` (feature-lane PRs with short-lived HEAD branches that get deleted post-merge — see the rationale comment on that spec's finding template), `review-pr-staging.md` reviews long-lived branches (`staging`, `milestone/*`) that persist after merge, so `CODE_BRANCH` uses `headRefName` (the PR's HEAD — the branch actually being reviewed), consistent with forge#1391's original fix. This replaces the previous hardcoded `staging` literal, which was silently wrong whenever this spec reviewed a `milestone/X → staging` PR instead of a `staging → main` PR — `CODE_BRANCH` used to always say `staging` even when the reviewed branch was `milestone/X`.
|
|
1318
|
-
|
|
1319
|
-
```bash
|
|
1320
|
-
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() { :; }; }
|
|
1321
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1322
|
-
CODE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefName --jq '.headRefName')
|
|
1323
|
-
MILESTONE_FLAG=""
|
|
1324
|
-
|
|
1325
|
-
# scripts/derive-finding-milestone.sh is the single source of truth for
|
|
1326
|
-
# milestone derivation — commands/review-pr.md and commands/review-pr-staging.md
|
|
1327
|
-
# both call it identically so the two specs cannot independently drift.
|
|
1328
|
-
# <!-- forge#2443 -->
|
|
1329
|
-
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() { :; }; }
|
|
1330
|
-
DERIVE_MILESTONE_SCRIPT=$(forge_resolve derive-finding-milestone.sh)
|
|
1331
|
-
# Unresolved helper: empty milestone (non-blocking), never a cwd-relative fallback.
|
|
1332
|
-
MILESTONE_TITLE=""
|
|
1333
|
-
[ -n "$DERIVE_MILESTONE_SCRIPT" ] && MILESTONE_TITLE=$(bash "$DERIVE_MILESTONE_SCRIPT" "${PR_NUMBER}" -R "$GH_REPO")
|
|
1334
|
-
# Quoted so multi-word milestone titles survive as a single value wherever
|
|
1335
|
-
# MILESTONE_FLAG is later interpolated — matches the quoting convention already
|
|
1336
|
-
# used for --title/--body-file/--label below.
|
|
1337
|
-
[ -n "$MILESTONE_TITLE" ] && MILESTONE_FLAG="--milestone \"$MILESTONE_TITLE\""
|
|
1338
|
-
```
|
|
1339
|
-
|
|
1340
|
-
Plain staging→main reviews resolve `CODE_BRANCH` to `staging` dynamically (same value as before, now derived rather than hardcoded) and typically resolve no milestone. A milestone→staging review resolves `CODE_BRANCH` to `milestone/X` and (via the shared script) the matching milestone.
|
|
1093
|
+
`classify` runs `scripts/classify-finding.sh` (never hand-classify) on every finding that survived 7A, then `scripts/cap-findings.sh`; the rules live there (HIGH/CRITICAL always ISSUE; a content-based safety exemption; otherwise LOW/POSSIBLE become NOTEs). The staging PR is not a finding fix, so `lineage=none`. **Scope**: with `SCOPE_MODE=scoped` a MEDIUM, LOW or INFO finding whose cited files are all outside the 0B.1 set becomes `NOTE out_of_scope` (listed, never dropped; never HIGH/CRITICAL, never a safety-exempt or re-flagged finding, never a finding that cites no file). **Cross-PR re-flag** (`--prior-prs`): a finding a bundle member PR's review or `FORGE:NOTE_DISPOSITION` list already covered becomes `NOTE reflag-*` and files nothing (never for HIGH/CRITICAL). **Per-review cap**: at most `review.max_filed_findings` ISSUEs (default 2), the overflow is `NOTE capped`. Each NOTE gets one disposition: list it in the PR body under `## Non-blocking notes` (edit, never replace) or drop a duplicate or nit; NOTEs never reach the filing call. The 7C fence runs `classify` for real, which posts the `FORGE:NOTE_DISPOSITION` record (counts, `out_of_scope=`, `scope=`, then the NOTE list, dropped NOTEs included) whenever findings were extracted. Read `FINDINGS_FILED`, `NOTES_LISTED`, `NOTES_CAPPED`, `NOTES_REFLAG` and `NOTE_DISPOSITION_POSTED`.
|
|
1341
1094
|
|
|
1342
|
-
###
|
|
1095
|
+
### 7C: Prepare and create issues
|
|
1343
1096
|
|
|
1344
|
-
|
|
1097
|
+
`file-prepare` does every pre-filing step for the ISSUE findings: managed labels, the milestone (`scripts/derive-finding-milestone.sh`), the title dedup (`scripts/issue-dedup.sh`) and the line-range check against open `review-finding` issues, a regression flag when a closed `review-finding` sits at the same location (elevated one priority level), `priority:*` from `**Severity**` only via `scripts/severity-to-priority.sh` (never from Confidence), and the pattern recurrence check. A dedup helper or lookup failure never suppresses a finding. Staging reviews long-lived branches (`staging`, `milestone/*`), so the code branch is the PR's `headRefName`, not a short-lived head.
|
|
1345
1098
|
|
|
1346
1099
|
```bash
|
|
1347
1100
|
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() { :; }; }
|
|
1348
1101
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1349
|
-
|
|
1350
|
-
#
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
#
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
1366
|
-
_DUP_N=$(printf '%s' "$DEDUP_OUT" | sed -n 's/^DUPLICATE: #\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1367
|
-
_DUP_BODY=$(gh issue view "${_DUP_N:-0}" -R "$GH_REPO" --json body --jq '.body' 2>/dev/null) || _DUP_BODY=""
|
|
1368
|
-
printf '%s' "$_DUP_BODY" | grep -qF -- "$_SAFE_FILE" || DEDUP_EXIT=0
|
|
1369
|
-
fi
|
|
1370
|
-
# Closed issues that cite this file at a line within +-5 of the finding. FINDING_FILE is finding content: strip quotes/backslashes so it cannot add search qualifiers.
|
|
1371
|
-
_FILE_RE=$(printf '%s' "$_SAFE_FILE" | sed 's/[][\.^$*+?(){}|\/]/\\&/g')
|
|
1372
|
-
REGRESSION_CHECK=ok; CLOSED_HIT=""
|
|
1373
|
-
# Capture the gh read first and test its own status: through a pipe a failed read looks like "no closed hit".
|
|
1374
|
-
_CL=$(gh issue list -R "$GH_REPO" --label review-finding --state closed --search "\"${_SAFE_FILE}\" in:body" --json number,title,body,stateReason --limit 100 2>/dev/null) || REGRESSION_CHECK=unavailable
|
|
1375
|
-
# FINDING_LINE may be a range such as 12-15: use its first integer, and 0 when there is none.
|
|
1376
|
-
_L=$(printf '%s' "${FINDING_LINE:-0}" | grep -oE '[0-9]+' | head -1); _L="${_L:-0}"
|
|
1377
|
-
_HITS=""; [ "$REGRESSION_CHECK" = "ok" ] && { _HITS=$(printf '%s' "$_CL" | jq -r --arg re "$_FILE_RE" --argjson l "$_L" '.[] | select((.body // "") != "") | select(.stateReason != "NOT_PLANNED")
|
|
1378
|
-
| select([.body | scan($re + "[:#L]+([0-9]+)") | .[0] | tonumber | select((. - $l) <= 5 and ($l - .) <= 5)] | length > 0) | "#\(.number) \(.title)"') || REGRESSION_CHECK=unavailable; }
|
|
1379
|
-
CLOSED_HIT=$(printf '%s\n' "$_HITS" | sed '/^$/d' | head -3)
|
|
1380
|
-
# Confirm the 3+ shared title keywords against each hit before treating it as a regression.
|
|
1102
|
+
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
|
|
1103
|
+
RF=$(forge_resolve review-finalize.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed), never a skipped gate
|
|
1104
|
+
[ -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
|
|
1105
|
+
OUT_DIR="$_SD/review-finalize-${PR_NUMBER}"
|
|
1106
|
+
SCOPE_FILE="$_SD/${PR_NUMBER}_scope-files.txt"; [ -r "$SCOPE_FILE" ] || SCOPE_FILE="" # written by 7A only for a scoped review
|
|
1107
|
+
PRIOR_PRS=$(printf '%s ' $ALL_PR_NUMBERS)
|
|
1108
|
+
# 7B for real: posts FORGE:NOTE_DISPOSITION with the caller's NOTE counts.
|
|
1109
|
+
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" \
|
|
1110
|
+
--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
|
|
1111
|
+
[ "$RC" -eq 0 ] || exit 1 # STOP: REVIEW_RESULT: status: BLOCKED was printed
|
|
1112
|
+
PATTERNS_FILE="$OUT_DIR/patterns.json"; [ -s "$PATTERNS_FILE" ] || PATTERNS_FILE=""
|
|
1113
|
+
bash "$RF" file-prepare --pr "$PR_NUMBER" --repo "$GH_REPO" --context "$CTX" --out-dir "$OUT_DIR" ${PATTERNS_FILE:+--patterns "$PATTERNS_FILE"}; RC=$?
|
|
1114
|
+
[ "$RC" -eq 0 ] || exit 1 # STOP
|
|
1115
|
+
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
|
|
1116
|
+
echo "CODE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefName --jq '.headRefName')"
|
|
1117
|
+
jq -c '.items[] | select(.action == "file")' "$OUT_DIR/worklist.json"
|
|
1381
1118
|
```
|
|
1382
1119
|
|
|
1383
|
-
|
|
1384
|
-
- `REGRESSION_CHECK=unavailable` (the closed-issue read or its filter failed): record `regression-check-unavailable` in the Phase 8 summary; it never suppresses filing.
|
|
1385
|
-
- `CLOSED_HIT` non-empty (closed issue, not closed as not-planned, at the same file with a location within +-5 lines and 3+ shared title keywords): potential regression: file it (it is not a duplicate of a fixed defect), elevate its priority one level, link the closed issue, and record it as `regression: #N`. A closed hit never suppresses a finding.
|
|
1386
|
-
- Record every deduped item (finding id, matched issue) for the Phase 8 summary and audit.
|
|
1120
|
+
Each printed line is one item to file (`skip-*` items are covered or unfilable: record them in the Phase 8 summary as `deduped: #N`, `regression: #N` or `dedup-unavailable`). An item carries `title`, `labels` (with `priority:*`), `milestone`, `batchable`, `regression_of`, `file`, `line`, `severity`, `agent` and `pattern`; pattern consolidation follows `commands/review-pr.md` §6C. **For each item to file**, write the issue body, then create it through the `/issue` programmatic invocation contract (`commands/issue.md` § "Programmatic Invocation Contract"), never the raw issue-creation command:
|
|
1387
1121
|
|
|
1388
|
-
### 7F: Create Issues
|
|
1389
|
-
Sequential creation. Title: `Staging Review: {summary} (staging → main)`. Labels: review-finding, needs-validation, staging-review, priority:P0-P3 (derived from Severity — see below). Body includes: source branch context (`${CODE_BRANCH}`, derived in Phase 7D — not a hardcoded `staging` literal), code context, evidence, validation checklist.
|
|
1390
|
-
|
|
1391
|
-
**For each finding** (classified `ISSUE` in 7B.5 and not deduped in 7E), create issue through the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") instead of calling the raw issue-creation command directly:
|
|
1392
1122
|
```bash
|
|
1393
1123
|
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() { :; }; }
|
|
1394
1124
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1395
|
-
|
|
1396
|
-
STAGING_FINDING_TITLE="${STAGING_FINDING_TITLE:-chore: [summary] (staging review — PR #${PR_NUMBER})}"
|
|
1397
|
-
# Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
|
|
1398
|
-
# an xargs-based tokenizer that never expands backtick/$(...) substitution, so
|
|
1399
|
-
# this is no longer required for safety — but strip it anyway so the raw title
|
|
1400
|
-
# stays readable if it round-trips through any other eval-based consumer.
|
|
1401
|
-
STAGING_FINDING_TITLE=$(printf '%s' "$STAGING_FINDING_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
|
|
1402
|
-
SCRATCHPAD="${SCRATCHPAD:-${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}}"
|
|
1403
|
-
REVIEW_AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-reviewer}-$$}"
|
|
1404
|
-
mkdir -p "$SCRATCHPAD"
|
|
1125
|
+
SCRATCHPAD="$_SD"; REVIEW_AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-reviewer}-$$}"
|
|
1405
1126
|
STAGING_FINDING_BODY_MARKER="FORGE:BODY-INTEGRITY:${PR_NUMBER}_staging-review_${REVIEW_AGENT_TOKEN}"
|
|
1406
1127
|
STAGING_FINDING_BODY_FILE=$(mktemp "$SCRATCHPAD/${PR_NUMBER}_staging-review_${REVIEW_AGENT_TOKEN}.XXXXXX.md")
|
|
1407
1128
|
cat <<'ISSUE_EOF' > "$STAGING_FINDING_BODY_FILE"
|
|
1408
1129
|
## Problem
|
|
1409
1130
|
|
|
1410
|
-
[One sentence: what bug
|
|
1131
|
+
[One sentence: what bug was found, where (`file:line`) and what it causes.]
|
|
1411
1132
|
|
|
1412
1133
|
**Source**: PR #[PR_NUMBER] — [TITLE]
|
|
1413
1134
|
**Confidence**: [CONFIRMED/LIKELY/POSSIBLE]
|
|
@@ -1421,81 +1142,42 @@ Files that need changes:
|
|
|
1421
1142
|
|
|
1422
1143
|
## Source Branch Context
|
|
1423
1144
|
|
|
1424
|
-
**Code branch**: `[CODE_BRANCH]`
|
|
1425
|
-
**Worktree base**: `origin/[CODE_BRANCH]`
|
|
1426
|
-
|
|
1427
|
-
> When fixing: `git worktree add ../fix-{slug} -b fix/{slug} origin/[CODE_BRANCH]`
|
|
1145
|
+
**Code branch**: `[CODE_BRANCH]` (worktree base `origin/[CODE_BRANCH]`)
|
|
1428
1146
|
|
|
1429
1147
|
## Code Context
|
|
1430
|
-
[10 lines around finding]
|
|
1148
|
+
[10 lines around the finding]
|
|
1431
1149
|
|
|
1432
1150
|
## Evidence
|
|
1433
|
-
[From agent comment]
|
|
1151
|
+
[From the agent comment]
|
|
1434
1152
|
|
|
1435
1153
|
## Acceptance Criteria
|
|
1436
1154
|
|
|
1437
1155
|
- [ ] Finding validated: VALIDATED / FALSE_POSITIVE / INCONCLUSIVE
|
|
1438
|
-
- [ ] If VALIDATED: fix implemented and tested on correct branch
|
|
1156
|
+
- [ ] If VALIDATED: fix implemented and tested on the correct branch
|
|
1157
|
+
[BATCHABLE_ANNOTATION]
|
|
1439
1158
|
ISSUE_EOF
|
|
1440
1159
|
printf '\n<!-- %s -->\n' "$STAGING_FINDING_BODY_MARKER" >> "$STAGING_FINDING_BODY_FILE"
|
|
1160
|
+
echo "STAGING_FINDING_BODY_FILE=$STAGING_FINDING_BODY_FILE"
|
|
1161
|
+
```
|
|
1441
1162
|
|
|
1442
|
-
#
|
|
1443
|
-
# body field (set above in the heredoc) — example assignment shown here for
|
|
1444
|
-
# clarity, same convention as STAGING_FINDING_TITLE etc.
|
|
1445
|
-
STAGING_FINDING_SEVERITY="LOW"
|
|
1163
|
+
Replace `[BATCHABLE_ANNOTATION]` with `<!-- FORGE:BATCHABLE -->` when the item's `batchable` is true, else remove it; add "Previously fixed in #N: regression" when `regression_of` is set. The filing call is a skill invocation, not a shell fence: `--label` repeats for every item label plus `staging-review` (never comma-joined), `--milestone` only when the item's is non-empty:
|
|
1446
1164
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
# source of truth for this mapping; commands/review-pr.md calls the identical
|
|
1451
|
-
# script so the two specs cannot independently drift. <!-- forge#2447 -->
|
|
1452
|
-
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() { :; }; }
|
|
1453
|
-
PRIORITY_SCRIPT=$(forge_resolve severity-to-priority.sh)
|
|
1454
|
-
# Unresolved helper: non-zero status so finding creation is skipped (never defaulted to a priority).
|
|
1455
|
-
if [ -n "$PRIORITY_SCRIPT" ]; then
|
|
1456
|
-
STAGING_FINDING_PRIORITY=$(bash "$PRIORITY_SCRIPT" "$STAGING_FINDING_SEVERITY")
|
|
1457
|
-
STAGING_FINDING_PRIORITY_EXIT=$?
|
|
1458
|
-
else
|
|
1459
|
-
STAGING_FINDING_PRIORITY=""; STAGING_FINDING_PRIORITY_EXIT=127
|
|
1460
|
-
fi
|
|
1165
|
+
```
|
|
1166
|
+
>>> INVOKE: Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"<item.title>\" --body-file \"{STAGING_FINDING_BODY_FILE}\" --label review-finding --label needs-validation --label staging-review --label \"<item.priority>\" [--milestone \"<item.milestone>\"]")
|
|
1167
|
+
```
|
|
1461
1168
|
|
|
1462
|
-
|
|
1463
|
-
# on a missing/unrecognized severity. Proceeding with an empty $STAGING_FINDING_PRIORITY
|
|
1464
|
-
# would call Skill(issue, --label "") instead of aborting issue creation for this finding.
|
|
1465
|
-
# Mirrors the identical check in commands/review-pr.md. <!-- forge#2479 -->
|
|
1466
|
-
if [ "$STAGING_FINDING_PRIORITY_EXIT" -ne 0 ]; then
|
|
1467
|
-
echo "PRIORITY: severity-to-priority.sh failed (exit $STAGING_FINDING_PRIORITY_EXIT) for severity '$STAGING_FINDING_SEVERITY' — skipping finding issue creation"
|
|
1468
|
-
# Skip this finding — do NOT fall through to issue creation with an empty label
|
|
1469
|
-
else
|
|
1169
|
+
For a pre-existing item use the item's `labels` (`pre-existing` plus priority) plus `staging-review`. `/issue` succeeds only after its API create-token read-back and prints `ISSUE_CREATE_RESULT:CREATED number=N` or `ISSUE_CREATE_RESULT:DEDUP number=N`; pass that line to `file-verify`, which prints `ISSUE_NUM=N` and exits 1 when no number was reported (the review then stops instead of silently dropping the finding):
|
|
1470
1170
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
#
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
rm -f "$STAGING_FINDING_BODY_FILE"
|
|
1477
|
-
|
|
1478
|
-
# /issue succeeds only after its API create-token read-back (Phase 4B). Its
|
|
1479
|
-
# explicit result marker distinguishes a verified create from an intentional
|
|
1480
|
-
# dedup STOP; do not use title search to mask a swallowed 403.
|
|
1481
|
-
ISSUE_NUM=$(printf '%s\n' "$ISSUE_SKILL_OUTPUT" | sed -n 's/.*ISSUE_CREATE_RESULT:CREATED number=\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1482
|
-
DEDUP_NUMBER=$(printf '%s\n' "$ISSUE_SKILL_OUTPUT" | sed -n 's/.*ISSUE_CREATE_RESULT:DEDUP number=\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1483
|
-
if [ -n "$DEDUP_NUMBER" ]; then
|
|
1484
|
-
ISSUE_NUM="$DEDUP_NUMBER"
|
|
1485
|
-
echo "Staging review finding deduped against existing issue #${ISSUE_NUM}."
|
|
1486
|
-
elif [ -z "$ISSUE_NUM" ]; then
|
|
1487
|
-
echo "ERROR: /issue did not report a verified staging review-finding number; stopping review instead of silently dropping the finding." >&2
|
|
1488
|
-
exit 1
|
|
1489
|
-
fi
|
|
1490
|
-
fi
|
|
1171
|
+
```bash
|
|
1172
|
+
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() { :; }; }
|
|
1173
|
+
RF=$(forge_resolve review-finalize.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed)
|
|
1174
|
+
[ -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
|
|
1175
|
+
printf '%s\n' "{ISSUE_RESULT_LINE}" | bash "$RF" file-verify || exit 1 # STOP: the finding was not verified as filed
|
|
1491
1176
|
```
|
|
1492
1177
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
**Filing disposition**: Filing follows §7B.5 and the `commands/review-pr.md` §6B.4/§6B.5 rules — LOW severity and POSSIBLE confidence findings are non-blocking NOTEs (recorded in `FORGE:NOTE_DISPOSITION`), not standalone issues. Only findings that are `ISSUE` after 7B.5 and not deduped in 7E become issues; NOTEs and duplicates are recorded, never silently dropped. Validation agents sort out false positives downstream for the findings that are filed.
|
|
1178
|
+
### 7D: Project board and PR description
|
|
1496
1179
|
|
|
1497
|
-
|
|
1498
|
-
### 7H: Update PR Description with Findings Table
|
|
1180
|
+
Add each created issue to the project board (`forge.yaml → project_board`; a missing config skips this silently) and append a `## Review Findings` table (summaries and issue links) to the PR body: edit, never replace.
|
|
1499
1181
|
|
|
1500
1182
|
---
|
|
1501
1183
|
|
|
@@ -1509,7 +1191,7 @@ Post summary with verdict:
|
|
|
1509
1191
|
4. CONFIRMED HIGH blocking (crashes, data loss) → NEEDS FIXES FIRST
|
|
1510
1192
|
5. All else → APPROVE FOR DEPLOY
|
|
1511
1193
|
|
|
1512
|
-
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 (
|
|
1194
|
+
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.
|
|
1513
1195
|
|
|
1514
1196
|
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.
|
|
1515
1197
|
|
|
@@ -1538,6 +1220,24 @@ If `TEST_GATE_VERDICT` is `BLOCK` and Phase 8 was reached (advisory posture or o
|
|
|
1538
1220
|
|
|
1539
1221
|
> **Test Gate: BLOCK (override/advisory)** — Runtime failures were detected but deploy is proceeding. Reason: `${TEST_GATE_REASON}`. Filed `test-failure` issues track the failures.
|
|
1540
1222
|
|
|
1223
|
+
**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.
|
|
1224
|
+
|
|
1225
|
+
```bash
|
|
1226
|
+
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() { :; }; }
|
|
1227
|
+
COST_SCRIPT=$(forge_resolve cost-report.sh)
|
|
1228
|
+
[ -n "$COST_SCRIPT" ] && bash "$COST_SCRIPT" --last 3 2>/dev/null || echo "Pipeline cost: unavailable"
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
**Pipeline stats** (fail-soft, informational; never changes the verdict or any `FORGE:GATE_*` marker; additive to the cost table above) <!-- Added: forge#3835 -->: embed the compact `forgedock stats` table (per version: cost, merged / needs-human / remediation rates, wall time, findings per review, acceptance block rate, delta vs the previous version). A missing CLI, no `gh` auth or no data omits the table.
|
|
1232
|
+
|
|
1233
|
+
```bash
|
|
1234
|
+
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() { :; }; }
|
|
1235
|
+
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1236
|
+
STATS_TMO=""; command -v timeout >/dev/null 2>&1 && STATS_TMO="timeout 120" # bound the crawl; stock macOS lacks timeout. A killed run keeps its cache checkpoints
|
|
1237
|
+
STATS_OUT=$($STATS_TMO forgedock stats --compact --repo "${GH_REPO}" 2>/dev/null || $STATS_TMO npx --no-install forgedock stats --compact --repo "${GH_REPO}" 2>/dev/null) || STATS_OUT=""
|
|
1238
|
+
[ -n "$STATS_OUT" ] && printf '%s\n' "$STATS_OUT" || echo "Pipeline stats: unavailable"
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1541
1241
|
**CRITICAL**: This review NEVER merges staging → main. User makes deploy decision via GitHub web UI.
|
|
1542
1242
|
|
|
1543
1243
|
---
|