# SymForge Defect Ledger

Plain-named defects (no euphemisms — a missing/broken/wrong feature is a DEFECT, not an "honest gap"). Each is tagged ROOT or SYMPTOM and clustered to its culprit. "Loudly refused / disclosed" means we stopped a *silent* wrong answer — it does NOT mean the defect is resolved.

Status: OPEN | IN-PROGRESS | FIXED. Owner: 012 (this lane) | 013 (predictor lane) | new.
Last updated: 2026-07-02.

> **as_of 2026-06-24 — Overlay redundancy (new finding).** The per-session
> base+overlay `IndexView` overlay is REDUNDANT in production. Every read sees
> edits via the shared live index (single-project `self.index`, `daemon.rs:1446`)
> or the refreshed `entry.base` (cross-project — `refresh_working_set_bases`
> empties the overlay, `daemon.rs:790-798`). Verified @086d6c4. The overlay is a
> deliberate vision seam (012 US3/SC-003), NOT obsolete, but has NO production
> consumer. Decision: `docs/reviews/overlay-redundancy-decision.md`. Honesty
> caveat: low-recall type-usage traces shipped no incompleteness signal —
> **FIXED (012f, R3):** find_references type/value-usage traces now carry a
> targeted recall-confidence caveat (best-effort modes ONLY, mirrors the kind
> filter; names the dynamic/macro/reflective/cross-language gaps; no false
> precision; not blanket noise).
>
> **as_of 2026-06-29 — Overlay TRACK RETIRED (round 3, decision approved).** The
> redundancy finding above is resolved by RETIREMENT, not by building precondition
> #1. Three converging code-verified investigations found the overlay WRITER has
> NO consumer: (1) for single-session speculative edits a per-file undo-log over
> the shared index is cheaper and more capable; (2) AAP — the only named
> in-process embedder — isolates edits at the git-worktree + Firecracker-VM
> boundary and is uniformly commit-then-reindex (Verdict B), never feeding
> uncommitted edits to a shared index; (3) the goal (isolated concurrent edits) is
> already met by OS-level worktree/VM/process isolation, strictly stronger than an
> in-memory overlay. The no-fake policy forbids a dormant writer seam, so Phase 1
> REMOVED the dead write side: `Overlay::{upsert,tombstone,rebase}`, the
> `edit_tools.rs` writer seam, `parse_indexed_for_overlay`, `SessionOverlay` /
> `session_working_set`, and every test that drove them. The LIVE cross-project
> find-fusion read path is byte-identical (`IndexView` is still built with an
> always-empty overlay). Phase 2 (separate, reviewed PR) collapses the
> now-provably-empty overlay scaffolding out of the live path. Design:
> `docs/superpowers/specs/2026-06-29-overlay-track-retirement-design.md`.

## CULPRITS (root causes — these get attacked, not their symptoms)

- **CULPRIT A — the STEL facade is a lossy, fabricating router.** It (1) emits numbers it never measured, (2) silently drops caller params it didn't curate, (3) makes claims it never validated. Every facade trust defect below is a symptom of this one design.
- **CULPRIT B — the engine's multi-view search has no per-view live derived indices and no live rebase.** `WorkingSet`/`IndexView` search runs base-only + overlay post-filter, so cross-project/overlay search is scoping-less, stale, and low-recall.
- **CULPRIT C — `/mcp` is a stateless single-index singleton.** No per-connection session state on the HTTP transport.
- **CULPRIT D — xref extraction incompleteness (`parsing/xref.rs`).** Qualified calls key under the inner identifier (not the type head) and struct/composite literals are never captured, so `find_references` on a type silently misses value/constructor/struct-literal usages. Single-project, ordinary read path — NO overlay/view involvement. (Was mis-filed under Culprit B / per-IndexView derived index; decoupled 2026-06-24, see `docs/reviews/overlay-redundancy-decision.md`.)

## Defects — CULPRIT A (lossy/fabricating facade)

