# M0 Plan — Workspace + commodity layer (`spt-proto` + `spt-store`)

> **Just-in-time, lightweight.** This is the missing granularity between ROADMAP.md
> (milestone sequence) and `traceable-reqs.toml` (requirement checklist) — an ordered
> task list for **M0 only**. It honors the roadmap's "lightweight but structured, GSD
> too heavy" stance (ROADMAP §13). M1–M5 get their own JIT plans when reached.
> Branch: `dev-freeform` (so `main` can stay a baseline if this approach proves too loose).

## Goal

Ship the two bottom crates of the R-ARCH-1 stack — `spt-proto` (wire) and `spt-store`
(persistence) — as a compiling, tested foundation that M1 (local messaging + binary)
can build straight onto. Stable formats are **copied verbatim** from `claude_skill_owl`
(ADR-0001); only the new identity layer is clean-room.

**M0 done =** both crates compile · `cargo test` green · `traceable-reqs check` green with
M0 reqs *activated* (not just present) · CI runs the gate.

## Scope

In: envelope/EVENT grammar + chunker, endpoint types, Ed25519 identity primitive,
wire-version, typed+binary payloads (proto); atomic-write, spool, perch layout, registry
persistence, info.json (store). Out (later milestones): daemon/broker (M2/M3), networking
& pairing (M4), live/Psyche (M2), terminal/PTY (M3), cross-node registry *resolution* (M4 —
M0 builds local persistence only).

## Sequencing rationale

`spt-proto` before `spt-store` (store serializes proto types). Within proto: codec → EVENT
framing → chunker (depends on framing) → types → identity → version/payloads. Within store:
atomic-write first (everything else writes through it) → spool → perch layout → registry →
info.json. Each task lands its evidence tags + activates its reqs in the same commit
(TRACEABILITY rule 1), and `check` must be green before the next task.

## Tasks — `spt-proto`

| # | Task | Source | Reqs / hazards | Acceptance |
|---|------|--------|----------------|------------|
| **T1** | Envelope HTML-entity codec (amp-last) + two-slice parser | copy ✓ `poll.rs` codec + `envelope.rs` | REQ-HAZARD-ENVELOPE-DECODE-ORDER, -PARSER-SAFE | **DONE** `4fa2949` (9 tests) |
| T2 | EVENT envelope grammar: compose/parse `<EVENT attrs>body</EVENT>`, attrs (from/type/seq/id/timestamp) | copy `poll.rs` compose sites + `parse_event_from_attr` | R-ARCH-4; R-EP-3 (framing) | compose→parse round-trip; attr escaping amp-last; malformed → None, no panic |
| T3 | EVENT-PART chunker + byte-exact reassembly (long body → seq=K/M id parts; drop orphans) | copy `poll.rs`/`wrapper/mod.rs`/`resume.rs` | R-ARCH-4 (+ new REQ-HAZARD-EVENTPART-REASSEMBLY) | property test: split(N)→reassemble == identity for random bodies/splits |
| T4 | Endpoint types: ReadyAgent, LiveAgent, Psyche, Worker, SptNode + Shell/PresenceChannel **seams**; open type system | copy/adapt `types.rs` | R-EP-1, R-EP-2 | type model compiles; agent-vs-Shell split present; unknown type tolerated |
| T5 | Ed25519 identity primitive: keypair gen, sign/verify, pubkey↔string | **clean-room** (`ed25519-dalek`; sister has none) | foundation for R-NET/R-PAIR (identity only, not pairing) | sign/verify round-trip; pubkey serialize/parse stable; known-answer test |
| T6 | Wire-protocol version constant + N-1 compat window helper | clean-room | R-ARCH-3 | version exposed; N-1 accept / N-2 reject unit test |
| T7 | Payload model: typed operation commands + arbitrary binary blobs | copy text path + clean-room binary | R-EP-3 | text + binary blob round-trip through envelope encode/decode |

## Tasks — `spt-store`

