Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This repository currently contains the product foundation and alpha planning art
- [.plans](.plans/README.md) - MVP execution plan with static HTML pages for phases, architecture, and validation.
- [docs/alpha-cli-distribution.md](docs/alpha-cli-distribution.md) - GitHub Release packaging, server-owned storage setup, and two-device alpha smoke testing.
- [docs/devbox/loom-and-devbox.md](docs/devbox/loom-and-devbox.md) - the product/engine split and the vocabulary to use in new work.
- [docs/devbox/workspace-adapters-alpha.md](docs/devbox/workspace-adapters-alpha.md) - current sparse folder, agent workspace, materialized fallback, and filesystem adapter alpha state.
- [docs/architecture/loom-storage-consistency.md](docs/architecture/loom-storage-consistency.md) - current storage consistency guarantees, non-guarantees, and evidence path.
- [docs/evidence/alpha-readiness.md](docs/evidence/alpha-readiness.md) - concise alpha evidence and the canonical smoke commands.

Expand Down Expand Up @@ -243,6 +244,7 @@ admin/debug smoke tests, but they are not the product path.

Alpha helper scripts:

- `scripts/alpha-workspace-adapters-smoke.ps1` runs the workspace adapter alpha proof for sparse folders, agent virtual sessions, materialized sandbox fallback, and filesystem adapter truthfulness.
- `scripts/alpha-two-device-smoke.sh` runs a local two-device proof with pairing, pending receiver refusal, live publish, latest pull, materialization, and redacted evidence logs.
- `scripts/devbox-live-sync-alpha.sh` maps `.env` values into a live daemon command for local, hosted object-transfer, or trusted direct-S3 remotes.
- `scripts/package-cli.sh <VERSION>` builds macOS/Linux alpha tool archives with Loom, Devbox CLI, daemon, metadata server, docs, separate user/operator env templates, and helper scripts.
Expand Down
6 changes: 4 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ a compatibility surface because developers use it today, but Git is not the foun
trying to build.

Start with [Loom And Devbox](devbox/loom-and-devbox.md) before reading older architecture slices.
For current alpha proof, read [Alpha Readiness Evidence](evidence/alpha-readiness.md) and run
`scripts/mvp-two-device-smoke`.
For current alpha proof, read [Alpha Readiness Evidence](evidence/alpha-readiness.md) and
[Workspace Adapter Alpha](devbox/workspace-adapters-alpha.md). Run `scripts/mvp-two-device-smoke`
for the product MVP path and `scripts/alpha-workspace-adapters-smoke.ps1` for sparse folder,
agent workspace, materialized fallback, and filesystem adapter evidence.

For the intended full-scale repository and language split, read
[Full-Scale Project Shape](architecture/full-scale-project-shape.md).
Expand Down
1 change: 1 addition & 0 deletions docs/devbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ this folder for user-facing product, platform, hosted API, account, device, and
Current CLI model docs:

- [Sparse Folders](sparse-folders.md)
- [Workspace Adapter Alpha](workspace-adapters-alpha.md)
148 changes: 148 additions & 0 deletions docs/devbox/workspace-adapters-alpha.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Workspace Adapter Alpha

This page is the current-state map for the Devbox/Loom workspace adapter arc. Devbox is the product
for folder continuity across machines. Loom is the engine boundary for objects, file versions, folder
revisions, cache entries, pins, checkpoints, cursors, overlays, and sandboxes.

The alpha adapter work proves explicit sparse folder flows, agent virtual sessions, materialized
sandbox fallback, and filesystem adapter boundaries. It does not claim native OS placeholder files
or hydrate-on-open behavior.

## Evidence Command

Run the adapter smoke locally:

```text
powershell -ExecutionPolicy Bypass -File scripts/alpha-workspace-adapters-smoke.ps1
```

The script builds local binaries if needed, starts a temporary in-memory `devbox-api`, uses only
temporary folders, and writes redacted logs under the printed evidence directory.

It proves:

