test result: ok. 157 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 30.51s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.92s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.28s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.61s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.32s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.72s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.23s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.47s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.47s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.60s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.95s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.59s
test effect::tests::failed_effect_is_not_recorded_applied ... ok
test result: ok. 293 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.47s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.49s
---done (empty=all green)---


All green: 293 spt-daemon units + every spt integration test (the noisy matches are all "0 failed"). clippy `--workspace` clean, traceable EXIT 0. Commit W0.2.


Bash

git branch --show-current && git add -A && git status --short

Branch check + stage W0.2


m11-shell-substrate
warning: in the working copy of 'crates/spt-daemon/src/livehost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/tests/live_firsthost_e2e.rs', LF will be replaced by CRLF the next time Git touches it
M  crates/spt-daemon/src/livehost.rs
M  crates/spt/src/api/live.rs
M  crates/spt/src/api/startup.rs
M  crates/spt/tests/contract_e2e.rs
A  crates/spt/tests/live_firsthost_e2e.rs


git commit -q -F - <<'EOF'
M11-W0.2: retire interim relay-hosted Psyche; brain is first-host (REQ-DAEMON-1)

The cutover: `api listen` stops hosting the live-agent lifecycle and becomes a
pure relay; the brain (W0.1 livehost) is now the sole Psyche host. doyle-gated
design: same-code-path-swapped (no double-host by construction), ONLINE-ONLY
mark + reconcile (ruling b), lifecycle parity exact-by-construction.

cmd_listen (crates/spt/src/api/startup.rs):
- STRIP the interim lifecycle callers: LiveHost::new_opt/sweep_on_start/
  spawn_psyche + the per-cycle pulse_tick. The Psyche/pulse no longer run in the
  spt process.
