{
  "summary": "## Verdict\n\nThe claude-spt `UserPromptSubmit` timeout **shares the Persistent Project Index root cause**. The hook synchronously invokes `spt whoami --json`; in spt-core 0.31.0, `whoami` is a thin alias for the full `endpoint list` projection. The JSON list path enriches **every local perch** with project history, synchronously traversing Git-backed context branches and deriving cwd project identity. That work already has a pinned 29–35 second reproduction on this node and now measures about 45.6 seconds through `whoami`, so it cannot fit Claude Code's 30-second hook limit.\n\nThere is **no proven endpoint-specific deployah-versus-doyle state discriminator**. The apparent endpoint selectivity is a sampling/threshold effect, and the archived Claude records directly falsify “doyle unaffected”: doyle also timed out. Deployah's two observed `UserPromptSubmit` runs were cancelled at 30,031 ms and 31,629 ms. Doyle had successes at 15,967, 22,821, 24,113, 26,155, 27,362, 27,421, 27,793, and 28,022 ms, but also cancellations at 31,258 ms and 31,386 ms. Thus the same path straddles the hard 30-second boundary depending on ordinary runtime variance/load.\n\n## Shared-root evidence\n\n1. `tools/claude-spt/src/hook.rs:987` reads `SPT_ENDPOINT_ID` at the beginning of `handle_user_prompt_submit`, and uses it for role-edit/stub park operations. However, the normal perched-turn identity block later calls `self_id(env, &sid)` at line 1085 rather than using that already-known ID.\n2. `self_id`, `hook.rs:567-590`, synchronously executes `spt whoami --json`, passing `OWL_SESSION_ID`.\n3. `crates/spt/src/cli.rs:6609-6612` implements `cmd_whoami(json)` by calling `cmd_endpoint_list(None, false, false, false, json)`.\n4. The JSON list path, `cli.rs:2956-2972`, maps over **all local roster entries**, calling `latest_project_ref` for every perch.\n5. `picker/data.rs:456-518` expands each latest-project lookup into `sessions::read_all`, context `branches_by_recency`, one `read_at_tip` membership check per `p-*` branch, and cwd project derivation. The preceding kernel at lines 383-455 shows duplicate cwds are deduplicated and owlery-internal psyche paths are excluded, but every list invocation still repeats global branch/project work.\n6. The existing RCA measured `endpoint list --json` at 30.065 s, 29.232 s repeated, 29.600 s after `subnets: []`, and 35.475 s under process sampling. It observed 128 Git processes and pinned a minimum 104 sequential branch operations at 13 local perches × (one branch enumeration + seven tip reads), before cwd derivation.\n7. The current owlery still has exactly 13 local perch `info.json` records: deployah, doyle, perri, emphasys, todlando, mobile-gw, hertz, lia, flynn, scratch-c, scratch-d, chert, and hall-bf. Removing HFENDULEAM from subnets cannot remove this local projection; the earlier `subnets: []` control already demonstrated that separation.\n8. Safe concurrent controls supplied by the parent investigation used the exact live SIDs: with deployah's `OWL_SESSION_ID`, `spt whoami --json` took 45.569 s and returned self=deployah; with doyle's SID, it took 45.606 s and returned self=doyle. The 37 ms difference is negligible relative to the shared 45.6-second projection and proves identity resolution itself is correct while the command surrounding it is pathologically broad.\n\n## Endpoint comparison\n\n- **Same cwd:** both `info.json` files record `C:\\Users\\decid\\Documents\\projects\\spt-core`; every relevant Claude attachment records the same cwd.\n- **Same hosting/profile:** both report `state=live_agent`, `status=online`, `adapter=claude-spt:ccs`, `home_subnet=SPT_DEV`, `controlled=true`, and the same `CLAUDE_CONFIG_DIR=C:\\Users\\decid\\.ccs\\instances\\bigscreen`.\n- **Same start era:** doyle `started=1784085781`; deployah `started=1784085915`, only 134 seconds apart.\n- **Same hook binary/cache path:** each session's `sessionstart-hook-3.sh` exports its own correct `OWL_SESSION_ID` but caches the identical `SPTC_HOOK_BIN=C:\\Users\\decid\\AppData\\Local\\spt-core\\adapters\\_github\\SaberMage-claude-spt/claude-spt` and `SPT_ADAPTER=claude-spt`. Therefore neither is paying an endpoint-specific adapter-resolution path.\n- **Same installed code:** the pointer record targets the same adapter source; installed manifest is claude-spt 0.20.1 with spt-core floor 0.27.0. The CCS instance globally loads the same sptc plugin 0.1.13; its static dispatcher uses cached `SPTC_HOOK_BIN` first and execs `claude-spt hook <event>` for both sessions. `hooks.json` has no custom timeout override.\n- **Different ledger size, but not causal/selective:** deployah's ledger ends at ordinal 14; doyle's ends at ordinal 353. Most of doyle's excess is repeated psyche/internal cwd history. `project_refs_from` explicitly excludes owlery-internal paths and deduplicates repeated cwd strings before project derivation. More importantly, `whoami` projects both deployah and doyle (and the other 11 perches) on every invocation, so both caller sessions inherit both ledgers. This difference cannot explain one caller being slow and the other fast.\n- **Outcome distributions overlap:** deployah has only two sampled UPS outcomes and both crossed the boundary. Doyle has many samples on both sides of the boundary, including two explicit timeouts. This is exactly the expected shape for a shared 15–45-second path under a 30-second cutoff, not an endpoint routing fork.\n\n## Logs and remaining observability gap\n\nClaude's session JSONL is the authoritative timeout marker available here: it records `hook_cancelled`, `timedOut:true`, `timeoutMs:30000`, the exact command, total `durationMs`, SID, cwd, version, and branch. The adapter's rolling `hook-trace.log` does not log normal `UserPromptSubmit` start/stage/end markers, and the daemon log does not attribute individual project subprocesses to a hook PID. Therefore the only missing artifact is a **per-stage trace for an individual cancelled hook** showing time spent in identity, busy, and poll. That artifact would sharpen attribution within a single invocation, but it is not required to identify the root because source tracing plus the safe same-SID `whoami` controls already isolate the shared 45.6-second command. No endpoint-specific skew is evidenced in existing logs.\n\n## Safe fix shape\n\n1. **Adapter hot-path fix:** in `handle_user_prompt_submit`, use the already-captured nonempty `SPT_ENDPOINT_ID` as the primary ID for normal busy/poll processing. Do not call `self_id`/`whoami` when that authoritative session environment value exists. SessionStart boundary rotation already establishes this precedence at `hook.rs:648+` because resolving a newly rotated SID through whoami is a known catch-22.\n2. **Identity-only fallback:** for sessions lacking `SPT_ENDPOINT_ID`, add/use a core read-only command that maps the current `OWL_SESSION_ID`/session ID to its local endpoint and returns only identity. It must read roster/session binding state and must not call `cmd_endpoint_list`, project history, Git, subnet projection, or network. Preserve `spt whoami`'s documented full-list alias if compatibility requires it; the hook simply must not depend on that broad presentation command.\n3. **Failure behavior:** if neither direct env nor identity-only lookup yields an ID, retain the current no-perch no-op. Do not fall back from an identity-only failure to synchronous full `whoami` on the latency-sensitive hook path; that recreates the timeout.\n4. **Systemic Project Index fix remains necessary:** the daemon-maintained materialized index is still the correct repair for human/JSON endpoint list and picker startup. It would incidentally make the present whoami alias fast, but the hook should independently use the narrow identity contract. The two fixes are complementary: index fixes the shared projection subsystem; direct identity removes an unnecessary load-bearing dependency from a blocking hook.\n5. **Do not treat a larger hook timeout as the fix:** current `whoami` is already ~45.6 seconds, workload scales with perch/branch/cwd counts, and extending the deadline merely moves the failure threshold.\n\n## Safe test shape\n\n- Adapter unit test with `SPT_ENDPOINT_ID=deployah`: normal UPS must issue busy and poll for `deployah`, and the recorder must prove zero `whoami`, endpoint-list, or project calls.\n- Adapter unit test without `SPT_ENDPOINT_ID`: identity-only fallback returns the endpoint for the supplied SID; subsequent busy/poll use that ID; no broad-list call occurs.\n- Adapter unit test with neither identity source: no busy/poll and clean no-op, preserving current no-perch behavior.\n- Precedence test: deliberately make fallback report a different endpoint while `SPT_ENDPOINT_ID` is set; the environment ID must win and fallback must not execute.\n- Core integration test around the identity-only command: correct SID→ID, unknown SID→null/empty, stale binding behavior, and no subnet/network dependency.\n- Deterministic complexity test using a Git/process spy: a normal UserPromptSubmit with a valid direct ID must spawn zero Git children. An identity-only fallback must also spawn zero Git children, even with a 13-perch/7-branch fixture.\n- Preserve the Persistent Project Index suite from the RCA for list/picker parity, cold/warm index behavior, invalidation, malformed data, last-known-good semantics, and bounded branch/cwd scans. Use wall-clock timing only as a manual acceptance check, not a shared-runner CI assertion.\n- Manual read-only acceptance after implementation: with both exact SIDs, normal non-mutating identity resolution should complete well below the 30-second hook bound and return the correct endpoint. Do not invoke the real UPS hook or poll queues during diagnosis/acceptance setup.",
  "files": [
    {
      "path": "C:/Users/decid/Documents/projects/spt-claude-code/tools/claude-spt/src/hook.rs",
      "description": "Primary adapter path. `self_id` at 567-590 calls `spt whoami --json`; `handle_user_prompt_submit` at 982-1125 reads `SPT_ENDPOINT_ID` but later uses `self_id` before busy/poll. `SysEnv::spt` at 1654-1672 waits synchronously with no adapter-side timeout."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs",
      "description": "Core command coupling. `cmd_whoami` at 6609-6612 delegates to full `cmd_endpoint_list`; JSON list at 2827-2980 projects all local perches and calls `latest_project_ref` per perch."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt/src/picker/data.rs",
      "description": "Project-history fanout. Lines 383-518 show cwd deduplication/owlery exclusion, session reads, context branch enumeration, per-branch tip membership reads, and cwd project derivation."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/.claude/reports/2026-07-10-hertz-session/03-cli-project-index-latency-rca.md",
      "description": "Canonical Persistent Project Index RCA: exact 29–35 s controls, 128 observed Git processes, 13×7 fanout calculation, subnet-removal negative control, and agreed materialized-index architecture/tests."
    },
    {
      "path": "C:/Users/decid/.ccs/instances/bigscreen/projects/C--Users-decid-Documents-projects-spt-core/89cc9038-e5d9-429e-b4a9-40fba8af71ee.jsonl",
      "description": "Deployah Claude trace: UserPromptSubmit cancellations at 30,031 ms and 31,629 ms, both with timeoutMs=30,000, exact SID/cwd/command evidence."
    },
    {
      "path": "C:/Users/decid/.ccs/instances/bigscreen/projects/C--Users-decid-Documents-projects-spt-core/1fae2b4c-9daf-44fc-afe5-06c410ac69b8.jsonl",
      "description": "Doyle Claude trace: successful UPS durations from 15,967–28,022 ms plus explicit 31,258 ms and 31,386 ms timeouts; disproves endpoint-specific immunity."
    },
    {
      "path": "C:/Users/decid/.ccs/instances/bigscreen/session-env/89cc9038-e5d9-429e-b4a9-40fba8af71ee/sessionstart-hook-3.sh",
      "description": "Deployah session exports: correct SID and shared cached claude-spt binary path/profile."
    },
    {
      "path": "C:/Users/decid/.ccs/instances/bigscreen/session-env/1fae2b4c-9daf-44fc-afe5-06c410ac69b8/sessionstart-hook-3.sh",
      "description": "Doyle session exports: correct SID and the same cached claude-spt binary path/profile."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/owlery/deployah/info.json",
      "description": "Deployah live metadata: cwd, adapter profile, status, start time, home subnet, control state, and CLAUDE_CONFIG_DIR."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/owlery/doyle/info.json",
      "description": "Doyle live metadata: matches deployah's cwd/profile/config/state and near-identical start era."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/owlery/deployah/sessions.log",
      "description": "Deployah session ledger, 15 entries ending at ordinal 14; recent external cwd is spt-core."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/owlery/doyle/sessions.log",
      "description": "Doyle ledger ending at ordinal 353; many excess entries are repeated owlery-internal psyche cwds that project code excludes."
    },
    {
      "path": "C:/Users/decid/.claude/plugins/cache/cplugs/sptc/0.1.13/hooks/dispatch.sh",
      "description": "Shared static dispatcher. Uses cached SPTC_HOOK_BIN first and execs the same installed binary; no endpoint-specific routing."
    },
    {
      "path": "C:/Users/decid/.claude/plugins/cache/cplugs/sptc/0.1.13/hooks/hooks.json",
      "description": "Shared hook registration; UPS runs the dispatcher with no custom timeout field."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-claude-spt/manifest.toml",
      "description": "Installed adapter manifest, version 0.20.1 and min_spt_core_version 0.27.0."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/adapters/claude-spt/record.toml",
      "description": "Pointer-mode adapter registration to the single shared installed source directory."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-claude-spt/hook-trace.log",
      "description": "Rolling adapter diagnostics. Contains event-specific diagnostics but no normal UPS start/stage/end timing, defining the remaining per-stage observability gap."
    },
    {
      "path": "C:/Users/decid/.claude/reference_docs/claude-code-hooks.md",
      "description": "Hook behavior reference: command hooks are synchronous and blocking for UserPromptSubmit; configuration supports timeout but the installed hook supplies none."
    }
  ],
  "architecture": "Current blocking path:\n\nClaude Code UserPromptSubmit (30 s ceiling)\n→ shared sptc 0.1.13 `dispatch.sh`\n→ cached shared claude-spt 0.20.1 binary\n→ `handle_user_prompt_submit`\n→ ignores already-read `SPT_ENDPOINT_ID` for normal identity\n→ `self_id`\n→ `spt whoami --json`\n→ `cmd_whoami`\n→ full `cmd_endpoint_list --json`\n→ enumerate 13 local perches\n→ for each perch: session ledger + all Git context branches + cwd project identity\n→ only after this finishes: state busy → poll → render context.\n\nThe 30-second cancellation usually occurs inside the broad identity call, before the hook can reach its actual busy/poll work. Since both endpoint SIDs select the same node-wide list projection, caller identity changes only the returned SELF pin, not the expensive global data set.\n\nTarget architecture:\n\nUserPromptSubmit\n→ if `SPT_ENDPOINT_ID` nonempty: use it directly (O(1), no subprocess for identity)\n→ else: narrow SID→endpoint read-only core API (O(1), no Git/network/project projection)\n→ busy → poll → render.\n\nSeparately:\n\nendpoint list / picker / presentation-oriented whoami\n→ read compact persisted Project Index\n→ join local roster\n→ return last-known data immediately\n\nwhile daemon asynchronously and single-flight refreshes branch/cwd-derived project metadata. This separation makes hook identity independent of presentation enrichment while fixing the systemic CLI latency at its source."
}