| # | Task | Source | Reqs / hazards | Acceptance |
|---|------|--------|----------------|------------|
| T8 | Atomic write (tmp + rename + retry on EBUSY) + UNC-prefix strip on serialized paths | copy `owlery.rs` | REQ-HAZARD-EBUSY-RENAME, -UNC-PATH-STRIP | concurrent-writer test tolerates EBUSY; `\\?\` stripped + re-readable |
| T9 | Spool: schema + `drain_non_deferred` vs `drain_all`, deferred rows, EVENT-PART persistence | copy `spool.rs` | REQ-HAZARD-DEFERRED-DRAIN, -DEFERRED-SURVIVE-DRAIN | deferred row survives event-stream drain; non-deferred drains; ordering preserved |
| T10 | Perch layout + single path resolver (no flat/nested siblings) | copy/adapt `perch_path.rs` | REQ-HAZARD-SINGLE-PATH-SOURCE, -WORKER-PATH | all perch paths route one resolver; no divergent sibling layouts |
| T11 | Registry persistence + stale-entry cleanup (local; cross-node *resolution* deferred M4) | copy `registry.rs` | REQ-HAZARD-REGISTRY-STALE-CLEAN; R-INST-7 (partial: local) | stale (dead-PID) entry cleaned before lookup; never hard-fails on stale |
| T12 | `info.json` shape + torn-read tolerance (atomic write + read-retry) | copy verbatim | REQ-HAZARD-INFO-JSON-TORN-READ | concurrent read during write yields valid or retried, never torn |

## Tasks — infra

| # | Task | Reqs | Acceptance |
|---|------|------|------------|
| T13 | CI workflow: `cargo test` + `traceable-reqs check --json` as a hard gate | R-DOCS-5 (partial) | PR fails on red test or missing activated-req evidence |
| T14 | M0 activation sweep: confirm every M0 req has real `required_stages` + green `check` | — | `check` green with all M0 reqs activated |

## M0 requirement-activation map

Activate as each task lands (set `required_stages`, default `["impl","unit"]`; copy-verbatim
hazards that were cross-process in the sister add `int` where a store/IPC boundary exists):

- **Done:** ENVELOPE-DECODE-ORDER, ENVELOPE-PARSER-SAFE (`impl`+`unit`).
- **Proto:** R-ARCH-1/3/4, R-EP-1/2/3, + new EVENTPART-REASSEMBLY hazard.
- **Store:** EBUSY-RENAME, UNC-PATH-STRIP, DEFERRED-DRAIN, DEFERRED-SURVIVE-DRAIN,
  SINGLE-PATH-SOURCE, WORKER-PATH, REGISTRY-STALE-CLEAN, INFO-JSON-TORN-READ; R-INST-7 (partial).

**Stay `[]` (later milestones):** all R-NET/R-PAIR (M4), R-DAEMON/R-API/R-SEAM/R-START (M2),
R-TERM (M3), R-UPD (M3), R-INST-1..6/8 cross-node (M4), R-REACH (M4), the lifecycle/live hazards.

## New requirement to register before its task

- `REQ-HAZARD-EVENTPART-REASSEMBLY` — long-body EVENT-PART split/reassembly is byte-exact;
  orphan parts (seq K/M with no matching 1/M across a session boundary) are dropped silently.
  Add to `traceable-reqs.toml` when starting T3 (TRACEABILITY rule 3: new req before satisfying).

## M0 status — COMPLETE (2026-06-01)

All tasks landed on `dev-freeform`. Both crates compile · `cargo test --workspace`
green (73 tests) · `clippy -D warnings` clean · `traceable-reqs check` green with
**18 M0 reqs activated** · CI gate wired (`.github/workflows/ci.yml`).

- **T1–T7 `spt-proto`:** envelope codec/parser, EVENT grammar, EVENT-PART
  chunker + reassembler, endpoint taxonomy, Ed25519 identity, wire-version,
  payload model (typed cmd + binary blob).
- **T8–T12 `spt-store`:** atomic write (EBUSY retry) + UNC strip, SQLite spool
  (deferred-aware), perch resolver, registry (stale-clean), info.json
  (torn-read tolerant).
- **T13–T14:** CI matrix + traceability hard gate; activation sweep.

Deviations from the plan, all deliberate (see commit bodies):
- **REQ-NODE-IDENTITY** registered for T5 (plan named no req; REQ-NET-1 was the
  wrong home — WAN messaging, M4). T5 KAT pinned to dalek output, not RFC 8032
  (recollected vector was wrong).
- **R-INST-7** (T11) and **R-DOCS-5** (T13) tagged *partial* but left **inactive** —
  cross-node resolution and full doc-anti-drift are later milestones.
- **R-ARCH-1** activated `impl`-only (architecture is build-evidenced; grows as
  crates are added M1+).

**Next:** M1 (local messaging + the `spt` binary) atop these two crates — gets its
own `M1-PLAN.md` when started.

## Risks carried into M0 (from Stage A / spikes)

- None block M0 (the FATALs were ADR-0004 daemon/M3 + ADR-0005 pairing/M4). M0 is pure
  commodity-layer port + the identity primitive; lowest-risk milestone. The open ADR
  decisions (context-merge model, seed-rotation, R-UPD-3 wording) are M3/M4 and do not gate M0.
