# CLI Latency RCA: Synchronous Git Project Projection

**Owner:** hertz  
**Date:** 2026-07-10  
**Status:** Root pinned; persistent materialized-index direction agreed with operator

## Problem

Commands including `spt endpoint list --json` and bare/partial `spt endpoint run` take 15–35 seconds. Removing HFENDULEAM from every subnet did not improve latency.

## Reproduction

Measured on HFENDULEAM:

| Command | Wall time |
|---|---:|
| `spt daemon status --json` | 0.335 s |
| `spt api endpoint-info hertz --json` | 1.210 s |
| `spt endpoint run --help` | 0.168 s |
| `spt endpoint list --json` | 30.065 s |
| repeated list | 29.232 s |
| list after `subnets: []` | 29.600 s |
| process-sampled list | 35.475 s |

The sampled list spawned 128 observed Git processes: 16 `for-each-ref`, 88 `git show`, 21 `remote get-url`, and 3 `rev-parse` children. Some observations include Git launcher/child pairs, but they directly establish the local process/filesystem hotspot.

The correlated daemon log contained only ordinary five-second brain connection write-start/clean-close records. There were no poisoned writes, PTY stalls, peer-pump failures, or retirements attributable to the command.

## Pinned root

`cmd_endpoint_list` computes a latest project for every local perch (`crates/spt/src/cli.rs:2954-2978`). It calls:

`latest_project_ref -> project_refs_for -> project_history_for`

(`crates/spt/src/picker/data.rs:456-518`).

For each endpoint, the path:

1. reads session history;
2. enumerates all Git-backed context branches;
3. runs a tip lookup for that endpoint on every `p-*` branch;
4. derives projects for recorded working directories with additional Git commands.

HFENDULEAM had 13 local perches and 7 project branches. Minimum branch work was therefore:

`13 × (1 branches_by_recency + 7 read_at_tip) = 104` sequential Git operations,

before cwd project derivation. `project_id_and_display_for_dir` runs `git remote get-url origin` and possibly `git rev-parse --show-toplevel`, each with a ten-second bound (`crates/spt-store/src/project.rs:24,93-142`). The observed approximately 30 seconds is cumulative ordinary process overhead, not a single timeout.

## Relationship to prior issues

This is independent of both prior diagnoses:

- It persisted after subnet removal and with `subnets: []`, so mesh isolation is not causal.
- The JSON list path projects persisted snapshots and local files; it performs no network dial.
- The daemon remained responsive and emitted no write-poison/PTy-stall signals, so the historical `SharedSend` freeze is not causal.

## Endpoint-run distinction

Dispatch is at `crates/spt/src/cli.rs:1331-1365` and `1679-1813`:

- Bare run, adapter-only run, and an unresolved/new ID enter the picker.
- Picker startup calls `gather_endpoints`, so it inherits the same project-history fanout before the TUI appears (`picker/mod.rs:40-72`).
- Fully qualified `--adapter A --id E` is direct and bypasses the picker.
- A lone existing `--id E` is direct when the perch records its adapter.
- Direct attach/view has a separate 25-second broker-session gate (`cli.rs:2049-2067,2395-2409`). `--start` bypasses that post-launch wait.

## Agreed architecture

Build a persistent, daemon-maintained materialized project index. User-facing list, picker, and endpoint-info reads must perform no Git work.

### Reader contract

1. Read one compact, versioned index.
2. Join it with the local perch roster.
3. Return immediately.
4. Use last-known data or `-` when stale/missing.
5. Never fall back to synchronous Git enrichment.
6. Allow daemon-offline readers to consume the last persisted snapshot.

### Ownership and writer contract

`spt-store` owns the versioned materialized-index format and read path, which preserves daemon-offline reads. The daemon is the sole single-flight writer:

1. Load the persisted index immediately at startup.
2. Become ready without waiting for validation.
3. Warm/reconcile in the background.
4. Enumerate branches once.
5. List each changed `p-*` branch tree once and derive all endpoint memberships.
6. Normalize/deduplicate cwd values and resolve each distinct cwd once.
7. Atomically replace the index.
8. Coalesce invalidations and preserve last-known-good data on failure.

### Invalidation

- New session/cwd: refresh that endpoint and shared cwd cache.
- Context-store mutation: schedule one debounced global membership refresh.
- No single authoritative context-store commit path currently exists, so freshness uses branch-tip fingerprinting plus periodic reconciliation rather than relying on a writer-maintained generation counter.
- Bind/start, rename, fork, purge: update affected endpoint rows.
- Cwd identity refresh only when path/repository identity or `.git/config` changes; ordinary commits do not alter project identity.

### Complexity target

Replace `O(P × B + C)` subprocess fanout with `O(P + B + F + C)` batched work, where `F` is relevant branch-tree entries. Prefer in-process BranchStore traversal or a fixed number of Git plumbing calls; moving the current 100+ process loop into a background thread is not acceptable.

### Index observability

Expose generated time, source generation, pending refresh, last duration/error, endpoint/project/cwd counts, cache hits/misses, stale reads, and repair count. Index presence alone is not health.

## Behavioral precedence

Preserve current result ordering:

1. newest usable session cwd;
2. origin cwd;
3. context branches by recency.

Optimization must not change rendered project IDs/display names.

## Regression tests

1. Behavioral parity for session/origin/context precedence.
2. Cold start: daemon ready and CLI fast before background index completes.
3. Warm start: persisted index immediately readable; unchanged generation performs no scan.
4. Session, context, rename, fork, and purge invalidation.
5. Multiple invalidations coalesce into one refresh.
6. Git unavailable, branch malformed/locked, cwd deleted, index truncated, and schema mismatch preserve fast reads and last-known-good data.
7. Complexity counters prove one branch enumeration, at most one tree scan per changed branch, and one derivation per distinct normalized cwd.
8. A 13-perch/7-branch fixture is used for manual latency acceptance; strict wall-clock timing is not a shared-runner CI gate.
9. Bare picker shares the indexed projection; fully qualified direct run does not invoke it.
10. Direct-run 25-second session gating remains separately tested and observable.

## Agreed milestone disposition

Treat this as a user-facing CLI responsiveness and derived-state architecture item. Plan the persistent `spt-store` index plus daemon single-writer refresh as one subsystem, not a narrow memoization patch. Complexity counters are the deterministic CI gate; wall-clock latency is manual acceptance. The plan follows Mesh Recovery in Doyle's recommended next-milestone order, subject to the operator's priority decision at the v0.32.0 boundary.
