Skip to content

Commit 42a0b60

Browse files
firatcandclaude
andauthored
feat(orchestrate): verb-only ship operation — SHA-bound push, fenced receipt, durable policy parks (FORGE-234) (#415)
* feat(orchestrate): verb-only ship operation — SHA-bound push, fenced receipt, durable policy parks, phase-aware answer resolution (FORGE-234) Co-Authored-By: Claude Fable 5 <[email protected]> * fix(orchestrate): impl-review R1 — manifest-derived production wiring, fail-closed scan base, per-retry fences, version-pinned completion CAS, transactional park/answer, skill ship rounds (FORGE-234) Co-Authored-By: Claude Fable 5 <[email protected]> * fix(orchestrate): impl-review R2 — fenced failure/drift outcomes, attempt-bound park repair and answers, skill worktree flag (FORGE-234) Co-Authored-By: Claude Fable 5 <[email protected]> * fix(orchestrate): impl-review R3 — fence-first failure carriers, version-bound failure completions, orphan-park cancel repair (FORGE-234) Co-Authored-By: Claude Fable 5 <[email protected]> * fix(orchestrate): commit ship-park resolution before the answer becomes durable (impl-review R4) (FORGE-234) Co-Authored-By: Claude Fable 5 <[email protected]> * fix(orchestrate): impl-review R5 — answer-file serialization restored, task-level cancellation (FORGE-234) Co-Authored-By: Claude Fable 5 <[email protected]> * docs(spec): document SHIP policy-park concurrency limits + follow-up scope (FORGE-234) Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]>
1 parent cac7ba7 commit 42a0b60

24 files changed

Lines changed: 2901 additions & 37 deletions

‎skills/forge-orchestrate/SKILL.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,39 @@ Per `spec/ORCHESTRATOR.md` §80-98:
225225
`forge orchestrate cancel <task_id> --reason "<reason>"` as the suggested
226226
recovery — does NOT auto-cancel. User decides.
227227

228+
## Ship rounds (FORGE-234 — VERB-ONLY, no worker spawn)
229+
230+
SHIP is **not** a worker phase: there is no ship worker prompt and no model
231+
subagent. When `forge orchestrate phases --ready --phase ship --json` lists a
232+
reviewed task with a satisfied dependency gate:
233+
234+
```bash
235+
# S1. Dispatch the first-class ship attempt (pointer self-loop; pins the
236+
# manifest to the ship record's reviewed SHA).
237+
# The ship worktree already exists from the implement/review rounds;
238+
# ensure-worktree is idempotent and returns its path.
239+
WORKTREE_PATH=$(forge orchestrate ensure-worktree --task "${TASK_ID}" --json | jq -r '.data.worktree_path')
240+
DISPATCH_OUT=$(forge orchestrate dispatch --task "${TASK_ID}" --claim "${CLAIM_ID}" \
241+
--run "${RUN_ID}" --phase ship --worktree "${WORKTREE_PATH}" --json)
242+
ATTEMPT_ID=$(echo "${DISPATCH_OUT}" | jq -r '.data.attempt_id')
243+
244+
# S2. Run the verb-only ship operation (verify → SHA-bound push → PR
245+
# create-or-get → tracker in_review → merge_pending). NEVER spawn a
246+
# worker for this; the verb is deterministic git/gh work.
247+
forge orchestrate ship --task "${TASK_ID}" --attempt "${ATTEMPT_ID}" --json
248+
```
249+
250+
Outcome handling:
251+
- `ok` with `next_state: merge_pending` — done; the merge itself happens on
252+
merge_pending ticks (FORGE-235), never here.
253+
- `SHIP_PARKED` — surface `details.question_id` to the user exactly like a
254+
worker question; the answer verb resolves it (`retry_ship` → re-ship later;
255+
`cancel_task` cancels).
256+
- `VERIFICATION_FAILED` head-drift — the task regressed to review; the next
257+
review round picks it up (no budget consumed).
258+
- Any budgeted failure — the task stays `reviewed` with backoff; it reappears
259+
in a later `--phase ship` listing when eligible.
260+
228261
## Exit codes
229262

230263
- `0` — success (round complete, or user typed `stop` cleanly)