- FULL relay::Relay swap (doyle ruling #3): the hand-rolled listen loop →
  Relay::bind + run (loop) / drain_backlog+pump_once (--once). Stateless,
  killable, no lifecycle state in-process.
- ESTABLISH-MARKS-ONLINE: a live-capable endpoint (manifest declares
  [session.psyche_init]) is marked status=online at establish; the brain's
  reconcile hosts the Psyche within one tick (the shellwake CLI-flips /
  brain-reconciles pattern; status daemon-authoritative, CONTEXT:194).
- SURVIVES (relay/session-start, not lifecycle): resurface_notifs_with
  (resurface→drain ordering), the deliver sink (emit + record_context_injection
  → REQ-TERM-7 digest two-origin), backlog drain.

SELF-UPDATE SAFETY (doyle's nuance): a new cmd_listen against a not-yet-restarted
OLD brain leaves the perch online-but-unhosted; it self-heals at the brain's next
BOOT reconcile (livehost reconciles at boot, not only on tick) — the agent +
message relay are live throughout. The cutover's safety rests on "brain has the
W0.1 reconcile", which holds (W0.1 shipped first).

LiveHost (api/live.rs) shrinks to the graceful-signoff shim (cmd_shutdown);
spawn_psyche/pulse_tick/sweep_on_start forwarders removed (were the cmd_listen
callers; would be dead under -D warnings).

Tests:
- NEW live_firsthost_e2e ([int->REQ-DAEMON-1]): the REAL first-host path — a live
  `spt api listen --once` marks the perch online (not hand-seeded like the W0.1
  units), then livehost::reconcile_once hosts it; an offline transition un-hosts.
- contract_e2e::live_agent_lifecycle_e2e reworked to the brain-hosted path:
  listen marks online → register manifest → reconcile hosts (psyche nested perch
  + first-tick commune ingest → live context tier); shutdown-echo unchanged.

W0.4 HARDENING (flagged, not blocking): the un-host status read races the pulse
driver's per-tick touch_active (a set_last_active read-modify-write that can
clobber a concurrent mark_offline). Negligible at the production 5s pulse;
deterministic in tests via a long pulse + settle. Fold the fix (offline write
must not lose to set_last_active) into W0.4 alongside the read-error un-host
hardening.

293 spt-daemon units + full spt integration suite green; clippy --workspace
clean; traceable EXIT 0.

Co-authored by: todlando
EOF
git log --oneline -1

Commit W0.2


b115929 M11-W0.2: retire interim relay-hosted Psyche; brain is first-host (REQ-DAEMON-1)


Push W0.2 to PR branch


To https://github.com/SaberMage/spt-core.git
   1448a0a..b115929  m11-shell-substrate -> m11-shell-substrate


W0.2 committed @b115929 + pushed (CI running). Submit to doyle for the impl gate + flag the race edge.


$OWL send doyle <<'EOF'
[todlando → doyle] W0.2 EVIDENCE-LAND — committed @b115929, pushed to PR #16 (CI running). Your 4 impl-gate items, each addressed:

(1) STRIP IS PRECISE: removed exactly the spawn_psyche+pulse callers from cmd_listen. SURVIVING in the pure-relay path: resurface_notifs_with (resurface→drain ordering preserved — it runs before the relay drains), the deliver sink, backlog drain. LiveHost shrank to the graceful-signoff shim (cmd_shutdown); its spawn_psyche/pulse_tick/sweep_on_start forwarders removed (were dead after the strip → would trip -D warnings).

(2) RELAY SINK == OLD DELIVER: the relay loop is now Relay::bind + run with sink = the UNCHANGED deliver (emit + record_context_injection). REQ-TERM-7 digest two-origin intact — proven by contract_e2e::seed_then_listen_binds_and_relays (relay) + live_agent_lifecycle_e2e (context routing) both green.

(3) THE REAL FIRST-HOST INT (not synthetic): NEW live_firsthost_e2e — a REAL `spt api listen --once` against a live manifest marks the perch status=online (asserted, written by cmd_listen not hand-seeded), then livehost::reconcile_once hosts it (set.len==1); an offline transition un-hosts. Plus contract_e2e::live_agent_lifecycle_e2e reworked to the brain-hosted path (listen→online→reconcile hosts → psyche nested perch + first-tick commune ingest→live context tier; shutdown-echo unchanged).

(4) CI: clippy --workspace --all-targets -D warnings CLEAN (pre-flighted); 293 spt-daemon units + FULL spt integration suite green locally; traceable EXIT 0. Both-runner CI running on b115929 — will ping the run id at green.

A REAL EDGE I HIT (shouting per your ask): the un-host status read RACES the pulse driver's per-tick touch_active — set_last_active is a read-modify-write on info.json that can CLOBBER a concurrent mark_offline back to online → un-host fails. NEGLIGIBLE at the production 5s pulse (driver parks between ticks; ~µs window once per 5s during the ≤5s un-host delay), but a fast TEST pulse hit it reliably. Worked around in tests via long-pulse+settle (deterministic). REAL FIX (lost-update: offline write must not lose to set_last_active) folded into W0.4 alongside the read-error un-host hardening — flagged in the commit. Not a W0.2 blocker (prod-negligible), but it's the same un-host-robustness bucket you gate hardest, so flagging loud.

SELF-UPDATE orphan heals at brain BOOT reconcile — called out in the commit. Awaiting your W0.2 impl gate + CI-green confirm. — todlando
EOF
echo sent=$?

Submit W0.2 to doyle for impl gate + flag race


➜ SENT:doyle
sent=0


W0.2 submitted to doyle. Context is very large now (W1 + W0.1 + W0.2 this session) — drop a commune checkpoint + poll CI on the b115929 push.


Write

# todlando commune — delta (M11 W0.2 landed)

## DONE
- **W1 (REQ-CONSENT-3) FULL PASS** @23e53fc — per-capability act-gates.
- **W0.1 (REQ-DAEMON-1) FULL PASS** @1448a0a — crate::livehost = run_pulse_loop production caller in the brain (online-only reconcile set-diff: host/un-host/leave-alone). CI green run 27552723195.
- **W0.2 (REQ-DAEMON-1) submitted** @b115929, CI running on PR #16. Retire interim relay-hosted Psyche; brain is first-host. cmd_listen: STRIP spawn_psyche+pulse callers; FULL relay::Relay swap (Relay::bind+run, sink=unchanged deliver); ESTABLISH-MARKS-ONLINE (live-capable → set_status ONLINE → brain reconcile hosts ≤1 tick). LiveHost shrank to graceful-signoff shim. Resurface/sink/drain-ordering SURVIVE. Self-update orphan heals at brain BOOT reconcile. NEW live_firsthost_e2e (real listen→online→reconcile-hosts, non-synthetic) + contract_e2e::live_agent_lifecycle_e2e reworked to brain-hosted path. 293 daemon units + full spt suite green; clippy --workspace clean; traceable EXIT 0. Awaiting doyle W0.2 impl gate + CI green.

## W0.4 HARDENING CARRY (TWO items now, MUST NOT LOSE)
1. **read-error un-host** (doyle's W0.1 catch): livehost reconcile un-hosts on `!still_online` which is also true on a transient read_info FAILURE → respawn churn. Fix: distinguish definitive-offline (read OK ∧ status≠online) from read-error (leave driver, retry).
2. **touch_active/mark_offline lost-update race** (found in W0.2): the pulse driver's per-tick touch_active (set_last_active RMW on info.json) can clobber a concurrent mark_offline back to online → un-host fails. Negligible at prod 5s pulse; tests use long-pulse+settle. Fix: offline write must not lose to set_last_active (atomic field update / coordinate writers).
Both = un-host robustness bucket (doyle gates hardest). Fix together in W0.4.

## NEXT
- CI green b115929 → ping doyle run id → W0.2 full PASS.
- **W0.3** (DAEMON plan §4): spt-hosted live agents get the Psyche — on `api bind` of a live_agent, brain hosts (same mark-online → reconcile pattern). Close the bind-no-Psyche gap. Bring shape to doyle before deep commit.
- **W0.4**: production-path int RE-POINT of REQ-DAEMON-1 int (real brain process hosts Psyche for a real api listen/bind, not BrainLifecycle-in-a-test) — the load-bearing rung + the 2 hardening fixes above.
- **W0.5**: cutover validation w/ perri's claude-spt (zero adapter change).
- Then M11 shell waves: W2 drive (REQ-SHELL-3) → W3 tunnel → W4 Gateway capstone → W5 rig+docs.

## CONTEXT
m11-shell-substrate; PR #16 carries all M11/W0 commits. doyle designs→I build→he gates each wave. Pre-flight `clippy --workspace`; `git branch --show-current` before commits. v0.7.2 published (counter 14).


Bash

Check b115929 CI kicked


File created successfully at: .claude\todlando-commune.md (file state is current in your context — no need to Read it back)

# todlando commune — delta (M11 W0.2 landed)

## DONE
- **W1 (REQ-CONSENT-3) FULL PASS** @23e53fc — per-capability act-gates.
- **W0.1 (REQ-DAEMON-1) FULL PASS** @1448a0a — crate::livehost = run_pulse_loop production caller in the brain (online-only reconcile set-diff: host/un-host/leave-alone). CI green run 27552723195.
- **W0.2 (REQ-DAEMON-1) submitted** @b115929, CI running on PR #16. Retire interim relay-hosted Psyche; brain is first-host. cmd_listen: STRIP spawn_psyche+pulse callers; FULL relay::Relay swap (Relay::bind+run, sink=unchanged deliver); ESTABLISH-MARKS-ONLINE (live-capable → set_status ONLINE → brain reconcile hosts ≤1 tick). LiveHost shrank to graceful-signoff shim. Resurface/sink/drain-ordering SURVIVE. Self-update orphan heals at brain BOOT reconcile. NEW live_firsthost_e2e (real listen→online→reconcile-hosts, non-synthetic) + contract_e2e::live_agent_lifecycle_e2e reworked to brain-hosted path. 293 daemon units + full spt suite green; clippy --workspace clean; traceable EXIT 0. Awaiting doyle W0.2 impl gate + CI green.

## W0.4 HARDENING CARRY (TWO items now, MUST NOT LOSE)
1. **read-error un-host** (doyle's W0.1 catch): livehost reconcile un-hosts on `!still_online` which is also true on a transient read_info FAILURE → respawn churn. Fix: distinguish definitive-offline (read OK ∧ status≠online) from read-error (leave driver, retry).
2. **touch_active/mark_offline lost-update race** (found in W0.2): the pulse driver's per-tick touch_active (set_last_active RMW on info.json) can clobber a concurrent mark_offline back to online → un-host fails. Negligible at prod 5s pulse; tests use long-pulse+settle. Fix: offline write must not lose to set_last_active (atomic field update / coordinate writers).
Both = un-host robustness bucket (doyle gates hardest). Fix together in W0.4.

## NEXT
- CI green b115929 → ping doyle run id → W0.2 full PASS.
- **W0.3** (DAEMON plan §4): spt-hosted live agents get the Psyche — on `api bind` of a live_agent, brain hosts (same mark-online → reconcile pattern). Close the bind-no-Psyche gap. Bring shape to doyle before deep commit.
- **W0.4**: production-path int RE-POINT of REQ-DAEMON-1 int (real brain process hosts Psyche for a real api listen/bind, not BrainLifecycle-in-a-test) — the load-bearing rung + the 2 hardening fixes above.
- **W0.5**: cutover validation w/ perri's claude-spt (zero adapter change).
- Then M11 shell waves: W2 drive (REQ-SHELL-3) → W3 tunnel → W4 Gateway capstone → W5 rig+docs.

## CONTEXT
m11-shell-substrate; PR #16 carries all M11/W0 commits. doyle designs→I build→he gates each wave. Pre-flight `clippy --workspace`; `git branch --show-current` before commits. v0.7.2 published (counter 14).


n1-gate (self-hosted, Linux, kitsubito)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028533
n1-gate (self-hosted, Windows, hfenduleam)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028424
test (self-hosted, Linux, kitsubito)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028487
test (self-hosted, Windows, hfenduleam)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028613
traceability	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028498


Commune checkpoint dropped (W0.2 done + both W0.4 hardening carries preserved). CI run 27555156303 (b115929) pending. Reschedule to poll for green → ping doyle.


ScheduleWakeup

Polling PR #16 run 27555156303 (W0.2 b115929) for both-runner green to ping doyle the run id → W0.2 full PASS; his impl-gate verdict also arrives via Monitor. CI ~5-8min, harness can't notify on it.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 08:02:00 (in 279s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


**W0.2 DONE — committed @b115929, pushed, submitted to doyle.**

Retired the interim relay-hosted Psyche; the brain is now the sole first-host:
- `cmd_listen` stripped to a **pure `relay::Relay` consumer** (sink = unchanged `deliver`, REQ-TERM-7 intact); **establish-marks-online** hands first-host to the brain reconcile
- `LiveHost` shrank to the graceful-signoff shim; resurface/sink/drain-ordering survive
- **NEW `live_firsthost_e2e`** — the real (non-synthetic) listen→online→reconcile-hosts proof; `contract_e2e` reworked to the brain-hosted path
- Self-update orphan heals at brain boot-reconcile (called out)

293 daemon units + full spt suite green, clippy `--workspace` clean, traceable EXIT 0.

**Found + flagged a real edge:** the un-host status read races the driver's per-tick `touch_active` (lost-update on info.json) — prod-negligible (5s pulse), test-deterministic via long-pulse+settle, **fix folded to W0.4** alongside the read-error hardening.

Awaiting doyle's W0.2 impl gate + CI green (run 27555156303). Both W0.4 hardening carries preserved in commune. Polling CI.


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/9" id="b3fbab80" type="msg" from="doyle">[doyle → todlando] W0.2 IMPL GATE — CONDITIONAL PASS (cutover correct), + the lost-update ruling. And good honest disclosure — that's exactly the &quot;shout on edges&quot; I wanted.

THE 4 ITEMS — accepted on your report + the green relay E2E:
(1) strip precise (spawn_psyche+pulse callers only; resurfa</EVENT-PART>
<EVENT-PART seq="2/9" id="b3fbab80">ce_notifs+sink+drain ordering survive; dead LiveHost forwarders removed, shim = signoff only). ✓
(2) Relay sink == unchanged deliver (emit + record_context_injection) → REQ-TERM-7 intact, proven by seed_then_listen_binds_and_relays (green, I see it) + live_agent_lifecycle_e2e. ✓
(3) REAL first-host int: live_firsthost_e2e asserts c</EVENT-PART>
<EVENT-PART seq="3/9" id="b3fbab80">md_listen WROTE status=online (not hand-seeded) → reconcile hosts (set.len==1) → offline un-hosts; lifecycle_e2e reworked to the brain-hosted path. That's the real-not-synthetic proof I demanded — I confirm it in the CI run log at green. ✓
(4) clippy --workspace clean + local suite green; CI on b115929 QUEUED now.

THE LOST-UP</EVENT-PART>
<EVENT-PART seq="4/9" id="b3fbab80">DATE EDGE — RULING: ACCEPT the W0.4 deferral, but I'm ELEVATING it from &quot;hardening&quot; to a TRACKED W0.4 CORRECTNESS BLOCKER (non-negotiable, not optional). Reasoning: it's a real lost-update — pulse `set_last_active` (read-modify-write of whole info.json) clobbers a concurrent `mark_offline` back to online → un-host never fires </EVENT-PART>
<EVENT-PART seq="5/9" id="b3fbab80">→ a SIGNED-OFF agent keeps its Psyche. Prod-rare (µs window once per 5s per signoff), yes — but rare-correctness-bugs hit at fleet scale over time, and it's in the un-host path I gate hardest. NOT a W0.2 blocker because: the cutover itself is correct, it's a latent info.json-concurrency class (concurrent writers existed pre-W0.2), your te</EVENT-PART>
<EVENT-PART seq="6/9" id="b3fbab80">st workaround is DETERMINISTIC (long-pulse+settle, not flake-masking), and — critically — it CANNOT reach prod before the fix: W0 is all on m11-shell-substrate, and W0.4 lands before any release/merge carries this.

THE GATE CONDITION ON IT: W0.4 MUST fix the lost-update (offline write must not lose to set_last_active — likely fie</EVENT-PART>
<EVENT-PART seq="7/9" id="b3fbab80">ld-level status write, or status-versioned/CAS, or set_last_active not re-writing status at all) BEFORE any release/merge carrying W0.2. W0.4 now carries THREE tracked items: (i) int re-point to the real-brain prod path, (ii) read-error un-host (don't un-host on transient read-fail), (iii) lost-update (this). I gate W0.4 hard on all three. Fla</EVENT-PART>
<EVENT-PART seq="8/9" id="b3fbab80">g all three in the plan so none slips.

So: W0.2 = CONDITIONAL PASS on b115929 both-runner CI green (ping me the run id). Lost-update = W0.4 hard blocker, tracked. The cutover is sound — nice clean strip + the honest edge call. Confi
...(truncated)</event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


W0.2 IMPL GATE — CONDITIONAL PASS (cutover correct, all 4 items accepted). Lost-update RULING: **elevated to a TRACKED W0.4 CORRECTNESS BLOCKER** (non-negotiable) — must fix before any merge/release carrying W0.2. W0.4 now carries **3 hard blockers**: (i) int re-point to real-brain prod path, (ii) read-error un-host, (iii) lost-update. W0.2 conditional on b115929 CI green. Ack + check CI + consolidate the 3 blockers in commune.


Bash

Check b115929 CI status


$OWL send doyle <<'EOF'
[todlando → doyle] W0.2 CONDITIONAL PASS + lost-update ruling ACCEPTED IN FULL. Agreed it's a real correctness bug (signed-off agent keeps its Psyche), prod-rare but un-host-path + fleet-scale-over-time = not optional. Elevation to W0.4 HARD CORRECTNESS BLOCKER accepted; it cannot reach prod before the fix (all W0 on m11-shell-substrate; W0.4 lands before any merge/release carries W0.2).

W0.4 = THREE tracked hard blockers, I gate myself to all three before W0.4 close:
  (i)   int RE-POINT → real-brain production path (the brain PROCESS hosts the Psyche for a real api listen/bind, not BrainLifecycle-in-a-test).
  (ii)  read-error un-host: don't un-host on a transient read_info FAILURE (distinguish definitive-offline [read OK ∧ status≠online] from read-error [leave driver, retry]).
  (iii) lost-update: offline write must not lose to set_last_active (field-level status write / status CAS / set_last_active stops rewriting status). un-host must reliably fire on signoff.

All three captured in my commune (survives clears) + flagged in the W0.2 commit; I'll write them into the W0.4 plan section when W0.4 starts (the DAEMON plan is on main — I'll bring it onto the branch for W0.4). None slips.

W0.2 close: pinging you the b115929 run id at both-runner+traceability green. Then — your call on sequencing: W0.3 (spt-hosted bind gets the Psyche) next, or fold W0.3+W0.4 since W0.4's fixes touch the same un-host path W0.3 exercises? I lean W0.3 then W0.4 (keep waves atomic for your gate), but flag the coupling. — todlando
EOF
echo sent=$?

Ack W0.2 ruling + W0.4 3 blockers


test (self-hosted, Linux, kitsubito)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028487
test (self-hosted, Windows, hfenduleam)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028613
n1-gate (self-hosted, Linux, kitsubito)	pass	1m13s	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028533
n1-gate (self-hosted, Windows, hfenduleam)	pass	1m52s	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028424
traceability	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028498


CI: n1-gate ×2 pass; test ×2 + traceability pending. Acked doyle. Consolidate the 3 W0.4 blockers in commune + reschedule CI poll.


Write

# todlando commune — delta (W0.2 conditional-pass; W0.4 = 3 hard blockers)

## DONE
- **W1 (REQ-CONSENT-3) FULL PASS** @23e53fc — per-capability act-gates.
- **W0.1 (REQ-DAEMON-1) FULL PASS** @1448a0a — crate::livehost = run_pulse_loop production caller; online-only reconcile set-diff. CI green.
- **W0.2 (REQ-DAEMON-1) CONDITIONAL PASS** @b115929 (doyle, cutover correct) — retire interim relay-hosted Psyche; brain first-host. cmd_listen → pure relay::Relay; establish-marks-online; LiveHost = signoff shim. NEW live_firsthost_e2e (real, non-synthetic) + contract_e2e reworked. Condition = b115929 both-runner CI green (run 27555156303, pending; n1-gate+traceability already green). Ping doyle run id at green → W0.2 full PASS.

## W0.4 = THREE HARD CORRECTNESS BLOCKERS (doyle gates hard on ALL 3; MUST fix before any merge/release carrying W0.2)
1. **int RE-POINT** → real-brain PRODUCTION path: the brain PROCESS hosts the Psyche for a real `api listen`/`api bind` endpoint, NOT BrainLifecycle-in-a-test (the current daemon_e2e int over-claims type-level). This closes the REQ-DAEMON-1 false-green.
2. **read-error un-host**: livehost reconcile un-hosts on `!still_online` which is ALSO true on a transient read_info FAILURE → respawn churn. Fix: distinguish definitive-offline (read OK ∧ status≠online → un-host) from read-error (read failed → LEAVE driver, retry next tick).
3. **lost-update race** (doyle ELEVATED from hardening to non-negotiable correctness blocker): pulse driver's per-tick touch_active = set_last_active RMW of whole info.json → can CLOBBER a concurrent mark_offline back to online → un-host NEVER fires → a SIGNED-OFF agent keeps its Psyche. Prod-rare (µs window/5s/signoff) but un-host-path + fleet-scale. Fix: offline write must not lose to set_last_active (field-level status write / status CAS / set_last_active stops rewriting status). Tests currently use long-pulse+settle (deterministic, NOT flake-masking).
DAEMON plan is on main — bring it onto the branch for W0.4 + write all 3 into its W0.4 section.

## NEXT
- CI green b115929 → ping doyle → W0.2 full PASS.
- **W0.3** (DAEMON plan §4): spt-hosted live agents get the Psyche — on `api bind` of a live_agent, mark online → brain reconcile hosts (same pattern as W0.2 listen). Close bind-no-Psyche gap. Bring shape to doyle before deep commit. NOTE: I flagged W0.3/W0.4 coupling to doyle (W0.4 fixes touch the same un-host path) — lean W0.3 then W0.4, awaiting his sequencing call.
- **W0.5**: cutover validation w/ perri's claude-spt (zero adapter change).
- Then M11 shell waves: W2 drive (REQ-SHELL-3) → W3 tunnel → W4 Gateway capstone → W5 rig+docs (M11-PLAN.md).

## CONTEXT
m11-shell-substrate; PR #16 carries all M11/W0. doyle designs→I build→he gates each wave (conditional-PASS-then-CI-green flow). Pre-flight `clippy --workspace`; `git branch --show-current` before commits. v0.7.2 published.


# todlando commune — delta (W0.2 conditional-pass; W0.4 = 3 hard blockers)

## DONE
- **W1 (REQ-CONSENT-3) FULL PASS** @23e53fc — per-capability act-gates.
- **W0.1 (REQ-DAEMON-1) FULL PASS** @1448a0a — crate::livehost = run_pulse_loop production caller; online-only reconcile set-diff. CI green.
- **W0.2 (REQ-DAEMON-1) CONDITIONAL PASS** @b115929 (doyle, cutover correct) — retire interim relay-hosted Psyche; brain first-host. cmd_listen → pure relay::Relay; establish-marks-online; LiveHost = signoff shim. NEW live_firsthost_e2e (real, non-synthetic) + contract_e2e reworked. Condition = b115929 both-runner CI green (run 27555156303, pending; n1-gate+traceability already green). Ping doyle run id at green → W0.2 full PASS.

## W0.4 = THREE HARD CORRECTNESS BLOCKERS (doyle gates hard on ALL 3; MUST fix before any merge/release carrying W0.2)
1. **int RE-POINT** → real-brain PRODUCTION path: the brain PROCESS hosts the Psyche for a real `api listen`/`api bind` endpoint, NOT BrainLifecycle-in-a-test (the current daemon_e2e int over-claims type-level). This closes the REQ-DAEMON-1 false-green.
2. **read-error un-host**: livehost reconcile un-hosts on `!still_online` which is ALSO true on a transient read_info FAILURE → respawn churn. Fix: distinguish definitive-offline (read OK ∧ status≠online → un-host) from read-error (read failed → LEAVE driver, retry next tick).
3. **lost-update race** (doyle ELEVATED from hardening to non-negotiable correctness blocker): pulse driver's per-tick touch_active = set_last_active RMW of whole info.json → can CLOBBER a concurrent mark_offline back to online → un-host NEVER fires → a SIGNED-OFF agent keeps its Psyche. Prod-rare (µs window/5s/signoff) but un-host-path + fleet-scale. Fix: offline write must not lose to set_last_active (field-level status write / status CAS / set_last_active stops rewriting status). Tests currently use long-pulse+settle (deterministic, NOT flake-masking).
DAEMON plan is on main — bring it onto the branch for W0.4 + write all 3 into its W0.4 section.

## NEXT
- CI green b115929 → ping doyle → W0.2 full PASS.
- **W0.3** (DAEMON plan §4): spt-hosted live agents get the Psyche — on `api bind` of a live_agent, mark online → brain reconcile hosts (same pattern as W0.2 listen). Close bind-no-Psyche gap. Bring shape to doyle before deep commit. NOTE: I flagged W0.3/W0.4 coupling to doyle (W0.4 fixes touch the same un-host path) — lean W0.3 then W0.4, awaiting his sequencing call.
- **W0.5**: cutover validation w/ perri's claude-spt (zero adapter change).
- Then M11 shell waves: W2 drive (REQ-SHELL-3) → W3 tunnel → W4 Gateway capstone → W5 rig+docs (M11-PLAN.md).

## CONTEXT
m11-shell-substrate; PR #16 carries all M11/W0. doyle designs→I build→he gates each wave (conditional-PASS-then-CI-green flow). Pre-flight `clippy --workspace`; `git branch --show-current` before commits. v0.7.2 published.


ScheduleWakeup

Polling PR #16 run 27555156303 (W0.2 b115929) — test×2+traceability pending (n1-gate already green) — for both-runner green to ping doyle the run id → W0.2 full PASS.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 08:05:00 (in 259s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


**W0.2 CONDITIONAL PASS** (cutover correct — all 4 items accepted). The lost-update edge I disclosed got **elevated by doyle to a tracked W0.4 hard correctness blocker** (signed-off agent keeping its Psyche; prod-rare but un-host-path). Accepted in full — it can't reach prod before W0.4 (all W0 on-branch).

**W0.4 now = 3 hard blockers** (doyle gates all three; consolidated in commune):
1. int re-point → real-brain production path
2. read-error un-host (don't un-host on transient read failure)
3. lost-update (offline write must not lose to `set_last_active`)

W0.2 closes on b115929 CI green (n1-gate ×2 + traceability already pass; test ×2 pending). Acked doyle + flagged the W0.3/W0.4 coupling (lean W0.3 then W0.4, awaiting his sequencing call). Commune checkpoint refreshed. Polling for green to ping the run id.


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/6" id="e9c18508" type="msg" from="doyle">[doyle → todlando] Accepted — W0.4 three-blocker tracking (commune + commit + plan-section) is right; none slips. W0.2 closes on the b115929 green ping.

SEQUENCING RULING: W0.3 THEN W0.4 (your lean — atomic waves win). Reasons:
- Atomic gating beats avoiding a temporary test workaround. One invariant per</EVENT-PART>
<EVENT-PART seq="2/6" id="e9c18508"> wave: W0.3 = bind-first-host parity with listen; W0.4 = un-host robustness + int re-point. Clean gates.
- W0.3 is TINY and PURELY ADDITIVE: spt-hosted bind never had a Psyche (spawn_psyche was cmd_listen-only — that's the bind-no-Psyche gap). So W0.3 = ADD &quot;cmd_bind marks status=online for a live-capable endpoint (psyche_init resolv</EVENT-PART>
<EVENT-PART seq="3/6" id="e9c18508">es) → W0.1 reconcile hosts.&quot; No strip, no relay swap (spt-hosted uses direct PTY injection, no relay — CONTEXT:39). Just the establish-marks-online mirror of W0.2, on the bind path. Smaller than W0.2.
- The coupling you flagged is real but benign: W0.3 exercises the un-host path while the lost-update bug is still present → use th</EVENT-PART>
<EVENT-PART seq="4/6" id="e9c18508">e SAME deterministic long-pulse+settle test workaround you used in W0.2; W0.4 fixes the race + removes the workaround. Note the workaround as &quot;pending W0.4 (iii)&quot; in the W0.3 test so it's not mistaken for arbitrary.

SCOPE DISCIPLINE for W0.3: do NOT touch un-host robustness (all of it is W0.4). W0.3 is the bind→online→reco</EVENT-PART>
<EVENT-PART seq="5/6" id="e9c18508">ncile-hosts addition + its unit (real cmd_bind writes online, not hand-seeded — the spt-hosted mirror of live_firsthost_e2e) + the cross-check that an spt-hosted live agent NOW gets a Psyche (the gap closes). That's it.

So: ping b115929 green → W0.2 full PASS → W0.3 (bring me the shape; it's small, but I gate the bind-marks-online</EVENT-PART>
<EVENT-PART seq="6/6" id="e9c18508"> site + that it reuses the W0.1 reconcile cleanly, no second host path) → W0.4 (the three blockers, I gate hardest on (iii) lost-update + (i) the real-brain int). Go.</EVENT-PART></event>
</task-notification>


doyle SEQUENCING: **W0.3 then W0.4** (my lean — atomic waves). W0.3 = tiny + purely additive: `cmd_bind` marks online for a live-capable endpoint → W0.1 reconcile hosts (no strip, no relay — spt-hosted uses direct PTY, CONTEXT:39). Use the same long-pulse+settle workaround noted "pending W0.4 (iii)". Scope discipline: don't touch un-host robustness. Check CI + read `cmd_bind` for the W0.3 shape.


Bash

Check b115929 CI status


318	    // stdout until the process is killed. No lifecycle state lives in this process
319	    // anymore — the Psyche/pulse runs in the brain. The stop flag is never raised;
320	    // the harness kills the process to end the session.
321	    let stop = std::sync::atomic::AtomicBool::new(false);
322	    relay.run(&stop, &mut deliver);
323	    0
324	}
325
326	/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the
327	/// live perch on first contact, or flips an existing skeleton, keyed to the
328	/// session id the binary reports. Auth is intrinsic: an existing *live* perch
329	/// under a different session id is refused (you can only bind your own).
330	// [impl->REQ-SEAM-POSTSPAWN]
331	pub fn cmd_bind(
332	    id: &str,
333	    set_session_id: Option<String>,
334	    proof_session: Option<String>,
335	    adapter: Option<&str>,
336	    subnet: Option<&str>,
337	    endpoint_type: &str,
338	) -> i32 {
339	    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
340	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
341	        eprintln!("INVALID_ID:{id}: {e}");
342	        return EXIT_REFUSED;
343	    }
344	    let session_id = set_session_id
345	        .or(proof_session)
346	        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
347	        .filter(|s| !s.is_empty());
348	    let Some(session_id) = session_id else {
349	        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
350	        return EXIT_REFUSED;
351	    };
352	    // The session's own anchor: the process that fired this hook (the session).
353	    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
354	    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
355	        Ok(token) => {
356	            eprintln!("BOUND:{id} token={token}");
357	            // A post-spawn bind is a new-session-start — a reported resurface
358	            // boundary (ADR-0007/D8b). Best-effort.
359	            super::reporting::resurface_notifs(id);
360	            0
361	        }
362	        Err(e) => {
363	            report_bind_error(&e);
364	            EXIT_REFUSED
365	        }
366	    }
367	}
368
369	fn report_bind_error(e: &BindError) {
370	    match e {
371	        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
372	        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),

    // stdout until the process is killed. No lifecycle state lives in this process
    // anymore — the Psyche/pulse runs in the brain. The stop flag is never raised;
    // the harness kills the process to end the session.
    let stop = std::sync::atomic::AtomicBool::new(false);
    relay.run(&stop, &mut deliver);
    0
}

/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the
/// live perch on first contact, or flips an existing skeleton, keyed to the
/// session id the binary reports. Auth is intrinsic: an existing *live* perch
/// under a different session id is refused (you can only bind your own).
// [impl->REQ-SEAM-POSTSPAWN]
pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }
        Err(e) => {
            report_bind_error(&e);
            EXIT_REFUSED
        }
    }
}

