forgedock 1.17.0 → 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 +24 -2
- package/bin/engine/phases.mjs +153 -102
- package/bin/engine/runlog.mjs +7 -0
- package/bin/engine-cli.mjs +5 -0
- package/bin/engine.mjs +84 -10
- package/bin/forgedock.mjs +1 -1
- package/bin/hooks/pre-tool-use.mjs +65 -32
- package/bin/runner.mjs +63 -4
- package/bin/stats/github-source.mjs +89 -18
- package/bin/stats/stats.test.mjs +89 -1
- package/commands/cleanup.md +10 -3
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-4-execution.md +11 -4
- package/commands/review-pr-staging.md +142 -519
- package/commands/review-pr.md +490 -3128
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +13 -1
- package/commands/work-on/build/implement.md +2 -1
- package/commands/work-on/build/validate.md +2 -0
- package/commands/work-on/build.md +107 -86
- package/commands/work-on/close.md +50 -15
- package/commands/work-on/decompose.md +72 -34
- package/commands/work-on/investigate.md +9 -12
- package/commands/work-on/remediate.md +27 -13
- package/commands/work-on/review.md +5 -3
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +44 -25
- package/docs/OUTCOME-CODES.md +7 -2
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +2 -1
- package/docs/TELEMETRY.md +2 -2
- package/docs/WORK-ON-RUNTIME.md +6 -0
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +5 -5
- package/scripts/cap-findings.sh +1 -1
- package/scripts/cap-findings.test.sh +1 -1
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/close-parent-c3.test.sh +69 -0
- package/scripts/diff-size.sh +93 -4
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/forge-root.test.sh +13 -11
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- 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 +18 -13
- package/scripts/review-pr-gate2.test.sh +9 -65
- package/scripts/review-pr-staging-scope.test.sh +14 -98
- package/scripts/review-prepare.sh +48 -12
- package/scripts/review-prepare.test.sh +19 -1
- package/scripts/review-roster.sh +16 -2
- package/scripts/review-roster.test.sh +22 -0
- package/scripts/trusted-comments.test.sh +9 -2
- package/scripts/wait-ci-green.test.sh +3 -3
|
@@ -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,50 +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
|
-
# Anchored post-filter: keep a search hit only if its title ends `PR #N)` or its body has a
|
|
126
|
-
# `**Source**: PR #N` line. Hidden markers (issue-create-token, etc.) never count as references.
|
|
127
|
-
FINDING_PR_FILTER='[.[] | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
128
|
-
|
|
129
|
-
# Load findings from prior passes that are still open
|
|
130
|
-
PRIOR_OPEN_FINDINGS=$(gh issue list ${GH_FLAG} \
|
|
131
|
-
--label "review-finding" \
|
|
132
|
-
--state open \
|
|
133
|
-
--search "PR #${PR_NUMBER}" \
|
|
134
|
-
--limit 50 \
|
|
135
|
-
--json number,title,labels,body 2>/dev/null \
|
|
136
|
-
| jq -r --arg n "${PR_NUMBER}" "$FINDING_PR_FILTER" 2>/dev/null || true) # uses FINDING_PR_FILTER
|
|
137
|
-
|
|
138
|
-
# Load findings that were resolved (closed) since the last pass
|
|
139
|
-
PRIOR_RESOLVED_FINDINGS=$(gh issue list ${GH_FLAG} \
|
|
140
|
-
--label "review-finding" \
|
|
141
|
-
--state closed \
|
|
142
|
-
--search "PR #${PR_NUMBER}" \
|
|
143
|
-
--limit 50 \
|
|
144
|
-
--json number,title,body 2>/dev/null \
|
|
145
|
-
| jq -r --arg n "${PR_NUMBER}" "$FINDING_PR_FILTER" 2>/dev/null || true) # uses FINDING_PR_FILTER
|
|
146
|
-
|
|
147
|
-
echo "Prior open findings (carry forward): $(echo "$PRIOR_OPEN_FINDINGS" | grep -c '.' || echo 0)"
|
|
148
|
-
echo "Prior resolved findings (exclude from verdict): $(echo "$PRIOR_RESOLVED_FINDINGS" | grep -c '.' || echo 0)"
|
|
149
|
-
fi
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
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.
|
|
153
|
-
|
|
154
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.
|
|
155
114
|
|
|
156
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)
|
|
157
119
|
# Resolve PR_NUMBER from the argument string (never name the loader placeholder in this block: every
|
|
158
120
|
# occurrence is substituted textually, comments included) <!-- Added: forge#3466 -->
|
|
159
121
|
# The argument string may be: a PR number, "staging", "feature", or "staging:feature"
|
|
@@ -212,6 +174,50 @@ if [ -n "$PR_NUMBER" ]; then printf '%s\n' "$PR_NUMBER" > "$_SD/staging_pr_numbe
|
|
|
212
174
|
|
|
213
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.
|
|
214
176
|
|
|
177
|
+
**Prior-finding awareness on re-entry**: On each re-invocation, load findings from prior review passes on the same PR before running any new checks. Previously-resolved findings (issues that are now closed or marked `false-positive`) are excluded from the current verdict to avoid re-surfacing already-fixed issues. Previously-unresolved findings (open issues from a prior pass) are carried forward as context so agents know what was previously flagged:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
181
|
+
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
182
|
+
GH_FLAG="-R $GH_REPO"
|
|
183
|
+
# Load prior FORGE:REVIEW_ROUTE comments to detect re-entry
|
|
184
|
+
PRIOR_ROUTES=$(gh api repos/${GH_REPO}/issues/${PR_NUMBER}/comments \
|
|
185
|
+
--jq '[.[] | select(.body | test("FORGE:REVIEW_ROUTE")) | .body] | length' 2>/dev/null || echo 0)
|
|
186
|
+
|
|
187
|
+
IS_REENTRY=0
|
|
188
|
+
if [ "$PRIOR_ROUTES" -gt 0 ]; then
|
|
189
|
+
IS_REENTRY=1
|
|
190
|
+
echo "Re-entry detected: $PRIOR_ROUTES prior review pass(es) on PR #${PR_NUMBER}"
|
|
191
|
+
|
|
192
|
+
# Anchored post-filter: keep a search hit only if its title ends `PR #N)` or its body has a
|
|
193
|
+
# `**Source**: PR #N` line. Hidden markers (issue-create-token, etc.) never count as references.
|
|
194
|
+
FINDING_PR_FILTER='[.[] | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
195
|
+
|
|
196
|
+
# Load findings from prior passes that are still open
|
|
197
|
+
PRIOR_OPEN_FINDINGS=$(gh issue list ${GH_FLAG} \
|
|
198
|
+
--label "review-finding" \
|
|
199
|
+
--state open \
|
|
200
|
+
--search "PR #${PR_NUMBER}" \
|
|
201
|
+
--limit 50 \
|
|
202
|
+
--json number,title,labels,body 2>/dev/null \
|
|
203
|
+
| jq -r --arg n "${PR_NUMBER}" "$FINDING_PR_FILTER" 2>/dev/null || true) # uses FINDING_PR_FILTER
|
|
204
|
+
|
|
205
|
+
# Load findings that were resolved (closed) since the last pass
|
|
206
|
+
PRIOR_RESOLVED_FINDINGS=$(gh issue list ${GH_FLAG} \
|
|
207
|
+
--label "review-finding" \
|
|
208
|
+
--state closed \
|
|
209
|
+
--search "PR #${PR_NUMBER}" \
|
|
210
|
+
--limit 50 \
|
|
211
|
+
--json number,title,body 2>/dev/null \
|
|
212
|
+
| jq -r --arg n "${PR_NUMBER}" "$FINDING_PR_FILTER" 2>/dev/null || true) # uses FINDING_PR_FILTER
|
|
213
|
+
|
|
214
|
+
echo "Prior open findings (carry forward): $(echo "$PRIOR_OPEN_FINDINGS" | grep -c '.' || echo 0)"
|
|
215
|
+
echo "Prior resolved findings (exclude from verdict): $(echo "$PRIOR_RESOLVED_FINDINGS" | grep -c '.' || echo 0)"
|
|
216
|
+
fi
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Agents receive `$IS_REENTRY`, `$PRIOR_OPEN_FINDINGS`, and `$PRIOR_RESOLVED_FINDINGS` as context. They MUST NOT re-file issues that already exist in `$PRIOR_OPEN_FINDINGS` (dedup gate in Phase 7 covers this). Resolved findings do NOT contribute to the BLOCK verdict for the current pass.
|
|
220
|
+
|
|
215
221
|
---
|
|
216
222
|
|
|
217
223
|
## Phase 0A: Open Review-Finding Gate (BLOCKING — runs before scope analysis)
|
|
@@ -221,6 +227,8 @@ if [ -n "$PR_NUMBER" ]; then printf '%s\n' "$PR_NUMBER" > "$_SD/staging_pr_numbe
|
|
|
221
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 -->
|
|
222
228
|
|
|
223
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
|
|
224
232
|
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
225
233
|
|
|
226
234
|
# Step 1: Find all PR numbers in the staging→main bundle
|
|
@@ -381,6 +389,8 @@ If the gate exits with `RESULT: PASS` → a `<!-- FORGE:GATE_PASS -->` structure
|
|
|
381
389
|
## Phase 0B: Scope Analysis
|
|
382
390
|
|
|
383
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
|
|
384
394
|
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
385
395
|
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --stat | tail -20
|
|
386
396
|
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --numstat | awk '{add+=$(1); del+=$(2)} END {print "Added:", add, "Deleted:", del, "Total:", add+del}'
|
|
@@ -512,7 +522,7 @@ fi
|
|
|
512
522
|
# Deploy/runtime readiness files are always in scope: migrations, env, workflows, compose/infra, dependency manifests.
|
|
513
523
|
_DW=$(git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --name-only) || { SCOPE_MODE=full; _DW=""; }
|
|
514
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)
|
|
515
|
-
# 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.
|
|
516
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"
|
|
517
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 .)"
|
|
518
528
|
```
|
|
@@ -525,6 +535,30 @@ echo "SCOPE_MODE=$SCOPE_MODE reviewed_prs=$REVIEWED_PR_COUNT unreviewed_commits=
|
|
|
525
535
|
|
|
526
536
|
---
|
|
527
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
|
+
|
|
528
562
|
## Phase 1: Automated Checks
|
|
529
563
|
|
|
530
564
|
### 1A: Python Linting
|
|
@@ -708,141 +742,15 @@ Reviewed-SHA: [REVIEW_SHA]
|
|
|
708
742
|
|
|
709
743
|
## Phase 5: Security & Billing Deep Dive
|
|
710
744
|
|
|
711
|
-
**
|
|
712
|
-
|
|
713
|
-
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:
|
|
714
|
-
|
|
715
|
-
```bash
|
|
716
|
-
TEMPLATE_BASE=""
|
|
717
|
-
# Tier 0: the running plugin's own root (Claude Code substitutes this exact spelling; elsewhere it stays a
|
|
718
|
-
# literal that the /* check rejects). Wins over an exported FORGE_HOME, which may name an older checkout.
|
|
719
|
-
_PLUGIN_ROOT='${CLAUDE_PLUGIN_ROOT}'; case "$_PLUGIN_ROOT" in /*) ;; *) _PLUGIN_ROOT="" ;; esac
|
|
720
|
-
if [[ -n "$_PLUGIN_ROOT" && -f "$_PLUGIN_ROOT/commands/review-pr-agents/protocols.md" ]]; then
|
|
721
|
-
TEMPLATE_BASE="$_PLUGIN_ROOT/commands/review-pr-agents"
|
|
722
|
-
TEMPLATE_SOURCE="plugin_root"
|
|
723
|
-
elif [[ -f "$FORGE_HOME/commands/review-pr-agents/protocols.md" ]]; then
|
|
724
|
-
TEMPLATE_BASE="$FORGE_HOME/commands/review-pr-agents"
|
|
725
|
-
TEMPLATE_SOURCE="forge_home"
|
|
726
|
-
else
|
|
727
|
-
FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
|
|
728
|
-
REPO_PATH=$(yq '.paths.root' "$FORGE_YAML" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
729
|
-
if [[ -f "$REPO_PATH/commands/review-pr-agents/protocols.md" ]]; then
|
|
730
|
-
TEMPLATE_BASE="$REPO_PATH/commands/review-pr-agents"
|
|
731
|
-
TEMPLATE_SOURCE="repo_path"
|
|
732
|
-
elif [[ -f "$REPO_PATH/commands/review-pr-agents.md" ]] && grep -q "^### Agent:" "$REPO_PATH/commands/review-pr-agents.md" 2>/dev/null; then
|
|
733
|
-
# Content check (not just existence) required: a post-split repo still ships a small
|
|
734
|
-
# router stub at this same path that only points back to the (missing) persona
|
|
735
|
-
# directory — reading it would provide no actual protocol/persona content.
|
|
736
|
-
MONOLITHIC_CATALOG="$REPO_PATH/commands/review-pr-agents.md"
|
|
737
|
-
TEMPLATE_SOURCE="monolithic_catalog"
|
|
738
|
-
else
|
|
739
|
-
TEMPLATE_SOURCE="none"
|
|
740
|
-
fi
|
|
741
|
-
fi
|
|
742
|
-
|
|
743
|
-
if [[ "$TEMPLATE_SOURCE" == "none" ]]; then
|
|
744
|
-
echo "FATAL: no review-pr-agents template source resolved (checked \$FORGE_HOME, repo-path fallback, monolithic catalog)."
|
|
745
|
-
# HARD STOP — post error, add needs-human, do NOT review
|
|
746
|
-
fi
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
**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.**
|
|
750
|
-
|
|
751
|
-
**If `TEMPLATE_SOURCE` is `plugin_root`, `forge_home` or `repo_path`** (normal cases — behavior unchanged): `Read: $TEMPLATE_BASE/protocols.md` and `Read: $TEMPLATE_BASE/<persona>.md` per selected agent.
|
|
752
|
-
|
|
753
|
-
**If `TEMPLATE_SOURCE` is `monolithic_catalog`** (last resort): `Read: $MONOLITHIC_CATALOG` and extract the shared protocols section plus each selected persona's section from within that single file.
|
|
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.
|
|
754
746
|
|
|
755
747
|
Launch domain-specific agents based on which domains have changes. Substitute PR diff commands with staging diff commands.
|
|
756
748
|
|
|
757
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.
|
|
758
750
|
|
|
759
|
-
**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
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
763
|
-
# Fetched once, UNCAPPED; agents do NOT re-fetch it. Never cap here: slicing must see the whole bundle.
|
|
764
|
-
FULL_DIFF=$(git diff "origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH")
|
|
765
|
-
|
|
766
|
-
# --- Security agent: always receives the full diff (cross-cutting domain) ---
|
|
767
|
-
DIFF_SLICE_SECURITY="$FULL_DIFF"
|
|
768
|
-
|
|
769
|
-
# --- Auth agent: auth, session, jwt, oauth, middleware, permission files ---
|
|
770
|
-
DIFF_SLICE_AUTH=$(echo "$FULL_DIFF" | awk '
|
|
771
|
-
/^diff --git/ { in_block=0 }
|
|
772
|
-
/^diff --git.*\/(auth|session|jwt|oauth|permission|middleware|login|token)/ { in_block=1 }
|
|
773
|
-
in_block { print }
|
|
774
|
-
')
|
|
775
|
-
[ -z "$DIFF_SLICE_AUTH" ] && DIFF_SLICE_AUTH="$FULL_DIFF"
|
|
776
|
-
|
|
777
|
-
# --- Billing agent: billing, payment, credit, pricing, stripe, subscription files ---
|
|
778
|
-
DIFF_SLICE_BILLING=$(echo "$FULL_DIFF" | awk '
|
|
779
|
-
/^diff --git/ { in_block=0 }
|
|
780
|
-
/^diff --git.*\/(billing|payment|credit|pricing|stripe|subscription|invoice|charge|refund)/ { in_block=1 }
|
|
781
|
-
in_block { print }
|
|
782
|
-
')
|
|
783
|
-
[ -z "$DIFF_SLICE_BILLING" ] && DIFF_SLICE_BILLING="$FULL_DIFF"
|
|
784
|
-
|
|
785
|
-
# --- Concurrency agent: transaction, lock, queue, async, worker, race-condition files ---
|
|
786
|
-
DIFF_SLICE_CONCURRENCY=$(echo "$FULL_DIFF" | awk '
|
|
787
|
-
/^diff --git/ { in_block=0 }
|
|
788
|
-
/^diff --git.*\/(worker|queue|task|async|lock|transaction|pipeline|job)/ { in_block=1 }
|
|
789
|
-
in_block { print }
|
|
790
|
-
')
|
|
791
|
-
[ -z "$DIFF_SLICE_CONCURRENCY" ] && DIFF_SLICE_CONCURRENCY="$FULL_DIFF"
|
|
792
|
-
|
|
793
|
-
# --- Database agent: migration, model, schema, ORM, SQL files ---
|
|
794
|
-
DIFF_SLICE_DATABASE=$(echo "$FULL_DIFF" | awk '
|
|
795
|
-
/^diff --git/ { in_block=0 }
|
|
796
|
-
/^diff --git.*\/(migration|model|schema|\.sql$|orm|database|db)/ { in_block=1 }
|
|
797
|
-
in_block { print }
|
|
798
|
-
')
|
|
799
|
-
[ -z "$DIFF_SLICE_DATABASE" ] && DIFF_SLICE_DATABASE="$FULL_DIFF"
|
|
800
|
-
|
|
801
|
-
# --- API Design agent: router, endpoint, openapi, schema, serializer files ---
|
|
802
|
-
DIFF_SLICE_API=$(echo "$FULL_DIFF" | awk '
|
|
803
|
-
/^diff --git/ { in_block=0 }
|
|
804
|
-
/^diff --git.*\/(router|route|endpoint|openapi|serializer|schema|api)/ { in_block=1 }
|
|
805
|
-
in_block { print }
|
|
806
|
-
')
|
|
807
|
-
[ -z "$DIFF_SLICE_API" ] && DIFF_SLICE_API="$FULL_DIFF"
|
|
808
|
-
|
|
809
|
-
# --- Infrastructure agent: docker, nginx, ci, deploy, infra, workflow files ---
|
|
810
|
-
DIFF_SLICE_INFRA=$(echo "$FULL_DIFF" | awk '
|
|
811
|
-
/^diff --git/ { in_block=0 }
|
|
812
|
-
/^diff --git.*\/(docker|nginx|infra|\.github|deploy|ci|traefik|k8s|helm)/ { in_block=1 }
|
|
813
|
-
in_block { print }
|
|
814
|
-
')
|
|
815
|
-
[ -z "$DIFF_SLICE_INFRA" ] && DIFF_SLICE_INFRA="$FULL_DIFF"
|
|
816
|
-
|
|
817
|
-
# --- Domain Logic agent: scraping/browser-automation files (does not match bare playwright/E2E test files) ---
|
|
818
|
-
DIFF_SLICE_SCRAPER=$(echo "$FULL_DIFF" | awk '
|
|
819
|
-
/^diff --git/ { in_block=0 }
|
|
820
|
-
/^diff --git.*\/(scrape|scraper|stealth|browser_pool|headless|anti_bot|captcha|crawl)/ { in_block=1 }
|
|
821
|
-
in_block { print }
|
|
822
|
-
')
|
|
823
|
-
[ -z "$DIFF_SLICE_SCRAPER" ] && DIFF_SLICE_SCRAPER="$FULL_DIFF"
|
|
824
|
-
|
|
825
|
-
# Cap each slice AFTER slicing and after the empty-slice fallback (a fallback slice is the truncated full diff)
|
|
826
|
-
truncate_slice() {
|
|
827
|
-
local slice="${1}"
|
|
828
|
-
local limit=102400
|
|
829
|
-
if [ "${#slice}" -gt "$limit" ]; then
|
|
830
|
-
echo "${slice:0:$limit}"
|
|
831
|
-
echo "... [TRUNCATED — diff exceeded 100K chars; focus on the files listed above]"
|
|
832
|
-
else
|
|
833
|
-
echo "$slice"
|
|
834
|
-
fi
|
|
835
|
-
}
|
|
836
|
-
DIFF_SLICE_SECURITY=$(truncate_slice "$DIFF_SLICE_SECURITY")
|
|
837
|
-
DIFF_SLICE_AUTH=$(truncate_slice "$DIFF_SLICE_AUTH")
|
|
838
|
-
DIFF_SLICE_BILLING=$(truncate_slice "$DIFF_SLICE_BILLING")
|
|
839
|
-
DIFF_SLICE_CONCURRENCY=$(truncate_slice "$DIFF_SLICE_CONCURRENCY")
|
|
840
|
-
DIFF_SLICE_DATABASE=$(truncate_slice "$DIFF_SLICE_DATABASE")
|
|
841
|
-
DIFF_SLICE_API=$(truncate_slice "$DIFF_SLICE_API")
|
|
842
|
-
DIFF_SLICE_INFRA=$(truncate_slice "$DIFF_SLICE_INFRA")
|
|
843
|
-
DIFF_SLICE_SCRAPER=$(truncate_slice "$DIFF_SLICE_SCRAPER")
|
|
844
|
-
```
|
|
845
|
-
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 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.
|
|
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).
|
|
752
|
+
|
|
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.
|
|
846
754
|
|
|
847
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:
|
|
848
756
|
```bash
|
|
@@ -1153,321 +1061,74 @@ fi
|
|
|
1153
1061
|
|
|
1154
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.
|
|
1155
1063
|
|
|
1156
|
-
### 7A: Extract
|
|
1157
|
-
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").
|
|
1064
|
+
### 7A: Extract, dedupe and gate (with the note-disposition dry run)
|
|
1158
1065
|
|
|
1159
|
-
|
|
1160
|
-
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() { :; }; }
|
|
1161
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1162
|
-
# 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
|
|
1163
|
-
# (an empty SHA would make the Reviewed-SHA match accept any head).
|
|
1164
|
-
[ -n "${REVIEW_SHA_FULL:-}" ] || REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
1165
|
-
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
|
|
1166
|
-
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
|
-
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
1168
|
-
[ -n "$REVIEW_SHA_FULL" ] || { echo "STAGING_REVIEW: BLOCKED - head SHA is not a commit id; refusing to extract findings (fail closed)"; exit 1; }
|
|
1169
|
-
COMMENTS_JSON=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1170
|
-
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
1171
|
-
echo "STAGING_REVIEW: BLOCKED - scripts/trusted-comments.sh unresolvable or PR comments unreadable; refusing to extract findings (fail closed)"; exit 1
|
|
1172
|
-
fi
|
|
1173
|
-
AGENT_RE="^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)"
|
|
1174
|
-
AGENT_BODIES=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies "$AGENT_RE" 2>/dev/null) \
|
|
1175
|
-
|| { echo "STAGING_REVIEW: BLOCKED - trusted reviewer comments unreadable (fail closed)"; exit 1; }
|
|
1176
|
-
ALL_FINDINGS=$(printf '%s\n' "$AGENT_BODIES" | jq -r 'scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null)
|
|
1177
|
-
```
|
|
1178
|
-
|
|
1179
|
-
Extract structured findings (`<!-- FINDING:... -->`) from `ALL_FINDINGS`. If none found, scan the trusted bodies for unstructured findings. If still 0 → skip to Phase 8.
|
|
1180
|
-
|
|
1181
|
-
### 7B: Filter & Deduplicate
|
|
1182
|
-
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).
|
|
1183
|
-
|
|
1184
|
-
Sort: CONFIRMED first, then by severity.
|
|
1185
|
-
|
|
1186
|
-
### 7B.6: Provenance and concreteness gates (forge#3452)
|
|
1187
|
-
|
|
1188
|
-
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.
|
|
1189
|
-
|
|
1190
|
-
### 7B.5: Note Disposition (same damper and classifier as `/review-pr` §6B.5)
|
|
1191
|
-
|
|
1192
|
-
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`.
|
|
1193
|
-
|
|
1194
|
-
**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).
|
|
1195
1067
|
|
|
1196
1068
|
```bash
|
|
1197
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() { :; }; }
|
|
1198
|
-
CLASSIFY_SCRIPT=$(forge_resolve classify-finding.sh)
|
|
1199
|
-
# 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).
|
|
1200
|
-
if [ -z "$CLASSIFY_SCRIPT" ]; then
|
|
1201
|
-
echo "REVIEW_RESULT: status: BLOCKED, blocker: classifier unavailable (classify-finding.sh not resolvable through the trusted resolver)"
|
|
1202
|
-
exit 0
|
|
1203
|
-
fi
|
|
1204
|
-
# Per-review cap (#3642): same resolver, same fail-closed rule.
|
|
1205
|
-
CAP_SCRIPT=$(forge_resolve cap-findings.sh)
|
|
1206
|
-
if [ -z "$CAP_SCRIPT" ]; then
|
|
1207
|
-
echo "REVIEW_RESULT: status: BLOCKED, blocker: cap helper unavailable (cap-findings.sh not resolvable through the trusted resolver)"
|
|
1208
|
-
exit 0
|
|
1209
|
-
fi
|
|
1210
|
-
# review.max_filed_findings (optional; helper default 2, falls back on invalid)
|
|
1211
|
-
MAX_FILED_FINDINGS=$(yq '.review.max_filed_findings // ""' forge.yaml 2>/dev/null || echo '')
|
|
1212
|
-
# Per-finding inputs come from each structured block: FINDING_ID, FINDING_SEVERITY, FINDING_CONFIDENCE, FINDING_AGENT, FINDING_TITLE, FINDING_BODY,
|
|
1213
|
-
# FINDING_FILE (primary) and FINDING_PATHS (newline-separated, every cited file). Run THIS WHOLE BLOCK in ONE Bash call, looping over every deduped finding
|
|
1214
|
-
# (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.
|
|
1215
|
-
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() { :; }; }
|
|
1216
1070
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
CAP_ROWS_FILE="$_SD/${PR_NUMBER}_cap-rows.tsv"; : > "$CAP_ROWS_FILE" # ISSUE rows awaiting the cap
|
|
1222
|
-
CAPPED_IDS_FILE="$_SD/${PR_NUMBER}_capped-ids.txt"; : > "$CAPPED_IDS_FILE" # capped ids: 7E/7F skip them
|
|
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}"
|
|
1223
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.
|
|
1224
1076
|
SCOPE_MODE=full; CROSS_PR_FILES=""; UNREVIEWED_FILES=""; DEPLOY_WIRING_FILES=""
|
|
1225
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
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
#
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
# from an earlier run of this same review, must never swallow its findings).
|
|
1236
|
-
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)
|
|
1237
|
-
local _loc="$PF_LOC" _file _r _a="" _b=""; _file="$_loc"
|
|
1238
|
-
case "$_loc" in *:[0-9]*) _r="${_loc##*:}"; case "$_r" in *[!0-9-]*|"") ;; *) _file="${_loc%:*}"; _a="${_r%%-*}"; _b="${_r#*-}"; [ "$_b" != "$_r" ] || _b="$_a" ;; esac ;; esac
|
|
1239
|
-
printf '%s\t%s\t%s\n' "$_file" "${_a:--}" "${_b:--}"
|
|
1240
|
-
}
|
|
1241
|
-
prior_findings_tsv() {
|
|
1242
|
-
local pr cj rows issues _id _c _s _loc _t _p _hit _f _a _b _src _dflt
|
|
1243
|
-
[ -n "${TRUSTED_SCRIPT:-}" ] || return 0
|
|
1244
|
-
rows=$(mktemp "${_pscr}/prior-rows.XXXXXX"); issues=$(mktemp "${_pscr}/prior-issues.XXXXXX")
|
|
1245
|
-
for pr in $PF_PRS; do
|
|
1246
|
-
case "$pr" in ''|*[!0-9]*) continue ;; esac
|
|
1247
|
-
cj=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" 2>/dev/null) || continue
|
|
1248
|
-
[ -n "$cj" ] || continue
|
|
1249
|
-
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 \
|
|
1250
|
-
| jq -r --arg skip "${PF_SKIP:-}" 'select($skip == "" or (test("(^|\\n)Reviewed-SHA: " + $skip + "\\r?(\\n|$)") | not)) | scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null \
|
|
1251
|
-
| while IFS='|' read -r _id _c _s _loc _t; do
|
|
1252
|
-
[ -n "$_t" ] || continue
|
|
1253
|
-
_p=$(PF_LOC="$_loc" pf_split_loc); printf 'F\t%s\t%s\n' "$_p" "$(printf '%s' "$_t" | tr '\t' ' ')"
|
|
1254
|
-
done >> "$rows"
|
|
1255
|
-
printf '%s' "$cj" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:NOTE_DISPOSITION' 2>/dev/null \
|
|
1256
|
-
| 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 \
|
|
1257
|
-
| sed -n 's/^- [^ ]* \([^ ]*\) \[[A-Za-z?]*\] \(.*\): [^:]*$/\1\t\2/p' \
|
|
1258
|
-
| while IFS="$(printf '\t')" read -r _loc _t; do
|
|
1259
|
-
[ "$_loc" != "?" ] || continue
|
|
1260
|
-
_p=$(PF_LOC="$_loc" pf_split_loc); printf 'N\t%s\t%s\n' "$_p" "$_t"
|
|
1261
|
-
done >> "$rows"
|
|
1262
|
-
gh issue list -R "$REPO" --label review-finding --state all --search "${pr} in:body" --limit 100 --json number,state,title,body 2>/dev/null \
|
|
1263
|
-
| jq -c '.[]' >> "$issues" 2>/dev/null || true
|
|
1264
|
-
done
|
|
1265
|
-
# Linkage: a filed issue whose body names the file and whose title shares >= 3 tokens with the finding (open preferred).
|
|
1266
|
-
# Rows carry a leading source column (F = reviewer FINDING marker, N = NOTE_DISPOSITION record), stripped on output. An unlinked F row may
|
|
1267
|
-
# have been filed under a reworded title, so it is `unknown` (never demotes MEDIUM CONFIRMED/LIKELY); an unlinked N row is a true `note`.
|
|
1268
|
-
sort -u "$rows" | while IFS="$(printf '\t')" read -r _src _f _a _b _t; do
|
|
1269
|
-
[ -n "$_f" ] || continue
|
|
1270
|
-
[ "$_a" != "-" ] || _a=""; [ "$_b" != "-" ] || _b=""
|
|
1271
|
-
case "$_src" in N) _dflt=note ;; *) _dflt=unknown ;; esac
|
|
1272
|
-
_hit=$(jq -rs --arg f "$_f" --arg t "$_t" 'def toks: ascii_downcase | [scan("[a-z0-9]{3,}")] | unique;
|
|
1273
|
-
($t | toks) as $a | [ .[] | select((.body // "") | contains($f))
|
|
1274
|
-
| select((((.title // "") | toks) as $b | [$a[] | select(. as $x | $b | index($x))] | length) >= 3)
|
|
1275
|
-
| {n: .number, s: (.state | ascii_downcase)} ]
|
|
1276
|
-
| (map(select(.s == "open")) + map(select(.s != "open"))) | .[0] // empty | "\(.s)\t\(.n)"' "$issues" 2>/dev/null) || _hit=""
|
|
1277
|
-
case "$_hit" in
|
|
1278
|
-
open*) printf '%s\t%s\t%s\t%s\tfiled\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
1279
|
-
closed*) printf '%s\t%s\t%s\t%s\tfiled-closed\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
1280
|
-
*) printf '%s\t%s\t%s\t%s\t%s\t\n' "$_f" "$_a" "$_b" "$_t" "$_dflt" ;;
|
|
1281
|
-
esac
|
|
1282
|
-
done
|
|
1283
|
-
rm -f "$rows" "$issues"
|
|
1284
|
-
}
|
|
1285
|
-
PRIOR_ROWS=$(PF_SKIP="" PF_PRS="$ALL_PR_NUMBERS" prior_findings_tsv)
|
|
1286
|
-
if [ -n "$PRIOR_ROWS" ]; then PRIOR_FILE="$_SD/${PR_NUMBER}_prior-findings.tsv"; printf '%s\n' "$PRIOR_ROWS" > "$PRIOR_FILE"; fi
|
|
1287
|
-
classify_one() {
|
|
1288
|
-
# FINDING_TEXT_FILE holds the finding text: the classifier's content-based safety exemption reads it, so an empty file silently disables the exemption.
|
|
1289
|
-
local FINDING_TEXT_FILE; FINDING_TEXT_FILE=$(mktemp "$_SD/staging-finding.XXXXXX")
|
|
1290
|
-
printf '%s\n%s\n%s\n' "${FINDING_TITLE:-}" "${FINDING_BODY:-}" "${FINDING_PATHS:-${FINDING_FILE:-}}" > "$FINDING_TEXT_FILE"
|
|
1291
|
-
DISPOSITION=""
|
|
1292
|
-
if [ -n "$CLASSIFY_SCRIPT" ]; then
|
|
1293
|
-
DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
|
|
1294
|
-
--agent "$FINDING_AGENT" --lineage none --text-file "$FINDING_TEXT_FILE" \
|
|
1295
|
-
${PRIOR_FILE:+--prior-findings "$PRIOR_FILE" --title "${FINDING_TITLE:-}" --file "${FINDING_FILE:-}"}) || DISPOSITION=""
|
|
1296
|
-
fi
|
|
1297
|
-
rm -f "$FINDING_TEXT_FILE"
|
|
1298
|
-
# 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).
|
|
1299
|
-
case "$DISPOSITION" in ISSUE*|NOTE*) ;; *) DISPOSITION="ISSUE classifier-error" ;; esac
|
|
1300
|
-
# 7B scope rule (the two SCOPE_RULE lines are executed here and evaluated by scripts/review-pr-staging-scope.test.sh):
|
|
1301
|
-
SEV=$(printf '%s' "$FINDING_SEVERITY" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]') # SCOPE_RULE
|
|
1302
|
-
case "$SEV" in MEDIUM|LOW|INFO) OOS_DEMOTE=1 ;; *) OOS_DEMOTE=0 ;; esac # SCOPE_RULE
|
|
1303
|
-
# Demote ONLY when scoped, the rule allows it, the classifier did not apply its safety exemption, and EVERY cited file is out of scope.
|
|
1304
|
-
# A finding with no file, or with any file in scope, is never demoted. The demotion is recorded as a listed note, never dropped.
|
|
1305
|
-
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
|
|
1306
|
-
if [ "$SCOPE_MODE" = "scoped" ] && [ "$OOS_DEMOTE" = "1" ] && [ -n "${FINDING_PATHS:-${FINDING_FILE:-}}" ]; then
|
|
1307
|
-
local _ANY_IN=false _f
|
|
1308
|
-
while IFS= read -r _f; do
|
|
1309
|
-
[ -n "$_f" ] || continue
|
|
1310
|
-
printf '%s\n' "$CROSS_PR_FILES" "$UNREVIEWED_FILES" "$DEPLOY_WIRING_FILES" | grep -qxF -- "$_f" && _ANY_IN=true
|
|
1311
|
-
done <<< "${FINDING_PATHS:-$FINDING_FILE}"
|
|
1312
|
-
if [ "$_ANY_IN" = "false" ]; then DISPOSITION="NOTE out_of_scope"; OUT_OF_SCOPE=$((OUT_OF_SCOPE+1)); fi
|
|
1313
|
-
fi
|
|
1314
|
-
case "$DISPOSITION" in
|
|
1315
|
-
"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
|
|
1316
|
-
NOTE*) NOTES_LISTED=$((NOTES_LISTED+1)); printf '%s\n' "- ${FINDING_ID:-?} ${FINDING_FILE:-?} [${SEV:-?}] ${FINDING_TITLE:-}: ${DISPOSITION}" >> "$NOTE_LIST_FILE" ;;
|
|
1317
|
-
*) [ -n "${CAP_ROWS_FILE:-}" ] && printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "${FINDING_ID:--}" "${SEV:--}" "${FINDING_CONFIDENCE:--}" "$DISPOSITION" "${FINDING_AGENT:--}" "${FINDING_FILE:--}" "$(printf '%s' "${FINDING_TITLE:--}" | tr '\t\n' ' ')" >> "$CAP_ROWS_FILE" ;; # ISSUE: counted by cap_pass after the cap
|
|
1318
|
-
esac
|
|
1319
|
-
}
|
|
1320
|
-
cap_pass() {
|
|
1321
|
-
# #3642: run ONCE after the last classify_one, before 7E; sets FINDINGS_FILED (post-cap) and NOTES_CAPPED.
|
|
1322
|
-
local _out="$_SD/${PR_NUMBER}_cap-out.tsv" _total
|
|
1323
|
-
FINDINGS_FILED=0; NOTES_CAPPED=0; : > "$CAPPED_IDS_FILE"
|
|
1324
|
-
[ -s "$CAP_ROWS_FILE" ] || return 0
|
|
1325
|
-
_total=$(grep -c . "$CAP_ROWS_FILE")
|
|
1326
|
-
# Helper failure keeps every ISSUE (fail toward filing).
|
|
1327
|
-
if ! bash "$CAP_SCRIPT" --lineage none ${MAX_FILED_FINDINGS:+--cap "$MAX_FILED_FINDINGS"} --input "$CAP_ROWS_FILE" > "$_out" 2>/dev/null || ! grep -q '^CAPPED=' "$_out"; then
|
|
1328
|
-
FINDINGS_FILED="$_total"; echo "WARNING: cap-findings.sh failed: all $_total ISSUE findings kept"; return 0
|
|
1329
|
-
fi
|
|
1330
|
-
NOTES_CAPPED=$(sed -n 's/^CAPPED=//p' "$_out" | tail -n 1); NOTES_CAPPED="${NOTES_CAPPED:-0}"
|
|
1331
|
-
awk -F'\t' -v ids="$CAPPED_IDS_FILE" 'NR == FNR { if ($0 !~ /^CAPPED=/) o[++n] = $2; next } # allowlist:positional-arg (awk fields)
|
|
1332
|
-
{ k++; if (o[k] == "NOTE capped") { printf "%s\n", $1 > ids; printf "- %s %s [%s] %s: NOTE capped (over review.max_filed_findings)\n", $1, $6, $2, $7 } }' "$_out" "$CAP_ROWS_FILE" >> "$NOTE_LIST_FILE" # allowlist:positional-arg (awk fields)
|
|
1333
|
-
FINDINGS_FILED=$((_total - NOTES_CAPPED))
|
|
1334
|
-
echo "cap_pass: findings_filed=$FINDINGS_FILED notes_capped=$NOTES_CAPPED"
|
|
1335
|
-
}
|
|
1336
|
-
# per deduped finding: set FINDING_*, call classify_one; then cap_pass ONCE. Ids in $CAPPED_IDS_FILE are capped NOTEs: skip them in 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
|
|
1337
1087
|
```
|
|
1338
1088
|
|
|
1339
|
-
|
|
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.
|
|
1340
1090
|
|
|
1341
|
-
|
|
1091
|
+
### 7B: Note disposition (same damper and classifier as `/review-pr` §6B.5)
|
|
1342
1092
|
|
|
1343
|
-
|
|
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`.
|
|
1344
1094
|
|
|
1345
|
-
|
|
1346
|
-
CLASSIFIER_MODE=script # the 7B.5 guard blocks the review when the classifier is unresolvable, so this is always script
|
|
1347
|
-
# 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.
|
|
1348
|
-
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() { :; }; }
|
|
1349
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1350
|
-
if [ -z "${PR_NUMBER:-}" ]; then
|
|
1351
|
-
echo "REVIEW_RESULT: status: BLOCKED, blocker: PR_NUMBER unavailable; FORGE:NOTE_DISPOSITION not posted (record lost)"
|
|
1352
|
-
else
|
|
1353
|
-
NOTE_LIST_FILE="${NOTE_LIST_FILE:-$_SD/${PR_NUMBER}_notes.md}"
|
|
1354
|
-
[ -n "${NOTES_LISTED:-}" ] || { NOTES_LISTED=$(grep -v ': NOTE capped (' "$NOTE_LIST_FILE" 2>/dev/null | grep -c '^- ' || true); echo "WARNING: classification counters lost (fresh shell): notes_listed recomputed from $NOTE_LIST_FILE; set SCOPE_MODE, FINDINGS_FILED, OUT_OF_SCOPE, NOTES_REFLAG and NOTES_CAPPED from the 7B.5 block output before posting"; }
|
|
1355
|
-
[ -n "${NOTES_CAPPED:-}" ] || NOTES_CAPPED=$(grep -c ': NOTE capped (' "$NOTE_LIST_FILE" 2>/dev/null || true)
|
|
1356
|
-
[ -n "${SCOPE_MODE:-}" ] || { SCOPE_MODE=full; echo "WARNING: SCOPE_MODE lost (fresh shell): recording scope=full"; }
|
|
1357
|
-
_DISP_BODY="$_SD/${PR_NUMBER}_note_disposition.md"
|
|
1358
|
-
{ 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} notes_capped=${NOTES_CAPPED:-0} lineage=none classifier=${CLASSIFIER_MODE} scope=${SCOPE_MODE} -->"; cat "$NOTE_LIST_FILE" 2>/dev/null; } > "$_DISP_BODY"
|
|
1359
|
-
gh pr comment "$PR_NUMBER" -R "$GH_REPO" --body-file "$_DISP_BODY" # allowlist:check-command-side-effects
|
|
1360
|
-
fi
|
|
1361
|
-
```
|
|
1362
|
-
|
|
1363
|
-
### 7C: Ensure Labels
|
|
1364
|
-
```bash
|
|
1365
|
-
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() { :; }; }
|
|
1366
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1367
|
-
# Colors match the canonical ForgeDock label manifest (bin/labels.json).
|
|
1368
|
-
# Run `npx forgedock labels setup` to bootstrap all managed labels at once.
|
|
1369
|
-
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
|
|
1370
|
-
gh label create "needs-validation" --color "FBCA04" --description "Review finding awaiting human validation. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null
|
|
1371
|
-
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
|
|
1372
|
-
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
|
|
1373
|
-
```
|
|
1095
|
+
### 7C: Prepare and create issues
|
|
1374
1096
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
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`.
|
|
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.
|
|
1378
1098
|
|
|
1379
1099
|
```bash
|
|
1380
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() { :; }; }
|
|
1381
1101
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
#
|
|
1387
|
-
|
|
1388
|
-
#
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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"
|
|
1398
1118
|
```
|
|
1399
1119
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
### 7E: Deduplicate Against Existing Issues (BEFORE every filing)
|
|
1403
|
-
|
|
1404
|
-
Re-reviewing the same bundle must not re-file existing findings. For each `ISSUE` finding, check open AND closed `review-finding` issues by file + pattern + location before creating anything:
|
|
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:
|
|
1405
1121
|
|
|
1406
1122
|
```bash
|
|
1407
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() { :; }; }
|
|
1408
1124
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1409
|
-
|
|
1410
|
-
# FINDING_FILE/FINDING_LINE come from the finding's structured block. Define the title and scratch dir BEFORE use (7F reuses them).
|
|
1411
|
-
SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$SCRATCHPAD"
|
|
1412
|
-
STAGING_FINDING_TITLE="chore: ${FINDING_TITLE:-[summary]} (staging review — PR #${PR_NUMBER})"
|
|
1413
|
-
EXISTING_ISSUE_EXCLUDE="${EXISTING_ISSUE_EXCLUDE:-}" # optional comma-separated issue numbers already linked to this bundle
|
|
1414
|
-
_SAFE_FILE=$(printf '%s' "$FINDING_FILE" | tr -d '"\\')
|
|
1415
|
-
# issue-dedup.sh ships with ForgeDock next to classify-finding.sh (resolved in 7B.5, install-root only, never the repo under review).
|
|
1416
|
-
DEDUP_SCRIPT=""; [ -n "${CLASSIFY_SCRIPT:-}" ] && [ -f "$(dirname "$CLASSIFY_SCRIPT")/issue-dedup.sh" ] && DEDUP_SCRIPT="$(dirname "$CLASSIFY_SCRIPT")/issue-dedup.sh"
|
|
1417
|
-
if [ -n "$DEDUP_SCRIPT" ]; then
|
|
1418
|
-
DEDUP_OUT=$(bash "$DEDUP_SCRIPT" "${FINDING_TITLE:-$STAGING_FINDING_TITLE}" "$GH_FLAG" ${EXISTING_ISSUE_EXCLUDE:+--exclude "$EXISTING_ISSUE_EXCLUDE"} 2>&1) # open issues, token match
|
|
1419
|
-
DEDUP_EXIT=$?
|
|
1420
|
-
else
|
|
1421
|
-
DEDUP_EXIT=127 # dedup unavailable
|
|
1422
|
-
fi
|
|
1423
|
-
# The title token match alone is weak (any two findings share words like "staging" and "review" in the boilerplate, which is why only the finding's own
|
|
1424
|
-
# title is matched above): an exit 1 is a duplicate ONLY when the matched open issue also cites this finding's file. Otherwise it is not a duplicate.
|
|
1425
|
-
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
1426
|
-
_DUP_N=$(printf '%s' "$DEDUP_OUT" | sed -n 's/^DUPLICATE: #\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1427
|
-
_DUP_BODY=$(gh issue view "${_DUP_N:-0}" -R "$GH_REPO" --json body --jq '.body' 2>/dev/null) || _DUP_BODY=""
|
|
1428
|
-
printf '%s' "$_DUP_BODY" | grep -qF -- "$_SAFE_FILE" || DEDUP_EXIT=0
|
|
1429
|
-
fi
|
|
1430
|
-
# 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.
|
|
1431
|
-
_FILE_RE=$(printf '%s' "$_SAFE_FILE" | sed 's/[][\.^$*+?(){}|\/]/\\&/g')
|
|
1432
|
-
REGRESSION_CHECK=ok; CLOSED_HIT=""
|
|
1433
|
-
# Capture the gh read first and test its own status: through a pipe a failed read looks like "no closed hit".
|
|
1434
|
-
_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
|
|
1435
|
-
# FINDING_LINE may be a range such as 12-15: use its first integer, and 0 when there is none.
|
|
1436
|
-
_L=$(printf '%s' "${FINDING_LINE:-0}" | grep -oE '[0-9]+' | head -1); _L="${_L:-0}"
|
|
1437
|
-
_HITS=""; [ "$REGRESSION_CHECK" = "ok" ] && { _HITS=$(printf '%s' "$_CL" | jq -r --arg re "$_FILE_RE" --argjson l "$_L" '.[] | select((.body // "") != "") | select(.stateReason != "NOT_PLANNED")
|
|
1438
|
-
| select([.body | scan($re + "[:#L]+([0-9]+)") | .[0] | tonumber | select((. - $l) <= 5 and ($l - .) <= 5)] | length > 0) | "#\(.number) \(.title)"') || REGRESSION_CHECK=unavailable; }
|
|
1439
|
-
CLOSED_HIT=$(printf '%s\n' "$_HITS" | sed '/^$/d' | head -3)
|
|
1440
|
-
# Confirm the 3+ shared title keywords against each hit before treating it as a regression.
|
|
1441
|
-
```
|
|
1442
|
-
|
|
1443
|
-
- `DEDUP_EXIT=1` (open duplicate): skip creation, record `deduped: #N`. Exit `0` means no duplicate. Any other exit (usage error 2, script unavailable 126/127) means dedup is unavailable: record `dedup-unavailable` in the Phase 8 summary and file the finding normally (the `/issue` Phase 2D dedup remains as the second pass); a broken dedup helper must never suppress a finding, least of all a CRITICAL or HIGH. Pass `--exclude` for any issue already linked to this bundle.
|
|
1444
|
-
- `REGRESSION_CHECK=unavailable` (the closed-issue read or its filter failed): record `regression-check-unavailable` in the Phase 8 summary; it never suppresses filing.
|
|
1445
|
-
- `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.
|
|
1446
|
-
- Record every deduped item (finding id, matched issue) for the Phase 8 summary and audit.
|
|
1447
|
-
|
|
1448
|
-
### 7F: Create Issues
|
|
1449
|
-
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.
|
|
1450
|
-
|
|
1451
|
-
**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:
|
|
1452
|
-
```bash
|
|
1453
|
-
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() { :; }; }
|
|
1454
|
-
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1455
|
-
# STAGING_FINDING_TITLE was built in 7E; re-assert it here if 7E was skipped.
|
|
1456
|
-
STAGING_FINDING_TITLE="${STAGING_FINDING_TITLE:-chore: [summary] (staging review — PR #${PR_NUMBER})}"
|
|
1457
|
-
# Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
|
|
1458
|
-
# an xargs-based tokenizer that never expands backtick/$(...) substitution, so
|
|
1459
|
-
# this is no longer required for safety — but strip it anyway so the raw title
|
|
1460
|
-
# stays readable if it round-trips through any other eval-based consumer.
|
|
1461
|
-
STAGING_FINDING_TITLE=$(printf '%s' "$STAGING_FINDING_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
|
|
1462
|
-
SCRATCHPAD="${SCRATCHPAD:-${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}}"
|
|
1463
|
-
REVIEW_AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-reviewer}-$$}"
|
|
1464
|
-
mkdir -p "$SCRATCHPAD"
|
|
1125
|
+
SCRATCHPAD="$_SD"; REVIEW_AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-reviewer}-$$}"
|
|
1465
1126
|
STAGING_FINDING_BODY_MARKER="FORGE:BODY-INTEGRITY:${PR_NUMBER}_staging-review_${REVIEW_AGENT_TOKEN}"
|
|
1466
1127
|
STAGING_FINDING_BODY_FILE=$(mktemp "$SCRATCHPAD/${PR_NUMBER}_staging-review_${REVIEW_AGENT_TOKEN}.XXXXXX.md")
|
|
1467
1128
|
cat <<'ISSUE_EOF' > "$STAGING_FINDING_BODY_FILE"
|
|
1468
1129
|
## Problem
|
|
1469
1130
|
|
|
1470
|
-
[One sentence: what bug
|
|
1131
|
+
[One sentence: what bug was found, where (`file:line`) and what it causes.]
|
|
1471
1132
|
|
|
1472
1133
|
**Source**: PR #[PR_NUMBER] — [TITLE]
|
|
1473
1134
|
**Confidence**: [CONFIRMED/LIKELY/POSSIBLE]
|
|
@@ -1481,81 +1142,42 @@ Files that need changes:
|
|
|
1481
1142
|
|
|
1482
1143
|
## Source Branch Context
|
|
1483
1144
|
|
|
1484
|
-
**Code branch**: `[CODE_BRANCH]`
|
|
1485
|
-
**Worktree base**: `origin/[CODE_BRANCH]`
|
|
1486
|
-
|
|
1487
|
-
> When fixing: `git worktree add ../fix-{slug} -b fix/{slug} origin/[CODE_BRANCH]`
|
|
1145
|
+
**Code branch**: `[CODE_BRANCH]` (worktree base `origin/[CODE_BRANCH]`)
|
|
1488
1146
|
|
|
1489
1147
|
## Code Context
|
|
1490
|
-
[10 lines around finding]
|
|
1148
|
+
[10 lines around the finding]
|
|
1491
1149
|
|
|
1492
1150
|
## Evidence
|
|
1493
|
-
[From agent comment]
|
|
1151
|
+
[From the agent comment]
|
|
1494
1152
|
|
|
1495
1153
|
## Acceptance Criteria
|
|
1496
1154
|
|
|
1497
1155
|
- [ ] Finding validated: VALIDATED / FALSE_POSITIVE / INCONCLUSIVE
|
|
1498
|
-
- [ ] If VALIDATED: fix implemented and tested on correct branch
|
|
1156
|
+
- [ ] If VALIDATED: fix implemented and tested on the correct branch
|
|
1157
|
+
[BATCHABLE_ANNOTATION]
|
|
1499
1158
|
ISSUE_EOF
|
|
1500
1159
|
printf '\n<!-- %s -->\n' "$STAGING_FINDING_BODY_MARKER" >> "$STAGING_FINDING_BODY_FILE"
|
|
1160
|
+
echo "STAGING_FINDING_BODY_FILE=$STAGING_FINDING_BODY_FILE"
|
|
1161
|
+
```
|
|
1501
1162
|
|
|
1502
|
-
#
|
|
1503
|
-
# body field (set above in the heredoc) — example assignment shown here for
|
|
1504
|
-
# clarity, same convention as STAGING_FINDING_TITLE etc.
|
|
1505
|
-
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:
|
|
1506
1164
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
# source of truth for this mapping; commands/review-pr.md calls the identical
|
|
1511
|
-
# script so the two specs cannot independently drift. <!-- forge#2447 -->
|
|
1512
|
-
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() { :; }; }
|
|
1513
|
-
PRIORITY_SCRIPT=$(forge_resolve severity-to-priority.sh)
|
|
1514
|
-
# Unresolved helper: non-zero status so finding creation is skipped (never defaulted to a priority).
|
|
1515
|
-
if [ -n "$PRIORITY_SCRIPT" ]; then
|
|
1516
|
-
STAGING_FINDING_PRIORITY=$(bash "$PRIORITY_SCRIPT" "$STAGING_FINDING_SEVERITY")
|
|
1517
|
-
STAGING_FINDING_PRIORITY_EXIT=$?
|
|
1518
|
-
else
|
|
1519
|
-
STAGING_FINDING_PRIORITY=""; STAGING_FINDING_PRIORITY_EXIT=127
|
|
1520
|
-
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
|
+
```
|
|
1521
1168
|
|
|
1522
|
-
|
|
1523
|
-
# on a missing/unrecognized severity. Proceeding with an empty $STAGING_FINDING_PRIORITY
|
|
1524
|
-
# would call Skill(issue, --label "") instead of aborting issue creation for this finding.
|
|
1525
|
-
# Mirrors the identical check in commands/review-pr.md. <!-- forge#2479 -->
|
|
1526
|
-
if [ "$STAGING_FINDING_PRIORITY_EXIT" -ne 0 ]; then
|
|
1527
|
-
echo "PRIORITY: severity-to-priority.sh failed (exit $STAGING_FINDING_PRIORITY_EXIT) for severity '$STAGING_FINDING_SEVERITY' — skipping finding issue creation"
|
|
1528
|
-
# Skip this finding — do NOT fall through to issue creation with an empty label
|
|
1529
|
-
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):
|
|
1530
1170
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
#
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
rm -f "$STAGING_FINDING_BODY_FILE"
|
|
1537
|
-
|
|
1538
|
-
# /issue succeeds only after its API create-token read-back (Phase 4B). Its
|
|
1539
|
-
# explicit result marker distinguishes a verified create from an intentional
|
|
1540
|
-
# dedup STOP; do not use title search to mask a swallowed 403.
|
|
1541
|
-
ISSUE_NUM=$(printf '%s\n' "$ISSUE_SKILL_OUTPUT" | sed -n 's/.*ISSUE_CREATE_RESULT:CREATED number=\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1542
|
-
DEDUP_NUMBER=$(printf '%s\n' "$ISSUE_SKILL_OUTPUT" | sed -n 's/.*ISSUE_CREATE_RESULT:DEDUP number=\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1543
|
-
if [ -n "$DEDUP_NUMBER" ]; then
|
|
1544
|
-
ISSUE_NUM="$DEDUP_NUMBER"
|
|
1545
|
-
echo "Staging review finding deduped against existing issue #${ISSUE_NUM}."
|
|
1546
|
-
elif [ -z "$ISSUE_NUM" ]; then
|
|
1547
|
-
echo "ERROR: /issue did not report a verified staging review-finding number; stopping review instead of silently dropping the finding." >&2
|
|
1548
|
-
exit 1
|
|
1549
|
-
fi
|
|
1550
|
-
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
|
|
1551
1176
|
```
|
|
1552
1177
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
**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
|
|
1556
1179
|
|
|
1557
|
-
|
|
1558
|
-
### 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.
|
|
1559
1181
|
|
|
1560
1182
|
---
|
|
1561
1183
|
|
|
@@ -1569,7 +1191,7 @@ Post summary with verdict:
|
|
|
1569
1191
|
4. CONFIRMED HIGH blocking (crashes, data loss) → NEEDS FIXES FIRST
|
|
1570
1192
|
5. All else → APPROVE FOR DEPLOY
|
|
1571
1193
|
|
|
1572
|
-
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.
|
|
1573
1195
|
|
|
1574
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.
|
|
1575
1197
|
|
|
@@ -1611,7 +1233,8 @@ COST_SCRIPT=$(forge_resolve cost-report.sh)
|
|
|
1611
1233
|
```bash
|
|
1612
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() { :; }; }
|
|
1613
1235
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
1614
|
-
|
|
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=""
|
|
1615
1238
|
[ -n "$STATS_OUT" ] && printf '%s\n' "$STATS_OUT" || echo "Pipeline stats: unavailable"
|
|
1616
1239
|
```
|
|
1617
1240
|
|