forgedock 1.12.0 → 1.13.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/phases-args.test.mjs +2 -1
- package/bin/engine/phases.mjs +349 -90
- package/bin/engine/runlog.mjs +25 -2
- package/bin/engine-cli.mjs +4 -1
- package/bin/engine.mjs +72 -19
- package/bin/hooks/interactive-engine.mjs +135 -209
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +10 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +258 -23
- package/bin/watch-utils.mjs +1 -0
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +8 -5
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +14 -19
- package/commands/orchestrate/phase-4-execution.md +80 -53
- package/commands/orchestrate/phase-6-report.md +6 -0
- package/commands/orchestrate/safety.md +1 -1
- package/commands/pipeline-health/phase-2-metrics.md +39 -0
- package/commands/pipeline-health/phase-5-report.md +1 -0
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +2 -2
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +3 -3
- package/commands/review-pr-agents/protocols.md +15 -6
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +12 -12
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +394 -53
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +106 -7
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +137 -25
- package/commands/work-on/review.md +25 -9
- package/commands/work-on.md +15 -15
- package/docs/CONFIG.md +29 -0
- package/docs/RELEASE-NOTES.md +45 -0
- package/docs/spec/forge-protocol-v1.md +22 -1
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +20 -5
- package/package.json +2 -1
- package/packages/protocol/src/phases.js +21 -0
- package/scripts/check-blast-radius.sh +164 -0
- package/scripts/check-blast-radius.test.sh +183 -0
- package/scripts/check-contract-scope.sh +137 -0
- package/scripts/check-contract-scope.test.sh +83 -0
- package/scripts/check-spec-bash.sh +60 -3
- package/scripts/check-spec-bash.test.sh +56 -0
- package/scripts/check-spec-markers.sh +29 -0
- package/scripts/check-test-wiring.sh +57 -0
- package/scripts/check-test-wiring.test.sh +70 -0
- package/scripts/classify-finding.sh +72 -8
- package/scripts/classify-finding.test.sh +77 -8
- package/scripts/close-dossier-no-main-commit.test.sh +12 -12
- package/scripts/diff-size.test.sh +3 -2
- package/scripts/doctor-pipeline-state.sh +35 -0
- package/scripts/forge-annotation.sh +3 -0
- package/scripts/forge-root.test.sh +103 -13
- package/scripts/forge-run.sh +2 -2
- package/scripts/recovery-claim-live.test.sh +10 -7
- package/scripts/remediate-basesync.test.sh +103 -0
- package/scripts/review-delta.sh +206 -0
- package/scripts/review-delta.test.sh +213 -0
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/review-pr-gate2.test.sh +84 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
- package/scripts/run-shell-tests.sh +115 -0
- package/scripts/select-fix-targets.sh +17 -5
- package/scripts/transition-label.sh +3 -1
|
@@ -62,10 +62,16 @@ fi
|
|
|
62
62
|
# List files in your domain slice
|
|
63
63
|
gh pr diff [PR_NUMBER] --name-only
|
|
64
64
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
65
|
+
# Materialize the pre-computed domain diff slice supplied by the orchestrator.
|
|
66
|
+
# Persona greps below read "$SLICE_FILE" -- they never re-fetch the full diff.
|
|
67
|
+
SLICE_FILE=$(mktemp)
|
|
68
|
+
cat > "$SLICE_FILE" <<'FORGE_SLICE_EOF'
|
|
69
|
+
[DOMAIN_DIFF_SLICE]
|
|
70
|
+
FORGE_SLICE_EOF
|
|
67
71
|
```
|
|
68
72
|
|
|
73
|
+
**Empty or truncated slice**: if the slice is empty or ends mid-hunk, read the specific changed files with bounded reads (`sed -n 'X,Yp'`, `head -N`) — never fall back to fetching the full PR diff.
|
|
74
|
+
|
|
69
75
|
**Tool-result truncation**: When reading individual files or running commands for deeper investigation, always cap output: `cat file.py | head -200`, `grep ... | head -50`. Never pipe unbounded output into context.
|
|
70
76
|
|
|
71
77
|
**Hot-spot prior**:
|
|
@@ -112,7 +118,7 @@ If a file you are reviewing is listed above as a hot-spot, apply deeper scrutiny
|
|
|
112
118
|
3. Will this cause degraded performance? → **MEDIUM**
|
|
113
119
|
4. Is it genuinely cosmetic with no runtime impact after tracing all consumers? → **LOW**
|
|
114
120
|
|
|
115
|
-
|
|
121
|
+
**Precision first.** Report defects that are likely to be real in the code this PR changes and the code it reaches. If you cannot verify a runtime impact after tracing the path, report the finding at **POSSIBLE** confidence — do not inflate its severity or confidence to be safe. A false positive costs reviewer trust and triggers a cascade of follow-up issues and re-reviews; an unverified suspicion belongs at POSSIBLE, where it stays a non-blocking note, not at an inflated severity.
|
|
116
122
|
|
|
117
123
|
### 5. INTERACTION ANALYSIS — "Pre-existing" Is Not "Safe"
|
|
118
124
|
|
|
@@ -125,6 +131,8 @@ A redundant import, an unused variable, or a duplicated constant may be harmless
|
|
|
125
131
|
2. For each reference, ask: "Does the pre-existing construct cause this new line to fail at runtime?"
|
|
126
132
|
3. If yes → CONFIRMED finding, not a dismissal
|
|
127
133
|
|
|
134
|
+
**Severity rule**: CRITICAL/HIGH findings, any confidence, are never routed as pre-existing by the `/review-pr` introduced-line gate (§6B.4): they stay blocking `review-finding` issues. The `**Scope**` tag does not gate routing.
|
|
135
|
+
|
|
128
136
|
### 6. FALSE POSITIVE PREVENTION
|
|
129
137
|
|
|
130
138
|
**Before claiming variable scope issues:** Read the FULL function, count indentation levels, check if/else structure.
|
|
@@ -150,7 +158,7 @@ Every finding must include:
|
|
|
150
158
|
|
|
151
159
|
## Structured Findings Protocol
|
|
152
160
|
|
|
153
|
-
**All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot
|
|
161
|
+
**All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot classify, file, or track what you found. The orchestrator (`/review-pr` §6B.4/§6B.5) decides disposition: CONFIRMED/LIKELY findings that pass its gates become issues, while LOW severity and POSSIBLE confidence findings are non-blocking NOTEs that are not filed as standalone issues.
|
|
154
162
|
|
|
155
163
|
### Persist Before Post
|
|
156
164
|
|
|
@@ -168,7 +176,7 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
168
176
|
|
|
169
177
|
### Rules
|
|
170
178
|
|
|
171
|
-
1. **Include
|
|
179
|
+
1. **Include every finding you can support at CONFIRMED, LIKELY, or POSSIBLE confidence in the block** — the orchestrator needs them all to classify. Inclusion does not mean a GitHub issue is filed: `/review-pr` §6B.4/§6B.5 decide filing. **POSSIBLE confidence and LOW severity findings are non-blocking NOTEs** — reported in the review, never filed as standalone issues, never block merge. CONFIRMED and LIKELY findings are blocking at P1/P2 respectively. Do not pad the block with unverified or speculative items; precision over recall.
|
|
172
180
|
2. **One line per finding** — sequential numbering (PREFIX-1, PREFIX-2, ...)
|
|
173
181
|
3. **Confidence**: `CONFIRMED`, `LIKELY`, or `POSSIBLE`
|
|
174
182
|
4. **Severity**: `CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`
|
|
@@ -176,8 +184,9 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
176
184
|
6. **Summary**: Concise one-line description (no pipe `|` characters in summary)
|
|
177
185
|
7. **Empty block**: If no findings at all, include just the START/END markers
|
|
178
186
|
8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
|
|
179
|
-
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the
|
|
187
|
+
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the marker domain the orchestrator passes you (the canonical value from the roster-to-marker table in `review-pr.md`, e.g. `infra` and `scraper`; never a lowercased display name such as `infrastructure` or `scraping`).
|
|
180
188
|
10. **Reviewed SHA**: Include exactly one line `Reviewed-SHA: [REVIEW_SHA]` (the full 40-hex head you were pinned to) in the persisted body, outside the marker. `/review-pr` scopes its completion wait and idempotent re-entry to this line, so a comment without the current head does not count as a completed review. Do NOT put the SHA inside the `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker.
|
|
189
|
+
11. **Failure scenario and scope (provenance inputs)**: every finding's body section (the prose above the findings block) MUST carry a `**Failure scenario**` line — concrete inputs leading to a wrong output or behaviour — and a `**Scope**: changed` or `**Scope**: pre-existing` line (`changed` = the cited line is added or modified by this PR; `pre-existing` = it was already there). `/review-pr` §6B.4 reads these: a finding without a failure scenario is demoted to a note (unless CRITICAL/HIGH, any confidence), and a MEDIUM or lower finding whose cited line is not in the PR diff is routed as pre-existing (CRITICAL/HIGH never are; see §5). The `**Scope**` tag is informational and never gates routing. Speculative hardening with no failure scenario is a note, not a finding. The one-line `<!-- FINDING:... -->` grammar above is unchanged. <!-- Added: forge#3452 -->
|
|
181
190
|
|
|
182
191
|
### Domain Prefixes
|
|
183
192
|
|
|
@@ -58,7 +58,7 @@ If no domain context is configured above, derive the architecture from the chang
|
|
|
58
58
|
```bash
|
|
59
59
|
# Identify gate conditions in changed router files
|
|
60
60
|
ROUTER_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "(router|route|endpoint|handler)" | head -10)
|
|
61
|
-
|
|
61
|
+
awk -v files="$ROUTER_FILES" 'BEGIN{n=split(files,a,"\n");for(i=1;i<=n;i++)w[a[i]]=1} /^diff --git /{p=((substr($4,3)) in w)} p' "$SLICE_FILE" | grep "^\+" | grep -E "if request\.|if.*or.*request\." | head -20
|
|
62
62
|
|
|
63
63
|
# For each gated field, check its pre-existing handling path
|
|
64
64
|
git log --all --oneline -10 -- $ROUTER_FILES | head -10
|
|
@@ -214,7 +214,7 @@ For each hit: read the fallback value.
|
|
|
214
214
|
```bash
|
|
215
215
|
UID_CHANGE=$(while IFS= read -r f; do
|
|
216
216
|
[ -z "$f" ] && continue
|
|
217
|
-
|
|
217
|
+
awk -v f="$f" '/^diff --git /{p=(substr($4,3)==f)} p' "$SLICE_FILE" | grep -E '^\+.*(USER\s+[^0]|su-exec|gosu|setuid)' | grep -v '^+++' && break
|
|
218
218
|
done <<< "$DOCKERFILE_FILES")
|
|
219
219
|
if [ -n "$UID_CHANGE" ]; then
|
|
220
220
|
echo "UID change detected — cross-referencing named volume mounts..."
|
|
@@ -249,19 +249,19 @@ For each named volume found: determine its container mount point. Named volumes
|
|
|
249
249
|
8. **Fix PR input-format coverage** (when PR title contains "fix" and the diff patches a specific input format failure): When a fix PR addresses one documented failure mode (e.g., blank string → `[]`), search `.env.example` for ALL documented input format examples for the affected env var or config field. For each documented format that the fix does NOT handle, flag as CONFIRMED HIGH.
|
|
250
250
|
```bash
|
|
251
251
|
# Find the env var(s) touched by the fix
|
|
252
|
-
FIXED_VARS=$(
|
|
252
|
+
FIXED_VARS=$(grep -oE "ENABLED_[A-Z_]+|[A-Z_]{3,}(?=.*decode_complex_value|.*list\[str\]|.*List\[str\])" "$SLICE_FILE" | sort -u)
|
|
253
253
|
# For each var, read its .env.example comment for documented format examples
|
|
254
254
|
for var in $FIXED_VARS; do
|
|
255
255
|
grep -A5 "$var" .env.example 2>/dev/null | grep -iE "comma.separated|csv|json|space.separated|pipe.separated|semicolon.separated"
|
|
256
256
|
done
|
|
257
257
|
# Then read the fix itself — what format(s) does it handle?
|
|
258
|
-
|
|
258
|
+
grep -E "^\+" "$SLICE_FILE" | grep -iE "json\.loads|split\(','\)|split\(' '\)|csv|,\.join"
|
|
259
259
|
```
|
|
260
260
|
Compare documented formats against handled formats. Any documented format not handled by the fix is a gap. A fix that addresses one documented failure mode (e.g., blank string) while leaving another documented format (e.g., CSV) unhandled is an incomplete fix. <!-- Added: forge#190 -->
|
|
261
261
|
9. **Scope creep detection**: Compare the PR title/description scope against the actual diff size. When a fix PR's diff contains significantly more logic than its stated scope implies, there is a high risk that the extra code introduces bugs the reviewer is not primed to look for — and that the builder agent added context from a different branch or a different issue.
|
|
262
262
|
```bash
|
|
263
263
|
# Count lines added in the diff (excluding whitespace-only lines and file headers)
|
|
264
|
-
DIFF_LINES_ADDED=$(
|
|
264
|
+
DIFF_LINES_ADDED=$(grep -c '^+[^+]' "$SLICE_FILE" 2>/dev/null || echo 0)
|
|
265
265
|
PR_TITLE=$(gh pr view [PR_NUMBER] --json title --jq '.title')
|
|
266
266
|
PR_BODY=$(gh pr view [PR_NUMBER] --json body --jq '.body')
|
|
267
267
|
```
|
|
@@ -308,7 +308,7 @@ For each named volume found: determine its container mount point. Named volumes
|
|
|
308
308
|
ROUTER_DIR=$(gh pr diff [PR_NUMBER] --name-only | grep -E "routers/.*\.py$" | head -1 | xargs dirname 2>/dev/null)
|
|
309
309
|
|
|
310
310
|
# Extract removed condition lines from the diff
|
|
311
|
-
REMOVED_CONDITIONS=$(
|
|
311
|
+
REMOVED_CONDITIONS=$(grep -E '^-.*\bif\b.*\bor\b|^-.*\bif\b.*(and|:)\s*$' "$SLICE_FILE" | grep -v '^---' | sed 's/^-//' | head -5)
|
|
312
312
|
|
|
313
313
|
if [ -n "$ROUTER_DIR" ] && [ -n "$REMOVED_CONDITIONS" ]; then
|
|
314
314
|
echo "$REMOVED_CONDITIONS" | while IFS= read -r condition; do
|
|
@@ -46,17 +46,17 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
|
|
|
46
46
|
|
|
47
47
|
2. **Then read the persona file for each selected agent**:
|
|
48
48
|
|
|
49
|
-
| Agent | Trigger | File |
|
|
50
|
-
|
|
51
|
-
| General Security & Quality Scan | ALWAYS RUNS | `review-pr-agents/security.md` |
|
|
52
|
-
| Auth Conventions Auditor | AUTH domain | `review-pr-agents/auth.md` |
|
|
53
|
-
| Billing Integrity Auditor | BILLING domain | `review-pr-agents/billing.md` |
|
|
54
|
-
| Concurrency & Race Condition Auditor | CONCURRENCY or BILLING domain | `review-pr-agents/concurrency.md` |
|
|
55
|
-
| Domain Logic Auditor | SCRAPING domain (requires `review.domains.scraping` in forge.yaml) | `review-pr-agents/scraper.md` |
|
|
56
|
-
| Frontend Quality Auditor | WEB service touched | `review-pr-agents/frontend.md` |
|
|
57
|
-
| API Design & Consistency Auditor | New/modified routers or SDK/OpenAPI files | `review-pr-agents/api.md` |
|
|
58
|
-
| Database & Migration Auditor | DATABASE domain | `review-pr-agents/database.md` |
|
|
59
|
-
| Infrastructure & Deploy Safety Auditor | INFRA service touched | `review-pr-agents/infra.md` |
|
|
49
|
+
| Agent | Trigger | Marker domain | File |
|
|
50
|
+
|-------|---------|---------------|------|
|
|
51
|
+
| General Security & Quality Scan | ALWAYS RUNS | `security` | `review-pr-agents/security.md` |
|
|
52
|
+
| Auth Conventions Auditor | AUTH domain | `auth` | `review-pr-agents/auth.md` |
|
|
53
|
+
| Billing Integrity Auditor | BILLING domain | `billing` | `review-pr-agents/billing.md` |
|
|
54
|
+
| Concurrency & Race Condition Auditor | CONCURRENCY or BILLING domain | `concurrency` | `review-pr-agents/concurrency.md` |
|
|
55
|
+
| Domain Logic Auditor | SCRAPING domain (requires `review.domains.scraping` in forge.yaml) | `scraper` | `review-pr-agents/scraper.md` |
|
|
56
|
+
| Frontend Quality Auditor | WEB service touched | `frontend` | `review-pr-agents/frontend.md` |
|
|
57
|
+
| API Design & Consistency Auditor | New/modified routers or SDK/OpenAPI files | `api` | `review-pr-agents/api.md` |
|
|
58
|
+
| Database & Migration Auditor | DATABASE domain | `database` | `review-pr-agents/database.md` |
|
|
59
|
+
| Infrastructure & Deploy Safety Auditor | INFRA service touched | `infra` | `review-pr-agents/infra.md` |
|
|
60
60
|
|
|
61
61
|
## Domain Prefixes (for Structured Findings)
|
|
62
62
|
|
|
@@ -90,6 +90,6 @@ The `protocols.md` file contains:
|
|
|
90
90
|
- Per-Agent Input Scoping rules
|
|
91
91
|
- Tool-Result Truncation Discipline
|
|
92
92
|
- Evidence-Based Review Protocol (all agents follow)
|
|
93
|
-
- Structured Findings Protocol (machine-readable findings block format)
|
|
93
|
+
- Structured Findings Protocol (machine-readable findings block format; each finding also carries a `**Failure scenario**` and `**Scope**: changed|pre-existing`, consumed by `/review-pr` §6B.4)
|
|
94
94
|
|
|
95
95
|
**Spec root (MANDATORY)**: read every file named in this catalog from `${CLAUDE_PLUGIN_ROOT}` — the install root of the ForgeDock plugin that is running this command (Claude Code fills it in when it loads the spec). Only if that path does not start with `/` (not a Claude Code plugin session: install.sh, Codex, OpenCode) use `$FORGE_HOME` instead. Never read sub-files from `$FORGE_HOME` when the plugin root resolved: an exported `FORGE_HOME` can point at a different, older ForgeDock checkout, and mixing roots runs stale phase specs.
|
|
@@ -360,6 +360,89 @@ Categorize by service (API, Worker, Web, Shared, Infra). Identify high-risk file
|
|
|
360
360
|
|
|
361
361
|
Create review chunks by priority: Billing/Pricing (CRITICAL) → Security/Auth (CRITICAL) → Scraper Core (HIGH) → API Routers (HIGH) → Worker (HIGH) → Web (MEDIUM) → Shared (MEDIUM) → Infra (MEDIUM) → Other (LOW).
|
|
362
362
|
|
|
363
|
+
|
|
364
|
+
### 0B.1: Bundle Review Scope (already-reviewed commit set)
|
|
365
|
+
|
|
366
|
+
A bundle PR already got a full line-level review before it merged to staging, so re-reviewing its diff here re-files the same defects. Compute the already-reviewed commit set and scope line-level review to what a feature-PR review could not see. Consumes `ALL_PR_NUMBERS` (Phase 0A). Trust comes from `scripts/trusted-comments.sh` (resolved with the canonical helper-script resolver below: plugin root, `FORGE_ROOT`, `FORGEDOCK_HOME`, `FORGE_HOME`, install symlink, marketplaces, pinned plugin cache; never the working directory, which is author-controlled).
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
SCOPE_MODE=full; REVIEWED_COMMITS=""; REVIEWED_MERGES=""; DEPLOY_WIRING_FILES=""; UNREVIEWED_COMMITS=""; UNREVIEWED_FILES=""; _LOG=""; CROSS_PR_FILES=""; REVIEWED_PR_COUNT=0
|
|
370
|
+
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_SD"; _PF="$_SD/${PR_NUMBER}_bundle-pr-files.txt"; : > "$_PF"
|
|
371
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
372
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
373
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
374
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
375
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | awk -F/ '$NF ~ /^[0-9]+\.[0-9]+\.[0-9]+$/{split($NF,a,".");printf "%d %d %d %s\n",a[1],a[2],a[3],$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true)"
|
|
376
|
+
done
|
|
377
|
+
TRUSTED_SCRIPT=""
|
|
378
|
+
while IFS= read -r _c; do
|
|
379
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
380
|
+
done <<< "$_tc"
|
|
381
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$ALL_PR_NUMBERS" ]; then
|
|
382
|
+
SCOPE_MODE=scoped
|
|
383
|
+
for pr in $ALL_PR_NUMBERS; do
|
|
384
|
+
_J=$(gh pr view "$pr" -R {GH_REPO} --json headRefOid,mergeCommit,commits,state 2>/dev/null) || { SCOPE_MODE=full; break; }
|
|
385
|
+
_HEAD=$(printf '%s' "$_J" | jq -r '.headRefOid')
|
|
386
|
+
case "$_HEAD" in *[!0-9a-f]*|"") SCOPE_MODE=full; break ;; esac # a malformed head id would make the sha match accept any comment (fail open): review everything
|
|
387
|
+
[ "${#_HEAD}" -eq 40 ] || [ "${#_HEAD}" -eq 64 ] || { SCOPE_MODE=full; break; }
|
|
388
|
+
_HEAD7=$(printf '%s' "$_HEAD" | cut -c1-7)
|
|
389
|
+
# Capture each gh read into a variable FIRST and test its own exit status: in a pipeline `||` only sees the last stage (no pipefail),
|
|
390
|
+
# so a failed gh read piped into another command would silently look like "no comments" / "no files".
|
|
391
|
+
_CM=$(gh api --paginate "repos/{GH_REPO}/issues/$pr/comments" 2>/dev/null) || { SCOPE_MODE=full; break; }
|
|
392
|
+
_FILES=$(gh pr diff "$pr" -R {GH_REPO} --name-only 2>/dev/null) || { SCOPE_MODE=full; break; }
|
|
393
|
+
# Reviewed = trusted FORGE:REVIEW_ROUTE (a real review mode) whose sha is the PR's FINAL head, plus at least one review-agent comment
|
|
394
|
+
# carrying the full-length `Reviewed-SHA: <head>` line and a findings block (same predicate as review-pr.md Phase 5; a 7-char substring or a comment with no findings block never counts), and no degraded/blocked review marker. A PR pushed to after its review has a stale sha, so its commits stay UNREVIEWED.
|
|
395
|
+
_ROUTE=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^<!-- FORGE:REVIEW_ROUTE mode=(?!spec-evolution-blocked)[^ ]+ spec=review-pr.md sha=${_HEAD7} -->") || { SCOPE_MODE=full; break; }
|
|
396
|
+
_AGENT=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${_HEAD}(\\r?\\n|$)") || { SCOPE_MODE=full; break; }
|
|
397
|
+
_DEGRADED=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^<!-- FORGE:(REVIEW_DEGRADED|REVIEW_BLOCKED)") || { SCOPE_MODE=full; break; }
|
|
398
|
+
case "$_ROUTE$_AGENT$_DEGRADED" in ''|*[!0-9]*) SCOPE_MODE=full; break ;; esac
|
|
399
|
+
if [ "$_ROUTE" -ge 1 ] && [ "$_AGENT" -ge 1 ] && [ "$_DEGRADED" -eq 0 ] && [ "$(printf '%s' "$_J" | jq '.commits|length')" -lt 100 ]; then
|
|
400
|
+
REVIEWED_PR_COUNT=$((REVIEWED_PR_COUNT+1))
|
|
401
|
+
REVIEWED_COMMITS="$REVIEWED_COMMITS $(printf '%s' "$_J" | jq -r '[.commits[].oid, .mergeCommit.oid // empty] | join(" ")')"
|
|
402
|
+
REVIEWED_MERGES="$REVIEWED_MERGES $(printf '%s' "$_J" | jq -r '.mergeCommit.oid // empty')"
|
|
403
|
+
fi
|
|
404
|
+
printf '%s\n' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"
|
|
405
|
+
done
|
|
406
|
+
fi
|
|
407
|
+
if [ "$SCOPE_MODE" = "scoped" ]; then
|
|
408
|
+
# Cross-PR interactions: files touched by 2+ bundle PRs.
|
|
409
|
+
CROSS_PR_FILES=$(cut -d' ' -f2- "$_PF" | sort | uniq -d)
|
|
410
|
+
# Unreviewed commits: on staging, not in main, and not a commit of any reviewed PR (direct pushes, fast-lane fixes, post-review pushes).
|
|
411
|
+
# Merge commits are included: a merge that is not a reviewed PR's own merge commit may carry conflict-resolution content nobody reviewed.
|
|
412
|
+
_LOG=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --format=%H) || SCOPE_MODE=full
|
|
413
|
+
fi
|
|
414
|
+
if [ "$SCOPE_MODE" = "scoped" ]; then
|
|
415
|
+
for c in $_LOG; do
|
|
416
|
+
case " $REVIEWED_COMMITS " in *" $c "*) continue ;; esac
|
|
417
|
+
UNREVIEWED_COMMITS="$UNREVIEWED_COMMITS $c"
|
|
418
|
+
done
|
|
419
|
+
# Files of every unreviewed commit (--cc limits a merge commit to its conflict-resolution content), so 7B can test file membership.
|
|
420
|
+
# Each git read is captured and its own status tested (a failure here must widen scope, never narrow it).
|
|
421
|
+
UNREVIEWED_FILES=""
|
|
422
|
+
for c in $UNREVIEWED_COMMITS; do
|
|
423
|
+
_DT=$(git diff-tree --no-commit-id --name-only -r "$c") || { SCOPE_MODE=full; break; }
|
|
424
|
+
_DC=$(git diff-tree --no-commit-id --name-only -r --cc "$c") || { SCOPE_MODE=full; break; }
|
|
425
|
+
UNREVIEWED_FILES="$UNREVIEWED_FILES"$'\n'"$_DT"$'\n'"$_DC"
|
|
426
|
+
done
|
|
427
|
+
# A reviewed PR's own merge commit may carry conflict-resolution content its review never saw: count its --cc files as unreviewed too.
|
|
428
|
+
for c in $REVIEWED_MERGES; do
|
|
429
|
+
_DC=$(git diff-tree --no-commit-id --name-only -r --cc "$c") || { SCOPE_MODE=full; break; }
|
|
430
|
+
UNREVIEWED_FILES="$UNREVIEWED_FILES"$'\n'"$_DC"
|
|
431
|
+
done
|
|
432
|
+
UNREVIEWED_FILES=$(printf '%s\n' "$UNREVIEWED_FILES" | sed '/^$/d' | sort -u)
|
|
433
|
+
fi
|
|
434
|
+
# Deploy/runtime readiness files are always in scope: migrations, env, workflows, compose/infra, dependency manifests.
|
|
435
|
+
_DW=$(git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --name-only) || { SCOPE_MODE=full; _DW=""; }
|
|
436
|
+
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)
|
|
437
|
+
# Persist the results for 7B.5 (a later Bash call is a fresh shell). %q keeps multi-line file lists intact.
|
|
438
|
+
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"
|
|
439
|
+
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 .)"
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Fail-open (never silently review less)**: every `gh`, `git log`, `git diff`, `git diff-tree` and marker-count read above is captured into a variable and tested on its own exit status (never through a pipe, where `||` sees only the last stage). If the trust script cannot be resolved, any such read fails, or a marker count is not an integer, `SCOPE_MODE=full` and Phases 2-6 review the full bundle diff as before. Only `SCOPE_MODE=scoped` narrows review.
|
|
443
|
+
|
|
444
|
+
**Scoped line-level review set** = `CROSS_PR_FILES` + `UNREVIEWED_FILES` (the files of `UNREVIEWED_COMMITS`, including merge-commit conflict resolutions) + `DEPLOY_WIRING_FILES`. Files and commits that belong only to an already-reviewed PR (the already-reviewed set) are excluded from line-level review.
|
|
445
|
+
|
|
363
446
|
---
|
|
364
447
|
|
|
365
448
|
## Phase 1: Automated Checks
|
|
@@ -456,6 +539,8 @@ For fixable failures: checkout staging, apply fix, verify locally, commit as `fi
|
|
|
456
539
|
|
|
457
540
|
## Phase 2: Material Change Analysis
|
|
458
541
|
|
|
542
|
+
**Review scope (applies to Phases 2-6, from Phase 0B.1)**: when `SCOPE_MODE=scoped`, pass every agent `REVIEWED_COMMITS`, `UNREVIEWED_COMMITS`, `CROSS_PR_FILES` and `DEPLOY_WIRING_FILES` in its prompt. Line-level review is limited to `CROSS_PR_FILES`, the diffs of `UNREVIEWED_COMMITS`, and a runtime/deploy readiness checklist (required secrets and env vars present and documented, migration ordering and collisions, deploy wiring and workflow sibling drift). Agents report every finding they see, with its severity and file, whatever file or commit it lives in; a defect that lives only in an already-reviewed PR's files was visible to that PR's own review, so the 7B scope filter may demote it, but only when it is `MEDIUM`, `LOW` or `INFO`: a `CRITICAL` or `HIGH` finding is always reported and kept. Safety-domain agents (Security, Auth, Billing, Concurrency, Database) still read the whole diff of any in-scope file, not just the changed hunks. When `SCOPE_MODE=full`, review the full bundle diff. Report the scope used in the agent comment. <!-- Added: forge#3451 -->
|
|
543
|
+
|
|
459
544
|
Launch agent (model: {SUBAGENT_MODEL}) to analyze all commits since last deploy. Categorize as: NEW FEATURE, ENHANCEMENT, BUG FIX, REFACTOR, SECURITY, PERFORMANCE, INFRASTRUCTURE, DEPENDENCY. Separate user-facing vs internal. Document breaking changes and required pre-deploy actions.
|
|
460
545
|
|
|
461
546
|
**MANDATORY — post findings as PR comment**: Before its first post attempt, the agent MUST persist the final report to a uniquely named durable file, then post it with `--body-file`. It MUST also return verdict, finding count, and one line per finding to the orchestrator whether the post succeeds or fails; on failure, return the file path and stop without retrying. After completing analysis, the agent posts its full report directly to the PR:
|
|
@@ -565,7 +650,17 @@ fi
|
|
|
565
650
|
|
|
566
651
|
**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.
|
|
567
652
|
|
|
568
|
-
Launch domain-specific agents based on which domains have changes. Substitute PR diff commands with staging diff commands.
|
|
653
|
+
Launch domain-specific agents based on which domains have changes. Substitute PR diff commands with staging diff commands.
|
|
654
|
+
|
|
655
|
+
**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 diff in the pipeline. Compute the slices once, then substitute `[DOMAIN_DIFF_SLICE]` in each persona's prompt before dispatch:
|
|
656
|
+
```bash
|
|
657
|
+
# Fetched once; agents do NOT re-fetch it
|
|
658
|
+
FULL_DIFF=$(git diff "origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH" | head -c 100000)
|
|
659
|
+
# DIFF_SLICE_{DOMAIN}: same per-domain awk filters, fallback, and security-gets-full-diff rule as
|
|
660
|
+
# commands/review-pr.md "Domain Diff Slicing", applied to this FULL_DIFF. Cap each slice at ~100K chars.
|
|
661
|
+
DIFF_SLICE_SECURITY="$FULL_DIFF"
|
|
662
|
+
```
|
|
663
|
+
For each dispatched persona, substitute `[DOMAIN_DIFF_SLICE]` → its matching `$DIFF_SLICE_*` variable (an empty slice falls back to the capped `$FULL_DIFF`). 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.
|
|
569
664
|
|
|
570
665
|
**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:
|
|
571
666
|
```bash
|
|
@@ -792,7 +887,86 @@ Missing comments are an explicit failure, never evidence of no findings. Do not
|
|
|
792
887
|
From PR comments, extract structured findings (`<!-- FINDING:... -->`). If none found, scan for unstructured findings. If still 0 → skip to Phase 8.
|
|
793
888
|
|
|
794
889
|
### 7B: Filter & Deduplicate
|
|
795
|
-
Keep ALL findings (CONFIRMED/LIKELY/POSSIBLE). Deduplicate by file:
|
|
890
|
+
Keep ALL findings (CONFIRMED/LIKELY/POSSIBLE) for 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).
|
|
891
|
+
|
|
892
|
+
Sort: CONFIRMED first, then by severity.
|
|
893
|
+
|
|
894
|
+
### 7B.5: Note Disposition (same damper and classifier as `/review-pr` §6B.5)
|
|
895
|
+
|
|
896
|
+
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 resolver (`${CLAUDE_PLUGIN_ROOT}`, `FORGE_ROOT`, `FORGEDOCK_HOME`, pinned plugin cache under `CLAUDE_CONFIG_DIR` then `~/.claude`). There is no `$PWD` tier: the repo under review is author-controlled. The staging PR is not a finding fix, so `FINDING_LINEAGE=none`.
|
|
897
|
+
|
|
898
|
+
```bash
|
|
899
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
900
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
901
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
902
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | awk -F/ '$NF ~ /^[0-9]+\.[0-9]+\.[0-9]+$/{split($NF,a,".");printf "%d %d %d %s\n",a[1],a[2],a[3],$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true)"
|
|
903
|
+
done
|
|
904
|
+
CLASSIFY_SCRIPT=""
|
|
905
|
+
while IFS= read -r _c; do
|
|
906
|
+
case "$_c" in /*) [ -z "$CLASSIFY_SCRIPT" ] && [ -f "$_c/scripts/classify-finding.sh" ] && CLASSIFY_SCRIPT="$_c/scripts/classify-finding.sh" ;; esac
|
|
907
|
+
done <<< "$_tc"
|
|
908
|
+
# 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).
|
|
909
|
+
if [ -z "$CLASSIFY_SCRIPT" ]; then
|
|
910
|
+
echo "REVIEW_RESULT: status: BLOCKED, blocker: classifier unavailable (classify-finding.sh not resolvable through the trusted resolver)"
|
|
911
|
+
exit 0
|
|
912
|
+
fi
|
|
913
|
+
# Per-finding inputs come from each structured block: FINDING_ID, FINDING_SEVERITY, FINDING_CONFIDENCE, FINDING_AGENT, FINDING_TITLE, FINDING_BODY,
|
|
914
|
+
# FINDING_FILE (primary) and FINDING_PATHS (newline-separated, every cited file). Run THIS WHOLE BLOCK in ONE Bash call, looping over every deduped finding
|
|
915
|
+
# (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.
|
|
916
|
+
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_SD"
|
|
917
|
+
NOTE_LIST_FILE="$_SD/${PR_NUMBER}_notes.md"; : > "$NOTE_LIST_FILE"
|
|
918
|
+
NOTES_LISTED=0; NOTES_DROPPED=0; FINDINGS_FILED=0; OUT_OF_SCOPE=0
|
|
919
|
+
# 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).
|
|
920
|
+
SCOPE_MODE=full; CROSS_PR_FILES=""; UNREVIEWED_FILES=""; DEPLOY_WIRING_FILES=""
|
|
921
|
+
[ -r "$_SD/${PR_NUMBER}_scope.env" ] && . "$_SD/${PR_NUMBER}_scope.env"
|
|
922
|
+
classify_one() {
|
|
923
|
+
# FINDING_TEXT_FILE holds the finding text: the classifier's content-based safety exemption reads it, so an empty file silently disables the exemption.
|
|
924
|
+
local FINDING_TEXT_FILE; FINDING_TEXT_FILE=$(mktemp "$_SD/staging-finding.XXXXXX")
|
|
925
|
+
printf '%s\n%s\n%s\n' "${FINDING_TITLE:-}" "${FINDING_BODY:-}" "${FINDING_PATHS:-${FINDING_FILE:-}}" > "$FINDING_TEXT_FILE"
|
|
926
|
+
DISPOSITION=""
|
|
927
|
+
if [ -n "$CLASSIFY_SCRIPT" ]; then
|
|
928
|
+
DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
|
|
929
|
+
--agent "$FINDING_AGENT" --lineage none --text-file "$FINDING_TEXT_FILE") || DISPOSITION=""
|
|
930
|
+
fi
|
|
931
|
+
rm -f "$FINDING_TEXT_FILE"
|
|
932
|
+
# 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).
|
|
933
|
+
case "$DISPOSITION" in ISSUE*|NOTE*) ;; *) DISPOSITION="ISSUE classifier-error" ;; esac
|
|
934
|
+
# 7B scope rule (the two SCOPE_RULE lines are executed here and evaluated by scripts/review-pr-staging-scope.test.sh):
|
|
935
|
+
SEV=$(printf '%s' "$FINDING_SEVERITY" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]') # SCOPE_RULE
|
|
936
|
+
case "$SEV" in MEDIUM|LOW|INFO) OOS_DEMOTE=1 ;; *) OOS_DEMOTE=0 ;; esac # SCOPE_RULE
|
|
937
|
+
# Demote ONLY when scoped, the rule allows it, the classifier did not apply its safety exemption, and EVERY cited file is out of scope.
|
|
938
|
+
# A finding with no file, or with any file in scope, is never demoted. The demotion is recorded as a listed note, never dropped.
|
|
939
|
+
case "$DISPOSITION" in *safety-exemption*) OOS_DEMOTE=0 ;; esac
|
|
940
|
+
if [ "$SCOPE_MODE" = "scoped" ] && [ "$OOS_DEMOTE" = "1" ] && [ -n "${FINDING_PATHS:-${FINDING_FILE:-}}" ]; then
|
|
941
|
+
local _ANY_IN=false _f
|
|
942
|
+
while IFS= read -r _f; do
|
|
943
|
+
[ -n "$_f" ] || continue
|
|
944
|
+
printf '%s\n' "$CROSS_PR_FILES" "$UNREVIEWED_FILES" "$DEPLOY_WIRING_FILES" | grep -qxF -- "$_f" && _ANY_IN=true
|
|
945
|
+
done <<< "${FINDING_PATHS:-$FINDING_FILE}"
|
|
946
|
+
if [ "$_ANY_IN" = "false" ]; then DISPOSITION="NOTE out_of_scope"; OUT_OF_SCOPE=$((OUT_OF_SCOPE+1)); fi
|
|
947
|
+
fi
|
|
948
|
+
case "$DISPOSITION" in
|
|
949
|
+
NOTE*) NOTES_LISTED=$((NOTES_LISTED+1)); printf '%s\n' "- ${FINDING_ID:-?} ${FINDING_FILE:-?} [${SEV:-?}] ${FINDING_TITLE:-}: ${DISPOSITION}" >> "$NOTE_LIST_FILE" ;;
|
|
950
|
+
*) FINDINGS_FILED=$((FINDINGS_FILED+1)) ;; # classified ISSUE; 7E dedup may still skip creation
|
|
951
|
+
esac
|
|
952
|
+
}
|
|
953
|
+
# for each deduped finding: set the FINDING_* inputs, call classify_one, then use $DISPOSITION (ISSUE continues to 7E/7F).
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
If the guard printed `REVIEW_RESULT: status: BLOCKED` (`classifier unavailable`), STOP: `exit 0` only ends that Bash block, so do not run 7E/7F or any later phase, file no issues, emit no deploy approval, and report `RESULT: BLOCK DEPLOY` with the blocker.
|
|
957
|
+
|
|
958
|
+
Only `ISSUE` findings continue to 7E/7F. Each `NOTE` is listed in the PR body (`## Non-blocking notes`, edit never replace) or dropped as a duplicate or nit; NOTEs are NEVER passed to `Skill(issue)`. Post one disposition record (counts only plus the NOTE list; every NOTE is recorded, never silently dropped), whenever 7A extracted at least one finding:
|
|
959
|
+
|
|
960
|
+
```bash
|
|
961
|
+
CLASSIFIER_MODE=script # the 7B.5 guard blocks the review when the classifier is unresolvable, so this is always script
|
|
962
|
+
_DISP_BODY="$_SD/${PR_NUMBER}_note_disposition.md"
|
|
963
|
+
{ 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} lineage=none classifier=${CLASSIFIER_MODE} scope=${SCOPE_MODE} -->"; cat "$NOTE_LIST_FILE" 2>/dev/null; } > "$_DISP_BODY"
|
|
964
|
+
gh pr comment ${PR_NUMBER} -R {GH_REPO} --body-file "$_DISP_BODY" # allowlist:check-command-side-effects
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### 7B.6: Provenance and concreteness gates (forge#3452)
|
|
968
|
+
|
|
969
|
+
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.
|
|
796
970
|
|
|
797
971
|
### 7C: Ensure Labels
|
|
798
972
|
```bash
|
|
@@ -800,6 +974,7 @@ Keep ALL findings (CONFIRMED/LIKELY/POSSIBLE). Deduplicate by file:line (keep hi
|
|
|
800
974
|
# Run `npx forgedock labels setup` to bootstrap all managed labels at once.
|
|
801
975
|
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
|
|
802
976
|
gh label create "needs-validation" --color "FBCA04" --description "Review finding awaiting human validation. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
|
|
977
|
+
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
|
|
803
978
|
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
|
|
804
979
|
```
|
|
805
980
|
|
|
@@ -824,21 +999,62 @@ MILESTONE_TITLE=$(bash scripts/derive-finding-milestone.sh "${PR_NUMBER}" -R {GH
|
|
|
824
999
|
|
|
825
1000
|
Plain staging→main reviews resolve `CODE_BRANCH` to `staging` dynamically (same value as before, now derived rather than hardcoded) and typically resolve no milestone. A milestone→staging review resolves `CODE_BRANCH` to `milestone/X` and (via the shared script) the matching milestone.
|
|
826
1001
|
|
|
827
|
-
### 7E: Deduplicate Against Existing Issues
|
|
828
|
-
|
|
1002
|
+
### 7E: Deduplicate Against Existing Issues (BEFORE every filing)
|
|
1003
|
+
|
|
1004
|
+
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:
|
|
1005
|
+
|
|
1006
|
+
```bash
|
|
1007
|
+
# FINDING_FILE/FINDING_LINE come from the finding's structured block. Define the title and scratch dir BEFORE use (7F reuses them).
|
|
1008
|
+
SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$SCRATCHPAD"
|
|
1009
|
+
STAGING_FINDING_TITLE="chore: ${FINDING_TITLE:-[summary]} (staging review — PR #${PR_NUMBER})"
|
|
1010
|
+
EXISTING_ISSUE_EXCLUDE="${EXISTING_ISSUE_EXCLUDE:-}" # optional comma-separated issue numbers already linked to this bundle
|
|
1011
|
+
_SAFE_FILE=$(printf '%s' "$FINDING_FILE" | tr -d '"\\')
|
|
1012
|
+
# issue-dedup.sh ships with ForgeDock next to classify-finding.sh (resolved in 7B.5, install-root only, never the repo under review).
|
|
1013
|
+
DEDUP_SCRIPT=""; [ -n "${CLASSIFY_SCRIPT:-}" ] && [ -f "$(dirname "$CLASSIFY_SCRIPT")/issue-dedup.sh" ] && DEDUP_SCRIPT="$(dirname "$CLASSIFY_SCRIPT")/issue-dedup.sh"
|
|
1014
|
+
if [ -n "$DEDUP_SCRIPT" ]; then
|
|
1015
|
+
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
|
|
1016
|
+
DEDUP_EXIT=$?
|
|
1017
|
+
else
|
|
1018
|
+
DEDUP_EXIT=127 # dedup unavailable
|
|
1019
|
+
fi
|
|
1020
|
+
# 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
|
|
1021
|
+
# 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.
|
|
1022
|
+
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
1023
|
+
_DUP_N=$(printf '%s' "$DEDUP_OUT" | sed -n 's/^DUPLICATE: #\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1024
|
+
_DUP_BODY=$(gh issue view "${_DUP_N:-0}" -R {GH_REPO} --json body --jq '.body' 2>/dev/null) || _DUP_BODY=""
|
|
1025
|
+
printf '%s' "$_DUP_BODY" | grep -qF -- "$_SAFE_FILE" || DEDUP_EXIT=0
|
|
1026
|
+
fi
|
|
1027
|
+
# 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.
|
|
1028
|
+
_FILE_RE=$(printf '%s' "$_SAFE_FILE" | sed 's/[][\.^$*+?(){}|\/]/\\&/g')
|
|
1029
|
+
REGRESSION_CHECK=ok; CLOSED_HIT=""
|
|
1030
|
+
# Capture the gh read first and test its own status: through a pipe a failed read looks like "no closed hit".
|
|
1031
|
+
_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
|
|
1032
|
+
# FINDING_LINE may be a range such as 12-15: use its first integer, and 0 when there is none.
|
|
1033
|
+
_L=$(printf '%s' "${FINDING_LINE:-0}" | grep -oE '[0-9]+' | head -1); _L="${_L:-0}"
|
|
1034
|
+
_HITS=""; [ "$REGRESSION_CHECK" = "ok" ] && { _HITS=$(printf '%s' "$_CL" | jq -r --arg re "$_FILE_RE" --argjson l "$_L" '.[] | select((.body // "") != "") | select(.stateReason != "NOT_PLANNED")
|
|
1035
|
+
| select([.body | scan($re + "[:#L]+([0-9]+)") | .[0] | tonumber | select((. - $l) <= 5 and ($l - .) <= 5)] | length > 0) | "#\(.number) \(.title)"') || REGRESSION_CHECK=unavailable; }
|
|
1036
|
+
CLOSED_HIT=$(printf '%s\n' "$_HITS" | sed '/^$/d' | head -3)
|
|
1037
|
+
# Confirm the 3+ shared title keywords against each hit before treating it as a regression.
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
- `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.
|
|
1041
|
+
- `REGRESSION_CHECK=unavailable` (the closed-issue read or its filter failed): record `regression-check-unavailable` in the Phase 8 summary; it never suppresses filing.
|
|
1042
|
+
- `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.
|
|
1043
|
+
- Record every deduped item (finding id, matched issue) for the Phase 8 summary and audit.
|
|
829
1044
|
|
|
830
1045
|
### 7F: Create Issues
|
|
831
1046
|
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.
|
|
832
1047
|
|
|
833
|
-
**For each finding** (
|
|
1048
|
+
**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:
|
|
834
1049
|
```bash
|
|
835
|
-
STAGING_FINDING_TITLE
|
|
1050
|
+
# STAGING_FINDING_TITLE was built in 7E; re-assert it here if 7E was skipped.
|
|
1051
|
+
STAGING_FINDING_TITLE="${STAGING_FINDING_TITLE:-chore: [summary] (staging review — PR #${PR_NUMBER})}"
|
|
836
1052
|
# Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
|
|
837
1053
|
# an xargs-based tokenizer that never expands backtick/$(...) substitution, so
|
|
838
1054
|
# this is no longer required for safety — but strip it anyway so the raw title
|
|
839
1055
|
# stays readable if it round-trips through any other eval-based consumer.
|
|
840
1056
|
STAGING_FINDING_TITLE=$(printf '%s' "$STAGING_FINDING_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
|
|
841
|
-
SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
|
|
1057
|
+
SCRATCHPAD="${SCRATCHPAD:-${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}}"
|
|
842
1058
|
REVIEW_AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-reviewer}-$$}"
|
|
843
1059
|
mkdir -p "$SCRATCHPAD"
|
|
844
1060
|
STAGING_FINDING_BODY_MARKER="FORGE:BODY-INTEGRITY:${PR_NUMBER}_staging-review_${REVIEW_AGENT_TOKEN}"
|
|
@@ -924,7 +1140,7 @@ fi
|
|
|
924
1140
|
|
|
925
1141
|
Labels: `review-finding` + `needs-validation` + `staging-review` + priority. `priority:*` is derived from the finding's `**Severity**` field via `scripts/severity-to-priority.sh` (identical script used by `commands/review-pr.md` — single documented mapping, see that script's header comment): `CRITICAL` → `priority:P0`, `HIGH` → `priority:P1`, `MEDIUM` → `priority:P2`, `LOW` → `priority:P3`. **Never derive `priority:*` from Confidence** (CONFIRMED/LIKELY/POSSIBLE) — conflating the two axes previously mislabeled LOW-severity CONFIRMED findings as `priority:P1`, defeating orchestrate's P3 batching rule. <!-- forge#2447 -->
|
|
926
1142
|
|
|
927
|
-
**
|
|
1143
|
+
**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.
|
|
928
1144
|
|
|
929
1145
|
### 7G: Add to Project Board
|
|
930
1146
|
### 7H: Update PR Description with Findings Table
|
|
@@ -941,6 +1157,8 @@ Post summary with verdict:
|
|
|
941
1157
|
4. CONFIRMED HIGH blocking (crashes, data loss) → NEEDS FIXES FIRST
|
|
942
1158
|
5. All else → APPROVE FOR DEPLOY
|
|
943
1159
|
|
|
1160
|
+
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 (7E). **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.
|
|
1161
|
+
|
|
944
1162
|
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.
|
|
945
1163
|
|
|
946
1164
|
**Risk Matrix must include a Test Gate row** (use `$TEST_GATE_VERDICT` and `$TEST_GATE_REASON` set in Phase 6.5):
|