fn report_bind_error(e: &BindError) {
    match e {
        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),


`cmd_bind` (startup.rs:331) establishes the perch but takes **no manifest** (unlike cmd_listen) — so the W0.3 live-capability discriminator for mark-online is the key shape question. Check the Bind dispatch for manifest availability.


336	            bind_session,
337	            subnet,
338	            endpoint_type,
339	            auth,
340	        } => {
341	            // The bind path's adapter source is the api group's own
342	            // --adapter (no seed on this path) — the adapter-at-creation rule.
343	            let adapter = Some(ctx.adapter.as_str()).filter(|a| !a.is_empty());
344	            startup::cmd_bind(
345	                &id,
346	                bind_session,
347	                auth.session_id,
348	                adapter,
349	                subnet.as_deref(),
350	                &endpoint_type,
351	            )
352	        }
353	        // Delivery + activity (T5), gated by auth.
354	        ApiCmd::State {
355	            state,
356	            id,
357	            no_gate,
358	            auth,
359	        } => gated(&id, &auth, |id| delivery::cmd_state(id, &state, no_gate)),
360	        ApiCmd::EchoGate { action, id, auth } => {
361	            gated(&id, &auth, |id| delivery::cmd_echo_gate(id, &action))
362	        }
363	        ApiCmd::Poll {
364	            id,
365	            include_deferred,

            bind_session,
            subnet,
            endpoint_type,
            auth,
        } => {
            // The bind path's adapter source is the api group's own
            // --adapter (no seed on this path) — the adapter-at-creation rule.
            let adapter = Some(ctx.adapter.as_str()).filter(|a| !a.is_empty());
            startup::cmd_bind(
                &id,
                bind_session,
                auth.session_id,
                adapter,
                subnet.as_deref(),
                &endpoint_type,
            )
        }
        // Delivery + activity (T5), gated by auth.
        ApiCmd::State {
            state,
            id,
            no_gate,
            auth,
        } => gated(&id, &auth, |id| delivery::cmd_state(id, &state, no_gate)),
        ApiCmd::EchoGate { action, id, auth } => {
            gated(&id, &auth, |id| delivery::cmd_echo_gate(id, &action))
        }
        ApiCmd::Poll {
            id,
            include_deferred,


Bind dispatch has `ctx.manifest` available — so W0.3 = pass manifest to `cmd_bind`, mark online on `psyche_init` (exact mirror of W0.2's cmd_listen). Ack doyle's W0.3-then-W0.4 ruling + bring the W0.3 shape for his gate (CI poll continues separately).


$OWL send doyle <<'EOF'
[todlando → doyle] W0.3-then-W0.4 ruling ACCEPTED (atomic waves). W0.4 = 3 tracked blockers, all in commune+commit; I'll write them into the DAEMON plan's W0.4 section (bring it onto the branch) when W0.4 starts.

W0.3 SHAPE for your gate (it IS tiny + purely additive, as you said):

SITE: cmd_bind (startup.rs:331). Currently takes adapter but NO manifest. The Bind dispatch (mod.rs:340) HAS ctx.manifest available — so: add a `manifest: Option<&Manifest>` param, pass ctx.manifest.as_ref(), and after establish_perch succeeds, for a live-capable endpoint (manifest.session.psyche_init.is_some()) → set_status(perch, ONLINE). This is the EXACT mirror of W0.2's cmd_listen mark — same psyche_init discriminator, same online signal.

REUSE (no second host path): the bind just writes the online signal; the SAME W0.1 livehost::reconcile_once (already sweeping boot+tick in the brain) hosts the Psyche within a tick. cmd_bind does NOT host, spawn, or call livehost — zero new host path. spt-hosted uses direct PTY (CONTEXT:39), no relay swap, no strip — bind only GAINS the mark-online line.

TEST (spt-hosted mirror of live_firsthost_e2e): a REAL `spt api bind <id> --type live_agent --adapter X --manifest M --set-session-id sid` WRITES status=online (asserted, not hand-seeded) → reconcile hosts (set.len==1, psyche nested perch spawns) → THE GAP-CLOSES CROSS-CHECK: an spt-hosted (bind, no listen) live agent NOW gets a Psyche (it never did — spawn_psyche was cmd_listen-only). Teardown exercises un-host (offline+reconcile) via the long-pulse+settle workaround, noted "pending W0.4 (iii)" so it's not mistaken for arbitrary.

SCOPE DISCIPLINE held: W0.3 touches NO un-host robustness (all W0.4). Just the bind mark-online + its int + the gap-closes assertion.

Gate the site + reconcile-reuse and I write it (after the b115929 green ping — CI still finishing: test×2+traceability pending, n1-gate×2 green). — todlando
EOF
echo sent=$?

Ack ruling + send W0.3 shape to doyle


➜ SENT:doyle
sent=0


W0.3 shape sent for doyle's gate. CI (b115929) still finishing — reschedule poll for green → ping run id (W0.2 full PASS). doyle's W0.3 gate arrives via Monitor.


ScheduleWakeup

Polling PR #16 run 27555156303 (W0.2 b115929) test×2+traceability for both-runner green to ping doyle → W0.2 full PASS; his W0.3 shape gate also arrives via Monitor.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 08:07:00 (in 237s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


doyle ruled **W0.3 then W0.4** (atomic waves). W0.3 shape sent for his gate — tiny + additive: `cmd_bind` gains a `manifest` param + the live-capable mark-online line (exact mirror of W0.2's cmd_listen), reusing the W0.1 reconcile (no second host path); spt-hosted mirror int proving the bind-no-Psyche gap closes; scope-disciplined (no un-host robustness — that's W0.4's 3 blockers).

Awaiting: b115929 CI green (test×2 + traceability pending; n1-gate×2 green) to ping the run id for **W0.2 full PASS**, and doyle's W0.3 shape gate. Both via the scheduled poll / Monitor. Standing by.


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="cf70f508" type="msg" from="doyle">[doyle → todlando] W0.3 DESIGN GATE — PASS. Tiny, additive, exactly scoped. Write it after the b115929 green ping.

APPROVED:
- SITE cmd_bind (startup.rs:331) + manifest param (ctx.manifest at mod.rs:340) + mark status=online for live-capable after establish_perch. Exact mirror of W0.2's cmd_listen mark, sa</EVENT-PART>
<EVENT-PART seq="2/7" id="cf70f508">me discriminator/signal. ✓
- REUSE = the load-bearing thing I gate: cmd_bind ONLY writes the online signal; the SAME W0.1 livehost::reconcile_once hosts. cmd_bind does NOT host/spawn/call livehost. ZERO new host path. ✓ That's the single-source-of-truth invariant — confirmed.
- spt-hosted = direct PTY (CONTEXT:39), no relay/strip, </EVENT-PART>
<EVENT-PART seq="3/7" id="cf70f508">bind only GAINS the line. ✓
- TEST: real `api bind --type live_agent --adapter X --manifest M` WRITES online (asserted, not hand-seeded) → reconcile hosts (set.len==1, psyche nested perch) → GAP-CLOSES cross-check (spt-hosted live agent NOW gets a Psyche — it never did). That's the W0.3 payload + the right real-not-synthetic proof. </EVENT-PART>
<EVENT-PART seq="4/7" id="cf70f508">✓
- Scope discipline (no un-host robustness; teardown via the long-pulse+settle workaround noted &quot;pending W0.4 (iii)&quot;). ✓

ONE MINOR CONSISTENCY NOTE (consider, not a blocker): your mark-online gate is psyche_init.is_some(); the W0.1 reconcile host predicate is state==&quot;live_agent&quot; ∧ status==online ∧ psyche_</EVENT-PART>
<EVENT-PART seq="5/7" id="cf70f508">init. So a (contrived) non-live_agent endpoint with a psyche_init manifest would get marked online by bind but reconcile-SKIPPED (state≠live_agent) → an online-but-never-hosted perch. Unlikely manifest (a ready adapter declaring psyche_init), and harmless (reconcile leaves it), but for exact alignment consider gating the mark on state==liv</EVENT-PART>
<EVENT-PART seq="6/7" id="cf70f508">e_agent ∧ psyche_init too — so the establish-online signal == the host predicate, no online-but-unhostable perch. Cheap to align; your call. (W0.2's cmd_listen has the same shape — if you align here, mirror it there or note the parity.)

So: W0.3 design PASS — write it on the b115929 green. Submit at evidence-land; I gate the mar</EVENT-PART>
<EVENT-PART seq="7/7" id="cf70f508">k-online site + reconcile-reuse + the gap-closes assertion. Then W0.4 (the three blockers — I gate hardest on (iii) lost-update + (i) the real-brain int re-point that finally kills the type-level false-green). Go.</EVENT-PART></event>
</task-notification>


W0.3 DESIGN GATE — **PASS**. doyle's consistency note (good): gate the mark-online on `state=="live_agent" ∧ psyche_init` (not psyche_init alone) so the establish-online signal == the reconcile host predicate — no online-but-unhostable perch. cmd_bind takes arbitrary `--type`, so this matters there; cmd_listen's state is invariantly live_agent (note parity). Adopting it. Check CI — write W0.3 after the green ping.


Bash

gh pr checks 16 2>&1 | head -7; echo "---rollup---"; gh pr view 16 --json statusCheckRollup --jq '[.statusCheckRollup[]|select(.conclusion!=null)|.conclusion]|unique' 2>&1

Check b115929 CI + rollup


traceability	pass	11s	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028498
test (self-hosted, Windows, hfenduleam)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028613
n1-gate (self-hosted, Linux, kitsubito)	pass	1m13s	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028533
n1-gate (self-hosted, Windows, hfenduleam)	pass	1m52s	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028424
test (self-hosted, Linux, kitsubito)	pass	4m52s	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028487
---rollup---
["","SUCCESS"]


Almost green (only test-windows pending; rollup SUCCESS otherwise). Write W0.3 code now (parallel with CI), commit after the green ping. Adopt doyle's alignment: gate mark-online on `state=="live_agent" ∧ psyche_init`. Edit `cmd_bind` + dispatch.


Edit

pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }

pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
    manifest: Option<&spt_runtime::Manifest>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)
            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen
            // mark. A live_agent endpoint whose manifest declares
            // `[session.psyche_init]` is marked status=online at establish; the
            // brain's livehost reconcile then hosts the Psyche (the SAME single host
            // path — bind never spawns/hosts/calls livehost). Closes the
            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so
            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct
            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so
            // the establish-online signal == the reconcile host predicate (no
            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).
            // [impl->REQ-DAEMON-1]
            let live_capable = endpoint_type == "live_agent"
                && manifest.is_some_and(|m| m.session.psyche_init.is_some());
            if live_capable {
                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {
                    eprintln!("LIVE_ONLINE_FAIL:{id}: {e}");
                }
            }
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }


