Skip to content

Build the Vfs foundation and pivot the executor to VfsRef - #35

Merged
vinniefalco merged 26 commits into
cppalliance:masterfrom
vinniefalco:master
Sep 12, 2026
Merged

Build the Vfs foundation and pivot the executor to VfsRef#35
vinniefalco merged 26 commits into
cppalliance:masterfrom
vinniefalco:master

Conversation

@vinniefalco

Copy link
Copy Markdown
Member

Summary

One filesystem abstraction replaces the run-scoped Store trait and serves every consumer: Lua, the model's tools, and the Bashkit engine. The Store survives as a public concrete facade with an unchanged Lua surface, mounted at /_promptforge/store inside the namespace it used to stand apart from, and executor::run() takes VfsRef in place of StoreRef.

What changes

  • shared-vfs (new, std-only, zero dependencies): the sync Vfs/VfsAccess trait pair behind a poison-safe cloneable VfsRef; a mount router with longest-prefix dispatch, builder-fixed mount tables, and nestable overlays; universal POSIX-shaped path canonicalization with interning; a claims model attributing every operation to an ExecId through an RAII Access capability, where concurrent write conflicts are fatal; a policy layer with reason-carrying Deny/Ask verdicts (AllowAll in v1); a generic memory backend; and a stage-1 host backend (HostBackend::identity() / HostBackend::rooted(dir)) with containment enforcement and failure-atomic writes.
  • promptforge-vfs (new): the promptforge policy layer - the /_promptforge mount layout, the empty() stock constructor with the store mount preinstalled, and ModePolicy (Ask / Plan / Agent) behind a UI-flippable shared mode.
  • promptforge-store: the Store trait, MemStore, and FileStore leave the public API; a public concrete Store facade wraps a prefix-scoped Access and is exposed as vfs.store(&access). The StoreError vocabulary, anchor-edit rules, numbered reads, idempotent delete, and glob grammar are preserved exactly. The WriteScope registry is deleted in favor of the claims model.
  • promptforge-core: execute::run() takes &VfsRef; RunContext builds the Store facade internally; Lua store operations become leaf yields in the coroutine protocol, answered via spawn_blocking uniformly for all backends; a claims violation maps to a fatal determinism RunErrorKind that terminates the run and is not catchable from Lua.

Verification

  • The existing promptforge-store test suite passes against the rewritten facade unmodified in intent (parity gate).
  • Router longest-prefix, nesting, shadowing, read-only enforcement, and mount-escape rejection matrices; cross-platform path canonicalization matrix.
  • Claims lifecycle: borrow-vs-spawn, transfer, drop release, alias collision on one interned key; compile-time Send/Sync assertions for VfsRef and Access.
  • Executor end-to-end on VfsRef, including a fanout whose cross-arm append terminates the run with a determinism violation naming both arms.
  • Bashkit adapter spike: FsBackend over VfsRef compile-checked and smoke-tested with an ls/cat/grep script across mounted backends.

Out of scope

The do_shell dispatcher, git builtins, approval policy, the SQLite run-record backend, terminal mirrors, Bashkit integration proper, and stage-2 host-backend hardening.

Plan: vibe/2026-09-11-3-vfs-foundation.md

Introduces a new zero-dependency crate that sits permanently at the bottom of the dependency stack, holding the generic virtual filesystem machinery: one shared error type, the value types backends exchange, and canonical interned virtual paths. Paths are normalized and interned the moment the API receives them, so claim lookups stay pointer-cheap and aliases cannot slip past the claims tables. The only way to form a path is through a crate-private canonicalizer, so canonicalization at receipt is enforced by visibility rather than convention. A self-policing test reads the crate manifest and fails if any dependency table gains an entry.

- `crates/shared-vfs/Cargo.toml` declares an empty `[dependencies]` table carrying the zero-dependency rule in comments; `the_manifest_declares_no_dependencies` reads the manifest through `CARGO_MANIFEST_DIR` and fails if any dependency table carries an entry.
- `VfsPath` wraps an interned id so equality is an integer compare; `canonicalize` is crate-private, making it the only way to form one.
- `Interner` is hand-rolled on std behind a poison-safe `OnceLock` mutex; each string is leaked once, so resolution is a vector index and a panicking writer cannot leave the tables half-updated.
- `FileType` names all seven POSIX kinds with no catch-all, so an exotic backend node cannot be hidden.
- `Stat` uses optional fields for mode, modified, and created, so a backend that does not track a field says `None` rather than fabricating a value.
- `canonicalize` treats backslashes as separators, collapses duplicates, removes dot segments, pops exactly one segment per dotdot, and rejects traversal past the root plus relative and empty paths; case is preserved and significant. Ten tests cover the matrix and interning identity.
- `canonicalize` and `Interner::intern` carry `#[allow(dead_code)]`: only tests call them today, with the production caller still to arrive.