- human sparse folder flow through `devbox login`, `devbox share`, `devbox clone --sparse`,
`devbox status`, `devbox hydrate`, `devbox warm`, `devbox keep`, and `devbox free-space`
- agent virtual workspace flow through `loom workspace open/read/write/exec/diff/checkpoint/discard`
- materialized sandbox fallback for real commands, including safe capture and unsafe host mutation
refusal
- filesystem adapter alpha behavior, including native fail-closed reporting and `local-dev`
metadata-only mount/status/unmount simulation

## Sparse Folder Quickstart

Use `devbox` for the human product flow:

```text
devbox login --api <DEVBOX_API_URL> --account <ACCOUNT> --device-name "Desktop"
devbox share <folder> --no-background-sync

DEVBOX_CONFIG_DIR=.devbox-laptop \
devbox login --api <DEVBOX_API_URL> --account <ACCOUNT> --device-name "Laptop"
DEVBOX_CONFIG_DIR=.devbox-laptop \
devbox clone <shared-folder-name> <target-folder> --sparse --no-background-sync

DEVBOX_CONFIG_DIR=.devbox-laptop devbox status <target-folder>
DEVBOX_CONFIG_DIR=.devbox-laptop devbox hydrate <target-folder>/README.md
DEVBOX_CONFIG_DIR=.devbox-laptop devbox warm <target-folder> --max-bytes 4096
DEVBOX_CONFIG_DIR=.devbox-laptop devbox keep <target-folder>/README.md
DEVBOX_CONFIG_DIR=.devbox-laptop devbox free-space <target-folder> --max-bytes 0
```

`hydrate` downloads and materializes a requested path now. `warm` chooses useful small source,
manifest, and config files. `keep` records offline retention intent; it does not download missing
bytes by itself. `free-space` only removes clean, unpinned local bytes when Devbox can prove a
hosted copy exists.

Sparse folders are explicit CLI workflows today. Cloud-only files do not appear as placeholder files
in Explorer, Finder, or shells.

## Agent Workspace Workflow

Agents use Loom workspace sessions over a folder revision:

```text
loom track <folder>
loom workspace open <folder> --session agent-1
loom workspace list <folder> --session agent-1
loom workspace read <folder> --session agent-1 README.md
loom workspace exec <folder> --session agent-1 -- cat README.md
loom workspace write <folder> --session agent-1 src/change.txt --text "agent edit"
loom workspace diff <folder> --session agent-1
loom workspace checkpoint <folder> --session agent-1 -m "agent checkpoint"
```

The virtual adapter reads base revision metadata and overlay files without exposing the full source
folder as a mutable host worktree. Writes land in the session overlay until checkpoint. Checkpoint
coalesces overlay file versions into a folder revision and creates a human-readable checkpoint. A
session that should be abandoned uses:

```text
loom workspace discard <folder> --session agent-1
```

## Virtual Versus Materialized Execution

`loom workspace exec` is a virtual command surface. It supports deterministic commands such as
`pwd`, `ls`, `cat`, `stat`, `rg`, and `write`. Unsupported commands return a clear failure and tell
the caller to use materialized sandbox fallback.

Use materialized fallback when a real shell command or tool needs a filesystem:

```text
loom workspace materialize-run <folder> --session agent-1 -- <COMMAND> [ARGS...]
```

Materialized fallback creates an isolated sandbox from the session view, runs the command there, and
captures safe changes back into the overlay. It refuses deleted tracked files, secret-looking files,
generated/dependency paths, and mutations to the real shared folder outside the sandbox capture.

This fallback is for commands that need real files; it is not permission to mutate the host folder
directly.

## Filesystem Adapter Alpha

The filesystem adapter boundary is exposed through:

```text
loom fs status <folder>
loom fs mount <folder> --mount <path>
loom fs mount <folder> --adapter local-dev --mount <path>
loom fs status <folder> --adapter local-dev --mount <path>
loom fs unmount <folder> --adapter local-dev --mount <path>
```

