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
|
@@ -336,7 +336,7 @@ case "$TIER" in
|
|
|
336
336
|
prose)
|
|
337
337
|
[ -n "${DRY_RUN:-}" ] && echo "DRY_RUN: set workflow:building" || \
|
|
338
338
|
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
|
|
339
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
339
|
+
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
340
340
|
;;
|
|
341
341
|
esac
|
|
342
342
|
```
|
|
@@ -529,7 +529,12 @@ Review findings per PR rise steeply with diff size, so an oversized diff is spli
|
|
|
529
529
|
|
|
530
530
|
```bash
|
|
531
531
|
# <Script resolution block from above goes here, verbatim>
|
|
532
|
-
|
|
532
|
+
# Fresh origin/base for the merge-base measurement: retry with 10s/30s/60s backoff, record whether it ever succeeded
|
|
533
|
+
FETCH_OK=0
|
|
534
|
+
for _delay in 0 10 30 60; do
|
|
535
|
+
[ "$_delay" -gt 0 ] && sleep "$_delay"
|
|
536
|
+
if git -C "{WORKTREE_PATH}" fetch origin "{PR_BASE}" 2>/dev/null; then FETCH_OK=1; break; fi
|
|
537
|
+
done
|
|
533
538
|
THRESHOLD=$(yq '.build.diff_size.threshold // 1000' forge.yaml 2>/dev/null || echo 1000)
|
|
534
539
|
EXCLUDES=()
|
|
535
540
|
while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
|
|
@@ -539,12 +544,13 @@ case "$SCRIPT_REF" in
|
|
|
539
544
|
prose:*) echo "SIZE_GATE: scripts/diff-size.sh unresolved (prose tier) — gate skipped with a note in the FORGE:BUILDER comment" ;;
|
|
540
545
|
*)
|
|
541
546
|
SIZE_OUT=$(bash "${SCRIPT_REF#*:}" --repo-path "{WORKTREE_PATH}" --base "{PR_BASE}" --threshold "$THRESHOLD" ${EXCLUDES[@]+"${EXCLUDES[@]}"}) && SIZE_RC=0 || SIZE_RC=$?
|
|
542
|
-
echo "SIZE_RC=${SIZE_RC}"; printf '%s\n' "$SIZE_OUT" ;;
|
|
547
|
+
echo "SIZE_RC=${SIZE_RC}"; echo "FETCH_OK=${FETCH_OK}"; printf '%s\n' "$SIZE_OUT" ;;
|
|
543
548
|
esac
|
|
544
549
|
```
|
|
545
550
|
|
|
546
551
|
- Prose tier (script unresolvable) → skip the gate, continue to B6 (the script is part of the install; a missing install must not strand every build).
|
|
547
|
-
- Exit 2, empty output, or output without `diff_lines=`, `threshold=` and `over
|
|
552
|
+
- Exit 2, empty output, or output without `diff_lines=`, `threshold=` and `over=`, AND `FETCH_OK=0` (the base fetch failed after the 10s/30s/60s retries, worst case about 100s) → Blocked exit with blocker `github-unavailable: could not fetch origin/{PR_BASE} for diff-size measurement` (transient; the router retry rule applies).
|
|
553
|
+
- The same unmeasurable result with `FETCH_OK=1` (the fetch succeeded, so the script itself failed) → Blocked exit with blocker `size-gate-unavailable: diff-size.sh failed (rc=<N>)`. Either way, do NOT treat an unmeasurable diff as under threshold.
|
|
548
554
|
- Otherwise parse `diff_lines`, `excluded_lines`, `threshold`, `over` and the `top=` lines.
|
|
549
555
|
|
|
550
556
|
**Step 2 — look up an override** (only when `over=true`). Both lookups are anchored, trust-filtered through `scripts/trusted-comments.sh` (trusted association, Bot, or `FORGE_TRAIL_TRUSTED_LOGINS`), and fail closed:
|
|
@@ -782,6 +788,7 @@ fi
|
|
|
782
788
|
If `GATE_PASS = false`, the build repairs itself **once** before anything escalates (a failing acceptance check is pipeline work, not a human decision):
|
|
783
789
|
|
|
784
790
|
1. Count `<!-- FORGE:ACCEPTANCE_REPAIR: issue={NUMBER} -->` comments on the issue. If one already exists, skip to step 4.
|
|
791
|
+
1.5. **Decomposition guard** <!-- Added: forge#3543 -->: read the `### Decomposition Assessment` of the latest FORGE:INVESTIGATOR comment. If it is `**YES**`, a failing check that belongs to a sub-item the assessment assigns to a separate issue (named in the check's `description`/`# item-N` tag, or matching a listed sub-issue title) is NOT repaired: exclude it from the `--fix-acceptance` list. When ambiguous, treat the check as decomposed-out (fail safe). If every remaining failure is decomposed-out, do not invoke implement: leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "acceptance check {ID} belongs to decomposed item {N} — repair refuses to implement work the investigator assigned to a separate issue".
|
|
785
792
|
2. Post that marker (with the failed check ids), remove `needs-human` if this gate added it, then invoke
|
|
786
793
|
`Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-acceptance \"<failed check ids and details>\"")`
|
|
787
794
|
then re-derive `{CHANGED_FILES}` from the repaired worktree (never reuse the pre-repair value, and never omit `--files`; an empty list falls back to this diff):
|
|
@@ -20,7 +20,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
|
|
|
20
20
|
**Invoked by**: the work-on router, when `INVESTIGATE_RESULT.decompose = YES`. This skill runs in an isolated forked context: it sees only its args and re-reads everything else from GitHub.
|
|
21
21
|
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set `workflow:decomposed`. STOP — each sub-issue runs its own /work-on. This skill is the SOLE owner of the `workflow:decomposed` label. On BLOCKED it adds `needs-human` and posts the blocker.
|
|
22
22
|
|
|
23
|
-
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are registered as a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and the matching `decompose` entry in `bin/engine/phases.mjs`'s `PHASES` array. The engine's `investigate` phase hands off to it on
|
|
23
|
+
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are registered as a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and the matching `decompose` entry in `bin/engine/phases.mjs`'s `PHASES` array. The engine's `investigate` phase hands off to it on `<!-- DECOMPOSE:YES -->` (or a `**YES**` verdict under `### Decomposition Assessment`) in the newest trusted FORGE:INVESTIGATOR report, instead of terminating in place, so a headless `runIssue()` walk dispatches this subcommand rather than stopping short.
|
|
24
24
|
|
|
25
25
|
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (label transitions, sub-issue creation) stay at this tier because they're interleaved with the reasoning-heavy sub-issue design steps in the same `Skill()` invocation. <!-- Added: forge#1827 -->
|
|
26
26
|
**NEVER use plan mode (EnterPlanMode).**
|
|
@@ -513,7 +513,7 @@ case "$TIER" in
|
|
|
513
513
|
adaptive|universal) run bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
|
|
514
514
|
prose)
|
|
515
515
|
run gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
|
|
516
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
516
|
+
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
517
517
|
;;
|
|
518
518
|
esac
|
|
519
519
|
```
|
|
@@ -416,7 +416,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
416
416
|
case "$TIER" in
|
|
417
417
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
|
|
418
418
|
prose)
|
|
419
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
419
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
420
420
|
;;
|
|
421
421
|
esac
|
|
422
422
|
```
|
|
@@ -609,7 +609,7 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
609
609
|
|
|
610
610
|
The comment MUST include a terminal sentinel at the very end, AFTER all required sections are present. **The sentinel is conditional on the resolved Verdict — it is NOT always `<!-- INVESTIGATION:COMPLETE -->`:**
|
|
611
611
|
|
|
612
|
-
- **Verdict is INVALID** → close with `<!-- INVESTIGATION:INVALID -->`. This is a distinct, already-wired-up terminal marker: `bin/engine/phases.mjs`'s `detectOutcome` for the `investigate` phase
|
|
612
|
+
- **Verdict is INVALID** → close with `<!-- INVESTIGATION:INVALID -->`. This is a distinct, already-wired-up terminal marker: `bin/engine/phases.mjs`'s `detectOutcome` for the `investigate` phase reads it as the closing line of the newest trusted `<!-- FORGE:INVESTIGATOR -->` comment (never as a substring of prose or from an untrusted author) and routes to `terminalReason: "invalid"`; `bin/hooks/interactive-engine.mjs`'s `PHASE_MARKERS` table also already treats it as terminal. Emitting `INVESTIGATION:COMPLETE` for an INVALID verdict is what previously caused every completed investigation to read as `{verdict: "CONFIRMED"}` regardless of actual outcome — do NOT regress this (forge#2350).
|
|
613
613
|
- **Verdict is CONFIRMED or PARTIAL** → close with `<!-- INVESTIGATION:COMPLETE -->` as before (PARTIAL still routes to `ready-to-build` in Phase 1D — only INVALID gets the distinct terminal sentinel).
|
|
614
614
|
|
|
615
615
|
Compute the sentinel once, before building the comment body:
|
|
@@ -635,6 +635,9 @@ ANNOTATION_BODY=$(node packages/protocol/src/cli.js emit INVESTIGATOR \
|
|
|
635
635
|
--field "Task Type={TASK_TYPE}" \
|
|
636
636
|
--field "Decomposition Assessment={YES|NO} — {reason}")
|
|
637
637
|
# ANNOTATION_BODY now has opening tag + required fields + INVESTIGATION:COMPLETE sentinel.
|
|
638
|
+
# After appending the body sections, the Decomposition Assessment MUST be followed by exactly one
|
|
639
|
+
# machine-readable marker line (`<!-- DECOMPOSE:YES -->` or `<!-- DECOMPOSE:NO -->`) — see the
|
|
640
|
+
# "Decomposition marker" rule below the template. The codec does not add it for you.
|
|
638
641
|
# NOTE: the codec's sentinel is fixed — do not use this path when Verdict=INVALID (see caveat above).
|
|
639
642
|
# Append the Markdown body sections to it before posting.
|
|
640
643
|
```
|
|
@@ -700,6 +703,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
700
703
|
### Decomposition Assessment
|
|
701
704
|
**{YES|NO}** — {reason}
|
|
702
705
|
{if YES: proposed sub-issues with titles and dependencies}
|
|
706
|
+
<!-- DECOMPOSE:{YES|NO} -->
|
|
703
707
|
|
|
704
708
|
### Acceptance Spec <!-- Added: forge#1829 -->
|
|
705
709
|
{For each item in the issue's ## Acceptance Criteria section, emit one machine-checkable check line using the format below. If the issue has no Acceptance Criteria section, derive checks from the Recommendation above. Each check MUST be specific, observable, and testable — not vague prose. Checks are consumed by build/validate Phase B6.5 as the merge gate.}
|
|
@@ -731,6 +735,12 @@ ${ANNOTATION_LINK_FOOTER}
|
|
|
731
735
|
${INVESTIGATION_SENTINEL}"
|
|
732
736
|
```
|
|
733
737
|
|
|
738
|
+
**Rules for the template above (instructions, not template text; never copy them into the posted comment):**
|
|
739
|
+
|
|
740
|
+
**Decomposition marker (MANDATORY, both output paths — template and codec)**: emit exactly one `<!-- DECOMPOSE:YES -->` or `<!-- DECOMPOSE:NO -->` line per investigator comment, matching the `**YES**`/`**NO**` verdict, on its own line (nothing else on that line) after the verdict line and any sub-issue list (the `**YES**`/`**NO**` line must stay directly under the heading — the resume parser reads it). The headless engine routes to `work-on/decompose` on this marker (or on the `**YES**` heading in older comments). Never quote or mention the opposite marker anywhere in the comment. Never emit it when Verdict=INVALID. <!-- Added: forge#3543 -->
|
|
741
|
+
|
|
742
|
+
**Decomposition scopes the Acceptance Spec**: when the assessment is YES, the Acceptance Spec MUST cover only the first, in-scope item — or tag each check with the sub-item it belongs to (append `# item-N` to the description). Never emit checks for work the assessment assigns to a separate sub-issue: build's acceptance gate would otherwise fail on them and its repair loop would pull that work into this branch. <!-- Added: forge#3543 -->
|
|
743
|
+
|
|
734
744
|
**Do not hardcode `<!-- INVESTIGATION:COMPLETE -->` as the closing line.** The closing line MUST be the `${INVESTIGATION_SENTINEL}` variable computed above — it resolves to `<!-- INVESTIGATION:INVALID -->` for an INVALID verdict and `<!-- INVESTIGATION:COMPLETE -->` otherwise. `INVESTIGATION:COMPLETE` and `INVESTIGATION:INVALID` are mutually exclusive within a single posted comment — never emit both.
|
|
735
745
|
|
|
736
746
|
---
|
|
@@ -1183,7 +1193,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
1183
1193
|
case "$TIER" in
|
|
1184
1194
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
|
|
1185
1195
|
prose)
|
|
1186
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1196
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1187
1197
|
;;
|
|
1188
1198
|
esac
|
|
1189
1199
|
```
|
|
@@ -1203,7 +1213,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
1203
1213
|
case "$TIER" in
|
|
1204
1214
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
|
|
1205
1215
|
prose)
|
|
1206
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1216
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1207
1217
|
;;
|
|
1208
1218
|
esac
|
|
1209
1219
|
gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}" # allowlist:check-command-side-effects
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
user-invocable: false
|
|
3
|
-
description: Remediate subcommand — checkout a needs-human PR, fix review findings, re-review, and re-gate with a FORGE:REMEDIATION paper trail
|
|
3
|
+
description: Remediate subcommand — checkout a needs-human or workflow:remediating PR, fix review findings, re-review, and re-gate with a FORGE:REMEDIATION paper trail
|
|
4
4
|
argument-hint: "[PR number] [--issue N] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BASE]"
|
|
5
5
|
context: fork
|
|
6
6
|
background: false
|
|
@@ -19,7 +19,7 @@ background: false
|
|
|
19
19
|
|
|
20
20
|
**Invoked by**:
|
|
21
21
|
- `work-on.md` Phase 0A.1 (router), standalone: `/work-on <pr> --remediate` (see forge#1813).
|
|
22
|
-
- `commands/orchestrate/phase-4-execution.md` item 6.4, auto-dispatched against a `needs-human`-gated predecessor's own open PR.
|
|
22
|
+
- `commands/orchestrate/phase-4-execution.md` item 6.4, auto-dispatched against a `needs-human`-gated or `workflow:remediating` predecessor's own open PR.
|
|
23
23
|
- `work-on.md` Phase 4R (router), when the review phase hands off a red CI gate or an in-PR fix request (`REVIEW_RESULT: status: NEXT`).
|
|
24
24
|
|
|
25
25
|
**Only the router invokes this skill** (forge#3398, `work-on.md` Hard Rule 1a). It re-reviews through `/review-pr`, which spawns domain reviewers, so it must run one level below the router. No phase may invoke it from inside its own fork.
|
|
@@ -29,7 +29,7 @@ background: false
|
|
|
29
29
|
**Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`.
|
|
30
30
|
**NEVER use plan mode (EnterPlanMode).**
|
|
31
31
|
|
|
32
|
-
**Scope note**: This mode owns exactly one gap — re-driving a `needs-human` PR's own remediation. It does NOT implement the `needs-human` sub-label taxonomy (#1815's scope) and it does NOT edit `review-pr.md`'s Phase 8 guard (forge#1810) — that guard's existing safe-default (`workflow:awaiting-merge` on any clean re-review of a previously-escalated PR) is reused as-is; this file only adds a bar-check *after* that guard has already fired.
|
|
32
|
+
**Scope note**: This mode owns exactly one gap — re-driving a `needs-human` or `workflow:remediating` PR's own remediation (the latter is the non-human "autonomous remediation pending" state review sets for ci-gate, in-pr-fix and base-sync handoffs, forge#3541). It does NOT implement the `needs-human` sub-label taxonomy (#1815's scope) and it does NOT edit `review-pr.md`'s Phase 8 guard (forge#1810) — that guard's existing safe-default (`workflow:awaiting-merge` on any clean re-review of a previously-escalated PR) is reused as-is; this file only adds a bar-check *after* that guard has already fired.
|
|
33
33
|
|
|
34
34
|
**Engine coverage** (forge#2379, #2889): this subcommand's `command` name (`work-on/remediate`) and completion marker (`FORGE:REMEDIATION:COMPLETE`, including the `**Re-gate outcome**` field Phase M8 posts below) are registered in the headless engine's phase table — `RESERVED_TYPES.REMEDIATION` in `packages/protocol/src/types.js`, `remediate` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and a matching `remediate` entry in `bin/engine/phases.mjs`'s `PHASES` array. A blocked review is committed with `terminalReason: "needs-human"`, then the engine continues directly into remediation; the divergence guard permits this specific handoff while keeping all other `needs-human` states paused.
|
|
35
35
|
|
|
@@ -38,7 +38,7 @@ background: false
|
|
|
38
38
|
## Inputs
|
|
39
39
|
|
|
40
40
|
Parse from $ARGUMENTS:
|
|
41
|
-
- `{PR_NUMBER}` — PR number to remediate (required, first positional arg). This is the `needs-human`-gated PR itself, NOT the linked issue number.
|
|
41
|
+
- `{PR_NUMBER}` — PR number to remediate (required, first positional arg). This is the `needs-human`-gated or `workflow:remediating` PR itself, NOT the linked issue number.
|
|
42
42
|
- `--issue {ISSUE_NUMBER}` — linked issue number (optional). If absent, resolved in Phase M0 from the PR body's `Closes #N` reference.
|
|
43
43
|
- `--repo {GH_REPO}` — GitHub repo (resolved from `forge.yaml → project` if omitted)
|
|
44
44
|
- `--gh-flag {GH_FLAG}` — gh CLI repo flag
|
|
@@ -79,7 +79,12 @@ ISSUE_STATE=$(gh issue view {ISSUE_NUMBER} {GH_FLAG} --json labels,state,body,mi
|
|
|
79
79
|
ISSUE_LABELS=$(echo "$ISSUE_STATE" | jq -r '[.labels[].name] | join(",")')
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
- If `needs-human` is
|
|
82
|
+
- If neither `needs-human` nor `workflow:remediating` is among `ISSUE_LABELS` → EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker: "issue #{ISSUE_NUMBER} is neither `needs-human` nor `workflow:remediating` — remediation mode only targets PRs in one of those two states; use the normal `/work-on {ISSUE_NUMBER}` resume path instead." This keeps blast radius scoped to exactly the gap this mode fills — it is not a general-purpose re-review trigger. `workflow:remediating` is the non-human state review sets when it hands an autonomous ci-gate / in-pr-fix / base-sync fix to this phase (forge#3541); a bare `needs-human` (older runs, genuine escalations being re-driven) is still accepted. <!-- Added: forge#3541 -->
|
|
83
|
+
- **Stranded-state rule**: `workflow:remediating` must never outlive this run. Every exit that returns `BLOCKED` or `UNFIXABLE` (including exits before Phase M1) removes `workflow:remediating` and adds `needs-human`, because at that point a human really is needed:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" --remove-label "workflow:remediating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
87
|
+
```
|
|
83
88
|
|
|
84
89
|
**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
90
|
|
|
@@ -87,11 +92,16 @@ ISSUE_LABELS=$(echo "$ISSUE_STATE" | jq -r '[.labels[].name] | join(",")')
|
|
|
87
92
|
# Trust predicate: ONE shared copy (scripts/trusted-comments.sh). Markers are matched as the comment's LEADING
|
|
88
93
|
# text (^ anchors at the start of the body, not of a line), never with a bare contains(): a review comment or
|
|
89
94
|
# an INPR_FIX work order that merely QUOTES FORGE:REMEDIATION:COMPLETE must not read as a remediation trail (forge#3412).
|
|
95
|
+
# 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.
|
|
90
96
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
97
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
98
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
99
|
+
[ -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)"
|
|
100
|
+
done
|
|
91
101
|
TRUSTED_SCRIPT=""
|
|
92
|
-
|
|
102
|
+
while IFS= read -r _c; do
|
|
93
103
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
94
|
-
done
|
|
104
|
+
done <<< "$_tc"
|
|
95
105
|
PR_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/{PR_NUMBER}/comments 2>/dev/null) || PR_COMMENTS=""
|
|
96
106
|
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$PR_COMMENTS" ]; then
|
|
97
107
|
# Real trail comments (M5 interim, M8 final) START with <!-- FORGE:REMEDIATION -->; M8 also carries <!-- FORGE:REMEDIATION:COMPLETE -->.
|
|
@@ -104,6 +114,21 @@ fi
|
|
|
104
114
|
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
115
|
```
|
|
106
116
|
|
|
117
|
+
**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:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$PR_COMMENTS" ]; then
|
|
121
|
+
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 "")
|
|
122
|
+
ISSUE_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments 2>/dev/null) || ISSUE_COMMENTS=""
|
|
123
|
+
BASESYNC_BOUND_N=$(printf '%s' "$ISSUE_COMMENTS" | bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:BASESYNC_REMEDIATION: pr={PR_NUMBER} -->' 2>/dev/null || echo "")
|
|
124
|
+
# A base-sync handoff (bound marker present) with no completed base-sync round yet is a fresh attempt for ITS kind,
|
|
125
|
+
# whatever earlier ci-gate / inpr-fix trails exist. Fail closed: an unreadable count keeps the kind-agnostic guard.
|
|
126
|
+
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
|
|
127
|
+
fi
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Completed trails of other kinds are left in place (never deleted); only the interrupted-partial cleanup below deletes anything.
|
|
131
|
+
|
|
107
132
|
- 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`.
|
|
108
133
|
- 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.
|
|
109
134
|
- 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:
|
|
@@ -134,20 +159,20 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
134
159
|
```
|
|
135
160
|
|
|
136
161
|
**Classify into FIXABLE vs. UNFIXABLE**:
|
|
137
|
-
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a
|
|
162
|
+
- **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).
|
|
138
163
|
- **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.
|
|
139
164
|
|
|
140
|
-
**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
|
|
165
|
+
**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` (add it and remove `workflow:remediating` if the issue entered as `workflow:remediating`), and return `REMEDIATE_RESULT: status: UNFIXABLE`. This satisfies AC5 — "genuinely-blocked PRs still terminate at `needs-human`."
|
|
141
166
|
|
|
142
|
-
**If at least one FIXABLE item exists**: transition the issue out of its terminal gate before proceeding to Phase M2. `needs-human` represents the prior review result, not an active automated remediation run; retaining it would make the dispatcher and recovery paths stop while remediation is in progress. Keep exactly one active workflow state:
|
|
167
|
+
**If at least one FIXABLE item exists**: transition the issue out of its terminal gate before proceeding to Phase M2. `needs-human` (or the `workflow:remediating` handoff state) represents the prior review result, not an active automated remediation run; retaining it would make the dispatcher and recovery paths stop while remediation is in progress. Keep exactly one active workflow state:
|
|
143
168
|
|
|
144
169
|
```bash
|
|
145
170
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
146
|
-
echo "DRY_RUN: would replace needs-human with workflow:in-review on issue #{ISSUE_NUMBER}"
|
|
171
|
+
echo "DRY_RUN: would replace needs-human / workflow:remediating with workflow:in-review on issue #{ISSUE_NUMBER}"
|
|
147
172
|
else
|
|
148
173
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} \
|
|
149
174
|
--add-label "workflow:in-review" \
|
|
150
|
-
--remove-label "needs-human" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
175
|
+
--remove-label "needs-human,workflow:remediating" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
151
176
|
fi
|
|
152
177
|
```
|
|
153
178
|
|
|
@@ -157,7 +182,7 @@ Do not perform this transition for an UNFIXABLE policy escalation. Any later qua
|
|
|
157
182
|
|
|
158
183
|
## Phase M2: Checkout the PR's Existing Branch
|
|
159
184
|
|
|
160
|
-
Remediation always fixes forward on top of the PR's existing head commit — never rebase
|
|
185
|
+
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`.
|
|
161
186
|
|
|
162
187
|
```bash
|
|
163
188
|
cd {REPO_PATH}
|
|
@@ -182,7 +207,39 @@ For each FIXABLE item from Phase M1: read the affected file(s) in `{WORKTREE_PAT
|
|
|
182
207
|
|
|
183
208
|
**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 -->
|
|
184
209
|
|
|
185
|
-
**
|
|
210
|
+
**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.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
cd {WORKTREE_PATH}
|
|
214
|
+
CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
215
|
+
case "{HEAD_BRANCH}" in
|
|
216
|
+
"{PR_BASE}"|main|master|staging|milestone/*) echo "refusing base sync: {HEAD_BRANCH} is a shared branch"; BASESYNC_REFUSED=true ;;
|
|
217
|
+
esac
|
|
218
|
+
[ "$CUR_BRANCH" = "{HEAD_BRANCH}" ] || BASESYNC_REFUSED=true
|
|
219
|
+
if [ "${BASESYNC_REFUSED:-false}" != "true" ]; then
|
|
220
|
+
git fetch origin {PR_BASE} # allowlist:check-command-side-effects
|
|
221
|
+
git merge origin/{PR_BASE} --no-edit # allowlist:check-command-side-effects
|
|
222
|
+
BASESYNC_RAN=true
|
|
223
|
+
fi
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- Clean merge: nothing more to resolve; the merge commit is the sync.
|
|
227
|
+
- 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).
|
|
228
|
+
- **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:
|
|
229
|
+
```bash
|
|
230
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
231
|
+
CONFLICT_FILES=$(git diff --name-only --diff-filter=U)
|
|
232
|
+
git merge --abort # allowlist:check-command-side-effects
|
|
233
|
+
run gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "<!-- FORGE:BASESYNC_FAILED -->
|
|
234
|
+
Base sync of PR #{PR_NUMBER} (\`origin/{PR_BASE}\` into \`{HEAD_BRANCH}\`) could not be resolved with confidence. Conflicting files:
|
|
235
|
+
|
|
236
|
+
\`\`\`
|
|
237
|
+
${CONFLICT_FILES}
|
|
238
|
+
\`\`\`" # allowlist:check-command-side-effects
|
|
239
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
240
|
+
```
|
|
241
|
+
EXIT `REMEDIATE_RESULT: status: BLOCKED` with blocker "base-sync: unresolvable conflicts". Only that issue is parked.
|
|
242
|
+
- 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).
|
|
186
243
|
|
|
187
244
|
**Quality Gate** (same loop as Phase 3G, max 3 iterations):
|
|
188
245
|
```
|
|
@@ -201,14 +258,42 @@ If still failing after 3 iterations: post a comment, re-affirm `needs-human`, EX
|
|
|
201
258
|
|
|
202
259
|
## Phase M4: Commit, Push, and Close Addressed Findings
|
|
203
260
|
|
|
261
|
+
**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.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# <Script resolution block from work-on/review.md, verbatim>
|
|
265
|
+
cd {WORKTREE_PATH}
|
|
266
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
267
|
+
git fetch origin {PR_BASE} >/dev/null 2>&1 || true
|
|
268
|
+
RESOLUTION=$(resolve_script 'check-branch-ancestry'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
269
|
+
if [ "$TIER" = "prose" ]; then
|
|
270
|
+
MERGE_COMMITS="check-branch-ancestry script not resolvable (fail closed)"; ANCESTRY_RC=2
|
|
271
|
+
else
|
|
272
|
+
MERGE_COMMITS=$(bash "$SCRIPT_PATH" {HEAD_BRANCH} origin/{PR_BASE} 2>&1); ANCESTRY_RC=$?
|
|
273
|
+
fi
|
|
274
|
+
if [ "$ANCESTRY_RC" -ne 0 ]; then
|
|
275
|
+
run gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "## Pre-Push Ancestry Guard Failed
|
|
276
|
+
|
|
277
|
+
Branch \`{HEAD_BRANCH}\` has merge commits from outside \`{PR_BASE}\`, or ancestry could not be verified (rc=${ANCESTRY_RC}). Not pushing.
|
|
278
|
+
|
|
279
|
+
\`\`\`
|
|
280
|
+
${MERGE_COMMITS}
|
|
281
|
+
\`\`\`" # allowlist:check-command-side-effects
|
|
282
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
283
|
+
# EXIT REMEDIATE_RESULT: status: BLOCKED (blocker: pre-push ancestry guard failed) - do not push
|
|
284
|
+
fi
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Then commit and push. A base-sync merge commit already exists from Phase M3; commit only additional quality-gate or fix edits on top.
|
|
288
|
+
|
|
204
289
|
```bash
|
|
205
290
|
cd {WORKTREE_PATH}
|
|
206
291
|
git add -u
|
|
207
|
-
git commit -s -m "fix(remediate): {description} (#{ISSUE_NUMBER})"
|
|
208
|
-
git push origin {HEAD_BRANCH}
|
|
292
|
+
git diff --cached --quiet || git commit -s -m "fix(remediate): {description} (#{ISSUE_NUMBER})"
|
|
293
|
+
git push origin {HEAD_BRANCH} # allowlist:check-command-side-effects
|
|
209
294
|
```
|
|
210
295
|
|
|
211
|
-
If push fails, retry with
|
|
296
|
+
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.
|
|
212
297
|
|
|
213
298
|
**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:
|
|
214
299
|
```bash
|
|
@@ -289,6 +374,8 @@ If the retained `REVIEW_RESULT` is `PHASE_TRAIL_FAILED` (forge#3102): the gate r
|
|
|
289
374
|
|
|
290
375
|
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.
|
|
291
376
|
|
|
377
|
+
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".
|
|
378
|
+
|
|
292
379
|
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:
|
|
293
380
|
|
|
294
381
|
- **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.
|
|
@@ -414,7 +501,7 @@ if [ "$MERGE_STATE" = "MERGED" ]; then
|
|
|
414
501
|
adaptive|universal) bash "$SCRIPT_PATH" {ISSUE_NUMBER} {GH_FLAG} merged ;;
|
|
415
502
|
prose)
|
|
416
503
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:merged" \
|
|
417
|
-
--remove-label "workflow:awaiting-merge,needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
504
|
+
--remove-label "workflow:awaiting-merge,needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
418
505
|
;;
|
|
419
506
|
esac
|
|
420
507
|
RE_GATE_OUTCOME="AUTO-LANDED"
|
|
@@ -455,6 +542,9 @@ esac
|
|
|
455
542
|
# forge#3413: embed the pushed head so the orchestrator scopes a REREVIEW-REQUIRED trail to the CURRENT head only.
|
|
456
543
|
REMEDIATED_HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || true)
|
|
457
544
|
|
|
545
|
+
# forge#3496: mark a base-sync round so Phase M0 scopes the single-attempt guard per kind.
|
|
546
|
+
BASESYNC_LINE=""; [ "${BASESYNC_RAN:-false}" = "true" ] && BASESYNC_LINE="**Base sync**: ran"
|
|
547
|
+
|
|
458
548
|
REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
459
549
|
## Remediation Complete for PR #{PR_NUMBER}
|
|
460
550
|
|
|
@@ -463,7 +553,7 @@ REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
|
463
553
|
**Auto-land bar**: ${AUTO_LAND_BAR_TEXT}
|
|
464
554
|
**Re-gate outcome**: ${RE_GATE_OUTCOME} ${OUTCOME_DETAIL}
|
|
465
555
|
**Head**: ${REMEDIATED_HEAD_SHA:-unknown}
|
|
466
|
-
|
|
556
|
+
${BASESYNC_LINE}
|
|
467
557
|
<!-- FORGE:REMEDIATION:COMPLETE -->"
|
|
468
558
|
|
|
469
559
|
gh pr comment {PR_NUMBER} {GH_FLAG} --body "$REMEDIATION_BODY"
|