Design: new facade @ crates/shared-vfs/src/lib.rs boundary: pub
Design: new global-state @ crates/shared-vfs/src/path.rs::interner
Design: new newtype @ crates/shared-vfs/src/path.rs::VfsPath boundary: pub
Design: new value-object @ crates/shared-vfs/src/path.rs::VfsPath boundary: pub
Design: new newtype @ crates/shared-vfs/src/path.rs::VfsPathBuf boundary: pub
Design: new value-object @ crates/shared-vfs/src/path.rs::VfsPathBuf boundary: pub
Design: new value-object @ crates/shared-vfs/src/types.rs::FileType boundary: pub
Design: new bag-of-state @ crates/shared-vfs/src/types.rs::GrepQuery boundary: pub
Deferred: canonicalize and Interner::intern carry #[allow(dead_code)] with no production caller until Access arrives
Plan: vibe/2026-09-11-3-vfs-foundation.md
This change defines the backend trait pair behind the virtual namespace: one trait for a backend that vends and releases identity-bound sessions, and one carrying all sixteen filesystem operations with defaults for ranged reads, string replacement, and content search. It adds the policy hook consulted before every operation, the operation and verdict types it matches on, and an allow-everything policy as the initial implementation. Execution identities are opaque and vended from a process-wide monotonic counter, so every operation can be attributed to one serial thread of execution. The default search matches literal text only and refuses regular expressions, since the crate is std-only and a regex engine must come from an overriding backend.

- `Vfs` - acquire and release are the only entry points; every operation lives on the acquired access object, so storage is unreachable without an identity. Send is required and Sync is not: the handle serializes access.
- `VfsAccess` - sixteen methods, with defaults for read_range, str_replace, grep, and the POSIX extras keeping the required set at eleven.
- `Verdict` - Deny and Ask carry reason strings: Deny's flows back to the model as its recovery path, Ask's is the approval dialog text.
- `ExecId` - an opaque single-field wrapper with no public constructor; vend draws from a process-global counter and has no caller until the handle arrives.
- `read_range` - the default reads the whole file and slices, clipping at the end and returning empty when the offset is past it.
- `str_replace` - the default rewrites the unique occurrence; zero or multiple matches are errors and leave the file unchanged.
- `grep` - the default globs, reads, and line-scans with literal substring matching; regex queries return Unsupported, non-UTF-8 files and directories are skipped, and results cap at max_results with a truncation flag.
- `AllowAll` - permits every operation.
- `write_owned` - noted in the write documentation but not implemented in v1; the zero-copy move for the memory overlay waits for profiling.
- `crates/shared-vfs/src/path.rs` - the dead-code allows on intern and canonicalize are removed now that the grep default canonicalizes.

Design: new newtype @ crates/shared-vfs/src/traits.rs::ExecId boundary: pub
Design: new global-state @ crates/shared-vfs/src/traits.rs::ExecId::vend
Design: new speculative-abstraction @ crates/shared-vfs/src/traits.rs::VfsAccess boundary: pub
Design: new speculative-abstraction @ crates/shared-vfs/src/traits.rs::Policy boundary: pub
Design: new value-object @ crates/shared-vfs/src/traits.rs::Op boundary: pub
Deferred: ExecId::vend carries #[allow(dead_code)] with no caller until the handle arrives in a later step
Deferred: the defaulted write_owned method is noted but not implemented in v1, to be added when profiling calls for it
Plan: vibe/2026-09-11-3-vfs-foundation.md
The virtual filesystem gains its public entry points: a cloneable handle over one backend and a must-use capability through which every operation flows. Each acquisition vends a process-unique identity, and every operation canonicalizes its path, consults the installed policy, and registers a read or write claim before the backend is touched, so overlapping writers fail loudly instead of racing. Claims are keyed by canonical path so aliased spellings of one file collide, and dropping the capability releases them, which keeps cancellation and panic paths from leaking conflicts. Spawning a child retires the parent's claims, making the spawn the happens-before edge that keeps sequential fan-out legal.

- `VfsRef::with_policy` installs the policy at construction next to the backend; `VfsRef::new` defaults to the allow-everything policy. This is the only seam through which a host interposes on operations.
- `check_policy` collapses both the deny and ask verdicts into a permission-denied error carrying the policy's reason string. No approval prompt exists at this layer.
- `Volume` holds the backend and the claims ledger as separately shareable arcs, so a later overlay can swap the backend while keeping one claims table.
- `Claims` keeps the readers map, writers map, and live set behind one poison-safe mutex, and retired or released claims are deleted outright rather than tombstoned.
- `gate` canonicalizes the path, consults the policy, then registers the claim, in that order, so a denied operation leaves no claim behind and every claim key is canonical.
- `claim` fails a write when any other live identity claims the path and a read only when another identity writes it; an identity never conflicts with itself. The conflict error names the path, both identities, and both claim kinds.
- `spawn` vends a fresh identity for the child and retires the parent's claims, and `impl Drop for Access` releases the identity's claims before a best-effort backend release.
- `glob` registers its claim against the canonicalized pattern while the backend receives the pattern verbatim.
- `read_range_numbered` emits one-based inclusive line ranges numbered absolutely from the start, padded to the widest emitted number.
- `ExecId::vend` is now wired: acquisition and spawn call it, and its dead-code allowance is removed.
- `VfsRef` wraps exactly one backend; there is no mount table, router, or overlay entry point yet.

Design: new constructor-injection @ crates/shared-vfs/src/handle.rs::VfsRef boundary: pub
Design: new shared-mutable-state @ crates/shared-vfs/src/handle.rs::Volume
Design: new surface-growth @ crates/shared-vfs/src/handle.rs boundary: pub
Plan: vibe/2026-09-11-3-vfs-foundation.md
The virtual filesystem handle can now serve several backends at once: each mounts at a path prefix, the longest matching prefix wins, and a backend's session is acquired only on first touch of its mount. Mounts install through a builder that freezes the table at build time, and routers nest because the router is itself a backend. A handle can also mount under another handle's router, so an overlay shares the base's conflict-detection table and policy while swapping only the storage view. Writes against read-only mounts are denied before anything is touched, and two-path operations that cross a mount boundary are rejected.

