-
Notifications
You must be signed in to change notification settings - Fork 0
Seam De Vaulting V5 6
Note
Status: complete (2026-06-18)
Plan: .harness/PLAN.md (V5-6 — Seam De-Vaulting, routing layer onto the storage seam)
Decision: Memory-storage seam
The third and final leg of the V5 de-vaulting arc: re-plumb the kernel's routing/index mechanisms so they speak Locators to the V5-1 storage seam instead of building vault_path() / … filesystem paths directly. After this plan, a fresh install with only the device-local backend can host a project, its harness state, and the repo registry without needing an Obsidian/GDrive vault.
Three-leg de-vaulting arc:
| Leg | Plan | Surface | Status |
|---|---|---|---|
| Data plane | V5-3 |
harness_state_dir / read_state_file / write_state_file / phase_recall / resolve_documenter_context
|
Shipped (v5.5.0) |
| Config plane | V5-7 (partial) |
agentm_config, vault_path() fail-loud guard |
Partially shipped |
| Routing plane | V5-6 (this) |
resolve_project / _vault_projects_dir — task 1 shipped; repo_registry — task 2 shipped; state_mode — task 3 shipped; gate extensions + conformance suite — task 4 shipped; ADR + docs — task 5 shipped
|
Complete |
Make the kernel's routing layer backend-agnostic. Three mechanisms hard-code vault_path() path construction today; all three route through the seam after this plan:
-
resolve_project/_vault_projects_dirinharness_memory.py— project-slug-to-path resolution. -
repo_registryinrepo_registry.py— the_meta/repos.jsoncross-repo index. -
state_moderesolver /resolve_documenter_context— the harness-state-location selector and its documenter-context reader.
Behavior-preserving (LC-1): on the obsidian-vault backend every mechanism resolves to identical bytes in identical locations. A per-thread parallel-run assertion proves this before vault-shaped path construction is removed.
harness_memory.py:339 (resolve_project) and :318 (_vault_projects_dir) currently build paths as vault_path() / "projects" / slug / …. After this plan they call backend.resolve("projects", slug, …) and callers that received a Path receive a Locator and use seam verbs (backend.read / backend.write). Slug producers (vault_project, detect_project) are confirmed vault-agnostic (LC-2) and unchanged.
repo_registry.py currently hardcodes <vault>/_meta/repos.json at line 68. All five public functions (read_registry, write_registry, register_repo, unregister_repo, list_repos) re-point to address _meta/repos.json through the active backend. On obsidian-vault the file lands in the same place; on device-local it lives on-device. The vault_path parameter threaded through registry functions is replaced by active-backend resolution.
_read_config_state_mode at harness_memory.py:228 is renamed: the non-local state-mode value is backend rather than vault. Existing state_mode: vault entries in device config and .harness/.project-mode markers alias to backend at read time — a one-line mapping, no rewrite, no operator action required (LC-5). resolve_documenter_context inherits correct routing by construction once reads go through the seam.
-
check-storage-seam-no-path-leak.shextended: fails ifresolve_project,_vault_projects_dir, or repo_registry read/write functions returnpathlib.Path. -
check-process-seam-import-direction.shextended: verifies de-vaulted mechanisms import the seam but never a capability plugin (LC-8). -
storage_conformance.pyextended: routing layer parameterized over both concrete backends —resolve_project+repo_registry+ harness-state resolution must produce identical outcomes on both backends for the same slug/key.
harness_memory._vault_projects_dir signature changed from (vault: Path) -> Path to (backend: StorageBackend) -> Locator. The function now calls backend.resolve(_VAULT_PROJECTS_REL_NEW) (the "projects" segment) and backend.resolve(_VAULT_PROJECTS_REL_LEGACY) (the "personal-projects" fallback), using backend.exists() to pick the active namespace. Returns a Locator instead of a Path — the no-path-leak gate constraint.
harness_memory.resolve_project completely rewritten. The return dict now carries {slug, project_locator, backend, project_root, layout} instead of the old {slug, vault_path, project_root, layout}. The backend is obtained via a lazy import backend_selection as _bs; backend = _bs.select_backend() to avoid the circular-import that a top-level import would cause (since backend_selection itself imports harness_memory).
Callers updated:
-
process_seam.pyline 161:enriched["target"]now usesproject_locator.keyinstead ofstr(vault_path). -
harness_memory.pycallers_invoke_toolkit_saveandoffer_savethat previously used_vault_projects_dir(vault).namewere updated to inline segment detection. -
memory_mcp_tools.pyline 263: segment detection inlined (no longer calls_vault_projects_dir).
Tests in TestVaultProjectsDir and TestResolveProject updated to use the Locator return type and mock-based backend injection.
Behavior-preserving (LC-1): on the obsidian-vault backend, backend.resolve("projects", slug) → Locator("projects/slug") maps to <vault>/projects/slug — the same bytes at the same path as the pre-V5-6 vault_path field.
repo_registry.py fully rewritten. The old registry_path(vault_path) -> Path entry-point is replaced by registry_locator(backend: StorageBackend) -> Locator (repo_registry.py#L77), which calls backend.resolve(*_REGISTRY_PARTS) — on the obsidian-vault backend this resolves to <vault>/_meta/repos.json (behavior-preserving, LC-1); on device-local it resolves to ~/.agentm/memory/_meta/repos.json.
_vault_or_none() replaced by _backend_or_none() (#L87) — lazy-imports backend_selection.select_backend() to avoid the circular import a top-level import would cause. All five public functions (read_registry, write_registry, register_repo, unregister_repo, list_repos) now take backend: StorageBackend instead of vault_path: Path.
write_registry (#L126) does an explicit content-hash CAS check before delegating to backend.write(). _mutate_registry (#L198) no longer holds vault_mutex directly — VaultBackend.write() handles it internally; the CAS retry loop handles cross-device correctness. The vault_mutex removal is captured in a doc comment at #L211.
project_config.is_registered takes backend=None instead of vault_path=None (project_config.py#L151). CLI graceful-skip fires when select_backend() raises (backend plugin unavailable), not merely when MEMORY_VAULT_PATH is unset.
Tests: 9 new/rewritten tests in TestRepoRegistry (test_harness_memory.py#L2110) using the DeviceLocalBackend pattern. TestRepoRegistryCLI.test_list_skipped_when_backend_unavailable (#L2297) replaces test_list_skipped_when_no_vault. LC-7 parallel-run test at #L2265 confirms VaultBackend registry_locator maps to <vault>/_meta/repos.json — same on-disk path as before V5-6. 20/20 check-all.sh.
harness_memory._read_config_state_mode (harness_memory.py#L228) now maps "vault" → "backend" at read time via a one-line guard at #L258-L260. No config rewrite, no operator migration — existing state_mode: vault entries in ~/.agentm-config.json continue to resolve correctly (LC-5).
harness_memory._read_project_mode (#L511) applies the same alias for the per-repo .harness/.project-mode marker at #L536-L537: a file containing the string "vault" now returns "backend". The device-level fallback path at #L540 delegates to _read_config_state_mode, which already normalizes.
agentm_config._STATE_MODES (agentm_config.py#L51) adds "backend" as the canonical value; "vault" is retained as a deprecated CLI alias. agentm_config.cmd_set_state_mode (#L125) normalizes "vault" → "backend" at write time at #L142-L144 and the CLI --state-mode help text at #L244-L245 surfaces the deprecation note.
resolve_documenter_context (harness_memory.py#L1030) closes LC-6 by construction — it already returns None after V5-3, so no change was needed.
Tests: 3 new tests in TestReadConfigStateMode (test_harness_memory.py#L1299): test_reads_backend (#L1299), test_reads_vault_aliases_to_backend (#L1304), and test_vault_and_backend_produce_identical_resolution (#L1310 — the dedicated alias-equivalence test required by plan). 2 updated tests in TestAgentmConfig (test_agentm_config.py#L338): test_set_state_mode_backend_writes_field_rc0 and test_set_state_mode_vault_normalizes_to_backend (#L345). verify-phases green (phase lifecycle exercises state_mode). 20/20 check-all.sh.
check-storage-seam-no-path-leak.py — Pass 2 (V5-6): A second scan pass is added that targets the two routing files (harness_memory.py and repo_registry.py) and checks seven named routing functions (resolve_project, _vault_projects_dir, registry_locator, read_registry, write_registry, register_repo, unregister_repo, list_repos) for any pathlib.Path return annotation. The scan is driven by a new ROUTING_FUNCTIONS set, a _ROUTING_FILENAMES constant, a _routing_files() collector, and an updated _scan_source(names=…) kwarg. Two new tests in PathLeakGate cover the negative (a Path-returning routing function fails) and positive (non-routing helpers in routing files are not caught) cases.
check-process-seam-import-direction.sh — LC-8 block: A new scan block (LC-8) checks harness_memory.py and repo_registry.py for any import storage_vault or from storage_vault import statement and fails loudly if found — enforcing that a routing mechanism may import the seam but never a capability plugin. Three new tests in ImportDirectionGate: import-form failure, from-import-form failure, and positive test that non-routing files are not caught.
storage_conformance.py — routing checks: check_routing_repo_registry(make_backend) proves the repo_registry register/list/unregister cycle works on any conforming backend. Added to a new ROUTING_CHECKS tuple, surfaced as test_routing_repo_registry() on ConformanceSuite, and gated by a new include_routing=False kwarg on run_conformance(). Three new tests in RoutingConformanceReport in test_storage_conformance.py exercise run_conformance(include_routing=True) on both DeviceLocalBackend and VaultBackend.
Verification: 20/20 check-all.sh PASS.
ADR 0019 created (wiki/decisions/0019-v5-6-routing-plane-devaulting.md) recording the three-leg de-vaulting arc completion, all four locked design calls (LC-1/4/5/6/7/8), and the load-bearing assumptions with re-audit triggers. Decisions.md and decisions/_Sidebar.md updated.
wiki/designs/device-wide-architecture.md v1.0 lifecycle entry updated from pending to 2026-06-18 complete with all four task commit SHAs and ADR 0019 cross-link.
wiki/reference/Storage-Seam.md routing layer NOTE block updated from in progress to complete with ADR 0019 link.
wiki/explanation/Single-Repo-State-Mode.md "V5-6 pending" callout updated to "shipped 2026-06-18"; state_mode value description updated from "local" | "vault" to "local" | "backend" with backward-compat note.
Verification: check-wiki --strict green.
-
Out of scope: V5-6 narrative-shed (docs/prose identity rewrite — separate plan), PM slim (gated on crickets github-projects plugin),
auto_orchestration3-way split (V5-5),agentm_config/detect_project/vault_projectslug-resolution (already vault-agnostic), V5-7 full config model. -
Kernel stays the OS map (LC-1): routing mechanisms are not moved to a plugin — they remain kernel-resident, now speaking
Locators. - One-way import direction (LC-8): de-vaulted mechanisms may import the seam, never a capability plugin.
- Related: Storage-Seam reference, Single-Repo-State-Mode explanation, Memory-storage seam — V5-3 storage cutover.