SDK reference
Agent SDK
@layerfs/agent is everything an agent does on one project: 32 calls in six groups. Every call takes positional arguments and returns a Promise; none takes an options object except where a signature below shows one. Each call is also one shell command, shown beside its signature: thelayerfs command makes the same call and prints its answer as JSON.
For coding agents: the layerfs-js-sdk skill is the guide an agent reads to work through the SDK: SKILL.md and one reference file per group, linked under each heading below. Where the skill and this page differ, this page follows the SDK source.
How an agent uses it
Agent skillSKILL.md · How an agent works
Every tool call an agent makes is one step, and every step that changes files becomes a Commit on the agent's Branch. Two ways in: fs for cheap, targeted file work, and a Workspace for Bash commands.
| Agent tool | LayerFS calls | Recorded as |
|---|---|---|
| Read, list, search, diff | fs.read, fs.list, fs.grep, fs.glob, fs.diff_files | Nothing: reads don't change history |
| Write, edit, many-file change | fs.write, fs.edit, fs.apply, fs.mkdir, fs.mv, fs.cp, fs.rm | One Commit per call; the whole change lands or nothing does |
| Bash | workspace.mount → exec → commit → unmount | One Commit with everything the command changed: sources, lockfiles, node_modules, build output |
- Prefer
fsfor one small read or edit. It runs in your own process, against the Store, with no container and no mount. - Use a Workspace for commands: installs, builds, tests, generators, and changes across many files where one command beats many calls. It lives in a Sandbox you name: start one with
sandbox.create, mount as many Workspaces there as you need, and close it when you're done. - Commit after every Bash call, even a failing one, with a message naming the step and its result, such as
"step 12: npm test (exit 1)". A step that changes nothing answersUpToDateand makes no Commit.
The client
Agent skillSKILL.md · Import the SDKSKILL.md · Connect to a project
A client is one project, opened in this process. LayerFS.open connects to a project that exists;LayerFS.init creates it first. Both answer a ready client. There is no new LayerFS(…), and no options argument: every setting comes from the environment, read once when the client is made.
import { LayerFS, isRefusal } from "@layerfs/agent";
const layerfs = await LayerFS.open("shop"); // opens the project's Store; NoSuchProject if it doesn't exist
try {
// … Store calls and Workspace calls …
} finally {
await layerfs.close(); // releases this process's Store; Workspaces stay mounted
}LayerFS.open
LayerFS.open(name)
no command: every layerfs command opens the project and closes it
Returnsan open client
RefusesNoSuchProject, InvalidInput (a bad name, or an options object), Unsupported (Store not on local Linux)
Opens the project's Store and looks the project up once, so a missing project is refused at once and nothing stays open. The name is 1 to 63 of a-z 0-9 _ - ., starting and ending with a letter or digit.
LayerFS.init
LayerFS.init(name, sourcePath, timeout = 300000)
layerfs init <name> <source-dir> [--timeout <n>]
Returnsan open client; Init's answer is on its created: { kind: "Initialized", project, layer, root, store }, where layer is main's first Layer
RefusesExists (already initialized: open it), InvalidInput (a bad path, name or timeout, or a source entry it can't store; detail names it), Failed with Deadline in the message (it ran past timeout), Unknown / Failed (stop and report)
Copies a folder into a new Store as main @ L0, once. Everything is imported, including.git and ignored files. timeout is in milliseconds, a positive whole number; there is no unlimited option, and a large tree takes a while. store is the installed Store file. Init's temporary sealed copy is deleted once the install is recorded; if that delete fails, the answer carriesleftover: { path, message } and the project is still ready.
SDKInit once, then open
// 1. Init reads the source tree once, ignored files included, makes it
// main's first Layer, and answers an open client.
const layerfs = await LayerFS.init("shop", "/work/shop")
layerfs.created
// → { kind: "Initialized", project: "shop", layer: "324a…", root: "e34e…", store: "/layerfs-store/global/store.sqlite" }
// 2. A second Init of the same name is refused. The project already exists:
// open it.
await LayerFS.init("shop", "/work/shop")
// → LayerFSError: Exists
// 3. Later processes open the project without reading the source again.
const later = await LayerFS.open("shop")
await later.project.list_branches()
// → { items: [{ name: "main", id: null, head: null, … }], hasMore: false }layerfs init shop /work/shoplayerfs init shop /work/shoplayerfs project list_brancheslayerfs.close
layerfs.close()
no command: every layerfs command opens the project and closes it
Returnsnothing
Releases the Store; the process's last client of it closes it. Workspaces stay mounted. Every later call on the client is refused InvalidInput, and a second close does nothing.await using layerfs = await LayerFS.open("shop") closes it at the end of the block.
Environment
Set these for every process of one run, with the same values. The SDK and the layerfs command read only these LAYERFS_ names; an integration that can set only names with its own prefix maps them to these in its own wrapper.
| Variable | Meaning |
|---|---|
LAYERFS_HOME | Private folder (mode 0700) where LayerFS records projects and Sandboxes |
LAYERFS_STORE_PATH | The installed Store file that Store calls open in this process: global/store.sqlite in the project's volume |
LAYERFS_DOCKER | Docker Engine socket; default /var/run/docker.sock |
LAYERFS_IMAGE | Runtime image digest, sha256:…, that a new Sandbox runs |
LAYERFS_VOLUME | Existing Docker volume shared by the project's daemons |
LAYERFS_DAEMON | Host path to the built Linux daemon |
LAYERFS_CONTAINER_PREFIX | Optional: names containers <prefix><project>-<n>; default layerfs- |
LAYERFS_DAEMON_HOST | Optional: the host this process dials for a daemon, such as host.docker.internal from inside a container |
LAYERFS_STORE_POLICY | Optional, Init only: JSON { smallFileThreshold, wholeFileDepth, chunkDepth, metadataDepth } |
LAYERFS_NATIVE | Optional: the native addon; default api/binding/layerfs.node |
LAYERFS_WORKER_ENDPOINT | macOS and Windows: the endpoint file of the LayerFS home's worker, <home>/worker/endpoint.json, which then makes every call. Any integration may set it |
LAYERFS_TRACE, LAYERFS_TRACE_STEP, LAYERFS_TRACE_START | Optional: a file that gets one JSON line per call, a step label for each line (read at every call), and 1 for a start line before each call |
LAYERFS_PROJECT | The layerfs command only: the project, when --project isn't given |
project
Agent skillmethods/project.md
project.list_branches
list_branches(pageSize = 50, pagePos = 0)
layerfs project list_branches [--page-size <n>] [--page-pos <n>]
Returnsa page of { name, id, base, head, forkedFrom }, main first
RefusesNoSuchProject, InvalidInput
For main, id and head are null and base is its current Layer.
SDKCheck for a Branch before forking
// 1. Branches come in pages, main first.
const page = await layerfs.project.list_branches(50, 0)
// → { items: [{ name: "main", … }, { name: "cart", base: "324a…", head: "c3c2…", forkedFrom: … }], hasMore: false }
// 2. Fork only if the name is free; forking a taken name is refused.
if (!page.items.some((b) => b.name === "search")) {
await layerfs.branch.fork("search")
}
// → { kind: "Forked", branch: "search", head: null }layerfs project list_branches --page-size 50layerfs project list_branches | jq -e '.items[] | select(.name == "search")' >/dev/null \ || layerfs branch fork searchproject.list_sandboxes
list_sandboxes(pageSize = 50, pagePos = 0)
layerfs project list_sandboxes [--page-size <n>] [--page-pos <n>]
Returnsa page of { sandbox, name, image, workspaces }, lowest number first
The project's running Sandboxes, from their records. It starts nothing and dials no daemon; a Sandbox that closed itself or whose container is gone is left out.
const { items } = await layerfs.project.list_sandboxes();
for (const s of items) console.log(s.sandbox, s.name, s.workspaces); // "1" "layerfs-shop-1" 2branch
Agent skillmethods/branch.md
branch.fork
fork(name, fromRef = "main")
layerfs branch fork <name> [<from-ref>]
Returns{ kind: "Forked", branch, base, head }; head is null when forked from a main Layer
RefusesNameTaken, ReservedName (main), NoSuchBranch / NoSuchCommit
Creates a Branch from main, a Branch, a Commit or a Layer. Metadata only: nothing is copied.
SDKFork from main
// 1. A fork from main starts at its newest Layer and has no Commit yet.
await layerfs.branch.fork("cart")
// → { kind: "Forked", branch: "cart", base: "324a…", head: null }
// 2. Its first write makes its first Commit.
await layerfs.fs.write("cart", "/NOTES.md", "Cart rewrite\n", "Add notes")
// → { kind: "Committed", head: "c1a0…" }layerfs branch fork cartprintf 'Cart rewrite\n' | layerfs fs write cart /NOTES.md -m "Add notes"SDKFork a fork, or any Commit
// 1. Fork from another Branch's head. Nothing is copied, at any depth.
await layerfs.branch.fork("cart.1", "cart")
// → { kind: "Forked", branch: "cart.1", base: "324a…", head: "c3c2…" }
// 2. Or from an older Commit, to try again from an earlier step.
await layerfs.branch.fork("cart.retry", "c1a0…")
// → { kind: "Forked", branch: "cart.retry", head: "c1a0…" }layerfs branch fork cart.1 cartlayerfs branch fork cart.retry c1a0…SDKA name that is taken
// 1. Branch names are unique, and main is reserved.
await layerfs.branch.fork("cart")
await layerfs.branch.fork("main")
// → LayerFSError: NameTaken
// → LayerFSError: ReservedNamelayerfs branch fork cartlayerfs branch fork mainbranch.list_commits
list_commits(branch, pageSize = 50, pagePos = 0)
layerfs branch list_commits <branch> [--page-size <n>] [--page-pos <n>]
Returnsa page of Commit records: id, branch, parent, secondParent, base, root, message, mergedFrom, time
RefusesNoSuchBranch
Newest first, along first parents. After an uncertain outcome, this is how you check whether a Commit happened.
const log = await layerfs.branch.list_commits("cart", 10, 0);
console.log(log.items.map((c) => `${c.id.slice(0, 12)} ${c.message}`).join("\n"));SDKPage through history
// 1. Commits come newest first, along first parents.
const p1 = await layerfs.branch.list_commits("cart", 2, 0)
// → { items: [{ id: "c3c2…", message: "Add the route" }, { id: "c2b1…", message: "Tax 8%" }], hasMore: true }
// 2. Ask for the next page at pagePos + items.length.
await layerfs.branch.list_commits("cart", 2, 2)
// → { items: [{ id: "c1a0…", message: "Add notes" }], hasMore: false }layerfs branch list_commits cart --page-size 2layerfs branch list_commits cart --page-size 2 --page-pos 2branch.diff_commits
diff_commits(from, to, pageSize = 50, pagePos = 0, { cursor, hunks } = {})
layerfs branch diff_commits <from-commit> <to-commit> [--page-size <n>] [--page-pos <n>] [--cursor <text>] [--no-hunks]
Returns{ from, to, items, hasMore, nextCursor }; each item { path, kind, type, before, after, content, metadata, identity, hunks, hunksOmitted }
RefusesInvalidInput (not two Commits), NoSuchCommit
What changed between two Commits. kind is Added, Removed orModified. Pass the previous page's nextCursor as cursor to resume after its last path; hunks: false lists paths without line changes.
const [newest, previous] = (await layerfs.branch.list_commits("cart", 2, 0)).items;
const d = await layerfs.branch.diff_commits(previous.id, newest.id);
console.log(d.items.map((c) => `${c.kind} ${c.path}`).join("\n"));
// Page by path instead of re-walking: resume after the last path listed.
const next = await layerfs.branch.diff_branches("main", "cart", 200, 0, { cursor: d.nextCursor, hunks: false });SDKWhat did the last step change?
// 1. Take the two newest Commits.
const log = await layerfs.branch.list_commits("cart", 2, 0)
const [newest, previous] = log.items
// → c3c2… "Add the route" · c2b1… "Tax 8%"
// 2. Diff them: each changed path with its kind and line hunks.
await layerfs.branch.diff_commits(previous.id, newest.id)
// → { items: [
// { path: "/src/routes/cart.js", kind: "Added", … },
// { path: "/src/basket.js", kind: "Removed", … } ], hasMore: false }layerfs branch list_commits cart --page-size 2 \ | jq -r '.items[] | "\(.id) \(.message)"'layerfs branch diff_commits c2b1… c3c2…branch.diff_branches
diff_branches(from, to, pageSize = 50, pagePos = 0, { cursor, hunks } = {})
layerfs branch diff_branches <from-branch> <to-branch> [--page-size <n>] [--page-pos <n>] [--cursor <text>] [--no-hunks]
Returnsthe same shape as diff_commits
RefusesInvalidInput (a Commit id), NoSuchBranch
The same, between the current heads of two Branches or main.
SDKList a big change by path
// 1. hunks: false lists paths without their contents, so a page fills with
// paths.
const d = await layerfs.branch.diff_branches("main", "cart", 200, 0,
{ hunks: false })
// → { items: [{ path: "/src/cart.js", kind: "Modified", hunks: null }, … 200 ], hasMore: true, nextCursor: "/src/k.js" }
// 2. Pass nextCursor back to resume after the last path, instead of
// re-walking the first pages.
await layerfs.branch.diff_branches("main", "cart", 200, 0,
{ hunks: false, cursor: d.nextCursor })
// → { items: [ … 37 ], hasMore: false }layerfs branch diff_branches main cart --page-size 200 --no-hunkslayerfs branch diff_branches main cart --page-size 200 --no-hunks --cursor /src/k.jsbranch.merge
merge(a, b, name, message?, units?)
layerfs branch merge <a> <b> <name> [-m <text>] [--units <json>]
Returns{ kind: "Merged", fastForward, branch, head, root, sources, mergeBase, conflicts: [], lww }
RefusesConflict (nothing created; detail has sources, mergeBase, conflicts, lww), NoMergeBase, NameTaken, NoSuchBranch
Merges a and b into a new Branch name; a andb don't change. fastForward means one side already held the other. Paths matchinga's /.lww keep the newer version and are listed in lww. There is no overwrite option.
units is an optional list of merge units, each a /.lww-style pattern or{ name, patterns }. When both sides changed a unit, the whole unit comes from the side with the later write, with no text merge inside it; the result's units lists what each one kept and dropped.
const m = await layerfs.branch.merge("cart", "main", "cart-release", "Merge main before publish");
console.log(m.branch, m.fastForward); // "cart-release" falseSDKA fast-forward
// cart.1 forked from cart and added a Commit; cart hasn't moved since.
// 1. One side already holds the other, so the merge just takes the newer
// head. A and b are left as they were.
await layerfs.branch.merge("cart", "cart.1", "cart.next", "Keep cart.1")
// → { kind: "Merged", fastForward: true, branch: "cart.next", head: "c4d3…" }layerfs branch merge cart cart.1 cart.next -m "Keep cart.1"SDKBring main in before publishing
// main gained a Layer since cart forked, and both changed different files.
// 1. A three-way merge against the merge base; a clean result is one Commit
// with two parents.
await layerfs.branch.merge("cart", "main", "cart-release", "Merge main")
// → { kind: "Merged", fastForward: false, branch: "cart-release", head: "c5e4…", conflicts: [], lww: [] }layerfs branch merge cart main cart-release -m "Merge main"SDKA conflict creates nothing
// 1. Both sides changed the same lines of src/cart.js.
await layerfs.branch.merge("cart.a", "cart.b", "cart.ab")
// → LayerFSError: Conflict
// detail: { sources, mergeBase: "c3c2…", conflicts: [{ path: "/src/cart.js", … }], lww: [] }
// 2. Make the sides agree on those lines (here cart.b takes cart.a's), then
// merge again.
await layerfs.fs.edit("cart.b", "/src/cart.js", …)
await layerfs.branch.merge("cart.a", "cart.b", "cart.ab")
// → { kind: "Merged", fastForward: false, branch: "cart.ab", … }layerfs branch merge cart.a cart.b cart.ablayerfs fs edit cart.b /src/cart.js --old-from b.txt --new-from a.txtlayerfs branch merge cart.a cart.b cart.abbranch.publish
publish(branch, commit?)
layerfs branch publish <branch> [<commit>]
ReturnsAdded with publishedLayer, basedOn, overwroteCount, overwrote (first 50), overwroteHasMore; or UpToDate, or NoChanges
RefusesNoCommit, HeadMoved (main moved during the call), InvalidInput (a Layer id)
Makes main show the Branch head, or the given Commit, as a new Layer. It overwrites: main Layers published since the Branch's base are skipped and listed. If someone may have published since you forked, merge main in first, test the merged Branch, then publish it.
const p = await layerfs.branch.publish("cart-release");
if (p.kind === "Added" && p.overwroteCount) console.log("skipped", p.overwrote);SDKPublish a Branch
// 1. The Branch's head becomes main's next Layer.
await layerfs.branch.publish("cart-release")
// → { kind: "Added", publishedLayer: { id: "l2d0…" }, basedOn: "l1c3…", overwroteCount: 0 }
// 2. New work forks from the new Layer.
await layerfs.branch.fork("cart-2")
// → { kind: "Forked", branch: "cart-2", base: "l2d0…", head: null }layerfs branch publish cart-releaselayerfs branch fork cart-2SDKPublishing overwrites
// 1. search was published as L1 after cart forked from L0. Publishing cart
// directly skips L1, and says so.
await layerfs.branch.publish("cart")
// → { kind: "Added", overwroteCount: 1, overwrote: [{ id: "l1c3…", … }], overwroteHasMore: false }
// 2. To keep it, merge main into the Branch first, then publish the merge.
await layerfs.branch.merge("cart", "main", "cart-release")
await layerfs.branch.publish("cart-release")
// → { kind: "Added", overwroteCount: 0 }layerfs branch publish cartlayerfs branch merge cart main cart-releaselayerfs branch publish cart-releaseSDKPublish an earlier Commit
// 1. Name a Commit to publish it instead of the head.
await layerfs.branch.publish("cart", "c2b1…")
// → { kind: "Added", source: { branch: "cart", commit: "c2b1…" }, … }layerfs branch publish cart c2b1…fs
Agent skillmethods/fs-read.mdmethods/fs-write.md
Reads take a ref (main, a Branch, a Commit id or a Layer id) and see committed state only, never a mounted Workspace's unsaved changes. Paths are absolute in that view. A single-file answer starts with{ kind: "ok", snapshot, root, path }, where snapshot names what was read, such as"cart@9f3c0a1b2c4d".
Lists are pages, { items, hasMore, nextPagePos }: pageSize 1 to 1000 (default 50),pagePos from 0. A short page while hasMore is true only means it hit its byte bound. Deep offsets skip linearly, so narrow the path rather than paging far.
const all = [];
for (let pos = 0; ; ) {
const page = await layerfs.fs.glob("cart", "src/**/*.js", "/", 1000, pos);
all.push(...page.items);
if (!page.hasMore) break;
pos += page.items.length;
}Reads
fs.read
read(ref, path, startLine = 1, endLine?)
layerfs fs read <ref> <path> [--start-line <n>] [--end-line <n>]
Returnstext, startLine, endLine, truncated, lineTruncated, nextLine, size
RefusesIsDirectory, Binary (NUL or invalid UTF-8), NoSuchPath
A 1-based, inclusive line range of a text file: at most 2000 lines and 48 KiB per answer. A huge single line comes back as a prefix and the next window starts at the following line.
// Read a whole file, window by window.
let line = 1, text = "";
for (;;) {
const r = await layerfs.fs.read("cart", "/src/cart.js", line);
text += r.text;
if (r.nextLine == null) break;
line = r.nextLine;
}SDKRead a long file in windows
// 1. A read returns up to 2000 lines. A longer file says where the next
// window starts.
await layerfs.fs.read("cart", "/src/cart.js")
// → { text: "…", startLine: 1, endLine: 2000, truncated: true, nextLine: 2001, size: 91204 }
// 2. Ask again from nextLine until it is null.
await layerfs.fs.read("cart", "/src/cart.js", 2001)
// → { text: "…", startLine: 2001, endLine: 2400, truncated: false, nextLine: null }layerfs fs read cart /src/cart.jslayerfs fs read cart /src/cart.js --start-line 2001SDKA few lines, at any ref
// 1. Lines are 1-based and inclusive. The ref can be a Branch, main, a
// Commit or a Layer.
await layerfs.fs.read("c2b1…", "/src/cart.js", 1, 5)
// → { text: "import …\n…", startLine: 1, endLine: 5, truncated: false }layerfs fs read c2b1… /src/cart.js --start-line 1 --end-line 5 | jq -r .textSDKNot a text file
// 1. A file with NUL bytes or invalid UTF-8 is refused. Use a Sandbox
// command for binary work.
await layerfs.fs.read("cart", "/public/logo.png")
// → LayerFSError: Binarylayerfs fs read cart /public/logo.pngfs.list
list(ref, path = "/", pageSize = 50, pagePos = 0)
layerfs fs list <ref> [<path>] [--page-size <n>] [--page-pos <n>]
Returnsa page of { name, path, type, inode }; type is file, dir or symlink
RefusesNotDirectory, NoSuchPath
One folder, names in byte order.
fs.stat
stat(ref, path)
layerfs fs stat <ref> <path>
Returns{ type, inode, size }
RefusesNoSuchPath
fs.grep
grep(ref, pattern, path = "/", pageSize = 50, pagePos = 0)
layerfs fs grep <ref> <pattern> [<path>] [--page-size <n>] [--page-pos <n>]
Returnsa page of { path, line, text, lineTruncated }, plus skipped binary files
RefusesInvalidInput (empty pattern)
Lines containing pattern as plain text, not a regular expression, in path order then line order. It searches everything under path, node_modules included, so keep path narrow. For a regular expression, run grep -rn in a Workspace.
SDKFind every use of a name
// 1. Search the contents under a folder. Each match gives its path, line
// and text.
await layerfs.fs.grep("cart", "TAX", "/src")
// → { items: [{ path: "/src/cart.js", line: 3, text: "const TAX = 0.07;" },
// { path: "/src/checkout.js", line: 12, text: "total * (1 + TAX)" }], hasMore: false, skipped: [] }layerfs fs grep cart TAX /srcfs.glob
glob(ref, pattern, path = "/", pageSize = 50, pagePos = 0)
layerfs fs glob <ref> <pattern> [<path>] [--page-size <n>] [--page-pos <n>]
Returnsa page of absolute path strings
* and ? match within one name; **/ matches zero or more folders, so src/**/*.js includes /src/cart.js.
SDKCollect every file of a kind
// 1. ** matches any number of folders, including none.
const page = await layerfs.fs.glob("cart", "src/**/*.js", "/", 1000, 0)
// → { items: ["/src/cart.js", "/src/routes/cart.js", "/src/server.js"], hasMore: false }
// 2. Results are pages: while hasMore is true, ask again at pagePos +
// items.length.
if (page.hasMore) {
await layerfs.fs.glob("cart", "src/**/*.js", "/", 1000, page.items.length)
}layerfs fs glob cart 'src/**/*.js' / --page-size 1000layerfs fs glob cart 'src/**/*.js' / --page-size 1000 --page-pos 1000fs.diff_files
diff_files(leftRef, leftPath, rightRef, rightPath)
layerfs fs diff_files <left-ref> <left-path> <right-ref> <right-path>
Returnskind (Text, Equal or Binary), left, right, contentEqual, sameInode, hunks of { op, text }, hunksOmitted
Two files, at any refs and paths. op is " ", "-" or "+".
SDKOne file, two refs
// 1. Compare a file on main with the same file on a Branch.
await layerfs.fs.diff_files("main", "/src/cart.js", "cart", "/src/cart.js")
// → { kind: "Text", contentEqual: false, hunks: [{ op: "-", text: "const TAX = 0.07;" }, { op: "+", text: "const TAX = 0.08;" }] }layerfs fs diff_files main /src/cart.js cart /src/cart.jsfs.blame
blame(ref, path, fromLine = 1)
layerfs fs blame <ref> <path> [--from-line <n>]
Returnslines of { n, text, lineTruncated, commit, message }, versions, comparisonCoarse, nextLine
The Commit, and so the step, that last changed each line. comparisonCoarse means a large change was reported as one removed and added block.
SDKWho wrote each line
// 1. Each line names the Commit, and so the tool call, that last changed
// it.
await layerfs.fs.blame("cart", "/src/cart.js")
// → { lines: [
// { n: 1, text: "import …", commit: "c1a0…", message: "Add cart" },
// { n: 3, text: "const TAX = 0.08;", commit: "c2b1…", message: "Tax 8%" }, … ], versions: 3 }layerfs fs blame cart /src/cart.js --textWrites
Each write changes one Branch (never main) and publishes one Commit, or nothing. It merges with the Branch head it finds when it publishes: if another writer changed different files meanwhile, it lands on top; if it changed the same path, the write is refused as Conflict and nothing is committed. Every write answers one of:
| kind | Fields | Meaning |
|---|---|---|
Committed | branch, head, commit | A new Commit; commit is its record |
UpToDate | branch, head, root | Nothing changed, so no Commit |
Shared refusals: ReadOnly (main), NoSuchBranch, Conflict, HeadMoved,NoSuchPath (a missing parent: fs.mkdir first), TooLarge andInvalidInput. A write's body travels in the call itself, since Store calls run in your process, and is written a record at a time, so no file size is capped. A Workspace mounted earlier doesn't see a write until you mount again or its next Commit merges it in.
fs.write
write(branch, path, content, message?)
layerfs fs write <branch> <path> [-m <text>] [--from <file>]
ReturnsCommitted or UpToDate
RefusesIsDirectory
Creates or replaces one file. content is a string, or bytes.
SDKWrite, then write the same again
// 1. A write creates or replaces the whole file, as one Commit.
await layerfs.fs.write("cart", "/src/routes/cart.js", "export default {};\n", "Add route")
// → { kind: "Committed", branch: "cart", head: "c4d3…" }
// 2. Writing what is already there changes nothing, so no Commit is made.
await layerfs.fs.write("cart", "/src/routes/cart.js", "export default {};\n")
// → { kind: "UpToDate", branch: "cart", head: "c4d3…" }printf 'export default {};\n' | layerfs fs write cart /src/routes/cart.js -m "Add route"printf 'export default {};\n' | layerfs fs write cart /src/routes/cart.jsfs.edit
edit(branch, path, oldText, newText, message?)
layerfs fs edit <branch> <path> [-m <text>] [--old <text>] [--old-from <file>] [--new <text>] [--new-from <file>]
ReturnsCommitted or UpToDate
RefusesNoMatch, AmbiguousMatch, Binary
Replaces exactly one occurrence of oldText. It streams large files, so it's the cheap way to change a few lines of a big one. On NoMatch or AmbiguousMatch nothing changed: read the file as it is now and include more surrounding lines.
SDKReplace one exact match
// 1. oldText must appear exactly once. The edit is one Commit.
await layerfs.fs.edit("cart", "/src/cart.js",
"const TAX = 0.07;", "const TAX = 0.08;", "Tax 8%")
// → { kind: "Committed", branch: "cart", head: "c2b1…" }layerfs fs edit cart /src/cart.js \ --old "const TAX = 0.07;" --new "const TAX = 0.08;" -m "Tax 8%"SDKNo match, or more than one
// 1. Text that isn't there is refused.
await layerfs.fs.edit("cart", "/src/cart.js", "const TAX = 0.07;", "…")
// → LayerFSError: NoMatch
// 2. So is text that appears more than once. Read the file again and widen
// oldText until it's unique.
await layerfs.fs.edit("cart", "/src/cart.js", "qty", "quantity")
// → LayerFSError: AmbiguousMatchlayerfs fs edit cart /src/cart.js --old "const TAX = 0.07;" --new "…"layerfs fs edit cart /src/cart.js --old qty --new quantitySDKAnother agent got there first
// 1. Agent A edits cart.js and lands.
await a.fs.edit("cart", "/src/cart.js", "price * qty", "cents(price) * qty")
// → { kind: "Committed", head: "c7a6…" }
// 2. Agent B changes the same file from an older head. Refused; nothing is
// committed or retried.
await b.fs.edit("cart", "/src/cart.js", "price * qty", "price * quantity")
// → LayerFSError: Conflict
// detail: { conflicts: ["/src/cart.js"], head: "c7a6…" }
// 3. B reads the new head and edits again.
await b.fs.read("cart", "/src/cart.js")
await b.fs.edit("cart", "/src/cart.js",
"cents(price) * qty", "cents(price) * quantity")
// → { kind: "Committed", head: "c8b7…" }layerfs fs edit cart /src/cart.js --old "price * qty" --new "cents(price) * qty" # agent Alayerfs fs edit cart /src/cart.js --old "price * qty" --new "price * quantity" # agent Blayerfs fs read cart /src/cart.js > /tmp/cart.json # agent Blayerfs fs edit cart /src/cart.js \ --old "cents(price) * qty" --new "cents(price) * quantity"fs.apply
apply(branch, changes, message?)
layerfs fs apply <branch> [-m <text>] [--from <file>]
ReturnsCommitted or UpToDate
Refusesthe first failing change's refusal, with detail.index (zero-based)
An ordered list of write, edit, mkdir, rm, cp andmv changes as one Commit. Later changes see earlier ones. The best way to scaffold or make any multi-file change.
await layerfs.fs.apply("cart", [
{ op: "mkdir", path: "/src/routes" },
{ op: "write", path: "/src/routes/cart.js", content: "export default {};\n" },
{ op: "edit", path: "/src/server.js", oldText: "// routes", newText: "import cart from './routes/cart.js';" },
{ op: "mv", from: "/src/basket.js", to: "/src/legacy/basket.js" },
], "cart: add the route"); // one Commit, or nothingSDKSeveral changes, one Commit
// 1. apply runs the changes in order and publishes once.
await layerfs.fs.apply("cart", [
{ op: "mkdir", path: "/src/routes" },
{ op: "write", path: "/src/routes/cart.js", content: "export default {};\n" },
{ op: "mv", from: "/src/basket.js", to: "/src/legacy/basket.js" },
], "Add the route")
// → { kind: "Committed", head: "c3c2…" }layerfs fs apply cart -m "Add the route" <<'EOF'[{ "op": "mkdir", "path": "/src/routes" }, { "op": "write", "path": "/src/routes/cart.js", "content": "export default {};\n" }, { "op": "mv", "from": "/src/basket.js", "to": "/src/legacy/basket.js" }]EOFSDKAll or nothing
// 1. If any change fails, none land. detail.index is the zero-based change
// that failed.
await layerfs.fs.apply("cart", [
{ op: "write", path: "/a.txt", content: "a" },
{ op: "rm", path: "/missing.txt" },
])
// → LayerFSError: NoSuchPath
// detail: { index: 1 }echo '[{"op":"write","path":"/a.txt","content":"a"},{"op":"rm","path":"/missing.txt"}]' \ | layerfs fs apply cartfs.mkdir
mkdir(branch, path, message?)
layerfs fs mkdir <branch> <path> [-m <text>]
ReturnsCommitted, or UpToDate for an existing folder
RefusesNotDirectory
Creates a folder and any missing parents.
fs.rm
rm(branch, path, message?)
layerfs fs rm <branch> <path> [-m <text>]
ReturnsCommitted
RefusesNoSuchPath
Removes a file or a whole folder tree.
SDKRemove, and still read the old version
// 1. rm removes a file or a whole folder as one Commit.
await layerfs.fs.rm("cart", "/src/legacy", "Drop legacy code")
// → { kind: "Committed", head: "c5e4…" }
// 2. Older Commits still have it.
await layerfs.fs.read("c4d3…", "/src/legacy/basket.js")
// → { text: "…", startLine: 1, … }layerfs fs rm cart /src/legacy -m "Drop legacy code"layerfs fs read c4d3… /src/legacy/basket.jsfs.cp
cp(branch, from, to, message?)
layerfs fs cp <branch> <from-path> <to-path> [-m <text>]
ReturnsCommitted
RefusesExists (to exists), InvalidInput (same path, or into its own subtree)
Copies a file or tree. Keeps modes, symlinks and hard links within the copied set; timestamps are new.
fs.mv
mv(branch, from, to, message?)
layerfs fs mv <branch> <from-path> <to-path> [-m <text>]
ReturnsCommitted
Refusesthe same as fs.cp
Moves or renames a file or tree, keeping its identity and timestamps.
workspace
Agent skillmethods/workspace.md
A Workspace is a complete, writable view of a Branch where ordinary commands run. It is{ sandbox, path }: the number of the Sandbox that holds it and its path there. Any later process with the same LAYERFS_HOME can use it.
- A mount names its Sandbox. A project's Sandboxes are numbered
1,2, …; each is a container with one daemon and holds up to 16 Workspaces of any Branches.sandbox.createis the only call that starts one;mount(sandbox, branch)mounts in the Sandbox it names and never starts one. There is no placement. - Each mount is a new Workspace. Pass the Workspace that
mountanswered toexec,commitandunmount; never build one yourself. Commands use relative paths from the Workspace root. - The view is fixed at mount. It shows the Branch as it was then.
commitmerges in what moved; to see it, mount again. - Everything in it is committed: sources,
.git,node_modules, build output, caches. Keep scratch files in/tmp, outside the Workspace. - Unmount in a
finally. Nothing unmounts a Workspace for you. A Sandbox closes itself two minutes after its last Workspace is unmounted; if its container dies, its Workspaces are gone.
One Bash call as one Workspace, in a Sandbox from sandbox.create:
const ws = await layerfs.workspace.mount(sandbox, "cart"); // a NEW Workspace: { sandbox: "1", path: "/workspaces/1", image }
try {
const run = await layerfs.workspace.exec(ws, "timeout 110 npm test 2>&1", "step 12");
await layerfs.workspace.commit(ws, `step 12: npm test (exit ${run.exitCode})`);
} finally {
await layerfs.workspace.unmount(ws); // ws is now invalid
}workspace.mount
mount(sandbox, branch)
layerfs workspace mount <sandbox> <branch>
Returnsthe Workspace, { sandbox, path, image }, such as { sandbox: "1", path: "/workspaces/1", image: "sha256:…" }
RefusesNoSuchBranch (main can't be mounted: fork it), NoSuchSandbox (not running, or it closed itself: sandbox.create a new one), Capacity (it holds 16 Workspaces: create another, or unmount ones you're done with), InvalidInput (a bad Sandbox number, or an options object: there are none), Failed
Mounts a Workspace on the Branch head at this moment, in the running Sandbox sandbox names, such as"1" from sandbox.create or project.list_sandboxes, and nowhere else. It doesn't scan or copy the tree, and never starts a Sandbox. The Sandbox fixes the image.
SDKMount a Branch
// 1. Each mount is a new Workspace in the Sandbox it names. It binds the
// whole tree without copying it.
await layerfs.workspace.mount("1", "cart")
await layerfs.workspace.mount("1", "search")
// → { sandbox: "1", path: "/workspaces/1", image: "sha256:…" }
// → { sandbox: "1", path: "/workspaces/2", image: "sha256:…" }
// 2. main can't be mounted. Fork it and mount the fork.
await layerfs.workspace.mount("1", "main")
// → LayerFSError: NoSuchBranch
// 3. A mount never starts a Sandbox. One that closed itself is refused;
// create a new one and mount there.
await layerfs.workspace.mount("3", "cart")
const own = await layerfs.sandbox.create()
await layerfs.workspace.mount(own.sandbox, "cart")
// → LayerFSError: NoSuchSandbox
// → { sandbox: "3", image: "sha256:…" }
// → { sandbox: "3", path: "/workspaces/1", image: "sha256:…" }layerfs workspace mount 1 cartlayerfs workspace mount 1 searchlayerfs workspace mount 1 mainlayerfs workspace mount 3 cartlayerfs sandbox createlayerfs workspace mount 3 cartworkspace.exec
exec(ws, command, message?, { stdout, stderr }?)
layerfs workspace exec <ws> <command> [-m <text>] [--stream]
Returns{ kind: "exit:<code>", exitCode, stdout, stderr, truncated, message }
RefusesNotMounted, NoSuchSandbox (its container is gone: mount again), Unknown (exit code not known)
Runs bash -c command at the Workspace root (HOME=/tmp) and waits until everything holding its output has ended. A non-zero exit is not a refusal. Output comes back whole, sotruncated is always false; send a long log to /tmp and read its end. There is no timeout, so wrap long commands: timeout 110 npm test. The message labels the run and creates no history.
With sinks, Node Writable streams such as process.stdout, the named streams' bytes are written to them as they arrive instead: the call holds one page of output however long it is, the result's field for each is null, and it carries stdoutBytes and stderrBytes. layerfs workspace exec --streamuses this and exits with the command's own code.
const run = await layerfs.workspace.exec(ws, "timeout 110 npm test 2>&1", "cart tests");
// { kind: "exit:0", exitCode: 0, stdout: "…# pass 3…", stderr: "", message: "cart tests", truncated: false }
// Long output: write it to /tmp, outside the Workspace, and read the end.
await layerfs.workspace.exec(ws, "timeout 110 npm run build >/tmp/build.log 2>&1; echo $? >/tmp/build.code");
const tail = await layerfs.workspace.exec(ws, "cat /tmp/build.code; tail -n 60 /tmp/build.log");SDKRun a command
// 1. The command runs with bash -c in the Workspace. Bound it with timeout:
// LayerFS never stops a command itself.
await layerfs.workspace.exec(ws, "timeout 110 npm test 2>&1", "Run tests")
// → { kind: "exit:0", exitCode: 0, stdout: "# pass 3 …", stderr: "", truncated: false }
// 2. A run makes no history. Commit afterwards to save what it changed.
await layerfs.workspace.commit(ws, "npm test")
// → { kind: "UpToDate", head: "c3c2…" }layerfs workspace exec 1:/workspaces/1 "timeout 110 npm test 2>&1" -m "Run tests"layerfs workspace commit 1:/workspaces/1 -m "npm test"SDKLong output
// 1. Send long output to a file in /tmp, outside the Workspace, so it isn't
// committed.
await layerfs.workspace.exec(ws,
"timeout 110 npm run build >/tmp/build.log 2>&1; echo $? >/tmp/build.code")
// → { kind: "exit:0", exitCode: 0, stdout: "" }
// 2. Then read just the end of it.
await layerfs.workspace.exec(ws,
"cat /tmp/build.code; tail -n 60 /tmp/build.log")
// → { exitCode: 0, stdout: "0\n… built in 4.2s" }layerfs workspace exec 1:/workspaces/1 \ "timeout 110 npm run build >/tmp/build.log 2>&1; echo $? >/tmp/build.code"layerfs workspace exec 1:/workspaces/1 "cat /tmp/build.code; tail -n 60 /tmp/build.log" --streamworkspace.commit
commit(ws, message?, force = false)
layerfs workspace commit <ws> [-m <text>] [--force]
ReturnsCommitted with head, commit, displacedHead, merged, lww; or UpToDate with head, root
RefusesConflict, HeadMoved, NotMounted
Saves everything changed in the Workspace as one Commit; the Workspace stays mounted. If the Branch moved since the mount it merges first (merged is then { from, head }). On Conflictnothing is committed and the conflicted files in the Workspace now hold markers: fix them with commands and commit again. force overwrites the newer head instead of merging and drops the other changes; avoid it.
SDKCommit what a command changed
// 1. The Commit saves the whole Workspace as it stands, including files
// from background processes.
await layerfs.workspace.exec(ws, "timeout 110 npm install dayjs")
await layerfs.workspace.commit(ws, "Add dayjs")
// → { kind: "Committed", head: "c4d3…", commit: { message: "Add dayjs", … } }
// 2. With nothing changed, it answers UpToDate and makes no Commit.
await layerfs.workspace.commit(ws, "Again")
// → { kind: "UpToDate", head: "c4d3…" }layerfs workspace exec 1:/workspaces/1 "timeout 110 npm install dayjs" --streamlayerfs workspace commit 1:/workspaces/1 -m "Add dayjs"layerfs workspace commit 1:/workspaces/1 -m "Again"SDKThe head moved meanwhile
// 1. Another agent committed to cart. This Commit changed other files, so
// it merges and lands on top.
await layerfs.workspace.commit(ws, "Tax 9%")
// → { kind: "Committed", head: "c6f5…", merged: { from: "c4d3…", head: "c5e4…" } }layerfs workspace commit 1:/workspaces/1 -m "Tax 9%"SDKA conflict in a Workspace
// 1. The same file changed on the new head. Nothing is published; marker
// blocks are written into the file.
await layerfs.workspace.commit(ws, "Rename qty")
// → LayerFSError: Conflict
// detail: { head: "c8b7…", conflicts: [{ path: "/src/cart.js", kind: "content", … }] }
// 2. Fix the markers with ordinary commands. A Commit is refused while a
// complete marker block remains.
await layerfs.workspace.exec(ws, "…edit src/cart.js…")
await layerfs.workspace.commit(ws, "Rename qty")
// → { kind: "Committed", head: "c9c8…" }layerfs workspace commit 1:/workspaces/1 -m "Rename qty"layerfs workspace exec 1:/workspaces/1 "…edit src/cart.js…"layerfs workspace commit 1:/workspaces/1 -m "Rename qty"workspace.unmount
unmount(ws, discard = false)
layerfs workspace unmount <ws> [--discard]
Returns{ kind: "Unmounted" }
RefusesDirty (detail.changed lists uncommitted paths), Busy (a background process still runs there), NotMounted
Deletes the Workspace's overlay; history stays. Commit first, or pass discard only for a Workspace you're abandoning, never one another agent mounted.
SDKUncommitted changes
// 1. Unmounting with changes not yet committed is refused, so nothing is
// lost by accident.
await layerfs.workspace.unmount(ws)
// → LayerFSError: Dirty
// detail: { changed: ["/scratch.txt"] }
// 2. Commit them, or discard them on purpose.
await layerfs.workspace.unmount(ws, true)
// → { kind: "Unmounted" }layerfs workspace unmount 1:/workspaces/1layerfs workspace unmount 1:/workspaces/1 --discardsandbox
Agent skillmethods/sandbox.md
Every Workspace lives in a Sandbox you name, so these calls come first. sandbox.create is the only call that starts one. Reuse a Sandbox with room (project.list_sandboxes shows how many Workspaces each holds), and close it when you're done. Another agent can mount in your Sandbox by naming it; close then waits on that Workspace's unmount.
const own = await layerfs.sandbox.create(); // { sandbox: "2", image }
try {
const ws = await layerfs.workspace.mount(own.sandbox, "cart");
try {
const run = await layerfs.workspace.exec(ws, "timeout 110 npm run build", "step 20");
await layerfs.workspace.commit(ws, `step 20: npm run build (exit ${run.exitCode})`);
} finally {
await layerfs.workspace.unmount(ws);
}
} finally {
await layerfs.sandbox.close(own.sandbox);
}sandbox.create
create({ image } = {})
layerfs sandbox create [--image <text>]
Returns{ sandbox, image }: its number, to pass to workspace.mount, and its image
RefusesFailed (with detail.removed: true nothing of it is left; detail.log has its daemon's last lines), Unknown (an earlier start's outcome is unknown: report it), Busy (the project's lock held for 60 s), InvalidInput (a missing setting, or an image that isn't a digest)
Starts the project's next Sandbox, the lowest free number, and waits for its daemon. It first reclaims the project's Sandboxes that closed themselves, so their numbers are used again. image is a digest,sha256:…, default LAYERFS_IMAGE; every Workspace mounted there runs on it. Like any Sandbox, it closes itself two minutes after its last Workspace is unmounted.
sandbox.list_workspaces
list_workspaces(sandbox, pageSize = 50, pagePos = 0)
layerfs sandbox list_workspaces <sandbox> [--page-size <n>] [--page-pos <n>]
Returnsa page of { sandbox, path, branch, base, activity, changed, headMovedSinceMount, pendingMerge }, plus a version
RefusesNoSuchSandbox, InvalidInput
The mounted Workspaces of one running Sandbox, from its daemon. Use it to clear leftovers: unmount only Workspaces that are Idle, have nothing uncommitted and that no agent is keeping on purpose.
sandbox.close
close(sandbox)
layerfs sandbox close <sandbox>
Returns{ kind: "Closed", sandbox }
RefusesMountsActive (detail.workspaces lists them), NoSuchSandbox (already closed), Failed / Unknown
Stops and deletes the container now, each step attempted once, then its records. The project and its history are unaffected.
SDKClose your own Sandbox
// 1. Close is refused while a Workspace is still mounted there.
await layerfs.sandbox.close(own.sandbox)
// → LayerFSError: MountsActive
// detail: { workspaces: [{ sandbox: "2", path: "/workspaces/1" }] }
// 2. Unmount it, then close. The container stops and is deleted; the
// project's history is unaffected.
await layerfs.workspace.unmount(ws)
await layerfs.sandbox.close(own.sandbox)
// → { kind: "Unmounted" }
// → { kind: "Closed", sandbox: "2" }layerfs sandbox close 2layerfs workspace unmount 2:/workspaces/1layerfs sandbox close 2The layerfs command
layerfs makes one @layerfs/agent call per command, so an agent whose only tool is Bash can use LayerFS without writing a script. It is only a wrapper: each command opens the project withLayerFS.open, makes exactly one call through the SDK's public API, prints the answer and closes the client. It adds no check or retry of its own, reads the same settings, and follows the same platform rules (in this process on Linux, through the LayerFS home's worker on macOS and Windows). Every call above shows its command beside its signature.
It is the api/agent/cli/ folder of a LayerFS checkout, not published to npm. It needs Node 20 or newer and the SDK's native addon (build:native, or LAYERFS_NATIVE).
npm --prefix "$LAYERFS_REPO/api" install # links the packages; downloads nothing
npm --prefix "$LAYERFS_REPO/api" run build:native # the SDK's native addon, on the host that loads it
npm --prefix "$LAYERFS_REPO/api/agent/cli" install # links the command to @layerfs/agent
"$LAYERFS_REPO/api/agent/cli/src/layerfs.js" help # or npm link in api/agent/cli, then: layerfs helpA short session, one call per line:
export LAYERFS_PROJECT=shop # or --project shop on each command
layerfs branch fork cart
printf '# Shop\n' | layerfs fs write cart /README.md -m readme
layerfs sandbox create # {"sandbox":"1","image":"sha256:…"}
layerfs workspace mount 1 cart # {"sandbox":"1","path":"/workspaces/3",…,"ws":"1:/workspaces/3"}
layerfs workspace exec 1:/workspaces/3 "timeout 110 npm test" --stream
layerfs workspace commit 1:/workspaces/3 -m "step 1: npm test"
layerfs workspace unmount 1:/workspaces/3
layerfs sandbox close 1How a call becomes a command
| Part | Rule |
|---|---|
| Name | family.call is layerfs <family> <call>, with the SDK's names (branch list_commits; list-commits is the same). LayerFS.init is layerfs init |
| Project | --project <name>, else LAYERFS_PROJECT. The command reads no other prefix |
| Arguments | The SDK's positional arguments, in its order. An optional one left out isn't sent, so the SDK applies its own default |
| Options | The SDK's remaining arguments as flags: --page-size <n>, --start-line <n>, --no-hunks (sends hunks: false), --image <digest>. A value is --name value or --name=value; -m <text> is the Commit message, the only short option. A whole number is digits only, --units is JSON, and a flag takes no value. An unknown or repeated option is refused; -- ends the options, so workspace exec 1:/workspaces/3 -- --version runs --version |
| Global | Anywhere before --: --project <name>, --text (readable output instead of JSON), -h/--help, --version |
| Workspace | Written <sandbox>:<path>, such as 1:/workspaces/3. workspace mount prints it as ws beside the SDK's { sandbox, path, image } |
| Bodies | fs write reads the file's bytes from stdin or --from <file>; fs apply reads its JSON list of changes the same way; fs edit takes --old or --old-from, and --new or --new-from. Input is read before anything opens |
| Exec | workspace exec prints the SDK's result, with the command's exitCode inside, and exits 0. With --stream it passes the command's stdout and stderr through as they arrive and exits with the command's own code |
Output and exit codes
| Exit | Means | Output |
|---|---|---|
0 | The call succeeded | stdout: the result as one line of JSON; --text prints readable key: value lines |
2 | The command line is wrong; nothing was opened or called | stderr: what is wrong, and the command's usage line |
125 | LayerFS refused or failed the call, including the SDK's own input checks | stderr: { code, message, detail }, the LayerFSError as the SDK gave it |
| the command's | workspace exec --stream only; 125 still means LayerFS failed | The command's own output |
- Same call, same answer. Tests run every call both ways, through the SDK and through
layerfs, and require the same binding call and the same answer: over a stand-in binding, and against a real Store, Docker and daemon. - Each command starts again. It opens the project and closes it, so a command pays Node's startup and the open each time; this hasn't been measured. For a loop of many calls, write an SDK script.
- Agent calls only.
@layerfs/runtimeand@layerfs/backendserve integration hosts and have no commands; the runtime's olderlfscommand is retired.
Errors and results
Agent skillSKILL.md · Rulesrules/conflicts.mdrules/lww.md
A refused call throws a LayerFSError with a stable code, a message for people and a structured detail. The code alone decides; isRefusal(error, code) tests for one.
import { isRefusal } from "@layerfs/agent";
try {
await layerfs.workspace.unmount(ws);
} catch (e) {
if (isRefusal(e, "Dirty")) console.log("uncommitted:", e.detail.changed);
else throw e;
}Read before you retry. Every call is attempted once; LayerFS never retries or resends on its own. OnUnknown or Failed the outcome is uncertain: stop, check with branch.list_commitsand fs.read, and report.
| Code | Means | Do |
|---|---|---|
Conflict | The same path changed on both sides; nothing committed | Read both sides, fix, try again. For a Workspace, fix the markers |
HeadMoved | The head moved during the call; nothing published | Look at the Branch, then decide |
Dirty | Unmount with uncommitted changes | Commit, or discard deliberately |
Busy | The project's lock was held for 60 s, or a background process still runs in the Workspace | Stop and report, or stop the process first |
Capacity | The Sandbox already holds 16 Workspaces | sandbox.create another and mount there, or unmount ones you're done with |
NoSuchPath, NoSuchBranch, NoSuchCommit, NoSuchProject, NoSuchSandbox | The thing named doesn't exist | List it: fs.list, project.list_branches, project.list_sandboxes |
NoMatch, AmbiguousMatch | fs.edit found zero or several matches | Read the file again; widen oldText |
IsDirectory, NotDirectory, Binary, NotText | Wrong kind of path or content | Use a Workspace command for binary work |
Exists, NameTaken, ReservedName | The name is taken or reserved | Use it, or pick another |
ReadOnly | A write to main | Write on a Branch, then publish |
MountsActive, NotMounted | Close with mounts, or a Workspace not mounted | Unmount first; use the Workspace from mount |
TooLarge | A Workspace call's request didn't fit one daemon control record; nothing sent | A smaller call, or one Workspace step |
Unsupported | Store calls where they can't run | Run on Linux with a local Store, or name the LayerFS home's worker with LAYERFS_WORKER_ENDPOINT |
InvalidInput | A bad argument, a missing setting, or a closed client | Read message |
Unknown, Failed | The outcome is uncertain or failed | Stop, check, report |
Results
- Every result has a
kind:Initialized,Committed,UpToDate,Forked,Merged,Added,Unmounted,Closed,exit:<code>, orokfor reads. - Messages are optional UTF-8, at most 4096 bytes; newline and tab are allowed. A message is part of the Commit's identity, and a message alone never makes a Commit.
- Ids are hex strings. A Commit or Layer id is a ref you can read, fork or diff.
- Pages are
{ items, hasMore, nextPagePos }; ask for the next atnextPagePos.