| ID | Defect (plain) | Root/Symptom | Status | Owner |
|----|----------------|--------------|--------|-------|
| D-A0 | The facade routes a curated subset of each tool's params and silently drops the rest; decorates output with values it didn't measure. **PARTIAL (verified @327ed91):** A1a's `classify_param_dispositions` choke point + conformance test routes every param to an explicit disposition; the unmeasured-numbers symptoms (D3/D7) are fixed. RESIDUAL (still open): a caller-set `symbol`/`path` that resolves `NotApplicable` is silently ignored (no caller-visible "ignored on this route" notice, `planner.rs:118-133`), and the planner completeness guard is a `debug_assert!` (compiled out of release, `planner.rs:507-512`). | **ROOT (Culprit A)** | PARTIAL | new |
| D1 | Economics numbers (`saved`/`predicted`/`net`) are hardcoded constants, not measured. The tool reports savings it cannot substantiate. | SYMPTOM(A) | IN-PROGRESS | 013 |
| D2 | serve/degrade/bypass gate decides on the fabricated economics; degrade/bypass branches are unreachable for real plans. | SYMPTOM(A) | OPEN | 013 |
| D3 | `session_net_vs_manual` is a rising gross token counter mislabeled as net savings. **FIXED (verified @327ed91):** renamed `session_tokens_served` ("only ever grows; NOT a net saving", `envelope.rs:32-34`, `tools.rs:8820/8830`); a negative-assert guard locks the old mislabel out (`envelope.rs:241`). | SYMPTOM(A) | FIXED | new |
| D4 | `status` reports an empty/zero index in daemon-proxy mode (reads the wrong process). project_root now surfaced (C4), but the proxy index/ledger numbers still read the front shell. | SYMPTOM(A) | PARTIAL | new |
| D5 | Multi-hop "A-009 VALIDATED" is 3 hardcoded query strings; the validation claim is false. **FIXED (012f, by honest removal — D5/D19):** the fake `plan_multi_hop_steps` 3-string decomposer + its circular golden rows (`routes.golden.jsonl` `chain:multi`) + replay scaffold were DELETED. Adversarial review `wv1e9m9sb` = `NO_REAL_CONSUMER` (the 3 matched strings ARE the golden corpus; only the tests written to exercise them called the code). The 3 ex-multi-hop queries now route honestly through the single-step / find-fusion path (non-empty plan asserted by `planner.rs::ex_multi_hop_queries_plan_non_empty_single_or_fused`); the capability is recorded as SUPERSEDED by the client agentic loop + find-fusion (`status.rs::SUPERSEDED_ITEMS`), not silently dropped. User no-fake policy honored. | SYMPTOM(A) | FIXED (012f) | 012 |
| D6 | Golden replay asserts route *shape*, never answer equivalence; `expected_equiv` is dead; any "95% trajectory" claim is unsupported. **FIXED (verified @327ed91):** the write-only `expected_equiv` field was REMOVED from `GoldenRouteRow` (`stel_core/types.rs:398-403` removal rationale); the 95% claim is already demoted to "not numerically measured" (`phase2-stel-checkpoint.md:119`). Replay-grades-shape-only is now honestly disclosed, not a silent lie. | SYMPTOM(A) | FIXED | new |
| D7 | `symbol` param ignored on read/impact/orient (query token taken as the symbol name). **FIXED (verified @327ed91):** `symbol` is consumed by `symbol_lookup_step`/`symbol_impact_step`/`orient_lookup_step` (`planner.rs:317/363/394`); the step arg is `request.symbol.clone().unwrap_or_else(|| request.query…)` — the query token is only the documented FALLBACK when `symbol` is absent, not a silent override. | SYMPTOM(A) | FIXED | 012 |
| D8 | `path:` reads as a project selector but is a within-project filter; vocabulary trap. Glossary + error added (loud), facade vocabulary still lossy. | SYMPTOM(A) | LOUD-ONLY | new |
| D9 | The `symforge` facade silently dropped `project`/`projects`. Now **loudly refused** — but the defect (facade does not route cross-project) is unfixed. | SYMPTOM(A) | LOUD-ONLY | new |
| D20 | `search_files` is planner-reachable + scope-capable but its input has NO `path_prefix` field, so a caller's `path` is silently dropped on `search_files` routes (path/file ranking runs repo-wide despite the caller scoping). A1b's planner forwarding cannot reach it (no arg to forward into). Found by adversarial review wf a9b73e8. Fix path: add `path_prefix` to `SearchFilesInput` + scope the handler, then add `search_files` to `PATH_PREFIX_FORWARD_TOOLS`. **FIXED (012f, PR #381):** added `path_prefix` to `SearchFilesInput` and threaded a `PathScope` into the PRE-COUNT `path_allowed` predicate inside `capture_search_files_view_with_noise` (so `total_matches`/`overflow_count`/`hits` are all consistently scoped before limiting — the true sibling mirror; an initial post-capture `retain` that let overflow-set files escape scoping was caught in review and corrected), plus `search_files` added to `PATH_PREFIX_FORWARD_TOOLS`. | SYMPTOM(A) | FIXED (012f) | 012 |

## Defects — CULPRIT B (engine multi-view search: no per-view derived index, no live rebase)

| ID | Defect (plain) | Root/Symptom | Status | Owner |
|----|----------------|--------------|--------|-------|
| D-B0 | `WorkingSet`/`IndexView` `search_text`/`find_references` run base-only + overlay post-filter; no per-view derived (trigram/reverse) index; no live rebase on change. **MOOT under the current single-shared-index architecture: no production overlay carries deltas no other path sees (every IndexView branch takes the base-only path under `deltas.is_empty()`; refresh empties overlays). Reframed: BLOCKED on precondition #1 (a real overlay WRITER — write-to-overlay-not-base), NOT on its own implementation. Do NOT build under the single shared live index. DECOUPLED from D13** (D13 is a single-project `xref.rs` defect, now Culprit D). **as_of 2026-06-29 — WON'T-BUILD under the single-shared-index architecture (overlay track retired, round 3): the precondition it was blocked on is itself retired with no consumer. Revival trigger: a future architecture that requires one shared index to reflect multiple consumers' uncommitted pre-merge edits simultaneously (accepting weaker isolation than worktree/VM). See `docs/superpowers/specs/2026-06-29-overlay-track-retirement-design.md`.** See `docs/reviews/overlay-redundancy-decision.md`. | **ROOT (Culprit B)** | WON'T-BUILD | 012 |
| D11 | Cross-project scoping — `path_prefix`/`language`/noise now **HONORED** (B1): threaded through the engine's option-honoring `search_*_with_options` on the empty-overlay cross-project path, built via the SAME helpers as single-project (identical behavior; proven by engine unit test + live daemon-HTTP test). `structural` (separate ast-grep pipeline) and `find_references` `path`/`symbol_kind`/`direction` (selectors / implementations-mode) remain honest capability-refusals — no cross-project entry point. Cross-project rendering of display params (`context`/`group_by`/`follow_refs`) is deferred to A1b (display, NOT scoping). | SYMPTOM(B) | FIXED | 012 |
| D12 | Cross-project reads now stay FRESH after ANY watched change (B2): a LAZY re-intern on the cross-project read path detects a stale interned base (`Arc::ptr_eq` of the entry's snapshot vs the project's current published index) and force-replaces it (`intern_base_refresh`, SC-002-preserving), re-fencing the entry's empty overlay. Proven by a daemon regression test (add/delete freshness, mismatch-gate, lone-non-active, SC-002) + a real-watcher live daemon-HTTP transcript; single-project parity byte-identical + frecency-neutral. PR #369 (745313d). | SYMPTOM(B) | FIXED | 012 |
| D14 | Cross-project results are now per-project `result_limit`-bounded + tier-RANKED before the cap (B1; was an unbounded `usize::MAX` dump then truncate). Output stays grouped-by-project in working-set order; a single GLOBAL relevance interleave across projects is still deferred (adversarial review wf a2eac32 — honest scope). | SYMPTOM(B) | PARTIAL | 012 |
| D15 | Single-project overlay edits in ordinary reads. **DORMANT SEAM (redundant in production), NOT "done."** PR #372 (`22e66ca`, merged) landed the single-project overlay-WRITER. as_of 2026-06-24: the writer is KEPT (gated `None` on the shared instance / local-stdio — byte-identical) as a deliberate 012-spec seam (US3/SC-003); the single-project overlay READ was REMOVED (`tools.rs` get_symbol) because it was redundant (the shared live index already holds the edit via `reindex_after_write`) AND carried a narrow staleness shadow risk (overlay-first, no freshness gate could shadow a base advanced by a watcher event). Read-migration Phases 2-5 PAUSED (no-op parity refactor until edits stop writing through to the shared base). Owner 012. Path to load-bearing = precondition #1 (commit-gated edits) + D16 + D-B0. See `docs/reviews/overlay-redundancy-decision.md`. **as_of 2026-06-29 — SUPERSEDED / REMOVED (overlay track retired, round 3):** the writer track is removed, not kept. Session-private in-index edits are SUPERSEDED by worktree / VM / process isolation (strictly stronger, OS-level, can't leak via a logic bug); the writer had NO consumer (AAP Verdict B: commit-then-reindex at the worktree/VM boundary). Phase 1 deleted the dead write side (`Overlay::{upsert,tombstone,rebase}`, the `edit_tools.rs` seam, `parse_indexed_for_overlay`, `SessionOverlay`/`session_working_set`, and their tests); the live cross-project read path is byte-identical (empty overlay). The paused phased migration plan is deleted as superseded. Design: `docs/superpowers/specs/2026-06-29-overlay-track-retirement-design.md`. | SYMPTOM(B) | SUPERSEDED/REMOVED | 012 |

## Defects — CULPRIT D (xref extraction incompleteness)

| ID | Defect (plain) | Root/Symptom | Status | Owner |
|----|----------------|--------------|--------|-------|
| D13 | Reference trace recall ~29% on type/value usages — `find_references` misses value/constructor/struct-literal sites. **#1 OPEN trust defect (HIGH impact x STRONGEST field evidence):** `find_references(MinimalFilter)` returned 2 definition lines, missed all 5 usages. Root cause (verified @086d6c4): `parsing/xref.rs:18` keys qualified calls (`MinimalFilter::new()`) under the inner identifier (`new`), not the type head; `struct_expression` is never captured (`grep struct_expression src/parsing/xref.rs` -> EMPTY). Scope: `parsing/xref.rs` + one `find_references` lookup branch. Reclassified out of Culprit B (per-IndexView derived index) on 2026-06-24 — single-project, ordinary read path, NO overlay/view involvement. **FIXED (012f):** `find_references` now also matches qualified-call sites by their IMMEDIATE QUALIFIER (the segment before the leaf) at any path depth (`X::method()`, `a::b::X::method()`), for Rust + C++ (C++ gained an `@ref.qualified_call` envelope, `xref.rs:238`). The "`struct_expression` never captured" premise was FALSE — the unscoped `(type_identifier)` capture already lands struct/composite literals under the head, so no query change there (adding `struct_expression` only duplicated). Proven by a reproduced 29%→all-5-sites-resolve fixture (non-vacuous: pre-fix returned 0 for the path-qualified site). Tracked-minor: the head-match also surfaces Import/TypeUsage multi-segment refs on their immediate qualifier — count-neutral noise (one head either way), byte_range points at the leaf token; tighten by gating to call kinds if it ever matters. | **ROOT (Culprit D)** | FIXED (012f) | 012 |

## Defects — CULPRIT C (transport) + independent

| ID | Defect (plain) | Root/Symptom | Status | Owner |
|----|----------------|--------------|--------|-------|
| D16 | `/mcp` is a stateless single-`SymForgeServer`-over-one-index; remote multi-tenant/multi-project impossible. **C-stopgap (012d): the silent-wrong HALF is CONTAINED** — `/mcp` loudly refuses `project`/`projects` on the three cross-project tools via `local_cross_project_refusal` (no daemon behind the handler → `proxy_tool_call`=`None` because `/mcp`'s `SymForgeServer::new` leaves `daemon_client=None`). Already wired since Phase 3 (`278864c`); 012d adds the missing regression lock (`tests/serve_http_attach.rs`) + names the `/mcp` transport in the refusal. The ROOT (per-connection `/mcp` daemon session) stays OPEN/tracked-large. **as_of 2026-06-29 — DECOUPLED from the overlay track (retired, round 3): D16 is an INDEPENDENT future feature with its own coarse-grained per-connection isolation if multi-tenant `/mcp` serving is ever wanted; it was NEVER blocked on the overlay and is unaffected by the retirement. See `docs/superpowers/specs/2026-06-29-overlay-track-retirement-design.md`.** | **ROOT (Culprit C)** | OPEN (silent-half CONTAINED) | new |
| D17 | open-vs-close TOCTOU race in the daemon (fail-loud, pre-existing on main). **FIXED (verified @327ed91; ledger row was stale vs HANDOFF):** `register_session_for_existing_project` does the contains-check AND `session_ids.insert` under ONE `projects.write()` hold, recovering by reload-under-lock if a concurrent close reaped the project (`daemon.rs:838-877`) — the race window is eliminated by recovery, not narrowed. Stress tests `test_concurrent_session_lifecycle_no_deadlock_or_panic` + `test_open_close_race_no_panic` (`daemon.rs:5035/8195`). (A disk error in `ProjectInstance::load` can still propagate — that is a fail-loud I/O error, not the race.) | ROOT (independent) | FIXED | 012 |
| D18 | Reading a missing symbol silently returned a stand-in instead of a not-found (D18/R3). Note: the actual silent-wrong path was a `content_anchored_symbol_window` (a substring-matched source window — e.g. a comment mention), not the parent outline as first described. **FIXED (012f):** D18 — `get_symbol` on an absent symbol now returns a loud `No symbol <name> in <path>` that auto-classifies as `OutcomeClass::NotFound` (shared formatter → both single- and batch-mode; PR #379). R3 — find_references type/value-usage traces carry a targeted recall-confidence caveat (best-effort modes ONLY, mirrors the kind filter so a typo'd kind cannot silently ship an unmarked best-effort trace; not blanket noise, no false precision). | SYMPTOM(A) | FIXED (012f) | 012 |
| D19 | No multi-step query decomposition (only the 3 hardcoded multi-hop strings). **FIXED (012f, by honest removal — see D5):** server-side dependent-query decomposition is SUPERSEDED by the client agentic loop (the model reads each result and issues the next call) + find-fusion (multi-surface fan-out the executor runs as one merged plan); no real consumer ever existed for the fake. The dishonest 3-string decomposer is gone and `status.rs` now surfaces `superseded: multi_step_planner` (was a misleading `deferred:`). Tracked-minor / defensive: the `classify_get_file_content_output` gap (`tools.rs:242`, a `_bind` strip-miss that misclassifies a failed `get_file_content` decode as `Found`) is now UNREACHABLE — only the deleted multi-hop dispatched a `get_file_content` step with bad args; find-fusion dispatches `search_*`, whose classifiers handle errors. Left as defensive, NOT fixed in this deletion PR (fixing unreachable code = scope creep). | SYMPTOM(A) | FIXED (012f) | 012 |
| D21 | `symforge admin` prints `Started operator dashboard - http://127.0.0.1:8787/admin` (fake success) but leaves NOTHING listening; the browser hits `ERR_CONNECTION_REFUSED`. Field-reproduced on installed 8.9.6 (operator dashboard report 2026-06-27). **Verified + root-caused @baa03d6 (current main = 8.9.6):** `admin::run` (`main.rs:145`) does NOT block — `start_operator_server` (`admin.rs:154-173`) runs `serve::run` on an IN-PROCESS background OS thread, polls until reachable, opens the browser, returns; then `main` returns -> process exits -> the serve thread + its listener die with it. The helper doc "the thread and its server live for the lifetime of the process" (`admin.rs:110-113`) is literally true but fatal for a fire-and-return verb whose process lives milliseconds. Likely-shared: `Commands::Setup` (`main.rs:144`) also calls `start_operator_server` -> same fire-and-return death (confirm when scoped). NOTE: the report's secondary "admin/setup not in source `Commands` enum" claim was a STALE checkout (`C:\AI_STUFF\...`); current main IS 8.9.6 and HAS both verbs + the full `src/server/admin/` surface — non-issue. Fix family (not yet designed): (1) spawn `serve` as a truly DETACHED child process that survives parent exit; (2) host the operator surface inside the always-on daemon (`:54642`) so `admin` degrades to open-only (cleanest, kills the orphan-serve failure mode); (3) make `admin` block foreground (contradicts open-and-return). **FIXED (wave2 2026-07-02, PR pending): fix family (3), refined.** `admin::run` now ends with `serve_foreground_if_started`: reuse path (a server another process owns is already reachable) prints the URL and returns 0 (the MCP `symforge-admin` prompt's reuse contract); fresh start prints a foreground notice and blocks on `tokio::signal::ctrl_c()` via a sibling current-thread runtime (`main` is sync — no nested-runtime hazard). Reproduced at HEAD before fixing (URL printed, exit 0, 3× connection-refused) and live-verified after (process stays alive, dashboard HTTP 200 twice over a gap, refused only after kill). Regression locks in-lib: `admin_keeps_serving_after_fresh_start_until_shutdown` (double-probe), `admin_does_not_hold_foreground_when_reusing`. `CLAUDE_ADMIN_COMMAND_BODY` (harness_command.rs) updated so agents launch it detached instead of hanging a shell tool. RESIDUAL — setup-wizard sibling: CONFIRMED 2026-07-02, **FIXED (wave3 2026-07-03)**: `setup::run`'s INTERACTIVE branch now binds the `WizardOutcome` and holds the foreground via `serve_foreground_if_wizard_started` (reuse → returns; `session: None` (in-harness/decline) → returns; fresh start → prints `FOREGROUND_SERVE_NOTICE` and blocks on Ctrl-C). Non-interactive branch deliberately UNCHANGED (scripted sink shows humans nothing; blocking would hang CI — FR-014; its transient verify-server promises no persistence). `run_wizard` contract untouched. Regression locks: `setup_holds_foreground_after_fresh_start_until_shutdown` (double-probe), `setup_does_not_hold_foreground_when_reusing`. Release-note-worthy: interactive `setup` (Server/Both) now holds the terminal on a fresh start. Known un-seamed layer (documented at the call site): `run`'s own branch wiring rides on `SetupContext::from_env` and is pinned by TTY verification, not an in-lib test. | ROOT (independent, operator-server lifetime) | FIXED (admin + setup wizard) | 012 |
| D22 | `status` reported the DAEMON process's env surface, not the surface actually served on the connection. Live-reproduced on installed 8.10.1 (2026-07-02): adapter spawned with `SYMFORGE_SURFACE=full` genuinely serving all 36 tools through a daemon spawned earlier WITHOUT the env → `status` printed `surface: compact` while legacy tools served. Root cause: surface gating is adapter-side, but `render_stel_status_body` computed the label from its own process env (`surface_profile_from_env`) — the label described a different process than the one serving. **FIXED (wave2 2026-07-02):** adapter stamps its own profile into `StelStatusRequest._connection_surface` (`#[serde(default)]` + `schemars(skip)` + `_`-prefixed internal rename — old-adapter→new-daemon falls back to env; new-adapter→old-daemon ignores the unknown field; no schema/byte-budget drift); daemon-side renderer prefers the label after `surface_label_from_str` validation (non-canonical input → `None` → env fallback, never echoed); divergence disclosed as ONE line `daemon_env_surface: <env> (serving connection: <conn>)`. Spoof scope (documented, deliberate): the adapter path overwrites the field unconditionally; a client attached DIRECTLY to the daemon `/mcp` can skew only its own cosmetic readout — no gate/routing reads the field. Regression locks: `status_reports_connection_surface_and_discloses_daemon_env_divergence`, `status_without_connection_surface_falls_back_to_env`, `status_same_env_and_connection_surface_has_no_divergence_line`. | ROOT (independent, adapter/daemon env skew) | FIXED (wave2) | new |
| D23 | `empty_index_recovery_hint` has the SAME adapter/daemon env-skew root as D22, with WORSE consequences than a cosmetic label: the recovery hint is rendered daemon-side from the daemon's env (`empty_guard_message`, `format.rs:~4881`, reached via `loading_guard_message_from_published`, `tools.rs:~3499`) and returned verbatim through the proxy with no adapter-side overlay. Compact-adapter + full-daemon emits a hint naming `index_folder`, which the adapter's own gate then REFUSES — recreating exactly the P0-1 "unrecoverable loop" this function was written to prevent (TR-02). Full-adapter + compact-daemon hides `index_folder` that IS callable. Trigger is narrower than D22 (requires the daemon's served index to be empty) but every proxied read/edit tool is a carrier. Fix direction: thread the connection profile the same way D22 did (`_connection_surface` pattern generalizes; any consumer must apply the same adapter-overwrite discipline). Found by the D22 fix's mandated read-only audit (2026-07-02). Audited clean in the same pass: `SurfaceView` (admin API `get_surface`) inspects the daemon directly — daemon env is its honest contract. **FIXED (wave3 2026-07-03) — class closed at all three renderers.** Design: out-of-band threading, NOT a per-request field — adapter's `DaemonSessionClient::call_tool_value` unconditionally stamps `x-symforge-connection-surface` from its own env on every proxied call; daemon's `call_tool_handler` (post-auth) binds a `tokio::task_local` around the entire `execute_tool_call` dispatch (`with_connection_surface`); leaves resolve via `connection_surface_or_env()` (`try_with` → env fallback, no panic path). JSON-envelope injection was REJECTED because `GetFileContentInput` is `deny_unknown_fields` — an injected params key would break new-adapter→old-daemon skew; the header leaves params untouched, degrading cleanly in both skew directions. Renderers fixed: `empty_guard_message` (all `loading_guard!` carriers), `session_stale_recovery` (edit_tools.rs — the live edit-path sibling a reviewer caught), `what_changed` repo-root guidance (uniformity; effectful case proven unreachable). Concurrency proven safe by review: each call runs on its own `spawn_blocking` thread held by `block_on` — cross-call leakage is structurally impossible; no `tokio::spawn` inside guard paths today (ceiling documented). Regression locks: `empty_guard_names_compact_recovery_when_connection_is_compact_over_full_env`, `empty_guard_names_index_folder_when_connection_is_full_over_compact_env`, `empty_guard_absent_connection_surface_falls_back_to_daemon_env`, `stale_session_recovery_names_compact_over_full_env`, `stale_session_recovery_names_index_folder_over_compact_env`. Residuals (tracked-minor): (1) two threading disciplines now carry the same datum (D22's typed status field vs D23's header+task_local) — consolidating every daemon-side surface hint onto `connection_surface_or_env()` would make the class a single greppable question; (2) a true cross-process header-divergence test is impossible in-process (adapter and daemon share env) — live-verify against installed binaries is the honest gap. | ROOT (independent, adapter/daemon env skew — same class as D22) | FIXED (wave3) | new |

## FIXED this engagement (012 lane, verified green)

- C4 wrong-repo binding → per-connection retarget + bound-root visibility (real daemon test).
- if_match TOCTOU/over-strict → normalized pre-flight compare, write-time byte guard preserved.
- Opaque `query`-missing error → clean validation.
- Glossary MCP resource added.
- Overlay-search correctness (noise/scope gate, caps, truncation, determinism, coherent counts).
- close_session multi-project leak → reaper + regression test.
- Strict-MCP-client schema rejection of Phase 3 `projects` fields → `schemars(with=...)`.
- Base+overlay engine primitive + cross-project query (US1) — live-verified.
- B1 cross-project scoping HONORED (D11): `path_prefix`/`language`/noise threaded through the option-honoring engine search via the single-project helpers; reject guard narrowed to the genuinely-unsupported params (`structural`, `find_references` selectors). Per-project ranked+bounded (D14 PARTIAL — global interleave deferred). Aligns cross-project text defaults with single-project (`include_vendor=false`); `ranked` is churn-blind cross-project (noted). Engine unit test + live daemon-HTTP scoping assertions (symbols + text); adversarial review wf a2eac32.
- A1b `path` forwarding (D-A0 / lossless-or-loud): `forward_caller_path` in the single plan choke point (`src/stel/planner.rs`) threads the caller's `path` into `path_prefix` on the path_prefix-capable search routes (`search_symbols`/`search_text`/`explore`), closing the `path` silent-drop there; `path`-as-selector routes (`get_symbol`/`get_file_content`/`find_references`/`find_dependents`) already carry it (Routed). `max_tokens` left as handler-`Forwarded` (already honored; injecting it would fight the degrade-cap logic and violate the `Forwarded` contract). Golden unaffected (tool-shape unchanged). Conformance test re-baselined NotApplicable→Routed + behavioral forwarding proof. Adversarial review wf a9b73e8 (verdict: correct + honest); newly-found `search_files` scope gap tracked as D20.
- C-stopgap (D16 silent-half): `/mcp` loudly REFUSES `project`/`projects` on the three cross-project tools (`search_symbols`/`search_text`/`find_references`). TRACE FINDING (the handoff left this trace unfinished and ASSUMED a silent drop): the refusal was ALREADY wired since Phase 3 (`local_cross_project_refusal`, commit `278864c`) — it fires on `/mcp` because that transport's `SymForgeServer::new` leaves `daemon_client=None`, so `proxy_tool_call` short-circuits to `None` BEFORE any single-project answer. The genuine gaps closed by 012d: (1) NO regression test guarded the no-daemon refusal (only the facade D9 path was tested) — now locked by `tests/serve_http_attach.rs::cross_project_targeting_is_refused_over_http` (real HTTP transport: all three tools refuse + name the `/mcp` transport; a no-params control does NOT refuse); (2) the refusal message now names the `/mcp` HTTP transport so it is honest for that caller. NO new refusal logic — the containment existed but was unguarded. Daemon HONOR path unchanged (`daemon::tests::test_cross_project_query_returns_attributed_hits_from_both_projects`).
- B2 cross-project freshness (D12 FIXED, sprint 012e): a LAZY re-intern on the cross-project read path force-replaces a stale interned base — detected via `Arc::ptr_eq` of the working-set entry's snapshot vs the project's CURRENT published index (every publish swaps a new `Arc`, so a pointer mismatch == the watcher reindexed) — preserving SC-002 (one `Arc<IndexBase>` per `BaseKey`, value replaced not duplicated) and re-fencing the entry's empty overlay via `WorkingSet::add`. Mismatch-gated (warm path takes no write lock); single-active reads are gated out by the shared `resolve_cross_project_targets` (byte-identical + frecency-neutral, SC-4). New daemon regression test `test_cross_project_read_is_fresh_after_watcher_reindex` (SC-1 add, SC-2 delete/no-ghost, mismatch-gate, lone-non-active `Targets::One`, SC-002-after-replace) + a real-watcher live daemon-HTTP transcript proved SC-1/SC-2 end to end against the built binary. `Arc::ptr_eq` over-triggers on mtime-only touches (a safe spurious re-intern, documented at the call site, not claimed exact). Full CI green; adversarial review wf `wusdyz8fx` (code-reviewer + security-reviewer APPROVE — no cross-project leak; skeptic APPROVE_WITH_NITS, applied). One file (`src/daemon.rs`); no `view.rs`/`embed.rs` change. PR #369, commit `745313d`.

## Attack plan (roots, not holes)

- **Attack Culprit A:** a structurally-enforced facade contract — **lossless-or-loud** (every caller param is routed, forwarded, or explicitly refused — never silently dropped) + **honest-envelope** (only measured or explicitly-`est.`-labeled values reach the wire), with a conformance test so the class cannot regress. Dissolves D-A0, D3, D5, D6, D8, D9, D18, D19 and removes the fabrication behind D1/D2.
- **Attack Culprit B:** per-`IndexView` derived indices + republish→rebase in the engine, so cross-project/overlay search honors scoping (D11), stays fresh (D12), ranks (D14). (D13 reclassified to Culprit D — NOT a view-index defect. D15 is a DORMANT SEAM, D-B0 is MOOT until a real overlay writer lands — see `docs/reviews/overlay-redundancy-decision.md`.)
- **Attack Culprit D:** `parsing/xref.rs` struct/composite-literal capture + a `find_references` lookup branch matching `qualified_name` head segments, closing value/constructor/struct-literal recall (D13) on the ordinary single-project read path. **DONE (012f):** the `find_references` immediate-qualifier head-match closed qualified-call construction recall (Rust + C++); struct/composite literals were already covered by the unscoped `(type_identifier)` capture (the `struct_expression`-missing premise was false, so zero query change there).
- **Attack Culprit C:** per-connection session dimension on `/mcp` (stateful mode / per-connection proxy server).
- Independent: D17 race hardening.

## Attack sequence — vetted 2026-06-22 (adversarial critique wf_981b5b87, verdict REVISE→PROCEED w/ A1a)

This ledger lives on `feat/012` (worktree); A1/A2/B1 all land here (the superset branch). The 013 lane keeps its own `013-findings-ledger.md` (root D3 = storage/transport coupling via `cfg(feature="server")`) — coordinate, out of this lane's scope but tracked.

Sequence by (defects-killed ÷ effort), gated by file independence:
1. **A1a** — `ParamDisposition` choke point in `build_plan_from_steps` + conformance test. Every `StelRequest` field resolves to `Routed|Forwarded|Refused|NotApplicable`; silent-absent is asserted-against. **Zero behavior change — `routes.golden.jsonl` does NOT move.** Erects the non-regressable guard against the silent-drop class (D-A0) at zero risk. **ATTACK FIRST.** Owner 012.
2. **D17** — collapse the open-vs-close TOCTOU (single `projects.write()` entry). S/LOW, isolated. Owner 012.
3. **B1** — DONE (implemented, gate-green): threaded caller options through the empty-overlay search path → D11 scoping FIXED, D14 ranking PARTIAL (per-project ranked+bounded; global interleave deferred). Owner 012.
4. **A1b** — DONE (implemented, gate-green, adversarially reviewed wf a9b73e8): `forward_caller_path` threads caller `path`→`path_prefix` on path_prefix-capable search routes (the real `path` silent-drop). HONEST refinement of the forecast: `max_tokens` was already honored (handler CCR `Forwarded`), so it is NOT forwarded into plan args (would violate the `Forwarded` contract); golden needed NO re-baseline (it asserts tool-shape only). New defect found: D20 (`search_files` unscoped). Owner 012.
5. **C-stopgap** — DONE (012d, behaviorally verified over the real `/mcp` HTTP transport). TRACE FINDING: the `/mcp` refusal was ALREADY wired since Phase 3 (`local_cross_project_refusal` fires because `/mcp`'s `SymForgeServer::new` has `daemon_client=None`); the real gaps were the missing regression lock (now `tests/serve_http_attach.rs`) + the message not naming the `/mcp` transport (now fixed). NO new refusal logic. D16's silent-wrong half is CONTAINED; the ROOT (per-connection `/mcp` session) stays tracked-large. Owner 012.
6. **B2** — DONE (012e, merged PR #369): LAZY re-intern/force-replace of the stale cross-project base on the read path → **D12 FIXED**. Full CI green, unanimous adversarial review, real-watcher live daemon-HTTP SC-1/SC-2 proof. Owner 012.

**NEXT frontier**: the **012f actionable set is COMPLETE** — D13, D18, R3, D20, and bases-table orphan GC are all FIXED + merged (release-please v8.9.0–v8.9.4); D15 reframed as a DORMANT SEAM. **as_of 2026-06-29 (round 3): the overlay WRITER TRACK is RETIRED, not built.** Precondition #1 has no consumer (AAP isolates at the worktree/VM boundary, Verdict B; single-session speculative edits are better served by an undo-log), and the goal — isolated concurrent edits — is already met by OS-level worktree/VM/process isolation. Phase 1 REMOVED the dead write side (D15 → SUPERSEDED/REMOVED, D-B0 → WON'T-BUILD, D16 → decoupled as an independent future feature); the live cross-project read path is byte-identical (empty overlay). Phase 2 (separate PR) collapses the empty overlay scaffolding out of the live path. Building precondition #1 returns only under a revival trigger (a shared index that must reflect multiple consumers' uncommitted pre-merge edits). Design: `docs/superpowers/specs/2026-06-29-overlay-track-retirement-design.md`. Owner 012. Decision + lesson invariant: `docs/reviews/overlay-redundancy-decision.md`; full campaign scorecard: `docs/reviews/012f-RESULTS.md`.

A2 (`Figure` provenance enum) = DEMOTED to regression-guard (envelope already honest); low priority, owner 012.

Tracked-LARGE (OPEN, real owner + blocked-on — NOT euphemized):
- **D-B0** per-view derived index for non-empty overlays (K-delta trigram merge) — owner 012, **WON'T-BUILD (as_of 2026-06-29, round 3): the overlay track is retired; the precondition it was blocked on has no consumer. Revival trigger: a shared-index-reflects-uncommitted-edits architecture. DECOUPLED from D13.** See `docs/superpowers/specs/2026-06-29-overlay-track-retirement-design.md`.
- **D15** overlay edits in ordinary reads — owner 012, **SUPERSEDED/REMOVED (as_of 2026-06-29, round 3):** the writer track is retired (Phase 1 removed `Overlay::{upsert,tombstone,rebase}`, the `edit_tools.rs` seam, `parse_indexed_for_overlay`, `SessionOverlay`/`session_working_set`, and their tests); session-private edits are superseded by worktree/VM/process isolation; the live cross-project read path is byte-identical (empty overlay). Phase 2 (separate PR) removes the empty overlay scaffolding. See `docs/superpowers/specs/2026-06-29-overlay-track-retirement-design.md`.
- **bases-table orphan GC** (tracked-MINOR, pre-existing, surfaced by the B2 review): the per-daemon `bases` intern table has no removal path; a genuine git-commit advance mints a NEW `BaseKey` and orphans the old key's `Arc` (unbounded only over a very-long-lived MULTI-COMMIT session; the common no-commit watcher path force-replaces the SAME key, so no growth). NOT a B2 regression (identical to pre-existing `intern_base`/retarget behavior). **FIXED (012f, PR #383):** `gc_orphaned_bases()` sweeps map-only orphans (`Arc::strong_count == 1` under `bases.write()`, SC-002-safe — a live base held by a session has count >= 2 and survives) on BOTH orphan paths: end of `refresh_working_set_bases` (commit-advance) and `close_session` (last-holder; the removed `SessionRecord` is dropped BEFORE the sweep so its `Arc` clones release first). Owner 012.
- **D16** `/mcp` per-connection daemon session (the ROOT; silent-wrong half already CONTAINED by C-stopgap/012d) — owner 012, blocked-on: stateful-mode substrate + parity re-test. **as_of 2026-06-29: DECOUPLED from the (retired) overlay track — an independent future feature with its own coarse-grained per-connection isolation; never blocked on the overlay.**
- **D13** xref recall — **FIXED (012f):** `find_references` immediate-qualifier head-match closed qualified-call construction recall (Rust + C++); struct/composite literals already covered by the unscoped `(type_identifier)` capture. Tracked-minor head-match noise class (Import/TypeUsage refs head-matched on their immediate qualifier; count-neutral, leaf-pointed byte_range) documented at the call site (`query.rs` find_references_for_name).
- **D2** gate decides on estimated economics for non-read routes — owner 013, blocked-on: grounding extension to search routes.
- **D5/D6** false "VALIDATED"/"95% trajectory" claims — doc demotion, owner 012.
