- name
- git-strata
- description
- Use when working in a repo managed by git-strata (macOS), or when the user asks you to start, checkpoint, diff, restore, publish or remove a git-strata session. Gives each agent session its own isolated APFS-cloned workspace over a shared base snapshot; covers the CLI workflow, the JSON envelope, error codes, and the commands an agent must hand to a human.
# git-strata
git-strata gives every coding session its own workspace: an APFS clone of a
shared, sealed base snapshot of each repo (plus its dependency set). One
session's edits never change another session's files, but they are readable:
`status --all`, `grep`/`files` with `--session S` or `--all`, and
`diff --session S` / `--between` show other sessions' content. Edits are
recorded as checkpoints, combined and
committed by `publish`, and sent to the remote only by `git-strata push` or a
publish that is configured to push. macOS only (APFS, FSEvents, launchd).
Use it when the user (or the repo's setup) works in git-strata sessions, when
several agents edit the same repos in parallel, or when you are asked to
checkpoint / restore / publish. `git strata ...` and `git-strata ...` are the
same binary. Full command and flag table: [reference.md](reference.md).
## Prerequisites
- `git-strata` is on `PATH`. The config lives in
`~/Library/Application Support/git-strata/config.toml`; the daemon socket is
`~/Library/Application Support/git-strata/daemon.sock`, the pinned safety
flags `~/Library/Application Support/git-strata/pins.json` (no env var or
flag changes them; only `$HOME` does). Workspaces live under the data root
(`[paths] root`, default `~/.strata`) as
`<root>/sessions/<session-id>/<repo>/`.
- The daemon reads `config.toml` only at start; after a config change, tell
the human the daemon needs a restart. Do not restart it yourself.
- **Agents never start, stop or restart the daemon** (`git-strata daemon
start` / `stop` are human-only; never run them, or `launchctl`, yourself,
not even with a temp `HOME`: the LaunchAgent domain is per user, not per
HOME). Check it
with `git-strata daemon status` (`data.running`). If it is down, keep
working in the workspace and tell the human it is down so they can run
`git-strata daemon start`. While it is down the hook answers `Busy`
`daemon_down` and that checkpoint is not recorded: tell the human which
edits were not checkpointed. Do not write a plist yourself.
- Without the daemon, only these work (read-only, straight from the store):
`status`, `grep`, `files`, `diff`, `deps list`, `daemon status`, and a
repeated `checkpoint --tool-call ID` whose checkpoint already exists. Every
other command returns `Busy` with `detail.reason = "daemon_down"`.
## Workflow
```sh
# 1. New session (ticket is just a label). Name the repos you will touch with
# --repo (repeatable): only those get a workspace, all tracked. This is the
# fast form; use it. A repo that links a sibling by relative path (../x)
# needs that sibling too: pass it, or add it later (step 1b).
# (Without --repo, `--repos A,B` gives EVERY configured repo a workspace
# and tracks only A,B; do not edit the untracked siblings.)
git-strata new PROJ-123 --repo tool --repo core-lib
# data.manifest.id -> session id, e.g. "s-k3q7xm2a"
# data.manifest.repos.<repo>.path -> that repo's workspace directory
# data.warnings -> [] when fine
# data.manifest.agent -> the agent recorded for this session
# The session records which AI agent created it. Claude Code, Codex and
# Cursor are detected from their environment. Any other agent: run
# `export STRATA_AGENT=<name>` (e.g. aider; 1-64 of [A-Za-z0-9._-],
# starting with a letter or digit) before `new`, or pass --agent <name>.
# A warning of kind AgentNotRecorded means the running daemon is older
# than this CLI; the session is fine, only the agent is not recorded.
# 1b. Need another repo later? Add it to the same session (tracked):
git-strata add --repo front # or: --session S --repo front
# data.added.<repo>.path; data.already lists repos it had already.
# 2. Work inside the workspace. Commands without --session use the cwd.
cd "<data.manifest.repos.tool.path>"
# ... edit files normally ...
# 3. Checkpoint (one of --tool-call / -m, never both)
git-strata checkpoint -m "parse config before connect"
git-strata checkpoint -m "only these" --path src/a.ts --path ../core-lib/x.php
# 4. Inspect
git-strata status # this session; --all for every session
git-strata diff # this session vs its pinned base (= --base)
git-strata diff --between s-aaaa1111 s-bbbb2222 --repo tool # two sessions
git-strata grep 'TODO' # this session; --session S or --all
git-strata files 'src/**/*.ts' # glob optional
# 5. Undo to a checkpoint (all paths, or only the listed ones)
git-strata restore 42
git-strata restore 42 src/a.ts
# 6. Publish checkpointed content into a commit
git-strata publish --from s-k3q7xm2a:src/a.ts --from s-k3q7xm2a:src/b.ts
# 7. Push the publish chain (only if the human asked; may need a TTY)
git-strata push tool
# 8. Remove the session once its work is published
git-strata rm --session s-k3q7xm2a
```
Notes:
- `--session` takes a session id or a ticket. A ticket shared by several
sessions gives `AmbiguousMatch`; use an id from `detail`. `--session` and
`--all` together is `InvalidInput` (`both_selectors`).
- `checkpoint` may record more than one repo of the session: every path is
mapped to the session repo whose workspace contains it; paths outside the
session are skipped. With no paths, every tracked repo of the session is
scanned. `data.checkpoints` has one row per repo recorded.
- `-m` records a new checkpoint only when something changed since the
latest one; with no change it returns that latest checkpoint (its id and
message, not yours). `--tool-call ID` is idempotent per (session, repo, ID):
every new ID records a row (even with no change), and repeating it returns
the same rows.
- `publish` publishes the selected paths' current content: before sending
the publish it records a fresh `BeforePublish` checkpoint of each
`--from` batch, so edits made to a selected file up to that moment are
included. There is no option to publish an older checkpoint; to keep an
edit out, revert it (or `restore` the path) before publishing.
Without `--from` it is interactive on a TTY and `InvalidInput`
(`no_batch`) otherwise, so agents always pass `--from SESSION:PATH`.
PATH is relative to the repo root with no repo prefix (`billing-api/app/x.php`
is `--from "${S}:app/x.php"`). Write `"${S}:path"`, never `"$S:path"`: zsh
reads `$S:t`, `$S:h`, ... as modifiers. Path mistakes come back before any
TTY check: `InvalidInput` `from_repo_prefix` (rerun with
`error.detail.expected`), `InvalidInput` `from_path` (no workspace has it),
`AmbiguousMatch` `from_repo` (several have it and not exactly one of their
checkpoints changed it; rerun with one of `detail.expected`). To pick the
repo yourself use `--from "${S}:REPO:PATH"` (e.g. `"${S}:admin:src/a.ts"`);
the middle part is REPO only when it is one of the session's repos.
- `publish` replies only after checks, commit, push and the rebase of every
session. `git push` waits for the remote's own hooks (bounded at 120 s,
then `Unknown` `push_unknown`), the rebase is unbounded: a minute or two of
silence after the remote branch moved is normal. Wait; do not interrupt or
rerun.
- Release builds never run in a workspace (no remotes, detached `HEAD`, push
guard). Publish (and push), then in the main checkout `git pull --ff-only`,
check `HEAD` equals `data.commits.<repo>`, and run the build there; the
build script is the human's step unless they asked you to run it. See
README "Release builds from a session".
- Multi-repo `publish` commits repo by repo. When one repo hits `Conflict`,
repos before it have already committed (and pushed, if push is on); the
call does not roll them back. See "Publish conflict: fix and re-run" below.
- `diff` and `diff --between` preview current workspace content, checkpointed
or not. Identical files are not listed; binary / >64 MiB entries carry
`code: "Binary"` / `"TooLarge"` instead of a preview and the call stays ok.
- `grep` with no hits is `ok` with `"hits": []` (not an error). Skipped binary
or oversized files are listed in `data.skipped`.
- `deps propose [--session S] [--repo R]` proposes a new dependency set after
you change install inputs; `deps list` shows open proposals. Add `--switch`
to move your own session onto the proposed set right away (no prompt; other
sessions stay pinned). Check `data.switched.kind`: `skipped` with
`code: OpenFiles` means a process holds files in your deps dir; stop it (only
if you started it) and run again. Approving for other sessions is
human-only (below). `data.pending_proposals: [{id, repo}]` in any
envelope means a proposal waits for the human.
- If you change install inputs (package.json deps, bun.lock, patches,
pubspec.yaml / pubspec.lock, ...) and publish without switching, publish
still runs the checks with deps that match the staged inputs: it uses a
sealed set (warning `DepsSubstituted`) or installs one first (slower;
warning `DepsReinstalled`, and the history shows an extra row stopped with
`Drift`). Plain drift does not refuse the publish; it can still stop with
`Drift` when the inputs move again during that install (`deps_drift`) or
when the deps dirs overlap files you publish (`deps_select_failed`,
`tracked_overlap`: follow its `fix`/`hint`). Still run the warning's `fix`
(`deps propose --switch`) so your workspace matches. `status` shows the same
`drift.fix`. If that install fails (`detail.stage: "deps_install"`), hand it
to the human with `detail.log_tail`; do not retry in a loop (a retry
returns `previous_attempt`).
## Publish conflict: fix and re-run
This is the expected loop for agents; the human is not needed unless the
conflict is `assigned`.
1. `publish` returns `ok: false`, `error.code = "Conflict"`.
- `error.detail.repo` is the repo that conflicted; nothing was committed
there.
- `error.detail.conflicts` lists its paths (with the hunks).
- `data.commits` / `data.pushes` hold repos that already landed in this
call (also in `error.detail.published_before`). They stay landed.
The same holds for any publish error: a repo whose commit was written
before its push timed out (`Unknown`) or was refused (`PushRefused`) is
in `data.commits` and `data.pushes` too.
2. Fix the listed paths in that repo's workspace (merge in the upstream
change; `git-strata diff` helps).
3. Optionally `git-strata checkpoint -m "resolve conflict" --path P ...`
to keep a named point; the re-run snapshots the paths itself.
4. Re-run the exact same `publish --from ...` command. Repos that already
landed are not committed again: they appear in
`data.already_published` as `{repo, publish, commit, push}`, and only the
rest is published (`data.commits`). The check is per `--from` path and by
content: a path (or directory) whose content is exactly what landed is
skipped; anything new or changed under it since (including new files in
a directory you selected) is published as a new commit, and the unchanged
paths of the same `--from` list do not cause `NoMatch`.
5. Repeat while another repo conflicts.
## Dev servers and `tauri dev`: per-session ports
Every session workspace gets its own block of 4 ports (a lease), so two
sessions can run `tauri dev` (or any dev server) at the same time. The main
checkout has no lease and keeps the project's default ports. Never change
ports by editing committed files (`tauri.conf.json`, `package.json`,
`next.config.*`, `.env`); start the server through git-strata instead:
```bash
cd <your workspace> # data.manifest.repos.<repo>.path
git-strata session ports # read the lease: data.leases[0].ports / .env
git-strata session exec -- bun run dev # any server that reads $PORT (Next.js does)
git-strata session exec -- sh -c 'vite --port "$PORT" --strictPort'
git-strata session exec --tauri -- bun run tauri dev
```
- `exec` runs the command in place of itself (its output, its exit code)
in exactly one workspace (`--repo R` when the session has several) with:
`PORT` = `STRATA_DEV_PORT` (web), `STRATA_CONNECTOR_PORT`,
`STRATA_CONNECTOR_MCP_PORT`, `STRATA_AUX_PORT`, `STRATA_SESSION`,
`STRATA_REPO`, `TAURI_CONNECTOR_PROJECT` (the Tauri app dir) and
`STRATA_TAURI_DEV_CONFIG`. Child processes (e.g. Tauri's
`beforeDevCommand`) inherit them.
- `--tauri` adds `--config $STRATA_TAURI_DEV_CONFIG` to `tauri dev`, which
points `build.devUrl` at `http://localhost:$PORT`. The frontend dev server
must then listen on `$PORT`. It understands `tauri dev`, `cargo tauri dev`,
`npx|bunx|pnpx tauri dev`, `pnpm exec tauri dev`, and `bun|pnpm|yarn|npm
run <script>` when that script is literally `tauri dev ...` (or the bare
`tauri` CLI, with `dev` right after the script name: `bun run tauri dev`,
`npm run tauri -- dev`). Always write `run`: `bun <script>` without it is
refused, since the package manager may run its own command of that name.
Options that pick another package (`--prefix`, `--filter`, `-w`,
`--workspace`, `--cwd`, ...) and, for npm, any option before its `--` are
refused. Anything else is `InvalidInput` `tauri_launcher`; use the form in
`detail.fix`:
`git-strata session exec -- sh -c 'tauri dev --config "$STRATA_TAURI_DEV_CONFIG"'`.
- `exec` does not set tauri-connector's variables. tauri-connector 0.17.7+
binds exactly `TAURI_CONNECTOR_PORT`, `TAURI_CONNECTOR_MCP_PORT` and
`TAURI_CONNECTOR_BRIDGE_PORT`; map the lease onto them:
`git-strata session exec -- sh -c 'TAURI_CONNECTOR_PORT=$STRATA_CONNECTOR_PORT TAURI_CONNECTOR_MCP_PORT=$STRATA_CONNECTOR_MCP_PORT TAURI_CONNECTOR_BRIDGE_PORT=$STRATA_AUX_PORT exec tauri dev --config "$STRATA_TAURI_DEV_CONFIG"'`.
Precedence: a port in the app's builder code > the exact env variable >
`TAURI_CONNECTOR_*_PORT_RANGE` > a builder range > the default. A busy
exact port is not replaced: the connector does not start (the app keeps
running); free the port or `--reassign`, then restart. The bound ports
are in the `.connector.json` under your workspace's `target/` (copies
from the main checkout are removed when the session is created).
- `exec`'s variables reach only the command it starts and its children, not
a later connector command or an MCP server that is already running. When
you talk to the app from such a client, select it explicitly: `tauri-connector
... --project <tauri_app_root>` (`tauri_app_root`, else `path`, from
`git-strata session ports`), or wrap the client:
`git-strata session exec --no-check -- sh -c 'TAURI_CONNECTOR_PORT=$STRATA_CONNECTOR_PORT tauri-connector ...'`
(without `--port` the CLI uses `TAURI_CONNECTOR_PORT`). Never connect
to the connector's default ports (9555/9556): they may be another
session's app.
- Before starting, `exec` checks the lease's ports (`--no-check` skips it;
`session ports --check` only reports):
- `PortInUse` `already_running`: a process of this workspace already
listens. Reuse it; stop it only if you started it.
- `PortInUse` `foreign`: something else holds a port. Nothing moves on its
own: run `detail.fix` (`git-strata session ports --reassign --session S
--repo R`) for your own session, then `exec` again. Never kill a process
you did not start.
- `Busy` `port_unidentified` / `port_probe_error`: retry shortly.
- A workspace without a lease yet (e.g. one created before this feature)
gets one from the daemon on first `ports` / `exec`; an existing lease works
without the daemon. `Busy` `daemon_down` from `ports` / `exec`: report it
to the human; do not start the daemon yourself. `Busy` `ports_exhausted`
(`ports` shows the workspace with `"ports": null`): no free block; tell the
View on GitHub