forgedock 1.8.1 → 1.8.3
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/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +1 -0
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +3 -2
- package/commands/orchestrate/phase-1-resolve.md +57 -12
- package/commands/orchestrate/phase-2-triage.md +1 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
- package/commands/orchestrate/phase-3-dependency.md +17 -5
- package/commands/orchestrate/phase-4-execution.md +625 -78
- package/commands/orchestrate/phase-5-cleanup.md +5 -2
- package/commands/orchestrate/phase-6-report.md +8 -3
- package/commands/orchestrate/safety.md +1 -0
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/config.md +1 -0
- package/commands/pipeline-health/phase-1-context.md +1 -0
- package/commands/pipeline-health/phase-2-metrics.md +1 -0
- package/commands/pipeline-health/phase-3-analyze.md +1 -0
- package/commands/pipeline-health/phase-4-proposals.md +7 -4
- package/commands/pipeline-health/phase-5-report.md +5 -2
- package/commands/pipeline-health/phase-6-summary.md +1 -0
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +219 -13
- package/commands/review-pr-agents/api.md +1 -0
- package/commands/review-pr-agents/auth.md +1 -0
- package/commands/review-pr-agents/billing.md +1 -0
- package/commands/review-pr-agents/concurrency.md +1 -0
- package/commands/review-pr-agents/database.md +1 -0
- package/commands/review-pr-agents/frontend.md +1 -0
- package/commands/review-pr-agents/infra.md +1 -0
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents/scraper.md +1 -0
- package/commands/review-pr-agents/security.md +1 -0
- package/commands/review-pr-agents/spec-cli.md +1 -0
- package/commands/review-pr-agents.md +10 -4
- package/commands/review-pr-staging.md +14 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +3 -1
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +82 -19
- package/commands/work-on/build/context.md +62 -18
- package/commands/work-on/build/implement.md +63 -18
- package/commands/work-on/build/validate.md +241 -60
- package/commands/work-on/build.md +345 -146
- package/commands/work-on/close.md +618 -246
- package/commands/work-on/decompose.md +240 -41
- package/commands/work-on/investigate.md +378 -61
- package/commands/work-on/remediate.md +43 -9
- package/commands/work-on/review.md +241 -29
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +21 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
- package/commands/work-on-monolithic.md +0 -326
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
---
|
|
2
|
+
user-invocable: false
|
|
2
3
|
description: Remediate subcommand — checkout a needs-human PR, fix review findings, re-review, and re-gate with a FORGE:REMEDIATION paper trail
|
|
3
4
|
argument-hint: "[PR number] [--issue N] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BASE]"
|
|
5
|
+
context: fork
|
|
4
6
|
---
|
|
5
7
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
8
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
9
|
|
|
8
10
|
# work-on/remediate — Remediation Subcommand
|
|
9
11
|
|
|
12
|
+
> **Skill Name Resolution (forked phase)**: `{FORGE_SKILL_PREFIX}` is the namespace this skill itself was invoked under — invoked as `forgedock:work-on:…` → `forgedock:` (nesting `:`); as `work-on:…` → empty (`install.sh`); as `forge-work-on-…` → `forge-` (Codex, nesting `-`); OpenCode → empty with `-` nesting. Confirm the target name in the available-skills list before calling it. A forked phase receives no resolved value from its caller; never guess, and if the target skill is not listed return BLOCKED "skill not found: <name>".
|
|
13
|
+
|
|
10
14
|
**Input**: $ARGUMENTS
|
|
11
15
|
|
|
12
16
|
**Invoked by**:
|
|
13
|
-
- `work-on.md` Phase 0A, standalone: `/work-on <pr> --remediate` (see forge#1813).
|
|
17
|
+
- `work-on.md` Phase 0A.1 (router), standalone: `/work-on <pr> --remediate` (see forge#1813).
|
|
14
18
|
- `commands/orchestrate/phase-4-execution.md` item 6.4, auto-dispatched against a `needs-human`-gated predecessor's own open PR.
|
|
15
19
|
|
|
16
20
|
**Output**: Checkout the PR's existing branch → classify the block reason (fixable vs. policy escalation) → apply fixes → quality-gate → commit/push → re-invoke `/review-pr --auto-merge` → compute the #1809 Q1 auto-land bar → merge-if-verified or hold at `workflow:awaiting-merge` → emit a `FORGE:REMEDIATION` paper trail. Return result to caller.
|
|
@@ -105,7 +109,7 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
105
109
|
```
|
|
106
110
|
|
|
107
111
|
**Classify into FIXABLE vs. UNFIXABLE**:
|
|
108
|
-
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a mergeability guard failure (`CONFLICTING`/`DIRTY`/`BLOCKED` — resolvable by rebasing onto `{PR_BASE}`),
|
|
112
|
+
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a mergeability guard failure (`CONFLICTING`/`DIRTY`/`BLOCKED` — resolvable by rebasing onto `{PR_BASE}`), 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).
|
|
109
113
|
- **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.
|
|
110
114
|
|
|
111
115
|
**If the block reason classifies as UNFIXABLE** (and no FIXABLE item accompanies it): do NOT attempt any fix. Skip directly to Phase M8 with verdict `UNFIXABLE`, re-affirm `needs-human` (it should already be present), and return `REMEDIATE_RESULT: status: UNFIXABLE`. This satisfies AC5 — "genuinely-blocked PRs still terminate at `needs-human`."
|
|
@@ -149,7 +153,7 @@ If the worktree/branch checkout fails for any reason (branch deleted, force-push
|
|
|
149
153
|
|
|
150
154
|
## Phase M3: Apply Fixes
|
|
151
155
|
|
|
152
|
-
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.md`
|
|
156
|
+
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.
|
|
153
157
|
|
|
154
158
|
**If the block reason was a mergeability conflict** (`CONFLICTING`/`DIRTY`/`BLOCKED`): resolve it by rebasing `{HEAD_BRANCH}` onto `origin/{PR_BASE}` (or merging `{PR_BASE}` in, whichever preserves a clean, reviewable history) — resolve conflicts manually, do not blindly take "ours"/"theirs".
|
|
155
159
|
|
|
@@ -158,7 +162,7 @@ For each FIXABLE item from Phase M1: read the affected file(s) in `{WORKTREE_PAT
|
|
|
158
162
|
iteration = 0
|
|
159
163
|
while iteration < 3:
|
|
160
164
|
iteration += 1
|
|
161
|
-
Skill("quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
|
|
165
|
+
Skill("{FORGE_SKILL_PREFIX}quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
|
|
162
166
|
if result == "QUALITY GATE: PASS": GATE_PASSED=true; break
|
|
163
167
|
else: fix each HIGH/MEDIUM finding, re-stage
|
|
164
168
|
```
|
|
@@ -226,7 +230,10 @@ Note the marker is `<!-- FORGE:REMEDIATION -->` with **no** `:COMPLETE` suffix y
|
|
|
226
230
|
## Phase M6: Re-Invoke /review-pr
|
|
227
231
|
|
|
228
232
|
```
|
|
229
|
-
|
|
233
|
+
if DRY_RUN=true:
|
|
234
|
+
record "Would invoke review-pr --auto-merge for PR #{PR_NUMBER}; skipped (dry-run)."
|
|
235
|
+
else:
|
|
236
|
+
Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
230
237
|
```
|
|
231
238
|
|
|
232
239
|
**OpenCode joined-child contract**: When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), run this required re-review through one native foreground `task`:
|
|
@@ -245,6 +252,10 @@ else:
|
|
|
245
252
|
|
|
246
253
|
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.
|
|
247
254
|
|
|
255
|
+
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. Re-run each missing phase named on the `MISSING:` lines via its `Skill(...)`, then re-invoke this phase once. If it is `PHASE_TRAIL_FAILED` again, keep `needs-human`, post `<!-- FORGE:PHASE_TRAIL_FAILED -->` listing the still-missing markers, and exit `REMEDIATE_RESULT: status: BLOCKED`. Never treat it as `HELD-AWAITING-MERGE` or `AUTO-LANDED`.
|
|
256
|
+
|
|
257
|
+
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.
|
|
258
|
+
|
|
248
259
|
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:
|
|
249
260
|
|
|
250
261
|
- **Re-escalated**: the re-review itself trips a fresh block (`CHANGES REQUESTED`, purpose-regression, calibration, trust, or a still-`CONFLICTING` mergeability check) → it adds `needs-human`, and this phase removes `workflow:in-review`, leaving one terminal state.
|
|
@@ -278,7 +289,7 @@ POST_REVIEW_LABELS=$(gh issue view {ISSUE_NUMBER} {GH_FLAG} --json labels --jq '
|
|
|
278
289
|
|
|
279
290
|
```bash
|
|
280
291
|
# Trust filter: only reviews/comments from repo collaborators (OWNER/MEMBER/COLLABORATOR
|
|
281
|
-
# authorAssociation) can contribute to the auto-land bar. Unlike work-on.md
|
|
292
|
+
# authorAssociation) can contribute to the auto-land bar. Unlike work-on/close.md's
|
|
282
293
|
# informational-only APPROVED: count (a summary-card/decision-record annotation, not a
|
|
283
294
|
# merge gate), this count directly drives `gh pr merge` below — so it must not trust
|
|
284
295
|
# unauthenticated signal. Any GitHub user can comment "APPROVED: ..." on a public PR;
|
|
@@ -320,7 +331,7 @@ if [ -n "$LIVE_BASE_REF" ] && [ "$LIVE_BASE_REF" != "null" ] && [ "$LIVE_BASE_RE
|
|
|
320
331
|
The strict `APPROVED_COUNT >= 2` verified-human requirement does NOT apply here: it is structurally unsatisfiable for bot-only pipeline review (bot reviews are `authorAssociation=NONE`), and `staging` is reversible — the real human gate is `staging → main`, which no agent performs. This makes the remediation bar identical to the normal fast-lane bar for the same target branch (the issue's core ask). The `authorAssociation` trust filter above is **unchanged** — the relaxation is scoped by *target branch only*, never by *who* may approve (forge#1976/#2519).
|
|
321
332
|
|
|
322
333
|
**`main` base (`IS_DEPLOY_GATE=true` — deploy gate)** — keep the strict #1809 Q1 bar, BOTH conditions required:
|
|
323
|
-
1. `APPROVED_COUNT >= 2` — at least two distinct adversarial `APPROVED:` review comments from repo collaborators (`OWNER`/`MEMBER`/`COLLABORATOR` authorAssociation only — see trust filter above; same counting convention as `work-on.md`
|
|
334
|
+
1. `APPROVED_COUNT >= 2` — at least two distinct adversarial `APPROVED:` review comments from repo collaborators (`OWNER`/`MEMBER`/`COLLABORATOR` authorAssociation only — see trust filter above; same counting convention as `work-on/close.md` C4).
|
|
324
335
|
2. `GATE_PASSED = true` from this remediation's own Phase M3 quality-gate loop.
|
|
325
336
|
|
|
326
337
|
**Evaluate the base-scoped bar**:
|
|
@@ -336,7 +347,30 @@ fi
|
|
|
336
347
|
|
|
337
348
|
**If the bar is met** (`BAR_MET=true`):
|
|
338
349
|
```bash
|
|
339
|
-
|
|
350
|
+
# CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
|
|
351
|
+
# green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
|
|
352
|
+
# protection required none and an auto-merge waits only for *required* checks.
|
|
353
|
+
CI_GATE_SCRIPT=""
|
|
354
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
355
|
+
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
|
|
356
|
+
case "$_c" in /*) [ -z "$CI_GATE_SCRIPT" ] && [ -f "$_c/scripts/wait-ci-green.sh" ] && CI_GATE_SCRIPT="$_c/scripts/wait-ci-green.sh" ;; esac
|
|
357
|
+
done
|
|
358
|
+
if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" {PR_NUMBER} {GH_FLAG}); CI_GATE_RC=$?
|
|
359
|
+
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
360
|
+
echo "$CI_GATE_OUT"
|
|
361
|
+
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
362
|
+
# rc 3 = CI still running: re-run this block (up to 3 more times) before treating it as a failure.
|
|
363
|
+
if [ "$CI_GATE_RC" -ne 0 ]; then
|
|
364
|
+
# Not green: do not land. Re-escalate with the gate output (counts as RE-ESCALATED below).
|
|
365
|
+
CI_MSG="⛔ Remediation auto-land refused for PR #{PR_NUMBER}: CI is not green (rc=${CI_GATE_RC}).
|
|
366
|
+
\`\`\`
|
|
367
|
+
${CI_GATE_OUT}
|
|
368
|
+
\`\`\`"
|
|
369
|
+
gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "$CI_MSG" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
370
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
371
|
+
else
|
|
372
|
+
gh pr merge {PR_NUMBER} {GH_FLAG} --merge --match-head-commit "$GATED_HEAD" # allowlist:check-command-side-effects (CI-gated merge)
|
|
373
|
+
fi
|
|
340
374
|
MERGE_STATE=$(gh pr view {PR_NUMBER} {GH_FLAG} --json state --jq '.state')
|
|
341
375
|
if [ "$MERGE_STATE" = "MERGED" ]; then
|
|
342
376
|
RESOLUTION=$(resolve_script 'transition-label')
|
|
@@ -399,7 +433,7 @@ gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "$REMEDIATION_BODY"
|
|
|
399
433
|
**If the outcome was `AUTO-LANDED`**: this Skill invocation is itself the caller's terminal delegate (Phase 0A.1 of `work-on.md` already told its own routing loop to STOP after dispatching here) — so `remediate.md` must drive the close phase itself rather than assume some other inline logic will. Invoke the close subcommand directly, the same way `work-on/review.md` does when it hands off from a spawned sub-agent context:
|
|
400
434
|
|
|
401
435
|
```
|
|
402
|
-
Skill("work-on:close", args="{ISSUE_NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --pr {PR_NUMBER} --base {PR_BASE}")
|
|
436
|
+
Skill("{FORGE_SKILL_PREFIX}work-on:close", args="{ISSUE_NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --pr {PR_NUMBER} --base {PR_BASE}")
|
|
403
437
|
```
|
|
404
438
|
|
|
405
439
|
`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.
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
---
|
|
2
|
+
user-invocable: false
|
|
2
3
|
description: Review subcommand — push branch, create PR, invoke /review-pr with --auto-merge
|
|
3
|
-
|
|
4
|
+
context: fork
|
|
5
|
+
argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE} --branch {BRANCH} --base {PR_BASE}"
|
|
4
6
|
---
|
|
5
7
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
8
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
9
|
|
|
8
10
|
# work-on/review — Review & PR Creation Subcommand
|
|
9
11
|
|
|
12
|
+
> **Skill Name Resolution (forked phase)**: `{FORGE_SKILL_PREFIX}` is the namespace this skill itself was invoked under — invoked as `forgedock:work-on:…` → `forgedock:` (nesting `:`); as `work-on:…` → empty (`install.sh`); as `forge-work-on-…` → `forge-` (Codex, nesting `-`); OpenCode → empty with `-` nesting. Confirm the target name in the available-skills list before calling it. A forked phase receives no resolved value from its caller; never guess, and if the target skill is not listed return BLOCKED "skill not found: <name>".
|
|
13
|
+
|
|
10
14
|
**Input**: $ARGUMENTS
|
|
11
15
|
|
|
12
|
-
**Invoked by**: `work-on
|
|
13
|
-
**Output**: Push branch, create PR, invoke /review-pr --auto-merge,
|
|
16
|
+
**Invoked by**: the `work-on` router, after `build/validate.md` returns `GATE_PASSED: true`.
|
|
17
|
+
**Output**: Push branch, create PR, invoke /review-pr --auto-merge, verify the merge, and print exactly one `REVIEW_RESULT:` block as the final reply (all paths, including every guard/failure).
|
|
14
18
|
|
|
15
19
|
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (label transitions, `FORGE:CHECKPOINT` writes) stay at this tier because they're interleaved with the review/merge-decision steps in the same `Skill()` invocation — see `work-on.md` section "Model and Effort Tiering — What Actually Applies". <!-- Added: forge#1827 -->
|
|
16
20
|
**NEVER use plan mode (EnterPlanMode).**
|
|
@@ -27,7 +31,91 @@ Parse from $ARGUMENTS:
|
|
|
27
31
|
- `--gh-flag {GH_FLAG}` — gh CLI repo flag (e.g. `-R {owner}/{repo}`)
|
|
28
32
|
- `--worktree {WORKTREE_PATH}` — absolute path to the git worktree
|
|
29
33
|
- `--branch {BRANCH}` — feature branch name (e.g. `feat/my-feature`)
|
|
30
|
-
- `--base {PR_BASE}` — PR target branch (e.g. `milestone/modular-pipeline-architecture` or `staging`)
|
|
34
|
+
- `--base {PR_BASE}` — PR target branch (e.g. `milestone/modular-pipeline-architecture` or `staging`). **Required.** The caller has already computed it and validated it against the classified lane; this skill never recomputes or re-validates the lane.
|
|
35
|
+
|
|
36
|
+
**Fail closed**: if `{NUMBER}`, `--repo`, `--worktree`, `--branch` or `--base` is missing, print the block below and STOP (no push, no PR):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
REVIEW_RESULT:
|
|
40
|
+
status: BLOCKED
|
|
41
|
+
pr_number:
|
|
42
|
+
pr_url:
|
|
43
|
+
merged_to:
|
|
44
|
+
blocker: missing required arg: --base (or --worktree/--branch/--repo/NUMBER)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Script resolution
|
|
48
|
+
|
|
49
|
+
**Shell state does not persist between Bash tool calls** (each call is a fresh shell): paste the block below at the top of every bash command in this skill that calls `resolve_script` or uses `FORGE_ROOT`/`UNIVERSAL_DIR`.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Canonical script resolution (keep byte-identical across specs; guarded by scripts/forge-root.test.sh).
|
|
53
|
+
# Shell state does NOT persist between Bash tool calls: include this whole block at the top of
|
|
54
|
+
# every command that uses resolve_script or FORGE_ROOT.
|
|
55
|
+
REPO_PATH="${REPO_PATH:-$(yq '.paths.root // ""' forge.yaml 2>/dev/null)}"
|
|
56
|
+
[ -n "$REPO_PATH" ] && [ "$REPO_PATH" != "null" ] || REPO_PATH="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
57
|
+
ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
|
|
58
|
+
ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
|
|
59
|
+
ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
|
|
60
|
+
# Bounds check: reject adaptive_scripts.directory values that escape the repo root.
|
|
61
|
+
# Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
|
|
62
|
+
# slash in paths.root does not inject a '//' into the glob and trigger a false positive.
|
|
63
|
+
REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
|
|
64
|
+
if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
65
|
+
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
66
|
+
ADAPTIVE_ENABLED=false
|
|
67
|
+
fi
|
|
68
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
|
|
69
|
+
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
70
|
+
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
71
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
|
|
72
|
+
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
73
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
74
|
+
FORGE_ROOT=""
|
|
75
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
76
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
77
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
78
|
+
# newest cached version first: numeric major.minor.patch of the version dir name only; a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
79
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ '$(NF-1)=="forgedock"{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5-)"
|
|
80
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
81
|
+
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
82
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
83
|
+
while IFS= read -r _c; do
|
|
84
|
+
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
85
|
+
done <<< "$_k"
|
|
86
|
+
fi
|
|
87
|
+
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
88
|
+
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
89
|
+
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
90
|
+
# FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
|
|
91
|
+
# Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
|
|
92
|
+
# locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
|
|
93
|
+
# does not exist, resolve_script() falls through to Tier 4 (prose) below,
|
|
94
|
+
# which is always safe and available. A missing script is never a reason
|
|
95
|
+
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
96
|
+
|
|
97
|
+
resolve_script() {
|
|
98
|
+
local operation="$1"
|
|
99
|
+
# Tier 2: per-repo adaptive (skip if disabled)
|
|
100
|
+
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
101
|
+
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
102
|
+
return
|
|
103
|
+
fi
|
|
104
|
+
# Tier 3: universal script
|
|
105
|
+
if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
|
|
106
|
+
echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
|
|
107
|
+
return
|
|
108
|
+
fi
|
|
109
|
+
# Tier 4: prose fallback
|
|
110
|
+
echo "prose:"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**BLOCKED output pattern**: every guard below that stops the phase prints the result block before exiting, e.g.:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number:\n pr_url:\n merged_to:\n blocker: %s\n' "<blocker text>"
|
|
118
|
+
```
|
|
31
119
|
|
|
32
120
|
---
|
|
33
121
|
|
|
@@ -48,8 +136,22 @@ gh pr list {GH_FLAG} --head {BRANCH} --json number,state,url 2>/dev/null
|
|
|
48
136
|
|
|
49
137
|
**Resume check**:
|
|
50
138
|
- If PR already exists AND is OPEN → run the **HEAD-unchanged re-review guard** below before proceeding to Phase R3
|
|
51
|
-
- If PR already exists AND is MERGED → return `REVIEW_RESULT: status: ALREADY_MERGED
|
|
52
|
-
|
|
139
|
+
- If PR already exists AND is MERGED → write the REVIEW checkpoint (same JSON as Phase R4) if one does not already exist, then return `REVIEW_RESULT: status: ALREADY_MERGED`:
|
|
140
|
+
```bash
|
|
141
|
+
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] would post the comment below"; exit 0; }
|
|
142
|
+
MERGED_PR=$(gh pr list {GH_FLAG} --head {BRANCH} --state merged --json number --jq '.[0].number' 2>/dev/null)
|
|
143
|
+
HAS_REVIEW_CKPT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
|
|
144
|
+
--jq '[.[] | select((.body | contains("FORGE:CHECKPOINT")) and (.body | contains("\"phase\": \"REVIEW\"")))] | length' 2>/dev/null | tail -1)
|
|
145
|
+
if [ "${HAS_REVIEW_CKPT:-0}" -eq 0 ]; then
|
|
146
|
+
CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
147
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
|
|
148
|
+
\`\`\`json
|
|
149
|
+
{\"phase\": \"REVIEW\", \"status\": \"COMPLETE\", \"next_phase\": \"CLOSE\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
|
|
150
|
+
\`\`\`" # allowlist:check-command-side-effects
|
|
151
|
+
fi
|
|
152
|
+
```
|
|
153
|
+
(`pr_number` in the result is `$MERGED_PR`.)
|
|
154
|
+
- If no `<!-- FORGE:BUILDER -->` comment exists → print `REVIEW_RESULT: status: BLOCKED`, blocker: "FORGE:BUILDER comment not found — implement phase may not have completed"
|
|
53
155
|
|
|
54
156
|
**HEAD-unchanged re-review guard** (MANDATORY when a PR already exists and is OPEN) <!-- Added: forge#2243 --> — a PR whose most recent review verdict was CHANGES REQUESTED must not be resubmitted for a full domain-agent review fan-out if nothing has changed since that verdict. `/review-pr` records the exact commit it reviewed in its verdict comment (`CHANGES REQUESTED: commit {sha} — ...`, see `commands/review-pr.md` Phase 8/9); compare that recorded sha against the PR's current `headRefOid`:
|
|
55
157
|
|
|
@@ -84,6 +186,7 @@ SKIP_EOF
|
|
|
84
186
|
gh issue comment {NUMBER} {GH_FLAG} --body "$REREVIEW_SKIP_BODY" # <!-- allowlist:check-command-side-effects -->
|
|
85
187
|
gh issue edit {NUMBER} {GH_FLAG} --add-label needs-human 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
86
188
|
# Return REVIEW_RESULT: status: BLOCKED — do not invoke /review-pr again on unchanged HEAD
|
|
189
|
+
printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number: {PR_NUMBER}\n pr_url:\n merged_to:\n blocker: %s\n' "HEAD unchanged since last CHANGES REQUESTED verdict (${LAST_VERDICT_SHA}) — re-review skipped; remediation required"
|
|
87
190
|
exit 1
|
|
88
191
|
fi
|
|
89
192
|
```
|
|
@@ -115,8 +218,9 @@ ${MERGE_COMMITS}
|
|
|
115
218
|
Do NOT push this branch. Human review required to identify the source of the merge commits and clean the branch history (e.g. via \`git rebase\` to replay only the intended commits onto \`origin/{PR_BASE}\`).
|
|
116
219
|
|
|
117
220
|
<!-- FORGE:PUSH_BLOCKED -->"
|
|
118
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
221
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
119
222
|
# Return REVIEW_RESULT: status: BLOCKED — do not push
|
|
223
|
+
printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number:\n pr_url:\n merged_to:\n blocker: %s\n' "pre-push ancestry guard failed: merge commits from outside {PR_BASE}"
|
|
120
224
|
exit 1
|
|
121
225
|
fi
|
|
122
226
|
fi
|
|
@@ -140,7 +244,8 @@ Branch \`{BRANCH}\` has 0 commits ahead of \`origin/{PR_BASE}\`. Pushing this br
|
|
|
140
244
|
**Resolution**: Delete this branch, re-run \`/work-on {NUMBER}\` to restart the build phase. The partial FORGE:BUILDER comment (lacking \`FORGE:BUILDER:COMPLETE\`) will be detected and deleted, and the build will restart cleanly.
|
|
141
245
|
|
|
142
246
|
<!-- FORGE:PUSH_BLOCKED_EMPTY_BRANCH -->"
|
|
143
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
247
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
248
|
+
printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number:\n pr_url:\n merged_to:\n blocker: %s\n' "branch has 0 commits ahead of origin/{PR_BASE} — empty branch not pushed"
|
|
144
249
|
exit 1
|
|
145
250
|
fi
|
|
146
251
|
echo "Commit count ahead of origin/{PR_BASE}: $COMMIT_COUNT — OK to push"
|
|
@@ -150,12 +255,12 @@ echo "Commit count ahead of origin/{PR_BASE}: $COMMIT_COUNT — OK to push"
|
|
|
150
255
|
|
|
151
256
|
```bash
|
|
152
257
|
cd {WORKTREE_PATH}
|
|
153
|
-
git push origin {BRANCH}
|
|
258
|
+
git push -u origin {BRANCH} # allowlist:check-command-side-effects
|
|
154
259
|
```
|
|
155
260
|
|
|
156
261
|
If push fails, retry with `--force-with-lease`:
|
|
157
262
|
```bash
|
|
158
|
-
git push origin {BRANCH} --force-with-lease
|
|
263
|
+
git push -u origin {BRANCH} --force-with-lease # allowlist:check-command-side-effects
|
|
159
264
|
```
|
|
160
265
|
|
|
161
266
|
If still fails:
|
|
@@ -170,9 +275,62 @@ This may indicate a merge conflict or remote rejection. Human review required.
|
|
|
170
275
|
|
|
171
276
|
<!-- FORGE:PUSH_FAILED -->"
|
|
172
277
|
|
|
173
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
278
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
279
|
+
printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number:\n pr_url:\n merged_to:\n blocker: %s\n' "git push failed" # allowlist:check-command-side-effects
|
|
174
280
|
```
|
|
175
|
-
|
|
281
|
+
Print the block above (`status: BLOCKED`, blocker: "git push failed") and STOP.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Phase R1.5: Phase-Trail Preflight (MANDATORY — before PR creation) <!-- Added: forge#3061 -->
|
|
286
|
+
|
|
287
|
+
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.
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# --no-renames: list BOTH sides of a rename so a file moved into docs/ cannot hide its source path (forge#3145).
|
|
291
|
+
CHANGED=$(git -C {WORKTREE_PATH} diff --name-only --no-renames origin/{PR_BASE}...HEAD)
|
|
292
|
+
# The verifier ships with ForgeDock (not the consumer repo): same resolution as every universal script.
|
|
293
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
294
|
+
FORGE_ROOT=""
|
|
295
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
296
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
297
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
298
|
+
# newest cached version first: numeric major.minor.patch of the version dir name only; a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
299
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ '$(NF-1)=="forgedock"{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5-)"
|
|
300
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
301
|
+
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
302
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
303
|
+
while IFS= read -r _c; do
|
|
304
|
+
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
305
|
+
done <<< "$_k"
|
|
306
|
+
fi
|
|
307
|
+
TRAIL_SCRIPT="$FORGE_ROOT/scripts/verify-phase-trail.sh"
|
|
308
|
+
# Docs-only predicate: ONE shared copy (scripts/is-docs-only.sh, forge#3134). Fail closed: unresolved script or empty diff -> no flag.
|
|
309
|
+
DOCS_ONLY_FLAG=""
|
|
310
|
+
if [ -n "$CHANGED" ] && [ -n "$FORGE_ROOT" ] && [ -f "$FORGE_ROOT/scripts/is-docs-only.sh" ] && echo "$CHANGED" | bash "$FORGE_ROOT/scripts/is-docs-only.sh"; then DOCS_ONLY_FLAG="--docs-only"; fi
|
|
311
|
+
# Band cross-check (forge#3149): a non-docs diff means an agent-chosen INVESTIGATION band must not waive requirements.
|
|
312
|
+
# Fail closed: anything not positively docs-only (including an empty/unreadable diff) is treated as a code diff.
|
|
313
|
+
CODE_DIFF_FLAG="--code-diff"
|
|
314
|
+
if [ -n "$DOCS_ONLY_FLAG" ]; then CODE_DIFF_FLAG=""; fi
|
|
315
|
+
# Bind the QUALITY_GATE PASS to the built tree (forge#3149). An unresolved tree is passed as empty -> the verifier exits 2 (fail closed).
|
|
316
|
+
HEAD_TREE=$(git -C {WORKTREE_PATH} rev-parse 'HEAD^{tree}' 2>/dev/null)
|
|
317
|
+
if [ -z "$FORGE_ROOT" ] || [ ! -f "$TRAIL_SCRIPT" ]; then
|
|
318
|
+
# Fail closed: never skip the gate when the verifier cannot be resolved (plugin installs set no FORGE_HOME).
|
|
319
|
+
echo "PHASE TRAIL: verify-phase-trail.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install)" >&2
|
|
320
|
+
TRAIL_RC=127
|
|
321
|
+
else
|
|
322
|
+
TRAIL=$(bash "$TRAIL_SCRIPT" {NUMBER} -R {GH_REPO} $DOCS_ONLY_FLAG $CODE_DIFF_FLAG --head-tree "$HEAD_TREE"); TRAIL_RC=$?
|
|
323
|
+
echo "$TRAIL"
|
|
324
|
+
fi
|
|
325
|
+
# Hard guard (same as review-pr.md Phase 8): an unreadable/unresolvable trail never falls through to PR creation.
|
|
326
|
+
if [ "$TRAIL_RC" -ge 2 ]; then printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number:\n pr_url:\n merged_to:\n blocker: phase trail unreadable (rc=%s)\n' "$TRAIL_RC"; exit 1; fi
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- `TRAIL_RC=0` → continue to Phase R2.
|
|
330
|
+
- `TRAIL_RC=1` → **do not create the PR.** For each `MISSING: <marker> -> <action>` line, run that phase now via its `Skill(...)` (the action text names it), then re-run this preflight (this skill is the SINGLE owner of the phase-trail re-dispatch; the router never re-dispatches). Do NOT hand-post the missing marker and do NOT escalate to a human: the refusal routes back to the missing phase. If the preflight still fails after one re-dispatch round, post a `<!-- FORGE:PHASE_TRAIL_FAILED -->` comment listing the still-missing markers, add `needs-human`, print `REVIEW_RESULT: status: BLOCKED`, blocker: "phase trail incomplete after re-dispatch". Do NOT close the issue.
|
|
331
|
+
- `TRAIL_RC>=2` (2 = trail unreadable; 127 = script not executable) → the trail could not be read; fail closed (the block above is printed) with `REVIEW_RESULT: status: BLOCKED`, blocker: "phase trail unreadable".
|
|
332
|
+
|
|
333
|
+
Run the same preflight again at the top of Phase R3, before `/review-pr --auto-merge` is invoked, since a resumed run can enter at R3 with an existing PR.
|
|
176
334
|
|
|
177
335
|
---
|
|
178
336
|
|
|
@@ -256,15 +414,19 @@ Use the existing PR number and continue.
|
|
|
256
414
|
### R2D: Update labels
|
|
257
415
|
|
|
258
416
|
```bash
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
417
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
418
|
+
case "$TIER" in
|
|
419
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
|
|
420
|
+
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 ;;
|
|
421
|
+
esac
|
|
262
422
|
```
|
|
263
423
|
|
|
264
424
|
---
|
|
265
425
|
|
|
266
426
|
## Phase R3: Invoke /review-pr with --auto-merge
|
|
267
427
|
|
|
428
|
+
**First re-run the Phase R1.5 phase-trail preflight** (resume entry can skip R1.5). Do not invoke `/review-pr` while it fails. <!-- Added: forge#3061 -->
|
|
429
|
+
|
|
268
430
|
Re-read the PR number (from creation or from resume check):
|
|
269
431
|
|
|
270
432
|
```bash
|
|
@@ -278,7 +440,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "## Submitting for Review
|
|
|
278
440
|
|
|
279
441
|
PR #${PR_NUMBER} created targeting \`{PR_BASE}\`. Invoking /review-pr with --auto-merge.
|
|
280
442
|
|
|
281
|
-
Review will: analyze changes → spawn domain agents → post findings → merge
|
|
443
|
+
Review will: analyze changes → spawn domain agents → post findings → merge. The issue is closed and the worktree cleaned up afterwards by `work-on:close`, not by /review-pr.
|
|
282
444
|
|
|
283
445
|
<!-- FORGE:REVIEW_STARTED -->"
|
|
284
446
|
```
|
|
@@ -286,7 +448,10 @@ Review will: analyze changes → spawn domain agents → post findings → merge
|
|
|
286
448
|
Invoke the review command:
|
|
287
449
|
|
|
288
450
|
```
|
|
289
|
-
|
|
451
|
+
if DRY_RUN=true:
|
|
452
|
+
record "Would invoke review-pr --auto-merge for PR #{PR_NUMBER}; skipped (dry-run)."
|
|
453
|
+
else:
|
|
454
|
+
Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER} --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
290
455
|
```
|
|
291
456
|
|
|
292
457
|
**OpenCode joined-child contract**: When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), invoke this load-bearing review through one native foreground `task` instead of treating the `Skill(...)` line as an asynchronous handoff:
|
|
@@ -305,7 +470,9 @@ else:
|
|
|
305
470
|
|
|
306
471
|
Wait for that task's completed result before Phase R4. Propagate its `REVIEW_RESULT` as this module's child state; do not return `REVIEW_RESULT`, report progress, release an orchestrator slot, or begin close work while the child is running. If the child errors or returns no parseable `REVIEW_RESULT`, return `REVIEW_RESULT: status: BLOCKED` with the child failure as the blocker. The normal `Skill(...)` invocation above remains the non-OpenCode path.
|
|
307
472
|
|
|
308
|
-
/review-pr handles: full domain-agent review → post findings as separate issues (non-blocking) → merge the PR
|
|
473
|
+
/review-pr handles: full domain-agent review → post findings as separate issues (non-blocking) → merge the PR. It does NOT close the issue or clean up the worktree — `work-on:close` does both.
|
|
474
|
+
|
|
475
|
+
If `review-pr` is not found under `{FORGE_SKILL_PREFIX}review-pr` or `review-pr`, print `REVIEW_RESULT: status: BLOCKED`, blocker: "skill not found: review-pr" and STOP. Never review inline.
|
|
309
476
|
|
|
310
477
|
---
|
|
311
478
|
|
|
@@ -322,10 +489,11 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
322
489
|
```
|
|
323
490
|
|
|
324
491
|
**Cases**:
|
|
325
|
-
- PR MERGED (issue OPEN or CLOSED) → write checkpoint, then return `REVIEW_RESULT: status: COMPLETE` — do NOT close the issue or add labels here; the
|
|
492
|
+
- PR MERGED (issue OPEN or CLOSED) → write checkpoint, then return `REVIEW_RESULT: status: COMPLETE` — do NOT close the issue or add labels here; the caller will route to `work-on:close` which handles issue closure, label updates, project board, trajectory log, and worktree cleanup.
|
|
326
493
|
|
|
327
494
|
Write machine-readable phase checkpoint before returning (MANDATORY when PR is MERGED):
|
|
328
495
|
```bash
|
|
496
|
+
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] would post the comment below"; exit 0; }
|
|
329
497
|
CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
330
498
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
|
|
331
499
|
\`\`\`json
|
|
@@ -333,17 +501,63 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
333
501
|
\`\`\`"
|
|
334
502
|
```
|
|
335
503
|
|
|
336
|
-
-
|
|
504
|
+
- `REVIEW_RESULT: status: PHASE_TRAIL_FAILED` from /review-pr (forge#3102; internal to this skill — never printed as this skill's status) → the review gate refused to merge because phase markers are missing. Do NOT hand-post markers and do NOT add `needs-human` yet. For each `MISSING: <marker> -> <action>` line, run that phase via its `Skill(...)`, then re-invoke Phase R3 once (this is the single re-dispatch owner; the trail preflight at the top of R3 re-runs). If the second attempt returns `PHASE_TRAIL_FAILED` again, post a `<!-- FORGE:PHASE_TRAIL_FAILED -->` comment listing the still-missing markers, add `needs-human`, and return `REVIEW_RESULT: status: BLOCKED`, blocker: "phase trail incomplete after re-dispatch". The PR stays open and unmerged throughout.
|
|
505
|
+
|
|
506
|
+
- `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.
|
|
507
|
+
|
|
508
|
+
- `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 -->
|
|
509
|
+
|
|
510
|
+
- `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: invoke remediation **once** — `Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="{PR_NUMBER} --issue {NUMBER} --base {PR_BASE}")` (forked; it classifies a CI-gate refusal as FIXABLE, clears `needs-human`, reads the failing job logs, fixes them on the PR branch, re-runs the quality gate and a full review, and auto-lands through the same CI gate). Bound: count `<!-- FORGE:CI_REMEDIATION: pr={PR_NUMBER} -->` comments on the issue first; if ≥ 1, do not remediate again. Post that marker before invoking. `REMEDIATE_RESULT` re-gate outcome `AUTO-LANDED` → treat as merged and return `REVIEW_RESULT: status: COMPLETE`; any other outcome → leave `needs-human` (remediation sets it) and return `REVIEW_RESULT: status: BLOCKED` with blocker "ci gate not green after remediation". <!-- forge#3191 -->
|
|
511
|
+
|
|
512
|
+
Persisted loop bounds for the two cases above — count first, post the marker, then act (`{BOUND}` is `STALE_REREVIEW` or `CI_REMEDIATION`):
|
|
513
|
+
```bash
|
|
514
|
+
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] bound check only"; }
|
|
515
|
+
if [ "{BOUND}" = "STALE_REREVIEW" ]; then
|
|
516
|
+
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
517
|
+
--jq '[.[] | select((.body | contains("FORGE:STALE_REREVIEW:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
518
|
+
else
|
|
519
|
+
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
520
|
+
--jq '[.[] | select((.body | contains("FORGE:CI_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
521
|
+
fi
|
|
522
|
+
if [ "$BOUND_COUNT" -ge 1 ]; then
|
|
523
|
+
echo "BOUND_EXHAUSTED: {BOUND} already used for PR #{PR_NUMBER}"
|
|
524
|
+
elif [ "${DRY_RUN:-false}" != "true" ]; then
|
|
525
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:STALE_REREVIEW: pr={PR_NUMBER} -->
|
|
526
|
+
Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head once." 2>/dev/null || true # when {BOUND}=STALE_REREVIEW
|
|
527
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CI_REMEDIATION: pr={PR_NUMBER} -->
|
|
528
|
+
CI gate refused PR #{PR_NUMBER}: dispatching remediation once to fix the failing checks." 2>/dev/null || true # when {BOUND}=CI_REMEDIATION
|
|
529
|
+
fi
|
|
530
|
+
```
|
|
531
|
+
Post only the comment matching `{BOUND}`. `BOUND_EXHAUSTED` → take the "already ≥ 1" branch of that case.
|
|
532
|
+
|
|
533
|
+
- PR NOT MERGED (and not a phase-trail, auto-merge-gate, stale-review or ci-gate 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**:
|
|
337
534
|
```bash
|
|
338
|
-
|
|
535
|
+
# CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
|
|
536
|
+
# green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
|
|
537
|
+
# protection required none and an auto-merge waits only for *required* checks.
|
|
538
|
+
CI_GATE_SCRIPT=""
|
|
539
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
540
|
+
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
|
|
541
|
+
case "$_c" in /*) [ -z "$CI_GATE_SCRIPT" ] && [ -f "$_c/scripts/wait-ci-green.sh" ] && CI_GATE_SCRIPT="$_c/scripts/wait-ci-green.sh" ;; esac
|
|
542
|
+
done
|
|
543
|
+
if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" {PR_NUMBER} {GH_FLAG}); CI_GATE_RC=$?
|
|
544
|
+
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
545
|
+
echo "$CI_GATE_OUT"
|
|
546
|
+
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
547
|
+
# rc 3 = CI still running: re-run this block (up to 3 more times) before treating it as a failure.
|
|
548
|
+
if [ "$CI_GATE_RC" -eq 0 ]; then
|
|
549
|
+
gh pr merge {PR_NUMBER} {GH_FLAG} --merge --auto --match-head-commit "$GATED_HEAD" # allowlist:check-command-side-effects (CI-gated merge)
|
|
550
|
+
else
|
|
551
|
+
echo "REVIEW_RESULT: status: BLOCKED, blocker: ci gate not green (rc=${CI_GATE_RC})"
|
|
552
|
+
fi
|
|
339
553
|
```
|
|
340
|
-
If merge fails: post comment, add `needs-human`, return `REVIEW_RESULT: status: BLOCKED`
|
|
554
|
+
If the gate refuses: post the gate output as an issue comment, add `needs-human`, return `REVIEW_RESULT: status: BLOCKED` (blocker "ci gate not green"). If merge fails: post comment, add `needs-human`, return `REVIEW_RESULT: status: BLOCKED`
|
|
341
555
|
|
|
342
556
|
---
|
|
343
557
|
|
|
344
558
|
## Output
|
|
345
559
|
|
|
346
|
-
|
|
560
|
+
**`REVIEW_RESULT: status: COMPLETE` is an intermediate result, NOT a terminal state: the pipeline is not done. Print the block as your final reply; the caller then invokes `work-on:close` (issue closure, `workflow:merged`, trajectory log, worktree cleanup). Do not close anything here.**
|
|
347
561
|
|
|
348
562
|
Output this structured block:
|
|
349
563
|
|
|
@@ -358,14 +572,12 @@ REVIEW_RESULT:
|
|
|
358
572
|
|
|
359
573
|
---
|
|
360
574
|
|
|
361
|
-
## Integration
|
|
575
|
+
## Integration
|
|
362
576
|
|
|
363
|
-
This
|
|
577
|
+
This skill is invoked by the `work-on` router (forked) after validate returns `GATE_PASSED: true` and before `work-on:close`:
|
|
364
578
|
|
|
365
579
|
```
|
|
366
|
-
|
|
367
|
-
4 → [THIS MODULE] Push + PR creation + /review-pr invocation + merge verification
|
|
368
|
-
5 → Close (by close.md) — trajectory, parent tracker, summary
|
|
580
|
+
build/validate → [THIS SKILL] push + PR creation + /review-pr invocation + merge verification → work-on:close
|
|
369
581
|
```
|
|
370
582
|
|
|
371
|
-
/review-pr is invoked within this
|
|
583
|
+
/review-pr is invoked within this skill (not by the router). The router sees only the final `REVIEW_RESULT:` block and never re-dispatches phase-trail failures.
|