world
The world namespace — 119 functions.
globals/world/add
world.add(path: string, opts: AddOpts?)
Stage one path's manifest row, expanding to the full asset
family if the path lives inside a composite asset. Idempotent
at (stage, manifest_row). Pass { force = true } to bypass
the .zmignore / .gitignore gate — same intent as
git add -f. Without force, attempts to stage an ignored
path (or a path whose .refs points at an ignored dep)
error.
{ stage = "<name>" } stages into one of the caller's own staging
areas instead of the shared default one, so a commit naming that area
freezes these paths and leaves every other caller's staged.
Parameters
pathstring— The path to stage. Must be a non-empty string.optsAddOpts(optional) — Optional{ force: boolean?, stage: string? }. Defaults to{ force = false }on the default staging area.
world.add("/source/foo.luau")
world.add("/source/scene_dirty/entities/42.json", { force = true })
world.add("/source/fauna.module", { stage = "fauna" })
globals/world/add_all
world.add_all(opts: StageOpts?) -> { string }
Stage the dirty manifest rows this caller can claim, skipping
any path that matches .zmignore / .gitignore. Paths that match
an ignore pattern are silently skipped — world.add(path, { force = true }) is the explicit way to override the gate
for an individual path. Rows still flagged conflicted by
world.pullAsset are held back too — resolve them (edit +
world.add(path), or world.resolvePullConflict) and re-run.
A path another staging area holds is held back as well: the
working tree is one per branch and staging areas are not, so a
path some other caller has already selected for a commit of its
own belongs to that caller until it commits or hands it over.
world.add(path) names a path deliberately and takes it either
way, which is how a claim is handed over — and a path this area
already holds stays staged here, whoever else holds it too.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area to stage into. Omitted, the call stages into the shared default area.
Returns { string } — The paths that were held back — those another staging area holds, then the conflicted ones. Empty when none were.
world.add_all()
world.add_all({ stage = "fauna" })
globals/world/affirm
world.affirm(token: string)
Consume an XXX-XXX-XXX-style affirmation token returned by
a destructive op surface (e.g. vfs.remove). The destruction
commits atomically with the pending-row delete; the token is
one-shot. Errors verbatim on expiry / wrong-user.
Parameters
tokenstring— The affirmation token.
world.affirm("ABC-DEF-GHI")
globals/world/args
world.args -> any
Returns any
globals/world/assetInstallable
world.assetInstallable(opts: AssetInstallableOpts) -> AssetInstallableReport
Report whether one published asset can be installed on its own,
without raising. world.previewInstall plans a real install and
raises when the closure will not resolve; this answers the prior
question — does this guid name something ZeroMind will hand over by
itself — as a verdict a caller can branch on. Content that ships
inside a larger library (a module inside a package, a material
inside a system) carries its own published identity, so the answer
is per asset rather than per library. Reads only.
Parameters
optsAssetInstallableOpts—{ guid }names the asset;refpins a commit-id instead of the latest.
Returns AssetInstallableReport — An AssetInstallableReport — installable, a verdict, a one-line detail, and the closure's nodes / tree_children / deps shape when one resolved.
world.assetInstallable({ guid = asset.guid("@builtin::materials.neon") })
globals/world/avatar_default_edit
world.avatar_default_edit -> any
Returns any
globals/world/avatar_default_play
world.avatar_default_play -> any
Returns any
globals/world/awaitOutgoingSync
world.awaitOutgoingSync()
Wait until every /source write this session made has reached
the branch it was written against. Raises naming the paths that did
not land. add / commit / push / checkout / merge / pull
already wait on their own; call this before rebinding after a burst
of writes, which world.checkout and world.swap refuse over.
Returns Nothing. Raises when a write did not land.
world.awaitOutgoingSync() ; world.checkout("main")
globals/world/branches
world.branches() -> { { branch: string, commit_id: string, current: boolean } }
Every branch this world has, with the commit each one's head
names and which one this session is on — git branch --list. Sorted
by name. A branch exists for everyone in the world; which one you are
on is yours alone, so current is true for at most one row here and
says nothing about where anybody else is.
Returns { { branch: string, commit_id: string, current: boolean } } — Array of { branch, commit_id, current }.
for _, b in ipairs(world.branches()) do print(b.branch, b.commit_id) end
globals/world/camera_default_edit
world.camera_default_edit -> any
Returns any
globals/world/camera_default_play
world.camera_default_play -> any
Returns any
globals/world/checkUpdates
world.checkUpdates() -> { UpdateReport }
Discover upstream changes for every asset this world has
pulled. Read-only — makes no local mutation and no VCS write.
Each locally-pulled row identifies its own origin asset via
origin_asset_guid (recorded as that entry's own asset guid at
pull time — see world.installAsset). For every distinct
origin asset among the pulled rows, this re-resolves that
asset's latest transitive closure and compares each returned
entry's checksum against the matching local row's recorded
origin_checksum.
Returns { UpdateReport } — An array of UpdateReport, one per re-resolved origin asset whose closure produced at least one changed entry. Empty when every pulled row is already current. A root whose closure can't be re-resolved (e.g. the origin world is unreachable) is silently skipped rather than aborting the whole scan.
local reports = world.checkUpdates()
globals/world/checkout
world.checkout(branch: string) -> string
Switch this session to another branch — git checkout <branch>.
The branch must already exist (create one with world.createBranch).
The tree is replaced by the branch's own content. Which branch this
session is on is this session's alone; the branch itself is shared,
so others may be on the one you move onto.
Uncommitted work is not at risk: it already has its row on the
branch it was written against and is in the tree again when you
check that branch out.
Returns only once the branch's content has landed, so world.head,
world.log, world.commit and the VFS all target the new branch
immediately afterwards.
Parameters
branchstring— The branch to switch to.
Returns string — The branch now checked out.
world.checkout("feature")
globals/world/commit
world.commit(message: string, opts: CommitOpts?) -> string
Open-or-resume a staging area, set the message, and materialise
the commit. Commits ONLY what's already staged via world.add /
world.add_all — git semantics, not git commit -a. The reducer
auto-deletes the stage row on success so a subsequent
world.commit opens a fresh one.
{ stage = "<name>" } materialises one of the caller's own staging
areas, so the commit carries the paths staged under that name and
leaves every other caller's staged. When a commit from another
caller has landed since the area was opened, this brings the area
onto the branch head there is now and commits it there.
Pre-flight .zmignore refs gate: every staged source's
aggregated deps (via asset.deps, which recurses composite
asset folders) are checked against the live ignore set. If
any dep target's path is currently ignored AND the dep target
is not itself in the stage, the commit is refused. This
mirrors the closure invariant — a commit whose deps can't
resolve cleanly shouldn't land. Force-staging the dep
alongside (world.add(dep, { force = true })) makes the
ignored dep satisfy the gate.
Parameters
messagestring— The commit message.optsCommitOpts(optional) — Optional{ stage: string? }naming the staging area to materialise. Omitted, the commit materialises the shared default area.
Returns string — The newly-allocated commit id (ULID string).
local id = world.commit("feat: ship widget")
local id = world.commit("fauna: the swallow colony", { stage = "fauna" })
globals/world/conflicts
world.conflicts() -> { ConflictEntry }
List every locally-pulled row currently flagged conflicted —
the findable surface world.pullAsset leaves behind on an
unresolved merge. Read-only.
Returns { ConflictEntry } — An array of ConflictEntry, one per conflicted row. Empty when nothing is conflicted.
local list = world.conflicts()
globals/world/connectedUsers
world.connectedUsers -> any
Returns any
globals/world/contentRequirements
world.contentRequirements(scope: { string }?) -> { { asset: string, typeName: string, detail: string } }
List the world's unmet content requirements: user-authored assets
whose type-declared content constraints are not yet satisfied (an
empty README, a .metadata with no description or tags — the
empty-skeleton state a fresh create emits for the author to fill).
The same walk world.push gates on: push refuses while this list is
non-empty, and world.publishBlockers reports it as the content
class beside the other two. Empty list = every checked asset meets
its type's contract.
Parameters
scope{ string }(optional) — Asset paths to restrict the check to — pass a status read's dirty + staged paths to check only content that would actually publish (a per-file path matches its containing asset; each path resolves directly, with no world enumeration). Omit for the full-world walk the push gate performs.
Returns { { asset: string, typeName: string, detail: string } } — Array of { asset, typeName, detail } requirement rows.
for _, r in ipairs(world.contentRequirements()) do print(r.asset, r.detail) end
globals/world/contribute
world.contribute(opts: ContributeOpts?) -> { ContributeOutcome }
Send improvements to installed content back upstream — git subtree push ending in a pull request. For each targeted origin
world: the diverging subtree is remapped to the origin's canonical
paths, three-way merged against the origin's CURRENT content
(regions the origin also changed become local conflicts to
resolve first), pushed as a contrib/<id> branch in the origin
world, and opened as a pull request there. With merge (the
default) the pull request is merged immediately when authorized —
a refusal leaves it open and reported, never a failure. After a
merge, the local fork re-pulls so its origin pins advance and the
asset no longer reads as ahead.
Parameters
optsContributeOpts(optional) — OptionalContributeOpts—targets(origin world guids; default all ahead),merge(default true),title,description,dryRun.
Returns { ContributeOutcome } — Array of ContributeOutcome, one per targeted origin.
local r = world.contribute({})
globals/world/createBranch
world.createBranch(name: string, fromCommit: string?)
Create a branch — git branch <name> [<start>]. The branch
starts at fromCommit (defaults to the session branch's HEAD)
and gets its own working tree, materialized from that commit.
The session stays on its current branch; move onto it with
world.checkout("<branch>") (git checkout).
Parameters
namestring— The new branch name.fromCommitstring(optional) — Commit id to start at. Defaults toworld.head().
world.createBranch("feature")
globals/world/deleteBranch
world.deleteBranch(branch: string)
Delete a branch — git branch -D <name>. Drops the branch and
the working tree it owns; its commits are left alone, since deleting
a branch is dropping the name and the tree under it, not rewriting
history. Uncommitted work on that branch goes with it and is NOT
recoverable from trash, so the call refuses the first time and
returns the affirmation needed to go through with it — affirm with
world.affirm(<token>). Refuses the branch this session is on
(check out another first) and the world's last branch.
Parameters
branchstring— The branch to delete.
world.deleteBranch("feature")
globals/world/diff
world.diff(...: string) -> any
Mirror git diff's CLI arg shape. Returns per-file diffs
by default; pass --stat for summary stats, --name-only for
just paths. Positional commit ids drive the two sources;
--staged pivots to staged-vs-HEAD. ---separated args
scope the diff to specific paths. --stage=<name> reads one of the
caller's own staging areas in place of the shared default one.
Parameters
...string— Variadic string args: flags, commit ids,--, path filters.
Returns any — Array of DiffFile tables (or string-list for --name-only).
local files = world.diff()
local files = world.diff("--staged")
local files = world.diff("abc", "def")
local names = world.diff("--name-only")
local files = world.diff("--staged", "--stage=fauna")
globals/world/discard
world.discard(opts: StageOpts?)
Drop a staging area without committing. Live manifest dirty flags are preserved so the user can re-stage later. No-op if the area doesn't exist.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area to drop. Omitted, the call drops the shared default area.
world.discard()
world.discard({ stage = "fauna" })
globals/world/discardFile
world.discardFile(path: string)
Discard one file's unstaged working edits, taking its content
back to what it was staged or committed as — the
git restore <path> shape. The stage is the baseline when the
path is staged, the last commit when it is not, and where it is
neither there is nothing to come back to, so the path goes away.
Staging is left exactly as it was; world.unstage is the verb
that changes it. One shot: a path that reverts to a committed
version snapshots the discarded bytes to trash first, so that
case is recoverable via world.restore(<handle>). Errors when
the path is not dirty (nothing to discard).
Parameters
pathstring— The VFS path whose unstaged edits to discard.
world.discardFile("/source/foo.luau")
globals/world/fetch
world.fetch(branch: string?) -> FetchResult
Update the origin/<branch> remote-tracking ref — git fetch.
Mirrors the world's ZeroMind branch head into the local commit
history (no working-tree change) and reports how the session
branch relates to it: behind origin commits to pull, ahead
local commits to push, diverged when both. A stale installed
pin or out-of-band ZeroMind change shows up here as behind —
reconcile with world.pull().
Parameters
branchstring(optional) — Remote branch to fetch. Defaults to the session branch.
Returns FetchResult — A FetchResult table.
local f = world.fetch()
globals/world/forkLive
world.forkLive(opts: { source: string, sourceBranch: string?, maxBatches: number? }) -> number
Seed THIS (empty) world's live content from another world by
copying its whole manifest as clean Pulled rows — the
in-engine half of "fork a world". Provenance is preserved: each
row points at the content's ORIGINAL owner (a fork of a
fork-of-A still points at A), so the fork never claims to have
authored what it pulled. The copy runs server-side in bounded
batches (idempotent + resumable), looping until the source is
fully mirrored. Pair with world.add_all() + world.commit() +
world.push() to publish the fork.
Parameters
opts{ source: string, sourceBranch: string?, maxBatches: number? }—{ source, sourceBranch?, maxBatches? }—sourceis the source world GUID; branches default to"main";maxBatchescaps the batch loop (default 60 ⇒ up to ~120k entries).
Returns number — The number of pulled (dirty) rows now staged-pending on the fork.
world.forkLive({ source = "e89aa92e-4c1f-460e-acd8-73859dd3a346" })
globals/world/forkStatus
world.forkStatus() -> { ForkStatus }
Per-asset "ahead of origin" report — the fork analogue of git
status against an upstream. Every installed (pulled) row whose
content diverges from its pinned origin is listed, partitioned by
the TRUE origin world it was pulled from (nested dependencies
carry the world that authored them, not the intermediary they
arrived through). This is information for judgment: decide
whether a change belongs upstream, then world.contribute.
Returns { ForkStatus } — Array of ForkStatus partitions.
for _, f in ipairs(world.forkStatus()) do print(f.origin_world, #f.entries) end
globals/world/head
world.head() -> string?
Return the current branch HEAD commit id, or nil if the branch has no commits yet.
Returns string? — The commit id string, or nil.
local id = world.head()
globals/world/installAsset
world.installAsset(opts: InstallAssetOpts) -> InstallAssetResult
Install a published asset into this world, pulling the asset and every dependency it closes over and writing them into the source tree. Reports what it wrote so a caller can tell a fresh install from a no-op.
Parameters
optsInstallAssetOpts—{ guid }names the root asset to install;refpins a specific commit-id instead of the latest.
Returns InstallAssetResult — { assets_written, blobs_downloaded, root_guid, root_path, root_version, deps }.
local r = world.installAsset({ guid = assetGuid })
globals/world/installLibrary
world.installLibrary(opts: InstallLibraryOpts) -> InstallLibraryResult
Declarative cross-world dependency. Writes a single marker
file at /source/libs/@<name> whose body is the
zero/world-import/v1 JSON. The next commit ships it as one
regular manifest entry; ZM's import-derivation pass at
finalize-time decodes the marker and stamps the new commit's
imports[]. Unmodified library content never ships in the
importing world's tree — it's fetched from the source world
on demand by the engine's library resolver.
Parameters
optsInstallLibraryOpts— SeeInstallLibraryOpts.opts.worldis the upstream world's guid (required).opts.commitis the upstream commit_id to pin (optional; resolvesopts.reformainif omitted).opts.asis the local library name (defaults to the upstream world's slug).opts.refis the human-meaningful ref recorded in the marker.
Returns InstallLibraryResult summarising the install.
world.installLibrary({ world = "guid", as = "combat" })
globals/world/installedAssets
world.installedAssets() -> { InstalledAsset }
Every asset this world carries from ZeroMind, keyed by the published guid it was pulled from. The read that answers "what is actually in this world" by identity rather than by path — an installed asset's local name can be chosen by the installer, so a path is not the thing to check a pull against.
Returns { InstalledAsset } — An array of InstalledAsset sorted by local path, one per pulled row carrying a published guid.
for _, a in ipairs(world.installedAssets()) do print(a.asset_guid, a.path) end
globals/world/list
world.list() -> { WorldEntry }
List every world the authenticated user has access to.
Calls the spacetime list_my_worlds procedure which wraps
ZeroMind's GET /v1/me/worlds. Flattens each entry to one
record per world with the role promoted to a top-level field.
Returns { WorldEntry } — Array of WorldEntry records.
local worlds = world.list()
globals/world/log
world.log(opts: LogOpts?) -> { CommitRow }
Return the commit log for the current branch, newest first.
Pass opts.path to get the per-path history (git log -- <path>):
only the commits that touched that file, newest-first.
Parameters
optsLogOpts(optional) — Optional.opts.limitcaps the number of commits (default 50, 0 = all).opts.pathscopes the log to one file.
Returns { CommitRow } — Array of CommitRow tables.
local commits = world.log({ limit = 20 })
local touched = world.log({ path = "/source/foo.luau" })
globals/world/merge
world.merge(sourceBranch: string) -> MergeResult
Merge another branch into the session branch — git merge <source>. The merge runs locally in the world's SpacetimeDB clone
and is abortable with world.mergeAbort; nothing reaches ZeroMind
until the result is pushed. Requires a clean working tree (commit
or stash first — that is also what makes abort exact). Clean →
a two-parent merge commit lands on the session branch and the
merged content appears in the working tree. Conflicts → git-style
markers are projected into each conflicting text file, the
cleanly-merged remainder is applied as working-tree changes, and
world.vcsStatus().unmerged lists what needs attention: resolve
each path (edit out the markers / rewrite / remove the file),
then world.add + world.commit — that commit records the merge
(second parent = the source head) and clears the unmerged set.
Parameters
sourceBranchstring— The branch to merge in.
Returns MergeResult — A MergeResult — status is clean (with commit), conflicts (with conflicts), or up_to_date.
local r = world.merge("feature")
globals/world/mergeAbort
world.mergeAbort()
Abort the in-progress merge — git merge --abort. Clears the
unmerged set and restores the working tree to the pre-merge state
(the target head's committed content; the branch head never moved
during a conflicted merge). Errors when no merge is in progress.
world.mergeAbort()
globals/world/offLoaded
world.offLoaded(handle: number) -> boolean
Stop a callback registered with world.onLoaded from running.
Parameters
handlenumber— The handleworld.onLoadedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offLoaded(h)
globals/world/offSaved
world.offSaved(handle: number) -> boolean
Stop a callback registered with world.onSaved from running.
Parameters
handlenumber— The handleworld.onSavedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offSaved(h)
globals/world/offUnloaded
world.offUnloaded(handle: number) -> boolean
Stop a callback registered with world.onUnloaded from running.
Parameters
handlenumber— The handleworld.onUnloadedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offUnloaded(h)
globals/world/onLoaded
world.onLoaded(cb: (...any) -> ()) -> number
Register a callback to run after a world finishes loading.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offLoaded.
local h = world.onLoaded(function() log.info("loaded") end)
globals/world/onSaved
world.onSaved(cb: (...any) -> ()) -> number
Register a callback to run after a world is saved.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offSaved.
local h = world.onSaved(function() log.info("saved") end)
globals/world/onUnloaded
world.onUnloaded(cb: (...any) -> ()) -> number
Register a callback to run after a world is unloaded.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offUnloaded.
local h = world.onUnloaded(function() log.info("unloaded") end)
globals/world/prConflicts
world.prConflicts(worldGuid: string?, number: number) -> any
Read a pull request's conflicts — what stands between it and a
merge. Returns the mergeability verdict, the merge base, both
heads, and one entry per conflicting path. A conflicting TEXT path
carries marked_text: the same <<<<<<< / ======= / >>>>>>>
rendering a merge leaves in the working tree, with the source and
target sides laid against their common ancestor. Resolve a path by
writing the settled bytes back to it and committing on the source
branch; the pull request re-analyses on the next read. A binary
path carries the two sides' hashes and no text — pick a side.
A mergeable pull request returns an empty conflict list.
Parameters
worldGuidstring(optional) — The world the pull request lives in. Defaults to the bound world.numbernumber— The pull request number.
Returns any — Decoded ZeroMind conflicts response.
local c = world.prConflicts(nil, 3)
for _, m in ipairs(world.prConflicts(originGuid, 3).markers) do print(m.path, m.marked_text) end
globals/world/prList
world.prList(worldGuid: string?, number: number?) -> any
Parameters
worldGuidstring(optional)numbernumber(optional)
Returns any
globals/world/prMerge
world.prMerge(worldGuid: string, number: number, strategy: string?) -> any
Merge a pull request — the agent-side merge button.
Parameters
worldGuidstring— The world the pull request lives in.numbernumber— The pull request number.strategystring(optional) —merge(default),squash, orfast_forward.
Returns any — Decoded ZeroMind merge response.
world.prMerge(originGuid, 3)
globals/world/prOpen
world.prOpen(opts: PrOpenOpts) -> any
List a world's pull requests, or fetch one.
Open a pull request — gh pr create. Proposes the work on one
(world, branch) pair to another. Defaults make the common cases one
argument: from a fork, the target is the world it was forked from, so
world.prOpen({ title = "..." }) proposes your work upstream. In an
ordinary world the target is the same world, so you get a
branch → main pull request.
The PR lives in — and is numbered by — the world it targets, exactly
as a forge numbers pull requests on the upstream repository. That is
also where world.prList finds it.
Parameters
optsPrOpenOpts—title(required), plusdescription,sourceWorld,sourceBranch,targetWorld,targetBranchto address any leg explicitly.
Returns any — Decoded ZeroMind response. The decoded ZeroMind pull request.
local prs = world.prList()
world.prOpen({ title = "fix the door hinge" })
world.prOpen({ title = "port the fix", targetWorld = otherGuid })
globals/world/prView
world.prView(worldGuid: string?, number: number) -> any
Read one pull request in full — gh pr view. Returns the record
plus a LIVE re-analysis against the current branch heads: mergeability
(clean / conflicts / fast_forwardable / up_to_date /
unrelated), conflict_count, and diff — every path the request
adds, modifies or deletes with its checksums. Read this before merging:
it is what tells you WHAT the request changes.
Parameters
worldGuidstring(optional) — The world the pull request lives in (its target world). Defaults to the session world.numbernumber— The pull request number.
Returns any — The decoded pull request view.
world.prView(nil, 1)
globals/world/previewInstall
world.previewInstall(opts: InstallAssetOpts) -> PreviewResult
Preview what installing an asset WOULD write, without writing
anything. Fetches + decodes the closure and plans placement (the
same helpers world.installAsset uses), returning a flat node
list plus rollup totals. A truncated closure is reported (not
raised) so a caller can surface it and block import.
Parameters
optsInstallAssetOpts—{ guid, at?, ref? }— same shape as installAsset.
Returns PreviewResult — nodes + totals + a truncated flag.
world.previewInstall({ guid = "..." })
globals/world/publishBlockers
world.publishBlockers() -> { PublishBlockerClass }
List every reason world.push would refuse to publish this
world, as one entry per blocker class: script errors in user
content, assets that don't meet their type's content requirements,
and asset references that can't be statically pinned. Each class
carries the same title the refusal prints, one items entry per
offending subject (an asset identity, or a <path>:<line> site),
and the single remedy covering that class. This is the account
world.push composes its refusal from, so it names the same
blockers with no push attempted — and in full, where a refusal
bounds how many of a class it prints. Empty list = the world
publishes. zm status prints this list.
Returns { PublishBlockerClass } — Array of PublishBlockerClass entries, empty when nothing blocks.
for _, c in ipairs(world.publishBlockers()) do
for _, i in ipairs(c.items) do print(c.kind, i.subject, i.detail) end
end
globals/world/pull
world.pull(branch: string?) -> PullResult
Fetch and reconcile with origin — git pull. Strictly behind →
fast-forward (the branch head moves to the origin mirror, no merge
commit). Diverged → three-way merge of the origin mirror, with the
same conflict/marker/resolve flow as world.merge (resolve the
unmerged paths, then world.add + world.commit; abortable with
world.mergeAbort). Requires a clean working tree.
Parameters
branchstring(optional) — Remote branch to pull. Defaults to the session branch.
Returns PullResult — A PullResult table.
local r = world.pull()
globals/world/pullAsset
world.pullAsset(opts: PullAssetOpts?) -> PullAssetResult
Pull upstream changes into a previously-installed asset,
three-way reconciling each entry against local edits. Re-resolves
the root's transitive closure at opts.ref (default latest),
then for every entry decides fast_forward / noop / converged
/ merge from (row.origin_checksum, localChecksum, entry.checksum) (M.__reconcileDecision):
noop— upstream hasn't moved; skipped.fast_forward/converged— the entry's latest text is written to the local path and the row's origin pointer advances. Binary and composite entries can't be content-synced through this call's only cross-world read primitive (text only), so a non-text entry with a real upstream change is surfaced as a conflict instead of silently going stale.merge(text entries) — a three-wayvcs.merge3runs over (base, local, theirs); a clean result is written and the origin pointer advances, a conflicted result is written WITH markers and the row is flagged conflicted (base + theirs checksums recorded forworld.resolvePullConflict).merge(binary / composite entries) — no text merge is possible; flagged conflicted with the structured base/theirs checksums (no marker write).
Closure drift: an entry the original install never landed is pulled fresh (added). A locally-pulled row nested under the root's own directory whose origin entry disappeared from the closure is removed when clean (pruned), or flagged conflicted when it carries local edits.
Parameters
optsPullAssetOpts(optional) — SeePullAssetOpts.opts.guidoropts.pathis required;opts.refpins the re-resolve to a specific upstream commit (defaults to latest finalized).
Returns PullAssetResult summarising what merged, conflicted, was pruned, and was newly added.
world.pullAsset({ guid = "..." })
globals/world/push
world.push(commitId: string?) -> (string?, string)
Publish the current branch to ZeroMind. The no-argument form
is a git merge --squash push: EVERY unpushed commit on the
branch collapses into a SINGLE ZeroMind commit (latest content
per path, parented on the branch's current remote HEAD). Because
only the merged final state's bytes are uploaded, a superseded or
lost intermediate-commit blob can never break the push — this is
what makes a churn-heavy world publishable. The local commit
history is preserved as the editing journal; on success every
squashed commit shares the one remote commit id. The explicit
commitId form still pushes that single commit verbatim via
publish_commit (advanced / chain-replay use; its parent must
already be on the remote).
Parameters
commitIdstring(optional) — Optional. A single commit to push verbatim. Omit for the squash push of the whole unpushed stack (the normal path).
Returns (string?, string) — Two values: the ZeroMind-allocated commit id, and the verdict. "published" with the new commit id when this call published; "already-published" when ZeroMind already carries what this call would have published — the state a push asks for, so it returns rather than raising. That verdict carries the commit's existing ZeroMind id when the publish names one (the single-commit form), and a nil id when it names none (the whole-stack form, which reports a chain). A squash whose merged final state carries dep.unresolved problems takes the slow path inside the same call: the engine re-resolves each pending literal against its live asset index and submits the resolutions to the publish procedure, which completes the push. A reference literal that still resolves to nothing is published with the asset holding it and stays a problem recorded on that asset, while a dep pin the squash severed raises instead; either way the engine names each one with its path, line, reference and reason. A publish ZeroMind refuses raises naming the condition and the command that clears it — a branch that moved under this push names world.pull().
local zmId = world.push()
local zmId, verdict = world.push()
world.push("01HABC...")
globals/world/reset
world.reset(targetCommitId: string) -> string?
Rewind HEAD to targetCommitId in one shot. Non-destructive
— orphaned commits stay in storage and each becomes a trash
entry the user can world.restore (in chain order) to re-attach
the branch. Errors when targetCommitId is not an ancestor of
HEAD. Returns a summary of what was rewound.
Parameters
targetCommitIdstring— The commit id to rewind to.
Returns string? — A summary message: the target plus the list of commits rewound past.
world.reset("01HABC...")
globals/world/resolveConflict
world.resolveConflict(path: string, mode: string, content: string?) -> ResolveResult
Resolve one conflicted /source record, one path at a time.
mode is merge | apply | take-local | take-backend |
discard. merge returns { merged, clean } and mutates nothing
— a clean merge can be finalized with apply, and a conflicted one
carries <<<<<<< / ======= / >>>>>>> markers to edit first. apply
writes the finalized content to /source; take-local writes the
retained local bytes; take-backend / discard keep the backend
head. Errors when the record is absent, a merge has no common
ancestor or hits binary content, or a write fails.
Parameters
pathstring— Canonical/sourcepath of the conflicted record.modestring— One of merge | apply | take-local | take-backend | discard.contentstring(optional) — Finalized bytes forapplymode.
Returns ResolveResult
world.resolveConflict("/zero/source/foo.luau", "take-local")
local r = world.resolveConflict(p, "merge"); if r.clean then world.resolveConflict(p, "apply", r.merged) end
globals/world/resolvePullConflict
world.resolvePullConflict(path: string, choice: string)
Resolve a conflicted row. A TEXT conflict is one where the file
currently contains conflict markers (world.pullAsset writes
markers only for a text three-way merge that didn't resolve
cleanly); a BINARY/composite conflict has no markers — the local
bytes were left untouched.
choice = "theirs" fetches clean upstream text by content hash
(conflict_theirs_blob_sha256 — path/rename-independent, since
blobs are content-addressed) and overwrites the local file with it
before clearing the flag. It errors, refusing to guess, when the
row records no theirs blob (a binary/composite conflict — a text
blob read can't address theirs for those; keep "ours" or
re-install the asset instead).
choice = "ours" keeps the local side. When the file carries
conflict markers, the local side is reconstructed from them (the
marker writers put ours first, so dropping each block's base and
theirs sections restores your bytes exactly) and written back;
a marker-free file is kept as-is. Either way the flag clears.
Either way, staging the resolved file (world.add) is the normal
git-add path once this returns — this call only clears the
manifest-level flag and (for "theirs") the file content.
Parameters
pathstring— The conflicted row's local VFS path.choicestring—"ours"or"theirs".
world.resolvePullConflict("/source/combat/rules.luau", "theirs")
globals/world/restore
world.restore(handle: any?)
Restore one trash entry by row_id. Errors verbatim on handler-not-yet-implemented / world-mismatch.
Parameters
handleany(optional) — The trash row id. May be a number or a numeric string.
world.restore(42)
globals/world/show
world.show(...: string) -> any
Mirror git show's CLI arg shape. Default returns commit
metadata + full diff vs parent. world.show("commit:/path")
returns just the bytes. Flags: --stat, --name-only.
Parameters
...string— Variadic string args: commit id, optional path, optional flags.
Returns any — Either a ShowResult table or a string (for commit:/path).
local r = world.show("abc123")
local r = world.show("--stat", "abc123")
local bytes = world.show("abc123:/foo.luau")
globals/world/startup_scene
world.startup_scene -> any
Returns any
globals/world/stash
world.stash(label: string?)
Save the caller's current pending dirty + staged state on
the active (world, branch) into a stash row. label is
optional free-form text. Non-destructive — dirty + staged
state is preserved on disk.
Parameters
labelstring(optional) — Optional. Free-form text label for the stash.
world.stash("wip widget refactor")
globals/world/stashDrop
world.stashDrop(handle: any?)
Request an affirmation token to drop a stash. Always
errors — successful mint surfaces the token as affirmation required: zm affirm <token>. The agent runs zm affirm <token> to actually drop; restoration via world.restore()
reappears the stash under a new row_id.
Parameters
handleany(optional) — The stash row id. May be a number or numeric string.
world.stashDrop(7)
globals/world/stashPop
world.stashPop(handle: any?) -> StashSnapshot
Author-only. Pop the stash row (deletes it server-side) and return the decoded snapshot. The caller is responsible for re-applying the snapshot to disk via the normal write paths (so ACL gates fire on every restored path).
Parameters
handleany(optional) — The stash row id. May be a number or numeric string.
Returns StashSnapshot containing dirty + staged entries.
local snap = world.stashPop(7)
globals/world/stashes
world.stashes() -> { StashRow }
List every stash row in the world. Anyone with read access
sees every stash; the per-row author_hex makes it clear
which entries the caller can pop / drop themselves.
Returns { StashRow } — Array of StashRow tables.
local rows = world.stashes()
globals/world/status
world.status -> any
Returns any
globals/world/status_text
world.status_text -> any
Returns any
globals/world/syncStatus
world.syncStatus() -> SyncStatus
Read the durable-sync status: { subscribed, content_synced, progress, pending_writes, unsaved_writes, uploads_abandoned, conflicts, binding }. conflicts maps each conflicted /source
path to { base_sha, local_sha, backend_sha, isBinary }.
unsaved_writes lists the /source paths this session wrote that
the server does not hold, and uploads_abandoned counts the uploads
the queue stopped carrying — read those two to tell a queue working
through a backlog from one that gave content up, which
pending_writes alone reads the same for. binding names which
world holds /source — bound, unbound, binding,
session_only, or unclassified before the boot has decided — and,
when an authorization attempt is on record,
which attempt is running and what the last one answered. Read
binding to tell a world that is still coming from one that was
never asked for: subscribed answers false for both. Synchronous.
Returns SyncStatus
local s = world.syncStatus(); print(s.pending_writes)
local s = world.syncStatus(); for _, p in ipairs(s.unsaved_writes) do print(p) end
local s = world.syncStatus(); if s.binding.awaiting_world then print(s.binding.state, s.binding.attempt) end
globals/world/trash
world.trash() -> { TrashRow }
List trash entries for the world. Anyone with read access
to the world can list trash — recovery is a shared safety net,
not a privacy boundary. The handle (row_id) feeds back into
world.restore.
Returns { TrashRow } — Array of TrashRow tables.
local rows = world.trash()
globals/world/uninstallLibrary
world.uninstallLibrary(name: string) -> string
Delete the library marker file. name accepts "@combat"
or "combat" (the leading @ is the convention carried by
the on-disk path).
Parameters
namestring— The library name, with or without the leading@.
Returns string — The marker path that was removed.
world.uninstallLibrary("@combat")
globals/world/unstage
world.unstage(path: string, opts: StageOpts?)
Remove a path from the staging area. Live manifest dirty state is untouched.
Parameters
pathstring— The path to unstage.optsStageOpts(optional) — Optional{ stage: string? }naming the staging area the path was staged into. Omitted, the call acts on the shared default area.
world.unstage("/source/foo.luau")
world.unstage("/source/foo.luau", { stage = "fauna" })
globals/world/vcsStatus
world.vcsStatus(opts: StageOpts?) -> StatusResult
Return the working-tree VCS status: dirty paths, staged
entries, ignored paths, untracked paths, and any unmerged ones.
An untracked path is one with no committed version behind it, and
it appears in dirty as well — git add . picks up new files too.
Named vcsStatus (not status) because world.status() is the
runtime-snapshot accessor owned by world_status.module; the
source-control surface keeps its own VCS-specific name so the
two never shadow each other.
Each dirty[i].dirtied_by is the identity of the most recent
writer; dirty_since_micros is the microsecond timestamp of the
first write of the current dirty run. local_identity is this
session's own writer identity in that same namespace — compare
the two to tell your own writes from another account's.
claimed_by_other_stages names the paths some other staging area
holds and which area holds each — the grain that tells two callers
apart when they share one writer identity, and the set
world.add_all holds back.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area whose staged set to report. The dirty, ignored and untracked sets are the world's working tree and read the same whichever area is named.
Returns StatusResult — A StatusResult table.
local s = world.vcsStatus()
local s = world.vcsStatus({ stage = "fauna" })
typed/builtin//modules/world_sync/world/resolveConflict
world.resolveConflict(path: string, mode: string, content: string?) -> ResolveResult
Resolve one conflicted /source record, one path at a time.
mode is merge | apply | take-local | take-backend |
discard. merge returns { merged, clean } and mutates nothing
— a clean merge can be finalized with apply, and a conflicted one
carries <<<<<<< / ======= / >>>>>>> markers to edit first. apply
writes the finalized content to /source; take-local writes the
retained local bytes; take-backend / discard keep the backend
head. Errors when the record is absent, a merge has no common
ancestor or hits binary content, or a write fails.
Parameters
pathstring— Canonical/sourcepath of the conflicted record.modestring— One of merge | apply | take-local | take-backend | discard.contentstring(optional) — Finalized bytes forapplymode.
Returns ResolveResult
world.resolveConflict("/zero/source/foo.luau", "take-local")
local r = world.resolveConflict(p, "merge"); if r.clean then world.resolveConflict(p, "apply", r.merged) end
typed/builtin//modules/world_sync/world/syncStatus
world.syncStatus() -> SyncStatus
Read the durable-sync status: { subscribed, content_synced, progress, pending_writes, unsaved_writes, uploads_abandoned, conflicts, binding }. conflicts maps each conflicted /source
path to { base_sha, local_sha, backend_sha, isBinary }.
unsaved_writes lists the /source paths this session wrote that
the server does not hold, and uploads_abandoned counts the uploads
the queue stopped carrying — read those two to tell a queue working
through a backlog from one that gave content up, which
pending_writes alone reads the same for. binding names which
world holds /source — bound, unbound, binding,
session_only, or unclassified before the boot has decided — and,
when an authorization attempt is on record,
which attempt is running and what the last one answered. Read
binding to tell a world that is still coming from one that was
never asked for: subscribed answers false for both. Synchronous.
Returns SyncStatus
local s = world.syncStatus(); print(s.pending_writes)
local s = world.syncStatus(); for _, p in ipairs(s.unsaved_writes) do print(p) end
local s = world.syncStatus(); if s.binding.awaiting_world then print(s.binding.state, s.binding.attempt) end
typed/builtin//modules/world_vcs/world/add
world.add(path: string, opts: AddOpts?)
Stage one path's manifest row, expanding to the full asset
family if the path lives inside a composite asset. Idempotent
at (stage, manifest_row). Pass { force = true } to bypass
the .zmignore / .gitignore gate — same intent as
git add -f. Without force, attempts to stage an ignored
path (or a path whose .refs points at an ignored dep)
error.
{ stage = "<name>" } stages into one of the caller's own staging
areas instead of the shared default one, so a commit naming that area
freezes these paths and leaves every other caller's staged.
Parameters
pathstring— The path to stage. Must be a non-empty string.optsAddOpts(optional) — Optional{ force: boolean?, stage: string? }. Defaults to{ force = false }on the default staging area.
world.add("/source/foo.luau")
world.add("/source/scene_dirty/entities/42.json", { force = true })
world.add("/source/fauna.module", { stage = "fauna" })
typed/builtin//modules/world_vcs/world/add_all
world.add_all(opts: StageOpts?) -> { string }
Stage the dirty manifest rows this caller can claim, skipping
any path that matches .zmignore / .gitignore. Paths that match
an ignore pattern are silently skipped — world.add(path, { force = true }) is the explicit way to override the gate
for an individual path. Rows still flagged conflicted by
world.pullAsset are held back too — resolve them (edit +
world.add(path), or world.resolvePullConflict) and re-run.
A path another staging area holds is held back as well: the
working tree is one per branch and staging areas are not, so a
path some other caller has already selected for a commit of its
own belongs to that caller until it commits or hands it over.
world.add(path) names a path deliberately and takes it either
way, which is how a claim is handed over — and a path this area
already holds stays staged here, whoever else holds it too.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area to stage into. Omitted, the call stages into the shared default area.
Returns { string } — The paths that were held back — those another staging area holds, then the conflicted ones. Empty when none were.
world.add_all()
world.add_all({ stage = "fauna" })
typed/builtin//modules/world_vcs/world/affirm
world.affirm(token: string)
Consume an XXX-XXX-XXX-style affirmation token returned by
a destructive op surface (e.g. vfs.remove). The destruction
commits atomically with the pending-row delete; the token is
one-shot. Errors verbatim on expiry / wrong-user.
Parameters
tokenstring— The affirmation token.
world.affirm("ABC-DEF-GHI")
typed/builtin//modules/world_vcs/world/assetInstallable
world.assetInstallable(opts: AssetInstallableOpts) -> AssetInstallableReport
Report whether one published asset can be installed on its own,
without raising. world.previewInstall plans a real install and
raises when the closure will not resolve; this answers the prior
question — does this guid name something ZeroMind will hand over by
itself — as a verdict a caller can branch on. Content that ships
inside a larger library (a module inside a package, a material
inside a system) carries its own published identity, so the answer
is per asset rather than per library. Reads only.
Parameters
optsAssetInstallableOpts—{ guid }names the asset;refpins a commit-id instead of the latest.
Returns AssetInstallableReport — An AssetInstallableReport — installable, a verdict, a one-line detail, and the closure's nodes / tree_children / deps shape when one resolved.
world.assetInstallable({ guid = asset.guid("@builtin::materials.neon") })
typed/builtin//modules/world_vcs/world/awaitOutgoingSync
world.awaitOutgoingSync()
Wait until every /source write this session made has reached
the branch it was written against. Raises naming the paths that did
not land. add / commit / push / checkout / merge / pull
already wait on their own; call this before rebinding after a burst
of writes, which world.checkout and world.swap refuse over.
Returns Nothing. Raises when a write did not land.
world.awaitOutgoingSync() ; world.checkout("main")
typed/builtin//modules/world_vcs/world/branches
world.branches() -> { { branch: string, commit_id: string, current: boolean } }
Every branch this world has, with the commit each one's head
names and which one this session is on — git branch --list. Sorted
by name. A branch exists for everyone in the world; which one you are
on is yours alone, so current is true for at most one row here and
says nothing about where anybody else is.
Returns { { branch: string, commit_id: string, current: boolean } } — Array of { branch, commit_id, current }.
for _, b in ipairs(world.branches()) do print(b.branch, b.commit_id) end
typed/builtin//modules/world_vcs/world/checkUpdates
world.checkUpdates() -> { UpdateReport }
Discover upstream changes for every asset this world has
pulled. Read-only — makes no local mutation and no VCS write.
Each locally-pulled row identifies its own origin asset via
origin_asset_guid (recorded as that entry's own asset guid at
pull time — see world.installAsset). For every distinct
origin asset among the pulled rows, this re-resolves that
asset's latest transitive closure and compares each returned
entry's checksum against the matching local row's recorded
origin_checksum.
Returns { UpdateReport } — An array of UpdateReport, one per re-resolved origin asset whose closure produced at least one changed entry. Empty when every pulled row is already current. A root whose closure can't be re-resolved (e.g. the origin world is unreachable) is silently skipped rather than aborting the whole scan.
local reports = world.checkUpdates()
typed/builtin//modules/world_vcs/world/checkout
world.checkout(branch: string) -> string
Switch this session to another branch — git checkout <branch>.
The branch must already exist (create one with world.createBranch).
The tree is replaced by the branch's own content. Which branch this
session is on is this session's alone; the branch itself is shared,
so others may be on the one you move onto.
Uncommitted work is not at risk: it already has its row on the
branch it was written against and is in the tree again when you
check that branch out.
Returns only once the branch's content has landed, so world.head,
world.log, world.commit and the VFS all target the new branch
immediately afterwards.
Parameters
branchstring— The branch to switch to.
Returns string — The branch now checked out.
world.checkout("feature")
typed/builtin//modules/world_vcs/world/commit
world.commit(message: string, opts: CommitOpts?) -> string
Open-or-resume a staging area, set the message, and materialise
the commit. Commits ONLY what's already staged via world.add /
world.add_all — git semantics, not git commit -a. The reducer
auto-deletes the stage row on success so a subsequent
world.commit opens a fresh one.
{ stage = "<name>" } materialises one of the caller's own staging
areas, so the commit carries the paths staged under that name and
leaves every other caller's staged. When a commit from another
caller has landed since the area was opened, this brings the area
onto the branch head there is now and commits it there.
Pre-flight .zmignore refs gate: every staged source's
aggregated deps (via asset.deps, which recurses composite
asset folders) are checked against the live ignore set. If
any dep target's path is currently ignored AND the dep target
is not itself in the stage, the commit is refused. This
mirrors the closure invariant — a commit whose deps can't
resolve cleanly shouldn't land. Force-staging the dep
alongside (world.add(dep, { force = true })) makes the
ignored dep satisfy the gate.
Parameters
messagestring— The commit message.optsCommitOpts(optional) — Optional{ stage: string? }naming the staging area to materialise. Omitted, the commit materialises the shared default area.
Returns string — The newly-allocated commit id (ULID string).
local id = world.commit("feat: ship widget")
local id = world.commit("fauna: the swallow colony", { stage = "fauna" })
typed/builtin//modules/world_vcs/world/conflicts
world.conflicts() -> { ConflictEntry }
List every locally-pulled row currently flagged conflicted —
the findable surface world.pullAsset leaves behind on an
unresolved merge. Read-only.
Returns { ConflictEntry } — An array of ConflictEntry, one per conflicted row. Empty when nothing is conflicted.
local list = world.conflicts()
typed/builtin//modules/world_vcs/world/contentRequirements
world.contentRequirements(scope: { string }?) -> { { asset: string, typeName: string, detail: string } }
List the world's unmet content requirements: user-authored assets
whose type-declared content constraints are not yet satisfied (an
empty README, a .metadata with no description or tags — the
empty-skeleton state a fresh create emits for the author to fill).
The same walk world.push gates on: push refuses while this list is
non-empty, and world.publishBlockers reports it as the content
class beside the other two. Empty list = every checked asset meets
its type's contract.
Parameters
scope{ string }(optional) — Asset paths to restrict the check to — pass a status read's dirty + staged paths to check only content that would actually publish (a per-file path matches its containing asset; each path resolves directly, with no world enumeration). Omit for the full-world walk the push gate performs.
Returns { { asset: string, typeName: string, detail: string } } — Array of { asset, typeName, detail } requirement rows.
for _, r in ipairs(world.contentRequirements()) do print(r.asset, r.detail) end
typed/builtin//modules/world_vcs/world/contribute
world.contribute(opts: ContributeOpts?) -> { ContributeOutcome }
Send improvements to installed content back upstream — git subtree push ending in a pull request. For each targeted origin
world: the diverging subtree is remapped to the origin's canonical
paths, three-way merged against the origin's CURRENT content
(regions the origin also changed become local conflicts to
resolve first), pushed as a contrib/<id> branch in the origin
world, and opened as a pull request there. With merge (the
default) the pull request is merged immediately when authorized —
a refusal leaves it open and reported, never a failure. After a
merge, the local fork re-pulls so its origin pins advance and the
asset no longer reads as ahead.
Parameters
optsContributeOpts(optional) — OptionalContributeOpts—targets(origin world guids; default all ahead),merge(default true),title,description,dryRun.
Returns { ContributeOutcome } — Array of ContributeOutcome, one per targeted origin.
local r = world.contribute({})
typed/builtin//modules/world_vcs/world/createBranch
world.createBranch(name: string, fromCommit: string?)
Create a branch — git branch <name> [<start>]. The branch
starts at fromCommit (defaults to the session branch's HEAD)
and gets its own working tree, materialized from that commit.
The session stays on its current branch; move onto it with
world.checkout("<branch>") (git checkout).
Parameters
namestring— The new branch name.fromCommitstring(optional) — Commit id to start at. Defaults toworld.head().
world.createBranch("feature")
typed/builtin//modules/world_vcs/world/deleteBranch
world.deleteBranch(branch: string)
Delete a branch — git branch -D <name>. Drops the branch and
the working tree it owns; its commits are left alone, since deleting
a branch is dropping the name and the tree under it, not rewriting
history. Uncommitted work on that branch goes with it and is NOT
recoverable from trash, so the call refuses the first time and
returns the affirmation needed to go through with it — affirm with
world.affirm(<token>). Refuses the branch this session is on
(check out another first) and the world's last branch.
Parameters
branchstring— The branch to delete.
world.deleteBranch("feature")
typed/builtin//modules/world_vcs/world/diff
world.diff(...: string) -> any
Mirror git diff's CLI arg shape. Returns per-file diffs
by default; pass --stat for summary stats, --name-only for
just paths. Positional commit ids drive the two sources;
--staged pivots to staged-vs-HEAD. ---separated args
scope the diff to specific paths. --stage=<name> reads one of the
caller's own staging areas in place of the shared default one.
Parameters
...string— Variadic string args: flags, commit ids,--, path filters.
Returns any — Array of DiffFile tables (or string-list for --name-only).
local files = world.diff()
local files = world.diff("--staged")
local files = world.diff("abc", "def")
local names = world.diff("--name-only")
local files = world.diff("--staged", "--stage=fauna")
typed/builtin//modules/world_vcs/world/discard
world.discard(opts: StageOpts?)
Drop a staging area without committing. Live manifest dirty flags are preserved so the user can re-stage later. No-op if the area doesn't exist.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area to drop. Omitted, the call drops the shared default area.
world.discard()
world.discard({ stage = "fauna" })
typed/builtin//modules/world_vcs/world/discardFile
world.discardFile(path: string)
Discard one file's unstaged working edits, taking its content
back to what it was staged or committed as — the
git restore <path> shape. The stage is the baseline when the
path is staged, the last commit when it is not, and where it is
neither there is nothing to come back to, so the path goes away.
Staging is left exactly as it was; world.unstage is the verb
that changes it. One shot: a path that reverts to a committed
version snapshots the discarded bytes to trash first, so that
case is recoverable via world.restore(<handle>). Errors when
the path is not dirty (nothing to discard).
Parameters
pathstring— The VFS path whose unstaged edits to discard.
world.discardFile("/source/foo.luau")
typed/builtin//modules/world_vcs/world/fetch
world.fetch(branch: string?) -> FetchResult
Update the origin/<branch> remote-tracking ref — git fetch.
Mirrors the world's ZeroMind branch head into the local commit
history (no working-tree change) and reports how the session
branch relates to it: behind origin commits to pull, ahead
local commits to push, diverged when both. A stale installed
pin or out-of-band ZeroMind change shows up here as behind —
reconcile with world.pull().
Parameters
branchstring(optional) — Remote branch to fetch. Defaults to the session branch.
Returns FetchResult — A FetchResult table.
local f = world.fetch()
typed/builtin//modules/world_vcs/world/forkLive
world.forkLive(opts: { source: string, sourceBranch: string?, maxBatches: number? }) -> number
Seed THIS (empty) world's live content from another world by
copying its whole manifest as clean Pulled rows — the
in-engine half of "fork a world". Provenance is preserved: each
row points at the content's ORIGINAL owner (a fork of a
fork-of-A still points at A), so the fork never claims to have
authored what it pulled. The copy runs server-side in bounded
batches (idempotent + resumable), looping until the source is
fully mirrored. Pair with world.add_all() + world.commit() +
world.push() to publish the fork.
Parameters
opts{ source: string, sourceBranch: string?, maxBatches: number? }—{ source, sourceBranch?, maxBatches? }—sourceis the source world GUID; branches default to"main";maxBatchescaps the batch loop (default 60 ⇒ up to ~120k entries).
Returns number — The number of pulled (dirty) rows now staged-pending on the fork.
world.forkLive({ source = "e89aa92e-4c1f-460e-acd8-73859dd3a346" })
typed/builtin//modules/world_vcs/world/forkStatus
world.forkStatus() -> { ForkStatus }
Per-asset "ahead of origin" report — the fork analogue of git
status against an upstream. Every installed (pulled) row whose
content diverges from its pinned origin is listed, partitioned by
the TRUE origin world it was pulled from (nested dependencies
carry the world that authored them, not the intermediary they
arrived through). This is information for judgment: decide
whether a change belongs upstream, then world.contribute.
Returns { ForkStatus } — Array of ForkStatus partitions.
for _, f in ipairs(world.forkStatus()) do print(f.origin_world, #f.entries) end
typed/builtin//modules/world_vcs/world/head
world.head() -> string?
Return the current branch HEAD commit id, or nil if the branch has no commits yet.
Returns string? — The commit id string, or nil.
local id = world.head()
typed/builtin//modules/world_vcs/world/installAsset
world.installAsset(opts: InstallAssetOpts) -> InstallAssetResult
Install a published asset into this world, pulling the asset and every dependency it closes over and writing them into the source tree. Reports what it wrote so a caller can tell a fresh install from a no-op.
Parameters
optsInstallAssetOpts—{ guid }names the root asset to install;refpins a specific commit-id instead of the latest.
Returns InstallAssetResult — { assets_written, blobs_downloaded, root_guid, root_path, root_version, deps }.
local r = world.installAsset({ guid = assetGuid })
typed/builtin//modules/world_vcs/world/installLibrary
world.installLibrary(opts: InstallLibraryOpts) -> InstallLibraryResult
Declarative cross-world dependency. Writes a single marker
file at /source/libs/@<name> whose body is the
zero/world-import/v1 JSON. The next commit ships it as one
regular manifest entry; ZM's import-derivation pass at
finalize-time decodes the marker and stamps the new commit's
imports[]. Unmodified library content never ships in the
importing world's tree — it's fetched from the source world
on demand by the engine's library resolver.
Parameters
optsInstallLibraryOpts— SeeInstallLibraryOpts.opts.worldis the upstream world's guid (required).opts.commitis the upstream commit_id to pin (optional; resolvesopts.reformainif omitted).opts.asis the local library name (defaults to the upstream world's slug).opts.refis the human-meaningful ref recorded in the marker.
Returns InstallLibraryResult summarising the install.
world.installLibrary({ world = "guid", as = "combat" })
typed/builtin//modules/world_vcs/world/installedAssets
world.installedAssets() -> { InstalledAsset }
Every asset this world carries from ZeroMind, keyed by the published guid it was pulled from. The read that answers "what is actually in this world" by identity rather than by path — an installed asset's local name can be chosen by the installer, so a path is not the thing to check a pull against.
Returns { InstalledAsset } — An array of InstalledAsset sorted by local path, one per pulled row carrying a published guid.
for _, a in ipairs(world.installedAssets()) do print(a.asset_guid, a.path) end
typed/builtin//modules/world_vcs/world/list
world.list() -> { WorldEntry }
List every world the authenticated user has access to.
Calls the spacetime list_my_worlds procedure which wraps
ZeroMind's GET /v1/me/worlds. Flattens each entry to one
record per world with the role promoted to a top-level field.
typed/builtin//modules/world_vcs/world/log
world.log(opts: LogOpts?) -> { CommitRow }
Return the commit log for the current branch, newest first.
Pass opts.path to get the per-path history (git log -- <path>):
only the commits that touched that file, newest-first.
Parameters
optsLogOpts(optional) — Optional.opts.limitcaps the number of commits (default 50, 0 = all).opts.pathscopes the log to one file.
Returns { CommitRow } — Array of CommitRow tables.
local commits = world.log({ limit = 20 })
local touched = world.log({ path = "/source/foo.luau" })
typed/builtin//modules/world_vcs/world/merge
world.merge(sourceBranch: string) -> MergeResult
Merge another branch into the session branch — git merge <source>. The merge runs locally in the world's SpacetimeDB clone
and is abortable with world.mergeAbort; nothing reaches ZeroMind
until the result is pushed. Requires a clean working tree (commit
or stash first — that is also what makes abort exact). Clean →
a two-parent merge commit lands on the session branch and the
merged content appears in the working tree. Conflicts → git-style
markers are projected into each conflicting text file, the
cleanly-merged remainder is applied as working-tree changes, and
world.vcsStatus().unmerged lists what needs attention: resolve
each path (edit out the markers / rewrite / remove the file),
then world.add + world.commit — that commit records the merge
(second parent = the source head) and clears the unmerged set.
Parameters
sourceBranchstring— The branch to merge in.
Returns MergeResult — A MergeResult — status is clean (with commit), conflicts (with conflicts), or up_to_date.
local r = world.merge("feature")
typed/builtin//modules/world_vcs/world/mergeAbort
world.mergeAbort()
Abort the in-progress merge — git merge --abort. Clears the
unmerged set and restores the working tree to the pre-merge state
(the target head's committed content; the branch head never moved
during a conflicted merge). Errors when no merge is in progress.
world.mergeAbort()
typed/builtin//modules/world_vcs/world/prConflicts
world.prConflicts(worldGuid: string?, number: number) -> any
Read a pull request's conflicts — what stands between it and a
merge. Returns the mergeability verdict, the merge base, both
heads, and one entry per conflicting path. A conflicting TEXT path
carries marked_text: the same <<<<<<< / ======= / >>>>>>>
rendering a merge leaves in the working tree, with the source and
target sides laid against their common ancestor. Resolve a path by
writing the settled bytes back to it and committing on the source
branch; the pull request re-analyses on the next read. A binary
path carries the two sides' hashes and no text — pick a side.
A mergeable pull request returns an empty conflict list.
Parameters
worldGuidstring(optional) — The world the pull request lives in. Defaults to the bound world.numbernumber— The pull request number.
Returns any — Decoded ZeroMind conflicts response.
local c = world.prConflicts(nil, 3)
for _, m in ipairs(world.prConflicts(originGuid, 3).markers) do print(m.path, m.marked_text) end
typed/builtin//modules/world_vcs/world/prList
world.prList(worldGuid: string?, number: number?) -> any
Parameters
worldGuidstring(optional)numbernumber(optional)
Returns any
typed/builtin//modules/world_vcs/world/prMerge
world.prMerge(worldGuid: string, number: number, strategy: string?) -> any
Merge a pull request — the agent-side merge button.
Parameters
worldGuidstring— The world the pull request lives in.numbernumber— The pull request number.strategystring(optional) —merge(default),squash, orfast_forward.
Returns any — Decoded ZeroMind merge response.
world.prMerge(originGuid, 3)
typed/builtin//modules/world_vcs/world/prOpen
world.prOpen(opts: PrOpenOpts) -> any
List a world's pull requests, or fetch one.
Open a pull request — gh pr create. Proposes the work on one
(world, branch) pair to another. Defaults make the common cases one
argument: from a fork, the target is the world it was forked from, so
world.prOpen({ title = "..." }) proposes your work upstream. In an
ordinary world the target is the same world, so you get a
branch → main pull request.
The PR lives in — and is numbered by — the world it targets, exactly
as a forge numbers pull requests on the upstream repository. That is
also where world.prList finds it.
Parameters
optsPrOpenOpts—title(required), plusdescription,sourceWorld,sourceBranch,targetWorld,targetBranchto address any leg explicitly.
Returns any — Decoded ZeroMind response. The decoded ZeroMind pull request.
local prs = world.prList()
world.prOpen({ title = "fix the door hinge" })
world.prOpen({ title = "port the fix", targetWorld = otherGuid })
typed/builtin//modules/world_vcs/world/prView
world.prView(worldGuid: string?, number: number) -> any
Read one pull request in full — gh pr view. Returns the record
plus a LIVE re-analysis against the current branch heads: mergeability
(clean / conflicts / fast_forwardable / up_to_date /
unrelated), conflict_count, and diff — every path the request
adds, modifies or deletes with its checksums. Read this before merging:
it is what tells you WHAT the request changes.
Parameters
worldGuidstring(optional) — The world the pull request lives in (its target world). Defaults to the session world.numbernumber— The pull request number.
Returns any — The decoded pull request view.
world.prView(nil, 1)
typed/builtin//modules/world_vcs/world/previewInstall
world.previewInstall(opts: InstallAssetOpts) -> PreviewResult
Preview what installing an asset WOULD write, without writing
anything. Fetches + decodes the closure and plans placement (the
same helpers world.installAsset uses), returning a flat node
list plus rollup totals. A truncated closure is reported (not
raised) so a caller can surface it and block import.
Parameters
optsInstallAssetOpts—{ guid, at?, ref? }— same shape as installAsset.
Returns PreviewResult — nodes + totals + a truncated flag.
world.previewInstall({ guid = "..." })
typed/builtin//modules/world_vcs/world/publishBlockers
world.publishBlockers() -> { PublishBlockerClass }
List every reason world.push would refuse to publish this
world, as one entry per blocker class: script errors in user
content, assets that don't meet their type's content requirements,
and asset references that can't be statically pinned. Each class
carries the same title the refusal prints, one items entry per
offending subject (an asset identity, or a <path>:<line> site),
and the single remedy covering that class. This is the account
world.push composes its refusal from, so it names the same
blockers with no push attempted — and in full, where a refusal
bounds how many of a class it prints. Empty list = the world
publishes. zm status prints this list.
Returns { PublishBlockerClass } — Array of PublishBlockerClass entries, empty when nothing blocks.
for _, c in ipairs(world.publishBlockers()) do
for _, i in ipairs(c.items) do print(c.kind, i.subject, i.detail) end
end
typed/builtin//modules/world_vcs/world/pull
world.pull(branch: string?) -> PullResult
Fetch and reconcile with origin — git pull. Strictly behind →
fast-forward (the branch head moves to the origin mirror, no merge
commit). Diverged → three-way merge of the origin mirror, with the
same conflict/marker/resolve flow as world.merge (resolve the
unmerged paths, then world.add + world.commit; abortable with
world.mergeAbort). Requires a clean working tree.
Parameters
branchstring(optional) — Remote branch to pull. Defaults to the session branch.
Returns PullResult — A PullResult table.
local r = world.pull()
typed/builtin//modules/world_vcs/world/pullAsset
world.pullAsset(opts: PullAssetOpts?) -> PullAssetResult
Pull upstream changes into a previously-installed asset,
three-way reconciling each entry against local edits. Re-resolves
the root's transitive closure at opts.ref (default latest),
then for every entry decides fast_forward / noop / converged
/ merge from (row.origin_checksum, localChecksum, entry.checksum) (M.__reconcileDecision):
noop— upstream hasn't moved; skipped.fast_forward/converged— the entry's latest text is written to the local path and the row's origin pointer advances. Binary and composite entries can't be content-synced through this call's only cross-world read primitive (text only), so a non-text entry with a real upstream change is surfaced as a conflict instead of silently going stale.merge(text entries) — a three-wayvcs.merge3runs over (base, local, theirs); a clean result is written and the origin pointer advances, a conflicted result is written WITH markers and the row is flagged conflicted (base + theirs checksums recorded forworld.resolvePullConflict).merge(binary / composite entries) — no text merge is possible; flagged conflicted with the structured base/theirs checksums (no marker write).
Closure drift: an entry the original install never landed is pulled fresh (added). A locally-pulled row nested under the root's own directory whose origin entry disappeared from the closure is removed when clean (pruned), or flagged conflicted when it carries local edits.
Parameters
optsPullAssetOpts(optional) — SeePullAssetOpts.opts.guidoropts.pathis required;opts.refpins the re-resolve to a specific upstream commit (defaults to latest finalized).
Returns PullAssetResult summarising what merged, conflicted, was pruned, and was newly added.
world.pullAsset({ guid = "..." })
typed/builtin//modules/world_vcs/world/push
world.push(commitId: string?) -> (string?, string)
Publish the current branch to ZeroMind. The no-argument form
is a git merge --squash push: EVERY unpushed commit on the
branch collapses into a SINGLE ZeroMind commit (latest content
per path, parented on the branch's current remote HEAD). Because
only the merged final state's bytes are uploaded, a superseded or
lost intermediate-commit blob can never break the push — this is
what makes a churn-heavy world publishable. The local commit
history is preserved as the editing journal; on success every
squashed commit shares the one remote commit id. The explicit
commitId form still pushes that single commit verbatim via
publish_commit (advanced / chain-replay use; its parent must
already be on the remote).
Parameters
commitIdstring(optional) — Optional. A single commit to push verbatim. Omit for the squash push of the whole unpushed stack (the normal path).
Returns (string?, string) — Two values: the ZeroMind-allocated commit id, and the verdict. "published" with the new commit id when this call published; "already-published" when ZeroMind already carries what this call would have published — the state a push asks for, so it returns rather than raising. That verdict carries the commit's existing ZeroMind id when the publish names one (the single-commit form), and a nil id when it names none (the whole-stack form, which reports a chain). A squash whose merged final state carries dep.unresolved problems takes the slow path inside the same call: the engine re-resolves each pending literal against its live asset index and submits the resolutions to the publish procedure, which completes the push. A reference literal that still resolves to nothing is published with the asset holding it and stays a problem recorded on that asset, while a dep pin the squash severed raises instead; either way the engine names each one with its path, line, reference and reason. A publish ZeroMind refuses raises naming the condition and the command that clears it — a branch that moved under this push names world.pull().
local zmId = world.push()
local zmId, verdict = world.push()
world.push("01HABC...")
typed/builtin//modules/world_vcs/world/reset
world.reset(targetCommitId: string) -> string?
Rewind HEAD to targetCommitId in one shot. Non-destructive
— orphaned commits stay in storage and each becomes a trash
entry the user can world.restore (in chain order) to re-attach
the branch. Errors when targetCommitId is not an ancestor of
HEAD. Returns a summary of what was rewound.
Parameters
targetCommitIdstring— The commit id to rewind to.
Returns string? — A summary message: the target plus the list of commits rewound past.
world.reset("01HABC...")
typed/builtin//modules/world_vcs/world/resolvePullConflict
world.resolvePullConflict(path: string, choice: string)
Resolve a conflicted row. A TEXT conflict is one where the file
currently contains conflict markers (world.pullAsset writes
markers only for a text three-way merge that didn't resolve
cleanly); a BINARY/composite conflict has no markers — the local
bytes were left untouched.
choice = "theirs" fetches clean upstream text by content hash
(conflict_theirs_blob_sha256 — path/rename-independent, since
blobs are content-addressed) and overwrites the local file with it
before clearing the flag. It errors, refusing to guess, when the
row records no theirs blob (a binary/composite conflict — a text
blob read can't address theirs for those; keep "ours" or
re-install the asset instead).
choice = "ours" keeps the local side. When the file carries
conflict markers, the local side is reconstructed from them (the
marker writers put ours first, so dropping each block's base and
theirs sections restores your bytes exactly) and written back;
a marker-free file is kept as-is. Either way the flag clears.
Either way, staging the resolved file (world.add) is the normal
git-add path once this returns — this call only clears the
manifest-level flag and (for "theirs") the file content.
Parameters
pathstring— The conflicted row's local VFS path.choicestring—"ours"or"theirs".
world.resolvePullConflict("/source/combat/rules.luau", "theirs")
typed/builtin//modules/world_vcs/world/restore
world.restore(handle: any?)
Restore one trash entry by row_id. Errors verbatim on handler-not-yet-implemented / world-mismatch.
Parameters
handleany(optional) — The trash row id. May be a number or a numeric string.
world.restore(42)
typed/builtin//modules/world_vcs/world/show
world.show(...: string) -> any
Mirror git show's CLI arg shape. Default returns commit
metadata + full diff vs parent. world.show("commit:/path")
returns just the bytes. Flags: --stat, --name-only.
Parameters
...string— Variadic string args: commit id, optional path, optional flags.
Returns any — Either a ShowResult table or a string (for commit:/path).
local r = world.show("abc123")
local r = world.show("--stat", "abc123")
local bytes = world.show("abc123:/foo.luau")
typed/builtin//modules/world_vcs/world/stash
world.stash(label: string?)
Save the caller's current pending dirty + staged state on
the active (world, branch) into a stash row. label is
optional free-form text. Non-destructive — dirty + staged
state is preserved on disk.
Parameters
labelstring(optional) — Optional. Free-form text label for the stash.
world.stash("wip widget refactor")
typed/builtin//modules/world_vcs/world/stashDrop
world.stashDrop(handle: any?)
Request an affirmation token to drop a stash. Always
errors — successful mint surfaces the token as affirmation required: zm affirm <token>. The agent runs zm affirm <token> to actually drop; restoration via world.restore()
reappears the stash under a new row_id.
Parameters
handleany(optional) — The stash row id. May be a number or numeric string.
world.stashDrop(7)
typed/builtin//modules/world_vcs/world/stashPop
world.stashPop(handle: any?) -> StashSnapshot
Author-only. Pop the stash row (deletes it server-side) and return the decoded snapshot. The caller is responsible for re-applying the snapshot to disk via the normal write paths (so ACL gates fire on every restored path).
Parameters
handleany(optional) — The stash row id. May be a number or numeric string.
Returns StashSnapshot containing dirty + staged entries.
local snap = world.stashPop(7)
typed/builtin//modules/world_vcs/world/stashes
world.stashes() -> { StashRow }
List every stash row in the world. Anyone with read access
sees every stash; the per-row author_hex makes it clear
which entries the caller can pop / drop themselves.
Returns { StashRow } — Array of StashRow tables.
local rows = world.stashes()
typed/builtin//modules/world_vcs/world/trash
world.trash() -> { TrashRow }
List trash entries for the world. Anyone with read access
to the world can list trash — recovery is a shared safety net,
not a privacy boundary. The handle (row_id) feeds back into
world.restore.
Returns { TrashRow } — Array of TrashRow tables.
local rows = world.trash()
typed/builtin//modules/world_vcs/world/uninstallLibrary
world.uninstallLibrary(name: string) -> string
Delete the library marker file. name accepts "@combat"
or "combat" (the leading @ is the convention carried by
the on-disk path).
Parameters
namestring— The library name, with or without the leading@.
Returns string — The marker path that was removed.
world.uninstallLibrary("@combat")
typed/builtin//modules/world_vcs/world/unstage
world.unstage(path: string, opts: StageOpts?)
Remove a path from the staging area. Live manifest dirty state is untouched.
Parameters
pathstring— The path to unstage.optsStageOpts(optional) — Optional{ stage: string? }naming the staging area the path was staged into. Omitted, the call acts on the shared default area.
world.unstage("/source/foo.luau")
world.unstage("/source/foo.luau", { stage = "fauna" })
typed/builtin//modules/world_vcs/world/vcsStatus
world.vcsStatus(opts: StageOpts?) -> StatusResult
Return the working-tree VCS status: dirty paths, staged
entries, ignored paths, untracked paths, and any unmerged ones.
An untracked path is one with no committed version behind it, and
it appears in dirty as well — git add . picks up new files too.
Named vcsStatus (not status) because world.status() is the
runtime-snapshot accessor owned by world_status.module; the
source-control surface keeps its own VCS-specific name so the
two never shadow each other.
Each dirty[i].dirtied_by is the identity of the most recent
writer; dirty_since_micros is the microsecond timestamp of the
first write of the current dirty run. local_identity is this
session's own writer identity in that same namespace — compare
the two to tell your own writes from another account's.
claimed_by_other_stages names the paths some other staging area
holds and which area holds each — the grain that tells two callers
apart when they share one writer identity, and the set
world.add_all holds back.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area whose staged set to report. The dirty, ignored and untracked sets are the world's working tree and read the same whichever area is named.
Returns StatusResult — A StatusResult table.
local s = world.vcsStatus()
local s = world.vcsStatus({ stage = "fauna" })