1	#!/bin/sh
2	# Stage the sptc cplugs SKELETON into a marketplace clone — the runbook "per skeleton bump" cp
3	# mechanic as a real, idempotent, validated script. DRY-RUN by default: prints the plan and stages
4	# nothing. Never pushes (marketplace credentials + the commit/push are the operator's step).
5	# [impl->REQ-DIST-PLUGIN-SKELETON]
6	#
7	# Usage:
8	#   package-skeleton.sh                 # dry-run against the default $MARKET
9	#   package-skeleton.sh --apply         # actually copy the skeleton subset into $MARKET
10	#   MARKET=/path/to/cplugs/plugins/sptc package-skeleton.sh --apply
11	set -u
12	HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
13	ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
14	PLUGIN="$ROOT/plugin/sptc"
15	MARKET="${MARKET:-$HOME/.claude/plugins/marketplaces/cplugs/plugins/sptc}"
16	APPLY=0
17	[ "${1:-}" = "--apply" ] && APPLY=1
18
19	# Validate first — refuse to stage an invalid / leaky skeleton.
20	echo "== validate skeleton =="
21	if ! sh "$HERE/validate-skeleton.sh" "$PLUGIN"; then
22	  echo "REFUSING to package: skeleton failed validation (fix above, then retry)." >&2
23	  exit 1
24	fi
25
26	# The skeleton SUBSET — exactly what cplugs carries (no binary, no manifest, no runtime state).
27	SUBSET=".claude-plugin/plugin.json hooks skills bootstrap.sh bootstrap.ps1"
28
29	echo
30	echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
31	echo "source : $PLUGIN"
32	echo "target : $MARKET"
33	for item in $SUBSET; do
34	  if [ -e "$PLUGIN/$item" ]; then echo "  stage  $item"; else echo "  WARN   missing source: $item"; fi
35	done
36
37	if [ "$APPLY" -ne 1 ]; then
38	  echo
39	  echo "DRY-RUN: nothing copied. Re-run with --apply to stage into \$MARKET, then commit+push the"
40	  echo "cplugs repo and 'claude plugin install sptc@cplugs' (see docs/RELEASE-RUNBOOK.md)."
41	  exit 0
42	fi
43
44	mkdir -p "$MARKET/.claude-plugin" "$MARKET/hooks" "$MARKET/skills" || { echo "FATAL: cannot create $MARKET"; exit 2; }
45	cp "$PLUGIN/.claude-plugin/plugin.json" "$MARKET/.claude-plugin/"
46	# Replace dir contents wholesale so removed skeleton files don't linger in the marketplace clone.
47	rm -rf "$MARKET/hooks" "$MARKET/skills"
48	cp -r "$PLUGIN/hooks" "$MARKET/hooks"
49	cp -r "$PLUGIN/skills" "$MARKET/skills"
50	for b in bootstrap.sh bootstrap.ps1; do [ -f "$PLUGIN/$b" ] && cp "$PLUGIN/$b" "$MARKET/"; done
51
52	echo
53	echo "STAGED into $MARKET. Next (operator): cd into the cplugs repo, 'git add plugins/sptc/',"
54	echo "commit + push, then 'claude plugin install sptc@cplugs' + /reload-plugins."
55	exit 0
56

#!/bin/sh
# Stage the sptc cplugs SKELETON into a marketplace clone — the runbook "per skeleton bump" cp
# mechanic as a real, idempotent, validated script. DRY-RUN by default: prints the plan and stages
# nothing. Never pushes (marketplace credentials + the commit/push are the operator's step).
# [impl->REQ-DIST-PLUGIN-SKELETON]
#
# Usage:
#   package-skeleton.sh                 # dry-run against the default $MARKET
#   package-skeleton.sh --apply         # actually copy the skeleton subset into $MARKET
#   MARKET=/path/to/cplugs/plugins/sptc package-skeleton.sh --apply
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
PLUGIN="$ROOT/plugin/sptc"
MARKET="${MARKET:-$HOME/.claude/plugins/marketplaces/cplugs/plugins/sptc}"
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# Validate first — refuse to stage an invalid / leaky skeleton.
echo "== validate skeleton =="
if ! sh "$HERE/validate-skeleton.sh" "$PLUGIN"; then
  echo "REFUSING to package: skeleton failed validation (fix above, then retry)." >&2
  exit 1
fi

# The skeleton SUBSET — exactly what cplugs carries (no binary, no manifest, no runtime state).
SUBSET=".claude-plugin/plugin.json hooks skills bootstrap.sh bootstrap.ps1"

echo
echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "source : $PLUGIN"
echo "target : $MARKET"
for item in $SUBSET; do
  if [ -e "$PLUGIN/$item" ]; then echo "  stage  $item"; else echo "  WARN   missing source: $item"; fi
done

if [ "$APPLY" -ne 1 ]; then
  echo
  echo "DRY-RUN: nothing copied. Re-run with --apply to stage into \$MARKET, then commit+push the"
  echo "cplugs repo and 'claude plugin install sptc@cplugs' (see docs/RELEASE-RUNBOOK.md)."
  exit 0
fi

mkdir -p "$MARKET/.claude-plugin" "$MARKET/hooks" "$MARKET/skills" || { echo "FATAL: cannot create $MARKET"; exit 2; }
cp "$PLUGIN/.claude-plugin/plugin.json" "$MARKET/.claude-plugin/"
# Replace dir contents wholesale so removed skeleton files don't linger in the marketplace clone.
rm -rf "$MARKET/hooks" "$MARKET/skills"
cp -r "$PLUGIN/hooks" "$MARKET/hooks"
cp -r "$PLUGIN/skills" "$MARKET/skills"
for b in bootstrap.sh bootstrap.ps1; do [ -f "$PLUGIN/$b" ] && cp "$PLUGIN/$b" "$MARKET/"; done

echo
echo "STAGED into $MARKET. Next (operator): cd into the cplugs repo, 'git add plugins/sptc/',"
echo "commit + push, then 'claude plugin install sptc@cplugs' + /reload-plugins."
exit 0


Neither publish script enforces CHANGELOG — that comes from the RUNBOOK. Read RELEASE-RUNBOOK for CHANGELOG format + version policy.


Bash

cd "C:\Users\decid\Documents\projects\spt-claude-code" && ls docs/ | grep -iE "runbook|release"; echo "==="; grep -niE "changelog|version|0\.0\.1|0\.1\.0|fails? (loud|without)|## \[|distinct" docs/RELEASE-RUNBOOK.md 2>/dev/null | head -40

Find CHANGELOG + version policy in runbook


RELEASE-RUNBOOK.md
===
3:> How an spt-claude-code release ships. The spine is fixed (changelog · bump · regenerate
33:1. **Bump** the version (the plugin `plugin.json` version and/or the adapter manifest version —
34:   note these are distinct numbers; see Notes) if needed; **regenerate generated docs**
37:2. **Write the user-facing changelog** — add a `## [<version>] - <date>` section at the top of
38:   `CHANGELOG.md` with **Added / Changed / Fixed** subsections. This section becomes the public
39:   release body verbatim, so it is the changelog every user reads. Rules:
45:   - The release **fails loudly** if the tagged version has no `## [<version>]` section — the
46:     changelog is not optional.
89:# 1. Bump plugin.json version. The plugin manager compares this to the cached copy and
90:#    SKIPS the update entirely if they match — a stale version = a silent no-update.
119:skip all of it): `owl.exe` binary sync · the cache targeted-prune + keep-PREVIOUS-version logic ·
132:  this is how logic and instruction churn reaches users. Version-of-truth = the
133:  manifest/binary version spt-core tracks (`spt adapter list` / `/spt:version`).
135:  marketplace skeleton on the rare structural bump. It is NOT the version-of-truth.
139:- **Distinct numbers — never conflate them:** the **plugin (marketplace) version** (in
140:  `plugin.json`, bumps rarely) and the **adapter manifest/binary version** (spt-core-tracked,
141:  the user-visible version-of-truth). They move on independent schedules — the plugin can sit
146:  in-place-edit; only old version dirs orphan on update and are GC'd after 7 days.)


