<!-- [doc->REQ-DAEMON-LIFECYCLE] [doc->REQ-DISCORD-TOKEN] [doc->REQ-DISCORD-PROVISION]
     [doc->REQ-SEED-CAPTURE] [doc->REQ-SEED-MINT] [doc->REQ-ALCHEMIST-ROLE]
     [doc->REQ-BOT-POST] [doc->REQ-RECONCILE-LOOP] [doc->REQ-EDIT-REQUEST]
     [doc->REQ-RELEASES-ANNOUNCE] -->
# Phase 3 Plan — Hub Daemon + Discord sync

> **SUPERSEDED IN PART at W25 (v0.5.0) — the daemon's start.** Ruling 3
> (auto-ensure on bind, detached spawn, `daemon` verb status/stop/restart) and every
> passage below that rests on it are **retired**. The Hub Daemon is now a
> **ResidentService** declared in the manifest's `[service]` section and started by
> spt's own daemon; alchemy holds no start path of its own and the `ensure`/`stop`/
> `restart` ops are gone (`status` and `project` remain). Ruling 4 (per-node singleton
> lock) is untouched and is what made the transition safe. See `W25-PLAN.md`,
> ADR-0012, ADR-0013, and `REQ-DAEMON-SUPERVISED`. Everything else here — ADR-0008's
> stateless reconcile, the Discord surface, `daemon setup` bar its closing
> auto-ensure — still stands as written.

Scope: ROADMAP Phase 3. The large build on the proven core: the Hub Daemon
(detached, single gateway connection per node, GitHub polling) plus the Discord
capture surface — `#`-Seeds, Type-reaction mint, Bot posts with controls, State
threads with relocation, Seeds/Updates/Releases threads — in the separate
Requests server. Gate: build + full test suite + `traceable-reqs check --group
phase3` green + live bringup on the real Requests server (test channels deleted
after) + the three accepted spt-core dogfood seeds filed as real Requests.

## Rulings (grill-with-docs with the Operator, 2026-07-21)

1. **Watch registry deferred to Phase 4** with the `watch` verb and delivery,
   where it is testable end-to-end. ROADMAP updated both directions.
2. **Discord is the daemon's sync index; polling is a stateless reconcile loop**
   — no durable local state, boot scan rebuilds the in-memory index from Bot
   post ref fields, no polling cursor. Full rationale and consequences in
   **ADR-0008**. Crash recovery and cold start are the same code path.
3. **Auto-ensure on bind**: any shell instance binding on the configured daemon
   node checks for a live daemon and spawns one detached if absent or
   version-stale. A `daemon` verb (status/stop/restart) gives explicit control.
4. **Singleton is per node**: an OS-level lock (released on process death) gates
   spawn on the configured node. Cross-node duplication requires deliberately
   configuring two nodes identically and is not defended (Operator ruling — a
   config cannot proliferate to another node on its own).
5. **Alchemist** (CONTEXT.md): per-Project write-control capability held as a
   Discord role, managed by a bot slash command, defaulting to the bot owner.
   Gates minting and every state-changing control; reads ungated. Renamed from
   the session's working term "Conductor" by Operator ruling.
6. **Non-`#` messages in a Project channel are ignored silently** — chat is
   allowed; the bot never nags.
7. **serenity** is the Discord crate (gateway + buttons + modals + slash
   commands + threads first-class).
8. **The daemon provisions everything**: it bootstraps a `「Alchemy」` category
   and creates one channel per configured Project repo, every channel name
   `_`-prefixed to prevent collisions (repo ↔ channel mapping is derived from
   the name — no mapping config). State/Seeds/Updates/Releases threads are
   ensured idempotently inside each channel, unarchiving as needed, plus a
   pinned **Contents post** linking every State thread in lifecycle order
   (Operator addition, 2026-07-21).
9. **Buttons write GitHub only, never Discord directly**; the post updates via
   an immediately-triggered targeted reconcile of that Request. GitHub stays
   the sole truth; echo loops are impossible by construction.