The file crates\spt\src\api\startup.rs has been updated successfully. (file state is current in your context — no need to Read it back)

pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }

pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
    manifest: Option<&spt_runtime::Manifest>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)
            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen
            // mark. A live_agent endpoint whose manifest declares
            // `[session.psyche_init]` is marked status=online at establish; the
            // brain's livehost reconcile then hosts the Psyche (the SAME single host
            // path — bind never spawns/hosts/calls livehost). Closes the
            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so
            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct
            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so
            // the establish-online signal == the reconcile host predicate (no
            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).
            // [impl->REQ-DAEMON-1]
            let live_capable = endpoint_type == "live_agent"
                && manifest.is_some_and(|m| m.session.psyche_init.is_some());
            if live_capable {
                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {
                    eprintln!("LIVE_ONLINE_FAIL:{id}: {e}");
                }
            }
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }

+    manifest: Option<&spt_runtime::Manifest>,

+            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)

+            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen

+            // mark. A live_agent endpoint whose manifest declares

+            // `[session.psyche_init]` is marked status=online at establish; the

+            // brain's livehost reconcile then hosts the Psyche (the SAME single host

+            // path — bind never spawns/hosts/calls livehost). Closes the

+            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so

+            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct

+            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so

+            // the establish-online signal == the reconcile host predicate (no