- `Router` is crate-private and itself a `Vfs`, so routers nest and nobody outside the crate can hold one; the mount table is a `BTreeMap` fixed at construction and cheap to `Arc`-share with every session it vends.
- `VfsRefBuilder` is the public way in: `mount` consumes and returns self and panics on a relative prefix or a duplicate mount, and `build` freezes the table into a handle under the `AllowAll` policy.
- `VfsRef::overlay` mounts a backend over the base handle's namespace while sharing the base's claims table and policy, so conflicts are detected across both views of the same storage; an overlay at `/` panics because it would replace the base entirely.
- `impl Vfs for VfsRef` lets a base handle mount under a child router, forwarding the caller's identity so the base's policy and claims apply to operations routed through it.
- `RoutingAccess` resolves the longest-prefix mount per operation, strips the prefix so each backend sees a rooted mount-relative path, and acquires each backend's session lazily on first touch; its `Drop` releases the identity at every touched mount.
- `check_writable` denies mutations on read-only mounts before any state is touched, so a denied operation never partially applies.
- `one_mount` rejects cross-mount rename and copy with `Unsupported`, since backend atomicity guarantees stop at the mount boundary.
- `glob` and `grep` rejoin the mount prefix on returned paths, so callers see full virtual paths while backends see mount-relative ones.
- `HandleAccess` keeps the trait defaults for byte-range reads and the POSIX extras; the public capability exposes neither, so there is nothing to forward to.
- `Router::release` and the mounted handle's `release` are no-ops; teardown flows through the routing session's `Drop`.

Design: new registry @ crates/shared-vfs/src/router.rs::Router
Design: new facade @ crates/shared-vfs/src/router.rs::VfsRefBuilder boundary: pub
Design: new surface-growth @ crates/shared-vfs/src/handle.rs::VfsRef boundary: pub
Design: extends value-object @ crates/shared-vfs/src/path.rs::VfsPathBuf
Design: new pure-function @ crates/shared-vfs/src/router.rs::resolve deps: Mounts,str
Design: new pure-function @ crates/shared-vfs/src/router.rs::strip_mount deps: str
Design: new pure-function @ crates/shared-vfs/src/router.rs::rejoin deps: str
Design: new pure-function @ crates/shared-vfs/src/router.rs::mount_matches deps: str
Plan: vibe/2026-09-11-3-vfs-foundation.md
Adds the generic in-memory backend for the virtual filesystem, carrying the former in-memory store semantics onto the new trait surface: bytes keyed by canonical path, writes that materialize their ancestor directories, and strict removals where absent is an error and a non-empty directory needs the recursive flag. Identity attribution on acquire and release is accepted as a no-op because every session shares the one map and conflict enforcement lives in the claims layer above. Adds a small glob grammar with validation plus a bounded, recursion-free matcher, so a hostile pattern cannot drive exponential time or blow the stack. The backend is exported publicly and its zero value is a meaningful empty filesystem.

- `MemoryBackend` stores files in an ordered map and directories in an ordered set behind a poison-safe shared lock, so listings come out sorted and clones share one storage.
- `MemoryBackend::acquire` accepts the identity as a no-op and every session shares the one map, because conflict enforcement lives in the claims model above the backend.
- `crates/shared-vfs/src/glob.rs` validates patterns against a grammar of literals, single-star within a segment, and double-star as a whole segment, then matches with an iterative dynamic program that is linear and recursion-free; patterns over 1024 bytes are refused outright.
- `MemoryAccess::write` and `MemoryAccess::append` materialize ancestor directories, so no mkdir is needed before a write; both validate the destination before mutating.
- `MemoryAccess::remove` is strict: absent is NotFound, a non-empty directory without the recursive flag is DirectoryNotEmpty, and the namespace root cannot be removed.
- `MemoryAccess::rename` finishes validation before any mutation so a failed rename changes nothing; a directory moves with its whole subtree and cannot move onto the root or into its own descendant.
- `MemoryAccess::glob` returns sorted results that include directories, compiling the pattern once and reusing the tokens across every key while the lock is held.
- `MemoryAccess::stat` reports kind and size with mode and timestamps as None rather than fabricating values.
- `MemoryBackend::release` does nothing; the backend holds no resources and teardown flows through drop.

Design: new surface-growth @ crates/shared-vfs/src/memory.rs::MemoryBackend boundary: pub
Design: new shared-mutable-state @ crates/shared-vfs/src/memory.rs::MemoryBackend
Design: new pure-function @ crates/shared-vfs/src/glob.rs::validate_glob_grammar deps: str
Design: new pure-function @ crates/shared-vfs/src/glob.rs::compile_glob deps: u8
Design: new pure-function @ crates/shared-vfs/src/glob.rs::tokenize_glob deps: u8
Design: new pure-function @ crates/shared-vfs/src/glob.rs::matches_tokens deps: GlobToken,u8
Design: new pure-function @ crates/shared-vfs/src/glob.rs::glob_match deps: u8
Design: new pure-function @ crates/shared-vfs/src/memory.rs::ancestors deps: str
Plan: vibe/2026-09-11-3-vfs-foundation.md
The virtual namespace gains a backend that serves host OS directories directly through standard filesystem calls. It offers two construction modes: identity, where a virtual path is the host path, and rooted, where every resolved path is containment-checked against a canonicalized root directory. Writes, copies, and renames are failure-atomic through a sibling temp file plus rename, and a read-only flag rejects every mutation before anything is touched. The backend is deliberately thin; resolver hardening and symlink policy are left to a later hardening pass.

