# M9 — Adapter customization & session surfaces (task plan)

> **STATUS: PLANNED (2026-06-12, doyle — direct product of the two-day Gateway
> grill with the operator; every feature here is operator-ratified and already
> normative in CONTEXT.md @ `bd469b9`). Executor: todlando (standing loop) on
> operator greenlight. Reviewer: doyle (three gates, §Review gates).** No
> design forks remain — the grill closed them; deviations from the CONTEXT.md
> definitions are STOP-and-ping, not code-around.

> Shell-free milestone: nothing here touches the M5 shell machinery (that's
> M10). Seven features, each independently shippable, clustered into three
> PR-sized waves. The CONTEXT.md entries are the authoritative spec for every
> feature — this plan adds only sequencing, evidence choreography, and the
> code-survey notes an executor needs.

## Goal

Adapters become **customizable without forking** (profiles/strings/hints),
the messaging surface gains **user authority** (`user-msg` + Gateway
acceptance), agents gain a **durable role** (`live-role.md`), and the session
digest is **re-founded on session logs** (the ADR-0008 amendment made real,
its REQ evidence honestly re-pointed). Concretely at M9-close:

- `spt adapter list` shows `claude-spt`, `claude-spt:work` (a local profile
  that survived an adapter re-add), and a shipped profile — all spawnable as
  distinct adapter options; a loosening overlay was REFUSED at registration.
- A hook script fetches per-profile context via `spt adapter get-string` and
  a keyword in a user message fires its hint exactly once per session.
- A message sent from a Gateway-typed endpoint arrives `user-msg`; the same
  payload from a LiveAgent arrives re-stamped `msg`.
- `spt endpoint role --overwrite` sets a role that renders FIRST in the next
  start-transition injection; nothing automated ever wrote it.
- `spt endpoint digest <id>` renders a digest for a **Claude-Code-hosted**
  endpoint (topology-independent — impossible under the PTY shape), built
  from normalized history records; the PTY-parse engine is GONE; REQ-TERM-4's
  title and evidence match reality.

## What is already satisfied (don't re-build)

- **Manifest parse + registration** (`spt adapter add/remove`, manifest-first,
  schema derives → JSON Schema drift gate) — profiles/strings/hints extend
  this, never replace it.
- **Consent plumbing** (grant store + interactive escalation, M5-D3d) — the
  tighten-only floor validation reuses its vocabulary; no new consent
  machinery in M9 (per-capability gates are M10).
- **History subsystem** (Path A locate+normalize, Path B native store,
  `HistoryFetcher`) — the digest revision CONSUMES it; gaps found here are
  in-scope only as far as the digest needs (incremental tail).
- **Envelope grammar + delivery surfaces** (`spt-proto`, dispatch, poll/hook
  delivery) — `user-msg` is one new kind riding existing rails.
- **Open endpoint-type system** — Gateway acceptance is mostly *verifying*
  openness end-to-end and un-hardcoding any agent-family assumptions found.
- **ADR-0008 amendment + CONTEXT.md entries** — the spec. Done.

## Per-commit discipline

Atomic commits; gates every commit: `cargo build` · `cargo test` · `cargo
clippy -D warnings` · `cargo build --no-default-features` · `traceable-reqs
check` EXIT=0 · `xtask check`. **Rule 3:** every new REQ minted in the toml in
the same change that starts satisfying it; **rule 5:** `required_stages`
activate with their evidence, never before. Executor surveys the existing
REQ-id numbering before minting (namespaces: `REQ-MANIFEST-*`, `REQ-MSG-*`,
`REQ-EP-*`, `REQ-TERM-*`). Branch per wave → PR → CI both runners → doyle
gate → merge. No tag/release rides M9 (operator calls the release separately).

---

## Wave 1 — the manifest cluster (branch `m9-adapter-custom`)

### T1 — Adapter profiles
The keystone. Per CONTEXT.md §adapter profile, exactly:
- Parse **shipped profiles** (`[profiles.<name>]` tables in the parent
  manifest) and **local profiles** (node-local overlay files registered
  beside the adapter; survive `adapter add` re-registration). Local may not
  shadow a shipped name (refuse at registration).