‎spec/ORCHESTRATOR.md‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ Each task flows through three phases sequentially. Each phase is its own subagen
874874
- Dispatch skill detects `reviewed` tasks.
875875
- **Dependency check before dispatch:** all `depends_on` tasks must be in state `shipped` — which per the lifecycle below means their PRs are **merged to base**. A dependency in `merge_pending` (PR open, not yet merged) defers SHIP until it merges. *(Implemented in FORGE-233: the gate's ONLY positive vector is a live RepoHost merge proof — `mergeResult(pr)` confirming merged into the dependency's recorded base at its recorded `reviewed_head_sha`. Local task state, ship records (write-ahead bookkeeping), phases.yaml, and tracker status never satisfy on their own; a `shipped` dependency is still live-probed until FORGE-235 introduces a versioned merge attestation. Enforcement: `phases --ready --phase ship` discovery filtering, `dispatch --phase ship` admission, and `complete --phase ship` defense-in-depth — all emitting a machine-readable `DEPS_NOT_MERGED` envelope with `details.dependency_gate` (versioned discriminated report; `retriable` iff every blocker is a waiting condition). The gc tracker-done→shipped mapping became report-only in the same change — proof-backed promotion is FORGE-235.)*
876876
- **Reviewed-SHA recording (final-SHA binding, part 1):** when REVIEW passes, the CLI (`complete --phase review`) records the **verified review target** (`verdict.target_sha`, checked against the dispatch-time `review_target_sha` and the current worktree HEAD — see Phase 2) as **`reviewed_head_sha`** — CLI-owned, immutable for the attempt, persisted in the ship record (below). This is the ONLY SHA the ship operation may ship. (Single-host mode records the CLI-verified IMPLEMENT head instead — see §Single-host mode.)
877-
- **The ship operation** (idempotent + crash-safe; FORGE-234): (1) **write-ahead**: persist/refresh the durable **ship record** at `.forge/orchestrator/tasks/<task_id>/ship-record.json` (reviewed_head_sha, resolved base repo + base branch, then per-side-effect: PR id/URL, merge-attempt status) — the record is written **before** each external side effect and reconciled idempotently after it, so a crash between push, PR-create, and merge recovers via create-or-get; (2) re-run `settings.verify` in the task worktree; (3) verify the local head equals `reviewed_head_sha` — any post-review change (rebase-on-drift, `update-branch`, conflict resolution, third-party push) produces a new SHA that must re-enter verify + re-review before shipping proceeds (dual-host: cross-host review; single-host: CLI re-verification — §Single-host mode); (4) final secrets scan; (5) push; (6) create-or-get the PR via the RepoHost; (7) mark tracker `in_review`. **Forge creates NO standing auto-merge enablement** (`gh pr merge --auto`): GitHub's persisted auto-merge request cannot hold an expected head SHA, and GitHub auto-disables it only for pushes by users *without* write permission — a write-capable push after enablement could therefore merge unreviewed code. The merge is instead the atomic, head-bound step below.
877+
- **The ship operation** (idempotent + crash-safe; implemented in FORGE-234 as the VERB-ONLY `forge orchestrate ship` — owner decision 2026-07-23: every step is deterministic git/gh/CLI work, no model worker exists for the ship phase; the verb finishes through the `complete --phase ship` choke point, which requires the pinned ship verdict + the fenced `ship_receipt.json` (attempt-scoped proof the operation ran: target SHA, admitted state version, probe disposition, PR identity) + a FRESH live PR-head read before `merge_pending` commits; SHIP policy failures — unsupported host, fork topology, PR conflict, probe failures — park durably via a `reviewed → blocked_on_question` question whose phase-aware answer resolves `retry_ship → reviewed` or cancels while blocked): (1) **write-ahead**: persist/refresh the durable **ship record** at `.forge/orchestrator/tasks/<task_id>/ship-record.json` (reviewed_head_sha, resolved base repo + base branch, then per-side-effect: PR id/URL, merge-attempt status) — the record is written **before** each external side effect and reconciled idempotently after it, so a crash between push, PR-create, and merge recovers via create-or-get; (2) re-run `settings.verify` in the task worktree; (3) verify the local head equals `reviewed_head_sha` — any post-review change (rebase-on-drift, `update-branch`, conflict resolution, third-party push) produces a new SHA that must re-enter verify + re-review before shipping proceeds (dual-host: cross-host review; single-host: CLI re-verification — §Single-host mode); (4) final secrets scan; (5) push; (6) create-or-get the PR via the RepoHost; (7) mark tracker `in_review`. **Forge creates NO standing auto-merge enablement** (`gh pr merge --auto`): GitHub's persisted auto-merge request cannot hold an expected head SHA, and GitHub auto-disables it only for pushes by users *without* write permission — a write-capable push after enablement could therefore merge unreviewed code. The merge is instead the atomic, head-bound step below.
878878
- On success: task state advances to **`merge_pending`** (non-terminal). Notification `merge_pending` event emitted (`auto_merge: true|false` — true when `ship.merge_policy: 'auto'`, i.e. forge will execute the head-bound merge on green).
879879
- **The merge step (`'auto'` only; runs on `merge_pending` ticks):** when the platform reports every required check green AND `headSha(pr) == reviewed_head_sha`, forge executes the **atomic head-bound merge**: `gh pr merge --squash --match-head-commit "<reviewed_head_sha>"`. The expected-head check is enforced **server-side at merge time** (GraphQL `expectedHeadOid`) — if the head moved between probe and call, the merge fails and the task enters drift handling. Branch protection is likewise enforced server-side on the call: forge cannot merge red (`--admin` and every bypass path prohibited). Durability trade-off (accepted): with no orchestrator running, nothing merges — the task waits in `merge_pending`, fail-safe (`approval`-equivalent), until the next tick.
880880
- **`merge_pending → shipped` (terminal) requires RepoHost confirmation** (gc/reconcile or dispatch-tick probe) that the PR merged into the **recorded base repo + base branch** AND the merged PR head equals **`reviewed_head_sha`**. Tracker status is never merge proof (see gc divergence table). Notification `shipped` emitted on confirmation.
@@ -886,6 +886,32 @@ Each task flows through three phases sequentially. Each phase is its own subagen
886886
- Red PR CI → regress via the changes-requested path with findings injected. Transient git/tracker failures → retry with backoff; after `retry_attempts` failures → `failed`, fatal notification.
887887
- **`'auto'` preconditions** (`ship.merge_policy: 'auto'`; default is `'approval'` = open PR, human merges): requires `agents.review_host_cli` configured (dual-host review — single-host + `auto` is a settings validation error) AND the RepoHost **honesty probe** passing: the *effective* base-branch rules (classic branch protection + rulesets) enforce at least one blocking required status check; the squash method is allowed; the authenticated identity has write permission; no admin bypass is in play; **the base branch has NO merge queue** (owner decision MQ: a queue-enabled base can queue/merge with no orchestrator running, breaking the head-bound guarantee — merge-queue repos are UNSUPPORTED for `'auto'`; the probe reports `merge_queue_enabled` and the ship path parks fail-closed). Probe failure → **park the task with a question** — never warn-and-merge, never a silent downgrade. (Tracker and repo host are orthogonal — a Linear-tracked repo hosted on GitHub gets the full path. Repos with no RepoHost cannot SHIP at all — see §RepoHost.)
888888