- `HostBackend::rooted` canonicalizes and validates the directory at construction, returning an error rather than panicking, so every later containment check compares against an already-canonical root.
- `HostBackend::acquire` accepts the execution identity as a no-op because the host filesystem holds no per-identity state; conflict enforcement lives above the backend.
- `atomic_write` creates a sibling temp file, syncs it, and renames it over the destination, removing the temp file when any step fails.
- `HostAccess::exists` returns Ok(false) only for a confirmed absence; permission denials and other I/O failures surface as errors.
- `walk_root` starts the glob walk at the literal prefix's parent directory, so a wildcard-free pattern matches the literal file itself.
- `HostAccess::rename` rejects renaming the mounted root, onto the mounted root, or into its own descendant before any syscall.
- `contain` canonicalizes only the nearest existing ancestor of a candidate, so a dangling symlink inside the root is not itself resolved; writing through one follows host symlink semantics.

Design: new encapsulated-invariant @ crates/shared-vfs/src/host.rs::HostBackend
  boundary: pub
Design: new global-state @ crates/shared-vfs/src/host.rs::TEMP_COUNTER
Design: new pure-function @ crates/shared-vfs/src/host.rs::map_io deps: std::io::Error,str
Plan: vibe/2026-09-11-3-vfs-foundation.md
Introduce a new crate carrying the promptforge-specific virtual filesystem policy: the run-scoped store mount layout, a stock handle constructor, and the editor mode gate. The gate refuses mutations according to the current mode while reads always flow, and the mode lives in a shared cell the UI flips mid-run so the next operation observes the change without executor involvement. The crate depends only on the shared machinery crate, keeping policy apart from generic mechanism.

- `STORE_MOUNT`: fixes the run-scoped store mount path as a public constant so hosts and callers never hardcode it.
- `empty()`: builds a router with a fresh memory backend at the store mount, empty of content but not of mounts, so callers can seed before the run and extract after.
- `ModePolicy`: Ask answers every mutation with an approval verdict naming the rule that fired, Plan permits mutations only to paths ending in `.md` case-sensitively, Agent allows everything, and reads are never gated.
- `ModeHandle`: the UI's cloneable half of the shared mode cell; setting it takes effect on the very next operation, and one-way versus reversible control is just who still holds the handle.
- `is_mutation` and `is_markdown`: the gate's two classification helpers, both pure; the markdown rule is deliberately case-sensitive because virtual paths are POSIX-strict.

Design: new value-object @ crates/promptforge-vfs/src/lib.rs::Mode boundary: pub
Design: new shared-mutable-state @ crates/promptforge-vfs/src/lib.rs::ModePolicy boundary: pub
Design: new shared-mutable-state @ crates/promptforge-vfs/src/lib.rs::ModeHandle boundary: pub
Design: new pure-function @ crates/promptforge-vfs/src/lib.rs::is_mutation deps: Op
Design: new pure-function @ crates/promptforge-vfs/src/lib.rs::is_markdown deps: VfsPath
Plan: vibe/2026-09-11-3-vfs-foundation.md
The store crate inverts from a public trait with pluggable backends into a concrete facade that borrows the caller's VFS capability, so every store operation is attributed to a live identity and participates in the claims model. Write-race detection moves out of a fanout-specific registry into identity claims, which also cover appends and any concurrency shape rather than only fanout arms. The caller-facing contract is preserved: validated logical paths, verbatim reads, anchor edits, numbered line ranges, idempotent delete, and the two-wildcard glob grammar, all reported in the existing error vocabulary. Poison handling changes from surfacing a backend error to poison-safe recovery, so a panicking operation cannot wedge the store.

- `Store` is now a concrete facade borrowing `&Access`: it validates logical paths, joins them onto the store mount prefix, delegates to the capability, and maps VFS failures back onto the store vocabulary.
- `StoreExt` supplies the `vfs.store(&access)` call shape as a prelude-exported extension trait, because the facade type lives above the VFS crates and cannot be an inherent method on `VfsRef`.
- `map_vfs` is the total error mapping: `VfsError::NotFound` becomes `StoreError::NotFound`, `VfsError::Conflict` becomes `StoreError::WriteRace`, and everything else becomes an opaque backend failure.
- `Store::str_replace` implements the anchor rules in the facade as read, count, write: an empty anchor is refused, zero or many matches error, exactly one occurrence is replaced.
- `Store::delete` stays idempotent by mapping `VfsError::NotFound` to `Ok`.
- `Store::glob` delegates matching to the backend under a mount-scoped pattern, refuses backslash patterns itself because the router canonicalizes separators first, and stat-filters matches to files only.
- `StoreRef`, `WriteScope`, `write_scoped`, and `next_write_token` are gone; a second live identity's conflicting write or append now surfaces as `StoreError::WriteRace` through the claims model.
- `MemStore`, `FileStore`, and the crate-private glob matcher are deleted with their modules; backends and matching move to the VFS crates this crate now imports.
- `StorePoisoned` is gone; the poisoned-lock-as-backend-error contract is replaced by recovery, covered by a panic-injection backend test.

