Skip to content

Latest commit

 

History

History
405 lines (330 loc) · 87.5 KB

File metadata and controls

405 lines (330 loc) · 87.5 KB

Cairn — Implementation Plan & Progress Tracker

Status: Draft v0.1 · Owner: Zoran Vukmirica · Last updated: 2026-06-27 Scope: When and in what order we build Cairn — and the single source of truth for progress. Product scope: PRD.md. Architecture: LLD.md. Rules: ../CLAUDE.md.

This document is updated in the SAME PR that does the work (see §9). A row whose status lies is a bug. Produced via the team-of-agents model (CLAUDE.md §2): project-manager + workflow-orchestrator, synthesized.


1. At-a-glance dashboard

Keep this block accurate on every merge — it is the first thing anyone reads.

Field Value
Phase "Finish the project" (2026-06-29): hold lifted — building every remaining milestone in order, network backends feature-gated + emulator-tested in CI
Design docs ✅ PRD · ✅ LLD · ✅ ADR-0001..0013 · ✅ RFC-0001..0008 · 🟡 RFC-0010 (plugin sandbox + brokered host fns, Draft) · ✅ RFC-0012 (file open/view/edit — P1+P2+P3 landed) · 🟡 RFC-0013 (archive backend — P4+P5 landed)
Current milestone Foundation: feature-gated backends + lean/full CI split (ADR-0006, PR-0). Next: M3-4 credentials → M4 SSH
v0.1 target Deep on local + SSH + S3; functional GCS/Azure; Docker/K8s/AI/plugins behind feature flags
Milestones delivered M0, M1, M2, M3 (lib) ✅ · M5 abstraction + M7 core & plan→confirm UI + M8 runtime, WIT RFC & keybindings + M4 SFTP-mapping + M6 Docker- & K8s-mapping ✅ · M6-3 complete: Docker logs + interactive exec (RFC-0009 §2, bollard create_exec/start_exec/inspect_exec, 4 hermetic + 1 dind test) ✅ · M6-6 complete: K8s logs + exec + port-forward (RFC-0009 §1+§3) ✅ · M6-7 exec+port-forward TUI pane: ExecPane/PortForwardStatus overlays + session TEA loop wired (PR-4) 🟡 · cloud providers + live-transport + LLM HTTP providers + WASM component bridge ⏭ · v0.1.0 release prep 🟡
Work items ✅ / 🟡 / ☐ / ⛔ / ⏭ 34 / 20 / 0 / 0 / 19
Cross-platform CI green ✅ Linux · ✅ macOS · ✅ Windows
Long-pole items cloud/container/plugin backends (need live services + heavy SDKs)

Unblocked (2026-06-29). The "env-deferred" hold is lifted. Network backends now build behind non-default Cargo features (ADR-0006) so the default cross-platform CI stays lean/hermetic and the full TLS build runs Linux-only; live verification uses emulators (sshd/MinIO/Azurite/fake-gcs/kind/ dind) in a dedicated, env-guarded integration job. Build order: SSH → object-store → containers → AI HTTP + MCP → plugin finish → v0.1 release. The legacy note below describes the prior state.

Environment note (legacy). Items marked ⏭ env-deferred require live or emulated network services (SSH servers, MinIO/Azurite/fake-gcs, dind, kind) and/or very heavy SDKs (aws-sdk-s3, kube, bollard, wasmtime) that cannot be built and integration-tested in the current environment. Their designs are complete (LLD §8/§11, ADR-0003/0004) and the provider-agnostic cores that can be tested hermetically are implemented — the Vfs/transfer abstractions (M1/M2), the object-store trait + listing synthesis + Vfs mapping with a mock (M5-1/2), and the AI provider trait + closed tools + plan machine + injection defense (M7 core). Each deferred backend becomes a focused PR (RFC → impl → emulator integration job) once those services are available.

