<!-- [doc->REQ-VERB-DISPATCH] [doc->REQ-MERGE-CLOSE-UNCONFIRMED] [doc->REQ-RELEASE-SWEEP-DONE] -->
# Phase 2 Plan — Dispatch + Done automation (the MVP)

Scope: ROADMAP Phase 2. The slice that removes the copy-paste dispatch bottleneck,
deliverable with no Discord and no daemon: `dispatch` pushes a Request at an agent
endpoint and flips it to Dispatched; merge-close flows back to Unconfirmed via an
on-demand sweep; a release sweep promotes Unconfirmed to Done and produces the
roundup. Gate: build + full test suite + `traceable-reqs check --group phase2`
green + hand-drive of the dispatch→fixes→sweep→release loop on the testbed.

## Rulings (flynn, 2026-07-21 — recorded in-plan; flag for operator veto, none
## block reversibility)

1. **`dispatch <ref> <target> [note]` rides `shell cmd`** (structured verb; long
   context belongs in the issue, not the dispatch line). Composite `note` tail is
   one quoted string, per the established vocab-check contract.
2. **Dispatch = the `state` verb's dispatched transition + a push + an audit
   comment.** Same label swap and ADR-0004 open/closed sync as `state #N
   dispatched` (so dispatching a closed Request reopens it — uniform semantics,
   no special guard); plus a durable push at the target; plus a `Dispatched to
   <target> by <requester>` comment on the issue for the audit trail. Any
   endpoint may dispatch to any other; no ratified-only guard (ROADMAP grants
   "any endpoint may dispatch to any other", and Phase 1's any-jump philosophy
   holds — the only standing guard remains no-ratified-while-needs-operator).
3. **The push is a durable `spt send <target> --from <shell-id>`** carrying the
   Request's facts (ref, title, type, URL) and the standing instruction to
   reference it with `fixes #N` in the implementing commits/PR. ANSWERED (W8
   live probe, 2026-07-21): spt PERMITS shell→third-party sends — `spt send
   <target> --from <shell-id>` accepted a non-owner target (exit 0; `--from`
   sends print no stdout descriptor, rendered as "accepted"). The `Pusher`
   seam and the loud relay fallback stay as armor, not as the path.
4. **Merge-close reconcile is an on-demand verb, `sweep` (no args).** No daemon
   in Phase 2, so nothing watches GitHub; the sweep is run by whoever wants the
   board honest (typically before a release). It finds issues GitHub closed as
   completed (merge-close via `fixes #N`) whose state label is still an open-
   loop state (captured/investigating/ratified/dispatched) and swaps the label
   to `state:unconfirmed` — the issue stays closed. Issues closed through the
   `state` verb already carry their terminal label and are untouched.
5. **Release sweep is `release <tag>`.** Reads the published release, then
   promotes every `state:unconfirmed` + closed-as-completed issue whose
   `closed_at` predates the release's `published_at` to `state:done`, and
   answers with the roundup (the promoted list + the release name/URL). Known
   approximation, recorded: this trusts that unconfirmed-closed-before-publish
   means "its merge is in the release" — true for single-main-branch repos with
   releases cut from head, which is every current Project. The commit-graph
   check waits until it is ever actually wrong. Roundup posting to Discord is
   Phase 3; in Phase 2 the roundup is the verb's reply.
6. **Operator Done override needs no new surface** — `state #N done` (Phase 1)
   is the override; ROADMAP option C ships free. Verification-pass sweeper (B)
   stays deferred.

## Waves

**W6 — dispatch.** `dispatch` verb end to end behind the `Pusher` seam: payload
grammar + manifest capability, state flip + audit comment + push composition
(`fixes #N` instruction included), unit coverage against httpmock + a recording
Pusher. Empirical probe of shell→third-party `spt send` rides the wave's
hand-check; outcome selects production wiring (direct push vs owner-relay) and
routes a doyle request if core refuses. Activate REQ-VERB-DISPATCH
(doc/impl/unit).

**W7 — sweeps.** `sweep` (merge-close → unconfirmed) and `release <tag>`
(unconfirmed → done + roundup): GitHub client gains `closed_at` on issues and
release-by-tag lookup; both verbs unit-tested against httpmock, including the
idempotence cases (second sweep finds nothing; release re-run promotes nothing
twice). Activate REQ-MERGE-CLOSE-UNCONFIRMED + REQ-RELEASE-SWEEP-DONE
(doc/impl/unit).

**W8 — integration + gate.** Testbed int coverage of the whole loop: dispatch a
minted Request (push captured via seam or live probe), simulate merge-close by
closing-as-completed through the API, `sweep` → unconfirmed, cut a real testbed
release, `release <tag>` → done + roundup; everything minted ends closed. Add
`int` to the three Phase 2 reqs. Hand-drive the loop live from this perch
(including the third-party push probe verdict). CI already runs the suite.
Update ROADMAP/plan, file any new core gaps with doyle.
*DONE (2026-07-21):* int loop green against the real testbed (with retry loops
absorbing GitHub's index-lag on closed/label listings — sweeps are idempotent,
so retrying is the honest shape); testbed seeded with its initial commit
(release tags need one); test releases/tags deleted on the way out. Hand-drove
the loop live: mint → dispatch #19 → hertz (push accepted — ruling 3 answered
YES), state done closed it. No new core gaps beyond the W5 list already filed.

## Non-goals (Phase 2)

Discord/daemon anything (Phase 3); watches, milestone automation beyond what
`fixes #<milestone-issue>` already gives, liaisons, `#< … #>` tags (Phase 4);
verification-pass sweeper (deferred option B); commit-graph release membership
(recorded approximation, ruling 5); attachment GC (standing non-goal).
