{
  "summary": "ROOT-CAUSE VERDICT: **Yes, the UserPromptSubmit timeout shares the Persistent Project Index root.** The adapter’s ordinary UserPromptSubmit path always executes `spt whoami --json` through `self_id` (`spt-claude-code/tools/claude-spt/src/hook.rs:567-584, 1085`), even though the handler already read `SPT_ENDPOINT_ID` at `hook.rs:987`. In spt-core 0.31.0, `whoami` is not an identity query: `cmd_whoami` is a full alias to `cmd_endpoint_list` (`spt-core/crates/spt/src/cli.rs:6608-6612`). The JSON list path projects the latest project for every local perch (`cli.rs:2954-2978`), which traverses session/project history (`picker/data.rs:462-516`) and launches serial bounded Git subprocesses (`spt-store/src/project.rs:93-132`; `branchstore.rs:237-320`; `gitrun.rs:18-23, 48-123, 148-165`). The adapter waits synchronously and without its own deadline for every `spt` child (`hook.rs:1655-1670`; strict path `1704-1710`). Because `hooks.json:6-8` supplies no timeout override, Claude Code’s observed 30-second hook ceiling kills the hook before `whoami` returns.\n\n**Shared root, no endpoint-specific source discriminator on the current HFENDULEAM controls.** Main’s safe controls with the exact current SIDs measured `OWL_SESSION_ID=deployah spt whoami --json` at 45.569 s and Doyle at 45.606 s; both returned the correct self. The on-disk endpoint records also put both endpoints in `C:\\Users\\decid\\Documents\\projects\\spt-core`, online, controlled, and on `claude-spt:ccs`. Both commands run the same full local-perch projection. Removing HFENDULEAM from subnets cannot remove the local project projection because the local loop is unconditional after subnet gathering. The reported deployah-affected/doyle-unaffected split is therefore a threshold/runtime observation, not a different code path or a subnet discriminator, unless separate Claude hook logs later prove different prompt branches or environments. Both endpoints are currently vulnerable.\n\n**Safe fix shape:** remove identity from the roster surface. For spt-hosted sessions, use `SPT_ENDPOINT_ID` only when it is proven bound to the current hook payload SID by the adapter’s session carrier. First fix carrier custody so `state/session/<id>.sid` advances only after a successful strict bind/boundary; today it advances unconditionally even after a logged registration failure (`hook.rs:709-769`), so carrier equality alone is not yet a safe proof. For missing/mismatched carrier and harness-hosted sessions, add a narrow core `api self-id`/`session-identity` JSON command that calls the existing `roster::detect_self_id` with `OWL_SESSION_ID` overridden to the hook payload SID, but never calls endpoint-list, project history, or Git. Do not use existing `api endpoint-info` as the fallback: it also calls `latest_project_ref` at `api/reporting.rs:953`. This preserves the current session-ID-first identity semantics while removing Persistent Project Index from the hook deadline.",
  "files": [
    {
      "path": "C:/Users/decid/Documents/projects/spt-claude-code/plugin/sptc/hooks/hooks.json",
      "description": "Lines 6-8 wire UserPromptSubmit to the shell dispatcher. The command hook has no explicit timeout override."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-claude-code/plugin/sptc/hooks/dispatch.sh",
      "description": "Lines 20-67 are the complete shell path. On a cache miss, lines 41-45 synchronously run `spt adapter get-string claude-spt hook_cmd`; lines 55-60 cache only during SessionStart; line 67 `exec`s the hook binary, so Claude Code waits for the binary’s entire synchronous call chain."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-claude-code/tools/claude-spt/src/hook.rs",
      "description": "Primary adapter RCA. `self_id` at 567-584 runs `spt whoami --json`. UserPromptSubmit is 978-1117, reads `SPT_ENDPOINT_ID` at 987 but still calls `self_id` at 1085. All production `spt` executions wait without an adapter deadline at 1655-1710. Session carrier definition/use is at 328-335, 698-765; carrier write is currently unconditional after registration at 764-769. Existing `HookEnv`/Recorder is the deterministic adapter test seam."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml",
      "description": "Lines 884-886 declare `SPT_ENDPOINT_ID` as broker-injected `{id}`. Lines 110-114 document the existing session carrier. Lines 435-451 and 726-765 document/scrub the known identity-env leak class, which is why a bare unvalidated environment fast path would weaken semantics."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs",
      "description": "Core shared root. Command dispatch routes Whoami at 1263; `cmd_whoami` at 6608-6612 calls full `cmd_endpoint_list`. In JSON mode, local endpoint project projection occurs at 2954-2978. The human path does the same at 3045-3063. Requirement comments confirm endpoint list/whoami merge the full local roster."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt/src/picker/data.rs",
      "description": "Persistent Project Index chain. `latest_project_ref` 456-477 expands the full `project_refs_for`; `project_refs_from` 399-453 derives each unique non-owlery CWD; `project_history_for` 484-516 enumerates BranchStore branches and performs `read_at_tip` for every `p-*` branch."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt-store/src/project.rs",
      "description": "Lines 93-132 synchronously run `git -C <cwd> remote get-url origin` with a 10-second budget, then, on failure/non-success, `git -C <cwd> rev-parse --show-toplevel` with another 10-second budget. This is repeated for every unique CWD projected."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt-store/src/branchstore.rs",
      "description": "`branches_by_recency` at 237-257 launches a Git ref scan; `read_at_tip`/`read_at` at 280-314 launches one `git show` per project branch. `open_or_init` at 42-57 can launch additional init/config Git children only when the store HEAD is absent."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt-store/src/gitrun.rs",
      "description": "Defines 30-second general Git budget at 18-23, spawns Git at 48-89, polls/kills at 101-137, and applies the 30-second default in `run_git_ok` at 148-165. Individual Git calls are bounded, but the projection performs them serially, so cumulative runtime can greatly exceed the hook’s 30-second ceiling."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt/src/roster.rs",
      "description": "Lines 92-183 define the identity semantics that must be preserved: OWL_SESSION_ID exact session match first, then SPT_AGENT_ID, then a bound-perch-validated SPT_ENDPOINT_ID, then process ancestry. A new identity-only API should reuse this resolver rather than inventing weaker adapter-side identity rules."
    },
    {
      "path": "C:/Users/decid/Documents/projects/spt-core/crates/spt/src/api/reporting.rs",
      "description": "Existing `api endpoint-info` is not a safe latency fallback. Although it self-resolves through `roster::detect_self_id` at 915-935, it immediately projects project history via `latest_project_ref` at 953-954."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/owlery/deployah/info.json",
      "description": "Runtime configuration evidence: deployah’s current SID is `89cc9038-e5d9-429e-b4a9-40fba8af71ee`; cwd is the spt-core project; adapter is `claude-spt:ccs`; endpoint is online and controlled."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/owlery/doyle/info.json",
      "description": "Runtime control evidence: Doyle’s current SID is `1fae2b4c-9daf-44fc-afe5-06c410ac69b8`; same cwd, adapter profile, online/controlled state, and start era as deployah."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/owlery/deployah/sessions.log",
      "description": "Deployah ledger contains the spt-core cwd. Its latest row matches the current deployah SID."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/owlery/doyle/sessions.log",
      "description": "Doyle ledger contains the same spt-core cwd plus historical owlery-internal Psyche paths. `project_refs_from` excludes paths under owlery before project derivation (`picker/data.rs:434-438`). More importantly, both whoami calls project every local perch, so caller identity does not isolate either ledger."
    },
    {
      "path": "C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-claude-spt/hook-trace.log",
      "description": "The rolling trace contains explicit diagnostics but no UserPromptSubmit start/end or per-subprocess timing. `SysEnv::log` is the only trace writer (`hook.rs:1745-1754`), and the normal UPS path does not log timing, so this file cannot establish a deployah-vs-Doyle latency discriminator."
    }
  ],
  "architecture": "## Exact synchronous call graph\n\n```text\nClaude Code UserPromptSubmit\n  -> hooks.json:6-8\n  -> sh dispatch.sh UserPromptSubmit\n       -> [only if SPTC_HOOK_BIN missing]\n          spt adapter get-string claude-spt hook_cmd       dispatch.sh:41-45\n       -> exec claude-spt hook UserPromptSubmit             dispatch.sh:67\n            -> main::Sub::Hook -> hook::run                 main.rs:59-60, 91-93\n            -> hook::run_event                              hook.rs:1879-1927\n            -> dispatch(\"UserPromptSubmit\")                 hook.rs:1556-1562\n            -> handle_user_prompt_submit                    hook.rs:984-1117\n                 -> conditional prompt-specific subprocesses\n                 -> self_id                                 hook.rs:1085\n                      -> spt whoami --json                   hook.rs:579-584\n                           -> Cmd::Whoami                    core cli.rs:1263\n                           -> cmd_whoami                     core cli.rs:6609-6612\n                           -> cmd_endpoint_list(json=true)   core cli.rs:2827-2985\n                                -> roster::detect_self_id\n                                -> roster::enumerate\n                                -> for EVERY local perch:\n                                   latest_project_ref        cli.rs:2954-2978\n                                     -> project_refs_for     picker/data.rs:462-477\n                                       -> sessions::read_all\n                                       -> project_history_for\n                                          -> git for-each-ref\n                                          -> git show per p-* branch\n                                       -> derive every unique non-owlery cwd\n                                          -> git remote get-url origin [10s]\n                                          -> if needed git rev-parse [10s]\n                 -> if self exists:\n                    spt api ... state busy                  hook.rs:1088\n                    spt api ... poll --include-deferred      hook.rs:1089-1091\n                    [only if poll has update notification]\n                    spt --version                            hook.rs:1103-1104\n```\n\n## Every direct synchronous subprocess reachable from UserPromptSubmit\n\nAll calls below are `HookEnv::{spt,spt_send,spt_strict}` production calls and block on child completion. None has an adapter-side timeout.\n\n1. **Dispatcher cold-cache resolution:** `spt adapter get-string claude-spt hook_cmd`, only when `SPTC_HOOK_BIN` is absent (`dispatch.sh:41-45`). SessionStart normally caches it, but a missing/not-exported cache makes this an extra per-prompt child.\n2. **Role-edit submission:** `spt endpoint role --overwrite ...` (`hook.rs:950-963`) and optional `spt endpoint description set ...` (`965-972`). Both strict and synchronous.\n3. **Bare `/sptc:role` round trip:** `endpoint role --id`, optional `endpoint description show --id`, then `send --force-native` (`1004-1019`).\n4. **Any `/sptc:<skill>` injection:** `spt adapter get-string claude-spt skills.<skill>` through `inject_skill -> get_string` (`599-610`, call sites `1024-1027`). A normal prompt has an empty skill and skips this.\n5. **`/sptc:live` or `/live`:** `spt endpoint list --show-all` (`1077-1078`). The live skill prompt generally also performs item 4, then later the always-on whoami, so it may project the roster twice in one hook.\n6. **Always:** `spt whoami --json` through `self_id` (`579-584`, call at `1085`). This is the timeout root.\n7. **Only when self resolved:** `spt api ... state busy` (`1088`) and `spt api ... poll ... --include-deferred` (`1089-1091`).\n8. **Only when the poll carried an spt-update notification:** `spt --version` (`1103-1104`).\n\nA plain perched prompt therefore performs three serial `spt` children after dispatch resolution: `whoami`, `state busy`, `poll`. Current control timings show the first child alone takes about 45.6 seconds, so the later two cannot complete within Claude Code’s 30-second hook window.\n\n## Why the project projection exceeds the deadline\n\n`cmd_endpoint_list(json=true)` does not stop after computing `self_pin`. It creates `all_local = roster::enumerate()` and then maps every local perch into `LocalPerchJson`, calling `latest_project_ref` on each (`cli.rs:2902-2914, 2954-2978`). `latest_project_ref` computes the full history before taking `.next()` (`picker/data.rs:462-477`), so “latest” is not cheap:\n\n- reads up to 64 session ledger rows per endpoint (`sessions.rs:29-31, 99-102`);\n- deduplicates CWDs but derives every unique non-owlery CWD (`picker/data.rs:424-445`);\n- each CWD may cost two serial 10-second Git children (`project.rs:93-132`);\n- per endpoint, enumerates every context-store branch with a 30-second-budget Git call and checks every `p-*` branch with another 30-second-budget `git show` (`picker/data.rs:493-516`; `branchstore.rs:237-320`; `gitrun.rs:148-165`).\n\nThe timeouts are per Git child, not an aggregate projection deadline. Serial bounded calls can therefore sum to far more than 30 seconds. The adapter then adds no shorter deadline of its own.\n\n## Shared root versus endpoint discriminator\n\n**Shared root: confirmed.** The exact same `cmd_endpoint_list` function underlies both the previously slow `endpoint list --json` and hook-side `whoami --json`. This is not merely a similar project lookup; it is the same function call.\n\n**Endpoint-specific discriminator: not found in the current same-node pair.** With the supplied exact SIDs, both commands took the same 45.6 seconds and returned the correct self. Both endpoint records have the same cwd/profile/state. The caller SID changes only which endpoint becomes the `self` pin. It does not scope the local projection; every local perch is projected for both callers.\n\nSource-level conditions that can make two hook invocations differ are limited to:\n\n- `SPTC_HOOK_BIN` cache present versus absent, adding dispatcher `get-string`;\n- prompt branch: ordinary versus slash skill, role edit, or `/live`;\n- whether self resolution succeeds, which gates state/poll/version;\n- different `SPT_HOME`, node, local roster, or stored project history;\n- transient scheduling/filesystem/Git behavior near the hard external deadline.\n\nNone separates current deployah and Doyle under the controlled `whoami` runs. Subnet membership is not the discriminator: the local-perch project loop is unconditional, and both callers see the same local store. The observed one-endpoint symptom is best classified as **threshold/runtime selectivity** until a hook start/end trace proves a different branch.\n\n## Identity-preserving dependency removal\n\n### Recommended two-tier resolver\n\n1. **Proven direct-env fast path for spt-hosted sessions.** `handle_user_prompt_submit` already reads `SPT_ENDPOINT_ID` (`hook.rs:987`), and the manifest injects it from the core-owned endpoint `{id}` (`claude-spt.toml:869-886`). Return that ID without a subprocess only when the adapter’s `state/session/<id>.sid` carrier exactly equals the UserPromptSubmit payload `session_id`.\n2. **Identity-only core fallback.** For a missing/mismatched carrier or a harness-hosted session with no `SPT_ENDPOINT_ID`, run a new narrow JSON command such as `spt api self-id`. The adapter must override `OWL_SESSION_ID` with the payload SID exactly as current `self_id` does. Core should call `roster::detect_self_id`, preserving its ordering and validation, and return only the identity DTO. It must not call `cmd_endpoint_list`, `latest_project_ref`, BranchStore, or Git.\n\n### Required carrier correction before tier 1 is safe\n\nCurrent SessionStart writes the carrier after registration regardless of whether strict bind/boundary succeeded (`hook.rs:709-769`). A leaked/stale `SPT_ENDPOINT_ID` can therefore acquire a matching carrier even after core rejected the registration. Using current carrier equality as proof would preserve the old F-036 seat-theft class in a new form.\n\nMake strict registration return/record success. Advance `state/session/<id>.sid` only after successful bind/boundary. On refusal, leave the prior carrier intact and log as today. Then `(injected endpoint id, current payload SID, successfully advanced carrier)` represents the same association core accepted.\n\n### Why not simpler alternatives\n\n- **Bare `SPT_ENDPOINT_ID` without proof:** weaker than current session-ID-first `roster::detect_self_id`; known daemon identity-env leakage previously stole Doyle’s seat (`claude-spt.toml:435-451`). Reject.\n- **Existing `api endpoint-info`:** still invokes `latest_project_ref` (`reporting.rs:953-954`). It retains the dependency. Reject.\n- **Keep `whoami`, add a larger hook timeout:** masks the projection and makes every prompt wait tens of seconds. It also leaves cumulative Git budgets capable of exceeding the new limit. Reject.\n- **Change `whoami --json` alone into a narrow command:** conflicts with the explicit `REQ-WHOAMI-1` full endpoint-list alias and the committed endpoint-list JSON shape (`traceable-reqs.toml:524-529, 1500-1502`). A new harness-contract API is cleaner.\n- **Fail open to the raw env when identity API times out:** weakens identity. Reject. A missing identity should remain no-perch/empty, as current parse failure does.\n\n## Deterministic regression seam\n\n### Adapter tests, no processes or clocks\n\nUse the existing `HookEnv` Recorder, already designed to record exact `spt` calls (`hook.rs:48-89` and tests beginning around `2307`). Add:\n\n1. `ups_verified_endpoint_env_skips_identity_subprocess`: environment has `SPT_ENDPOINT_ID=deployah`, carrier contains the same payload SID. Call `handle_user_prompt_submit` with an ordinary prompt. Assert no call starts with `whoami`, `endpoint list`, `endpoint-info`, or identity fallback; assert `state busy` precedes `poll` and both use `deployah` plus the payload SID. This fails on current line 1085.\n2. `ups_mismatched_carrier_uses_identity_only_fallback`: env says deployah but carrier differs. Recorder returns `{\"self\":{\"id\":\"deployah\"}}` only for the new identity API. Assert exactly one identity query, with extra `OWL_SESSION_ID=<payload sid>`, and zero whoami/list calls.\n3. `ups_identity_fallback_null_is_no_perch`: no env/carrier and identity API returns null. Assert no state/poll and no reachability notice, preserving current semantics.\n4. `failed_bind_does_not_advance_sid_carrier`: make `spt_strict(bind)` fail. Assert old carrier remains and the failure is logged. Pair with successful bind/boundary tests that assert advancement.\n5. Keep the existing `whoami_json_self_id_contract` parser tests (`hook.rs:2323-2331`) only if the new identity API reuses the same JSON envelope; otherwise replace them with the new committed DTO parser.\n\n### Core tests, deterministic “no Persistent Project Index” proof\n\n1. Add command parse/dispatch unit coverage for `spt api self-id`.\n2. Unit-test the identity DTO over an injected detector: exact ID, no-self refusal/null contract, JSON field stability.\n3. Add a child-process e2e with isolated `SPT_HOME`, seeded `info.json/session_id`, and a fake `git` executable first on that child’s PATH. The fake writes a sentinel file if invoked. Run only `spt api self-id` with the matching `OWL_SESSION_ID`; assert correct identity JSON and that the sentinel does not exist. This proves absence of project/Git projection without a flaky wall-clock threshold or mutating live endpoint state.\n4. Optionally run `endpoint list --json` against the same fixture as a positive control and assert the Git sentinel is written. That proves the poison seam is capable of detecting the forbidden dependency.\n\nNo source edits, builds, daemon actions, hook executions, poll drains, or endpoint mutations were performed."
}