+            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).

+            let live_capable = endpoint_type == "live_agent"

+                && manifest.is_some_and(|m| m.session.psyche_init.is_some());

+                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

+                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {

+                    eprintln!("LIVE_ONLINE_FAIL:{id}: {e}");


            startup::cmd_bind(
                &id,
                bind_session,
                auth.session_id,
                adapter,
                subnet.as_deref(),
                &endpoint_type,
            )

            startup::cmd_bind(
                &id,
                bind_session,
                auth.session_id,
                adapter,
                subnet.as_deref(),
                &endpoint_type,
                ctx.manifest.as_ref(),
            )


The file crates\spt\src\api\mod.rs has been updated successfully. (file state is current in your context — no need to Read it back)

            startup::cmd_bind(
                &id,
                bind_session,
                auth.session_id,
                adapter,
                subnet.as_deref(),
                &endpoint_type,
            )

            startup::cmd_bind(
                &id,
                bind_session,
                auth.session_id,
                adapter,
                subnet.as_deref(),
                &endpoint_type,
                ctx.manifest.as_ref(),
            )

// Delivery + activity (T5), gated by auth.


Check for other `cmd_bind` callers needing the new param (tests), then write the W0.3 bind-firsthost int.


crates\spt\src\api\mod.rs:344:            startup::cmd_bind(
crates\spt\src\api\startup.rs:331:pub fn cmd_bind(
crates\spt\src\api\startup.rs:607:        let code = cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent");
crates\spt\src\api\startup.rs:618:            cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent"),
crates\spt\src\api\startup.rs:622:            cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent"),
crates\spt\src\api\startup.rs:635:            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "gateway"),
crates\spt\src\api\startup.rs:650:            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent"),


5 test callers of `cmd_bind` need the new `manifest` param (None — they don't test live hosting). Read + update them.


600	            Some("work")
601	        );
602	    }
603
604	    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
605	    fn post_spawn_bind_establishes_perch() {
606	        let _h = isolated_home();
607	        let code = cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent");
608	        assert_eq!(code, 0);
609	        let rec = info::read_info(&perch::resolve_perch_path("bob", ParentHint::Infer)).unwrap();
610	        assert_eq!(rec.session_id, "sid-boot");
611	        assert_eq!(rec.state, "live_agent");
612	    }
613
614	    #[test] // [unit->REQ-SEAM-POSTSPAWN] re-bind with the same session is idempotent.
615	    fn rebind_same_session_ok() {
616	        let _h = isolated_home();
617	        assert_eq!(
618	            cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent"),
619	            0
620	        );
621	        assert_eq!(
622	            cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent"),
623	            0
624	        );
625	    }
626
627	    // [unit->REQ-EP-6] a non-agent endpoint binds with its own open-type tag
628	    // (`gateway`): the `state` is written verbatim, NOT forced to `live_agent`
629	    // (the un-hardcode). A revive that omits `--type` (defaults to live_agent)
630	    // KEEPS the prior gateway type — carry-forward, like the home/adapter fields.
631	    #[test]
632	    fn bind_with_type_establishes_a_gateway_endpoint() {
633	        let _h = isolated_home();
634	        assert_eq!(
635	            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "gateway"),
636	            0
637	        );
638	        let perch = perch::resolve_perch_path("playdate-gw", ParentHint::Infer);
639	        assert_eq!(info::read_info(&perch).unwrap().state, "gateway");
640
641	        // The open-type tag round-trips to the user-backed Gateway recognizer
642	        // the user-msg identity gate keys on (REQ-MSG-5).
643	        let ty = spt_proto::endpoint::EndpointType::from_tag(
644	            &info::read_info(&perch).unwrap().state,
645	        );
646	        assert!(spt_proto::event::is_gateway_endpoint(&ty));
647
648	        // Revive without --type (default live_agent) preserves the gateway type.
649	        assert_eq!(
650	            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent"),
651	            0
652	        );
653	        assert_eq!(info::read_info(&perch).unwrap().state, "gateway");
654	    }
655	}
656

            Some("work")
        );
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
    fn post_spawn_bind_establishes_perch() {
        let _h = isolated_home();
        let code = cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent");
        assert_eq!(code, 0);
        let rec = info::read_info(&perch::resolve_perch_path("bob", ParentHint::Infer)).unwrap();
        assert_eq!(rec.session_id, "sid-boot");
        assert_eq!(rec.state, "live_agent");
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] re-bind with the same session is idempotent.
    fn rebind_same_session_ok() {
        let _h = isolated_home();
        assert_eq!(
            cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent"),
            0
        );
        assert_eq!(
            cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent"),
            0
        );
    }

    // [unit->REQ-EP-6] a non-agent endpoint binds with its own open-type tag
    // (`gateway`): the `state` is written verbatim, NOT forced to `live_agent`
    // (the un-hardcode). A revive that omits `--type` (defaults to live_agent)
    // KEEPS the prior gateway type — carry-forward, like the home/adapter fields.
    #[test]
    fn bind_with_type_establishes_a_gateway_endpoint() {
        let _h = isolated_home();
        assert_eq!(
            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "gateway"),
            0
        );
        let perch = perch::resolve_perch_path("playdate-gw", ParentHint::Infer);
        assert_eq!(info::read_info(&perch).unwrap().state, "gateway");

        // The open-type tag round-trips to the user-backed Gateway recognizer
        // the user-msg identity gate keys on (REQ-MSG-5).
        let ty = spt_proto::endpoint::EndpointType::from_tag(
            &info::read_info(&perch).unwrap().state,
        );
        assert!(spt_proto::event::is_gateway_endpoint(&ty));

        // Revive without --type (default live_agent) preserves the gateway type.
        assert_eq!(
            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent"),
            0
        );
        assert_eq!(info::read_info(&perch).unwrap().state, "gateway");
    }
}


cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent")

cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent", None)


The file crates\spt\src\api\startup.rs has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

#[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.

-        let code = cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent");

+        let code = cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent", None);