10. **Relocation is delete + repost** (Discord cannot move messages). Message
    id churn updates the in-memory index; old jump links break — accepted.
11. **Seeds are edited by their author directly on their own message**; the
    daemon mirrors edits into the Seeds-thread entry. `#edit` reply and the
    prefilled Edit modal are **Request-only** surfaces writing title/body to
    the GitHub issue.
12. **Releases thread is fed by polling the releases API** (reconcile spirit):
    any newly published release is announced with its roundup, even releases
    cut by hand without the `release` verb. The verb's inline roundup reply
    remains from Phase 2.
13. **Gate proves out straight on the real Requests server**; `_`-prefixed test
    channels are deleted afterward. Bringup files the three accepted spt-core
    dogfood seeds (silent-intent-drop defect + 2 features) as real Requests.

## Mechanics (plan-level, veto anytime)

- **Packaging**: daemon is a subcommand of the alchemy binary (`alchemy daemon
  run`), spawned detached by auto-ensure; ships in the adapter package per
  CONTEXT. Version-staleness checked via the binary's own version against the
  lockfile's recorded one.
- **Discord token**: resolution mirrors REQ-TOKEN-RESOLUTION — flag → env →
  Windows Credential Manager (new entry), never persisted to config. Config
  gains: daemon node designation, guild id, polling interval, Project repo
  list.
- **Mint fields**: title = Seed first line sans `#`; body = remainder +
  Requester footer (the Seed author's Discord handle; endpoint-id form arrives
  with Phase 4 liaisons); labels `type: <TYPE>` + `state: BACKLOG` — the
  existing create path's contract. Bot pre-seeds the four Type reactions on
  every Seed: 🐛 Bugfix, 🔧 Change, ➕ Addition, ✨ Feature (confirm at bringup).
- **Updates thread**: one notification per observed state transition, rendered
  from the reconcile diff.
- **Polling cadence**: incremental reconcile (issues updated in a trailing
  window + releases) each interval, default 60s; full reconcile on boot.

## Waves

**W9 — daemon skeleton.** `alchemy daemon run` subcommand; node lock + spawn
gating; auto-ensure on bind + version-stale restart; `daemon` verb
(status/stop/restart) in the manifest; Discord token resolution; serenity
gateway connect; `「Alchemy」` category + `_`-channel + thread provisioning,
idempotent and unarchive-safe, behind a `DiscordPort` seam for unit tests.
Activate REQ-DAEMON-LIFECYCLE, REQ-DISCORD-TOKEN, REQ-DISCORD-PROVISION
(doc/impl/unit) + REQ-HAZARD-DOUBLE-DAEMON (unit).
*DONE (2026-07-22):* full suite green (94 unit + 2 int), registry green with
the four W9 activations. Provisioning includes the pinned Contents post
(Operator addition). No test spawns a real daemon — the in-process lock
stands in (doyle's leaked-test-daemon lesson); the detached-spawn path proves
out at W12 with explicit teardown. Live-gateway serenity adapter is
compile-proven only until W12's real-server bringup.

**W10 — capture.** `#`-Seed detection + pre-seeded Type reactions +
Seeds-thread entry with author-edit mirroring; Alchemist role slash command
(add/remove, default bot owner); Type-reaction mint through the existing issue
path; Bot post render (ref field, GitHub footer, buttons incl. Needs Operator
toggle — writes GitHub only). Activate REQ-SEED-CAPTURE, REQ-SEED-MINT,
REQ-ALCHEMIST-ROLE, REQ-BOT-POST (doc/impl/unit) + REQ-HAZARD-REF-FIELD (unit).
*DONE (2026-07-22):* capture decision core + botpost render/parse pure and
unit-covered (103 unit green); serenity handlers wired (message, edit-mirror,
reaction mint, Needs Operator toggle, /alchemist role command). Alchemist role
naming: `alchemist:<channel>` (e.g. `alchemist:_spt-core`), bot owner always
counts. Recorded gap, healed at W11: a crash between GitHub mint and the
Discord swap leaves Seed + issue coexisting until reconcile. Button writes
GitHub only; post text refreshes when W11's targeted reconcile lands.

**W11 — reconcile.** Boot scan → in-memory index; polling reconcile (issues +
releases): relocation (delete + repost, archive-safe), Updates notifications,
Releases announcements + roundup; targeted reconcile after button writes; Edit
modal + `#edit` reply writing GitHub. Idempotence coverage: second cycle over a
consistent Project mutates nothing. Activate REQ-RECONCILE-LOOP,
REQ-EDIT-REQUEST, REQ-RELEASES-ANNOUNCE (doc/impl/unit) +
REQ-HAZARD-RECONCILE-IDEMPOTENT, REQ-HAZARD-THREAD-ARCHIVE (unit).
*DONE (2026-07-22):* reconcile core pure over the port (scan, relocate,
drift-edit, duplicate convergence, loud orphans, unarchive-before-every-write,
Updates notes, release announce-once) — 109 unit green, zero-mutation
idempotence asserted by mutation counter. Buttons/edits trigger a targeted
full-channel reconcile (`refresh_soon`); poll loop starts once per process
(Ready refires on gateway resume). REQ-EDIT-REQUEST activated doc/impl only —
modal/#edit surfaces are interaction-shaped, proven live at W12. Loud-orphan
policy: posts referencing vanished issues are reported, never deleted.

<!-- [doc->REQ-DAEMON-SETUP] -->
**W11.5 — agent-driven onboarding (`daemon setup`).** Operator ruling
(2026-07-22): the end user never runs a CLI — the endpoint's agent drives
setup over the shell verb and relays each reply conversationally. Stateless
step machine: portal instructions (link + create-app/reset-token/intent-toggle
steps) → token validation (`/users/@me`) + Credential Manager write (never
echoed) + Message Content intent check (`/applications/@me` flags) →
permission-scoped invite URL → guild auto-detect (`/users/@me/guilds`; asks
on ambiguity) → daemon.toml write → auto-ensure starts the daemon. Projects
staged in `daemon-setup.toml` between steps. Irreducibly human: portal app
creation, token copy, intent toggle, clicking the invite. Activate
REQ-DAEMON-SETUP (doc/impl/unit).

**W12 — integration + gate.** Real-server bringup: bot invited, category
bootstrapped, `_`-test channel + testbed repo; live loop hand-driven from this
perch — seed → mint → buttons → shell-verb state changes → dispatch → release
announce — plus daemon kill/restart proving the boot-scan self-heal. Add `int`
to REQ-DAEMON-LIFECYCLE, REQ-RECONCILE-LOOP, REQ-HAZARD-DOUBLE-DAEMON. Delete
test channels; provision the real spt-core channel; file the three dogfood
seeds as real Requests. Update ROADMAP/plan; file new core gaps with doyle.
*DONE (2026-07-22):* gate driven end-to-end on the real Requests server via
the `daemon setup` conversational flow (the end-user path — no operator CLI).
Live-proven: provisioning convergence ("guild already consistent"), full
testbed board posted then zero-mutation cycles, relocation Backlog→WIP with
Updates note, capture drive by the Operator (seed → 🐛 mint #26 → Needs
Operator toggle → Edit modal → #edit reply, all worked), release announce-once
(2 real releases), kill/restart self-heal across ~8 restarts. Bot posts
reworked mid-gate to the Operator's embed-card spec (see commit 63664f6);
legacy text posts converted in place. Bringup defects fixed at the root:
Discord's 2025 Pin Messages permission split (invite bits + pin-repair),
thread-notice cleanup, spt_int config-dir isolation after the suite spawned a
real daemon. Cross-process int: `tests/daemon_int.rs` (lock-yield before any
network + status across the process boundary) — int added to
REQ-DAEMON-LIFECYCLE + REQ-HAZARD-DOUBLE-DAEMON. **Deferral, recorded:**
REQ-RECONCILE-LOOP keeps doc/impl/unit — its int evidence this gate is the
live drive above; an automated int needs a scratch-guild harness, revisit if
reconcile regresses. Teardown done (test channel deleted, testbed dropped
from daemon.toml, #26 closed as Cut). Dogfood seeds filed:
spt-bs-core#61/#62/#63, doyle acked ("banked to triage backlog, Bot cards
mean I do not need to poll"). Note for Phase 4: daemon-configured Projects
never ran the bind-time label bootstrap — spt-bs-core's reserved labels were
created by hand this gate; fold label-ensure into provisioning or setup.

<!-- [doc->REQ-TOC-ALCHEMISTS] [doc->REQ-TOC-COUNTS] [doc->REQ-SEED-MIRROR-LINK]
     [doc->REQ-SEED-DELETE-MIRROR] [doc->REQ-UPDATES-FEED] -->
**W13 — post-gate polish (Operator spec, 2026-07-22).** Build before Phase 4:

1. **TOC: Alchemists section** — living list of the Project's Alchemists in
   each Contents post, each an @mention (member list of the `alchemist:<chan>`
   role; bot owner noted). Refreshed like everything else — reconcile-owned,
   since role membership changes without provisioning running.
2. **TOC: item counts** — `- [Seeds](url) - **7**` beside Seeds, Releases,
   and every State thread; exclude Updates only. Counts = bot posts in each
   thread. Makes the Contents post reconcile-refreshed (counts drift on every
   mint/relocation/announce), no longer provision-only.
3. **Seeds mirror: title IS the link** — entry becomes
   `- [first line](jump url)`; no separate URL. Machine key moves to the
   masked link's url part (same jump-link string).
4. **Fix: seed deletion orphans the mirror** — handle message_delete: OG Seed
   deleted → its Seeds-thread entry deleted. (Today the bot does nothing.)
5. **Updates thread: ALL transitions, live, linked** — `new seed added`
   (capture), `seed → Backlog` (mint), `Backlog → Eval`, … each linking the
   corresponding Discord post (jump link to the card/mirror). Capture-side
   events post directly; reconcile keeps covering label-side transitions.

*DONE (2026-07-23):* five reqs activated (REQ-TOC-ALCHEMISTS, REQ-TOC-COUNTS,
REQ-SEED-MIRROR-LINK, REQ-SEED-DELETE-MIRROR, REQ-UPDATES-FEED), doc/impl/unit
green — 123 unit + 3 int, registry 39/39. One `desired_contents` renders the
Contents post (counts + Alchemists roster) for BOTH provisioning and the new
reconcile-owned `refresh_contents`, so the two can never fight; refresh is
zero-mutation when consistent and never posts (a missing pin cannot fork).
Seeds mirror is a masked link keyed by its url part; legacy bare-URL entries
stay matched — edit-mirror and delete both shapes, no migration. New
`message_delete` handler clears the mirror; post-mint cleanup maps to None
and no-ops. Updates notes are masked jump links: capture + mint post
directly from their handlers, reconcile links each reposted card.
Live-verified on the real server: boot provisioning rewrote the _spt-bs-core
Contents post to the new shape; #61 driven Backlog→Eval→Backlog relocated
both ways with linked notes and `contents refreshed` firing on count drift
(board restored after). Capture-side surfaces (seed post → masked mirror,
author delete → mirror cleared, capture/mint notes) are human-event-shaped —
Operator drive pending, same bar as W12's capture drive. NOTE: the member
listing REST endpoint needs the portal's **Server Members Intent** once an
`alchemist:*` role exists — until granted the roster stays owner-only and
`contents ... FAILED` lines appear in daemon.log each cycle.

Then Phase 4 (Watches + registry, milestones, liaisons, `#< … #>` tags,
legacy import).

## Non-goals (Phase 3)

Watches + Watch registry, liaisons, `#< … #>` request tags, milestones
automation, legacy import (all Phase 4); multi-guild support (one Requests
server); commit-graph release membership (standing Phase 2 approximation);
attachment GC (standing non-goal); defense against deliberately configuring
two daemon nodes (ruling 4).
