forgedock 1.10.3 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/phases-args.test.mjs +112 -0
- package/bin/engine/phases.mjs +152 -45
- package/bin/engine/runlog.mjs +9 -0
- package/bin/engine-cli.mjs +13 -2
- package/bin/engine.mjs +144 -18
- package/bin/forgedock.mjs +2 -2
- package/bin/runner.mjs +89 -12
- package/commands/ci-audit.md +3 -3
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-pr.md +6 -6
- package/commands/explain.md +2 -2
- package/commands/fix-ci.md +2 -2
- package/commands/forge-stats.md +1 -1
- package/commands/forgedock-init.md +3 -3
- package/commands/optimize.md +5 -5
- package/commands/orchestrate/phase-1-resolve.md +2 -2
- package/commands/orchestrate/phase-4-execution.md +208 -48
- package/commands/orchestrate/phase-6-report.md +29 -0
- package/commands/quality-gate.md +97 -13
- package/commands/recover-orphans.md +6 -6
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-staging.md +28 -6
- package/commands/review-pr.md +509 -132
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +2 -2
- package/commands/test-gate.md +7 -7
- package/commands/work-on/build/architect.md +21 -0
- package/commands/work-on/build/context.md +1 -1
- package/commands/work-on/build/implement.md +17 -0
- package/commands/work-on/build/validate.md +60 -9
- package/commands/work-on/build.md +98 -9
- package/commands/work-on/close.md +33 -11
- package/commands/work-on/decompose.md +19 -5
- package/commands/work-on/investigate.md +34 -4
- package/commands/work-on/remediate.md +117 -13
- package/commands/work-on/review.md +66 -26
- package/commands/work-on.md +32 -15
- package/docs/CONFIG.md +30 -0
- package/docs/RELEASE-NOTES.md +9 -0
- package/docs/WORK-ON-RUNTIME.md +24 -4
- package/docs/site/architecture.md +1 -1
- package/docs/spec/forge-protocol-v1.md +10 -0
- package/docs/spec/review-protocol.md +1 -0
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +6 -1
- package/packages/protocol/src/types.js +17 -0
- package/scripts/check-branch-ancestry.sh +82 -0
- package/scripts/check-branch-ancestry.test.sh +78 -0
- package/scripts/check-spec-bash.sh +45 -2
- package/scripts/check-spec-bash.test.sh +24 -1
- package/scripts/check-spec-markers.sh +55 -0
- package/scripts/diff-size.sh +84 -0
- package/scripts/diff-size.test.sh +117 -0
- package/scripts/forge-root.test.sh +16 -1
- package/scripts/remediation-marker-match.test.sh +51 -0
- package/scripts/review-pr-arguments.test.sh +180 -0
- package/scripts/spec-no-worktree-script.test.sh +46 -0
- package/scripts/trusted-comments.test.sh +23 -0
|
@@ -76,7 +76,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
76
76
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
77
77
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
78
78
|
# 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)
|
|
79
|
-
_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)"
|
|
79
|
+
_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)"
|
|
80
80
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
81
81
|
# '${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.
|
|
82
82
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -95,7 +95,7 @@ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped,
|
|
|
95
95
|
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
96
96
|
|
|
97
97
|
resolve_script() {
|
|
98
|
-
local operation="$1"
|
|
98
|
+
local operation="${1}"
|
|
99
99
|
# Tier 2: per-repo adaptive (skip if disabled)
|
|
100
100
|
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
101
101
|
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
@@ -164,7 +164,7 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
164
164
|
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR") | not) | {author: .user.login, body: .body}'
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
|
|
167
|
+
Ignore machine-posted marker comments (`FORGE:DIFF_SIZE`, `FORGE:SIZE_OVERRIDE`) in this scan: a `FORGE:DIFF_SIZE` comment is a plan source (below), never an owner override direction. Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
|
|
168
168
|
|
|
169
169
|
1. **Document the override**: Note which direction the owner is steering (e.g., "remove the feature" vs. investigation's "keep with warnings")
|
|
170
170
|
2. **Re-derive sub-issue scopes**: Derive sub-issue titles, bodies, and file scope from the override direction — NOT from the original investigation recommendation. The investigation's Decomposition Assessment may list sub-issues that are now stale or contradictory with the override.
|
|
@@ -177,6 +177,20 @@ Scan non-agent comments for override signals — phrases like "do not", "do NOT"
|
|
|
177
177
|
|
|
178
178
|
Extract from investigation report:
|
|
179
179
|
- Decomposition Assessment section: list of proposed sub-issues with titles and dependencies
|
|
180
|
+
|
|
181
|
+
**Size-gate plan source** <!-- Added: forge#3450 -->: when the build's diff-size gate routed here, the investigation said no decomposition, so its assessment lists no sub-issues. Read the latest TRUSTED `FORGE:DIFF_SIZE` comment (anchored, through `scripts/trusted-comments.sh`; the Script resolution block applies) and use it when it has `result: OVER`:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# <Script resolution block, verbatim>
|
|
185
|
+
set -o pipefail
|
|
186
|
+
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
187
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ]; then
|
|
188
|
+
SIZE_PLAN=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:DIFF_SIZE' | jq -r 'select(type == "string")' ) \
|
|
189
|
+
&& echo "SIZE_PLAN_READ=ok" || echo "SIZE_PLAN_READ=FAILED"
|
|
190
|
+
else echo "SIZE_PLAN_READ=FAILED"; fi
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
A `FAILED` read or a comment without `result: OVER` means there is no size-gate plan (the assessment rules below then apply unchanged). Its `### Split Proposal` lines (`- **{title}** — {files}`) are the proposed sub-issues, in dependency order, for Phase D2.
|
|
180
194
|
- Milestone (from issue metadata)
|
|
181
195
|
- Priority label (P0/P1/P2) from issue labels
|
|
182
196
|
|
|
@@ -203,7 +217,7 @@ If `PARENT_REF` is non-empty → BLOCKED, blocker: "Issue #{NUMBER} is a sub-iss
|
|
|
203
217
|
|
|
204
218
|
**Guard 2 — investigation report present**: If the FORGE:INVESTIGATOR comment is absent → BLOCKED, blocker: "No investigation report found — run investigate first".
|
|
205
219
|
|
|
206
|
-
**Guard 3 — decomposition plan present**: If the investigation report has no Decomposition Assessment section, OR the assessment does not list any sub-issues → BLOCKED, blocker: "Investigation report has no decomposition plan — re-run investigate with explicit decomposition scope".
|
|
220
|
+
**Guard 3 — decomposition plan present**: If the investigation report has no Decomposition Assessment section, OR the assessment does not list any sub-issues, AND there is no trusted `FORGE:DIFF_SIZE` comment with `result: OVER` and a `### Split Proposal` (the size-gate plan source from D0) → BLOCKED, blocker: "Investigation report has no decomposition plan — re-run investigate with explicit decomposition scope".
|
|
207
221
|
|
|
208
222
|
**Resume check**:
|
|
209
223
|
|
|
@@ -246,7 +260,7 @@ If `GIST_URLS` is non-empty, a `## Prior Investigation` section will be appended
|
|
|
246
260
|
|
|
247
261
|
## Phase D2: Design Sub-Issues
|
|
248
262
|
|
|
249
|
-
From the Decomposition Assessment in the investigation report (adjusted for any owner override detected in Phase D0), extract:
|
|
263
|
+
From the Decomposition Assessment in the investigation report (adjusted for any owner override detected in Phase D0), or, when the assessment lists none, from the size-gate `### Split Proposal` in the trusted `FORGE:DIFF_SIZE` comment (Phase D0), extract:
|
|
250
264
|
1. Sub-issue titles (in dependency order — independent issues first)
|
|
251
265
|
2. Dependencies between sub-issues (if issue B depends on issue A, A is created first)
|
|
252
266
|
3. Brief description for each sub-issue body
|
|
@@ -91,7 +91,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
91
91
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
92
92
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
93
93
|
# 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)
|
|
94
|
-
_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)"
|
|
94
|
+
_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)"
|
|
95
95
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
96
96
|
# '${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.
|
|
97
97
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -110,7 +110,7 @@ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped,
|
|
|
110
110
|
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
111
111
|
|
|
112
112
|
resolve_script() {
|
|
113
|
-
local operation="$1"
|
|
113
|
+
local operation="${1}"
|
|
114
114
|
# Tier 2: per-repo adaptive (skip if disabled)
|
|
115
115
|
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
116
116
|
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
@@ -581,6 +581,26 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
581
581
|
> **Scope-Gap Warning**: The issue spec lists `{PRIMARY_FILE}` but the same pattern exists in `{SIBLING_FILE}:{LINE}`. These were likely introduced together. Recommend widening scope to fix all callers in this PR, or creating follow-up issues for the other files before proceeding.
|
|
582
582
|
|
|
583
583
|
Do NOT silently exclude sibling matches. The appropriate output when sibling files have the same bug is to flag them explicitly — even if the issue spec's silence appears intentional. The fix-approach validation step (step 8) will confirm whether to widen scope or create follow-ups. <!-- Added: forge#383 -->
|
|
584
|
+
7.6. **Finding Pattern Sweep** *(conditional — eligible when the issue has label `review-finding`, or its body carries a `FORGE:PATTERN` or `FORGE:PATTERN-CLASS` tag)*: Review findings are instances of a defect class. Fixing only the cited instance lets sibling instances resurface in the next review, so sweep the whole repo for the class before settling the affected-file list. For eligible issues this step replaces step 7.5; step 7.5 runs for non-eligible issues, and as the fallback when 7.6 is skipped or degrades. <!-- Added: forge#3449 -->
|
|
585
|
+
```bash
|
|
586
|
+
REPO="{GH_REPO}"; NUMBER="{NUMBER}"
|
|
587
|
+
BODY=$(gh issue view "$NUMBER" -R "$REPO" --json body --jq '.body') || BODY=""
|
|
588
|
+
# Slug comes from model-written text: validate before ANY shell or jq use.
|
|
589
|
+
# Class-level issues carry `<!-- FORGE:PATTERN: slug -->` too (review-pr Phase 6C), so one extractor covers both.
|
|
590
|
+
SLUG=$(printf '%s\n' "$BODY" | grep -o 'FORGE:PATTERN: [A-Za-z0-9_-]*' | head -1 | sed 's/^FORGE:PATTERN: //')
|
|
591
|
+
if ! [[ "$SLUG" =~ ^[a-z0-9-]+$ ]]; then
|
|
592
|
+
echo "PATTERN SWEEP SKIPPED: no valid FORGE:PATTERN slug (expected ^[a-z0-9-]+$) — fall back to step 7.5"
|
|
593
|
+
SLUG=""
|
|
594
|
+
fi
|
|
595
|
+
```
|
|
596
|
+
When `SLUG` is non-empty:
|
|
597
|
+
- **Derive queries** from: the slug itself, the finding's **Prevention** sentence (extract its key identifiers), every path under **Files**, and the symbol, call, or condition cited at the defect site. Use 2-5 queries; each query is a fixed string, not a regex. A query must match `^[A-Za-z0-9_.:/-]{3,80}$` (identifier-like, no spaces, quotes, backticks, `$`, `|`); drop any query that does not, and never run an empty query (`-F ""` matches every line).
|
|
598
|
+
- **Search the whole repo** (not just the affected directory), capturing the exit status so a failure is visible instead of masked by a pipe: `OUT=$(timeout 30 git grep -n -F -- "$QUERY"); RC=$?` (use `timeout` only if `command -v timeout` succeeds; stock macOS lacks it). `RC=0` means hits, `RC=1` means no match (0 hits is a valid result), any other `RC` (including 124, timeout) means the query failed. Quote the variable, always pass `-F` and `--`. Then cap: `printf '%s\n' "$OUT" | head -31`.
|
|
599
|
+
- **Hit cap**: record at most 30 hits per query. If a query returns more, record the first 30 and mark the query `truncated at 30` — never silently drop hits.
|
|
600
|
+
- **Disposition per hit** (MANDATORY, no unlabeled hits): `fix` (same defect class, must be changed in this PR) or `not-affected` with a one-line reason (e.g. already guarded, different semantics). The cited instance is always a `fix` row. Record each hit as `file:line` only (never the matched line text), one table row per hit.
|
|
601
|
+
- **Scope cap**: if more than 25 `fix` rows result, do not grow one PR to fit them. Record the cited instance plus the rows in the same subsystem as `fix`, mark the rest `fix-deferred`, and recommend decomposition in the report (set `decompose: YES` if the remaining scope is too large for one PR).
|
|
602
|
+
- **Degrade, never block**: if any query fails (`RC` other than 0 or 1), write `Pattern sweep skipped: {reason}` in the `### Pattern Sweep` section, fall back to step 7.5, and continue. A failed query must never be recorded as 0 hits. The sweep is advisory evidence, not a gate.
|
|
603
|
+
- Every `fix` row MUST appear in `### Affected Files`, and the acceptance spec MUST include a class-wide coverage check plus a check that the added test exercises more than the reviewer's single repro (see Phase 1C).
|
|
584
604
|
8. **Fix-approach validation** — if the issue proposes a fix, don't adopt it as spec. Trace through the target system's middleware, auth, routing, config. Cross-domain: if fix in domain A interacts with domain B, read domain B's files too.
|
|
585
605
|
|
|
586
606
|
---
|
|
@@ -622,7 +642,7 @@ ANNOTATION_BODY=$(node packages/protocol/src/cli.js emit INVESTIGATOR \
|
|
|
622
642
|
Before posting, resolve the attribution annotation link from `forge.yaml`:
|
|
623
643
|
|
|
624
644
|
```bash
|
|
625
|
-
ATTRIBUTION_ANNOTATION_LINK=$(grep -A5 "^attribution:" forge.yaml 2>/dev/null | grep "annotation_link:" | awk '{print $2}' | tr -d '"' || echo "false")
|
|
645
|
+
ATTRIBUTION_ANNOTATION_LINK=$(grep -A5 "^attribution:" forge.yaml 2>/dev/null | grep "annotation_link:" | awk '{print $(2)}' | tr -d '"' || echo "false")
|
|
626
646
|
ANNOTATION_LINK_FOOTER=""
|
|
627
647
|
if [ "$ATTRIBUTION_ANNOTATION_LINK" = "true" ]; then
|
|
628
648
|
ANNOTATION_LINK_FOOTER="
|
|
@@ -632,6 +652,7 @@ if [ "$ATTRIBUTION_ANNOTATION_LINK" = "true" ]; then
|
|
|
632
652
|
fi
|
|
633
653
|
```
|
|
634
654
|
|
|
655
|
+
<!-- allowlist:check-spec-bash -->
|
|
635
656
|
```bash
|
|
636
657
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
637
658
|
## Investigation Report
|
|
@@ -651,7 +672,14 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
651
672
|
{specific root cause, with file:line references where applicable}
|
|
652
673
|
|
|
653
674
|
### Affected Files
|
|
654
|
-
{numbered list of files that need changes}
|
|
675
|
+
{numbered list of files that need changes. For \`review-finding\` issues, include every file with a \`fix\` row from the Pattern Sweep below.}
|
|
676
|
+
|
|
677
|
+
### Pattern Sweep
|
|
678
|
+
{Emit for \`review-finding\` issues (step 7.6). Omit this section for other issues. One row per hit, hit as \`file:line\` only. A query with no hits gets one row with Hit \`none\`. On skip, write \`Pattern sweep skipped: {reason}\`. Queries are pre-validated identifier-like text (step 7.6), so they are safe to place in this template.}
|
|
679
|
+
|
|
680
|
+
| Query | Hit | Disposition |
|
|
681
|
+
|-------|-----|-------------|
|
|
682
|
+
| {validated fixed-string query} | {file:line, \`none\`, or \`truncated at 30\`} | {fix \| not-affected — reason} |
|
|
655
683
|
|
|
656
684
|
### Evidence
|
|
657
685
|
{specific findings — function names, line numbers, behavior observed}
|
|
@@ -696,6 +724,8 @@ ACCEPTANCE_CHECK: id=ac-4 type=command target="grep -qE '(>= ?2|2\+)' commands/o
|
|
|
696
724
|
|
|
697
725
|
**Self-defeating pipe guideline**: do NOT chain a `-q`/`--quiet` command into a downstream pipe consumer (e.g. `grep -q ... | grep ...`). A `-q` flag suppresses all stdout, so the next command in the pipe always receives empty input and the check can never pass regardless of the actual file content. If a check needs to verify two conditions against the same output, sequence them instead — e.g. `grep -qE 'first' file && grep -qE 'second' file` — or capture the output once and grep the captured variable.
|
|
698
726
|
|
|
727
|
+
**Class-wide checks (review-finding issues)**: when a Pattern Sweep was recorded, emit at least one `ACCEPTANCE_CHECK` that verifies the fix covers every `fix` row (for example a `command` check that the defective pattern no longer matches anywhere in the swept paths) and one that verifies the added test covers a representative set of instances rather than the reviewer's single repro. <!-- Added: forge#3449 -->
|
|
728
|
+
|
|
699
729
|
**Skipping**: if the issue has no verifiable acceptance criteria and none can be derived from the recommendation, emit a single sentinel: `ACCEPTANCE_CHECK: id=ac-skip type=skipped target="none" matcher="none" description=No machine-checkable criteria available — human review required`
|
|
700
730
|
${ANNOTATION_LINK_FOOTER}
|
|
701
731
|
${INVESTIGATION_SENTINEL}"
|
|
@@ -84,15 +84,48 @@ ISSUE_LABELS=$(echo "$ISSUE_STATE" | jq -r '[.labels[].name] | join(",")')
|
|
|
84
84
|
**Idempotency / resume check** — the paper trail lives on **both** the PR (primary — checked by the orchestrator's item 6.4 dispatch guard) and the linked issue (mirror — keeps `/work-on`'s standard FORGE-annotation trajectory and resume logic consistent with every other phase):
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
# Trust predicate: ONE shared copy (scripts/trusted-comments.sh). Markers are matched as the comment's LEADING
|
|
88
|
+
# text (^ anchors at the start of the body, not of a line), never with a bare contains(): a review comment or
|
|
89
|
+
# an INPR_FIX work order that merely QUOTES FORGE:REMEDIATION:COMPLETE must not read as a remediation trail (forge#3412).
|
|
90
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
91
|
+
TRUSTED_SCRIPT=""
|
|
92
|
+
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
|
|
93
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
94
|
+
done
|
|
95
|
+
PR_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/{PR_NUMBER}/comments 2>/dev/null) || PR_COMMENTS=""
|
|
96
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$PR_COMMENTS" ]; then
|
|
97
|
+
# Real trail comments (M5 interim, M8 final) START with <!-- FORGE:REMEDIATION -->; M8 also carries <!-- FORGE:REMEDIATION:COMPLETE -->.
|
|
98
|
+
REMEDIATION_COMPLETE_N=$(printf '%s' "$PR_COMMENTS" | bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:REMEDIATION -->[\s\S]*<!-- FORGE:REMEDIATION:COMPLETE -->' 2>/dev/null || echo "")
|
|
99
|
+
REMEDIATION_TRAIL_N=$(printf '%s' "$PR_COMMENTS" | bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:REMEDIATION -->' 2>/dev/null || echo "")
|
|
100
|
+
else
|
|
101
|
+
REMEDIATION_COMPLETE_N=""; REMEDIATION_TRAIL_N=""
|
|
102
|
+
fi
|
|
103
|
+
# Partial (interim, no COMPLETE) comment ids for cleanup: anchored leading marker, no COMPLETE marker, same bot/association trust.
|
|
104
|
+
PARTIAL_PR_COMMENT_IDS=$(printf '%s' "$PR_COMMENTS" | jq -rs '[.[][] | select((.body // "") | startswith("<!-- FORGE:REMEDIATION -->")) | select((.body | contains("<!-- FORGE:REMEDIATION:COMPLETE -->")) | not) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | .id] | .[]' 2>/dev/null || true)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Per-kind scoping of the single-attempt guard** (forge#3496): the guard is scoped per remediation kind so a base-sync round is not swallowed by an earlier `ci-gate` / `inpr-fix` remediation on the same PR. A base-sync handoff is recognised by the router's `FORGE:BASESYNC_REMEDIATION: pr={PR_NUMBER}` bound marker on the issue (the router posts it immediately before invoking this phase); a completed base-sync round is recognised by a `FORGE:REMEDIATION:COMPLETE` trail carrying the `**Base sync**: ran` line that Phase M8 posts. The base-sync round is itself bounded to one, by that marker count in `work-on/review.md` R4 and by this check:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$PR_COMMENTS" ]; then
|
|
111
|
+
BASESYNC_DONE_N=$(printf '%s' "$PR_COMMENTS" | bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:REMEDIATION -->[\s\S]*\*\*Base sync\*\*: ran[\s\S]*<!-- FORGE:REMEDIATION:COMPLETE -->' 2>/dev/null || echo "")
|
|
112
|
+
ISSUE_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments 2>/dev/null) || ISSUE_COMMENTS=""
|
|
113
|
+
BASESYNC_BOUND_N=$(printf '%s' "$ISSUE_COMMENTS" | bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:BASESYNC_REMEDIATION: pr={PR_NUMBER} -->' 2>/dev/null || echo "")
|
|
114
|
+
# A base-sync handoff (bound marker present) with no completed base-sync round yet is a fresh attempt for ITS kind,
|
|
115
|
+
# whatever earlier ci-gate / inpr-fix trails exist. Fail closed: an unreadable count keeps the kind-agnostic guard.
|
|
116
|
+
if [ -n "$BASESYNC_DONE_N" ] && [ -n "$BASESYNC_BOUND_N" ] && [ "$BASESYNC_BOUND_N" -ge 1 ] && [ "$BASESYNC_DONE_N" -eq 0 ]; then REMEDIATION_COMPLETE_N=0; fi
|
|
117
|
+
fi
|
|
89
118
|
```
|
|
90
119
|
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
Completed trails of other kinds are left in place (never deleted); only the interrupted-partial cleanup below deletes anything.
|
|
121
|
+
|
|
122
|
+
- If `REMEDIATION_COMPLETE_N` or `REMEDIATION_TRAIL_N` is empty (comments or `scripts/trusted-comments.sh` unreadable or unresolvable) → fail closed: EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker: "github-unavailable: remediation trail unreadable (comments or scripts/trusted-comments.sh unresolvable)". Never treat unreadable as "no trail" and never as `ALREADY_DONE`.
|
|
123
|
+
- If `REMEDIATION_COMPLETE_N` is at least 1 (a trusted comment that starts with `<!-- FORGE:REMEDIATION -->` and contains `<!-- FORGE:REMEDIATION:COMPLETE -->`) → EXIT `REMEDIATE_RESULT: status: ALREADY_DONE`. **Single-attempt semantics (AC5)**: once a genuine `FORGE:REMEDIATION:COMPLETE` trail comment exists for this PR, do NOT re-attempt fixes on a subsequent invocation, regardless of the prior verdict — this is what prevents an infinite remediation retry loop on a genuinely-blocked PR. A comment that only quotes the marker mid-body (review findings, an `INPR_FIX` work order, a disposition record) or comes from an untrusted author never counts.
|
|
124
|
+
- If `REMEDIATION_COMPLETE_N` is 0 and `REMEDIATION_TRAIL_N` is at least 1 → a prior attempt was interrupted mid-flight (same failure mode as the investigation phase's partial-comment case). Delete the partial comment(s) on both the PR and the issue (ids from `PARTIAL_PR_COMMENT_IDS` above and the same anchored filter over the issue's comments), then continue below as a fresh attempt:
|
|
93
125
|
```bash
|
|
94
|
-
gh api repos/{GH_REPO}/issues/comments
|
|
95
|
-
gh api repos/{GH_REPO}/issues/
|
|
126
|
+
for _id in $PARTIAL_PR_COMMENT_IDS; do gh api repos/{GH_REPO}/issues/comments/$_id -X DELETE 2>/dev/null || true; done
|
|
127
|
+
ISSUE_PARTIAL_IDS=$(gh api --paginate repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments 2>/dev/null | jq -rs '[.[][] | select((.body // "") | startswith("<!-- FORGE:REMEDIATION -->")) | select((.body | contains("<!-- FORGE:REMEDIATION:COMPLETE -->")) | not) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | .id] | .[]' 2>/dev/null || true)
|
|
128
|
+
for _id in $ISSUE_PARTIAL_IDS; do gh api repos/{GH_REPO}/issues/comments/$_id -X DELETE 2>/dev/null || true; done
|
|
96
129
|
```
|
|
97
130
|
- If no comment is found → fresh attempt, continue below.
|
|
98
131
|
|
|
@@ -116,7 +149,7 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
116
149
|
```
|
|
117
150
|
|
|
118
151
|
**Classify into FIXABLE vs. UNFIXABLE**:
|
|
119
|
-
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a
|
|
152
|
+
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a base conflict (`base-conflict`: `CONFLICTING`/`DIRTY`, or `BEHIND` under required-up-to-date protection — resolvable by merging `origin/{PR_BASE}` in, the base-sync step in Phase M3; `BLOCKED` alone is branch protection that a sync cannot clear and is UNFIXABLE unless a conflict state is also present), a quality-gate/build failure, or a CI-gate refusal (`ci gate not green` — the failing/cancelled/timed-out checks are listed on the linked issue; read each failing job's log with `gh run view --log-failed`, fix the cause on the PR branch, or re-run a check that failed for an infrastructure reason), or an in-PR fix request (`in-pr fix required` — review-pr §6B.6 posted a `<!-- FORGE:INPR_FIX: round=1 head=<sha> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM findings in files this PR changed. That comment is the work order: fix exactly the listed findings, at the listed `file:line`, and nothing else. Do not file them as issues; the re-review in Phase M6 files any that remain).
|
|
120
153
|
- **UNFIXABLE (policy escalation)** — `HAS_PURPOSE_REGRESSION=true` (the PR's behavior diverges from the issue's intent — a judgment call, not a code defect), `CALIBRATION_NEEDS_HUMAN=true` (statistical trust threshold), or `TRUST_NEEDS_HUMAN=true` (provenance `NOVEL_NEEDS_HUMAN` tier, insufficient prior data — a policy gate, not a bug). None of these are mechanically "fixable" by re-editing code.
|
|
121
154
|
|
|
122
155
|
**If the block reason classifies as UNFIXABLE** (and no FIXABLE item accompanies it): do NOT attempt any fix. Skip directly to Phase M8 with verdict `UNFIXABLE`, re-affirm `needs-human` (it should already be present), and return `REMEDIATE_RESULT: status: UNFIXABLE`. This satisfies AC5 — "genuinely-blocked PRs still terminate at `needs-human`."
|
|
@@ -139,7 +172,7 @@ Do not perform this transition for an UNFIXABLE policy escalation. Any later qua
|
|
|
139
172
|
|
|
140
173
|
## Phase M2: Checkout the PR's Existing Branch
|
|
141
174
|
|
|
142
|
-
Remediation always fixes forward on top of the PR's existing head commit — never rebase
|
|
175
|
+
Remediation always fixes forward on top of the PR's existing head commit — never rebase and never force-push. The only history-affecting operation permitted is the base sync (`git merge origin/{PR_BASE}`, Phase M3), which adds a merge commit and pushes with a plain `git push`.
|
|
143
176
|
|
|
144
177
|
```bash
|
|
145
178
|
cd {REPO_PATH}
|
|
@@ -164,7 +197,39 @@ For each FIXABLE item from Phase M1: read the affected file(s) in `{WORKTREE_PAT
|
|
|
164
197
|
|
|
165
198
|
**Never delete, skip, or weaken a failing check to go green.** Removing, skipping (`skip`, `xfail`, `continue-on-error`, `if: false`, commenting out) or weakening a failing test or CI step is never a valid fix. Fix the cause in the code under test, or classify the item UNFIXABLE, add/re-affirm `needs-human`, and post a comment that names the failing tests and failing assertions (test title plus assertion text from the CI log). The only exception is a deletion justified by removal of the tested code itself (see quality-gate 2U). Enforcement is the quality gate's coverage-reduction check (2U): a `COVERAGE-1` finding is fixed by restoring the test or step, never by suppressing the finding. <!-- Added: forge#3257 -->
|
|
166
199
|
|
|
167
|
-
**
|
|
200
|
+
**Base-sync (merge-only)** <!-- Added: forge#3496 -->: runs when the block reason was a base conflict (`base-conflict`, or `CONFLICTING`/`DIRTY`, or `BEHIND` under required-up-to-date protection). This step is authorized by the operator's batch dispatch and by the router's `FORGE:BASESYNC_REMEDIATION` bound; it applies to the PR's own branch in `{WORKTREE_PATH}` only. Never rebase, never force-push, never merge into or push to a shared branch.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
cd {WORKTREE_PATH}
|
|
204
|
+
CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
205
|
+
case "{HEAD_BRANCH}" in
|
|
206
|
+
"{PR_BASE}"|main|master|staging|milestone/*) echo "refusing base sync: {HEAD_BRANCH} is a shared branch"; BASESYNC_REFUSED=true ;;
|
|
207
|
+
esac
|
|
208
|
+
[ "$CUR_BRANCH" = "{HEAD_BRANCH}" ] || BASESYNC_REFUSED=true
|
|
209
|
+
if [ "${BASESYNC_REFUSED:-false}" != "true" ]; then
|
|
210
|
+
git fetch origin {PR_BASE} # allowlist:check-command-side-effects
|
|
211
|
+
git merge origin/{PR_BASE} --no-edit # allowlist:check-command-side-effects
|
|
212
|
+
BASESYNC_RAN=true
|
|
213
|
+
fi
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- Clean merge: nothing more to resolve; the merge commit is the sync.
|
|
217
|
+
- Conflicts: read BOTH sides of every conflicted hunk and the surrounding code, then write the combined result by hand. No `-X ours`/`-X theirs`, no wholesale `git checkout --ours/--theirs`, no blanket take-one-side. After resolving, `git add` the files and conclude with `git commit -s --no-edit` (the merge commit).
|
|
218
|
+
- **Unresolvable with confidence** (a hunk whose intent on either side you cannot reconcile, or the branch is refused above): capture the file list first, then abort, and park:
|
|
219
|
+
```bash
|
|
220
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
221
|
+
CONFLICT_FILES=$(git diff --name-only --diff-filter=U)
|
|
222
|
+
git merge --abort # allowlist:check-command-side-effects
|
|
223
|
+
run gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "<!-- FORGE:BASESYNC_FAILED -->
|
|
224
|
+
Base sync of PR #{PR_NUMBER} (\`origin/{PR_BASE}\` into \`{HEAD_BRANCH}\`) could not be resolved with confidence. Conflicting files:
|
|
225
|
+
|
|
226
|
+
\`\`\`
|
|
227
|
+
${CONFLICT_FILES}
|
|
228
|
+
\`\`\`" # allowlist:check-command-side-effects
|
|
229
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
230
|
+
```
|
|
231
|
+
EXIT `REMEDIATE_RESULT: status: BLOCKED` with blocker "base-sync: unresolvable conflicts". Only that issue is parked.
|
|
232
|
+
- After a sync, the quality gate below re-runs on the merged tree, and Phase M6's re-review covers the new head (the reviewed-head guard forces a fresh review).
|
|
168
233
|
|
|
169
234
|
**Quality Gate** (same loop as Phase 3G, max 3 iterations):
|
|
170
235
|
```
|
|
@@ -183,14 +248,42 @@ If still failing after 3 iterations: post a comment, re-affirm `needs-human`, EX
|
|
|
183
248
|
|
|
184
249
|
## Phase M4: Commit, Push, and Close Addressed Findings
|
|
185
250
|
|
|
251
|
+
**Pre-push ancestry guard** (same guard as `work-on/review.md` Phase R1; reuse it, do not write a parallel check): a base-sync merge of `origin/{PR_BASE}` passes it, a merge that brings in history from outside the base fails it.
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# <Script resolution block from work-on/review.md, verbatim>
|
|
255
|
+
cd {WORKTREE_PATH}
|
|
256
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
257
|
+
git fetch origin {PR_BASE} >/dev/null 2>&1 || true
|
|
258
|
+
RESOLUTION=$(resolve_script 'check-branch-ancestry'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
259
|
+
if [ "$TIER" = "prose" ]; then
|
|
260
|
+
MERGE_COMMITS="check-branch-ancestry script not resolvable (fail closed)"; ANCESTRY_RC=2
|
|
261
|
+
else
|
|
262
|
+
MERGE_COMMITS=$(bash "$SCRIPT_PATH" {HEAD_BRANCH} origin/{PR_BASE} 2>&1); ANCESTRY_RC=$?
|
|
263
|
+
fi
|
|
264
|
+
if [ "$ANCESTRY_RC" -ne 0 ]; then
|
|
265
|
+
run gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "## Pre-Push Ancestry Guard Failed
|
|
266
|
+
|
|
267
|
+
Branch \`{HEAD_BRANCH}\` has merge commits from outside \`{PR_BASE}\`, or ancestry could not be verified (rc=${ANCESTRY_RC}). Not pushing.
|
|
268
|
+
|
|
269
|
+
\`\`\`
|
|
270
|
+
${MERGE_COMMITS}
|
|
271
|
+
\`\`\`" # allowlist:check-command-side-effects
|
|
272
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
273
|
+
# EXIT REMEDIATE_RESULT: status: BLOCKED (blocker: pre-push ancestry guard failed) - do not push
|
|
274
|
+
fi
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Then commit and push. A base-sync merge commit already exists from Phase M3; commit only additional quality-gate or fix edits on top.
|
|
278
|
+
|
|
186
279
|
```bash
|
|
187
280
|
cd {WORKTREE_PATH}
|
|
188
281
|
git add -u
|
|
189
|
-
git commit -s -m "fix(remediate): {description} (#{ISSUE_NUMBER})"
|
|
190
|
-
git push origin {HEAD_BRANCH}
|
|
282
|
+
git diff --cached --quiet || git commit -s -m "fix(remediate): {description} (#{ISSUE_NUMBER})"
|
|
283
|
+
git push origin {HEAD_BRANCH} # allowlist:check-command-side-effects
|
|
191
284
|
```
|
|
192
285
|
|
|
193
|
-
If push fails, retry with
|
|
286
|
+
If the push fails, do NOT retry with a force flag (never force-push): post a comment, add `needs-human`, EXIT `REMEDIATE_RESULT: status: BLOCKED`. A raced remote is resolved by a re-run, which fetches and merges naturally.
|
|
194
287
|
|
|
195
288
|
**Close each addressed review-finding issue directly** (this remediation fixes findings in-place on the existing PR, rather than each finding spawning its own downstream `/work-on` pipeline — leaving them open would have a future run rediscover already-fixed code). Track the closed numbers in `ADDRESSED_FINDING_NUMBERS[]` — Phase M8 reports this array in the final paper trail:
|
|
196
289
|
```bash
|
|
@@ -249,6 +342,8 @@ else:
|
|
|
249
342
|
Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
250
343
|
```
|
|
251
344
|
|
|
345
|
+
**Reviewer ownership (spawn-site rule, `docs/WORK-ON-RUNTIME.md` R6)**: `/review-pr` runs inline in this fork. Under async `Agent` dispatch its domain reviewers are attributed to the root session, so their completion notifications never reach this phase: do not end the turn to wait for them. `/review-pr` Phase 4 waits on the reviewers' current-SHA `FORGE:REVIEW-AGENT` PR comments (bounded), and that GitHub comment, not a notification, is the completion signal. If the call returns with no parseable `REVIEW_RESULT` (empty, running or backgrounded), first re-read the PR and issue: a complete current-SHA reviewer panel plus a `FORGE:REVIEW` verdict means consume that state; otherwise re-invoke `/review-pr` once more with the same args (idempotent: it reuses the current-SHA reviewer comments already posted and dispatches only missing domains). Never review inline.
|
|
346
|
+
|
|
252
347
|
**OpenCode joined-child contract**: When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), run this required re-review through one native foreground `task`:
|
|
253
348
|
|
|
254
349
|
```
|
|
@@ -269,6 +364,8 @@ If the retained `REVIEW_RESULT` is `PHASE_TRAIL_FAILED` (forge#3102): the gate r
|
|
|
269
364
|
|
|
270
365
|
If the retained `REVIEW_RESULT` is `BLOCKED` with a blocker mentioning the phase trail (e.g. "phase trail unreadable (rc=N)": the Phase 8 verifier exited ≥2 / 127, forge#3147): the gate could not run, so nothing is missing and nothing is re-run. Keep `needs-human` (review-pr Phase 8 already re-asserted it), do not merge, and exit `REMEDIATE_RESULT: status: BLOCKED` with the same blocker.
|
|
271
366
|
|
|
367
|
+
If the retained `REVIEW_RESULT` is `BLOCKED` with blocker "base-conflict" (forge#3496): the base moved again, or the sync did not clear the conflict. Treat it as re-escalated: the `BASESYNC_REMEDIATION` bound is already used, so do NOT sync a second time. Add `needs-human`, post a comment naming the PR and the conflicting files (`git diff --name-only --diff-filter=U` against a trial merge, or the GitHub mergeability report), and exit `REMEDIATE_RESULT: status: BLOCKED` with blocker "base conflict persists after base-sync remediation".
|
|
368
|
+
|
|
272
369
|
This re-runs the full review (domain agents → verdict → Phase 8 auto-merge gate). The FIXABLE transition above left the issue at the non-terminal `workflow:in-review` state. `review-pr.md` recognizes the in-progress `FORGE:REMEDIATION` marker posted in Phase M5 as evidence of the prior escalation, so one of two things happens inside Phase 8:
|
|
273
370
|
|
|
274
371
|
- **Re-escalated**: the re-review itself trips a fresh block (`CHANGES REQUESTED`, purpose-regression, calibration, trust, or a still-`CONFLICTING` mergeability check) → it adds `needs-human`, and this phase removes `workflow:in-review`, leaving one terminal state.
|
|
@@ -432,6 +529,12 @@ case "$RE_GATE_OUTCOME" in
|
|
|
432
529
|
*) AUTO_LAND_BAR_TEXT="N/A"; OUTCOME_DETAIL="" ;;
|
|
433
530
|
esac
|
|
434
531
|
|
|
532
|
+
# forge#3413: embed the pushed head so the orchestrator scopes a REREVIEW-REQUIRED trail to the CURRENT head only.
|
|
533
|
+
REMEDIATED_HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || true)
|
|
534
|
+
|
|
535
|
+
# forge#3496: mark a base-sync round so Phase M0 scopes the single-attempt guard per kind.
|
|
536
|
+
BASESYNC_LINE=""; [ "${BASESYNC_RAN:-false}" = "true" ] && BASESYNC_LINE="**Base sync**: ran"
|
|
537
|
+
|
|
435
538
|
REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
436
539
|
## Remediation Complete for PR #{PR_NUMBER}
|
|
437
540
|
|
|
@@ -439,7 +542,8 @@ REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
|
439
542
|
**Re-review verdict**: ${RE_REVIEW_VERDICT:-unknown}
|
|
440
543
|
**Auto-land bar**: ${AUTO_LAND_BAR_TEXT}
|
|
441
544
|
**Re-gate outcome**: ${RE_GATE_OUTCOME} ${OUTCOME_DETAIL}
|
|
442
|
-
|
|
545
|
+
**Head**: ${REMEDIATED_HEAD_SHA:-unknown}
|
|
546
|
+
${BASESYNC_LINE}
|
|
443
547
|
<!-- FORGE:REMEDIATION:COMPLETE -->"
|
|
444
548
|
|
|
445
549
|
gh pr comment {PR_NUMBER} {GH_FLAG} --body "$REMEDIATION_BODY"
|
|
@@ -86,7 +86,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
86
86
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
87
87
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
88
88
|
# 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)
|
|
89
|
-
_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)"
|
|
89
|
+
_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)"
|
|
90
90
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
91
91
|
# '${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.
|
|
92
92
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -105,7 +105,7 @@ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped,
|
|
|
105
105
|
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
106
106
|
|
|
107
107
|
resolve_script() {
|
|
108
|
-
local operation="$1"
|
|
108
|
+
local operation="${1}"
|
|
109
109
|
# Tier 2: per-repo adaptive (skip if disabled)
|
|
110
110
|
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
111
111
|
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
@@ -207,30 +207,66 @@ If `LAST_VERDICT_SHA` is empty (no prior CHANGES REQUESTED verdict found) or dif
|
|
|
207
207
|
|
|
208
208
|
## Phase R1: Pre-Push Ancestry Guard
|
|
209
209
|
|
|
210
|
-
Before pushing, verify the branch contains no merge commits from
|
|
210
|
+
Before pushing, verify the branch contains no merge commits that bring in history from outside the PR base. Merging `origin/{PR_BASE}` itself into the branch (a base sync) is allowed: every non-first parent of each merge in `origin/{PR_BASE}..{BRANCH}` must be an ancestor of `origin/{PR_BASE}`. This is the final defense against milestone-code-onto-staging contamination.
|
|
211
211
|
|
|
212
212
|
```bash
|
|
213
|
+
# <Script resolution block from the top of this file, verbatim>
|
|
213
214
|
cd {WORKTREE_PATH}
|
|
214
215
|
# Skip if PR_BASE does not exist on origin yet (new branch — no contamination possible)
|
|
215
216
|
if git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1; then
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
git fetch origin {PR_BASE} >/dev/null 2>&1 || true
|
|
218
|
+
RESOLUTION=$(resolve_script 'check-branch-ancestry'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
219
|
+
if [ "$TIER" = "prose" ]; then
|
|
220
|
+
# Prose fallback: same checks inline (exit 0 clean, 1 foreign, 2 error). Fails closed: unresolvable refs or git errors give rc=2.
|
|
221
|
+
ANCESTRY_RC=0; MERGE_COMMITS=""
|
|
222
|
+
if ! git rev-parse --verify --quiet "origin/{PR_BASE}^{commit}" >/dev/null 2>&1 || ! git rev-parse --verify --quiet "{BRANCH}^{commit}" >/dev/null 2>&1; then
|
|
223
|
+
ANCESTRY_RC=2
|
|
224
|
+
elif ! MERGES=$(git rev-list --merges origin/{PR_BASE}..{BRANCH} 2>/dev/null) || ! FP=$(git rev-list --first-parent origin/{PR_BASE}..{BRANCH} 2>/dev/null); then
|
|
225
|
+
ANCESTRY_RC=2
|
|
226
|
+
else
|
|
227
|
+
for M in $MERGES; do
|
|
228
|
+
PARENTS=$(git rev-list --parents -n 1 "$M" 2>/dev/null) || { ANCESTRY_RC=2; continue; }
|
|
229
|
+
for P in $(printf '%s\n' "$PARENTS" | cut -d' ' -f3-); do
|
|
230
|
+
git merge-base --is-ancestor "$P" origin/{PR_BASE} 2>/dev/null; rc=$?
|
|
231
|
+
if [ "$rc" -eq 1 ]; then MERGE_COMMITS="${MERGE_COMMITS}${M} ${P}
|
|
232
|
+
"; [ "$ANCESTRY_RC" -eq 2 ] || ANCESTRY_RC=1
|
|
233
|
+
elif [ "$rc" -ne 0 ]; then ANCESTRY_RC=2; fi
|
|
234
|
+
done
|
|
235
|
+
done
|
|
236
|
+
# First-parent line must not carry milestone history (branch cut from a milestone, then base sync).
|
|
237
|
+
for F in $(git for-each-ref --format='%(refname)' refs/remotes/origin/milestone/ refs/heads/milestone/ 2>/dev/null); do
|
|
238
|
+
for C in $FP; do
|
|
239
|
+
git merge-base --is-ancestor "$C" "$F" 2>/dev/null; rc=$?
|
|
240
|
+
if [ "$rc" -eq 0 ]; then MERGE_COMMITS="${MERGE_COMMITS}${C} first-parent history reachable from ${F}
|
|
241
|
+
"; [ "$ANCESTRY_RC" -eq 2 ] || ANCESTRY_RC=1
|
|
242
|
+
elif [ "$rc" -ne 1 ]; then ANCESTRY_RC=2; fi
|
|
243
|
+
done
|
|
244
|
+
done
|
|
245
|
+
fi
|
|
246
|
+
else
|
|
247
|
+
MERGE_COMMITS=$(bash "$SCRIPT_PATH" {BRANCH} origin/{PR_BASE} 2>&1); ANCESTRY_RC=$?
|
|
248
|
+
fi
|
|
249
|
+
if [ "$ANCESTRY_RC" -ne 0 ]; then
|
|
250
|
+
if [ "$ANCESTRY_RC" -eq 1 ]; then
|
|
251
|
+
echo "PRE-PUSH ANCESTRY GUARD FAILED: merge commits from outside {PR_BASE} detected"
|
|
252
|
+
else
|
|
253
|
+
echo "PRE-PUSH ANCESTRY GUARD FAILED: could not verify ancestry (failing closed)"
|
|
254
|
+
fi
|
|
219
255
|
gh issue comment {NUMBER} {GH_FLAG} --body "## Pre-Push Ancestry Guard Failed
|
|
220
256
|
|
|
221
|
-
Branch \`{BRANCH}\` contains merge commits from
|
|
257
|
+
Branch \`{BRANCH}\` contains merge commits that bring in history from outside the PR base (\`{PR_BASE}\`), or ancestry could not be verified. Pushing this branch risks contaminating \`{PR_BASE}\` with unapproved code (e.g. milestone code leaking onto staging). Merges of \`{PR_BASE}\` itself are allowed and do not trigger this guard.
|
|
222
258
|
|
|
223
|
-
**Detected merge commits
|
|
259
|
+
**Detected merge commits** (merge, foreign parent, subject — or the verification error):
|
|
224
260
|
\`\`\`
|
|
225
261
|
${MERGE_COMMITS}
|
|
226
262
|
\`\`\`
|
|
227
263
|
|
|
228
|
-
Do NOT push this branch. Human review required to identify the source of the merge commits and clean the branch history (
|
|
264
|
+
Do NOT push this branch. Human review required to identify the source of the merge commits and clean the branch history (a human re-creates the branch from \`origin/{PR_BASE}\` with only the intended commits).
|
|
229
265
|
|
|
230
|
-
<!-- FORGE:PUSH_BLOCKED -->"
|
|
266
|
+
<!-- FORGE:PUSH_BLOCKED -->" # allowlist:check-command-side-effects
|
|
231
267
|
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
232
268
|
# Return REVIEW_RESULT: status: BLOCKED — do not push
|
|
233
|
-
printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number:\n pr_url:\n merged_to:\n blocker: %s\n' "pre-push ancestry guard failed: merge commits from outside {PR_BASE}"
|
|
269
|
+
printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number:\n pr_url:\n merged_to:\n blocker: %s\n' "pre-push ancestry guard failed: merge commits from outside {PR_BASE} or ancestry unverifiable"
|
|
234
270
|
exit 1
|
|
235
271
|
fi
|
|
236
272
|
fi
|
|
@@ -268,12 +304,7 @@ cd {WORKTREE_PATH}
|
|
|
268
304
|
git push -u origin {BRANCH} # allowlist:check-command-side-effects
|
|
269
305
|
```
|
|
270
306
|
|
|
271
|
-
If push fails, retry with
|
|
272
|
-
```bash
|
|
273
|
-
git push -u origin {BRANCH} --force-with-lease # allowlist:check-command-side-effects
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
If still fails:
|
|
307
|
+
If the push fails, do NOT retry with a force flag (never force-push): a raced or diverged remote is a human decision, and a conflict with the base is handled by the base-sync path (`base-conflict` in Phase R4), not by overwriting the remote. Fall straight through to:
|
|
277
308
|
```bash
|
|
278
309
|
gh issue comment {NUMBER} {GH_FLAG} --body "## Push Failed
|
|
279
310
|
|
|
@@ -312,7 +343,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
312
343
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
313
344
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
314
345
|
# 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)
|
|
315
|
-
_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)"
|
|
346
|
+
_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)"
|
|
316
347
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
317
348
|
# '${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.
|
|
318
349
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -376,7 +407,7 @@ Derive from issue title:
|
|
|
376
407
|
Before creating the PR, check `forge.yaml → attribution.pr_footer`:
|
|
377
408
|
|
|
378
409
|
```bash
|
|
379
|
-
ATTRIBUTION_PR_FOOTER=$(grep -A5 "^attribution:" forge.yaml 2>/dev/null | grep "pr_footer:" | awk '{print $2}' | tr -d '"' || echo "false")
|
|
410
|
+
ATTRIBUTION_PR_FOOTER=$(grep -A5 "^attribution:" forge.yaml 2>/dev/null | grep "pr_footer:" | awk '{print $(2)}' | tr -d '"' || echo "false")
|
|
380
411
|
```
|
|
381
412
|
|
|
382
413
|
If `ATTRIBUTION_PR_FOOTER` is `true`, append the following footer to the PR body (once — never duplicate on retries):
|
|
@@ -480,6 +511,8 @@ else:
|
|
|
480
511
|
Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER} --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
481
512
|
```
|
|
482
513
|
|
|
514
|
+
**Reviewer ownership (spawn-site rule, `docs/WORK-ON-RUNTIME.md` R6)**: `/review-pr` runs inline in this fork. Under async `Agent` dispatch its domain reviewers are attributed to the root session, so their completion notifications never reach this phase: do not end the turn to wait for them. `/review-pr` Phase 4 waits on the reviewers' current-SHA `FORGE:REVIEW-AGENT` PR comments (bounded), and that GitHub comment, not a notification, is the completion signal. If the call returns with no parseable `REVIEW_RESULT` (empty, running or backgrounded), first re-read the PR and issue: a complete current-SHA reviewer panel plus a `FORGE:REVIEW` verdict means consume that state; otherwise re-invoke `/review-pr` once more with the same args (idempotent: it reuses the current-SHA reviewer comments already posted and dispatches only missing domains). Never review inline.
|
|
515
|
+
|
|
483
516
|
**OpenCode joined-child contract**: When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), invoke this load-bearing review through one native foreground `task` instead of treating the `Skill(...)` line as an asynchronous handoff:
|
|
484
517
|
|
|
485
518
|
```
|
|
@@ -545,18 +578,21 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
545
578
|
In-PR fix round did not land; the remaining CONFIRMED MEDIUM findings are filed as issues on the next review instead of blocking the merge." # allowlist:check-command-side-effects
|
|
546
579
|
```
|
|
547
580
|
|
|
548
|
-
Persisted loop bounds for the cases above — count first, then act (`{BOUND}` is `STALE_REREVIEW`, `CI_REMEDIATION` or `
|
|
581
|
+
Persisted loop bounds for the cases above — count first, then act (`{BOUND}` is `STALE_REREVIEW`, `CI_REMEDIATION`, `INPR_REMEDIATION` or `BASESYNC_REMEDIATION`). This phase posts only the `STALE_REREVIEW` marker; `CI_REMEDIATION`, `INPR_REMEDIATION` and `BASESYNC_REMEDIATION` are posted by the router in Phase 4R, immediately before it invokes remediation (forge#3398):
|
|
549
582
|
```bash
|
|
550
583
|
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] bound check only"; }
|
|
551
584
|
if [ "{BOUND}" = "STALE_REREVIEW" ]; then
|
|
552
585
|
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
553
|
-
--jq '[.[] | select((.body | contains("FORGE:STALE_REREVIEW:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
586
|
+
--jq '[.[] | select((.body | contains("FORGE:STALE_REREVIEW:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
554
587
|
elif [ "{BOUND}" = "INPR_REMEDIATION" ]; then
|
|
555
588
|
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
556
|
-
--jq '[.[] | select((.body | contains("FORGE:INPR_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
589
|
+
--jq '[.[] | select((.body | contains("FORGE:INPR_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
590
|
+
elif [ "{BOUND}" = "BASESYNC_REMEDIATION" ]; then
|
|
591
|
+
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
592
|
+
--jq '[.[] | select((.body | contains("FORGE:BASESYNC_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
557
593
|
else
|
|
558
594
|
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
559
|
-
--jq '[.[] | select((.body | contains("FORGE:CI_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
595
|
+
--jq '[.[] | select((.body | contains("FORGE:CI_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
560
596
|
fi
|
|
561
597
|
if [ "$BOUND_COUNT" -ge 1 ]; then
|
|
562
598
|
echo "BOUND_EXHAUSTED: {BOUND} already used for PR #{PR_NUMBER}"
|
|
@@ -567,7 +603,11 @@ Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head on
|
|
|
567
603
|
```
|
|
568
604
|
`BOUND_EXHAUSTED` → take the "already ≥ 1" branch of that case.
|
|
569
605
|
|
|
570
|
-
-
|
|
606
|
+
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "base-conflict" (review-pr Phase 7B / Phase 8: `mergeable=CONFLICTING` or `mergeStateStatus=DIRTY`, or `BEHIND` under required-up-to-date protection, and no other blocking finding; forge#3496): do NOT attempt the manual merge below and do NOT merge by any other route. Syncing the base is pipeline work: merge `origin/{PR_BASE}` into the issue's own branch (merge only, never rebase, never force-push, never touch a shared branch). Run **one** sync round, bounded by `<!-- FORGE:BASESYNC_REMEDIATION: pr={PR_NUMBER} -->` on the issue (`{BOUND}` = `BASESYNC_REMEDIATION` below).
|
|
607
|
+
- **Bound unused**: add `needs-human` (remediation only targets `needs-human`-gated PRs, and its Phase M1 clears it as FIXABLE). Then return `REVIEW_RESULT: status: NEXT` with `next: remediate` and `remediation: base-sync`. The router posts the bound marker (Phase 4R) and runs `work-on:remediate`, which merges the base in, re-runs the quality gate and re-reviews the new head. Never invoke `work-on:remediate` from this phase (same reason as the ci-gate case).
|
|
608
|
+
- **Bound already used** (the sync ran and the PR still conflicts, or the sync could not resolve a hunk): do not sync again. Leave `needs-human` and return `REVIEW_RESULT: status: BLOCKED` with blocker "base conflict persists after base-sync remediation". List the conflicting files in the issue comment you post, taken from the remediation's `FORGE:BASESYNC_FAILED` comment (the `git diff --name-only --diff-filter=U` list) when present.
|
|
609
|
+
|
|
610
|
+
- PR NOT MERGED (and not a phase-trail, auto-merge-gate, stale-review, ci-gate, in-pr-fix or base-conflict BLOCKED above) → attempt manual merge, **only if the PR head is still the commit named in the latest `<!-- FORGE:REVIEW -->` APPROVED verdict** (otherwise re-invoke Phase R3 instead, as for "stale review") **and only after the same CI gate**:
|
|
571
611
|
```bash
|
|
572
612
|
# CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
|
|
573
613
|
# green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
|
|
@@ -590,7 +630,7 @@ Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head on
|
|
|
590
630
|
# FORGE_TRAIL_TRUSTED_LOGINS. A GitHub App bot always has author_association NONE, so an association-only filter drops the pipeline's own disposition.
|
|
591
631
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
592
632
|
TRUSTED_SCRIPT=""
|
|
593
|
-
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$
|
|
633
|
+
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock"; do
|
|
594
634
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
595
635
|
done
|
|
596
636
|
if [ -n "$TRUSTED_SCRIPT" ]; then DISPOSITION_COUNT=$(printf '%s' "$DISPO_JSON" | bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:NOTE_DISPOSITION' 2>/dev/null || echo ""); else DISPOSITION_COUNT=""; fi
|
|
@@ -621,7 +661,7 @@ REVIEW_RESULT:
|
|
|
621
661
|
pr_url: {PR_URL}
|
|
622
662
|
merged_to: {PR_BASE}
|
|
623
663
|
next: {remediate, only when status=NEXT}
|
|
624
|
-
remediation: {ci-gate | inpr-fix, only when status=NEXT}
|
|
664
|
+
remediation: {ci-gate | inpr-fix | base-sync, only when status=NEXT}
|
|
625
665
|
blocker: {description if status=BLOCKED}
|
|
626
666
|
```
|
|
627
667
|
|