889+
### SHIP policy parks — known concurrency limits (FORGE-234; hardening tracked separately)
890+
891+
The SHIP policy park (unsupported host, fork topology, PR conflict, honesty-probe
892+
failure) writes a durable question and resolves it through the `answer` verb. Answer
893+
selection and state resolution are **two independently serialized writes** — the answer
894+
file's `wx` write is the single-writer reservation, and the state CAS is separately fenced.
895+
That is sufficient for the sequential lifecycle and for competing supervisors (the durable
896+
option always decides), but two narrow windows remain when a park races a *successor
897+
attempt's* progress:
898+
899+
1. **Cancellation vs. successor lifecycle.** `cancel_task` is task-level and applies from
900+
`blocked_on_question` or `reviewed`. If a superseding SHIP attempt has already moved the
901+
task to `ready_for_review` (drift) or `merge_pending` before the operator answers, the
902+
cancellation is published but cannot converge; the operator must use `forge orchestrate
903+
cancel` directly.
904+
2. **Orphan repair vs. resolved retry.** A SHIP invocation that snapshots an orphaned park
905+
can commit its `reviewed → blocked_on_question` repair after a concurrent `retry_ship`
906+
resolution reported "already applied", re-parking a task the operator just released.
907+
Re-answering converges.
908+
909+
Neither window touches the merge path, the ship proof chain, or a normal ship: parks only
910+
fire on repositories the orchestrator cannot legitimately ship to at all. Closing them
911+
properly requires a **shared per-question transaction** covering answer selection and state
912+
resolution together (rather than two serialized writes) — tracked as its own task so the
913+
primitive gets a full design review.
914+
889915
### Single-host mode
890916

891917
If `agents.review_host_cli` is `null`, REVIEW phase is skipped. Task flows IMPLEMENT → SHIP directly. A one-time warning at orchestrator first-run: *"Second-opinion review disabled — running single-host. Forge recommends configuring review_host_cli for adversarial review."*

0 commit comments

Comments
 (0)