33	1. **Bump** the version (the plugin `plugin.json` version and/or the adapter manifest version —
34	   note these are distinct numbers; see Notes) if needed; **regenerate generated docs**
35	   (the mdBook `docs-site/` build + `llms.txt` / schema / CLI-help exports); land everything
36	   green.
37	2. **Write the user-facing changelog** — add a `## [<version>] - <date>` section at the top of
38	   `CHANGELOG.md` with **Added / Changed / Fixed** subsections. This section becomes the public
39	   release body verbatim, so it is the changelog every user reads. Rules:
40	   - **User-facing UX only.** What a person using the product notices or does differently. Name
41	     the actual commands / flags / surfaces they touch (`/spt:*` skills, `cc`, `/spt:setup`).
42	   - **No internal lingo** — no requirement ids, internal module names, commit hashes, or
43	     milestone / hazard codes. A reader who has never seen the source must understand every line.
44	   - **Flag breaking changes** prominently under Changed.
45	   - The release **fails loudly** if the tagged version has no `## [<version>]` section — the
46	     changelog is not optional.
47	3. **Tag**: `git tag <vX.Y.Z> && git push origin <vX.Y.Z>`. This triggers:
48	   - the **docs-publish workflow** — build `docs-site/` and publish to GitHub Pages (drift-gated;
49	     see `docs/DOCS-STRATEGY.md`).
50	4. **Publish** (light — signing delegated to `spt`):
51	   - **Skeleton plugin** → push to the `SaberMage/cplugs` marketplace **only when the plugin
52	     skeleton structurally changed** (new/removed skill skeleton, hook wiring, bootstrap, or
53	     `plugin.json`). Most releases do not bump the marketplace.
54	   - **Manifest + binary** → publish through the **spt-core adapter registry**; the `spt`
55	     binary performs signing/integrity as part of spt-core's adapter-update framework. You do
56	     **not** run a separate signing step.
57
58	## cplugs skeleton publish — concrete mechanics
59
60	<!-- [doc->REQ-DIST-PLUGIN-SKELETON] -->
61	The concrete cplugs marketplace steps, captured from the sister project's
62	`claude_skill_owl/docs/DEPLOY.md`. **Take only the SKELETON SUBSET.** spt-claude-code's cplugs
63	target is the **thin skeleton** — `/spt:*` skill skeletons, `hooks.json`, the SessionStart
64	bootstrap, `plugin.json`. **No binary, no manifest in cplugs** — the binary + adapter manifest
65	ride the **spt-core adapter registry** (spt-conducted; the *other* publish target above), so
66	none of owl's binary-lifecycle machinery applies here.
67
68	> **Now scripted (`ci/publish/`).** The validate + stage mechanics below are codified:
69	> `ci/publish/validate-skeleton.sh` is a binary installability gate (valid `plugin.json` with
70	> `name=sptc`; valid `hooks.json` whose referenced wrappers all exist; every skill has a
71	> `SKILL.md`; **no runtime-state / binary / manifest leak in the published surface**) — it runs in
72	> `ci/run-gates.sh` (the `skeleton-validate` gate) and is unit-tested by `tests/skeleton-validate.sh`.
73	> `ci/publish/package-skeleton.sh` codifies the per-bump copy below — validates first, **dry-run by
74	> default**, stages only the skeleton subset, never pushes (the marketplace commit/push stays the
75	> operator's step).
76
77	**One-time marketplace setup:**
78
79	```bash
80	# Clone the marketplace repo
81	git clone https://github.com/SaberMage/cplugs.git ~/.claude/plugins/marketplaces/cplugs
82	# Register a "cplugs" entry in ~/.claude/plugins/known_marketplaces.json
83	#   source: { source: "github", repo: "SaberMage/cplugs" }
84	```
85
86	**Per skeleton bump** (only when the skeleton *structurally* changes — see "Per release"):
87
88	```bash
89	# 1. Bump plugin.json version. The plugin manager compares this to the cached copy and
90	#    SKIPS the update entirely if they match — a stale version = a silent no-update.
91	# 2. Copy skeleton files (NO binary) into the marketplace clone:
92	MARKET=~/.claude/plugins/marketplaces/cplugs/plugins/sptc

1. **Bump** the version (the plugin `plugin.json` version and/or the adapter manifest version —
   note these are distinct numbers; see Notes) if needed; **regenerate generated docs**
   (the mdBook `docs-site/` build + `llms.txt` / schema / CLI-help exports); land everything
   green.
2. **Write the user-facing changelog** — add a `## [<version>] - <date>` section at the top of
   `CHANGELOG.md` with **Added / Changed / Fixed** subsections. This section becomes the public
   release body verbatim, so it is the changelog every user reads. Rules:
   - **User-facing UX only.** What a person using the product notices or does differently. Name
     the actual commands / flags / surfaces they touch (`/spt:*` skills, `cc`, `/spt:setup`).
   - **No internal lingo** — no requirement ids, internal module names, commit hashes, or
     milestone / hazard codes. A reader who has never seen the source must understand every line.
   - **Flag breaking changes** prominently under Changed.
   - The release **fails loudly** if the tagged version has no `## [<version>]` section — the
     changelog is not optional.
3. **Tag**: `git tag <vX.Y.Z> && git push origin <vX.Y.Z>`. This triggers:
   - the **docs-publish workflow** — build `docs-site/` and publish to GitHub Pages (drift-gated;
     see `docs/DOCS-STRATEGY.md`).
4. **Publish** (light — signing delegated to `spt`):
   - **Skeleton plugin** → push to the `SaberMage/cplugs` marketplace **only when the plugin
     skeleton structurally changed** (new/removed skill skeleton, hook wiring, bootstrap, or
     `plugin.json`). Most releases do not bump the marketplace.
   - **Manifest + binary** → publish through the **spt-core adapter registry**; the `spt`
     binary performs signing/integrity as part of spt-core's adapter-update framework. You do
     **not** run a separate signing step.

## cplugs skeleton publish — concrete mechanics

<!-- [doc->REQ-DIST-PLUGIN-SKELETON] -->
The concrete cplugs marketplace steps, captured from the sister project's
`claude_skill_owl/docs/DEPLOY.md`. **Take only the SKELETON SUBSET.** spt-claude-code's cplugs
target is the **thin skeleton** — `/spt:*` skill skeletons, `hooks.json`, the SessionStart
bootstrap, `plugin.json`. **No binary, no manifest in cplugs** — the binary + adapter manifest
ride the **spt-core adapter registry** (spt-conducted; the *other* publish target above), so
none of owl's binary-lifecycle machinery applies here.

> **Now scripted (`ci/publish/`).** The validate + stage mechanics below are codified:
> `ci/publish/validate-skeleton.sh` is a binary installability gate (valid `plugin.json` with
> `name=sptc`; valid `hooks.json` whose referenced wrappers all exist; every skill has a
> `SKILL.md`; **no runtime-state / binary / manifest leak in the published surface**) — it runs in
> `ci/run-gates.sh` (the `skeleton-validate` gate) and is unit-tested by `tests/skeleton-validate.sh`.
> `ci/publish/package-skeleton.sh` codifies the per-bump copy below — validates first, **dry-run by
> default**, stages only the skeleton subset, never pushes (the marketplace commit/push stays the
> operator's step).

**One-time marketplace setup:**

```bash
# Clone the marketplace repo
git clone https://github.com/SaberMage/cplugs.git ~/.claude/plugins/marketplaces/cplugs
# Register a "cplugs" entry in ~/.claude/plugins/known_marketplaces.json
#   source: { source: "github", repo: "SaberMage/cplugs" }
```

**Per skeleton bump** (only when the skeleton *structurally* changes — see "Per release"):

```bash
# 1. Bump plugin.json version. The plugin manager compares this to the cached copy and
#    SKIPS the update entirely if they match — a stale version = a silent no-update.
# 2. Copy skeleton files (NO binary) into the marketplace clone:
MARKET=~/.claude/plugins/marketplaces/cplugs/plugins/sptc


Phase E understood. **Version decision:** plugin.json `0.0.1 → 0.1.0` (first structural publish; semver-conventional first minor). The "distinct from adapter manifest" rule governs independent *evolution*, not a forced t=0 difference — I'll document the independence in CHANGELOG.

Confirm the published skill set to write an accurate first-release CHANGELOG.


cd "C:\Users\decid\Documents\projects\spt-claude-code" && echo "===published skills==="; ls plugin/sptc/skills/; echo "===bootstrap==="; ls plugin/sptc/bootstrap.* 2>/dev/null; echo "===docs gen scripts==="; ls ci/docs/ 2>/dev/null

List published skills + bootstrap + docs scripts


===published skills===
commune
force-stop
list-agents
live
ready
send
setup
signoff
subnet
version
===bootstrap===
plugin/sptc/bootstrap.ps1
plugin/sptc/bootstrap.sh
===docs gen scripts===
check-docs.sh
gen-llms.sh


10 skills confirmed. Write the first-release CHANGELOG (user-facing, no internal lingo per runbook).


Write

# Changelog

All notable user-facing changes to the **sptc** plugin (Spacetime adapter for Claude Code).
The format follows [Keep a Changelog](https://keepachangelog.com/); each release section is the
public release body verbatim.

> The plugin (marketplace) version below tracks the *plugin skeleton* and moves on its own
> schedule. It is independent of the spt engine version you see in `/sptc:version` — that one is
> the version of truth and updates itself behind the scenes.

## [0.1.0] - 2026-06-15

First public release.

### Added
- **One-step setup — `/sptc:setup`.** Sets the plugin up and installs the Spacetime (`spt`)
  engine for you if it isn't already present, so messaging and live agents just work. Also offers
  to create or join a private network and to register the always-on background service.
- **Agent messaging.** `/sptc:ready` turns on your inbox so other agents can reach this session;
  `/sptc:send` messages another agent; `/sptc:list-agents` shows who is currently active.
- **Live agents.** `/sptc:live` upgrades the current session into a live agent with a persistent
  companion that keeps its own running context. `/sptc:commune` pushes a context update to that
  companion, and `/sptc:signoff` ends a live session cleanly with a final summary.
- **`/sptc:force-stop`** immediately tears down a listening or live agent when you need it gone.
- **Cross-machine networks — `/sptc:subnet`.** Pair machines into a private network (create,
  show the join code, or join an existing one) so your agents can reach each other across nodes.
- **`/sptc:version`** reports the running engine version.
- **Alternate model backends (advanced).** Ships a ready-made `ccs` profile template you can
  select to launch sessions through [ccs](https://github.com/kaitranntt/ccs) — useful for routing
  an agent to a different model or billing backend. You supply your own ccs configuration.
- **Invisible engine install.** Starting a session automatically installs the `spt` engine the
  first time if it is missing — no separate install step.


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\CHANGELOG.md (file state is current in your context — no need to Read it back)

# Changelog

All notable user-facing changes to the **sptc** plugin (Spacetime adapter for Claude Code).
The format follows [Keep a Changelog](https://keepachangelog.com/); each release section is the
public release body verbatim.

> The plugin (marketplace) version below tracks the *plugin skeleton* and moves on its own
> schedule. It is independent of the spt engine version you see in `/sptc:version` — that one is
> the version of truth and updates itself behind the scenes.

## [0.1.0] - 2026-06-15

First public release.

### Added
- **One-step setup — `/sptc:setup`.** Sets the plugin up and installs the Spacetime (`spt`)
  engine for you if it isn't already present, so messaging and live agents just work. Also offers
  to create or join a private network and to register the always-on background service.
- **Agent messaging.** `/sptc:ready` turns on your inbox so other agents can reach this session;
  `/sptc:send` messages another agent; `/sptc:list-agents` shows who is currently active.
- **Live agents.** `/sptc:live` upgrades the current session into a live agent with a persistent
  companion that keeps its own running context. `/sptc:commune` pushes a context update to that
  companion, and `/sptc:signoff` ends a live session cleanly with a final summary.
- **`/sptc:force-stop`** immediately tears down a listening or live agent when you need it gone.
- **Cross-machine networks — `/sptc:subnet`.** Pair machines into a private network (create,
  show the join code, or join an existing one) so your agents can reach each other across nodes.
- **`/sptc:version`** reports the running engine version.
- **Alternate model backends (advanced).** Ships a ready-made `ccs` profile template you can
  select to launch sessions through [ccs](https://github.com/kaitranntt/ccs) — useful for routing
  an agent to a different model or billing backend. You supply your own ccs configuration.
- **Invisible engine install.** Starting a session automatically installs the `spt` engine the
  first time if it is missing — no separate install step.


Bump plugin.json version.


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


1	{
2	  "name": "sptc",
3	  "version": "0.0.1",
4	  "description": "Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).",
5	  "author": {
6	    "name": "perri"
7	  },
8	  "keywords": ["spt", "spacetime", "messaging", "agents", "live", "claude-spt"]
9	}
10

sptc

spt spacetime messaging agents live claude-spt


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

"description": "Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).",


Now run validate + package dry-run, regen docs, and the full gate.


Bash

cd "C:\Users\decid\Documents\projects\spt-claude-code" && echo "===VALIDATE==="; sh ci/publish/validate-skeleton.sh 2>&1 | tail -5; echo "VRC=${PIPESTATUS[0]}"; echo "===PACKAGE DRY-RUN==="; sh ci/publish/package-skeleton.sh 2>&1 | tail -12; echo "PRC=${PIPESTATUS[0]}"

Validate + package skeleton dry-run


===VALIDATE===
ok   skill ok: subnet
ok   skill ok: version
ok   published surface clean (no runtime/binary/manifest leak)

=== SKELETON: INSTALLABLE ===
VRC=0
===PACKAGE DRY-RUN===

== plan (DRY-RUN) ==
source : /c/Users/decid/Documents/projects/spt-claude-code/plugin/sptc
target : /c/Users/decid/.claude/plugins/marketplaces/cplugs/plugins/sptc
  stage  .claude-plugin/plugin.json
  stage  hooks
  stage  skills
  stage  bootstrap.sh
  stage  bootstrap.ps1

DRY-RUN: nothing copied. Re-run with --apply to stage into $MARKET, then commit+push the
cplugs repo and 'claude plugin install sptc@cplugs' (see docs/RELEASE-RUNBOOK.md).
PRC=0


Both green. Now regenerate docs (drift gate) + full gate.


cd "C:\Users\decid\Documents\projects\spt-claude-code" && echo "===GEN LLMS==="; sh ci/docs/gen-llms.sh 2>&1 | tail -5; echo "GRC=${PIPESTATUS[0]}"; echo "===git diff stat after gen==="; git status --short

Regenerate llms docs, check drift


===GEN LLMS===
wrote /c/Users/decid/Documents/projects/spt-claude-code/docs-site/llms.txt
GRC=0
===git diff stat after gen===
 M PARITY-PUBLISH-PLAN.md
 M plugin/sptc/.claude-plugin/plugin.json
?? CHANGELOG.md


No doc drift. Run full gate + traceable to confirm everything green.


cd "C:\Users\decid\Documents\projects\spt-claude-code" && sh ci/run-gates.sh >/tmp/gate2.log 2>&1; echo "GATE_RC=$?"; grep -iE "FAIL|RESULT|coverage" /tmp/gate2.log; traceable-reqs check >/dev/null 2>&1; echo "TRACE_RC=$?"

Full gate + traceable check


GATE_RC=0
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
=== GATE: traceable-reqs check (requirement coverage) ===
ok  coverage green
=== RESULT: PASS ===
TRACE_RC=0


All green. Mark Phase E done in the plan, then commit.


Edit

- ⏭ **Phase E** (publish prep as sptc) — NEXT. CHANGELOG.md + plugin.json bump + validate/package
  skeleton dry-run + docs regen. cplugs publish = operator's step.

- ✅ **Phase E** (publish prep as sptc) — CHANGELOG.md created (`## [0.1.0] - 2026-06-15`,
  user-facing UX only); plugin.json bumped 0.0.1→0.1.0 (name stays `sptc`; version independent of
  the adapter-manifest version per RUNBOOK). validate-skeleton INSTALLABLE + package-skeleton
  dry-run green; llms.txt regen = no drift. Gate PASS + traceable green.
  **Remaining = OPERATOR's step only:** `package-skeleton.sh --apply` → cplugs commit/push →
  `claude plugin install sptc@cplugs`. Plan stops at validated dry-run (credentials + pointer flip
  are operator-owned).


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

- ✅ **Phase E** (publish prep as sptc) — CHANGELOG.md created (`## [0.1.0] - 2026-06-15`,
  user-facing UX only); plugin.json bumped 0.0.1→0.1.0 (name stays `sptc`; version independent of
  the adapter-manifest version per RUNBOOK). validate-skeleton INSTALLABLE + package-skeleton
  dry-run green; llms.txt regen = no drift. Gate PASS + traceable green.
  **Remaining = OPERATOR's step only:** `package-skeleton.sh --apply` → cplugs commit/push →
  `claude plugin install sptc@cplugs`. Plan stops at validated dry-run (credentials + pointer flip
  are operator-owned).

# JIT plan — parity audit → fill LOCKED-ADD gaps → cplugs publish (as sptc)

> Next slice after the live sub-slice (4b042cc) + F-004 flip (692b67c). Goal: PROVE parity
> against the SCOPE.md LOCKED parity-trim, fill the remaining LOCKED-ADD gaps, then publish the
> thin skeleton to cplugs **as `sptc`** (NOT `spt` — succession flip stays gated, see
> [[succession-flip-gated]] / RELEASE-RUNBOOK). Public-surface-only still binds (AGENTS.md).

## Operator decisions (locked this session, 2026-06-15)
1. **Slice = audit + fill gaps + publish.** Build the absent LOCKED-ADD surface (subnet skills,
   ccs profiles), reconcile whoami, THEN publish as sptc. `new-alarm` stays **deferred** — doyle
   TRIAGED it (high-prio 2026-06-15) as an **ACCEPTED parity gap = "deferred-pending-arrangement-
   system", NOT missing**: spt-core has only the alarm event shape + relay handling + a test
   fixture; the durable timer is a deliberate deferral (DEFERRED.md:43, ADR-0018 Q4/V3), and the
   operator has a superseding "arrangement" system in mind. **Do NOT build an adapter-side stopgap
   timer** (throwaway vs the coming model), **no REQ minted**. Remove the dead stub (deferred ≠
   shipping a no-op skill); doyle loops me when the arrangement design surfaces.
2. **Drop the `/whoami` SKILL.** Honor the LOCKED SCOPE decision — core `spt whoami` stays
   (becomes the `endpoint list` alias in M12); the adapter ships no whoami skill.

## Parity matrix (current vs SCOPE LOCKED parity-trim)

| Legacy / target surface        | SCOPE disposition        | Adapter today          | Action (this slice)            |
|--------------------------------|--------------------------|------------------------|--------------------------------|
| /live /commune /ready /send    | KEEP                     | operative (file-backed)| ✓ none                         |
| /list-agents /signoff /force-stop | KEEP                  | operative              | ✓ none                         |
| /new-alarm                     | ACCEPTED GAP (deferred-pending-arrangement-system) | dead stub, no manifest | **remove stub**; no REQ, no build |
| /spt:setup /spt:version        | ADD                      | operative              | ✓ none                         |
| profiles + strings + hints     | ADD                      | wired (deep/live)      | ✓ none                         |
| **subnet skills (create/join/show-code)** | ADD (LOCKED)  | **ABSENT**             | **BUILD** (Phase C)            |
| **ccs profiles (claude-spt:glm …)** | ADD (LOCKED)        | **ABSENT**             | **BUILD** (Phase D)            |
| /whoami SKILL                  | DROP skill               | **operative (violation)** | **REMOVE** (Phase B)        |
| /fork /amend-signoff /revive /clear-psyche | DROP         | absent                 | ✓ none (confirm absent)        |

Public surface confirms the ADD gaps are buildable: `spt subnet {create,join,show-code,status}`
exists; ccs = profile overlays retargeting command templates (SCOPE §ccs, profile *templates*
only — user supplies their own ccs config/keys).

## Progress (this session)
- ✅ **Phase A** (b1db67f) — docs/PARITY.md matrix + REQ seed (REQ-PARITY-AUDIT [doc] satisfied;
  REQ-SKILL-SUBNET / REQ-CCS-PROFILES seeded). DROP set confirmed absent.
- ✅ **Phase B** (0595910) — whoami skill DROPPED (manifest + body + stub + hint + test repoints);
  dead new-alarm stub removed. Surface 11→9.
- ✅ **Phase C** (e3382a0) — /sptc:subnet skill added (one skill wrapping `spt subnet`).
  REQ-SKILL-SUBNET [doc,int] green on v0.7.2. how-to-subnet finding logged. Surface 9→10.
- ✅ **Phase D** (b15c1bf) — `claude-spt:ccs` profile. Scoped to bare `ccs` (default account, NOT
  glm/kimi — operator ruling); validated vs sister project claude_skill_owl. Spawn seam
  (`[profiles.ccs.session.self].command = "ccs"`, a drop-in for `claude`) + observability string
  leaf. Log-dir seam landed IN the extractor: claude-spt-digest now honors `CLAUDE_CONFIG_DIR`
  (owl `owlery::claude_projects_root` parity) — ccs relocates CC's transcript tree per-account, a
  runtime value with no static path, so no `[profiles.ccs.digest]` leaf. REQ-CCS-PROFILES
  [doc,impl,unit,int] green. Finding logged (SCOPE conflated ccs profiles vs accounts).
  **docs/PARITY.md verdict → PROVEN (adapter surface).** Gate PASS; registration-int OK on v0.7.2.
- ⏭ **Phase E** (publish prep as sptc) — NEXT. CHANGELOG.md + plugin.json bump + validate/package
  skeleton dry-run + docs regen. cplugs publish = operator's step.

## Phases (sequence; one commit + green gate each)

### Phase A — Parity audit artifact + REQ seed
- Write `docs/PARITY.md` (the matrix above + per-row evidence/finding) — the "proven parity"
  record the succession flip gates on. Tag `<!-- doc->REQ-PARITY-AUDIT -->`.
- Seed REQs in `traceable-reqs.toml` FIRST (before building): `REQ-SKILL-SUBNET`,
  `REQ-CCS-PROFILES`, `REQ-PARITY-AUDIT`. Activate stages as each phase lands.
- Confirm the DROP set is truly absent (grep no fork/revive/clear-psyche/amend-signoff residue).

### Phase B — Drop the whoami skill (honor LOCKED)
- Remove `whoami = { file = "skills/whoami.md" }` from manifest `[strings.skills]`.
- Delete `adapter/strings/skills/whoami.md` + `plugin/sptc/skills/whoami/`.
- **registration-int.sh** uses `skills.whoami` as the canonical file-backed + UPS-injection
  example (4b/4c) — repoint to a surviving skill (`skills.ready`).
- Redirect the `who am i` **hint** (currently → /sptc:whoami) to core `spt whoami` guidance.
- Audit REQ evidence: nothing is tagged `REQ-*` on whoami.md directly, but check
  `tests/hooks-parse.sh` / manifest-shortcut for whoami coupling before deleting.

### Phase C — Subnet skills (LOCKED ADD; public verbs exist)
- **Open shape decision (resolve at top of phase):** three skills
  (`/sptc:subnet-create|join|show-code`) vs one routing `/sptc:subnet` skill. Lean: match SCOPE's
  enumeration but as ONE `subnet` skill body that documents create/join/show-code (fewer
  surfaces, cleaner UX) — confirm before authoring.
- File-backed body(ies) `adapter/strings/skills/subnet*.md` (run `spt how-to subnet` / wrap the
  real `spt subnet …` verbs); manifest `[strings.skills]` entries; thin SKILL.md stub(s); hint(s).
- `REQ-SKILL-SUBNET` [doc,impl] (+ int via registration-int file-backed-resolve assertion).

### Phase D — ccs profiles (LOCKED ADD)
- **Research first** (mcp/context7 not needed — read SCOPE §ccs + the existing `[profiles.*]`
  command seams). ccs profile = a `[profiles.<glm|kimi>]` overlay that leaf-replaces the spawn /
  psyche / echo command templates to route through `ccs <profile> …`, with a per-profile `~/.ccs`
  log dir. Author profile *templates* only (no user keys).
- Add `[profiles.glm]` (+ maybe `[profiles.kimi]`) overlays; observable via `spt adapter
  get-string claude-spt:glm …` + resolves in registration-int (mirror the :deep/:live assertions).
- `REQ-CCS-PROFILES` [doc,impl] (+ int).

### Phase E — Publish prep (as sptc; NO name flip)
- **Create `CHANGELOG.md`** with a `## [<version>] - 2026-06-15` section (Added/Changed/Fixed,
  user-facing UX only, no internal lingo) — release fails loudly without it.
- **Bump `plugin.json`** `version` 0.0.1 → first real (structural first publish). Keep `name=sptc`.
- Run `sh ci/publish/validate-skeleton.sh` + `sh ci/publish/package-skeleton.sh` (DRY-RUN) green.
- Regenerate docs (mdBook + llms.txt) — drift gate green.
- The actual cplugs commit/push + `claude plugin install sptc@cplugs` stays the **operator's**
  step (credentials + pointer flip) — plan stops at a validated dry-run + a publish checklist.

### Gate (every phase)
`sh ci/run-gates.sh` PASS + `traceable-reqs check` green. Commit `Co-authored by: perri`.

## Open questions (resolve in-phase, don't block planning)
- Subnet skill shape: 3 skills vs 1 routing skill (Phase C top).
- ccs: which profiles ship as templates (glm only, or glm+kimi)? SCOPE names `claude-spt:glm`.
- plugin.json first-publish version number (0.1.0? matches adapter manifest 0.1.0, but RUNBOOK
  says keep them DISTINCT — pick independently).

## Findings carried (not ours to fix)
- new-alarm core alarm/deferred-pulse primitive (blocks new-alarm parity) — doyle.
- hard/no-grace force-stop primitive — doyle.
- Live relay int held for spt-core M11 counter-15 release ([[live-int-gated-m11-counter15]]).

## Done immediately before (context)
- 4b042cc live sub-slice (claude-spt-psyche runner + /sptc:live body, REQ-SKILL-LIVE [doc,impl,unit]).
- 692b67c F-004 flip (digest-proof int GREEN on v0.7.2, REQ-DIST-DIGEST-EXTRACTOR full).
- spt upgraded 0.7.1 → 0.7.2 (operator-approved daemon bounce).

-- ⏭ **Phase E** (publish prep as sptc) — NEXT. CHANGELOG.md + plugin.json bump + validate/package

-  skeleton dry-run + docs regen. cplugs publish = operator's step.

+- ✅ **Phase E** (publish prep as sptc) — CHANGELOG.md created (`## [0.1.0] - 2026-06-15`,

+  user-facing UX only); plugin.json bumped 0.0.1→0.1.0 (name stays `sptc`; version independent of

+  the adapter-manifest version per RUNBOOK). validate-skeleton INSTALLABLE + package-skeleton

+  dry-run green; llms.txt regen = no drift. Gate PASS + traceable green.

+  **Remaining = OPERATOR's step only:** `package-skeleton.sh --apply` → cplugs commit/push →

+  `claude plugin install sptc@cplugs`. Plan stops at validated dry-run (credentials + pointer flip


Bash

cd "C:\Users\decid\Documents\projects\spt-claude-code" && git add -A && git commit -q -F - <<'EOF'
chore(release): publish prep as sptc 0.1.0 (Phase E) — CHANGELOG + version bump + validated dry-run

First structural cplugs publish prep. Stops at a validated dry-run; the
marketplace commit/push + install is the operator's step (credentials + pointer
flip), per docs/RELEASE-RUNBOOK.md.

- CHANGELOG.md: new `## [0.1.0] - 2026-06-15` section (Added/Changed/Fixed shape,
  user-facing UX only — names the /sptc:* skills, setup, subnet, live, ccs
  profile, invisible engine install; no internal lingo). Becomes the public
  release body verbatim; the release fails loudly without it.
- plugin.json: version 0.0.1 -> 0.1.0, name stays `sptc`. This marketplace
  (skeleton) version is INDEPENDENT of the adapter-manifest/binary version
  (the /sptc:version version-of-truth); they move on separate schedules
  (RUNBOOK Notes — never conflate). 0.1.0 chosen as the conventional first
  minor; distinctness is about independent evolution, not a forced t=0 gap.
- validate-skeleton.sh: INSTALLABLE (name=sptc, hooks wrappers present, every
  skill has SKILL.md, no runtime/binary/manifest leak). package-skeleton.sh
  dry-run green (stages the skeleton subset only). llms.txt regen: no drift.

Gate PASS + traceable-reqs check green. NOT a name flip (sptc->spt succession
stays gated on proven-parity + legacy-owl retirement + M11 counter-15).

Co-authored by: perri
EOF
echo "RC=$?"; git log --oneline -3

Commit Phase E


RC=0
34fded7 chore(release): publish prep as sptc 0.1.0 (Phase E) — CHANGELOG + version bump + validated dry-run
b15c1bf feat(ccs): add claude-spt:ccs profile (Phase D) — spawn retarget + CLAUDE_CONFIG_DIR-aware digest
7073e6a docs(plan): mark parity Phases A/B/C done; Phase D (ccs) next


Both phases done. Parity slice complete (everything but the operator's publish step). Commune a delta to my Psyche per context-hygiene.


Write

**Parity slice DONE through Phase E — ccs landed + publish-prepped (delta since "Phases A/B/C done").**
2 commits past 7073e6a:

- **b15c1bf (Phase D — ccs profiles):** Added `claude-spt:ccs` profile. **Operator ruling:** scope =
  bare `ccs` (default account via `ccs auth default`), NOT glm/kimi — those were SCOPE *examples*;
  this profile IS the worked example + operator's own SPT use. **Validated against sister project
  claude_skill_owl** (operator-directed, known-good): (1) ccs is a drop-in for the `claude` binary,
  same argv (live/wrapper/claude.rs Tier-2 latch); (2) ccs relocates CC's transcript tree via
  `CLAUDE_CONFIG_DIR`, resolved in-binary by owlery::claude_projects_root.
  Overlay = `[profiles.ccs.session.self].command="ccs"` (spawn seam) + `[profiles.ccs.strings].
  adapter_label` (observability, mirrors :deep). Log-dir seam landed IN claude-spt-digest: now
  prefers `$CLAUDE_CONFIG_DIR/projects` over `--in` root on the dir-locate branch (`--sample`
  untouched), +3 unit tests. NO `[profiles.ccs.digest]` leaf (env value is per-account runtime, no
  static catalog path). REQ-CCS-PROFILES [doc,impl,unit,int] GREEN. impl/unit = the extractor
  resolver (the .toml-leaf-isn't-impl trace gotcha avoided); doc = PARITY.md; int = registration-int
  :ccs assertions (resolve + adapter_label observable), GREEN on live v0.7.2, registry left clean.
  **Finding logged:** SCOPE conflated ccs *profiles* (glm/kimi = API env-swap, no relocation) vs
  *accounts* (isolated instances that DO relocate via CLAUDE_CONFIG_DIR); operator runs the account
  path so the digest seam is real but its mechanism is CLAUDE_CONFIG_DIR, not a fixed `~/.ccs` path.

- **34fded7 (Phase E — publish prep as sptc):** CHANGELOG.md `## [0.1.0] - 2026-06-15` (user-facing
  UX only; the public release body). plugin.json 0.0.1→0.1.0, name stays `sptc` (marketplace version
  INDEPENDENT of the manifest/binary version-of-truth, per RUNBOOK). validate-skeleton INSTALLABLE +
  package-skeleton dry-run green; llms.txt regen no-drift. Gate PASS + traceable green throughout.

**docs/PARITY.md verdict → PROVEN (adapter surface).** All 3 open divergences closed: subnet added,
ccs added, whoami dropped; DROP set absent. The sptc→spt succession flip stays SEPARATELY gated
(legacy owl retirement + live-relay int on spt-core M11 counter-15) — this verdict clears only the
parity precondition.

**IMMEDIATE NEXT = OPERATOR's step only:** `sh ci/publish/package-skeleton.sh --apply` → cplugs
commit/push → `claude plugin install sptc@cplugs`. (credentials + pointer flip are operator-owned).
No further autonomous work queued in PARITY-PUBLISH-PLAN.md — slice is structurally complete.


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\.claude\perri-commune.md (file state is current in your context — no need to Read it back)

**Parity slice DONE through Phase E — ccs landed + publish-prepped (delta since "Phases A/B/C done").**
2 commits past 7073e6a:

- **b15c1bf (Phase D — ccs profiles):** Added `claude-spt:ccs` profile. **Operator ruling:** scope =
  bare `ccs` (default account via `ccs auth default`), NOT glm/kimi — those were SCOPE *examples*;
  this profile IS the worked example + operator's own SPT use. **Validated against sister project
  claude_skill_owl** (operator-directed, known-good): (1) ccs is a drop-in for the `claude` binary,
  same argv (live/wrapper/claude.rs Tier-2 latch); (2) ccs relocates CC's transcript tree via
  `CLAUDE_CONFIG_DIR`, resolved in-binary by owlery::claude_projects_root.
  Overlay = `[profiles.ccs.session.self].command="ccs"` (spawn seam) + `[profiles.ccs.strings].
  adapter_label` (observability, mirrors :deep). Log-dir seam landed IN claude-spt-digest: now
  prefers `$CLAUDE_CONFIG_DIR/projects` over `--in` root on the dir-locate branch (`--sample`
  untouched), +3 unit tests. NO `[profiles.ccs.digest]` leaf (env value is per-account runtime, no
  static catalog path). REQ-CCS-PROFILES [doc,impl,unit,int] GREEN. impl/unit = the extractor
  resolver (the .toml-leaf-isn't-impl trace gotcha avoided); doc = PARITY.md; int = registration-int
  :ccs assertions (resolve + adapter_label observable), GREEN on live v0.7.2, registry left clean.
  **Finding logged:** SCOPE conflated ccs *profiles* (glm/kimi = API env-swap, no relocation) vs
  *accounts* (isolated instances that DO relocate via CLAUDE_CONFIG_DIR); operator runs the account
  path so the digest seam is real but its mechanism is CLAUDE_CONFIG_DIR, not a fixed `~/.ccs` path.

- **34fded7 (Phase E — publish prep as sptc):** CHANGELOG.md `## [0.1.0] - 2026-06-15` (user-facing
  UX only; the public release body). plugin.json 0.0.1→0.1.0, name stays `sptc` (marketplace version
  INDEPENDENT of the manifest/binary version-of-truth, per RUNBOOK). validate-skeleton INSTALLABLE +
  package-skeleton dry-run green; llms.txt regen no-drift. Gate PASS + traceable green throughout.

**docs/PARITY.md verdict → PROVEN (adapter surface).** All 3 open divergences closed: subnet added,
ccs added, whoami dropped; DROP set absent. The sptc→spt succession flip stays SEPARATELY gated
(legacy owl retirement + live-relay int on spt-core M11 counter-15) — this verdict clears only the
parity precondition.

**IMMEDIATE NEXT = OPERATOR's step only:** `sh ci/publish/package-skeleton.sh --apply` → cplugs
commit/push → `claude plugin install sptc@cplugs`. (credentials + pointer flip are operator-owned).
No further autonomous work queued in PARITY-PUBLISH-PLAN.md — slice is structurally complete.


Parity slice complete. Both phases shipped:

**Phase D — `claude-spt:ccs` profile** (`b15c1bf`)
- Operator ruling honored: scope = bare `ccs` (default account), NOT glm/kimi. Profile = the worked example + your own SPT use.
- Validated against sister project **claude_skill_owl** (your steer): ccs = drop-in for `claude` binary; relocates transcripts via `CLAUDE_CONFIG_DIR`.
- Overlay: `[profiles.ccs.session.self].command="ccs"` + observability `adapter_label` leaf.
- Log-dir seam landed **in the extractor** (`claude-spt-digest` now prefers `$CLAUDE_CONFIG_DIR/projects`, owl-parity), not a manifest leaf — `--sample` untouched, +3 unit tests.
- **Finding:** SCOPE conflated ccs *profiles* (glm/kimi = API env-swap, no relocation) vs *accounts* (relocate via `CLAUDE_CONFIG_DIR`). You run the account path → digest seam real, mechanism is `CLAUDE_CONFIG_DIR`.
- REQ-CCS-PROFILES `[doc,impl,unit,int]` green on live v0.7.2; registry left clean.

**Phase E — publish prep as sptc** (`34fded7`)
- CHANGELOG.md `## [0.1.0] - 2026-06-15` (user-facing only). plugin.json `0.0.1→0.1.0`, name stays `sptc` (independent of manifest version-of-truth).
- validate-skeleton INSTALLABLE + package dry-run green; docs no-drift. Gate PASS + traceable green.

**docs/PARITY.md verdict → PROVEN (adapter surface).** All 3 divergences closed (subnet added, ccs added, whoami dropped). Succession flip stays separately gated (legacy owl retirement + M11 counter-15).

**Your step only** (credentials + pointer flip):
```
sh ci/publish/package-skeleton.sh --apply
# → cplugs commit/push → claude plugin install sptc@cplugs
```
No autonomous work left in the plan — slice structurally complete. Commune dropped.


<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="34fded72fe3767b25b4be7fb493e7899e680fa52" head_subject="chore(release): publish prep as sptc 0.1.0 (Phase E) — CHANGELOG + versi…" commits_since="0" commits_unpulled="0"/>
**Parity slice DONE through Phase E — ccs landed + publish-prepped (delta since "Phases A/B/C done").**
2 commits past 7073e6a:

- **b15c1bf (Phase D — ccs profiles):** Added `claude-spt:ccs` profile. **Operator ruling:** scope =
  bare `ccs` (default account via `ccs auth default`), NOT glm/kimi — those were SCOPE *examples*;
  this profile IS the worked example + operator's own SPT use. **Validated against sister project
  claude_skill_owl** (operator-directed, known-good): (1) ccs is a drop-in for the `claude` binary,
  same argv (live/wrapper/claude.rs Tier-2 latch); (2) ccs relocates CC's transcript tree via
  `CLAUDE_CONFIG_DIR`, resolved in-binary by owlery::claude_projects_root.
  Overlay = `[profiles.ccs.session.self].command="ccs"` (spawn seam) + `[profiles.ccs.strings].
  adapter_label` (observability, mirrors :deep). Log-dir seam landed IN claude-spt-digest: now
  prefers `$CLAUDE_CONFIG_DIR/projects` over `--in` root on the dir-locate branch (`--sample`
  untouched), +3 unit tests. NO `[profiles.ccs.digest]` leaf (env value is per-account runtime, no
  static catalog path). REQ-CCS-PROFILES [doc,impl,unit,int] GREEN. impl/unit = the extractor
  resolver (the .toml-leaf-isn't-impl trace gotcha avoided); doc = PARITY.md; int = registration-int
  :ccs assertions (resolve + adapter_label observable), GREEN on live v0.7.2, registry left clean.
  **Finding logged:** SCOPE conflated ccs *profiles* (glm/kimi = API env-swap, no relocation) vs
  *accounts* (isolated instances that DO relocate via CLAUDE_CONFIG_DIR); operator runs the account
  path so the digest seam is real but its mechanism is CLAUDE_CONFIG_DIR, not a fixed `~/.ccs` path.

- **34fded7 (Phase E — publish prep as sptc):** CHANGELOG.md `## [0.1.0] - 2026-06-15` (user-facing
  UX only; the public release body). plugin.json 0.0.1→0.1.0, name stays `sptc` (marketplace version
  INDEPENDENT of the manifest/binary version-of-truth, per RUNBOOK). validate-skeleton INSTALLABLE +
  package-skeleton dry-run green; llms.txt regen no-drift. Gate PASS + traceable green throughout.

**docs/PARITY.md verdict → PROVEN (adapter surface).** All 3 open divergences closed: subnet added,
ccs added, whoami dropped; DROP set absent. The sptc→spt succession flip stays SEPARATELY gated
(legacy owl retirement + live-relay int on spt-core M11 counter-15) — this verdict clears only the
parity precondition.

**IMMEDIATE NEXT = OPERATOR's step only:** `sh ci/publish/package-skeleton.sh --apply` → cplugs
commit/push → `claude plugin install sptc@cplugs`. (credentials + pointer flip are operator-owned).
No further autonomous work queued in PARITY-PUBLISH-PLAN.md — slice is structurally complete.<project-context-resolved name="spt-claude-code"/>
**spt-claude-code — v1 acceptance status (parity slice in flight).** claude-spt is spt-core's v1
acceptance proof: a rebuilt Claude Code harness adapter built against the PUBLIC surface ONLY
(spt-releases + sabermage.github.io/spt-releases). End goal unchanged: spt-core's v1 acceptance
proof + first casual-end-user entrypoint (cplugs plugin `sptc`, skills `/sptc:*`) + invisible
spt-core installer. On parity-proof: cplugs publish, then the sptc→spt succession.

**Where parity stands:** of 3 open LOCKED-ADD/DROP gaps, **2 closed** (whoami skill dropped;
subnet skill added). **1 remains: ccs profiles** (Phase D). Then publish (Phase E). docs/PARITY.md
is the authoritative reconciliation record + the "proven parity" gate the name-flip depends on —
its verdict flips to PROVEN once ccs lands (everything else already ✅).

**spt-core dep state:** local binary upgraded to **v0.7.2** this session (operator-approved daemon
bounce) — carries doyle's F-004 fix (digest-proof int GREEN) + PR#15 harness-contract docs. Two
spt-core gates remain non-blocking: (1) live relay int held for the **M11 counter-15** release
(daemon Psyche loop complete in core but not in published bytes — doyle pings on tag); (2)
sptc→spt name flip held until proven parity AND legacy owl retired.

**Carried findings (doyle's, not ours to fix):** alarm/deferred-pulse primitive (new-alarm parity,
superseded by the coming arrangement model); hard/no-grace force-stop primitive; no `spt how-to
subnet` topic. v1-acceptance arc after this slice: parity-proven → cplugs publish (as sptc) →
succession.</psyche-context>


record this session's learnings about building an SPT harness adapter in `SPT_HARNESS_ADAPTER_TIPS.md`. only record what applies to the existing public framework, and ignore any docs gaps which may have arisen. if there WERE any docs gaps, be sure to inform doyle if you haven't already.


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


Two tasks: write the tips file (public framework only), and ensure doyle knows of any docs gaps. Check findings ledger + whether doyle already informed.


1	# spt-core public-surface findings
2
3	> Per the public-surface-only constraint (`AGENTS.md`, `HANDOFF.md` §1): if a capability needed
4	> to build the adapter is missing/ambiguous in spt-core's **published** surface (`spt-releases`
5	> releases + GH Pages docs), that is a **finding** — `spt-core`'s published contract has a gap.
6	> We report it to the spt-core owner (**doyle**) and do **not** reverse-engineer from legacy
7	> `claude_skill_owl` or reach into spt-core source. This file is the in-repo log of those findings.
8
9	| # | Date | Status | Summary |
10	|---|------|--------|---------|
11	| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |
12	| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
13	| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |
14	| F-004 | 2026-06-15 | **CONFIRMED-IMPL-BUG (doyle); fix in progress** — `digest-proof` will fill `{id}`+`{session_id}` matching runtime; int deferred until the carrying release | `spt adapter digest-proof --sample` passes an empty substitution-key map → false-fails any extractor whose command uses `{session_id}` (incl. the published example) |
15
16	---
17
18	## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter
19
20	**Reported:** 2026-06-14 to doyle (owl). **Status:** awaiting ruling / docs fix.
21
22	**What IS published (sufficient):**
23	- `harness-contract/api.md` — the full `spt api` primitive surface: `seed`, `listen`, `bind`,
24	  `boundary`, `session-end`, `shutdown`, `state`, `echo-gate`, `presence`, `driven-by`, `poll`,
25	  `history-log`, `worker-start/poll/stop`, `bind-shell`, `emit`, `owner-shutdown`, `capability`.
26	- `harness-contract/install-on-demand.md` — install-on-demand bootstrap, exact `sh` + `ps1`
27	  snippets, canonical installer URLs, env-var customization. **Fully buildable.**
28	- `harness-contract/integration-checklist.md` + `manifest.md` map: SessionStart →
29	  `api seed --pid {parent_pid} --session-id {session_id}` then `api listen <id>`; Idle →
30	  `api state idle|busy`; PreCompact/clear → `api boundary clear|compact <id> --to-session-id <sid>`;
31	  teardown → `api session-end <id>` / `api shutdown <id>`.
32
33	**The gaps:**
34
35	1. **Hook-event → `spt api` mapping is incomplete.** No published mapping for **UserPromptSubmit**,
36	   **PreToolUse**, **SubagentStart**, **SubagentStop**. (`worker-start/stop` exist but are not tied
37	   to the Subagent hook events.)
38	2. **Manifest `[hooks.*]` vs harness-native hook config is unspecified.** `manifest.md` declares
39	   hooks as `[hooks.<event>] fires="api ..." reads=[...] can_inject=bool`, with spt-core performing
40	   `{key}` substitution from `reads`. It is **silent** on whether spt-core *materializes* the
41	   harness-native config (Claude Code `hooks.json`) from the manifest, or the adapter author
42	   hand-writes `hooks.json` that shells out to `spt api`. These imply different substitution models:
43	   Claude Code delivers hook data as JSON on **stdin**, but the manifest model is spt-core-side
44	   `{placeholder}` fill — so if `hooks.json` is hand-written, it is unclear who fills
45	   `--pid {parent_pid}`. (Also reframes the `SCOPE.md` assumption "the plugin ships a `hooks.json`
46	   calling `spt api`" — it may instead be manifest-declared + spt-materialized.)
47	3. **Injection mechanism is undocumented.** `can_inject` + an `[inject]` section (methods
48	   `pty`/`hook`/`relay`/`http`) exist, but there is **no technical detail** on how an adapter emits
49	   `additionalContext` back through the hook channel. Blocks (a) general mid-session message
50	   delivery on a prompt/tool hook, and (b) this project's **UPS-injection** skill-instruction design
51	   (`docs/adr/0001-*`) specifically — it cannot be validated against the public surface until this
52	   is documented.
53
54	**Impact (as first reported):** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now.
55	UserPromptSubmit + PreToolUse + Subagent wiring + the entire injection path were *thought* blocked.
56
57	### Resolution (2026-06-15 — converged with doyle, grounded in `CONTEXT.md`)
58
59	The boundary canon (`CONTEXT.md` L52/L181/L112/L149/L137) re-frames F-001: **spt-core is
60	harness-independent and supplies the agnostic `spt api` primitives + their I/O format; the adapter
61	authors the harness-specific wiring + output-formatting.** So most of F-001 is **adapter-authoring
62	work (ours), not an spt-core gap**:
63
64	- **Gap 1 (CC-event → api mapping) — OURS, closed.** Which CC hook drives which `api` primitive is
65	  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`
66	  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle
67	  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /
68	  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit
69	  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.
70	- **Gap 2 (medium) — OURS, closed.** The `sptc` plugin **hand-writes** its CC `hooks.json` shelling
71	  `spt api`. spt-core materializing a CC `hooks.json` would violate L52/L181 — it correctly doesn't.
72	- **Gap 3 (injection HOW) — OURS, closed.** `api poll` emits message frames to **stdout** by design
73	  (L149) — that IS the hook-injection delivery path; routing/formatting that stdout into CC's
74	  `additionalContext` channel is adapter glue (L112). The earlier "`can_inject`/`[inject]`
75	  method-discard is dead code" alarm is **retracted** — it is M2a roadmap staging (stdout/hook only
76	  now; PTY at M3), and `can_inject` drives the *built* echo-gate/relay fallback (L137) for hooks that
77	  cannot inject (e.g. CC Stop).
78	- **CC param-sourcing** ({parent_pid}/{session_id}) — OURS: read off the CC hook-input schema
79	  (`session_id` is a common stdin field; pid via the hook process). Not an spt-core concern.
80
81	**Residual spt-core item (doyle owns; non-blocking):** publish the **agnostic primitives** to the
82	adapter-facing docs-site — specifically `api poll`'s **emit frame format** (the frame we parse +
83	format for CC) and the **substitution-key catalog** (currently code-only). doyle verifies against
84	`CONTEXT.md`/ADRs then propagates (no new design). We will report the **observed** `api poll` frame
85	format so the publish can confirm-match. Until published we may rely on observed behavior of the
86	public binary (observable behavior = public surface).
87
88	### Validation note (2026-06-15, against live v0.7.0 `spt adapter add` + the shipped mock-adapter)
89
90	Authored + registered `adapter/claude-spt.toml` against the real v0.7.0 binary
91	(`REQ-DIST-MANIFEST-SCHEMA` int — `ci/manifest/registration-int.sh`, 6/6 green). Confirms the
92	residual: the **published docs still omit** two things a manifest author needs, both learnable only
93	from the **shipped mock-adapter source** (which IS public surface) + live `adapter add` errors:
94
95	1. **Substitution-key catalog.** `{parent_pid}` and `{adapter_name}` are spt-filled SessionStart
96	   keys (mock `api seed --pid {parent_pid} … --adapter {adapter_name}`); the docs-site lists only a
97	   partial set. The full catalog is still code-only.
98	2. **`[digest]` cross-field rule.** The JSON schema accepts `[digest]` with just `extractor`, but
99	   `spt adapter add` **rejects** it: `[digest] needs source (own-source) or a [history]
100	   locate_template`. Not documented prose-side; surfaced only at registration. (Worked around with
101	   `source = "{home}/.claude/projects"` — CC's cwd-slug subdir is not expressible as a flat
102	   `locate_template`, so `source` names the per-project root and the extractor finds
103	   `{session_id}.jsonl` within. A CC-shaped extractor is the right design vs the mock's log-less
104	   `[history] native`.) **Non-blocking** (resolved by reading the shipped mock + the error text);
105	   logged so doyle can publish the key catalog + the digest source-requirement to the docs-site.
106
107	Also observed (not a gap, a roadmap fact): `spt endpoint run` notes the **interactive picker that
108	emits the `<basename>-<id>` shortcut "lands in a later wave"** — so the `sptc-<id>` *emission* int
109	(`REQ-DIST-SHORTCUT-BASENAME`) stays held on that wave (and on authoring `[session.self]`, which
110	`endpoint run` spawns). `shortcut_basename = "sptc"` is confirmed to round-trip into the stored
111	resolved manifest, so the declaration side is proven.
112
113	---
114
115	## F-002 — `api poll` (agent path) has no inter-frame delimiter — multi-message drains unsplittable
116
117	**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has
118	raised the framing as a contract decision to the operator.
119
120	**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained
121	message with `print!("{msg}")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**
122	(`spool.rs:91-97`). So a multi-message drain **concatenates frames with nothing between them**.
123	Combined with the frame format (anonymous senders emit a **bare body**, and bodies may be
124	multiline), a multi-message `poll` stdout is **genuinely unsplittable** — there is no delimiter to
125	parse on. Tell that this is a rough edge, not intent: the **shell** drain (`cmd_poll_shell`,
126	`delivery.rs:169`) uses `println!` (newline-framed); only the agent path omits it. The framing is
127	unspecified in `CONTEXT.md` / `docs/api.md`.
128
129	**Impact / our handling:**
130	- **Single-message poll = unambiguous, works now** — `__REPLY_TO__:<id>\n<body>` or bare body.
131	- **Multi-frame splitter is HELD** — we do not assume a delimiter that does not exist. The
132	  UserPromptSubmit wrapper surfaces whatever `poll` returns: a single frame is cleanly per-sender
133	  formatted; a (rare) concatenated multi-drain degrades to a surfaced-but-unattributed blob rather
134	  than a parser guessing boundaries.
135
136	**Resolution — RESOLVED-BY-DESIGN (ADR-0020, operator-ruled 2026-06-15):** the framing question is
137	settled, more cleanly than a delimiter patch. The **canonical format at every surface — including
138	`api poll` — is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope** (`spt-proto::event`,
139	the ADR-0001 grammar the live listener already emits). `__REPLY_TO__` was a **mis-elevated relic**
140	(wrongly frozen as the "stable wire format" during the clean-room port) and is being **deleted from
141	spt-core**. The `<EVENT>` envelope is **self-delimiting**, so multi-message drains split cleanly on
142	`</EVENT>` — no delimiter needed, and the F-002 multi-frame hold is **lifted**.
143
144	- **Our parser** now targets `<EVENT>` (the `from` attr → `<sptc_messages from="…">`, `<br>` →
145	  newline, entity unescape with `&` last) — exactly the live-agent body-parsing rule. See
146	  `render_frames` in `plugin/sptc/hooks/_common.sh` + `tests/hooks-parse.sh` (multi-message covered).
147	- **VERIFIED ON THE PUBLISHED SURFACE (v0.7.1, 2026-06-15).** ADR-0020 shipped in **v0.7.1**
148	  (counter 13); `__REPLY_TO__` relic gone. A throwaway byte-capture against the live 0.7.1 `spt api
149	  poll` drain (`od`-verified) confirms the canonical envelope end-to-end:
150	  - single msg: `<EVENT type="msg" from="probe-sender">hello from probe
second <line>
151	    & "stuff"</EVENT>\n` — body escaping (`
` + `</>/"` + `&` last)
152	    is the **exact** `render_frames` decode rule.
153	  - whole envelopes, self-delimiting **and** `\n`-framed; **no `<EVENT-PART>`** on a normal hook
154	    drain; **no `__REPLY_TO__`**. Multi-drain splits cleanly on `</EVENT>` → **F-002 dissolved on the
155	    published binary**.
156	  - raw drain piped through our `render_frames` → correct `<sptc_messages from=…>` (parser
157	    confirm-match PASS). `notify` events ride the same envelope.
158	  Locked in by `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated, ≥0.7.1, 5/5): bind→send→`api
159	  poll`→`render_frames` assert. **`REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION` `int` flipped GREEN.**
160	  ADR-0020 loop closed: design→impl→gate→ship→real-surface-verify.
161
162	---
163
164	## F-003 — No file-backed `[strings]` mechanism on the M12/v0.7.0 public surface (ADR-0001 dependency unmet)
165
166	**Reported:** 2026-06-15 to doyle + todlando. **Status:** **RESOLVED 2026-06-15** — the capability
167	is **shipped** in v0.7.0; the residual is a **docs-visibility** gap (doyle is publishing the syntax).
168	The text below records the original (wrong) "absent" framing for provenance; see **Resolution**.
169
170	**The dependency (ours, ADR-0001):** UPS-injection delivers `/sptc:X` skill bodies from the adapter
171	`[strings]` tree, explicitly **"file-backed, so the manifest doesn't bloat"** (`REQ-UPS-INJECTION`).
172	ADR-0001's own Open/to-confirm names it: *"File-backed `[strings]` is an M12 spt-core dependency —
173	until M12 publishes, instruction bodies cannot be externalized."* M12 is now public (`spt 0.7.0`).
174
175	**The gap (confirmed against the live binary AND the published docs):** v0.7.0 exposes **no** way to
176	externalize a `[strings]` value to a file. Every avenue is inline-only:
177
178	- **Schema** (`adapter/manifest.schema.json`, vendored from the docs-site): `[strings]` is a plain
179	  `object`, `additionalProperties: true` — no `$file`/`include`/`@file` value convention.
180	- **`spt adapter get-string <opt> <key>`**: "prints the value (strings raw, else JSON)" — reads the
181	  merged inline view; no file-reference resolution.
182	- **`spt adapter set-string <opt> <key> <VALUE>`**: takes a **literal** `<VALUE>` positional — no
183	  `--from-file`/stdin path for an individual value.
184	- **`spt adapter create-profile --from <file>`**: ingests a *whole overlay TOML* whose values are
185	  still inline — not a per-value file backing.
186	- **Published docs** (`llms-full.txt`, manifest + CLI reference): no `@file`/include/external-ref
187	  syntax documented anywhere; strings are described purely as inline manifest-resident data.
188
189	(`[update] file_pull` ships adapter *files*, but it needs an Ed25519 signing key + a registry repo
190	target we do not hold — already a finding-class gap, see manifest header — and it does not make a
191	`[strings]` *value* resolve from a shipped file. Not a substitute.)
192
193	**Impact / our handling:** non-blocking, by ADR-0001's own fallback — *"skeleton SKILL.md files may
194	carry interim inline instructions."* So:
195	- The `/sptc:whoami` + `/sptc:setup` skeletons keep **interim inline bodies** in the cplugs skeleton;
196	  the manifest does **not** carry (and does not externalize) skill bodies in v0.7.0.
197	- `REQ-UPS-INJECTION` stays at `[doc, impl, unit]` (activate-don't-pre-fail) — the externalization
198	  half is held on this capability, not failed.
199	- The manifest-bloat avoidance ADR-0001 promised is **unachievable on the published surface** until
200	  spt-core adds a file-backed string mechanism (or rules that large bodies belong in another layer).
201
202	**Ask for doyle/todlando:** does M12 intend file-backed `[strings]` and it is unpublished/unshipped,
203	or has the design moved (e.g. bodies belong in a file-pull-shipped layer, not `[strings]`)? Either a
204	docs+binary capability or an ADR-0001 amendment closes this.
205
206	### Resolution (2026-06-15 — capability-confirmed-shipped; residual is docs-only)
207
208	The "absent" framing was **wrong**: the avenue search probed CLI verbs (`set-string --from-file`,
209	`create-profile --from`) and value-prefix syntax (`@file`/include), but the shipped mechanism is a
210	**value-position inline-table FILE POINTER** authored directly in `[strings]` — a shape an open
211	`additionalProperties` schema accepts and a verb-search cannot surface. doyle + todlando both ruled it
212	shipped (spt-core `REQ-MANIFEST-5`, M12-W3 @ `e08cea0`: `profile.rs::as_file_pointer` +
213	`registry.rs` lazy `get_string`). Confirmed by **live byte-test** against `spt 0.7.0`:
214
215	```toml
216	[strings]
217	inline_key  = "INLINE-VALUE"            # inline string — get-string prints as-is
218	filebacked  = { file = "probe.txt" }     # pointer — get-string RESOLVES to file contents (lazy)
219	```
220	```text
221	$ spt adapter add <dir>                            # ADAPTER_ADD:<name>:Harness:Copy (registered)
222	$ spt adapter get-string <name> inline_key         # INLINE-VALUE
223	$ spt adapter get-string <name> filebacked         # RESOLVED-FROM-FILE-OK   (file body, not the table)
224	# containment (negative): escape = { file = "../outside.txt" }
225	$ spt adapter add <dir>
226	#   ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer ../outside.txt must be a relative
227	#   path inside the strings/ dir (no absolute paths, no `..` traversal)   ← manifest-first: nothing registered
228	```
229
230	**Confirmed behavior (now the basis of our authoring + doyle's doc):**
231	- Pointer files live in the per-adapter aux dir `adapters/<adapter>/strings/`; `file = "x"` is a bare
232	  relative path **inside** that dir. On `adapter add` the whole adapter dir (incl. `strings/`) is
233	  **copied** into the registry.
234	- `get-string` **resolves the pointer lazily at read time** (live edits to the shipped file reflect
235	  without re-register) and prints the **file contents**; inline string values print as-is.
236	- **Containment** is enforced at register: `..`/absolute escaping `strings/` → `ADAPTER_ADD_FAIL`,
237	  manifest-first (nothing registers). Missing-at-read is skip-diagnostic (mirrors `[digest]`), not a crash.
238	- **Update-safe (todlando):** a **local profile's** pointers resolve to the *user-owned local-profile
239	  dir*, not adapter-shipped `strings/` (adapter updates won't stomp user overrides). A value-table
240	  carrying a `file` key is **reserved** as the pointer form — it cannot double as inline data.
241
242	**Disposition:** ADR-0001's "file-backed `[strings]`" dependency is **SATISFIED** — no ADR amendment,
243	no spt-core build change. We author UPS-injection skill bodies as `[strings.skills].<x> = { file =
244	"skills/<x>.md" }` over `adapter/strings/skills/<x>.md`. **Residual = published-docs visibility only:**
245	the pointer syntax was undocumented on the GH-Pages surface (it lived in spt-core-internal
246	`CONTEXT.md §adapter strings`) — same class as the `{key}` catalog + `[digest]` cross-field rule.
247	**CLOSED in v0.7.1 (2026-06-15):** doyle published the `[strings]` file-pointer syntax (plus the
248	`{key}` substitution catalog and the `[digest]` register rule) to the docs-site manifest reference +
249	`MANIFEST.md`. The next adapter author discovers the pointer form from the published surface alone —
250	F-003 fully resolved.
251
252	---
253
254	## F-004 — `spt adapter digest-proof --sample` does not fill `{session_id}` (false-fails the published extractor shape)
255
256	**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor (ADR-0019).
257
258	**Symptom.** `spt adapter digest-proof --sample <log>` substitutes only `{source}` (= the sample
259	path) into the extractor command and hard-fails on any other key. The claude-spt extractor command
260	is the production-correct, published-example shape:
261
262	```
263	extractor = "claude-spt-digest --session {session_id} --in {source}"
264	```
265
266	→ `DIGEST_PROOF_EXTRACT_FAIL:claude-spt: digest extractor failed: no value for substitution key {session_id}`.
267
268	**Isolation (same adapter, only the command line differs):**
269	- **Variant A** — `<exe> --in {source}` (source-only) → `DIGEST_PROOF_OK`, **parsed 5 / dropped 0**,
270	  rendered digest correct (incl. sprint-collapse `used: Write(src/a.rs), Bash(cargo build)`). Proves
271	  the extractor + the digest-proof render pipeline are correct.
272	- **Variant B** — `<exe> --session {session_id} --in {source}` → fails on the unfilled `{session_id}`.
273
274	**Why claude-spt needs `{session_id}` on the command (not a workaround — the only correct shape).**
275	CC's transcript path is `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`. The `<cwd-slug>` subdir
276	is CC-internal and has **no published key** — and per doyle, *should* not (spt-core stays
277	harness-agnostic; it must not bake a harness's directory scheme into the key catalog). So `source`
278	must be the projects **root** and the extractor receives `{session_id}` on the command, resolving
279	`<slug>/<session_id>.jsonl` itself (the slug is the harness's business). This matches the published
280	example and what `spt endpoint digest` fills at runtime.
281
282	**Root cause (doyle, spt-core).** `digest-proof --sample` builds an **empty** substitution-key map
283	(`cli.rs:5135`, `let keys = BTreeMap::new()` — comment admits "placeholders like `{session_id}`
284	will fail"), whereas the runtime daemon path fills `{id}`+`{session_id}` before running the same
285	extractor (`digest.rs:208-210`). digest-proof is meant to be the author-time half of the *same*
286	ADR-0019 engine (§diagnostics), so it must supply the same keys. It doesn't → it is infidelitous to
287	runtime and false-fails any `{session_id}`-templated extractor, including the published example.
288
289	Aside: `{home}` is **not** a catalog key — hard-failing on it is correct. Use `~` for home
290	expansion in `source` (we switched `source` to `~/.claude/projects`; the extractor also expands a
291	leading `~/` defensively).
292
293	**Status — CONFIRMED-IMPL-BUG; fix in progress (doyle's worktree).** digest-proof will fill
294	`{id}`+`{session_id}` matching runtime exactly (fidelity: *"passes proof" ⟺ "works at runtime"*),
295	plus an optional `--session <id>` override (default placeholder). The published contract is right;
296	the tool is catching up. **Our posture:** the extractor is built to the published contract and
297	proven (cargo unit tests + Variant A `DIGEST_PROOF_OK`); the digest-proof `int`
298	(`ci/digest/digest-proof-int.sh`, `REQ-DIST-DIGEST-EXTRACTOR`) **skips on the exact substitution
299	error** until doyle's fix ships, then flips green on the carrying release.
300
301	---
302
303	## Finding: no `spt how-to subnet` topic (published-guidance gap, minor)
304
305	**Surface:** `spt how-to <topic>` ships canonical, always-current guidance for `ready` and `send`
306	(the messaging hot path) — the claude-spt `ready`/`send` skills lean on it (`spt how-to ready`).
307	But there is **no `subnet` topic** (`spt how-to subnet` → `NO_SUCH_TOPIC`), despite `spt subnet`
308	being a multi-verb, pairing-flow-heavy command (create/show-code/join, 6-digit codes, QR) — exactly
309	the kind of thing a how-to topic serves well.
310
311	**Impact (low):** the `/sptc:subnet` skill wraps `spt subnet --help` as its canonical reference
312	instead. Works, but inconsistent with ready/send (which get richer how-to prose), and a casual user
313	running `spt how-to subnet` hits a dead end. **Disposition:** mint a `how-to subnet` topic in
314	spt-core (cross-machine pairing is the highest-value first-run flow; it deserves the same guided
315	prose as messaging). Carried as a parity finding — NOT a blocker for `/sptc:subnet`.
316

# spt-core public-surface findings

> Per the public-surface-only constraint (`AGENTS.md`, `HANDOFF.md` §1): if a capability needed
> to build the adapter is missing/ambiguous in spt-core's **published** surface (`spt-releases`
> releases + GH Pages docs), that is a **finding** — `spt-core`'s published contract has a gap.
> We report it to the spt-core owner (**doyle**) and do **not** reverse-engineer from legacy
> `claude_skill_owl` or reach into spt-core source. This file is the in-repo log of those findings.

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |
| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |
| F-004 | 2026-06-15 | **CONFIRMED-IMPL-BUG (doyle); fix in progress** — `digest-proof` will fill `{id}`+`{session_id}` matching runtime; int deferred until the carrying release | `spt adapter digest-proof --sample` passes an empty substitution-key map → false-fails any extractor whose command uses `{session_id}` (incl. the published example) |

---

## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter

**Reported:** 2026-06-14 to doyle (owl). **Status:** awaiting ruling / docs fix.

**What IS published (sufficient):**
- `harness-contract/api.md` — the full `spt api` primitive surface: `seed`, `listen`, `bind`,
  `boundary`, `session-end`, `shutdown`, `state`, `echo-gate`, `presence`, `driven-by`, `poll`,
  `history-log`, `worker-start/poll/stop`, `bind-shell`, `emit`, `owner-shutdown`, `capability`.
- `harness-contract/install-on-demand.md` — install-on-demand bootstrap, exact `sh` + `ps1`
  snippets, canonical installer URLs, env-var customization. **Fully buildable.**
- `harness-contract/integration-checklist.md` + `manifest.md` map: SessionStart →
  `api seed --pid {parent_pid} --session-id {session_id}` then `api listen <id>`; Idle →
  `api state idle|busy`; PreCompact/clear → `api boundary clear|compact <id> --to-session-id <sid>`;
  teardown → `api session-end <id>` / `api shutdown <id>`.

**The gaps:**

1. **Hook-event → `spt api` mapping is incomplete.** No published mapping for **UserPromptSubmit**,
   **PreToolUse**, **SubagentStart**, **SubagentStop**. (`worker-start/stop` exist but are not tied
   to the Subagent hook events.)
2. **Manifest `[hooks.*]` vs harness-native hook config is unspecified.** `manifest.md` declares
   hooks as `[hooks.<event>] fires="api ..." reads=[...] can_inject=bool`, with spt-core performing
   `{key}` substitution from `reads`. It is **silent** on whether spt-core *materializes* the
   harness-native config (Claude Code `hooks.json`) from the manifest, or the adapter author
   hand-writes `hooks.json` that shells out to `spt api`. These imply different substitution models:
   Claude Code delivers hook data as JSON on **stdin**, but the manifest model is spt-core-side
   `{placeholder}` fill — so if `hooks.json` is hand-written, it is unclear who fills
   `--pid {parent_pid}`. (Also reframes the `SCOPE.md` assumption "the plugin ships a `hooks.json`
   calling `spt api`" — it may instead be manifest-declared + spt-materialized.)
3. **Injection mechanism is undocumented.** `can_inject` + an `[inject]` section (methods
   `pty`/`hook`/`relay`/`http`) exist, but there is **no technical detail** on how an adapter emits
   `additionalContext` back through the hook channel. Blocks (a) general mid-session message
   delivery on a prompt/tool hook, and (b) this project's **UPS-injection** skill-instruction design
   (`docs/adr/0001-*`) specifically — it cannot be validated against the public surface until this
   is documented.

**Impact (as first reported):** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now.
UserPromptSubmit + PreToolUse + Subagent wiring + the entire injection path were *thought* blocked.

### Resolution (2026-06-15 — converged with doyle, grounded in `CONTEXT.md`)

The boundary canon (`CONTEXT.md` L52/L181/L112/L149/L137) re-frames F-001: **spt-core is
harness-independent and supplies the agnostic `spt api` primitives + their I/O format; the adapter
authors the harness-specific wiring + output-formatting.** So most of F-001 is **adapter-authoring
work (ours), not an spt-core gap**:

- **Gap 1 (CC-event → api mapping) — OURS, closed.** Which CC hook drives which `api` primitive is
  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`
  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle
  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /
  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit
  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.
- **Gap 2 (medium) — OURS, closed.** The `sptc` plugin **hand-writes** its CC `hooks.json` shelling
  `spt api`. spt-core materializing a CC `hooks.json` would violate L52/L181 — it correctly doesn't.
- **Gap 3 (injection HOW) — OURS, closed.** `api poll` emits message frames to **stdout** by design
  (L149) — that IS the hook-injection delivery path; routing/formatting that stdout into CC's
  `additionalContext` channel is adapter glue (L112). The earlier "`can_inject`/`[inject]`
  method-discard is dead code" alarm is **retracted** — it is M2a roadmap staging (stdout/hook only
  now; PTY at M3), and `can_inject` drives the *built* echo-gate/relay fallback (L137) for hooks that
  cannot inject (e.g. CC Stop).
- **CC param-sourcing** ({parent_pid}/{session_id}) — OURS: read off the CC hook-input schema
  (`session_id` is a common stdin field; pid via the hook process). Not an spt-core concern.

**Residual spt-core item (doyle owns; non-blocking):** publish the **agnostic primitives** to the
adapter-facing docs-site — specifically `api poll`'s **emit frame format** (the frame we parse +
format for CC) and the **substitution-key catalog** (currently code-only). doyle verifies against
`CONTEXT.md`/ADRs then propagates (no new design). We will report the **observed** `api poll` frame
format so the publish can confirm-match. Until published we may rely on observed behavior of the
public binary (observable behavior = public surface).

### Validation note (2026-06-15, against live v0.7.0 `spt adapter add` + the shipped mock-adapter)

Authored + registered `adapter/claude-spt.toml` against the real v0.7.0 binary
(`REQ-DIST-MANIFEST-SCHEMA` int — `ci/manifest/registration-int.sh`, 6/6 green). Confirms the
residual: the **published docs still omit** two things a manifest author needs, both learnable only
from the **shipped mock-adapter source** (which IS public surface) + live `adapter add` errors:

1. **Substitution-key catalog.** `{parent_pid}` and `{adapter_name}` are spt-filled SessionStart
   keys (mock `api seed --pid {parent_pid} … --adapter {adapter_name}`); the docs-site lists only a
   partial set. The full catalog is still code-only.
2. **`[digest]` cross-field rule.** The JSON schema accepts `[digest]` with just `extractor`, but
   `spt adapter add` **rejects** it: `[digest] needs source (own-source) or a [history]
   locate_template`. Not documented prose-side; surfaced only at registration. (Worked around with
   `source = "{home}/.claude/projects"` — CC's cwd-slug subdir is not expressible as a flat
   `locate_template`, so `source` names the per-project root and the extractor finds
   `{session_id}.jsonl` within. A CC-shaped extractor is the right design vs the mock's log-less
   `[history] native`.) **Non-blocking** (resolved by reading the shipped mock + the error text);
   logged so doyle can publish the key catalog + the digest source-requirement to the docs-site.

Also observed (not a gap, a roadmap fact): `spt endpoint run` notes the **interactive picker that
emits the `<basename>-<id>` shortcut "lands in a later wave"** — so the `sptc-<id>` *emission* int
(`REQ-DIST-SHORTCUT-BASENAME`) stays held on that wave (and on authoring `[session.self]`, which
`endpoint run` spawns). `shortcut_basename = "sptc"` is confirmed to round-trip into the stored
resolved manifest, so the declaration side is proven.

---

## F-002 — `api poll` (agent path) has no inter-frame delimiter — multi-message drains unsplittable

**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has
raised the framing as a contract decision to the operator.

**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained
message with `print!("{msg}")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**
(`spool.rs:91-97`). So a multi-message drain **concatenates frames with nothing between them**.
Combined with the frame format (anonymous senders emit a **bare body**, and bodies may be
multiline), a multi-message `poll` stdout is **genuinely unsplittable** — there is no delimiter to
parse on. Tell that this is a rough edge, not intent: the **shell** drain (`cmd_poll_shell`,
`delivery.rs:169`) uses `println!` (newline-framed); only the agent path omits it. The framing is
unspecified in `CONTEXT.md` / `docs/api.md`.

**Impact / our handling:**
- **Single-message poll = unambiguous, works now** — `__REPLY_TO__:<id>\n<body>` or bare body.
- **Multi-frame splitter is HELD** — we do not assume a delimiter that does not exist. The
  UserPromptSubmit wrapper surfaces whatever `poll` returns: a single frame is cleanly per-sender
  formatted; a (rare) concatenated multi-drain degrades to a surfaced-but-unattributed blob rather
  than a parser guessing boundaries.

**Resolution — RESOLVED-BY-DESIGN (ADR-0020, operator-ruled 2026-06-15):** the framing question is
settled, more cleanly than a delimiter patch. The **canonical format at every surface — including
`api poll` — is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope** (`spt-proto::event`,
the ADR-0001 grammar the live listener already emits). `__REPLY_TO__` was a **mis-elevated relic**
(wrongly frozen as the "stable wire format" during the clean-room port) and is being **deleted from
spt-core**. The `<EVENT>` envelope is **self-delimiting**, so multi-message drains split cleanly on
`</EVENT>` — no delimiter needed, and the F-002 multi-frame hold is **lifted**.

- **Our parser** now targets `<EVENT>` (the `from` attr → `<sptc_messages from="…">`, `<br>` →
  newline, entity unescape with `&` last) — exactly the live-agent body-parsing rule. See
  `render_frames` in `plugin/sptc/hooks/_common.sh` + `tests/hooks-parse.sh` (multi-message covered).
- **VERIFIED ON THE PUBLISHED SURFACE (v0.7.1, 2026-06-15).** ADR-0020 shipped in **v0.7.1**
  (counter 13); `__REPLY_TO__` relic gone. A throwaway byte-capture against the live 0.7.1 `spt api
  poll` drain (`od`-verified) confirms the canonical envelope end-to-end:
  - single msg: `<EVENT type="msg" from="probe-sender">hello from probe
second <line>
    & "stuff"</EVENT>\n` — body escaping (`
` + `</>/"` + `&` last)
    is the **exact** `render_frames` decode rule.
  - whole envelopes, self-delimiting **and** `\n`-framed; **no `<EVENT-PART>`** on a normal hook
    drain; **no `__REPLY_TO__`**. Multi-drain splits cleanly on `</EVENT>` → **F-002 dissolved on the
    published binary**.
  - raw drain piped through our `render_frames` → correct `<sptc_messages from=…>` (parser
    confirm-match PASS). `notify` events ride the same envelope.
  Locked in by `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated, ≥0.7.1, 5/5): bind→send→`api
  poll`→`render_frames` assert. **`REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION` `int` flipped GREEN.**
  ADR-0020 loop closed: design→impl→gate→ship→real-surface-verify.

---

## F-003 — No file-backed `[strings]` mechanism on the M12/v0.7.0 public surface (ADR-0001 dependency unmet)

**Reported:** 2026-06-15 to doyle + todlando. **Status:** **RESOLVED 2026-06-15** — the capability
is **shipped** in v0.7.0; the residual is a **docs-visibility** gap (doyle is publishing the syntax).
The text below records the original (wrong) "absent" framing for provenance; see **Resolution**.

**The dependency (ours, ADR-0001):** UPS-injection delivers `/sptc:X` skill bodies from the adapter
`[strings]` tree, explicitly **"file-backed, so the manifest doesn't bloat"** (`REQ-UPS-INJECTION`).
ADR-0001's own Open/to-confirm names it: *"File-backed `[strings]` is an M12 spt-core dependency —
until M12 publishes, instruction bodies cannot be externalized."* M12 is now public (`spt 0.7.0`).

**The gap (confirmed against the live binary AND the published docs):** v0.7.0 exposes **no** way to
externalize a `[strings]` value to a file. Every avenue is inline-only:

- **Schema** (`adapter/manifest.schema.json`, vendored from the docs-site): `[strings]` is a plain
  `object`, `additionalProperties: true` — no `$file`/`include`/`@file` value convention.
- **`spt adapter get-string <opt> <key>`**: "prints the value (strings raw, else JSON)" — reads the
  merged inline view; no file-reference resolution.
- **`spt adapter set-string <opt> <key> <VALUE>`**: takes a **literal** `<VALUE>` positional — no
  `--from-file`/stdin path for an individual value.
- **`spt adapter create-profile --from <file>`**: ingests a *whole overlay TOML* whose values are
  still inline — not a per-value file backing.
- **Published docs** (`llms-full.txt`, manifest + CLI reference): no `@file`/include/external-ref
  syntax documented anywhere; strings are described purely as inline manifest-resident data.

(`[update] file_pull` ships adapter *files*, but it needs an Ed25519 signing key + a registry repo
target we do not hold — already a finding-class gap, see manifest header — and it does not make a
`[strings]` *value* resolve from a shipped file. Not a substitute.)

**Impact / our handling:** non-blocking, by ADR-0001's own fallback — *"skeleton SKILL.md files may
carry interim inline instructions."* So:
- The `/sptc:whoami` + `/sptc:setup` skeletons keep **interim inline bodies** in the cplugs skeleton;
  the manifest does **not** carry (and does not externalize) skill bodies in v0.7.0.
- `REQ-UPS-INJECTION` stays at `[doc, impl, unit]` (activate-don't-pre-fail) — the externalization
  half is held on this capability, not failed.
- The manifest-bloat avoidance ADR-0001 promised is **unachievable on the published surface** until
  spt-core adds a file-backed string mechanism (or rules that large bodies belong in another layer).

**Ask for doyle/todlando:** does M12 intend file-backed `[strings]` and it is unpublished/unshipped,
or has the design moved (e.g. bodies belong in a file-pull-shipped layer, not `[strings]`)? Either a
docs+binary capability or an ADR-0001 amendment closes this.

### Resolution (2026-06-15 — capability-confirmed-shipped; residual is docs-only)

The "absent" framing was **wrong**: the avenue search probed CLI verbs (`set-string --from-file`,
`create-profile --from`) and value-prefix syntax (`@file`/include), but the shipped mechanism is a
**value-position inline-table FILE POINTER** authored directly in `[strings]` — a shape an open
`additionalProperties` schema accepts and a verb-search cannot surface. doyle + todlando both ruled it
shipped (spt-core `REQ-MANIFEST-5`, M12-W3 @ `e08cea0`: `profile.rs::as_file_pointer` +
`registry.rs` lazy `get_string`). Confirmed by **live byte-test** against `spt 0.7.0`:

```toml
[strings]
inline_key  = "INLINE-VALUE"            # inline string — get-string prints as-is
filebacked  = { file = "probe.txt" }     # pointer — get-string RESOLVES to file contents (lazy)
```
```text
$ spt adapter add <dir>                            # ADAPTER_ADD:<name>:Harness:Copy (registered)
$ spt adapter get-string <name> inline_key         # INLINE-VALUE
$ spt adapter get-string <name> filebacked         # RESOLVED-FROM-FILE-OK   (file body, not the table)
# containment (negative): escape = { file = "../outside.txt" }
$ spt adapter add <dir>
#   ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer ../outside.txt must be a relative
#   path inside the strings/ dir (no absolute paths, no `..` traversal)   ← manifest-first: nothing registered
```

**Confirmed behavior (now the basis of our authoring + doyle's doc):**
- Pointer files live in the per-adapter aux dir `adapters/<adapter>/strings/`; `file = "x"` is a bare
  relative path **inside** that dir. On `adapter add` the whole adapter dir (incl. `strings/`) is
  **copied** into the registry.
- `get-string` **resolves the pointer lazily at read time** (live edits to the shipped file reflect
  without re-register) and prints the **file contents**; inline string values print as-is.
- **Containment** is enforced at register: `..`/absolute escaping `strings/` → `ADAPTER_ADD_FAIL`,
  manifest-first (nothing registers). Missing-at-read is skip-diagnostic (mirrors `[digest]`), not a crash.
- **Update-safe (todlando):** a **local profile's** pointers resolve to the *user-owned local-profile
  dir*, not adapter-shipped `strings/` (adapter updates won't stomp user overrides). A value-table
  carrying a `file` key is **reserved** as the pointer form — it cannot double as inline data.

**Disposition:** ADR-0001's "file-backed `[strings]`" dependency is **SATISFIED** — no ADR amendment,
no spt-core build change. We author UPS-injection skill bodies as `[strings.skills].<x> = { file =
"skills/<x>.md" }` over `adapter/strings/skills/<x>.md`. **Residual = published-docs visibility only:**
the pointer syntax was undocumented on the GH-Pages surface (it lived in spt-core-internal
`CONTEXT.md §adapter strings`) — same class as the `{key}` catalog + `[digest]` cross-field rule.
**CLOSED in v0.7.1 (2026-06-15):** doyle published the `[strings]` file-pointer syntax (plus the
`{key}` substitution catalog and the `[digest]` register rule) to the docs-site manifest reference +
`MANIFEST.md`. The next adapter author discovers the pointer form from the published surface alone —
F-003 fully resolved.

---

## F-004 — `spt adapter digest-proof --sample` does not fill `{session_id}` (false-fails the published extractor shape)

**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor (ADR-0019).

**Symptom.** `spt adapter digest-proof --sample <log>` substitutes only `{source}` (= the sample
path) into the extractor command and hard-fails on any other key. The claude-spt extractor command
is the production-correct, published-example shape:

```
extractor = "claude-spt-digest --session {session_id} --in {source}"
```

→ `DIGEST_PROOF_EXTRACT_FAIL:claude-spt: digest extractor failed: no value for substitution key {session_id}`.

**Isolation (same adapter, only the command line differs):**
- **Variant A** — `<exe> --in {source}` (source-only) → `DIGEST_PROOF_OK`, **parsed 5 / dropped 0**,
  rendered digest correct (incl. sprint-collapse `used: Write(src/a.rs), Bash(cargo build)`). Proves
  the extractor + the digest-proof render pipeline are correct.
- **Variant B** — `<exe> --session {session_id} --in {source}` → fails on the unfilled `{session_id}`.

**Why claude-spt needs `{session_id}` on the command (not a workaround — the only correct shape).**
CC's transcript path is `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`. The `<cwd-slug>` subdir
is CC-internal and has **no published key** — and per doyle, *should* not (spt-core stays
harness-agnostic; it must not bake a harness's directory scheme into the key catalog). So `source`
must be the projects **root** and the extractor receives `{session_id}` on the command, resolving
`<slug>/<session_id>.jsonl` itself (the slug is the harness's business). This matches the published
example and what `spt endpoint digest` fills at runtime.

**Root cause (doyle, spt-core).** `digest-proof --sample` builds an **empty** substitution-key map
(`cli.rs:5135`, `let keys = BTreeMap::new()` — comment admits "placeholders like `{session_id}`
will fail"), whereas the runtime daemon path fills `{id}`+`{session_id}` before running the same
extractor (`digest.rs:208-210`). digest-proof is meant to be the author-time half of the *same*
ADR-0019 engine (§diagnostics), so it must supply the same keys. It doesn't → it is infidelitous to
runtime and false-fails any `{session_id}`-templated extractor, including the published example.

Aside: `{home}` is **not** a catalog key — hard-failing on it is correct. Use `~` for home
expansion in `source` (we switched `source` to `~/.claude/projects`; the extractor also expands a
leading `~/` defensively).

**Status — CONFIRMED-IMPL-BUG; fix in progress (doyle's worktree).** digest-proof will fill
`{id}`+`{session_id}` matching runtime exactly (fidelity: *"passes proof" ⟺ "works at runtime"*),
plus an optional `--session <id>` override (default placeholder). The published contract is right;
the tool is catching up. **Our posture:** the extractor is built to the published contract and
proven (cargo unit tests + Variant A `DIGEST_PROOF_OK`); the digest-proof `int`
(`ci/digest/digest-proof-int.sh`, `REQ-DIST-DIGEST-EXTRACTOR`) **skips on the exact substitution
error** until doyle's fix ships, then flips green on the carrying release.

---

## Finding: no `spt how-to subnet` topic (published-guidance gap, minor)

**Surface:** `spt how-to <topic>` ships canonical, always-current guidance for `ready` and `send`
(the messaging hot path) — the claude-spt `ready`/`send` skills lean on it (`spt how-to ready`).
But there is **no `subnet` topic** (`spt how-to subnet` → `NO_SUCH_TOPIC`), despite `spt subnet`
being a multi-verb, pairing-flow-heavy command (create/show-code/join, 6-digit codes, QR) — exactly
the kind of thing a how-to topic serves well.

**Impact (low):** the `/sptc:subnet` skill wraps `spt subnet --help` as its canonical reference
instead. Works, but inconsistent with ready/send (which get richer how-to prose), and a casual user
running `spt how-to subnet` hits a dead end. **Disposition:** mint a `how-to subnet` topic in
spt-core (cross-machine pairing is the highest-value first-run flow; it deserves the same guided
prose as messaging). Carried as a parity finding — NOT a blocker for `/sptc:subnet`.


record this session's learnings about building an SPT harness adapter in `SPT_HARNESS_ADAPTER_TIPS.md`. only record what applies to the existing public framework, and ignore any docs gaps which may ha…


Findings ledger reviewed. Two things:

1. **how-to-subnet gap** (Phase C): logged but the entry lacks the "Reported to doyle" marker every F-00x entry has → doyle likely not informed. This session (D/E) surfaced **no new** spt-core docs gap (profile overlays, `get-string`, `adapter add` all worked on live v0.7.2; `CLAUDE_CONFIG_DIR` is a CC detail, not spt-core).

Writing the tips file + informing doyle of the subnet gap.