Shoal’s native filesystem commands return typed values and can record effects in a journal. That enables safer inspection and targeted undo, but only for operations with a captured inverse. It is not a universal transactional filesystem.
Filesystem query builtins🔗
ls returns a table. Each row currently contains:
| Field | Type | Meaning |
|---|---|---|
path | path | full path used by the evaluator |
name | str | basename |
type | str | file, dir, symlink, or other |
size | size | metadata byte length |
modified | datetime or null | modification time |
(ls .)
.where(.type == "file")
.where(.size > 1mb)
.sort_by(.modified)ls --all/-a includes dot entries. Multiple path arguments are accepted and results are sorted by path.
stat returns one record for one path and a table for multiple paths, using the same metadata shape:
(stat ./Cargo.toml).size
stat ./README.md ./LICENSEOther read-oriented builtins:
cat ./one.txt ./two.txt # bytes concatenated
head ./service.log 20 # list<str>, lossy UTF-8
which cargo # Reef-aware resolution report in evaluator surface
env # environment record
env HOME # string or nullBecause which is intercepted by Reef-aware dispatch, its public result is richer than the low-level fallback path lookup. See Reef environments.
Mutation builtins🔗
mkdir --parents ./build/output
touch ./build/output/ready
cp --recursive ./assets ./build/assets
mv ./draft.txt ./final.txt
ln --symbolic ../final.txt ./build/final-link
rm ./obsolete.txt| Command | Result payload |
|---|---|
mkdir | list of created paths |
touch | list of touched paths |
cp | list of destination paths |
mv | list of destination paths |
ln | {target, link, symbolic} |
default rm | list of {path, trash} records |
rm --permanent | list of removed paths |
Directories require cp --recursive and rm --recursive where relevant. An unmatched/empty removal is an error rather than a silently successful destructive command.
Default rm moves targets into a process-specific directory under the system temporary directory. It is designed to support same-session journal undo, not as a polished desktop trash with indefinite retention. rm --permanent deletes directly and has no restore inverse.
Path methods and save forms🔗
let p = path("./notes.txt")
p.read
p.read_bytes
p.lines
p.exists
p.size
"replacement".save(p)
"append this".append(p)
save(p, "replacement")Method form places the content on the receiver. The evaluator’s save(path, value) function form places the path first. Use bytes when exact content matters.
open delegates to the host/platform opener surface. It is a host interaction, not a data read; do not use it in headless automation without checking availability.
Current directory and directory stack🔗
pwd
cd ./crates
pushd ../site
dirs
popd
cd -dirs returns list<path> with current directory first. j/jump use a frecency database in interactive sessions. Current-directory mutations are session state and are disallowed inside function bodies; scope them with with cwd: instead.
Tasks🔗
Create tasks with a block or trailing ampersand:
let test_task = spawn { cargo test }
sleep 30s &
jobs
test_task.is_done()
test_task.await()Task methods are:
await wait cancel is_done suspend resume is_suspendedawait/wait return the task result or propagate its error. cancel requests cancellation. Local suspension methods are meaningful where the task owns controllable local process state.
Local process-group job control🔗
The interactive Unix REPL gives foreground externals a process group:
Ctrl-Cinterrupts the foreground tree without killing Shoal.Ctrl-Zstops the foreground group and registers a job.jobsreports IDs, descriptions, state, completion, and suspension.fg %Nresumes in the foreground.bg %Nresumes in the background.
jobs
bg %1
fg %1fg task_variable is rewritten to resume and await a language task. A known preview gap is incomplete asynchronous state refresh for an external resumed with bg; after it exits, its table state may stay “running” until foregrounded or session shutdown.
This is local REPL behavior. Kernel task suspend/resume currently returns TASK_CONTROL_UNAVAILABLE, and MCP PTY control is a separate rendered-terminal protocol.
Line history versus journal🔗
flowchart TB
accTitle: Line history versus journal
accDescr: Shows the components and relationships described in Line history versus journal.
S["submitted statement"] --> H["line history"]
S --> J["journal entry"]
H --> R["recall / search"]
J --> M["source + principal + status + effects"]
J --> O["captured outputs / refs"]
J --> I["undo inverses"]Line history powers the editor. The journal is structured execution history. View journal rows with either command head:
journal
history
journal --head=rm --limit=20
journal --principal=human --limit=50Rows contain id, ts, principal, full src, ok, status, and serialized effects. A host without an installed journal returns an empty table.
In the local REPL, state defaults under $XDG_STATE_HOME/shoal, falling back to ~/.local/state/shoal. Journal ownership and principal information are more important in a kernel session, where humans and agents share a durable session host.
Undo protocol🔗
undo # newest journal entry with an inverse
undo 42 # specific journal entry ID
undo out[3] # REPL maps transcript result to entry IDUndo first validates that the target still matches the fingerprint recorded after the original mutation. If later activity changed it, Shoal raises stale_undo instead of overwriting the newer state.
sequenceDiagram
accTitle: Undo protocol
accDescr: Shows the components and relationships described in Undo protocol.
participant C as Command
participant J as Journal
participant F as Filesystem
C->>J: capture prior state (when eligible)
C->>F: apply mutation
C->>J: store inverse + post-fingerprint
Note over F: later work may change target
J->>F: undo request: verify fingerprint
alt target unchanged
J->>F: apply inverse
else target drifted
J-->>C: stale_undo, refuse
endWhat is reversible🔗
| Operation | Current inverse | Important condition |
|---|---|---|
default rm | move temp-trash item back | trash item still matches fingerprint |
overwrite via cp | restore prior bytes | prior file captured below journal cap |
overwrite via mv | move destination back | paths/fingerprints still safe |
overwrite via save/path .save | restore prior bytes | prior file existed and fit cap |
| overwrite/append redirect | restore prior bytes | target existed and fit cap |
What is not reversible🔗
rm --permanent;- opaque changes made inside
sh { ... }or an arbitrary external command; - creation of a previously nonexistent file by redirect/save (no delete inverse yet);
- a prior file too large to capture without truncation;
- changes whose post-state fingerprint no longer matches;
- network, process, and other non-filesystem effects;
- work executed in a host with no installed journal.
“Journaled” means recorded, not automatically reversible. Inspect the effects and the command’s documented inverse class before assuming rollback.
Captured output and CAS🔗
Value-position process output is bounded in memory. The default resident capture cap is 64 MiB and can be changed with SHOAL_CAPTURE_CAP_BYTES. In a journaled session, overflow can spill into content-addressed storage up to the spill cap (default 1 GiB, SHOAL_CAPTURE_SPILL_CAP_BYTES). A lazy bytes reference preserves the true length and loads content on demand.
The journal also has an output hard cap (default 256 MiB). Prior file content that exceeds the applicable journal cap is deliberately left without an undo inverse; restoring truncated bytes would be worse than refusing undo.
Large captures and kernel response refs are detailed in Agents, kernel, and MCP and Current status and limits.
Safety checklist🔗
Before a destructive workflow:
- Run it in value position or
plan { ... }when you need inspection. - Prefer native builtins/adapters over opaque
shfor effect visibility. - Verify the session actually has a journal.
- Avoid
--permanentunless irreversibility is intentional. - Check
journal --head=<command>immediately after the operation. - Treat
undoas a guarded inverse, not as a filesystem snapshot.