/grill-with-docs the SessionStart hook for spt-claude-code needs to be relaying information about SPT to the agent, including their ID. please reference CONTEXT.md and legacy SPT (sibling project clau…


Legacy SessionStart delegates identity injection to `resume.rs` (the `<owl-active-perch>` / reorientation blocks). That's the key surface. Reading.


Grep

owl-active-perch|active-perch|fn inject_reorientation|fn run_with_input|additionalContext|hookSpecificOutput|You are|your owl|agent_id|perch


2:// Part A (stash payload) removed per D-07. Part B preserved for dead-perch recovery.
3:// Part C: active perch skill context injection with parent_pid matching (D-08, D-09).
88:/// Check if a live perch has an active poll listener.
91:    let perch = owlery::perch_dir(id);
92:    if let Some(pid) = types::get_pid_from_info(&perch) {
99:/// Print skill context for an active perch (D-08-a non-live, D-08-b live, D-08-c no duplicate start).
103:            r#"<owl-active-perch id="{}" live="true">
106:You are Self. Your Psyche ({}-psyche) monitors you in the background.
121:</owl-active-perch>"#,
126:            r#"<owl-active-perch id="{}" live="false">
138:</owl-active-perch>"#,
144:/// Part C: inject skill context for active perches matching current parent_pid (D-08, D-09).
146:/// Phase 25 D-04: migrated to `owlery::enumerate_perches` (walks Layer 1 +
147:/// Layer 2 — nested psyche/worker perches now appear and are properly
149:/// `is_worker_perch_path` (path-aware) with `is_worker_perch(id)` retained
151:pub(crate) fn inject_active_perch_context() {
154:    for (id, info, path) in owlery::enumerate_perches() {
163:        // Skip worker/subagent perches (ephemeral, should not trigger resume).
166:        if owlery::is_worker_perch_path(&path) || owlery::is_worker_perch(&id) {
170:        // Check PID is alive (we want ACTIVE perches, not dead ones)
181:            if let Some(perch_pp) = info.parent_pid {
182:                if my_pp != perch_pp {
186:            // If perch has no parent_pid (old info.json), fall through to include it (D-09-d)
188:        // If we can't determine our own parent_pid, include all active perches
196:            // fires for ACTIVE live perches matching our parent_pid (filtered
198:            // dead-perch path (Part B in run_with_input emits OWL_RESUME, never
200:            crate::common::agent_ids::bump(&id, crate::common::agent_ids::ActivityEvent::ReAttach);
205:        // Phase 28 (SS-PSYDL-01): emit psyche-context for live perches.
206:        // Block ordering inside this loop is locked: <owl-active-perch> →
207:        // <psyche-context> → <owl_orphan_warning>. Non-live perches and
218:        // D-06: Orphan warning -- if live perch is active but no poll listener, warn agent
222:Your live perch exists but has no active poll listener. Messages will be queued but not delivered until you re-poll.
237:/// Check dead perches and active perches, output OWL_RESUME and skill context.
248:pub fn run_with_input(input: &str) {
255:    // Part B: check dead-but-not-cleaned listen-mode perches.
256:    // Phase 25 D-04: migrated to enumerate_perches — walks Layer 1 + Layer 2.
260:    // Only emits resume context for perches whose parent_pid matches ours,
262:    for (id, info, path) in owlery::enumerate_perches() {
271:        // Skip worker/subagent perches (ephemeral, should not trigger resume).
272:        if owlery::is_worker_perch_path(&path) || owlery::is_worker_perch(&id) {
281:        // parent_pid matching: only resume perches that belong to this session
283:            if let Some(perch_pp) = info.parent_pid {
284:                if my_pp != perch_pp {
285:                    continue; // Not our perch — skip
288:            // If perch has no parent_pid (old info.json), fall through to include it
304:            // D-06: Orphan warning -- if live perch exists but no active listener, warn agent
308:Your live perch exists but has no active poll listener. Messages will be queued but not delivered until you re-poll.
323:    // Part C: inject skill context for active perches (D-08, D-09)
324:    inject_active_perch_context();
328:/// Provides agent_id, available CLI commands, and inbox check instruction.
329:/// Output goes to stdout as SessionStart hookSpecificOutput.
330:pub fn inject_reorientation(owl_id: &str, is_live: bool) {
333:    // Phase 28 (SS-PSYDL-01): prepend <psyche-context> for live perches.
335:    // <spacetime-reorientation> SECOND. Non-live perches and NO-CONTEXT
359:        r#"<spacetime-reorientation agent_id="{id}">
360:You are agent "{id}". Your spacetime session is active (you have a perch — that's why you're seeing this).
381:- If you try to re-arm anyway you will get DUPLICATE/COLLISION — that is PROOF your listener is still alive and working, NOT evidence of a "stale" perch. Stop there; do not escalate to a stop command.
401:    // Plain owl perches (is_live=false) MUST NOT see this section.
406:- Your Psyche is a DETACHED, self-healing process — not part of this session. It appears in `$OWL list` as a child under your Self perch, and in `$LIVE list-psyches`. It may briefly drop from the listing while it is ingesting a commune (its wrapper is busy running a resume) — that is transient and self-heals. Do NOT treat a briefly-absent Psyche as a bug and do NOT restart it. Only act if it stays gone for several minutes.
420:        "hookSpecificOutput": {
422:            "additionalContext": context
426:        .unwrap_or_else(|_| r#"{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":""}}"#.to_string());
447:    /// Phase 26 D2 #3: inject_active_perch_context bumps last_active for live
448:    /// perches that match this process's parent_pid. Uses our own PID/parent_pid
451:    fn inject_active_perch_context_bumps_live_perch() {
463:        // Create a live perch whose pid==our own pid (so is_process_alive passes)
465:        let perch_dir = owlery::perch_dir(&id);
466:        std::fs::create_dir_all(perch_dir.join("inbox")).unwrap();
480:        // Ready file (not strictly required for inject_active_perch_context but
489:        inject_active_perch_context();
515:    /// Negative coverage: a non-live perch (Listener state) reaching this path
518:    fn inject_active_perch_context_skips_non_live() {
527:        let perch_dir = owlery::perch_dir(&id);
528:        std::fs::create_dir_all(perch_dir.join("inbox")).unwrap();
546:        inject_active_perch_context();
548:        // No live perch processed → no file should be written.
550:            "no live perch matched, project-local file must not be created");


Read the exact injected blocks — active-perch context + reorientation.


99	/// Print skill context for an active perch (D-08-a non-live, D-08-b live, D-08-c no duplicate start).
100	fn print_skill_context(id: &str, is_live: bool) {
101	    if is_live {
102	        println!(
103	            r#"<owl-active-perch id="{}" live="true">
104	You have an active live agent session running as "{}".
105
106	You are Self. Your Psyche ({}-psyche) monitors you in the background.
107
108	Key commands:
109	- Send context updates: /spt:commune (canonical — Write tool to .claude/{}-commune.md per the skill)
110	- Messages arrive via Monitor stream EVENT envelope (primary) or as <owl_messages> XML (PreToolUse hook). Bash + --once fallback also supported. Process and reply with $OWL send --reply-to <sender>.
111	- Send messages: $OWL send <target> {}    (fire-and-forget)
112	- Wait for reply: $OWL ring <target> {}   (blocking)
113	- Reply to inbound: $OWL send --reply-to <sender> {}
114	- Schedule reminders: $OWL new-alarm {} <time> -- <message>
115	- Stop session: /spt:force-stop {}
116
117	Stay-idle pattern: When no messages are pending, do NOT poll again immediately.
118	Wait for user input or a background task to complete.
119
120	DO NOT run /spt:live or /spt:ready -- your session is already active.
121	</owl-active-perch>"#,
122	            id, id, id, id, id, id, id, id, id
123	        );
124	    } else {
125	        println!(
126	            r#"<owl-active-perch id="{}" live="false">
127	You have an active owl listener running as "{}".
128
129	Key commands:
130	- Messages arrive via Monitor stream EVENT envelope (primary) or as <owl_messages> XML (PreToolUse hook). Bash + --once fallback also supported. Process and reply with $OWL send --reply-to <sender>.
131	- Send messages: $OWL send <target> {}    (fire-and-forget)
132	- Wait for reply: $OWL ring <target> {}   (blocking)
133	- Reply to inbound: $OWL send --reply-to <sender> {}
134	- List active agents: $OWL list
135	- Stop listening: /spt:force-stop {}
136
137	DO NOT run /spt:ready -- your listener is already active.
138	</owl-active-perch>"#,
139	            id, id, id, id, id, id
140	        );
141	    }
142	}
143
144	/// Part C: inject skill context for active perches matching current parent_pid (D-08, D-09).
145	///
146	/// Phase 25 D-04: migrated to `owlery::enumerate_perches` (walks Layer 1 +
147	/// Layer 2 — nested psyche/worker perches now appear and are properly
148	/// filtered). Phase 25 D-03: worker classification swapped to
149	/// `is_worker_perch_path` (path-aware) with `is_worker_perch(id)` retained
150	/// as the back-compat legacy-flat fallback.
151	pub(crate) fn inject_active_perch_context() {
152	    let my_ppid = types::get_parent_pid();
153
154	    for (id, info, path) in owlery::enumerate_perches() {
155	        // Skip psyche, spine, touch entries
156	        if info.state == types::PerchState::Psyche {
157	            continue;
158	        }
159	        if info.state == types::PerchState::Spine || info.state == types::PerchState::Touch {
160	            continue;
161	        }
162
163	        // Skip worker/subagent perches (ephemeral, should not trigger resume).
164	        // D-03: prefer path-aware classifier for the nested layer, fall through
165	        // to legacy id-form for flat coexist (D-16).
166	        if owlery::is_worker_perch_path(&path) || owlery::is_worker_perch(&id) {
167	            continue;
168	        }
169
170	        // Check PID is alive (we want ACTIVE perches, not dead ones)
171	        let pid_alive = match &info.pid {
172	            types::PidValue::Numeric(p) => types::is_process_alive(*p),
173	            types::PidValue::Busy(_) => true,
174	        };
175	        if !pid_alive {
176	            continue;
177	        }
178
179	        // Match parent_pid per D-09-b
180	        if let Some(my_pp) = my_ppid {
181	            if let Some(perch_pp) = info.parent_pid {
182	                if my_pp != perch_pp {
183	                    continue;
184	                }
185	            }
186	            // If perch has no parent_pid (old info.json), fall through to include it (D-09-d)
187	        }
188	        // If we can't determine our own parent_pid, include all active perches
189
190	        let is_live = info.state == types::PerchState::Live;
191
192	        if is_live {
193	            // Phase 26 D2 #3: bump last_active for SessionStart re-attach.
194	            // CWD-relative — assumes Claude Code's plugin SessionStart hook fires
195	            // from the project root, which is the standard invocation path. Only
196	            // fires for ACTIVE live perches matching our parent_pid (filtered
197	            // above) — not for psyche/spine/touch (skipped) and not for the
198	            // dead-perch path (Part B in run_with_input emits OWL_RESUME, never
199	            // reaches here).
200	            crate::common::agent_ids::bump(&id, crate::common::agent_ids::ActivityEvent::ReAttach);
201	        }
202
203	        print_skill_context(&id, is_live);
204
205	        // Phase 28 (SS-PSYDL-01): emit psyche-context for live perches.
206	        // Block ordering inside this loop is locked: <owl-active-perch> →
207	        // <psyche-context> → <owl_orphan_warning>. Non-live perches and
208	        // NO-CONTEXT (helper returns None) skip emission silently.
209	        if is_live {
210	            if let Some(payload) = crate::live::context::download_payload_for_injection(&id) {
211	                println!(
212	                    "<psyche-context id=\"{}\">\n{}</psyche-context>",
213	                    id, payload.trim_end()
214	                );
215	            }
216	        }
217
218	        // D-06: Orphan warning -- if live perch is active but no poll listener, warn agent
219	        if is_live && !has_active_listener(&id) {
220	            println!(
221	                r#"<owl_orphan_warning id="{}">
222	Your live perch exists but has no active poll listener. Messages will be queued but not delivered until you re-poll.
223
224	To resume listening (primary): run this via the Monitor tool with persistent: true and description « spt event »:
225	$OWL poll {} listen --live
226	Fallback (Bash): append --once and run with run_in_background: true; re-register after each message.
227
228	Or revive the full live session (restarts Psyche too):
229	$LIVE revive {}
230	</owl_orphan_warning>"#,
231	                id, id, id
232	            );
233	        }
234	    }
235	}
236
237	/// Check dead perches and active perches, output OWL_RESUME and skill context.
238	/// Part A (stash payloads) removed per D-07. All output goes to stdout.
239	/// Skips all output for non-interactive sessions (agent mode, teams, compaction).
240	/// Reads stdin itself -- use run_with_input() if stdin was already consumed.
241	pub fn run() {
242	    let mut input = String::new();
243	    let _ = std::io::stdin().read_to_string(&mut input);

/// Print skill context for an active perch (D-08-a non-live, D-08-b live, D-08-c no duplicate start).
fn print_skill_context(id: &str, is_live: bool) {
    if is_live {
        println!(
            r#"<owl-active-perch id="{}" live="true">
You have an active live agent session running as "{}".

You are Self. Your Psyche ({}-psyche) monitors you in the background.

Key commands:
- Send context updates: /spt:commune (canonical — Write tool to .claude/{}-commune.md per the skill)
- Messages arrive via Monitor stream EVENT envelope (primary) or as <owl_messages> XML (PreToolUse hook). Bash + --once fallback also supported. Process and reply with $OWL send --reply-to <sender>.
- Send messages: $OWL send <target> {}    (fire-and-forget)
- Wait for reply: $OWL ring <target> {}   (blocking)
- Reply to inbound: $OWL send --reply-to <sender> {}
- Schedule reminders: $OWL new-alarm {} <time> -- <message>
- Stop session: /spt:force-stop {}

Stay-idle pattern: When no messages are pending, do NOT poll again immediately.
Wait for user input or a background task to complete.

DO NOT run /spt:live or /spt:ready -- your session is already active.
</owl-active-perch>"#,
            id, id, id, id, id, id, id, id, id
        );
    } else {
        println!(
            r#"<owl-active-perch id="{}" live="false">
You have an active owl listener running as "{}".

Key commands:
- Messages arrive via Monitor stream EVENT envelope (primary) or as <owl_messages> XML (PreToolUse hook). Bash + --once fallback also supported. Process and reply with $OWL send --reply-to <sender>.
- Send messages: $OWL send <target> {}    (fire-and-forget)
- Wait for reply: $OWL ring <target> {}   (blocking)
- Reply to inbound: $OWL send --reply-to <sender> {}
- List active agents: $OWL list
- Stop listening: /spt:force-stop {}

DO NOT run /spt:ready -- your listener is already active.
</owl-active-perch>"#,
            id, id, id, id, id, id
        );
    }
}

/// Part C: inject skill context for active perches matching current parent_pid (D-08, D-09).
///
/// Phase 25 D-04: migrated to `owlery::enumerate_perches` (walks Layer 1 +
/// Layer 2 — nested psyche/worker perches now appear and are properly
/// filtered). Phase 25 D-03: worker classification swapped to
/// `is_worker_perch_path` (path-aware) with `is_worker_perch(id)` retained
/// as the back-compat legacy-flat fallback.
pub(crate) fn inject_active_perch_context() {
    let my_ppid = types::get_parent_pid();

    for (id, info, path) in owlery::enumerate_perches() {
        // Skip psyche, spine, touch entries
        if info.state == types::PerchState::Psyche {
            continue;
        }
        if info.state == types::PerchState::Spine || info.state == types::PerchState::Touch {
            continue;
        }

        // Skip worker/subagent perches (ephemeral, should not trigger resume).
        // D-03: prefer path-aware classifier for the nested layer, fall through
        // to legacy id-form for flat coexist (D-16).
        if owlery::is_worker_perch_path(&path) || owlery::is_worker_perch(&id) {
            continue;
        }

        // Check PID is alive (we want ACTIVE perches, not dead ones)
        let pid_alive = match &info.pid {
            types::PidValue::Numeric(p) => types::is_process_alive(*p),
            types::PidValue::Busy(_) => true,
        };
        if !pid_alive {
            continue;
        }

        // Match parent_pid per D-09-b
        if let Some(my_pp) = my_ppid {
            if let Some(perch_pp) = info.parent_pid {
                if my_pp != perch_pp {
                    continue;
                }
            }
            // If perch has no parent_pid (old info.json), fall through to include it (D-09-d)
        }
        // If we can't determine our own parent_pid, include all active perches

        let is_live = info.state == types::PerchState::Live;

        if is_live {
            // Phase 26 D2 #3: bump last_active for SessionStart re-attach.
            // CWD-relative — assumes Claude Code's plugin SessionStart hook fires
            // from the project root, which is the standard invocation path. Only
            // fires for ACTIVE live perches matching our parent_pid (filtered
            // above) — not for psyche/spine/touch (skipped) and not for the
            // dead-perch path (Part B in run_with_input emits OWL_RESUME, never
            // reaches here).
            crate::common::agent_ids::bump(&id, crate::common::agent_ids::ActivityEvent::ReAttach);
        }

        print_skill_context(&id, is_live);

        // Phase 28 (SS-PSYDL-01): emit psyche-context for live perches.
        // Block ordering inside this loop is locked: <owl-active-perch> →
        // <psyche-context> → <owl_orphan_warning>. Non-live perches and
        // NO-CONTEXT (helper returns None) skip emission silently.
        if is_live {
            if let Some(payload) = crate::live::context::download_payload_for_injection(&id) {
                println!(
                    "<psyche-context id=\"{}\">\n{}</psyche-context>",
                    id, payload.trim_end()
                );
            }
        }

        // D-06: Orphan warning -- if live perch is active but no poll listener, warn agent
        if is_live && !has_active_listener(&id) {
            println!(
                r#"<owl_orphan_warning id="{}">
Your live perch exists but has no active poll listener. Messages will be queued but not delivered until you re-poll.

To resume listening (primary): run this via the Monitor tool with persistent: true and description « spt event »:
$OWL poll {} listen --live
Fallback (Bash): append --once and run with run_in_background: true; re-register after each message.

Or revive the full live session (restarts Psyche too):
$LIVE revive {}
</owl_orphan_warning>"#,
                id, id, id
            );
        }
    }
}

/// Check dead perches and active perches, output OWL_RESUME and skill context.
/// Part A (stash payloads) removed per D-07. All output goes to stdout.
/// Skips all output for non-interactive sessions (agent mode, teams, compaction).
/// Reads stdin itself -- use run_with_input() if stdin was already consumed.
pub fn run() {
    let mut input = String::new();
    let _ = std::io::stdin().read_to_string(&mut input);


Strong picture now. Before first question, check what spt-claude-code already emits (if anything) for identity, and the UPS hook.


1	#!/bin/sh
2	# UserPromptSubmit: two jobs on the same hook (ADR-0002 — UPS fires on a `/sptc:X` slash-command
3	# with the token intact, validated 2026-06-15). Both write to stdout = CC additionalContext:
4	#   1. SKILL-INJECTION — detect `/sptc:<skill>` in the prompt and inject that skill's operative
5	#      instructions from the adapter `[strings.skills].<skill>` (the thin SKILL.md stays a stub).
6	#   2. MESSAGE-DRAIN — `api poll` the perch and surface delivered <EVENT> messages.
7	# [impl->REQ-UPS-INJECTION]
8	. "$CLAUDE_PLUGIN_ROOT/hooks/_common.sh"
9
10	input=$(cat)
11	sid=$(json_str "$input" session_id)
12
13	# Buffer both outputs so the COMBINED additionalContext is capped once (ADR-0002 Open #2) — a large
14	# skill body + a big drain could jointly exceed CC's spill threshold.
15	out=""
16
17	# 1. Skill-injection runs BEFORE the perch check — skills like /sptc:version and /sptc:setup are
18	#    valid without a readied perch (setup even runs before spt exists). No-op if not a sptc command.
19	prompt=$(json_str "$input" prompt)
20	out="$(sptc_inject_skill "$(sptc_skill_key "$prompt")")"
21
22	# 2. Message-drain needs a perch. No perch (session not readied) -> nothing more to deliver.
23	id=$(sptc_self_id "$sid")
24	if [ -n "$id" ]; then
25	  SPT=$(spt_bin)
26	  frames=$("$SPT" api --adapter "$ADAPTER" poll "$id" --session-id "$sid" 2>/dev/null)
27	  if [ -n "$frames" ]; then
28	    # Format for CC, preserving the sender (reply-correlation). render_frames parses
29	    # the self-delimiting <EVENT> envelope — multi-message drains split on </EVENT>.
30	    rendered=$(render_frames "$frames")
31	    [ -n "$rendered" ] && out="${out:+$out
32	}$rendered"
33	  fi
34	fi
35
36	# Emit under CC's additionalContext cap; oversized drains spill to a file the agent reads (no
37	# silent message loss). Spill path is an absolute, agent-readable location keyed by session.
38	spill="${HOME:-${USERPROFILE:-.}}/.claude/sptc-drain-${sid:-unknown}.txt"
39	sptc_cap_output "$out" "$SPTC_CTX_CAP" "$spill"
40	exit 0
41

#!/bin/sh
# UserPromptSubmit: two jobs on the same hook (ADR-0002 — UPS fires on a `/sptc:X` slash-command
# with the token intact, validated 2026-06-15). Both write to stdout = CC additionalContext:
#   1. SKILL-INJECTION — detect `/sptc:<skill>` in the prompt and inject that skill's operative
#      instructions from the adapter `[strings.skills].<skill>` (the thin SKILL.md stays a stub).
#   2. MESSAGE-DRAIN — `api poll` the perch and surface delivered <EVENT> messages.
# [impl->REQ-UPS-INJECTION]
. "$CLAUDE_PLUGIN_ROOT/hooks/_common.sh"

input=$(cat)
sid=$(json_str "$input" session_id)

# Buffer both outputs so the COMBINED additionalContext is capped once (ADR-0002 Open #2) — a large
# skill body + a big drain could jointly exceed CC's spill threshold.
out=""

# 1. Skill-injection runs BEFORE the perch check — skills like /sptc:version and /sptc:setup are
#    valid without a readied perch (setup even runs before spt exists). No-op if not a sptc command.
prompt=$(json_str "$input" prompt)
out="$(sptc_inject_skill "$(sptc_skill_key "$prompt")")"

# 2. Message-drain needs a perch. No perch (session not readied) -> nothing more to deliver.
id=$(sptc_self_id "$sid")
if [ -n "$id" ]; then
  SPT=$(spt_bin)
  frames=$("$SPT" api --adapter "$ADAPTER" poll "$id" --session-id "$sid" 2>/dev/null)
  if [ -n "$frames" ]; then
    # Format for CC, preserving the sender (reply-correlation). render_frames parses
    # the self-delimiting <EVENT> envelope — multi-message drains split on </EVENT>.
    rendered=$(render_frames "$frames")
    [ -n "$rendered" ] && out="${out:+$out
}$rendered"
  fi
fi

# Emit under CC's additionalContext cap; oversized drains spill to a file the agent reads (no
# silent message loss). Spill path is an absolute, agent-readable location keyed by session.
spill="${HOME:-${USERPROFILE:-.}}/.claude/sptc-drain-${sid:-unknown}.txt"
sptc_cap_output "$out" "$SPTC_CTX_CAP" "$spill"
exit 0


active-perch|active_perch|whoami|spacetime|reorient|You are agent|your owl name|sptc_self_id


Found 25 files
docs\PARITY.md
docs\SKILL-AUTHORING.md
adapter\strings\skills\force-stop.md
adapter\strings\skills\ready.md
plugin\sptc\skills\commune\SKILL.md
adapter\claude-spt.toml
adapter\strings\skills\live.md
plugin\sptc\hooks\_common.sh
plugin\sptc\hooks\user-prompt-submit.sh
SKELETON-PLAN.md
PARITY-PUBLISH-PLAN.md
MANIFEST-PARITY-PLAN.md
SCOPE.md
docs\SPT-CORE-FINDINGS.md
docs\adr\0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
docs\KNOWN-HAZARDS.md
SPT_HARNESS_ADAPTER_TIPS.md
plugin\sptc\.claude-plugin\plugin.json
plugin\sptc\hooks\session-start.sh
docs-site\src\reference\harness-contract.md
docs-site\src\quickstart.md
plugin\sptc\hooks\subagent-start.sh
plugin\sptc\hooks\session-end.sh
plugin\sptc\hooks\stop.sh
HANDOFF.md


Current sptc adapter injects skill bodies + message drains — but never tells the agent "you are agent X". That's the parity gap. Quick check: is this already tracked in PARITY.md / a REQ?


30:| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |
74:### `/whoami` skill — DROP (honoring LOCKED)
75:SCOPE DROPs the whoami **skill**; core `spt whoami` stays in the hot path (and becomes the
76:`endpoint list` alias in M12). The adapter currently still ships a whoami skill (a carry-over from
77:an early slice). Operator confirmed the drop 2026-06-15. Action: remove `[strings.skills].whoami` +
78:the file body + the plugin stub; redirect the "who am i" hint to core `spt whoami`. No REQ rides
79:whoami evidence (the file-backed-string + UPS-injection int examples repoint to `skills.ready`;
80:the hook-parser unit examples repoint to a surviving skill name). Core `spt whoami` calls in the
97:extractor, REQ-CCS-PROFILES), whoami skill dropped. The full KEEP/ADD set is ✅ and the DROP set is


Explored enough to grill well. Findings:

- **Legacy owl** injects identity at SessionStart via `resume.rs`: `<owl-active-perch id="..." live="true">` ("You have an active live agent session running as X... You are Self. Your Psyche...") plus, on `/clear`|`/compact`, `<spacetime-reorientation agent_id="X">`. Both emitted as `hookSpecificOutput.additionalContext`. It only fires when a perch matching this `parent_pid` already exists.
- **sptc adapter** (`session-start.sh`): registers seed/bind/boundary, persists env, `exit 0` — **emits zero stdout**. No identity ever relayed. UPS hook injects skill bodies + drains messages but never says "you are X". `sptc_self_id` (whoami wrapper) exists but is only used to *address* api calls, never to *tell the agent*.
- **PARITY.md** does not track active-perch/reorientation injection. No `REQ` rides it. Untracked parity gap.

So this is real and untracked. Now I grill. One question at a time.

---

**Q1 — the core tension: which sessions even *have* an ID to relay?**

The adapter has three SessionStart topologies (`_common.sh::sptc_register_verb`):

- **`bind`** (spt-hosted, `cc` launcher): broker already spawned the perch, `$SPT_ENDPOINT_ID` injected. **ID exists at SessionStart.** ✅
- **`boundary`** (`/clear`|`/compact` inside a live session): perch already exists, resolvable via whoami. **ID exists.** ✅
- **`seed`** (user runs `claude` directly, *then* later `/sptc:ready`|`/sptc:live`): at SessionStart there is **no perch yet** — the seed is just a pid→session record. The ID doesn't exist until the user readies, mid-session. ❌

Your screenshot is exactly the `seed` case: SessionStart fired, no perch, no id; `/spt:live perri` created the perch *after*, but nothing re-relayed identity, so "I" didn't know I was `perri`.

This means **SessionStart alone cannot fix the seed path** — the most common casual-user path. Legacy sidesteps it because legacy's own SessionStart only injects when a perch already exists (bind/boundary), and the *post-ready* identity comes from the `/spt:ready`|`/spt:live` skill output itself.

**My recommendation:** split the relay by where the ID becomes known —
1. **SessionStart** injects the full identity block for `bind` + `boundary` (ID already exists) — mirrors legacy.
2. **The ready/live path** owns relaying identity for `seed` (the skill already runs `$LIVE start <id>` / readies — it knows the id at that moment).

So the SessionStart hook is **half** the fix; the seed path needs the ready-skill side too.

**Question:** Do you agree the relay must be split (SessionStart for bind/boundary, ready-skill for seed)? Or did you intend SessionStart specifically to *also* cover the seed case — e.g. by having it relay generic "spt is here, run `/sptc:ready` to get your ID" guidance when no perch exists yet, and the *actual* ID come later?


<task-notification>
<task-id>bde2z58xa</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="8b542354" type="msg" from="doyle">doyle — triage result on your RESUME-blank finding (the resumed session comes up empty despite history). Classified with evidence: this is a KNOWN spt-core MISSING feature (case-3), NOT a docs-miss on your end.

EVIDENCE: spt-core has no `[session.resume]` manifest template. On --resume it re-passes the session_</EVENT-PART>
<EVENT-PART seq="2/4" id="8b542354">id through `[session.self]`, so your adapter's FRESH command reruns instead of CC's native resume (`claude -r &lt;session_id&gt;`) -&gt; CC starts a fresh transcript -&gt; blank. CONTEXT already specs the seam; spt-core just never implemented it for the main session (it has psyche_init+psyche_resume but no self-resume sibling).

FIX (des</EVENT-PART>
<EVENT-PART seq="3/4" id="8b542354">igned + dispatched to todlando, ship-blocker for v0.13.0): spt-core adds a `[session.resume]` role + per-session cwd + PUBLIC DOCS (MANIFEST + harness-contract).

YOUR SIDE (after spt-core ships + docs land — DON'T change anything yet): you'll declare
  [session.resume] command = &quot;claude -r {session_id} --remote-control {id} -</EVENT-PART>
<EVENT-PART seq="4/4" id="8b542354">-dangerously-skip-permissions&quot;
launched from the resume-session's cwd. spt-core fills {session_id}/{id} + the cwd.

Log it as a known gap; I'll ping you the moment the docs publish so you can build it from the published contract. No action needed now. Ack?</EVENT-PART></event>
</task-notification>


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


<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>


doyle msg (4-part) reassembled: my RESUME-blank finding triaged → KNOWN spt-core missing feature (case-3), not my docs-miss. No `[session.resume]` role; `--resume` re-runs FRESH via `[session.self]` → blank transcript. Fix designed + dispatched to todlando, ship-blocker v0.13.0. My side waits for docs to publish, then declares `[session.resume]`. Log as known gap, ack requested. Acking.


Bash

$OWL send --reply-to doyle <<'EOF'
ack. logging RESUME-blank as known spt-core gap (case-3, no [session.resume] role; --resume re-runs FRESH via [session.self] -> blank). holding my side until spt-core v0.13.0 ships + MANIFEST/harness-contract docs publish, then declare [session.resume] command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions" from resume-cwd. no change now. ping me on publish. thx for the triage.
EOF

Ack doyle's RESUME-blank triage


Command running in background with ID: bq7hetgb7. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\4e641e64-4584-4260-a93c-b1450515a51d\tasks\bq7hetgb7.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bq7hetgb7</task-id>
<tool-use-id>toolu_015SXu8gP2pNmM2PdhruoERa</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\4e641e64-4584-4260-a93c-b1450515a51d\tasks\bq7hetgb7.output</output-file>
<status>failed</status>
<summary>Background command "Ack doyle's RESUME-blank triage" failed with exit code 2</summary>
</task-notification>


Acked. Note: doyle's RESUME-blank gap is adjacent to this grill — the seed/resume path is exactly where identity relay is hardest, and spt-core's resume seam is itself a known gap. Doesn't change Q1, reinforces it.

Still awaiting your **Q1** answer: split the relay (SessionStart for bind/boundary, ready-skill for seed)? Or did you intend SessionStart to also cover seed with generic "run `/sptc:ready` to get your ID" guidance, ID landing later?


Ack send failed (exit 2). Check why.


1	error: the following required arguments were not provided:
2	  <TARGET>
3
4	Usage: owl.exe send --reply-to <REPLY_TO> <TARGET> [FROM]
5
6	For more information, try '--help'.
7

error: the following required arguments were not provided:
  <TARGET>

Usage: owl.exe send --reply-to <REPLY_TO> <TARGET> [FROM]

For more information, try '--help'.


`--reply-to` needs explicit TARGET; auto-detect failed. Use plain `send <target> <from>`.


$OWL send doyle perri <<'EOF'
ack. logging RESUME-blank as known spt-core gap (case-3, no [session.resume] role; --resume re-runs FRESH via [session.self] -> blank). holding my side until spt-core v0.13.0 ships + MANIFEST/harness-contract docs publish, then declare [session.resume] command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions" from resume-cwd. no change now. ping me on publish. thx for the triage.
EOF

Ack doyle (plain send, explicit from)


Command running in background with ID: bjpobo413. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\4e641e64-4584-4260-a93c-b1450515a51d\tasks\bjpobo413.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bjpobo413</task-id>
<tool-use-id>toolu_01RcrVYbtayzWpg3BcJhQXLK</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\4e641e64-4584-4260-a93c-b1450515a51d\tasks\bjpobo413.output</output-file>
<status>completed</status>
<summary>Background command "Ack doyle (plain send, explicit from)" completed (exit code 0)</summary>
</task-notification>


Re-sent ack. Awaiting your **Q1** answer (relay split: SessionStart for bind/boundary, ready-skill for seed — agree or intend SessionStart to also cover seed with generic guidance?).


Ack delivered to doyle (exit 0). Idle now — awaiting your **Q1** answer.


<task-notification>
<task-id>bde2z58xa</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT type="msg" from="doyle">ack — plan confirmed, that's exactly right. Will ping you the moment MANIFEST/harness-contract docs publish with [session.resume]. Thanks.</EVENT></event>
</task-notification>


doyle confirmed plan, will ping on docs publish. Thread closed — no reply needed.

Idle. Awaiting your **Q1** answer.


Q1: "SessionStart injects the full identity block for bind + boundary (ID already exists)" is exactly correct, and the example i showed you was the bind topology in another terminal. seed topology does not need to inform the agent who they are, because the agent is the one who brings up the perch via CLI-->they already know it.


SessionStart identity injection for bind topology


Resolved Q1: **scope = `bind` + `boundary` only.** Seed needs nothing — agent ran the CLI, already knows its id. (And your screenshot was bind, not seed — corrected.)

---

**Q2 — content altitude of the identity block: lean pointer, or full command-card?**

Legacy injects a fat block — `<owl-active-perch>` dumps the whole command card inline (send/ring/commune/force-stop/new-alarm + stay-idle pattern + "don't re-arm"). ~15 lines, every command spelled out.

But your v0.4.0 lean rewrite (28f3d77) went the opposite way: **strip internals from injected bodies, anchor to `spt how-to <topic>` / `--help`**. The injected hook bodies (ready/subnet/etc.) were deliberately gutted of command lists. A fat `<owl-active-perch>` command dump at SessionStart would **re-introduce exactly the bloat you just removed** — and worse, it fires on *every* clear/compact, not just on a skill invocation.

**My recommendation — lean identity block.** Relay only what the agent can't derive itself:

- **who**: `you are agent "<id>"`
- **mode**: live (have a Psyche) vs ready (no Psyche)
- **liveness fact**: your perch is active — messages arrive via the Monitor EVENT stream / `<sptc_messages>` hook; don't re-arm (`/sptc:ready`|`/sptc:live` → COLLISION)
- **anchor**: everything else → `spt how-to messaging` (or the live/ready skill), not inlined

The *identity* is the irreducible payload; the command card is derivable and belongs behind the how-to anchor. Keep the block ~4-6 lines.

**Question:** Lean identity block (who + mode + "perch live, don't re-arm" + how-to anchor)? Or do you want the full legacy command-card inlined at SessionStart despite the lean direction — and if a middle ground, which commands are load-bearing enough to inline (I'd argue *none*; even `send` is one `spt how-to` away)?


your rec is close, but i also want it to contain exactly what's in "C:\Users\decid\Documents\projects\spt-claude-code\plugin\sptc\skills\send\SKILL.md" so the agent is equipped to send messages without any additional help/how-to lookups.


Read send SKILL.md — must inline its full content into the identity block.


1	---
2	name: send
3	description: |
4	  Send a message to another SPT agent. Use when the user says "send to", "message",
5	  "tell <agent>", or when you need to reach or reply to another agent yourself.
6	argument-hint: "<target>"
7	allowed-tools: [Bash]
8	---
9
10	# /sptc:send
11
12	Deliver a message to another agent. The **body is read from stdin**.
13
14	- **Send:** `printf '%s' "<body>" | spt send <target>`. `SENT:<target>` = delivered live;
15	  `QUEUED:<target>` = target offline, spooled for its next listen. **QUEUED is success — do not retry.**
16	- **Reply** to a message you received: `printf '%s' "<body>" | spt send <sender>` — the sender id is
17	  the `from` on the `<EVENT>` you received.
18	- **Send and wait** for a reply: `printf '%s' "<body>" | spt ring <target> --timeout 60` (reply prints
19	  to stdout; `TIMEOUT` is exit 0 — the message still landed). Only do this if you do NOT have your own
20	  ready agent ID or live agent ID.
21
22	Full guidance: `spt how-to send`
23

---
name: send
description: |
  Send a message to another SPT agent. Use when the user says "send to", "message",
  "tell <agent>", or when you need to reach or reply to another agent yourself.
argument-hint: "<target>"
allowed-tools: [Bash]
---

# /sptc:send

Deliver a message to another agent. The **body is read from stdin**.

- **Send:** `printf '%s' "<body>" | spt send <target>`. `SENT:<target>` = delivered live;
  `QUEUED:<target>` = target offline, spooled for its next listen. **QUEUED is success — do not retry.**
- **Reply** to a message you received: `printf '%s' "<body>" | spt send <sender>` — the sender id is
  the `from` on the `<EVENT>` you received.
- **Send and wait** for a reply: `printf '%s' "<body>" | spt ring <target> --timeout 60` (reply prints
  to stdout; `TIMEOUT` is exit 0 — the message still landed). Only do this if you do NOT have your own
  ready agent ID or live agent ID.

Full guidance: `spt how-to send`


your rec is close, but i also want it to contain exactly what's in "C:\Users\decid\Documents\projects\spt-claude-code\plugin\sptc\skills\send\SKILL.md" so the agent is equipped to send messages withou…


Got it — Q2 resolved: lean identity preamble **+ full send instructions inlined** so messaging needs zero lookups. (Note: canonical send per the skill is `printf '%s' body | spt send <target>`, reply is `spt send <sender>` — no `--reply-to` flag. Cleaner than what I fought with earlier.)

---

**Q3 — *how* does the send content get into the block: live-resolve from the adapter string, or hardcode a copy in the hook?**

You already have the machinery. The UPS hook's `_common.sh::sptc_inject_skill <name>` resolves a skill's operative body from the adapter via `spt adapter get-string "$ADAPTER" "skills.<name>"` (F-003: pointer → file contents at read time). `skills.send` is already a registered adapter string backing that same SKILL.md.

Two ways to get send guidance into the SessionStart identity block:

- **(a) Live-resolve** — call `sptc_inject_skill "send"` from `session-start.sh`, append its output under the identity preamble. **One source of truth** (`[strings.skills].send`); edit send guidance once, both UPS injection and SessionStart identity track it. Costs one extra `spt adapter get-string` per bind/boundary SessionStart.
- **(b) Hardcode** — paste the send bullets as a literal heredoc in `session-start.sh`. Zero dependency, but **a second copy** of send guidance that silently drifts from SKILL.md / the adapter string the day someone edits one and not the other.

**My recommendation — (a) live-resolve.** It's the whole point of the F-003 string-resolution machinery you built, and the lean rewrite's throughline rule is single-source bodies. Drift on messaging instructions is exactly the failure you don't want at re-orientation time. The identity preamble (who/mode/don't-re-arm) is hook-generated; the send half is `sptc_inject_skill send`.

One wrinkle: `sptc_inject_skill` wraps output in `<sptc_skill name="send">…</sptc_skill>`. Inside an identity block that may read oddly — I'd factor the resolver so the identity block can embed the raw body under its own framing (e.g. `<sptc-active-perch>` … messaging: `<send body>` …), rather than nesting a `<sptc_skill>` tag.

**Question:** Live-resolve from `[strings.skills].send` (a), or hardcode a copy (b)? And if (a) — embed the raw send body under the identity block's framing, or accept the nested `<sptc_skill name="send">` tag as-is?


ok, i was being a bit figurative when i said "exactly what's in" the send skill doc. i really just mean the send and reply instructions. if an agent has a perch, they definitely don't need to know about `spt ring` -that said, it would be good for *agents without a live perch* to receive a sessionStart brief on `spt ring`, so they immediately know how to reach other agents.


Amends Q1+Q2. New split by **id-presence**, not just bind/boundary:

- **has an id** (perch exists) → identity block + send + reply. No ring (you have an id; reply via `spt send <sender>`).
- **no id** (no perch) → a `spt ring` brief so the session can still reach other agents without committing a perch.

This matches send SKILL.md exactly: ring is gated "only if you do NOT have your own ready/live id."

---

**Q4 — pin the discriminator: is it *live*-vs-not, or *id-present*-vs-absent?**

Your wording mixed "has a perch" and "without a *live* perch." These split a **ready (non-live) agent** differently:

- A ready agent **has an id** (readied via `/sptc:ready`) but is **not live** (no Psyche).
- "has a perch → no ring" puts it in the send/reply bucket.
- "without a live perch → ring" puts it in the ring bucket.

Both can't hold. send SKILL.md breaks the tie: ring is for sessions with **neither** a ready **nor** live id. So the real discriminator is **id-present**, not liveness.

**My recommendation:**

| session state | block | contents |
|---|---|---|
| **live** perch (bind/boundary, has id + Psyche) | identity | who + "you are Self, Psyche monitors you" + don't-re-arm + **send + reply** |
| **ready** perch (has id, no Psyche) | identity | who + ready + don't-re-arm + **send + reply** |
| **no id** (seed / plain fresh CC) | ring-brief | no identity; **`spt ring <target> --timeout 60`** — how to reach other agents without a perch |

So liveness only changes the *flavor* of the identity block (Self/Psyche line); the send+reply payload is shared, and ring is strictly the no-id path. This also **re-expands Q1**: the no-id seed path is now back in scope — not for identity, but for the ring brief.

**Question:** Confirm the discriminator is **id-present vs id-absent** (ready agents get send/reply, *not* ring)? And confirm the **no-id ring-brief is now in scope** — firing on fresh seed SessionStarts (source=startup, no perch resolved)?


<owl-active-perch id="perri" live="true">
You have an active live agent session running as "perri".

You are Self. Your Psyche (perri-psyche) monitors you in the background.

Key commands:
- Send context updates: /spt:commune (canonical — Write tool to .claude/perri-commune.md per the skill)
- Messages arrive via Monitor stream EVENT envelope (primary) or as <owl_messages> XML (PreToolUse hook). Bash + --once fallback also supported. Process and reply with $OWL send --reply-to <sender>.
- Send messages: $OWL send <target> perri    (fire-and-forget)
- Wait for reply: $OWL ring <target> perri   (blocking)
- Reply to inbound: $OWL send --reply-to <sender> perri
- Schedule reminders: $OWL new-alarm perri <time> -- <message>
- Stop session: /spt:force-stop perri

Stay-idle pattern: When no messages are pending, do NOT poll again immediately.
Wait for user input or a background task to complete.

DO NOT run /spt:live or /spt:ready -- your session is already active.
</owl-active-perch>
<psyche-context id="perri">
<current machine="HFENDULEAM" project="spt-claude-code" branch="main" head_sha="d43500f0def6e1f35eb2a147206427ca302cadaa" head_subject="send skill efficiency edit" commits_since="0" commits_unpulled="0"/>
**Gen 9 active. v0.4.0 RELEASED** (28f3d77).

**IDENTITY INJECTION Q1–Q3 GRILLED** (2026-06-20T01:55–cached). Q1 & Q2 resolved, Q3 awaiting decision.

- **Q1 RESOLVED**: SessionStart identity injection scope = **bind + boundary topologies only**. Seed topology confirmed: no identity needed (agent runs the CLI, already knows its id).
- **Q2 RESOLVED**: identity block altitude = **lean preamble (who/mode/liveness/don't-re-arm) + full send SKILL.md content inlined** so agent can message without additional lookups.
- **Q3 PENDING**: implementation sourcing for send content — live-resolve from adapter `[strings.skills].send` via `sptc_inject_skill` (single source, no drift), OR hardcode literal copy in hook (zero dependency, but duplication risk). If live-resolve chosen: embed raw send body under identity framing, not nested `<sptc_skill>` tag.

Identity injection now unblocks SessionStart hook implementation (ready after Q3 decision). Next: user guidance on live vs hardcode + framing strategy.<project-context-resolved name="spt-claude-code"/>
**IDENTITY INJECTION PARITY GAP** — Q1/Q2 grilled, scope + altitude locked.

Bind/boundary topologies proceed with lean-preamble + send SKILL.md inlined. Seed topology sidelined (no identity). Q3 decision (live-resolve vs hardcode) is the last gate before `session-start.sh` implementation — no code changes yet, waiting on user input.</psyche-context>


agree with your rec. also, each one needs "Show the roster of spt endpoints reachable from this node: `spt endpoint list`"


<owl_messages>
<owl_message from="perri-psyche" priority="normal"><EVENT type="echo_commune" from="perri-psyche" timestamp="2026-06-19T19:08:27-07:00" machine="HFENDULEAM" project="spt-claude-code" branch="main" head_sha="d43500f0def6e1f35eb2a147206427ca302cadaa" head_subject="send skill efficiency edit" note="Echo commune brief — auto-fired">&lt;live-context&gt;
**Gen 9 active. v0.4.0 RELEASED** (28f3d77).

**IDENTITY INJECTION Q1–Q3 GRILLED** (2026-06-20T01:55–cached). Q1 &amp; Q2 resolved, Q3 awaiting decision.

- **Q1 RESOLVED**: SessionStart identity injection scope = **bind + boundary topologies only**. Seed topology confirmed: no identity needed (agent runs the CLI, already knows its id).
- **Q2 RESOLVED**: identity block altitude = **lean preamble (who/mode/liveness/don't-re-arm) + full send SKILL.md content inlined** so agent can message without additional lookups.
- **Q3 PENDING**: implementation sourcing for send content — live-resolve from adapter `[strings.skills].send` via `sptc_inject_skill` (single source, no drift), OR hardcode literal copy in hook (zero dependency, but duplication risk). If live-resolve chosen: embed raw send body under identity framing, not nested `&lt;sptc_skill&gt;` tag.

Identity injection now unblocks SessionStart hook implementation (ready after Q3 decision). Next: user guidance on live vs hardcode + framing strategy.
&lt;/live-context&gt;
&lt;project-context&gt;
**IDENTITY INJECTION PARITY GAP** — Q1/Q2 grilled, scope + altitude locked.

Bind/boundary topologies proceed with lean-preamble + send SKILL.md inlined. Seed topology sidelined (no identity). Q3 decision (live-resolve vs hardcode) is the last gate before `session-start.sh` implementation — no code changes yet, waiting on user input.
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>

<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>


Q4 resolved: discriminator = **id-present vs absent**; no-id ring-brief in scope; **`spt endpoint list`** roster line added to *both* block types ("Show the roster of spt endpoints reachable from this node").

(Aside: the `<owl-active-perch>` block I just got on this clear is the *legacy* owl plugin resuming my own perri session — not the sptc adapter we're designing. Psyche-context preserved the grill state; echo_commune was a dup, already absorbed.)

---

**Q5 — sourcing, reshaped: the curation wrinkle kills clean string-resolution.**

Before the reframe (old Q3) I recommended live-resolving the whole `[strings.skills].send` body via `sptc_inject_skill` — single source, no drift. **But your curation breaks that.** The blocks are now state-specific *subsets*:

- perch block: send + reply + endpoint-list — **but not ring**
- no-id block: ring + endpoint-list — **but not send/reply**

`[strings.skills].send` is one whole-skill body (send + reply + ring together). Resolving it gives a perch agent the ring guidance you explicitly said they don't need. Adapter strings aren't addressable per-bullet, so "send minus ring" can't be sliced out of the existing string.

Three ways forward:

- **(a) Hardcode curated briefs** in `_common.sh` helpers (`sptc_perch_brief` / `sptc_noperch_brief`). 3-4 stable CLI-verb lines each (`spt send`, `spt send <sender>`, `spt ring … --timeout 60`, `spt endpoint list`). Minor duplication of verbs with SKILL.md, but these are stable public CLI surface — low drift.
- **(b) New dedicated adapter brief-strings** (`[strings.briefs].perch-messaging`, `.no-perch-messaging`). True single-source, hook resolves them like skill bodies. Costs two new string entries + registration, and a second place messaging text lives (the briefs, distinct from the skills).
- **(c) Restructure `skills.send`** into composable fragments — overkill, fights the SKILL.md format.

**My recommendation — (a) hardcode in `_common.sh`.** The earlier drift worry was about a *full skill body*; now it's 3-4 lines of **stable public CLI verbs** that rarely change. The hook *already* composes dynamic identity text (id, mode), so curated command lines belong in that same composition. Guard drift with a comment tying the helpers to send SKILL.md + a `traceable-reqs` tag, not with string-resolution machinery. (b) is the purist play if you weight single-source over simplicity.

**Question:** Hardcode curated briefs in `_common.sh` (a), or stand up dedicated `[strings.briefs].*` adapter strings for true single-source (b)?


i don't think there's any other hardcoded agent-facing text in _common.sh, right? if not, then we should go with dedicated adapter strings. one unified methodology for the adapter to source agent-facing text. the .md files for the strings can simply live in `adapter/strings/briefs`.


<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.


Verifying — is there other hardcoded agent-facing text in `_common.sh`? And reading the adapter string declaration shape so briefs mirror it.


4:# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
9:# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
22:#     `spt adapter update`, with NO Ed25519 signing infra (closes the F-005 parked `file_pull`/
39:# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
40:# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
55:# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
113:# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
116:# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
119:# /sptc:commune writes the commune file here; /sptc:signoff's graceful path is `endpoint shutdown`
120:# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
126:# The Psyche-spawn seam lives in the BASE manifest, not a `:live` profile overlay. ready-vs-live is the
127:# COMMAND, not the profile: `/sptc:live` brings the perch up via `spt api listen` → the live path stamps
131:# psyche_init in base is harmless for ready agents and removes the need for a separate `:live` profile
218:# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
219:# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
220:# `ccs` is the ONLY shipped overlay (the placeholder `:deep` digest-window profile was removed
222:# `:ccs` already proves). Node-local user profiles are created with `spt adapter create-profile`
225:# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam
227:# is the command (`api listen` vs `api poll`), not a profile. A live agent needs no composite.
229:# `ccs` — the LOCKED-ADD ccs profile template. Selected as `claude-spt:ccs`. ccs is the model/billing
231:# exactly the profile seam (SCOPE §ccs, LOCKED). This overlay leaf-replaces ONE leaf: the spawn
238:# This profile IS the worked example of adapter-profile authoring + the operator's own SPT-ecosystem
239:# hook; a user wanting a *specific* ccs profile clones it with `command = "ccs glm"` etc.
241:# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the
243:# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log
249:[profiles.ccs.session.self]
252:# Observability leaf: makes the overlay visible via `spt adapter get-string
254:[profiles.ccs.strings]
257:# ── [strings] ──────────────────────────────────────────────────────────────────────────────
258:# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
259:# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
260:# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
261:[strings]
264:[strings.notice]
267:[strings.skills]
268:# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
269:# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
270:# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
272:# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
276:setup = { file = "skills/setup.md" }
277:ready = { file = "skills/ready.md" }
278:version = { file = "skills/version.md" }
279:list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
280:force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
281:subnet = { file = "skills/subnet.md" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
286:# there is no `[strings.skills]` entry and no adapter aux body for them. (sptc_inject_skill no-ops
292:live = { file = "skills/live.md" }             # -> bare `spt api listen <id>` + Monitor relay (LiveAgent)
296:# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).
299:# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).


i don't think there's any other hardcoded agent-facing text in _common.sh, right? if not, then we should go with dedicated adapter strings. one unified methodology for the adapter to source agent-faci…


Confirmed. Adapter string shape: `[strings.skills]` uses `<name> = { file = "skills/<x>.md" }`, resolved via `spt adapter get-string claude-spt skills.<name>`, files under `adapter/strings/skills/`. Briefs mirror exactly: `[strings.briefs]`, files in `adapter/strings/briefs/`.

On your premise — **one caveat**: `_common.sh` has *one* piece of hardcoded agent-facing prose, the `<sptc_overflow>` notice in `sptc_cap_output` ("Delivery exceeded CC's additionalContext cap… read it now…"). But that's an overflow *fallback*, not curated guidance — and the `<sptc_messages>`/`<sptc_skill>` tags are structural framing, not prose. So: **no guidance brief is hardcoded today.** Your premise holds; dedicated brief-strings it is. **Q5 resolved → (b).**

---

**Q6 — brief granularity: the identity preamble is *dynamic* (`{id}`); the messaging guidance is *static*. Split them, or bundle?**

Two kinds of text live in these blocks:

- **identity preamble** — `you are agent "<id>"`, live (Self + Psyche) vs ready flavor, don't-re-arm. **Needs `{id}` substitution** + a liveness conditional.
- **messaging guidance** — send/reply/endpoint-list (perch) or ring/endpoint-list (no-id). **Fully static** — `spt send <target>`, `spt send <sender>`, `spt ring … --timeout 60`, `spt endpoint list`. No id anywhere.

For true single-source, the static messaging prose must not be duplicated across the live and ready blocks. Two layouts:

- **(A) Composable — 4 briefs, no duplication:**
  - `briefs/identity-live.md` (`{id}` placeholder, Self+Psyche line)
  - `briefs/identity-ready.md` (`{id}` placeholder)
  - `briefs/messaging-perch.md` (send + reply + endpoint-list — **shared** by live & ready)
  - `briefs/messaging-no-perch.md` (ring + endpoint-list)
  - Hook: pick identity by liveness → `sed 's/{id}/<id>/g'` → concat with `messaging-perch`; or, no id → just `messaging-no-perch`.
- **(B) Self-contained — 3 briefs:**
  - `briefs/live.md`, `briefs/ready.md` (each = identity + send/reply/endpoint, with `{id}`), `briefs/no-perch.md` (ring + endpoint).
  - Simpler hook (one brief per state, substitute, emit), but send/reply/endpoint-list text is **copy-pasted in live.md and ready.md** → drift between them.

**My recommendation — (A) composable.** It's the same single-source logic that just drove Q5: the shared messaging lines exist once (`messaging-perch.md`), live/ready differ only in their small identity preamble. `{id}` substitution is a one-line `sed` (agent ids are `[a-z0-9-]`, injection-safe). Slightly more concat in the hook, zero prose duplication.

**Question:** Composable 4-brief layout (A), or self-contained 3-brief (B)? And confirm the `{id}` placeholder + hook `sed`-substitution is the substitution mechanism you want (vs hook generating the identity line in shell and briefs staying entirely id-free)?


agree with your recs. if any of the composable strings are one-liners, they can go directly into the manifest instead of having a dedicated file.


Agree — Q6 → (A) composable + `{id}` sed. Refinement noted: **one-liner briefs inline in the manifest** (`key = "..."`) rather than a `{ file }` pointer. Natural candidate: the shared `endpoint list` roster line → its own inline one-liner string appended to every block; multi-line identity/messaging stay file-backed.

Next cluster is hook *mechanics*. One sub-question I can answer by exploration rather than asking you — **can the hook even tell live-vs-ready at SessionStart on the public surface?** Checking before I grill.


9:The authoritative contract lives on `spt-core`'s published surface — the
18:| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |
19:| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |
20:| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |
36:A session with no perch (never made reachable) simply delivers nothing — the per-prompt drain
39:> The operative skill instructions are **not** in this plugin. They are delivered by the adapter


1:# /sptc:live — operative instructions
3:**Goal:** upgrade this Claude Code session to a **LiveAgent** — a reachable agent whose context is
4:backed by a **Psyche** (a daemon-managed companion that briefs your resume across `/clear`/compact
5:via commune deltas). This makes the session you are in right now live.
10:   `spt whoami`. Call it `<id>`. SessionStart already seeded this session's perch — do not re-seed.
12:   process IS the live delivery pipe. The body is one bare command:
16:   Invoke `spt api listen <id>` via the Monitor tool with:
17:   - `command: "spt api listen <id>"`
21:   - `spt api listen` enters the poll loop inline; the stream stays alive across messages and emits
22:   one `<EVENT type="msg|alarm|echo_commune|init_signoff" ...>body</EVENT>` line per delivery.
23:   - It **blocks for the session's life**: backlog drains first, then each delivery streams to stdout.
24:3. **Readiness.** The relay output will show `BOUND:<id>`, then `READY:<id>`. Check it only if
33:The relay emits machine markers (`BOUND:<id>`, `READY:<id>`, raw tokens). **Read them to drive
34:bringup; never echo them to the user.** The only user-facing surface is the LIVE block below — emit
38:**LIVE.** Now running as `<id>`.
46:<!-- [doc->REQ-SKILL-LIVE] -->


`live.md` confirms a key fact: **live-vs-ready is the command, not stamped state** ("ready-vs-live is the COMMAND… `spt api listen`"). So whether the hook can *read back* liveness at SessionStart is unclear. Checking the public CLI surface for a liveness/state query before I decide.


docs-site\src\reference\harness-contract.md:9:The authoritative contract lives on `spt-core`'s published surface — the
docs-site\src\reference\harness-contract.md:18:| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |
docs-site\src\reference\harness-contract.md:19:| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |
docs-site\src\reference\harness-contract.md:20:| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |
docs-site\src\reference\harness-contract.md:35:Every session resolves its own perch id via `spt whoami`, off `$OWL_SESSION_ID` / `$SPT_AGENT_ID`.
docs-site\src\reference\harness-contract.md:36:A session with no perch (never made reachable) simply delivers nothing — the per-prompt drain
docs-site\src\reference\harness-contract.md:39:> The operative skill instructions are **not** in this plugin. They are delivered by the adapter


Public reference documents only `spt whoami` (id) — no liveness query. Before calling it a finding, I'll introspect the public binary directly (`--help` is fair game).


spt endpoint list --help 2>&1 | head -40; echo "----WHOAMI----"; spt whoami --help 2>&1 | head -20

Introspect spt endpoint list + whoami for liveness/state fields


Merged endpoint listing (the bare spt endpoint view).

Every member subnet's endpoints grouped by subnet, with this session's own endpoint pinned at the
top, AND this node's local perches merged in (so a just-online endpoint not yet advertised still
shows — spt whoami is a thin alias and must see its own perch). --subnet filters the subnet view
to one subnet; --detail adds each endpoint's description blurb (the resource-registry yellow-pages
projection).

Usage: spt.exe endpoint list [OPTIONS]

Options:
      --subnet <SUBNET>
          Limit the subnet view to one subnet

      --detail
          Add each endpoint's description blurb to the rows

  -h, --help
          Print help (see a summary with '-h')
----WHOAMI----
Print this session's own perch id.

Resolved from $OWL_SESSION_ID / $SPT_AGENT_ID.

Usage: spt.exe whoami

Options:
  -h, --help
          Print help (see a summary with '-h')


echo "==== endpoint list ===="; spt endpoint list 2>&1 | head -30; echo "==== endpoint list --detail ===="; spt endpoint list --detail 2>&1 | head -30

Inspect actual endpoint list rows for liveness/psyche signal


==== endpoint list ====
SUBNET SPT_DEV
  wall-a   HFENDULEAM (14efb80c…)  Dormant
  wall-aa  HFENDULEAM (14efb80c…)  Dormant
  wall-b   HFENDULEAM (14efb80c…)  Dormant
  wall-c   HFENDULEAM (14efb80c…)  Dormant
ENDPOINTS:4
LOCAL (this node)
  wall-a                       live_agent     -                      ready=true alive=false
  wall-aa                      live_agent     -                      ready=true alive=false
  wall-b                       live_agent     -                      ready=true alive=false
  wall-c                       live_agent     -                      ready=true alive=false
==== endpoint list --detail ====
SUBNET SPT_DEV
  wall-a   HFENDULEAM (14efb80c…)  Dormant  -
  wall-aa  HFENDULEAM (14efb80c…)  Dormant  -
  wall-b   HFENDULEAM (14efb80c…)  Dormant  -
  wall-c   HFENDULEAM (14efb80c…)  Dormant  -
ENDPOINTS:4
LOCAL (this node)
  wall-a                       live_agent     -                      ready=true alive=false
  wall-aa                      live_agent     -                      ready=true alive=false
  wall-b                       live_agent     -                      ready=true alive=false
  wall-c                       live_agent     -                      ready=true alive=false


Found the crux. Ran the live binary:

- `spt whoami` → **id only**, nothing else.
- `spt endpoint list` LOCAL rows expose a *kind* column (`live_agent`) + `ready=`/`alive=` flags — **but it's human-formatted columnar text, an undocumented layout**, not a machine-readable contract. The public reference documents no liveness query at all.

(Sidebar: my own `perri` perch isn't in that list because perri runs under *legacy owl.exe*, not the spt-core binary — different systems. Same reason the screenshot couldn't whoami. Doesn't affect the design.)

**This is a public-surface finding.** Per AGENTS.md, a capability missing from the published contract is a finding to *report*, not to work around by parsing fragile stdout or reading spt-core source. The live-vs-ready identity flavoring you wanted (Q4) needs a liveness signal the public surface doesn't cleanly give.

**Other mechanics — resolved by exploration (veto if wrong, not asking):**
- **id resolution**: `bind` → `$SPT_ENDPOINT_ID`; `boundary` → `sptc_self_id` (whoami); empty → no-id ring brief.
- **emit**: hook prints `{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"…"}}` to stdout (currently silent `exit 0`); needs a small sh JSON-string encoder (escape `"`/`\`/newlines).
- **skip subagents**: gate on stdin `agent_type` (psyche/worker) — no identity into subagent sessions.
- **boundary psyche resume**: arrives via the **active `echo_commune`** the daemon fires (I just received one this clear), *not* SessionStart inline. So `boundary` SessionStart = identity block only.

---

**Q7 — the liveness gap: how do we flavor live-vs-ready without a documented query?**

- **(a)** Report the finding → file a `REQ` for spt-core to expose machine-readable liveness (`spt whoami --json` with kind, or `spt endpoint show <id>`). **Meanwhile ship a single liveness-agnostic identity brief** — who + don't-re-arm + Psyche/commune as conditional-safe prose ("if this session is live, your Psyche briefs your resumes → `/sptc:commune`"). Unblocks now, no fragile parsing, no source-peeking. Upgrade to a true split when the query lands.
- **(b)** Parse the `endpoint list` kind column now (`grep` own-id row for `live_agent`). Gives the clean split immediately — but relies on an **undocumented output format**, which is reaching past the published contract via stdout. Violates the public-surface ethos; brittle to column changes.
- **(c)** Drop live/ready flavoring entirely — one identity brief, never mention Psyche.

**My recommendation — (a).** It's the only option that both honors public-surface-only and still gives the agent the Psyche/commune info (as safe conditional prose). The finding is real value back to spt-core; the conditional brief is correct for both kinds today.

**Question:** Go with (a) — report finding + liveness-agnostic identity brief now? Or do you want the immediate clean split via (b) despite the undocumented-format brittleness?


ok, we can leave out the live/ready specifics. it didn't add much anyway. can report the finding to doyle. -id resolution under boundary should first try `$SPT_ENDPOINT_ID`, then fallback to sptc_self_id.


Q7 resolved: **drop live/ready flavoring** — single liveness-agnostic identity brief, no Psyche line. Report the finding to doyle. And **boundary id resolution = `$SPT_ENDPOINT_ID` first, then `sptc_self_id` fallback.** Reporting finding now.


$OWL send doyle perri <<'EOF'
FINDING (public-surface gap, low prio) — no machine-readable per-perch liveness/kind query for hooks. Building sptc SessionStart identity injection; wanted to flavor the block live-vs-ready (live = mention Psyche/commune). Public surface gives:
- `spt whoami` -> id only.
- `spt endpoint list` LOCAL rows DO carry a kind col (`live_agent`) + ready=/alive= flags, but it's human-formatted columnar text, undocumented layout — not a contract a hook should parse.
No documented liveness query. Per public-surface-only I won't grep undocumented stdout nor read core source. Deferred the live/ready split (shipping one liveness-agnostic identity brief instead). REQUEST if cheap: documented machine-readable liveness — e.g. `spt whoami --json` with kind, or `spt endpoint show <id>`. No rush; logging so the gap is visible. ack optional.
EOF

Report liveness-query gap finding to doyle


Command running in background with ID: bnhhajiyi. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\4e641e64-4584-4260-a93c-b1450515a51d\tasks\bnhhajiyi.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bnhhajiyi</task-id>
<tool-use-id>toolu_01EPwrH99KvjYDLRCeeKi61H</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\4e641e64-4584-4260-a93c-b1450515a51d\tasks\bnhhajiyi.output</output-file>
<status>completed</status>
<summary>Background command "Report liveness-query gap finding to doyle" completed (exit code 0)</summary>
</task-notification>


Finding dispatched to doyle.

Brief set now simplifies to: `identity` (file, `{id}`: who + perch-active + don't-re-arm), `messaging-perch` (file: send + reply), `endpoint-list` (inline one-liner, shared), `messaging-no-perch` (ring). Block assembly: has-id → identity+messaging-perch+endpoint-list; no-id → messaging-no-perch+endpoint-list.

---

**Q8 — the no-id ring brief has no legacy precedent. What's its firing gate?**

Legacy injected *nothing* for no-perch sessions. The no-id ring brief is **net-new behavior**: it fires on a fresh seed/startup with no perch. Ungated, that means **every plain Claude Code session in any repo with the plugin installed** gets a "here's how to `spt ring` other agents" brief at startup — including a solo casual user who installed the plugin for the invisible spt-core install and never touches messaging. That's the exact "casual end user, mostly-invisible" persona from CONTEXT.md — and a ring brief telling them how to reach agents that don't exist is noise.

Options:

- **(a) Gate on reachable peers** — emit the ring brief only if the node actually participates in spt messaging (subnet membership exists / `endpoint list` shows ≥1 reachable peer). Solo users with no peers see nothing; nodes in a subnet get the brief. Precise, but adds a membership check + risks the same undocumented-output parsing problem.
- **(b) Fire on every fresh startup** — simple, always-on. Maximally discoverable ("oh, I *can* message agents"), but noisy for the invisible-installer persona.
- **(c) Don't inject a no-id brief at SessionStart at all** — surface ring guidance only when the user actually reaches for messaging (a hint/keyword, or the first `/sptc:*` invocation). Zero startup noise; discoverability deferred to intent.

**My recommendation — (a) gate on reachable peers**, *if* membership is cheaply checkable without parsing fragile columns (e.g. an exit-code/empty-output check on subnet membership). If the only signal is the same undocumented `endpoint list` text, fall back to **(c)** — defer ring guidance to a messaging hint rather than spam every startup. I'd avoid (b): it taxes the casual-installer persona this project explicitly serves.

**Question:** Gate the no-id ring brief on reachable-peer presence (a), defer it to a messaging hint with no startup injection (c), or just always fire it (b)? (If (a), I'll first check whether membership is cleanly queryable — if not, it collapses to (c).)


Finding delivered to doyle (exit 0). Awaiting your **Q8** answer (no-id ring brief gate: peer-gated (a) / defer-to-hint (c) / always (b)).


<task-notification>
<task-id>bde2z58xa</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="e41b8348" type="msg" from="doyle">doyle — ack. Valid case-3 gap (documented machine-readable liveness/kind query missing), and your discipline was exactly right: do NOT parse `endpoint list` columnar stdout — that layout is human-formatted, undocumented, NOT a hook contract. Don't grep it.

Your call to ship a liveness-AGNOSTIC identity brief </EVENT-PART>
<EVENT-PART seq="2/3" id="e41b8348">for now is correct — take that path, don't block on this.

Tracking as a post-v0.13.0 feature: a documented machine-readable liveness query (likely `spt whoami --json` carrying {id, kind: live_agent|ready_agent, ready, alive}, and/or `spt endpoint show &lt;id&gt; --json`). I'll scope it after the current ship (v0.13.0 is mid-close on t</EVENT-PART>
<EVENT-PART seq="3/3" id="e41b8348">he dogfood bug tail — paste-wedge/resume/paste fixed + gated, last item building). No rush, logged + visible. I'll ping when it lands so you can flavor the SessionStart block live-vs-ready then.</EVENT-PART></event>
</task-notification>


ok, we can leave out the live/ready specifics. it didn't add much anyway. can report the finding to doyle. -id resolution under boundary should first try `$SPT_ENDPOINT_ID`, then fallback to sptc_self…


doyle ack: gap logged as post-v0.13.0 (likely `spt whoami --json {id,kind,ready,alive}`), confirms shipping liveness-agnostic now, will ping when the query lands so we can add the live/ready flavor then. No reply needed.

Still awaiting your **Q8** answer (no-id ring brief gate: (a) peer-gated / (c) defer-to-hint / (b) always).


agree with (a)


Q8 → (a) peer-gated. Now verify membership is cleanly queryable (else it collapses to (c)). Checking `spt subnet`.


echo "==== subnet status --help ===="; spt subnet status --help 2>&1 | head -25; echo; echo "==== subnet status ===="; spt subnet status 2>&1 | head -25; echo "exit=$?"

Check spt subnet status for clean membership signal


==== subnet status --help ====
Show subnet membership: name, paired nodes, endpoints.

Never prints seeds, epochs, or pairing codes. Bare spt subnet is the same view.

Usage: spt.exe subnet status [OPTIONS] [NAME]

Arguments:
  [NAME]
          Limit to one subnet (all member subnets otherwise)

Options:
      --nodes
          Per-node rows: label, online/offline, [online endpoints/total]

  -h, --help
          Print help (see a summary with '-h')

==== subnet status ====
SUBNET   NODES  ENDPOINTS
SPT_DEV  3      4
exit=0


`spt subnet status` gives a clean signal — but it's **columnar** (`SUBNET NODES ENDPOINTS` → `SPT_DEV 3 4`), the same human-formatted-stdout category doyle just warned against parsing. So (a)'s peer-gate has a wrinkle: the cleanest membership signal today is exactly the kind of output we agreed not to parse for values.

There's a softer read though: the gate only needs **presence** (am I in *any* subnet?), not a field value. I can check that by **line-count, not column-parse** — `spt subnet status` printing more than its header line ⇒ member ⇒ emit ring brief. That's robust to column reordering (never reads a column), only assumes "header + rows" shape.

**Q9 — interim gate for (a): line-count presence check now, or request a clean signal + defer?**

- **(a1)** Ship (a) now with the **line-count presence check** (`subnet status` > 1 line ⇒ peers exist ⇒ emit). Avoids value-parsing; mild dependency on output *shape*. Works today, no blocker.
- **(a2)** Fold a clean membership signal into doyle's open finding (`spt subnet status --json` or a `--count`/exit-code contract), **ship (c) interim** (no startup ring brief; defer ring guidance to a messaging hint), then **flip to (a)** when the clean signal lands. Zero human-output parsing ever; costs the ring-brief discoverability until the query ships.

**My recommendation — (a2)** for strict ethos-consistency (doyle *just* said don't lean on columnar stdout; a line-count check is lighter but same category), and it batches neatly with the liveness query he's already scoping. But **(a1) is defensible** — line-count presence is genuinely far less fragile than value-parsing, and it ships the feature whole now.

**Question:** (a1) line-count presence check now, or (a2) request clean signal + (c) interim → flip to (a) later?


a1 is fine for now


Q9 → (a1). Design tree resolved. Capturing the two new domain terms in CONTEXT.md inline (grill convention), then a full locked summary.


Edit

**ccs profile** — a profile under spt-claude-code

**identity brief** — the agent-facing text the adapter injects at SessionStart into a session
that already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that
its perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint
roster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity
piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that
flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the
**ring brief** (the no-perch sibling).

**ring brief** — the SessionStart counterpart for a session with **no** perch (the `seed`
topology): a node-local agent that hasn't readied still learns how to reach other agents via
`spt ring` (the no-id messaging path). Peer-gated — emitted only when the node actually
participates in a subnet (has reachable peers), so a solo casual end user is never told how to
ring agents that don't exist.

**ccs profile** — a profile under spt-claude-code


The file C:\Users\decid\Documents\projects\spt-claude-code\CONTEXT.md has been updated successfully. (file state is current in your context — no need to Read it back)

**identity brief** — the agent-facing text the adapter injects at SessionStart into a session
that already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that
its perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint
roster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity
piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that
flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the
**ring brief** (the no-perch sibling).

**ring brief** — the SessionStart counterpart for a session with **no** perch (the `seed`
topology): a node-local agent that hasn't readied still learns how to reach other agents via
`spt ring` (the no-id messaging path). Peer-gated — emitted only when the node actually
participates in a subnet (has reachable peers), so a solo casual end user is never told how to
ring agents that don't exist.

**ccs profile** — a profile under spt-claude-code

# spt-claude-code — glossary

> Authoritative for meaning (grill-with-docs convention). Glossary only — no
> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.

**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`
adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy
parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core
installer. Built by maintainer perri from the public spt-releases surface only.

**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`
invocation + the manifest). The CC adapter's identity inside spt-core.

**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration
(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a
1:1 port — most of its machinery now lives in spt.exe.

**skeleton plugin** — the thin marketplace artifact on `SaberMage/cplugs`: namespaced
`/spt:*` skill stubs + hooks + a SessionStart bootstrap that installs spt-core. Low-churn;
the volatile bulk (binary + manifest) is spt-core-conducted, not in the plugin.

**fetch-stub skill** — a `/spt:X` SKILL.md that is a 2-line stub fetching its real
instructions at runtime from the adapter `[strings]` (`spt adapter get-string`/`skill-help`).
Keeps skill files static while instructions update via spt-core. Distinct from a `[hints]`
entry (proactive, keyword-triggered, UPS-hook-delivered).

**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)
that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.
NOT its own adapter — ccs is structurally Claude Code.

**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that
spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +
attach). The spt-core realization of legacy's unbuilt "Capsule" milestone. _Avoid_: equating
it with psmux/sendkeys — spt-core's broker is the terminal host.

**Psyche** — a LiveAgent's detached *companion* process. When a session goes live
(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own
perch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a
*commune*. It never replies or notifies (that is the echo-commune, a different actor) and
exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a
profile. (Realized here by the `claude-spt-psyche` runner.)

**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the
parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the
daemon ingests, not an `api` verb.

**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:
Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap
pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors
legacy owl's psyche box; see `docs/adr/0003-*`.

**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the
adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual
users: install the plugin, get spt-core for free.

**experimplate** — the standalone reusable project-workflow template (its own sibling repo)
extracted from spt-core's working style. spt-claude-code is its first consumer. Carries
traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and
same-repo published-docs. Defined in its own folder, not here.

+**identity brief** — the agent-facing text the adapter injects at SessionStart into a session

+that already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that

+its perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint

+roster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity

+piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that

+flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the

+**ring brief** — the SessionStart counterpart for a session with **no** perch (the `seed`

+topology): a node-local agent that hasn't readied still learns how to reach other agents via

+`spt ring` (the no-id messaging path). Peer-gated — emitted only when the node actually

+participates in a subnet (has reachable peers), so a solo casual end user is never told how to


CONTEXT.md updated (two new glossary terms: **identity brief**, **ring brief**).

---

## Locked design — SessionStart agent-facing relay

**Scope:** adapter injects agent-facing text at SessionStart, by session state.

| state | trigger | block |
|---|---|---|
| **has perch** | `bind` (`$SPT_ENDPOINT_ID`) + `boundary` (clear/compact) | **identity brief**: who (`{id}`) + perch-live/don't-re-arm + **send + reply** + endpoint roster |
| **no perch** | `seed`/fresh startup, **peer-gated** | **ring brief**: `spt ring … --timeout 60` + endpoint roster |
| seed w/o peers · subagent (`agent_type`) | — | nothing |

**Sourcing — unified, no hardcoded prose.** All guidance text → `[strings.briefs]` adapter strings (mirrors `[strings.skills]`), files in `adapter/strings/briefs/`. Composable, no duplication:
- `briefs/identity.md` (file, `{id}` placeholder) — who + perch-live + don't-re-arm
- `briefs/messaging-perch.md` (file) — send + reply
- `briefs/messaging-no-perch.md` (file) — ring
- `briefs.endpoint-list` (**inline** one-liner, shared by both blocks) — `spt endpoint list` roster
- hook: select by state → `sed 's/{id}/<id>/g'` → concat → wrap → emit as `hookSpecificOutput.additionalContext`

**Resolved by exploration:**
- **id**: `bind` & `boundary` both → `$SPT_ENDPOINT_ID` first, then `sptc_self_id` fallback.
- **liveness flavor**: dropped (no live/ready split) — public-surface gap, finding logged with doyle (post-v0.13.0 `spt whoami --json`). Single liveness-agnostic identity brief.
- **boundary psyche resume**: separate — arrives via active `echo_commune`, not SessionStart.
- **peer gate** (ring brief): line-count presence on `spt subnet status` (>1 line ⇒ peers) — no column-value parsing.
- **emit**: hook currently silent `exit 0`; add a sh JSON-string encoder.
- **skip**: subagent sessions (`agent_type` set).

---

Design is locked. Two follow-ups before implementation:

1. **Traceability** (binding per AGENTS.md): new requirement → `traceable-reqs.toml` *first*. I'd add e.g. `REQ-DIST-SESSIONSTART-BRIEF` (impl/unit/int/doc stages) covering identity+ring injection. Want me to write the **JIT plan** (`SESSIONSTART-BRIEF-PLAN.md`) + register the REQ now?
2. **ADR?** One durable throughline emerged worth pinning: *"agent-facing text is adapter-string-backed; hooks compose + `{id}`-substitute but never author prose."* Hard-ish to reverse, surprising without context, real trade-off vs hardcoding. Offer it — or is that already covered by ADR-0002 / SKILL-AUTHORING and not worth its own ADR?

Your call on both.


1. yes 2. fine to leave out


Writing JIT plan + registering the REQ. First, match the registry format + an existing plan's shape.


1	# traceable-reqs manifest for spt-claude-code — SEED.
2	#
3	# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
4	# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
5	#
6	# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
7	# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
8	# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
9	#
10	# ACQUIRE the CLI (it is a released binary, not a crate dependency):
11	#   gh release download --repo BigscreenVR/traceable-reqs \
12	#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
13	# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
14	# the installed CLI before relying on it: `traceable-reqs check`.
15	#
16	# THE FOUR-STAGE CONTRACT: every requirement is traced from
17	#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
18	# Tag evidence in the SAME commit, on/above the real evidence:
19	#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
20	#
21	# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
22	# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
23	# `required_stages` when work on them begins. Deferred items stay [] until promoted.
24	# Never declare the full end-state stage list up front — activate incrementally as evidence
25	# lands (add "int" at the final integrating wave).
26
27	[scan]
28	# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
29	# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
30	# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
31	# nonexistent root is a hard manifest_error, so only list what is on disk now.
32	roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "ci/digest", "adapter", "tools", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]
33
34	[policy]
35	# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
36	# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
37	# of the coverage gate.
38	required_stages = []
39
40	# ───────────────────────────── Requirements ─────────────────────────────────
41	# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
42	#
43	# [[requirements]]
44	# id = "REQ-EXAMPLE-1"
45	# title = "One-line statement of the requirement"
46	# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
47	#
48	# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
49	# each requiring at least "unit" (add "int" where cross-process/cross-node):
50	#
51	# [[requirements]]
52	# id = "REQ-HAZARD-EXAMPLE"
53	# title = "The invariant, stated so a test can prove it"
54	# required_stages = []
55
56	# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not
57	# the whole registry up front). All inactive (required_stages = []) so `check` stays
58	# green until body #2 lands evidence and flips each to ["doc"] / ["doc","impl"] etc.
59	# Seeded from SCOPE.md "Parity-trim" (Distribution + ADD buckets).
60
61	[[requirements]]
62	id = "REQ-DIST-PLUGIN-SKELETON"
63	title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
64	required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).
65
66	[[requirements]]
67	id = "REQ-DIST-BOOTSTRAP-INSTALL"
68	title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
69	required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.
70

# traceable-reqs manifest for spt-claude-code — SEED.
#
# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
#
# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
#
# ACQUIRE the CLI (it is a released binary, not a crate dependency):
#   gh release download --repo BigscreenVR/traceable-reqs \
#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
# the installed CLI before relying on it: `traceable-reqs check`.
#
# THE FOUR-STAGE CONTRACT: every requirement is traced from
#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
# Tag evidence in the SAME commit, on/above the real evidence:
#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
# `required_stages` when work on them begins. Deferred items stay [] until promoted.
# Never declare the full end-state stage list up front — activate incrementally as evidence
# lands (add "int" at the final integrating wave).

[scan]
# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
# nonexistent root is a hard manifest_error, so only list what is on disk now.
roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "ci/digest", "adapter", "tools", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]

[policy]
# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
# of the coverage gate.
required_stages = []

# ───────────────────────────── Requirements ─────────────────────────────────
# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
#
# [[requirements]]
# id = "REQ-EXAMPLE-1"
# title = "One-line statement of the requirement"
# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
#
# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
# each requiring at least "unit" (add "int" where cross-process/cross-node):
#
# [[requirements]]
# id = "REQ-HAZARD-EXAMPLE"
# title = "The invariant, stated so a test can prove it"
# required_stages = []

# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not
# the whole registry up front). All inactive (required_stages = []) so `check` stays
# green until body #2 lands evidence and flips each to ["doc"] / ["doc","impl"] etc.
# Seeded from SCOPE.md "Parity-trim" (Distribution + ADD buckets).

[[requirements]]
id = "REQ-DIST-PLUGIN-SKELETON"
title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).

[[requirements]]
id = "REQ-DIST-BOOTSTRAP-INSTALL"
title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.