Design: strategy -> facade @ crates/promptforge-store/src/lib.rs::Store boundary: pub
Design: removes shared-mutable-state @ crates/promptforge-store/src/lib.rs::StoreRef
Design: new surface-growth @ crates/promptforge-store/src/lib.rs::StoreExt boundary: pub
Design: new pure-function @ crates/promptforge-store/src/lib.rs::map_vfs deps: VfsError,str
Design: new pure-function @ crates/promptforge-store/src/lib.rs::full deps: str
Plan: vibe/2026-09-11-3-vfs-foundation.md
Every entry point that used to take the run's store handle now takes the virtual filesystem handle, and each chain step of a run installs its own access capability so every store operation is attributed to a live identity. The walk and the live pass acquire their own capabilities, a blocking call chain borrows its parent's, and each fanout arm spawns one from the caller, which retires the caller's claims as the happens-before edge. A capability is released when its chain ends, so a finished arm's claims never linger into the join's merge. A hand-built handle lacking the store mount gets a fresh memory store overlaid as a defensive fallback, while a mounted but failing backend fails the run instead of being shadowed. The fanout-specific write registry is gone: conflict detection now comes entirely from the claims model, which also covers appends.

- `Chain` holds the chain step's access capability in an `Option` that `finish` and `abort_subtree` take at chain end, so a finished arm's claims release before the fanout join's merge rather than lingering until scheduler drop. A call chain stores an `Arc` clone of its parent's capability: a blocking child is the same serial thread, so it gets no new identity and never false-conflicts with the caller.
- `ArmTemplate` carries the fanout caller's capability in place of the per-fanout write token; each arm's chain spawns its own capability from it at dispatch, retiring the caller's claims as the happens-before edge, so two live arms touching one path meet the claims model's conflict rule.
- `run_agent` acquires one capability for the whole run and passes it into the agent VM setup, matching the agent's single serial thread of execution.
- `install_store_table` captures an `Arc` clone of the section's capability in each closure and builds the borrowing `Store` facade per call, so every store op is attributed to the identity the executor installed for that chain step.
- `store_mount_present` stats the store mount root through a throwaway capability and treats only `NotFound` as absent; any other error fails the run through the new `Error::Store` variant, so a mounted-but-failing backend is never shadowed by the defensive memory overlay.
- `WriteScope` is removed from the scheduler's arm state, the VM setup struct, and the store crate's re-exports; the claims model is now the only write-conflict guard, and it covers the appends the registry never tracked.
- `fanout-store-writes.md` drops its ready-marker rendezvous and yield section: polling a live sibling's writes is the cross-arm pattern the claims model rejects, so the fixture now writes only arm-scoped paths.

Design: new surface-growth @ crates/promptforge-core/src/execute.rs::run
  boundary: pub
Design: new temporal-coupling @ crates/promptforge-core/src/execute/scheduler.rs::Chain
Repairs: one store path claimed by two live identities fails the second claimant @ crates/promptforge-core/src/execute/scheduler.rs - two live fanout arms appending one path ran untracked instead of failing with a write-write race
Plan: vibe/2026-09-11-3-vfs-foundation.md
Every store operation a script makes now suspends its block as a leaf yield that the driver answers on the blocking pool against the sync store, uniformly for memory and host backends with no inline fast path, so a run's interleaving behavior can no longer depend on which backend serves the mount. A claims conflict between two live execution identities is now fatal to the whole run: the driver intercepts it at the answer boundary and ends the run on the spot rather than resuming it into the script, where author error handling could catch it. Agent VMs keep their direct store closures, since their driver is a single-identity loop with no interleaving for the claims model to govern.

- `install_store_shims` replaces a VM's direct store closures with the yield shims, and section setup installs it only after the shared replay: the shared chunk runs as a main chunk that cannot yield, so its load-time store calls must still reach the direct closures.
- `run_store_op` is the single operation implementation behind both the legacy direct closures and the yield dispatch, so the two paths cannot drift.
- `dispatch_store` runs the operation with spawn_blocking because the store is sync, and fires the operation's observation before posting the answer so the event stream keeps the legacy closure path's ordering.
- `classify_store_failure` maps a claims conflict to the fatal `Error::Determinism`; every other store failure rides back as the call's answer classified `Lua`, exactly as the legacy closures surfaced it.
- `Answer::Store` interception in the driver loop returns the determinism error instead of resuming a chain; parked fanout arms drop unarmed and report cancelled, exactly as on the cancellation path.
- `StoreError::WriteRace` gains a `detail` field carrying the claims model's full conflict diagnosis, exposed through `conflict_detail` and carried verbatim into the fatal violation.
- `parse_store` validates the operation name and author arguments once at the protocol boundary; a wrong shape resumes as the call's error, catchable by an author `pcall` like the legacy callback's conversion failures.
- `two_arms_appending_one_path_boom_without_any_other_suspension` inverts the old premise: the store operation alone is now the interleaving point, so cross-arm same-path appends boom with no other suspension, and exactly one arm's append lands.
- `store_observations` maps each operation to its succeeded/failed observation pair and reports nothing for `exists`, matching the legacy closures event for event.

Design: extends dispatch-on-tag @ crates/promptforge-lua/src/protocol.rs::Request::from_yield
Design: new temporal-coupling @ crates/promptforge-lua/src/coro.rs::install_store_shims deps: Lua boundary: pub
Design: new pure-function @ crates/promptforge-lua/src/protocol.rs::call_optional_line deps: mlua::Table,str
Design: new pure-function @ crates/promptforge-lua/src/protocol.rs::parse_store deps: mlua::Table
Design: new pure-function @ crates/promptforge-core/src/execute/scheduler.rs::store_observations deps: StoreOp
Design: new pure-function @ crates/promptforge-core/src/execute/scheduler.rs::classify_store_failure deps: StoreError
Design: extends surface-growth @ crates/promptforge-core/src/execute/error.rs::RunErrorKind boundary: pub
Design: extends surface-growth @ crates/promptforge-store/src/error.rs::StoreError boundary: pub
Repairs: claims conflicts are fatal and uncatchable @ crates/promptforge-core/src/execute/scheduler.rs - a claims conflict resumed into Lua as a catchable answer an author pcall could swallow
Plan: vibe/2026-09-11-3-vfs-foundation.md
Spike a storage adapter that lets the script engine run its filesystem over the shared virtual filesystem handle, evidencing that the trait subsumes the engine's storage contract. Reads, writes, and directory operations forward through the capability-based access layer, so every script operation is attributed to one execution identity and the claims model sees the whole session as a single thread of execution. Operations the virtual filesystem does not implement report the engine's own unsupported error, and honestly absent metadata becomes deterministic defaults rather than fabricated values.