Burn-up note: Delivered M0–M3 (a working dual-pane local file manager + transfer engine + copy/move/delete with confirm + encrypted vault + broker + config), the M5 object-store abstraction (hermetic, mock-tested), and the M7 agentic AI core (provider/tools/plan→confirm/injection-defense). 11 library crates + the binary, ~140 hermetic tests, green cross-platform CI (PRs #5–#17). Remaining network/SDK backends are env-deferred (see note).

Status legend (used in every work-item table)

Symbol Meaning
Not started
🟡 In progress (branch active / PR open) — append the PR #id
Done (merged, gates passed, docs updated) — append the PR #id
Blocked (note blocked-by: <item>)
Deferred (post-v0.1 / descoped; link tracking issue)
🔁 Needs rework (a review/gate sent it back)

2. Phasing strategy — vertical depth first, breadth second

The PRD (§11) flags "all seven backends at launch" as the top risk. We reject building seven shallow backends in parallel and instead:

  1. Prove the abstraction once, end-to-end, before replicating it. Everything rests on the Vfs trait + TEA loop + transfer engine + broker boundary. Build one complete vertical slice (local: browse → operate → transfer, on screen, non-blocking) so a flaw in Vfs is found at M1 (cheap) not after six backends (catastrophic).
  2. Order backends by how hard they stress the abstraction — but promote the object store early because the LLD (§3.6) says it exercises the hardest paths (pagination, multipart, resume, server-copy): local → SSH/SFTP → S3 (deep) → GCS + Azure (cheap via contract tests) → Docker + Kubernetes (the exec/logs/port-forward action model).
  3. Security infrastructure precedes any credentialed backend. Vault + broker land at M3, before SSH/object stores: CLAUDE.md forbids plaintext secrets anywhere, and the broker boundary is a compile-time dependency-graph property far cheaper to establish up front than to retrofit.
  4. AI and plugins come last — both depend only on cairn-broker, both are untrusted, and both are only meaningful once there is real functionality to drive. (The AI lane may start in parallel against a MockBroker once the broker API is frozen — see §6.)

Depth-vs-breadth contract for v0.1: deep on local, SSH, S3; functional on GCS/Azure (parity via contract tests, lighter polish); Docker/K8s, AI, plugins behind feature flags, demoable but "preview". Matches the LLD feature-flag design (default build = local + SSH).

Milestone map

Milestone Theme Demoable outcome Primary crates
M0 Scaffolding & guardrails green CI on 3 OSes; empty app opens/quits; protections + milestones live workspace, cairn, CI
M1 The abstraction, proven (local slice) dual-pane browse of local FS; sort/filter/nav; 100k-entry dir scrolls smoothly, non-blocking cairn-types, cairn-core, cairn-vfs, cairn-backend-local, cairn-tui
M2 Operations & transfer engine copy/move/delete/mkdir/rename with a live progress queue (pause/resume/cancel) cairn-transfer, cairn-core, cairn-tui
M3 Secrets foundation (vault + broker) create/unlock encrypted vault; store/list a credential; broker mediates; nothing plaintext cairn-secrets, cairn-vault, cairn-broker, cairn-config
M4 First remote (SSH/SFTP) connect via vault key/agent; browse, edit-in-place, transfer local↔SSH cairn-backend-ssh
M5 Object storage (S3 → GCS/Azure) browse a huge bucket; multipart + resumable upload; cross-backend local↔S3; GCS/Azure parity cairn-backend-object, cairn-transfer
M6 Containers & clusters (action model) browse Docker/K8s; stream logs; exec; port-forward; copy pod→S3 cairn-backend-docker, cairn-backend-k8s
M7 Agentic AI (plan→confirm→execute) NL request → reviewed plan → confirmed execution via broker; cloud + Ollama cairn-ai, cairn-broker, cairn-tui
M8 Extensibility (WASM plugins) load a sandboxed plugin backend, capability-gated, indistinguishable from built-in cairn-plugin, cairn-plugin-sdk
v0.1 Release tagged cross-platform binaries; docs/install/changelog; §2 depth-vs-breadth met all

Each milestone is a GitHub Milestone; bold work items become GitHub Issues as they enter "Ready" (§9).


3. Work breakdown

Item IDs (M<n>-<k>) are stable. Each item is PR-sized (one logical change). "Docs" lists the minimum per CLAUDE.md §5 (every PR also needs a full description + CHANGELOG entry). Every functional item carries a test obligation. Append the PR #id to the Status cell.

M0 — Scaffolding & guardrails

ID Item (crate) Lead Deps Docs Exit criteria Status
M0-1 Cargo workspace + crates with strict acyclic deps; lints tuned for -D warnings; toolchain software-architect, rust-staff-engineer ADR-0001; rustdoc stubs workspace builds on 3 OSes; clippy -D warnings clean 🟡 lint tuning + bin/types #5; crates created lazily per milestone
M0-2 CI matrix (fmt, clippy -D, test, doc, deny) × Linux/macOS/Windows devops-engineer CI README; PR template; CODEOWNERS all checks green on 3 OSes; required for merge ✅ #1
M0-3 Branch protection, labels, GitHub Milestones M0–M8+v0.1, issue templates project-manager, devops-engineer M0-2 CONTRIBUTING main rejects direct push; milestones + labels exist ✅ #1, #4 (milestones)
M0-4 Binary edge: bootstrap, tracing, panic hook, --help/--version (redaction layer → M3) rust-staff-engineer M0-1 rustdoc launches, prints version, exits 0; CLI + panic-hook tests ✅ #5
M0-5 cairn-types: VfsPath (rejects ../control), Entry, Caps, ids rust-staff-engineer M0-1 rustdoc on all public items path parse/traversal-rejection tests green ✅ #5
M0-6 Test/QA harness: hermetic-offline policy (MockVfs lands with cairn-vfs in M1) qa-engineer M0-5 cargo test (no features) hermetic & offline 🟡 policy in force; MockVfs in M1

M1 — The abstraction, proven (local vertical slice)

ID Item (crate) Lead Deps Docs Exit criteria Status
M1-1 Vfs trait set (async_trait, streaming list, Read/Write handles), CapabilityProvider, VfsRegistry, MockVfs rust-staff-engineer, software-architect M0-5 confirm ADR-0001; rustdoc object-safe (Arc<dyn Vfs>); MockVfs read/write/list/remove tests pass ✅ #6
M1-2 RFC: local backend deep design (symlinks, perms, watch, Windows paths) rust-staff-engineer, technical-writer M1-1 RFC merged approved before M1-3 ✅ #8 (RFC-0001)
M1-3 cairn-backend-local: list, stat, read/write (ranged), mkdir/remove/rename/set_perms; correct Caps software-engineer, rust-staff-engineer M1-2 rustdoc; RFC-0001 unit + temp-dir tests green (6) ✅ #8
M1-4 cairn-core TEA skeleton: AppState, Msg/AppEvent/AppEffect, pure update() rust-staff-engineer, software-architect M0-5 confirm ADR-0001; rustdoc update() unit-tested pure (10 tests); no I/O in core ✅ #9
M1-5 Effect runner (binary): tokio rt, VfsRegistry, async list dispatch, bounded event_tx rust-staff-engineer M1-1, M1-4 rustdoc spawns listing tasks; results flow back as events (tested) ✅ #10
M1-6 cairn-tui render: ratatui dual panes, titles, status bar; pure over &AppState tui-engineer M1-4 rustdoc renders static AppState (TestBackend tests); zero I/O in render ✅ #10
M1-7 Input + keymap: blocking-thread reader, MC/vim default keymap (chords/presets later) tui-engineer M1-6 rustdoc nav/quit keys mapped (tests); input off the async runtime ✅ #10
M1-8 Wire-up: TEA event loop in the binary; browse local FS in both panes, nav in/out, non-blocking tui-engineer, software-engineer M1-3, M1-5, M1-7 rustdoc Demo: cairn opens cwd dual-pane, navigate, Tab, marks, quit ✅ #10
M1-9 Large-list virtualization + off-thread sort/filter, filter-as-you-type tui-engineer, performance-tuning-engineer M1-8 perf note 100k dir smooth; first page <100 ms ✅ filter-as-you-type (#39) + row virtualization (#42): only the on-screen window of rows is materialized (cursor-centred list_window), so a 100k-dir frame is O(viewport) not O(entries). Off-thread sort/filter (the O(n) in-thread filtered scan) is a later perf pass, not a feature gap
M1-10 Multi-select, sort (name/size/date/type), show/hide hidden software-engineer, tui-engineer M1-8 user docs selection + sort unit tests ✅ marks + name/size/modified/type sort (s) + hidden toggle (.) done (#35, #38): cursor follows its entry across re-sort, modes configurable via [ui.keybindings], status shown per pane

M2 — Operations & transfer engine

ID Item (crate) Lead Deps Docs Exit criteria Status
M2-1 RFC: transfer engine (queue, conflict, resume format, pause/cancel) storage-engineer, software-architect, technical-writer M1-1 RFC merged approved before M2-2 ✅ #11 (RFC-0002)
M2-2 cairn-transfer core: stream-through copy + dir-tree walk, server-copy fast path storage-engineer, rust-staff-engineer M2-1 rustdoc cross-backend copy via MockVfs (tests) ✅ #11
M2-3 Move = rename-or-(copy→verify→delete); conflict policy (Skip/Overwrite/Rename/NewerWins/Prompt) storage-engineer M2-2 rustdoc move + all conflict modes tested ✅ #11
M2-4 Pause/resume/cancel, retry w/ backoff+jitter, global+per-backend semaphores storage-engineer, network-engineer M2-2 rustdoc cancel mid-chunk (tested) 🟡 engine cancellation + UI cancel (Esc) wired (#41): the in-flight transfer's CancellationToken is held runtime-side and fired by a CancelTransfer effect; cancel reports a partial-changes warning. Pause/resume done (#53 engine + UI): run_transfer takes a watch::Receiver<bool> and blocks at the next check-point while paused (deadlock-safe clone + borrow_and_update + select!; cancel preempts a pause). The event loop owns a watch::Sender<bool> per transfer, driven by a SetTransferPaused effect; p toggles pause/resume (no-op when idle), status shows ⏸ paused and drops rate/ETA. Concurrent transfers done (id-keyed transfer collection #66, then bounded concurrency): up to N run at once ([transfers] concurrency, default 2, clamped ≥1) via per-transfer cancel/pause keyed by a stable TransferId; Esc/p act on all active, the rest queue and start as slots free; a panicked transfer task always releases its slot (drop-guard). Retry/backoff still deferred to M5; partial-outcome counts on cancel done (#43)
M2-5 Transfer queue UI overlay: per-item progress/speed/ETA, reorder, controls tui-engineer M2-2, M1-6 user docs Demo: copy a big tree local→local with live queue 🟡 live byte-progress (#40) + sequential FIFO queue (#45) + queue view overlay (#46, Ctrl-T) + throughput rate (#48) + queue controls (#50) + reorder (#51, Shift-K/J): a transfer issued while one runs is queued and auto-started; status shows bytes, rate, queue depth; the queue view can reorder or drop a specific pending transfer. progress %/ETA done (#52, best-effort source pre-scan → X/Y (Z%) + ETA, degrades gracefully). Pause/resume + concurrent transfers done (#53/#56/#66 + this): status aggregates N active (⇅ 2 active · … (+K queued)), the Ctrl-T view lists every active transfer + pending; idle status messages now render. MC-style auto-opening progress dialog done (this PR): the dialog (retitled " Transfer ") now auto-opens on Copy/Move (unless another overlay already has the user's attention) and renders each active transfer as label + a text progress bar (/, --% when the total is unknown) + a byte/rate/ETA line, above the existing pending list. b backgrounds it (transfer keeps running, status line summary continues); Ctrl-T restores it; p pauses/resumes from inside it (previously swallowed by the overlay); Esc inside it now aborts every active transfer (matching the no-overlay binding) instead of just closing, degrading to a plain close when only pending (not yet started) items remain; it auto-closes once the last transfer finishes and nothing is queued behind it. The dialog itself is deliberately excluded from the overlays that block queue draining/conflict prompts, so queued transfers keep starting and a same-transfer overwrite conflict still surfaces its prompt over it. Per-transfer (not just all) pause/cancel from the overlay = follow-up
M2-6 Operation keys: copy (F5/c), move (F6/m), delete (F8/d) wired to engine tui-engineer, software-engineer M2-2 user docs copy/move/delete flows work; delete confirms ✅ copy/move/delete + mkdir (F7) + rename (F2) done (#36); overwrite-confirm (#44): interactive copy/move asks before clobbering existing destinations (no silent overwrite), rename refuses overwrite
M2-7 Confirm-dialog + overlay input interception (foundation for plan→confirm) tui-engineer, security-engineer M2-6 rustdoc destructive op cannot dispatch without confirm (tested) ✅ #12

M3 — Secrets foundation (vault + broker) · security-review required on every item

ID Item (crate) Lead Deps Docs Exit criteria Status
M3-1 cairn-secrets: SecretString/Box, Zeroizing, redaction layer (AWS/bearer/SAS/PEM/JWT) security-engineer, rust-staff-engineer M0-4 rustdoc; threat-model note no Debug/Serialize leak (compile test); redaction tests ✅ #13
M3-2 Vault at rest (ADR-0002): XChaCha20-Poly1305, header-AAD, encrypted index, per-entry DEKs, postcard, atomic write+.bak+lock security-engineer M3-1 confirm ADR-0002; format spec seal/open round-trip; tamper/rollback tests; refuse unknown version ✅ #13
M3-3 Key hierarchy + unlock: KEK in OS keychain (keyring), Argon2id fallback, auto-lock security-engineer M3-2 rustdoc; SECURITY.md update keychain + passphrase paths tested; auto-lock zeroizes 🟡 UnlockProvider landed (PR-C): trait (passphrase/store/persists) + open_with; PassphraseUnlockProvider (headless/prompt fallback), KeychainUnlockProvider over keyring 4.x behind the non-default keychain feature (Secret Service/Keychain/Cred-Manager, service cairn/account vault; NoEntryUnlockError::NotFound, secret-free Backend), MockUnlockProvider (test-utils). Keychain path tested hermetically vs keyring-core's mock store (no real keychain/dbus). Zeroizing passphrase unlock from M3-2. Remaining: auto-lock (zeroize on idle) + TUI unlock overlay (M3-7); SECURITY.md threat-model note for keychain trade-off
M3-4 Credential model: typed CredentialSecret + delegation variants; TokenCache security-engineer M3-2 rustdoc variants seal; delegation stores no secret (test) 🟡 RFC-0008 + broker-api split landed (PR-A): cairn-broker-api (CredentialDirectory/CredentialInfo), cairn-ai no longer reaches cairn-vault (compile-time, enforced by a cargo metadata dep-closure test); CredentialId moved to cairn-types. PR-B done: typed CredentialSecret (SSH variant: password/private-key/agent) sealed via a pub(crate) zeroizing wire-mirror, no Debug/Serialize on the public type (compile_fail-guarded), Broker::resolve→typed secret, CredentialInfo carries CredentialShape, vault format v2. Next: PR-C keychain UnlockProvider+auto-lock, PR-D vault-unlock TUI; other backends' variants + TokenCache land with M5/M6
M3-5 cairn-broker: authorize/execute split, capability+scope, resolve CredentialId→secret inside execute, journal Actor security-engineer, software-architect M3-3, M3-4 confirm ADR-0002; rustdoc secret never leaves execute; no API returns a secret (compile + review) ✅ #14 (broker: resolve + journal; full authorize/confirm in M7)
M3-6 cairn-config: TOML, ConnectionProfile (ref only, type-enforced no-secret), state dir, schema+migration software-engineer M0-5 rustdoc; config docs config↔vault boundary compile-enforced; migration round-trip ✅ #14
M3-7 Vault TUI: create/unlock, list (labels only), add credential; on-screen redaction tui-engineer, security-engineer M3-3, M2-7 user docs Demo: create vault, store cred, relock — nothing plaintext 🟡 unlock flow landed: Overlay::VaultUnlock (Ctrl-U / vault_unlock) with a no-echo masked field (MaskedInput: redacted Debug, zeroized on drop/clear; the typed secret never enters AppState Debug or logs). Submitting emits AppEffect::UnlockVault { passphrase: SecretString }; the runtime opens the vault off the render path (Vault::open via spawn_blocking), broker.unlock(vault), then retries the credential-bearing connections deferred at startup because the vault was locked, adding the successes to the switcher and reporting the count. Wrong passphrase / missing vault keep the overlay open with a secret-free, retryable error. The Arc<Broker> + deferred profiles now live in the runtime VaultContext (not in AppState). New [vault] path config (default …/cairn/vault.cvlt). Reducer + render + effect tests (open/cancel, no-echo invariant, wrong-passphrase-retry, success transition vs a temp fast_for_tests vault; deferral→unlock→retry under all-backends). Remaining follow-ups: vault creation UI (landed in RFC-0011 P4.5, feat/conn-p45-vault-create) + add/list-credential UI, and auto-lock-on-idle (zeroize the broker after inactivity)

M4 — First remote (SSH/SFTP)

ID Item (crate) Lead Deps Docs Exit criteria Status
M4-1 RFC: SSH/SFTP backend (auth chain, bastion/jump, keepalive, !Send proxy strategy) network-engineer, technical-writer M1-1, M3-5 RFC merged approved before M4-2; assert_send plan documented ✅ #19 (RFC-0003)
M4-2 cairn-backend-ssh: connect (key/agent via broker), list/stat/read/write (streaming, RANDOM_READ) network-engineer, rust-staff-engineer M4-1 rustdoc; backend README against SSH image (CI): browse + read/write; auth via broker only ✅ #19 SFTP→Vfs mapping + russh-sftp adapter (mock-tested); live connect done: ssh_connect (russh, ssh feature) — TCP→handshake→host-key (Strict/AcceptNew, changed-key always rejected; unit-tested)→auth (password/key+passphrase/agent, consumes CredentialSecret::Ssh)→sftp subsystem→SftpVfs. assert_send_sync guard for the russh !Send risk. Live full round-trip via the sshd integration job (M4-6)
M4-3 SSH mutations + exec (remote grep→SEARCH_CONTENT), edit-in-place save-back network-engineer M4-2 rustdoc rename/delete/mkdir; exec returns Stream; edit→save round-trip 🟡 rename/remove/mkdir done; exec + edit-save + live transport deferred
M4-4 Transport resilience: timeouts, keepalive, retry/backoff, bastion/proxy-jump chain network-engineer M4-2 rustdoc; resilience note simulated stall fails+retries (no hang); jump-host test 🟡 retry/backoff core (#31): cairn_vfs::retry + RetryPolicy (capped exponential backoff, retries only VfsError::is_retryable, mutations excluded), unit-tested against a flaky op; adopted on the SFTP adapter's idempotent stat. Keepalive, bastion/jump-host chain, and live timeouts = integration step
M4-5 Connection switcher UI + new-SSH flow, profile persistence (ref-only) tui-engineer, software-engineer M4-2, M3-7 user docs Demo: Ctrl-K → connect → browse → transfer local↔SSH 🟡 switcher UI (#28): Ctrl-O overlay lists registered connections (built-in local roots + scheme="local" config profiles) and re-points the active pane; reducer + render mock-tested. Broker-backed connection opener landed (first integration slice): crate::connect::ConnectionOpener resolves a profile's secret_ref via the Broker, builds the scheme's *ConnectParams from endpoint fields, and dispatches to ssh_connect/s3_connect/gcs_connect/azure_connectArc<dyn Vfs>; binary feature umbrellas ssh/s3/gcs/azure (+ cloud/all-backends) keep the default build lean (ADR-0006), and a scheme whose feature is off errors "not built in" with no I/O. Startup opens credential-bearing config profiles through it (Actor::User), skipping un-openable ones with a warning. Hermetic tests: dispatch, profile→params, and the resolve→connect path (S3 profile + SSH credential → Auth pre-network). RFC-0011 P1 landed: ConnectionCoordinator + BuiltinLocalProvider + SavedProfileProvider; descriptor side-map established; DeferredConnection; id-assignment contract tests. RFC-0011 P2 landed (ADR-0007): lazy on-select connection opening. Profile targets enumerated as NeedsOpen/NeedsVault at startup — no open attempt. AppEffect::OpenConnection / AppEvent::ConnectionOpened TEA round-trip: the effect runner looks up the descriptor, opens async (spawned), navigates on success. Vault-unlock reconciliation: flip all NeedsVault→NeedsOpen, auto-open only the triggering conn. DeferredConnection retired (always empty). run_vault_unlock_effect returns Result<(), String>. Stable id reuse via prior_descriptors map. All CI checks green (lean + all-features clippy, workspace tests, rustdoc). Remaining: new-remote-connection TUI (create/trigger a profile). RFC-0011 P3 landed (branch feat/conn-p3-discovery): Docker socket auto-discovery (DockerProvider, 3 candidates probed concurrently with 500 ms timeout via bollard, rootless + Podman sockets), Kubeconfig + in-cluster discovery (KubeconfigProvider, spawn_blocking for FS/YAML, SA-token existence check), join_all coordinator, DiscoveryConfig in cairn-config (docker, kubernetes, hidden, pinned), hidden/pinned overlays, [auto] badge in TUI, feature-gated (docker/k8s) with clean lean build, all CI checks green (lean + all-features clippy, workspace tests, rustdoc). RFC-0011 P4 landed (branch feat/conn-p4-form): in-app connection form (add/edit/delete). Ctrl-N opens scheme picker → field form; e edits selected Profile; d deletes. cairn_core::forms::{FieldSpec,ProfileData,scheme_fields,KNOWN_SCHEMES} provides a dep-free mirror of ConnectionProfile. AppEffect::SaveConnection/DeleteConnection + AppEvent::ConnectionSaved/ConnectionDeleted complete the TEA round-trip. run_save_connection_effect/run_delete_connection_effect in cairn/src/app.rs load/save cairn.toml and rebuild the switcher via register_connections. AppState::saved_profiles populated at startup. Credential capture deferred to P5 (one-line hint shown in form). All CI checks green. RFC-0011 P4.5 landed (branch feat/conn-p45-vault-create): vault-create-from-UI security gate. Overlay::VaultCreate (two MaskedInput fields, Tab-cycling focus, [Ctrl-R] Remember keychain toggle); submit path validates min-length (8 chars), compares fields without exposing bytes, emits AppEffect::CreateVault { passphrase: SecretString, remember }. Argon2id in spawn_blocking; vault written atomically at 0600; broker unlocked + NeedsVault connections flipped to NeedsOpen on success; AppState::vault_file_exists drives VaultCreate-vs-VaultUnlock branch. Debug never reveals passphrase; error strings are value-free. 17 new hermetic tests (field editing, mismatch, min-length, double-submit guard, pending-conn routing, 0600 perms, Debug redaction). All CI checks green (lean + all-features clippy, workspace tests, rustdoc). RFC-0011 P5 landed (branch feat/conn-p5-credentials): reference-first credential provisioning. Connection form gains two new stages: CredentialMethodPicker (delegation-first ordering: SSH agent / key-file reference / inline PEM / password, AWS default chain / named profile / static key, GCP ADC, Azure AD) and CredentialFields (secret-bearing methods only). MaskedInputCredentialDraft (in AppEffect::ProvisionAndSaveConnection) → CredentialSecret (assembled at binary edge only). Vault gating: non-delegation methods gate on vault availability; pending_save carried in VaultUnlock/VaultCreate overlay for automatic completion after unlock. OS-source detection (AppEffect::DetectOsSources) at startup defaults the picker. Edit-mode prepends KeepExisting (preserves vault ref). SshCredential::PrivateKeyFile handler in cairn-backend-ssh reads key at connect time. GCS service-account JSON and Azure non-delegation methods present-but-deferred. cairn-core isolation verified: no transitive dep on cairn-vault/cairn-broker. MaskedInput::Clone → empty footgun documented; validation uses immutable borrow. 14 new hermetic P5 tests + 3 updated P4 tests. ADR-0009 added. All CI checks green. RFC-0011 P6 landed (branch feat/conn-p6-polish): test-connection + pin/hide. Switcher gains t (test — AppEffect::TestConnection/AppEvent::ConnectionTested, reuses run_open_connection_effect's per-scheme construction but never registers the result; ChoiceStatus::NeedsVault reports "needs unlock" purely from state, no I/O), P/H (pin/hide — AppEffect::SetConnectionPinned/SetConnectionHidden, persisted to [discovery].pinned/.hidden), and S (this-session "show hidden" toggle, Overlay::Connections::show_hidden). ConnectionChoice gains pinned/hidden fields; hiding now marks an entry instead of dropping it from enumeration (recoverable via S). New cairn_core::visible_connection_indices is the single filter shared by the reducer and renderer so cursor/selection and the drawn list can never disagree. cairn_config::Config::save now writes atomically (temp file + rename, mirroring cairn-vault); tempfile promoted from a dev-dependency to a real one. Edit-endpoint-fields (P4) and remove-with-credential-cleanup (P5) were already done, not re-scoped. Deferred: discovered-entry rename (needs a new [discovery].aliases field + prompt overlay — larger than fits cleanly here) and an automatic background reachability sweep (only the "don't re-probe an already-Ready/in-flight entry" debounce landed). [discovery].pinned/.hidden are the existing P3 fields, not new — P6 is the switcher UI writing to them. Post-review gate fixes: pinning now re-points the switcher cursor at the pinned entry's new (post-reorder) position instead of silently retargeting the highlight onto whatever slid into the old slot; the hint line leads with [Esc] Close (dropping lower-priority tokens under width constraints, never mid-token) so it's never truncated off a narrow terminal; every cairn.toml load→mutate→save cycle (save/delete/pin/hide) is now serialized behind a process-wide CONFIG_WRITE_LOCK so two rapid-fire toggles can't clobber each other; the pin badge is [pinned] (bracket tag, not emoji) for consistency with [auto]/[hidden] and terminals without color-emoji support. All CI checks green (lean + all-features clippy, workspace tests, rustdoc).
M4-6 Cross-backend transfer validation local↔SSH qa-engineer, storage-engineer M4-2, M2-2 test docs copy/move both directions; integrity verified ⏭ env-deferred (live SSH server + russh SDK)
M4-7 RFC: file open/view/edit — read-only pager (P1), in-place editor + remote writeback (P2–P3) tui-engineer, software-architect, storage-engineer M1-1 RFC-0012; rustdoc; ADR-0011 P1: F3/Enter-to-view opens a read-only pager, text or hex mode. P2: F4/Enter-on-text edits local files via $EDITOR. P3: the same edit flow works for remote files via download/writeback with conflict detection RFC-0012 P1 landed: Action::View (F3) and Action::Enter on a non-directory entry (File/Symlink) now route to a new read-only pager. Reducer stays pure: AppEffect::SniffFile reads a bounded ~8 KiB prefix and classifies it (detect_file_kind, NUL-byte heuristic) off the render path; AppEvent::FileSniffed seeds Overlay::Pager with the prefetch for an instant first frame, then AppEffect::OpenPager streams the rest (run_pager_effect, 64 KiB chunks, CancellationToken-keyed like the log viewer). Overlay::Pager is modeled on Overlay::LogViewer but never evicts from the front — a static file shows its first N bytes up to PAGER_MAX_BYTES (~8 MiB), not a live tail's most recent N; hitting the cap sets PagerStatus::Truncated and proactively cancels the stream. PagerMode::Hex stores raw 16-byte rows via a lossless byte↔char mapping (char::from(byte)) so binary content round-trips exactly; render_pager/format_hex_row do the `offset

| M4-8 | RFC: archive backend — browse a local .tar/.zip as a directory (P4); compressed tar (P5) | storage-engineer, security-review | M4-7 | RFC-0013; rustdoc; ADR-0012 | Enter on a recognized (magic-byte) tar/zip mounts it read-only; .. at its root leaves back to the origin; copy-out works | ✅ RFC-0013 P4 landed: new cairn-backend-archive crate — ArchiveVfs (Caps::LIST\|READ\|RANDOM_READ only; every mutation stays the Vfs default Unsupported) over an object-safe ArchiveOps (dyn, not generic like SftpOps, since tar-vs-zip is chosen at runtime from magic bytes, not compile time), implemented once each by TarOps (one sequential entries_with_seek scan recording raw_file_position + size, then seek+capped-read per member) and ZipOps (central-directory parse once, by_index decode per read). A shared IndexBuilder/ArchiveIndex synthesizes missing ancestor directories from the flat member list so list_children/entry_meta never re-derive the tree per call. Scheme::Archive added (cairn-types); gated behind the binary's archive feature (cairn's all-backends umbrella), lean-build fallback reports "not built in" cleanly. Mount model (ADR-0012): an archive mount is a real, ephemeral connection minted at a disjoint id range (ARCHIVE_CONN_ID_BASE) and inserted into the VfsRegistry, not a transient overlay. detect_file_kind (cairn-core) gains FileKind::Archive(ArchiveFormat), detected purely by magic bytes within the existing ~8 KiB sniff prefix (zip PK\x03\x04 @ 0, tar ustar @ 257) — never a file extension. AppEvent::FileSniffed's new Archive arm emits AppEffect::MountArchive; the runner resolves Vfs::local_path first (None — e.g. a .zip on S3 — refuses cleanly with a "copy to a local pane" message; remote staging is a follow-up), then ArchiveVfs::open (indexing runs in spawn_blocking), mints the id, registers it, and reports AppEvent::ArchiveMounted. New PaneState::mount_stack: Vec<MountFrame> records the pre-mount (conn, cwd); leave_dir pops it when cwd.parent() is None and the stack is non-empty, restoring the origin (generalizes to nested mounts). Security guards (all with a dedicated hermetic test): bounded per-member (64 MiB) + cumulative per-session (512 MiB) decoded-byte caps regardless of declared size (archive bomb); zip compression-ratio guard (>100:1 above a 1 MiB floor) checked from central-directory metadata before decompression (real deflate-bomb test); entry-count cap (100k, checked against every raw header seen — shared check_entry_count helper so tar/zip can't drift); path validation rejects absolute/UNC/drive-letter/../NUL/overlong names (validate_member_name, delegates traversal+control-char rejection to VfsPath::parse); symlink/hardlink members are inert (EntryKind::Symlink, target shown best-effort, never followed — open_read on one returns Unsupported); device/FIFO/socket entries and setuid/setgid/sticky members are skipped, never materialized; all size/offset arithmetic uses min/saturating_* (a header claiming u64::MAX cannot over-allocate or overflow, exercised with a genuinely truncated tar). No nested-archive auto-descent (shown as a plain file). 40+ new hermetic tests across cairn-backend-archive + cairn-core. Crates pinned via Context7-verified current versions: tar = "0.4.46" (MIT/Apache-2.0, the crate Cargo itself uses), zip = "8.6" (zip2 fork, MIT, default-features = false, features = ["deflate"] to stay lean). All CI checks green (lean + all-features clippy, workspace tests, rustdoc). Deferred (at P4): compressed tar, remote-archive staging, nested-archive browsing, mount refcount/unmount. ✅ RFC-0013 P5 landed (feat/archive-compressed-tar): compressed tar (.tgz/.tar.gz, .tbz2/.tar.bz2, .tzst/.tar.zst) now mounts and browses exactly like a plain tar. sniff_format gains a third outcome, Format::CompressedTar(Compression), from four magic-byte checks (compressed_tar::sniff; gzip 1f 8b, bzip2 "BZh", xz fd 37 7a 58 5a 00, zstd 28 b5 2f fd), checked only after the zip/plain-tar structural signatures have been ruled out. Design: decompress-once-to-temp, then index as a plain tartar_backend's entries_with_seek "skip via Seek" trick has nothing to seek to in a compressed byte stream, so re-decompressing per read would be O(n); instead the whole file is decoded exactly once, up front, into a private (owner-only, RAII-deleted, preferring $XDG_RUNTIME_DIR when available) temp file, which the unmodified TarOps::build then indexes — every P4 tar guard (path validation, symlink inertness, special/setuid skip, entry-count cap, checked arithmetic) applies for free, zero duplicated logic. New CompressedTarOps (ArchiveOps impl wrapping TarOps + the temp-file RAII handle). Decompression-bomb guards, both enforced incrementally (per decoder-write, never after the fact) via a shared CappedWriter: an absolute decoded-byte cap (ARCHIVE_MAX_DECOMPRESSED_BYTES — numerically equal to, but an independent budget from, the pre-existing ARCHIVE_SESSION_BYTE_CAP; a mount's worst-case combined footprint is ~1 GiB, not ~512 MiB) and a compression-ratio guard (compression_ratio_is_bomb, generalized from P4's zip-only zip_ratio_is_bomb so both share one threshold). Multi-stream/multi-frame guard: gzip decodes via MultiGzDecoder (correctly reconstructs concatenated members, e.g. bgzip output); bzip2-rs/ruzstd don't continue past their first stream/frame, so contains_magic_after_start scans the original file's own bytes for a second magic occurrence and refuses (compressed_tar_multi_stream) rather than opening a silently-truncated archive — deliberately independent of what a decoder reports consuming, since bzip2_rs::DecoderReader was found (during gate-fix remediation, see below) to read its entire input regardless of stream count, which would defeat a naive "bytes consumed" check. Decoder selection — pure-Rust for the three codecs actually decoded, xz dropped, recorded in ADR-0013: gzip via flate2 (rust_backend/miniz_oxide, already transitively present), zstd via ruzstd = "0.8" (actively maintained, tens of millions of downloads — note: contains internal unsafe, "pure-Rust" means no C/FFI, not "no unsafe"), bzip2 via bzip2-rs = "0.1" (chosen over the ~1000x-more-downloaded C-FFI bzip2 crate specifically for the memory-safety benefit of a pure-Rust decoder parsing untrusted bytes — the sharpest trade-off, flagged explicitly for security-review). xz/.txz is recognized but NOT decoded: the initial commit shipped lzma-rs, but the bug-bot/security-review gate traced its LZMA2 path and found it is not memory-bounded against a decompression bomb (its dictionary buffer flushes to the output sink only on stream-controlled reset points, deferrable indefinitely, so a tiny .txz could accumulate multiple GiB in process RAM before the output-byte cap ever sees a byte) — an unconditional OOM vector, not the bounded residual risk the initial ADR-0013 draft believed. lzma-rs was removed; Compression::Xz/its sniff magic are kept so .txz is recognized and refused with a clear, typed compressed_tar_xz_unsupported error rather than decoded or misclassified. tempfile moves from dev-only to a real dependency; banzai (pure-Rust bzip2 encoder) added as a dev-dependency solely to build real bzip2 test fixtures (bzip2-rs is decode-only). 22 new hermetic tests in compressed_tar (real decode+index round-trip per decoded codec, random-access re-read after one decode, decode-error-not-panic, a real high-ratio bomb fixture per decoded codec — gzip/bzip2 via their own encoders, zstd via a hand-built minimal frame per RFC 8878 since no pure-Rust zstd encoder exists in the tree — the absolute-cap-independent-of-ratio case, xz-recognized-not-decoded, the temp-file permission/RAII-cleanup test, gzip multi-member reconstruction, and the bzip2/zstd multi-stream refusal regression tests) + 3 new vfs tests (magic-byte sniff for all four formats; an end-to-end real .tar.gz mount; an end-to-end .txz friendly-refusal test). All CI checks green (lean + all-features clippy, workspace + all-features tests, rustdoc with RUSTDOCFLAGS="-D warnings", cargo deny check licenses bans advisories). Deferred, tracked as follow-ups: xz/lzma decoding (needs a memory-bounded decoder or a security-review-gated C liblzma binding with memlimit), full multi-stream/multi-frame bzip2/zstd decode (currently detected-and-refused, not decoded), a dedicated cargo fuzz target for the bzip2/zstd decode paths, remote-archive staging, nested-archive browsing, mount refcount/unmount. |

M5 — Object storage (S3 → GCS/Azure)

ID Item (crate) Lead Deps Docs Exit criteria Status
M5-1 ObjectStore trait + ObjectStoreVfs wrapper (ADR-0003) storage-engineer, software-architect M1-1 confirm ADR-0003; rustdoc trait+wrapper compile; MockObjectStore harness ✅ #17 (ObjectStore trait, MockObjectStore, prefix→Dir merge, ObjectStoreVfs; 8 tests)
M5-2 Object-store contract test suite (all three providers) qa-engineer, storage-engineer M5-1 TESTING.md runs against MockObjectStore + MinIO; gates each provider 🟡 MockObjectStore + listing tests done; MinIO integration job landed (integration.yml, env-guarded CAIRN_IT_S3: full put→list→head→get(+range)→copy→delete round-trip vs a MinIO service). Multi-provider (GCS/Azure) emulator suite still needs those SDKs
M5-3 S3 provider (aws-sdk-s3): list (continuation, common-prefixes), head, ranged GET storage-engineer M5-1 rustdoc against MinIO: list via bounded window; contract pass S3ObjectStore/s3_connect behind the s3 feature (list w/ continuation+common-prefixes, head, ranged GET, put, delete, server-copy); typed CredentialSecret::Aws (static/profile/default-chain); MinIO endpoint + path-style. Multipart/resume = M5-4/5
M5-4 S3 multipart upload (16 MiB threshold, 8 MiB parts, concurrent, CRC32C), abort-on-drop storage-engineer M5-3 rustdoc >16 MiB multipart; abort leaves no orphans (test) ⏭ env-deferred (S3/GCS/Azure SDKs + emulators)
M5-5 S3 resume (part-state, list_parts reconcile, SourceChanged), server-copy storage-engineer M5-4, M2-1 rustdoc; resume spec kill+resume completes; same-provider fast-path ⏭ env-deferred (S3/GCS/Azure SDKs + emulators)
M5-6 S3 integrity/consistency: conditional writes (412→Conflict), VerifyPolicy, broker creds (ArcSwap) storage-engineer, security-engineer M5-3, M3-5 rustdoc 412→conflict-policy test; presigned URLs redacted ⏭ env-deferred (S3/GCS/Azure SDKs + emulators)
M5-7 Cross-backend local↔S3 and SSH↔S3 (checksum verify) qa-engineer, storage-engineer M5-4, M4-2 test docs Demo: copy local→S3, SSH→S3 with verification ⏭ env-deferred (S3/GCS/Azure SDKs + emulators)
M5-8 GCS provider (google-cloud-storage, crc32c, generation preconds, ADC/SA via broker) storage-engineer M5-2, M5-6 rustdoc contract green vs fake-gcs-server GcsObjectStore/gcs_connect behind the gcs feature (list+page-token, head, ranged read, write, delete, server-copy via rewrite; data-plane HTTP Storage + control-plane gRPC StorageControl); typed CredentialSecret::Gcp (service-account JSON / ADC); endpoint+anonymous for emulators. Live fake-gcs job deferred (control plane is gRPC; HTTP emulators don't cover it) — adapter type-checked, ObjectStoreVfs mapping proven by the S3 MinIO job
M5-9 Azure provider (azure_storage_blobs, per-block MD5, shared-key/SAS/AAD via broker) storage-engineer M5-2, M5-6 rustdoc contract green vs Azurite AzureObjectStore/azure_connect behind the azure feature (list+continuation, head, ranged read, write, delete, server-copy); typed CredentialSecret::Azure (shared-key / SAS; AAD reserved); 0.21 SDK on rustls; live Azurite job (integration.yml, CAIRN_IT_AZURE). AAD adapter wiring + per-block MD5 = follow-ups
M5-10 Backend-aware UX: tier badges, versioned soft-delete honesty, archive-tier cost confirm tui-engineer, ux-engineer M5-3 user docs Glacier read raises cost confirm; delete-marker messaging clear ⏭ env-deferred (S3/GCS/Azure SDKs + emulators)

M6 — Containers & clusters (action model)

ID Item (crate) Lead Deps Docs Exit criteria Status
M6-1 RFC: Docker backend (fs via archive API, image layers, exec/logs) container-backend-engineer, technical-writer M1-1 RFC merged approved before M6-2 ✅ (RFC-0004, #20)
M6-2 cairn-backend-docker (bollard): list containers+images; browse container fs (tar); image layers RO container-backend-engineer M6-1, M3-5 rustdoc; backend README against dind: browse fs; copy in/out live fs browsing landed: BollardDocker lists containers+images AND browses a running container's fs — list_dir/stat/read over the Engine archive API (download_from_container → tar parse), 404→NotFound; dind integration job (CAIRN_IT_DOCKER). Image content browsing landed (ADR-0010, branch feat/docker-image-browse): /images/<tag>/… browses the image's merged rootfs via an ephemeral (docker create, never started) container reused through the same list_dir/stat/read archive path, id-keyed single-flight cache (tokio::sync::OnceCell, tag/raw-id aliases share one container — digest aliases don't yet, tracked follow-up — failed creates auto-retry), idle-TTL reaper (5 min) + label+age crash-safety sweep (30 min, cairn.role=image-browse-ephemeral) as the cleanup safety net. EntryExt::Image.layers now sourced from inspect_image's RootFS.Layers (was hardcoded 0) via a cheap resolve_image_id lookup kept separate from the N+1-inspect_image list_images so per-navigation-step cost stays O(1); repo tags containing / (namespaced/registry images — the common non-official-library case) are listed and browsed by image id since a VfsPath segment can't hold /; the crash-safety sweep skips containers this process's own registry still tracks as live so it only reaps true orphans. 7 new hermetic tests + 3 pure-function reaper-threshold tests + 1 dind test. Post-review fixes from bug-bot/code-review gates (round 1): unified error mapping, .expect()→soft-fail on mutex locks, dropped the risky entrypoint/cmd override (image config now stands unmodified). Round-2 gate fixes: idle reaper now evicts-then-removes with a re-check-under-lock (cid + still-idle) immediately before committing, closing the TOCTOU where a resumed browse's container got yanked mid-fetch (evict_if_still_idle, regression-tested); images with no CMD/ENTRYPOINT now fail with a clear image_no_command error instead of an opaque one; the crash-safety sweep starts on real connection-open (BollardDocker::ensure_background_tasks, called from open_docker_socket/ConnectionOpener::open_docker) rather than only on first image browse; both reaper intervals use MissedTickBehavior::Skip; ADR-0010 corrected to accurately describe the (accepted, documented) cross-instance >30-min-sweep limitation and the digest-matching gap rather than overclaiming full protection. 12 new hermetic tests covering all of the above. Graceful-shutdown teardown hook, cross-instance last-access-label re-stamp, repo_digests matching, and true per-layer OCI view are follow-ups. Copy-in = still a follow-up
M6-3 Docker actions: exec, logs (Stream), start/stop container-backend-engineer M6-2 rustdoc exec interactive stream; logs follow logs + exec landed: DockerVfs::invoke("logs") returns ActionOutcome::Stream (bollard demuxed frames, follow/tail from ActionCtx::Logs). DockerVfs::invoke("exec") with ActionCtx::Exec { argv, tty } returns ActionOutcome::Session(SessionHandle) via bollard create_execstart_exec → relay tasks → inspect_exec. ContainerOps::exec seam on the trait; MockDocker::exec (echo-style, resize-drain, cancel→Ok(-1), stdin-close→Ok(0)); BollardDocker::exec (stdin relay AsyncWrite task, resize relay resize_exec task, main tokio::select! loop over output stream, inspect_exec for exit code). 4 new hermetic unit tests (invalid_ctx, non-tty echo round-trip, tty resize+cancel, unknown-container NotFound) + 1 dind integration test (docker_container_exec_round_trip: echo CAIRN_EXEC_MARKER, assert marker in stdout, assert exit code 0, CAIRN_IT_DOCKER-gated). Workspace clippy + rustdoc clean. Remaining: start/stop (deferred; not in RFC-0009 scope).
M6-4 RFC: Kubernetes backend (ctx→ns→pod→container→fs, exec/cp/logs/port-forward, auth) kube-staff-engineer, technical-writer M1-1 RFC merged approved before M6-5 ✅ (RFC-0005, #21)
M6-5 cairn-backend-k8s (kube): navigable tree, watch strategy, kubeconfig/exec-plugin auth via broker kube-staff-engineer M6-4, M3-5 rustdoc; backend README against kind: browse ns/pods; multi-context live tree landed: kube-rs 4.0 adapter behind the k8s feature — list_contexts/list_namespaces/list_pods/list_containers (rustls, no OpenSSL); 403→Forbidden, 404→NotFound; kind integration job (CAIRN_IT_K8S). In-container fs (exec-tar), watch, and exec-plugin/broker auth = follow-ups (M6-5b/M6-6)
M6-5b In-container filesystem browsing via tar-over-exec kube-staff-engineer M6-5 rustdoc list_dir/stat/read on a kind pod; exec_unavailable on no-tar container landed: list_dir/stat/read implemented over exec (tar cf - -C <path> . / tar cf - -C <parent> <basename>); pure tar helpers in tar_exec module (11 hermetic unit tests); exec_unavailable returned when container lacks tar; kind integration test exercises /, /etc, /etc/hostname stat+read with graceful skip on no-tar; tar dep added behind k8s feature
M6-6 K8s streaming: logs(follow) Stream, exec (tty Session), port-forward kube-staff-engineer M6-5b rustdoc; RFC-0009 cp out of pod completes (no stall); port-forward holds complete: logs + exec + port-forward all landed. KubeVfs::invoke("logs")ActionOutcome::Stream; KubeVfs::invoke("exec") with ActionCtx::ExecActionOutcome::Session(SessionHandle) via Api::<Pod>::exec + AttachParams (relay tasks: stdin/stdout/stderr + TTY resize); KubeVfs::invoke("port-forward") with ActionCtx::PortForwardActionOutcome::Session(SessionHandle) via Api::<Pod>::portforward + TcpListener accept loop (one Portforwarder per connection, copy_bidirectional relay, ephemeral-port support, cancel→Ok(0)). SessionHandle refined per RFC-0009 §1: done: Result<i32, VfsError>, resize field, SessionHandle::new() constructor. MockKube implements echo-style exec + echo-server port-forward (real TcpListener). 9 new hermetic unit tests (4 exec + 4 port-forward + 1 was pre-existing); 2 kind integration tests (k8s_exec_non_tty_round_trip + k8s_port_forward_binds_and_accepts_connection, CAIRN_IT_K8S-gated). Workspace clippy + rustdoc clean. TUI exec/port-forward pane = M6-7 (PR-4).
M6-7 Stream/Session UI: live log viewer (follow+filter), exec pane, port-forward status tui-engineer M6-3, M6-6 user docs; RFC-0009 Demo: stream pod logs; exec; copy pod→S3 🟡 exec pane + port-forward status landed (M6-7 PR-4): SessionId/SessionRecord/SessionEnd types; Overlay::ExecPane (cooked-mode line I/O, scroll, follow, Ctrl-] detach, Ctrl-D close-stdin); Overlay::PortForwardStatus (bound-port display, one-key teardown); AppEffect::OpenExecSession/OpenPortForward/CloseSession/SendSessionInput/ResizeSession; AppEvent::SessionOutput/SessionEnded/PortForwardBound; SessionControls + run_exec_session_effect/run_port_forward_effect in app.rs; render_exec_pane/render_port_forward_status in cairn-tui. 10 reducer unit tests + 5 render tests. Remaining: in-viewer log filter.

M7 — Agentic AI (plan→confirm→execute) · security-review required

ID Item (crate) Lead Deps Docs Exit criteria Status
M7-1 LlmProvider trait + StreamChunk normalization; Claude provider ai-integration-engineer M3-5 rustdoc; ADR ref MockProvider + Claude path; no live API in CI 🟡 LlmProvider trait + MockProvider done; cloud/local HTTP providers + streaming deferred
M7-2 Ollama + OpenAI-compat providers w/ tool degradation (Native→JsonSchema→Text) ai-integration-engineer M7-1 rustdoc; local-model doc degradation tiers tested vs MockProvider 🟡 degradation core (#26): ToolSupport tier on the provider trait + degrade module (encode tools-vs-prompt / decode tool-call·bare-JSON·fenced-block), request_plan adapts to the tier; all three tiers tested vs MockProvider. Live HTTP transport done (behind the non-default http feature, reqwest+rustls): AnthropicProvider (Claude Messages API — x-api-key/anthropic-version, Role::System→top-level system, first tool_useToolCall else text→Text; Native tier) and OllamaProvider (/api/chat; robust JsonSchema tier — Ollama native tools are model-dependent). Non-streaming; transport/HTTP errors → secret-free ProviderError::Transport (never the api_key), bad body → InvalidResponse; api_key is a plain String so the secret-isolation closure test still holds. Hermetic wiremock coverage + opt-in #[ignore] live smoke (CAIRN_IT_AI+ANTHROPIC_API_KEY). OpenAI-compat provider + streaming = follow-ups
M7-3 Closed tool registry (handles only; schemars; ToolNotFound) → broker ai-integration-engineer, security-engineer M7-1, M3-5 rustdoc; threat-model no tool returns/accepts a secret (compile + review) ✅ #15 (closed set via capability_for; unknown tool rejected)
M7-4 Plan state machine, engine-driven execution, per-step confirm, partial-failure surfacing ai-integration-engineer M7-3 rustdoc engine runs steps; irreversible step pauses for confirm (test) ✅ #15
M7-5 Context WorldSnapshot (sanitized, budgeted, no secrets), injection defenses ai-integration-engineer, security-engineer M7-3 rustdoc; injection-defense doc snapshot carries no secret (test); heuristics flag off-scope ✅ #16 (WorldSnapshot + untrusted-data wrapping + out-of-scope heuristic + system policy)
M7-6 AI side panel + plan→confirm overlay; [Approve all] only when all steps Safe/Recoverable tui-engineer, security-engineer M7-4, M2-7 user docs Demo: "archive logs >30d to S3" plan→step-through; bulk-approve absent if destructive 🟡 plan→confirm and execute (#23, #30): Overlay::AiPlan + reducer (step-through / reversibility-gated bulk-approve / reject / abort), ratatui overlay, Ctrl-A via offline MockProvider; BinaryStepExecutor runs approved safe/local steps (list/stat/read/copy/move/delete) against the backends per RFC-0007. exec now routes through Vfs::invoke (#34) — allow-list enforced, errors redacted, a live Stream/Session outcome rejected loudly rather than dropped; only the live backend transport remains. Freeform prompt entry done (#37): Ctrl-A opens a text prompt and the typed request drives the flow (overlay-openers suppressed while a plan is pending). Plan-execution cancellation done (#47): Esc aborts a running plan (token polled between steps); competing ops refused while executing. Step-output summaries done (#49, RFC-0007 Gap 1): list/stat/read/delete steps surface a short secret-free summary (count/size/kind) to the user (never to the model), shown in the plan-complete status. Live providers (M7-2) + logs/port-forward execution = integration
M7-7 cairn-mcp: MCP client foundation (consume external MCP tools) + later expose actions as MCP server through same broker+confirm ai-integration-engineer M7-3 rustdoc external MCP client hits same confirm gate (test) 🟡 MCP client foundation done (RFC-mcp-client): standalone cairn-mcp crate over rmcp 2.0McpClient::connect_stdio/connect_http (child-process + streamable-HTTP, rustls/no-OpenSSL) → initialize/list_tools/call_tool, mapped to secret-free McpToolInfo/McpToolResult/McpError. Transport/protocol layer only: no dep on cairn-ai/broker/vault/secrets, not wired into the agent's closed tool set. Hermetic in-process round-trip test (dev-only rmcp server feature over an in-memory duplex). Remaining: (1) the agent-integration + safety-model RFC (RFC-mcp-client) — how untrusted MCP tools meet the capability-gated, plan→confirm tool set; (2) the inverse MCP server that exposes Cairn actions through the same broker+confirm gate.

M8 — Extensibility (WASM plugins) · security-review required

ID Item (crate) Lead Deps Docs Exit criteria Status
M8-1 RFC: plugin host & WIT ABI (worlds, capability grants, streaming-by-polling, versioning) plugin-systems-engineer, technical-writer M3-5 RFC merged; confirm ADR-0004 approved before M8-2 ✅ (RFC-0006, #22)
M8-2 cairn-plugin: wasmtime host, WIT bindings, default-deny Linker, per-instance Store, ResourceLimiter, runaway-guest bound plugin-systems-engineer M8-1 rustdoc spinning plugin can't hang UI (fuel test); ungranted import fails at instantiate ✅ #18 (wasmtime host: fuel limit traps a runaway guest, memory cap, default-deny imports; 6 tests). NOTE: M8-2's bound is fuel only; the wall-clock epoch deadline (owed because fuel doesn't advance during host calls) landed in M8-4
M8-3 PluginVfsBackend bridge: guest backend export → Vfs (chunked-poll) plugin-systems-engineer, rust-staff-engineer M8-2 rustdoc plugin backend passes MockVfs contract suite full Vfs contract done (M8-3a/b): cairn:plugin@1.0.0 WIT package (RFC-0006), wasmtime component-model + generated host bindings, PluginComponent wrapper calling the non-streaming exports (scheme/backend-capsCaps/caps-at/stat/list-page) with mem-cap+fuel and an empty deny-all WASI ctx; a committed guest fixture component proves it hermetically (no WASM toolchain in CI). M8-3b read path done: PluginVfsBackend implements Vfs for scheme/connection/caps/list(paginated stream)/stat via a dedicated-thread bridge (the Store is !Send; the Send+Sync backend messages it over a channel + oneshot replies, fuel refilled per call); granted host imports linked (log/now-secs real, brokered fns deny-stubbed → M8-4). Browsable as Arc<dyn Vfs>, tested against the committed fixture. M8-3b PR2 done: open_read bridges a guest read-stream resource to a ReadHandle (AsyncRead) — chunk-pulled on demand, resource owned on the plugin thread (it is !Send) and addressed by opaque id, freed on drop; hostile-guest hardening (per-stream byte cap Limits::max_stream_bytes, oversized-chunk rejection, control-char/length-capped guest error strings). M8-3b PR3 done: open_write bridges a guest write-sinkWriteHandle (chunked write → finishEntry; drop-without-finish aborts, not commits) and create_dir/remove(recursive flag)/rename are wired through with per-error VfsError mapping — the full Vfs contract, write sinks owned on the plugin thread and freed on drop. Remaining before live untrusted use (M8-4): epoch deadline before any blocking host import + real brokered host functions
M8-4 Brokered creds/HTTP for plugins (UUID stand-in, host substitutes secret), journaled Actor::Plugin plugin-systems-engineer, security-engineer M8-2, M3-5 rustdoc; threat-model; RFC-0010 plugin never sees secret value (test); brokered HTTP only RFC-0010 PR-B done: real host::http-fetch and host::use-credential replace the deny-stubs. http-fetch (behind plugin-network feature): reqwest+rustls, per-plugin hostname allow-list, SSRF guards (IP-literal + DNS pre-flight, blocked: loopback/RFC-1918/CGNAT/link-local/ULA/IPv4-mapped), 8 MiB response cap, Set-Cookie stripped, sensitive headers redacted from logs, redirect re-validation per hop. use-credential: CredentialBroker trait in cairn-broker-api (secret-free boundary); BrokerCredentialAdapter in cairn-broker resolves vault secret, performs closed-vocabulary action (bearer-token/basic-auth-header), zeroizes secret, journals event — raw CredentialSecret never crosses WIT ABI. Both gated by PluginGrants; no grant → deny-stub without touching vault or network. dep-closure invariant verified: cairn-plugin depends on cairn-broker-api only. New public API: PluginGrants, PluginHostConfig, PluginComponent::instantiate_with_grants. 78 tests green (plugin-network feature hermetically tested via wiremock; default cargo test offline). Previously done (this item): epoch deadline, WASI narrowing (PR-A). Remaining before live untrusted use: PR-C loader (M8-5)
M8-5 Manifest (plugin.toml + wasm section), install-time capability approval UI, revocation plugin-systems-engineer, tui-engineer M8-2, M8-4 user docs; plugin author guide; RFC-0010 Demo: install a sample plugin backend, approve caps, browse 🟡 PR-C1 done: PluginManifest (plugin.toml via serde+toml, all structs deny_unknown_fields; §5.2 schema: [plugin]/[capabilities]/[network]/[limits], semantic validation: name charset/length, SemVer, api major vs HOST_API_MAJOR, description length); PluginLoader (directory discovery, ABI check, grants intersection fail-closed, full §5.6 load pipeline); PluginGrantsRecord+PluginEntry in cairn-config under [plugins."<name>@<version>".grants] with plugin_grants()/set_plugin_grants()/revoke_plugin_grants() accessors; SEC-1 fix (issue #103, release-gating): PinnedSsrfDnsResolver closes DNS-rebinding TOCTOU — single resolve+validate; reqwest never re-resolves; SEC-8: 6to4/Teredo/NAT64 tunnel IPv4 extraction. 25 new tests (12 manifest + 7 loader + 6 config). All gates green. Remaining: PR-C2 (TUI approval overlay) + PR-C3 (cairn plugin install CLI)
M8-6 cairn-plugin-sdk (optional) + sample guest .wasm fixtures for CI plugin-systems-engineer M8-3 SDK docs fixtures checked in; plugin tests need no WASM toolchain in CI ⏭ deferred (Component Model/WIT + Vfs bridge — next layer; runtime core done in M8-2)
M8-7 Declarative config extensions (keybinds, themes, shell-command actions, aliases) software-engineer, tui-engineer M3-6 user docs config-only action runs without a plugin 🟡 config-driven keybindings (#24) + themes (#32): [ui.keybindings] chord→action overrides + [ui.colors] role→color overrides over the preset (Theme resolver, threaded through render), Config::load wired into the binary. Multiple built-in theme presets (dark/mc/nord/gruvbox/light) with optional background/foreground, plus live switching (Shift-T cycles presets; AppState.theme_name resolved per-frame). Shell-command actions done (ADR-0005, security-reviewed): [[shell_actions]] (name/key/command/args with {path}/{dir}/{name}) binds a key to run a local program on the entry under the cursor. Built on Vfs::local_path (canonicalize+confine; Caps::LOCAL_PATH) for local-only enforcement. Hardened: argv-only no shell, confirm-before-run (opt-out per action), file-trust gate (ignores actions from a world/group-writable or non-owned config on Unix), env scrub (no secrets to the child), explicit cwd, stdin closed, captured+capped output, wall-clock timeout + process-group kill. Non-interactive only; output summarized to status (never echoed/sent to AI). Interactive (TUI-suspend) + aliases deferred

v0.1 — Release

ID Item Lead Deps Docs Exit criteria Status
R-1 Release engineering: cross-platform binaries (musl/universal-mac/Windows), Homebrew/cargo, signing devops-engineer M5 (M4) release docs tagged build attaches binaries on 3 OSes 🟡 release workflow ready (.github/workflows/release.yml): on a v* tag (or manual dispatch) it builds cairn --release --features all-backends for Linux x86_64 gnu + musl, macOS aarch64 + x86_64, and Windows x86_64, packaging each binary + SHA-256 into a release asset (aws-lc-rs deps wired: CMake/musl-tools on Linux, NASM on Windows, pre-generated bindings on macOS). Tagging is the maintainer's call. Homebrew/cargo publish + signing still ⏭
R-2 Docs completeness pass: README, user guide, backend docs, glossary, --help technical-writer all README/docs docs match shipped features (no stale) ⏭ deferred (follows backend milestones)
R-3 Release QA: regression matrix, graceful degradation (no truecolor/Nerd-Font/narrow), session restore qa-engineer all TESTING.md crash-free smoke on 3 OSes + limited terminals ⏭ deferred (follows backend milestones)
R-4 CHANGELOG roll-up → v0.1, version bump, ADR/RFC index check technical-writer, project-manager R-1..R-3 CHANGELOG tagged v0.1.0 from main; dashboard shows v0.1 ✅ 🟡 version bumped 0.0.00.1.0 (workspace package + all internal-crate pins; workspace resolves) and CHANGELOG.md rolled [Unreleased][0.1.0] - 2026-06-30 with a fresh empty [Unreleased]. Tag v0.1.0 is the maintainer's outward-facing call (not created here)

4. Critical path (the spine)

Strict order; each step unblocks the next. This is the earliest route to a demoable end-to-end slice.

  1. cairn-typesVfsPath/Entry/Caps/ids/errors. Biggest single unblock (nothing compiles without it). Gate: VfsPath::parse rejects ../control chars.
  2. cairn-vfscairn-secrets (first parallel moment; no mutual dep). cairn-vfs is the highest-churn-risk interface — validate it on paper against every backend model (LLD §3.6) before any real backend. Gate: MockVfs passes a contract suite.
  3. cairn-transfer (type stubs + local→local copy_one) — cairn-core imports its types.
  4. cairn-core (pure TEA reducer; no handles, no I/O). Gate: navigate/list/cancel unit tests.
  5. cairn-tui skeleton wired to MockVfsSlice 0 "it boots" (renders, navigates, resizes).
  6. cairn-backend-local (full) — Gate: contract suite on 3 OSes; 100k list smooth.
  7. cairn binary (effect runner + DI) → Slice 1 "local read-only browser", the earliest demoable end-to-end validation of the entire spine.

After step 7, multiple lanes run in parallel (§6).

5. Dependency DAG

Tier 0:  cairn-types
Tier 1:  cairn-secrets   cairn-vfs   cairn-config        (vfs blocks ALL backends/transfer/broker/plugin/core)
Tier 2:  backend-{local,ssh,object,docker,k8s} (siblings, independent)   cairn-transfer(→vfs)   cairn-vault(→secrets; NO vfs)
Tier 3:  cairn-core(→vfs,transfer)        cairn-broker(→vault,vfs,secrets)
Tier 4:  cairn-tui(→core)   cairn-ai(→broker only)   cairn-plugin(→vfs,broker)
Tier 5:  cairn-mcp(→broker,ai)   [feature-gated]
Tier 6:  cairn (binary)

Blocking edges:  vfs → {all backends, transfer, broker, plugin, core};  transfer → core;
                 vault → broker;  core → tui;  broker → {ai, plugin, mcp};  tui → first runnable binary.
Independence:    backends are siblings (cross-backend logic lives ONLY in cairn-transfer);
                 cairn-ai cannot name vault/backends (compile-time); vault sub-spine is independent
                 of the backend sub-spine until they converge in cairn-broker.

6. Parallelization lanes (after the spine)

Lane Owner Blocked until Convergence note
A — Object stores (S3→GCS→Azure) storage-engineer RFC-transfer-resume; broker for cloud creds co-develop multipart with transfer engine; add MinIO CI job
B — SSH/SFTP network-engineer RFC-ssh; broker first cross-backend transfer (local↔ssh)
C — Docker container-backend-engineer RFC-docker; broker (registry auth) exec Stream in TUI; container→S3
D — Kubernetes kube-staff-engineer RFC-k8s; broker; K8s cred types in vault deepest nesting; caps_at; port-forward Session; "pod→bucket"
E — Vault + broker security-engineer secrets types (vault has NO vfs dep → start in parallel with local backend) broker is the convergence point of the security + backend sub-spines
F — AI ai-integration-engineer broker API frozen (use MockBroker) only depends on broker; can run parallel to C/D
G — Transfer depth storage-engineer (+rust-staff) RFC-transfer-resume; ≥2 backends for real cross-backend tests multipart path co-developed with Lane A
H — Plugins plugin-systems-engineer broker + vfs stable plugin backend wrapped as Arc<dyn Vfs>

Start Lane E (vault) in parallel with the local backend — vault is pure crypto+disk with no vfs dependency, so finishing it early means the broker is ready when credentialed backends need it. A --dev plaintext-credential path (never in release builds) lets lanes A–D run emulator tests before the broker lands.

7. RFC sequencing (RFC-before-large-impl, CLAUDE.md §5)

RFC Must land before Deferrable?
RFC-local M1-3 local backend impl No — on the critical path
RFC-ssh M4-2 SSH impl No — draft early so it doesn't stall Lane B
RFC-transfer-resume S3 multipart-persist (M5-5) / transfer depth No — the format must be right the first time
RFC-docker M6-2 Short defer (Docker starts later)
RFC-k8s M6-5 Short defer (last complex backend)
RFC-reveal-secret-ux reveal-in-TUI flow Yes — stub a locked placeholder initially
RFC-team-vault team-vault feature Yes — P2, post-v1 (KEK layer extends to per-recipient age)
RFC-plugin-registry registry/signing (not the host) Yes — P2; host ships with local-file installs
RFC-0010 (plugin sandbox + brokered host fns) M8-4 (PR-A WASI narrowing, PR-B1/B2 brokered fns), M8-5 (PR-C1/C2/C3 loader + approval) No — gates live untrusted plugin loading
RFC-mcp-client consuming external MCP tools Yes — explicitly post-v1. Note: the transport/protocol client foundation (cairn-mcp, rmcp 2.0) has landed ahead of the RFC; the RFC still owns the safety model — how untrusted MCP tools meet the closed, capability-gated, plan→confirm tool set.

8. Vertical-slice demo checkpoints

These map onto the milestones and are the moments to show real progress (record an asciinema/ screenshot in the milestone issue): 0 it boots (MockVfs) · 1 local read-only browser (earliest demoable) · 2 local full CRUD + queue · 3 viewer + config/session · 4 vault unlocks · 5 +SSH (first credentialed backend; cross-backend transfer) · 6 +S3 (multipart, pagination, resume) · 7 GCS+Azure (contract-generalized) · 8 +AI (plan→confirm) · 9 +Docker (exec/log stream) · 10 +K8s (pod→bucket flagship) · 11 +plugins.


9. Progress-tracking design (how this stays the source of truth)

The cardinal rule: status is updated in the same PR that does the work. A PR implementing M5-4 flips its row 🟡→✅ in the same diff, appends the PR #id, and adjusts the §1 dashboard counts. This is the only thing that keeps the tracker honest.

  • GitHub mapping: milestones M0–M8+v0.1 → GitHub Milestones; each work item → a GitHub Issue (M<n>-<k>: <desc>, assigned to its milestone, labeled with the lead's area:* + a type:*) created as it enters "Ready" (§10); dependencies become issue task-list checkboxes; PRs Closes #<id>.
  • Doc ↔ GitHub: this doc is the human-readable plan + at-a-glance status; Issues/Milestones are machine-tracked state; they must agree.
  • Automation (M0-3 follow-up): a CI/Action step recomputes the §1 dashboard counts from issue state on merge and can fail a PR whose touched item row is left stale — making "update the tracker" a merge gate, not a good intention. Until that lands, the merging maintainer updates the row by hand.
  • Generated assets (dependency diagram, burn-up) committed under docs/assets/.

CLAUDE.md §5 codifies the same-PR update as a documentation requirement.

10. Definition of Ready / Definition of Done

Ready (may start): a GitHub Issue under the right milestone with a lead assigned; unambiguous, testable acceptance criteria; all dependency items ✅ (or a documented mock unblocks it); prerequisite RFC/ADR merged; scope fits one PR; specialist(s) identified per CLAUDE.md §2.

Done (✅): merged via squash PR with ≥1 approval and green cross-platform CI; tests (unit; regression for fixes; backend integration + contract where applicable); gates addressed (bug-bot + code-review; security-review where applicable); docs done (rustdoc, ADR/RFC, user docs, CHANGELOG); fmt/clippy -D/test/doc/deny green; no secrets introduced (redaction verified for new #[source]); tracker row updated in the same PR.


11. Risk register

# Risk L/I Mitigation Owner Tripwire
R1 Scope: 7 backends at launch (PRD §11) H/H vertical-depth-first; v0.1 deep on local/SSH/S3, rest "preview"; ⏭ aggressively project-manager M5 slips → cut GCS/Azure polish to fast-follow
R2 async_trait / heavy-SDK compile times M/M crate-per-backend + feature flags; default build local+SSH; watch --timings rust-staff, devops cold build over target → split/gate
R3 Secret-handling defect (leak/log) L/Crit compile-time broker boundary; mandatory security-review; redaction tests; secrecy/zeroize security-engineer any redaction-test failure blocks release
R4 Cross-platform quirks (Windows, headless keychain) H/M 3-OS CI from M0; passphrase fallback; grapheme widths; limited-terminal tests devops, tui any OS red on main blocks merges
R5 AI safety on irreversible cloud ops M/H plan→confirm enforced by broker/UI not model; closed registry; per-step confirm; capability containment ai-integration, security any path to bulk-approve a destructive step = blocker
R6 WASM streaming immaturity M/L chunked-poll baseline; additive stream<T> later; epoch timeouts plugin-systems perf unacceptable → keep polling
R7 !Send SDK futures (russh) M/M assert_send compile test per backend; channel-proxy isolation rust-staff, network assert_send fails → proxy that backend
R8 AI cost/privacy deters users M/M local Ollama first-class & documented; AI fully optional ai-integration
R9 "Cairn" trademark conflict L/M clear before branding lock-in product-branding, project-manager conflict → rename before v0.1 marketing
R10 Tracker drift M/H §9 same-PR rule + CI enforcement + Action-regenerated dashboard project-manager drift CI check red

Convergence/integration risks (highest-rework points)

  • Vfs trait churn affecting all backends → validate on paper + MockVfs contract suite before replicating; freeze the public trait after S3 + local both implement it unchanged (Slice 6), guarded by a breaking-change CI check.
  • Broker API churn affecting AI + plugins + cloud creds → freeze the broker API (review by security + rust-staff) before Lanes F/H start consuming it (target: by Slice 5).
  • TEA Msg/Effect/Event churn → keep discriminants coarse (AppEffect::Ai(AiCommand)), all #[non_exhaustive], so new subsystems add one outer variant, never explode match arms.

12. Cross-cutting workstreams (continuous; folded into feature PRs)

Workstream Owner Standing obligations
CI / Release devops-engineer keep 3-OS matrix + emulator integration jobs green; cache tuning; deny/audit/Dependabot triage; release pipeline ready before v0.1
Documentation technical-writer every feature PR ships its docs; ADRs immutable (supersede); RFCs before large impl; README/glossary/CHANGELOG current; PRD stays high-level
Testing / QA qa-engineer hermetic-offline default cargo test; maintain Mock{Vfs,Provider,Broker,ObjectStore}; object-store contract suite; emulator jobs; release regression
Security security-engineer security-review gate; redaction upkeep; living threat model; verify broker boundary holds as AI/plugins land
Architecture software-architect guard dependency-graph seams + ADR consistency; review every public API shape
Performance performance-tuning-engineer watch list/transfer/compile benchmarks as features land
Project management project-manager keep §1 dashboard + GitHub state in sync; sequence RFCs vs rollout; weekly burn-up note; escalate ⛔

CI evolution (added as capabilities land)

M0 base (fmt/clippy/test/doc/deny × 3 OSes) → +vault (CI-safe Argon2 params) → +ssh (sshd service) → +object (MinIO/fake-gcs/Azurite + contract job + throughput check) → +docker (dind) → +k8s (kind) → +ai (MockProvider always; live Claude/Ollama secrets-gated optional) → release (musl/universal-mac/Windows binaries, Homebrew tap, crates.io). Default cargo test stays hermetic and offline throughout; integration jobs are feature-gated.


Appendix — long-poles (watch these)

  • M1-1 Vfs trait gates every backend — review before replication; freeze after Slice 6.
  • M3-5 broker gates SSH, all object stores, Docker, K8s, AI, plugins — highest fan-out; don't slip.
  • RFC items (M1-2, M2-1, M4-1, M6-1, M6-4, M8-1) each gate their implementation.
  • M5-2 contract suite makes GCS/Azure breadth cheap.