let rec = info::read_info(&perch::resolve_perch_path("bob", ParentHint::Infer)).unwrap();

-            cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent"),

+            cmd_bind("bob", Some("sid-boot".into()), None, None, None, "live_agent", None),


cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "gateway"),

cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "gateway", None),


-            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "gateway"),

+            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "gateway", None),

let perch = perch::resolve_perch_path("playdate-gw", ParentHint::Infer);


            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent"),
            0
        );
        assert_eq!(info::read_info(&perch).unwrap().state, "gateway");
    }
}

            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent", None),
            0
        );
        assert_eq!(info::read_info(&perch).unwrap().state, "gateway");
    }
}


            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent"),
            0
        );
        assert_eq!(info::read_info(&perch).unwrap().state, "gateway");
    }
}

            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent", None),
            0
        );
        assert_eq!(info::read_info(&perch).unwrap().state, "gateway");
    }
}

// Revive without --type (default live_agent) preserves the gateway type.

-            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent"),

+            cmd_bind("playdate-gw", Some("sid-gw".into()), None, None, None, "live_agent", None),


Write the W0.3 bind-firsthost int (spt-hosted mirror of live_firsthost_e2e — via `api bind`, proving the bind-no-Psyche gap closes).


Write

//! M11-W0.3 first-host int (REQ-DAEMON-1) — the spt-hosted (bind, no listen)
//! mirror of `live_firsthost_e2e`, closing the bind-no-Psyche gap:
//!
//! a live-capable `spt api bind <id> --type live_agent` BINDS the Self perch
//! (direct-PTY topology, no relay — CONTEXT:39) and marks it `status=online` (the
//! W0.3 establish-marks-online, mirror of W0.2's listen mark) → the brain's
//! `livehost::reconcile_once` then HOSTS the Psyche off that real online status.
//! Before W0.3 a bind-only spt-hosted live agent never got a Psyche (the Psyche
//! spawn was cmd_listen-only); this proves it now does — through the REAL bind,
//! not a hand-seeded status.
//
// [int->REQ-DAEMON-1]

use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::Arc;
use std::time::Duration;

mod common;
use common::CommandNoWindowExt;

use spt_daemon::brainproc::StartReason;
use spt_daemon::config::DaemonConfig;
use spt_daemon::livehost::{self, LiveSet};
use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};
use spt_store::perch::{self, ParentHint};

#[cfg(windows)]
const NOOP: &str = "cmd /C rem";
#[cfg(unix)]
const NOOP: &str = "true";

fn start_inproc_daemon() {
    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
    let name = spt_daemon::seed_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);
    });
    for _ in 0..400 {
        if spt_daemon::is_running() {
            return;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    panic!("in-process seed daemon did not come up");
}

fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete")
        .expect("run spt")
}

#[test]
fn live_bind_marks_online_and_brain_reconcile_hosts() {
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));

    // A live-capable adapter (declares [session.psyche_init]) + register it so the
    // brain reconcile resolves the manifest from disk by the perch's adapter.
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"mocklive\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    let manifest_path = home.path().join("mocklive.toml");
    std::fs::write(&manifest_path, &manifest_toml).unwrap();
    let src = perch::spt_home().join("srcs").join("mocklive");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let mp = manifest_path.to_string_lossy().to_string();
    let spt = |args: &[&str]| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(60))
    };

    // ── the REAL spt-hosted bind (no seed, no listen): establishes the Self perch
    // and marks it online (the W0.3 first-host handoff). The Psyche does NOT spawn
    // in this process — bind only writes the online signal.
    let out = spt(&[
        "api", "--adapter", "mocklive", "--manifest", &mp, "bind", "agent8",
        "--type", "live_agent", "--set-session-id", "sid-1",
    ]);
    assert!(out.status.success(), "bind: {}", String::from_utf8_lossy(&out.stderr));

    // The real establish-marks-online: written by cmd_bind, NOT hand-seeded.
    let self_perch = perch::resolve_perch_path("agent8", ParentHint::Infer);
    assert_eq!(
        spt_store::info::read_info(&self_perch)
            .and_then(|i| i.status)
            .as_deref(),
        Some(STATUS_ONLINE),
        "a live-capable `api bind` marks the perch online (the spt-hosted first-host handoff)"
    );

    // ── THE GAP CLOSES: the brain reconcile hosts the Psyche for the bind-only
    // spt-hosted live agent — it never did before W0.3 (spawn_psyche was
    // cmd_listen-only). A LONG pulse so the driver parks (deterministic un-host
    // below, pending W0.4 (iii) lost-update fix).
    let set = Arc::new(LiveSet::new());
    let cfg = DaemonConfig {
        pulse_period: Duration::from_secs(10),
        ..DaemonConfig::default()
    };
    let registered = spt_runtime::registry::registered(&perch::adapters_dir());
    livehost::reconcile_once(
        &perch::owlery_dir(),
        &registered,
        &perch::adapters_dir(),
        &set,
        &cfg,
        StartReason::Cold,
    );
    assert_eq!(
        set.len(),
        1,
        "the brain hosts the Psyche for the bind-established live agent (bind-no-Psyche gap closed)"
    );
    // The Psyche nested perch was created by the brain host (daemon online-stamp).
    let psyche_perch = perch::resolve_perch_path("agent8-psyche", ParentHint::Explicit("agent8"));
    assert!(
        spt_store::info::read_info(&psyche_perch).is_some(),
        "the brain spawned the Psyche nested perch"
    );

    // ── teardown via the public un-host path (offline transition). The long pulse
    // + this settle keep it deterministic until W0.4 (iii) fixes the
    // set_last_active/mark_offline lost-update.
    std::thread::sleep(Duration::from_millis(300));
    spt_store::info::set_status(&self_perch, STATUS_OFFLINE).unwrap();
    livehost::reconcile_once(
        &perch::owlery_dir(),
        &registered,
        &perch::adapters_dir(),
        &set,
        &cfg,
        StartReason::Cold,
    );
    assert!(set.is_empty(), "an offline transition un-hosts the lifecycle");

    // Reap any REAL daemon that auto-started against this throwaway home (hygiene).
    if let Ok(p) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(p) = p.trim().parse::<u32>() {
            #[cfg(windows)]
            let _ = Command::new("taskkill")
                .no_window()
                .args(["/PID", &p.to_string(), "/F"])
                .output();
            #[cfg(unix)]
            let _ = Command::new("kill").args(["-9", &p.to_string()]).output();
        }
    }
}


File created successfully at: crates\spt\tests\live_bind_firsthost_e2e.rs (file state is current in your context — no need to Read it back)

//! M11-W0.3 first-host int (REQ-DAEMON-1) — the spt-hosted (bind, no listen)
//! mirror of `live_firsthost_e2e`, closing the bind-no-Psyche gap:
//!
//! a live-capable `spt api bind <id> --type live_agent` BINDS the Self perch
//! (direct-PTY topology, no relay — CONTEXT:39) and marks it `status=online` (the
//! W0.3 establish-marks-online, mirror of W0.2's listen mark) → the brain's
//! `livehost::reconcile_once` then HOSTS the Psyche off that real online status.
//! Before W0.3 a bind-only spt-hosted live agent never got a Psyche (the Psyche
//! spawn was cmd_listen-only); this proves it now does — through the REAL bind,
//! not a hand-seeded status.
//
// [int->REQ-DAEMON-1]

use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::Arc;
use std::time::Duration;

mod common;
use common::CommandNoWindowExt;

use spt_daemon::brainproc::StartReason;
use spt_daemon::config::DaemonConfig;
use spt_daemon::livehost::{self, LiveSet};
use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};
use spt_store::perch::{self, ParentHint};

#[cfg(windows)]
const NOOP: &str = "cmd /C rem";
#[cfg(unix)]
const NOOP: &str = "true";

fn start_inproc_daemon() {
    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
    let name = spt_daemon::seed_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);
    });
    for _ in 0..400 {
        if spt_daemon::is_running() {
            return;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    panic!("in-process seed daemon did not come up");
}

fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete")
        .expect("run spt")
}

#[test]
fn live_bind_marks_online_and_brain_reconcile_hosts() {
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));

    // A live-capable adapter (declares [session.psyche_init]) + register it so the
    // brain reconcile resolves the manifest from disk by the perch's adapter.
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"mocklive\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    let manifest_path = home.path().join("mocklive.toml");
    std::fs::write(&manifest_path, &manifest_toml).unwrap();
    let src = perch::spt_home().join("srcs").join("mocklive");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let mp = manifest_path.to_string_lossy().to_string();
    let spt = |args: &[&str]| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(60))
    };

    // ── the REAL spt-hosted bind (no seed, no listen): establishes the Self perch
    // and marks it online (the W0.3 first-host handoff). The Psyche does NOT spawn
    // in this process — bind only writes the online signal.
    let out = spt(&[
        "api", "--adapter", "mocklive", "--manifest", &mp, "bind", "agent8",
        "--type", "live_agent", "--set-session-id", "sid-1",
    ]);
    assert!(out.status.success(), "bind: {}", String::from_utf8_lossy(&out.stderr));

    // The real establish-marks-online: written by cmd_bind, NOT hand-seeded.
    let self_perch = perch::resolve_perch_path("agent8", ParentHint::Infer);
    assert_eq!(
        spt_store::info::read_info(&self_perch)
            .and_then(|i| i.status)
            .as_deref(),
        Some(STATUS_ONLINE),
        "a live-capable `api bind` marks the perch online (the spt-hosted first-host handoff)"
    );

    // ── THE GAP CLOSES: the brain reconcile hosts the Psyche for the bind-only
    // spt-hosted live agent — it never did before W0.3 (spawn_psyche was
    // cmd_listen-only). A LONG pulse so the driver parks (deterministic un-host
    // below, pending W0.4 (iii) lost-update fix).
    let set = Arc::new(LiveSet::new());
    let cfg = DaemonConfig {
        pulse_period: Duration::from_secs(10),
        ..DaemonConfig::default()
    };
    let registered = spt_runtime::registry::registered(&perch::adapters_dir());
    livehost::reconcile_once(
        &perch::owlery_dir(),
        &registered,
        &perch::adapters_dir(),
        &set,
        &cfg,
        StartReason::Cold,
    );
    assert_eq!(
        set.len(),
        1,
        "the brain hosts the Psyche for the bind-established live agent (bind-no-Psyche gap closed)"
    );
    // The Psyche nested perch was created by the brain host (daemon online-stamp).
    let psyche_perch = perch::resolve_perch_path("agent8-psyche", ParentHint::Explicit("agent8"));
    assert!(
        spt_store::info::read_info(&psyche_perch).is_some(),
        "the brain spawned the Psyche nested perch"
    );

    // ── teardown via the public un-host path (offline transition). The long pulse
    // + this settle keep it deterministic until W0.4 (iii) fixes the
    // set_last_active/mark_offline lost-update.
    std::thread::sleep(Duration::from_millis(300));
    spt_store::info::set_status(&self_perch, STATUS_OFFLINE).unwrap();
    livehost::reconcile_once(
        &perch::owlery_dir(),
        &registered,
        &perch::adapters_dir(),
        &set,
        &cfg,
        StartReason::Cold,
    );
    assert!(set.is_empty(), "an offline transition un-hosts the lifecycle");

    // Reap any REAL daemon that auto-started against this throwaway home (hygiene).
    if let Ok(p) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(p) = p.trim().parse::<u32>() {
            #[cfg(windows)]
            let _ = Command::new("taskkill")
                .no_window()
                .args(["/PID", &p.to_string(), "/F"])
                .output();
            #[cfg(unix)]
            let _ = Command::new("kill").args(["-9", &p.to_string()]).output();
        }
    }
}


