forgedock 1.12.1 → 1.13.1
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.mjs +54 -12
- package/bin/engine/runlog.mjs +12 -0
- package/bin/engine-cli.mjs +3 -1
- package/bin/engine.mjs +64 -16
- package/bin/hooks/interactive-engine.mjs +135 -212
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +5 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/runner.mjs +234 -21
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +7 -4
- 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 +50 -38
- 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/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +9 -3
- package/commands/review-pr-agents/protocols.md +19 -5
- package/commands/review-pr-agents/scraper.md +4 -1
- package/commands/review-pr-agents/security.md +17 -5
- package/commands/review-pr-agents.md +1 -1
- package/commands/review-pr-staging.md +302 -8
- package/commands/review-pr.md +254 -34
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +95 -3
- package/commands/work-on/remediate.md +115 -13
- package/commands/work-on.md +4 -4
- 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/review-protocol.md +24 -4
- package/package.json +2 -1
- 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 +70 -1
- 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-gate2.test.sh +97 -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
|
@@ -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,93 @@ 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. Slice the uncapped diff first, then cap each slice at ~100K chars with `truncate_slice` (same order as `commands/review-pr.md` "Domain Diff Slicing"). Compute the slices once, then substitute `[DOMAIN_DIFF_SLICE]` in each persona's prompt before dispatch:
|
|
656
|
+
```bash
|
|
657
|
+
# Fetched once, UNCAPPED; agents do NOT re-fetch it. Never cap here: slicing must see the whole bundle.
|
|
658
|
+
FULL_DIFF=$(git diff "origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH")
|
|
659
|
+
|
|
660
|
+
# --- Security agent: always receives the full diff (cross-cutting domain) ---
|
|
661
|
+
DIFF_SLICE_SECURITY="$FULL_DIFF"
|
|
662
|
+
|
|
663
|
+
# --- Auth agent: auth, session, jwt, oauth, middleware, permission files ---
|
|
664
|
+
DIFF_SLICE_AUTH=$(echo "$FULL_DIFF" | awk '
|
|
665
|
+
/^diff --git/ { in_block=0 }
|
|
666
|
+
/^diff --git.*\/(auth|session|jwt|oauth|permission|middleware|login|token)/ { in_block=1 }
|
|
667
|
+
in_block { print }
|
|
668
|
+
')
|
|
669
|
+
[ -z "$DIFF_SLICE_AUTH" ] && DIFF_SLICE_AUTH="$FULL_DIFF"
|
|
670
|
+
|
|
671
|
+
# --- Billing agent: billing, payment, credit, pricing, stripe, subscription files ---
|
|
672
|
+
DIFF_SLICE_BILLING=$(echo "$FULL_DIFF" | awk '
|
|
673
|
+
/^diff --git/ { in_block=0 }
|
|
674
|
+
/^diff --git.*\/(billing|payment|credit|pricing|stripe|subscription|invoice|charge|refund)/ { in_block=1 }
|
|
675
|
+
in_block { print }
|
|
676
|
+
')
|
|
677
|
+
[ -z "$DIFF_SLICE_BILLING" ] && DIFF_SLICE_BILLING="$FULL_DIFF"
|
|
678
|
+
|
|
679
|
+
# --- Concurrency agent: transaction, lock, queue, async, worker, race-condition files ---
|
|
680
|
+
DIFF_SLICE_CONCURRENCY=$(echo "$FULL_DIFF" | awk '
|
|
681
|
+
/^diff --git/ { in_block=0 }
|
|
682
|
+
/^diff --git.*\/(worker|queue|task|async|lock|transaction|pipeline|job)/ { in_block=1 }
|
|
683
|
+
in_block { print }
|
|
684
|
+
')
|
|
685
|
+
[ -z "$DIFF_SLICE_CONCURRENCY" ] && DIFF_SLICE_CONCURRENCY="$FULL_DIFF"
|
|
686
|
+
|
|
687
|
+
# --- Database agent: migration, model, schema, ORM, SQL files ---
|
|
688
|
+
DIFF_SLICE_DATABASE=$(echo "$FULL_DIFF" | awk '
|
|
689
|
+
/^diff --git/ { in_block=0 }
|
|
690
|
+
/^diff --git.*\/(migration|model|schema|\.sql$|orm|database|db)/ { in_block=1 }
|
|
691
|
+
in_block { print }
|
|
692
|
+
')
|
|
693
|
+
[ -z "$DIFF_SLICE_DATABASE" ] && DIFF_SLICE_DATABASE="$FULL_DIFF"
|
|
694
|
+
|
|
695
|
+
# --- API Design agent: router, endpoint, openapi, schema, serializer files ---
|
|
696
|
+
DIFF_SLICE_API=$(echo "$FULL_DIFF" | awk '
|
|
697
|
+
/^diff --git/ { in_block=0 }
|
|
698
|
+
/^diff --git.*\/(router|route|endpoint|openapi|serializer|schema|api)/ { in_block=1 }
|
|
699
|
+
in_block { print }
|
|
700
|
+
')
|
|
701
|
+
[ -z "$DIFF_SLICE_API" ] && DIFF_SLICE_API="$FULL_DIFF"
|
|
702
|
+
|
|
703
|
+
# --- Infrastructure agent: docker, nginx, ci, deploy, infra, workflow files ---
|
|
704
|
+
DIFF_SLICE_INFRA=$(echo "$FULL_DIFF" | awk '
|
|
705
|
+
/^diff --git/ { in_block=0 }
|
|
706
|
+
/^diff --git.*\/(docker|nginx|infra|\.github|deploy|ci|traefik|k8s|helm)/ { in_block=1 }
|
|
707
|
+
in_block { print }
|
|
708
|
+
')
|
|
709
|
+
[ -z "$DIFF_SLICE_INFRA" ] && DIFF_SLICE_INFRA="$FULL_DIFF"
|
|
710
|
+
|
|
711
|
+
# --- Domain Logic agent: scraping/browser-automation files (does not match bare playwright/E2E test files) ---
|
|
712
|
+
DIFF_SLICE_SCRAPER=$(echo "$FULL_DIFF" | awk '
|
|
713
|
+
/^diff --git/ { in_block=0 }
|
|
714
|
+
/^diff --git.*\/(scrape|scraper|stealth|browser_pool|headless|anti_bot|captcha|crawl)/ { in_block=1 }
|
|
715
|
+
in_block { print }
|
|
716
|
+
')
|
|
717
|
+
[ -z "$DIFF_SLICE_SCRAPER" ] && DIFF_SLICE_SCRAPER="$FULL_DIFF"
|
|
718
|
+
|
|
719
|
+
# Cap each slice AFTER slicing and after the empty-slice fallback (a fallback slice is the truncated full diff)
|
|
720
|
+
truncate_slice() {
|
|
721
|
+
local slice="${1}"
|
|
722
|
+
local limit=102400
|
|
723
|
+
if [ "${#slice}" -gt "$limit" ]; then
|
|
724
|
+
echo "${slice:0:$limit}"
|
|
725
|
+
echo "... [TRUNCATED — diff exceeded 100K chars; focus on the files listed above]"
|
|
726
|
+
else
|
|
727
|
+
echo "$slice"
|
|
728
|
+
fi
|
|
729
|
+
}
|
|
730
|
+
DIFF_SLICE_SECURITY=$(truncate_slice "$DIFF_SLICE_SECURITY")
|
|
731
|
+
DIFF_SLICE_AUTH=$(truncate_slice "$DIFF_SLICE_AUTH")
|
|
732
|
+
DIFF_SLICE_BILLING=$(truncate_slice "$DIFF_SLICE_BILLING")
|
|
733
|
+
DIFF_SLICE_CONCURRENCY=$(truncate_slice "$DIFF_SLICE_CONCURRENCY")
|
|
734
|
+
DIFF_SLICE_DATABASE=$(truncate_slice "$DIFF_SLICE_DATABASE")
|
|
735
|
+
DIFF_SLICE_API=$(truncate_slice "$DIFF_SLICE_API")
|
|
736
|
+
DIFF_SLICE_INFRA=$(truncate_slice "$DIFF_SLICE_INFRA")
|
|
737
|
+
DIFF_SLICE_SCRAPER=$(truncate_slice "$DIFF_SLICE_SCRAPER")
|
|
738
|
+
```
|
|
739
|
+
For each dispatched persona, substitute `[DOMAIN_DIFF_SLICE]` → its matching `$DIFF_SLICE_*` variable (an empty slice falls back to the full diff, which is then truncated like every other slice). Also substitute `[AGENT_DOMAIN]` → the persona's marker domain (the `{domain}` used in its `FORGE:REVIEW-AGENT` marker), and keep `[PR_NUMBER]` / `[REVIEW_SHA_SHORT]` bound to the staging review's PR number and short head SHA (`staging` as the key when there is no PR number): personas derive their slice path `forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff` from them. 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
740
|
|
|
570
741
|
**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
742
|
```bash
|
|
@@ -792,7 +963,86 @@ Missing comments are an explicit failure, never evidence of no findings. Do not
|
|
|
792
963
|
From PR comments, extract structured findings (`<!-- FINDING:... -->`). If none found, scan for unstructured findings. If still 0 → skip to Phase 8.
|
|
793
964
|
|
|
794
965
|
### 7B: Filter & Deduplicate
|
|
795
|
-
Keep ALL findings (CONFIRMED/LIKELY/POSSIBLE). Deduplicate by file:
|
|
966
|
+
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).
|
|
967
|
+
|
|
968
|
+
Sort: CONFIRMED first, then by severity.
|
|
969
|
+
|
|
970
|
+
### 7B.5: Note Disposition (same damper and classifier as `/review-pr` §6B.5)
|
|
971
|
+
|
|
972
|
+
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`.
|
|
973
|
+
|
|
974
|
+
```bash
|
|
975
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
976
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
977
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
978
|
+
[ -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)"
|
|
979
|
+
done
|
|
980
|
+
CLASSIFY_SCRIPT=""
|
|
981
|
+
while IFS= read -r _c; do
|
|
982
|
+
case "$_c" in /*) [ -z "$CLASSIFY_SCRIPT" ] && [ -f "$_c/scripts/classify-finding.sh" ] && CLASSIFY_SCRIPT="$_c/scripts/classify-finding.sh" ;; esac
|
|
983
|
+
done <<< "$_tc"
|
|
984
|
+
# 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).
|
|
985
|
+
if [ -z "$CLASSIFY_SCRIPT" ]; then
|
|
986
|
+
echo "REVIEW_RESULT: status: BLOCKED, blocker: classifier unavailable (classify-finding.sh not resolvable through the trusted resolver)"
|
|
987
|
+
exit 0
|
|
988
|
+
fi
|
|
989
|
+
# Per-finding inputs come from each structured block: FINDING_ID, FINDING_SEVERITY, FINDING_CONFIDENCE, FINDING_AGENT, FINDING_TITLE, FINDING_BODY,
|
|
990
|
+
# FINDING_FILE (primary) and FINDING_PATHS (newline-separated, every cited file). Run THIS WHOLE BLOCK in ONE Bash call, looping over every deduped finding
|
|
991
|
+
# (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.
|
|
992
|
+
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_SD"
|
|
993
|
+
NOTE_LIST_FILE="$_SD/${PR_NUMBER}_notes.md"; : > "$NOTE_LIST_FILE"
|
|
994
|
+
NOTES_LISTED=0; NOTES_DROPPED=0; FINDINGS_FILED=0; OUT_OF_SCOPE=0
|
|
995
|
+
# 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).
|
|
996
|
+
SCOPE_MODE=full; CROSS_PR_FILES=""; UNREVIEWED_FILES=""; DEPLOY_WIRING_FILES=""
|
|
997
|
+
[ -r "$_SD/${PR_NUMBER}_scope.env" ] && . "$_SD/${PR_NUMBER}_scope.env"
|
|
998
|
+
classify_one() {
|
|
999
|
+
# FINDING_TEXT_FILE holds the finding text: the classifier's content-based safety exemption reads it, so an empty file silently disables the exemption.
|
|
1000
|
+
local FINDING_TEXT_FILE; FINDING_TEXT_FILE=$(mktemp "$_SD/staging-finding.XXXXXX")
|
|
1001
|
+
printf '%s\n%s\n%s\n' "${FINDING_TITLE:-}" "${FINDING_BODY:-}" "${FINDING_PATHS:-${FINDING_FILE:-}}" > "$FINDING_TEXT_FILE"
|
|
1002
|
+
DISPOSITION=""
|
|
1003
|
+
if [ -n "$CLASSIFY_SCRIPT" ]; then
|
|
1004
|
+
DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
|
|
1005
|
+
--agent "$FINDING_AGENT" --lineage none --text-file "$FINDING_TEXT_FILE") || DISPOSITION=""
|
|
1006
|
+
fi
|
|
1007
|
+
rm -f "$FINDING_TEXT_FILE"
|
|
1008
|
+
# 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).
|
|
1009
|
+
case "$DISPOSITION" in ISSUE*|NOTE*) ;; *) DISPOSITION="ISSUE classifier-error" ;; esac
|
|
1010
|
+
# 7B scope rule (the two SCOPE_RULE lines are executed here and evaluated by scripts/review-pr-staging-scope.test.sh):
|
|
1011
|
+
SEV=$(printf '%s' "$FINDING_SEVERITY" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]') # SCOPE_RULE
|
|
1012
|
+
case "$SEV" in MEDIUM|LOW|INFO) OOS_DEMOTE=1 ;; *) OOS_DEMOTE=0 ;; esac # SCOPE_RULE
|
|
1013
|
+
# Demote ONLY when scoped, the rule allows it, the classifier did not apply its safety exemption, and EVERY cited file is out of scope.
|
|
1014
|
+
# A finding with no file, or with any file in scope, is never demoted. The demotion is recorded as a listed note, never dropped.
|
|
1015
|
+
case "$DISPOSITION" in *safety-exemption*) OOS_DEMOTE=0 ;; esac
|
|
1016
|
+
if [ "$SCOPE_MODE" = "scoped" ] && [ "$OOS_DEMOTE" = "1" ] && [ -n "${FINDING_PATHS:-${FINDING_FILE:-}}" ]; then
|
|
1017
|
+
local _ANY_IN=false _f
|
|
1018
|
+
while IFS= read -r _f; do
|
|
1019
|
+
[ -n "$_f" ] || continue
|
|
1020
|
+
printf '%s\n' "$CROSS_PR_FILES" "$UNREVIEWED_FILES" "$DEPLOY_WIRING_FILES" | grep -qxF -- "$_f" && _ANY_IN=true
|
|
1021
|
+
done <<< "${FINDING_PATHS:-$FINDING_FILE}"
|
|
1022
|
+
if [ "$_ANY_IN" = "false" ]; then DISPOSITION="NOTE out_of_scope"; OUT_OF_SCOPE=$((OUT_OF_SCOPE+1)); fi
|
|
1023
|
+
fi
|
|
1024
|
+
case "$DISPOSITION" in
|
|
1025
|
+
NOTE*) NOTES_LISTED=$((NOTES_LISTED+1)); printf '%s\n' "- ${FINDING_ID:-?} ${FINDING_FILE:-?} [${SEV:-?}] ${FINDING_TITLE:-}: ${DISPOSITION}" >> "$NOTE_LIST_FILE" ;;
|
|
1026
|
+
*) FINDINGS_FILED=$((FINDINGS_FILED+1)) ;; # classified ISSUE; 7E dedup may still skip creation
|
|
1027
|
+
esac
|
|
1028
|
+
}
|
|
1029
|
+
# for each deduped finding: set the FINDING_* inputs, call classify_one, then use $DISPOSITION (ISSUE continues to 7E/7F).
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
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.
|
|
1033
|
+
|
|
1034
|
+
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:
|
|
1035
|
+
|
|
1036
|
+
```bash
|
|
1037
|
+
CLASSIFIER_MODE=script # the 7B.5 guard blocks the review when the classifier is unresolvable, so this is always script
|
|
1038
|
+
_DISP_BODY="$_SD/${PR_NUMBER}_note_disposition.md"
|
|
1039
|
+
{ 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"
|
|
1040
|
+
gh pr comment ${PR_NUMBER} -R {GH_REPO} --body-file "$_DISP_BODY" # allowlist:check-command-side-effects
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### 7B.6: Provenance and concreteness gates (forge#3452)
|
|
1044
|
+
|
|
1045
|
+
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
1046
|
|
|
797
1047
|
### 7C: Ensure Labels
|
|
798
1048
|
```bash
|
|
@@ -800,6 +1050,7 @@ Keep ALL findings (CONFIRMED/LIKELY/POSSIBLE). Deduplicate by file:line (keep hi
|
|
|
800
1050
|
# Run `npx forgedock labels setup` to bootstrap all managed labels at once.
|
|
801
1051
|
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
1052
|
gh label create "needs-validation" --color "FBCA04" --description "Review finding awaiting human validation. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
|
|
1053
|
+
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
1054
|
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
1055
|
```
|
|
805
1056
|
|
|
@@ -824,21 +1075,62 @@ MILESTONE_TITLE=$(bash scripts/derive-finding-milestone.sh "${PR_NUMBER}" -R {GH
|
|
|
824
1075
|
|
|
825
1076
|
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
1077
|
|
|
827
|
-
### 7E: Deduplicate Against Existing Issues
|
|
828
|
-
|
|
1078
|
+
### 7E: Deduplicate Against Existing Issues (BEFORE every filing)
|
|
1079
|
+
|
|
1080
|
+
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:
|
|
1081
|
+
|
|
1082
|
+
```bash
|
|
1083
|
+
# FINDING_FILE/FINDING_LINE come from the finding's structured block. Define the title and scratch dir BEFORE use (7F reuses them).
|
|
1084
|
+
SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$SCRATCHPAD"
|
|
1085
|
+
STAGING_FINDING_TITLE="chore: ${FINDING_TITLE:-[summary]} (staging review — PR #${PR_NUMBER})"
|
|
1086
|
+
EXISTING_ISSUE_EXCLUDE="${EXISTING_ISSUE_EXCLUDE:-}" # optional comma-separated issue numbers already linked to this bundle
|
|
1087
|
+
_SAFE_FILE=$(printf '%s' "$FINDING_FILE" | tr -d '"\\')
|
|
1088
|
+
# issue-dedup.sh ships with ForgeDock next to classify-finding.sh (resolved in 7B.5, install-root only, never the repo under review).
|
|
1089
|
+
DEDUP_SCRIPT=""; [ -n "${CLASSIFY_SCRIPT:-}" ] && [ -f "$(dirname "$CLASSIFY_SCRIPT")/issue-dedup.sh" ] && DEDUP_SCRIPT="$(dirname "$CLASSIFY_SCRIPT")/issue-dedup.sh"
|
|
1090
|
+
if [ -n "$DEDUP_SCRIPT" ]; then
|
|
1091
|
+
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
|
|
1092
|
+
DEDUP_EXIT=$?
|
|
1093
|
+
else
|
|
1094
|
+
DEDUP_EXIT=127 # dedup unavailable
|
|
1095
|
+
fi
|
|
1096
|
+
# 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
|
|
1097
|
+
# 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.
|
|
1098
|
+
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
1099
|
+
_DUP_N=$(printf '%s' "$DEDUP_OUT" | sed -n 's/^DUPLICATE: #\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1100
|
+
_DUP_BODY=$(gh issue view "${_DUP_N:-0}" -R {GH_REPO} --json body --jq '.body' 2>/dev/null) || _DUP_BODY=""
|
|
1101
|
+
printf '%s' "$_DUP_BODY" | grep -qF -- "$_SAFE_FILE" || DEDUP_EXIT=0
|
|
1102
|
+
fi
|
|
1103
|
+
# 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.
|
|
1104
|
+
_FILE_RE=$(printf '%s' "$_SAFE_FILE" | sed 's/[][\.^$*+?(){}|\/]/\\&/g')
|
|
1105
|
+
REGRESSION_CHECK=ok; CLOSED_HIT=""
|
|
1106
|
+
# Capture the gh read first and test its own status: through a pipe a failed read looks like "no closed hit".
|
|
1107
|
+
_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
|
|
1108
|
+
# FINDING_LINE may be a range such as 12-15: use its first integer, and 0 when there is none.
|
|
1109
|
+
_L=$(printf '%s' "${FINDING_LINE:-0}" | grep -oE '[0-9]+' | head -1); _L="${_L:-0}"
|
|
1110
|
+
_HITS=""; [ "$REGRESSION_CHECK" = "ok" ] && { _HITS=$(printf '%s' "$_CL" | jq -r --arg re "$_FILE_RE" --argjson l "$_L" '.[] | select((.body // "") != "") | select(.stateReason != "NOT_PLANNED")
|
|
1111
|
+
| select([.body | scan($re + "[:#L]+([0-9]+)") | .[0] | tonumber | select((. - $l) <= 5 and ($l - .) <= 5)] | length > 0) | "#\(.number) \(.title)"') || REGRESSION_CHECK=unavailable; }
|
|
1112
|
+
CLOSED_HIT=$(printf '%s\n' "$_HITS" | sed '/^$/d' | head -3)
|
|
1113
|
+
# Confirm the 3+ shared title keywords against each hit before treating it as a regression.
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
- `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.
|
|
1117
|
+
- `REGRESSION_CHECK=unavailable` (the closed-issue read or its filter failed): record `regression-check-unavailable` in the Phase 8 summary; it never suppresses filing.
|
|
1118
|
+
- `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.
|
|
1119
|
+
- Record every deduped item (finding id, matched issue) for the Phase 8 summary and audit.
|
|
829
1120
|
|
|
830
1121
|
### 7F: Create Issues
|
|
831
1122
|
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
1123
|
|
|
833
|
-
**For each finding** (
|
|
1124
|
+
**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
1125
|
```bash
|
|
835
|
-
STAGING_FINDING_TITLE
|
|
1126
|
+
# STAGING_FINDING_TITLE was built in 7E; re-assert it here if 7E was skipped.
|
|
1127
|
+
STAGING_FINDING_TITLE="${STAGING_FINDING_TITLE:-chore: [summary] (staging review — PR #${PR_NUMBER})}"
|
|
836
1128
|
# Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
|
|
837
1129
|
# an xargs-based tokenizer that never expands backtick/$(...) substitution, so
|
|
838
1130
|
# this is no longer required for safety — but strip it anyway so the raw title
|
|
839
1131
|
# stays readable if it round-trips through any other eval-based consumer.
|
|
840
1132
|
STAGING_FINDING_TITLE=$(printf '%s' "$STAGING_FINDING_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
|
|
841
|
-
SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
|
|
1133
|
+
SCRATCHPAD="${SCRATCHPAD:-${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}}"
|
|
842
1134
|
REVIEW_AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-reviewer}-$$}"
|
|
843
1135
|
mkdir -p "$SCRATCHPAD"
|
|
844
1136
|
STAGING_FINDING_BODY_MARKER="FORGE:BODY-INTEGRITY:${PR_NUMBER}_staging-review_${REVIEW_AGENT_TOKEN}"
|
|
@@ -924,7 +1216,7 @@ fi
|
|
|
924
1216
|
|
|
925
1217
|
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
1218
|
|
|
927
|
-
**
|
|
1219
|
+
**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
1220
|
|
|
929
1221
|
### 7G: Add to Project Board
|
|
930
1222
|
### 7H: Update PR Description with Findings Table
|
|
@@ -941,6 +1233,8 @@ Post summary with verdict:
|
|
|
941
1233
|
4. CONFIRMED HIGH blocking (crashes, data loss) → NEEDS FIXES FIRST
|
|
942
1234
|
5. All else → APPROVE FOR DEPLOY
|
|
943
1235
|
|
|
1236
|
+
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.
|
|
1237
|
+
|
|
944
1238
|
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
1239
|
|
|
946
1240
|
**Risk Matrix must include a Test Gate row** (use `$TEST_GATE_VERDICT` and `$TEST_GATE_REASON` set in Phase 6.5):
|