- **Leaf-replace merge** as a pure function (a profile key replaces the whole
  value at that path; arrays replaced, never spliced) — unit table is the
  contract: scalar/table/array replacement, nested paths, no-op profile.
- **Tighten-only consent floors**: a profile that loosens any
  `require_approval`-class field vs its parent is invalid at registration
  (pure validation fn + unit table).
- **Composite addressing** `<adapter>:<profile>`: survey EVERY `adapter_name`
  consumer (perch `info.json`, manifest/capability resolution, `api`
  invocations, telemetry, `spt adapter list`) and resolve through the merged
  view. The bare name = parent unmodified.
- CLI: `spt adapter create-profile <adapter> <name>` / `delete-profile`
  (local only — shipped profiles are refused), `adapter list` rendering
  parent + profiles adjacent.
Evidence: mint `REQ-MANIFEST-<n>` (profiles) `[doc, impl, unit]` — doc =
CONTEXT.md entry (tag it), impl/unit per above.

### T2 — Adapter strings
`[strings]` manifest section (KV tree); `spt adapter get-string
<adapter-option> <key.path>` resolves through the same leaf-replace overlay;
`set-string` writes a LOCAL profile's `[strings]` only (refuses adapter-owned
files; auto-target the named local profile). **Data-only** — assert nothing
executes strings (review fact + the doc line). Evidence: `REQ-MANIFEST-<n+1>`
`[doc, impl, unit]`.

### T3 — Keyword hints
`[hints]` section: entries `{keywords (literal default, regex opt-in), text}`.
`spt api hint --session <id>` (stdin = full user message → stdout = matched
hint lines in the CONTEXT.md format). Daemon-side per-session seen-set (each
hint once per session; `/clear` = new session = natural re-arm) + **max ONE
hint emitted per message** (first match wins, declaration order). Profiles
overlay `[hints]` like any section (one unit proves it). Evidence:
`REQ-MANIFEST-<n+2>` `[doc, impl, unit]`.

**Gate G1** (doyle): the merge/validation unit tables vs the CONTEXT.md
contract; the adapter_name-consumer survey completeness; CLI surface shape.

## Wave 2 — authority + role (branch `m9-user-surfaces`)