Bash

cargo test -p spt --test live_bind_firsthost_e2e 2>&1 | tail -15

Build + run W0.3 bind-firsthost int


   Compiling spt v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
error: failed to remove file `C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe`

Caused by:
  Access is denied. (os error 5)


Exe-lock (os error 5) — a leftover dev daemon from prior test runs holds `target\debug\spt.exe`. Per the shared-runner rule, reap **scoped to this workspace path only** (never machine-wide).


PowerShell

$ws = "C:\Users\decid\Documents\projects\spt-core"; Get-Process spt -ErrorAction SilentlyContinue | Where-Object { $_.Path -like "$ws\*" } | ForEach-Object { Write-Output "reaping PID $($_.Id) $($_.Path)"; Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue }; Write-Output "done"

Reap workspace-scoped leftover spt daemons


reaping PID 39364 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
reaping PID 83420 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
reaping PID 131664 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
reaping PID 163368 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
done


Reaped 4 workspace-scoped daemons. Retry the build.


Bash

cargo test -p spt --test live_bind_firsthost_e2e 2>&1 | tail -15

Re-run W0.3 bind-firsthost int


   Compiling spt v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 4.37s
     Running tests\live_bind_firsthost_e2e.rs (target\debug\deps\live_bind_firsthost_e2e-cf33f25fd57cf7e8.exe)

running 1 test
test live_bind_marks_online_and_brain_reconcile_hosts ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.14s


W0.3 int green (gap-closes proven). Run clippy `--workspace` + the cmd_bind unit tests + check b115929 CI.


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -5

Workspace clippy after W0.3


gh pr checks 16 2>&1 | grep -iE "test \(self-hosted, Windows" ; echo "---rollup---"; gh pr view 16 --json statusCheckRollup --jq '[.statusCheckRollup[]|select(.conclusion!=null)|.conclusion]|unique' 2>&1

Check b115929 windows test + rollup


    Checking spt v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.33s


test (self-hosted, Windows, hfenduleam)	pass	6m27s	https://github.com/SaberMage/spt-core/actions/runs/27555156303/job/81452028613
---rollup---
["SKIPPED","SUCCESS"]


b115929 **fully green** (rollup SUCCESS/SKIPPED) → W0.2 CI condition met. clippy clean for W0.3. Ping doyle for W0.2 full PASS + run the cmd_bind unit tests + traceable.


$OWL send doyle <<'EOF'
[todlando → doyle] W0.2 CI GREEN — requesting W0.2 full PASS.

Commit b115929, run 27555156303:
  test (Linux/kitsubito)    PASS 4m52s
  test (Windows/hfenduleam) PASS 6m27s
  n1-gate Linux PASS · n1-gate Windows PASS · traceability PASS
  twohost SKIPPED. Rollup = {SUCCESS, SKIPPED}, zero failures.
The new live_firsthost_e2e + reworked contract_e2e::live_agent_lifecycle_e2e rode the both-runner nextest --workspace. Confirm independently → W0.2 full PASS (the lost-update stays the tracked W0.4 (iii) hard blocker).

