forgedock 1.11.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/phases-args.test.mjs +113 -0
- package/bin/engine/phases.mjs +433 -109
- package/bin/engine/runlog.mjs +21 -1
- package/bin/engine-cli.mjs +14 -2
- package/bin/engine.mjs +150 -19
- package/bin/forgedock.mjs +2 -2
- package/bin/hooks/interactive-engine.mjs +3 -0
- package/bin/labels.json +5 -0
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +111 -12
- package/bin/watch-utils.mjs +1 -0
- package/commands/cleanup.md +1 -1
- package/commands/orchestrate/phase-4-execution.md +58 -24
- package/commands/orchestrate/phase-6-report.md +29 -0
- package/commands/quality-gate.md +96 -12
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents.md +11 -11
- package/commands/review-pr.md +227 -33
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +58 -7
- package/commands/work-on/build.md +11 -4
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +109 -19
- package/commands/work-on/review.md +79 -25
- package/commands/work-on.md +23 -15
- package/docs/WORK-ON-RUNTIME.md +2 -2
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +27 -1
- package/scripts/check-branch-ancestry.sh +82 -0
- package/scripts/check-branch-ancestry.test.sh +78 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/forge-root.test.sh +36 -15
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/spec-no-worktree-script.test.sh +46 -0
- package/scripts/transition-label.sh +3 -1
|
@@ -176,7 +176,7 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
176
176
|
6. **Summary**: Concise one-line description (no pipe `|` characters in summary)
|
|
177
177
|
7. **Empty block**: If no findings at all, include just the START/END markers
|
|
178
178
|
8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
|
|
179
|
-
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the
|
|
179
|
+
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the marker domain the orchestrator passes you (the canonical value from the roster-to-marker table in `review-pr.md`, e.g. `infra` and `scraper`; never a lowercased display name such as `infrastructure` or `scraping`).
|
|
180
180
|
10. **Reviewed SHA**: Include exactly one line `Reviewed-SHA: [REVIEW_SHA]` (the full 40-hex head you were pinned to) in the persisted body, outside the marker. `/review-pr` scopes its completion wait and idempotent re-entry to this line, so a comment without the current head does not count as a completed review. Do NOT put the SHA inside the `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker.
|
|
181
181
|
|
|
182
182
|
### Domain Prefixes
|
|
@@ -46,17 +46,17 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
|
|
|
46
46
|
|
|
47
47
|
2. **Then read the persona file for each selected agent**:
|
|
48
48
|
|
|
49
|
-
| Agent | Trigger | File |
|
|
50
|
-
|
|
51
|
-
| General Security & Quality Scan | ALWAYS RUNS | `review-pr-agents/security.md` |
|
|
52
|
-
| Auth Conventions Auditor | AUTH domain | `review-pr-agents/auth.md` |
|
|
53
|
-
| Billing Integrity Auditor | BILLING domain | `review-pr-agents/billing.md` |
|
|
54
|
-
| Concurrency & Race Condition Auditor | CONCURRENCY or BILLING domain | `review-pr-agents/concurrency.md` |
|
|
55
|
-
| Domain Logic Auditor | SCRAPING domain (requires `review.domains.scraping` in forge.yaml) | `review-pr-agents/scraper.md` |
|
|
56
|
-
| Frontend Quality Auditor | WEB service touched | `review-pr-agents/frontend.md` |
|
|
57
|
-
| API Design & Consistency Auditor | New/modified routers or SDK/OpenAPI files | `review-pr-agents/api.md` |
|
|
58
|
-
| Database & Migration Auditor | DATABASE domain | `review-pr-agents/database.md` |
|
|
59
|
-
| Infrastructure & Deploy Safety Auditor | INFRA service touched | `review-pr-agents/infra.md` |
|
|
49
|
+
| Agent | Trigger | Marker domain | File |
|
|
50
|
+
|-------|---------|---------------|------|
|
|
51
|
+
| General Security & Quality Scan | ALWAYS RUNS | `security` | `review-pr-agents/security.md` |
|
|
52
|
+
| Auth Conventions Auditor | AUTH domain | `auth` | `review-pr-agents/auth.md` |
|
|
53
|
+
| Billing Integrity Auditor | BILLING domain | `billing` | `review-pr-agents/billing.md` |
|
|
54
|
+
| Concurrency & Race Condition Auditor | CONCURRENCY or BILLING domain | `concurrency` | `review-pr-agents/concurrency.md` |
|
|
55
|
+
| Domain Logic Auditor | SCRAPING domain (requires `review.domains.scraping` in forge.yaml) | `scraper` | `review-pr-agents/scraper.md` |
|
|
56
|
+
| Frontend Quality Auditor | WEB service touched | `frontend` | `review-pr-agents/frontend.md` |
|
|
57
|
+
| API Design & Consistency Auditor | New/modified routers or SDK/OpenAPI files | `api` | `review-pr-agents/api.md` |
|
|
58
|
+
| Database & Migration Auditor | DATABASE domain | `database` | `review-pr-agents/database.md` |
|
|
59
|
+
| Infrastructure & Deploy Safety Auditor | INFRA service touched | `infra` | `review-pr-agents/infra.md` |
|
|
60
60
|
|
|
61
61
|
## Domain Prefixes (for Structured Findings)
|
|
62
62
|
|
package/commands/review-pr.md
CHANGED
|
@@ -538,6 +538,27 @@ fi
|
|
|
538
538
|
```
|
|
539
539
|
|
|
540
540
|
```bash
|
|
541
|
+
# Install-root-only classifier resolution (no cwd-relative `scripts/` fallback: the PR under review is author-controlled).
|
|
542
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
543
|
+
FORGE_ROOT=""
|
|
544
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
545
|
+
_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
|
|
546
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
547
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
548
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
549
|
+
# 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.
|
|
550
|
+
_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")")"
|
|
551
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
552
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
553
|
+
# 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)
|
|
554
|
+
_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)"
|
|
555
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
556
|
+
# '${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.
|
|
557
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
558
|
+
while IFS= read -r _c; do
|
|
559
|
+
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
|
|
560
|
+
done <<< "$_k"
|
|
561
|
+
fi
|
|
541
562
|
# Check each configured language for a test command
|
|
542
563
|
for lang in python typescript go rust; do
|
|
543
564
|
TEST_CMD=$(yq ".verification.commands.${lang}.test // \"\"" forge.yaml 2>/dev/null || echo '')
|
|
@@ -550,9 +571,8 @@ for lang in python typescript go rust; do
|
|
|
550
571
|
# If ALL failing test names appear in $QUARANTINED_TESTS, suppress the block.
|
|
551
572
|
# Without per-test granularity from the test runner, fall back to treating
|
|
552
573
|
# any failure as blocking unless the classifier script is available.
|
|
553
|
-
CLASSIFIER="${
|
|
554
|
-
[ -
|
|
555
|
-
if [ -f "$CLASSIFIER" ]; then
|
|
574
|
+
CLASSIFIER="${FORGE_ROOT:+$FORGE_ROOT/scripts/flaky-quarantine.sh}"
|
|
575
|
+
if [ -n "$CLASSIFIER" ] && [ -f "$CLASSIFIER" ]; then
|
|
556
576
|
CL_RESULT=$(bash "$CLASSIFIER" \
|
|
557
577
|
--test "$TEST_CMD" \
|
|
558
578
|
--base "$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null | sed 's|origin/||' || echo main)" \
|
|
@@ -1152,11 +1172,47 @@ echo "=== BASELINE ROSTER (top domains): $SELECTED_AGENTS ==="
|
|
|
1152
1172
|
| Cross-critical domains | `SCORE_AUTH >= 2` AND `SCORE_DATABASE >= 3` | Auth + Database |
|
|
1153
1173
|
| Cross-critical domains | `SCORE_BILLING >= 2` AND `SCORE_CONCURRENCY >= 2` | Billing + Concurrency + Database |
|
|
1154
1174
|
|
|
1175
|
+
**Roster label to marker domain (single source of truth)**: roster labels come in three spellings (score keys such as `INFRA`/`SCRAPING`, display names such as `Infrastructure`/`Scraping`, persona files such as `infra`/`scraper`). The marker domain in `<!-- FORGE:REVIEW-AGENT:{domain} -->` is always the canonical value below, never the lowercased roster label. Phase 4 stays fail closed on that canonical marker: no alias is accepted. <!-- Added: forge#3485 -->
|
|
1176
|
+
|
|
1177
|
+
| Roster label (any case) | Marker domain | Persona file |
|
|
1178
|
+
|-------------------------|---------------|--------------|
|
|
1179
|
+
| `Security` | `security` | `security.md` |
|
|
1180
|
+
| `Auth` | `auth` | `auth.md` |
|
|
1181
|
+
| `Billing` | `billing` | `billing.md` |
|
|
1182
|
+
| `Concurrency` | `concurrency` | `concurrency.md` |
|
|
1183
|
+
| `Database` | `database` | `database.md` |
|
|
1184
|
+
| `INFRA`, `Infrastructure` | `infra` | `infra.md` |
|
|
1185
|
+
| `SCRAPING`, `Scraping`, `Scraper` | `scraper` | `scraper.md` |
|
|
1186
|
+
| `FRONTEND`, `Frontend`, `Web` | `frontend` | `frontend.md` |
|
|
1187
|
+
| `API` | `api` | `api.md` |
|
|
1188
|
+
|
|
1189
|
+
A label outside the table falls back to its lowercase form. The `agent_domain()` helper is defined inline in every Bash block that needs it (each Bash call is a fresh shell); all copies must stay identical, which `scripts/review-pr-domain-map.test.sh` enforces.
|
|
1190
|
+
|
|
1155
1191
|
```bash
|
|
1156
1192
|
# Apply escalation triggers
|
|
1193
|
+
# BEGIN review-pr-agent-domain
|
|
1194
|
+
agent_domain() {
|
|
1195
|
+
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
1196
|
+
security) echo security ;;
|
|
1197
|
+
auth) echo auth ;;
|
|
1198
|
+
billing) echo billing ;;
|
|
1199
|
+
concurrency) echo concurrency ;;
|
|
1200
|
+
database) echo database ;;
|
|
1201
|
+
infra|infrastructure) echo infra ;;
|
|
1202
|
+
scraper|scraping) echo scraper ;;
|
|
1203
|
+
frontend|web) echo frontend ;;
|
|
1204
|
+
api) echo api ;;
|
|
1205
|
+
*) printf '%s' "${1}" | tr '[:upper:]' '[:lower:]' ;;
|
|
1206
|
+
esac
|
|
1207
|
+
}
|
|
1208
|
+
# END review-pr-agent-domain
|
|
1157
1209
|
add_agent() {
|
|
1158
|
-
local AGENT="
|
|
1159
|
-
|
|
1210
|
+
local AGENT CUR HAVE=""
|
|
1211
|
+
AGENT=$(agent_domain "${1}")
|
|
1212
|
+
for CUR in $SELECTED_AGENTS; do
|
|
1213
|
+
[ "$(agent_domain "$CUR")" = "$AGENT" ] && HAVE=1
|
|
1214
|
+
done
|
|
1215
|
+
[ -n "$HAVE" ] || SELECTED_AGENTS="$SELECTED_AGENTS $AGENT"
|
|
1160
1216
|
}
|
|
1161
1217
|
|
|
1162
1218
|
[ "$SCORE_AUTH" -ge 3 ] && add_agent "Auth"
|
|
@@ -1392,6 +1448,34 @@ if [ "$THOROUGH" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; then
|
|
|
1392
1448
|
fi
|
|
1393
1449
|
```
|
|
1394
1450
|
|
|
1451
|
+
**Roster canonicalization (final step of 3B, mandatory)**: rewrite `SELECTED_AGENTS` to canonical marker domains with one entry per persona, so `INFRA` and `Infrastructure` (or `SCRAPING` and `Scraping`) dispatch once and `SELECTED_AGENT_COUNT` counts each persona once. Pass the rewritten value to Phase 3C and Phase 4 as `{SELECTED_AGENTS}`. <!-- Added: forge#3485 -->
|
|
1452
|
+
|
|
1453
|
+
```bash
|
|
1454
|
+
# BEGIN review-pr-agent-domain
|
|
1455
|
+
agent_domain() {
|
|
1456
|
+
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
1457
|
+
security) echo security ;;
|
|
1458
|
+
auth) echo auth ;;
|
|
1459
|
+
billing) echo billing ;;
|
|
1460
|
+
concurrency) echo concurrency ;;
|
|
1461
|
+
database) echo database ;;
|
|
1462
|
+
infra|infrastructure) echo infra ;;
|
|
1463
|
+
scraper|scraping) echo scraper ;;
|
|
1464
|
+
frontend|web) echo frontend ;;
|
|
1465
|
+
api) echo api ;;
|
|
1466
|
+
*) printf '%s' "${1}" | tr '[:upper:]' '[:lower:]' ;;
|
|
1467
|
+
esac
|
|
1468
|
+
}
|
|
1469
|
+
# END review-pr-agent-domain
|
|
1470
|
+
CANON_ROSTER=""
|
|
1471
|
+
for AGENT in $SELECTED_AGENTS; do
|
|
1472
|
+
D=$(agent_domain "$AGENT")
|
|
1473
|
+
case " $CANON_ROSTER " in *" $D "*) ;; *) CANON_ROSTER="$CANON_ROSTER $D" ;; esac
|
|
1474
|
+
done
|
|
1475
|
+
SELECTED_AGENTS="${CANON_ROSTER# }"
|
|
1476
|
+
echo "=== CANONICAL ROSTER: $SELECTED_AGENTS ==="
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1395
1479
|
**Why cross-critical domain pairs always escalate**: A 2-file PR touching both `services/api/app/core/auth.py` and `services/api/app/routers/billing.py` creates interaction bugs that single-domain reviewers cannot catch. Never rely on a single agent for multi-domain risk.
|
|
1396
1480
|
|
|
1397
1481
|
**General Security agent ALWAYS runs.** If BILLING is selected, Concurrency is always added. If SHARED module is touched, add agents for all importing services.
|
|
@@ -1670,14 +1754,35 @@ if [ "$REVIEW_SHA_OK" != "true" ]; then
|
|
|
1670
1754
|
fi
|
|
1671
1755
|
WAIT_DIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/forge-review-wait-$(id -u)" # absolute and identical in every Bash call (cwd is not)
|
|
1672
1756
|
rm -f "${WAIT_DIR}/${PR_NUMBER}_${REVIEW_SHA}_review-wait-deadline" # a (re)dispatch always starts a fresh wait window
|
|
1757
|
+
# 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
1758
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1759
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1760
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1761
|
+
[ -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)"
|
|
1762
|
+
done
|
|
1674
1763
|
TRUSTED_SCRIPT=""
|
|
1675
|
-
|
|
1764
|
+
while IFS= read -r _c; do
|
|
1676
1765
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1677
|
-
done
|
|
1766
|
+
done <<< "$_tc"
|
|
1678
1767
|
PENDING_AGENTS=""
|
|
1768
|
+
# BEGIN review-pr-agent-domain
|
|
1769
|
+
agent_domain() {
|
|
1770
|
+
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
1771
|
+
security) echo security ;;
|
|
1772
|
+
auth) echo auth ;;
|
|
1773
|
+
billing) echo billing ;;
|
|
1774
|
+
concurrency) echo concurrency ;;
|
|
1775
|
+
database) echo database ;;
|
|
1776
|
+
infra|infrastructure) echo infra ;;
|
|
1777
|
+
scraper|scraping) echo scraper ;;
|
|
1778
|
+
frontend|web) echo frontend ;;
|
|
1779
|
+
api) echo api ;;
|
|
1780
|
+
*) printf '%s' "${1}" | tr '[:upper:]' '[:lower:]' ;;
|
|
1781
|
+
esac
|
|
1782
|
+
}
|
|
1783
|
+
# END review-pr-agent-domain
|
|
1679
1784
|
for AGENT in $SELECTED_AGENTS; do
|
|
1680
|
-
AGENT_DOMAIN=$(
|
|
1785
|
+
AGENT_DOMAIN=$(agent_domain "$AGENT")
|
|
1681
1786
|
HAVE=""
|
|
1682
1787
|
if [ -n "$TRUSTED_SCRIPT" ]; then
|
|
1683
1788
|
HAVE=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
|
|
@@ -1689,7 +1794,7 @@ done
|
|
|
1689
1794
|
echo "PENDING_AGENTS:${PENDING_AGENTS:- (none)}"
|
|
1690
1795
|
```
|
|
1691
1796
|
|
|
1692
|
-
**CRITICAL**: Launch ALL agents in `PENDING_AGENTS` in a SINGLE message using multiple `{DISPATCH_TOOL}` calls. If `PENDING_AGENTS` is empty, every selected reviewer already posted for this head: launch nothing and go straight to Phase 4. Each agent must persist its finalized body before posting it with `gh pr comment --body-file`, start the body with `<!-- FORGE:REVIEW-AGENT:{
|
|
1797
|
+
**CRITICAL**: Launch ALL agents in `PENDING_AGENTS` in a SINGLE message using multiple `{DISPATCH_TOOL}` calls. If `PENDING_AGENTS` is empty, every selected reviewer already posted for this head: launch nothing and go straight to Phase 4. Each agent must persist its finalized body before posting it with `gh pr comment --body-file`, start the body with `<!-- FORGE:REVIEW-AGENT:{marker-domain} -->` (`{marker-domain}` is the canonical value from the roster-to-marker table, i.e. the `AGENT_DOMAIN` computed above for that agent; pass it to each persona explicitly) followed by a `Reviewed-SHA: ${REVIEW_SHA}` line (the full head SHA, outside the marker), and return its verdict and findings to the orchestrator independently of GitHub delivery.
|
|
1693
1798
|
|
|
1694
1799
|
**Dispatch failure and partial-panel guard (MANDATORY):** Count the selected roster before dispatch. If any launch fails, including from pool exhaustion, do not continue with the agents that did launch as a sufficient panel. Immediately run the **Panel hard-stop block** below with `ACTUAL_AGENT_COUNT=0` and exit without a verdict. After reviewers complete (joined `Task`/`task` return) or, under async `Agent`, after the Phase 4 bounded wait ends, independently compare their posted trusted current-SHA `FORGE:REVIEW-AGENT` markers with the selected count; a smaller count is the same hard stop. This catches a reviewer that accepted dispatch but failed before posting. Under async `Agent` the comparison runs only after the wait, so a reviewer that is merely late is not a false degraded stop.
|
|
1695
1800
|
|
|
@@ -1788,16 +1893,37 @@ if [ "$NOW_SHA" != "$REVIEW_SHA" ]; then
|
|
|
1788
1893
|
echo "PANEL_STATUS: STOP"; exit 0
|
|
1789
1894
|
fi
|
|
1790
1895
|
|
|
1896
|
+
# 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.
|
|
1791
1897
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1898
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1899
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1900
|
+
[ -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)"
|
|
1901
|
+
done
|
|
1792
1902
|
TRUSTED_SCRIPT=""
|
|
1793
|
-
|
|
1903
|
+
while IFS= read -r _c; do
|
|
1794
1904
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1795
|
-
done
|
|
1905
|
+
done <<< "$_tc"
|
|
1796
1906
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1797
1907
|
MISSING_AGENT_COMMENTS=""
|
|
1798
1908
|
ACTUAL_AGENT_COUNT=0
|
|
1909
|
+
# BEGIN review-pr-agent-domain
|
|
1910
|
+
agent_domain() {
|
|
1911
|
+
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
1912
|
+
security) echo security ;;
|
|
1913
|
+
auth) echo auth ;;
|
|
1914
|
+
billing) echo billing ;;
|
|
1915
|
+
concurrency) echo concurrency ;;
|
|
1916
|
+
database) echo database ;;
|
|
1917
|
+
infra|infrastructure) echo infra ;;
|
|
1918
|
+
scraper|scraping) echo scraper ;;
|
|
1919
|
+
frontend|web) echo frontend ;;
|
|
1920
|
+
api) echo api ;;
|
|
1921
|
+
*) printf '%s' "${1}" | tr '[:upper:]' '[:lower:]' ;;
|
|
1922
|
+
esac
|
|
1923
|
+
}
|
|
1924
|
+
# END review-pr-agent-domain
|
|
1799
1925
|
for AGENT in $SELECTED_AGENTS; do
|
|
1800
|
-
AGENT_DOMAIN=$(
|
|
1926
|
+
AGENT_DOMAIN=$(agent_domain "$AGENT")
|
|
1801
1927
|
N=""
|
|
1802
1928
|
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$COMMENTS_JSON" ]; then
|
|
1803
1929
|
N=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "^<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${REVIEW_SHA}(\\r?\\n|$)" 2>/dev/null || echo "")
|
|
@@ -1840,11 +1966,16 @@ case "$REVIEW_SHA" in *[!0-9a-f]*|"") REVIEW_SHA_OK=false ;; *) [ "${#REVIEW_SHA
|
|
|
1840
1966
|
# CURRENT head. The head SHA is public, so the SHA line alone proves nothing: a bare contains() selection would let any
|
|
1841
1967
|
# commenter forge FINDING markers that get filed as issues or inflate the agent count. Earlier-head comments are excluded
|
|
1842
1968
|
# so a re-entry or head move does not re-file already-fixed findings. Uses jq scan() (POSIX-portable, no PCRE grep).
|
|
1969
|
+
# 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.
|
|
1843
1970
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1971
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1972
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1973
|
+
[ -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)"
|
|
1974
|
+
done
|
|
1844
1975
|
TRUSTED_SCRIPT=""
|
|
1845
|
-
|
|
1976
|
+
while IFS= read -r _c; do
|
|
1846
1977
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1847
|
-
done
|
|
1978
|
+
done <<< "$_tc"
|
|
1848
1979
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1849
1980
|
AGENT_RE="^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${REVIEW_SHA}(\\r?\\n|$)"
|
|
1850
1981
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
@@ -1884,11 +2015,16 @@ case "$REVIEW_SHA" in *[!0-9a-f]*|"") REVIEW_SHA_OK=false ;; *) [ "${#REVIEW_SHA
|
|
|
1884
2015
|
# Only TRUSTED bodies for the CURRENT head count (scripts/trusted-comments.sh + anchored marker + Reviewed-SHA line):
|
|
1885
2016
|
# findings from earlier heads are already fixed or re-reviewed, and an untrusted commenter must never be able to inject
|
|
1886
2017
|
# findings or post a forged synthesis block that makes the review read clean (the head SHA is public).
|
|
2018
|
+
# 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.
|
|
1887
2019
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2020
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2021
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2022
|
+
[ -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)"
|
|
2023
|
+
done
|
|
1888
2024
|
TRUSTED_SCRIPT=""
|
|
1889
|
-
|
|
2025
|
+
while IFS= read -r _c; do
|
|
1890
2026
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1891
|
-
done
|
|
2027
|
+
done <<< "$_tc"
|
|
1892
2028
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1893
2029
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
1894
2030
|
echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/trusted-comments.sh unresolvable or PR comments unreadable; refusing to read clean (fail closed)"; exit 0
|
|
@@ -1958,11 +2094,16 @@ fi
|
|
|
1958
2094
|
# fix round on this PR. Any trusted FORGE:INPR_FIX* marker already on the PR means the round is used.
|
|
1959
2095
|
INPR_DIFF_FILE=""
|
|
1960
2096
|
if [ "${AUTO_MERGE:-false}" = "true" ] && [ -n "${MERGE_ISSUE:-}" ]; then
|
|
2097
|
+
# 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.
|
|
1961
2098
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2099
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2100
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2101
|
+
[ -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)"
|
|
2102
|
+
done
|
|
1962
2103
|
TRUSTED_SCRIPT=""
|
|
1963
|
-
|
|
2104
|
+
while IFS= read -r _c; do
|
|
1964
2105
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1965
|
-
done
|
|
2106
|
+
done <<< "$_tc"
|
|
1966
2107
|
if [ -n "$TRUSTED_SCRIPT" ]; then INPR_MARKERS=$(gh api --paginate "repos/{GH_REPO}/issues/{PR_NUMBER}/comments" 2>/dev/null \
|
|
1967
2108
|
| bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:INPR_FIX' 2>/dev/null || echo ""); else INPR_MARKERS=""; fi
|
|
1968
2109
|
if [ "$INPR_MARKERS" = "0" ]; then
|
|
@@ -2449,21 +2590,37 @@ Verdict determined by standard blocking criteria.
|
|
|
2449
2590
|
1. Phase 2 automated checks failed (build error, type error, test failure)
|
|
2450
2591
|
2. Agent finding is CONFIRMED at HIGH or CRITICAL severity
|
|
2451
2592
|
3. **[Milestone PRs only]** Phase 7A Purpose Regression Gate flagged `HAS_PURPOSE_REGRESSION=true` for this finding — regardless of whether it causes a runtime error
|
|
2452
|
-
4. `MERGE_HEALTH == "CONFLICTING"
|
|
2453
|
-
- Verdict: CHANGES REQUESTED. Message: "
|
|
2593
|
+
4. **Base conflict** — `BASE_CONFLICT=true` (computed in the block below): `MERGE_HEALTH == "CONFLICTING"`, OR `MERGE_HEALTH_STATE == "DIRTY"`, OR `MERGE_HEALTH_STATE == "BEHIND"` while the base branch requires up-to-date branches — PR cannot be merged cleanly into its base branch <!-- Added: forge#194, forge#3496 -->
|
|
2594
|
+
- Verdict: CHANGES REQUESTED. Message: "Base conflict with `{base}`. Merge `origin/{base}` into `{head}` (merge only; no rebase, no force-push), resolve the conflicting files by reading both sides, then re-run /review-pr."
|
|
2595
|
+
- A merely-`BEHIND` branch on a base that does NOT require up-to-date branches is not a conflict and needs no sync: GitHub merges it fine.
|
|
2596
|
+
- `MERGE_HEALTH_STATE == "BLOCKED"` alone (branch protection not satisfied: missing required review or check) is NOT a base conflict: syncing the base cannot clear it. Emit a WARNING line in the verdict body and leave `HAS_MERGE_CONFLICT=false`; the Phase 8 pre-merge guard handles it as a protection abort.
|
|
2454
2597
|
- If `MERGE_HEALTH == "UNKNOWN"` after retries: emit a WARNING in the verdict body (do NOT treat as a block — GitHub may still be computing it).
|
|
2455
2598
|
5. A CONFIRMED coverage reduction (deleted test, removed test case, or disabled/removed workflow test step) in a PR that responds to a red check, or that has no justification tying it to removal of the tested code <!-- Added: forge#3257 -->
|
|
2456
2599
|
- Verdict: CHANGES REQUESTED. Message: "Coverage reduction: a deleted test or removed workflow test step is not a fix for a red check. Restore it and fix the code under test, or escalate naming the failing assertions." Detection follows quality-gate 2U (`COVERAGE-1`); never dedup this finding against the PR's own issue.
|
|
2457
2600
|
|
|
2458
2601
|
```bash
|
|
2459
2602
|
# Determine if mergeability is a blocker (MERGE_HEALTH/MERGE_HEALTH_STATE set in Phase 1A; BASE/HEAD set in Phase 0 Mode 3)
|
|
2603
|
+
# BASE_CONFLICT is the single source of truth for "a base sync would fix this"; the Phase 8 guards read it. <!-- Added: forge#3496 -->
|
|
2460
2604
|
HAS_MERGE_CONFLICT=false
|
|
2605
|
+
BASE_CONFLICT=false
|
|
2461
2606
|
MERGE_CONFLICT_MSG=""
|
|
2462
|
-
|
|
2607
|
+
MERGE_PROTECTION_WARNING=""
|
|
2608
|
+
BASE_REQUIRES_UPTODATE=false
|
|
2609
|
+
if [ "$MERGE_HEALTH_STATE" = "BEHIND" ]; then
|
|
2610
|
+
# Unreadable protection (404 / 403 / no admin access) is treated as "not required": no sync, and a later failed merge lands in the existing needs-human path.
|
|
2611
|
+
[ "$(gh api "repos/${REPO}/branches/${BASE}/protection" --jq '.required_status_checks.strict // false' 2>/dev/null)" = "true" ] && BASE_REQUIRES_UPTODATE=true
|
|
2612
|
+
fi
|
|
2613
|
+
if [ "$MERGE_HEALTH" = "CONFLICTING" ] || [ "$MERGE_HEALTH_STATE" = "DIRTY" ] || { [ "$MERGE_HEALTH_STATE" = "BEHIND" ] && [ "$BASE_REQUIRES_UPTODATE" = "true" ]; }; then
|
|
2463
2614
|
HAS_MERGE_CONFLICT=true
|
|
2464
|
-
|
|
2615
|
+
BASE_CONFLICT=true
|
|
2616
|
+
MERGE_CONFLICT_MSG="Base conflict with \`${BASE}\`. Merge \`origin/${BASE}\` into \`${HEAD}\` (merge only, no rebase, no force-push), resolve the conflicting files by reading both sides, then re-run /review-pr."
|
|
2617
|
+
elif [ "$MERGE_HEALTH_STATE" = "BLOCKED" ]; then
|
|
2618
|
+
MERGE_PROTECTION_WARNING="Merge state is BLOCKED: branch protection is not satisfied (required review or check). Syncing the base cannot clear this."
|
|
2465
2619
|
fi
|
|
2466
2620
|
|
|
2621
|
+
# ONLY_BASE_CONFLICT is NOT computed here: CALIBRATION_NEEDS_HUMAN is only set later (Phase 7B.5) and TRUST_NEEDS_HUMAN in Phase 3B.5,
|
|
2622
|
+
# so it is evaluated at the top of the Phase 8 guard, after every human gate has been set. <!-- forge#3496 -->
|
|
2623
|
+
|
|
2467
2624
|
# Resolve attribution footer (forge.yaml → attribution.pr_footer)
|
|
2468
2625
|
ATTRIBUTION_PR_FOOTER=$(grep -A5 "^attribution:" forge.yaml 2>/dev/null | grep "pr_footer:" | awk '{print $(2)}' | tr -d '"' || echo "false")
|
|
2469
2626
|
ATTRIBUTION_FOOTER_LINE=""
|
|
@@ -2490,7 +2647,9 @@ $([ "$MERGE_HEALTH" = "UNKNOWN" ] && echo "
|
|
|
2490
2647
|
gh pr review "$PR_NUMBER" -R "$REPO" --comment --body "CHANGES REQUESTED: commit $REVIEW_SHA_SHORT — [N] blocking issues found. See GitHub issues.
|
|
2491
2648
|
${TRUST_ANNOTATION}
|
|
2492
2649
|
$([ "$HAS_MERGE_CONFLICT" = "true" ] && echo "
|
|
2493
|
-
🔴
|
|
2650
|
+
🔴 Base Conflict: ${MERGE_CONFLICT_MSG}")
|
|
2651
|
+
$([ -n "$MERGE_PROTECTION_WARNING" ] && echo "
|
|
2652
|
+
⚠ ${MERGE_PROTECTION_WARNING}")
|
|
2494
2653
|
$([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
|
|
2495
2654
|
⚠ Purpose Regression: [N] finding(s) contradict the milestone's stated goal and are automatically blocking regardless of runtime impact. See: ${PURPOSE_REGRESSION_FINDINGS[@]}")${ATTRIBUTION_FOOTER_LINE}"
|
|
2496
2655
|
```
|
|
@@ -2599,11 +2758,16 @@ fi
|
|
|
2599
2758
|
FINDING_COUNT=$(printf '%s' "$DISPO_JSON" | jq -s '[.[][] | select(.body | test("<!-- FINDING:"))] | length')
|
|
2600
2759
|
# Trust predicate: ONE shared copy (scripts/trusted-comments.sh, same as verify-phase-trail.sh): trusted association, Bot account, or
|
|
2601
2760
|
# FORGE_TRAIL_TRUSTED_LOGINS. A GitHub App bot always has author_association NONE, so an association-only filter drops the pipeline's own markers.
|
|
2761
|
+
# 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.
|
|
2602
2762
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2763
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2764
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2765
|
+
[ -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)"
|
|
2766
|
+
done
|
|
2603
2767
|
TRUSTED_SCRIPT=""
|
|
2604
|
-
|
|
2768
|
+
while IFS= read -r _c; do
|
|
2605
2769
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2606
|
-
done
|
|
2770
|
+
done <<< "$_tc"
|
|
2607
2771
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$FINDING_COUNT" ]; then
|
|
2608
2772
|
echo "NOTE DISPOSITION: scripts/trusted-comments.sh unresolvable or findings unreadable — refusing to merge (fail closed)" >&2
|
|
2609
2773
|
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "note disposition unreadable".
|
|
@@ -2747,7 +2911,19 @@ If the preflight failed, skip the rest of Phase 8. On exit code 1, return `REVIE
|
|
|
2747
2911
|
# These vars are set in Phase 7A/7B/7B.5/3B.5 earlier in the same agent session.
|
|
2748
2912
|
# An unset/empty VERDICT is safe — it evaluates to "" which does not equal "CHANGES REQUESTED".
|
|
2749
2913
|
# TRUST_NEEDS_HUMAN: set to true by Phase 3B.5 when INTENSITY_TIER=NOVEL_NEEDS_HUMAN AND shadow mode is off.
|
|
2750
|
-
|
|
2914
|
+
# ONLY_BASE_CONFLICT: the base conflict is the ONLY reason the verdict is CHANGES REQUESTED. Computed HERE (after Phase 7B.5 and 3B.5) so the
|
|
2915
|
+
# calibration and trust human gates are already set and can never be skipped for a base-conflict PR. <!-- forge#3496 -->
|
|
2916
|
+
# Set OTHER_BLOCKING=true when ANY other blocking criterion (1, 2, 3 or 5 in Phase 7B) fired for this PR. When true, route to needs-human below.
|
|
2917
|
+
ONLY_BASE_CONFLICT=false
|
|
2918
|
+
if [ "${BASE_CONFLICT:-false}" = "true" ] && [ "${OTHER_BLOCKING:-false}" != "true" ] && [ "${HAS_PURPOSE_REGRESSION:-false}" != "true" ] && [ "${CALIBRATION_NEEDS_HUMAN:-false}" != "true" ] && [ "${TRUST_NEEDS_HUMAN:-false}" != "true" ]; then
|
|
2919
|
+
ONLY_BASE_CONFLICT=true
|
|
2920
|
+
fi
|
|
2921
|
+
if [ "${ONLY_BASE_CONFLICT:-false}" = "true" ] && [ "$VERDICT" = "CHANGES REQUESTED" ]; then
|
|
2922
|
+
# Pure base conflict (set in Phase 7B: the ONLY blocker): route to the base-sync path, NOT needs-human. <!-- Added: forge#3496 -->
|
|
2923
|
+
# work-on/review.md R4 sets workflow:remediating (not needs-human) when it hands off to remediate; a standalone /review-pr caller just sees the blocker.
|
|
2924
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: base conflict with \`{MERGE_BASE}\` (the only blocking reason). No operator action is needed: the pipeline is performing the base sync automatically (merging \`origin/{MERGE_BASE}\` into the PR branch, merge only, no rebase, no force-push), re-running the quality gate, then re-reviewing the new head. A human is pulled in only if the conflict cannot be resolved automatically." # <!-- allowlist:check-command-side-effects -->
|
|
2925
|
+
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "base-conflict". Do NOT add needs-human and do NOT attempt gh pr merge.
|
|
2926
|
+
elif [ "$VERDICT" = "CHANGES REQUESTED" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ] || [ "$CALIBRATION_NEEDS_HUMAN" = "true" ] || [ "${TRUST_NEEDS_HUMAN:-false}" = "true" ]; then
|
|
2751
2927
|
BLOCK_REASON=""
|
|
2752
2928
|
[ "$VERDICT" = "CHANGES REQUESTED" ] && BLOCK_REASON="review verdict is CHANGES REQUESTED (blocking finding confirmed by Phase 7B)"
|
|
2753
2929
|
[ "$HAS_PURPOSE_REGRESSION" = "true" ] && BLOCK_REASON="${BLOCK_REASON:+${BLOCK_REASON}; }purpose regression detected by Phase 7A (\`HAS_PURPOSE_REGRESSION=true\`)"
|
|
@@ -2765,10 +2941,23 @@ PRE_MERGE_RESULT=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json mergeable,merge
|
|
|
2765
2941
|
PRE_MERGE_HEALTH=${PRE_MERGE_RESULT%%|*}
|
|
2766
2942
|
PRE_MERGE_HEALTH_STATE=${PRE_MERGE_RESULT##*|}
|
|
2767
2943
|
|
|
2768
|
-
|
|
2769
|
-
|
|
2944
|
+
PRE_MERGE_BASE_CONFLICT=false
|
|
2945
|
+
if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIRTY" ]; then
|
|
2946
|
+
PRE_MERGE_BASE_CONFLICT=true
|
|
2947
|
+
elif [ "$PRE_MERGE_HEALTH_STATE" = "BEHIND" ]; then
|
|
2948
|
+
# BEHIND is a conflict only under required-up-to-date protection; unreadable protection counts as "not required". <!-- Added: forge#3496 -->
|
|
2949
|
+
[ "$(gh api "repos/${REPO}/branches/{MERGE_BASE}/protection" --jq '.required_status_checks.strict // false' 2>/dev/null)" = "true" ] && PRE_MERGE_BASE_CONFLICT=true
|
|
2950
|
+
fi
|
|
2951
|
+
|
|
2952
|
+
if [ "$PRE_MERGE_BASE_CONFLICT" = "true" ]; then
|
|
2953
|
+
# Base moved during review: route to the base-sync path, NOT needs-human. <!-- Added: forge#3496 -->
|
|
2954
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: base conflict (\`mergeable=${PRE_MERGE_HEALTH}\`, \`mergeStateStatus=${PRE_MERGE_HEALTH_STATE}\`). No operator action is needed: the pipeline is performing the base sync automatically (merging \`origin/{MERGE_BASE}\` into the PR branch, merge only, no rebase, no force-push) and then re-reviewing. A human is pulled in only if the conflict cannot be resolved automatically." # <!-- allowlist:check-command-side-effects -->
|
|
2955
|
+
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "base-conflict". Do NOT add needs-human and do NOT attempt gh pr merge.
|
|
2956
|
+
elif [ "$PRE_MERGE_HEALTH_STATE" = "BLOCKED" ]; then
|
|
2957
|
+
# Branch protection not satisfied: syncing the base cannot clear it, so this is NOT a base-conflict blocker.
|
|
2958
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: branch protection is not satisfied (\`mergeStateStatus=BLOCKED\`); syncing the base cannot clear it. Satisfy the required review/checks, then re-run /review-pr."
|
|
2770
2959
|
gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "needs-human" 2>/dev/null || true
|
|
2771
|
-
# STOP —
|
|
2960
|
+
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "branch protection not satisfied" (never "base-conflict")
|
|
2772
2961
|
else
|
|
2773
2962
|
|
|
2774
2963
|
# Previously-escalated re-review guard <!-- Added: forge#1810; base-scoped: forge#2570 -->
|
|
@@ -2789,7 +2978,7 @@ else
|
|
|
2789
2978
|
# bot-only re-review (authorAssociation=NONE) could never satisfy the strict ≥2 verified-
|
|
2790
2979
|
# human bar and so stranded staging PRs the fast lane would auto-merge.
|
|
2791
2980
|
PREVIOUSLY_ESCALATED=$(gh issue view {MERGE_ISSUE} {MERGE_GH_FLAG} --json labels,comments \
|
|
2792
|
-
--jq '([.labels[].name | . == "needs-human"] + [.comments[].body | contains("FORGE:REMEDIATION")]) | any' 2>/dev/null || echo "false")
|
|
2981
|
+
--jq '([.labels[].name | . == "needs-human" or . == "workflow:remediating"] + [.comments[].body | contains("FORGE:REMEDIATION")]) | any' 2>/dev/null || echo "false")
|
|
2793
2982
|
GUARD_BASE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2794
2983
|
|
|
2795
2984
|
if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
@@ -2813,7 +3002,7 @@ if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
|
2813
3002
|
adaptive|universal) bash "$SCRIPT_PATH" {MERGE_ISSUE} {MERGE_GH_FLAG} awaiting-merge ;;
|
|
2814
3003
|
prose)
|
|
2815
3004
|
gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "workflow:awaiting-merge" \
|
|
2816
|
-
--remove-label "needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
3005
|
+
--remove-label "needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
2817
3006
|
;;
|
|
2818
3007
|
esac
|
|
2819
3008
|
# STOP — do not attempt gh pr merge here. The merge decision belongs to a human for a
|
|
@@ -3049,11 +3238,16 @@ A degraded run that skipped Task-based agent dispatch must be visible from this
|
|
|
3049
3238
|
|
|
3050
3239
|
```bash
|
|
3051
3240
|
# Same trust rule as Phase 3C/4: only trusted comments whose body starts with the marker count.
|
|
3241
|
+
# 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.
|
|
3052
3242
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
3243
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
3244
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
3245
|
+
[ -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)"
|
|
3246
|
+
done
|
|
3053
3247
|
TRUSTED_SCRIPT=""
|
|
3054
|
-
|
|
3248
|
+
while IFS= read -r _c; do
|
|
3055
3249
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
3056
|
-
done
|
|
3250
|
+
done <<< "$_tc"
|
|
3057
3251
|
ACTUAL_AGENT_DOMAINS=""
|
|
3058
3252
|
if [ -n "$TRUSTED_SCRIPT" ]; then
|
|
3059
3253
|
ACTUAL_AGENT_DOMAINS=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
|
|
@@ -42,7 +42,7 @@ Parse from $ARGUMENTS:
|
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
- `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason.
|
|
45
|
+
- `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason. **Decomposition refusal** <!-- Added: forge#3543 -->: never implement work that the FORGE:INVESTIGATOR `### Decomposition Assessment` (`**YES**`) assigns to a separate sub-issue, even if a listed check targets it; return `status: BLOCKED` with blocker "check <id> belongs to decomposed item <N>" instead.
|
|
46
46
|
|
|
47
47
|
## Phase I1: Load Context from GitHub
|
|
48
48
|
|
|
@@ -559,21 +559,72 @@ Reference `#{NUMBER}` in the message. This is the **only** commit for this build
|
|
|
559
559
|
|
|
560
560
|
### V5 Step 3: Post-Commit Ancestry Audit (MANDATORY)
|
|
561
561
|
|
|
562
|
-
After committing, run the ancestry audit to detect merge commits from
|
|
562
|
+
After committing, run the ancestry audit to detect merge commits that bring in history from outside the PR base before the branch is pushed. Merging `origin/{PR_BASE}` itself into the branch (a base sync) is allowed: every non-first parent of each merge must be an ancestor of `origin/{PR_BASE}`. If `origin/{PR_BASE}` does not exist yet (new branch), skip this check — no contamination is possible from a non-existent base.
|
|
563
563
|
|
|
564
564
|
```bash
|
|
565
565
|
cd {WORKTREE_PATH}
|
|
566
566
|
if git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1; then
|
|
567
567
|
git fetch origin {PR_BASE} >/dev/null 2>&1 || true
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
568
|
+
# Same install-root-only resolution as work-on/review.md R1 (resolve_script 'check-branch-ancestry'): the script gates this branch's own push, so it is NEVER taken from the worktree (the PR branch under audit) or a cwd-relative path. Unresolvable -> prose fallback below, which fails closed.
|
|
569
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
570
|
+
FORGE_ROOT=""
|
|
571
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
572
|
+
_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
|
|
573
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
574
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
575
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
576
|
+
# 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.
|
|
577
|
+
_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")")"
|
|
578
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
579
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
580
|
+
# 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)
|
|
581
|
+
_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)"
|
|
582
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
583
|
+
# '${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.
|
|
584
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
585
|
+
while IFS= read -r _c; do
|
|
586
|
+
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
|
|
587
|
+
done <<< "$_k"
|
|
588
|
+
fi
|
|
589
|
+
ANCESTRY_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-branch-ancestry.sh}"; [ -f "$ANCESTRY_SCRIPT" ] || ANCESTRY_SCRIPT=""
|
|
590
|
+
if [ -n "$ANCESTRY_SCRIPT" ]; then
|
|
591
|
+
MERGE_COMMITS=$(bash "$ANCESTRY_SCRIPT" HEAD origin/{PR_BASE} 2>&1); ANCESTRY_RC=$?
|
|
592
|
+
else
|
|
593
|
+
# Prose fallback: same checks inline (exit 0 clean, 1 foreign, 2 error). Fails closed: unresolvable refs or git errors give rc=2.
|
|
594
|
+
ANCESTRY_RC=0; MERGE_COMMITS=""
|
|
595
|
+
if ! git rev-parse --verify --quiet "origin/{PR_BASE}^{commit}" >/dev/null 2>&1 || ! git rev-parse --verify --quiet "HEAD^{commit}" >/dev/null 2>&1; then
|
|
596
|
+
ANCESTRY_RC=2
|
|
597
|
+
elif ! MERGES=$(git rev-list --merges origin/{PR_BASE}..HEAD 2>/dev/null) || ! FP=$(git rev-list --first-parent origin/{PR_BASE}..HEAD 2>/dev/null); then
|
|
598
|
+
ANCESTRY_RC=2
|
|
599
|
+
else
|
|
600
|
+
for M in $MERGES; do
|
|
601
|
+
PARENTS=$(git rev-list --parents -n 1 "$M" 2>/dev/null) || { ANCESTRY_RC=2; continue; }
|
|
602
|
+
for P in $(printf '%s\n' "$PARENTS" | cut -d' ' -f3-); do
|
|
603
|
+
git merge-base --is-ancestor "$P" origin/{PR_BASE} 2>/dev/null; rc=$?
|
|
604
|
+
if [ "$rc" -eq 1 ]; then MERGE_COMMITS="${MERGE_COMMITS}${M} ${P}
|
|
605
|
+
"; [ "$ANCESTRY_RC" -eq 2 ] || ANCESTRY_RC=1
|
|
606
|
+
elif [ "$rc" -ne 0 ]; then ANCESTRY_RC=2; fi
|
|
607
|
+
done
|
|
608
|
+
done
|
|
609
|
+
# First-parent line must not carry milestone history (branch cut from a milestone, then base sync).
|
|
610
|
+
for F in $(git for-each-ref --format='%(refname)' refs/remotes/origin/milestone/ refs/heads/milestone/ 2>/dev/null); do
|
|
611
|
+
for C in $FP; do
|
|
612
|
+
git merge-base --is-ancestor "$C" "$F" 2>/dev/null; rc=$?
|
|
613
|
+
if [ "$rc" -eq 0 ]; then MERGE_COMMITS="${MERGE_COMMITS}${C} first-parent history reachable from ${F}
|
|
614
|
+
"; [ "$ANCESTRY_RC" -eq 2 ] || ANCESTRY_RC=1
|
|
615
|
+
elif [ "$rc" -ne 1 ]; then ANCESTRY_RC=2; fi
|
|
616
|
+
done
|
|
617
|
+
done
|
|
618
|
+
fi
|
|
619
|
+
fi
|
|
620
|
+
if [ "$ANCESTRY_RC" -ne 0 ]; then
|
|
621
|
+
echo "ANCESTRY AUDIT FAILED (rc=$ANCESTRY_RC): merge commits from outside {PR_BASE} detected, or ancestry could not be verified:"
|
|
571
622
|
echo "$MERGE_COMMITS"
|
|
572
623
|
ANCESTRY_BODY="## Ancestry Audit Failed
|
|
573
624
|
|
|
574
|
-
Branch \`{BRANCH}\` contains merge commits from
|
|
625
|
+
Branch \`{BRANCH}\` contains merge commits that bring in history from outside the PR base (\`{PR_BASE}\`), or ancestry could not be verified (failing closed). This is a staging contamination risk — these commits may carry code from milestone branches that has not been approved for \`{PR_BASE}\`. Merges of \`{PR_BASE}\` itself are allowed and do not trigger this audit.
|
|
575
626
|
|
|
576
|
-
**Detected merge commits
|
|
627
|
+
**Detected merge commits** (merge, foreign parent, subject — or the verification error):
|
|
577
628
|
\`\`\`
|
|
578
629
|
${MERGE_COMMITS}
|
|
579
630
|
\`\`\`
|
|
@@ -590,7 +641,7 @@ else
|
|
|
590
641
|
fi
|
|
591
642
|
```
|
|
592
643
|
|
|
593
|
-
If the output contains `ANCESTRY_FAILED=1`: do NOT append `:COMPLETE` and do NOT push. Print `VALIDATE_RESULT` with `gate_passed: false` and `blocker: ancestry audit failed — merge commits from
|
|
644
|
+
If the output contains `ANCESTRY_FAILED=1`: do NOT append `:COMPLETE` and do NOT push. Print `VALIDATE_RESULT` with `gate_passed: false` and `blocker: ancestry audit failed — merge commits from outside the PR base` as the final reply, and STOP.
|
|
594
645
|
|
|
595
646
|
### V5 Step 4: Mark Build Complete and Record Verification Status (MANDATORY)
|
|
596
647
|
|