### T4 — `user-msg`
New envelope kind in `spt-proto` (wire-compat additive — N-1 receivers must
tolerate it; verify against the EVENTPART/parser hazards). Daemon-side
**identity gate**: `user-msg` permitted from user-backed origins only
(Gateway-typed endpoints, the local user's CLI send path); agent-family
senders re-stamped to `msg` (never rejected — degraded, loud in the log).
Delivery surfaces render the type (`<EVENT type="user-msg">`). Evidence:
`REQ-MSG-<n>` `[doc, impl, unit]` (unit = the gate truth table:
gateway-origin pass / CLI pass / agent re-stamp).

### T5 — Gateway type acceptance
End-to-end verification + un-hardcoding: a `Gateway`-typed perch binds
(`api bind`), advertises in the registry, is addressable, may own shells
(ownership checks must not assume agent family — survey `shellhost.rs`
owner validation; the full audit is M10, but a Gateway must SPAWN today),
subscribes to digests, and is the T4 gate's user-backed origin. In-tree mock
gateway fixture (manifest + fake binary, the R-DOCS-2 mock-adapter pattern —
no lecturn code in spt-core, per the adapter boundary). Evidence:
`REQ-EP-<n>` `[doc, impl, unit]`.

### T6 — `live-role.md`
File lives in `tracked/` beside `live-context.md` (replicates with the mind).
Start-transition injection assembly renders **role → live context → project
context** (survey the psyche-download/context-injection assembly point).
`spt endpoint role` (print) / `--overwrite <file>` (replace; the SOLE
writer). **Mechanical no-automated-writer guarantee**: unit/review evidence
that Psyche reconcile, echo-commune, and signoff writers structurally exclude
the file. Evidence: `REQ-EP-<n+1>` `[doc, impl, unit]`.

**Gate G2** (doyle): wire-compat of the new kind (N-1 tolerance), the
identity-gate truth table, injection order, no-automated-writer proof.

## Wave 3 — the digest revision (branch `m9-digest-logs`)

### T7 — Session-digest log projection
The heavy one. Per the ADR-0008 amendment:
- **T7a — projection engine**: digest buffer built from normalized history
  records (the echo-commune pipeline's format), keeping the presentation
  formula (N-turn window, collapsed tool sprints, ~25-char arg truncation).
  Incremental tail (file-watch / record-cursor — design note in plan only;
  executor picks the mechanism with the history subsystem's grain).
  `api digest-entry` = the push door for log-less adapters (entries arrive
  pre-formed). Unit: projection table from fixture records; digest-entry
  merge.
- **T7b — re-wire + retire**: `spt endpoint digest` (snapshot pull) + the
  structured-delta subscription ride the new engine; the PTY-parse path
  (`spt-daemon::digest` pattern engine + `spt-term::digest` + the manifest
  `pty_digest` seam) is REMOVED with its units; `docs/MANIFEST.md` drops the
  pattern-seam schema (digest needs no manifest seam — it rides `[history]`).
  CC-hosted digest E2E = the topology-independence proof (fixture JSONL via
  the Path A locate+normalize of the mock adapter).
- **REQ-TERM-4 re-point** (restoration-style): retitle (log-source, not
  "adapter-supplied patterns over broker PTY"), move its `impl`/`unit`/`int`
  tags onto the new engine + E2E **in the same commit** the old evidence is
  deleted — never a moment un-evidenced.

**Gate G3** (doyle, pre-merge of wave 3 + milestone close): REQ-TERM-4
re-point audit (no orphaned tags, title honest), retire completeness (zero
PTY-parse remnants), the CC-fixture E2E, then the full-suite run + docs sweep
(mdBook digest/manifest pages, CHANGELOG `[Unreleased]` entries).

---

## Sequencing

Wave 1 (T1→T2→T3, one PR) → **G1** → Wave 2 (T4→T5→T6, one PR; T4 before T5
because the Gateway fixture exercises the gate) → **G2** → Wave 3 (T7a→T7b,
one PR) → **G3** → milestone close (ROADMAP ✅ + commune). Waves 1 and 2 are
independent in principle — parallelize only if two executors exist; otherwise
sequential as listed. Wave 3 last: it deletes code and re-points active
evidence, so it lands on the quietest tree.

## Traceability

Five new REQs (exact numbers surveyed at mint): profiles, strings, hints
(`REQ-MANIFEST-*`), user-msg (`REQ-MSG-*`), Gateway acceptance + live-role
(`REQ-EP-*`); each activated `[doc, impl, unit]` in its landing commit (doc =
the CONTEXT.md entries, tagged). One re-point: REQ-TERM-4 (title + evidence,
in-commit with the retire). `traceable-reqs check` EXIT=0 at every commit.

## Risks / watch-items

- **Composite addressing is a cross-cutting rename-class change** — the
  `adapter_name` consumer survey (T1) must be exhaustive; a missed consumer
  resolves a profile to its parent silently. G1 reviews the survey list
  itself, not just the diff.
- **N-1 wire tolerance for `user-msg`**: v0.4.2 receivers must not choke on
  the unknown kind (the EVENTPART parser hazards bound this — verify against
  REQ-HAZARD-ENVELOPE-DECODE-ORDER's posture, and the n1-gate covers the
  broker IPC half).
- **REQ-TERM-4 carries `int` today** — the re-point must land a replacement
  `int` (the CC-fixture E2E) in the same commit, restoration discipline.
- **History-subsystem grain**: if Path A normalize proves too coarse for
  incremental tailing, T7a's fallback is Path-B-first (native records tail
  cleanly) + Path A snapshot-only — an accepted degradation to flag at G3,
  not silently ship.
- **Don't expand scope**: no shell work (M10), no rc (M11), no lecturn code,
  no PresenceChannel. Profiles do NOT grow conditional logic (no if/else in
  manifests — overlays only).

## Immediate next step

On operator greenlight: todlando starts T1 (profiles — schema, merge fn +
unit table, registration validation, addressing survey), pings doyle at G1
after Wave 1 lands CI-green.