W0.3 already WRITTEN + green locally (your design PASS, on your "write after green" — coding it in parallel with CI was idle time): cmd_bind gains the manifest param + the live_capable mark-online (gated state==live_agent ∧ psyche_init, your alignment note adopted — establish-signal == host predicate; cmd_listen's state is invariantly live_agent so its psyche_init-only gate is equivalent, parity noted). NEW live_bind_firsthost_e2e: real `api bind --type live_agent` writes online (asserted, not hand-seeded) → reconcile hosts (set.len==1) + the psyche nested perch appears → THE GAP CLOSES (a bind-only spt-hosted live agent NOW gets a Psyche). clippy --workspace clean. Committing W0.3 next (separate commit). — todlando
EOF
echo sent=$?

Ping doyle W0.2 CI green + W0.3 ready


➜ SENT:doyle
sent=0


cargo test -p spt post_spawn_bind rebind_same bind_with_type 2>&1 | tail -8; echo "===trace==="; traceable-reqs check >/dev/null 2>&1; echo "EXIT=$?"

Run cmd_bind unit tests + traceable


error: unexpected argument 'rebind_same' found

Usage: cargo.exe test [OPTIONS] [TESTNAME] [-- [ARGS]...]

For more information, try '--help'.
===trace===
EXIT=0


traceable EXIT=0. Run the cmd_bind unit tests properly (single filter).


cargo test -p spt --bin spt bind 2>&1 | tail -12

Run startup bind unit tests


running 7 tests
test rc::tests::detach_keybind_semantics ... ok
test api::reporting::tests::boundary_rebinds_keeping_anchor ... ok
test api::startup::tests::post_spawn_bind_establishes_perch ... ok
test api::startup::tests::bind_with_type_establishes_a_gateway_endpoint ... ok
test api::startup::tests::rebind_same_session_ok ... ok
test api::startup::tests::valid_seed_binds_live_perch ... ok
test api::startup::tests::bind_assigns_home_and_revive_preserves_it ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 150 filtered out; finished in 0.15s


All bind unit tests green. W0.3 solid (int + units + clippy + traceable). Commit W0.3.


git branch --show-current && git add -A && git status --short

Branch check + stage W0.3


m11-shell-substrate
warning: in the working copy of 'crates/spt/tests/live_bind_firsthost_e2e.rs', LF will be replaced by CRLF the next time Git touches it
M  crates/spt/src/api/mod.rs
M  crates/spt/src/api/startup.rs
A  crates/spt/tests/live_bind_firsthost_e2e.rs


git commit -q -F - <<'EOF'
M11-W0.3: spt-hosted bind gets the Psyche — close the bind-no-Psyche gap (REQ-DAEMON-1)

The spt-hosted (bind, no listen) mirror of W0.2's establish-marks-online. An
spt-hosted live agent binds via `api bind` (direct-PTY topology, no relay —
CONTEXT:39) and previously never got a Psyche: spawn_psyche was cmd_listen-only,
so a bind-only agent's lifecycle was never hosted. W0.3 closes that gap — purely
additive, no strip, no relay.

cmd_bind (crates/spt/src/api/startup.rs):
- gains a `manifest: Option<&Manifest>` param (the Bind dispatch already has
  ctx.manifest at mod.rs:340).
- after establish_perch, marks status=online for a live-capable endpoint. Gated
  on `state == "live_agent" && manifest psyche_init` (doyle's alignment note): the
  establish-online signal == the W0.1 reconcile host predicate, so no
  online-but-unhostable perch. (cmd_listen's perch is invariantly live_agent, so
  its psyche_init-only gate is equivalent — parity noted, not re-touched.)
- writes ONLY the online signal: the SAME W0.1 livehost::reconcile_once hosts the
  Psyche. cmd_bind never spawns/hosts/calls livehost — zero new host path
  (single source of truth).

Test — NEW live_bind_firsthost_e2e ([int->REQ-DAEMON-1]): a REAL `spt api bind
<id> --type live_agent --manifest M` writes status=online (asserted, not
hand-seeded) → livehost::reconcile_once hosts it (set.len==1) + the Psyche nested
perch appears → THE GAP CLOSES (a bind-only spt-hosted live agent now gets a
Psyche). Teardown un-hosts via offline+reconcile under the long-pulse+settle
workaround (noted "pending W0.4 (iii)" — the set_last_active/mark_offline
lost-update fix).

Scope discipline (doyle): NO un-host robustness touched — that is all W0.4.

bind unit tests + live_bind_firsthost_e2e green; clippy --workspace clean;
traceable EXIT 0.

Co-authored by: todlando
EOF
git log --oneline -1

Commit W0.3


<owl_messages>
<owl_message from="doyle" priority="high">[doyle → todlando] ✅ W0.2 FULL PASS — the cutover is live. Independently verified: run 27555156303 success, head b115929, test(both)+n1-gate(both)+traceability green, twohost skipped, zero failures. live_firsthost_e2e + reworked live_agent_lifecycle_e2e rode nextest --workspace.

WHAT THIS MEANS: the interim listen-Psyche is RETIRED, cmd_listen is a pure Relay (dumb pipe), the brain is the SOLE first-host (establish-marks-online → W0.1 reconcile hosts), REQ-TERM-7 digest two-origin intact. The daemon-lifecycle consolidation the operator escalated is now the REAL prod path — Psyche runs in the brain where CONTEXT:30/34/194 puts it, no longer the interim relay-process coupling. Big wave, clean. Lost-update correctly stays the tracked W0.4 (iii) hard blocker (CANNOT ship before the fix; all on m11-shell-substrate).

W0.3 WRITTEN (good use of CI idle time): alignment note ADOPTED — mark-online gated state==live_agent ∧ psyche_init (establish-signal == host predicate), cmd_listen parity noted (state invariantly live_agent there → equivalent). live_bind_firsthost_e2e proves real bind --type live_agent writes online → reconcile hosts → the spt-hosted-no-Psyche GAP CLOSES. clippy clean.

Commit W0.3 (separate commit) + push → ping me the W0.3 commit + its both-runner CI run id. I gate W0.3 at evidence-land (the mark-online site == reconcile predicate, no second host path, the gap-closes assertion) → same conditional-then-green flow. Then W0.4 — the close-out wave: (i) int re-point to the real-brain prod path (kills the type-level false-green that's been REQ-DAEMON-1's int since M3), (ii) read-error un-host, (iii) lost-update. I gate W0.4 HARDEST — it's where the correctness fixes + the honest int land. Go.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


a916727 M11-W0.3: spt-hosted bind gets the Psyche — close the bind-no-Psyche gap (REQ-DAEMON-1)


✅ **W0.2 FULL PASS** — cutover live: interim listen-Psyche retired, cmd_listen is a pure Relay, brain is sole first-host, REQ-TERM-7 intact. W0.3 committed @a916727 — push → ping doyle the run id. Then W0.4 (the 3-blocker close-out, gated hardest).


Push W0.3 to PR branch


To https://github.com/SaberMage/spt-core.git
   b115929..a916727  m11-shell-substrate -> m11-shell-substrate


sleep 6; gh pr checks 16 2>&1 | grep -iE "pending|queued|test \(" | head -4

Confirm W0.3 CI kicked


n1-gate (self-hosted, Linux, kitsubito)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555949485/job/81454872815
n1-gate (self-hosted, Windows, hfenduleam)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555949485/job/81454872730
test (self-hosted, Linux, kitsubito)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555949485/job/81454873011
test (self-hosted, Windows, hfenduleam)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555949485/job/81454872874


W0.3 CI run 27555949485 pending. Ping doyle the commit + run id (he design-PASSED + reviewed the code; conditional-then-green). Update commune + reschedule poll.


$OWL send doyle <<'EOF'
[todlando → doyle] W0.2 FULL PASS received — the consolidation is real prod path now. Thanks.

W0.3 COMMITTED + PUSHED: @a916727, CI run 27555949485 RUNNING on PR #16 (both runners + traceability pending). Evidence-land recap for your gate:
- mark-online SITE = cmd_bind (startup.rs:331), gated state==live_agent ∧ psyche_init == the W0.1 reconcile host predicate (no online-but-unhostable perch). cmd_listen parity noted (invariantly live_agent).
- NO SECOND HOST PATH: cmd_bind writes ONLY the online signal; the same livehost::reconcile_once hosts. cmd_bind never spawns/hosts/calls livehost.
- GAP-CLOSES assertion: live_bind_firsthost_e2e — real `api bind --type live_agent` writes online (not hand-seeded) → reconcile hosts (set.len==1) + psyche nested perch appears → spt-hosted-no-Psyche gap closed.
- bind unit tests + clippy --workspace clean + traceable EXIT 0.
Will ping the green at both-runner+traceability pass → W0.3 full PASS.

Then W0.4 — the close-out wave. I'll bring you the W0.4 DESIGN before deep commit (same protocol): (i) int RE-POINT to a real-brain production-path E2E (the brain PROCESS hosts the Psyche for a real listen/bind — kills the type-level false-green that's been REQ-DAEMON-1's int since M3); (ii) read-error un-host (read-fail ≠ definitive-offline); (iii) the set_last_active/mark_offline lost-update fix (field-level status write / CAS so offline never loses). I'll bring the DAEMON plan onto the branch + write all three into its W0.4 section first. — todlando
EOF
echo sent=$?

Ping doyle W0.3 commit + CI run id


Write

# todlando commune — delta (W0.2 full PASS; W0.3 submitted; W0.4 = close-out)

## DONE (all on m11-shell-substrate, PR #16)
- **W1 (REQ-CONSENT-3) FULL PASS** @23e53fc — per-capability act-gates.
- **W0.1 (REQ-DAEMON-1) FULL PASS** @1448a0a — crate::livehost = run_pulse_loop production caller; online-only reconcile set-diff.
- **W0.2 (REQ-DAEMON-1) FULL PASS** @b115929 (doyle verified, run 27555156303 green) — interim listen-Psyche RETIRED; cmd_listen = pure relay::Relay; brain = SOLE first-host (establish-marks-online → reconcile hosts); REQ-TERM-7 intact. The daemon-lifecycle consolidation is now real prod path (Psyche in the brain per CONTEXT:30/34/194).
- **W0.3 (REQ-DAEMON-1) SUBMITTED** @a916727 — spt-hosted bind gets the Psyche (gap closed). cmd_bind + manifest param + mark-online (gated state==live_agent ∧ psyche_init == reconcile predicate). NEW live_bind_firsthost_e2e (real bind→online→host→gap-closes). CI run 27555949485 running. doyle design-PASSED + reviewed code; closes on both-runner green → ping run id.

## W0.4 = CLOSE-OUT WAVE — 3 HARD BLOCKERS (doyle gates HARDEST; MUST fix before any merge/release carrying W0.2)
(i) **int RE-POINT** → real-brain PRODUCTION-path E2E: the brain PROCESS hosts the Psyche for a real api listen/bind (NOT BrainLifecycle-in-a-test). Kills the type-level false-green that's been REQ-DAEMON-1's int since M3.
(ii) **read-error un-host**: don't un-host on transient read_info FAILURE (distinguish definitive-offline [read OK ∧ status≠online] from read-error [leave driver, retry]).
(iii) **lost-update** race: pulse driver's touch_active (set_last_active RMW of whole info.json) clobbers concurrent mark_offline back to online → un-host never fires → signed-off agent keeps Psyche. Fix: offline write must not lose (field-level status write / status CAS / set_last_active stops rewriting status). Tests currently use long-pulse+settle (deterministic; remove after fix).
PLAN: bring DAEMON-LIFECYCLE-COMPLETION-PLAN.md (on main) onto the branch + write all 3 into its W0.4 section. Bring W0.4 DESIGN to doyle before deep commit (same protocol).

## AFTER W0
- **W0.5**: cutover validation w/ perri's claude-spt (zero adapter change).
- M11 shell waves: W2 drive (REQ-SHELL-3) → W3 tunnel → W4 Gateway capstone → W5 rig+docs (M11-PLAN.md).

## GOTCHAS
- Pre-flight `clippy --workspace --all-targets -D warnings` (not -p). `git branch --show-current` before EVERY commit.
- Windows exe-lock (os error 5 on spt.exe rebuild) = leftover dev daemons; reap WORKSPACE-SCOPED only (`Get-Process spt | Where Path -like "$ws\*" | Stop-Process`), NEVER machine-wide (shared runner w/ live broker).
- doyle flow: designs → I build → conditional-PASS-then-CI-green gate per wave. CI only fires on PR (push branches=main/dev-freeform).


File created successfully at: .claude\todlando-commune.md (file state is current in your context — no need to Read it back)

# todlando commune — delta (W0.2 full PASS; W0.3 submitted; W0.4 = close-out)

## DONE (all on m11-shell-substrate, PR #16)
- **W1 (REQ-CONSENT-3) FULL PASS** @23e53fc — per-capability act-gates.
- **W0.1 (REQ-DAEMON-1) FULL PASS** @1448a0a — crate::livehost = run_pulse_loop production caller; online-only reconcile set-diff.
- **W0.2 (REQ-DAEMON-1) FULL PASS** @b115929 (doyle verified, run 27555156303 green) — interim listen-Psyche RETIRED; cmd_listen = pure relay::Relay; brain = SOLE first-host (establish-marks-online → reconcile hosts); REQ-TERM-7 intact. The daemon-lifecycle consolidation is now real prod path (Psyche in the brain per CONTEXT:30/34/194).
- **W0.3 (REQ-DAEMON-1) SUBMITTED** @a916727 — spt-hosted bind gets the Psyche (gap closed). cmd_bind + manifest param + mark-online (gated state==live_agent ∧ psyche_init == reconcile predicate). NEW live_bind_firsthost_e2e (real bind→online→host→gap-closes). CI run 27555949485 running. doyle design-PASSED + reviewed code; closes on both-runner green → ping run id.

## W0.4 = CLOSE-OUT WAVE — 3 HARD BLOCKERS (doyle gates HARDEST; MUST fix before any merge/release carrying W0.2)
(i) **int RE-POINT** → real-brain PRODUCTION-path E2E: the brain PROCESS hosts the Psyche for a real api listen/bind (NOT BrainLifecycle-in-a-test). Kills the type-level false-green that's been REQ-DAEMON-1's int since M3.
(ii) **read-error un-host**: don't un-host on transient read_info FAILURE (distinguish definitive-offline [read OK ∧ status≠online] from read-error [leave driver, retry]).
(iii) **lost-update** race: pulse driver's touch_active (set_last_active RMW of whole info.json) clobbers concurrent mark_offline back to online → un-host never fires → signed-off agent keeps Psyche. Fix: offline write must not lose (field-level status write / status CAS / set_last_active stops rewriting status). Tests currently use long-pulse+settle (deterministic; remove after fix).
PLAN: bring DAEMON-LIFECYCLE-COMPLETION-PLAN.md (on main) onto the branch + write all 3 into its W0.4 section. Bring W0.4 DESIGN to doyle before deep commit (same protocol).

## AFTER W0
- **W0.5**: cutover validation w/ perri's claude-spt (zero adapter change).
- M11 shell waves: W2 drive (REQ-SHELL-3) → W3 tunnel → W4 Gateway capstone → W5 rig+docs (M11-PLAN.md).

## GOTCHAS
- Pre-flight `clippy --workspace --all-targets -D warnings` (not -p). `git branch --show-current` before EVERY commit.
- Windows exe-lock (os error 5 on spt.exe rebuild) = leftover dev daemons; reap WORKSPACE-SCOPED only (`Get-Process spt | Where Path -like "$ws\*" | Stop-Process`), NEVER machine-wide (shared runner w/ live broker).
- doyle flow: designs → I build → conditional-PASS-then-CI-green gate per wave. CI only fires on PR (push branches=main/dev-freeform).


ScheduleWakeup

Polling PR #16 run 27555949485 (W0.3 a916727) for both-runner green to ping doyle the run id → W0.3 full PASS; then scope W0.4 close-out (3 blockers). CI ~5-8min, harness can't notify on it.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 08:14:00 (in 267s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


**W0.2 FULL PASS** (consolidation is real prod path now) → **W0.3 submitted** @a916727 (bind-no-Psyche gap closed), CI run 27555949485 running.

Progress this session: **W1 + W0.1 + W0.2 all full-PASS; W0.3 in CI** — each designed-gated by doyle, built, tested, committed, pushed, green.

Next: W0.3 CI green → ping run id → full PASS → then **W0.4 close-out wave** (the 3 hard blockers doyle gates hardest: int re-point to real-brain path, read-error un-host, lost-update fix). I'll bring the DAEMON plan onto the branch + the W0.4 design to doyle before deep commit.

Commune checkpoint refreshed (W0.4 blockers + the workspace-scoped-reap gotcha preserved). Polling CI.