---
name: counter38-17-digest-env-verdict
description: "#17 REQ-DIGEST-PROFILE-ENV — digest-proof verdict = SPT-CORE-SIDE; awaiting doyle A/B ruling before build"
metadata: 
  node_type: memory
  type: project
  originSessionId: 7b460723-7bdb-4e3f-970f-3e7ef0cc79ee
---

**#17 FULLY CLOSED spt-core-side: W6 @9305d19 (env-read seam) + W6b @b9e5ce9 (fetcher strategy = the {project} gate close).** The {project} design-gate flagged during W6 was RULED by doyle (CONTEXT-symmetry: give [digest] a `fetcher` strategy mirroring [history]'s — adapter locates+reads+emits, spt-core feeds harness-neutral {session_id}+{cwd}(info.json.cwd)+read-vars, NO {project} slug) and BUILT as W6b/REQ-DIGEST-FETCHER-STRATEGY (impl+unit+int+doc, gate-green). perri loop UNBLOCKED. Details of the W6 env-read seam (still correct + needed — it feeds the fetcher root) below.\n\n_(historical, pre-W6b:)_ The env-read SEAM is built exactly as the ruling below specifies (capture_read_env/inject_read_env_keys/expand_tilde in spt-runtime + InfoJson.read_env + establish_perch capture-at-bind BOTH topologies + daemon activity_spanned resolve + digest-proof live-resolve + MANIFEST.md doc; 4 units; traceable/clippy/nextest green). **BUT eel-a end-to-end is NOT closed — a NEW design-gate surfaced during build (escalated to doyle):** perri's relocated [digest].source must resolve to a per-session FILE, but (a) spt-core provides NO `{project}` key — the CC project-slug is harness-specific munging of cwd, so spt-core inventing it = the exact charter violation FIX-A was rejected for — AND (b) extract_digest PRE-READS `{source}` as a file (digest.rs:99 fs::read), so the CURRENT `source="~/.claude/projects"` (a DIRECTORY root) can't resolve regardless. doyle must rule: (a) provide a harness-neutral cwd-derived key, (b) stop pre-reading a dir as a file + let the extractor locate from a root (conflicts with the "keep stdin-bytes seam" clause), or (c) adapter carries the slug another way. Until then the perri ~2-line manifest loop is BLOCKED. Build details below. Part of [[counter38-field-bug-milestone]]. Respects [[spt-core-harness-boundary-and-grounding]].

**ROOT (reframed, harness-agnostic):** spt-core does NOT hardcode ~/.claude — that base lives in the ADAPTER's `[digest].source` template. Real gap: `fill_template` (spt-runtime runtime.rs:98) only substitutes explicit `{key}`; the manifest's `[env.<VAR>] direction="read"` (`EnvDirection::Read`, manifest.rs:319) is DECLARED BUT DORMANT — only Inject is wired (manifest.rs:1016). **Wire the read direction.**

**FIX (operator-ratified, BUILD):**
1. WIRE `[env] direction="read"` capture: at BIND, spt-core captures ONLY the manifest-DECLARED read-vars (explicit allowlist — NEVER the whole env; privacy) from the session's launch environment. Persist them in perch info.json (additive serde-default). Capture-at-bind because the [digest] extractor runs LATER in daemon/brain context where ambient env is GONE.
2. EXPOSE captured read-vars as fill_template substitution keys → available to `[digest].source` + `[history].locate_template`. spt-core KEEPS locate+read+pipe-stdin (locate_normalize stays; stdin-bytes seam UNTOUCHED — the win over old-B).
3. FALLBACK = the `[env]` directive's own `value` field (today "value to inject; required for inject" — ignored for read; REPURPOSE for read as fallback-when-unset). Resolution order: ambient-captured → declared `value` → neither ⇒ SKIP-DIAGNOSE (diagnostic + "not set", mirror [digest]/[strings] posture, CONTEXT L84; never silent-drop/hard-error).
4. spt-core NAMES no harness var, bakes no ~/.claude. Adapter (perri) declares, AFTER spt-core ships+docs (~2 lines):
   `[env.CLAUDE_CONFIG_DIR] direction="read" value="~/.claude"`
   `[digest] source = "{CLAUDE_CONFIG_DIR}/projects/{project}/{session_id}.jsonl"`
   → one template serves plain CC (var unset→~/.claude) AND ccs (var set→relocated).

**BUILD-VERIFY (doyle):**
- ~/home expansion: doyle found NO tilde-expansion in spt-core — confirm how ~/.claude resolves TODAY (adapter absolute? a {home} key?) so captured + fallback + literal all expand consistently. RESOLVE THIS FIRST.
- Bind capture site: api bind/seed must carry the declared read-vars (bind payload doesn't today). BOTH topologies: harness-hosted (api bind runs IN the ccs-env'd process → reads its OWN env) + spt-hosted (broker knows the spawn env).
- info.json read at the daemon-context digest call site.
- DOCS todlando owns IN the build commit (doc-drift-gated, doc→REQ-DIGEST-PROFILE-ENV tagged): (1) docs/MANIFEST.md — teach [env] direction="read" capture: declared-var allowlist, capture-at-bind, `value` = read-fallback-when-unset, captured read-vars become fill_template subst keys usable in [digest].source + [history].locate_template. (2) harness-contract doc (adapter-facing) — same, framed for blind adapter devs. (3) INTEGRATION-CHECKLIST doc (operator flagged) — add the read-var capture step for an adapter dev relocating a transcript store. (4) doc→REQ tags on the above. CONTEXT.md ALREADY amended by doyle ("Profile-relocated transcript roots — env-read capture", history subsystem) — do NOT re-edit, just CITE it.
- REQ activate impl+unit. UNITS (3): (a) ccs-relocation resolve (var set → relocated path), (b) var-unset → `value` fallback (plain-CC path), (c) declared-but-absent + no value → skip-diagnose. Traceable per-wave activation discipline. perri loop = only the ~2 manifest lines at the end.

