forgedock 1.12.0 → 1.13.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 +2 -1
- package/bin/engine/phases.mjs +349 -90
- package/bin/engine/runlog.mjs +25 -2
- package/bin/engine-cli.mjs +4 -1
- package/bin/engine.mjs +72 -19
- package/bin/hooks/interactive-engine.mjs +135 -209
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +10 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +258 -23
- package/bin/watch-utils.mjs +1 -0
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +8 -5
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +14 -19
- package/commands/orchestrate/phase-4-execution.md +80 -53
- package/commands/orchestrate/phase-6-report.md +6 -0
- package/commands/orchestrate/safety.md +1 -1
- package/commands/pipeline-health/phase-2-metrics.md +39 -0
- package/commands/pipeline-health/phase-5-report.md +1 -0
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +2 -2
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +3 -3
- package/commands/review-pr-agents/protocols.md +15 -6
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +12 -12
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +394 -53
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +106 -7
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +137 -25
- package/commands/work-on/review.md +25 -9
- package/commands/work-on.md +15 -15
- package/docs/CONFIG.md +29 -0
- package/docs/RELEASE-NOTES.md +45 -0
- package/docs/spec/forge-protocol-v1.md +22 -1
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +20 -5
- package/package.json +2 -1
- package/packages/protocol/src/phases.js +21 -0
- package/scripts/check-blast-radius.sh +164 -0
- package/scripts/check-blast-radius.test.sh +183 -0
- package/scripts/check-contract-scope.sh +137 -0
- package/scripts/check-contract-scope.test.sh +83 -0
- package/scripts/check-spec-bash.sh +60 -3
- package/scripts/check-spec-bash.test.sh +56 -0
- package/scripts/check-spec-markers.sh +29 -0
- package/scripts/check-test-wiring.sh +57 -0
- package/scripts/check-test-wiring.test.sh +70 -0
- package/scripts/classify-finding.sh +72 -8
- package/scripts/classify-finding.test.sh +77 -8
- package/scripts/close-dossier-no-main-commit.test.sh +12 -12
- package/scripts/diff-size.test.sh +3 -2
- package/scripts/doctor-pipeline-state.sh +35 -0
- package/scripts/forge-annotation.sh +3 -0
- package/scripts/forge-root.test.sh +103 -13
- package/scripts/forge-run.sh +2 -2
- package/scripts/recovery-claim-live.test.sh +10 -7
- package/scripts/remediate-basesync.test.sh +103 -0
- package/scripts/review-delta.sh +206 -0
- package/scripts/review-delta.test.sh +213 -0
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/review-pr-gate2.test.sh +84 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
- package/scripts/run-shell-tests.sh +115 -0
- package/scripts/select-fix-targets.sh +17 -5
- package/scripts/transition-label.sh +3 -1
|
@@ -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,7 +114,7 @@ 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
|
|
|
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
|
|
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 router handoff is bounded to one per PR (the marker count in `work-on/review.md` R4, unchanged) and this check; inside remediation the sync is bounded per distinct base head and hard-capped at `BASESYNC_MAX_ROUNDS` pushed sync merges on the branch, counted from git rather than shell state (see Phase M3 and M6):
|
|
108
118
|
|
|
109
119
|
```bash
|
|
110
120
|
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$PR_COMMENTS" ]; then
|
|
@@ -152,17 +162,17 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
152
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).
|
|
153
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.
|
|
154
164
|
|
|
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
|
|
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`."
|
|
156
166
|
|
|
157
|
-
**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:
|
|
158
168
|
|
|
159
169
|
```bash
|
|
160
170
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
161
|
-
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}"
|
|
162
172
|
else
|
|
163
173
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} \
|
|
164
174
|
--add-label "workflow:in-review" \
|
|
165
|
-
--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 -->
|
|
166
176
|
fi
|
|
167
177
|
```
|
|
168
178
|
|
|
@@ -193,12 +203,14 @@ If the worktree/branch checkout fails for any reason (branch deleted, force-push
|
|
|
193
203
|
|
|
194
204
|
## Phase M3: Apply Fixes
|
|
195
205
|
|
|
196
|
-
For each FIXABLE item from Phase M1: read the affected file(s) in `{WORKTREE_PATH}` before editing (never assume current state), apply the fix. Follow the same implementation discipline as `work-on/build/implement.md` I3 (cross-lane import guard, library-callback verification, deliverable-type consistency, no unrequested scope) — this file does not restate those rules, it inherits them.
|
|
206
|
+
For each FIXABLE item from Phase M1 (the base-sync item, when present, goes first; see below): read the affected file(s) in `{WORKTREE_PATH}` before editing (never assume current state), apply the fix. Follow the same implementation discipline as `work-on/build/implement.md` I3 (cross-lane import guard, library-callback verification, deliverable-type consistency, no unrequested scope) — this file does not restate those rules, it inherits them.
|
|
197
207
|
|
|
198
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 -->
|
|
199
209
|
|
|
200
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.
|
|
201
211
|
|
|
212
|
+
**Ordering**: the base sync runs first in M3, right after M2's clean checkout and before any other FIXABLE edit. `git merge` needs a clean tree, and merging over uncommitted edits is refused (rc=1, no merge in progress) or leaves a merge that cannot be safely aborted. Apply the other FIXABLE items only after the sync has concluded (clean merge, or conflicts resolved and committed). Runs with no base-conflict block reason skip this step and are unchanged. <!-- Added: forge#3515 -->
|
|
213
|
+
|
|
202
214
|
```bash
|
|
203
215
|
cd {WORKTREE_PATH}
|
|
204
216
|
CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
@@ -206,20 +218,48 @@ case "{HEAD_BRANCH}" in
|
|
|
206
218
|
"{PR_BASE}"|main|master|staging|milestone/*) echo "refusing base sync: {HEAD_BRANCH} is a shared branch"; BASESYNC_REFUSED=true ;;
|
|
207
219
|
esac
|
|
208
220
|
[ "$CUR_BRANCH" = "{HEAD_BRANCH}" ] || BASESYNC_REFUSED=true
|
|
221
|
+
# Clean-tree precondition: never merge over pending edits.
|
|
222
|
+
if ! { git diff --quiet && git diff --cached --quiet; }; then
|
|
223
|
+
echo "refusing base sync: working tree has uncommitted changes"; BASESYNC_REFUSED=true
|
|
224
|
+
fi
|
|
225
|
+
git fetch origin {PR_BASE} {HEAD_BRANCH} # allowlist:check-command-side-effects
|
|
226
|
+
# Durable sync state (forge#3517): every block runs in a fresh shell, so never carry rounds or the base head in shell vars.
|
|
227
|
+
# Rounds = merge commits on the PUSHED branch's first-parent chain that are not on the base (a round counts only once pushed;
|
|
228
|
+
# a manual merge counts too, fail closed). Recorded base head = the newest such merge's second parent, when that is a base commit.
|
|
229
|
+
# An unreadable count is the cap.
|
|
230
|
+
BASESYNC_MAX_ROUNDS=2 # hard cap on sync rounds per PR (M3 plus M6 re-syncs)
|
|
231
|
+
BASESYNC_ROUNDS=$(git rev-list --first-parent --merges --count origin/{PR_BASE}..origin/{HEAD_BRANCH} 2>/dev/null) || BASESYNC_ROUNDS=$BASESYNC_MAX_ROUNDS
|
|
232
|
+
_lm=$(git rev-list --first-parent --merges -1 origin/{PR_BASE}..origin/{HEAD_BRANCH} 2>/dev/null || true); _p2=""
|
|
233
|
+
[ -n "$_lm" ] && _p2=$(git rev-parse -q --verify "$_lm^2" 2>/dev/null) && git merge-base --is-ancestor "$_p2" origin/{PR_BASE} 2>/dev/null || _p2=""
|
|
234
|
+
BASESYNC_BASE_SHA="$_p2"
|
|
235
|
+
if [ "$BASESYNC_ROUNDS" -ge "$BASESYNC_MAX_ROUNDS" ]; then
|
|
236
|
+
echo "refusing base sync: ${BASESYNC_ROUNDS} sync round(s) already on {HEAD_BRANCH} (cap ${BASESYNC_MAX_ROUNDS})"; BASESYNC_REFUSED=true
|
|
237
|
+
fi
|
|
209
238
|
if [ "${BASESYNC_REFUSED:-false}" != "true" ]; then
|
|
210
|
-
git
|
|
211
|
-
git merge origin/{PR_BASE} --no-edit # allowlist:check-command-side-effects
|
|
212
|
-
|
|
239
|
+
BASESYNC_BASE_SHA=$(git rev-parse origin/{PR_BASE}) # the base head this round merges; persisted by M8 as `Base sync**: ran (base=<sha>)`
|
|
240
|
+
MERGE_OUT=$(git merge origin/{PR_BASE} --no-edit 2>&1); MERGE_RC=$? # allowlist:check-command-side-effects
|
|
241
|
+
echo "$MERGE_OUT"
|
|
242
|
+
# rc alone cannot tell a conflict from a refusal (both exit 1); MERGE_HEAD can.
|
|
243
|
+
if [ "$MERGE_RC" -eq 0 ]; then
|
|
244
|
+
BASESYNC_RAN=true
|
|
245
|
+
elif git rev-parse -q --verify MERGE_HEAD >/dev/null; then
|
|
246
|
+
BASESYNC_CONFLICTED=true # merge in progress with conflicts: resolve or park below
|
|
247
|
+
else
|
|
248
|
+
BASESYNC_MERGE_REFUSED=true # rc!=0 and no MERGE_HEAD: refused, nothing was merged
|
|
249
|
+
fi
|
|
213
250
|
fi
|
|
214
251
|
```
|
|
215
252
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
-
|
|
253
|
+
Exactly one outcome applies. `BASESYNC_RAN=true` is set only on a clean merge (or after a conflicted merge is concluded below); a refused merge is never treated as synced.
|
|
254
|
+
|
|
255
|
+
- Clean merge (`MERGE_RC=0`): nothing more to resolve; the merge commit is the sync.
|
|
256
|
+
- Conflicts (`MERGE_RC` nonzero, `MERGE_HEAD` present): 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), then set `BASESYNC_RAN=true`.
|
|
257
|
+
- **Unresolvable with confidence** (a hunk whose intent on either side you cannot reconcile, or the branch/tree is refused above): capture the file list first, then abort only if a merge is in progress, and park:
|
|
219
258
|
```bash
|
|
220
259
|
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
221
260
|
CONFLICT_FILES=$(git diff --name-only --diff-filter=U)
|
|
222
|
-
|
|
261
|
+
[ -n "$CONFLICT_FILES" ] || CONFLICT_FILES="(no conflicted files reported)"
|
|
262
|
+
if git rev-parse -q --verify MERGE_HEAD >/dev/null; then git merge --abort; fi # allowlist:check-command-side-effects
|
|
223
263
|
run gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "<!-- FORGE:BASESYNC_FAILED -->
|
|
224
264
|
Base sync of PR #{PR_NUMBER} (\`origin/{PR_BASE}\` into \`{HEAD_BRANCH}\`) could not be resolved with confidence. Conflicting files:
|
|
225
265
|
|
|
@@ -229,6 +269,19 @@ fi
|
|
|
229
269
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
230
270
|
```
|
|
231
271
|
EXIT `REMEDIATE_RESULT: status: BLOCKED` with blocker "base-sync: unresolvable conflicts". Only that issue is parked.
|
|
272
|
+
- **Refused merge** (`MERGE_RC` nonzero and no `MERGE_HEAD`, i.e. `BASESYNC_MERGE_REFUSED=true`): git declined to start the merge (for example local changes would be overwritten). This is not a conflict and not a sync. There is nothing to abort. Park with the git output:
|
|
273
|
+
```bash
|
|
274
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
275
|
+
run gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "<!-- FORGE:BASESYNC_FAILED -->
|
|
276
|
+
Base sync of PR #{PR_NUMBER} (\`origin/{PR_BASE}\` into \`{HEAD_BRANCH}\`) was refused by git (rc=${MERGE_RC}); no merge was started. Git output:
|
|
277
|
+
|
|
278
|
+
\`\`\`
|
|
279
|
+
${MERGE_OUT}
|
|
280
|
+
\`\`\`" # allowlist:check-command-side-effects
|
|
281
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
282
|
+
```
|
|
283
|
+
EXIT `REMEDIATE_RESULT: status: BLOCKED` with blocker "base-sync: merge refused (rc=N)" (N = `MERGE_RC`). Never fall through to M4 as synced.
|
|
284
|
+
- **Re-entry (per distinct base head)**: Phase M3's sync is re-enterable from Phase M6, but only when the base advanced past `BASESYNC_BASE_SHA` and `BASESYNC_ROUNDS` is below `BASESYNC_MAX_ROUNDS`. A re-sync round runs this same block on the new head with the same guards (shared-branch refusal, clean tree, merge-only, no force-push, resolve-or-park, `FORGE:BASESYNC_FAILED`). The block derives `BASESYNC_ROUNDS` and `BASESYNC_BASE_SHA` from git on every entry (pushed sync merges on the branch), never from shell state, and refuses at the cap; an unreadable count is the cap. `BASESYNC_RAN=true` is still set only on a clean or concluded merge.
|
|
232
285
|
- 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).
|
|
233
286
|
|
|
234
287
|
**Quality Gate** (same loop as Phase 3G, max 3 iterations):
|
|
@@ -274,7 +327,7 @@ ${MERGE_COMMITS}
|
|
|
274
327
|
fi
|
|
275
328
|
```
|
|
276
329
|
|
|
277
|
-
Then commit and push.
|
|
330
|
+
Then commit and push. When a base sync ran, it ran first in Phase M3 on a clean tree and its merge commit already exists; commit only the additional quality-gate or fix edits on top of it.
|
|
278
331
|
|
|
279
332
|
```bash
|
|
280
333
|
cd {WORKTREE_PATH}
|
|
@@ -335,6 +388,41 @@ Note the marker is `<!-- FORGE:REMEDIATION -->` with **no** `:COMPLETE` suffix y
|
|
|
335
388
|
|
|
336
389
|
If no dispatch tool resolves, a missing tool is not a human decision, so do NOT invoke `review-pr`, do NOT add `needs-human`, and leave `workflow:in-review` in place. Set `RE_GATE_OUTCOME="REREVIEW-REQUIRED"`, skip Phase M7, go to Phase M8 (which posts `FORGE:REMEDIATION:COMPLETE` with a `REREVIEW-REQUIRED` re-gate line), and return `REMEDIATE_RESULT: status: REREVIEW_REQUIRED`. The caller re-runs `/review-pr {PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE}` from a session that has dispatch. The caller owns the terminal fallback if that re-review cannot run (`commands/work-on.md` Phase 0A.1); remediation itself stays single-attempt and adds no `needs-human` here. If a dispatch tool resolves, continue below.
|
|
337
390
|
|
|
391
|
+
**Pre-review freshness pass** (not in dry-run): the quality gate, push and trail took minutes, so re-fetch right before the re-review. The block derives its state from git, as M3 does (a fresh shell has no `BASESYNC_*` vars, forge#3612): it runs only when the pushed branch already carries a sync merge (`BASESYNC_BASE_SHA` non-empty), the base advanced past it, and the round count is below the cap.
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
cd {WORKTREE_PATH}
|
|
395
|
+
git fetch origin {PR_BASE} {HEAD_BRANCH} # allowlist:check-command-side-effects
|
|
396
|
+
BASESYNC_MAX_ROUNDS=2
|
|
397
|
+
BASESYNC_ROUNDS=$(git rev-list --first-parent --merges --count origin/{PR_BASE}..origin/{HEAD_BRANCH} 2>/dev/null) || BASESYNC_ROUNDS=$BASESYNC_MAX_ROUNDS
|
|
398
|
+
_lm=$(git rev-list --first-parent --merges -1 origin/{PR_BASE}..origin/{HEAD_BRANCH} 2>/dev/null || true); _p2=""
|
|
399
|
+
[ -n "$_lm" ] && _p2=$(git rev-parse -q --verify "$_lm^2" 2>/dev/null) && git merge-base --is-ancestor "$_p2" origin/{PR_BASE} 2>/dev/null || _p2=""
|
|
400
|
+
BASESYNC_BASE_SHA="$_p2"
|
|
401
|
+
NOW_BASE_SHA=$(git rev-parse origin/{PR_BASE})
|
|
402
|
+
FRESH_MERGED=false
|
|
403
|
+
if [ -n "$BASESYNC_BASE_SHA" ] && [ "$NOW_BASE_SHA" != "$BASESYNC_BASE_SHA" ] && [ "$BASESYNC_ROUNDS" -lt "$BASESYNC_MAX_ROUNDS" ] \
|
|
404
|
+
&& [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/{HEAD_BRANCH})" ] && git diff --quiet && git diff --cached --quiet; then
|
|
405
|
+
echo "base advanced since the last sync (${BASESYNC_BASE_SHA} -> ${NOW_BASE_SHA}); re-merging once"
|
|
406
|
+
if git merge origin/{PR_BASE} --no-edit; then # allowlist:check-command-side-effects
|
|
407
|
+
FRESH_MERGED=true
|
|
408
|
+
elif git rev-parse -q --verify MERGE_HEAD >/dev/null; then git merge --abort; fi # allowlist:check-command-side-effects
|
|
409
|
+
fi
|
|
410
|
+
echo "FRESH_MERGED=$FRESH_MERGED"
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Clean merges only: a conflicted or refused freshness merge is aborted (MERGE_HEAD-guarded) and left to the `base-conflict` handling below. A clean freshness merge (`FRESH_MERGED=true`) is local and unpushed, and it goes through the same gates as any other push before it leaves the worktree:
|
|
414
|
+
|
|
415
|
+
1. Re-run the Phase M3 quality-gate loop on the merged tree.
|
|
416
|
+
2. Run the Phase M4 pre-push ancestry guard block verbatim.
|
|
417
|
+
3. Push with a checked rc:
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
cd {WORKTREE_PATH}
|
|
421
|
+
if git push origin HEAD:{HEAD_BRANCH}; then echo "FRESH_PUSHED=true"; else echo "FRESH_PUSHED=false"; fi # allowlist:check-command-side-effects
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
If the gate does not pass, the ancestry guard fails, or the push fails, drop the unpushed merge (`git reset --hard origin/{HEAD_BRANCH}`; local only, the pushed head is untouched, never a force-push) and continue to the re-review on the pushed head. The `base-conflict` handling below then decides. The round counts against `BASESYNC_MAX_ROUNDS` only once pushed, because the count is read from `origin/{HEAD_BRANCH}`. An ancestry-guard failure still parks at `needs-human` as M4 specifies.
|
|
425
|
+
|
|
338
426
|
```
|
|
339
427
|
if DRY_RUN=true:
|
|
340
428
|
record "Would invoke review-pr --auto-merge for PR #{PR_NUMBER}; skipped (dry-run)."
|
|
@@ -364,7 +452,24 @@ If the retained `REVIEW_RESULT` is `PHASE_TRAIL_FAILED` (forge#3102): the gate r
|
|
|
364
452
|
|
|
365
453
|
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.
|
|
366
454
|
|
|
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.
|
|
455
|
+
If the retained `REVIEW_RESULT` is `BLOCKED` with blocker "base-conflict" (forge#3496): either the base moved again during the re-review, or the sync did not clear the conflict. Tell the two apart by base head, not by attempt count:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
cd {WORKTREE_PATH}
|
|
459
|
+
git fetch origin {PR_BASE} {HEAD_BRANCH} # allowlist:check-command-side-effects
|
|
460
|
+
CUR_BASE_SHA=$(git rev-parse origin/{PR_BASE})
|
|
461
|
+
# Recorded head and rounds come from the pushed branch's sync merges (same derivation as M3), never from shell vars or
|
|
462
|
+
# the M8 trail line, which is written after this phase (forge#3517). No recorded head is treated as advanced, but only below the cap.
|
|
463
|
+
BASESYNC_MAX_ROUNDS=2
|
|
464
|
+
BASESYNC_ROUNDS=$(git rev-list --first-parent --merges --count origin/{PR_BASE}..origin/{HEAD_BRANCH} 2>/dev/null) || BASESYNC_ROUNDS=$BASESYNC_MAX_ROUNDS
|
|
465
|
+
_lm=$(git rev-list --first-parent --merges -1 origin/{PR_BASE}..origin/{HEAD_BRANCH} 2>/dev/null || true); _p2=""
|
|
466
|
+
[ -n "$_lm" ] && _p2=$(git rev-parse -q --verify "$_lm^2" 2>/dev/null) && git merge-base --is-ancestor "$_p2" origin/{PR_BASE} 2>/dev/null || _p2=""
|
|
467
|
+
BASESYNC_BASE_SHA="$_p2"
|
|
468
|
+
echo "recorded=${BASESYNC_BASE_SHA:-none} current=${CUR_BASE_SHA} rounds=${BASESYNC_ROUNDS}/${BASESYNC_MAX_ROUNDS}"
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
- **Base advanced** (`CUR_BASE_SHA` differs from the recorded `BASESYNC_BASE_SHA`) and `BASESYNC_ROUNDS` is below `BASESYNC_MAX_ROUNDS` (2 pushed sync merges on the branch): this is a new conflict a human did not cause. Re-run the Phase M3 base-sync on the new head (same resolve-or-park rules), then the Phase M4 quality gate, ancestry guard and push, the M5 trail, and this Phase M6 once more. The pushed merge raises the derived count and moves the recorded head, so nothing is carried in shell state. Do not post a second `FORGE:REMEDIATION:COMPLETE`; only M8 does, once.
|
|
472
|
+
- **Base unchanged** (the sync did not clear the conflict) or `BASESYNC_ROUNDS` has reached `BASESYNC_MAX_ROUNDS` (a base that never settles): park. Add `needs-human`, post a comment naming the PR, the base SHA(s) tried (recorded and current) 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
473
|
|
|
369
474
|
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:
|
|
370
475
|
|
|
@@ -491,7 +596,7 @@ if [ "$MERGE_STATE" = "MERGED" ]; then
|
|
|
491
596
|
adaptive|universal) bash "$SCRIPT_PATH" {ISSUE_NUMBER} {GH_FLAG} merged ;;
|
|
492
597
|
prose)
|
|
493
598
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:merged" \
|
|
494
|
-
--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
|
|
599
|
+
--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
|
|
495
600
|
;;
|
|
496
601
|
esac
|
|
497
602
|
RE_GATE_OUTCOME="AUTO-LANDED"
|
|
@@ -533,7 +638,12 @@ esac
|
|
|
533
638
|
REMEDIATED_HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || true)
|
|
534
639
|
|
|
535
640
|
# forge#3496: mark a base-sync round so Phase M0 scopes the single-attempt guard per kind.
|
|
536
|
-
|
|
641
|
+
# Persist the merged base head so it survives across blocks: fall back to the merge's second parent when the shell var is unset.
|
|
642
|
+
BASESYNC_LINE=""
|
|
643
|
+
if [ "${BASESYNC_RAN:-false}" = "true" ]; then
|
|
644
|
+
BASESYNC_BASE_SHA="${BASESYNC_BASE_SHA:-$(git -C {WORKTREE_PATH} rev-parse -q --verify 'HEAD^2' 2>/dev/null || true)}"
|
|
645
|
+
BASESYNC_LINE="**Base sync**: ran (base=${BASESYNC_BASE_SHA:-unknown})"
|
|
646
|
+
fi
|
|
537
647
|
|
|
538
648
|
REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
539
649
|
## Remediation Complete for PR #{PR_NUMBER}
|
|
@@ -558,6 +668,8 @@ Skill("{FORGE_SKILL_PREFIX}work-on:close", args="{ISSUE_NUMBER} --repo {GH_REPO}
|
|
|
558
668
|
|
|
559
669
|
`work-on:close` handles project board update, final issue body, parent tracker, trajectory log, and worktree cleanup (including the remediation worktree at `{WORKTREE_PATH}`) — do not duplicate any of that here.
|
|
560
670
|
|
|
671
|
+
**Under the headless engine** (forge#3624): the engine does not trust that this close ran. After `AUTO-LANDED` it reads the issue's real state; if the issue is not CLOSED with `workflow:merged`, the engine runs `work-on/close` itself. Close is idempotent (it skips work already done), so the self-driven close above stays correct for the Agent-spawn path.
|
|
672
|
+
|
|
561
673
|
**If the outcome was `REREVIEW-REQUIRED`** (forge#3240): do not invoke close and do not add `needs-human`. Leave the worktree and `workflow:in-review` in place and return `REMEDIATE_RESULT: status: REREVIEW_REQUIRED`; the caller runs the re-review and, on `AUTO-LANDED`, drives close itself; if the re-review cannot run, the caller applies the terminal fallback (`commands/work-on.md` Phase 0A.1).
|
|
562
674
|
|
|
563
675
|
**If the outcome was `HELD-AWAITING-MERGE`, `RE-ESCALATED`, or `UNFIXABLE`**: leave the worktree in place (a human may need it for manual inspection/merge) and return the structured result below without invoking close. Do not close the issue.
|
|
@@ -578,4 +690,4 @@ REMEDIATE_RESULT:
|
|
|
578
690
|
blocker: {description if status=BLOCKED}
|
|
579
691
|
```
|
|
580
692
|
|
|
581
|
-
**Caller behavior**: this Skill already drives its own close phase when `re_gate_outcome: AUTO-LANDED` (see Phase M8) — the caller does not need to invoke close itself. For `re_gate_outcome: REREVIEW-REQUIRED` (`status: REREVIEW_REQUIRED`, forge#3240) the fix is pushed but no review ran because this session has no sub-agent dispatch tool: the caller MUST run `review-pr {PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE}` from a session that has dispatch (never inline) and then drive close itself on a merge. For every other `re_gate_outcome`, this result is terminal for the current invocation: the issue is left at `needs-human` or `workflow:awaiting-merge`, both already recognized as terminal states in the Universal Phase Dispatcher (see `work-on.md`). Whether invoked standalone (`/work-on <pr> --remediate`) or via the orchestrator's item 6.4 dispatch, no further action is required from the caller.
|
|
693
|
+
**Caller behavior**: this Skill already drives its own close phase when `re_gate_outcome: AUTO-LANDED` (see Phase M8) — the caller does not need to invoke close itself (under the headless engine, the engine verifies the issue is CLOSED + `workflow:merged` and runs close if it is missing). For `re_gate_outcome: REREVIEW-REQUIRED` (`status: REREVIEW_REQUIRED`, forge#3240) the fix is pushed but no review ran because this session has no sub-agent dispatch tool: the caller MUST run `review-pr {PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE}` from a session that has dispatch (never inline) and then drive close itself on a merge. For every other `re_gate_outcome`, this result is terminal for the current invocation: the issue is left at `needs-human` or `workflow:awaiting-merge`, both already recognized as terminal states in the Universal Phase Dispatcher (see `work-on.md`). Whether invoked standalone (`/work-on <pr> --remediate`) or via the orchestrator's item 6.4 dispatch, no further action is required from the caller.
|
|
@@ -474,7 +474,7 @@ Use the existing PR number and continue.
|
|
|
474
474
|
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
475
475
|
case "$TIER" in
|
|
476
476
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
|
|
477
|
-
prose) gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" --remove-label "workflow:investigating" --remove-label "workflow:ready-to-build" --remove-label "workflow:building" --remove-label "workflow:awaiting-merge" --remove-label "workflow:merged" --remove-label "workflow:invalid" --remove-label "workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects ;;
|
|
477
|
+
prose) gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" --remove-label "workflow:investigating" --remove-label "workflow:ready-to-build" --remove-label "workflow:building" --remove-label "workflow:awaiting-merge" --remove-label "workflow:remediating" --remove-label "workflow:merged" --remove-label "workflow:invalid" --remove-label "workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects ;;
|
|
478
478
|
esac
|
|
479
479
|
```
|
|
480
480
|
|
|
@@ -566,14 +566,25 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
566
566
|
|
|
567
567
|
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "stale review" (a commit landed on the PR after the verdict, or the head moved during the CI wait, so the code that would merge is not the reviewed code): do NOT merge. The re-review bound is persisted, not remembered: count `<!-- FORGE:STALE_REREVIEW: pr={PR_NUMBER} -->` comments on the issue. If the count is 0, post that marker (with the new head SHA), run the quality gate on the new head (`Skill(skill="{FORGE_SKILL_PREFIX}quality-gate", args="<changed files> --worktree {WORKTREE_PATH}")`, which posts a fresh `FORGE:QUALITY_GATE` for the code that will actually merge), then re-invoke Phase R3 once — a full review of the new head. If the count is already ≥ 1 (the head keeps moving after review), add `needs-human` and return `REVIEW_RESULT: status: BLOCKED`, blocker: "PR head keeps moving after review". <!-- forge#3188 -->
|
|
568
568
|
|
|
569
|
-
-
|
|
569
|
+
- **Remediation-pending handoff (shared by the ci-gate, in-pr-fix and base-conflict cases below)** <!-- Added: forge#3541 -->: these three cases are autonomous pipeline work, not a human decision, so the issue must NOT carry `needs-human` while remediation is pending. `needs-human` is a human-gate signal (operators alert on it, and orchestrate holds DAG dependents as GATED on it). Mark the handoff with the non-human state `workflow:remediating` instead, and clear any `needs-human` that review-pr Phase 8 already set for the ci-gate refusal. `work-on:remediate` accepts either label (a bare `needs-human` from an older run still works), clears `workflow:remediating` on every exit, and re-asserts `needs-human` only when it cannot fix the PR. Run this block, then return `REVIEW_RESULT: status: NEXT`:
|
|
570
|
+
```bash
|
|
571
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
572
|
+
case "$TIER" in
|
|
573
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} remediating ;;
|
|
574
|
+
prose) gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:remediating" --remove-label "workflow:investigating,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
|
|
575
|
+
esac
|
|
576
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
577
|
+
gh pr edit {PR_NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "ci gate" (Phase 8 refused to merge because checks failed, were cancelled, or stayed pending past the gate retries): do NOT attempt the manual merge below — that would bypass the CI gate. Fixing red CI is pipeline work, not a human decision: hand remediation to the router **once**. Bound: count `<!-- FORGE:CI_REMEDIATION: pr={PR_NUMBER} -->` comments on the issue first; if ≥ 1, do not remediate again — remove `workflow:remediating`, label the issue `needs-human` (the bound is exhausted, so a human is now genuinely needed) and return `REVIEW_RESULT: status: BLOCKED` with blocker "ci gate not green after remediation". Otherwise run the remediation-pending handoff block above (sets `workflow:remediating`, removes the `needs-human` that Phase 8 set) and return `REVIEW_RESULT: status: NEXT` with `next: remediate` and `remediation: ci-gate` (see Output). Do not post the marker here: the router posts it immediately before it invokes remediation (Phase 4R), so a handoff interrupted before remediation starts is retried instead of being counted as used. **Never invoke `work-on:remediate` from this phase** (forge#3398): remediation re-reviews through `/review-pr`, which spawns domain-reviewer sub-agents, so it must run one level below the router, not nested inside this fork. Nested here, it lands at the sub-agent depth limit under `/orchestrate` with no `Agent` tool and strands the PR at `REREVIEW-REQUIRED` (forge#3391). The router runs `work-on:remediate`, which classifies a CI-gate refusal as FIXABLE, swaps `workflow:remediating` (or a legacy `needs-human`) for `workflow:in-review`, fixes the failing jobs on the PR branch, re-runs the quality gate and a full review, and auto-lands through the same CI gate. <!-- forge#3191 -->
|
|
570
581
|
|
|
571
582
|
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "in-pr fix required" (review-pr §6B.6: CONFIRMED MEDIUM findings in files this PR changed, requested on the PR in a `FORGE:INPR_FIX` comment at the current head): do NOT merge and do NOT leave the issue at `needs-human` because of this gate. Run **one** fix round, bounded by `<!-- FORGE:INPR_REMEDIATION: pr={PR_NUMBER} -->` on the issue (`{BOUND}` = `INPR_REMEDIATION` below).
|
|
572
|
-
- **Bound unused**:
|
|
573
|
-
- **Bound already used** (including the re-invocation after a remediation that did not land): waive the gate for the current head. Remove `needs-human
|
|
583
|
+
- **Bound unused**: run the remediation-pending handoff block above (set `workflow:remediating`, no `needs-human`; remediation accepts that state and its Phase M1 clears it as FIXABLE). Then return `REVIEW_RESULT: status: NEXT` with `next: remediate` and `remediation: inpr-fix` (the router posts the bound marker, as in the ci-gate case) — never invoke `work-on:remediate` from this phase (same reason as the ci-gate case). The router runs remediation, which reads the `FORGE:INPR_FIX` work order, fixes exactly those findings, re-reviews the new head and auto-lands. On the re-review, any finding still present is filed as an issue (the round is used). If remediation does not land, the router re-invokes this phase, which then finds the bound used and takes the branch below.
|
|
584
|
+
- **Bound already used** (including the re-invocation after a remediation that did not land): waive the gate for the current head. Remove `needs-human` and `workflow:remediating` (restoring `workflow:in-review`), post `<!-- FORGE:INPR_FIX_WAIVED: head=<current head SHA> -->` on the **PR**, then re-invoke Phase R3 once under the `STALE_REREVIEW` bound. That review files the remaining findings as issues and merges as it would have before this gate. Only if `STALE_REREVIEW` is also exhausted, label the issue `needs-human`.
|
|
574
585
|
```bash
|
|
575
586
|
INPR_WAIVE_HEAD=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq .headRefOid)
|
|
576
|
-
gh issue edit {NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
587
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "needs-human,workflow:remediating" --add-label "workflow:in-review" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
577
588
|
gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:INPR_FIX_WAIVED: head=${INPR_WAIVE_HEAD} -->
|
|
578
589
|
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
|
|
579
590
|
```
|
|
@@ -604,8 +615,8 @@ Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head on
|
|
|
604
615
|
`BOUND_EXHAUSTED` → take the "already ≥ 1" branch of that case.
|
|
605
616
|
|
|
606
617
|
- `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**:
|
|
608
|
-
- **Bound already used** (the sync ran and the PR still conflicts, or the sync could not resolve a hunk): do not sync again.
|
|
618
|
+
- **Bound unused**: run the remediation-pending handoff block above (set `workflow:remediating`, no `needs-human`; a pure base sync needs no human). 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).
|
|
619
|
+
- **Bound already used** (the sync ran and the PR still conflicts, or the sync could not resolve a hunk): do not sync again. Remove `workflow:remediating`, label the issue `needs-human` (a genuine human decision now) 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
620
|
|
|
610
621
|
- 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**:
|
|
611
622
|
```bash
|
|
@@ -628,11 +639,16 @@ Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head on
|
|
|
628
639
|
FINDING_COUNT=$(printf '%s' "$DISPO_JSON" | jq -s '[.[][] | select(.body | test("<!-- FINDING:"))] | length' 2>/dev/null || echo "")
|
|
629
640
|
# Trust predicate: ONE shared copy (scripts/trusted-comments.sh, same as verify-phase-trail.sh): trusted association, Bot account, or
|
|
630
641
|
# FORGE_TRAIL_TRUSTED_LOGINS. A GitHub App bot always has author_association NONE, so an association-only filter drops the pipeline's own disposition.
|
|
642
|
+
# 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.
|
|
631
643
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
644
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
645
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
646
|
+
[ -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)"
|
|
647
|
+
done
|
|
632
648
|
TRUSTED_SCRIPT=""
|
|
633
|
-
|
|
649
|
+
while IFS= read -r _c; do
|
|
634
650
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
635
|
-
done
|
|
651
|
+
done <<< "$_tc"
|
|
636
652
|
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
|
|
637
653
|
if [ -z "$DISPO_JSON" ] || [ -z "$FINDING_COUNT" ] || [ -z "$DISPOSITION_COUNT" ]; then
|
|
638
654
|
echo "REVIEW_RESULT: status: BLOCKED, blocker: note disposition unreadable (comments or scripts/trusted-comments.sh unresolvable; fail closed)"
|
package/commands/work-on.md
CHANGED
|
@@ -31,7 +31,7 @@ Orchestrator for the full issue lifecycle: investigate → decompose (if needed)
|
|
|
31
31
|
|
|
32
32
|
4. **PRs NEVER target `main`.** Target `staging` (fast lane) or `milestone/{slug}` (feature lane). The router computes and validates the target (Lane Resolution) and passes it to the phases as `--base`.
|
|
33
33
|
|
|
34
|
-
5. **`needs-human` is for genuine decisions and external actions only.** A phase that can fix its own problem (failing checks, review findings, a moved PR head, a missing marker it can produce) does so inside its own fork; the router never adds `needs-human` for a condition a phase reported as fixable.
|
|
34
|
+
5. **`needs-human` is for genuine decisions and external actions only.** A phase that can fix its own problem (failing checks, review findings, a moved PR head, a missing marker it can produce) does so inside its own fork; the router never adds `needs-human` for a condition a phase reported as fixable. A fix handed to remediation (ci-gate, in-pr-fix, base-sync) is marked with the non-terminal, non-human state `workflow:remediating`, never `needs-human` (forge#3541).
|
|
35
35
|
|
|
36
36
|
### Skill Name Resolution (`{FORGE_SKILL_PREFIX}`)
|
|
37
37
|
|
|
@@ -224,7 +224,7 @@ fi
|
|
|
224
224
|
Worktree/branch-already-exists and stale-label conditions are surfaced later (by the build phase's worktree step and the `## Error Handling` section) with their own recovery guidance in `docs/site/troubleshooting.md`.
|
|
225
225
|
|
|
226
226
|
### 0A: Parse input
|
|
227
|
-
Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human`, `workflow:decomposed`, and `workflow:awaiting-merge`, pick highest priority.
|
|
227
|
+
Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human`, `workflow:remediating`, `workflow:decomposed`, and `workflow:awaiting-merge`, pick highest priority.
|
|
228
228
|
|
|
229
229
|
**Resolve `UNDER_ORCHESTRATION`**: `true` if the invocation args contain `--under-orchestration`, else `false`. This is a single parse done once, here — every later gated block (heartbeats) just checks this variable, no re-parsing.
|
|
230
230
|
|
|
@@ -298,17 +298,17 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="${REMEDIATE_PR_NUMBER
|
|
|
298
298
|
|
|
299
299
|
**Claim pre-check (forge#3413; one per-head marker shared with the orchestrator)**: the orchestrator's re-review handoff (`commands/orchestrate/phase-4-execution.md` Step 1/2) dedupes on `<!-- FORGE:REREVIEW_DISPATCHED -->`, so the router MUST write the SAME marker before `review-pr`, or the orchestrator re-dispatches after the router already re-reviewed. In order, failing closed (any failure = no dispatch, terminal fallback):
|
|
300
300
|
1. **Labels first**: read the issue's state and labels (`gh issue view ${ISSUE} {GH_FLAG} --json labels,state`). If the issue is closed, already has `needs-human`, or lacks `workflow:in-review`, do NOT dispatch and do NOT apply the fallback (something else already advanced it); STOP.
|
|
301
|
-
2. **Existing claim**: list the PR's comments and apply the trusted-author, leading-marker predicate. Bash blocks are independent executions and a standalone router session has none of the orchestrator's shell functions, so every claim, read-back, classification and release block MUST paste verbatim, at its top, the **required helper set** from `phase-4-execution.md`: `trail_cache_dir` and `resolve_orch_login` (defined earlier in that file; transitive deps of `rereview_trusted_rows`), then the Step 1 SHARED HELPERS `rereview_trusted_rows`, `REREVIEW_CLAIMS`, `REREVIEW_DONE_FILTER`, `REREVIEW_LEASE_SECS` / `REREVIEW_MAX_SECS`, `REREVIEW_LEASE_FILTER`, `REREVIEW_FIRST_LEASE_FILTER` and `REREVIEW_RELEASED_FILTER`. Never improvise the predicate: too loose lets an untrusted commenter suppress a re-review, too strict drops the router's own bot-authored claim. If a trusted claim (lowest comment id wins) already embeds the PR's current head SHA (`headRefOid`), classify it with the single definition in `phase-4-execution.md` Step 1 (forge#3428
|
|
301
|
+
2. **Existing claim**: list the PR's comments and apply the trusted-author, leading-marker predicate. Bash blocks are independent executions and a standalone router session has none of the orchestrator's shell functions, so every claim, read-back, classification and release block MUST paste verbatim, at its top, the **required helper set** from `phase-4-execution.md`: `trail_cache_dir` and `resolve_orch_login` (defined earlier in that file; transitive deps of `rereview_trusted_rows`), then the Step 1 SHARED HELPERS `rereview_trusted_rows`, `REREVIEW_CLAIMS`, `REREVIEW_DONE_FILTER`, `REREVIEW_LEASE_SECS` / `REREVIEW_MAX_SECS`, `REREVIEW_LEASE_FILTER`, `REREVIEW_FIRST_LEASE_FILTER` and `REREVIEW_RELEASED_FILTER`. Never improvise the predicate: too loose lets an untrusted commenter suppress a re-review, too strict drops the router's own bot-authored claim. If a trusted claim (lowest comment id wins) already embeds the PR's current head SHA (`headRefOid`), classify it with the single definition in `phase-4-execution.md` Step 1 (forge#3428, forge#3486: an unexpired or renewed lease, or an expired lease with liveness such as a recent trusted `FORGE:REVIEW*` comment or running checks, means another actor is live (`CLAIMED`); a merged/closed PR or an issue advanced past in-review is done (`NONE`)): for live or done do NOT dispatch; STOP without the fallback. A `FORGE:REREVIEW_RELEASED` marker (`Outcome: SETTLED`) is not a stand-down: step 1 guarantees the issue is still at `workflow:in-review`, so a SETTLED release reads `STALE`, as does an expired lease with no liveness and an open PR (including an unparseable `Lease:`); `STALE` goes to the terminal fallback with reason `stale re-review claim`; a failed liveness lookup fails closed to STOP (never `STALE`). Do not duplicate the predicate here (reference and paste, never re-write).
|
|
302
302
|
2b. **Unresolved login (fail closed, before the claim)**: with the helpers pasted, run `resolve_orch_login`. If it prints an empty login (and `FORGE_BOT_LOGIN` is unset), the router's own claim could never be verified: do NOT post the claim and do NOT dispatch. Treat it as `UNREADABLE` (writes nothing, no terminal fallback write, never `STALE`), report the unresolved login in the operator-visible report line, and STOP; the orchestrator re-checks next cycle (mirrors `phase-4-execution.md` Step 1 SEC-8).
|
|
303
|
-
3. **Claim**: unless `DRY_RUN=true` (then record "would claim and re-review" and STOP), post the marker with `Actor: router`, the head SHA, a unique `Claim:` id and a UTC `Lease:` timestamp, in the exact format defined in `phase-4-execution.md` Step 1 (`<!-- FORGE:REREVIEW_DISPATCHED -->` as the first line, then `Head: <sha>`, `Actor: router`, `Claim: router-<epoch>-<pid>`, `Lease: <UTC ISO-8601>`), with the `gh pr comment` call checked (a failed post is a failure, never ignored).
|
|
304
|
-
4. **Read back** the PR comments (trusted rows only, projecting the comment `id`, after a short settle delay): the lowest-comment-id trusted claim for this head (`.[0]` of the id-sorted `REREVIEW_CLAIMS`) must carry this router's `Claim:` id; re-read once after verifying and treat a late lower-id claim as `LOST`. A missing row, an API failure, or a different earliest claim (`LOST`) means do NOT dispatch, and none of them stands down blindly: re-run the shared Step 1 classification from `phase-4-execution.md` (forge#3431; the same required helper set from step 2, including `trail_cache_dir` and `resolve_orch_login`, pasted at the top of the block, no router-local lease helper or constant) for this head and map the printed `REREVIEW_STATE`: `STALE` (an expired lease with no liveness and an open PR, a lease past `REREVIEW_MAX_SECS`, or an unparseable `Lease:` on the winning claim; a crashed earlier claimant) goes to the terminal fallback with reason `stale re-review claim`; `CLAIMED` (another actor's live claim) and `NONE` (
|
|
305
|
-
Only after the read-back verifies does the router invoke `review-pr`. After `review-pr` returns (any result), the router posts a checked, DRY_RUN-guarded `<!-- FORGE:REREVIEW_RELEASED -->` marker (`Head: <sha>`, `Actor: router`, `Claim: <
|
|
303
|
+
3. **Claim**: unless `DRY_RUN=true` (then record "would claim and re-review" and STOP), post the marker with `Actor: router`, the head SHA, a unique `Claim:` id and a UTC `Lease:` timestamp, in the exact format defined in `phase-4-execution.md` Step 1 (`<!-- FORGE:REREVIEW_DISPATCHED -->` as the first line, then `Head: <sha>`, `Actor: router`, `Claim: router-<epoch>-<pid>`, `Lease: <UTC ISO-8601>`), with the `gh pr comment` call checked (a failed post is a failure, never ignored). The claim block MUST declare `DRY_RUN` explicitly (a fresh shell has none) and MUST print its claim id as a labelled final line, `CLAIM_ID=router-<epoch>-<pid>` (forge#3472), because step 4 and the release run in later independent blocks that can only use that printed value.
|
|
304
|
+
4. **Read back** the PR comments (trusted rows only, projecting the comment `id`, after a short settle delay): the lowest-comment-id trusted claim for this head (`.[0]` of the id-sorted `REREVIEW_CLAIMS`) must carry this router's `Claim:` id; re-read once after verifying and treat a late lower-id claim as `LOST`. A missing row, an API failure, or a different earliest claim (`LOST`) means do NOT dispatch, and none of them stands down blindly: re-run the shared Step 1 classification from `phase-4-execution.md` (forge#3431; the same required helper set from step 2, including `trail_cache_dir` and `resolve_orch_login`, pasted at the top of the block, no router-local lease helper or constant) for this head and map the printed `REREVIEW_STATE`: `STALE` (an expired lease with no liveness and an open PR, a lease past `REREVIEW_MAX_SECS`, or an unparseable `Lease:` on the winning claim; a crashed earlier claimant) goes to the terminal fallback with reason `stale re-review claim`; `CLAIMED` (another actor's live claim) and `NONE` (merged, closed, or advanced past in-review) stand down without the fallback; `UNREADABLE` writes nothing (fail closed, never `STALE`); `PENDING` goes to the fallback only for the missing-row / API-failure case. For the missing-row / API-failure case only, the printed `FIRST_CLAIM_ID` (the winning `.[0]` claim) equal to this router's own `CLAIM_ID` printed by step 3 goes to the fallback (our own unverified claim must not stand the router down on itself), while another actor's id is classified like `LOST`. On `LOST` the winner is by definition another actor's claim, so a `STALE` winner is never excused as "owned by another actor".
|
|
305
|
+
Only after the read-back verifies does the router invoke `review-pr`. After `review-pr` returns (any result) and the outcome is settled (a labelled terminal state, or the terminal fallback's label write verified; never before, forge#3486), the router posts a checked, DRY_RUN-guarded `<!-- FORGE:REREVIEW_RELEASED -->` marker (`Head: <sha>`, `Actor: router`, `Claim: <the CLAIM_ID printed by step 3>`, `Outcome: SETTLED`) in the format defined in `phase-4-execution.md` Step 2 (Renew and release; the release block also pastes the same required helper set). A release found while the issue is still at `workflow:in-review` is not terminal and reads `STALE`, so the fallback re-applies; the router may also renew the lease between phases by re-posting the claim with the same `Claim:` id and a fresh `Lease:`.
|
|
306
306
|
|
|
307
307
|
**Terminal fallback** (canonical definition; Phase 4R and `commands/orchestrate/phase-4-execution.md` item 6.4 reference it, so the review and remediate paths share one): a re-review that cannot run must never leave the issue at `workflow:in-review` with nothing to advance it. First re-read the issue's labels: if `needs-human` is already set and `workflow:in-review` is absent, the fallback has already been applied (or something else escalated it), so skip the edit and report that state (idempotent; forge#3413). Otherwise post a comment naming the PR and the reason (e.g. `review-pr could not be dispatched: <reason>`), then in ONE `gh issue edit` add `needs-human` and remove `workflow:in-review` (never one without the other), and re-read the labels to verify the write. If the label write failed, report that failure as the outcome instead of treating the fallback as done. Before commenting, re-check the PR state and liveness (forge#3428): a merged or closed PR, or another actor's claimant with recent activity per `phase-4-execution.md` Step 1 (this actor's own `Claim:` id never counts as that claimant, or an own-claim fallback would cancel itself), means skip the escalation and report that state. Never review inline, and never re-nest remediation to retry.
|
|
308
308
|
|
|
309
|
-
**After `REMEDIATE_RESULT` returns (any other status), STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("{FORGE_SKILL_PREFIX}work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
|
|
309
|
+
**After `REMEDIATE_RESULT` returns (any other status), STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` / `workflow:remediating` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("{FORGE_SKILL_PREFIX}work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
|
|
310
310
|
|
|
311
|
-
This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated predecessor's own PR).
|
|
311
|
+
This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated or `workflow:remediating` predecessor's own PR).
|
|
312
312
|
|
|
313
313
|
**Skip this entire section if `--remediate` is absent from `$ARGUMENTS`** — proceed to the normal parse below.
|
|
314
314
|
|
|
@@ -381,7 +381,7 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments --jq '.[] | {id: .id, author: .u
|
|
|
381
381
|
|
|
382
382
|
**Resume preflight (MANDATORY on any resume past Phase 1)** <!-- Added: forge#3061 -->: before routing to Phase 3 (build), 4 (review) or 5 (close) from existing state, run `bash "$FORGE_ROOT/scripts/verify-phase-trail.sh" {NUMBER} -R {GH_REPO}` (add `--docs-only` for docs-only diffs, or `--code-diff` when the diff has any non-docs file so an INVESTIGATION band cannot waive requirements), with `FORGE_ROOT` resolved by the bootstrap in "Script resolution" below. If `FORGE_ROOT` is empty or the script is missing, treat it as `PHASE_TRAIL: ERROR` (fail closed: stop and add `needs-human`; never skip the preflight). On `PHASE_TRAIL: FAIL`, go BACK and run each missing phase through its `Skill(...)` (investigate, Phase 3B classification, build contract/context/architect, validate) before continuing — never continue forward over a gap, never hand-post a missing marker, and never treat a recovered uncommitted worktree as a substitute for the skipped phases. The same verifier gates PR creation (`work-on/review.md` Phase R1.5) and auto-merge (`review-pr.md` Phase 8).
|
|
383
383
|
|
|
384
|
-
**Determine resume point**: no `FORGE:INVESTIGATOR` → Phase 1 (investigate). Investigation complete with decompose YES and no `FORGE:DECOMPOSED` → Phase 2 (decompose); with `FORGE:DECOMPOSED` → Phase 5 (`--terminal-state decomposed`). Investigation complete (decompose NO) without `FORGE:BUILDER:COMPLETE` → Phase 3 (build; a partial BUILDER comment is cleaned up by the build phase). `FORGE:BUILDER:COMPLETE` and no merged PR → Phase 4 (review), except an **unfinished remediation handoff**, which is checked first and overrides the checkpoint routing in 0B.5: the issue does NOT carry `needs-human
|
|
384
|
+
**Determine resume point**: no `FORGE:INVESTIGATOR` → Phase 1 (investigate). Investigation complete with decompose YES and no `FORGE:DECOMPOSED` → Phase 2 (decompose); with `FORGE:DECOMPOSED` → Phase 5 (`--terminal-state decomposed`). Investigation complete (decompose NO) without `FORGE:BUILDER:COMPLETE` → Phase 3 (build; a partial BUILDER comment is cleaned up by the build phase). `FORGE:BUILDER:COMPLETE` and no merged PR → Phase 4 (review), except an **unfinished remediation handoff**, which is checked first and overrides the checkpoint routing in 0B.5: the issue does NOT carry `needs-human` (it may carry `workflow:remediating`), and its latest `FORGE:CI_REMEDIATION: pr={PR}`/`FORGE:INPR_REMEDIATION: pr={PR}`/`FORGE:BASESYNC_REMEDIATION: pr={PR}` marker (for the open PR) has no `FORGE:REMEDIATION:COMPLETE` trail comment on that PR created after it (a trail comment is one that STARTS with `<!-- FORGE:REMEDIATION -->`, contains `<!-- FORGE:REMEDIATION:COMPLETE -->` and comes from a trusted author, read with `scripts/trusted-comments.sh`; a comment that merely quotes the marker never counts, forge#3412; if the comments or the helper are unreadable, fail closed to `needs-human` instead of resuming) → first set `workflow:remediating` (the non-human "autonomous remediation pending" state, forge#3541: `transition-label.sh {NUMBER} {GH_FLAG} remediating`, with a comment "resuming an interrupted remediation of PR #{PR}"), because remediation's Phase M0 only accepts `needs-human` or `workflow:remediating` issues and its M1 swaps the label for `workflow:in-review` on a FIXABLE run; never add `needs-human` for a resume; then Phase 4R with that marker's kind, skipping the marker post (it is already recorded). This is the silent strand: M1 cleared the pending label and the run was lost before M8. It cannot loop: a remediation that finishes posts `FORGE:REMEDIATION:COMPLETE` (its single-attempt guard then returns `ALREADY_DONE`), and any other exit leaves `needs-human` on the issue (remediation's stranded-state rule and Phase 4R both add it on a `BLOCKED` result), so this rule no longer matches. A run lost before M1 still carries `workflow:remediating` (or a legacy `needs-human`): `workflow:remediating` re-enters Phase 4R by this same rule, a legacy `needs-human` routes to review, which finds the bound used and stops visibly at `needs-human`. Re-entering review instead would find the bound used and stop without remediation ever finishing. PR merged and issue open → Phase 5 (close). `workflow:invalid` → STOP.
|
|
385
385
|
|
|
386
386
|
### 0B.5: Read Phase Checkpoint (MANDATORY — executes before any phase-skip decision)
|
|
387
387
|
|
|
@@ -767,14 +767,14 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="{PR_NUMBER} --issue {
|
|
|
767
767
|
| `REMEDIATE_RESULT` | Router action |
|
|
768
768
|
|---|---|
|
|
769
769
|
| `re_gate_outcome: AUTO-LANDED` | Merged. Remediation's Phase M8 already ran `work-on:close`; if the issue is still open, run Phase 5 with `--terminal-state merged`. Then done. |
|
|
770
|
-
| `status: REREVIEW_REQUIRED` | Fallback only (should not occur at this depth): run the re-review from this router as in 0A.1 (labels checked and the per-head `FORGE:REREVIEW_DISPATCHED` claim posted and read back first, then `FORGE:REREVIEW_RELEASED` posted after
|
|
771
|
-
| `status: BLOCKED`, blocker starts with `github-unavailable:` | Transient, not a decision (see Transient GitHub failures). Re-
|
|
772
|
-
| `status: BLOCKED` (any kind) | Make sure `needs-human` is on the issue (add
|
|
770
|
+
| `status: REREVIEW_REQUIRED` | Fallback only (should not occur at this depth): run the re-review from this router as in 0A.1 (labels checked and the per-head `FORGE:REREVIEW_DISPATCHED` claim posted and read back first, then `FORGE:REREVIEW_RELEASED` with `Outcome: SETTLED` posted only after the outcome is settled, forge#3486), then Phase 5 on `REVIEW_RESULT: status: COMPLETE`. If the re-review cannot run or returns anything else, apply the terminal fallback defined in 0A.1 (labels re-read first, comment with reason, add `needs-human`, remove `workflow:in-review`), then STOP; do not poll. |
|
|
771
|
+
| `status: BLOCKED`, blocker starts with `github-unavailable:` | Transient, not a decision (see Transient GitHub failures). Re-assert `workflow:remediating` with a short comment (remediation's Phase M0 only accepts `needs-human` or `workflow:remediating` issues and its M1 swaps the label for `workflow:in-review` on a FIXABLE run; a transient outage is not a human decision, so do not add `needs-human`), wait 2 min (5 min before the second retry), then re-invoke `work-on:remediate` with the same args; do NOT re-post the bound marker. At most 2 retries; if the third attempt still returns `github-unavailable:`, fall through to the `status: BLOCKED` (any kind) row below. |
|
|
772
|
+
| `status: BLOCKED` (any kind) | Make sure `needs-human` is on the issue and `workflow:remediating` is not (add/remove accordingly, with the blocker as a comment if absent: remediation can exit after its M1 cleared the label). STOP. |
|
|
773
773
|
| `status: ALREADY_DONE` (single-attempt guard: an earlier remediation already completed on this PR), `remediation: inpr-fix` | Same as any other `inpr-fix` outcome below: re-invoke Phase 4 once, which waives the in-PR gate. |
|
|
774
|
-
| `status: ALREADY_DONE`, `remediation: base-sync` | Make sure `needs-human` is on the issue (add
|
|
775
|
-
| `status: ALREADY_DONE`, `remediation: ci-gate` | Make sure `needs-human` is on the issue (add
|
|
774
|
+
| `status: ALREADY_DONE`, `remediation: base-sync` | Make sure `needs-human` is on the issue and `workflow:remediating` is not (add/remove accordingly, with a comment naming the PR, that a base sync already ran and the conflicting files if known, if absent). STOP. Do NOT re-invoke Phase 4: a still-conflicting PR would loop. |
|
|
775
|
+
| `status: ALREADY_DONE`, `remediation: ci-gate` | Make sure `needs-human` is on the issue and `workflow:remediating` is not (add/remove accordingly, with a comment naming the PR and that remediation already ran, if absent). STOP. |
|
|
776
776
|
| any other outcome, `remediation: inpr-fix` | Re-invoke Phase 4 (`work-on:review`) once with the same args. Review finds its `INPR_REMEDIATION` bound used, waives the in-PR gate for the current head, files the remaining findings as issues and re-reviews. |
|
|
777
|
-
| any other outcome, `remediation: base-sync` | Terminal: make sure `needs-human` is on the issue (add
|
|
777
|
+
| any other outcome, `remediation: base-sync` | Terminal: make sure `needs-human` is on the issue and `workflow:remediating` is not (add/remove accordingly, with a comment naming the PR and the conflicting files from remediation's `FORGE:BASESYNC_FAILED` comment, if absent). STOP. Do NOT re-invoke Phase 4: the bound is used and a still-conflicting PR would loop. |
|
|
778
778
|
| any other outcome, `remediation: ci-gate` | Terminal: remediation left `needs-human` (or `workflow:awaiting-merge`) with its reason. STOP. |
|
|
779
779
|
|
|
780
780
|
Row order matters: the `github-unavailable:` row is evaluated before the generic `status: BLOCKED` (any kind) row, which only handles non-transient blockers and the exhausted retry budget (see Transient GitHub failures). <!-- Added: forge#3402 -->
|
package/docs/CONFIG.md
CHANGED
|
@@ -457,6 +457,35 @@ pipeline:
|
|
|
457
457
|
|
|
458
458
|
---
|
|
459
459
|
|
|
460
|
+
## `engine` (OPTIONAL)
|
|
461
|
+
|
|
462
|
+
Per-command `--model` and `--max-turns` for the CLI backend (`forgedock run-issue` phases and `forgedock run <command>`). Opt-in: with no keys set, the `claude` invocation is unchanged and uses the CLI's own default model with no turn bound. `forge.yaml` is parsed one level deep, so settings are **flat** keys under `engine:`, named `<key>_model` and `<key>_max_turns`, where `<key>` is the command name with every non-alphanumeric character replaced by `_`.
|
|
463
|
+
|
|
464
|
+
```yaml
|
|
465
|
+
engine:
|
|
466
|
+
# work-on/review -> work_on_review (the review phase is the most expensive).
|
|
467
|
+
work_on_review_model: sonnet
|
|
468
|
+
work_on_review_max_turns: 120
|
|
469
|
+
|
|
470
|
+
# review-pr-staging -> review_pr_staging (non-work-on commands work the same way).
|
|
471
|
+
review_pr_staging_max_turns: 80
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
| Field | Type | Required | Description |
|
|
475
|
+
|-------|------|----------|-------------|
|
|
476
|
+
| `<key>_model` | string | No | Model alias (`sonnet`, `opus`, `haiku`) or full model id passed as `--model`. Invalid values are ignored. |
|
|
477
|
+
| `<key>_max_turns` | integer | No | Positive integer passed as `--max-turns`. Invalid values are ignored. |
|
|
478
|
+
|
|
479
|
+
**Precedence** (model): `engine.<key>_model`, then the run-level model the caller passed explicitly, then omitted. `max_turns` comes only from this section. No defaults ship.
|
|
480
|
+
|
|
481
|
+
**Version gate**: the flags are added only when the installed `claude --version` supports them (`--max-turns` is hidden from `claude --help`, so help text is never scraped); an unparseable version omits both flags.
|
|
482
|
+
|
|
483
|
+
**Turn exhaustion**: when `--max-turns` is exhausted the run ends `engine-error` (detail `max-turns`) and the phase is never committed, so `forgedock run-issue --retry` re-enters the same phase. Raise `<key>_max_turns` before retrying. Phase runlog events record `model` and `turns` next to `usage` so a limit can be sized from measured turn counts.
|
|
484
|
+
|
|
485
|
+
**Commands that use this section**: CLI backend (`runCommand` in `bin/runner.mjs`)
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
460
489
|
## `build` (OPTIONAL)
|
|
461
490
|
|
|
462
491
|
Tuning for the `/work-on` build phase. Today it holds the diff-size gate: after the implement phase stages its changes and before validation commits them, the build measures the changed lines (added plus deleted) of the index and any earlier commits against `origin/{base}`, using `scripts/diff-size.sh`. Review findings per PR rise steeply with diff size, so an oversized build is split before it is validated.
|