# AGENTS.md Operating instructions for opencode agents working in this repo. opencode auto-loads this file as instructions, overriding the built-in defaults for this project. Custom agents in `.opencode/agents/` inherit these rules unless their own prompts say otherwise. ## Git Workflow **Commit and push when reasonable.** When a change is complete and verified, commit and push to `origin/main` without asking. This overrides the built-in default of "only commit when explicitly asked." The workflow: 1. Make the change 2. Verify (once `src/` exists): `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`, `cargo doc --no-deps` if docs changed. While the repo is docs-only (Phase 0), verification is a careful re-read of the changed docs and a check that every referenced path/ADR/OQ id exists 3. Inspect `git status` and `git diff` before staging — stage only the intended files, never secrets 4. Write a concise commit message matching the repo style (see `git log --oneline -10`). For multi-point changes, use a summary line plus a body with bullet points and a verification block. 5. `git push origin main` 6. Report the commit hash and the verification summary Exceptions — **do not** commit or push without asking: - The change is exploratory / speculative (you're not sure the user wants it kept) - The user is actively reviewing the diff and may ask for changes - The change touches a wire format or a trait shape that backends or consumers implement (one-way doors — see "Wire formats are stable" and "Backend/storage trait shapes are one-way doors" below; once consumers exist, those signatures are wire-stable contracts) - You'd be force-pushing, amending a published commit, creating an empty commit, or skipping hooks Never commit secrets, keys, or credentials. If a commit fails or hooks reject it, fix the issue and create a new commit — do not amend the failed one. Git identity is preconfigured (`glm-5.3-flash `). Do not change `git config`, skip hooks, or use `git commit -i`. ## Project Conventions (Rust / virtual filesystem crate) This is the alkfs repo — a content-addressed, branch-aware virtual filesystem for the alk family. The working direction (pending Phase 0 convergence): a two-crate split — `alkblobs` (bucket-style content-addressed blob store: appfile layout, git-compatible sha1/sha256 identity, chunk-level dedup for large content) beneath `alkfs` (the path-tree mapping + branches + write sessions, plus a producer/consumer serving protocol on alkcall channels) — so that alkgit's object storage, the coming alksftp, and the alknet filesystem vision all compose on the same substrate. The anchor workload is remote mounting: a dev-server workspace mounted on a local Linux machine (today via sshfs/vanilla SFTP, whose per-syscall round trips are the problem to beat). It sits in the alk* family: alkcall (call + channels — the substrate), alktty, alktunnels, alksocks (the protocol-crate siblings), alkgit (the first in-family storage consumer). The conventions below apply to all work in `src/` and `tests/` once they exist. They mirror `.opencode/agents/implementation-specialist.md` §Project Conventions and are repeated here so they apply to every session, not just spawned implementation agents. 1. **No comments in code** unless the user explicitly asks. This is a project-wide convention. Doc comments (`///`, `//!`) are fine and expected on public API. Inline `//` comments only when the user asks or when a non-obvious safety/correctness constraint would otherwise be missed (e.g., durability ordering constraints — "the blob bytes must be durable before the path-tree row that names them commits, or a crash orphans the reference"). 2. **Error handling** — `thiserror` for library error types. No panics in library code. No `unwrap()` or `expect()` outside tests. If you reach for `unwrap`, the error path wasn't specified — stop and decide what should actually happen. For poisoned `RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())` so a panic in one operation does not cascade to other operations. Filesystem and storage errors get faithful, typed mapping — never collapse a durability/corruption error into a generic one (a silent-corruption bug in a filesystem crate is the worst-case failure mode). 3. **`tokio` is the async runtime** — all I/O is async. Use the wasm-clean tokio subset (`rt`, `sync`, `io-util`, `macros`, `time`) — **do NOT use `features = ["full"]`**. CPU-heavy storage work (hashing, pack/pao assembly, fsync-heavy batches) goes through `tokio::task::spawn_blocking`, the alkgit POC-2 pattern. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for lifecycle correlation; `parking_lot` for short-held internal locks. 4. **WASM target is load-bearing by default, with an honest escape hatch** — the family default (alktty/alktunnels/alksocks) is a wasm-clean protocol layer. A filesystem crate has heavier native gravity (real FS I/O, SQLite); the expected shape is a wasm-clean protocol/ops layer with the storage engine native-only behind features — mirroring how alksocks keeps socket I/O behind `local`. Whether even the protocol layer stays wasm-clean is a Phase 0 question, not a settled convention; decide deliberately and record it. Run `cargo check --target wasm32-unknown-unknown` once the split exists. 5. **Wire formats are stable** — no wire format exists yet; when the first one is specced (the serving ALPN, its channel open-op params, any chunk/framing on the data path), it is a one-way door: decide via ADR before the first consumer exists, then additive-only. The ALPN string (provisional `alk/fs`; final naming per alkcall ADR-004/006) and the params-object shape follow the alkcall ADR-039 precedent (`params` is ALPN-specific, interpreted by the open handler). 6. **Producer/consumer, not server/client** — both sides of a channels connection can initiate. A producer exposes filesystem resources (registers openable channels via `ChannelCore::register_openable`); a consumer opens them and speaks the file protocol inside. Both sides can be both simultaneously — connection direction is independent of service direction. Avoid "server" and "client" framing in docs and API names; use "producer" and "consumer," or "accept side" / "connect side" for the connection-establishment half specifically. See alkcall ADR-022, ADR-037. 7. **Substrate-agnostic by construction** — the protocol layer must not know whether the far side is a channels `BiStream`, an in-process handle, a local loopback, or a door adapter (alksftp, FUSE, a sync client). The file-protocol state machine is generic over `T: AsyncRead + AsyncWrite + Unpin` (the fast-socks5/alksocks genericity precedent); substrate-specific types are confined to feature-gated modules injected at the assembly layer (alktty `TtyBackend` inversion-point pattern). 8. **No forced host-FS access** — the defining requirement of this crate, the analogue of alksocks' "never binds a port." The virtual filesystem manages its own content store and never reads, writes, or mounts the host filesystem unless explicitly configured to. Bridges to the real world are explicit, optional, feature-gated assembly shapes: a local blob-file store, an SFTP door (alksftp), a mount adapter, a directory-sync tool. The engine itself is storage-agnostic about what backs its blob layer (filesystem, SQLite, remote). 9. **Metadata/content split is the load-bearing architecture** — small structured state (path edges, branches, snapshots, refs, cached sizes) lives in a transactional store; content bytes live in a content-addressed blob layer (dedup by hash, branch sharing for free). This is the alknet-filesystem POC's three-layer conclusion and the same "shape" as git + gitlfs and iroh-blobs' inline/outboard split. Path-tree operations are O(path edges), never O(bytes) — rename is O(1) on edges regardless of file size. Do not let byte-scale concerns leak into the metadata layer or vice versa. See `docs/research/phase-0.md`. 10. **Content addressing and durability ordering** — content is identified by its hash, not its path, with git-compatible identity (sha1/sha256, algorithm-tagged): git objects are natively addressable in the blob layer, never mapped through a foreign hash. Identical content is shared across branches/paths by construction; chunk-level dedup applies to large content (git objects are effectively single-chunk and ride whole-file identity). The durability contract is the crash-safety story: content must be durable before any metadata naming it commits; a crash mid-write leaves the old version intact and visible (the POC's branch-on-write/merge-on-close property). Across the alkblobs/alkfs seam the contract is the put/get API (what a returned handle promises about durability is part of that seam's spec). Exact chunking/manifest choices are Phase 0 OQs; the *invariant* (no torn versions visible, no orphaned-name commits) is not. 11. **Vendored core types come from alkcall** — `Connection`, `ProtocolHandler`, `BiStream`, `BidiStreamSource`, `AuthContext`, `Identity`, `IdentityProvider`, `AccessControl`, `OwnershipProvider`, `HandlerError`, `StreamError` come from `alkcall::core`. Do not vendor copies into this crate. alkcall is ours and co-developed — breaking changes are expected at this major-zero stage; find and fix issues upstream rather than working around them. Pin deliberately and bump deliberately. The establishment surface (alkcall ADR-049 + amendments) and the identity seam (CF-005/CF-006) are load-bearing for the serving protocol, same as the alktty/alktunnels/alksocks pattern. 12. **Access control** — a filesystem is arbitrary read/write by nature: the open gate and path-scope policy are the security boundary, the same posture as alksocks' arbitrary egress. Scope-gate file opens (an `alkfs`-shaped scope following alktty's `TTY_OPEN_SCOPE` / alksocks' `SOCKS5_OPEN_SCOPE` precedent), wire producer openable channels through `AccessControl` for free via `ChannelCore::register_openable`, and treat per-path/per-bucket policy as a Phase 1 design question (OQ). Multi-tenancy isolation (the POC's `bucket` concept) is a where-clause, not an afterthought; bucket semantics across the alkblobs/alkfs seam are part of the seam's spec, not an afterthought. 13. **Caches key on content hashes, never on paths** — content under a hash is immutable, so content-hash-keyed caches never invalidate on bytes; only the path/branch-head → hash *mapping* goes stale, and that is cheap to refresh. Any cache (store-layer decorator per rudolfs, mount-door cache, session prefetch) follows this rule. Path-keyed caches reintroduce coherence problems content addressing exists to remove. 14. **Backend/storage trait shapes are one-way doors** — the seam the protocol crate exposes to storage implementers (the alkgit `GitRefs`/`GitPackGen`/`GitPackIngest` trait-family precedent, `docs/architecture/backend.md` there) is a contract once consumers exist: keep traits small, orthogonal, and substrate-blind; never leak gix/iroh/SQLite types across them. The alknet-filesystem probe (`alknet-blobs-external-store-probe.md`) is the cautionary example — the sealed/`pub(crate)` surface is exactly what turns "external store" into "fork". 15. **Feature flags** — substrate backends and heavy dependencies are feature-gated if the need arises. The base crate should compile lean (no SQLite, no gix, no kernel-FS access unless the feature is on). Verify both `cargo test` (default) and `cargo test --all-features` pass if features are added. 16. **Naming** — Rust standard: `snake_case` for functions/variables/ modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants. 17. **Module structure** — one module per file under `src/`, re-exported from `src/lib.rs`. Public API surface is `lib.rs` re-exports. The expected shape (pending Phase 0/1 pinning): storage engine modules (path tree, blob store, write sessions, GC), backend traits + feature-gated implementations, and the producer/consumer protocol modules mirroring the alktty/alktunnels/alksocks structure. Backend modules are feature-gated and never imported from the protocol/adapter/client modules. 18. **Upstream posture** — alkcall, alktunnels, and the alk* crates are ours to shape: file asks early and land them there rather than working around them locally (the alktunnels E-01/E-02 precedent — filed from Phase 0, landed within a day). Third-party crates (iroh-blobs, sqlite/rusqlite, russh-sftp, gix, automerge, honker, rudolfs) are NOT ours and never will be: wrap, extract, or fork deliberately per the alksocks precedent (OQ-SK-04 → ADR-013: extraction as owned code with provenance notices, differential tests against the reference checkout, no silent absorption) — and only when the carried changes pay for themselves. `/workspace/iroh-blobs`, `/workspace/russh-sftp`, `/workspace/rudolfs`, etc. are read-only reference checkouts. ## Verification Commands Run these before committing (once `src/` exists). All must pass. ```bash cargo test # full suite cargo clippy --all-targets -- -D warnings cargo fmt --check cargo doc --no-deps # if docs changed cargo test --all-features # if features are added cargo check --target wasm32-unknown-unknown # if the wasm posture applies (see convention 4) cargo publish --dry-run --allow-dirty # before a release ``` While the repo is docs-only (Phase 0), there is no build to verify — verification means re-reading changed docs and checking that every referenced path, ADR, and OQ id exists. ## Architecture Context - `docs/research/phase-0.md` — the Phase 0 (Exploration) document and the current state of this repo: vision, prior art, open questions (OQ-FS-NN), and the POC register. Read it before non-trivial work. The SDD process lives in `docs/sdd_process.md` (Phase 0 in progress; `docs/architecture/` does not exist yet — do not create it; that is Phase 1). - The ancestor research: - **alknet-filesystem POCs** — `/workspace/@alkdev/alknet/docs/research/alknet-filesystem/` (`poc-summary.md` — the three-iteration POC: SQLite path tree + iroh-blobs + honker, branch-on-write/merge-on-close, automerge sync; `alknet-blobs-external-store-probe.md` — the iroh-blobs Command-actor probe). This crate is the decomposition-era continuation of that research on the published alk* substrate. - **alkgit** — `/workspace/@alkdev/alkgit`: the first in-family storage consumer (currently paused on this problem). Its backend trait family (`docs/architecture/backend.md` — `GitRefs`, `GitPackGen`, `GitPackIngest` over gix-odb) is the shape alkfs must serve efficiently: streaming pack generation/ingestion, CAS ref transactions, budgeted resources. Its `docs/research/` holds the gitoxide and git-protocol surveys. - The substrate and sibling crates: - **alkcall** — `/workspace/@alkdev/alkcall` (v0.8.0, crates.io). The substrate: call protocol + channels multiplexing. This crate will consume `alkcall::core` types and the channels `ChannelCore`/`ChannelClient`/`register_openable_with_establisher` surface, same as alktty/alktunnels/alksocks. - **alktty** — `/workspace/@alkdev/alktty`: the first producer/consumer protocol crate; the backend inversion point, wasm-clean default, scope-gating, and feature-gated backend precedents. - **alktunnels** — `/workspace/@alkdev/alktunnels`: `-L`/`-R`/`-D` tunnels on channels; the Phase 0 findings format and POC placement conventions (`docs/research/phase-0-findings.md`) this repo inherits. - **alksocks** — `/workspace/@alkdev/alksocks`: the SOCKS5 sibling; the closest Phase 0 template (`docs/research/phase-0.md` shape, OQ numbering, upstream-posture convention). - **alkvault** — `/workspace/@alkdev/alkvault`: secure secret handling; the family rule is metadata stores hold vault references, never plaintext secrets (alkgit vision principle 3). - Key upstream ADRs that inform this crate's design (alkcall numbers unless noted): - ADR-035 — channels pure channel multiplexing (8-byte header); the file-session rides inside a channel's `BiStream` - ADR-050 — two-pump shutdown-on-completion (`channels::pump_bidi`); use it, do not hand-roll - ADR-049 (amendments) — the establishment phase; `register_openable_with_establisher`; refused sessions are typed `channel:open_failed` call errors, never phantom channels - ADR-039 — `params` is ALPN-specific; ADR-037 — channel lifecycle ops on channel 0; ADR-034 — the channels wire format (one-way door); ADR-036 — channel 0 is pre-negotiated `alk/call` - ADR-042 — hub relay (terminate-and-re-produce); ADR-051 — `ChannelRelay`/`HubLegTemplate` - Ledger CF-005/CF-006 — the per-call opener identity seam on the open-op hooks - If a TODO references a design direction that a later ADR has decided against, the TODO is stale — remove it and align with the ADR. Do not implement the rejected design. ## Phase 0 working direction (quick reference) Recorded so sessions don't relitigate settled leanings — see `docs/research/phase-0.md` §Working direction for full context: - **Git-compatible identity (sha1/sha256), not BLAKE3** — the ancestor POCs' BLAKE3 was an iroh-blobs artifact; iroh-blobs is evaluated, not adopted (findings transfer, code doesn't). - **Chunk-level dedup is the working goal** for large content; git objects ride whole-file identity (single-chunk regime). - **Two-crate split (working direction): `alkblobs` + `alkfs`** — the blob store beneath the path-tree mapping; repo shape (one repo, two crates vs two repos) is an open sub-question. - **Anchor workload: remote mounting** — sshfs/vanilla-SFTP replacement; per-syscall round trips are the problem to beat. Second named workload: the multi-agent fork fan-out (many sessions forking the same workspace — containers or remote mounts), which is what makes content-addressed dedup load-bearing (OQ-FS-17). - These are **leanings, not decisions**: every one is an open OQ (OQ-FS-01..17) until the Phase 0 convergence; don't mark anything "settled" in code or docs until the checklist starts ticking.