- `VfsBackend` holds one access capability for the engine session's lifetime: the constructor either captures a fresh identity from the handle or binds a capability the host already holds.
- `crates/promptforge-bashkit/Cargo.toml` takes a spike-only path dependency on the local engine clone with default features off, and the crate stays unpublished.
- `to_io` maps each virtual filesystem error onto the engine's io-error channel, folding claims conflicts and policy denials into PermissionDenied while preserving the source chain.
- `metadata` emits the 0o644 or 0o755 mode defaults and the epoch for absent timestamps instead of inventing the current time.
- `file_type` maps the first four kinds directly and reports the three specials as File with a warning trace.
- `an_ls_cat_grep_script_runs_against_the_store_mount` and its memory-backed sibling run ls, cat, and grep end to end through the engine, asserting on exit code and stdout.
- `symlink`, `read_link`, and `chmod` return the engine's unsupported error rather than faking success.

Design: new constructor-injection @ crates/promptforge-bashkit/src/lib.rs::VfsBackend boundary: pub
Design: new pure-function @ crates/promptforge-bashkit/src/lib.rs::vfs_path deps: Path
Design: new pure-function @ crates/promptforge-bashkit/src/lib.rs::to_io deps: VfsError
Design: new pure-function @ crates/promptforge-bashkit/src/lib.rs::metadata deps: Stat
Design: new pure-function @ crates/promptforge-bashkit/src/lib.rs::unsupported deps: str
Plan: vibe/2026-09-11-3-vfs-foundation.md
Plan: vibe/2026-09-11-3-vfs-foundation.md
The plan removes test-only re-exports from non-test modules across five crates, retires the committed run ledgers, removes the bashkit spike crate, de-interns VfsPath, and makes handle acquisition fallible.

Plan: vibe/2026-09-12-1-test-namespace-vfs-debt.md
Test modules across five crates no longer reach helper names through cfg-gated re-exports on their parent modules; each consumer now imports the name from the module that defines it. The executor drops the compat namespace it kept alive for its test glob, and its scheduler module and run context return to module-private visibility. One artifact submodule becomes crate-visible so cache tests reach a marker-path helper by its real path. No production behavior changes.

- `crates/promptforge-core/src/execute.rs` deletes the test-only re-export block and the stray `ModelSet` re-export, narrows `pub(crate) mod scheduler` to `mod scheduler`, and makes `RunContext` a private import; the executor test module now imports those names from their defining submodules.
- `crates/gateway-local/src/artifacts.rs` widens `mod confine` to `pub(crate) mod confine` so `cache.rs` tests import `source_marker_path` by its real path instead of through a test-only re-export.
- `run_prose_inference` and the test-only `model_client` helper now name `promptforge_lua::Compactor` and `promptforge_model_client::client::GatewayClient` directly in their signatures, replacing aliases that existed only under cfg(test).
- `crates/promptforge-core/src/lua.rs`, `crates/promptforge-core/src/cancel.rs`, `crates/promptforge-core/src/client.rs`, `crates/promptforge-core/src/model.rs`, `crates/promptforge-lua/src/lib.rs`, and `crates/gateway-config/src/config.rs` each lose their cfg(test) re-exports; the six cancellation call sites invoke `scope(` from `promptforge_core_support::cancel` directly.
- No `#[cfg(test)]` import or re-export remains in any non-test module touched here, and no call site changes observable behavior.

Design: removes shim @ crates/promptforge-core/src/execute.rs
Plan: vibe/2026-09-12-1-test-namespace-vfs-debt.md
Deletes the two tracked ledger files that recorded accumulated run history, one at the repository root and one under the run directory. That history is no longer kept in version control.

- `vibe-ledger.md` - Deleted from the repository root; it held the accumulated run ledger.
- `vibe/vibe-ledger.md` - Deleted; it held the same kind of accumulated run ledger under the run directory.

Plan: vibe/2026-09-12-1-test-namespace-vfs-debt.md
Removes the spike crate that adapted the script engine's filesystem backend trait over the shared virtual filesystem handle. The spike existed to prove the filesystem trait subsumes the engine's storage contract; that evidence is recorded, so the crate has no remaining purpose. Its manifest pinned a path dependency on a sibling checkout outside the repository, which broke dependency resolution on any machine without that checkout. The lockfile is regenerated, dropping the engine and every dependency that existed only to serve it.

- `crates/promptforge-bashkit` is removed outright rather than relocated into a spikes directory or excluded from the workspace manifest; both alternatives keep dead weight whose evidence is already recorded.
- `Cargo.lock` drops seventeen packages with the regeneration, including the engine crate and its exclusive transitive dependencies, and collapses the duplicate sha1 and fancy-regex entries each to one version.
- `crates/promptforge-bashkit/src/lib.rs` needs no caller edits alongside its deletion; no source outside the removed crate changes.