Native adapters for Windows, macOS, and Linux are alpha stubs. They report host direction and fail
closed for mount. They do not record successful native mount metadata, do not create placeholder
files, and do not support hydrate-on-open.

`--adapter local-dev` is a deterministic metadata simulation for tests and wiring. It records mount
state under `.loom/fs`, reports status, and supports idempotent unmount. It does not create the
mount path, project files into the OS, or hydrate bytes on open.

## Safety Guarantees

The current alpha keeps these guarantees:

- Loom captures file versions frequently and coalesces folder revisions at stable boundaries such as
commands, sync, restore, sandbox merge, and checkpoint.
- Cache metadata records object byte availability separately from file versions and folder
revisions.
- Sparse cleanup keeps dirty files, pinned files, unsupported local entries, and files without
hosted proof.
- Agent overlay writes and materialized captures re-check secret/generated/dependency policy before
object bytes enter the cache.
- Materialized fallback refuses host shared-folder mutation outside the sandbox.
- Native filesystem adapters fail closed until real OS integrations exist.

## Non-Goals

These are not implemented in the current alpha:

- native Windows Cloud Files, Projected File System, macOS File Provider, macFUSE, or Linux FUSE
drivers
- placeholder files in normal OS file browsers or shells
- hydrate-on-open, sparse reads, or kernel callback hydration
- remote protocol v2, chunk transfer, compression, or lazy byte-range transport
- broad automatic conflict resolution
- weakening secret, generated dependency, or unsupported filesystem policies
26 changes: 25 additions & 1 deletion docs/evidence/alpha-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ polish PR.

## Canonical Proof Path

Run:
For the product MVP path, run:

