diff --git a/.kiro/breadth.toml b/.kiro/breadth.toml
new file mode 100644
index 0000000..34b5455
--- /dev/null
+++ b/.kiro/breadth.toml
@@ -0,0 +1,251 @@
+# Breadth ledger: every spec and every top-level capability, with a status and the deletion that closes it.
+# Draft 2026-10-13 from the 22 spec.json files and the Pathfinder feature inventory (PATHFINDER-2026-10-13/).
+# Nothing here changes code; it records decisions so a gate can hold the line.
+#
+# status values
+# active = on one of the four lines in .kiro/steering/overview.md (2026-10-13); work may land
+# done = implemented and closed; the code it produced is owned by a capability below
+# parked = no work lands; scaffolding (stubs, unused flags, dead task modules) is removed; spec stays as history
+# retiring = the "closes" deletion is scheduled; the spec directory goes with it
+# review = Pathfinder 2026-10-13 decides between parked and retiring
+#
+# open_fronts (the weekly number) = specs with status active or review
+# + capabilities with status review or retiring whose "closes" has not run.
+#
+# gate (to implement after the map is approved):
+# fail when a spec.json reaches phase "tasks-generated" without an entry here;
+# fail when a new directory under src/openultrasast/ lands without a [capability.*] entry;
+# fail when an entry has status "retiring" for more than 14 days with its "closes" unexecuted.
+
+[meta]
+drafted = "2026-10-13"
+spec_count = 22
+open_fronts = 0 # filled by the gate script once it exists
+
+# ---------------------------------------------------------------- specs
+
+[spec.hunter-learning-loop]
+status = "active"
+line = "hunter-learning-loop"
+phase = "requirements-generated"
+owner = ["src/openultrasast/learn", "src/openultrasast/tool_hunter.py", "src/openultrasast/hunter_tools.py"]
+closes = "n/a (active)"
+
+[spec.pre-push-safety-net]
+status = "active"
+line = "pre-push-safety-net"
+phase = "implementation (77 of 87 tasks)"
+owner = ["src/openultrasast/push"]
+closes = "n/a (active)"
+
+[spec.search-with-proof]
+status = "active"
+line = "search-with-proof"
+phase = "tasks-generated (11 of 24 done)"
+owner = ["src/openultrasast/search", "src/openultrasast/sandbox"]
+closes = "n/a (active)"
+
+[spec.proof-arm-opencode]
+status = "active"
+line = "search-with-proof"
+phase = "brief"
+owner = ["src/openultrasast/search"]
+closes = "n/a (active)"
+
+[spec.engine-change-scoped-analysis]
+status = "active"
+line = "engine-change-scoped-analysis"
+phase = "brief"
+owner = ["src/openultrasast/cpg", "src/openultrasast/model", "src/openultrasast/semantic"]
+closes = "n/a (active)"
+
+[spec.valid-detection-benchmark]
+status = "active"
+line = "hunter-learning-loop" # the benchmark every line is measured on
+phase = "tasks (18 of 24 done)"
+owner = ["benchmarks/valid", "benchmarks/unseen/replay.py", "benchmarks/unseen/score.py"]
+closes = "n/a (active)"
+
+[spec.learned-decision-engine]
+status = "parked"
+phase = "implementation (20 of 47 tasks)"
+owner = ["src/openultrasast/learn"]
+note = "the classifier it built is the second stage of the hunter arm and stays; new learn work goes through hunter-learning-loop, not here"
+closes = "mark remaining 27 tasks superseded by hunter-learning-loop; no code deletion"
+
+[spec.plane-on-kubernetes]
+status = "parked"
+phase = "implementation (14 of 43 tasks)"
+owner = ["src/openultrasast/plane"]
+note = "kube move paused after groups 1-2 (2026-10-02); the ax lane is blocked on the gVisor checkpoint path (operator-owned)"
+closes = "when the ax lane is restored: finish; otherwise delete plane/runner.py container path and ops/k8s"
+
+[spec.contributor-scan]
+status = "parked"
+phase = "implementation (27 of 51 tasks), paused since 2026-09-23"
+owner = ["src/openultrasast/model", "src/openultrasast/cpg"]
+closes = "fold the open census/family-context tasks into engine-change-scoped-analysis; close this spec"
+
+[spec.mcp-shift-left-agent]
+status = "parked"
+phase = "brief"
+owner = ["src/openultrasast/mcp.py", "src/openultrasast/skills.py", "src/openultrasast/index.py"]
+note = "follow-up to the pre-push hook; waits for a hook that catches something"
+closes = "if not started by 2026-12-01: delete mcp.py, skills.py, index.py and their tests"
+
+[spec.authorization-obligations]
+status = "review"
+phase = "tasks-generated (0 of 24)"
+owner = ["src/openultrasast/semantic/obligations"]
+note = "overview.md calls it superseded in practice by the dominance arbiter; obligations code exists and is called from scan (cli.py:1388)"
+closes = "Pathfinder U5 decides: keep obligations as an annotation producer for the hunter, or delete semantic/obligations and its tests"
+
+[spec.finding-feedback-loop]
+status = "retiring"
+phase = "design, dormant since 2026-09-09"
+owner = []
+closes = "delete .kiro/specs/finding-feedback-loop (no code was written)"
+
+[spec.reachability-flow-model]
+status = "retiring"
+phase = "tasks-generated (0 of 23), dormant"
+owner = []
+note = "largely delivered by adopting Joern (overview.md)"
+closes = "delete .kiro/specs/reachability-flow-model"
+
+[spec.flow-aware-ranking]
+status = "retiring"
+phase = "brief"
+owner = []
+closes = "delete .kiro/specs/flow-aware-ranking; keep the one sentence it owns in safety-net.md"
+
+[spec.zero-with-a-reason]
+status = "retiring"
+phase = "brief"
+owner = []
+note = "its idea (coverage gaps are reported, never a clean verdict) is already a product principle in safety-net.md"
+closes = "delete .kiro/specs/zero-with-a-reason"
+
+[spec.ir-consolidation]
+status = "retiring"
+phase = "brief"
+owner = []
+closes = "delete .kiro/specs/ir-consolidation"
+
+[spec.ai-service-plane]
+status = "done"
+owner = ["src/openultrasast/plane"]
+
+[spec.model-grounded-detection]
+status = "done"
+owner = ["src/openultrasast/model", "src/openultrasast/cpg"]
+
+[spec.pair-corpus-honesty]
+status = "done"
+owner = ["src/openultrasast/pairs.py", "benchmarks/pairs"]
+
+[spec.corpus-seeded-mechanisms]
+status = "done"
+owner = ["src/openultrasast/learn"]
+
+[spec.propose-adjudicate-prove]
+status = "done"
+owner = ["src/openultrasast/semantic", "src/openultrasast/ruleset"]
+
+[spec.tree-sitter-overlay-extra]
+status = "done"
+owner = ["src/openultrasast/semantic"]
+
+# ---------------------------------------------------------------- capabilities (top-level code)
+
+[capability.push]
+status = "active"
+spec = "pre-push-safety-net"
+lines = 5074
+
+[capability.engine]
+status = "active"
+spec = "engine-change-scoped-analysis"
+modules = ["cpg", "model (minus endpoint.py)"]
+lines = 7764
+
+[capability.semantic]
+status = "active"
+spec = "engine-change-scoped-analysis"
+modules = ["semantic", "complexity"]
+lines = 4301
+note = "complexity/ hotspots feed the hunter; obligations under review (see spec.authorization-obligations)"
+
+[capability.llm-hunter-and-client]
+status = "active"
+spec = "hunter-learning-loop"
+modules = ["tool_hunter.py", "hunter_tools.py", "provider/openrouter.py", "model/endpoint.py", "plane/budget.py"]
+lines = 1700
+note = "Pathfinder U6 counts the chat-client classes; expected outcome: one client protocol, one metering path"
+
+[capability.learn]
+status = "active"
+spec = "hunter-learning-loop"
+modules = ["learn", "plane/memory.py", "plane/tasks (fact producers)"]
+lines = 11350
+
+[capability.search-with-proof]
+status = "active"
+spec = "search-with-proof"
+modules = ["search", "sandbox"]
+lines = 4803
+
+[capability.core]
+status = "active"
+modules = ["cli.py", "config.py", "contracts.py", "preprocess.py", "findings.py", "rank.py", "mapping.py", "ruleset", "policy", "reports.py", "stages.py", "run.py", "provenance.py", "profiling.py"]
+note = "hunter.py is the regex quick-rule pool and should be renamed; harness.py processor machinery is unreachable (U1)"
+
+[capability.pairs-and-benchmarks]
+status = "active"
+spec = "valid-detection-benchmark"
+modules = ["pairs.py", "benchmark.py", "repos.py"]
+note = "pairs.py label functions feed learn/labels.py; the `ousast pairs` CLI surface is under review (U10)"
+
+[capability.plane-executor]
+status = "review"
+spec = "plane-on-kubernetes"
+modules = ["plane (reconciler, runner, generate, manifests, harvest, egress, delivery, router, doctor, profile, engine_alerts, workspaces)"]
+lines = 4885
+closes = "Pathfinder U8 lists which modules only the retired HarnessX loop used (engine_alerts, harvest, workspaces, profile are the suspects); delete those"
+
+[capability.regress]
+status = "review"
+modules = ["regress"]
+lines = 549
+closes = "Pathfinder U9: if search/ covers the triggerable verdict, delete regress/ and the deep-mode REGRESS branch in cli.py:839-880"
+
+[capability.improve]
+status = "retiring"
+modules = ["improve"]
+lines = 1175
+note = "HarnessX-era improve lever; no active spec names it"
+closes = "delete improve/, `ousast improve` (cli.py:237, 433, 1106), plane/tasks/loop.py lazy imports, tests/test_cli_improve.py, tests/test_improve_*.py"
+
+[capability.scan-extras]
+status = "review"
+modules = ["fusion.py", "verification.py", "calibration.py", "scoring", "harness.py (processors)"]
+closes = "U1 found fusion decisions never reach findings or score, verification runs before MAP, processors unreachable; decide keep-and-fix or delete per module"
+
+[capability.mcp-and-skills]
+status = "parked"
+spec = "mcp-shift-left-agent"
+modules = ["mcp.py", "skills.py", "index.py"]
+lines = 830
+closes = "see spec.mcp-shift-left-agent"
+
+[capability.gates]
+status = "review"
+modules = ["gate.py", "map_gate.py", "pair_gate.py"]
+lines = 428
+closes = "U10: keep gate.py (CI + plane loop); delete map_gate.py and pair_gate.py if CI does not need them"
+
+[capability.push-evaluation-instruments]
+status = "review"
+modules = ["push_inputs.py", "push_scoring.py"]
+lines = 462
+closes = "U3: move under benchmarks/push or delete if only tests use them"
diff --git a/PATHFINDER-2026-10-13/00-features.md b/PATHFINDER-2026-10-13/00-features.md
new file mode 100644
index 0000000..8f55fab
--- /dev/null
+++ b/PATHFINDER-2026-10-13/00-features.md
@@ -0,0 +1,55 @@
+# Feature inventory, src/openultrasast (2026-10-13)
+
+199 Python files, 51,860 lines (ast count; `wc -l` with blank lines says 60k). Discovery used an ast import
+resolver over every module (about 690 internal edges). The four active lines come from
+`.kiro/steering/overview.md`, section 2026-10-13: hunter-learning-loop, pre-push-safety-net,
+search-with-proof, engine-change-scoped-analysis.
+
+## Map units (what the flowcharts cover)
+
+| Unit | Features | Purpose | Entry points | Core files | Lines | Active line |
+|---|---|---|---|---|---|---|
+| U1 Scan and reports | F1 | `ousast scan` quick/standard/deep, run artifacts, SARIF and markdown | cli.py:226 parser, cli.py:613 `_scan`, cli.py:619 `_run_scan`; mcp.py:62 | cli.py (613-1075), stages.py, run.py, harness.py, verification.py, fusion.py, reports.py, scoring/, calibration.py, provenance.py, profiling.py | ~2,000 + part of cli.py | none directly (feeds the pre-push quick tier) |
+| U2 Shared core | F2 | file enumeration, regex quick rules, CWE policy, ranking, config, contracts, mapping | quick-rule pool `run_hunter_pool` cli.py:670 | config.py, contracts.py, redaction.py, preprocess.py, findings.py, rank.py, policy/, ruleset/, mapping.py, hunter.py (regex pool, not the LLM hunter) | ~3,030 | all, indirectly |
+| U3 Pre-push hook | F3, F3b | advisory head-vs-base check on pushed commits; evaluation instruments | cli.py:143 parser, cli.py:523 `_hook_main`, cli.py:529 `_pre_push`; push/install.py; push_inputs.py:143, push_scoring.py:280 | push/*.py, push_inputs.py, push_scoring.py | 5,536 | pre-push-safety-net |
+| U4 Engine (CPG + model layer) | F4, F6 | Joern graph build and session; arbitration of detector claims (taint, dominance, config, evidence ladder) | `ousast model candidates` cli.py:306; cli.py:1286 `_run_model_layer`; cli.py:1304 lazy | cpg/*.py, model/*.py (minus endpoint.py) | 7,764 | engine-change-scoped-analysis, pre-push-safety-net |
+| U5 Semantic layer | F5, F13 | tree-sitter CST/IR, fact tables, overlay, taint, obligations; complexity hotspots for MAP | cli.py:82, 90, 1403-1442 (lazy) | semantic/*.py, semantic/obligations/*.py, complexity/*.py | 4,301 | engine-change-scoped-analysis |
+| U6 LLM hunter and model client | F7 (+ model/endpoint.py, plane/budget.py) | tool-calling LLM hunter over hotspots, its tools, the chat client, metering | cli.py:962 `_attach_tool_hunter`, cli.py:982; plane/tasks/verify.py:29; benchmarks/valid/hunter_arm.py:245 | tool_hunter.py, hunter_tools.py, provider/openrouter.py, model/endpoint.py, plane/budget.py | ~1,700 | hunter-learning-loop |
+| U7 Learn harness, memory store, plane fact tasks | F8, F10, F9b | labels, examples, features, roles, compile, evaluate, experiments, leak audit; the row/blob store; fact-producing tasks | learn/cli.py:18-89; cli.py:315 `learn labels`; plane/tasks/*.py `__main__` | learn/*.py, plane/memory.py, plane/tasks/*.py | 11,350 | hunter-learning-loop |
+| U8 Plane executor | F9 | run manifests on the ax cluster: reconciler, router, runner, egress, harvest | cli.py:326-379 `plane ...`, cli.py:1530; plane/runner.py:989 (container PID 1); plane/profile.py:257 | plane/reconciler.py, runner.py, generate.py, manifests.py, harvest.py, egress.py, delivery.py, router.py, doctor.py, profile.py, engine_alerts.py, workspaces.py | 4,885 | hunter-learning-loop (runs its tasks) |
+| U9 Search with proof, sandbox, regress | F11, F12 | coordinator, worker, executor, differential verification in a sandbox; deep-mode regress verdicts | `__main__` in search/coordinator.py:487, worker.py:530, executor.py:691, verify.py:714; benchmarks/search/pilot_run.py:39-47; cli.py:66, 80 | search/*.py, sandbox/*.py, regress/*.py | 5,352 | search-with-proof |
+| U10 Periphery | F14, F15, F16, F17 | improve lever; pair corpus, benchmark manifests, pinned repos; CI gates; MCP server and skill router | `ousast improve` cli.py:237; `benchmark`/`pairs`/`repos` cli.py:232/283/296; gate.py:184, map_gate.py:118, pair_gate.py:80; `ousast mcp` cli.py:305 | improve/*.py, pairs.py, benchmark.py, repos.py, gate.py, map_gate.py, pair_gate.py, mcp.py, skills.py, index.py | 4,429 | pairs feed hunter-learning-loop labels; the rest none |
+
+## Edges that matter (feature → feature, heaviest sites)
+
+- Pre-push → Model: push/policy.py:18-23, push/runner.py:23-27, push/vetoes.py:24-25. Pre-push → CPG: push/runner.py:20-21, push/cache.py:21-22. Pre-push → LLM client: push/assistance.py:18.
+- Model → CPG: model/scan.py:30, model/pipeline.py:30, model/taint.py:17. **CPG → Model (cycle)**: cpg/backend.py:37, cpg/session.py:44 import `model.contracts`.
+- Core → Model (cycle, lazy): findings.py:86 → model.partitions; preprocess.py:255 → model.layout.
+- Learn → Memory and budget: learn/compile.py:39-40, learn/program.py:35-36. Learn → plane tasks: learn/features.py:27, learn/labels.py:48, learn/roles.py:35. **Plane tasks → Learn (cycle)**: plane/tasks/features.py:34, plane/tasks/roles.py:33-34, plane/tasks/remember.py:42.
+- **Memory → Plane (layering inversion, lazy)**: plane/memory.py:933, 989, 1002 import the reconciler and tasks.loop.
+- Search → Memory and budget: search/coordinator.py:22-23, search/board.py:18, search/worker.py:17. Search → sandbox: search/_sandbox.py:24, search/executor.py:32.
+- Improve → Memory: improve/memory.py:49, 464. Improve → pairs: improve/evolve.py:21, 71.
+- MCP → scan: mcp.py:63, 164 (lazy import of cli). skills.py is imported only by hunter.py:139 (lazy).
+
+## Ambiguous ownership (discovery's findings, kept for phase 3)
+
+1. model/contracts.py is shared by cpg, push and model and causes the CPG↔Model cycle; contracts.py and push/contracts.py are siblings.
+2. model/endpoint.py is an LLM client that imports tool_hunter; used by learn, plane/budget, search/worker, push/assistance.
+3. plane/budget.py is metering used by learn, search and plane tasks.
+4. plane/memory.py is a shared store that imports the plane reconciler and loop lazily.
+5. plane/tasks/{repo_facts,roles,features,alerts}.py are fact producers that learn imports and that import learn.
+6. mapping.py (1,087 lines) serves scan, push, gates, pairs, hunter_tools, learn and model.
+7. Name collisions: hunter.py (regex pool) vs tool_hunter.py (LLM); harness.py (scan trace config) vs the learn harness; calibration.py (scan feedback) vs learn/calibrate.py.
+8. push_scoring.py and push_inputs.py sit at top level but belong to pre-push evaluation.
+9. regress/ (deep mode) and search/ (proof arm) both run code in the sandbox for a triggerable verdict.
+
+## Standalone modules (no internal importer except cli.py or tests)
+
+No importer at all: model/audit.py, push_inputs.py, map_gate.py, plane/runner.py (container entry), plane/tasks/agree.py, plane/tasks/features.py, search/worker.py, search/probe.py (driven by benchmarks/search/pilot_run.py:43, 47).
+Only via package `__init__`: policy/verycode.py, semantic/prove_filter.py, scoring/project_score.py, regress/candidate.py, sandbox/probe.py.
+Imported only by cli.py: calibration, fusion, harness, hunter, improve, learn.cli, mcp, model/report, pair_gate, plane/engine_alerts, plane/harvest, plane/tasks/remember, plane/workspaces, profiling, provenance, regress, reports, stages, semantic/obligations/intent.
+
+## Gaps
+
+Discovery did not read tests or the spec directories; cli.py line ranges per feature are approximate; where the
+measured "LLM hunter" runs in the plane (likely plane/tasks/verify.py:29) is unconfirmed.
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U1-scan.md b/PATHFINDER-2026-10-13/01-flowcharts/U1-scan.md
new file mode 100644
index 0000000..aa8b4c8
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U1-scan.md
@@ -0,0 +1,86 @@
+# U1 Scan orchestration and reports (`ousast scan`)
+
+Traced read-only on 2026-10-13. Three premises from discovery were wrong and are corrected here:
+`run_hunter_pool` (cli.py:670) is the **standard** branch and is the same regex matcher as quick, with no model
+call; the tool hunter runs in standard as well as deep whenever MAP runs; scoring and fusion run **before** MAP,
+so model-layer, overlay, obligation and tool-hunter findings never reach `score.json` or `fusion.json`.
+
+## Happy path, `--mode standard`
+1. `main` → `_main` (cli.py:126, 134); `load_dotenv()` (cli.py:139); parser `scan` (cli.py:226-230).
+2. `_scan` (cli.py:613) → `_run_scan` (cli.py:619) → `plan_for_mode` standard = (STATIC, MAP) (stages.py:13-17, 28); `load_config` (cli.py:624).
+3. `create_scan_run` makes the run dir (run.py:18-26); `HarnessRuntime` with no processors, `write_harness_config` (cli.py:627-633); stages run through `run_stage` (harness.py:103-111).
+4. `load_policy`, `load_ruleset`, `assert_rules_resolve` (cli.py:634-638); `_load_static_hints` (cli.py:1037); `preprocess_repository` (cli.py:641-644).
+5. `analyze_entry_points`, `attach_reachability_hints` (cli.py:656-659); `provenance.fingerprint` (cli.py:660, provenance.py:66).
+6. `rank_targets`, `calibrate_rankings` (cli.py:661-666).
+7. Standard: `run_hunter_pool` (cli.py:667-673) = thread pool of `quick_scan_findings` (hunter.py:103-160). Quick/deep: `quick_scan_findings` directly (cli.py:674-687).
+8. `write_findings` (cli.py:708); `verify_findings` deterministic (verification.py:73-123, cli.py:711); `_persist_calibration_feedback` (cli.py:1011).
+9. Fusion only when standard and enabled: `fuse_findings` (cli.py:720-726, fusion.py:191); decisions do not change findings.
+10. Scoring: `build_score_artifact` → `score.json` (cli.py:731-746); `record_completed(STATIC)` (cli.py:747).
+11. MAP (cli.py:757-835): `build_complexity_map` (758), `load_facts` (769), overlay `adjudicate` (774), `_check_obligations` (789, 1388), `_run_model_layer` (804, 1286-1347), `_attach_tool_hunter` if `hunter_model` set (816, 962-1008).
+12. Deep adds REGRESS (cli.py:839-880): sandbox probe → `run_regression` → `write_verdicts`, `persist_verdicts`. Deep skips fusion (cli.py:720).
+13. `write_markdown_report` (886), `write_sarif_report` (905), `write_manifest` (915); `scan_exit_code` (reports.py:281-296).
+
+```mermaid
+flowchart TD
+ A[main/_main
cli.py:126] --> B[parse scan args
cli.py:226]
+ B --> C[_scan
cli.py:613] --> D[_run_scan
cli.py:619] --> E[plan_for_mode
stages.py:28]
+ E --> F[create_scan_run
run.py:18] --> G[HarnessRuntime + write_harness_config
cli.py:627]
+ G --> H[load_policy / load_ruleset
cli.py:634] --> I[_load_static_hints
cli.py:1037]
+ I --> J[preprocess_repository
cli.py:641] --> K[analyze_entry_points
cli.py:656]
+ K --> L[fingerprint
provenance.py:66] --> M[rank_targets + calibrate_rankings
cli.py:661]
+ M --> N{mode == standard
cli.py:667}
+ N -- standard --> O[run_hunter_pool
hunter.py:103]
+ N -- quick/deep --> P[quick_scan_findings
findings.py:45]
+ O --> Q[write_findings
cli.py:708]
+ P --> Q
+ Q --> R[verify_findings
verification.py:117] --> S[_persist_calibration_feedback
cli.py:1011]
+ S --> T{standard and fusion.enabled
cli.py:720}
+ T -- yes --> U[fuse_findings
fusion.py:191] --> V
+ T -- no --> V[build_score_artifact
cli.py:732]
+ V --> W{MAP requested
cli.py:757}
+ W -- quick --> AC
+ W -- standard/deep --> X[build_complexity_map
cli.py:758] --> Y[adjudicate overlay
cli.py:774]
+ Y --> Z[_check_obligations
cli.py:1388] --> AA[_run_model_layer
cli.py:1286] --> AB[_attach_tool_hunter
cli.py:962]
+ AB --> AR{REGRESS requested
cli.py:839}
+ AR -- deep, sandbox ok --> AS[run_regression
cli.py:856] --> AC
+ AR -- no --> AC[write_markdown_report
cli.py:886]
+ AC --> AD[write_sarif_report
cli.py:905] --> AE[write_manifest
cli.py:915] --> AF[scan_exit_code
reports.py:281]
+```
+
+## Side effects
+Run dir `///`: resolved_config.json (run.py:24), harness.json (cli.py:632),
+trace/events.jsonl (harness.py:76), mapping/static_hints.json and entry_points.json (cli.py:640, 657),
+preprocess/file_targets.json, rank/ranking.json, calibration/*.json, shadow_findings.json, findings.json
+(rewritten up to 6 times: cli.py:708, 788, 801, 814, 877, 1005), traces/hunter_trajectories.jsonl,
+verification.json, fusion.json, score.json, complexity_map.json, overlay.json, verdicts.json, report.md,
+report.sarif, manifest.json. Into the scanned repo itself: `.openultrasast/calibration/false_positive_learnings.json`
+(cli.py:1021) and `complexity_ledger.json` (cli.py:765, 872). Subprocesses: `git log` (provenance.py:102-108),
+Joern via `JoernBackend` (cpg/backend.py:1817), Docker in deep mode (sandbox/runner.py:64). Model calls:
+`scan_repository` with `resolve_chat_endpoint` (cli.py:1321-1336), `adjudicate_intent` with
+`resolve_hunter_client` (cli.py:1450), `run_tool_hunter` (cli.py:976-989).
+
+## External dependencies
+Core: preprocess_repository (cli.py:643), load_ruleset (637), load_policy (634), analyze_entry_points (656),
+rank_targets (661), quick_scan_findings (677). Engine: resolve_cpg_backend (1309), regions_for (1315),
+coverage_rows (1323), scan_repository (1333). Semantic: load_facts (770), adjudicate (777), check_obligations
+(1426-1450), filter_promoted_hotspots (847). LLM hunter: run_tool_hunter (976-982). Complexity:
+build_complexity_map (760). Sandbox/regress: resolve_sandbox_runner (845), run_regression (858).
+
+## Never touched by the scan path (removal candidates)
+- `calibration.compute_ranking_metrics` (calibration.py:108): only tests/test_calibration.py.
+- `findings.build_quick_hunter_prompt` (findings.py:102): only tests/test_findings.py.
+- `verification.validate_evidence_transition` (verification.py:39): only tests/test_verification.py.
+- `fusion.should_fuse` (fusion.py:94): only in `__all__`.
+- Processor machinery in harness.py (Processor, ProcessorSpec, _run_processor, _contract_violation,
+ harness.py:41-55, 125-166): the runtime is always built with no processors (cli.py:627-632).
+- `fusion.json` decisions never feed findings or score; the quick-rule fallback to PATTERN_RULES
+ (findings.py:55-56) never triggers from the CLI.
+- Kept: profiling.py is opt-in by env var (profiling.py:40-42).
+
+## Likely bugs (statement order, unconfirmed)
+Scoring (cli.py:732) and verification (cli.py:711) run before MAP (cli.py:757), so MAP findings are absent from
+score.json and unverified, which affects `--fail-on verified`. Deep skips fusion (cli.py:720). A stage
+exception aborts with no report (harness.py:107-109).
+
+## Confidence
+High for cli.py:613-959 and stages.py; medium for side effects inside other units (not read here).
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U10-periphery.md b/PATHFINDER-2026-10-13/01-flowcharts/U10-periphery.md
new file mode 100644
index 0000000..d783384
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U10-periphery.md
@@ -0,0 +1,111 @@
+# U10 Periphery: improve, pairs/benchmark/repos, gates, MCP and skills
+
+Traced read-only 2026-10-13.
+
+## (a) improve/ (1,175 lines: evolve 416, memory 549, validator 125, journal 53)
+`ousast improve` (cli.py:237-281, 433) → `_improve` (1106): holdout split of the pair catalog (1129), manifest
+and target (1130-1131), optional `MemorySession` (1133, 1170) → `run_improvement` (evolve.py:259) loops
+`run_round` (150): `_scan`/`_evaluate` (307, 319; `benchmark.evaluate_benchmark`), propose status edits (126)
+plus memory proposals (176-183), `EvolveValidator` (validator.py:69), replay boot `_load_with` (333), rescan;
+accept only if recall ≥ floor, FP < ceiling, score and matched count do not drop (212-214) plus `evaluate_profiles`
+(59, 220-228); write the ledger on accept (231), always the journal (`_record` 378 → journal.py:21/45).
+Writes `/.openultrasast/calibration/rule_policy.json`, `improve_journal.json`, `rule_signals.json`,
+and with `--memory` `memory_proposals.jsonl`, `signals.json`, outcome rows to the store (memory.py:355-380,
+508, 519). Callers: cli.py:45, 1170; lazily plane/tasks/loop.py:69, 80, 194-195, 219-221; plane/memory.py:1002
+→ loop.write_snapshot → improve.memory Guard; `loop-improve` task registered at plane/generate.py:380,
+loop.py:269. Nothing under benchmarks/, dagger/, .github imports it. No active spec names the command; only
+.kiro/steering/overview.md:65 and docs (README.md:220, 314; docs/memory.md, examples.md, index.md,
+threat-model.md; ops/ax/README.md:101). Tests: test_improve, test_improve_memory, test_cli_improve,
+test_subtraction_complete (:67-89), test_gate.py:135-136.
+
+```mermaid
+flowchart TD
+ A[cli improve
cli.py:433] --> B[_improve
cli.py:1106]
+ B --> M[MemorySession.open
improve/memory.py:460] --> F[MemorySession.finish
improve/memory.py:506]
+ B --> R[run_improvement
improve/evolve.py:259] --> RR[run_round
improve/evolve.py:150]
+ RR --> S[_scan / _evaluate
improve/evolve.py:307,319]
+ RR --> V[EvolveValidator
improve/validator.py:69]
+ RR --> P[evaluate_profiles
improve/evolve.py:59]
+ RR --> J[write_journal
improve/journal.py:21]
+```
+
+## (b) pairs.py (1,207), benchmark.py (504), repos.py (285)
+`ousast pairs` (cli.py:283-294) → `_pairs` (1175) → `load_pair_catalog` (pairs.py:229) → `select_slice`/
+`select_profile`/`select_split` (649/661/667) → `evaluate_catalog` (438) → `_evaluate_overlay_pair` (973) or the
+inventory path (876) → `result_payload` (673) or `pair_gate.print_pair_metrics` (pair_gate.py:52, cli.py:49).
+`ousast benchmark` (cli.py:232) → `_benchmark` (1076) → `create_benchmark_run` (benchmark.py:185) → `_run_scan` →
+`evaluate_benchmark` (224) → `write_benchmark_artifacts` (283). `ousast repos` (cli.py:296) → `_repos` (1199) →
+`load_repo_recipes` (repos.py:92) → `select` (282) → `resolve` (172; `_fetch` 188 only with `--fetch` or
+OPENULTRASAST_REPOS_NETWORK=1) → `verify` (224).
+
+The hunter-learning-loop label path needs only `load_pair_catalog` (with `_load_catalog_file`, `PairCase`,
+CatalogError) and `pair_cache_dir` (pairs.py:328): learn/labels.py:142, 615; learn/examples.py:292;
+benchmarks/pairs/advisory-fixes/materialize.py:51 (loaded by benchmarks/valid/catalog.py:41). Other non-CLI
+users: plane/harvest.py:131, loop.py:70, 222 (`load_pair_catalog`, `select_split`); improve/evolve.py:71
+(`evaluate_catalog`, `select_tier`, `select_vendored`, `GATING_TIERS`); model/candidates.py:268
+(`_materialize_side`, `_overlay_scan`, `_quick_scan`); benchmarks/learn/* (`_materialize_side`, `_targets`).
+Only CLI/pair_gate/tests: `select_profile`, `result_payload`, `duplicate_groups`, `split_by_repository`,
+`_header_lines`, `_body_digest`, `is_weak_label`, `pointer_recipe`, `load_mechanisms`, `load_datasets` (no
+non-test caller), `build_pair_signals` (re-exported by improve only). benchmark.py is core (cli.py:18,
+gate.py:22, map_gate.py:17, pairs.py:28, mcp.py:163, improve/evolve.py:21, plane/tasks/loop.py:68, 175).
+repos.py callers: cli.py:68-77, push_inputs.py:18, benchmarks/{ranking/position.py:26, push/*.py, repos/measure.py:34}.
+
+```mermaid
+flowchart TD
+ P[_pairs
cli.py:1175] --> L[load_pair_catalog
pairs.py:229]
+ P --> E[evaluate_catalog
pairs.py:438] --> O[_evaluate_overlay_pair
pairs.py:973]
+ P --> PM[print_pair_metrics
pair_gate.py:52]
+ B[_benchmark
cli.py:1076] --> CR[create_benchmark_run
benchmark.py:185]
+ B --> EB[evaluate_benchmark
benchmark.py:224]
+ B --> W[write_benchmark_artifacts
benchmark.py:283]
+ R[_repos
cli.py:1199] --> RS[resolve
repos.py:172]
+ R --> VF[verify
repos.py:224]
+```
+
+## (c) Gates (gate 205, map_gate 130, pair_gate 93)
+`gate.main` (gate.py:182): for each of eight `LANGUAGE_MANIFESTS` (30-35) load `benchmarks/manifests/.toml`
+→ `benchmarks/fixtures/`, quick scan (80), `evaluate_benchmark`; recall ≥ 0.90 and FP < 0.10 per language
+and overall (114-146). `map_gate.main` (118): scans split-sink-{c,java,javascript,python} (24-29); planted sink
+must be in the top hotspot band (69); Python map order must differ from hit-count order (96-99).
+`pair_gate.main` (80): vendored `local` slice of benchmarks/pairs/catalog.toml must have pair_pass_rate 100%
+(34-47). Callers: gate: ci.yml:28, dagger/ci.py:53, cli.py:42, loop.py:218 (constants), measure_php_quick.py:129,
+many tests via `LANGUAGE_MANIFESTS`; map_gate: ci.yml:30, dagger/ci.py:54, model/audit.py:30, test_map_gate,
+test_degradation_matrix; pair_gate: ci.yml:32, dagger/ci.py:55, **cli.py:49 (`print_pair_metrics`, module
+level)**, model/audit.py:30, test_pair_corpus.py:11, 138, test_degradation_matrix. Deleting map_gate/pair_gate
+breaks ci.yml:29-32, dagger/ci.py:54-55, every `ousast` command (cli.py:49), test_degradation_matrix.py:34, 41
+(diffs against benchmarks/measurements/2026-09-06-gate-baseline.txt), test_map_gate, test_pair_corpus.py:138,
+model/audit.py:30 `ROOTS`.
+
+```mermaid
+flowchart TD
+ CI[ci.yml checks
.github/workflows/ci.yml:27-32] --> G[gate.main
gate.py:182] --> DG[detection_gate
gate.py:114]
+ CI --> MG[map_gate.main
map_gate.py:118] --> SS[split_sink_map_gate
map_gate.py:102]
+ CI --> PG[pair_gate.main
pair_gate.py:80] --> PGF[pair_gate
pair_gate.py:34] --> EC[evaluate_catalog
pairs.py:438]
+```
+
+## (d) mcp.py (384), skills.py (372), index.py (74)
+`ousast mcp` (cli.py:305, 467) → lazy `serve` (mcp.py:362) → `McpServer.handle` (306) → tools (238): `scan`
+(62, quick/standard only, into `cli._run_scan`); `benchmark` (162, into benchmark + `_run_scan`); read-only
+over run_dir JSON: `status` (81), `findings` (101), `get_finding` (128), `evidence` (143), `artifacts` (157),
+`explain` (188), `export_report` (226); `propose_patch` (215) returns `not_implemented`. Only importer
+cli.py:468. skills: cli.py:667-670 standard mode `run_hunter_pool` → `schedule_hunter_tasks` (hunter.py:70) →
+`select_skill_snippets` (137) → `skills.route_skill_ids` (skills.py:318); ids stored as `selected_skills` on
+the task and trajectory (hunter.py:96, 157) and end up in `traces/hunter_trajectories.jsonl` (cli.py:707-710);
+detection is plain `quick_scan_findings` (151) and never reads the skills. `select_skill_context`, `skill_chunks`
+(322, 339) only test_skills; index.py imported only by skills.py:15 for `skill_chunks`, otherwise test-only.
+The `skills`/`mcp` strings in plane/manifests.py:556 and reconciler.py:130-132 are Workspace fields, not these
+modules.
+
+```mermaid
+flowchart TD
+ C[cli mcp
cli.py:467] --> SV[serve
mcp.py:362] --> T[_build_tools
mcp.py:238]
+ T --> SC[_scan
mcp.py:62] --> RS[_run_scan
cli.py:619]
+ T --> BM[_benchmark
mcp.py:162] --> RS
+ H[run_hunter_pool standard
cli.py:670] --> SK[select_skill_snippets
hunter.py:137] --> RT[route_skill_ids
skills.py:318]
+ CH[skill_chunks tests only
skills.py:339] --> IX[chunk_text_namespace
index.py:28]
+```
+
+## Confidence
+High on caller lists for gates, mcp, skills, index, the improve read/write sets and the CI dependencies;
+medium on the "CLI or tests only" list for pairs.py. Not read: `propose_from_memory` (improve/memory.py:282);
+whether ops/ runs `loop-improve`.
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U2-core.md b/PATHFINDER-2026-10-13/01-flowcharts/U2-core.md
new file mode 100644
index 0000000..23d69c4
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U2-core.md
@@ -0,0 +1,63 @@
+# U2 Shared core: the quick-rule path
+
+Traced read-only 2026-10-13. `hunter.py` is the regex quick-rule pool, not the LLM hunter. No model call
+anywhere on this path: `rank.py:32 complete_json` is a method on a Protocol used only by
+`rank_targets_with_model` (rank.py:46), whose only caller is tests/test_rank.py:82.
+
+## Happy path (`_run_scan`, cli.py:619)
+1. `load_config` (cli.py:624 → config.py:225): loads `.env` into os.environ (config.py:77-92), reads TOML, rejects retired keys (config.py:236).
+2. `load_policy` reads CWE_Score.tsv (policy/verycode.py:30-37); `load_ruleset` parses every rules `*.toml` by rglob, filters framework priors, applies the ledger overlay (ruleset/store.py:44-71); `assert_rules_resolve` (verycode.py:73).
+3. `ingest_sarif` (mapping.py:101) → `write_static_hints` (mapping.py:240).
+4. `preprocess_repository` (preprocess.py:61): `enumerate_source_files` with `git ls-files` (preprocess.py:96, 240), `build_file_target` + `detect_language` + `detect_tags` (114, 130, 146), `attach_static_hints` (mapping.py:116), `_git_commit` (285), `write_preprocess_artifact` (90).
+5. `uncovered_languages` (findings.py:78); `analyze_entry_points` (mapping.py:123-130) → `attach_reachability_hints` (mapping.py:181).
+6. `rank_targets` without a model → `heuristic_rank` (rank.py:36, 78); reachability from hints (rank.py:112, 227); `calibrate_rankings` (calibration.py:161); `write_rankings` (rank.py:72).
+7. Standard: `run_hunter_pool` (hunter.py:103) → `schedule_hunter_tasks` (70) → ThreadPool of 8 × `_run_hunter_task` → `quick_scan_findings` (151). Other modes: `quick_scan_findings` directly with emit floors (cli.py:675-685).
+8. `quick_scan_findings` (findings.py:45) compiles rules (60-62) → `_finding_from_match` (119) → `resolve_severity` (verycode.py:56), `_reachability_for_line` (154) → `StaticFinding` (16).
+9. `write_findings` (findings.py:113), `write_hunter_trajectories` (hunter.py:123), then score (`build_score_artifact`, scoring/project_score.py:98).
+
+```mermaid
+flowchart TD
+ CFG[load_config
config.py:225] --> POL[load_policy
policy/verycode.py:30] --> RS[load_ruleset
ruleset/store.py:44]
+ RS --> CHK[assert_rules_resolve
policy/verycode.py:73] --> SARIF[ingest_sarif
mapping.py:101] --> PRE[preprocess_repository
preprocess.py:61]
+ PRE --> ENUM[enumerate_source_files
preprocess.py:96]
+ PRE --> ASH[attach_static_hints
mapping.py:116]
+ PRE --> EP[analyze_entry_points
mapping.py:123] --> ARH[attach_reachability_hints
mapping.py:181]
+ ARH --> RANK[rank_targets / heuristic_rank
rank.py:36 / rank.py:78] --> CAL[calibrate_rankings
calibration.py:161]
+ CAL -->|standard| POOL[run_hunter_pool
hunter.py:103] --> TASK[_run_hunter_task
hunter.py:144] --> QS
+ CAL -->|other modes| QS[quick_scan_findings
findings.py:45]
+ QS --> FM[_finding_from_match
findings.py:119] --> SEV[resolve_severity
policy/verycode.py:56]
+ FM --> SF[StaticFinding
findings.py:16] --> SCORE[build_score_artifact
scoring/project_score.py:98]
+```
+
+## Side effects
+Subprocesses: `git ls-files -z` (preprocess.py:240), git commit hash (287). Writes: preprocess.py:93,
+findings.py:116, rank.py:75, hunter.py:125, mapping.py:242/247, config.py:258; ruleset/store.py:102, 113 are
+written by the learning loop, not scan. config.py:92 mutates os.environ. findings.py:42 loads PATTERN_RULES at
+import time. The hunter pool computes `selected_skills` and `retrieval_context` (hunter.py:96-97, 112) but only
+logs their ids; they never affect findings. **Bug:** the standard-mode pool skips
+`min_emit_priority`/`min_emit_precision` (compare cli.py:670 with cli.py:683-684).
+
+## Dependencies on other units
+hunter.py:139 → skills.route_skill_ids (skills.py:318); preprocess.py:255 → model.layout.is_vendored (lazy);
+findings.py:89 → model.partitions.FRONTENDS (lazy); config.py:11 → contracts.Contract. Same pipeline is also
+called by gate.py:81-84, map_gate.py:52-54, pairs.py:794-815, plane/tasks/alerts.py:120-122, push/quick.py:87-90,
+improve/evolve.py:311-314, hunter_tools.py:232/307.
+
+## mapping.py (1,087 lines): who imports what
+| Function | Importers |
+|---|---|
+| ingest_sarif :101, attach_static_hints :116, write_static_hints :240, write_entry_points :245 | cli.py / preprocess.py (+ tests) |
+| analyze_entry_points :123 | cli, gate, map_gate, pairs, hunter_tools, plane/tasks/alerts, learn/features, improve/evolve, push/quick, push/runner; 7 tests; 6 benchmarks |
+| attach_reachability_hints :181 | cli, gate, map_gate, pairs, plane/tasks/alerts, push/quick, improve/evolve |
+| php_hook_callbacks :134 | model/scan.py, model/taint.py; 2 benchmarks |
+| verifier_evidence_candidates :188, semgrep_mapping_tasks :204, codeql_mapping_tasks :218, differential_mapping_record :232, sharp_edge_record :236 | **tests only** (test_mapping.py); their records MappingTaskRecord, DifferentialMappingRecord, SharpEdgeRecord (:39, :48, :56) are produced nowhere else |
+
+## Overlap check
+- policy/ (CWE severity table) vs push/policy.py (revision/capability admission): different things.
+- scoring/ `REACH_MULT` (project_score.py:15) duplicates `_BASE_REACH_MULT` (calibration.py:255).
+- calibration.py: only cli.py:27 and tests import it.
+- contracts.py (base `Contract`), model/contracts.py (question/scope schemas), push/contracts.py (push schemas, builds on model/contracts): layered, not duplicates.
+
+## Confidence
+High on the call chain; medium on importer lists (name greps with -w). Not traced: parsers inside
+mapping.py:250-1087, skills routing internals, families.toml consumers beyond model/taxonomy.py:32.
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U3-pre-push.md b/PATHFINDER-2026-10-13/01-flowcharts/U3-pre-push.md
new file mode 100644
index 0000000..4e895b9
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U3-pre-push.md
@@ -0,0 +1,101 @@
+# U3 Pre-push hook
+
+Traced read-only 2026-10-13. Key finding for the hunter spec: **in the shipped configuration admission can never
+produce a finding.** `eligibility.json` has no `"enabled": true` entry (grep count 0), verdicts are NO-GO or
+experimental, so every candidate gets `capability_unavailable` or `capability_disabled` (policy.py:913-919).
+Engine hits appear only as advisory lines (background.py:113-145, report.py:241). The hook default deadline is
+30 s (hook/pre-push:24, cli.py:182, config.py:196); 300 s is the replay benchmark's `long_deadline` arm
+(benchmarks/unseen/replay.py:25); the background engine defaults to 900 s (cli.py:210).
+
+## Happy path
+1. Git runs `push/hook/pre-push:6-38` → `ousast pre-push . --artifact $dir/push-$$.json --deadline ${OUSAST_PUSH_DEADLINE:-30} --mode advisory --remote NAME URL`; docker wrapper if present (:10-12); prior hook chained (:34-36).
+2. `_advisory_hook` (cli.py:513) → `_hook_main` (523); argument errors become exit 0 "check did not run" (412-415). `_pre_push` (529) builds `PushConfig` (config.py:196), duplicates stdin (570), calls `runner.push` (584).
+3. `push()` (runner.py:609) creates one `ExecutionBudget(started + deadline, allowance=2)` (632); bounded work runs in a forked child via `_prepare` (56-115).
+4. Refs: `_prepare(updates)` 1 MiB cap (638-640) → `SnapshotAdapter.resolve_updates` (snapshot.py:200) → `parse_updates` (124), `_resolve` with merge-base (167, 191). No base → `_last_commit_quick`, quick only (runner.py:646-654, 503-523).
+5. `_analyze` (runner.py:257): provenance (322-327, 131); optional `ArtifactCache` only with `--cache-dir` (328-341).
+6. Snapshots: `materialize(head)`, `materialize(base)` (344-351, snapshot.py:462) into `mkdtemp("ousast-snapshot-")` (485).
+7. Discovery: `reuse.discovery(..., discover=_discover)` for head and base (356-363); `_discover` (118) uses preprocess, mapping, `regions_for`; `ReusingBackend` (365-385).
+8. Attribution: `adapter.compare` → `ChangeContext` (386, snapshot.py:226, diff-tree 270); `_uncovered_languages` (212, 389).
+9. Quick tier: `head_spans` → `_prepare(run_quick_tier)` (385-391; quick.py:47, 60); matches kept only on a changed line or in a changed function (quick.py:121-130); "advisory, not admitted, never blocking" (quick.py:35).
+10. Engine tier (`--engine inline` default): head `scan_repository(..., ScanBudget(max_model_calls=0))` (288, 396-406); base `policy.compare_targeted_base` (410-423, policy.py:731) → `compare_evidence` (policy.py:489) → `CandidateDelta`s.
+11. Admission: `AdmissionCandidate` with `CapabilityKey`, `dependency_gaps` (428-446) → `_prepare(_admit)` (447) → `_admit` (169) → `load_registry` (eligibility.py:152) + `admit_candidates` (policy.py:889).
+12. Vetoes only with explicit replay + `--experimental-record-vetoes` (449-457, 180-209; vetoes.py:412); never on the hook path (cli.py:550-551).
+13. `deadline_exhausted`, `analysis_incomplete` appended (465-477); `admission_push_result` (488, policy.py:1013); merge across comparisons (676-686) → `PushReport` (707); remote URL stored as sha256 only (700).
+14. `_background` (526): `background.previous()` always; `background.start()` only with `--engine background` (719-727).
+15. `assist(report, config=model_config)` (730-732) returns at once without `--model-config` (assistance.py:31-32); the hook never passes it.
+16. `_deliver` (560) → `deliver_report` (report.py:423) → `_save` forked, atomic, 0o600 (158, 120) → `render_report` (340); CLI prints (cli.py:595); chained hook (596-605); exit = `int(push_disposition == "block")` (report.py:104-105), only possible in `mode == "blocking"` (policy.py:1029-1031).
+
+Replay: `replay.py:19-50` packages `hook_flags=["--deadline", N]`; `replay_entry.py:141-156` runs
+`ousast pre-push --base B --head H --artifact out/push.json --deadline N` through `runner.replay`
+(runner.py:567); subprocess timeout N+30; exit 0 or 1 accepted (:159).
+
+```mermaid
+flowchart TD
+ G[git push
hook/pre-push:38] --> W{docker wrapper?
hook/pre-push:10}
+ W -->|yes| D[docker run --network none
hook/ousast-docker:46] --> M
+ W -->|no| M[_main / _advisory_hook
cli.py:405-411]
+ M --> HM[_hook_main
cli.py:523] --> PP[_pre_push
cli.py:529] --> PU[push
runner.py:609]
+ RP[replay_entry --base/--head
replay_entry.py:141] --> RE[replay
runner.py:567] --> AN
+ PU --> B[ExecutionBudget
runner.py:632] --> RU[resolve_updates
snapshot.py:200]
+ RU -->|no base| LC[_last_commit_quick
runner.py:503] --> APR
+ RU --> AN[_analyze
runner.py:257] --> MAT[materialize head/base
snapshot.py:462]
+ MAT --> DIS[discovery/_discover
reuse.py:160 runner.py:118] --> CMP[compare -> ChangeContext
snapshot.py:226]
+ CMP --> QT[run_quick_tier
quick.py:60] --> ENG{engine mode
runner.py:392}
+ ENG -->|inline| HS[scan_repository head
runner.py:396] --> BS[compare_targeted_base
policy.py:731]
+ BS --> ADM[_admit / admit_candidates
runner.py:169 policy.py:889]
+ ADM --> VR[record_vetoes replay-only
runner.py:449]
+ ADM --> APR[admission_push_result
policy.py:1013]
+ ENG -->|off/background| APR
+ APR --> BG[_background
runner.py:526] --> BGS[background.start Popen
background.py:47]
+ BG --> AS[assist witness index only
assistance.py:29] --> DL[deliver_report/_save
report.py:423 report.py:158] --> EX[exit_code
report.py:104 cli.py:610]
+```
+
+## Side effects
+Artifact JSON at `${OUSAST_ARTIFACT_DIR:-$XDG_CACHE_HOME|~/.cache}/openultrasast/push/push-.json`, atomic
+`.partial-`, 0o600 (report.py:120-150), refused inside the repo (runner.py:561). Snapshots in
+`$TMPDIR/ousast-snapshot-*`, removed after (snapshot.py:480-485, 648). Cache only with `--cache-dir` outside the
+repo (runner.py:310, 328). Background results under `/engine//` (background.py:29-33,
+60-104). Git: rev-parse, cat-file, check-ref-format, merge-base, diff-tree, ls-tree, cat-file blob, all with
+`-c protocol.allow=never -c core.hooksPath=/dev/null` (snapshot.py:63-570); no fetch. Subprocesses: forked
+children (`_prepare` runner.py:66, `_save` report.py:168), Joern (cpg/backend.py:641, 1303, 1428), detached
+background CLI (background.py:69-101), chained hook (cli.py:600). Model calls: none by default
+(`max_model_calls=0` runner.py:288; `assist` at most one call, only with `--model-config`, assistance.py:89-110).
+
+## External dependencies
+Core: `enumerate_source_files`/`build_file_target` (runner.py:28, 121-122), `detect_language` (quick.py:69),
+`analyze_entry_points` (runner.py:22, 123), `attach_reachability_hints` (quick.py:68), `PATTERN_RULES` /
+`quick_scan_findings` (quick.py:67, 89), `rank_targets`, `load_policy` (quick.py:70-71). Engine: `JoernBackend`,
+`engine_runtime_identity`, `joern_version` (runner.py:21), `scan_repository` (26), `regions_for`,
+`declared_sources`, `layout_facts` (24-27); policy.py:19-22 imports `model.pipeline._arbitrate_all` and
+`model.ladder.Rung`. LLM client: `model.endpoint.ChatEndpoint`/`resolve_chat_endpoint` (assistance.py:18).
+push_scoring: `_metrics`, `score` (eligibility.py:18, 163-167) on every admission.
+
+## Where an LLM detection tier plugs in
+Defects enter admission at runner.py:428-447: the tuple of `AdmissionCandidate`s from `delta.candidates` goes
+to `_admit` (169) → `admit_candidates(candidates, capabilities=...)` (policy.py:889, origin-agnostic per its
+docstring :890). A hunter tier adds its candidates before runner.py:447. A candidate must carry (policy.py:61-75,
+837-845; gates 906-967): a `CandidateDelta` with family, `site` = `"{path}:{line}:"`, `defect_id`, a non-null
+`witness`; `rung == Rung.ENTAILED` (else `insufficient_rung`; :933-935 says a non-engine producer needs its own
+protocol); `novelty`/`reason` from four allowed pairs with non-empty `change_evidence` (:941-946); matching
+base/head OIDs and semantics; a `head_operation: EvidenceOperation` whose (language, family, mechanism) matches
+the `CapabilityKey` and whose operation symbol is in the capability's `operation_symbols`; and an enabled PASS
+`CapabilityAdmission` from eligibility.json (none exists today) with empty `dependency_gaps`. Hunter output that
+cannot meet that bar can mirror the quick tier: an advisory, never-admitted payload on `PushReport`
+(runner.py:499, report.py:143). `assistance.py` cannot add a finding: it asks for an index only (22-26), accepts
+exactly `{"selections":[{defect_id, witness_index}]}` over existing admitted ids (120-140), replaces only
+`model_assistance` (153).
+
+## Off the happy path
+- `vetoes.py`: only via `_record` under `--experimental-record-vetoes`, rejected outside replay (cli.py:550); importer runner.py only; `benchmarks/push/m1a_record_vetoes.py` needs it.
+- `install.py`: install-time only (cli.py:476).
+- `cache.py` / `reuse.ReusingBackend`: inactive by default (no `--cache-dir` in the hook); `reuse.discovery` still runs.
+- `background.py` start: off unless `OUSAST_PUSH_ENGINE=background`; does not work through Docker (ousast-docker:17).
+- `assistance.py`: no-op on the hook path.
+- `eligibility.py`: always runs, yields no enabled capability; admission is structurally empty.
+- `push_inputs.py`: only `benchmarks/push/{measure,m1a_record_vetoes,transfer_replay}.py` and one test.
+- `push_scoring.py`: production (eligibility.py:18) plus benchmarks/push/measure.py:20 and tests.
+
+## Confidence
+High for runner, CLI, assistance, quick, background, admission, report. Medium for snapshot.compare internals
+(snapshot.py:226-460), `compare_evidence` (policy.py:489-730), cache/reuse bodies. Not verified: eligibility
+bindings vs current provenance (`eligibility_provenance_mismatch`, eligibility.py:159-160); ops/install-pre-push.
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U4-engine.md b/PATHFINDER-2026-10-13/01-flowcharts/U4-engine.md
new file mode 100644
index 0000000..034a575
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U4-engine.md
@@ -0,0 +1,86 @@
+# U4 Engine: CPG backend and model layer
+
+Traced read-only 2026-10-13.
+
+## Happy path A, standard scan (no deadline)
+1. cli.py:804 → `_run_model_layer` (1286), early return if disabled (1300).
+2. `resolve_cpg_backend` (cpg/backend.py:1813) → `has_cpg` (cpg/capability.py:14) → `semantic.engines.joern_available` (engines.py:25); `JoernBackend` or `NullBackend`; `cpg_unavailable` degradation (cli.py:1310-1312).
+3. `regions_for(..., shipped=declared_sources(root))` (model/regions.py:78, model/shipped.py:45; cli.py:1315); `coverage_rows` (model/report.py:87; cli.py:1324).
+4. `run_stage("model", scan_repository(...))` (cli.py:1332); no `execution_budget`, so no deadline.
+5. `scan_repository` (model/scan.py:143): drop vendored regions (164-165), reject duplicate question ids (167), `_scan_repository_impl` (213) inside a finally that disposes graphs and closes the session (187-198).
+6. `load_families` (model/taxonomy.py:201; scan.py:233).
+7. `build_partitions` (model/partitions.py:168; scan.py:244): `os.fwalk` no-follow, bucket by `LANGUAGE_BY_EXTENSION`, copy each language into `mkdtemp("ousast-input-")` (249), `build(scratch, frontend)` (280) via `scan._build` (scan.py:1444) with `vendored_directories` as exclude.
+8. `JoernBackend.build` (backend.py:419) → `_build_impl` (573): `mkdtemp("ousast-cpg-")` (584), readability precondition (600), then sharded frontend (609) / JS-TS direct frontend + `_apply_overlays` (630-638) / `joern-parse --output cpg.bin` (642-643) with frontend retry (648). Returns `CpgResult` with run, run_batch, run_batch_once, cleanup (655-663).
+9. `_evidence_pass` (scan.py:1275; called 345): taint requests `evidenceOnly` via `_portioned_evidence` (1252) and `cpg.run_batch("taint")` → `evidence_from_rows` (model/evidence.py:130; scan.py:1338); `order_by_evidence` (1399; 359).
+10. `_scope_work` (751; 401): scoping and tier-0 pruning.
+11. Query phase per kind (434-524): `_grouped` (1479) with each arbiter's `request_params` (taint.py:22, dominance.py:30, config_value.py:22); `_batched` (969) → `run_batch_once` → `JoernBackend._batch_once` (backend.py:1193): writes `/-requests-*.json` (1222-1224), tries the session (1229), else `joern --script queries/.sc --param cpgFile=… --param requestsFile=…` (1231-1242) via `_run` (1514, Popen, process group killed on timeout); `extract_payload` (1250). `__census__` checks empty graph/shards (scan.py:490-509).
+12. Arbitration (540-586): prefetched `CpgResult` per question (548) → `scan_region` (model/pipeline.py:124) → `_arbitrate_all` with taint/dominance/config verdicts; rungs on the ladder (model/ladder.py:29 `Rung`, 45 `Verdict`): ENTAILED = graph decided (pipeline.py:139-146); CORROBORATED and SUSPICION only from a judge (151-181); `enumerate_candidates` only with a judge (scan.py:556-558).
+13. Graph-gap demotion (591-655), `_deduplicated`/`_ordered` (659), `ModelScanResult` (662); CLI `_finding_from_model` (cli.py:1361).
+
+## Happy path B, pre-push (with deadline)
+`push()` (runner.py:609) → `ExecutionBudget` (632; replay 586) → `_analyze` (257) with `JoernBackend()` (290) and
+optional `ArtifactCache` (315); `discovery` wraps `_discover` (118); `ReusingBackend.build` (push/reuse.py:32) →
+`graph_identity` (backend.py:447), cache hit `load_graph` (522) else `build` + `describe_graph` (494, seals a
+`GraphArtifact` from `census.sc`) + `cache.publish`. `scan_repository(..., execution_budget, ranking_mode=
+"evidence", change_context)` (runner.py:396), client withheld (scan.py:177), `max_model_calls=0` (runner.py:288),
+`affected_context` (regions.py:258; scan.py:363). Base via `compare_targeted_base` (policy.py:731, 774).
+Deadline enforcement: `_within_deadline` (scan.py:1435) with `_arbitration_reserve` (884), `_build` pre-checks
+(1449-1462), partition walk checks (partitions.py:182-273), `_time_available` (backend.py:1377), `_run_bounded`
+(1414-1471: timeout capped at deadline, cancel window, partial stdout harvested), `_cleanup_limit` (1399);
+`deadline_exhausted` appended (scan.py:203, runner.py:458).
+
+```mermaid
+flowchart TD
+ CLI[_run_model_layer
cli.py:1286] --> RES[resolve_cpg_backend
cpg/backend.py:1813] --> CAP[has_cpg / joern_available
cpg/capability.py:14]
+ CLI --> REG[regions_for + declared_sources
model/regions.py:78]
+ PUSH[push / _analyze
push/runner.py:609 / :257] --> BUD[ExecutionBudget
model/contracts.py:19]
+ PUSH --> REU[ReusingBackend.build
push/reuse.py:32] --> ART[graph_identity/load_graph/describe_graph
cpg/backend.py:447-570]
+ CLI --> SR[scan_repository
model/scan.py:143]
+ PUSH --> SR
+ PUSH --> BASE[compare_targeted_base
push/policy.py:731] --> SR
+ SR --> IMPL[_scan_repository_impl
model/scan.py:213] --> PART[build_partitions
model/partitions.py:168] --> BLD[_build
model/scan.py:1444]
+ BLD --> JB[JoernBackend.build/_build_impl
cpg/backend.py:419/573] --> JP[joern-parse / frontend subprocess
cpg/backend.py:642]
+ IMPL --> EVP[_evidence_pass
model/scan.py:1275] --> EVR[evidence_from_rows
model/evidence.py:130]
+ IMPL --> SCOPE[_scope_work
model/scan.py:751] --> GRP[_grouped request_params
model/scan.py:1479] --> BO[_batch_once
cpg/backend.py:1193]
+ EVP --> BO
+ BO --> SES[EngineSession opt-in
cpg/session.py:68]
+ BO --> JS[joern --script queries/*.sc
cpg/backend.py:1231]
+ IMPL --> SREG[scan_region
model/pipeline.py:124] --> VER[taint/dominance/config verdict
model/taint.py:72] --> LAD[Rung/Verdict
model/ladder.py:29]
+ IMPL --> RESULT[ModelScanResult
model/scan.py:662] --> SF[_finding_from_model
cli.py:1361]
+```
+
+## Side effects
+JVMs: `joern-parse` or a frontend, then `joern --script` once per query kind, killed as process groups
+(backend.py:1514-1550, 1659); optional `joern --server` under `OUSAST_ENGINE_SESSION=1` with a budget
+(session.py:132-137; backend.py:65, 405, 1319). No Docker inside U4; the container boundary is the hook's
+`ousast-docker` wrapper. Files: `$TMPDIR/ousast-input-*` (partitions.py:249), `$TMPDIR/ousast-cpg-*` with
+cpg.bin, `*-requests-*.json`, `engine-session/` (backend.py:584, 1222, 1324), removed via cleanup (scan.py:1425);
+stale trees swept only without a budget (backend.py:586-587, 1638). Push-only `ArtifactCache` 0700 outside the
+repo (push/cache.py:35-44; runner.py:286). Standard scan has no graph cache.
+
+## The CPG↔Model cycle
+cpg/backend.py:37 and cpg/session.py:44 import exactly one name, `ExecutionBudget` (model/contracts.py:19-34): a
+frozen dataclass over `openultrasast.contracts.Contract` with two floats and a `__post_init__`. No model logic,
+so it can move to a shared contracts module. Reverse couplings: model/partitions.py:14 imports the private
+`_DeadlineExpired`, `_remove_owned_tree` from cpg; cpg/artifact.py:151 imports the same two lazily.
+
+## Dependencies
+Out: semantic (`joern_available` cpg/capability.py:11; `load_facts` scan.py:31, layout.py:14, specs.py:24;
+obligations facts specs.py:25; `parse_file`/obligations candidates.py:107, 215-217); core (`Contract`
+model/contracts.py:15, cpg/artifact.py:13; `LANGUAGE_BY_EXTENSION` partitions.py:15; `php_hook_callbacks`
+scan.py:1521; redaction pipeline.py:31; `StaticFinding` report.py:20; pairs candidates.py:268 `ceiling` only).
+In: cli.py:1302-1306, 1322, 1705; push/runner.py:20-27, 218; push/policy.py:22; push/reuse.py; push/cache.py;
+learn/slice.py:18; learn/features.py:26, learn/labels.py:47, plane/tasks/features.py:55 (taxonomy);
+plane/tasks/repo_facts.py:28, preprocess.py:255 (layout); findings.py:86 (`partitions.FRONTENDS`).
+
+## Removal candidates
+- `model/audit.py`: no src importer; maintainer `__main__` tool (audit.py:45, 255); tests/test_module_audit.py, tests/test_search_demo_replay.py:200, measurement 2026-09-08-module-audit.json.
+- `model/report.py`: only `coverage_rows` live (cli.py:1322); `SliceRow`, `overfitting_gap`, `render`, `render_findings` (24-75) only tests/test_model_report.py.
+- `cpg/queries/vocabulary.sc`: only tests/test_cpg_queries_compile.py:32 and benchmarks/push/ontology_gap.py:102.
+- `model/candidates.py`: only with a judge (scan.py:556); push never reaches it; `ceiling`/`labeled_*` serve cli.py:1705 only.
+- Conditional, not dead: cpg/session.py (opt-in), cpg/artifact.py sealing (push with cache_dir). `model/shipped.py` is live (cli.py:1306, runner.py:27, 123).
+
+## Confidence
+High on the chain, subprocess boundary, deadline plumbing; medium on unused-module claims. Not read in detail:
+`_batched`/`_PortionSizer` (scan.py:969-1232), `_build_sharded` (backend.py:765-1115), session internals,
+`.sc` bodies, `taint.verdicts` ranking (taint.py:131-312).
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U5-semantic.md b/PATHFINDER-2026-10-13/01-flowcharts/U5-semantic.md
new file mode 100644
index 0000000..cbff555
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U5-semantic.md
@@ -0,0 +1,87 @@
+# U5 Semantic layer (tree-sitter) and complexity map
+
+Traced read-only 2026-10-13. Correction: `prove_filter` acts on hotspots, not findings, and runs only in deep
+mode before regress (cli.py:847). MAP runs in standard and deep (stages.py:15-16).
+
+## (a) Overlay path, under Stage.MAP (cli.py:758)
+1. `load_facts` (cli.py:770 → facts.py:412) reads `ruleset/semantic/*.toml` (facts.py:16, 179-213), memoised (420-422); FactLoadError → degradation (cli.py:771-773).
+2. `adjudicate` (overlay.py:65) → `_flows_for` (161): per target read text (170), `parse_file` (ir.py:52) → `parse_with_cst` (cst.py:65) with `grammar_for` (extra.py:36) → `file_ir_from_tree` (cst.py:86) → FileIR of FunctionIR with Binds and CallSites (ir.py:10-50). Python falls back to `parse_python_ast` (ir.py:59-60); other languages `language_unsupported` (61).
+3. `named_ranges_from_ir` (functions.py:96); `analyze_ir` (taint.py:60) → `_analyze_function` (76): parameters start TAINTED, up to 8 fixpoint passes (`_bind_state` 101), `_analyze_call` (123) emits TaintPath, Demotion or incomplete.
+4. `_joern_paths` (overlay.py:192) probes `joern_available` but `joern_flow` always returns `()` (engines.py:42-47).
+5. `_record_for_finding` (overlay.py:203) → promote / demote / unadjudicated; unmatched taint paths become coverage records (94-121). `write_overlay` (126); `finding_from_coverage` (132) → findings.
+6. Deep + sandbox: `filter_promoted_hotspots` (prove_filter.py:13) → `run_regression` (cli.py:854).
+
+Obligations (default on, config.py:181): `_check_obligations` (cli.py:1388) → `parse_file`, `load_declared_policy` (policy.py:59) → `check_obligations` (check.py:55: find_operations, find_discharges, sibling_sets, consistency_anomalies, OrderDominance dominance.py:27) → `adjudicate_intent` **LLM call** (intent.py:26, 47) → `findings_to_static` (check.py:145) → `rank_obligations` (cli.py:800).
+
+```mermaid
+flowchart TD
+ A[run scan MAP
cli.py:758] --> B[load_facts
facts.py:412] --> C[adjudicate
overlay.py:65] --> D[_flows_for
overlay.py:161]
+ D --> E[parse_file
ir.py:52] --> F[parse_with_cst
cst.py:65] --> G[grammar_for
extra.py:36]
+ F --> H[file_ir_from_tree
cst.py:86]
+ E --> E2[parse_python_ast fallback
ir.py:63]
+ H --> I[named_ranges_from_ir
functions.py:96]
+ H --> J[analyze_ir
taint.py:60] --> K[_analyze_function
taint.py:76] --> L[_analyze_call
taint.py:123]
+ D --> M[_joern_paths stub
overlay.py:192]
+ C --> N[_record_for_finding
overlay.py:203] --> O[write_overlay
overlay.py:126]
+ O --> P[finding_from_coverage
overlay.py:132]
+ O --> Q[filter_promoted_hotspots
prove_filter.py:13] --> R[run_regression
cli.py:854]
+```
+
+## (b) Complexity MAP path
+`build_complexity_map` (cli.py:760 → map.py:37) → `collect_signals` (signals.py:44: nesting via
+`measure_nesting` 86, adjacent tests, tags, reachability hints, `_score` per (path, function)) →
+`_hotspot_from_signals` bands high ≥ 6, medium ≥ 3 (map.py:62-102) → `attach_test_hints` (hints.py:52, 79) →
+`apply_overlay(load_ledger)` (map.py:47-48, ledger.py:37, 103) → `write_complexity_map` (map.py:57).
+Consumers: `_attach_tool_hunter` passes the top `max_hunter_hotspots` to `run_tool_hunter` (cli.py:964,
+tool_hunter.py:323) and rewrites the map (cli.py:1007); deep mode `filter_promoted_hotspots` →
+`regress/candidate.select_candidates` (candidate.py:63) with `hotspot_key`, `select_forced_candidates`
+(ledger.py:33, 142); `plane/tasks/verify.py:179-186` builds synthetic Hotspots for the hunter; map_gate.py:18, 96.
+After regress `persist_verdicts` (cli.py:872 → ledger.py:60, 81, 49) writes the ledger for the next scan.
+
+```mermaid
+flowchart TD
+ A[MAP stage
cli.py:760] --> B[build_complexity_map
map.py:37] --> C[collect_signals
signals.py:44]
+ C --> D[_hotspot_from_signals
map.py:62] --> E[attach_test_hints
hints.py:52] --> F[load_ledger + apply_overlay
ledger.py:37,103]
+ F --> G[write_complexity_map
map.py:57]
+ G --> H[_attach_tool_hunter
cli.py:964] --> I[run_tool_hunter
tool_hunter.py:323]
+ G --> J[filter_promoted_hotspots
prove_filter.py:13] --> K[select_candidates
regress/candidate.py:63] --> L[persist_verdicts
ledger.py:60] --> M[write_ledger
ledger.py:49]
+ N[plane hunt
plane/tasks/verify.py:179] --> I
+```
+
+## Side effects
+`grammar_for` builds a new `Language(...)` on every call, no cache, once per parsed file (extra.py:46-53,
+cst.py:68). `joern_available` runs `joern --help` (5 s timeout) once per target from `_joern_paths` and again in
+`joern_flow`, uncached and pure overhead since `joern_flow` returns `()` (engines.py:33-47, overlay.py:193).
+Writes `/overlay.json`, `/complexity_map.json`, and into the scanned repo
+`.openultrasast/calibration/complexity_ledger.json` (ledger.py:49-57 via cli.py:872). Reads ruleset TOMLs and
+the target's `.openultrasast/obligations.toml` (config.py:183). Network: `adjudicate_intent` (intent.py:47).
+
+## Facts the hunter annotation model can reuse (hunter-learning-loop R3)
+| Fact | Producer | Without Joern? |
+|---|---|---|
+| Enclosing function at a line | `function_at` (functions.py:136) over `named_function_ranges` (120) / `named_ranges_from_ir` (96) | yes (regex scan; Python uses ast, functions.py:126-133) |
+| Callers of a function | not in U5; `learn/features.py:371` (`callers`), `plane/tasks/verify.py:128` (`callers_of`) | n/a, U5 IR has CallSites per function (ir.py:22-40) but no caller index |
+| Entry points | `mapping.analyze_entry_points` (consumed by check.py:84, 87) | yes |
+| Source/sink/sanitizer roles | `load_facts` SourceFact/SinkFact/SanitizerFact (facts.py:37-80, 412); matched by `_source_in`, `_sink_for`, `_sanitizer_for` (taint.py:222-238); already reused by learn/slice.py:21, learn/roles.py:38 | yes |
+| Def-use within a function | Bind/CallSite (ir.py:11-30), taint state (taint.py:76-98), TaintPath.names (taint.py:16) | yes, needs tree-sitter or Python ast |
+| Guards/obligations | `find_operations`, `find_discharges` (operations.py:47, 76), `OrderDominance.dominates` (dominance.py:32), `check_obligations` (check.py:55); sanitizer demotions with `dominating_fact` (taint.py:136-146) | yes, order-based only |
+
+## Dependencies
+U5 imports findings.StaticFinding, policy, preprocess.FileTarget, ruleset.frameworks. Imported by cli.py:37-38,
+82-90, 1403-1442; hunter_tools.py:224, 254-257, 319; pairs.py:41-44, 838-840; model/candidates.py:107, 215-217;
+model/specs.py:24-25; model/layout.py:14; model/scan.py:31; learn/roles.py:38; learn/slice.py:19-21;
+learn/features.py:164, 487; mapping.py:154; cpg/capability.py:10; map_gate.py:18; tool_hunter.py:13;
+regress/candidate.py:10-11; plane/tasks/verify.py:179; benchmarks/valid/hunter_arm.py:15.
+
+## Removal candidates (no non-test caller)
+- `prove_filter.promoted_findings` (prove_filter.py:28): test-only; cli.py:848-852 reimplements it inline.
+- `engines.joern_flow` stub (engines.py:42-47) and `taint.merge_joern_paths` (taint.py:277): Joern is never used on the semantic path.
+- `engines.tree_sitter_available`: export + tests only. `taint.analyze_file` (taint.py:53): test-only.
+- `obligations/shapes.py` public API (`derive_obligation` 111, `explain_skip` 139, `obligation_mechanisms` 406, `is_obligation_row` 401, `ObligationShape` 41): test-only; private helpers live via operations.py:19.
+- `check_obligations(store_shapes=())` is always empty (cli.py:1438, "the store was deleted"), so `_known_fix`, `_usable_shape` (check.py:252-258) are dead in practice.
+- obligations package as a whole is **live** (cli.py:1403-1444, hunter_tools.py:251-285, pairs.py:834-862, model/candidates.py:217-222). All public complexity/* functions are live.
+
+## Confidence
+High on call chains; medium on dead-symbol claims (greps over src, tests, benchmarks). Not read: cst.py `_KIND`
+tables, signals._score internals, find_discharges, siblings, policy, run_regression, map_gate beyond :96; whether
+`hunter_tools.flows` is registered as a hunter tool.
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U6-hunter-client.md b/PATHFINDER-2026-10-13/01-flowcharts/U6-hunter-client.md
new file mode 100644
index 0000000..d1390ed
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U6-hunter-client.md
@@ -0,0 +1,103 @@
+# U6 LLM hunter and model client
+
+Traced read-only 2026-10-13. The hunt path is clean. Below it: seven chat-client types or wrappers with two call
+methods, four separate usage-to-cost paths, four curated tools that no caller ever enables, and a prompt that
+names only the three default tools.
+
+## Happy path of one hunt
+1. `cli.py:976` `_attach_tool_hunter` → `resolve_hunter_client` (tool_hunter.py:239): `OPENULTRASAST_HUNTER_CLIENT` scripted/dump/unsafe → `scripted_hunter_client` (246-252); `DEEPSEEK_API_KEY` → `DeepSeekChatClient(OpenRouterChatClient(base_url=DeepSeek), endpoint=ChatEndpoint(prices=price_of("deepseek-flash")))` (253-259); `OPENROUTER_API_KEY` → `OpenRouterHunterClient.from_env()` (260-262); else None and the CLI skips (cli.py:977-978).
+2. `run_stage("tool_hunter", run_tool_hunter(root, hotspots, client, model, max_steps=4))` (cli.py:980-988; DEFAULT_MAX_STEPS tool_hunter.py:30).
+3. `run_tool_hunter` (323): `[]` if max_steps<=0 (339); `_tool_schemas(tools)` (341, 417); `DEFAULT_TOOL_NAMES = ("read_file","grep_repo","find_refs")` (33).
+4. `_SYSTEM_PROMPT` (140-145) names only those three and asks for path, line, title, rationale (no family).
+5. `_user_message` (423): context files first via `read_file` (429-434) for prefix caching; question last, `user_prompt` or `_hotspot_prompt` (439-454: hotspot JSON with path, function_name, score, band, rationale).
+6. Tool loop (347-356): `client.complete(model, messages, tools=schemas)` (348); `_assistant_tool_message` replays `reasoning_content` (457, 473-475); `_tool_result_message` per call (479) → `_run_tool` (488, errors in-band, output cut at 16*max_chars 496-497) → `_dispatch_tool` (500-529) → hunter_tools `read_file` (28), `grep_repo` (69), `find_refs(root, symbol, None)` (83; mapping index always None, tool_hunter.py:528), optional `read_definition` (174), `entry_points` (194), `flows` (220), `obligations` (251). Paths clamped by `clamp_repo_path` (hunter_tools.py:15); `_source_lines` memo shared (41-59).
+7. Ending: content with no tool used, or empty → `[]` (358-360); JSON → `_findings_from_content` (361-363); prose or out of steps after tools → `_final_answer` (364-368): one extra `complete(..., tools=[], json_object=False)` asking for path, line, title, rationale **and family** (388-405).
+8. `_findings_from_content` (557) accepts a bare array, `{"findings":[...]}` or `{"value":[...]}` (561-569) via `_load_json` (581).
+9. `_finding_from_item` (593): clamp path (600); `StaticFinding` with `evidence_level="suspicion"`, `confidence="low"`, id `tool-hunter::` (615-631); `tags=[*tags, "family:"]` when given (611-614, 628); `ranking_priority` from hotspot score (629); sorted by id (578). CLI dedups and caps (cli.py:989-1000).
+
+```mermaid
+flowchart TD
+ A[_attach_tool_hunter
cli.py:962] --> B[resolve_hunter_client
tool_hunter.py:239] --> C[run_tool_hunter
tool_hunter.py:323]
+ C --> D[_SYSTEM_PROMPT
tool_hunter.py:140]
+ C --> E[_user_message
tool_hunter.py:423] --> E2[_hotspot_prompt
tool_hunter.py:439]
+ E --> RF[read_file
hunter_tools.py:28]
+ C --> L[client.complete loop
tool_hunter.py:347-356] --> T[_dispatch_tool
tool_hunter.py:500]
+ T --> RF
+ T --> G[grep_repo
hunter_tools.py:69] --> SL[_source_lines memo
hunter_tools.py:41]
+ T --> FR[find_refs
hunter_tools.py:83] --> SL
+ T -.never enabled.-> RD[read_definition
hunter_tools.py:174]
+ T -.never enabled.-> EP[entry_points
hunter_tools.py:194]
+ T -.never enabled.-> FL[flows
hunter_tools.py:220]
+ T -.never enabled.-> OB[obligations
hunter_tools.py:251]
+ L --> FA[_final_answer
tool_hunter.py:368] --> FC
+ L --> FC[_findings_from_content
tool_hunter.py:557] --> FI[_finding_from_item
tool_hunter.py:593] --> SF[StaticFinding + family: tag
tool_hunter.py:615]
+```
+```mermaid
+flowchart TD
+ CP[ChatClient Protocol .complete
tool_hunter.py:163]
+ CA[Caller.ask cache/replay
learn/program.py:369] --> MC[MeteredClient .complete
plane/budget.py:68]
+ FM[FillMeter subclass
learn/fill.py:44] -.is-a.-> MC
+ REC[_Recording
plane/tasks/verify.py:66] --> MC
+ MC --> DS[DeepSeekChatClient .complete
model/endpoint.py:91]
+ MC --> ORH[OpenRouterHunterClient .complete
tool_hunter.py:203]
+ MC --> SC[ScriptedChatClient
tool_hunter.py:176]
+ DS --> EPC[ChatEndpoint.cost / Prices
model/endpoint.py:70,43]
+ DS --> OR[OpenRouterChatClient .complete_chat_raw
provider/openrouter.py:84]
+ ORH --> ORC[OpenRouterChatClient .complete_chat
provider/openrouter.py:69] --> OR
+ OR --> RT[call_with_retry
provider/openrouter.py:34]
+ W[search Worker._call + SpendBudget
search/worker.py:218] --> OR
+ DS -.implements.-> CP
+ MC -.implements.-> CP
+```
+`Caller` wraps `MeteredClient` (program.py:371-372; hunter_arm.py:65); verify.py's `_Recording` wraps it (181,
+267, 378). The CLI hunt (cli.py:976) passes the bare client: no meter.
+
+## Side effects
+Every call POSTs `{base_url}/chat/completions` (openrouter.py:100-112), `temperature: 0` (94), 3 retries with
+backoff on 429/5xx (34-52, 115). DeepSeek adds `thinking: disabled`, `response_format`, `logprobs`
+(endpoint.py:164-175) and re-calls once on empty JSON (135-144): one `complete` = up to 2×3 HTTP attempts. A hunt
+costs at most max_steps+1 calls. Tools read only inside the repo root; `entry_points`, `flows`, `obligations`
+re-run `preprocess_repository` and quick-scan/adjudication or IR parsing over every target, uncached
+(hunter_tools.py:304-308, 232-234, 268-274, 170-171). The hunter writes nothing; `Caller.ask` writes
+`responses/.json` (program.py:412-413). Usage: `DeepSeekChatClient.usage` one row per raw call
+(endpoint.py:98, 181-183); `cost_usd` via `ChatEndpoint.cost` (147-151, 78-88); `MeteredClient._absorb_rows`
+diffs and prices (budget.py:154-167), raises `BudgetExhausted`/`AccountError` (107-116, 141-145). The
+`OpenRouterHunterClient` path records no usage (tool_hunter.py:223 uses `complete_chat`): a meter sees zero.
+
+## Duplication inside the unit
+Chat-client types: `ChatClient` Protocol `.complete` (tool_hunter.py:163); `ScriptedChatClient` (176);
+`OpenRouterHunterClient` adapter over `complete_chat` (203; drops `json_object`, records no usage 222-223);
+`DeepSeekChatClient` `.complete/.bounded/.cost_usd` over `complete_chat_raw` (endpoint.py:91);
+`OpenRouterChatClient` `.complete_chat/.complete_chat_raw/.complete_json` (openrouter.py:56, 69, 84, 128);
+`MeteredClient` (budget.py:68) + `FillMeter` (learn/fill.py:44); `Caller.ask` (program.py:369, 397). Thin
+wrappers: `_Recording` (verify.py:66), `_CountingClient` (model/scan.py:1628). Related: `RankerClient.complete_json`
+(rank.py:31), `StubModel.complete_chat_raw` (search/worker.py:153-163), `_NoModel`/`_RefusingChat`
+(verify.py:121, experiments.py:1003). Two resolvers: `resolve_hunter_client` (tool_hunter.py:239) and
+`resolve_chat_endpoint` (endpoint.py:192); the hunter ignores config models and prices against
+`DEFAULT_DETECTOR_MODEL` not the model it runs (tool_hunter.py:258); the OpenRouter branches build different
+wrappers (tool_hunter.py:262 vs endpoint.py:221). Three "scripted flags" sets (tool_hunter.py:36-39,
+endpoint.py:39). Four cost paths: `ChatEndpoint.cost`→`cost_usd` (endpoint.py:78, 147; push/assistance.py:103-110);
+`cost_of` + `_absorb_rows` (budget.py:59, 158, arithmetic copied from the first per budget.py:60; `Caller.usd`
+program.py:404-406, 420-423; `verify._sum_usage` 232-244); `SpendBudget`/`SpendReservation` (budget.py:260-322)
+driven by search/worker.py:218-270 which repeats the DeepSeek extra_body logic (249-259); `_CountingClient.cost_usd`
+(scan.py:1628-1640). Shape mismatch: `DeepSeekChatClient.usage` is a list (endpoint.py:98), `MeteredClient.usage`
+a dict (budget.py:95); `Caller.ask` reads a dict (program.py:405), hence the shim in hunter_arm.py:56-65.
+
+Dead tools: no `run_tool_hunter(` call in src or benchmarks passes `tools=` (cli.py:982, verify.py:189,
+hunter_arm.py:245, benchmarks/archive/*). Only tests/test_hunter_curated_tools.py:143 enables `read_definition`,
+`obligations`. So `read_definition`, `entry_points`, `flows`, `obligations` plus their schemas (tool_hunter.py:94-137)
+and dispatch branches (505-521) are dead in production; `find_refs` mapping-record branch (hunter_tools.py:86-101,
+118-164) too. Prompt mismatch: `_SYSTEM_PROMPT` names only 3 tools (141) and asks for no `family` (143) while
+`_final_answer` asks for `family` (394); verify.py overrides the prompt (190).
+
+## Dependencies
+`complexity.map.Hotspot` (tool_hunter.py:13; inline at verify.py:178-186); `findings.StaticFinding` (14),
+`quick_scan_findings` (hunter_tools.py:222, 232); preprocess (hunter_tools.py:8, 113, 305, 318);
+`mapping.analyze_entry_points` (196, 202, 275, 279); `rank.rank_targets` (223, 232); semantic adjudicate, facts,
+ir, obligations, dominance, functions (224-322); `config.ObligationsConfig`/`ResolvedConfig`. Cycle:
+model/endpoint.py:22 imports tool_hunter; tool_hunter.py:254 lazily imports model.endpoint; plane/budget.py:16-17
+imports both.
+
+## Confidence
+High on the happy path, client stack and dead tools. Not read in full: learn/fill.py, learn/experiments.py,
+model/scan.py; callers of `complete_json` and `resolve_chat_endpoint` beyond push/assistance.py:84, learn/cli.py:171, 204.
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U7-learn-memory-tasks.md b/PATHFINDER-2026-10-13/01-flowcharts/U7-learn-memory-tasks.md
new file mode 100644
index 0000000..bc4c182
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U7-learn-memory-tasks.md
@@ -0,0 +1,107 @@
+# U7 Learn harness, memory store, plane fact tasks
+
+Traced read-only 2026-10-13. Corrections to the brief: `learn memory build` does not call features, roles or
+slice; it reads feature records from `--features` JSONL and `kind=features` store rows (produced by
+benchmarks/learn/build_features.py or the plane `features`/`remember` tasks); slice.py runs when a prompt is
+built (program.py:41). `learn/fill.py` (learn/cli.py:167) and `plane/tasks/agree.py` (runner.py:133 via
+agree.yaml) are live. The lazy `plane.tasks.alerts` import at learn/features.py:484-486 is inside
+`build_for_scan`, which only tests call.
+
+## Happy path
+1. `ousast learn labels` (cli.py:317-324, 466) → `_learn_labels` (1503) → `build_labels` (labels.py:938) → `load_sources(sources.toml)` (101; refuses unknown kinds, duplicates, excluded files) → `LabelBuilder.run` (501) reading every file through `SourceGuard` (126-167) → `Clone` cache `~/.cache/openultrasast` (374, 53) → `snapshot` (871), `write_labels` → `OUT/labels-.jsonl` (956; cli.py:1516-1517).
+2. Feature records: static profile `benchmarks/learn/build_features.py:58-79` (learn.features, learn.roles); plane profile: `features` task then `remember` writes `kind=features` rows.
+3. `learn memory build` (learn/cli.py:102 → `_memory_build` 371): `load_sources`, `read_jsonl` labels + features, store `features` rows (383), `SideReader` (examples.py:274) → `build_examples` (146): refuses `origin: user` and unlisted sources (167-171); `excerpt`/`delta_diff`/`function_bounds` (excerpt.py:339, 383, 302) with `identities_of` redaction (173); `put_blob("excerpts")` (213); `example` rows (216); `put_rows` (229).
+4. `learn memory embed` (optional): `_memory_embed` (342) → `embed_excerpts`, vectors at `embeddings//` (embeddings.py:67-80).
+5. `learn compile`: `_program_command` (274): `open_store`, `load_spec(compile.toml)` (compile.py:80), `load_examples` (examples.py:265), `EmbeddingCache`, `_caller` (241) = `MeteredClient` + `Caller`. `compile_program` (compile.py:347): `compile_split` + `outer_folds` + `assert_disjoint` (364-367); `bootstrap` via `Program.decide` (166) + `draw_demo_sets` (212); `propose` one call at temperature 0.7 (257); `score` with `balanced_brier` (296); `best` (315); `program_id` = sha256 of canonical JSON (328); `put_blob("programs", name=pid)` (479).
+6. `learn evaluate`: `load_program` hash-checked (compile.py:484), `Program(spec_of(...))`; `compile_split`, `outer_folds(exclude=split.groups)`, `targets`/`unevaluable_families` (learn/cli.py:312-314); `predict` (evaluate.py:398), `evaluate` (457): `cross_fit` (calibrate.py:148), `check`/`offered` (197, 221), `nested_points` (evaluate.py:113), `summary`/`strata` cluster bootstrap (318-363). Report to stdout and `--out`; nothing to the store.
+7. `learn experiment`: `_experiment` (learn/cli.py:114); `register` (experiments.py:364, put_row 403); `units` → `freeze_units` (466); `run` (899): `check_registered`, `read_units`, `program_folds`, seeded coin for arm order, one `Program.decide` per arm, `put_row(outcome_row)` (975), optional early stop (766), summary row (987); `analyse` (815): `paired_units`, `paired_bootstrap` (673), `exact_mcnemar` (722), `sequential_looks`, optional TOST, `decide` (795); `--record` writes `result_row` and `benchmarks/experiments//result.json` (learn/cli.py:141-144, experiments.py:67).
+
+```mermaid
+flowchart TD
+ CLI[ousast learn labels
cli.py:317] --> LL[_learn_labels
cli.py:1503] --> BL[build_labels
learn/labels.py:938]
+ BL --> LS[load_sources sources.toml
learn/labels.py:101]
+ BL --> SG[SourceGuard.check
learn/labels.py:150]
+ BL --> WL[write_labels labels-sha.jsonl
learn/labels.py:956] --> MB
+ BF[build_features static
benchmarks/learn/build_features.py:1] --> MB
+ REM[remember features rows
plane/tasks/remember.py:101] --> MB[_memory_build
learn/cli.py:371]
+ MB --> BE[build_examples
learn/examples.py:146] --> EX[excerpt / delta_diff
learn/excerpt.py:339]
+ BE --> ST[(put_blob excerpts + put_rows example
plane/memory.py:364)] --> PC[_program_command
learn/cli.py:274]
+ PC --> LE[load_examples
learn/examples.py:265]
+ PC --> CP[compile_program
learn/compile.py:347]
+ CP --> SP[compile_split / outer_folds
learn/folds.py:95]
+ CP --> BS[bootstrap + draw_demo_sets
learn/compile.py:166] --> DEC
+ CP --> PR[propose
learn/compile.py:257]
+ CP --> SC[score + best
learn/compile.py:296] --> DEC[Program.decide
learn/program.py:528]
+ DEC --> SL[function_slice
learn/slice.py:41]
+ DEC --> CA[Caller.ask responses cache
learn/program.py:397]
+ CP --> ART[(programs/pid blob
learn/compile.py:479)]
+ ART --> EV[evaluate
learn/evaluate.py:457] --> PD[predict
learn/evaluate.py:398]
+ EV --> CF[cross_fit / check
learn/calibrate.py:148]
+ EV --> NP[nested_points
learn/evaluate.py:113]
+ ART --> XR[experiments.run
learn/experiments.py:899] --> OR[(arm_outcome rows
learn/experiments.py:975)] --> XA[analyse
learn/experiments.py:815]
+ XA --> PB[paired_bootstrap
learn/experiments.py:673]
+ XA --> MN[exact_mcnemar
learn/experiments.py:722]
+```
+```mermaid
+flowchart TD
+ OS[open_store
plane/memory.py:929] --> RR[reconciler.results_root
plane/reconciler.py:64]
+ OS --> FS[FileStore
plane/memory.py:493]
+ OS --> S3[S3Store + verify_bucket
plane/memory.py:612] --> S3C[S3Client boto3
plane/memory.py:762]
+ MS[MemoryStore put_rows/rows/put_blob/get_blob
plane/memory.py:267] --> FS
+ MS --> S3
+ RF[repo_facts facts.json
plane/tasks/repo_facts.py:264] --> VE[verify units.jsonl
plane/tasks/verify.py:247] --> AG[agree agreed.json
plane/tasks/agree.py:153] --> FE
+ AL[alerts alerts.jsonl
plane/tasks/alerts.py:117] --> FE
+ RO[roles roles.json
plane/tasks/roles.py:141] --> FE[features features.jsonl
plane/tasks/features.py:114]
+ RF --> FE
+ FE --> RE[remember memory.jsonl
plane/tasks/remember.py:267] --> ING[ingest / ingest_rows
plane/memory.py:968] --> MS
+ SEED[seed
plane/memory.py:984] --> LR[reconciler.load_run/mark_done
plane/reconciler.py:102]
+ SEED --> WS[loop.write_snapshot
plane/tasks/loop.py:77] --> GU[improve.memory Guard
plane/tasks/loop.py:67]
+ FE -.imports.-> LF[learn.features parts
learn/features.py:53]
+ FE -.imports.-> LRO[learn.roles infer/from_model_roles
learn/roles.py:426]
+ LF -.imports.-> RF
+ LRO -.imports.-> RF
+```
+
+## Side effects
+Chat calls through `Caller.ask` (program.py:397), cached as `responses/` (413; key = model, params
+digest, messages, temperature, salted sample 356); `--replay-only` raises `ReplayMiss` (404); endpoint from
+`resolve_chat_endpoint` (learn/cli.py:254); `--budget-usd` enforced by `MeteredClient`, exit 3 when hit
+(321-323). Embeddings via OpenRouter (356), cached. Plane tasks `verify` (verify.py:336) and `roles`
+(roles.py:95) also call models, metered, resumable through units.jsonl. Files: labels jsonl + snapshot, clones in
+`~/.cache/openultrasast`, `--out` reports, `benchmarks/experiments//result.json`, units file; git subprocesses
+(experiments.py:327-329). Default store `$OUSAST_RESULTS|~/ousast-results/plane/memory` (memory.py:938,
+reconciler.py:64-65); layout `repos//.jsonl` (389), `facts/.json`, excerpts, programs,
+responses, embeddings, `index.jsonl` (432); FileStore atomic writes (529-533), 1 GiB free check (106). S3 via
+`OUSAST_MEMORY=s3://…`, boto3 lazy (771), `verify_bucket` once per store (620-624), S3 Select reads (733).
+`seed` writes `//facts.json`, `summary.json`, snapshot `rows.jsonl`/`index.json` (1016-1019;
+loop.py:94-102).
+
+## The Learn↔PlaneTasks cycle
+learn → plane.tasks: labels.py:48, features.py:27, roles.py:35, excerpt.py:35 import `repo_facts` tables and
+helpers (`_EXCLUDED_DIRS`, `DECLARATION`, `GLOBAL`, `_declared_name`, `declared_functions`, `enclosing`,
+`product_files`, `KEYWORDS`); features.py:484-486 imports `alerts.covers`, `engine_languages`, `quick_languages`
+(test-only path). plane.tasks → learn: features.py:34-54 (18 `learn.features` parts + `learn.roles`),
+roles.py:33-34 (`learn.roles.MODEL_ROLE_KINDS`, `learn.schema`), remember.py:42 (`learn.schema.validate_record`).
+What learn takes is pure tables and helpers (repo_facts.py:30-146, alerts.py:56-74). `features` and `roles`
+task logic can move into learn/ (case_features :114, recorded_case; chunks/parse_roles/classify/assemble
+roles.py:61-127), leaving `main()` wrappers because the runner resolves `openultrasast.plane.tasks.`
+by name (runner.py:24, 133). `repo_facts` and `alerts` tables are also used by plane/generate.py:72,
+plane/harvest.py:46, remember.py:44, plane/engine_alerts.py:39-41 and benchmarks/learn/*: they belong in a
+neutral module both sides import.
+
+## The Memory→Plane inversion
+memory.py:933 (`open_store`) imports `reconciler.results_root` only for the default path, lazily because
+reconciler.py:51 imports `open_store` at top. memory.py:989 (`seed`) imports `load_run`, `run_dir`,
+`_read_json`, `mark_done`; memory.py:1002 imports `tasks.loop.write_snapshot` (which pulls improve.memory,
+loop.py:67-70). `seed` belongs with orchestration (callers cli.py:1645): move it to reconciler.py or
+plane/seed.py; `results_root` to a leaf module. `ingest` (968) has no plane dependency.
+
+## Test-only code
+`folds.leave_one_source_out` (129), `leave_one_framework_out` (138): tests only. `roles.load_roles` (539): no
+caller at all. `features.build_for_scan` (467): only tests/test_learn_schema.py:15. `plane/tasks/features.py:202
+recorded_case`: only tests/test_plane_features.py. Live: fill.py, schema.py (11 importers), agree.py, slice.py.
+
+## Confidence
+High on dispatch, compile/evaluate/experiment flow, store API, the lazy imports and cycle edges. Not read in
+detail: `LabelBuilder.run` (labels.py:501-838), retrieve.py, bootstrap internals, `run_rule`/`analyse_rule`,
+S3 `verify_bucket`/`_select`, verify/agree/loop internals.
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U8-plane-executor.md b/PATHFINDER-2026-10-13/01-flowcharts/U8-plane-executor.md
new file mode 100644
index 0000000..1518d1c
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U8-plane-executor.md
@@ -0,0 +1,89 @@
+# U8 Plane executor (ax)
+
+Traced read-only 2026-10-13. Correction: `ousast plane run` never calls generate.py; it reads Run/Task/Model/
+Workspace YAML written earlier by `plane workspaces --validation-set` (increment) or `plane harvest`.
+Dockerfile.runner is at repo-root `plane/Dockerfile.runner`.
+
+## Happy path of `ousast plane run `
+1. cli.py:464 → `_plane` (1530) → `load_profile` (1534, profile.py:203).
+2. `_plane_memory("seed")` (1542, 1633) → `memory.seed` (memory.py:984) → `reconciler.mark_done` (280) for reusable facts.
+3. `reconciler.run` (384): `open_store` before the lock, no fallback (396, memory.py:929); `Delivery(store, run, base)` (397); `state.json` seeded on rerun (398-401); `_acquire_lock` (269, 402).
+4. `load_run` (102) + sibling `tasks/`, `models/`, `workspaces/` (105-107) via `load_manifests` (manifests.py:593) → `parse_manifest` (562) → `check_run_tasks` (545).
+5. Optional legacy receiver if `OUSAST_DELIVERY=receiver` (409-412, egress.py:233).
+6. `open_router` (router.py:147): `kubectl port-forward svc/atenet-router :80` in `ate-system` (43, 168-169) unless `router_url`/`OUSAST_ROUTER_URL` (160-163); reconciler.py:431.
+7. Scheduling loop (432-457): producers, `serialize` labels, `--workers`; one `worker` thread per task (419-427).
+8. `_execute` (335): `render_task` (145) builds Model/Workspace/Task docs with `OUSAST_RUN/TASK/OUTPUT_DIR`, `OUSAST_BUDGET_*`, `OUSAST_INPUTS`, `OUSAST_GIT_PINS`, writes `//task.yaml` (348); `delivery.body` (delivery.py:107) presigns PUT `runs///output.tar` and GET per input; `delivery.reset` (114); `credentials(model)` (router.py:58) reads `secretKey.key` from host env, `StartError` names the variable never the value; `policy_for` (egress.py:87) grants TLS passthrough 443 to store, git hosts, model hosts; `ax.apply` (reconciler.py:197) → `egress.apply` (egress.py:194, `kubectl-ate create/update egress-policy`) → `egress.settle` sleeps 11 s (229).
+9. `_await` (292): `ax resume task` (202), `ax get task` (207); on Running calls `Router.start_task` (router.py:100): `POST START_PATH` with header `ate-target-actor: /` and body `{run, task, credentials, delivery}` (107-110); done when `delivery.delivered` (delivery.py:120) sees the tar.
+10. Runner in the container: `/usr/local/bin/ax-task-runner` → `python -m openultrasast.plane.runner` (Dockerfile.runner:27); `main` (runner.py:989) → health server (999-1003) → `_run_once` (906): `_prepare` parses `AX_TASK_YAML` (860); `gate.arm/wait` (924-930), `StartGate.offer` (303) checks credential names and delivery URLs (`parse_delivery` 257, https hostname only) → 202; `_materialise_once` (972) → `materialise` (546) git clone/fetch at pins; `fetch_inputs` (785); `run_command` (671) → `Child` (610) runs `python -m openultrasast.plane.tasks.` (`task_module` 600), credentials only in the child env (684); `summary_after` (650) → `_finish` (887) → `_deliver_output` (821) → `deliver` (719) PUTs the tar (`build_tar` 694); marker `/.done.json` (900); serves until SIGTERM (1013-1015).
+11. `delivery.collect` (125): downloads, extracts after rejecting symlinks, hardlinks, traversal (136-138); `publish` (155); `done.json` (150); then `ax.delete`, `egress.delete` (reconciler.py:371-374); `outcome_of` (258) reads summary.json.
+12. `_State.set/_flush` (237-251) → local state.json mirrored via `delivery.save_state` (173); final status (470-475).
+13. `_plane_memory("ingest")` (1549) → `remember_run` (tasks/remember.py:229); failure reported, not fatal (1660-1662).
+14. `plane status` → `reconciler.status` (531) → `attribution` (510) → attribution.json.
+
+Credentials: read on the host only (router.py:58-72), travel only in the start POST body (107), never in
+ActorTemplate env (reconciler.py:358); router errors redacted (router.py:113-116, 126, 130, `redact` 50); the
+runner logs status and reply text only (runner.py:357), child stderr redacted (621); delivery logs use
+`safe_url` (713, 743). No log line prints a key.
+
+```mermaid
+flowchart TD
+ CLI[_plane
cli.py:1530] --> PROF[load_profile
profile.py:203]
+ CLI --> SEED[memory.seed
memory.py:984] --> MD[mark_done
reconciler.py:280]
+ CLI --> RUN[reconciler.run
reconciler.py:384]
+ RUN --> STORE[open_store
memory.py:929]
+ RUN --> LR[load_run
reconciler.py:102] --> LM[load_manifests
manifests.py:593]
+ RUN --> DEL[Delivery
delivery.py:78]
+ RUN --> OR[open_router atenet-router
router.py:147]
+ RUN --> EX[_execute
reconciler.py:335]
+ EX --> RT[render_task
reconciler.py:145]
+ EX --> BODY[Delivery.body presign
delivery.py:107]
+ EX --> CRED[credentials
router.py:58]
+ EX --> POL[policy_for
egress.py:87]
+ EX --> AXA[Ax.apply
reconciler.py:197] --> EGA[Egress.apply kubectl-ate
egress.py:194]
+ EX --> AW[_await resume/poll
reconciler.py:292] --> ST[Router.start_task POST
router.py:100] --> RM[runner.main
runner.py:989]
+ RM --> GATE[StartGate.offer
runner.py:303] --> MAT[materialise
runner.py:546] --> FI[fetch_inputs
runner.py:785] --> RC[run_command tasks.X
runner.py:671] --> FIN[_finish / PUT tar
runner.py:887]
+ FIN --> DLV[Delivery.delivered
delivery.py:120] --> COL[Delivery.collect
delivery.py:125] --> OUT[outcome_of
reconciler.py:258] --> STATE[_State.set + save_state
reconciler.py:237]
+ STATE --> ING[remember_run ingest
tasks/remember.py:229] --> STAT[status/attribution
reconciler.py:531]
+```
+
+## Side effects
+Host subprocesses: `ax apply/resume/get/delete` (reconciler.py:191-228), `kubectl-ate get/create/update
+egress-policy` (egress.py:174-216), `kubectl port-forward` (router.py:168-169), receiver-mode `kubectl get svc`
+(egress.py:133). Store: `runs//{state.json, /output.tar, /, /done.json}`
+(delivery.py:11-16), presigned URLs (96-105), seed/ingest rows. Local `$OUSAST_RESULTS/plane//` (lock,
+state.json, task.yaml, outputs, attribution.json; reconciler.py:64-69). Container: git clone/fetch
+(runner.py:422-530), child `python -m`, HTTP on 80, marker in `/workspace/.ousast-state` (134). Sleeps: 11 s
+egress settle, 3 s poll (reconciler.py:296).
+
+## Dependencies and callers
+Memory: `open_store` (929), `RUNS_PREFIX` (593), presign (547-555, 751-755). Budget: budget.py:16-17 imports
+`Prices` (model.endpoint) and `ChatClient` (tool_hunter); the reconciler only exports `OUSAST_BUDGET_*`
+(158-159). generate.py:67 is `price_of`, a price lookup, plus tasks.verify/repo_facts (72-73) and reconciler
+helpers (71). learn: harvest.py:119 (`learn.excerpt.function_bounds`), 198 (`learn.labels.Clone`). Callers of
+`reconciler.run`: only cli.py:1544 and ops/ax/smoke-run.sh:17. benchmarks/ax/batch.py:28 imports plane.memory
+and runs its own `ax` CLI calls (62, 246), not the reconciler; search/coordinator.py:22-23 imports budget and
+memory only; benchmarks/unseen/{draft.py:842, eligibility.py:332} import `reconciler.results_root`.
+
+## What the replay and the hunter arm use
+benchmarks/unseen/replay.py has no plane import (uses benchmarks.ax.batch → plane.memory only);
+benchmarks/valid/hunter_arm.py:21 uses `plane.budget.MeteredClient` only. None of the executor modules are on
+their path.
+
+| Module | Callers |
+|---|---|
+| engine_alerts.py | cli.py:1597; benchmarks/learn/engine_pairs.py:37 (`docker_command`, `frozen_source`, `read_record`); benchmarks/learn/engine_trace.py:37 (`export_pin`); tests; model/audit.py:69 |
+| harvest.py | cli.py:1579; `declares` used by benchmarks/learn/build_features.py:75 and engine_trace.py:635; tests |
+| workspaces.py | cli.py:1570; tests/test_plane_reconciler.py:35; model/audit.py:80 |
+| profile.py | live: cli.py:1532, reconciler.py:52, 390, doctor.py, ops/ax/*.sh, tests |
+| generate.py | cli.py:1578 (`repin_templates`), 1668 (`increment`); harvest.py:43; tests |
+
+## Removal candidates (never on `run`)
+engine_alerts.py (its helpers would move to benchmarks/learn first), harvest.py (`declares` needs a home),
+workspaces.py (no benchmark or ops caller), generate.py (manifest producer for harvest/increment only),
+receiver mode in egress.py:128-160, 233-324 and runner.py:829-840 ("task 2.6 removes it", reconciler.py:409,
+egress.py:106; ops/ax/up.sh:121-123 still creates the Service). doctor.py is live via `plane doctor`.
+
+## Confidence
+High on run/execute/await/start/runner/collect and credential handling. Medium on caller lists. Not read:
+`memory.seed` body beyond 984-1020, tasks/remember.py, `materialise` internals, profile/doctor/engine_alerts/
+harvest bodies, ops/ax/*.tmpl, prepare-substrate.py.
diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U9-search-sandbox-regress.md b/PATHFINDER-2026-10-13/01-flowcharts/U9-search-sandbox-regress.md
new file mode 100644
index 0000000..4d8655f
--- /dev/null
+++ b/PATHFINDER-2026-10-13/01-flowcharts/U9-search-sandbox-regress.md
@@ -0,0 +1,98 @@
+# U9 Search with proof, sandbox, regress
+
+Traced read-only 2026-10-13. fossil-mcp's "unreachable Coordinator" is half wrong: `Coordinator` is live
+(benchmarks/search/pilot_run.py:39, 378); `BatchExecutor` really has no src or benchmarks caller.
+
+## Happy path A, search with proof (pilot)
+1. `pilot_select.main` (pilot_select.py:302) writes `benchmarks/search/private/pilot.json` (310, 330) and a public record (311, 349).
+2. `pilot_run.main` (pilot_run.py:411): validates manifest (466-505), `OpenRouterChatClient(max_attempts=1)` (509), `FileStore`/`open_store` (520), `AXLane` (522); dry run uses `probe.materialise` and `StubModel` (529-531, 60).
+3. `Pilot.__init__` (79): `Board(store, "pilot-…")` + candidate hint commit (105-116), `Worker(...)` (117).
+4. `Pilot.run` (376) → `Coordinator(self.board, self.dispatch, budget=SearchBudget(retries=0))` (378) `.run()`.
+5. `Coordinator.run` (coordinator.py:210) seeds `bootstrap` (214-229); explore intent → `_call("explore")` (285).
+6. `Coordinator._call` (125): reserve spend, `SearchTask` (28/149), checkpoint `_save` (85) → `Board.commit` (board.py:51) → `put_blob` (63); `executor(task)` (160) = `Pilot.dispatch`.
+7. `Pilot.dispatch` (290) explore (327): `_executor` (178) opens an AX `SearchExecutorTask` (189) running `python -m openultrasast.search.executor` in the pod (batch.py:735) with the repo cloned (pilot_run.py:193); the worker's executor is `remote.client` (`ObjectStoreExecutor`, batch.py:752).
+8. `Worker.__call__` (worker.py:471) → `explore` (351) → `_call` (218) reserves budget, `client.complete_chat_raw` (251); tools via `Worker.tool` (283): read_file/grep/run → `executor.submit` → pod `serve` → `InProcessExecutor.execute` (executor.py:622, 120, 197).
+9. `write_demo` → `validate_demo` (demo.py:350) writes `demo/demo.json` (worker.py:294-310); `finish` → `load_demo` + `validate_oracle` (440-446) returns a fact with `demo`.
+10. Coordinator stores the fact (coordinator.py:300-313); next loop `_call("verify")` (246).
+11. `Pilot.dispatch` verify (297) → `Pilot.verification` (230): VM lane per revision `prepare` (217) → `SearchExecutorTask.prepare` → `prepare_verification` (batch.py:758-760, executor.py:206) → `extract_checkout` (verify_task.py:161), `validate_spec` (219), `verify(...)` (pilot_run.py:282); AX lane `verify(..., task_dispatcher=AXSideDispatcher)` (252-271) → `python -m openultrasast.search.verify_task` (search_verify.py:94) → `task_main`/`run_side`/`verify_side_task` (verify_task.py:279, 273; verify.py:632).
+12. `verify` (verify.py:648): `_sandbox.isolation_check` (673); `_side` for affected and safe (690); `_side` (280) runs 3 fresh runs (327): `oracle_for` (399), `Canary.fresh` (402; oracles.py:307, 30), app launched via `_sandbox.run` (bwrap, _sandbox.py:105, 125), `oracle.observe` (verify.py:556).
+13. `demonstrated` only if 3/3 affected observe and 0/3 safe do (697-698) → `VerificationRecord`.
+14. `dispatch` returns outcome, `evidence_refs=["verify:owned"]` (318-324); coordinator appends `verified-…` and `_end("goal_met")` (256-268); `Pilot.run` fills spend and metrics (396-403) → `output.write` (540), summary (547-567).
+
+## Happy path B, deep-mode regress
+cli.py:66 imports `run_regression`; at 839 if REGRESS requested, `resolve_sandbox_probe().available()` (840)
+runs `docker info` (sandbox/probe.py:22-33); `resolve_sandbox_runner()` (845) → `DockerCliRunner`
+(runner.py:59-64); `run_stage("regress", run_regression(...))` (856-870) → `candidate.run_regression`
+(candidate.py:177) → `select_candidates` (63) with `complexity.ledger.select_forced_candidates` (81); per
+hotspot `_proposed_snippet`/`snippet_for` (203, 153) → `RegressionRunner.run_recipe` (126) → `recipe_for`
+(recipes.py:16) → `run_snippet` (141) → `check_snippet_safety` (safety.py:63) → `DockerCliRunner.run`
+(runner.py:72: `docker run --rm --network none --read-only …` 113-137); `verdict_from_result` (verdict.py:64):
+`triggerable` on non-zero exit or oracle, `not_triggerable` on exit 0 (91-99); `write_verdicts` → verdicts.json
+(cli.py:871, candidate.py:222); `persist_verdicts` (872); obligations re-ranked on TRIGGERABLE (874-877).
+
+```mermaid
+flowchart TD
+ A[pilot_select.main
pilot_select.py:302] --> B[pilot_run.main
pilot_run.py:411] --> C[Pilot.__init__ Board+Worker
pilot_run.py:79] --> D[Pilot.run
pilot_run.py:376]
+ D --> E[Coordinator.run
coordinator.py:210] --> F[Coordinator._call / SearchTask
coordinator.py:125]
+ F --> G[Board.commit
board.py:51]
+ F --> H[Pilot.dispatch
pilot_run.py:290]
+ H --> I[SearchExecutorTask AX pod
batch.py:675] --> J[executor.serve/InProcessExecutor
executor.py:622]
+ H --> K[Worker.explore
worker.py:351] --> L[Worker._call LLM
worker.py:218]
+ K --> M[write_demo validate_demo
demo.py:350]
+ H --> N[Pilot.verification
pilot_run.py:230] --> O[prepare_verification
executor.py:390]
+ N --> P[verify
verify.py:648] --> Q[_side x3 runs
verify.py:280]
+ Q --> R[_sandbox.run bwrap
_sandbox.py:105]
+ Q --> S[oracle_for/Canary
oracles.py:307]
+ P --> T[demonstrated
verify.py:698] --> U[_end goal_met
coordinator.py:268] --> V[record jsonl
pilot_run.py:540]
+```
+```mermaid
+flowchart TD
+ A[cli scan deep REGRESS
cli.py:839] --> B[SandboxProbe.available
sandbox/probe.py:22] --> C[resolve_sandbox_runner
sandbox/runner.py:59] --> D[run_regression
regress/candidate.py:177]
+ D --> E[select_candidates
regress/candidate.py:63]
+ D --> F[RegressionRunner.run_recipe
regress/candidate.py:126]
+ F --> G[recipe_for
regress/recipes.py:16]
+ F --> H[check_snippet_safety
regress/safety.py:63]
+ F --> I[DockerCliRunner.run
sandbox/runner.py:72] --> J[verdict_from_result
regress/verdict.py:64] --> K[write_verdicts
regress/candidate.py:222] --> L[rank_obligations on TRIGGERABLE
cli.py:874]
+```
+
+## Side effects
+Model: `complete_chat_raw` with DeepSeek settings (worker.py:251; client pilot_run.py:509), reserved against
+`SpendBudget` (228, 238). AX: an executor pod per explore/prepare (pilot_run.py:189, batch.py:735), verify pods
+in AX lane (search_verify.py:94), cap 32 tasks (pilot_run.py:213). Local: bwrap (_sandbox.py:125, 194), Chromium
+`BrowserExecutor` for xss (pilot_run.py:286). Files: `benchmarks/search/private/runs//…` (demo.json,
+built-*, verify-demo, boards/), `--out` jsonl opened with "x" (517). Regress: `docker info`, `docker run`,
+scratch seed dir (runner.py:74), verdicts.json, complexity_ledger.json.
+
+## Overlap between regress/ and search/
+| | search/ | regress/ |
+|---|---|---|
+| Sandbox | `_sandbox.run` bwrap+seccomp (_sandbox.py:105); `_run_task` (verify.py:576); AX pods | `DockerCliRunner.run` (sandbox/runner.py:72) |
+| Shared types | `SandboxJob`/`SandboxResult` from `..sandbox` (_sandbox.py:24, verify.py:29) | same (candidate.py:15) |
+| Verdicts | demonstrated / inconclusive / no_oracle / could_not_run / could_not_build; no negative (verify.py:660, 692-701) | triggerable, not_triggerable, already_covered, inconclusive (verdict.py:10-26) |
+| Oracle | owned canary oracles per family (oracles.py:30, 307), differential 3 vs 3 | exit code or optional callable (verdict.py:91-99), single run, not differential |
+
+The only production caller of regress/ is cli.py:66, 858; all other references are tests. `run_recipe(oracle=…)`
+is never passed an oracle in production (candidate.py:205).
+
+## Coordinator and BatchExecutor
+`Coordinator` is reachable (pilot_run.py:378; tests). `BatchExecutor` (coordinator.py:426-484): only
+tests/test_search_coordinator.py:177, 189; the pilot uses `SearchExecutorTask` and `AXSideDispatcher`.
+`coordinator.main` (487), `worker.main` (worker.py:530), `verify.main` (verify.py:714): shims with no in-repo
+invoker (the `-m` launches target only search.executor and search.verify_task). Retry path `lane="docker"`
+(coordinator.py:202-205) unreachable from the pilot (retries=0). `probe.probe`/`main` (probe.py:157, 239) used by
+tests and benchmarks/ax/search_probe.py; only `materialise` (39) in dry runs. Fake sandbox runner/probe are
+env-flag test doubles.
+
+## Dependencies
+Memory: `FileStore` (plane/memory.py:493), `open_store` (929); `Board` uses put_blob/get_blob (board.py:63, 42).
+Budget: `BudgetExhausted` (budget.py:23), `SpendBudget` (260); `search/budget.py:8` `SearchBudget`, 49
+`verification_timeout`. LLM: provider/openrouter.py:56, 84; `price_of` (pilot_run.py:26-33, 425).
+benchmarks/ax: batch.py:675-762 (`SearchExecutorTask`, `prepare`), 498 (`dispatch`, BatchExecutor only),
+search_verify.py:56 (`AXSideDispatcher`), 48. plane/tasks/verify.py:179 uses `complexity.map.Hotspot`, the
+same type regress uses (candidate.py:11); search/ does not. Regress also uses complexity/ledger.py and
+`config.SandboxConfig`.
+
+## Confidence
+High on call chains, verdict sets, Coordinator/BatchExecutor callers (benchmarks/search/private excluded). Not
+read in full: `_side` build/readiness/HTTP driver (verify.py:345-560), `_prepare_verification`
+(executor.py:390-488), recipes/safety bodies.
diff --git a/PATHFINDER-2026-10-13/02-duplication-report.md b/PATHFINDER-2026-10-13/02-duplication-report.md
new file mode 100644
index 0000000..ae4e9fa
--- /dev/null
+++ b/PATHFINDER-2026-10-13/02-duplication-report.md
@@ -0,0 +1,222 @@
+# Duplication report (2026-10-13)
+
+Every claim was checked against the source by the phase-2 agents, not taken from the flowcharts. Paths are
+relative to `src/openultrasast/` unless they start with `benchmarks/`. "First" dates are from
+`git log --diff-filter=A` and give rough order only.
+
+Verdict in one line: of ten candidate concerns, seven are real duplication (D1 to D5, D7, D10), two are one
+owner missing (D8, D9), one is a misplaced type (D6). The three that matter most: D1 (LLM call and cost),
+D4+D5 (enclosing function, callers, declaration tables), D3 (the quick-rule pipeline).
+
+## Part A: concerns duplicated across features
+
+### D1. Chat client, resolver and cost metering (accidental, highest value)
+Where: `ChatClient` Protocol `.complete` (tool_hunter.py:163); `RankerClient.complete_json` (rank.py:31, tests
+only). Clients: `ScriptedChatClient` (tool_hunter.py:176); `OpenRouterHunterClient` (203) drops `json_object`,
+calls `complete_chat`, records no usage (222-223); `DeepSeekChatClient` (model/endpoint.py:91) uses
+`complete_chat_raw`, keeps `usage` as a list (98), has its own retry on empty JSON (135-144);
+`OpenRouterChatClient` (provider/openrouter.py:56, 69, 84). Wrappers: `MeteredClient` (plane/budget.py:68,
+`usage` is a dict 95); `FillMeter(MeteredClient)` (learn/fill.py:44) reserves before each call; `Caller`
+(learn/program.py:369) caches and replays, diffs `client.usage` as a dict (404-406); `_Recording`
+(plane/tasks/verify.py:66); `_CountingClient` (model/scan.py:1628). Two resolvers: `resolve_hunter_client`
+(tool_hunter.py:239) ignores config and prices against `DEFAULT_DETECTOR_MODEL` whatever model runs (258);
+`resolve_chat_endpoint(config)` (endpoint.py:192) honours `[models]`; their OpenRouter branches build different
+wrappers (tool_hunter.py:262 unmetered vs endpoint.py:221 metered). Duplicate env-flag sets tool_hunter.py:36-39
+and endpoint.py:39. Four cost paths: `ChatEndpoint.cost` (endpoint.py:70-80; `cost_usd` 147; push/assistance.py:
+103-110 inline); `cost_of` (budget.py:59, docstring: "the arithmetic of ChatEndpoint.cost") used by
+`_absorb_rows` (158-167), `Caller.usd` (program.py:420-423), `verify._sum_usage` (232-244); `SpendBudget`/
+`SpendReservation` (budget.py:260-322) with search/worker.py:218-270 repeating the DeepSeek `extra_body`;
+`_CountingClient.cost_usd` (scan.py:1639). `_tokens` copied (budget.py:170, endpoint.py:262). Fallout: the
+list-vs-dict `usage` mismatch needs the shim in benchmarks/valid/hunter_arm.py:56-65 ("made every live
+classification fail while stubbed tests passed"); the CLI hunt passes the bare client (cli.py:976) so the
+shipped scan's hunter spend is never metered.
+Why: tool_hunter 09-03, endpoint 09-08, budget 09-29, program 10-01, fill 10-03, worker 10-05; each consumer
+added the wrapper it needed (tool_hunter.py:240-244, 257; budget.py:263-266 "existing MeteredClient callers
+retain their historical post-call metering").
+Legitimate parts: the worker's hard reservation (fail-closed, `max_attempts=1` worker.py:188-189); `Caller`'s
+replay cache; `ScriptedChatClient` as a test double. Accidental: two resolvers, two adapters, four cost
+formulas, two usage shapes.
+Unification: one `resolve_chat_client(config|None) -> (client, endpoint)`; one client class (today's
+`DeepSeekChatClient`, renamed) with `usage: list[row]`; one `Meter` with `reserve(bound)`/`settle(row)` so
+`MeteredClient`, `FillMeter` and the worker's `_call` are configurations (post-call vs reserve-first); one
+`cost(row, prices)`; `Caller` stays as the cache layer; `_CountingClient`, `_Recording` become counters read
+off the meter. Lost: calling OpenRouter unmetered; the hunter prices by its configured model (recorded dollars change).
+
+### D2. Sandboxed execution for a verdict (runtime split legitimate, verdict split not)
+Shared `SandboxJob`/`SandboxResult` (sandbox/runner.py:29-45; search/_sandbox.py:24; regress/candidate.py:15).
+Runners: `DockerCliRunner.run` (sandbox/runner.py:67, 107-137) implements `SandboxRunner` (24);
+`search/_sandbox.run` (105) is bwrap+seccomp with extra keyword-only args and does not satisfy the Protocol.
+Probes: `resolve_sandbox_probe` (`docker info`, sandbox/probe.py:22-49) vs `_sandbox.isolation_check` (68).
+Verdicts: regress `triggerable/not_triggerable/already_covered/inconclusive` + 14 reasons (regress/verdict.py:
+10-26); search `demonstrated/inconclusive/no_oracle/could_not_run/could_not_build`, no negative
+(search/verify.py:350-701); learn `vulnerable/not_vulnerable/unsure` (program.py:43, classification). Gap:
+neither path produces `Rung.EXECUTION_CONFIRMED` (model/ladder.py:33; consumers push_scoring.py:84, mcp.py:98,
+learn/schema.py:77), so the evidence ladder never receives execution evidence.
+Why: regress/sandbox 09-03; _sandbox.py 10-05 for AX pods without Docker (docstring :1-8).
+Unification: make `_sandbox.run` a `SandboxRunner` (options into `SandboxJob`) selected by
+`resolve_sandbox_runner`; one outcome record `{outcome ∈ confirmed|refuted|inconclusive|unavailable, reason}`
+that emits `Rung.EXECUTION_CONFIRMED`; keep two oracle strategies. Lost: nothing.
+
+### D3. The quick-rule pipeline, re-implemented by each caller (accidental)
+Each site runs preprocess → `analyze_entry_points` → `attach_reachability_hints` → `rank_targets` →
+`quick_scan_findings` itself:
+
+| Site | Ruleset | Shadow findings | Emit floors |
+|---|---|---|---|
+| cli.py:643-687 | `load_ruleset` + target ledger (634-638) | filtered (691) | quick/deep only; the standard pool skips them (670 vs 683-684) |
+| gate.py:81-84 | None → `PATTERN_RULES` | not filtered here | no |
+| map_gate.py:52-54 | `PATTERN_RULES` | not filtered | no |
+| pairs.py:794-801, 815 | param | filtered | no |
+| plane/tasks/alerts.py:120-122 | `PATTERN_RULES` | kept on purpose (docstring :5) | no |
+| improve/evolve.py:311-315 | param | filtered | no |
+| push/quick.py:87-92 | `PATTERN_RULES` | filtered; no preprocess, targets from changed files | no |
+| hunter_tools.py:232 (`flows`) | None | filtered | no; re-runs preprocess per tool call (307), uncached |
+| hunter.py:151 | pool | per task | no |
+
+Plus benchmarks/learn/build_features.py and benchmarks/measurements/2026-09-30-php-quick-rules/measure_php_quick.py.
+Legitimate differences: push's changed-files target set; alerts keeping shadow findings. The divergent ledger
+and floors mean a gate measures different rules from what `ousast scan` ships.
+Unification: one `quick_pipeline(root, targets=None, *, ruleset, policy, floors, include_shadow=False) ->
+(targets, entries, rankings, findings)`; cli wraps steps in `run_stage` through a callback. Lost: nothing;
+gates pass `ruleset=PATTERN_RULES` explicitly to keep today's numbers.
+
+### D4. Enclosing function, function spans and callers (two naming systems, accidental)
+Tree-sitter path: semantic/functions.py `_DECL` (56-69), `RESERVED_FUNCTION_LABELS` (23-51), sentinel
+`` (20), `function_at` (136); docstring (1-9): "the single source of that naming. Every consumer must
+therefore name ranges the same way, or function-scoped matching silently fails". Used by hunter_tools
+`_named_span` (317-323), overlay, pairs, model/candidates. Regex path: plane/tasks/repo_facts.py `DECLARATION`
+(48-63), `KEYWORDS` (64-88), sentinel `` (34), `enclosing` (131), `declared_functions` (118),
+`call_sites` (154); used by plane/tasks/alerts.`function_at` (108-114); learn/features
+`FeatureContext.function_at` (365-370), line-for-line the same as the alerts copy; learn/excerpt
+`function_bounds` (302) with its own brace matcher; learn/labels.py:48, learn/roles.py:35, plane/harvest.py:46.
+Two caller indexes: `repo_facts.call_sites` (154) written to facts.json (301), read by `verify.callers_of` (128)
+accepting two facts shapes (131-141); `learn/features.callers()` (372-395) with a separate `_CALL` regex (49).
+`hunter_tools.find_refs` (83) is a plain `\bsymbol\b` grep. `mapping.analyze_entry_points` (123) is the single
+source for entry points, recomputed per caller (nine D3 sites plus learn/features.py:415, push/runner.py:123).
+Why: functions.py 09-05 for the pair scorer; repo_facts.py 09-29 as a pod task avoiding tree-sitter, so learn
+imported it. functions.py already has a regex fallback (120-133). Two sentinels and two keyword sets mean labels
+(``) and scorer ranges (``) can name the same function differently, the exact failure
+functions.py:6-8 warns about.
+Unification: one `functions` module with `ranges(path, text, lang)` (tree-sitter, regex fallback),
+`enclosing(line)`, `bounds(name)`, `call_sites(names)`, one sentinel, one reserved set; `repo_facts` re-exports
+it so the runner still finds the task by name. Lost: Java/Go/Ruby/PHP patterns in DECLARATION must merge into
+`_DECL` or labels change; re-label and diff before switching.
+
+### D5. Declaration tables, product-file filter, language detection (accidental)
+Product-file filters: `repo_facts.product_files` (96-115, rglob, own `_EXCLUDED_DIRS`/`_NOT_PRODUCT` 36-43,
+`LANGUAGES` 33) vs `preprocess.enumerate_source_files` (96-100, 240, `git ls-files`); excluded-dir lists also at
+preprocess.py:9 and model/partitions.py:194. Language detection: `preprocess.detect_language`/
+`LANGUAGE_BY_EXTENSION` (11, 130) is the one extension map (push/runner.py:219-226, regress/candidate.py:14);
+`learn/excerpt.resolve_language` (200) sniffs; alias maps at semantic/facts.py:18, semantic/obligations/facts.py:21,
+model/candidates.py:49; language enumerations at learn/schema.py:54, repo_facts.py:33. The tables are imported
+by learn/labels, features, roles, excerpt; plane/harvest, generate (`_digest` 72), remember (44); plane/tasks/
+alerts, features: this is the Learn↔PlaneTasks cycle.
+Unification: move `DECLARATION`/`KEYWORDS`/`product_files`/`_digest` into a neutral `source.py` or D4's module;
+one `LANGUAGE_ALIASES` beside `LANGUAGE_BY_EXTENSION`. Lost: nothing; the cycle goes away.
+
+### D6. Contracts and the CPG↔Model cycle (layered by design, one type misplaced)
+contracts.py:1-93 generic `Contract`; model/contracts.py 12 classes (19-205); push/contracts.py 9 classes
+(18-161) building on `QuestionIdentity`/`ScopeDecision` (14). Layered, not duplicated. The cycle is
+`ExecutionBudget` only (model/contracts.py:19-33; cpg/backend.py:37, cpg/session.py:44); reverse direction
+private names `_DeadlineExpired`/`_remove_owned_tree` (cpg/backend.py:1554) imported by model/partitions.py:14,
+cpg/artifact.py:151 (lazy) and push/cache.py:22 (missed by the flowcharts). Budget types `ScanBudget`
+(scan.py:79), `HunterBudget` (hunter.py:19), `SearchBudget` (search/budget.py:8), `SpendBudget` (budget.py:260),
+manifest `Budget` (plane/manifests.py:185) cover different quantities: legitimate.
+Unification: move `ExecutionBudget` and public `DeadlineExpired`/`remove_owned_tree` into contracts.py or a
+`deadline.py`. Lost: nothing.
+
+### D7. Reachability multipliers and severity (accidental constants, legitimate severity split)
+Same dict at scoring/project_score.py:15 `REACH_MULT` and calibration.py:255 `_BASE_REACH_MULT` (both
+06-17/22). The set `{"reachable","inferred-file-surface"}` at regress/verdict.py:28 and complexity/ledger.py:23;
+produced once at findings.py:164. Severity: policy-derived via `SEVERITY_LABEL` (findings.py:39) for quick rules
+(132) and overlay (overlay.py:134); hard-coded "medium" for model-layer findings (cli.py:1373); LLM's own or
+"medium" for the hunter (tool_hunter.py:35, 619); sensitivity-mapped for obligations (check.py:162).
+Unification: calibration imports `REACH_MULT`; one `REACHABLE_STATUSES`; `resolve_severity` whenever a family
+maps to a CWE. Lost: nothing.
+
+### D8. Hotspot construction for the hunter (legitimate inputs, no constructor)
+`Hotspot` (complexity/map.py:20-28, `band` documented "high | medium | low") built by `_hotspot_from_signals`
+(62-102), plane/tasks/verify.py:180-186 with `band="candidate"` (outside the vocabulary), and
+benchmarks/valid/hunter_arm.py:233-242 with `band="low"`. Unification: `Hotspot.for_file(path, function=None,
+rationale, band="low")` or let `run_tool_hunter` take `(path, function)` targets. Lost: nothing.
+
+### D9. Result and record writers (accidental sequencing, one owner missing)
+`findings.json` written at cli.py:708, 788, 801, 814, 877, 1005 after each proposer; merge-by-id dedup repeated
+(784-787, 989-1000). Writes into the scanned repo all come from `CALIBRATION_DIR` (cli.py:94):
+`false_positive_learnings.json` (635, 1021-1022), `rule_policy.json` (636; improve 1137),
+`complexity_ledger.json` (765, 872), `rule_signals.json` (improve/evolve.py:173-174), `improve_journal.json`
+(1141-1144); plane/tasks/loop.py:43 re-declares the path. The plane `MemoryStore` (memory.py:267) is a separate
+store that knows nothing of these. Unification: accumulate findings per proposer, write once in a `finally`
+(and move scoring after the merge, fixing the U1 bug); one `CalibrationLedger(target)` owns the five files.
+Lost: partial findings.json on a crash unless the finally write stays.
+
+### D10. Other
+- JSON-in-fences parsers: provider/openrouter.py:205 `parse_json_content` (raises; used by program.py:326,
+ compile.py:264, worker.py:329); tool_hunter.py:581 `_load_json` (returns None); tool_hunter.py:409
+ `_looks_like_json`. `cpg/backend.extract_payload` (203) is not a duplicate (Joern BEGIN/END markers, partial
+ bodies). Fix: tool_hunter wraps `parse_json_content`.
+- Retry: one helper `call_with_retry` (openrouter.py:34, used 115, 168); the duplication is in policy:
+ DeepSeek's empty-JSON re-call (endpoint.py:135-144) makes up to 2×3 attempts; `bounded()` (103-113) and the
+ worker (worker.py:188) force one attempt. One policy on the client.
+- `resolve_*` factories (tool_hunter.py:239, endpoint.py:192, sandbox/runner.py:59, sandbox/probe.py:49,
+ cpg/backend.py:1813): legitimate pattern; only the first two overlap (D1).
+- Two evidence vocabularies on one record: `StaticFinding.evidence_level` (findings.py:139, overlay.py:115,
+ tool_hunter.py:621, check.py:164) and `rung` (findings.py:34-35: "the older overlay pipeline's word"). Fold into `rung`.
+- Excerpt helpers `hunter_tools.read_definition` (174) and `learn/excerpt.function_bounds` (302): covered by D4.
+
+Confidence: high on D1, D3, D4, D6, D7, D9; medium-high on D2, D5, D8. Unverified: whether
+`evaluate_benchmark` filters shadow findings for the gates; `MeteredEmbeddingClient`/`UnmeteredUsage`
+(budget.py) as a fifth metering path; callers of `resolve_chat_endpoint` beyond push/assistance.py:84 and
+learn/cli.py:254.
+
+## Part B: repetition inside the big units (22 items, about 650 to 800 lines; four are correctness risks)
+
+Correctness risks first: an unbounded Joern path drops partial answers on timeout (W4.3); plane uploads follow
+redirects while search uploads refuse them (W8.2); the advisory engine findings are deduplicated differently in
+the background summary and the terminal report (W3.3); evaluate builds folds differently from experiments (W7.2).
+
+### U3 push/
+- **W3.1 Git command prefix twice.** `["git","-c","protocol.allow=never","-c","core.hooksPath=…","-C",repo,*args]` at snapshot.py:94 (`_git`, subprocess.run, 10 s) and :570 (`_bounded_git`, Popen+selector); `_git` already routes through `_bounded_git` when a budget exists (83-91) and `_repository_path` (74-79) fakes a 10 s budget. Fix: one argv constant, then always `_bounded_git`. ~20 lines.
+- **W3.2 Two forked-child helpers.** `_prepare` (runner.py:56-115: os.pipe, selector, pickle, 32 MiB cap) vs `_save`/`_write_artifact` (report.py:120-199: multiprocessing.Pipe); identical finally blocks (runner.py:107-115, report.py:190-199). Fix: `run_forked(fn, *, until, output_cap)`. ~35 lines.
+- **W3.3 Advisory engine filter twice, different dedup keys.** `background._summary` (background.py:113-145) dedups on (family, path, line); `report._advisory_engine_lines` (report.py:241-265) on `defect_id`. Counts can differ for one push. Fix: `is_advisory(disposition)` + one key. ~15 lines.
+- **W3.4 Two "scan complete" checks that disagree.** `report._completed_scan` (108-117) vs `policy.compare_evidence.complete` (policy.py:502-512; allows tier_zero deferrals); push/contracts.py:64 a third partial copy. Fix: one `scan_complete(scan, *, allow_tier_zero)`. ~15 lines.
+- **W3.5 Budget arithmetic written out 66 times** across 14 files (push 47, model 13, cpg 16); `ExecutionBudget` (model/contracts.py:19-32) has no methods. Existing copies: snapshot.py:635, 642; scan.py:1435; backend.py:1377, 1399; report.py:160; partitions.py:117, 183, 188, 205, 253, 267; regions.py:313; scan.py:203. Fix: `expired()`, `remaining()`, `cleanup_end`, `check()` on `ExecutionBudget`, landing with the D6 move. ~40 lines.
+- Unreachable: none at def level. vetoes.py (533 lines) only under `--experimental-record-vetoes` in replay; callers runner.py:49, benchmarks/push/m1a_record_vetoes.py:32, transfer_replay.py:37, tests. `GraphLease` Protocol (cpg/artifact.py:70) only tests/test_push_contracts.py.
+
+### U4 model/ + cpg/
+- **W4.1 Arbiter chosen by spec type three times**: `scan._grouped` (scan.py:1491-1506), `pipeline._arbitrate_all` (93-98), `pipeline._arbitrate` (117-121); identical `request_params(spec, *, function, file)` in taint.py:22, dominance.py:30, config_value.py:22; three row normalisers (taint._flows 178, dominance._operations 144, config_value._settings 125) and three `_location` builders (taint.py:297, dominance.py:124, config_value.py:96). Fix: `ARBITERS = {TaintSpec: Arbiter(...)}` table + shared `_rows`, `_site`. ~45 lines. Divergence: `_grouped` passes `hook_callbacks`, `field_parameter_sources` (1498-1505) but `taint.verdicts` (taint.py:124-127) does not, so the single-query fallback (scan.py:519) asks a different question from the batch.
+- **W4.2 Three build routes in `_build_impl`** (backend.py:573-664): "frontend → None ⇒ dispose; `_apply_overlays`" twice (630-638, 648-653); unparsed warning twice (618-619, 654-655); `CpgResult(...)` twice (622-630, 656-664). Fix: `_frontend_or_none()`, `_result(paths, unparsed, dispose)`. ~30 lines. Bug candidate: the joern-parse retry (648) calls `_build_with_frontend` without `exclude`, so vendored trees are included on retry.
+- **W4.3 `_run` vs `_run_bounded`** (backend.py:1514-1555 vs 1414-1484): same Popen/`_jvm_env`/communicate/group kill; the unbounded `_run` returns None on timeout and never calls `_partial_stdout`, so the partial-answer harvest at `_batch_once` (1247) works only under a budget. Fix: one `_run(command, timeout, cwd)` with deadline `min(timeout, budget.remaining())`. ~35 lines.
+- **W4.4 `query` (1285-1316) vs `_batch_once` (1193-1281)**: same script check, session-or-`joern --script`, `_run`, `_engine_failure`, `extract_payload`, `_time_available`. Fix: `_invoke(cpg_path, query, params, *, harvest)`. ~25 lines.
+- **W4.5 Shard merges** `query_across` (933) / `query_batch_across` (951). ~8 lines.
+- Unreachable, new: `pipeline._arbitrate` config and taint branches (119-121; only caller 96 is inside `isinstance(spec, DominanceSpec)`), so `config_value.verdict` (90) and `taint.verdict` (72) are test-only; `taxonomy.hierarchical` (76) no caller at all; `taxonomy.related` (80) tests only. Confirmed: model/report.py `SliceRow`, `overfitting_gap`, `render_findings` tests only; model/audit.py no src importer.
+
+### U7 learn/
+- **W7.1 Repository-cluster bootstrap four times**: `evaluate.cluster_bootstrap` (318-337), `experiments._group_bootstrap_precision` (1107-1122), `experiments.paired_bootstrap` (673-706), `calibrate.check` (198-213); percentile rule inconsistent (`int(q*(n-1))` thrice vs `round` in calibrate._percentile 192). Fix: `cluster_resample(groups, rng)` + `percentile_ci(values, alpha)`. ~45 lines, one quantile rule.
+- **W7.2 Compile split and folds three times**: learn/cli.py:312-313, compile.py:367-370, experiments.program_folds (458-463); cli and compile ignore a configured `k`, only compile calls `assert_disjoint`, experiments reads `k`. Fix: `folds.evaluation_folds(memory, cfg) -> (split, folds, digest)`. ~10 lines, closes an evaluate/experiment mismatch.
+- **W7.3 JSONL readers five times**: `examples.read_jsonl` (333), plane/tasks/features._jsonl (188), remember._jsonl (156) byte-identical; loop.py:227 inline; typed `experiments.read_units` (440), `memory.read_memory` (956). Fix: one `read_jsonl`. ~15 lines.
+- **W7.4 `run` (899-996) vs `run_rule` (1035-1096)**: `_checked_units` copied at 921-930 vs 1021-1032; summary `put_row` duplicated 989-995 vs 1088-1094; the row header repeated at 393, 521, 862-866, 990, 1093, 1305-1309. Fix: call `_checked_units`; `_experiment_row(manifest, pin, task, **extra)`. ~35 lines.
+- **W7.5 SourceGuard vs improve.memory.Guard: not duplicates** (path allowlist vs row exclusion).
+- Unreachable: `roles.load_roles` (539) no caller; `features.language_of` (65) no caller (new); `folds.leave_one_*` (129, 138) only tests/test_learn_retrieve.py:169-170; `features.build_for_scan` (467) only tests/test_learn_roles.py:180; `plane/tasks/features.recorded_case` (202) tests only. `program.prepare_ensemble` is live (program.py:521).
+
+### U8 + U9 plane/, search/, benchmarks/ax
+- **W8.1 Safe tar extraction five times**: `delivery.collect` (delivery.py:134-140) and `egress.Receiver.accept` (egress.py:278-284) byte-identical; `runner._export` (runner.py:500-504, `filter="data"`); `executor.unpack_checkout` (executor.py:658-690, size/count/.git caps); `verify_task.extract_checkout` (161-215, gzip cap). The plane copies have no size cap and no `filter="data"`. Fix: one `safe_extract(data, dest, *, max_bytes, max_files, allow_dirs)`. ~50 lines.
+- **W8.2 Presigned PUT/GET with different redirect handling**: plane `runner.deliver`/`fetch` (719-790) follow redirects; `search/executor.URLTransport` (508-535) no proxy + NoRedirect; `verify_task.upload` (262-270) its own `_NoRedirect`. Fix: one `ObjectURL` client (no proxy, no redirect, bounded read, backoff). ~60 lines.
+- **W8.3 Presigning: one store API, two layouts**: `Delivery.body` (delivery.py:95-111, `runs///`) vs batch.py:289-304, 716-723 (`engine-queue/...`). Part of W8.4.
+- **W8.4 Two task-dispatch stacks**: plane (reconciler `Ax` 185-228 → Router POST → plane/runner 1,037 lines → tar PUT → `Delivery.collect`) vs search (batch.py `lane.ax` 241-262 → `SearchExecutorTask` 675-800 → `search/executor.serve` 622 → part PUTs). Four CLI wrappers: `reconciler.Ax._run` (188; no timeout, no KUBECONFIG, no `--atespace`), `batch.ax` (241-262), `egress.Egress._run` (174), `batch.check_cluster_health` (221); three error tails. First step: one `Cli(executable, base_args, env, timeout).run(*args)` + `error_tail(proc)` in plane/ (~40 lines). Merging the stacks is a phase-3 decision.
+- **W8.5 Redaction three ways**: `router.redact` (router.py:50, exact values), `redaction.redact_secrets` (redaction.py:38, patterns, `***REDACTED***`), `search/task_storage.diagnostic` (100-120, own `sk-`/`gh*_`/`Bearer` regexes, `[redacted]`); `runner.safe_url` (713) URL-only. runner.py:621 imports `router.redact` (not a copy). Fix: `redaction.py` takes `values=`, `urls=True`. ~20 lines.
+- Unreachable: receiver delivery mode (reconciler.py:409-412, egress.py:233-324, runner.py:829-840; `OUSAST_DELIVERY` set by no script, ops/ax/up.sh:121-124 still creates the Service); `delivery.put_input` (167) tests only; `coordinator.BatchExecutor` (426) tests only; `memory.purge` (836) + `delete_version` (833) tests only; `memory.drop_rows` (399) used by benchmarks/learn/build_features.py.
+
+### U1 + U2 cli.py and core
+- **W1.1 Argument plumbing**: `_main` (cli.py:426-470) unpacks the namespace for `_scan` (4), `_benchmark` (3), `_improve` (14, 432-447), `_pairs` (6), `_repos` (4). Fix: `set_defaults(func=…)`, handlers take `args`. ~45 lines.
+- **W1.2 "Is a file" guards** at 1077-1078, 1123-1124, 1128, 1184-1185. Fix: `type=existing_file`. ~10 lines.
+- **W1.3 `_benchmark` vs `mcp._benchmark`** (mcp.py:160-183): the MCP copy skips `runtime_seconds`, `baseline_findings`, `write_benchmark_artifacts`, so MCP benchmark runs leave no artifacts. Fix: `benchmark.run_benchmark(manifest, config, mode)` used by both; inline `_load_scan_findings` (1499). ~20 lines.
+- **W1.4 Degradation recording 13 times** (cli.py:647, 698, 773, 817, 843, 996, 1312, 1330, 1343-1344, 1411, 1427, 1445-1446), mixing raw dicts and `stages.skip_as_degradation` (stages.py:32). Fix: `runtime.degrade(stage, reason, detail)`. ~15 lines.
+- **W1.5 `write_*` wrappers of one json.dumps** (24 sites in 19 files: findings.py:113, verification.py:121, mapping.py:240, 245, config.py:256, rank.py:72, calibration.py:185, 190, overlay.py:126, preprocess.py:90, regress/candidate.py:222, complexity/map.py:57; inline cli.py:726, 746, 1713; `_write_json` in reconciler.py:76 and repo_facts.py:211). Fix: `run.write_json`, `write_records`. ~35 lines.
+- **W1.6 Run-dir creators** run.py:18-26 vs benchmark.py:185-190 (~6 lines). **W1.7 findings.json six writes** (~5 lines).
+- Unreachable, each only one test file: `calibration.compute_ranking_metrics`, `findings.build_quick_hunter_prompt`, `verification.validate_evidence_transition`, `fusion.should_fuse`, `harness.ProcessorSpec` (+ `_run_processor`, `_contract_violation` with no reference at all), `rank.rank_targets_with_model`, mapping's five task/record functions.
+
+Confidence: high for W3.2-W3.4, W4.1-W4.4, W7.1-W7.4, W8.1, W8.2, W8.5, W1.1-W1.5 and the unreachable
+list (name greps across src, tests, benchmarks, ops; dispatch-table or getattr calls would be missed); medium
+for W8.4 and the line estimates. The `hook_callbacks` divergence, the missing `exclude` on retry, the redirect
+handling and the plane tar limits come from reading, not from running anything.
diff --git a/PATHFINDER-2026-10-13/03-unified-proposal.md b/PATHFINDER-2026-10-13/03-unified-proposal.md
new file mode 100644
index 0000000..8f893c1
--- /dev/null
+++ b/PATHFINDER-2026-10-13/03-unified-proposal.md
@@ -0,0 +1,164 @@
+# Unified proposal (2026-10-13)
+
+Written by the orchestrator from 00 to 02. One rule throughout: prefer deletion over abstraction, one path over
+configurable paths, and no new layer "for flexibility". Everything here serves the four active lines in
+`.kiro/steering/overview.md`: hunter-learning-loop, pre-push-safety-net, search-with-proof,
+engine-change-scoped-analysis. What serves none of them is cut.
+
+## What the map says in three sentences
+The code is not rotten (14 clone findings, 13 unreferenced lines), it is wide: each spec added a package and
+nothing retired one. The same five jobs are done in several places: calling a model and counting its cost
+(seven client types, four cost paths), finding the enclosing function and its callers (two naming systems, two
+caller indexes, one grep), running the quick rules (nine copies), running code in a sandbox for a verdict (two
+runtimes, two verdict vocabularies, neither reaching the evidence ladder), and writing scan results (findings
+written six times, five ledgers under one directory). And three subsystems serve no active line: the improve
+lever, the deep-mode regress arm, and the HarnessX-era half of the plane executor.
+
+## Systems to unify (S1 to S6), in the order they pay off
+
+### S1. One source-facts module (D4 + D5): the hunter's annotation producer
+Today: semantic/functions.py (tree-sitter + regex fallback, sentinel ``), plane/tasks/repo_facts.py
+(regex only, sentinel ``, DECLARATION/KEYWORDS/product_files), learn/features.callers (own regex),
+plane/tasks/verify.callers_of, learn/excerpt.function_bounds, hunter_tools.find_refs (grep), plus alias maps in
+three places. This is the exact set of facts hunter-learning-loop R3 names (enclosing function, callers, entry
+points, roles, def-use), so it is the first thing to own.
+Unified: `src/openultrasast/source.py` with `ranges(path, text, lang)`, `enclosing(ranges, line)`,
+`bounds(ranges, name)`, `call_sites(text, names)`, `product_files(root)`, `DECLARATION`, `KEYWORDS`,
+`LANGUAGE_ALIASES` (beside `preprocess.LANGUAGE_BY_EXTENSION`), one sentinel, one reserved set. Entry point:
+`source.facts_for(root, path, line) -> SourceFacts(function, span, callers, entry_points)`; entry points keep
+coming from `mapping.analyze_entry_points`, called once per run and passed in.
+Old call sites: semantic/functions.py becomes the implementation moved here (keep `function_at` as an alias
+for one release); plane/tasks/repo_facts.py keeps `main()` and imports the tables from source.py; learn/labels.py:48,
+learn/features.py:27, 365-395, learn/roles.py:35, learn/excerpt.py:35, 302, plane/tasks/alerts.py:108-114,
+plane/tasks/verify.py:128, plane/harvest.py:46, hunter_tools.py:83, 317-323 → source.py.
+Lost: the per-language DECLARATION patterns for Java/Go/Ruby/PHP must be merged into `_DECL`; re-label the pair
+corpus and diff `labels-.jsonl` before switching. The Learn↔PlaneTasks cycle disappears.
+
+### S2. One chat client, one meter, one resolver (D1 + D10 parsers and retry policy)
+Unified: `model/endpoint.py` keeps the single client (today's `DeepSeekChatClient`, renamed `ChatAdapter`; usage
+is a list of rows) and the single resolver `resolve_chat_client(config | None) -> (client, endpoint)` that
+honours `[models]` and prices by the model that runs. `plane/budget.py` keeps one `Meter` with
+`reserve(bound)` and `settle(row)`; `MeteredClient` (post-call) and the reserve-first mode replace `FillMeter`,
+`SpendBudget`/`SpendReservation` and search/worker `_call`. One `cost(row, prices)`. `Caller` (learn/program.py)
+stays as the cache and replay layer on top of the meter. `parse_json_content` (provider/openrouter.py:205) is
+the one JSON-in-fences parser; retry policy lives on the client only.
+Old call sites: tool_hunter.py:239-262 `resolve_hunter_client` → `resolve_chat_client`; `OpenRouterHunterClient`
+(203) and the env-flag set (36-39) deleted; `_load_json`/`_looks_like_json` (581, 409) → `parse_json_content`;
+cli.py:976 passes a metered client; push/assistance.py:103-110 uses `cost`; search/worker.py:218-270 uses the
+client and the reserve-first meter; learn/fill.py:44 `FillMeter` deleted; plane/tasks/verify.py:66 `_Recording`
+and model/scan.py:1628 `_CountingClient` read counts off the meter; benchmarks/valid/hunter_arm.py:56-65 shim
+deleted.
+Lost: unmetered OpenRouter calls; recorded hunter dollars change because the hunter prices by its real model.
+
+### S3. One quick-rule pipeline (D3)
+Unified: `findings.quick_pipeline(root, targets=None, *, ruleset, policy, floors=None, include_shadow=False)
+-> QuickRun(targets, entry_points, rankings, findings)`. cli.py wraps each step in `run_stage` through a
+callback argument. Old call sites: cli.py:641-687 (and the standard-mode pool at hunter.py:103-160, which keeps
+its thread pool but calls the pipeline), gate.py:81-84, map_gate.py:52-54, pairs.py:794-815, plane/tasks/alerts.py:
+120-122, improve/evolve.py:311-315 (deleted with S9), push/quick.py:87-92 (passes its changed-file targets),
+hunter_tools.py:232 (deleted with S8), benchmarks/learn/build_features.py. Fixes the standard-mode floors bug
+(cli.py:670 vs 683-684). Lost: nothing; gates pass `ruleset=PATTERN_RULES` to keep their numbers.
+
+### S4. One sandbox verdict on the evidence ladder (D2)
+Unified: `search/_sandbox.run` implements `sandbox.SandboxRunner` (its options move into `SandboxJob`);
+`resolve_sandbox_runner` picks bwrap or docker; one outcome record `ExecutionOutcome(outcome ∈ confirmed |
+refuted | inconclusive | unavailable, reason, runs)` that emits `Rung.EXECUTION_CONFIRMED` (model/ladder.py:33)
+so push_scoring.py:84, mcp.py:98 and learn/schema.py:77 finally receive execution evidence. Two oracle
+strategies stay (owned canary, exit code). regress/ is deleted (S9): the deep-mode REGRESS stage calls the
+proof arm's verify path once that lands, and until then deep mode equals standard mode and says so in a
+degradation. Lost: snippet-based regress in `scan --mode deep` until search-with-proof is wired to scan. This
+is the one capability loss in the proposal and it is deliberate: the maintainer named proof as the block
+gate and the search line owns it.
+
+### S5. Deadline type and the CPG↔Model cycle (D6 + W3.5)
+Unified: `ExecutionBudget` moves to `contracts.py` with `expired()`, `remaining()`, `cleanup_end`, `check()`;
+`DeadlineExpired` and `remove_owned_tree` become public in a `deadline.py` next to it. Old call sites: the 66
+budget expressions (push 47, model 13, cpg 16), cpg/backend.py:37, cpg/session.py:44, model/partitions.py:14,
+cpg/artifact.py:151, push/cache.py:22. Also W4.3: one `_run` in cpg/backend.py so standard scans get partial
+stdout; W4.1: one `ARBITERS` table used by scan.py and pipeline.py (and the `hook_callbacks` divergence closes).
+Lost: nothing.
+
+### S6. Scan assembly: write once, score last, one ledger (D9 + U1 bugs + W1.4 + W1.5)
+Unified: `_run_scan` accumulates findings per proposer in memory and writes `findings.json` once in a
+`finally`; scoring and verification move after MAP so model, overlay, obligation and hunter findings are scored
+and verified (the U1 bug); `CalibrationLedger(target)` owns the five files under `.openultrasast/calibration/`;
+`runtime.degrade(stage, reason, detail)` replaces 13 inline appends; `run.write_json` replaces 24 `write_*`
+wrappers; `benchmark.run_benchmark` is shared by cli and mcp (W1.3). fusion.py is deleted: its decisions never
+reach findings or score (U1), and harness.py loses the processor machinery nobody builds. Lost: a partial
+findings.json on a crash, unless the finally write is kept (it is).
+
+### S7. Memory store as a leaf; plane executor trimmed (U7 inversion + U8 + W8)
+`plane/memory.py` loses its three lazy imports: `seed` moves to `plane/seed.py`, `results_root` to
+`plane/paths.py`. The executor keeps reconciler, runner, delivery, router, egress, doctor, profile and loses
+engine_alerts.py, harvest.py, workspaces.py, generate.py and receiver mode (their live helpers `declares`,
+`docker_command`, `frozen_source`, `read_record`, `export_pin` move into benchmarks/learn, their only users).
+One `Cli` wrapper and `error_tail` for ax and kubectl (reconciler, egress, benchmarks/ax/batch); one
+`safe_extract` and one `ObjectURL` client shared by plane/runner, delivery, search/executor and verify_task
+(plane extraction gains the size cap and the no-redirect rule). The two dispatch stacks stay two for now: the
+search stack is on the active proof line and the plane stack is parked with plane-on-kubernetes; merging them
+is a decision for when the ax lane is back.
+
+## Cuts (S8, S9): what leaves the tree
+S8, hunter unit: the four never-enabled tools and their schemas and dispatch branches (hunter_tools.py:174-290,
+tool_hunter.py:94-137, 505-521), the dead `find_refs` mapping branch (hunter_tools.py:86-101, 118-164),
+`OpenRouterHunterClient`, `_load_json`, `_looks_like_json`, `ScriptedChatClient` stays. The facts those tools
+computed come back as annotations from S1, which is what the hunter-learning-loop spec asks for.
+S9, off every active line: improve/ (1,175 lines; `ousast improve`, the `loop-improve` task at
+plane/generate.py:380 and plane/tasks/loop.py, plane/memory.py:1002's snapshot guard, docs/memory.md and
+README sections); regress/ (549) with the REGRESS branch cli.py:839-880; skills.py + index.py (446; the routed
+ids are trajectory metadata only, hunter.py:137-139, 96, 157); model/audit.py (255) to a maintainer script or
+gone; test-only functions in mapping.py (188-237), calibration.py:108, findings.py:102, verification.py:39,
+rank.py:46-69, model/report.py:24-75, taxonomy.hierarchical/related, config_value.verdict, taint.verdict,
+pipeline._arbitrate's dead branches, learn/roles.load_roles, learn/features.language_of and build_for_scan,
+folds.leave_one_*, plane/tasks/features.recorded_case, coordinator.BatchExecutor, memory.purge/delete_version,
+delivery.put_input, worker.main/verify.main/coordinator.main shims, cpg/queries/vocabulary.sc. Each goes with
+its test file. push_inputs.py moves under benchmarks/push. mcp.py stays until the mcp-shift-left-agent spec is
+decided (breadth.toml: parked, close date 2026-12-01). map_gate.py and pair_gate.py stay with CI for now; their
+removal needs cli.py:49, ci.yml, dagger/ci.py and the gate baseline changed together.
+
+Estimated effect (lines, rough): duplication removal 650 to 800 in src; cuts about 4,500 in src (improve 1,175,
+regress 549, plane dead modules about 1,300, skills+index 446, audit 255, hunter tools and client about 450,
+test-only functions about 350) and the tests that only exercised them, about 9,000. src goes from 60k toward
+55k and tests from 54k toward 45k, with every remaining package on an active line or in breadth.toml as parked
+with a close date.
+
+## Order of work
+1. S9 cuts and S8 (cheap, no design, unblocks everything else; one commit per package with its tests).
+2. S1 source facts (the annotation producer hunter-learning-loop needs first).
+3. S2 client and meter (the ablation runs need one metered client with replay).
+4. S3 quick pipeline, S5 deadline type (small, mechanical).
+5. S6 scan assembly (fixes the scoring-before-MAP bug; touches cli.py most).
+6. S4 sandbox verdict (with search-with-proof's next increment).
+7. S7 plane trim (when plane-on-kubernetes resumes or is closed).
+
+## The proposed system
+```mermaid
+flowchart TD
+ subgraph inputs
+ SCAN[ousast scan
cli.py:619] --> QP
+ PUSH[pre-push runner
push/runner.py:609] --> QP
+ GATE[gate.py / pairs / alerts] --> QP
+ end
+ QP[quick_pipeline
findings.py (S3)] --> FACTS
+ QP --> ENGINE
+ FACTS[source.facts_for
source.py (S1)] --> ANN[annotation model
hunter-learning-loop R3]
+ ENGINE[scan_repository
model/scan.py:143] --> RUNG
+ ANN --> HUNT[run_tool_hunter
tool_hunter.py:323]
+ HUNT --> CLIENT[ChatAdapter + Meter + Caller
model/endpoint.py, plane/budget.py, learn/program.py (S2)]
+ CLIENT --> CLS[classify
learn/program.py:473]
+ CLS --> RUNG[evidence ladder Rung
model/ladder.py:29]
+ RUNG --> ADMIT[admit_candidates
push/policy.py:889]
+ RUNG --> PROOF[search verify
search/verify.py:648]
+ PROOF --> EXEC[ExecutionOutcome -> EXECUTION_CONFIRMED
sandbox (S4)]
+ EXEC --> ADMIT
+ ADMIT --> REPORT[deliver_report
push/report.py:423]
+ ENGINE --> ASSEMBLE[write once, score last
cli.py (S6)]
+ HUNT --> ASSEMBLE
+ ASSEMBLE --> LEDGER[CalibrationLedger
(S6)]
+ CLS --> STORE[(MemoryStore leaf
plane/memory.py (S7))]
+ LEARN[learn compile / evaluate / experiment
learn/cli.py] --> CLIENT
+ LEARN --> STORE
+ FACTS --> LEARN
+ BUDGET[ExecutionBudget + DeadlineExpired
contracts.py (S5)] -.-> ENGINE
+ BUDGET -.-> PUSH
+```
diff --git a/PATHFINDER-2026-10-13/04-handoff-prompts.md b/PATHFINDER-2026-10-13/04-handoff-prompts.md
new file mode 100644
index 0000000..7c93a75
--- /dev/null
+++ b/PATHFINDER-2026-10-13/04-handoff-prompts.md
@@ -0,0 +1,156 @@
+# Handoff prompts (copy each into /make-plan)
+
+Order follows 03-unified-proposal.md. Every prompt is read-only until its plan is approved. Shared guards:
+work on main, no branches; one package per commit with its tests; run `ruff check` and the touched test files
+before each commit; plain English in comments and commit messages; never read or echo `.env`; update
+`.kiro/breadth.toml` when a capability changes status.
+
+## P0. Cuts: packages and functions on no active line (S9 + S8)
+```
+Plan the removal of code that serves none of the four active lines in .kiro/steering/overview.md, using
+PATHFINDER-2026-10-13/03-unified-proposal.md section "Cuts" and the per-unit "removal candidates" in
+PATHFINDER-2026-10-13/01-flowcharts/*.md as the inventory. One commit per package, tests included.
+Packages: src/openultrasast/improve/ (and `ousast improve` in cli.py:237-281, 433, 1106-1170; the loop-improve
+task at plane/generate.py:380 and plane/tasks/loop.py:67-104, 194-224; plane/memory.py:1002's snapshot guard;
+README.md:220, 314; docs/memory.md, docs/examples.md:58-85, docs/index.md:45, docs/threat-model.md:18;
+ops/ax/README.md:101; tests test_improve*.py, test_cli_improve.py, test_subtraction_complete.py:67-89,
+test_gate.py:135-136); src/openultrasast/regress/ with cli.py:839-880 (deep mode records a degradation
+"regress retired, proof arm pending" and otherwise equals standard; tests test_regress_*.py,
+test_split_sink_regress.py, test_reports.py:12); skills.py + index.py (hunter.py:137-139 stops routing; drop
+`selected_skills` from the task and trajectory at hunter.py:96, 157; tests test_skills, test_index, the skills
+part of test_hunter); model/audit.py (either delete with tests/test_module_audit.py and the
+2026-09-08-module-audit.json reference in overview.md, or move to ops/tools/); hunter tools never enabled
+(hunter_tools.py:174-290 read_definition/entry_points/flows/obligations, tool_hunter.py:94-137 schemas,
+505-521 dispatch, the find_refs mapping branch hunter_tools.py:86-101, 118-164; tests
+test_hunter_curated_tools.py); cpg/queries/vocabulary.sc with tests/test_cpg_queries_compile.py:32 and
+benchmarks/push/ontology_gap.py:102.
+Functions (each with its only test): mapping.py:188-237 five task/record functions and their record classes
+(:39-56); calibration.py:108; findings.py:102; verification.py:39; rank.py:46-69 and RankerClient (:31);
+fusion.py:94 and, if S6 is accepted, fusion.py entirely (cli.py:720-726, fusion.json); harness.py:41-55,
+125-166 processor machinery; model/report.py:24-75; model/taxonomy.py:76, 80; model/config_value.py:90 and
+model/taint.py:72 (tests move onto `verdicts()[0]`), model/pipeline.py:117-121 becomes dominance only;
+learn/roles.py:539; learn/features.py:65, 467; learn/folds.py:129-157; plane/tasks/features.py:202;
+search/coordinator.py:426-484 and :487, search/worker.py:530, search/verify.py:714; plane/memory.py:833-843;
+plane/delivery.py:167; receiver mode plane/egress.py:128-160, 233-324, plane/runner.py:829-840,
+plane/reconciler.py:409-412, ops/ax/up.sh:121-124. Move push_inputs.py to benchmarks/push/ (callers
+benchmarks/push/{measure,m1a_record_vetoes,transfer_replay}.py, tests/test_push_inputs.py).
+Guards: do not touch mcp.py, map_gate.py, pair_gate.py, vetoes.py (parked or CI-bound, see
+.kiro/breadth.toml); confirm each deletion with `grep -rnw src tests benchmarks ops docs` before
+removing; keep a table in the commit message of lines removed per package; re-run `fossil-mcp dead-code src
+--min-confidence certain` at the end and expect zero.
+```
+
+## P1. One source-facts module (S1)
+```
+Plan `src/openultrasast/source.py` as the single owner of function ranges, enclosing function, function
+bounds, call sites, product-file enumeration, declaration tables and language aliases, per
+PATHFINDER-2026-10-13/02-duplication-report.md D4 and D5 and the facts table in 01-flowcharts/U5-semantic.md.
+Entry points: `ranges(path, text, lang)`, `enclosing(ranges, line)`, `bounds(ranges, name)`,
+`call_sites(text, names)`, `product_files(root)`, `facts_for(root, path, line, *, entry_points)`.
+Implementation moves from semantic/functions.py (tree-sitter with the regex fallback at :120-133, sentinel
+``, RESERVED_FUNCTION_LABELS :23-51, `_DECL` :56-69 merged with repo_facts DECLARATION :48-63 and
+KEYWORDS :64-88 for Java, Go, Ruby, PHP). Call sites to rewrite: plane/tasks/repo_facts.py:30-146 (keep
+main() and re-export the tables), plane/tasks/alerts.py:108-114, plane/tasks/verify.py:128-141,
+learn/labels.py:48, learn/features.py:27, 49, 365-395, 415, learn/roles.py:35, learn/excerpt.py:35, 302,
+plane/harvest.py:46, plane/generate.py:72, plane/tasks/remember.py:44, hunter_tools.py:83, 317-323,
+benchmarks/valid/hunter_arm.py (annotation producer for hunter-learning-loop R3).
+Guards: one sentinel and one reserved set, no compatibility shim beyond a one-release `function_at` alias;
+before switching, regenerate labels-.jsonl with the old and the new module and diff them, and record
+the diff in benchmarks/measurements/; do not add a registry of "range providers", a switch on language is
+enough; nothing in source.py may read labels, advisories or commit messages (hunter-learning-loop R3.2).
+```
+
+## P2. One chat client, one meter, one resolver (S2)
+```
+Plan the collapse of the LLM client stack described in PATHFINDER-2026-10-13/02-duplication-report.md D1 and
+D10 and 01-flowcharts/U6-hunter-client.md. Target: model/endpoint.py keeps one client class (rename
+DeepSeekChatClient to ChatAdapter, usage as a list of rows, retry policy including the empty-JSON re-call
+lives here and nowhere else) and one resolver `resolve_chat_client(config | None) -> (client, endpoint)`
+that honours [models] and prices by the running model; plane/budget.py keeps one Meter with
+`reserve(bound)`/`settle(row)` and one `cost(row, prices)`; learn/program.py Caller stays as the cache and
+replay layer; provider/openrouter.py parse_json_content is the only JSON-in-fences parser.
+Call sites: tool_hunter.py:36-39, 163-262 (delete OpenRouterHunterClient and the env-flag sets, keep
+ScriptedChatClient and the ChatClient Protocol), 409, 581; cli.py:976 (pass a metered client);
+push/assistance.py:84, 103-110; search/worker.py:153-165, 188-189, 218-270 (reserve-first mode of the
+Meter; StubModel stays); learn/fill.py:44 (FillMeter deleted); learn/cli.py:241, 254; plane/tasks/verify.py:
+66, 181, 232-244, 267, 378; model/scan.py:1628-1640; budget.py:59-60, 154-170, 260-322; endpoint.py:39,
+135-144, 262; benchmarks/valid/hunter_arm.py:55-66 (shim deleted).
+Guards: no new abstraction above ChatAdapter; do not keep OpenRouterHunterClient behind a flag; the only
+behaviour change allowed is "hunter dollars are priced by the configured model", record it in the commit;
+error messages carry exception types, never a key; run tests/test_hunter_arm.py,
+tests/test_model_endpoint.py, tests/test_plane_budget*.py, tests/test_search_worker.py.
+```
+
+## P3. One quick-rule pipeline and the deadline type (S3 + S5)
+```
+Plan two mechanical consolidations from PATHFINDER-2026-10-13/02-duplication-report.md D3, D6 and W3.5,
+W4.1, W4.3.
+(a) `findings.quick_pipeline(root, targets=None, *, ruleset, policy, floors=None, include_shadow=False)
+-> QuickRun(targets, entry_points, rankings, findings)` with an optional `stage(name, fn)` callback so
+cli.py can keep run_stage. Call sites: cli.py:641-687, hunter.py:103-160 (keeps its pool, calls the
+pipeline per target), gate.py:81-84, map_gate.py:52-54 (pass ruleset=PATTERN_RULES explicitly), pairs.py:
+794-815, plane/tasks/alerts.py:120-122 (include_shadow=True), push/quick.py:87-92 (passes changed-file
+targets), benchmarks/learn/build_features.py. Fix the standard-mode floors bug (cli.py:670 vs 683-684).
+(b) Move ExecutionBudget (model/contracts.py:19-33) to contracts.py with expired(), remaining(),
+cleanup_end, check(exc); make DeadlineExpired and remove_owned_tree public in deadline.py; rewrite the 66
+budget expressions (push 47, model 13, cpg 16: snapshot.py:635, 642, scan.py:203, 1435, backend.py:1377,
+1399, report.py:160, partitions.py:117-267, regions.py:313), cpg/backend.py:37, cpg/session.py:44,
+model/partitions.py:14, cpg/artifact.py:151, push/cache.py:22. Then one `_run` in cpg/backend.py (merge
+1514-1555 into 1414-1484 so standard scans harvest partial stdout) and one ARBITERS table for scan.py:
+1491-1506 and pipeline.py:93-121 (closing the hook_callbacks divergence at taint.py:124-127).
+Guards: no behaviour change except the two named bugs; gate numbers in benchmarks/measurements/
+2026-09-06-gate-baseline.txt must not move; run the gate, map_gate, pair_gate and test_push_*, test_model_*,
+test_cpg_* files.
+```
+
+## P4. Scan assembly: write once, score last, one ledger (S6)
+```
+Plan the rewrite of `_run_scan` (cli.py:619-959) per PATHFINDER-2026-10-13/01-flowcharts/U1-scan.md
+"Likely bugs" and 02-duplication-report.md D9, W1.3-W1.5: findings accumulate per proposer and are written
+once in a finally; scoring (cli.py:731-746) and verification (711) move after MAP (757-835) so model,
+overlay, obligation and hunter findings are scored and verified; `CalibrationLedger(target)` owns
+false_positive_learnings.json, rule_policy.json, complexity_ledger.json (cli.py:94, 635-636, 765, 872,
+1021-1022; plane/tasks/loop.py:43 reads the same path); `runtime.degrade(stage, reason, detail)` on
+HarnessRuntime replaces the 13 inline appends (cli.py:647-1446) and stages.skip_as_degradation;
+`run.write_json` and `write_records` replace the 24 write_* wrappers; `benchmark.run_benchmark` is shared
+by cli.py:1076 and mcp.py:160-183 so MCP benchmark runs write artifacts; fusion.py is deleted (cli.py:720-726,
+fusion.json); harness.py processor machinery is deleted (41-55, 125-166, cli.py:627-632).
+Guards: keep the finally write so a crash still leaves findings.json; the manifest and SARIF formats do not
+change; add one test that a model-layer finding appears in score.json and verification.json; run
+tests/test_scan_pipeline_integrity.py, test_reports.py, test_scoring.py, test_mcp.py, test_benchmark.py.
+```
+
+## P5. One sandbox verdict on the ladder (S4)
+```
+Plan the unification in PATHFINDER-2026-10-13/02-duplication-report.md D2 and
+01-flowcharts/U9-search-sandbox-regress.md: search/_sandbox.run (:105) implements sandbox.SandboxRunner
+(options move into SandboxJob, sandbox/runner.py:29-45); resolve_sandbox_runner (:59) chooses bwrap or
+docker; one `ExecutionOutcome(outcome in confirmed|refuted|inconclusive|unavailable, reason, runs)` that
+search/verify.py:648-701 produces and that emits Rung.EXECUTION_CONFIRMED (model/ladder.py:33) into the
+evidence ladder so push_scoring.py:84, mcp.py:98 and learn/schema.py:77 receive execution evidence; the
+canary oracle (oracles.py:30, 307) and the exit-code oracle remain two strategies. regress/ is already gone
+(P0); the deep-mode REGRESS stage (cli.py:839-880) becomes a call into search's verify path for findings
+that carry a demo, else a degradation.
+Guards: do not keep the regress verdict vocabulary behind a flag; the differential 3-vs-3 rule and the 0
+false proofs requirement of search-with-proof Req 6.2 are unchanged; the only new rung producer is the
+verify path; run tests/test_search_verify*.py, test_sandbox*.py, test_push_scoring.py.
+```
+
+## P6. Memory store as a leaf; plane executor trimmed (S7)
+```
+Plan, per PATHFINDER-2026-10-13/01-flowcharts/U7-learn-memory-tasks.md "Memory→Plane inversion",
+U8-plane-executor.md "Removal candidates" and 02-duplication-report.md W8.1-W8.5: move `seed`
+(plane/memory.py:984-1020) to plane/seed.py and `results_root` to plane/paths.py so memory.py has no plane
+import; delete plane/engine_alerts.py, harvest.py, workspaces.py, generate.py and their cli.py subcommands
+(1570-1611, 1667-1700) after moving `declares` (harvest.py:116), `docker_command`, `frozen_source`,
+`read_record`, `export_pin` (engine_alerts.py) into benchmarks/learn/, their only users; one
+`Cli(executable, base_args, env, timeout).run(*args)` and `error_tail(proc)` in plane/ for
+reconciler.py:185-228, egress.py:174-216, benchmarks/ax/batch.py:221-262; one `safe_extract(data, dest, *,
+max_bytes, max_files)` for delivery.py:134-140, runner.py:500-504, search/executor.py:658-690,
+search/verify_task.py:161-215; one `ObjectURL` client (no proxy, no redirect, bounded read, backoff) for
+runner.py:719-790, search/executor.py:508-541, verify_task.py:240-270; redaction.py takes `values=` and
+`urls=True` so router.redact (:50), task_storage.diagnostic (:100-120) and runner.safe_url (:713) are thin.
+Guards: the two dispatch stacks stay two (decision deferred to plane-on-kubernetes); credentials still travel
+only in the start POST body; nothing logs a URL with a query string; run tests/test_plane_*.py,
+test_search_executor*.py, test_ax_batch.py.
+```