Plan: vibe/2026-09-12-1-test-namespace-vfs-debt.md
Virtual paths now hold their canonical string in a reference-counted allocation per value lineage instead of an id into a process-global interner, so a dropped path's string frees with its last owner rather than leaking for the life of the process. The zero-dependency manifest check now recognizes dependency sub-tables and target-qualified tables, closing a bypass that let a dependency sub-table declare a dependency undetected. The architecture document now describes the store facade and the virtual filesystem layer as separate components with their real dependency directions.

- `VfsPath` replaces its u32 interner id with an `Arc<str>` field; the type loses `Copy`, and `as_str` returns a borrow of self instead of a static string.
- `is_dependency_table` extracts the manifest test's section matching into one helper that counts exact dependency tables, their sub-tables, and target-qualified tables as dependency declarations.
- `vibe/archdoc.md` replaces the store component line with a Store facade entry that depends on the VFS layer and adds a VFS layer entry covering the shared machinery and the policy gate.
- `gate` hands the claims table its own clone of the canonical path, so the string frees when the claim and every other owner drop.
- `canonicalize` allocates one shared string per call; there is no global table and no lock.
- `crates/shared-vfs/src/router.rs` and the host and handle internals now pass paths by reference, a mechanical adaptation to the loss of Copy.
- `Interner` is gone along with its process-global mutex and the u32-exhaustion panic.

Design: removes global-state @ crates/shared-vfs/src/path.rs::interner
Design: new pure-function @ crates/shared-vfs/src/lib.rs::tests::is_dependency_table deps: &str
Repairs: a dropped path's string frees with its last owner @ crates/shared-vfs/src/path.rs::canonicalize - the interner leaked every distinct canonicalized path for the process lifetime
Repairs: the zero-dependency manifest check rejects every dependency table @ crates/shared-vfs/src/lib.rs::tests::is_dependency_table - a [dependencies.foo] sub-table declared a dependency while passing the exact-match section check
Plan: vibe/2026-09-12-1-test-namespace-vfs-debt.md
The virtual filesystem handle used to panic when a backend refused to vend an execution identity, even though the backend trait's contract has always allowed refusal. Acquisition and spawn now return errors instead: the executor surfaces a refusal as its store error, and the agent surfaces it as a program failure before the run starts. A failed spawn leaves the parent capability's claims registered, so the conflict bookkeeping stays intact for later identities. Callers, tests, and doc examples update mechanically to the fallible signatures.

- `VfsRef::acquire` and `acquire_with` now return `Result<Access, VfsError>` and propagate a backend refusal with `?`; the panic-on-refusal path is deleted.
- `spawn` returns `Result<Access, VfsError>`, and a failed spawn leaves the parent's claims registered: `a_backend_refusal_fails_spawn_and_keeps_the_parents_claims` shows a second identity still conflicting with the parent's write.
- `install_root_slots` and the fan-out arm refill map a refusal to `Error::Store`; the agent's `drive` maps it to `AgentError::Program` with the source error attached.

Design: new surface-growth @ crates/shared-vfs/src/handle.rs::VfsRef::acquire boundary: pub
Design: new surface-growth @ crates/shared-vfs/src/handle.rs::Access::spawn boundary: pub
Design: new shotgun-surgery @ crates/shared-vfs/src/handle.rs::VfsRef::acquire
Repairs: Vfs::acquire's Result contract permits backend refusal @ crates/shared-vfs/src/handle.rs::VfsRef::acquire - a refusing backend panicked the process instead of returning the error
Repairs: Vfs::acquire's Result contract permits backend refusal @ crates/shared-vfs/src/handle.rs::Access::spawn - a refusing backend panicked instead of returning the error
Plan: vibe/2026-09-12-1-test-namespace-vfs-debt.md
Plan: vibe/2026-09-12-1-test-namespace-vfs-debt.md
Reworks the repository's contributor policy. The four cross-product dependency rules leave the engineering list for a new structure section that names each crate family, states what it may not depend on, adds the shared and build crate kinds, and binds normal, development, build, and target-specific dependencies alike. The do-more-with-less bullet grows into a principles section with an explicit reuse-then-improve-then-add priority order, and a roles section states what each of the three products is. Two design records for the finished gateway speech work leave the tree because they now live in the design repository.

- `AGENTS.md` gains Principles, Roles, and Structure sections. The Structure section restates the dependency rules per crate family with naming conventions, adds the shared-* and build-* crate kinds, and extends the rules to normal, dev, build, and target-specific dependencies.
- `AGENTS.md` drops the four-rule dependency bullet and the do-more-with-less bullet from Engineering; the new sections subsume both.
- `design/design-gateway-tts-phase-1.md` and `design/note-gateway-tts-phase-1-verification.md` are deleted; both records moved to the design repository.

Plan: vibe/2026-09-12-2-dependency-rules-vfs-hook.md
The wire protocol crate leaves the shared product family and joins the gateway family, since every crate that consumes it is a gateway crate. The rename is mechanical across manifests, imports, the lockfile, and docs; no wire type, trait, or helper changes behavior. The dependency boundary check gains a fifth rule forbidding any shared crate from depending on a gateway, promptforge, or workshop crate, and workshop membership becomes prefix-based so a prefixed package can no longer slip past the workshop rule.

