forgedock 1.13.1 → 1.14.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.mjs +160 -18
- package/bin/engine-cli.mjs +18 -5
- package/bin/engine.mjs +109 -6
- package/bin/forgedock.mjs +4 -1
- package/commands/orchestrate/phase-3-dependency.md +5 -2
- package/commands/orchestrate/phase-4-execution.md +144 -24
- package/commands/pipeline-health/phase-2-metrics.md +5 -3
- package/commands/review-pr-staging.md +289 -39
- package/commands/review-pr.md +604 -42
- package/commands/validate.md +11 -3
- package/commands/work-on/build/architect.md +11 -8
- package/commands/work-on/build.md +27 -6
- package/commands/work-on/close.md +5 -4
- package/commands/work-on/decompose.md +7 -2
- package/commands/work-on/investigate.md +11 -0
- package/commands/work-on/remediate.md +135 -74
- package/commands/work-on/review.md +3 -3
- package/docs/CONFIG.md +14 -1
- package/docs/RELEASE-NOTES.md +8 -0
- package/package.json +1 -1
- 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/checkbox-sections.test.sh +19 -0
- package/scripts/classify-finding.sh +96 -1
- package/scripts/classify-finding.test.sh +47 -0
- package/scripts/forge-root.test.sh +48 -1
- package/scripts/orchestrate-soft-edge.test.sh +58 -0
- package/scripts/remediate-basesync.test.sh +62 -3
- package/scripts/review-delta.sh +3 -2
- package/scripts/review-delta.test.sh +1 -1
- package/scripts/review-pr-delta-wiring.test.sh +266 -0
- package/scripts/review-pr-staging-scope.test.sh +60 -0
- package/scripts/trusted-comments.test.sh +11 -0
package/commands/review-pr.md
CHANGED
|
@@ -1536,6 +1536,210 @@ DOMAIN_FILES_API=$(echo "$FILES" | grep -iE "router|route|endpoint|openapi|sdk|a
|
|
|
1536
1536
|
|
|
1537
1537
|
### 3C: Load Agent Templates & Launch
|
|
1538
1538
|
|
|
1539
|
+
#### Review mode: FULL, DELTA or BASE_SYNC_ONLY (run first, before loading any template)
|
|
1540
|
+
|
|
1541
|
+
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.
|
|
1542
|
+
|
|
1543
|
+
| Mode | Meaning | What changes |
|
|
1544
|
+
|------|---------|--------------|
|
|
1545
|
+
| `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. |
|
|
1546
|
+
| `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. |
|
|
1547
|
+
| `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. |
|
|
1548
|
+
|
|
1549
|
+
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.
|
|
1550
|
+
|
|
1551
|
+
```bash
|
|
1552
|
+
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
1553
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; SELECTED_AGENTS="{SELECTED_AGENTS}"
|
|
1554
|
+
THOROUGH="{THOROUGH}"; IS_MILESTONE_TO_STAGING="{IS_MILESTONE_TO_STAGING}"
|
|
1555
|
+
# 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.
|
|
1556
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1557
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1558
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1559
|
+
[ -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)"
|
|
1560
|
+
done
|
|
1561
|
+
TRUSTED_SCRIPT=""
|
|
1562
|
+
while IFS= read -r _c; do
|
|
1563
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1564
|
+
done <<< "$_tc"
|
|
1565
|
+
# 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.
|
|
1566
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1567
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1568
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1569
|
+
[ -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)"
|
|
1570
|
+
done
|
|
1571
|
+
REVIEW_DELTA_SCRIPT=""
|
|
1572
|
+
while IFS= read -r _c; do
|
|
1573
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
1574
|
+
done <<< "$_tc"
|
|
1575
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
1576
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1577
|
+
# BEGIN review-pr-delta-mode
|
|
1578
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
1579
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
1580
|
+
# 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.
|
|
1581
|
+
review_delta_mode() {
|
|
1582
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
1583
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
1584
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
1585
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
1586
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
1587
|
+
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=""
|
|
1588
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
1589
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
1590
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
1591
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
1592
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
1593
|
+
if [ "$kind" = "DELTA" ]; then
|
|
1594
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
1595
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
1596
|
+
fi
|
|
1597
|
+
echo "FULL - $rounds"
|
|
1598
|
+
}
|
|
1599
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
1600
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
1601
|
+
carry_verified_from() {
|
|
1602
|
+
local kind from rounds n
|
|
1603
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
1604
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
1605
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
1606
|
+
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 "")
|
|
1607
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
1608
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
1609
|
+
return 0
|
|
1610
|
+
}
|
|
1611
|
+
# END review-pr-delta-mode
|
|
1612
|
+
# BEGIN review-pr-delta-roster
|
|
1613
|
+
# Effective roster for a DELTA head. While fewer than two full panels have run (FULL_ROUNDS < 2) the 3B roster stands;
|
|
1614
|
+
# from the third review on it narrows to Security plus the domains whose trigger paths the delta touches.
|
|
1615
|
+
# Args: FULL_ROUNDS, the canonical 3B roster (space separated), the delta file list (one path per line).
|
|
1616
|
+
delta_roster() {
|
|
1617
|
+
local rounds="${1}" full="${2}" files="${3}" out="security"
|
|
1618
|
+
case "$rounds" in ''|*[!0-9]*) printf '%s' "$full"; return 0 ;; esac
|
|
1619
|
+
if [ "$rounds" -lt 2 ]; then printf '%s' "$full"; return 0; fi
|
|
1620
|
+
printf '%s\n' "$files" | grep -qiE "auth|session|login|logout|jwt|oauth|token|user|permission|middleware" && out="$out auth"
|
|
1621
|
+
printf '%s\n' "$files" | grep -qiE "billing|credit|payment|charge|refund|subscription|pricing|tier" && out="$out billing"
|
|
1622
|
+
printf '%s\n' "$files" | grep -qiE "lock|queue|atomic|transaction|worker|job|task|celery|redis|claim|lease|engine-state" && out="$out concurrency"
|
|
1623
|
+
printf '%s\n' "$files" | grep -qiE "migration|\.sql$|model|schema|db|database|alembic" && out="$out database"
|
|
1624
|
+
printf '%s\n' "$files" | grep -qiE "docker|nginx|traefik|\.github|Makefile|deploy|infra" && out="$out infra"
|
|
1625
|
+
printf '%s\n' "$files" | grep -qiE "scrape|crawler|stealth|browser.*pool|headless|anti.bot|captcha" && out="$out scraper"
|
|
1626
|
+
printf '%s\n' "$files" | grep -qiE "^web/|\.tsx?$|\.jsx?$|component|page|layout|style|css" && out="$out frontend"
|
|
1627
|
+
printf '%s\n' "$files" | grep -qiE "router|route|endpoint|openapi|sdk|api" && out="$out api"
|
|
1628
|
+
printf '%s\n' "$files" | grep -qE "^(commands|bin)/" && out="$out spec-cli"
|
|
1629
|
+
printf '%s' "$out"
|
|
1630
|
+
}
|
|
1631
|
+
# END review-pr-delta-roster
|
|
1632
|
+
REVIEW_MODE="FULL"; DELTA_FROM=""; CARRIED_FROM=""; FULL_ROUNDS=0
|
|
1633
|
+
if [ "$THOROUGH" != "true" ] && [ "$IS_MILESTONE_TO_STAGING" != "true" ]; then
|
|
1634
|
+
read -r RD_KIND RD_ARG RD_ROUNDS <<< "$(review_delta_mode)"
|
|
1635
|
+
FULL_ROUNDS="${RD_ROUNDS:-0}"
|
|
1636
|
+
case "$RD_KIND" in
|
|
1637
|
+
DELTA) REVIEW_MODE="DELTA"; DELTA_FROM="$RD_ARG" ;;
|
|
1638
|
+
BASE_SYNC_ONLY) REVIEW_MODE="BASE_SYNC_ONLY"; CARRIED_FROM="$RD_ARG" ;;
|
|
1639
|
+
esac
|
|
1640
|
+
fi
|
|
1641
|
+
if [ "$REVIEW_MODE" = "DELTA" ]; then
|
|
1642
|
+
RD_TOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
1643
|
+
DELTA_FILES=$(git -C "$RD_TOP" -c core.quotepath=off diff --name-only --no-renames "${DELTA_FROM}" "${REVIEW_SHA}" 2>/dev/null) || DELTA_FILES=""
|
|
1644
|
+
if [ -z "$DELTA_FILES" ]; then
|
|
1645
|
+
REVIEW_MODE="FULL"; DELTA_FROM="" # unreadable or empty delta: review the full diff
|
|
1646
|
+
else
|
|
1647
|
+
SELECTED_AGENTS=$(delta_roster "$FULL_ROUNDS" "$SELECTED_AGENTS" "$DELTA_FILES")
|
|
1648
|
+
fi
|
|
1649
|
+
fi
|
|
1650
|
+
[ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ] && SELECTED_AGENTS=""
|
|
1651
|
+
echo "REVIEW_MODE: ${REVIEW_MODE}"
|
|
1652
|
+
echo "DELTA_FROM: ${DELTA_FROM}"
|
|
1653
|
+
echo "CARRIED_FROM: ${CARRIED_FROM}"
|
|
1654
|
+
echo "FULL_ROUNDS: ${FULL_ROUNDS}"
|
|
1655
|
+
echo "SELECTED_AGENTS: ${SELECTED_AGENTS}"
|
|
1656
|
+
```
|
|
1657
|
+
|
|
1658
|
+
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.
|
|
1659
|
+
|
|
1660
|
+
**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:
|
|
1661
|
+
|
|
1662
|
+
```bash
|
|
1663
|
+
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
1664
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"
|
|
1665
|
+
# 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.
|
|
1666
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1667
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1668
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1669
|
+
[ -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)"
|
|
1670
|
+
done
|
|
1671
|
+
TRUSTED_SCRIPT=""
|
|
1672
|
+
while IFS= read -r _c; do
|
|
1673
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1674
|
+
done <<< "$_tc"
|
|
1675
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
1676
|
+
REVIEW_DELTA_SCRIPT=""
|
|
1677
|
+
while IFS= read -r _c; do
|
|
1678
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
1679
|
+
done <<< "$_tc"
|
|
1680
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
1681
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1682
|
+
# BEGIN review-pr-delta-mode
|
|
1683
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
1684
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
1685
|
+
# 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.
|
|
1686
|
+
review_delta_mode() {
|
|
1687
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
1688
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
1689
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
1690
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
1691
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
1692
|
+
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=""
|
|
1693
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
1694
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
1695
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
1696
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
1697
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
1698
|
+
if [ "$kind" = "DELTA" ]; then
|
|
1699
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
1700
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
1701
|
+
fi
|
|
1702
|
+
echo "FULL - $rounds"
|
|
1703
|
+
}
|
|
1704
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
1705
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
1706
|
+
carry_verified_from() {
|
|
1707
|
+
local kind from rounds n
|
|
1708
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
1709
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
1710
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
1711
|
+
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 "")
|
|
1712
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
1713
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
1714
|
+
return 0
|
|
1715
|
+
}
|
|
1716
|
+
# END review-pr-delta-mode
|
|
1717
|
+
read -r RD_KIND RD_FROM RD_ROUNDS <<< "$(review_delta_mode)"
|
|
1718
|
+
if [ "$RD_KIND" != "BASE_SYNC_ONLY" ]; then
|
|
1719
|
+
echo "CARRY_RECORD: NOT_APPLICABLE (resolver says ${RD_KIND}); run the FULL path"; exit 0
|
|
1720
|
+
fi
|
|
1721
|
+
if [ -n "$(carry_verified_from)" ]; then
|
|
1722
|
+
echo "CARRY_RECORD: PRESENT (verified from ${RD_FROM})"; exit 0 # re-entry: never post a second record
|
|
1723
|
+
fi
|
|
1724
|
+
gh pr comment "$PR_NUMBER" -R "$REPO" --body "<!-- FORGE:REVIEW-CARRIED -->
|
|
1725
|
+
Reviewed-SHA: ${REVIEW_SHA}
|
|
1726
|
+
Carried-From-SHA: ${RD_FROM}
|
|
1727
|
+
|
|
1728
|
+
## Review carried forward
|
|
1729
|
+
|
|
1730
|
+
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."
|
|
1731
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1732
|
+
if [ -n "$(carry_verified_from)" ]; then
|
|
1733
|
+
echo "CARRY_RECORD: POSTED (verified from ${RD_FROM})"
|
|
1734
|
+
else
|
|
1735
|
+
echo "CARRY_RECORD: UNVERIFIED (the record is not trusted or could not be read back); run the FULL path"
|
|
1736
|
+
fi
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
`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.
|
|
1740
|
+
|
|
1741
|
+
**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 -->
|
|
1742
|
+
|
|
1539
1743
|
**>>> MANDATORY TEMPLATE-RESOLUTION GUARD — run BEFORE any Read of persona templates:**
|
|
1540
1744
|
|
|
1541
1745
|
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 +1800,7 @@ Read: $MONOLITHIC_CATALOG
|
|
|
1596
1800
|
```
|
|
1597
1801
|
Then extract the shared protocols section and each selected persona's section from within that single file — same content, different packaging.
|
|
1598
1802
|
|
|
1599
|
-
Persona file names: `security.md` (always), `auth.md`, `billing.md`, `concurrency.md`, `scraper.md`, `frontend.md`, `api.md`, `database.md`, `infra.md
|
|
1803
|
+
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/**`).
|
|
1600
1804
|
See `review-pr-agents.md` for the full routing table mapping domains → persona files.
|
|
1601
1805
|
|
|
1602
1806
|
(`$FORGE_HOME` defaults to `~/.claude`)
|
|
@@ -1628,7 +1832,17 @@ Before launching agents, query the code index to build a domain-scoped file list
|
|
|
1628
1832
|
|
|
1629
1833
|
```bash
|
|
1630
1834
|
REPO_PATH=$(yq '.paths.root' "$FORGE_YAML" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
1631
|
-
CODE_INDEX_SCRIPT
|
|
1835
|
+
# 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.
|
|
1836
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1837
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1838
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1839
|
+
[ -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)"
|
|
1840
|
+
done
|
|
1841
|
+
CODE_INDEX_SCRIPT=""
|
|
1842
|
+
while IFS= read -r _c; do
|
|
1843
|
+
case "$_c" in /*) [ -z "$CODE_INDEX_SCRIPT" ] && [ -f "$_c/scripts/code-index.sh" ] && CODE_INDEX_SCRIPT="$_c/scripts/code-index.sh" ;; esac
|
|
1844
|
+
done <<< "$_tc"
|
|
1845
|
+
# REPO_PATH is the PR checkout (author-controlled): it is only the index target, never the script source. Unresolved: skip the index.
|
|
1632
1846
|
|
|
1633
1847
|
# Ensure index is current (cache-hit on unchanged HEAD — instant if already built)
|
|
1634
1848
|
if [[ -x "$CODE_INDEX_SCRIPT" ]]; then
|
|
@@ -1665,10 +1879,22 @@ Each domain agent receives only the diff slice relevant to its domain, not the f
|
|
|
1665
1879
|
|
|
1666
1880
|
```bash
|
|
1667
1881
|
# 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}"
|
|
1882
|
+
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"; REVIEW_SHA="{REVIEW_SHA}"; REVIEW_MODE="{REVIEW_MODE}"; DELTA_FROM="{DELTA_FROM}"
|
|
1669
1883
|
# Full diff fetched once — agents do NOT re-fetch it
|
|
1670
1884
|
FULL_DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO")
|
|
1671
1885
|
FULL_FILES=$(gh pr diff "$PR_NUMBER" -R "$REPO" --name-only)
|
|
1886
|
+
# DELTA (review-delta mode): the slice source becomes the range diff since the last fully reviewed head, and FULL_FILES stays as
|
|
1887
|
+
# context. Every failure (bad SHA, commit missing from the checkout, unreadable or empty range) keeps the full PR diff above.
|
|
1888
|
+
# The variable keeps its FULL_DIFF name so every awk slice below is unchanged.
|
|
1889
|
+
if [ "$REVIEW_MODE" = "DELTA" ]; then
|
|
1890
|
+
is_hex() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
1891
|
+
RD_TOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
1892
|
+
if is_hex "$DELTA_FROM" && is_hex "$REVIEW_SHA" && [ -n "$RD_TOP" ] \
|
|
1893
|
+
&& 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
|
|
1894
|
+
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=""
|
|
1895
|
+
[ -n "$DELTA_DIFF" ] && FULL_DIFF="$DELTA_DIFF"
|
|
1896
|
+
fi
|
|
1897
|
+
fi
|
|
1672
1898
|
|
|
1673
1899
|
# --- Security agent: always receives the full diff (cross-cutting domain) ---
|
|
1674
1900
|
DIFF_SLICE_SECURITY="$FULL_DIFF"
|
|
@@ -1736,6 +1962,14 @@ DIFF_SLICE_SCRAPER=$(echo "$FULL_DIFF" | awk '
|
|
|
1736
1962
|
in_block { print }
|
|
1737
1963
|
')
|
|
1738
1964
|
[ -z "$DIFF_SLICE_SCRAPER" ] && DIFF_SLICE_SCRAPER="$FULL_DIFF"
|
|
1965
|
+
|
|
1966
|
+
# --- Spec/CLI agent (DELTA rosters only): commands/** and bin/** files ---
|
|
1967
|
+
DIFF_SLICE_SPEC_CLI=$(echo "$FULL_DIFF" | awk '
|
|
1968
|
+
/^diff --git/ { in_block=0 }
|
|
1969
|
+
/^diff --git a\/(commands|bin)\// { in_block=1 }
|
|
1970
|
+
in_block { print }
|
|
1971
|
+
')
|
|
1972
|
+
[ -z "$DIFF_SLICE_SPEC_CLI" ] && DIFF_SLICE_SPEC_CLI="$FULL_DIFF"
|
|
1739
1973
|
```
|
|
1740
1974
|
|
|
1741
1975
|
**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 +1995,7 @@ DIFF_SLICE_FRONTEND=$(truncate_slice "$DIFF_SLICE_FRONTEND")
|
|
|
1761
1995
|
DIFF_SLICE_API=$(truncate_slice "$DIFF_SLICE_API")
|
|
1762
1996
|
DIFF_SLICE_INFRA=$(truncate_slice "$DIFF_SLICE_INFRA")
|
|
1763
1997
|
DIFF_SLICE_SCRAPER=$(truncate_slice "$DIFF_SLICE_SCRAPER")
|
|
1998
|
+
DIFF_SLICE_SPEC_CLI=$(truncate_slice "$DIFF_SLICE_SPEC_CLI")
|
|
1764
1999
|
```
|
|
1765
2000
|
|
|
1766
2001
|
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 +2012,7 @@ The `protocols.md` file contains the Evidence-Based Review Protocol, Structured
|
|
|
1777
2012
|
|
|
1778
2013
|
**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
2014
|
|
|
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.
|
|
2015
|
+
**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
2016
|
|
|
1782
2017
|
```bash
|
|
1783
2018
|
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
@@ -1900,7 +2135,7 @@ Reviewer completion is read from GitHub, never from notifications (see the "Revi
|
|
|
1900
2135
|
|
|
1901
2136
|
```bash
|
|
1902
2137
|
# 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}"
|
|
2138
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; SELECTED_AGENTS="{SELECTED_AGENTS}"; REVIEW_MODE="{REVIEW_MODE}"
|
|
1904
2139
|
DISPATCH_TOOL="{DISPATCH_TOOL}" # Task | Agent | task (OpenCode)
|
|
1905
2140
|
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
2141
|
if [ "$REVIEW_SHA_OK" != "true" ]; then
|
|
@@ -1951,9 +2186,58 @@ TRUSTED_SCRIPT=""
|
|
|
1951
2186
|
while IFS= read -r _c; do
|
|
1952
2187
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1953
2188
|
done <<< "$_tc"
|
|
2189
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
2190
|
+
REVIEW_DELTA_SCRIPT=""
|
|
2191
|
+
while IFS= read -r _c; do
|
|
2192
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
2193
|
+
done <<< "$_tc"
|
|
2194
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
1954
2195
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1955
2196
|
MISSING_AGENT_COMMENTS=""
|
|
1956
2197
|
ACTUAL_AGENT_COUNT=0
|
|
2198
|
+
# BEGIN review-pr-delta-mode
|
|
2199
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
2200
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
2201
|
+
# 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.
|
|
2202
|
+
review_delta_mode() {
|
|
2203
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
2204
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
2205
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
2206
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
2207
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
2208
|
+
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=""
|
|
2209
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
2210
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
2211
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
2212
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
2213
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
2214
|
+
if [ "$kind" = "DELTA" ]; then
|
|
2215
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
2216
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
2217
|
+
fi
|
|
2218
|
+
echo "FULL - $rounds"
|
|
2219
|
+
}
|
|
2220
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
2221
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
2222
|
+
carry_verified_from() {
|
|
2223
|
+
local kind from rounds n
|
|
2224
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
2225
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
2226
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
2227
|
+
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 "")
|
|
2228
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
2229
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
2230
|
+
return 0
|
|
2231
|
+
}
|
|
2232
|
+
# END review-pr-delta-mode
|
|
2233
|
+
# A zero-reviewer panel is acceptable only for a BASE_SYNC_ONLY head whose carry record is trusted AND re-verified by the
|
|
2234
|
+
# resolver right now. An empty roster with no verified record is a shortfall, never a clean panel (fail closed).
|
|
2235
|
+
CARRIED_FROM=""
|
|
2236
|
+
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then CARRIED_FROM=$(carry_verified_from); fi
|
|
2237
|
+
EMPTY_UNVERIFIED=false
|
|
2238
|
+
if [ "$SELECTED_AGENT_COUNT" -eq 0 ] && [ -z "$CARRIED_FROM" ]; then
|
|
2239
|
+
EMPTY_UNVERIFIED=true; MISSING_AGENT_COMMENTS=" (empty roster without a verified carried record)"
|
|
2240
|
+
fi
|
|
1957
2241
|
# BEGIN review-pr-agent-domain
|
|
1958
2242
|
agent_domain() {
|
|
1959
2243
|
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
@@ -1981,8 +2265,9 @@ for AGENT in $SELECTED_AGENTS; do
|
|
|
1981
2265
|
done
|
|
1982
2266
|
|
|
1983
2267
|
if [ -z "$MISSING_AGENT_COMMENTS" ] && [ "$ACTUAL_AGENT_COUNT" -ge "$SELECTED_AGENT_COUNT" ]; then
|
|
1984
|
-
rm -f "$DEADLINE_FILE"
|
|
1985
|
-
|
|
2268
|
+
rm -f "$DEADLINE_FILE"
|
|
2269
|
+
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
|
|
2270
|
+
elif [ "$JOINED" = "true" ] || [ "$EMPTY_UNVERIFIED" = "true" ] || [ "$(date +%s)" -ge "$DEADLINE" ]; then
|
|
1986
2271
|
rm -f "$DEADLINE_FILE"
|
|
1987
2272
|
echo "REVIEW DELIVERY FAILURE: missing trusted current-SHA findings comment(s) for:${MISSING_AGENT_COMMENTS}"
|
|
1988
2273
|
echo "Use each agent's returned verdict, findings, and durable body path to recover the review."
|
|
@@ -1995,17 +2280,19 @@ fi
|
|
|
1995
2280
|
|
|
1996
2281
|
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
2282
|
|
|
2283
|
+
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.
|
|
2284
|
+
|
|
1998
2285
|
**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
2286
|
|
|
2000
2287
|
---
|
|
2001
2288
|
|
|
2002
2289
|
## Phase 5: Synthesis (Multi-Agent Arbitration)
|
|
2003
2290
|
|
|
2004
|
-
**Skip if**: Only 1 agent OR total findings ≤ 3.
|
|
2291
|
+
**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
2292
|
|
|
2006
2293
|
```bash
|
|
2007
2294
|
# 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}"
|
|
2295
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; REVIEW_MODE="{REVIEW_MODE}"
|
|
2009
2296
|
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
2297
|
[ "$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
2298
|
# Extract structured finding IDs from FINDING HTML comments.
|
|
@@ -2024,8 +2311,56 @@ TRUSTED_SCRIPT=""
|
|
|
2024
2311
|
while IFS= read -r _c; do
|
|
2025
2312
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2026
2313
|
done <<< "$_tc"
|
|
2314
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
2315
|
+
REVIEW_DELTA_SCRIPT=""
|
|
2316
|
+
while IFS= read -r _c; do
|
|
2317
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
2318
|
+
done <<< "$_tc"
|
|
2319
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2027
2320
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
2028
|
-
|
|
2321
|
+
# BEGIN review-pr-delta-mode
|
|
2322
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
2323
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
2324
|
+
# 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.
|
|
2325
|
+
review_delta_mode() {
|
|
2326
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
2327
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
2328
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
2329
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
2330
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
2331
|
+
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=""
|
|
2332
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
2333
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
2334
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
2335
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
2336
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
2337
|
+
if [ "$kind" = "DELTA" ]; then
|
|
2338
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
2339
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
2340
|
+
fi
|
|
2341
|
+
echo "FULL - $rounds"
|
|
2342
|
+
}
|
|
2343
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
2344
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
2345
|
+
carry_verified_from() {
|
|
2346
|
+
local kind from rounds n
|
|
2347
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
2348
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
2349
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
2350
|
+
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 "")
|
|
2351
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
2352
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
2353
|
+
return 0
|
|
2354
|
+
}
|
|
2355
|
+
# END review-pr-delta-mode
|
|
2356
|
+
# BASE_SYNC_ONLY: no reviewer posted for this head, so the findings are the carried head's. Only a verified carry record
|
|
2357
|
+
# switches the SHA (the SHA comes from the validated record, never from untrusted text); an unverified claim keeps the
|
|
2358
|
+
# current head, finds no reviewer bodies and blocks below (fail closed).
|
|
2359
|
+
FINDINGS_SHA="$REVIEW_SHA"
|
|
2360
|
+
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then
|
|
2361
|
+
CARRIED_FROM=$(carry_verified_from); [ -n "$CARRIED_FROM" ] && FINDINGS_SHA="$CARRIED_FROM"
|
|
2362
|
+
fi
|
|
2363
|
+
AGENT_RE="^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${FINDINGS_SHA}(\\r?\\n|$)"
|
|
2029
2364
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
2030
2365
|
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
2366
|
fi
|
|
@@ -2033,7 +2368,7 @@ AGENT_BODIES=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies "$AG
|
|
|
2033
2368
|
|| { echo "REVIEW_RESULT: status: BLOCKED, blocker: trusted reviewer comments unreadable (fail closed)"; exit 0; }
|
|
2034
2369
|
ALL_FINDINGS=$(printf '%s\n' "$AGENT_BODIES" | jq -r 'scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null)
|
|
2035
2370
|
# 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: ${
|
|
2371
|
+
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
2372
|
case "$AGENT_COUNT" in ''|*[!0-9]*) AGENT_COUNT=0 ;; esac # fail closed
|
|
2038
2373
|
FINDING_COUNT=$(echo "$ALL_FINDINGS" | grep -c '.' || echo 0)
|
|
2039
2374
|
```
|
|
@@ -2057,10 +2392,10 @@ If synthesis needed, launch a `general-purpose` Task (model: `"{SUBAGENT_MODEL}"
|
|
|
2057
2392
|
|
|
2058
2393
|
```bash
|
|
2059
2394
|
# 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}"
|
|
2395
|
+
REPO="{GH_REPO}"; PR_NUMBER="{PR_NUMBER}"; REVIEW_SHA="{REVIEW_SHA}"; REVIEW_MODE="{REVIEW_MODE}"; DELTA_FROM="{DELTA_FROM}"
|
|
2061
2396
|
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
2397
|
[ "$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):
|
|
2398
|
+
# 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
2399
|
# findings from earlier heads are already fixed or re-reviewed, and an untrusted commenter must never be able to inject
|
|
2065
2400
|
# findings or post a forged synthesis block that makes the review read clean (the head SHA is public).
|
|
2066
2401
|
# 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 +2408,82 @@ TRUSTED_SCRIPT=""
|
|
|
2073
2408
|
while IFS= read -r _c; do
|
|
2074
2409
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2075
2410
|
done <<< "$_tc"
|
|
2411
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
2412
|
+
REVIEW_DELTA_SCRIPT=""
|
|
2413
|
+
while IFS= read -r _c; do
|
|
2414
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
2415
|
+
done <<< "$_tc"
|
|
2416
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2076
2417
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
2077
2418
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
2078
2419
|
echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/trusted-comments.sh unresolvable or PR comments unreadable; refusing to read clean (fail closed)"; exit 0
|
|
2079
2420
|
fi
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2421
|
+
# BEGIN review-pr-delta-mode
|
|
2422
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
2423
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
2424
|
+
# 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.
|
|
2425
|
+
review_delta_mode() {
|
|
2426
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
2427
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
2428
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
2429
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
2430
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
2431
|
+
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=""
|
|
2432
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
2433
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
2434
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
2435
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
2436
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
2437
|
+
if [ "$kind" = "DELTA" ]; then
|
|
2438
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
2439
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
2440
|
+
fi
|
|
2441
|
+
echo "FULL - $rounds"
|
|
2442
|
+
}
|
|
2443
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
2444
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
2445
|
+
carry_verified_from() {
|
|
2446
|
+
local kind from rounds n
|
|
2447
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
2448
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
2449
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
2450
|
+
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 "")
|
|
2451
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
2452
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
2453
|
+
return 0
|
|
2454
|
+
}
|
|
2455
|
+
# END review-pr-delta-mode
|
|
2456
|
+
# Trusted synthesized block for one head when present, else that head's trusted agent bodies (the same predicate as before,
|
|
2457
|
+
# now per head). Returns non-zero when the trust script cannot count or read.
|
|
2458
|
+
bodies_at() {
|
|
2459
|
+
local sha="${1}" synth agent has
|
|
2460
|
+
synth="^<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${sha}(\\r?\\n|$)"
|
|
2461
|
+
agent="^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${sha}(\\r?\\n|$)"
|
|
2462
|
+
has=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "$synth" 2>/dev/null || echo "")
|
|
2463
|
+
case "$has" in ''|*[!0-9]*) return 1 ;; esac
|
|
2464
|
+
if [ "$has" -gt 0 ]; then
|
|
2465
|
+
printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies "$synth" 2>/dev/null || return 1
|
|
2466
|
+
else
|
|
2467
|
+
printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies "$agent" 2>/dev/null || return 1
|
|
2468
|
+
fi
|
|
2469
|
+
}
|
|
2470
|
+
# FULL: the current head's bodies, exactly as before. BASE_SYNC_ONLY: the head has no reviewers, so the findings are the
|
|
2471
|
+
# carried head's, and only a VERIFIED carry record (trusted, same head, resolver agrees) selects that SHA. DELTA: reviewers
|
|
2472
|
+
# saw only the delta, so the earlier fully reviewed head's findings are read too (they must still reach the Phase 8 gates);
|
|
2473
|
+
# the §6B.4 head-SHA and provenance gates then drop any whose cited line is gone from the current head.
|
|
2474
|
+
PRIMARY_SHA="$REVIEW_SHA"; EARLIER_SHA=""
|
|
2475
|
+
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then
|
|
2476
|
+
CARRIED_FROM=$(carry_verified_from); [ -n "$CARRIED_FROM" ] && PRIMARY_SHA="$CARRIED_FROM"
|
|
2477
|
+
fi
|
|
2478
|
+
if [ "$REVIEW_MODE" = "DELTA" ] && is_sha "$DELTA_FROM"; then EARLIER_SHA="$DELTA_FROM"; fi
|
|
2479
|
+
SOURCE_BODIES=$(bodies_at "$PRIMARY_SHA") \
|
|
2480
|
+
|| { echo "REVIEW_RESULT: status: BLOCKED, blocker: trusted reviewer comments unreadable (fail closed)"; exit 0; }
|
|
2481
|
+
# The panel guard guarantees at least one trusted reviewer body for this head; none means a misread, never "clean".
|
|
2482
|
+
[ -n "$SOURCE_BODIES" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: no trusted reviewer comments for head ${PRIMARY_SHA} (fail closed)"; exit 0; }
|
|
2483
|
+
if [ -n "$EARLIER_SHA" ]; then
|
|
2484
|
+
EARLIER_BODIES=$(bodies_at "$EARLIER_SHA") && [ -n "$EARLIER_BODIES" ] \
|
|
2485
|
+
|| { echo "REVIEW_RESULT: status: BLOCKED, blocker: earlier reviewed head ${EARLIER_SHA} findings unreadable (fail closed)"; exit 0; }
|
|
2486
|
+
SOURCE_BODIES=$(printf '%s\n%s' "$SOURCE_BODIES" "$EARLIER_BODIES")
|
|
2095
2487
|
fi
|
|
2096
2488
|
FINDINGS=$(printf '%s\n' "$SOURCE_BODIES" | jq -r 'scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null)
|
|
2097
2489
|
```
|
|
@@ -2161,7 +2553,8 @@ Filing a standalone `review-finding` issue for every LOW/POSSIBLE reviewer note
|
|
|
2161
2553
|
- **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
2554
|
- **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
2555
|
- **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
|
-
- **
|
|
2556
|
+
- **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, OR >= 3 shared title tokens) 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`; an unreadable source only shrinks the TSV, it never blocks.
|
|
2557
|
+
- **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
2558
|
|
|
2166
2559
|
```bash
|
|
2167
2560
|
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|
|
@@ -2212,7 +2605,7 @@ fi
|
|
|
2212
2605
|
# issue still open) or `accepted-risk:` become NOTEs. `not-affected` items never demote. Fail toward
|
|
2213
2606
|
# filing: any missing input (no linked issue, no trusted contract, unparseable section, unreadable
|
|
2214
2607
|
# 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,
|
|
2608
|
+
# verification (closed, unreadable, a PR, MERGE_ISSUE itself, no DEFERRED_FROM marker + path, or not authored by a trusted author) is dropped.
|
|
2216
2609
|
CONTRACT_SCOPE_FILE=""; CONTRACT_OPEN_FILE=""; PR_FILES_FILE=""; CONTRACT_DEMOTED=0
|
|
2217
2610
|
if [ -n "${MERGE_ISSUE:-}" ]; then
|
|
2218
2611
|
# 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 +2635,9 @@ if [ -n "${MERGE_ISSUE:-}" ]; then
|
|
|
2242
2635
|
&& [ -s "$CONTRACT_SCOPE_FILE" ] \
|
|
2243
2636
|
&& gh pr diff {PR_NUMBER} -R {GH_REPO} --name-only > "$PR_FILES_FILE" 2>/dev/null; then
|
|
2244
2637
|
# 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
|
|
2638
|
+
# carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` plus the row's path, and is authored by a trusted author
|
|
2639
|
+
# (OWNER/MEMBER/COLLABORATOR or Bot; same predicate as trusted-comments.sh; missing fields fail closed).
|
|
2640
|
+
# Unverified rows are dropped.
|
|
2246
2641
|
_verified=$(mktemp "$_scratch/{PR_NUMBER}_contract-verified.XXXXXX")
|
|
2247
2642
|
while IFS="$(printf '\t')" read -r _disp _ipath _n; do
|
|
2248
2643
|
if [ "$_disp" = "deferred" ]; then
|
|
@@ -2250,7 +2645,7 @@ if [ -n "${MERGE_ISSUE:-}" ]; then
|
|
|
2250
2645
|
[ "$_n" = "${MERGE_ISSUE#\#}" ] && continue
|
|
2251
2646
|
gh api "repos/{GH_REPO}/issues/$_n" 2>/dev/null \
|
|
2252
2647
|
| 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 \
|
|
2648
|
+
'.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
2649
|
|| continue
|
|
2255
2650
|
grep -Fxq -- "$_n" "$CONTRACT_OPEN_FILE" || echo "$_n" >> "$CONTRACT_OPEN_FILE"
|
|
2256
2651
|
fi
|
|
@@ -2263,18 +2658,109 @@ if [ -n "${MERGE_ISSUE:-}" ]; then
|
|
|
2263
2658
|
rm -f "$CONTRACT_BODY_FILE"
|
|
2264
2659
|
fi
|
|
2265
2660
|
fi
|
|
2661
|
+
PRIOR_FILE=""; DIFF_MAP_FILE=""; NOTES_REFLAG=0
|
|
2662
|
+
_pscr="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_pscr"
|
|
2663
|
+
REPO="{GH_REPO}"; REVIEW_SHA="{REVIEW_SHA}"
|
|
2664
|
+
if [ -z "${TRUSTED_SCRIPT:-}" ]; then
|
|
2665
|
+
# 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.
|
|
2666
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2667
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2668
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2669
|
+
[ -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)"
|
|
2670
|
+
done
|
|
2671
|
+
TRUSTED_SCRIPT=""
|
|
2672
|
+
while IFS= read -r _c; do
|
|
2673
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2674
|
+
done <<< "$_tc"
|
|
2675
|
+
fi
|
|
2676
|
+
# Cross-head re-flag input (#3641). Fail soft: every unreadable source just leaves the TSV smaller, never blocks the review.
|
|
2677
|
+
# 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
|
|
2678
|
+
# (disposition: filed = linked issue OPEN, filed-closed = linked issue CLOSED, note = no linked issue). Every comment is read through
|
|
2679
|
+
# scripts/trusted-comments.sh, so an untrusted commenter cannot pre-post a finding to suppress a real one. With PF_SKIP set, reviewer
|
|
2680
|
+
# bodies for that head are skipped and only NOTE_DISPOSITION records carrying a different `reviewed_sha=` count (this head's own record,
|
|
2681
|
+
# from an earlier run of this same review, must never swallow its findings).
|
|
2682
|
+
pf_split_loc() { # PF_LOC="file[:N[-M]]" -> file<TAB>N<TAB>M (line columns empty when absent)
|
|
2683
|
+
local _loc="$PF_LOC" _file _r _a="" _b=""; _file="$_loc"
|
|
2684
|
+
case "$_loc" in *:[0-9]*) _r="${_loc##*:}"; case "$_r" in *[!0-9-]*|"") ;; *) _file="${_loc%:*}"; _a="${_r%%-*}"; _b="${_r#*-}"; [ "$_b" != "$_r" ] || _b="$_a" ;; esac ;; esac
|
|
2685
|
+
printf '%s\t%s\t%s\n' "$_file" "$_a" "$_b"
|
|
2686
|
+
}
|
|
2687
|
+
prior_findings_tsv() {
|
|
2688
|
+
local pr cj rows issues _id _c _s _loc _t _p _hit _f _a _b
|
|
2689
|
+
[ -n "${TRUSTED_SCRIPT:-}" ] || return 0
|
|
2690
|
+
rows=$(mktemp "${_pscr}/prior-rows.XXXXXX"); issues=$(mktemp "${_pscr}/prior-issues.XXXXXX")
|
|
2691
|
+
for pr in $PF_PRS; do
|
|
2692
|
+
case "$pr" in ''|*[!0-9]*) continue ;; esac
|
|
2693
|
+
cj=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" 2>/dev/null) || continue
|
|
2694
|
+
[ -n "$cj" ] || continue
|
|
2695
|
+
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 \
|
|
2696
|
+
| jq -r --arg skip "${PF_SKIP:-}" 'select($skip == "" or (test("(^|\\n)Reviewed-SHA: " + $skip + "\\r?(\\n|$)") | not)) | scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null \
|
|
2697
|
+
| while IFS='|' read -r _id _c _s _loc _t; do
|
|
2698
|
+
[ -n "$_t" ] || continue
|
|
2699
|
+
_p=$(PF_LOC="$_loc" pf_split_loc); printf '%s\t%s\n' "$_p" "$(printf '%s' "$_t" | tr '\t' ' ')"
|
|
2700
|
+
done >> "$rows"
|
|
2701
|
+
printf '%s' "$cj" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:NOTE_DISPOSITION' 2>/dev/null \
|
|
2702
|
+
| 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 \
|
|
2703
|
+
| sed -n 's/^- [^ ]* \([^ ]*\) \[[A-Za-z?]*\] \(.*\): [^:]*$/\1\t\2/p' \
|
|
2704
|
+
| while IFS="$(printf '\t')" read -r _loc _t; do
|
|
2705
|
+
[ "$_loc" != "?" ] || continue
|
|
2706
|
+
_p=$(PF_LOC="$_loc" pf_split_loc); printf '%s\t%s\n' "$_p" "$_t"
|
|
2707
|
+
done >> "$rows"
|
|
2708
|
+
gh issue list -R "$REPO" --label review-finding --state all --search "${pr} in:body" --limit 100 --json number,state,title,body 2>/dev/null \
|
|
2709
|
+
| jq -c '.[]' >> "$issues" 2>/dev/null || true
|
|
2710
|
+
done
|
|
2711
|
+
# Linkage: a filed issue whose body names the file and whose title shares >= 3 tokens with the finding (open preferred).
|
|
2712
|
+
sort -u "$rows" | while IFS="$(printf '\t')" read -r _f _a _b _t; do
|
|
2713
|
+
[ -n "$_f" ] || continue
|
|
2714
|
+
_hit=$(jq -rs --arg f "$_f" --arg t "$_t" 'def toks: ascii_downcase | [scan("[a-z0-9]{3,}")] | unique;
|
|
2715
|
+
($t | toks) as $a | [ .[] | select((.body // "") | contains($f))
|
|
2716
|
+
| select((((.title // "") | toks) as $b | [$a[] | select(. as $x | $b | index($x))] | length) >= 3)
|
|
2717
|
+
| {n: .number, s: (.state | ascii_downcase)} ]
|
|
2718
|
+
| (map(select(.s == "open")) + map(select(.s != "open"))) | .[0] // empty | "\(.s)\t\(.n)"' "$issues" 2>/dev/null) || _hit=""
|
|
2719
|
+
case "$_hit" in
|
|
2720
|
+
open*) printf '%s\t%s\t%s\t%s\tfiled\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
2721
|
+
closed*) printf '%s\t%s\t%s\t%s\tfiled-closed\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
2722
|
+
*) printf '%s\t%s\t%s\t%s\tnote\t\n' "$_f" "$_a" "$_b" "$_t" ;;
|
|
2723
|
+
esac
|
|
2724
|
+
done
|
|
2725
|
+
rm -f "$rows" "$issues"
|
|
2726
|
+
}
|
|
2727
|
+
PRIOR_ROWS=$(PF_SKIP="$REVIEW_SHA" PF_PRS="{PR_NUMBER}" prior_findings_tsv)
|
|
2728
|
+
if [ -n "$PRIOR_ROWS" ]; then
|
|
2729
|
+
PRIOR_FILE=$(mktemp "$_pscr/{PR_NUMBER}_prior-findings.XXXXXX"); printf '%s\n' "$PRIOR_ROWS" > "$PRIOR_FILE"
|
|
2730
|
+
# 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.
|
|
2731
|
+
PRIOR_SHA=$(gh api --paginate "repos/{GH_REPO}/issues/{PR_NUMBER}/comments" 2>/dev/null \
|
|
2732
|
+
| 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 \
|
|
2733
|
+
| jq -r 'scan("(^|\\n)Reviewed-SHA: ([0-9a-f]{40,64})") | .[1]' 2>/dev/null | grep -vx "$REVIEW_SHA" | tail -n 1)
|
|
2734
|
+
_top=$(git rev-parse --show-toplevel 2>/dev/null) || _top=""
|
|
2735
|
+
if [ -n "$PRIOR_SHA" ] && [ -n "$_top" ]; then
|
|
2736
|
+
DIFF_MAP_FILE=$(mktemp "$_pscr/{PR_NUMBER}_prior-diffmap.XXXXXX")
|
|
2737
|
+
{ git -C "$_top" diff -U0 --no-color --no-renames "$PRIOR_SHA" "$REVIEW_SHA" 2>/dev/null || true; } | while IFS= read -r _l; do
|
|
2738
|
+
case "$_l" in
|
|
2739
|
+
"+++ b/"*) _f="${_l#+++ b/}" ;;
|
|
2740
|
+
"@@ "*) _h="${_l#@@ -}"; _o="${_h%% *}"; _n="${_h#* +}"; _n="${_n%% *}"
|
|
2741
|
+
case "$_o" in *,*) _oc="${_o#*,}" ;; *) _oc=1 ;; esac; case "$_n" in *,*) _nc="${_n#*,}" ;; *) _nc=1 ;; esac
|
|
2742
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$_f" "${_o%%,*}" "$_oc" "${_n%%,*}" "$_nc" ;;
|
|
2743
|
+
esac
|
|
2744
|
+
done > "$DIFF_MAP_FILE" || : > "$DIFF_MAP_FILE"
|
|
2745
|
+
fi
|
|
2746
|
+
fi
|
|
2266
2747
|
# Per deduped finding (FINDING_TEXT_FILE = mktemp file holding title + body + affected paths;
|
|
2267
2748
|
# FINDING_FILE = the finding's file path, file:line accepted):
|
|
2268
2749
|
# DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
|
|
2269
2750
|
# --agent "$FINDING_AGENT" --lineage "$FINDING_LINEAGE" --text-file "$FINDING_TEXT_FILE" \
|
|
2270
2751
|
# ${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"}
|
|
2752
|
+
# ${CONTRACT_SCOPE_FILE:+--contract-scope "$CONTRACT_SCOPE_FILE" --contract-open "$CONTRACT_OPEN_FILE" --pr-files "$PR_FILES_FILE" --merge-issue "$MERGE_ISSUE"} \
|
|
2753
|
+
# ${PRIOR_FILE:+--prior-findings "$PRIOR_FILE" --title "$FINDING_TITLE" ${DIFF_MAP_FILE:+--diff-map "$DIFF_MAP_FILE"}} \
|
|
2754
|
+
# ${FINDING_LINE_START:+--line-start "$FINDING_LINE_START" --line-end "${FINDING_LINE_END:-$FINDING_LINE_START}"})
|
|
2272
2755
|
# case "$DISPOSITION" in ISSUE*) ;; NOTE*) ;; INPR_FIX*) ;; esac # first word is the class, the rest is the reason
|
|
2273
2756
|
# Contract-matched NOTE (`NOTE contract-deferred #N`): comment the finding (file:line + title) on the open
|
|
2274
2757
|
# deferred issue instead of filing a new one, and count it:
|
|
2275
2758
|
# 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
2759
|
# CONTRACT_DEMOTED=$((CONTRACT_DEMOTED + 1))
|
|
2277
2760
|
# `NOTE contract-accepted-risk` is counted in CONTRACT_DEMOTED too (no comment; the contract already records the reason).
|
|
2761
|
+
# 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
|
|
2762
|
+
# (`tracked on #N` or `already noted on an earlier head`), file nothing, and count it ONLY in notes_reflag (not notes_listed):
|
|
2763
|
+
# NOTES_REFLAG=$((NOTES_REFLAG + 1))
|
|
2278
2764
|
```
|
|
2279
2765
|
|
|
2280
2766
|
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 +2772,15 @@ Each NOTE gets exactly one disposition, in this preference order:
|
|
|
2286
2772
|
|
|
2287
2773
|
**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
2774
|
|
|
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.
|
|
2775
|
+
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
2776
|
|
|
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.
|
|
2777
|
+
**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
2778
|
|
|
2293
2779
|
```bash
|
|
2294
2780
|
CLASSIFIER_MODE=script # the §6B.5 guard blocks the review when the classifier is unresolvable, so this is always script
|
|
2781
|
+
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
2782
|
# 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} -->
|
|
2783
|
+
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
2784
|
$(cat "$NOTE_LIST_FILE" 2>/dev/null)" # allowlist:check-command-side-effects
|
|
2298
2785
|
```
|
|
2299
2786
|
|
|
@@ -2351,7 +2838,19 @@ MILESTONE_FLAG=""
|
|
|
2351
2838
|
# milestone via Closes/Fixes/Resolves #N -> branch-slug match against
|
|
2352
2839
|
# milestone/* branches) supersedes the old base/head-branch-only slug match
|
|
2353
2840
|
# that used to be hand-rolled here.
|
|
2354
|
-
|
|
2841
|
+
# 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.
|
|
2842
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2843
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2844
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2845
|
+
[ -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)"
|
|
2846
|
+
done
|
|
2847
|
+
DERIVE_MILESTONE_SCRIPT=""
|
|
2848
|
+
while IFS= read -r _c; do
|
|
2849
|
+
case "$_c" in /*) [ -z "$DERIVE_MILESTONE_SCRIPT" ] && [ -f "$_c/scripts/derive-finding-milestone.sh" ] && DERIVE_MILESTONE_SCRIPT="$_c/scripts/derive-finding-milestone.sh" ;; esac
|
|
2850
|
+
done <<< "$_tc"
|
|
2851
|
+
# Unresolved helper: empty milestone (non-blocking), never a cwd-relative fallback.
|
|
2852
|
+
MILESTONE_TITLE=""
|
|
2853
|
+
[ -n "$DERIVE_MILESTONE_SCRIPT" ] && MILESTONE_TITLE=$(bash "$DERIVE_MILESTONE_SCRIPT" "${PR_NUMBER}" -R "${REPO}")
|
|
2355
2854
|
# Quoted so multi-word milestone titles (e.g. "Watch & Fleet Observability") survive
|
|
2356
2855
|
# as a single value wherever MILESTONE_FLAG is later interpolated — matches the
|
|
2357
2856
|
# quoting convention already used for --title/--body-file/--label below.
|
|
@@ -2607,8 +3106,23 @@ FINDING_SEVERITY="LOW"
|
|
|
2607
3106
|
# (CONFIRMED/LIKELY/POSSIBLE). scripts/severity-to-priority.sh is the single
|
|
2608
3107
|
# source of truth for this mapping; commands/review-pr-staging.md calls the
|
|
2609
3108
|
# identical script so the two specs cannot independently drift. <!-- forge#2447 -->
|
|
2610
|
-
|
|
2611
|
-
|
|
3109
|
+
# 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.
|
|
3110
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
3111
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
3112
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
3113
|
+
[ -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)"
|
|
3114
|
+
done
|
|
3115
|
+
PRIORITY_SCRIPT=""
|
|
3116
|
+
while IFS= read -r _c; do
|
|
3117
|
+
case "$_c" in /*) [ -z "$PRIORITY_SCRIPT" ] && [ -f "$_c/scripts/severity-to-priority.sh" ] && PRIORITY_SCRIPT="$_c/scripts/severity-to-priority.sh" ;; esac
|
|
3118
|
+
done <<< "$_tc"
|
|
3119
|
+
# Unresolved helper: non-zero status so finding creation is skipped (never defaulted to a priority).
|
|
3120
|
+
if [ -n "$PRIORITY_SCRIPT" ]; then
|
|
3121
|
+
FINDING_PRIORITY=$(bash "$PRIORITY_SCRIPT" "$FINDING_SEVERITY")
|
|
3122
|
+
FINDING_PRIORITY_EXIT=$?
|
|
3123
|
+
else
|
|
3124
|
+
FINDING_PRIORITY=""; FINDING_PRIORITY_EXIT=127
|
|
3125
|
+
fi
|
|
2612
3126
|
|
|
2613
3127
|
# Exit code MUST be checked before use — severity-to-priority.sh exits 1 (empty stdout)
|
|
2614
3128
|
# on a missing/unrecognized severity, mirroring the DEDUP_EXIT idiom above. Proceeding
|
|
@@ -3454,7 +3968,7 @@ A degraded run that skipped Task-based agent dispatch must be visible from this
|
|
|
3454
3968
|
|
|
3455
3969
|
```bash
|
|
3456
3970
|
# 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}"
|
|
3971
|
+
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"; REVIEW_SHA="{REVIEW_SHA}"; REVIEW_MODE="{REVIEW_MODE}"
|
|
3458
3972
|
# Same trust rule as Phase 3C/4: only trusted comments whose body starts with the marker count.
|
|
3459
3973
|
# 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
3974
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
@@ -3466,9 +3980,54 @@ TRUSTED_SCRIPT=""
|
|
|
3466
3980
|
while IFS= read -r _c; do
|
|
3467
3981
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
3468
3982
|
done <<< "$_tc"
|
|
3983
|
+
# Same trusted tiers as the TRUSTED_SCRIPT resolver above (_tc is that block's list); never the working directory or the PR checkout.
|
|
3984
|
+
REVIEW_DELTA_SCRIPT=""
|
|
3985
|
+
while IFS= read -r _c; do
|
|
3986
|
+
case "$_c" in /*) [ -z "$REVIEW_DELTA_SCRIPT" ] && [ -f "$_c/scripts/review-delta.sh" ] && REVIEW_DELTA_SCRIPT="$_c/scripts/review-delta.sh" ;; esac
|
|
3987
|
+
done <<< "$_tc"
|
|
3988
|
+
BASE_BRANCH=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
3989
|
+
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
3990
|
+
# BEGIN review-pr-delta-mode
|
|
3991
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
3992
|
+
# Prints "<MODE> <ARG> <FULL_ROUNDS>" from scripts/review-delta.sh: "FULL - n", "DELTA <reviewed-sha> n" or "BASE_SYNC_ONLY <reviewed-sha> n".
|
|
3993
|
+
# 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.
|
|
3994
|
+
review_delta_mode() {
|
|
3995
|
+
local out l1 l2 kind arg from to rounds=0 top
|
|
3996
|
+
is_sha "$REVIEW_SHA" || { echo "FULL - 0"; return 0; }
|
|
3997
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$REVIEW_DELTA_SCRIPT" ] || [ -z "$BASE_BRANCH" ]; then echo "FULL - 0"; return 0; fi
|
|
3998
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=""
|
|
3999
|
+
[ -n "$top" ] || { echo "FULL - 0"; return 0; }
|
|
4000
|
+
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=""
|
|
4001
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
4002
|
+
case "$l2" in FULL_ROUNDS=*) rounds="${l2#FULL_ROUNDS=}" ;; esac
|
|
4003
|
+
case "$rounds" in ''|*[!0-9]*) echo "FULL - 0"; return 0 ;; esac
|
|
4004
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
4005
|
+
if [ "$kind" = "BASE_SYNC_ONLY" ] && is_sha "$arg" && [ "$arg" != "$REVIEW_SHA" ]; then echo "BASE_SYNC_ONLY $arg $rounds"; return 0; fi
|
|
4006
|
+
if [ "$kind" = "DELTA" ]; then
|
|
4007
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
4008
|
+
if is_sha "$from" && [ "$to" = "$REVIEW_SHA" ] && [ "$from" != "$to" ]; then echo "DELTA $from $rounds"; return 0; fi
|
|
4009
|
+
fi
|
|
4010
|
+
echo "FULL - $rounds"
|
|
4011
|
+
}
|
|
4012
|
+
# Prints the Carried-From-SHA only when a TRUSTED FORGE:REVIEW-CARRIED record for this head names it AND the resolver
|
|
4013
|
+
# still answers BASE_SYNC_ONLY from that same reviewed SHA right now. Needs COMMENTS_JSON as well. Marker text alone never counts.
|
|
4014
|
+
carry_verified_from() {
|
|
4015
|
+
local kind from rounds n
|
|
4016
|
+
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then return 0; fi
|
|
4017
|
+
read -r kind from rounds <<< "$(review_delta_mode)"
|
|
4018
|
+
[ "$kind" = "BASE_SYNC_ONLY" ] || return 0
|
|
4019
|
+
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 "")
|
|
4020
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
4021
|
+
[ "$n" -ge 1 ] && printf '%s' "$from"
|
|
4022
|
+
return 0
|
|
4023
|
+
}
|
|
4024
|
+
# END review-pr-delta-mode
|
|
4025
|
+
# BASE_SYNC_ONLY: zero reviewers is the expected panel only when the carry record is trusted and the resolver re-verifies it.
|
|
4026
|
+
CARRIED_FROM=""
|
|
4027
|
+
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then CARRIED_FROM=$(carry_verified_from); fi
|
|
3469
4028
|
ACTUAL_AGENT_DOMAINS=""
|
|
3470
4029
|
if [ -n "$TRUSTED_SCRIPT" ]; then
|
|
3471
|
-
ACTUAL_AGENT_DOMAINS=$(
|
|
4030
|
+
ACTUAL_AGENT_DOMAINS=$(printf '%s' "$COMMENTS_JSON" \
|
|
3472
4031
|
| bash "$TRUSTED_SCRIPT" bodies "^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->" 2>/dev/null \
|
|
3473
4032
|
| jq -rs '[.[] | scan("^<!-- FORGE:REVIEW-AGENT:([a-z-]+) -->") | .[0]] | unique | join(", ")' 2>/dev/null || echo "") # unresolvable => empty => count 0 (degraded, fail closed)
|
|
3474
4033
|
fi
|
|
@@ -3478,9 +4037,12 @@ fi
|
|
|
3478
4037
|
# second "0" line, corrupting the value to "0\n0" in exactly the zero-agent case this
|
|
3479
4038
|
# check exists to detect. <!-- Added: forge#1849 -->
|
|
3480
4039
|
ACTUAL_AGENT_COUNT=$(echo "$ACTUAL_AGENT_DOMAINS" | tr ',' '\n' | grep -c '.' 2>/dev/null)
|
|
4040
|
+
# A verified carried head dispatched no reviewers by design: report it as carried instead of a degraded zero-agent panel.
|
|
4041
|
+
if [ -n "$CARRIED_FROM" ]; then ACTUAL_AGENT_DOMAINS="carried from ${CARRIED_FROM}"; ACTUAL_AGENT_COUNT=0; fi
|
|
4042
|
+
echo "CARRIED_FROM: ${CARRIED_FROM}"
|
|
3481
4043
|
```
|
|
3482
4044
|
|
|
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.
|
|
4045
|
+
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
4046
|
|
|
3485
4047
|
```bash
|
|
3486
4048
|
# Each Bash call is a fresh shell: re-declare the state this block needs with the orchestrator's values.
|