# PSYCHE-EPHEMERAL-PLAN — psyche invocation redesign (resident wrapper → ephemeral shim)

> JIT plan per AGENTS.md. Operator-directed (doyle work-order 2026-07-04, brief 9e7583cc,
> saved in session scratchpad). doyle gates this plan; build + publish on his greenlight.
> Non-goal reaffirmed: ZERO spt-core source changes — any core gap found = STOP + classify
> to doyle with evidence (F-027-style).

## Scope

Replace the resident `claude-spt psyche` wrapper (seed turn + perch-poll pulse loop +
thrash guard, `tools/claude-spt/src/psyche.rs`) with a **run-to-completion ephemeral
shim**: one invocation = one headless `claude -p` turn, then exit. The daemon drives
every turn — `[session.psyche_init]` for the first, a **newly declared**
`[session.psyche_resume]` for each subsequent event ({psyche_prompt} carries the event,
per the work-order premise). Continuity = the CC transcript + session id, not a process.

Payoff chain (from the brief): ephemeral shim picks up adapter updates on next invocation
by construction → dissolves the psyche's reason for the core-side `.live-bin` own-copy
dance (core follow-on, not ours) → removes the last update-in-place wart.

## Design decisions (gate criteria a)

### D1 — Shim stays a `claude-spt` subcommand (not direct `claude` in the manifest)

`claude-spt psyche` is REPURPOSED to the ephemeral shim (same subcommand name, resident
loop deleted). Justification vs a direct `claude` template:
1. The sandbox flag bundle (`--model sonnet … --tools Read,Edit,Write`,
   ADR-0003 legacy-parity box) stays versioned + unit-testable in ONE place; a manifest
   template would freeze it into spt-core's registered copy until the next manifest apply.
2. Greedy `--prompt` parsing stays as defense-in-depth (tokenize-then-fill gives one argv
   element since v0.8.2, but the shim is the documented backstop — F-019 heritage).
3. Windows spawn semantics + `Stdio::null` + exit-code shaping (see D4) need a binary.
4. The blank-event billing guard (D5) needs code.

### D2 — Session identity: uuid_inject, NO capture-and-report

The daemon fills `{session_id}` into BOTH psyche roles (published contract: same four
keys `{id, session_id, psyche_dir, psyche_prompt}` for both). Claude Code headless
accepts an EXPLICIT id: `--session-id <uuid>` ("must be a valid UUID", verified against
`claude --help` on this box) and resumes by id with `-r <session_id>`. So:

- **init turn**: shim runs `claude -p {psyche_prompt} --session-id {session_id} [sandbox]`
  (cwd = `{psyche_dir}`) — CC *adopts* the daemon's sid; nothing to capture, nothing to
  report back. The `[identity]`/post_spawn machinery is NOT needed for the psyche.
- **resume turn**: `claude -p {psyche_prompt} --resume {session_id} [sandbox]`.

**Sid source (doyle gate answer Q1b, binding): the F-030 core wave mints a PSYCHE-OWN
sid, persists it on the psyche perch record, and fills `{session_id}` for BOTH roles
from there** (reseed = mint fresh sid + re-fire psyche_init — which also settles reseed
custody). Today's fill threads the PARENT live session's sid (livehost base_keys) — CC
adopting that would collide with the parent transcript, so the shim contract targets the
F-030 fill and the floor (D7) enforces it. No adapter-side workaround.

### D3 — Manifest rewrites

```toml
[session.psyche_init]
command = "{adapter_dir}/claude-spt psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
cwd = "{psyche_dir}"
detach = true
env_remove = ["OWL_SESSION_ID", "SPT_AGENT_ID"]   # F-028 C2 identity scrub — unchanged
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

[session.psyche_resume]   # NEW declaration (published role, never declared by us before)
command = "{adapter_dir}/claude-spt psyche --resume --id {id} --session-id {session_id} --prompt {psyche_prompt}"
cwd = "{psyche_dir}"
detach = true
env_remove = ["OWL_SESSION_ID", "SPT_AGENT_ID"]
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
```

The init template is argv-identical to today's (mode change is in the binary) — a
deliberate skew-safety property: new-binary × old-registered-manifest still behaves
ephemerally sane (one seed turn, exit 0), and old-binary × new-manifest only appears
during a partial apply (core applies manifest+binary atomically; noted, not designed for).

### D4 — Failure semantics: LOUD, nonzero, run-to-completion