- `crates/gateway-protocol/Cargo.toml` declares the crate under its new name with the product qualifier dropped from the description and `publish = false` kept.
- `crates/gateway-protocol/AGENTS.md` restates the crate as a gateway crate and replaces the no-dependency-back-into-Gateway rule with a single shared error surface for every gateway consumer.
- `PackageSet` gains `Shared` (prefix `shared-`) and `AnyProduct` variants, and `Workshop` now matches any `workshop-` prefix instead of only `workshop` and `workshop-server`.
- `PRODUCT_DEPENDENCY_RULES` grows from four rows to five with a Shared-cannot-depend-on-AnyProduct row, so the workspace check rejects a shared crate reaching into any product.
- `adversarial_metadata_triggers_each_product_dependency_rule` adds `workshop-shell` and `shared-source` fixture violators and expects seven violations instead of five.
- `crates/gateway-protocol/src/` moves `error.rs`, `http_util.rs`, `lib.rs`, `upstream.rs`, and `wire.rs` without a single content change, so the rename carries no behavior risk inside the crate.

Design: new shotgun-surgery @ crates/gateway-protocol/Cargo.toml
Repairs: Workshop boundary membership @ crates/gateway-stt/tests/it/architecture.rs::PackageSet - a workshop-prefixed package depending on a gateway crate passed the product dependency check
Plan: vibe/2026-09-12-2-dependency-rules-vfs-hook.md
A store operation's access capability now drops after the operation and its observation but before the answer posts, so the claims it holds release before a resumed chain can acquire overlapping claims. Only the timing of the release changes, never whether an operation succeeds. The Unix file-mode helper also gains a lint expectation recording that its optional return exists to unify the platform signatures, since the non-Unix variant has no mode bits to return.

- `drop(access);` places the capability clone's destruction after the operation and its observation and before the answer posts, and the accompanying comment names the claims-release ordering constraint the placement enforces.
- `clippy::unnecessary_wraps` is now expected on the Unix `mode_of`, whose `Option` return unifies the platform signatures because the non-Unix variant returns `None`.
- Neither `mode_of` nor the `drop(access);` ordering gains a regression test in this change.

Design: new temporal-coupling @ crates/promptforge-core/src/execute/scheduler.rs::dispatch_store
Plan: vibe/2026-09-12-2-dependency-rules-vfs-hook.md
Every filesystem capability now carries a caller-supplied origin, a label plus the most precise source position the caller knows, and a handle with an installed operation sink fires it on every admitted operation, after policy and claims pass and before the backend executes. The origin is pure observability: claims still key on the internal identity, a policy-denied operation never fires, and a mounted-handle forward fires nothing rather than doubling the event with a less precise label. The executor and the agent stamp the prompt's position explicitly, while host code, probes, and tests get the Rust call site stamped for free.

- `Origin` is a non-exhaustive struct with mandatory label, file, and line; `Origin::new` stamps the Rust call site through `#[track_caller]`, and `Origin::at` sets an explicit position so the executor and agent can substitute the prompt's position.
- `on_op` installs the operation sink, which must be cheap because store operations fire it inline from the blocking pool; `policy` installs the handle's policy the same way, defaulting to `AllowAll`.
- `fire` runs after policy and claims pass and before the backend executes; rename and copy fire one event per canonical path, and the crate-private mount forward carries no origin so the outer handle's event is never doubled.
- `crates/shared-vfs/AGENTS.md` gains the most-specific-label rule - a section name for a chain, a tool id for a tool, a fixture name for a test - so the guidance lands as instructions, not only doc comments.
- `acquire` and `spawn` now take a mandatory origin, and every call site in this change passes one: the scheduler derives prompt positions through `prompt_origin` and `first_chunk_line`, while fixtures and probes label themselves.
- `a_sink_receives_events_in_order_with_op_path_and_label` proves ordering, op, path, and label through a recording sink, and a policy-denied operation is proven never to fire; the only sink installations in this change are those tests, with the bounded event log, the Lua pull query, and enrichment policies named in the module docs as later subscribers.

Design: new event-hook @ crates/shared-vfs/src/router.rs::VfsRefBuilder::on_op boundary: pub
Design: new surface-growth @ crates/shared-vfs/src/handle.rs::VfsRef::acquire boundary: pub
Design: new pure-function @ crates/promptforge-core/src/execute/scheduler.rs::first_chunk_line deps: Block
Design: new pure-function @ crates/promptforge-core/src/execute/scheduler.rs::prompt_origin deps: Block,Prompt,str
Plan: vibe/2026-09-12-2-dependency-rules-vfs-hook.md
Plan: vibe/2026-09-12-2-dependency-rules-vfs-hook.md
A run that ended while a leaf op was still in flight could deliver its result before the op's access clone was gone, so a fresh access acquired after the run could meet a lingering claim. `drive` now funnels every terminal outcome through `drain_io_tasks`, which aborts each recorded task and then awaits it; aborting a blocking-pool op detaches rather than interrupts, so only the join bounds the release of the claims the op's access clone holds.

- `io_tasks` stores `JoinHandle<()>` instead of `AbortHandle` so a terminal outcome can await completion, not only abort.
- `abort_subtree` keeps the aborted handle in `io_tasks` instead of removing it, so the run-end drain also covers a fanout-aborted sibling op; a late answer that arrives first still takes the handle in the answer loop.
- `a_terminal_failure_releases_an_in_flight_arms_claims_before_returning` gates a winning arm's append behind `AppendGate` in a `GatedStore` backend and fails without the drain with the same WriteRace signature CI showed on Linux.
@vinniefalco
vinniefalco merged commit 5b79db3 into cppalliance:master Sep 12, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant