forgedock 1.8.1 → 1.8.2
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/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 +2 -2
- package/commands/orchestrate/phase-1-resolve.md +56 -12
- package/commands/orchestrate/phase-3-dependency.md +15 -5
- package/commands/orchestrate/phase-4-execution.md +593 -78
- package/commands/orchestrate/phase-5-cleanup.md +4 -2
- package/commands/orchestrate/phase-6-report.md +7 -3
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/phase-4-proposals.md +6 -4
- package/commands/pipeline-health/phase-5-report.md +4 -2
- 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 +172 -13
- package/commands/review-pr-agents.md +6 -4
- package/commands/review-pr-staging.md +13 -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 +81 -19
- package/commands/work-on/build/context.md +61 -18
- package/commands/work-on/build/implement.md +62 -18
- package/commands/work-on/build/validate.md +240 -60
- package/commands/work-on/build.md +344 -146
- package/commands/work-on/close.md +617 -246
- package/commands/work-on/decompose.md +239 -41
- package/commands/work-on/investigate.md +377 -61
- package/commands/work-on/remediate.md +42 -9
- package/commands/work-on/review.md +240 -29
- package/commands/work-on-monolithic.md +48 -7
- 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 +11 -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
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Remediate subcommand — checkout a needs-human PR, fix review findings, re-review, and re-gate with a FORGE:REMEDIATION paper trail
|
|
3
3
|
argument-hint: "[PR number] [--issue N] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BASE]"
|
|
4
|
+
context: fork
|
|
4
5
|
---
|
|
5
6
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
7
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
8
|
|
|
8
9
|
# work-on/remediate — Remediation Subcommand
|
|
9
10
|
|
|
11
|
+
> **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>".
|
|
12
|
+
|
|
10
13
|
**Input**: $ARGUMENTS
|
|
11
14
|
|
|
12
15
|
**Invoked by**:
|
|
13
|
-
- `work-on.md` Phase 0A, standalone: `/work-on <pr> --remediate` (see forge#1813).
|
|
16
|
+
- `work-on.md` Phase 0A.1 (router), standalone: `/work-on <pr> --remediate` (see forge#1813).
|
|
14
17
|
- `commands/orchestrate/phase-4-execution.md` item 6.4, auto-dispatched against a `needs-human`-gated predecessor's own open PR.
|
|
15
18
|
|
|
16
19
|
**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 +108,7 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
105
108
|
```
|
|
106
109
|
|
|
107
110
|
**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}`),
|
|
111
|
+
- **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
112
|
- **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
113
|
|
|
111
114
|
**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 +152,7 @@ If the worktree/branch checkout fails for any reason (branch deleted, force-push
|
|
|
149
152
|
|
|
150
153
|
## Phase M3: Apply Fixes
|
|
151
154
|
|
|
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`
|
|
155
|
+
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
156
|
|
|
154
157
|
**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
158
|
|
|
@@ -158,7 +161,7 @@ For each FIXABLE item from Phase M1: read the affected file(s) in `{WORKTREE_PAT
|
|
|
158
161
|
iteration = 0
|
|
159
162
|
while iteration < 3:
|
|
160
163
|
iteration += 1
|
|
161
|
-
Skill("quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
|
|
164
|
+
Skill("{FORGE_SKILL_PREFIX}quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
|
|
162
165
|
if result == "QUALITY GATE: PASS": GATE_PASSED=true; break
|
|
163
166
|
else: fix each HIGH/MEDIUM finding, re-stage
|
|
164
167
|
```
|
|
@@ -226,7 +229,10 @@ Note the marker is `<!-- FORGE:REMEDIATION -->` with **no** `:COMPLETE` suffix y
|
|
|
226
229
|
## Phase M6: Re-Invoke /review-pr
|
|
227
230
|
|
|
228
231
|
```
|
|
229
|
-
|
|
232
|
+
if DRY_RUN=true:
|
|
233
|
+
record "Would invoke review-pr --auto-merge for PR #{PR_NUMBER}; skipped (dry-run)."
|
|
234
|
+
else:
|
|
235
|
+
Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
230
236
|
```
|
|
231
237
|
|
|
232
238
|
**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 +251,10 @@ else:
|
|
|
245
251
|
|
|
246
252
|
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
253
|
|
|
254
|
+
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`.
|
|
255
|
+
|
|
256
|
+
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.
|
|
257
|
+
|
|
248
258
|
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
259
|
|
|
250
260
|
- **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 +288,7 @@ POST_REVIEW_LABELS=$(gh issue view {ISSUE_NUMBER} {GH_FLAG} --json labels --jq '
|
|
|
278
288
|
|
|
279
289
|
```bash
|
|
280
290
|
# Trust filter: only reviews/comments from repo collaborators (OWNER/MEMBER/COLLABORATOR
|
|
281
|
-
# authorAssociation) can contribute to the auto-land bar. Unlike work-on.md
|
|
291
|
+
# authorAssociation) can contribute to the auto-land bar. Unlike work-on/close.md's
|
|
282
292
|
# informational-only APPROVED: count (a summary-card/decision-record annotation, not a
|
|
283
293
|
# merge gate), this count directly drives `gh pr merge` below — so it must not trust
|
|
284
294
|
# unauthenticated signal. Any GitHub user can comment "APPROVED: ..." on a public PR;
|
|
@@ -320,7 +330,7 @@ if [ -n "$LIVE_BASE_REF" ] && [ "$LIVE_BASE_REF" != "null" ] && [ "$LIVE_BASE_RE
|
|
|
320
330
|
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
331
|
|
|
322
332
|
**`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`
|
|
333
|
+
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
334
|
2. `GATE_PASSED = true` from this remediation's own Phase M3 quality-gate loop.
|
|
325
335
|
|
|
326
336
|
**Evaluate the base-scoped bar**:
|
|
@@ -336,7 +346,30 @@ fi
|
|
|
336
346
|
|
|
337
347
|
**If the bar is met** (`BAR_MET=true`):
|
|
338
348
|
```bash
|
|
339
|
-
|
|
349
|
+
# CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
|
|
350
|
+
# green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
|
|
351
|
+
# protection required none and an auto-merge waits only for *required* checks.
|
|
352
|
+
CI_GATE_SCRIPT=""
|
|
353
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
354
|
+
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
|
|
355
|
+
case "$_c" in /*) [ -z "$CI_GATE_SCRIPT" ] && [ -f "$_c/scripts/wait-ci-green.sh" ] && CI_GATE_SCRIPT="$_c/scripts/wait-ci-green.sh" ;; esac
|
|
356
|
+
done
|
|
357
|
+
if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" {PR_NUMBER} {GH_FLAG}); CI_GATE_RC=$?
|
|
358
|
+
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
359
|
+
echo "$CI_GATE_OUT"
|
|
360
|
+
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
361
|
+
# rc 3 = CI still running: re-run this block (up to 3 more times) before treating it as a failure.
|
|
362
|
+
if [ "$CI_GATE_RC" -ne 0 ]; then
|
|
363
|
+
# Not green: do not land. Re-escalate with the gate output (counts as RE-ESCALATED below).
|
|
364
|
+
CI_MSG="⛔ Remediation auto-land refused for PR #{PR_NUMBER}: CI is not green (rc=${CI_GATE_RC}).
|
|
365
|
+
\`\`\`
|
|
366
|
+
${CI_GATE_OUT}
|
|
367
|
+
\`\`\`"
|
|
368
|
+
gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "$CI_MSG" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
369
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
370
|
+
else
|
|
371
|
+
gh pr merge {PR_NUMBER} {GH_FLAG} --merge --match-head-commit "$GATED_HEAD" # allowlist:check-command-side-effects (CI-gated merge)
|
|
372
|
+
fi
|
|
340
373
|
MERGE_STATE=$(gh pr view {PR_NUMBER} {GH_FLAG} --json state --jq '.state')
|
|
341
374
|
if [ "$MERGE_STATE" = "MERGED" ]; then
|
|
342
375
|
RESOLUTION=$(resolve_script 'transition-label')
|
|
@@ -399,7 +432,7 @@ gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "$REMEDIATION_BODY"
|
|
|
399
432
|
**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
433
|
|
|
401
434
|
```
|
|
402
|
-
Skill("work-on:close", args="{ISSUE_NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --pr {PR_NUMBER} --base {PR_BASE}")
|
|
435
|
+
Skill("{FORGE_SKILL_PREFIX}work-on:close", args="{ISSUE_NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --pr {PR_NUMBER} --base {PR_BASE}")
|
|
403
436
|
```
|
|
404
437
|
|
|
405
438
|
`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,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Review subcommand — push branch, create PR, invoke /review-pr with --auto-merge
|
|
3
|
-
|
|
3
|
+
context: fork
|
|
4
|
+
argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE} --branch {BRANCH} --base {PR_BASE}"
|
|
4
5
|
---
|
|
5
6
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
7
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
8
|
|
|
8
9
|
# work-on/review — Review & PR Creation Subcommand
|
|
9
10
|
|
|
11
|
+
> **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>".
|
|
12
|
+
|
|
10
13
|
**Input**: $ARGUMENTS
|
|
11
14
|
|
|
12
|
-
**Invoked by**: `work-on
|
|
13
|
-
**Output**: Push branch, create PR, invoke /review-pr --auto-merge,
|
|
15
|
+
**Invoked by**: the `work-on` router, after `build/validate.md` returns `GATE_PASSED: true`.
|
|
16
|
+
**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
17
|
|
|
15
18
|
**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
19
|
**NEVER use plan mode (EnterPlanMode).**
|
|
@@ -27,7 +30,91 @@ Parse from $ARGUMENTS:
|
|
|
27
30
|
- `--gh-flag {GH_FLAG}` — gh CLI repo flag (e.g. `-R {owner}/{repo}`)
|
|
28
31
|
- `--worktree {WORKTREE_PATH}` — absolute path to the git worktree
|
|
29
32
|
- `--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`)
|
|
33
|
+
- `--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.
|
|
34
|
+
|
|
35
|
+
**Fail closed**: if `{NUMBER}`, `--repo`, `--worktree`, `--branch` or `--base` is missing, print the block below and STOP (no push, no PR):
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
REVIEW_RESULT:
|
|
39
|
+
status: BLOCKED
|
|
40
|
+
pr_number:
|
|
41
|
+
pr_url:
|
|
42
|
+
merged_to:
|
|
43
|
+
blocker: missing required arg: --base (or --worktree/--branch/--repo/NUMBER)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Script resolution
|
|
47
|
+
|
|
48
|
+
**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`.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Canonical script resolution (keep byte-identical across specs; guarded by scripts/forge-root.test.sh).
|
|
52
|
+
# Shell state does NOT persist between Bash tool calls: include this whole block at the top of
|
|
53
|
+
# every command that uses resolve_script or FORGE_ROOT.
|
|
54
|
+
REPO_PATH="${REPO_PATH:-$(yq '.paths.root // ""' forge.yaml 2>/dev/null)}"
|
|
55
|
+
[ -n "$REPO_PATH" ] && [ "$REPO_PATH" != "null" ] || REPO_PATH="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
56
|
+
ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
|
|
57
|
+
ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
|
|
58
|
+
ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
|
|
59
|
+
# Bounds check: reject adaptive_scripts.directory values that escape the repo root.
|
|
60
|
+
# Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
|
|
61
|
+
# slash in paths.root does not inject a '//' into the glob and trigger a false positive.
|
|
62
|
+
REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
|
|
63
|
+
if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
64
|
+
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
65
|
+
ADAPTIVE_ENABLED=false
|
|
66
|
+
fi
|
|
67
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
|
|
68
|
+
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
69
|
+
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
70
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
|
|
71
|
+
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
72
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
73
|
+
FORGE_ROOT=""
|
|
74
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
75
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
76
|
+
_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")")"
|
|
77
|
+
# 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)
|
|
78
|
+
_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-)"
|
|
79
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
80
|
+
# '${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.
|
|
81
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
82
|
+
while IFS= read -r _c; do
|
|
83
|
+
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
|
|
84
|
+
done <<< "$_k"
|
|
85
|
+
fi
|
|
86
|
+
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
87
|
+
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
88
|
+
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
89
|
+
# FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
|
|
90
|
+
# Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
|
|
91
|
+
# locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
|
|
92
|
+
# does not exist, resolve_script() falls through to Tier 4 (prose) below,
|
|
93
|
+
# which is always safe and available. A missing script is never a reason
|
|
94
|
+
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
95
|
+
|
|
96
|
+
resolve_script() {
|
|
97
|
+
local operation="$1"
|
|
98
|
+
# Tier 2: per-repo adaptive (skip if disabled)
|
|
99
|
+
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
100
|
+
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
101
|
+
return
|
|
102
|
+
fi
|
|
103
|
+
# Tier 3: universal script
|
|
104
|
+
if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
|
|
105
|
+
echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
|
|
106
|
+
return
|
|
107
|
+
fi
|
|
108
|
+
# Tier 4: prose fallback
|
|
109
|
+
echo "prose:"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**BLOCKED output pattern**: every guard below that stops the phase prints the result block before exiting, e.g.:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
printf 'REVIEW_RESULT:\n status: BLOCKED\n pr_number:\n pr_url:\n merged_to:\n blocker: %s\n' "<blocker text>"
|
|
117
|
+
```
|
|
31
118
|
|
|
32
119
|
---
|
|
33
120
|
|
|
@@ -48,8 +135,22 @@ gh pr list {GH_FLAG} --head {BRANCH} --json number,state,url 2>/dev/null
|
|
|
48
135
|
|
|
49
136
|
**Resume check**:
|
|
50
137
|
- 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
|
-
|
|
138
|
+
- 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`:
|
|
139
|
+
```bash
|
|
140
|
+
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] would post the comment below"; exit 0; }
|
|
141
|
+
MERGED_PR=$(gh pr list {GH_FLAG} --head {BRANCH} --state merged --json number --jq '.[0].number' 2>/dev/null)
|
|
142
|
+
HAS_REVIEW_CKPT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
|
|
143
|
+
--jq '[.[] | select((.body | contains("FORGE:CHECKPOINT")) and (.body | contains("\"phase\": \"REVIEW\"")))] | length' 2>/dev/null | tail -1)
|
|
144
|
+
if [ "${HAS_REVIEW_CKPT:-0}" -eq 0 ]; then
|
|
145
|
+
CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
146
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
|
|
147
|
+
\`\`\`json
|
|
148
|
+
{\"phase\": \"REVIEW\", \"status\": \"COMPLETE\", \"next_phase\": \"CLOSE\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
|
|
149
|
+
\`\`\`" # allowlist:check-command-side-effects
|
|
150
|
+
fi
|
|
151
|
+
```
|
|
152
|
+
(`pr_number` in the result is `$MERGED_PR`.)
|
|
153
|
+
- If no `<!-- FORGE:BUILDER -->` comment exists → print `REVIEW_RESULT: status: BLOCKED`, blocker: "FORGE:BUILDER comment not found — implement phase may not have completed"
|
|
53
154
|
|
|
54
155
|
**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
156
|
|
|
@@ -84,6 +185,7 @@ SKIP_EOF
|
|
|
84
185
|
gh issue comment {NUMBER} {GH_FLAG} --body "$REREVIEW_SKIP_BODY" # <!-- allowlist:check-command-side-effects -->
|
|
85
186
|
gh issue edit {NUMBER} {GH_FLAG} --add-label needs-human 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
86
187
|
# Return REVIEW_RESULT: status: BLOCKED — do not invoke /review-pr again on unchanged HEAD
|
|
188
|
+
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
189
|
exit 1
|
|
88
190
|
fi
|
|
89
191
|
```
|
|
@@ -115,8 +217,9 @@ ${MERGE_COMMITS}
|
|
|
115
217
|
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
218
|
|
|
117
219
|
<!-- FORGE:PUSH_BLOCKED -->"
|
|
118
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
220
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
119
221
|
# Return REVIEW_RESULT: status: BLOCKED — do not push
|
|
222
|
+
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
223
|
exit 1
|
|
121
224
|
fi
|
|
122
225
|
fi
|
|
@@ -140,7 +243,8 @@ Branch \`{BRANCH}\` has 0 commits ahead of \`origin/{PR_BASE}\`. Pushing this br
|
|
|
140
243
|
**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
244
|
|
|
142
245
|
<!-- FORGE:PUSH_BLOCKED_EMPTY_BRANCH -->"
|
|
143
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
246
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
247
|
+
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
248
|
exit 1
|
|
145
249
|
fi
|
|
146
250
|
echo "Commit count ahead of origin/{PR_BASE}: $COMMIT_COUNT — OK to push"
|
|
@@ -150,12 +254,12 @@ echo "Commit count ahead of origin/{PR_BASE}: $COMMIT_COUNT — OK to push"
|
|
|
150
254
|
|
|
151
255
|
```bash
|
|
152
256
|
cd {WORKTREE_PATH}
|
|
153
|
-
git push origin {BRANCH}
|
|
257
|
+
git push -u origin {BRANCH} # allowlist:check-command-side-effects
|
|
154
258
|
```
|
|
155
259
|
|
|
156
260
|
If push fails, retry with `--force-with-lease`:
|
|
157
261
|
```bash
|
|
158
|
-
git push origin {BRANCH} --force-with-lease
|
|
262
|
+
git push -u origin {BRANCH} --force-with-lease # allowlist:check-command-side-effects
|
|
159
263
|
```
|
|
160
264
|
|
|
161
265
|
If still fails:
|
|
@@ -170,9 +274,62 @@ This may indicate a merge conflict or remote rejection. Human review required.
|
|
|
170
274
|
|
|
171
275
|
<!-- FORGE:PUSH_FAILED -->"
|
|
172
276
|
|
|
173
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
277
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
278
|
+
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
279
|
```
|
|
175
|
-
|
|
280
|
+
Print the block above (`status: BLOCKED`, blocker: "git push failed") and STOP.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Phase R1.5: Phase-Trail Preflight (MANDATORY — before PR creation) <!-- Added: forge#3061 -->
|
|
285
|
+
|
|
286
|
+
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.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# --no-renames: list BOTH sides of a rename so a file moved into docs/ cannot hide its source path (forge#3145).
|
|
290
|
+
CHANGED=$(git -C {WORKTREE_PATH} diff --name-only --no-renames origin/{PR_BASE}...HEAD)
|
|
291
|
+
# The verifier ships with ForgeDock (not the consumer repo): same resolution as every universal script.
|
|
292
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
293
|
+
FORGE_ROOT=""
|
|
294
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
295
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
296
|
+
_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")")"
|
|
297
|
+
# 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)
|
|
298
|
+
_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-)"
|
|
299
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
300
|
+
# '${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.
|
|
301
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
302
|
+
while IFS= read -r _c; do
|
|
303
|
+
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
|
|
304
|
+
done <<< "$_k"
|
|
305
|
+
fi
|
|
306
|
+
TRAIL_SCRIPT="$FORGE_ROOT/scripts/verify-phase-trail.sh"
|
|
307
|
+
# Docs-only predicate: ONE shared copy (scripts/is-docs-only.sh, forge#3134). Fail closed: unresolved script or empty diff -> no flag.
|
|
308
|
+
DOCS_ONLY_FLAG=""
|
|
309
|
+
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
|
|
310
|
+
# Band cross-check (forge#3149): a non-docs diff means an agent-chosen INVESTIGATION band must not waive requirements.
|
|
311
|
+
# Fail closed: anything not positively docs-only (including an empty/unreadable diff) is treated as a code diff.
|
|
312
|
+
CODE_DIFF_FLAG="--code-diff"
|
|
313
|
+
if [ -n "$DOCS_ONLY_FLAG" ]; then CODE_DIFF_FLAG=""; fi
|
|
314
|
+
# Bind the QUALITY_GATE PASS to the built tree (forge#3149). An unresolved tree is passed as empty -> the verifier exits 2 (fail closed).
|
|
315
|
+
HEAD_TREE=$(git -C {WORKTREE_PATH} rev-parse 'HEAD^{tree}' 2>/dev/null)
|
|
316
|
+
if [ -z "$FORGE_ROOT" ] || [ ! -f "$TRAIL_SCRIPT" ]; then
|
|
317
|
+
# Fail closed: never skip the gate when the verifier cannot be resolved (plugin installs set no FORGE_HOME).
|
|
318
|
+
echo "PHASE TRAIL: verify-phase-trail.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install)" >&2
|
|
319
|
+
TRAIL_RC=127
|
|
320
|
+
else
|
|
321
|
+
TRAIL=$(bash "$TRAIL_SCRIPT" {NUMBER} -R {GH_REPO} $DOCS_ONLY_FLAG $CODE_DIFF_FLAG --head-tree "$HEAD_TREE"); TRAIL_RC=$?
|
|
322
|
+
echo "$TRAIL"
|
|
323
|
+
fi
|
|
324
|
+
# Hard guard (same as review-pr.md Phase 8): an unreadable/unresolvable trail never falls through to PR creation.
|
|
325
|
+
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
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- `TRAIL_RC=0` → continue to Phase R2.
|
|
329
|
+
- `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.
|
|
330
|
+
- `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".
|
|
331
|
+
|
|
332
|
+
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
333
|
|
|
177
334
|
---
|
|
178
335
|
|
|
@@ -256,15 +413,19 @@ Use the existing PR number and continue.
|
|
|
256
413
|
### R2D: Update labels
|
|
257
414
|
|
|
258
415
|
```bash
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
416
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
417
|
+
case "$TIER" in
|
|
418
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
|
|
419
|
+
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 ;;
|
|
420
|
+
esac
|
|
262
421
|
```
|
|
263
422
|
|
|
264
423
|
---
|
|
265
424
|
|
|
266
425
|
## Phase R3: Invoke /review-pr with --auto-merge
|
|
267
426
|
|
|
427
|
+
**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 -->
|
|
428
|
+
|
|
268
429
|
Re-read the PR number (from creation or from resume check):
|
|
269
430
|
|
|
270
431
|
```bash
|
|
@@ -278,7 +439,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "## Submitting for Review
|
|
|
278
439
|
|
|
279
440
|
PR #${PR_NUMBER} created targeting \`{PR_BASE}\`. Invoking /review-pr with --auto-merge.
|
|
280
441
|
|
|
281
|
-
Review will: analyze changes → spawn domain agents → post findings → merge
|
|
442
|
+
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
443
|
|
|
283
444
|
<!-- FORGE:REVIEW_STARTED -->"
|
|
284
445
|
```
|
|
@@ -286,7 +447,10 @@ Review will: analyze changes → spawn domain agents → post findings → merge
|
|
|
286
447
|
Invoke the review command:
|
|
287
448
|
|
|
288
449
|
```
|
|
289
|
-
|
|
450
|
+
if DRY_RUN=true:
|
|
451
|
+
record "Would invoke review-pr --auto-merge for PR #{PR_NUMBER}; skipped (dry-run)."
|
|
452
|
+
else:
|
|
453
|
+
Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER} --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
290
454
|
```
|
|
291
455
|
|
|
292
456
|
**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 +469,9 @@ else:
|
|
|
305
469
|
|
|
306
470
|
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
471
|
|
|
308
|
-
/review-pr handles: full domain-agent review → post findings as separate issues (non-blocking) → merge the PR
|
|
472
|
+
/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.
|
|
473
|
+
|
|
474
|
+
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
475
|
|
|
310
476
|
---
|
|
311
477
|
|
|
@@ -322,10 +488,11 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
322
488
|
```
|
|
323
489
|
|
|
324
490
|
**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
|
|
491
|
+
- 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
492
|
|
|
327
493
|
Write machine-readable phase checkpoint before returning (MANDATORY when PR is MERGED):
|
|
328
494
|
```bash
|
|
495
|
+
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] would post the comment below"; exit 0; }
|
|
329
496
|
CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
330
497
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
|
|
331
498
|
\`\`\`json
|
|
@@ -333,17 +500,63 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
333
500
|
\`\`\`"
|
|
334
501
|
```
|
|
335
502
|
|
|
336
|
-
-
|
|
503
|
+
- `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.
|
|
504
|
+
|
|
505
|
+
- `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.
|
|
506
|
+
|
|
507
|
+
- `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 -->
|
|
508
|
+
|
|
509
|
+
- `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 -->
|
|
510
|
+
|
|
511
|
+
Persisted loop bounds for the two cases above — count first, post the marker, then act (`{BOUND}` is `STALE_REREVIEW` or `CI_REMEDIATION`):
|
|
512
|
+
```bash
|
|
513
|
+
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] bound check only"; }
|
|
514
|
+
if [ "{BOUND}" = "STALE_REREVIEW" ]; then
|
|
515
|
+
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
516
|
+
--jq '[.[] | select((.body | contains("FORGE:STALE_REREVIEW:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
517
|
+
else
|
|
518
|
+
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
519
|
+
--jq '[.[] | select((.body | contains("FORGE:CI_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
520
|
+
fi
|
|
521
|
+
if [ "$BOUND_COUNT" -ge 1 ]; then
|
|
522
|
+
echo "BOUND_EXHAUSTED: {BOUND} already used for PR #{PR_NUMBER}"
|
|
523
|
+
elif [ "${DRY_RUN:-false}" != "true" ]; then
|
|
524
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:STALE_REREVIEW: pr={PR_NUMBER} -->
|
|
525
|
+
Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head once." 2>/dev/null || true # when {BOUND}=STALE_REREVIEW
|
|
526
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CI_REMEDIATION: pr={PR_NUMBER} -->
|
|
527
|
+
CI gate refused PR #{PR_NUMBER}: dispatching remediation once to fix the failing checks." 2>/dev/null || true # when {BOUND}=CI_REMEDIATION
|
|
528
|
+
fi
|
|
529
|
+
```
|
|
530
|
+
Post only the comment matching `{BOUND}`. `BOUND_EXHAUSTED` → take the "already ≥ 1" branch of that case.
|
|
531
|
+
|
|
532
|
+
- 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
533
|
```bash
|
|
338
|
-
|
|
534
|
+
# CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
|
|
535
|
+
# green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
|
|
536
|
+
# protection required none and an auto-merge waits only for *required* checks.
|
|
537
|
+
CI_GATE_SCRIPT=""
|
|
538
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
539
|
+
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
|
|
540
|
+
case "$_c" in /*) [ -z "$CI_GATE_SCRIPT" ] && [ -f "$_c/scripts/wait-ci-green.sh" ] && CI_GATE_SCRIPT="$_c/scripts/wait-ci-green.sh" ;; esac
|
|
541
|
+
done
|
|
542
|
+
if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" {PR_NUMBER} {GH_FLAG}); CI_GATE_RC=$?
|
|
543
|
+
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
544
|
+
echo "$CI_GATE_OUT"
|
|
545
|
+
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
546
|
+
# rc 3 = CI still running: re-run this block (up to 3 more times) before treating it as a failure.
|
|
547
|
+
if [ "$CI_GATE_RC" -eq 0 ]; then
|
|
548
|
+
gh pr merge {PR_NUMBER} {GH_FLAG} --merge --auto --match-head-commit "$GATED_HEAD" # allowlist:check-command-side-effects (CI-gated merge)
|
|
549
|
+
else
|
|
550
|
+
echo "REVIEW_RESULT: status: BLOCKED, blocker: ci gate not green (rc=${CI_GATE_RC})"
|
|
551
|
+
fi
|
|
339
552
|
```
|
|
340
|
-
If merge fails: post comment, add `needs-human`, return `REVIEW_RESULT: status: BLOCKED`
|
|
553
|
+
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
554
|
|
|
342
555
|
---
|
|
343
556
|
|
|
344
557
|
## Output
|
|
345
558
|
|
|
346
|
-
|
|
559
|
+
**`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
560
|
|
|
348
561
|
Output this structured block:
|
|
349
562
|
|
|
@@ -358,14 +571,12 @@ REVIEW_RESULT:
|
|
|
358
571
|
|
|
359
572
|
---
|
|
360
573
|
|
|
361
|
-
## Integration
|
|
574
|
+
## Integration
|
|
362
575
|
|
|
363
|
-
This
|
|
576
|
+
This skill is invoked by the `work-on` router (forked) after validate returns `GATE_PASSED: true` and before `work-on:close`:
|
|
364
577
|
|
|
365
578
|
```
|
|
366
|
-
|
|
367
|
-
4 → [THIS MODULE] Push + PR creation + /review-pr invocation + merge verification
|
|
368
|
-
5 → Close (by close.md) — trajectory, parent tracker, summary
|
|
579
|
+
build/validate → [THIS SKILL] push + PR creation + /review-pr invocation + merge verification → work-on:close
|
|
369
580
|
```
|
|
370
581
|
|
|
371
|
-
/review-pr is invoked within this
|
|
582
|
+
/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.
|