- `claude` spawn fails or exits nonzero → shim exits NONZERO with a one-line stderr
  marker (`PSYCHE_TURN_FAIL:<id>: …`). Ephemeral death is exactly what the daemon's
  `psyche_host_error` surface consumes (work-order premise 4); we never retry internally
  (core C3(b) owns pacing/crashloop). No silent degrade: a resume-turn whose transcript
  is gone does NOT silently reseed — it fails loud (the daemon owns the reseed decision
  via psyche_init; an adapter-side auto-reseed would fork sid custody). [F-019/KH §2.6
  channel lessons: fail as a process death, not a log line nobody reads.]
- Thrash guard DELETED — no loop to guard. Crashloop = repeated ephemeral deaths =
  core C3(b) ledger-rate backstop territory, by design.

### D5 — Blank-event guard stays in the shim

Today `is_actionable()` prevents a blank pulse from burning a billed claude turn. The
equivalent survives: blank/whitespace `{psyche_prompt}` → exit 0 WITHOUT spawning claude
(cheap, unit-testable, keeps the billing guard adapter-side where the model cost lives).

## Duty relocation table (gate criteria c)

| Wrapper duty today | Post-redesign home | Status |
|---|---|---|
| sid custody (implicit via `--continue` cwd-recency) | core: daemon fills `{session_id}` both roles; CC adopts via `--session-id`/`--resume` | already core (premise 3) |
| seed turn | shim init mode | this plan |
| pulse ingestion → turn driving (`spt ready --once` loop) | daemon livehost invokes psyche_resume per event | already core (premise 3) |
| psyche perch registration (`<parent>-psyche`) | **core (existing `spawn_and_bind` — binds the nested psyche perch at every psyche spawn already); the shim must NOT mint** (doyle gate answer Q2: the runner's ready-once minting was redundant legacy; dies with the wrapper; list-psyches + echo addressing unchanged) | resolved at gate |
| lifetime signal (perch closes ⇒ runner exits) | daemon: it simply stops invoking | core by construction |
| self-heal / crashloop | core C3(b) + daemon reconcile | already core |
| thrash guard (KH §2.6) | retired — see REQ disposition below | this plan |
| blank-pulse billing guard | shim (D5) | this plan |
| sandbox flags (ADR-0003) | shim, every turn | unchanged |
| identity-env scrub (KH §2.5) | role `env_remove` on BOTH tables | unchanged |
| detach/stdio | role `detach=true` + shim `Stdio::null` on claude | unchanged |

## Trust/env (gate criteria d)

The shim inherits the F-027 pre_spawn + perch-stamp env rules — frozen design @56211da
plus the atomic-write amendment @5744e6e (write-temp + fsync + rename; my rig incident
2026-07-04 is the demonstrated failure mode). Referenced, not re-designed. The psyche
cwd (`{psyche_dir}`) trust requirement is unchanged from today (KH §2.2 / F-h class:
untrusted cwd = CC dies at spawn — post-redesign that is a LOUD ephemeral nonzero exit
feeding psyche_host_error, strictly better than the old silent resident thrash).

## Hazard / REQ dispositions

- **REQ-HAZARD-PSYCHE-HOST-THRASH / KH §2.6**: root cause class (resident loop with
  instant cycles) is REMOVED BY DESIGN. KH entry gets a resolution note (kept for
  history per KH conventions); REQ re-scoped to the successor invariant — *the shim is
  run-to-completion: exactly one claude spawn per invocation, exit after* (unit-asserted)
  — so the hazard stays covered by a test instead of silently deactivated. The deferred
  live give-up int dies with the loop.
- **REQ-HAZARD-PSYCHE-IDENTITY-ENV / KH §2.5**: unchanged, now on two tables; manifest
  unit extended to assert env_remove on BOTH psyche roles.
- **REQ-HAZARD-PSYCHE-PERMS-DEADLOCK / KH §2.2**: unchanged (`--dangerously-skip-permissions`
  in sandbox flags, every turn).
- **REQ-SKILL-LIVE**: impl/unit anchors move from the resident loop to the shim; int
  unchanged in meaning (live bringup still hosts a Psyche — the daemon just drives it).
- New **REQ-PSYCHE-EPHEMERAL-SHIM** (registered before build, per traceability rule 3):
  the invocation contract above (init adopts sid; resume resumes it; loud nonzero fail;
  blank-event no-spawn).

## Tests (gate criteria e) — red-first per change

1. **Shim units** (fail against today's resident runner, pass post-change):
   `init_turn_adopts_the_daemon_sid` (argv carries `--session-id <sid>`, `-p`, sandbox);
   `resume_turn_resumes_by_sid` (`--resume <sid>`); `one_spawn_then_exit`
   (run-to-completion successor invariant, REQ-HAZARD-PSYCHE-HOST-THRASH);
   `blank_event_spawns_nothing_exit_zero`; `claude_failure_exits_nonzero_loud`
   (PSYCHE_TURN_FAIL on stderr); `every_turn_is_sandboxed` (kept, re-anchored).
2. **Manifest units**: `psyche_resume` table present with exactly the four keys +
   `detach` + `env_remove` pair; init/resume command shapes; schema validation
   (ci gate, existing).
3. **E2E (live-verify window)**: commune drop → daemon ingest → psyche_resume invocation
   → synthesis lands in tracked context — THROUGH the new shim. Rig: fresh probe endpoint
   live bringup on the floor core; assert (a) exactly one `claude-spt psyche` process per
   event, zero resident survivors between events; (b) commune content reflects the event.

## Wrapper deprecation + rollout (gate criteria b, f)

- **Adapter version: v0.14.0** (behavioral minor — invocation model change + new manifest
  table; not a patch).
- **Floor (min_spt_core)**: **the F-030 core release** (doyle gate answer Q1: NO current
  core drives per-event `psyche_resume` — the re-entry machinery (resume_psyche,
  run_psyche_turn, psyche_turn_and_relay) is built + unit-tested core-side with zero
  production callers; today the resident wrapper IS the driver. F-030 wires the driver +
  mints the psyche-own sid (D2) post-v0.24.0; this release TRAILS it).
  N-1 statement: new adapter on a BELOW-FLOOR core = psyche does one seed commune then
  starves (daemon never re-invokes) — silent-degradation class, hence the hard floor bump
  rather than a compat shim.
- **Build/publish sequencing (gate ruling)**: BUILD now (shim + units + manifest are
  core-independent, red-first against the contract); **PUBLISH holds** until the F-030
  core lands + a live-verify window.
- **Running resident wrappers at apply time**: the update cannot and need not stop them —
  a live wrapper runs from the core-side `.live-bin` own-copy (W3) and keeps servicing
  its session on the OLD model until that session ends; its perch closes → it exits
  (existing lifecycle). **Migration note (release notes): bounce live agents once after
  updating** (signoff/revive or endpoint restart) so their Psyches switch to the new
  model; until then they run the old one harmlessly.
- **cplugs skeleton**: untouched (no structural plugin change) — adapter.spt release only.
- **CHANGELOG**: UX-only per operator rule (what changes: Psyche updates now take effect
  immediately at the next event after an adapter update; one-time bounce note).

## Gate answers (doyle verdict 69276f6c, 2026-07-04 — CONDITIONAL PASS, edits applied)

1. **Floor**: NO current core drives per-event `psyche_resume` — the brief's premise-3
   was overstated (seams built + unit-tested core-side, zero production callers; the
   resident wrapper is today's driver). F-030 core wave (post-v0.24.0) wires
   inbound→psyche_turn_and_relay, commune-ingest→resume_psyche(event), and the reseed
   path. Floor = that release; v0.14.0 trails it. This plan = the adapter half of a
   JOINT wave.
2. **Psyche perch**: core-owned ALREADY (`spawn_and_bind` binds the nested perch at
   every psyche spawn). The runner's ready-once minting was redundant legacy — the shim
   must NOT mint. list-psyches + echo addressing unchanged.
3. **Resume-fail**: D4 CONFIRMED as the shim contract. Daemon reseed trigger
   (nonzero/ResumeError → psyche_init re-fire + psyche_host_error stamp) = F-030 design
   detail.
4. **Docs findings**: (a) per-event psyche_resume undocumented because UNWIRED — feature
   gap, docs land with F-030; (b) `{psyche_context}` example-vs-keylist = genuine
   published-docs bug, folded into the F-030 docs pass. Neither blocks the adapter half.

## Tasks

1. Register REQ-PSYCHE-EPHEMERAL-SHIM + REQ disposition edits (traceable-reqs.toml).
2. Rewrite `psyche.rs` → ephemeral shim (delete loop/guard/poll; add `--resume` mode,
   sid adoption, loud-fail, blank guard) + unit suite (red-first).
3. Manifest: `[session.psyche_resume]` table + init command sid-adoption note; manifest
   unit extensions.
4. KH §2.6 resolution note + §2.5 both-tables note; docs regen (drift gate).
5. Gates green (`ci/run-gates.sh` + `traceable-reqs check`), both-platform build, pack.
6. Version/floor/changelog per doyle's Q1 answer; publish on greenlight; migration note.
7. E2E in the next live-verify window (with the RC re-runs already queued there).

**Gate**: doyle plan-gate on this file → build → `ci/run-gates.sh` + `traceable-reqs
check` green → doyle greenlight → publish.
