forgedock 1.13.1 → 1.15.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 +11 -0
- package/bin/engine/phases.mjs +268 -25
- package/bin/engine/phases.test.mjs +24 -0
- package/bin/engine-cli.mjs +18 -5
- package/bin/engine.mjs +149 -12
- package/bin/forgedock.mjs +4 -1
- package/commands/issue.md +17 -2
- package/commands/orchestrate/phase-3-dependency.md +5 -2
- package/commands/orchestrate/phase-4-execution.md +154 -27
- package/commands/pipeline-health/phase-2-metrics.md +5 -3
- package/commands/review-pr-staging.md +554 -54
- package/commands/review-pr.md +662 -51
- package/commands/validate.md +11 -3
- package/commands/work-on/build/architect.md +11 -8
- package/commands/work-on/build/implement.md +15 -1
- package/commands/work-on/build.md +34 -10
- package/commands/work-on/close.md +5 -4
- package/commands/work-on/decompose.md +7 -2
- package/commands/work-on/investigate.md +13 -0
- package/commands/work-on/remediate.md +332 -91
- package/commands/work-on/review.md +3 -3
- package/docs/CONFIG.md +14 -1
- package/docs/RELEASE-NOTES.md +20 -0
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +6 -0
- package/scripts/architect-resume-match.test.sh +53 -0
- package/scripts/check-spec-bash.sh +73 -3
- package/scripts/check-spec-bash.test.sh +50 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/checkbox-sections.test.sh +19 -0
- package/scripts/classify-finding.sh +103 -1
- package/scripts/classify-finding.test.sh +56 -0
- package/scripts/forge-root.test.sh +86 -2
- package/scripts/orchestrate-soft-edge.test.sh +58 -0
- package/scripts/remediate-approved-head.test.sh +43 -0
- package/scripts/remediate-basesync.test.sh +62 -3
- package/scripts/remediation-marker-match.test.sh +6 -0
- package/scripts/resolve-deploy-gate.sh +51 -0
- package/scripts/resolve-deploy-gate.test.sh +59 -0
- package/scripts/review-delta.sh +3 -2
- package/scripts/review-delta.test.sh +1 -1
- package/scripts/review-pr-delta-wiring.test.sh +274 -0
- package/scripts/review-pr-domain-map.test.sh +10 -0
- package/scripts/review-pr-staging-scope.test.sh +159 -0
- package/scripts/trusted-comments.test.sh +11 -0
package/commands/review-pr.md
CHANGED
|
@@ -1520,7 +1520,7 @@ Each agent receives only its domain-relevant diff slice rather than the full cha
|
|
|
1520
1520
|
# Domain file patterns — used in Phase 3C to scope each agent's input
|
|
1521
1521
|
DOMAIN_FILES_AUTH=$(echo "$FILES" | grep -iE "auth|session|login|logout|jwt|oauth|token|user|permission|middleware" || echo "")
|
|
1522
1522
|
DOMAIN_FILES_BILLING=$(echo "$FILES" | grep -iE "billing|credit|payment|charge|refund|subscription|pricing|tier" || echo "")
|
|
1523
|
-
DOMAIN_FILES_CONCURRENCY=$(echo "$FILES" | grep -iE "lock|queue|atomic|transaction|worker|job|task|celery|redis" || echo "")
|
|
1523
|
+
DOMAIN_FILES_CONCURRENCY=$(echo "$FILES" | grep -iE "lock|queue|atomic|transaction|worker|job|task|celery|redis|claim|lease|engine-state" || echo "")
|
|
1524
1524
|
DOMAIN_FILES_DATABASE=$(echo "$FILES" | grep -iE "migration|\.sql$|model|schema|db|database|alembic" || echo "")
|
|
1525
1525
|
DOMAIN_FILES_INFRA=$(echo "$FILES" | grep -iE "docker|nginx|traefik|\.github|Makefile|deploy|infra" || echo "")
|
|
1526
1526
|
DOMAIN_FILES_SCRAPING=$(echo "$FILES" | grep -iE "scrape|crawler|stealth|browser.*pool|headless|anti.bot|detection.*keyword|captcha" || echo "")
|
|
@@ -1528,14 +1528,225 @@ DOMAIN_FILES_FRONTEND=$(echo "$FILES" | grep -iE "^web/|\.tsx?$|\.jsx?$|componen
|
|
|
1528
1528
|
DOMAIN_FILES_API=$(echo "$FILES" | grep -iE "router|route|endpoint|openapi|sdk|api" || echo "")
|
|
1529
1529
|
|
|
1530
1530
|
# Fallback: if a domain has no specific files matched, give it the full file list
|
|
1531
|
-
# (
|
|
1531
|
+
# (a domain can be on the roster without a pattern hit, e.g. via delta_roster or Security's full-diff scope)
|
|
1532
1532
|
[ -z "$DOMAIN_FILES_AUTH" ] && DOMAIN_FILES_AUTH="$FILES"
|
|
1533
1533
|
[ -z "$DOMAIN_FILES_BILLING" ] && DOMAIN_FILES_BILLING="$FILES"
|
|
1534
|
+
[ -z "$DOMAIN_FILES_CONCURRENCY" ] && DOMAIN_FILES_CONCURRENCY="$FILES"
|
|
1534
1535
|
[ -z "$DOMAIN_FILES_DATABASE" ] && DOMAIN_FILES_DATABASE="$FILES"
|
|
1536
|
+
[ -z "$DOMAIN_FILES_INFRA" ] && DOMAIN_FILES_INFRA="$FILES"
|
|
1537
|
+
[ -z "$DOMAIN_FILES_SCRAPING" ] && DOMAIN_FILES_SCRAPING="$FILES"
|
|
1538
|
+
[ -z "$DOMAIN_FILES_FRONTEND" ] && DOMAIN_FILES_FRONTEND="$FILES"
|
|
1539
|
+
[ -z "$DOMAIN_FILES_API" ] && DOMAIN_FILES_API="$FILES"
|
|
1535
1540
|
```
|
|
1536
1541
|
|
|
1537
1542
|
### 3C: Load Agent Templates & Launch
|
|
1538
1543
|
|
|
1544
|
+
#### Review mode: FULL, DELTA or BASE_SYNC_ONLY (run first, before loading any template)
|
|
1545
|
+
|
|
1546
|
+
Re-reviewing a head that only moved since the last fully reviewed head does not need the whole panel over the whole diff. `scripts/review-delta.sh` (resolved from the trusted install, never the working directory or the PR checkout) classifies the move from GitHub state and git history. It is fail closed: every error, an unresolvable script or trust script, unreadable output or an unvalidated SHA means **FULL**, which is the review this spec ran before the resolver existed.
|
|
1547
|
+
|
|
1548
|
+
| Mode | Meaning | What changes |
|
|
1549
|
+
|------|---------|--------------|
|
|
1550
|
+
| `FULL` | First review, anything the resolver cannot prove, `--thorough`, milestone-to-staging, or a same-head re-entry | Nothing. The 3B roster reviews the full PR diff. |
|
|
1551
|
+
| `DELTA` | Head moved by ordinary commits since the last fully reviewed head | Reviewers get `git diff <reviewed>..<head>` as their slice (`FULL_FILES` stays as context). The 3B roster stands while `FULL_ROUNDS < 2`; from the third review on the roster is Security plus the domains whose trigger paths the delta touches. Findings of the earlier reviewed head are read again in Phase 6A. |
|
|
1552
|
+
| `BASE_SYNC_ONLY` | Head moved only by clean merges of `origin/<base>` with the PR's own change byte-identical | Zero reviewers. A trusted `FORGE:REVIEW-CARRIED` record stands in for the per-domain markers in Phases 4, 5, 6A and 9. |
|
|
1553
|
+
|
|
1554
|
+
Phase 2 checks, the §6B.4 provenance gates (against the full PR diff at the current head) and the Phase 7B/8 gates run unchanged in every mode.
|
|
1555
|
+
|
|
1556
|
+
```bash
|
|
1557
|
+
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
1558
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; SELECTED_AGENTS="{SELECTED_AGENTS}"
|
|
1559
|
+
THOROUGH="{THOROUGH}"; IS_MILESTONE_TO_STAGING="{IS_MILESTONE_TO_STAGING}"
|
|
1560
|
+
# 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.
|
|
1561
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1562
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1563
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1564
|
+
[ -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)"
|
|
1565
|
+
done
|
|
1566
|
+
TRUSTED_SCRIPT=""
|
|
1567
|
+
while IFS= read -r _c; do
|
|
1568
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1569
|
+
done <<< "$_tc"
|
|
1570
|
+
# HELPER_SCRIPT resolver (REVIEW_DELTA_SCRIPT; trusted-install tiers only, never the working directory or the PR checkout, #3400/#3483/#3596): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. A PR that shipped its own resolver could otherwise decide that its own head needs no review. The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
1571
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1572
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1573
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1574
|
+
[ -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)"
|
|
1575
|
+
done
|
|
1576
|
+
REVIEW_DELTA_SCRIPT=""
|
|
1577
|
+
while IFS= read -r _c; do
|
|
1578
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
1579
|
+
done <<< "$_tc"
|
|
1580
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
1581
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1582
|
+
# BEGIN review-pr-delta-mode
|
|
1583
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
1584
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
1585
|
+
# Needs REPO PR_NUMBER REVIEW_SHA BASE_BRANCH TRUSTED_SCRIPT REVIEW_DELTA_SCRIPT in scope. Every failure (unresolved script, bad output, bad SHA, empty delta) prints FULL, never a skipped review.
|
|
1586
|
+
review_delta_mode() {
|
|
1587
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
1588
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
1589
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
1590
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
1591
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
1592
|
+
out=$(bash "$REVIEW_DELTA_SCRIPT" --pr "$PR_NUMBER" --head "$REVIEW_SHA" --base "$BASE_BRANCH" --trusted-script "$TRUSTED_SCRIPT" --repo "$REPO" --repo-path "$top" 2>/dev/null) || out=""
|
|
1593
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
1594
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
1595
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
1596
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
1597
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
1598
|
+
if [ "$kind" = "DELTA" ]; then
|
|
1599
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
1600
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
1601
|
+
fi
|
|
1602
|
+
echo "FULL - $rounds"
|
|
1603
|
+
}
|
|
1604
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
1605
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
1606
|
+
carry_verified_from() {
|
|
1607
|
+
local kind from rounds n
|
|
1608
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
1609
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
1610
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
1611
|
+
n=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "\\A(?=[\\s\\S]*\\nReviewed-SHA: ${REVIEW_SHA}\\r?(\\n|$))(?=[\\s\\S]*\\nCarried-From-SHA: ${from}\\r?(\\n|$))<!-- FORGE:REVIEW-CARRIED -->" 2>/dev/null || echo "")
|
|
1612
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
1613
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
1614
|
+
return 0
|
|
1615
|
+
}
|
|
1616
|
+
# END review-pr-delta-mode
|
|
1617
|
+
# BEGIN review-pr-delta-roster
|
|
1618
|
+
# Effective roster for a DELTA head. While fewer than two full panels have run (FULL_ROUNDS < 2) the 3B roster stands;
|
|
1619
|
+
# from the third review on it narrows to Security plus the domains whose trigger paths the delta touches.
|
|
1620
|
+
# spec-cli is internal-only (not shipped to consumers): it is added only when the persona file exists in the repo under review.
|
|
1621
|
+
# Args: FULL_ROUNDS, the canonical 3B roster (space separated), the delta file list (one path per line).
|
|
1622
|
+
delta_roster() {
|
|
1623
|
+
local rounds="${1}" full="${2}" files="${3}" out="security" root
|
|
1624
|
+
root="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
1625
|
+
case "$rounds" in ''|*[!0-9]*) printf '%s' "$full"; return 0 ;; esac
|
|
1626
|
+
if [ "$rounds" -lt 2 ]; then printf '%s' "$full"; return 0; fi
|
|
1627
|
+
printf '%s\n' "$files" | grep -qiE "auth|session|login|logout|jwt|oauth|token|user|permission|middleware" && out="$out auth"
|
|
1628
|
+
printf '%s\n' "$files" | grep -qiE "billing|credit|payment|charge|refund|subscription|pricing|tier" && out="$out billing"
|
|
1629
|
+
printf '%s\n' "$files" | grep -qiE "lock|queue|atomic|transaction|worker|job|task|celery|redis|claim|lease|engine-state" && out="$out concurrency"
|
|
1630
|
+
printf '%s\n' "$files" | grep -qiE "migration|\.sql$|model|schema|db|database|alembic" && out="$out database"
|
|
1631
|
+
printf '%s\n' "$files" | grep -qiE "docker|nginx|traefik|\.github|Makefile|deploy|infra" && out="$out infra"
|
|
1632
|
+
printf '%s\n' "$files" | grep -qiE "scrape|crawler|stealth|browser.*pool|headless|anti.bot|detection.*keyword|captcha" && out="$out scraper"
|
|
1633
|
+
printf '%s\n' "$files" | grep -qiE "^web/|\.tsx?$|\.jsx?$|component|page|layout|style|css" && out="$out frontend"
|
|
1634
|
+
printf '%s\n' "$files" | grep -qiE "router|route|endpoint|openapi|sdk|api" && out="$out api"
|
|
1635
|
+
printf '%s\n' "$files" | grep -qE "^(commands|bin)/" && [ -f "$root/commands/review-pr-agents/spec-cli.md" ] && out="$out spec-cli"
|
|
1636
|
+
printf '%s' "$out"
|
|
1637
|
+
}
|
|
1638
|
+
# END review-pr-delta-roster
|
|
1639
|
+
REVIEW_MODE="FULL"; DELTA_FROM=""; CARRIED_FROM=""; FULL_ROUNDS=0
|
|
1640
|
+
if [ "$THOROUGH" != "true" ] && [ "$IS_MILESTONE_TO_STAGING" != "true" ]; then
|
|
1641
|
+
read -r RD_KIND RD_ARG RD_ROUNDS <<< "$(review_delta_mode)"
|
|
1642
|
+
FULL_ROUNDS="${RD_ROUNDS:-0}"
|
|
1643
|
+
case "$RD_KIND" in
|
|
1644
|
+
DELTA) REVIEW_MODE="DELTA"; DELTA_FROM="$RD_ARG" ;;
|
|
1645
|
+
BASE_SYNC_ONLY) REVIEW_MODE="BASE_SYNC_ONLY"; CARRIED_FROM="$RD_ARG" ;;
|
|
1646
|
+
esac
|
|
1647
|
+
fi
|
|
1648
|
+
if [ "$REVIEW_MODE" = "DELTA" ]; then
|
|
1649
|
+
RD_TOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
1650
|
+
DELTA_FILES=$(git -C "$RD_TOP" -c core.quotepath=off diff --name-only --no-renames "${DELTA_FROM}" "${REVIEW_SHA}" 2>/dev/null) || DELTA_FILES=""
|
|
1651
|
+
if [ -z "$DELTA_FILES" ]; then
|
|
1652
|
+
REVIEW_MODE="FULL"; DELTA_FROM="" # unreadable or empty delta: review the full diff
|
|
1653
|
+
else
|
|
1654
|
+
SELECTED_AGENTS=$(delta_roster "$FULL_ROUNDS" "$SELECTED_AGENTS" "$DELTA_FILES")
|
|
1655
|
+
fi
|
|
1656
|
+
fi
|
|
1657
|
+
[ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ] && SELECTED_AGENTS=""
|
|
1658
|
+
echo "REVIEW_MODE: ${REVIEW_MODE}"
|
|
1659
|
+
echo "DELTA_FROM: ${DELTA_FROM}"
|
|
1660
|
+
echo "CARRIED_FROM: ${CARRIED_FROM}"
|
|
1661
|
+
echo "FULL_ROUNDS: ${FULL_ROUNDS}"
|
|
1662
|
+
echo "SELECTED_AGENTS: ${SELECTED_AGENTS}"
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
From here on substitute the printed `REVIEW_MODE`, `DELTA_FROM`, `CARRIED_FROM` and `SELECTED_AGENTS` into every later block as `{REVIEW_MODE}`, `{DELTA_FROM}`, `{CARRIED_FROM}` and `{SELECTED_AGENTS}`. The printed `SELECTED_AGENTS` is the **effective roster**: the panel guard here, the Phase 4 count and the Phase 9 count compare against what is actually dispatched, never against a stale full roster. A block that does not receive these values behaves as FULL (never as a skipped review). A printed `REVIEW_MODE` is advisory: Phases 4, 5, 6A and 9 re-verify `BASE_SYNC_ONLY` themselves before honouring it.
|
|
1666
|
+
|
|
1667
|
+
**If `REVIEW_MODE` is `BASE_SYNC_ONLY`**: dispatch zero reviewers. Skip the template guard, config load, code index, diff slicing and the dispatch below, post the carry record, then go straight to Phase 4:
|
|
1668
|
+
|
|
1669
|
+
```bash
|
|
1670
|
+
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
1671
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"
|
|
1672
|
+
# 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.
|
|
1673
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1674
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1675
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1676
|
+
[ -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)"
|
|
1677
|
+
done
|
|
1678
|
+
TRUSTED_SCRIPT=""
|
|
1679
|
+
while IFS= read -r _c; do
|
|
1680
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1681
|
+
done <<< "$_tc"
|
|
1682
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
1683
|
+
REVIEW_DELTA_SCRIPT=""
|
|
1684
|
+
while IFS= read -r _c; do
|
|
1685
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
1686
|
+
done <<< "$_tc"
|
|
1687
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
1688
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1689
|
+
# BEGIN review-pr-delta-mode
|
|
1690
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
1691
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
1692
|
+
# Needs REPO PR_NUMBER REVIEW_SHA BASE_BRANCH TRUSTED_SCRIPT REVIEW_DELTA_SCRIPT in scope. Every failure (unresolved script, bad output, bad SHA, empty delta) prints FULL, never a skipped review.
|
|
1693
|
+
review_delta_mode() {
|
|
1694
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
1695
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
1696
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
1697
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
1698
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
1699
|
+
out=$(bash "$REVIEW_DELTA_SCRIPT" --pr "$PR_NUMBER" --head "$REVIEW_SHA" --base "$BASE_BRANCH" --trusted-script "$TRUSTED_SCRIPT" --repo "$REPO" --repo-path "$top" 2>/dev/null) || out=""
|
|
1700
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
1701
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
1702
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
1703
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
1704
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
1705
|
+
if [ "$kind" = "DELTA" ]; then
|
|
1706
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
1707
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
1708
|
+
fi
|
|
1709
|
+
echo "FULL - $rounds"
|
|
1710
|
+
}
|
|
1711
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
1712
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
1713
|
+
carry_verified_from() {
|
|
1714
|
+
local kind from rounds n
|
|
1715
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
1716
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
1717
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
1718
|
+
n=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "\\A(?=[\\s\\S]*\\nReviewed-SHA: ${REVIEW_SHA}\\r?(\\n|$))(?=[\\s\\S]*\\nCarried-From-SHA: ${from}\\r?(\\n|$))<!-- FORGE:REVIEW-CARRIED -->" 2>/dev/null || echo "")
|
|
1719
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
1720
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
1721
|
+
return 0
|
|
1722
|
+
}
|
|
1723
|
+
# END review-pr-delta-mode
|
|
1724
|
+
read -r RD_KIND RD_FROM RD_ROUNDS <<< "$(review_delta_mode)"
|
|
1725
|
+
if [ "$RD_KIND" != "BASE_SYNC_ONLY" ]; then
|
|
1726
|
+
echo "CARRY_RECORD: NOT_APPLICABLE (resolver says ${RD_KIND}); run the FULL path"; exit 0
|
|
1727
|
+
fi
|
|
1728
|
+
if [ -n "$(carry_verified_from)" ]; then
|
|
1729
|
+
echo "CARRY_RECORD: PRESENT (verified from ${RD_FROM})"; exit 0 # re-entry: never post a second record
|
|
1730
|
+
fi
|
|
1731
|
+
gh pr comment "$PR_NUMBER" -R "$REPO" --body "<!-- FORGE:REVIEW-CARRIED -->
|
|
1732
|
+
Reviewed-SHA: ${REVIEW_SHA}
|
|
1733
|
+
Carried-From-SHA: ${RD_FROM}
|
|
1734
|
+
|
|
1735
|
+
## Review carried forward
|
|
1736
|
+
|
|
1737
|
+
This head only merged \`origin/${BASE_BRANCH}\` into the PR (the PR's own change is byte-identical to the fully reviewed commit \`${RD_FROM}\`), so no reviewer was dispatched. The panel result and findings of \`${RD_FROM}\` stand; the merge gates still run on this head."
|
|
1738
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1739
|
+
if [ -n "$(carry_verified_from)" ]; then
|
|
1740
|
+
echo "CARRY_RECORD: POSTED (verified from ${RD_FROM})"
|
|
1741
|
+
else
|
|
1742
|
+
echo "CARRY_RECORD: UNVERIFIED (the record is not trusted or could not be read back); run the FULL path"
|
|
1743
|
+
fi
|
|
1744
|
+
```
|
|
1745
|
+
|
|
1746
|
+
`CARRY_RECORD: PRESENT` or `POSTED` proceeds to Phase 4 with an empty roster. `NOT_APPLICABLE` or `UNVERIFIED` falls back to `REVIEW_MODE=FULL` with the 3B roster: set `{REVIEW_MODE}` back to `FULL`, restore the 3B `{SELECTED_AGENTS}` and continue with the template guard below. A record that cannot be trusted (for example the posting identity is not a trusted author) must never be the reason a head goes unreviewed.
|
|
1747
|
+
|
|
1748
|
+
**Otherwise (`FULL` or `DELTA`)** continue below. Under `DELTA`, append to every reviewer prompt: "Delta review: your diff slice is the change since the fully reviewed commit `{DELTA_FROM}`. The file list is context only. Findings of that earlier commit are carried separately; do not re-report code this slice does not show." <!-- Added: forge#3637 -->
|
|
1749
|
+
|
|
1539
1750
|
**>>> MANDATORY TEMPLATE-RESOLUTION GUARD — run BEFORE any Read of persona templates:**
|
|
1540
1751
|
|
|
1541
1752
|
Missing persona templates are a fatal setup error, not permission to skip multi-agent review. Resolve the template source through this ordered chain and STOP if none resolve — never fall through to reviewing inline in the main context:
|
|
@@ -1596,7 +1807,7 @@ Read: $MONOLITHIC_CATALOG
|
|
|
1596
1807
|
```
|
|
1597
1808
|
Then extract the shared protocols section and each selected persona's section from within that single file — same content, different packaging.
|
|
1598
1809
|
|
|
1599
|
-
Persona file names: `security.md` (always), `auth.md`, `billing.md`, `concurrency.md`, `scraper.md`, `frontend.md`, `api.md`, `database.md`, `infra.md
|
|
1810
|
+
Persona file names: `security.md` (always), `auth.md`, `billing.md`, `concurrency.md`, `scraper.md`, `frontend.md`, `api.md`, `database.md`, `infra.md`, and `spec-cli.md` (marker domain `spec-cli`, only ever on a `DELTA` roster whose delta touches `commands/**` or `bin/**` AND whose repo contains `commands/review-pr-agents/spec-cli.md`, i.e. ForgeDock itself; consumer repos never get it).
|
|
1600
1811
|
See `review-pr-agents.md` for the full routing table mapping domains → persona files.
|
|
1601
1812
|
|
|
1602
1813
|
(`$FORGE_HOME` defaults to `~/.claude`)
|
|
@@ -1628,7 +1839,17 @@ Before launching agents, query the code index to build a domain-scoped file list
|
|
|
1628
1839
|
|
|
1629
1840
|
```bash
|
|
1630
1841
|
REPO_PATH=$(yq '.paths.root' "$FORGE_YAML" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
1631
|
-
CODE_INDEX_SCRIPT
|
|
1842
|
+
# HELPER_SCRIPT resolver (CODE_INDEX_SCRIPT; trusted-install tiers only, never the working directory or the target repo, #3400/#3483): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
1843
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1844
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1845
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1846
|
+
[ -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)"
|
|
1847
|
+
done
|
|
1848
|
+
CODE_INDEX_SCRIPT=""
|
|
1849
|
+
while IFS= read -r _c; do
|
|
1850
|
+
case "$_c" in /*) [ -z "$CODE_INDEX_SCRIPT" ] && [ -f "$_c/scripts/code-index.sh" ] && CODE_INDEX_SCRIPT="$_c/scripts/code-index.sh" ;; esac
|
|
1851
|
+
done <<< "$_tc"
|
|
1852
|
+
# REPO_PATH is the PR checkout (author-controlled): it is only the index target, never the script source. Unresolved: skip the index.
|
|
1632
1853
|
|
|
1633
1854
|
# Ensure index is current (cache-hit on unchanged HEAD — instant if already built)
|
|
1634
1855
|
if [[ -x "$CODE_INDEX_SCRIPT" ]]; then
|
|
@@ -1665,10 +1886,22 @@ Each domain agent receives only the diff slice relevant to its domain, not the f
|
|
|
1665
1886
|
|
|
1666
1887
|
```bash
|
|
1667
1888
|
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
1668
|
-
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"
|
|
1889
|
+
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"; REVIEW_SHA="{REVIEW_SHA}"; REVIEW_MODE="{REVIEW_MODE}"; DELTA_FROM="{DELTA_FROM}"
|
|
1669
1890
|
# Full diff fetched once — agents do NOT re-fetch it
|
|
1670
1891
|
FULL_DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO")
|
|
1671
1892
|
FULL_FILES=$(gh pr diff "$PR_NUMBER" -R "$REPO" --name-only)
|
|
1893
|
+
# DELTA (review-delta mode): the slice source becomes the range diff since the last fully reviewed head, and FULL_FILES stays as
|
|
1894
|
+
# context. Every failure (bad SHA, commit missing from the checkout, unreadable or empty range) keeps the full PR diff above.
|
|
1895
|
+
# The variable keeps its FULL_DIFF name so every awk slice below is unchanged.
|
|
1896
|
+
if [ "$REVIEW_MODE" = "DELTA" ]; then
|
|
1897
|
+
is_hex() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
1898
|
+
RD_TOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
1899
|
+
if is_hex "$DELTA_FROM" && is_hex "$REVIEW_SHA" && [ -n "$RD_TOP" ] \
|
|
1900
|
+
&& git -C "$RD_TOP" cat-file -e "${DELTA_FROM}^{commit}" 2>/dev/null && git -C "$RD_TOP" cat-file -e "${REVIEW_SHA}^{commit}" 2>/dev/null; then
|
|
1901
|
+
DELTA_DIFF=$(git -C "$RD_TOP" -c core.quotepath=off diff --no-color --no-ext-diff "${DELTA_FROM}..${REVIEW_SHA}" 2>/dev/null) || DELTA_DIFF=""
|
|
1902
|
+
[ -n "$DELTA_DIFF" ] && FULL_DIFF="$DELTA_DIFF"
|
|
1903
|
+
fi
|
|
1904
|
+
fi
|
|
1672
1905
|
|
|
1673
1906
|
# --- Security agent: always receives the full diff (cross-cutting domain) ---
|
|
1674
1907
|
DIFF_SLICE_SECURITY="$FULL_DIFF"
|
|
@@ -1736,6 +1969,14 @@ DIFF_SLICE_SCRAPER=$(echo "$FULL_DIFF" | awk '
|
|
|
1736
1969
|
in_block { print }
|
|
1737
1970
|
')
|
|
1738
1971
|
[ -z "$DIFF_SLICE_SCRAPER" ] && DIFF_SLICE_SCRAPER="$FULL_DIFF"
|
|
1972
|
+
|
|
1973
|
+
# --- Spec/CLI agent (DELTA rosters only): commands/** and bin/** files ---
|
|
1974
|
+
DIFF_SLICE_SPEC_CLI=$(echo "$FULL_DIFF" | awk '
|
|
1975
|
+
/^diff --git/ { in_block=0 }
|
|
1976
|
+
/^diff --git a\/(commands|bin)\// { in_block=1 }
|
|
1977
|
+
in_block { print }
|
|
1978
|
+
')
|
|
1979
|
+
[ -z "$DIFF_SLICE_SPEC_CLI" ] && DIFF_SLICE_SPEC_CLI="$FULL_DIFF"
|
|
1739
1980
|
```
|
|
1740
1981
|
|
|
1741
1982
|
**Tool-result truncation discipline**: All `gh pr diff` and file-read outputs piped to agents MUST be capped at ~100K characters. This mirrors the runner's built-in 100K-char tool-result cap (`bin/runner.mjs`). Any diff slice exceeding 100K chars must be truncated before substitution — agents that receive oversized context perform worse, not better.
|
|
@@ -1761,6 +2002,7 @@ DIFF_SLICE_FRONTEND=$(truncate_slice "$DIFF_SLICE_FRONTEND")
|
|
|
1761
2002
|
DIFF_SLICE_API=$(truncate_slice "$DIFF_SLICE_API")
|
|
1762
2003
|
DIFF_SLICE_INFRA=$(truncate_slice "$DIFF_SLICE_INFRA")
|
|
1763
2004
|
DIFF_SLICE_SCRAPER=$(truncate_slice "$DIFF_SLICE_SCRAPER")
|
|
2005
|
+
DIFF_SLICE_SPEC_CLI=$(truncate_slice "$DIFF_SLICE_SPEC_CLI")
|
|
1764
2006
|
```
|
|
1765
2007
|
|
|
1766
2008
|
The `protocols.md` file contains the Evidence-Based Review Protocol, Structured Findings Protocol, Per-Agent Input Scoping rules, and Tool-Result Truncation Discipline. Each persona file contains that agent's full prompt template. For each agent in `$SELECTED_AGENTS` (computed in Phase 3B):
|
|
@@ -1777,7 +2019,7 @@ The `protocols.md` file contains the Evidence-Based Review Protocol, Structured
|
|
|
1777
2019
|
|
|
1778
2020
|
**Reviewer-comment trust and shape (applies to every check below and in Phase 4)**: a reviewer comment counts only when it is from a trusted author (`scripts/trusted-comments.sh`, the same predicate as the in-PR fix gate and `verify-phase-trail.sh`: trusted association, Bot account, or `FORGE_TRAIL_TRUSTED_LOGINS`), its body **starts with** `<!-- FORGE:REVIEW-AGENT:{domain} -->`, and it carries a line `Reviewed-SHA: ${REVIEW_SHA}` for the current head. The head SHA is public, so the SHA line alone proves nothing: an untrusted commenter must never be able to suppress dispatch or satisfy the panel guard. If the trust script cannot be resolved, or its count is not an integer, the reviewer is treated as **not posted** (fail closed: re-dispatch in 3C, missing in Phase 4).
|
|
1779
2021
|
|
|
1780
|
-
**Idempotent re-entry (before launching)**: a re-invoked review must reuse reviewers that already posted for the current head instead of re-dispatching the full panel. For each domain in `$SELECTED_AGENTS`, check for an existing trusted comment of the shape above; launch only the domains without one (possibly none), then continue to Phase 4. Comments for a different SHA never count. The selected roster for the panel guard stays the full `$SELECTED_AGENTS` set.
|
|
2022
|
+
**Idempotent re-entry (before launching)**: a re-invoked review must reuse reviewers that already posted for the current head instead of re-dispatching the full panel. For each domain in `$SELECTED_AGENTS`, check for an existing trusted comment of the shape above; launch only the domains without one (possibly none), then continue to Phase 4. Comments for a different SHA never count. The selected roster for the panel guard stays the full effective `$SELECTED_AGENTS` set (the one printed by the review-mode block above, not the 3B roster when `DELTA` narrowed it).
|
|
1781
2023
|
|
|
1782
2024
|
```bash
|
|
1783
2025
|
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
@@ -1900,7 +2142,7 @@ Reviewer completion is read from GitHub, never from notifications (see the "Revi
|
|
|
1900
2142
|
|
|
1901
2143
|
```bash
|
|
1902
2144
|
# Each Bash call is a fresh shell: re-declare state with the orchestrator's values (never rely on Phase 3C variables).
|
|
1903
|
-
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; SELECTED_AGENTS="{SELECTED_AGENTS}"
|
|
2145
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; SELECTED_AGENTS="{SELECTED_AGENTS}"; REVIEW_MODE="{REVIEW_MODE}"
|
|
1904
2146
|
DISPATCH_TOOL="{DISPATCH_TOOL}" # Task | Agent | task (OpenCode)
|
|
1905
2147
|
case "$REVIEW_SHA" in *[!0-9a-f]*|"") REVIEW_SHA_OK=false ;; *) [ "${#REVIEW_SHA}" -eq 40 ] || [ "${#REVIEW_SHA}" -eq 64 ] && REVIEW_SHA_OK=true || REVIEW_SHA_OK=false ;; esac
|
|
1906
2148
|
if [ "$REVIEW_SHA_OK" != "true" ]; then
|
|
@@ -1951,9 +2193,58 @@ TRUSTED_SCRIPT=""
|
|
|
1951
2193
|
while IFS= read -r _c; do
|
|
1952
2194
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1953
2195
|
done <<< "$_tc"
|
|
2196
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
2197
|
+
REVIEW_DELTA_SCRIPT=""
|
|
2198
|
+
while IFS= read -r _c; do
|
|
2199
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
2200
|
+
done <<< "$_tc"
|
|
2201
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
1954
2202
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1955
2203
|
MISSING_AGENT_COMMENTS=""
|
|
1956
2204
|
ACTUAL_AGENT_COUNT=0
|
|
2205
|
+
# BEGIN review-pr-delta-mode
|
|
2206
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
2207
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
2208
|
+
# Needs REPO PR_NUMBER REVIEW_SHA BASE_BRANCH TRUSTED_SCRIPT REVIEW_DELTA_SCRIPT in scope. Every failure (unresolved script, bad output, bad SHA, empty delta) prints FULL, never a skipped review.
|
|
2209
|
+
review_delta_mode() {
|
|
2210
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
2211
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
2212
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
2213
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
2214
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
2215
|
+
out=$(bash "$REVIEW_DELTA_SCRIPT" --pr "$PR_NUMBER" --head "$REVIEW_SHA" --base "$BASE_BRANCH" --trusted-script "$TRUSTED_SCRIPT" --repo "$REPO" --repo-path "$top" 2>/dev/null) || out=""
|
|
2216
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
2217
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
2218
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
2219
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
2220
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
2221
|
+
if [ "$kind" = "DELTA" ]; then
|
|
2222
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
2223
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
2224
|
+
fi
|
|
2225
|
+
echo "FULL - $rounds"
|
|
2226
|
+
}
|
|
2227
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
2228
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
2229
|
+
carry_verified_from() {
|
|
2230
|
+
local kind from rounds n
|
|
2231
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
2232
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
2233
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
2234
|
+
n=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "\\A(?=[\\s\\S]*\\nReviewed-SHA: ${REVIEW_SHA}\\r?(\\n|$))(?=[\\s\\S]*\\nCarried-From-SHA: ${from}\\r?(\\n|$))<!-- FORGE:REVIEW-CARRIED -->" 2>/dev/null || echo "")
|
|
2235
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
2236
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
2237
|
+
return 0
|
|
2238
|
+
}
|
|
2239
|
+
# END review-pr-delta-mode
|
|
2240
|
+
# A zero-reviewer panel is acceptable only for a BASE_SYNC_ONLY head whose carry record is trusted AND re-verified by the
|
|
2241
|
+
# resolver right now. An empty roster with no verified record is a shortfall, never a clean panel (fail closed).
|
|
2242
|
+
CARRIED_FROM=""
|
|
2243
|
+
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then CARRIED_FROM=$(carry_verified_from); fi
|
|
2244
|
+
EMPTY_UNVERIFIED=false
|
|
2245
|
+
if [ "$SELECTED_AGENT_COUNT" -eq 0 ] && [ -z "$CARRIED_FROM" ]; then
|
|
2246
|
+
EMPTY_UNVERIFIED=true; MISSING_AGENT_COMMENTS=" (empty roster without a verified carried record)"
|
|
2247
|
+
fi
|
|
1957
2248
|
# BEGIN review-pr-agent-domain
|
|
1958
2249
|
agent_domain() {
|
|
1959
2250
|
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
@@ -1981,8 +2272,9 @@ for AGENT in $SELECTED_AGENTS; do
|
|
|
1981
2272
|
done
|
|
1982
2273
|
|
|
1983
2274
|
if [ -z "$MISSING_AGENT_COMMENTS" ] && [ "$ACTUAL_AGENT_COUNT" -ge "$SELECTED_AGENT_COUNT" ]; then
|
|
1984
|
-
rm -f "$DEADLINE_FILE"
|
|
1985
|
-
|
|
2275
|
+
rm -f "$DEADLINE_FILE"
|
|
2276
|
+
if [ -n "$CARRIED_FROM" ]; then echo "PANEL_STATUS: COMPLETE (carried from ${CARRIED_FROM}; no reviewers dispatched)"; else echo "PANEL_STATUS: COMPLETE (${ACTUAL_AGENT_COUNT}/${SELECTED_AGENT_COUNT})"; fi
|
|
2277
|
+
elif [ "$JOINED" = "true" ] || [ "$EMPTY_UNVERIFIED" = "true" ] || [ "$(date +%s)" -ge "$DEADLINE" ]; then
|
|
1986
2278
|
rm -f "$DEADLINE_FILE"
|
|
1987
2279
|
echo "REVIEW DELIVERY FAILURE: missing trusted current-SHA findings comment(s) for:${MISSING_AGENT_COMMENTS}"
|
|
1988
2280
|
echo "Use each agent's returned verdict, findings, and durable body path to recover the review."
|
|
@@ -1995,17 +2287,19 @@ fi
|
|
|
1995
2287
|
|
|
1996
2288
|
On `PANEL_STATUS: STOP`, return the printed `REVIEW_RESULT` and do nothing else (no hard stop, no verdict, no label change: the panel is intact, only the head moved).
|
|
1997
2289
|
|
|
2290
|
+
A `BASE_SYNC_ONLY` head has no reviewer comments to wait for. The block above accepts it only through a trusted `FORGE:REVIEW-CARRIED` record for this head whose `Carried-From-SHA` equals the reviewed SHA the resolver names right now; a missing, untrusted, forged or stale record falls into the existing hard stop (`ACTUAL_AGENT_COUNT=0`). The record never substitutes for reviewers on a `FULL` or `DELTA` head: those still need every selected reviewer's trusted current-SHA marker.
|
|
2291
|
+
|
|
1998
2292
|
**Do NOT proceed until ALL selected agents' current-SHA comments are visible on the PR.** Under OpenCode, each foreground `task(..., background: false)` returns only when that reviewer is complete; verify its corresponding structured PR comment before continuing. A missing comment after the wait is an explicit delivery failure: do not synthesize a verdict or treat it as a clean result. Do not infer a clean review from a missing comment: a reviewer's return text may contain findings that could not be posted because GitHub writes were throttled.
|
|
1999
2293
|
|
|
2000
2294
|
---
|
|
2001
2295
|
|
|
2002
2296
|
## Phase 5: Synthesis (Multi-Agent Arbitration)
|
|
2003
2297
|
|
|
2004
|
-
**Skip if**: Only 1 agent OR total findings ≤ 3.
|
|
2298
|
+
**Skip if**: Only 1 agent OR total findings ≤ 3, or `REVIEW_MODE` is `BASE_SYNC_ONLY` (no reviewer ran for this head; the carried head's synthesis, if any, stands and Phase 6A reads it). Under `DELTA` this phase synthesizes the current head's reviewers only; Phase 6A adds the earlier reviewed head's findings.
|
|
2005
2299
|
|
|
2006
2300
|
```bash
|
|
2007
2301
|
# Each Bash call is a fresh shell: re-declare state with the orchestrator's values.
|
|
2008
|
-
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"
|
|
2302
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; REVIEW_MODE="{REVIEW_MODE}"
|
|
2009
2303
|
case "$REVIEW_SHA" in *[!0-9a-f]*|"") REVIEW_SHA_OK=false ;; *) [ "${#REVIEW_SHA}" -eq 40 ] || [ "${#REVIEW_SHA}" -eq 64 ] && REVIEW_SHA_OK=true || REVIEW_SHA_OK=false ;; esac
|
|
2010
2304
|
[ "$REVIEW_SHA_OK" = "true" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: REVIEW_SHA is not a commit id (an empty SHA would count every head's findings)"; exit 0; }
|
|
2011
2305
|
# Extract structured finding IDs from FINDING HTML comments.
|
|
@@ -2024,8 +2318,56 @@ TRUSTED_SCRIPT=""
|
|
|
2024
2318
|
while IFS= read -r _c; do
|
|
2025
2319
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2026
2320
|
done <<< "$_tc"
|
|
2321
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
2322
|
+
REVIEW_DELTA_SCRIPT=""
|
|
2323
|
+
while IFS= read -r _c; do
|
|
2324
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
2325
|
+
done <<< "$_tc"
|
|
2326
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2027
2327
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
2028
|
-
|
|
2328
|
+
# BEGIN review-pr-delta-mode
|
|
2329
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
2330
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
2331
|
+
# Needs REPO PR_NUMBER REVIEW_SHA BASE_BRANCH TRUSTED_SCRIPT REVIEW_DELTA_SCRIPT in scope. Every failure (unresolved script, bad output, bad SHA, empty delta) prints FULL, never a skipped review.
|
|
2332
|
+
review_delta_mode() {
|
|
2333
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
2334
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
2335
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
2336
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
2337
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
2338
|
+
out=$(bash "$REVIEW_DELTA_SCRIPT" --pr "$PR_NUMBER" --head "$REVIEW_SHA" --base "$BASE_BRANCH" --trusted-script "$TRUSTED_SCRIPT" --repo "$REPO" --repo-path "$top" 2>/dev/null) || out=""
|
|
2339
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
2340
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
2341
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
2342
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
2343
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
2344
|
+
if [ "$kind" = "DELTA" ]; then
|
|
2345
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
2346
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
2347
|
+
fi
|
|
2348
|
+
echo "FULL - $rounds"
|
|
2349
|
+
}
|
|
2350
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
2351
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
2352
|
+
carry_verified_from() {
|
|
2353
|
+
local kind from rounds n
|
|
2354
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
2355
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
2356
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
2357
|
+
n=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "\\A(?=[\\s\\S]*\\nReviewed-SHA: ${REVIEW_SHA}\\r?(\\n|$))(?=[\\s\\S]*\\nCarried-From-SHA: ${from}\\r?(\\n|$))<!-- FORGE:REVIEW-CARRIED -->" 2>/dev/null || echo "")
|
|
2358
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
2359
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
2360
|
+
return 0
|
|
2361
|
+
}
|
|
2362
|
+
# END review-pr-delta-mode
|
|
2363
|
+
# BASE_SYNC_ONLY: no reviewer posted for this head, so the findings are the carried head's. Only a verified carry record
|
|
2364
|
+
# switches the SHA (the SHA comes from the validated record, never from untrusted text); an unverified claim keeps the
|
|
2365
|
+
# current head, finds no reviewer bodies and blocks below (fail closed).
|
|
2366
|
+
FINDINGS_SHA="$REVIEW_SHA"
|
|
2367
|
+
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then
|
|
2368
|
+
CARRIED_FROM=$(carry_verified_from); [ -n "$CARRIED_FROM" ] && FINDINGS_SHA="$CARRIED_FROM"
|
|
2369
|
+
fi
|
|
2370
|
+
AGENT_RE="^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${FINDINGS_SHA}(\\r?\\n|$)"
|
|
2029
2371
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
2030
2372
|
echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/trusted-comments.sh unresolvable or PR comments unreadable; refusing to read reviewer findings (fail closed)"; exit 0
|
|
2031
2373
|
fi
|
|
@@ -2033,7 +2375,7 @@ AGENT_BODIES=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies "$AG
|
|
|
2033
2375
|
|| { echo "REVIEW_RESULT: status: BLOCKED, blocker: trusted reviewer comments unreadable (fail closed)"; exit 0; }
|
|
2034
2376
|
ALL_FINDINGS=$(printf '%s\n' "$AGENT_BODIES" | jq -r 'scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null)
|
|
2035
2377
|
# Agent bodies start with the REVIEW-AGENT marker and carry REVIEW-FINDINGS-START inside, so count it with a lookahead.
|
|
2036
|
-
AGENT_COUNT=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${
|
|
2378
|
+
AGENT_COUNT=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${FINDINGS_SHA}(\\r?\\n|$)" 2>/dev/null || echo "")
|
|
2037
2379
|
case "$AGENT_COUNT" in ''|*[!0-9]*) AGENT_COUNT=0 ;; esac # fail closed
|
|
2038
2380
|
FINDING_COUNT=$(echo "$ALL_FINDINGS" | grep -c '.' || echo 0)
|
|
2039
2381
|
```
|
|
@@ -2057,10 +2399,10 @@ If synthesis needed, launch a `general-purpose` Task (model: `"{SUBAGENT_MODEL}"
|
|
|
2057
2399
|
|
|
2058
2400
|
```bash
|
|
2059
2401
|
# Each Bash call is a fresh shell: re-declare state with the orchestrator's values.
|
|
2060
|
-
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"
|
|
2402
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; REVIEW_MODE="{REVIEW_MODE}"; DELTA_FROM="{DELTA_FROM}"
|
|
2061
2403
|
case "$REVIEW_SHA" in *[!0-9a-f]*|"") REVIEW_SHA_OK=false ;; *) [ "${#REVIEW_SHA}" -eq 40 ] || [ "${#REVIEW_SHA}" -eq 64 ] && REVIEW_SHA_OK=true || REVIEW_SHA_OK=false ;; esac
|
|
2062
2404
|
[ "$REVIEW_SHA_OK" = "true" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: REVIEW_SHA is not a commit id (an empty SHA would count every head's findings)"; exit 0; }
|
|
2063
|
-
# Only TRUSTED bodies for the CURRENT head count (scripts/trusted-comments.sh + anchored marker + Reviewed-SHA line):
|
|
2405
|
+
# Only TRUSTED bodies for the CURRENT head count (BASE_SYNC_ONLY and DELTA add the carried / earlier reviewed head, see below) (scripts/trusted-comments.sh + anchored marker + Reviewed-SHA line):
|
|
2064
2406
|
# findings from earlier heads are already fixed or re-reviewed, and an untrusted commenter must never be able to inject
|
|
2065
2407
|
# findings or post a forged synthesis block that makes the review read clean (the head SHA is public).
|
|
2066
2408
|
# 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.
|
|
@@ -2073,25 +2415,82 @@ TRUSTED_SCRIPT=""
|
|
|
2073
2415
|
while IFS= read -r _c; do
|
|
2074
2416
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2075
2417
|
done <<< "$_tc"
|
|
2418
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
2419
|
+
REVIEW_DELTA_SCRIPT=""
|
|
2420
|
+
while IFS= read -r _c; do
|
|
2421
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
2422
|
+
done <<< "$_tc"
|
|
2423
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2076
2424
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
2077
2425
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
2078
2426
|
echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/trusted-comments.sh unresolvable or PR comments unreadable; refusing to read clean (fail closed)"; exit 0
|
|
2079
2427
|
fi
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2428
|
+
# BEGIN review-pr-delta-mode
|
|
2429
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
2430
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
2431
|
+
# Needs REPO PR_NUMBER REVIEW_SHA BASE_BRANCH TRUSTED_SCRIPT REVIEW_DELTA_SCRIPT in scope. Every failure (unresolved script, bad output, bad SHA, empty delta) prints FULL, never a skipped review.
|
|
2432
|
+
review_delta_mode() {
|
|
2433
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
2434
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
2435
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
2436
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
2437
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
2438
|
+
out=$(bash "$REVIEW_DELTA_SCRIPT" --pr "$PR_NUMBER" --head "$REVIEW_SHA" --base "$BASE_BRANCH" --trusted-script "$TRUSTED_SCRIPT" --repo "$REPO" --repo-path "$top" 2>/dev/null) || out=""
|
|
2439
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
2440
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
2441
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
2442
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
2443
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
2444
|
+
if [ "$kind" = "DELTA" ]; then
|
|
2445
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
2446
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
2447
|
+
fi
|
|
2448
|
+
echo "FULL - $rounds"
|
|
2449
|
+
}
|
|
2450
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
2451
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
2452
|
+
carry_verified_from() {
|
|
2453
|
+
local kind from rounds n
|
|
2454
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
2455
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
2456
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
2457
|
+
n=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "\\A(?=[\\s\\S]*\\nReviewed-SHA: ${REVIEW_SHA}\\r?(\\n|$))(?=[\\s\\S]*\\nCarried-From-SHA: ${from}\\r?(\\n|$))<!-- FORGE:REVIEW-CARRIED -->" 2>/dev/null || echo "")
|
|
2458
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
2459
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
2460
|
+
return 0
|
|
2461
|
+
}
|
|
2462
|
+
# END review-pr-delta-mode
|
|
2463
|
+
# Trusted synthesized block for one head when present, else that head's trusted agent bodies (the same predicate as before,
|
|
2464
|
+
# now per head). Returns non-zero when the trust script cannot count or read.
|
|
2465
|
+
bodies_at() {
|
|
2466
|
+
local sha="${1}" synth agent has
|
|
2467
|
+
synth="^<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${sha}(\\r?\\n|$)"
|
|
2468
|
+
agent="^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${sha}(\\r?\\n|$)"
|
|
2469
|
+
has=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "$synth" 2>/dev/null || echo "")
|
|
2470
|
+
case "$has" in ''|*[!0-9]*) return 1 ;; esac
|
|
2471
|
+
if [ "$has" -gt 0 ]; then
|
|
2472
|
+
printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies "$synth" 2>/dev/null || return 1
|
|
2473
|
+
else
|
|
2474
|
+
printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies "$agent" 2>/dev/null || return 1
|
|
2475
|
+
fi
|
|
2476
|
+
}
|
|
2477
|
+
# FULL: the current head's bodies, exactly as before. BASE_SYNC_ONLY: the head has no reviewers, so the findings are the
|
|
2478
|
+
# carried head's, and only a VERIFIED carry record (trusted, same head, resolver agrees) selects that SHA. DELTA: reviewers
|
|
2479
|
+
# saw only the delta, so the earlier fully reviewed head's findings are read too (they must still reach the Phase 8 gates);
|
|
2480
|
+
# the §6B.4 head-SHA and provenance gates then drop any whose cited line is gone from the current head.
|
|
2481
|
+
PRIMARY_SHA="$REVIEW_SHA"; EARLIER_SHA=""
|
|
2482
|
+
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then
|
|
2483
|
+
CARRIED_FROM=$(carry_verified_from); [ -n "$CARRIED_FROM" ] && PRIMARY_SHA="$CARRIED_FROM"
|
|
2484
|
+
fi
|
|
2485
|
+
if [ "$REVIEW_MODE" = "DELTA" ] && is_sha "$DELTA_FROM"; then EARLIER_SHA="$DELTA_FROM"; fi
|
|
2486
|
+
SOURCE_BODIES=$(bodies_at "$PRIMARY_SHA") \
|
|
2487
|
+
|| { echo "REVIEW_RESULT: status: BLOCKED, blocker: trusted reviewer comments unreadable (fail closed)"; exit 0; }
|
|
2488
|
+
# The panel guard guarantees at least one trusted reviewer body for this head; none means a misread, never "clean".
|
|
2489
|
+
[ -n "$SOURCE_BODIES" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: no trusted reviewer comments for head ${PRIMARY_SHA} (fail closed)"; exit 0; }
|
|
2490
|
+
if [ -n "$EARLIER_SHA" ]; then
|
|
2491
|
+
EARLIER_BODIES=$(bodies_at "$EARLIER_SHA") && [ -n "$EARLIER_BODIES" ] \
|
|
2492
|
+
|| { echo "REVIEW_RESULT: status: BLOCKED, blocker: earlier reviewed head ${EARLIER_SHA} findings unreadable (fail closed)"; exit 0; }
|
|
2493
|
+
SOURCE_BODIES=$(printf '%s\n%s' "$SOURCE_BODIES" "$EARLIER_BODIES")
|
|
2095
2494
|
fi
|
|
2096
2495
|
FINDINGS=$(printf '%s\n' "$SOURCE_BODIES" | jq -r 'scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null)
|
|
2097
2496
|
```
|
|
@@ -2161,7 +2560,8 @@ Filing a standalone `review-finding` issue for every LOW/POSSIBLE reviewer note
|
|
|
2161
2560
|
- **Safety exemption is content-based** (it bypasses only this severity damper, never the §6B.4 provenance gate: a pre-existing auth gap is flagged through the pre-existing route, not attributed to this PR): a LOW or POSSIBLE finding is still filed when its file path / title / body matches the security/billing keyword set — a LOW finding additionally needs `**Confidence**: CONFIRMED`, so LOW LIKELY/POSSIBLE with a keyword is a NOTE (`security|auth|authz|authn|billing|payment|stripe|charge|invoice|injection|xss|csrf|ssrf|idor|secret(s)|credential(s)|permission(s)|sql|token|password|redact`, whole words, `_`/`-`/`/` separate words — so `auth_service` matches but `author`/`tokenizer` do not), or when it came from a dedicated, signal-selected domain agent (Auth, Billing, Concurrency, Database) AND is MEDIUM — a domain agent rescues MEDIUM only, and a LOW finding stays a NOTE whichever reviewer raised it, CONFIRMED or not. **Origin from the always-on General Security & Quality agent alone does NOT exempt a finding**: that agent runs on every PR, so origin-based exemption filed nearly every LOW note it raised (the 2026-10-08 cascade audit: 50 of 60 would-be notes in one batch were filed only for that reason).
|
|
2162
2561
|
- **Review-finding lineage** (the PR's linked issue `MERGE_ISSUE` carries the `review-finding` label, any priority — this PR is itself a fix for a finding): only MEDIUM findings that are CONFIRMED, or LIKELY and safety-exempt, become issues; LOW and POSSIBLE are always NOTEs. A fix for a finding must not mint a new generation of findings.
|
|
2163
2562
|
- **Otherwise**: `**Severity**: LOW`, or `**Confidence**: POSSIBLE` below HIGH, is a NOTE unless safety-exempt (LOW: keyword AND CONFIRMED; MEDIUM: keyword at any confidence, or a domain agent); everything else (MEDIUM CONFIRMED/LIKELY) is an ISSUE.
|
|
2164
|
-
- **
|
|
2563
|
+
- **Cross-head re-flag** <!-- Added: forge#3641 -->: a finding that an earlier head's review already posted, or that an earlier `FORGE:NOTE_DISPOSITION` list recorded, is not filed or noted a second time after its line moved. The caller builds a prior-findings TSV (file, line-start, line-end, title, prior-disposition, issue#) from trusted prior-head reviewer comments (`Reviewed-SHA` other than the current head) and trusted `FORGE:NOTE_DISPOSITION` records carrying a different `reviewed_sha=`, all read through `scripts/trusted-comments.sh`, and passes it as `--prior-findings`. A finding re-flags a row when it is the same file AND (line IoU >= 0.6 after a best-effort `git diff` hunk-offset shift AND >= 1 shared non-stopword title token, OR >= 3 shared title tokens of which >= 2 are non-stopword; generic stopwords such as `the`, `with`, `before`, `write` never count as topical similarity) and becomes `NOTE reflag-filed #N` (the prior row's issue is still open) or `NOTE reflag-note`. Never demoted: HIGH/CRITICAL and unparseable severity (the classifier exits on them before this gate, so they keep blocking via §7B), and any row whose linked issue is closed (a regressed defect is classified normally). Prior rows with no issue link are passed as `note` (recorded NOTE_DISPOSITION) or `unknown` (reviewer-body finding whose linkage failed; the classifier never demotes a MEDIUM CONFIRMED/LIKELY re-flag against an `unknown` row); an unreadable source only shrinks the TSV, it never blocks.
|
|
2564
|
+
- **Contract-declared scope** <!-- Added: forge#3447 -->: the trusted, latest FORGE:CONTRACT of `MERGE_ISSUE` lists out-of-scope items as `deferred → #N`, `not-affected:` or `accepted-risk:` (parsed by `scripts/check-contract-scope.sh list`). A finding whose file equals, or sits under, a `deferred` item whose issue is verified becomes `NOTE contract-deferred #N` and is commented on that issue instead of being filed again. A finding under an `accepted-risk` item becomes `NOTE contract-accepted-risk`. A deferred issue is verified only when it is OPEN, is an issue (not a PR), is not `MERGE_ISSUE` itself, and its body carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` and the deferred path, and it is authored by a trusted author (`author_association` OWNER/MEMBER/COLLABORATOR, or `user.type` Bot); an unverified item is dropped from the scope list. Never demoted: HIGH/CRITICAL, `not-affected` items (a contradicting finding means the contract was wrong), safety-exempt findings under either disposition, findings on a file this PR changed, and anything when the contract is missing, untrusted, unparseable or the deferred issue is closed, unverified or unreadable.
|
|
2165
2565
|
|
|
2166
2566
|
```bash
|
|
2167
2567
|
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
@@ -2212,7 +2612,7 @@ fi
|
|
|
2212
2612
|
# issue still open) or `accepted-risk:` become NOTEs. `not-affected` items never demote. Fail toward
|
|
2213
2613
|
# filing: any missing input (no linked issue, no trusted contract, unparseable section, unreadable
|
|
2214
2614
|
# PR file list) leaves CONTRACT_SCOPE_FILE empty and nothing demotes. A deferred row whose issue fails
|
|
2215
|
-
# verification (closed, unreadable, a PR, MERGE_ISSUE itself,
|
|
2615
|
+
# verification (closed, unreadable, a PR, MERGE_ISSUE itself, no DEFERRED_FROM marker + path, or not authored by a trusted author) is dropped.
|
|
2216
2616
|
CONTRACT_SCOPE_FILE=""; CONTRACT_OPEN_FILE=""; PR_FILES_FILE=""; CONTRACT_DEMOTED=0
|
|
2217
2617
|
if [ -n "${MERGE_ISSUE:-}" ]; then
|
|
2218
2618
|
# 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.
|
|
@@ -2242,7 +2642,9 @@ if [ -n "${MERGE_ISSUE:-}" ]; then
|
|
|
2242
2642
|
&& [ -s "$CONTRACT_SCOPE_FILE" ] \
|
|
2243
2643
|
&& gh pr diff {PR_NUMBER} -R {GH_REPO} --name-only > "$PR_FILES_FILE" 2>/dev/null; then
|
|
2244
2644
|
# A deferred row counts only if issue N is OPEN, is an issue (not a PR), is not MERGE_ISSUE, and its body
|
|
2245
|
-
# carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` plus the row's path
|
|
2645
|
+
# carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` plus the row's path, and is authored by a trusted author
|
|
2646
|
+
# (OWNER/MEMBER/COLLABORATOR or Bot; same predicate as trusted-comments.sh; missing fields fail closed).
|
|
2647
|
+
# Unverified rows are dropped.
|
|
2246
2648
|
_verified=$(mktemp "$_scratch/{PR_NUMBER}_contract-verified.XXXXXX")
|
|
2247
2649
|
while IFS="$(printf '\t')" read -r _disp _ipath _n; do
|
|
2248
2650
|
if [ "$_disp" = "deferred" ]; then
|
|
@@ -2250,7 +2652,7 @@ if [ -n "${MERGE_ISSUE:-}" ]; then
|
|
|
2250
2652
|
[ "$_n" = "${MERGE_ISSUE#\#}" ] && continue
|
|
2251
2653
|
gh api "repos/{GH_REPO}/issues/$_n" 2>/dev/null \
|
|
2252
2654
|
| jq -e --arg m "FORGE:DEFERRED_FROM: #${MERGE_ISSUE#\#} -->" --arg p "$_ipath" \
|
|
2253
|
-
'.state == "open" and .pull_request == null and ((.body // "") | contains($m) and contains($p))' >/dev/null 2>&1 \
|
|
2655
|
+
'.state == "open" and .pull_request == null and ((.body // "") | contains($m) and contains($p)) and (((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR")) or ((.user.type // "") == "Bot"))' >/dev/null 2>&1 \
|
|
2254
2656
|
|| continue
|
|
2255
2657
|
grep -Fxq -- "$_n" "$CONTRACT_OPEN_FILE" || echo "$_n" >> "$CONTRACT_OPEN_FILE"
|
|
2256
2658
|
fi
|
|
@@ -2263,18 +2665,113 @@ if [ -n "${MERGE_ISSUE:-}" ]; then
|
|
|
2263
2665
|
rm -f "$CONTRACT_BODY_FILE"
|
|
2264
2666
|
fi
|
|
2265
2667
|
fi
|
|
2668
|
+
PRIOR_FILE=""; DIFF_MAP_FILE=""; NOTES_REFLAG=0
|
|
2669
|
+
_pscr="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_pscr"
|
|
2670
|
+
REPO="{GH_REPO}"; REVIEW_SHA="{REVIEW_SHA}"
|
|
2671
|
+
if [ -z "${TRUSTED_SCRIPT:-}" ]; then
|
|
2672
|
+
# 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.
|
|
2673
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2674
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2675
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2676
|
+
[ -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)"
|
|
2677
|
+
done
|
|
2678
|
+
TRUSTED_SCRIPT=""
|
|
2679
|
+
while IFS= read -r _c; do
|
|
2680
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2681
|
+
done <<< "$_tc"
|
|
2682
|
+
fi
|
|
2683
|
+
# Cross-head re-flag input (#3641). Fail soft: every unreadable source just leaves the TSV smaller, never blocks the review.
|
|
2684
|
+
# PF_SKIP="<sha or empty>" PF_PRS="<pr numbers>" prior_findings_tsv prints file<TAB>line-start<TAB>line-end<TAB>title<TAB>disposition<TAB>issue# rows
|
|
2685
|
+
# (disposition: filed = linked issue OPEN, filed-closed = linked issue CLOSED, note = a recorded NOTE_DISPOSITION with no linked issue, unknown = a reviewer-body finding whose issue linkage failed, so the classifier must not let it demote a MEDIUM CONFIRMED/LIKELY re-flag). Every comment is read through
|
|
2686
|
+
# scripts/trusted-comments.sh, so an untrusted commenter cannot pre-post a finding to suppress a real one. With PF_SKIP set, reviewer
|
|
2687
|
+
# bodies for that head are skipped and only NOTE_DISPOSITION records carrying a different `reviewed_sha=` count (this head's own record,
|
|
2688
|
+
# from an earlier run of this same review, must never swallow its findings).
|
|
2689
|
+
pf_split_loc() { # PF_LOC="file[:N[-M]]" -> file<TAB>N<TAB>M (`-` sentinel when a line column is absent: a tab IFS read collapses empty fields)
|
|
2690
|
+
local _loc="$PF_LOC" _file _r _a="" _b=""; _file="$_loc"
|
|
2691
|
+
case "$_loc" in *:[0-9]*) _r="${_loc##*:}"; case "$_r" in *[!0-9-]*|"") ;; *) _file="${_loc%:*}"; _a="${_r%%-*}"; _b="${_r#*-}"; [ "$_b" != "$_r" ] || _b="$_a" ;; esac ;; esac
|
|
2692
|
+
printf '%s\t%s\t%s\n' "$_file" "${_a:--}" "${_b:--}"
|
|
2693
|
+
}
|
|
2694
|
+
prior_findings_tsv() {
|
|
2695
|
+
local pr cj rows issues _id _c _s _loc _t _p _hit _f _a _b _src _dflt
|
|
2696
|
+
[ -n "${TRUSTED_SCRIPT:-}" ] || return 0
|
|
2697
|
+
rows=$(mktemp "${_pscr}/prior-rows.XXXXXX"); issues=$(mktemp "${_pscr}/prior-issues.XXXXXX")
|
|
2698
|
+
for pr in $PF_PRS; do
|
|
2699
|
+
case "$pr" in ''|*[!0-9]*) continue ;; esac
|
|
2700
|
+
cj=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" 2>/dev/null) || continue
|
|
2701
|
+
[ -n "$cj" ] || continue
|
|
2702
|
+
printf '%s' "$cj" | bash "$TRUSTED_SCRIPT" bodies '^<!-- (FORGE:REVIEW-AGENT:[a-z-]+|REVIEW-FINDINGS-SYNTHESIZED-START) -->[\s\S]*(^|\n)Reviewed-SHA: [0-9a-f]{40,64}' 2>/dev/null \
|
|
2703
|
+
| jq -r --arg skip "${PF_SKIP:-}" 'select($skip == "" or (test("(^|\\n)Reviewed-SHA: " + $skip + "\\r?(\\n|$)") | not)) | scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null \
|
|
2704
|
+
| while IFS='|' read -r _id _c _s _loc _t; do
|
|
2705
|
+
[ -n "$_t" ] || continue
|
|
2706
|
+
_p=$(PF_LOC="$_loc" pf_split_loc); printf 'F\t%s\t%s\n' "$_p" "$(printf '%s' "$_t" | tr '\t' ' ')"
|
|
2707
|
+
done >> "$rows"
|
|
2708
|
+
printf '%s' "$cj" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:NOTE_DISPOSITION' 2>/dev/null \
|
|
2709
|
+
| jq -r --arg skip "${PF_SKIP:-}" 'select($skip == "" or ((capture("^<!-- FORGE:NOTE_DISPOSITION:[^>]*reviewed_sha=(?<s>[0-9a-f]{40,64})") | .s) as $s | $s != $skip)) | split("\n")[] | select(startswith("- "))' 2>/dev/null \
|
|
2710
|
+
| sed -n 's/^- [^ ]* \([^ ]*\) \[[A-Za-z?]*\] \(.*\): [^:]*$/\1\t\2/p' \
|
|
2711
|
+
| while IFS="$(printf '\t')" read -r _loc _t; do
|
|
2712
|
+
[ "$_loc" != "?" ] || continue
|
|
2713
|
+
_p=$(PF_LOC="$_loc" pf_split_loc); printf 'N\t%s\t%s\n' "$_p" "$_t"
|
|
2714
|
+
done >> "$rows"
|
|
2715
|
+
gh issue list -R "$REPO" --label review-finding --state all --search "${pr} in:body" --limit 100 --json number,state,title,body 2>/dev/null \
|
|
2716
|
+
| jq -c '.[]' >> "$issues" 2>/dev/null || true
|
|
2717
|
+
done
|
|
2718
|
+
# Linkage: a filed issue whose body names the file and whose title shares >= 3 tokens with the finding (open preferred).
|
|
2719
|
+
# Rows carry a leading source column (F = reviewer FINDING marker, N = NOTE_DISPOSITION record), stripped on output. An unlinked F row may
|
|
2720
|
+
# have been filed under a reworded title, so it is `unknown` (never demotes MEDIUM CONFIRMED/LIKELY); an unlinked N row is a true `note`.
|
|
2721
|
+
sort -u "$rows" | while IFS="$(printf '\t')" read -r _src _f _a _b _t; do
|
|
2722
|
+
[ -n "$_f" ] || continue
|
|
2723
|
+
[ "$_a" != "-" ] || _a=""; [ "$_b" != "-" ] || _b=""
|
|
2724
|
+
case "$_src" in N) _dflt=note ;; *) _dflt=unknown ;; esac
|
|
2725
|
+
_hit=$(jq -rs --arg f "$_f" --arg t "$_t" 'def toks: ascii_downcase | [scan("[a-z0-9]{3,}")] | unique;
|
|
2726
|
+
($t | toks) as $a | [ .[] | select((.body // "") | contains($f))
|
|
2727
|
+
| select((((.title // "") | toks) as $b | [$a[] | select(. as $x | $b | index($x))] | length) >= 3)
|
|
2728
|
+
| {n: .number, s: (.state | ascii_downcase)} ]
|
|
2729
|
+
| (map(select(.s == "open")) + map(select(.s != "open"))) | .[0] // empty | "\(.s)\t\(.n)"' "$issues" 2>/dev/null) || _hit=""
|
|
2730
|
+
case "$_hit" in
|
|
2731
|
+
open*) printf '%s\t%s\t%s\t%s\tfiled\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
2732
|
+
closed*) printf '%s\t%s\t%s\t%s\tfiled-closed\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
2733
|
+
*) printf '%s\t%s\t%s\t%s\t%s\t\n' "$_f" "$_a" "$_b" "$_t" "$_dflt" ;;
|
|
2734
|
+
esac
|
|
2735
|
+
done
|
|
2736
|
+
rm -f "$rows" "$issues"
|
|
2737
|
+
}
|
|
2738
|
+
PRIOR_ROWS=$(PF_SKIP="$REVIEW_SHA" PF_PRS="{PR_NUMBER}" prior_findings_tsv)
|
|
2739
|
+
if [ -n "$PRIOR_ROWS" ]; then
|
|
2740
|
+
PRIOR_FILE=$(mktemp "$_pscr/{PR_NUMBER}_prior-findings.XXXXXX"); printf '%s\n' "$PRIOR_ROWS" > "$PRIOR_FILE"
|
|
2741
|
+
# Best-effort hunk map from the most recent prior reviewed head to this one, so a moved line still overlaps. Any failure leaves the unshifted ranges.
|
|
2742
|
+
PRIOR_SHA=$(gh api --paginate "repos/{GH_REPO}/issues/{PR_NUMBER}/comments" 2>/dev/null \
|
|
2743
|
+
| bash "$TRUSTED_SCRIPT" bodies '^<!-- (FORGE:REVIEW-AGENT:[a-z-]+|REVIEW-FINDINGS-SYNTHESIZED-START) -->[\s\S]*(^|\n)Reviewed-SHA: [0-9a-f]{40,64}' 2>/dev/null \
|
|
2744
|
+
| jq -r 'scan("(^|\\n)Reviewed-SHA: ([0-9a-f]{40,64})") | .[1]' 2>/dev/null | grep -vx "$REVIEW_SHA" | tail -n 1)
|
|
2745
|
+
_top=$(git rev-parse --show-toplevel 2>/dev/null) || _top=""
|
|
2746
|
+
if [ -n "$PRIOR_SHA" ] && [ -n "$_top" ]; then
|
|
2747
|
+
DIFF_MAP_FILE=$(mktemp "$_pscr/{PR_NUMBER}_prior-diffmap.XXXXXX")
|
|
2748
|
+
{ git -C "$_top" diff -U0 --no-color --no-renames "$PRIOR_SHA" "$REVIEW_SHA" 2>/dev/null || true; } | while IFS= read -r _l; do
|
|
2749
|
+
case "$_l" in
|
|
2750
|
+
"+++ b/"*) _f="${_l#+++ b/}" ;;
|
|
2751
|
+
"@@ "*) _h="${_l#@@ -}"; _o="${_h%% *}"; _n="${_h#* +}"; _n="${_n%% *}"
|
|
2752
|
+
case "$_o" in *,*) _oc="${_o#*,}" ;; *) _oc=1 ;; esac; case "$_n" in *,*) _nc="${_n#*,}" ;; *) _nc=1 ;; esac
|
|
2753
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$_f" "${_o%%,*}" "$_oc" "${_n%%,*}" "$_nc" ;;
|
|
2754
|
+
esac
|
|
2755
|
+
done > "$DIFF_MAP_FILE" || : > "$DIFF_MAP_FILE"
|
|
2756
|
+
fi
|
|
2757
|
+
fi
|
|
2266
2758
|
# Per deduped finding (FINDING_TEXT_FILE = mktemp file holding title + body + affected paths;
|
|
2267
2759
|
# FINDING_FILE = the finding's file path, file:line accepted):
|
|
2268
2760
|
# DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
|
|
2269
2761
|
# --agent "$FINDING_AGENT" --lineage "$FINDING_LINEAGE" --text-file "$FINDING_TEXT_FILE" \
|
|
2270
2762
|
# ${FINDING_FILE:+--file "$FINDING_FILE"} ${INPR_DIFF_FILE:+--inpr-diff "$INPR_DIFF_FILE"} \
|
|
2271
|
-
# ${CONTRACT_SCOPE_FILE:+--contract-scope "$CONTRACT_SCOPE_FILE" --contract-open "$CONTRACT_OPEN_FILE" --pr-files "$PR_FILES_FILE" --merge-issue "$MERGE_ISSUE"}
|
|
2763
|
+
# ${CONTRACT_SCOPE_FILE:+--contract-scope "$CONTRACT_SCOPE_FILE" --contract-open "$CONTRACT_OPEN_FILE" --pr-files "$PR_FILES_FILE" --merge-issue "$MERGE_ISSUE"} \
|
|
2764
|
+
# ${PRIOR_FILE:+--prior-findings "$PRIOR_FILE" --title "$FINDING_TITLE" ${DIFF_MAP_FILE:+--diff-map "$DIFF_MAP_FILE"}} \
|
|
2765
|
+
# ${FINDING_LINE_START:+--line-start "$FINDING_LINE_START" --line-end "${FINDING_LINE_END:-$FINDING_LINE_START}"})
|
|
2272
2766
|
# case "$DISPOSITION" in ISSUE*) ;; NOTE*) ;; INPR_FIX*) ;; esac # first word is the class, the rest is the reason
|
|
2273
2767
|
# Contract-matched NOTE (`NOTE contract-deferred #N`): comment the finding (file:line + title) on the open
|
|
2274
2768
|
# deferred issue instead of filing a new one, and count it:
|
|
2275
2769
|
# gh issue comment N -R {GH_REPO} --body "Review of #{PR_NUMBER} also found this on a path declared deferred here: <file:line> <title>" # allowlist:check-command-side-effects
|
|
2276
2770
|
# CONTRACT_DEMOTED=$((CONTRACT_DEMOTED + 1))
|
|
2277
2771
|
# `NOTE contract-accepted-risk` is counted in CONTRACT_DEMOTED too (no comment; the contract already records the reason).
|
|
2772
|
+
# Re-flag NOTE (`NOTE reflag-filed #N` / `NOTE reflag-note`): an earlier head already raised or recorded this finding. List it in the NOTE list
|
|
2773
|
+
# (`tracked on #N` or `already noted on an earlier head`), file nothing, and count it ONLY in notes_reflag (not notes_listed):
|
|
2774
|
+
# NOTES_REFLAG=$((NOTES_REFLAG + 1))
|
|
2278
2775
|
```
|
|
2279
2776
|
|
|
2280
2777
|
If the guard printed `REVIEW_RESULT: status: BLOCKED` (`classifier unavailable`), STOP the whole review: `exit 0` only ends that Bash block, so do not continue to 6C or any later phase, file no issues, post no `FORGE:NOTE_DISPOSITION`, and do not merge (Phase 8 never runs, `--auto-merge` included). The caller treats it like any other `BLOCKED` result.
|
|
@@ -2286,14 +2783,15 @@ Each NOTE gets exactly one disposition, in this preference order:
|
|
|
2286
2783
|
|
|
2287
2784
|
**Metrics note (forge#3106)**: because NOTEs are not filed as issues, `review-finding` issue volume and any metric derived from it (findings per PR, `/pipeline-health` finding rates, amplification ratio) drop relative to pre-#3060 history. Compare against the `notes_*` counts in the review summary, not issue counts alone, when judging review depth across the change.
|
|
2288
2785
|
|
|
2289
|
-
Every NOTE disposition must be recorded (never silently dropped): list each NOTE (id, file:line, one-line reason) with its disposition in the review summary comment, including dropped ones, so a reviewer can audit the decision. NOTES are never passed to `Skill(issue)`. Record counts in the review summary (`notes_fixed`, `notes_listed`, `notes_dropped`, `findings_filed`, `inpr_fix`, `contract_demoted`) and the §6B.4 counts (`findings_dropped_stale`, `findings_preexisting`, `notes_demoted_no_scenario`). A contract-demoted NOTE is listed with its disposition (`tracked on #N` or `accepted-risk`) and counted only in `contract_demoted`. If no finding is an ISSUE after this step, skip 6C and continue to Phase 7. Findings classified `INPR_FIX` (§6B.6) are neither notes nor filed this round.
|
|
2786
|
+
Every NOTE disposition must be recorded (never silently dropped): list each NOTE (id, file:line, one-line reason) with its disposition in the review summary comment, including dropped ones, so a reviewer can audit the decision. NOTES are never passed to `Skill(issue)`. Record counts in the review summary (`notes_fixed`, `notes_listed`, `notes_dropped`, `findings_filed`, `inpr_fix`, `contract_demoted`, `notes_reflag`) and the §6B.4 counts (`findings_dropped_stale`, `findings_preexisting`, `notes_demoted_no_scenario`). A contract-demoted NOTE is listed with its disposition (`tracked on #N` or `accepted-risk`) and counted only in `contract_demoted`. If no finding is an ISSUE after this step, skip 6C and continue to Phase 7. Findings classified `INPR_FIX` (§6B.6) are neither notes nor filed this round.
|
|
2290
2787
|
|
|
2291
|
-
**Disposition marker (MANDATORY whenever Phase 6A extracted at least one finding):** post one PR comment whose first line is the machine-readable record below, followed by the NOTE list. Phase 8 refuses to auto-merge a PR that has `FINDING:` markers but no `FORGE:NOTE_DISPOSITION` comment — the disposition step is enforced, not advisory.
|
|
2788
|
+
**Disposition marker (MANDATORY whenever Phase 6A extracted at least one finding):** post one PR comment whose first line is the machine-readable record below, followed by the NOTE list. Phase 8 refuses to auto-merge a PR that has `FINDING:` markers but no `FORGE:NOTE_DISPOSITION` comment — the disposition step is enforced, not advisory. `notes_reflag` counts findings re-flagged from an earlier head (see Cross-head re-flag); `reviewed_sha` is the head this record covers. Consumers match the marker prefix only, so extra keys are safe.
|
|
2292
2789
|
|
|
2293
2790
|
```bash
|
|
2294
2791
|
CLASSIFIER_MODE=script # the §6B.5 guard blocks the review when the classifier is unresolvable, so this is always script
|
|
2792
|
+
REVIEW_SHA="{REVIEW_SHA}"; NOTES_REFLAG="${NOTES_REFLAG:-0}" # fresh shell: reviewed_sha lets a later review of ANOTHER head read this record as a prior list (never the same head's own)
|
|
2295
2793
|
# NOTE_LIST_FILE: mktemp file named for the PR number, holding one line per NOTE (id, file:line, disposition, reason).
|
|
2296
|
-
gh pr comment {PR_NUMBER} -R {GH_REPO} --body "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=${NOTES_FIXED:-0} notes_listed=${NOTES_LISTED:-0} notes_dropped=${NOTES_DROPPED:-0} findings_filed=${FINDINGS_FILED:-0} inpr_fix=${INPR_FIX_COUNT:-0} contract_demoted=${CONTRACT_DEMOTED:-0} findings_dropped_stale=${FINDINGS_DROPPED_STALE:-0} findings_preexisting=${FINDINGS_PREEXISTING:-0} notes_demoted_no_scenario=${NOTES_DEMOTED_NO_SCENARIO:-0} lineage=${FINDING_LINEAGE} classifier=${CLASSIFIER_MODE} -->
|
|
2794
|
+
gh pr comment {PR_NUMBER} -R {GH_REPO} --body "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=${NOTES_FIXED:-0} notes_listed=${NOTES_LISTED:-0} notes_dropped=${NOTES_DROPPED:-0} findings_filed=${FINDINGS_FILED:-0} inpr_fix=${INPR_FIX_COUNT:-0} contract_demoted=${CONTRACT_DEMOTED:-0} findings_dropped_stale=${FINDINGS_DROPPED_STALE:-0} findings_preexisting=${FINDINGS_PREEXISTING:-0} notes_demoted_no_scenario=${NOTES_DEMOTED_NO_SCENARIO:-0} lineage=${FINDING_LINEAGE} classifier=${CLASSIFIER_MODE} notes_reflag=${NOTES_REFLAG:-0} reviewed_sha=${REVIEW_SHA} -->
|
|
2297
2795
|
$(cat "$NOTE_LIST_FILE" 2>/dev/null)" # allowlist:check-command-side-effects
|
|
2298
2796
|
```
|
|
2299
2797
|
|
|
@@ -2351,7 +2849,19 @@ MILESTONE_FLAG=""
|
|
|
2351
2849
|
# milestone via Closes/Fixes/Resolves #N -> branch-slug match against
|
|
2352
2850
|
# milestone/* branches) supersedes the old base/head-branch-only slug match
|
|
2353
2851
|
# that used to be hand-rolled here.
|
|
2354
|
-
|
|
2852
|
+
# HELPER_SCRIPT resolver (DERIVE_MILESTONE_SCRIPT; trusted-install tiers only, never the working directory or the target repo, #3400/#3483): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
2853
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2854
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2855
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2856
|
+
[ -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)"
|
|
2857
|
+
done
|
|
2858
|
+
DERIVE_MILESTONE_SCRIPT=""
|
|
2859
|
+
while IFS= read -r _c; do
|
|
2860
|
+
case "$_c" in /*) [ -z "$DERIVE_MILESTONE_SCRIPT" ] && [ -f "$_c/scripts/derive-finding-milestone.sh" ] && DERIVE_MILESTONE_SCRIPT="$_c/scripts/derive-finding-milestone.sh" ;; esac
|
|
2861
|
+
done <<< "$_tc"
|
|
2862
|
+
# Unresolved helper: empty milestone (non-blocking), never a cwd-relative fallback.
|
|
2863
|
+
MILESTONE_TITLE=""
|
|
2864
|
+
[ -n "$DERIVE_MILESTONE_SCRIPT" ] && MILESTONE_TITLE=$(bash "$DERIVE_MILESTONE_SCRIPT" "${PR_NUMBER}" -R "${REPO}")
|
|
2355
2865
|
# Quoted so multi-word milestone titles (e.g. "Watch & Fleet Observability") survive
|
|
2356
2866
|
# as a single value wherever MILESTONE_FLAG is later interpolated — matches the
|
|
2357
2867
|
# quoting convention already used for --title/--body-file/--label below.
|
|
@@ -2370,8 +2880,23 @@ PR_NUMBER="{PR_NUMBER}"; MERGE_ISSUE="{MERGE_ISSUE}"
|
|
|
2370
2880
|
FINDING_TITLE_DEDUP="fix: brief description of finding (review finding — PR #${PR_NUMBER})"
|
|
2371
2881
|
# Never dedup against the issue this PR fixes (MERGE_ISSUE): it is about to be closed, so a
|
|
2372
2882
|
# finding "matched" to it is tracked nowhere. Field test #3169: SEC-2 matched #3169 itself and was dropped.
|
|
2373
|
-
|
|
2374
|
-
|
|
2883
|
+
# HELPER_SCRIPT resolver (DEDUP_SCRIPT; trusted-install tiers only, never the working directory or the target repo, #3400/#3483): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
2884
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2885
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2886
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2887
|
+
[ -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)"
|
|
2888
|
+
done
|
|
2889
|
+
DEDUP_SCRIPT=""
|
|
2890
|
+
while IFS= read -r _c; do
|
|
2891
|
+
case "$_c" in /*) [ -z "$DEDUP_SCRIPT" ] && [ -f "$_c/scripts/issue-dedup.sh" ] && DEDUP_SCRIPT="$_c/scripts/issue-dedup.sh" ;; esac
|
|
2892
|
+
done <<< "$_tc"
|
|
2893
|
+
# Unresolved helper: "not deduped" (exit 0, empty result) so the line-range check still runs; NEVER exit 1 (which means "duplicate, skip").
|
|
2894
|
+
if [ -n "$DEDUP_SCRIPT" ]; then
|
|
2895
|
+
DEDUP_RESULT=$(bash "$DEDUP_SCRIPT" "$FINDING_TITLE_DEDUP" "$GH_FLAG" ${MERGE_ISSUE:+--exclude "$MERGE_ISSUE"} 2>&1)
|
|
2896
|
+
DEDUP_EXIT=$?
|
|
2897
|
+
else
|
|
2898
|
+
DEDUP_RESULT=""; DEDUP_EXIT=0
|
|
2899
|
+
fi
|
|
2375
2900
|
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
2376
2901
|
echo "DEDUP: Skipping — $DEDUP_RESULT"
|
|
2377
2902
|
# Skip this finding — do NOT create a duplicate issue
|
|
@@ -2607,8 +3132,23 @@ FINDING_SEVERITY="LOW"
|
|
|
2607
3132
|
# (CONFIRMED/LIKELY/POSSIBLE). scripts/severity-to-priority.sh is the single
|
|
2608
3133
|
# source of truth for this mapping; commands/review-pr-staging.md calls the
|
|
2609
3134
|
# identical script so the two specs cannot independently drift. <!-- forge#2447 -->
|
|
2610
|
-
|
|
2611
|
-
|
|
3135
|
+
# HELPER_SCRIPT resolver (PRIORITY_SCRIPT; trusted-install tiers only, never the working directory or the target repo, #3400/#3483): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
3136
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
3137
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
3138
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
3139
|
+
[ -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)"
|
|
3140
|
+
done
|
|
3141
|
+
PRIORITY_SCRIPT=""
|
|
3142
|
+
while IFS= read -r _c; do
|
|
3143
|
+
case "$_c" in /*) [ -z "$PRIORITY_SCRIPT" ] && [ -f "$_c/scripts/severity-to-priority.sh" ] && PRIORITY_SCRIPT="$_c/scripts/severity-to-priority.sh" ;; esac
|
|
3144
|
+
done <<< "$_tc"
|
|
3145
|
+
# Unresolved helper: non-zero status so finding creation is skipped (never defaulted to a priority).
|
|
3146
|
+
if [ -n "$PRIORITY_SCRIPT" ]; then
|
|
3147
|
+
FINDING_PRIORITY=$(bash "$PRIORITY_SCRIPT" "$FINDING_SEVERITY")
|
|
3148
|
+
FINDING_PRIORITY_EXIT=$?
|
|
3149
|
+
else
|
|
3150
|
+
FINDING_PRIORITY=""; FINDING_PRIORITY_EXIT=127
|
|
3151
|
+
fi
|
|
2612
3152
|
|
|
2613
3153
|
# Exit code MUST be checked before use — severity-to-priority.sh exits 1 (empty stdout)
|
|
2614
3154
|
# on a missing/unrecognized severity, mirroring the DEDUP_EXIT idiom above. Proceeding
|
|
@@ -3167,7 +3707,7 @@ else
|
|
|
3167
3707
|
# forge#2570: the hold is now scoped by the PR's target branch. This PR still transitions
|
|
3168
3708
|
# to workflow:awaiting-merge (clearing needs-human) in BOTH cases below — the transition is
|
|
3169
3709
|
# unchanged; only who acts next differs:
|
|
3170
|
-
# - `main`
|
|
3710
|
+
# - the deploy gate (`main`, the GitHub `default_branch`, or the default branch's committed `branches.default`; never read from the PR worktree) (or an unresolved base — fail closed): held for a HUMAN merge
|
|
3171
3711
|
# decision, because `staging → main` is the genuine human gate no agent performs.
|
|
3172
3712
|
# - any other base (staging, milestone/* — reversible integration branches): NOT held for
|
|
3173
3713
|
# a human. It is parked for remediate.md Phase M7's base-scoped auto-land bar (bot APPROVED
|
|
@@ -3183,15 +3723,38 @@ if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
|
3183
3723
|
# forge#2625: `jq -r '.baseRefName'` stringifies a JSON null to the literal text "null" (not
|
|
3184
3724
|
# an empty string), so the `-n` check alone does not catch it — add an explicit `!= "null"`
|
|
3185
3725
|
# check so a literal-string "null" is treated the same as an empty/unresolved base (strict).
|
|
3186
|
-
|
|
3187
|
-
|
|
3726
|
+
# forge#3753: trusted resolver only (GitHub default_branch + committed default-branch config); never forge.yaml from $PWD (the PR worktree is author-controlled). Install roots only; unresolved script => strict.
|
|
3727
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
3728
|
+
FORGE_ROOT=""
|
|
3729
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
3730
|
+
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
3731
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
3732
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
3733
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
3734
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
3735
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
3736
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
3737
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
3738
|
+
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
3739
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
3740
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
3741
|
+
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
3742
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
3743
|
+
while IFS= read -r _c; do
|
|
3744
|
+
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
3745
|
+
done <<< "$_k"
|
|
3746
|
+
fi
|
|
3747
|
+
GATE_SCRIPT=""; [ -n "$FORGE_ROOT" ] && [ -f "$FORGE_ROOT/scripts/resolve-deploy-gate.sh" ] && GATE_SCRIPT="$FORGE_ROOT/scripts/resolve-deploy-gate.sh"
|
|
3748
|
+
if [ -n "$GATE_SCRIPT" ]; then GUARD_GATE=$(bash "$GATE_SCRIPT" "$GUARD_BASE" {GH_REPO} 2>/dev/null || echo true); else GUARD_GATE=true; fi; [ "$GUARD_GATE" = "false" ] || GUARD_GATE=true
|
|
3749
|
+
if [ "$GUARD_GATE" = "false" ]; then
|
|
3750
|
+
# forge#2570: non-default-branch base — defer to remediate.md M7's auto-land, not a human.
|
|
3188
3751
|
# forge#2626: standalone `/review-pr` (no remediate.md M6→M7 chain) would otherwise
|
|
3189
3752
|
# strand this PR at `workflow:awaiting-merge` with no actionable guidance — add a
|
|
3190
3753
|
# manual-merge fallback so the comment is correct in both the chained and standalone cases.
|
|
3191
3754
|
# GOVERNOR-exempt: intentional coordination side-effect (best-effort lease/board/finding post), DRY_RUN-safe — reviewed & accepted for the check-command-side-effects gate. Flagged only by the staging->main full-diff; passes on every feature PR. forge#2627
|
|
3192
|
-
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check on non
|
|
3755
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check on non-default-branch base \`${GUARD_BASE}\`. It is parked at \`workflow:awaiting-merge\` for the remediation auto-land bar (\`remediate.md\` Phase M7, forge#2570) — which auto-merges it on the same fast-lane condition the normal \`/work-on\` path uses for this target branch — NOT for a human merge decision. If no remediation \`M7\` follows (e.g. this review was invoked standalone), merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
|
|
3193
3756
|
else
|
|
3194
|
-
# forge#2570:
|
|
3757
|
+
# forge#2570: default branch / deploy gate (or unresolved base — fail closed to the human gate).
|
|
3195
3758
|
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check. This PR targets the deploy gate (\`${GUARD_BASE:-unresolved base}\`), so it is held at \`workflow:awaiting-merge\` for a human merge decision — \`staging → main\` is the genuine human gate. Merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
|
|
3196
3759
|
fi
|
|
3197
3760
|
# Shell state does not persist between Bash calls and `resolve_script` is not defined in this spec:
|
|
@@ -3454,7 +4017,7 @@ A degraded run that skipped Task-based agent dispatch must be visible from this
|
|
|
3454
4017
|
|
|
3455
4018
|
```bash
|
|
3456
4019
|
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
3457
|
-
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"
|
|
4020
|
+
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"; REVIEW_SHA="{REVIEW_SHA}"; REVIEW_MODE="{REVIEW_MODE}"
|
|
3458
4021
|
# Same trust rule as Phase 3C/4: only trusted comments whose body starts with the marker count.
|
|
3459
4022
|
# 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.
|
|
3460
4023
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
@@ -3466,9 +4029,54 @@ TRUSTED_SCRIPT=""
|
|
|
3466
4029
|
while IFS= read -r _c; do
|
|
3467
4030
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
3468
4031
|
done <<< "$_tc"
|
|
4032
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
4033
|
+
REVIEW_DELTA_SCRIPT=""
|
|
4034
|
+
while IFS= read -r _c; do
|
|
4035
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
4036
|
+
done <<< "$_tc"
|
|
4037
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
4038
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
4039
|
+
# BEGIN review-pr-delta-mode
|
|
4040
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
4041
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
4042
|
+
# Needs REPO PR_NUMBER REVIEW_SHA BASE_BRANCH TRUSTED_SCRIPT REVIEW_DELTA_SCRIPT in scope. Every failure (unresolved script, bad output, bad SHA, empty delta) prints FULL, never a skipped review.
|
|
4043
|
+
review_delta_mode() {
|
|
4044
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
4045
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
4046
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
4047
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
4048
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
4049
|
+
out=$(bash "$REVIEW_DELTA_SCRIPT" --pr "$PR_NUMBER" --head "$REVIEW_SHA" --base "$BASE_BRANCH" --trusted-script "$TRUSTED_SCRIPT" --repo "$REPO" --repo-path "$top" 2>/dev/null) || out=""
|
|
4050
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
4051
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
4052
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
4053
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
4054
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
4055
|
+
if [ "$kind" = "DELTA" ]; then
|
|
4056
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
4057
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
4058
|
+
fi
|
|
4059
|
+
echo "FULL - $rounds"
|
|
4060
|
+
}
|
|
4061
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
4062
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
4063
|
+
carry_verified_from() {
|
|
4064
|
+
local kind from rounds n
|
|
4065
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
4066
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
4067
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
4068
|
+
n=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "\\A(?=[\\s\\S]*\\nReviewed-SHA: ${REVIEW_SHA}\\r?(\\n|$))(?=[\\s\\S]*\\nCarried-From-SHA: ${from}\\r?(\\n|$))<!-- FORGE:REVIEW-CARRIED -->" 2>/dev/null || echo "")
|
|
4069
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
4070
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
4071
|
+
return 0
|
|
4072
|
+
}
|
|
4073
|
+
# END review-pr-delta-mode
|
|
4074
|
+
# BASE_SYNC_ONLY: zero reviewers is the expected panel only when the carry record is trusted and the resolver re-verifies it.
|
|
4075
|
+
CARRIED_FROM=""
|
|
4076
|
+
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then CARRIED_FROM=$(carry_verified_from); fi
|
|
3469
4077
|
ACTUAL_AGENT_DOMAINS=""
|
|
3470
4078
|
if [ -n "$TRUSTED_SCRIPT" ]; then
|
|
3471
|
-
ACTUAL_AGENT_DOMAINS=$(
|
|
4079
|
+
ACTUAL_AGENT_DOMAINS=$(printf '%s' "$COMMENTS_JSON" \
|
|
3472
4080
|
| bash "$TRUSTED_SCRIPT" bodies "^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->" 2>/dev/null \
|
|
3473
4081
|
| jq -rs '[.[] | scan("^<!-- FORGE:REVIEW-AGENT:([a-z-]+) -->") | .[0]] | unique | join(", ")' 2>/dev/null || echo "") # unresolvable => empty => count 0 (degraded, fail closed)
|
|
3474
4082
|
fi
|
|
@@ -3478,9 +4086,12 @@ fi
|
|
|
3478
4086
|
# second "0" line, corrupting the value to "0\n0" in exactly the zero-agent case this
|
|
3479
4087
|
# check exists to detect. <!-- Added: forge#1849 -->
|
|
3480
4088
|
ACTUAL_AGENT_COUNT=$(echo "$ACTUAL_AGENT_DOMAINS" | tr ',' '\n' | grep -c '.' 2>/dev/null)
|
|
4089
|
+
# A verified carried head dispatched no reviewers by design: report it as carried instead of a degraded zero-agent panel.
|
|
4090
|
+
if [ -n "$CARRIED_FROM" ]; then ACTUAL_AGENT_DOMAINS="carried from ${CARRIED_FROM}"; ACTUAL_AGENT_COUNT=0; fi
|
|
4091
|
+
echo "CARRIED_FROM: ${CARRIED_FROM}"
|
|
3481
4092
|
```
|
|
3482
4093
|
|
|
3483
|
-
Substitute `ACTUAL_AGENT_COUNT`/`ACTUAL_AGENT_DOMAINS` into the summary's `**Agents**: [N] ([names])` field below — do NOT substitute a manually-counted or remembered figure. Compare it to `SELECTED_AGENT_COUNT` from Phase 3C. If it is smaller, the panel is degraded: the Phase 3C hard-stop path must already have labelled the PR `review-degraded`, added `needs-human`, and exited without a verdict. Never summarize, approve, or merge a partial panel. `ACTUAL_AGENT_COUNT=0` is the same hard stop, not a solo/inline review mode.
|
|
4094
|
+
Substitute `ACTUAL_AGENT_COUNT`/`ACTUAL_AGENT_DOMAINS` into the summary's `**Agents**: [N] ([names])` field below — do NOT substitute a manually-counted or remembered figure. Compare it to `SELECTED_AGENT_COUNT` from Phase 3C. If it is smaller, the panel is degraded: the Phase 3C hard-stop path must already have labelled the PR `review-degraded`, added `needs-human`, and exited without a verdict. Never summarize, approve, or merge a partial panel. `ACTUAL_AGENT_COUNT=0` is the same hard stop, not a solo/inline review mode, with one exception: when the block printed a non-empty `CARRIED_FROM` (a `BASE_SYNC_ONLY` head whose trusted `FORGE:REVIEW-CARRIED` record the resolver re-verified), zero reviewers is the correct panel; write `**Agents**: carried from <CARRIED_FROM>` in the summary and skip this comparison. An empty `CARRIED_FROM` on a `BASE_SYNC_ONLY` head is the hard stop.
|
|
3484
4095
|
|
|
3485
4096
|
```bash
|
|
3486
4097
|
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|