forgedock 1.18.0 → 1.20.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/README.md +2 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +54 -4
- package/bin/engine/runlog.mjs +10 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +32 -2
- package/bin/engine.mjs +187 -10
- package/bin/runner.mjs +89 -13
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +136 -28
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +201 -40
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr-staging.md +4 -4
- package/commands/review-pr.md +31 -20
- package/commands/work-on/build/architect.md +54 -1
- package/commands/work-on/build/implement.md +28 -1
- package/commands/work-on/build/validate.md +321 -29
- package/commands/work-on/build.md +184 -17
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/investigate.md +7 -0
- package/commands/work-on/remediate.md +43 -17
- package/commands/work-on/review.md +43 -22
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +93 -2
- package/docs/OUTCOME-CODES.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/STATS.md +11 -4
- package/docs/TELEMETRY.md +5 -2
- package/docs/WORK-ON-RUNTIME.md +29 -3
- package/package.json +2 -2
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/cap-findings.test.sh +2 -0
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-markers.sh +4 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +379 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +451 -0
- package/scripts/ci-local/scoping.mjs +531 -0
- package/scripts/ci-local.sh +98 -0
- package/scripts/ci-local.test.sh +1108 -0
- package/scripts/classify-finding.sh +33 -7
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +2 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/lib/telemetry.sh +3 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +294 -53
- package/scripts/review-finalize.test.sh +347 -9
- package/scripts/review-pr-delta-wiring.test.sh +13 -0
- package/scripts/review-prepare.sh +5 -3
- package/scripts/review-prepare.test.sh +18 -2
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +10 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/trusted-comments.test.sh +2 -1
- package/scripts/verify-phase-trail.test.sh +14 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +41 -3
- package/scripts/with-run-tmpdir.sh +20 -0
|
@@ -31,7 +31,7 @@ background: false
|
|
|
31
31
|
|
|
32
32
|
**Scope note**: This mode owns exactly one gap — re-driving a `needs-human` or `workflow:remediating` PR's own remediation, plus resuming an interrupted M7 auto-land from `workflow:awaiting-merge` when no `FORGE:REMEDIATION:COMPLETE` trail exists (forge#3647; M1-M6 are skipped, only M7/M8 run), finishing a merged PR whose M7 run was cut off before M8 (forge#3685), and re-entering a held resume (the `workflow:remediating` state 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) and does NOT edit `review-pr.md`'s Phase 8 guard (forge#1810), whose safe-default (`workflow:awaiting-merge` on a clean re-review of a previously-escalated PR) is reused as-is; this file only adds a bar-check after it fires.
|
|
33
33
|
|
|
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. The non-terminal held-resume record `FORGE:REMEDIATION_HELD` (forge#3685) is NOT a completion marker; the engine's `detectOutcome` reads it as `terminalReason: "awaiting-merge"
|
|
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. The non-terminal held-resume record `FORGE:REMEDIATION_HELD` (forge#3685) is NOT a completion marker; the engine's `detectOutcome` reads it as `terminalReason: "awaiting-merge"`, except a `Hold reason` of `CI pending`, which it reads as a wait (`wait: "ci-pending"`): the engine polls CI with backoff and re-dispatches this resume (forge#4018). 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. Any `**Re-gate outcome**` outside the Output list fails the attempt; `INPR-FIX-REQUESTED` (with `**INPR round**: N`) re-arms one further in-PR fix remediation instead of parking (forge#4021).
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
@@ -85,7 +85,10 @@ ISSUE_LABELS=$(echo "$ISSUE_STATE" | jq -r '[.labels[].name] | join(",")')
|
|
|
85
85
|
- **Stranded-state rule**: `workflow:remediating` must never outlive this run. Every exit that returns `BLOCKED` or `UNFIXABLE` (including exits before Phase M1; except a `RESUME_AT_M7` run, which leaves the issue at `workflow:awaiting-merge` untouched, and except a `deferred-live-run:` exit, which leaves every label untouched: a live run owns the issue) removes `workflow:remediating` and adds `needs-human`, because at that point a human really is needed:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
|
|
88
|
+
# forge#3567: ensure the label, add first, then remove (a missing label cancels a whole edit)
|
|
89
|
+
gh label create "needs-human" --color "E4E669" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
90
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
91
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --remove-label "workflow:remediating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
**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):
|
|
@@ -132,11 +135,14 @@ if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$PR_COMMENTS" ] && [ -n "$ISSUE_COMMENTS" ]
|
|
|
132
135
|
[.[][] | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | select((.body // "") | type == "string")] as $c
|
|
133
136
|
| [$c[] | select(.body | test("\\A<!-- FORGE:(CI|INPR|BASESYNC)_REMEDIATION: pr=" + $pr + " -->")) | {t: .created_at, k: (.body | capture("FORGE:(?<k>CI|INPR|BASESYNC)_REMEDIATION").k + "_REMEDIATION")}] as $m
|
|
134
137
|
| [$c[] | select(.body | test("\\A<!-- FORGE:REMEDIATION -->[\\s\\S]*<!-- FORGE:REMEDIATION:COMPLETE -->")) | .created_at] as $trails
|
|
135
|
-
|
|
|
138
|
+
| ([$c[] | select(.body | test("\\A<!-- FORGE:INPR_FIX: round=[0-9]+ head=")) | .created_at] | max // "") as $fix
|
|
139
|
+
| [$trails[] as $t | select($kind != "INPR_REMEDIATION" or $fix == "" or $t > $fix) | ([$m[] | select(.t <= $t)] | sort_by(.t) | last | .k // "") | select(. == $kind)] | length' 2>/dev/null || echo "")
|
|
136
140
|
if [ -n "$KIND_DONE_N" ] && [ "$KIND_DONE_N" -eq 0 ]; then REMEDIATION_COMPLETE_N=0; fi
|
|
137
141
|
fi
|
|
138
142
|
```
|
|
139
143
|
|
|
144
|
+
**Re-armed INPR round** <!-- Added: forge#4037 -->: for `INPR_REMEDIATION` a completed trail counts only when it is newer than the newest trusted `FORGE:INPR_FIX: round=N head=` work order (the `$fix` bound above), so the re-gate's second in-PR round (a newer `INPR_FIX` than the newest trail) dispatches instead of returning `ALREADY_DONE`; with no work order the per-kind rule above is unchanged. The router bounds re-arms to one.
|
|
145
|
+
|
|
140
146
|
Completed trails of other kinds are left in place (never deleted); only the interrupted-partial cleanup below deletes anything.
|
|
141
147
|
|
|
142
148
|
- 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`.
|
|
@@ -198,7 +204,7 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
198
204
|
```
|
|
199
205
|
|
|
200
206
|
**Classify into FIXABLE vs. UNFIXABLE**:
|
|
201
|
-
- **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> max_severity=<MEDIUM|HIGH|CRITICAL> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM/HIGH/CRITICAL 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).
|
|
207
|
+
- **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 `degraded-gate-unreviewed` refusal (not a code fix: re-run the review so its `### Degraded Paths Reviewed` section names each degraded check), 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> max_severity=<MEDIUM|HIGH|CRITICAL> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM/HIGH/CRITICAL 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).
|
|
202
208
|
- **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.
|
|
203
209
|
|
|
204
210
|
**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`."
|
|
@@ -209,9 +215,11 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
209
215
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
210
216
|
echo "DRY_RUN: would replace needs-human / workflow:remediating with workflow:in-review on issue #{ISSUE_NUMBER}"
|
|
211
217
|
else
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
# forge#3567: ensure label, add first, single-label removes
|
|
219
|
+
gh label create "workflow:in-review" --color "5319E7" --force {GH_FLAG} 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
220
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:in-review" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
221
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
222
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --remove-label "workflow:remediating" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
215
223
|
fi
|
|
216
224
|
```
|
|
217
225
|
|
|
@@ -500,8 +508,12 @@ else:
|
|
|
500
508
|
|
|
501
509
|
Wait for the completed child result and retain its `REVIEW_RESULT` in remediation state before continuing to Phase M7. A running/progress response is not a completion result. If the child errors or does not return a parseable `REVIEW_RESULT`, stop with `REMEDIATE_RESULT: status: BLOCKED`; do not report remediation in progress as a terminal parent result. The `Skill(...)` invocation above remains the non-OpenCode path.
|
|
502
510
|
|
|
511
|
+
If the re-review approved but requested a further in-PR fix round (forge#4021): the retained `REVIEW_RESULT` is not `BLOCKED` and a trusted (Bot or OWNER/MEMBER/COLLABORATOR) `<!-- FORGE:INPR_FIX: round=N head=<sha> ... -->` comment, posted on the PR after this Phase M6 started, names the live head. Not a hold or escalation: set `RE_GATE_OUTCOME="INPR-FIX-REQUESTED"` and `INPR_ROUND=N`, do NOT add `needs-human`, skip Phase M7, go to Phase M8 and return `REMEDIATE_RESULT: status: COMPLETE`. The caller dispatches one further in-PR fix remediation; a second request is bounded by the `INPR_REMEDIATION` marker and escalates. <!-- Added: forge#4021 -->
|
|
512
|
+
|
|
503
513
|
If the retained `REVIEW_RESULT` is `PHASE_TRAIL_FAILED` (forge#3102): the gate refused to merge on missing phase markers, not on a code finding. This phase does NOT re-dispatch the missing phases: it is a dispatching phase running in a fork (`docs/WORK-ON-RUNTIME.md` R1), and `work-on:review` Phase R4 is the single re-dispatch owner. Keep `needs-human`, post `<!-- FORGE:PHASE_TRAIL_FAILED -->` listing the missing markers from the `MISSING:` lines, and exit `REMEDIATE_RESULT: status: BLOCKED` with blocker "phase trail incomplete" so the router can hand the `MISSING:` lines back to the review owner. Never treat it as `HELD-AWAITING-MERGE` or `AUTO-LANDED`. A still-failing trail can also be cleared by a human `<!-- FORGE:PHASE_TRAIL_OVERRIDE -->` comment (forge#3152, see `scripts/verify-phase-trail.sh -h`); the verifier decides, this phase never posts one.
|
|
504
514
|
|
|
515
|
+
If the retained `REVIEW_RESULT` is `BLOCKED` with blocker `degraded-gate-unreviewed` (forge#3628): the build gate was DEGRADED and the review's `### Degraded Paths Reviewed` section does not name every degraded check. It is a review gap, not a code defect, so do not edit code for it. Keep `needs-human` (Phase 8 set it), do not merge, and exit `REMEDIATE_RESULT: status: BLOCKED` with the same blocker; `work-on:review` R4 owns the single re-review that adds the section.
|
|
516
|
+
|
|
505
517
|
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.
|
|
506
518
|
|
|
507
519
|
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:
|
|
@@ -559,6 +571,7 @@ if [ "$(printf '%s' "$PR_LIVE" | jq -r '.state // ""' 2>/dev/null)" = "MERGED" ]
|
|
|
559
571
|
case "$TIER" in
|
|
560
572
|
universal) bash "$TRANSITION_SCRIPT" {ISSUE_NUMBER} {GH_FLAG} merged ;;
|
|
561
573
|
prose|*)
|
|
574
|
+
gh label create "workflow:remediating" --color "C5DEF5" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
562
575
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:merged" --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
|
|
563
576
|
;;
|
|
564
577
|
esac
|
|
@@ -661,6 +674,8 @@ The strict `APPROVED_COUNT >= 2` verified-human requirement does NOT apply here:
|
|
|
661
674
|
|
|
662
675
|
A `RESUME_AT_M7=true` run on a deploy-gate base NEVER meets this bar (`BAR_MET=false` is enforced in the `if` below): no M3 gate ran in the resume invocation, so it holds for a human. <!-- Added: forge#3684 -->
|
|
663
676
|
|
|
677
|
+
**CI pending is a hold (forge#4018)**: `wait-ci-green.sh` rc 3 (pending) or rc 4 (rate-limited) at the CI gate below sets `RE_GATE_OUTCOME=HELD-AWAITING-MERGE` with `HOLD_REASON="CI pending..."` and posts no refusal and no `needs-human`; only rc 1/2 and the head-binding refusal re-escalate.
|
|
678
|
+
|
|
664
679
|
**Evaluate the base-scoped bar**:
|
|
665
680
|
```bash
|
|
666
681
|
# Fresh shell: re-derive the live base (as above); :-true fail-closed default.
|
|
@@ -689,6 +704,8 @@ if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" {PR_NUMBE
|
|
|
689
704
|
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
690
705
|
echo "$CI_GATE_OUT"
|
|
691
706
|
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
707
|
+
# rc 3 (TIMEOUT: checks still pending) and rc 4 (RATE_LIMITED: GitHub throttled or briefly down) are waits, not failures: captured BEFORE the local head check below reuses rc 4.
|
|
708
|
+
CI_WAIT=false; case "$CI_GATE_RC" in 3|4) CI_WAIT=true ;; esac
|
|
692
709
|
# forge#3726 / forge#3686: on EVERY path the merge must land the exact SHA the approval was bound to. Reload the persisted head (never re-read the PR here); unset/invalid fails closed.
|
|
693
710
|
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
|
|
694
711
|
APPROVED_HEAD=$(head -n 1 "$_SD/remediate_approved_head_{PR_NUMBER}" 2>/dev/null || true)
|
|
@@ -698,8 +715,10 @@ if [ "$CI_GATE_RC" -eq 0 ] && { [ -z "${APPROVED_HEAD:-}" ] || [ "$GATED_HEAD" !
|
|
|
698
715
|
CI_GATE_OUT="${CI_GATE_OUT}
|
|
699
716
|
CI_GATE: head ${GATED_HEAD:-unknown} is not the approved head ${APPROVED_HEAD:-none} (fail closed)"; CI_GATE_RC=4
|
|
700
717
|
fi
|
|
701
|
-
|
|
702
|
-
|
|
718
|
+
if [ "$CI_WAIT" = "true" ]; then
|
|
719
|
+
# CI pending / rate-limited: hold, never needs-human, no refusal comment. The engine polls CI (no model) and re-dispatches this resume.
|
|
720
|
+
RE_GATE_OUTCOME="HELD-AWAITING-MERGE"; HOLD_REASON="CI pending (gate rc=${CI_GATE_RC}); nothing to fix, the re-gate resumes once CI settles"
|
|
721
|
+
elif [ "$CI_GATE_RC" -ne 0 ]; then
|
|
703
722
|
# Not green: do not land. Re-escalate with the gate output (counts as RE-ESCALATED below).
|
|
704
723
|
CI_MSG="⛔ Remediation auto-land refused for PR #{PR_NUMBER}: CI is not green (rc=${CI_GATE_RC}).
|
|
705
724
|
\`\`\`
|
|
@@ -717,6 +736,7 @@ if [ "$MERGE_STATE" = "MERGED" ]; then
|
|
|
717
736
|
case "$TIER" in
|
|
718
737
|
universal) bash "$TRANSITION_SCRIPT" {ISSUE_NUMBER} {GH_FLAG} merged ;;
|
|
719
738
|
prose|*)
|
|
739
|
+
gh label create "workflow:remediating" --color "C5DEF5" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
720
740
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:merged" --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
|
|
721
741
|
;;
|
|
722
742
|
esac
|
|
@@ -734,7 +754,7 @@ fi
|
|
|
734
754
|
|
|
735
755
|
## Phase M8: Finalize FORGE:REMEDIATION Paper Trail
|
|
736
756
|
|
|
737
|
-
Post the completion body to **both** `{PR_NUMBER}` and `{ISSUE_NUMBER}` — this is the single idempotency marker checked by Phase M0 (this file, on future resume) and by the orchestrator's item 6.4 dispatch guard. **Exception (forge#3685)**: a `RESUME_AT_M7=true` run whose outcome is `HELD-AWAITING-MERGE` posts a `<!-- FORGE:REMEDIATION_HELD -->` record (carrying `**Re-gate outcome**` and the hold reason) and NO `FORGE:REMEDIATION:COMPLETE`, so a transient hold (CI pending, stale approval) does not strand the PR behind the single-attempt guard. AUTO-LANDED, RE-ESCALATED, UNFIXABLE, REREVIEW-REQUIRED and non-resume HELD keep COMPLETE:
|
|
757
|
+
Post the completion body to **both** `{PR_NUMBER}` and `{ISSUE_NUMBER}` — this is the single idempotency marker checked by Phase M0 (this file, on future resume) and by the orchestrator's item 6.4 dispatch guard. **Exception (forge#3685)**: a `RESUME_AT_M7=true` run, or a first run held on `CI pending`, whose outcome is `HELD-AWAITING-MERGE` posts a `<!-- FORGE:REMEDIATION_HELD -->` record (carrying `**Re-gate outcome**` and the hold reason) and NO `FORGE:REMEDIATION:COMPLETE`, so a transient hold (CI pending, stale approval) does not strand the PR behind the single-attempt guard. AUTO-LANDED, RE-ESCALATED, UNFIXABLE, REREVIEW-REQUIRED, INPR-FIX-REQUESTED and non-resume HELD (other than CI pending) keep COMPLETE:
|
|
738
758
|
|
|
739
759
|
```bash
|
|
740
760
|
# Fresh shell: re-derive the live base (as above); :-true fail-closed default.
|
|
@@ -757,9 +777,14 @@ case "$RE_GATE_OUTCOME" in
|
|
|
757
777
|
RE-ESCALATED) AUTO_LAND_BAR_TEXT="N/A — re-escalated before the bar was evaluated"; OUTCOME_DETAIL="at needs-human" ;;
|
|
758
778
|
UNFIXABLE) AUTO_LAND_BAR_TEXT="N/A — unfixable (see Phase M1 classification)"; OUTCOME_DETAIL="at needs-human" ;;
|
|
759
779
|
REREVIEW-REQUIRED) AUTO_LAND_BAR_TEXT="N/A — re-review not run (no sub-agent dispatch tool in this session)"; OUTCOME_DETAIL="at workflow:in-review, re-review required from a session with dispatch" ;;
|
|
760
|
-
|
|
780
|
+
INPR-FIX-REQUESTED) AUTO_LAND_BAR_TEXT="N/A — in-pr fix requested by the re-review (forge#4021)"; OUTCOME_DETAIL="at workflow:in-review, in-pr fix round ${INPR_ROUND:-1}" ;;
|
|
781
|
+
*) echo "remediate: unknown RE_GATE_OUTCOME '${RE_GATE_OUTCOME}' (a known state must have its own arm above)" >&2; AUTO_LAND_BAR_TEXT="N/A"; OUTCOME_DETAIL="" ;;
|
|
761
782
|
esac
|
|
762
783
|
|
|
784
|
+
# forge#4021: machine-readable in-PR round for the engine (empty for every other outcome).
|
|
785
|
+
INPR_ROUND_LINE=""; [ "$RE_GATE_OUTCOME" = "INPR-FIX-REQUESTED" ] && INPR_ROUND_LINE="**INPR round**: ${INPR_ROUND:-1}
|
|
786
|
+
"
|
|
787
|
+
|
|
763
788
|
# forge#3413: embed the pushed head so the orchestrator scopes a REREVIEW-REQUIRED trail to the CURRENT head only.
|
|
764
789
|
REMEDIATED_HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || true)
|
|
765
790
|
|
|
@@ -794,8 +819,9 @@ if [ "$BASESYNC_KIND" = "BASESYNC_REMEDIATION" ]; then
|
|
|
794
819
|
fi
|
|
795
820
|
fi
|
|
796
821
|
|
|
797
|
-
|
|
798
|
-
|
|
822
|
+
_CI_HOLD=false; case "${HOLD_REASON:-}" in "CI pending"*) _CI_HOLD=true ;; esac
|
|
823
|
+
if { [ "${RESUME_AT_M7:-false}" = "true" ] || [ "$_CI_HOLD" = "true" ]; } && [ "$RE_GATE_OUTCOME" = "HELD-AWAITING-MERGE" ]; then
|
|
824
|
+
# forge#3685: a resume (or a first-run CI-pending hold) that ends HELD is NOT a completed attempt. Post a distinct, non-terminal record: header is
|
|
799
825
|
# FORGE:REMEDIATION_HELD (never "<!-- FORGE:REMEDIATION -->", so the M0 partial-trail cleanup and the COMPLETE/TRAIL
|
|
800
826
|
# matchers never see it) and NO FORGE:REMEDIATION:COMPLETE, so the next invocation re-enters M7 instead of ALREADY_DONE.
|
|
801
827
|
REMEDIATION_BODY="<!-- FORGE:REMEDIATION_HELD -->
|
|
@@ -803,7 +829,7 @@ if [ "${RESUME_AT_M7:-false}" = "true" ] && [ "$RE_GATE_OUTCOME" = "HELD-AWAITIN
|
|
|
803
829
|
|
|
804
830
|
**Re-gate outcome**: ${RE_GATE_OUTCOME} ${OUTCOME_DETAIL}
|
|
805
831
|
**Auto-land bar**: ${AUTO_LAND_BAR_TEXT}
|
|
806
|
-
**Hold reason**: ${HOLD_REASON:-
|
|
832
|
+
**Hold reason**: ${HOLD_REASON:-approval not bound to the live head, deploy-gate base, or a CI wait (set HOLD_REASON in M7; see its output above)}
|
|
807
833
|
**Head**: ${REMEDIATED_HEAD_SHA:-unknown}"
|
|
808
834
|
else
|
|
809
835
|
REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
@@ -813,7 +839,7 @@ REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
|
813
839
|
**Re-review verdict**: ${RE_REVIEW_VERDICT:-unknown}
|
|
814
840
|
**Auto-land bar**: ${AUTO_LAND_BAR_TEXT}
|
|
815
841
|
**Re-gate outcome**: ${RE_GATE_OUTCOME} ${OUTCOME_DETAIL}
|
|
816
|
-
**Head**: ${REMEDIATED_HEAD_SHA:-unknown}
|
|
842
|
+
${INPR_ROUND_LINE}**Head**: ${REMEDIATED_HEAD_SHA:-unknown}
|
|
817
843
|
${BASESYNC_LINE}
|
|
818
844
|
<!-- FORGE:REMEDIATION:COMPLETE -->"
|
|
819
845
|
fi
|
|
@@ -847,9 +873,9 @@ REMEDIATE_RESULT:
|
|
|
847
873
|
status: COMPLETE | ALREADY_DONE | UNFIXABLE | BLOCKED | REREVIEW_REQUIRED
|
|
848
874
|
pr_number: {PR_NUMBER}
|
|
849
875
|
issue_number: {ISSUE_NUMBER}
|
|
850
|
-
re_gate_outcome: AUTO-LANDED | HELD-AWAITING-MERGE | RE-ESCALATED | UNFIXABLE | REREVIEW-REQUIRED | N/A
|
|
876
|
+
re_gate_outcome: AUTO-LANDED | HELD-AWAITING-MERGE | RE-ESCALATED | UNFIXABLE | REREVIEW-REQUIRED | INPR-FIX-REQUESTED | N/A
|
|
851
877
|
findings_addressed: [{finding_number}, ...]
|
|
852
878
|
blocker: {description if status=BLOCKED; `deferred-live-run: ...` when M0 deferred to a live run}
|
|
853
879
|
```
|
|
854
880
|
|
|
855
|
-
**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 (a held resume, which wrote `FORGE:REMEDIATION_HELD` and not COMPLETE, can be re-invoked by an operator later and re-enters Phase M7): 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`).
|
|
881
|
+
**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 `re_gate_outcome: INPR-FIX-REQUESTED` (`status: COMPLETE`, forge#4021) the caller dispatches one further in-PR fix remediation (engine kind `inpr-fix`, bounded by `INPR_REMEDIATION`) and never parks the first request at `needs-human`. For every other `re_gate_outcome`, this result is terminal for the current invocation (a held resume, which wrote `FORGE:REMEDIATION_HELD` and not COMPLETE, can be re-invoked by an operator later and re-enters Phase M7): 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`).
|
|
@@ -188,6 +188,25 @@ If `LAST_VERDICT_SHA` is empty (no prior CHANGES REQUESTED verdict found) or dif
|
|
|
188
188
|
|
|
189
189
|
---
|
|
190
190
|
|
|
191
|
+
## Pre-Handoff Base Sync (before the R1 ancestry guard and the first push) <!-- Added: forge#3969 -->
|
|
192
|
+
|
|
193
|
+
Never hand off a PR born stale. If `origin/{PR_BASE}` exists and is not an ancestor of `{BRANCH}`, merge it now, while the build context is warm. Merge only: no rebase, no force.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
cd {WORKTREE_PATH}
|
|
197
|
+
PRE_SYNC_TREE=$(git rev-parse 'HEAD^{tree}') # the tree the build-time QUALITY_GATE PASS is bound to
|
|
198
|
+
if git fetch origin {PR_BASE} >/dev/null 2>&1 && ! git merge-base --is-ancestor origin/{PR_BASE} HEAD 2>/dev/null; then
|
|
199
|
+
git merge --no-edit origin/{PR_BASE} >/dev/null 2>&1 || { echo "BASE SYNC CONFLICT: resolve in this context, then git commit; on doubt git merge --abort and return BLOCKED + needs-human"; git diff --name-only --diff-filter=U; }
|
|
200
|
+
fi
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
On conflict, resolve every file, keep both sides' intent, `git add`, `git commit --no-edit`. If not resolvable with confidence: `git merge --abort`, label `needs-human`, return `REVIEW_RESULT: status: BLOCKED` with blocker "pre-handoff base sync conflict". One bounded round; resolve SHAs only after it.
|
|
204
|
+
|
|
205
|
+
**Re-gate the merged head** <!-- Added: forge#3983 -->: the `FORGE:QUALITY_GATE` PASS is bound to `HEAD^{tree}` (R1.5 `--head-tree`), so a merge that changes the tree invalidates it. After the merge and any conflict resolution, compare `PRE_SYNC_TREE` with the current `HEAD^{tree}` (fail closed if either cannot be resolved):
|
|
206
|
+
|
|
207
|
+
- Unchanged: do nothing; never re-gate.
|
|
208
|
+
- Changed: run the scoped quality gate on the merged head here (`work-on/build/validate` V1 loop over the PR's changed files), then post the marker via validate.md V5 Step 1 "Marker-only re-gate mode" (`**Tree**` = merged `HEAD^{tree}`, `**Commands run**` = what ran). Post the real result: on FAIL, fix and re-run within the validate bounds, else add `needs-human` and return `REVIEW_RESULT: status: BLOCKED`, blocker "post-sync quality gate failed"; never post PASS without a run.
|
|
209
|
+
|
|
191
210
|
## Phase R1: Pre-Push Ancestry Guard
|
|
192
211
|
|
|
193
212
|
Before pushing, verify the branch contains no merge commits that bring in history from outside the PR base. Merging `origin/{PR_BASE}` itself into the branch (a base sync) is allowed: every non-first parent of each merge in `origin/{PR_BASE}..{BRANCH}` must be an ancestor of `origin/{PR_BASE}`. This is the final defense against milestone-code-onto-staging contamination.
|
|
@@ -308,7 +327,7 @@ Print the block above (`status: BLOCKED`, blocker: "git push failed") and STOP.
|
|
|
308
327
|
|
|
309
328
|
## Phase R1.5: Phase-Trail Preflight (MANDATORY — before PR creation) <!-- Added: forge#3061 -->
|
|
310
329
|
|
|
311
|
-
A PR must not be opened for work whose earlier phases were skipped. Run the deterministic verifier; it requires INVESTIGATOR, FAST_PATH and CONTRACT always, CONTEXT and ARCHITECT unless the band is TRIVIAL/INVESTIGATION, and a passing FORGE:QUALITY_GATE unless the diff is docs-only.
|
|
330
|
+
A PR must not be opened for work whose earlier phases were skipped. Run the deterministic verifier; it requires INVESTIGATOR, FAST_PATH and CONTRACT always, CONTEXT and ARCHITECT unless the band is TRIVIAL/INVESTIGATION, and a passing FORGE:QUALITY_GATE unless the diff is docs-only. The PASS must be bound to the current `HEAD^{tree}`; the Pre-Handoff Base Sync re-gates a merged head, so re-dispatch here is only a backstop. <!-- Added: forge#3983 -->
|
|
312
331
|
|
|
313
332
|
```bash
|
|
314
333
|
# --no-renames: list BOTH sides of a rename so a file moved into docs/ cannot hide its source path (forge#3145).
|
|
@@ -440,7 +459,9 @@ Use the existing PR number and continue.
|
|
|
440
459
|
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
441
460
|
case "$TIER" in
|
|
442
461
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
|
|
443
|
-
prose) gh
|
|
462
|
+
prose) gh label create "workflow:remediating" --color "C5DEF5" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
463
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
464
|
+
gh issue edit {NUMBER} {GH_FLAG} --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 ;;
|
|
444
465
|
esac
|
|
445
466
|
```
|
|
446
467
|
|
|
@@ -547,20 +568,25 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
547
568
|
|
|
548
569
|
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker mentions the phase trail (any blocker containing "phase trail": "phase trail unreadable" when the Phase 8 verifier exited ≥2 / 127, "phase trail incomplete…", or "auto-merge requires --issue", forge#3147): the merge gate refused or could not run. Do NOT re-run phases (nothing is missing) and do NOT attempt the manual merge below, because that would bypass the gate. Add `needs-human` and return `REVIEW_RESULT: status: BLOCKED` with the same blocker. The PR stays open and unmerged.
|
|
549
570
|
|
|
550
|
-
- `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),
|
|
571
|
+
- `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), re-gate the new head per the Pre-Handoff Base Sync "Re-gate the merged head" procedure (the quality-gate skill alone posts no marker), then re-invoke Phase R3 once — a review of the new head (scoped by /review-pr: full, delta or carried forward). 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 -->
|
|
551
572
|
|
|
552
573
|
- **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`:
|
|
553
574
|
```bash
|
|
575
|
+
# forge#3567: ensure the label first, add before remove
|
|
576
|
+
gh label create "workflow:remediating" --color "C5DEF5" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
554
577
|
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
555
578
|
case "$TIER" in
|
|
556
579
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} remediating ;;
|
|
557
|
-
prose) gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:remediating"
|
|
580
|
+
prose) gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:remediating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
581
|
+
gh issue edit {NUMBER} {GH_FLAG} --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
|
|
558
582
|
esac
|
|
559
583
|
gh issue edit {NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
560
584
|
gh pr edit {PR_NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
561
585
|
```
|
|
562
586
|
|
|
563
|
-
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "
|
|
587
|
+
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "degraded-gate-unreviewed" (the build gate was DEGRADED, a check ran nothing, and no trusted `### Degraded Paths Reviewed` PR section names every degraded check; forge#3628): do NOT merge by any other route, and do NOT leave `needs-human` because of this gate on the first pass. Remove the `needs-human` that Phase 8 set, then re-review once on its own bound (forge#3996; never the `STALE_REREVIEW` bound, which other causes consume): count `<!-- FORGE:DEGRADED_REREVIEW: pr={PR_NUMBER} -->` (the `DEGRADED_REREVIEW` bound below), post the marker, and re-invoke Phase R3 once so `/review-pr` reads the degraded list from the issue's FORGE:QUALITY_GATE and adds the section. If the bound is already used, add `needs-human` and return `REVIEW_RESULT: status: BLOCKED`, blocker "degraded-gate-unreviewed after re-review".
|
|
588
|
+
|
|
589
|
+
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "checks still pending" (Phase 8 merge-gate timed out while CI was still running; nothing failed, #3919): do NOT remediate and do NOT add `needs-human`. Return that BLOCKED result unchanged so the caller retries the review phase, whose merge gate re-runs once CI finishes. The engine (`forgedock run-issue`) is that caller: it recognises the blocker, polls CI with `scripts/wait-ci-green.sh` and backoff (no model calls, bounded by `engine.ci_pending_max_minutes`, default 90; a rate-limited gate, `wait-ci-green.sh` rc 4, is waited out the same way, forge#4018), then re-dispatches this review phase once; budget exhaustion ends `needs-human` with `review.ci_pending_timeout`. The router and interactive caller path is unchanged.
|
|
564
590
|
|
|
565
591
|
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "ci gate" (Phase 8 refused to merge because checks failed or were cancelled): 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 re-reviews the new head (scoped by /review-pr: full, delta or carried forward), and auto-lands through the same CI gate. <!-- forge#3191 -->
|
|
566
592
|
|
|
@@ -569,32 +595,27 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
569
595
|
- **Bound already used** (including the re-invocation after a remediation that did not land): first read `max_severity=` from the latest trusted `FORGE:INPR_FIX` marker on the PR (`gh pr view {PR_NUMBER} {GH_FLAG} --json comments`, author-trusted as elsewhere in this phase; absent means MEDIUM). **HIGH/CRITICAL are never waived**: remove `workflow:remediating`, label the issue `needs-human`, leave the PR unmerged and return `REVIEW_RESULT: status: BLOCKED` with blocker "in-pr fix not landed (HIGH/CRITICAL, no waiver)". The `FORGE:INPR_FIX` work order stays on the PR for the human. For MEDIUM, 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`.
|
|
570
596
|
```bash
|
|
571
597
|
INPR_WAIVE_HEAD=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq .headRefOid)
|
|
572
|
-
gh
|
|
598
|
+
gh label create "workflow:in-review" --color "5319E7" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
599
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
600
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
601
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:remediating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
573
602
|
gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:INPR_FIX_WAIVED: head=${INPR_WAIVE_HEAD} -->
|
|
574
603
|
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
|
|
575
604
|
```
|
|
576
605
|
|
|
577
|
-
Persisted loop bounds for the cases above — count first, then act (`{BOUND}` is `STALE_REREVIEW`, `CI_REMEDIATION`, `INPR_REMEDIATION` or `BASESYNC_REMEDIATION`). This phase posts only the `STALE_REREVIEW`
|
|
606
|
+
Persisted loop bounds for the cases above — count first, then act (`{BOUND}` is `STALE_REREVIEW`, `DEGRADED_REREVIEW`, `CI_REMEDIATION`, `INPR_REMEDIATION` or `BASESYNC_REMEDIATION`). This phase posts only the `STALE_REREVIEW` and `DEGRADED_REREVIEW` markers; `CI_REMEDIATION`, `INPR_REMEDIATION` and `BASESYNC_REMEDIATION` are posted by the router in Phase 4R, immediately before it invokes remediation (forge#3398):
|
|
578
607
|
```bash
|
|
579
608
|
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] bound check only"; }
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
--jq '[.[] | select((.body | contains("FORGE:STALE_REREVIEW:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
583
|
-
elif [ "{BOUND}" = "INPR_REMEDIATION" ]; then
|
|
584
|
-
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
585
|
-
--jq '[.[] | select((.body | contains("FORGE:INPR_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
586
|
-
elif [ "{BOUND}" = "BASESYNC_REMEDIATION" ]; then
|
|
587
|
-
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
588
|
-
--jq '[.[] | select((.body | contains("FORGE:BASESYNC_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
589
|
-
else
|
|
590
|
-
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
591
|
-
--jq '[.[] | select((.body | contains("FORGE:CI_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
592
|
-
fi
|
|
609
|
+
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
610
|
+
--jq '[.[] | select((.body | contains("FORGE:{BOUND}:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
593
611
|
if [ "$BOUND_COUNT" -ge 1 ]; then
|
|
594
612
|
echo "BOUND_EXHAUSTED: {BOUND} already used for PR #{PR_NUMBER}"
|
|
595
613
|
elif [ "{BOUND}" = "STALE_REREVIEW" ] && [ "${DRY_RUN:-false}" != "true" ]; then
|
|
596
614
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:STALE_REREVIEW: pr={PR_NUMBER} -->
|
|
597
615
|
Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head once." 2>/dev/null || true
|
|
616
|
+
elif [ "{BOUND}" = "DEGRADED_REREVIEW" ] && [ "${DRY_RUN:-false}" != "true" ]; then
|
|
617
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DEGRADED_REREVIEW: pr={PR_NUMBER} -->
|
|
618
|
+
Degraded-gate review on PR #{PR_NUMBER}: re-reviewing once so the review names each degraded check." 2>/dev/null || true
|
|
598
619
|
fi
|
|
599
620
|
```
|
|
600
621
|
`BOUND_EXHAUSTED` → take the "already ≥ 1" branch of that case.
|
|
@@ -603,7 +624,7 @@ Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head on
|
|
|
603
624
|
- **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).
|
|
604
625
|
- **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.
|
|
605
626
|
|
|
606
|
-
- 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**:
|
|
627
|
+
- PR NOT MERGED (and not a phase-trail, auto-merge-gate, stale-review, degraded-gate-unreviewed, 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**:
|
|
607
628
|
```bash
|
|
608
629
|
# CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
|
|
609
630
|
# green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
|
|
@@ -617,7 +638,7 @@ Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head on
|
|
|
617
638
|
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
618
639
|
echo "$CI_GATE_OUT"
|
|
619
640
|
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
620
|
-
# rc 3 = CI still running: re-run this block (up to 3 more times) before treating
|
|
641
|
+
# rc 3 = CI still running, rc 4 = GitHub rate-limited/unavailable (forge#4018): both are waits, not failures; re-run this block (up to 3 more times) before treating either as a failure.
|
|
621
642
|
# Note-disposition gate (same rule as review-pr.md Phase 8): findings without a recorded
|
|
622
643
|
# §6B.5 disposition must not merge, because that step is what bounds the review-finding cascade.
|
|
623
644
|
DISPO_JSON=$(gh api --paginate "repos/{GH_REPO}/issues/{PR_NUMBER}/comments" 2>/dev/null) || DISPO_JSON=""
|
package/commands/work-on.md
CHANGED
|
@@ -21,7 +21,7 @@ Orchestrator for the full issue lifecycle: investigate → decompose (if needed)
|
|
|
21
21
|
|
|
22
22
|
1. **This file is a router. Every phase runs as a forked sub-skill via `Skill(...)`; the router never executes phase logic itself.** Each phase sub-skill (`work-on:investigate`, `work-on:decompose`, `work-on:build`, `work-on:review`, `work-on:close`, `work-on:remediate`) declares `context: fork`, so Claude Code runs it in an isolated sub-agent context that holds only that phase's spec and the args you pass. The phase re-reads everything else from GitHub/git and returns exactly one `*_RESULT:` block — that block is all the router sees. Do not read phase spec files, do not re-implement a phase step here, and do not "help" a phase by posting its markers or labels. If a skill name does not resolve, STOP with "skill not found" (see Skill Name Resolution) — never run a phase inline.
|
|
23
23
|
|
|
24
|
-
1a. **Dispatching phases are invoked only by this router.** `work-on:review` and `work-on:remediate` spawn sub-agents (the `/review-pr` domain reviewers), and Claude Code only grants the `Agent` tool down to a fixed depth (see Depth Budget). Invoking them from here keeps them one level below the router, where `Agent` works. No phase may invoke them; a phase that needs one returns `status: NEXT` with `next: <phase>`, and the router runs it (Phase 4R). Non-dispatching phases may nest forks freely. <!-- forge#3398 -->
|
|
24
|
+
1a. **Dispatching phases are invoked only by this router.** `work-on:review` and `work-on:remediate` spawn sub-agents (the `/review-pr` domain reviewers), and `work-on:build` spawns the build-time self-review reviewers (B5.6, forge#3971; it degrades to a recorded skip when `Agent` is withheld), and Claude Code only grants the `Agent` tool down to a fixed depth (see Depth Budget). Invoking them from here keeps them one level below the router, where `Agent` works. No phase may invoke them; a phase that needs one returns `status: NEXT` with `next: <phase>`, and the router runs it (Phase 4R). Non-dispatching phases may nest forks freely. <!-- forge#3398 -->
|
|
25
25
|
|
|
26
26
|
2. **Route only on `*_RESULT` blocks and GitHub state.** After each phase returns, apply the routing table for that phase (below). Never infer success from a phase's prose; never continue past a `BLOCKED`/`FAILED` result.
|
|
27
27
|
|
|
@@ -103,11 +103,11 @@ Layers are counted below the main conversation (layer 0). Claude Code grants the
|
|
|
103
103
|
|-----------------------------|-----------|-------------------|
|
|
104
104
|
| 0 / 0 | solo `/work-on` router / `/orchestrate` | orchestrate: one worker per issue |
|
|
105
105
|
| — / 1 | `/work-on` router (orchestrated worker) | no (`Skill` only) |
|
|
106
|
-
| 1 / 2 | every phase invoked by the router, including `work-on:review` and `work-on:remediate` | review/remediate: `/review-pr` domain reviewers |
|
|
106
|
+
| 1 / 2 | every phase invoked by the router, including `work-on:build`, `work-on:review` and `work-on:remediate` | build: self-review reviewers (B5.6); review/remediate: `/review-pr` domain reviewers |
|
|
107
107
|
| 2 / 3 | domain reviewers (leaves); build children (context, architect, implement, validate) | no |
|
|
108
108
|
| 3 / 4 | quality gate (inside validate or review) | no |
|
|
109
109
|
|
|
110
|
-
The only layer constraint that matters is the spawner: `work-on:review` and `work-on:remediate` must sit at layer ≤ 2, which Hard Rule 1a guarantees. The Phase 0A spawn-depth preflight checks the effective depth before any work starts.
|
|
110
|
+
The only layer constraint that matters is the spawner: `work-on:build`, `work-on:review` and `work-on:remediate` must sit at layer ≤ 2, which Hard Rule 1a guarantees. The Phase 0A spawn-depth preflight checks the effective depth before any work starts.
|
|
111
111
|
|
|
112
112
|
### Model and Effort Tiering — What Actually Applies
|
|
113
113
|
|
|
@@ -277,7 +277,7 @@ If detected, dispatch immediately and STOP — do NOT fall through to Phase 0B's
|
|
|
277
277
|
Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="${REMEDIATE_PR_NUMBER} ${REMEDIATE_ISSUE_FLAG} --repo {GH_REPO} --gh-flag {GH_FLAG}")
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
-
**If `REMEDIATE_RESULT: status: REREVIEW_REQUIRED`** (forge#3240, fallback only since forge#3398: fix pushed, but remediation had no sub-agent dispatch tool, which cannot happen when the Phase 0A spawn-depth preflight passed and remediation is invoked from this router): run the re-review from this top-level session, which has dispatch, after the **claim pre-check** below — `Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="${REMEDIATE_PR_NUMBER} --auto-merge --issue ${ISSUE} --base ${PR_BASE} --gh-flag {GH_FLAG}")` (`ISSUE` and `PR_BASE` from the result and the PR) — never review inline. If it returns `REVIEW_RESULT: status: COMPLETE` (merged), invoke `Skill("{FORGE_SKILL_PREFIX}work-on:close", ...)` as remediate M8 does for `AUTO-LANDED`. If the claim cannot be recorded, the `review-pr` skill does not resolve, cannot dispatch, or returns anything other than `REVIEW_RESULT: status: COMPLETE` while the issue is still at `workflow:in-review` with no `needs-human` (no state anything will advance), apply the **terminal fallback**. Single attempt: no retry loop, no polling. Then STOP. <!-- Added: forge#3406; claim pre-check forge#3413 -->
|
|
280
|
+
**If `REMEDIATE_RESULT: status: REREVIEW_REQUIRED`** (forge#3240, fallback only since forge#3398: fix pushed, but remediation had no sub-agent dispatch tool, which cannot happen when the Phase 0A spawn-depth preflight passed and remediation is invoked from this router): run the re-review from this top-level session, which has dispatch, after the **claim pre-check** below — `Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="${REMEDIATE_PR_NUMBER} --auto-merge --issue ${ISSUE} --base ${PR_BASE} --gh-flag {GH_FLAG}")` (`ISSUE` and `PR_BASE` from the result and the PR) — never review inline. If it returns `REVIEW_RESULT: status: COMPLETE` (merged), invoke `Skill("{FORGE_SKILL_PREFIX}work-on:close", ...)` as remediate M8 does for `AUTO-LANDED`. If the claim cannot be recorded, the `review-pr` skill does not resolve, cannot dispatch, or returns anything other than `REVIEW_RESULT: status: COMPLETE` while the issue is still at `workflow:in-review` with no `needs-human` (no state anything will advance), apply the **terminal fallback**. **Base-conflict mapping (forge#3516)**: if `review-pr` returns `REVIEW_RESULT: status: BLOCKED` with a blocker containing `base-conflict`, it added no label (the base sync is pipeline work), so this router maps it before the claim release: re-read the labels first (closed, already `needs-human`, or no longer `workflow:in-review` means skip and report that state; idempotent); unless `DRY_RUN=true` (then record "would set workflow:remediating"), run `transition-label.sh ${ISSUE} {GH_FLAG} remediating` and re-read the labels to verify `workflow:remediating`. The next router pass (Phase 4R) runs the bounded `FORGE:BASESYNC_REMEDIATION` round via `work-on:remediate`. If the transition fails or is not verified, apply the **terminal fallback** (`needs-human`) and do not release the claim as settled. Never fall through to a manual merge. A `base-conflict` combined with other blockers is reported by the script as a `needs-human` outcome, so this mapping does not fire for it. Single attempt: no retry loop, no polling. Then STOP. <!-- Added: forge#3406; claim pre-check forge#3413 -->
|
|
281
281
|
|
|
282
282
|
**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):
|
|
283
283
|
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.
|
package/docs/CONFIG.md
CHANGED
|
@@ -481,13 +481,17 @@ engine:
|
|
|
481
481
|
| `<key>_model` | string | No | Model alias (`sonnet`, `opus`, `haiku`) or full model id passed as `--model`. Invalid values are ignored. |
|
|
482
482
|
| `<key>_max_turns` | integer | No | Positive integer passed as `--max-turns`. Invalid values are ignored. |
|
|
483
483
|
|
|
484
|
+
| `ci_pending_max_minutes` | integer | No | Minutes the engine waits for CI when the review phase ends BLOCKED with "checks still pending", or the remediate phase holds at its M7 re-gate on "CI pending" (the merge gate's wait window ended while CI was still running). Default: 90. Invalid values are ignored. This key is flat like the others but is not part of the `<key>_model` / `<key>_max_turns` pattern. |
|
|
485
|
+
|
|
486
|
+
**CI still pending**: the engine polls `scripts/wait-ci-green.sh` (no model calls) one read per poll on a 60 s, 2 min, 4 min, 5 min (cap) backoff with +/-20% jitter, while the lease keeps renewing, then re-dispatches the phase that was waiting, review or remediate (idempotent at an unchanged head: review re-runs the merge gate and merges; remediate resumes at its M7 re-gate). Before each poll it reads `gh api rate_limit` (not itself rate-limited) and, below 10% of the REST budget, sleeps until the window resets instead of polling; a rate-limited or transient (5xx, network) poll is retried and never re-dispatches the phase. Red CI takes the normal ci-gate path. When the budget is exhausted the run ends `needs-human` with outcome code `review.ci_pending_timeout`; `--retry` resumes. Each wait emits a `ci_pending_wait` telemetry event (`docs/TELEMETRY.md`) with the minutes waited, polls made and rate-limit sleeps.
|
|
487
|
+
|
|
484
488
|
**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.
|
|
485
489
|
|
|
486
490
|
**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.
|
|
487
491
|
|
|
488
492
|
**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.
|
|
489
493
|
|
|
490
|
-
**Per-run scratch dir** (`tmp_root`): every engine child runs with `TMPDIR` and `CLAUDE_CODE_TMPDIR` set to `<tmp_root>/<issue>` so venvs and Claude session data stay off a size-limited `/tmp`. `engine.tmp_root` is an absolute or `~`-prefixed path; default `~/.forge/tmp`. When `tmp_root` is unset and the operator already exported `TMPDIR` or `CLAUDE_CODE_TMPDIR`, that choice is respected and never cleaned. The dir is removed on a terminal outcome and kept on `engine-error` so `--retry` resumes. A disk-full/quota failure (EDQUOT, ENOSPC, errno -28/-122) ends a retryable `engine-error` (outcome code `infra.disk_quota`), never `needs-human`.
|
|
494
|
+
**Per-run scratch dir** (`tmp_root`): every engine child runs with `TMPDIR` and `CLAUDE_CODE_TMPDIR` set to `<tmp_root>/<repo-key>/<issue>` so venvs and Claude session data stay off a size-limited `/tmp`. `engine.tmp_root` is an absolute or `~`-prefixed path; default `~/.forge/tmp`. The `<repo-key>` is the checkout's directory name plus 8 hex characters of a hash of its real path, so the same issue number in two repos never shares a dir; the dir is created `0700` and an existing symlink or dir owned by another user is not used (the child keeps the inherited tmp). Old `<tmp_root>/<issue>` dirs from earlier versions are not migrated. When `tmp_root` is unset and the operator already exported `TMPDIR` or `CLAUDE_CODE_TMPDIR`, that choice is respected and never cleaned. The dir is removed on a terminal outcome and kept on `engine-error` so `--retry` resumes. A disk-full/quota failure (EDQUOT, ENOSPC, errno -28/-122) ends a retryable `engine-error` (outcome code `infra.disk_quota`), never `needs-human`.
|
|
491
495
|
|
|
492
496
|
**Commands that use this section**: CLI backend (`runCommand` in `bin/runner.mjs`)
|
|
493
497
|
|
|
@@ -736,6 +740,10 @@ verification:
|
|
|
736
740
|
- name: "web"
|
|
737
741
|
container: "acme-web-blue"
|
|
738
742
|
health_url: "http://localhost:3000"
|
|
743
|
+
|
|
744
|
+
consumer_sweep:
|
|
745
|
+
mode: advisory # advisory (default) | blocking
|
|
746
|
+
budget_seconds: 60 # wall-clock budget for the sweep
|
|
739
747
|
```
|
|
740
748
|
|
|
741
749
|
| Field | Type | Required | Description |
|
|
@@ -746,8 +754,91 @@ verification:
|
|
|
746
754
|
| `services[].name` | string | **Yes (per entry)** | Human-readable service name |
|
|
747
755
|
| `services[].container` | string | No | Docker container name for `docker exec` verification |
|
|
748
756
|
| `services[].health_url` | string | No | Local URL for container-level health checks |
|
|
757
|
+
| `ci_local` | object | No | Local CI gate settings (see below) |
|
|
758
|
+
| `consumer_sweep.mode` | string | No | `advisory` (default): the validate consumer sweep (`scripts/consumer-sweep.sh`, V4.6) records unaddressed referencing sites and never fails. `blocking`: unaddressed sites fail validate with `consumer_unaddressed`, build runs one repair round, and sites still unaddressed are recorded as `CONSUMERS: unresolved` and passed to review as known risk (never `needs-human`). Flip to `blocking` only after `bash scripts/consumer-sweep.sh --eval-history 20` shows a mean of at most 2 flagged sites per merge and none above 10 on your repo |
|
|
759
|
+
| `consumer_sweep.budget_seconds` | integer | No | Wall-clock budget for the sweep (default `60`). When it runs out the sweep stops with `BUDGET_EXCEEDED`, reports the partial result, and never fails, in either mode |
|
|
760
|
+
|
|
761
|
+
**Commands that use this section**: `quality-gate`, `validate` (V2 runs `verification.commands`; V4.8 runs the local CI gate; health checks and `consumer_sweep`), `work-on`
|
|
762
|
+
|
|
763
|
+
### `verification.ci_local` (OPTIONAL)
|
|
764
|
+
|
|
765
|
+
Validate Phase V4.8 runs `scripts/ci-local.sh` before review: it derives the repo's pull-request checks from its own CI
|
|
766
|
+
definitions and runs them locally in the build worktree, so deterministic CI failures are fixed in build (at most 2 fix
|
|
767
|
+
iterations; a failure still unfixable after that is DEGRADED by default, see `mode`). It never contacts GitHub and never triggers a remote
|
|
768
|
+
workflow; remote CI still runs once, at merge. Everything works with no configuration.
|
|
769
|
+
|
|
770
|
+
```yaml
|
|
771
|
+
verification:
|
|
772
|
+
ci_local:
|
|
773
|
+
mode: advisory
|
|
774
|
+
exclude_jobs: ["gha:ci:e2e-*"]
|
|
775
|
+
include_jobs: []
|
|
776
|
+
extra_commands:
|
|
777
|
+
- "make lint-docs"
|
|
778
|
+
services: auto
|
|
779
|
+
budget_minutes: 15
|
|
780
|
+
max_parallel: 4
|
|
781
|
+
```
|
|
749
782
|
|
|
750
|
-
|
|
783
|
+
| Field | Type | Default | Description |
|
|
784
|
+
|-------|------|---------|-------------|
|
|
785
|
+
| `mode` | `advisory` \| `blocking` | `advisory` | `advisory`: a runnable job still failing after the 2 fix iterations is DEGRADED (`DEGRADED_CHECKS+=ci-local`), recorded in `FORGE:BUILDER` under `### Local CI unresolved` and surfaced to review; the build is not blocked. `blocking` (opt-in): the build is blocked with `ci-local: <jobs>`. Flip to `blocking` only once telemetry (`ci_local` events) shows a low false-fail rate on your repo |
|
|
786
|
+
| `exclude_jobs` | array of globs | `[]` | Job ids or names to drop from the plan (matched against `gha:<workflow>:<job>`, `gitlab:<job>`, and the job name) |
|
|
787
|
+
| `include_jobs` | array of globs | `[]` | When non-empty, only matching CI jobs run (an allowlist); `extra_commands` and the portability checks (when they apply) always run |
|
|
788
|
+
| `extra_commands` | array of strings or `{name, command}` | `[]` | Extra commands always added to the plan. `verification.commands` is not repeated here: V2 already runs it as the fast pre-step |
|
|
789
|
+
| `services` | `auto` \| `off` | `auto` | `auto` starts a job's service containers with docker (same image, env, ports), tearing them down afterwards; without docker, or with `off`, such jobs are deferred |
|
|
790
|
+
| `budget_minutes` | number | `15` | Overall wall-time budget; jobs not finished or started within it leave the run DEGRADED (never PASS, never FAIL). One invocation is capped at 9 minutes whatever this says (the Bash tool limit is 10): beyond that the run is a partial DEGRADED result and re-running resumes from the result cache (`FORGE_CI_LOCAL_MAX_INVOCATION_SECONDS`, default 540) |
|
|
791
|
+
| `max_parallel` | number | `4` | Jobs run in parallel by one run (also bounded machine-wide, see below) |
|
|
792
|
+
|
|
793
|
+
**Trusted config**: every field above is read from the main checkout's `forge.yaml` (for a plain checkout, the base ref's
|
|
794
|
+
copy), never from the PR's own tree, so a PR cannot exclude jobs from the gate that checks it.
|
|
795
|
+
|
|
796
|
+
**Where checks come from**: GitHub Actions (`.github/workflows`), GitLab CI, CircleCI, Azure Pipelines, Bitbucket Pipelines
|
|
797
|
+
and Buildkite definitions, read from the trusted base ref plus the PR's own edits (a PR can add a check but cannot delete
|
|
798
|
+
one). Only when the repo has NO CI definitions are its task runners used; when CI exists but its path filters leave no job
|
|
799
|
+
applicable the run executes nothing and records `no_applicable_jobs` (a docs-only change never triggers a full suite) (`package.json` scripts, Makefile, justfile,
|
|
800
|
+
Taskfile, tox/nox, pre-commit, pyproject tools, Cargo, Go, Maven, Gradle). Deterministic portability checks (Windows-invalid
|
|
801
|
+
filenames, case collisions, CRLF in scripts, bash 3.2 constructs and GNU-only flags on added lines, plus the repo's
|
|
802
|
+
`scripts/check-shell-portability.sh` when present) run only when the repo's CI definitions target macOS or Windows (a
|
|
803
|
+
`macos-*`/`windows-*` runner, matrix entry or executor); for a Linux-only repo they are skipped with a note, so correct
|
|
804
|
+
Linux-only code is never blocked on bash 3.2 rules.
|
|
805
|
+
|
|
806
|
+
**Deferred to CI** (listed in the `FORGE:BUILDER` comment under `### Deferred to CI`, never guessed): jobs that reference
|
|
807
|
+
`secrets.` or `environment:`, deploy/publish jobs, `pull_request_target`, schedule/dispatch-only workflows, unknown `uses:`
|
|
808
|
+
actions, orbs, plugins and templates, service containers without docker, macOS/Windows variants on another host, matrix
|
|
809
|
+
variants beyond the first host-compatible one, and jobs whose tools are not installed locally. A failed dependency install
|
|
810
|
+
(for example offline) defers the job instead of failing it, and so does a failure of a job whose pinned toolchain differs
|
|
811
|
+
from the host by a major version (`setup-node` 18 vs host 26: version drift, listed with its reason). A job timeout is
|
|
812
|
+
DEGRADED, never FAIL, and its clock starts when the job starts executing, not while it waits for a machine slot.
|
|
813
|
+
|
|
814
|
+
**Exact scoping** (runs only what the change can affect): job-level path filters from the CI definition (including
|
|
815
|
+
`dorny/paths-filter` outputs) gate whole jobs; per-file linters (shellcheck, eslint, prettier, ruff, black, flake8, ...) run
|
|
816
|
+
on the changed files only; tests use the ecosystem selector (`jest --findRelatedTests`, `vitest related`, `go` affected
|
|
817
|
+
packages with reverse dependencies, `cargo test -p` affected crates, Maven `-pl ... -amd`, `nx affected`, `turbo --filter`) or a
|
|
818
|
+
git-grep reference graph for file-list runners (pytest, mocha, `node --test`, bats, rspec, direct `*.test.*` invocations);
|
|
819
|
+
changed test files always run. **Fail-safe widening**: a lockfile, build or lint config, CI definition, unreferenced shared
|
|
820
|
+
fixture, an unknown file type, or an empty test selection for a changed source file widens the step to the full command (the
|
|
821
|
+
reason is reported, never silently narrowed). Short module names (`app/db.py`, `__init__.py`) are matched by their qualified
|
|
822
|
+
import forms. `${{ github.* }}` expressions in `run:` scripts are passed to the script as environment variables, never
|
|
823
|
+
substituted into the script text. CircleCI secondary containers run on a per-job docker network and publish only declared
|
|
824
|
+
(or well-known default) ports on 127.0.0.1, never `--network host`.
|
|
825
|
+
Passing results are cached by input hash under `~/.forge/ci-local/results` and reused across build iterations, lanes and
|
|
826
|
+
issues on the same base. An identical check (command, cwd, shell, env, input hash) that appears in several jobs of one invocation
|
|
827
|
+
runs once; the later jobs reuse its result (`cache=dedup`, a failure is shared). Machine slots are granted in FIFO ticket order
|
|
828
|
+
across invocations (`~/.forge/ci-local.lock/queue`; tickets of dead processes are reaped), and a slot is taken only right before a
|
|
829
|
+
step really executes. Job lines, `CI_LOCAL_CHECK`, `CI_LOCAL_RESULT` and the `ci_local` telemetry report `wait_ms` (time queued
|
|
830
|
+
for a slot) separately from `duration_ms` (execution). Ready jobs run narrow/scoped first, then cheapest first (cached durations).
|
|
831
|
+
|
|
832
|
+
**Environment** (all optional): `FORGE_CI_LOCAL_SLOTS` (machine-wide concurrent jobs across all runs, default
|
|
833
|
+
`max(2, min(8, cpus/2))`; slots live under `~/.forge/ci-local.lock` and stale ones are reclaimed), `FORGE_CI_LOCAL_BUDGET_MINUTES`,
|
|
834
|
+
`FORGE_CI_LOCAL_MAX_PARALLEL`, `FORGE_CI_LOCAL_JOB_TIMEOUT` (seconds, default min(900, budget)), `FORGE_CI_LOCAL_SERVICES=off`,
|
|
835
|
+
`FORGE_CI_LOCAL_MAX_INVOCATION_SECONDS` (default 540), `FORGE_CI_LOCAL_HOME` (default `~/.forge`), `FORGE_CI_LOCAL_NO_CACHE=1`. Jobs run in a runner-like environment: an allowlist of
|
|
836
|
+
system and toolchain variables (PATH, HOME, locale, XDG_*, CARGO_*, GOPATH, JAVA_HOME, proxies, ...) plus `CI=true`; credentials and
|
|
837
|
+
ambient operator state (agent tokens, `FORGE_*`/`CLAUDE_*` settings, git hook variables) never reach them. Extra names can be
|
|
838
|
+
listed in `FORGE_CI_LOCAL_PASSTHROUGH` (comma separated). Requires `node`, `git`, and mikefarah `yq` v4 for YAML CI definitions (without `yq`
|
|
839
|
+
the gate is DEGRADED and reported in `verification_skipped`, never a silent pass).
|
|
840
|
+
|
|
841
|
+
**Commands that use this section**: `work-on` (validate V4.8)
|
|
751
842
|
|
|
752
843
|
---
|
|
753
844
|
|
package/docs/OUTCOME-CODES.md
CHANGED
|
@@ -20,7 +20,8 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
|
|
|
20
20
|
| `review.rereview_skipped` | Re-review was skipped | reserved | No structured evidence source yet |
|
|
21
21
|
| `review.blocking_issue` | Review left the PR open at an unchanged head with a filed blocking ISSUE and handed off to remediate | review | Trusted `FORGE:NOTE_DISPOSITION` on the PR with `findings_filed` > 0 and `reviewed_sha` equal to the live head |
|
|
22
22
|
| `review.pr_open` | Review ended with the PR open and unmerged and no handoff signal | review | Live PR open, no `REVIEW_RESULT` remediate handoff, no `needs-human`/`workflow:remediating` label, no blocking disposition |
|
|
23
|
-
| `
|
|
23
|
+
| `review.ci_pending_timeout` | CI was still pending at the merge gate after the engine's bounded wait (`engine.ci_pending_max_minutes`) ran out | engine | Review ended `REVIEW_RESULT` `status: BLOCKED` with a blocker containing `checks still pending` (or remediate held at its M7 re-gate on `CI pending`), and `scripts/wait-ci-green.sh` still reported pending when the budget expired |
|
|
24
|
+
| `build.acceptance_failed` | Newest acceptance gate marker is blocked | build | `FORGE:ACCEPTANCE_GATE:FAILED`/`:BLOCKED` or `FORGE:BUILD_BLOCKED` (`:DEGRADED` is a pass-class marker, not blocked) |
|
|
24
25
|
| `build.acceptance_malformed` | Acceptance spec was malformed | reserved | Indistinguishable from `build.acceptance_failed` by sentinel alone |
|
|
25
26
|
| `build.size_over_gate` | Retired (forge#3862): the old size gate routed a finished build to decompose; never emitted now, kept so historic runs still parse | reserved | A finished build is never routed to decompose |
|
|
26
27
|
| `build.cohesive` | Retired (forge#3862): the old size gate judged an oversized diff cohesive | reserved | No size gate remains |
|
|
@@ -28,12 +29,15 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
|
|
|
28
29
|
| `remediate.deferred_live_run` | Remediation deferred to a live earlier run | remediate, engine | Trusted `FORGE:REMEDIATION_DEFERRED` record |
|
|
29
30
|
| `remediate.re_escalated` | Remediation re-escalated to a human | remediate | `**Re-gate outcome**: RE-ESCALATED` |
|
|
30
31
|
| `remediate.unfixable` | Remediation judged the findings unfixable | remediate | `**Re-gate outcome**: UNFIXABLE` |
|
|
32
|
+
| `remediate.inpr_requested` | Re-gate approved but requested one further in-PR fix round; the run continues, never `needs-human` | remediate | `**Re-gate outcome**: INPR-FIX-REQUESTED` |
|
|
31
33
|
| `ci.red` | CI failed | reserved | `REVIEW_RESULT` carries no CI field yet |
|
|
32
34
|
| `ci.timeout` | CI timed out | reserved | `REVIEW_RESULT` carries no CI field yet |
|
|
33
35
|
| `base.conflict` | Base-branch conflict blocked the PR | reserved | `REVIEW_RESULT` carries no conflict field yet |
|
|
34
36
|
| `rate_limited` | Session or usage limit outlasted the pause budget | engine | `err.usageLimit` / `err.resetAtEpochMs` |
|
|
35
37
|
| `engine.error` | Engine or tool failure (missing key/SDK, CLI crash, max turns, unresolved args, runner threw every attempt) | engine | `err.code` |
|
|
36
38
|
| `infra.disk_quota` | Disk full or quota exhausted (EDQUOT/ENOSPC, errno -28/-122); retryable infra failure, never `needs-human` | engine | Runner error text |
|
|
39
|
+
| `infra.cli_spawn_transient` | CLI spawn failed transiently before the child ran (ETIMEDOUT, EAGAIN, ENOMEM, EMFILE); retried with backoff on a separate bounded counter that does not consume a phase attempt | engine | `err.code` `CLI_SPAWN_TRANSIENT` |
|
|
40
|
+
| `cli.timeout` | CLI child ran and was killed at the wall-clock limit; a normal attempt (consumes one of `maxAttempts`), usage recorded when the CLI emitted any | engine | `err.code` `CLI_TIMEOUT` |
|
|
37
41
|
| `lease.held` | Another live agent holds the issue lease | engine | Foreign live lease on acquire |
|
|
38
42
|
| `dependency.open` | A trusted `**Depends on**: #N` dependency is still open, so the run was refused before any state write | engine | Trusted issue body `**Depends on**` line + the dependency issue's open state |
|
|
39
43
|
| `dependency.unreadable` | The issue body or a declared dependency could not be read, so the run was refused (fail closed) | engine | Failed or unparsable `gh api` read |
|