**HOLD still applies:** build the WHOLE cluster (#18 + #1 + #17) ONCE, fresh context. #18/#1 unaffected by harness-agnosticism; sequence #17 with them.

**Superseded FIX-A (do NOT build — kept for history):**
1. CAPTURE the endpoint's CLAUDE_CONFIG_DIR at BIND/live-spawn (ambient env present THEN — spt-core launches the harness under the ccs profile env). CRUX: digest runs LATER in daemon/brain context (lifecycle @5s + on-demand) where ambient env is GONE → MUST persist at bind, read at digest. Persist into info.json (additive, serde-default). VERIFY the exact bind capture site: endpoint-run spawn in cli.rs vs brainproc; and that info.json is read at the digest call site in daemon context.
2. INJECT into the digest `keys` map as a GENERIC subst key `{config_dir}` (harness-agnostic = "endpoint config root", NOT "claude's dir") → FALLBACK to the ~/.claude default when unset/absent = full back-compat for non-relocated CC.
3. Adapter `[digest].source` template references `{config_dir}/projects/...` instead of `~/.claude/projects/...` — ONE-LINE manifest change perri makes AFTER spt-core ships the key + docs (NOT an extractor rewrite; keep the stdin-bytes seam — spt-core keeps fs::read+piping, B rejected because it breaks the published stdin-bytes seam for no boundary gain).
HARNESS BOUNDARY: spt-core provides the {config_dir} substitution; the ADAPTER's manifest declares how its transcript path uses it. spt-core does NOT bake CC's ~/.claude layout. [[spt-core-harness-boundary-and-grounding]].
DOCS: teach {config_dir} in MANIFEST.md + harness-contract so perri builds blind. Activate REQ-DIGEST-PROFILE-ENV (impl+unit; the ccs-relocation resolve = the unit). perri loop = ONLY the final manifest-source line, after spt-core ships + docs.

**Original digest-proof evidence (STOPPED-before-build, now ruled):**

**Live evidence (hfenduleam, installed spt @counter-37, eel-a = claude-spt:ccs, session d84df205):**
1. `spt adapter digest-proof claude-spt:ccs --session d84df205…` → `DIGEST_PROOF_EXTRACT_FAIL: reading the digest source failed: ... (os error 3)`. Reproduces NO_DIGEST.
2. SAME `--sample <real ~/.ccs transcript>` → parsed 8 records, dropped 0, clean digest, exit 0. **The extractor WORKS given the transcript** (adapter-side is fine).
3. SAME with `CLAUDE_CONFIG_DIR=~/.ccs/instances/bigscreen` set, no --sample → STILL os error 3 (env is NOT what gates it).

**ROOT (code):** `spt-live/src/digest.rs::extract_digest` (~:96-99): `resolve_source_template` → base `~/.claude/projects` (from manifest `[digest].source`); `fill_template(source, keys)` → path; `std::fs::read(&path)` → maps miss to `DigestExtractError::Read`. **spt-core does the transcript LOCATE itself and READS the file, then pipes bytes to the extractor on STDIN** (`{source}` only informational; test `extracts_records_from_the_located_source` confirms extractor reads stdin). So the manifest's documented env-aware extractor resolver (`claude-spt-digest` prefers `$CLAUDE_CONFIG_DIR/projects`, owlery::claude_projects_root) is DEAD CODE in this flow — spt-core short-circuits it. That's why setting CLAUDE_CONFIG_DIR did nothing: spt-core's locate ignores it. ccs relocates the transcript to `~/.ccs/instances/<account>/projects/<slug>/<session>.jsonl`; spt-core resolves `~/.claude/projects/<slug>/` which doesn't exist → os error 3.

**doyle A/B decision pending (which to build):**
- (A, SMALLER/lower-risk, recommended): spt-core resolves the source under the endpoint's REAL transcript root (persist/carry the ccs CLAUDE_CONFIG_DIR / projects-root per endpoint) instead of bare `~/.claude/projects`. Keeps the stdin-bytes contract.
- (B, matches manifest DESIGN): spt-core stops pre-reading; passes root via `--in` + sets CLAUDE_CONFIG_DIR in the child env from the endpoint's persisted profile env → lets the env-aware extractor locate. Bigger; touches the digest stdin-bytes-vs-extractor-locates contract.
- BOTH need spt-core to persist the per-endpoint CLAUDE_CONFIG_DIR at bind — it is NOT in info.json today; eel-a's `~/.ccs/.../session-env/<sid>/` dir holds only hooks (sessionstart-hook-*.sh), not the env.

Do NOT build #17 until doyle rules A vs B.