```text
scripts/mvp-two-device-smoke
Expand All @@ -21,6 +21,17 @@ The smoke builds local binaries when needed, starts a temporary `devbox-api` wit
metadata, simulates two machines, and writes redacted logs under the printed evidence directory.
It does not require live R2 or Postgres.

For the workspace adapter alpha path, run:

```text
powershell -ExecutionPolicy Bypass -File scripts/alpha-workspace-adapters-smoke.ps1
```

That smoke uses a temporary in-memory `devbox-api` plus local Loom folders to prove sparse folder
intents, agent virtual sessions, materialized sandbox fallback, and filesystem adapter alpha
truthfulness. It is local-dev evidence only; it does not claim native hydrate-on-open or OS
placeholder support.

## What The Smoke Proves

- Loom can track, checkpoint, sync, clone, sparse clone, hydrate, evict, pin, cache status, and
Expand All @@ -36,6 +47,19 @@ It does not require live R2 or Postgres.
object cache, and evidence logs.
- Divergent cursor/conflict states refuse safely instead of auto-merging.

## Workspace Adapter Alpha Proofs

- Devbox sparse clone starts metadata-only, then `status`, `hydrate`, `warm`, `keep`, and
`free-space` expose cache intent without treating cloud-only files as deletions.
- `free-space` succeeds only for clean, unpinned local bytes with hosted proof and refuses when that
proof is missing.
- Loom agent workspace sessions can virtual-read, virtual-exec, write overlays, diff, checkpoint,
and discard without materializing the whole folder.
- Materialized fallback runs a real command in an isolated sandbox, captures safe source changes,
and refuses host shared-folder mutation outside capture.
- Native filesystem adapters report unsupported mount status truthfully and record no success, while
`--adapter local-dev` records only metadata-only simulated mount/status/unmount state.

## Focused Checks

Useful targeted checks for this PR:
Expand Down
15 changes: 8 additions & 7 deletions loom/crates/loom-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -935,15 +935,15 @@ fn run_cache_status(folder: Option<PathBuf>) -> Result<(), String> {
}

fn run_cache_free_space(args: &[String], command: &str) -> Result<(), String> {
let parsed = parse_cache_prune_args(args)?;
let parsed = parse_cache_prune_args(args, command)?;
let store = open_store_from_optional_folder(parsed.folder)?;
let versions =
tracked_versions_for_scope(&store, Path::new("")).map_err(|error| error.to_string())?;
let no_remote_proof = BTreeSet::new();
let current_status = cache_status_for_scope(&store, Path::new(""), &no_remote_proof)
.map_err(|error| error.to_string())?;
let remote_available_objects = if current_status.hydrated_bytes() > parsed.max_bytes {
remote_available_objects_for_versions(&store, &versions, "cache prune")?
remote_available_objects_for_versions(&store, &versions, command)?
} else {
no_remote_proof
};
Expand Down Expand Up @@ -2725,7 +2725,7 @@ fn parse_sync_daemon_args(args: &[String], command: &str) -> Result<SyncDaemonAr
})
}

fn parse_cache_prune_args(args: &[String]) -> Result<CachePruneArgs, String> {
fn parse_cache_prune_args(args: &[String], command: &str) -> Result<CachePruneArgs, String> {
let mut folder = None;
let mut max_bytes = None;
let mut index = 0;
Expand All @@ -2742,16 +2742,17 @@ fn parse_cache_prune_args(args: &[String]) -> Result<CachePruneArgs, String> {
} else if let Some(value) = arg.strip_prefix("--max-bytes=") {
max_bytes = Some(parse_u64_flag("--max-bytes", value)?);
} else if arg.starts_with('-') {
return Err(format!("cache prune unknown option '{arg}'"));
return Err(format!("{command} unknown option '{arg}'"));
} else if folder.replace(PathBuf::from(arg)).is_some() {
return Err("cache prune accepts at most one folder".to_string());
return Err(format!("{command} accepts at most one folder"));
}
index += 1;
}

let max_bytes = max_bytes.ok_or_else(|| {
"cache prune requires --max-bytes <BYTES>\nUsage: loom cache prune --max-bytes <BYTES> [FOLDER]"
.to_string()
format!(
"{command} requires --max-bytes <BYTES>\nUsage: loom {command} --max-bytes <BYTES> [FOLDER]"
)
})?;

Ok(CachePruneArgs { folder, max_bytes })
Expand Down
37 changes: 37 additions & 0 deletions loom/crates/loom-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ fn command_help_prints_usage() {
assert!(stdout.contains("Status: implemented for the local offline engine"));
}

#[test]
fn cache_free_space_errors_use_free_space_wording() {
let output = run_loom(["cache", "free-space", "--bad"]);

assert!(!output.status.success());
let stderr = stderr(&output);
assert!(stderr.contains("cache free-space unknown option '--bad'"));
assert!(!stderr.contains("cache prune unknown option"));
}

#[test]
fn workspace_help_lists_agent_session_commands() {
let output = run_loom(["workspace", "--help"]);
Expand Down Expand Up @@ -1431,6 +1441,33 @@ fn cache_prune_refuses_without_remote_object_proof() {
assert!(status_stdout.contains("cache hits/misses: not measured yet"));
}

#[test]
fn cache_free_space_refusal_uses_free_space_wording_without_remote() {
let dir = tempfile::tempdir().expect("temp dir");
let fixture = dir.path().join("fixture");
std::fs::create_dir_all(&fixture).expect("fixture creates");
std::fs::write(fixture.join("README.md"), "local only\n").expect("readme writes");

assert_success(&run_loom(["track", fixture.to_str().expect("UTF-8 path")]));

let free_space = run_loom([
"cache",
"free-space",
"--max-bytes",
"0",
fixture.to_str().expect("UTF-8 path"),
]);

assert!(!free_space.status.success());
let stderr = stderr(&free_space);
assert!(stderr.contains("cache free-space refused because no Loom remote is configured"));
assert!(!stderr.contains("cache prune refused because no Loom remote is configured"));
assert_eq!(
std::fs::read_to_string(fixture.join("README.md")).expect("readme reads"),
"local only\n"
);
}

#[test]
fn cache_status_counts_materialized_duplicate_objects_once_per_present_file() {
let dir = tempfile::tempdir().expect("temp dir");
Expand Down
Loading
Loading