---
title: The paved road (git work on a worktree, over the CLI)
---

# The paved road (git work on a worktree, over the CLI)

## What it is

When a session works in its own git worktree, the operations it wants are the ordinary ones —
commit what I changed, catch up with the shared branch, mark my branch finished, throw the worktree
away. Each of them has a raw git command, and **each of those raw commands is the wrong tool**,
because Omniscio keeps a record of every worktree — who owns it, what it is for, whether it is still
in use — and a raw command changes the repository without changing the record. The worktree then
stops matching its own description: it reads as abandoned while it is still being worked in, or as
live while it is gone.

The **paved road** is the set of routes that do those same operations *and* keep the record
correct. Routing work through it is what makes a worktree's state trustworthy to everyone else.

## Where to find it

This is a **CLI-only surface** — there is no button for it. The routes live on Omniscio's local
control server at `127.0.0.1:19519`, and they are the machines' side of the Work in Flight and
Saved Work views.

- **`GET /worktrees`** lists the worktrees with their branch, task and state, and is safe to read
  with any token. Start there to find the `:id` every other route takes.
- **Everything that changes something needs the full-trust token** (`~/.amc/cli-token` from the
  Settings → CLI Control page). A session's own scoped token is deliberately refused on every
  mutation: a worktree operation can rewrite history or delete a branch, which is more than a
  spawned session should be able to do to the machine it is running on.

## How it behaves

The routes fall into five jobs. Every body is **strict**: an unrecognised field is a `400` rather
than a silently-ignored typo, which matters most on `/sync`, where a mistyped action would
otherwise quietly default to starting a rebase. Five routes carry no body at all — `remove`,
`reclaim`, `release`, `gate-hold` and `gate-hold/release` — and sending one is likewise a `400`.

**Reading what is really there.** `GET /worktrees/:id/status` reports the branch, how far ahead and
behind it is, whether it is dirty, the last commit, whether the ready-to-merge tag is set, and
whether a rebase is half-finished. This is the honest read — use it instead of inferring state from
the branch name.

**Finishing your own work.**
- `POST /worktrees/:id/commit` stages **the files you name**, with the message you give it, and
  commits them. Naming them is the point: there is no "commit everything" and the paths are literal
  names rather than a wildcard, so one agent cannot sweep another's half-written work into its own
  commit.
- `POST /worktrees/:id/sync` brings the branch up to date by replaying its commits on top of the
  shared branch. It rewrites this branch's history, so it takes an action: **start** (the default),
  **continue** or **abort**. A conflict stops the replay mid-way and reports it; you then either
  resolve by hand and `continue`, or `abort` to put the branch back exactly as it was. A half-done
  rebase is a state you are expected to finish, not one the route will guess its way out of.
- `POST /worktrees/:id/ready` stamps the **ready-to-merge** tag, and requires a **summary** line
  that is recorded *inside* the tag. The tag names the exact commit it was taken at, so any later
  change to the branch invalidates it — that is the point: the tag is a claim about one specific
  tree, not a flag on the branch. It also accepts an optional **evidence** note and an optional
  **human override**; the override exists so a person can order a tag past a red gate by saying why,
  and a session using its own scoped token is **refused** that option — a check an agent can wave
  through is not a check.

**Gates.** `POST /worktrees/:id/gate-hold` marks a gate as running on this worktree and
`POST /worktrees/:id/gate-hold/release` takes the mark off. Holding is what stops a cleanup sweep
from reclaiming a worktree in the middle of a build; releasing it lets the sweep back in.

**Passing a worktree between owners.** `POST /worktrees/:id/adopt` takes ownership of the record,
and `POST /worktrees/:id/release` hands it back. Neither touches git — they change only who the
record says is responsible, which is what lets work be handed over without either side having to
guess.

**Retiring one.**
- `POST /worktrees/:id/remove` retires **your own** worktree — the sanctioned replacement for the
  raw delete. It archives the branch tip before anything else, so the work is recoverable even on
  the path that also deletes the branch. A removal that is refused comes back as a `200` saying it
  did **not** remove (`removed: false`) with the reason, never as a success.
- `POST /worktrees/:id/reclaim` retires a **stranded** worktree the caller does not own, and
  **keeps the branch** — it is the gentler of the two, for clearing up someone else's abandoned
  workspace without touching their commits. `GET /worktrees/:id/reclaim-preview` runs the same
  decision and writes nothing, so you can see what it would do first.
- `POST /worktrees/block` parks a branch as blocked when there is no worktree involved at all.

**Repairing one.** `POST /worktrees/:id/install-deps` rebuilds a dependency folder that was left
half-written, in place. It is the correct move when a worktree reports broken dependencies —
deleting the folder by hand leaves the record disagreeing with the disk.

**One route has a longer deadline than the rest.** `/ready` is allowed to run well past the usual
request timeout, because deciding whether a branch is landable involves a series of checks that
cannot be hurried. A client that gives up on it early will see a failure where the operation
actually completed.

## For agents

Every route, with the token tier and body each one takes:

| Method | Path | Body | Token | What it does |
| --- | --- | --- | --- | --- |
| `GET` | `/worktrees/:id/status` | — | any | branch, ahead/behind, dirty, last commit, ready tag, rebase state |
| `POST` | `/worktrees/:id/commit` | `{ files[], message }` | full-trust | stage those files and commit (explicit paths, never a pathspec; ≤500) |
| `POST` | `/worktrees/:id/sync` | `{ action }` | full-trust | rebase onto the shared branch, rerere off (`start` \| `continue` \| `abort`, default `start`) |
| `POST` | `/worktrees/:id/ready` | `{ summary, evidence?, humanOverride? }` | full-trust | stamp SHA-bound ready-to-merge (`dispatchDeadlineMs = 15 min`) |
| `POST` | `/worktrees/:id/gate-hold` | — | full-trust | stamp the gate-in-progress holding tag |
| `POST` | `/worktrees/:id/gate-hold/release` | — | full-trust | retract the holding tag |
| `POST` | `/worktrees/:id/adopt` | — | full-trust | take ownership of the ledger row (no git) |
| `POST` | `/worktrees/:id/release` | — | full-trust | hand the row back (no git) |
| `POST` | `/worktrees/block` | repo + branch facts | full-trust | park a branch `blocked`, no worktree needed |
| `POST` | `/worktrees/:id/reclaim` | — | full-trust | retire a stranded row the caller does not own, keeping the branch |
| `GET` | `/worktrees/:id/reclaim-preview` | — | full-trust | read-only dry run of reclaim |
| `POST` | `/worktrees/:id/remove` | — | full-trust | retire your own worktree (replaces `git worktree remove`) |
| `POST` | `/worktrees/:id/install-deps` | — | full-trust | repair a torn `node_modules` in place |

Implementation: `src/main/services/cli/cli-server-worktree-ops-routes.ts` (lifecycle) and
`cli-server-worktree-ledger-routes.ts` (`/worktrees`, `/worktrees/:id`, `landed-proof`, `pin`,
`install-deps`, `create`, lease restore). Five routes — `remove`, `reclaim`, `release`, `gate-hold`
and `gate-hold/release` — take **no** body and answer `refusedUnhonouredBody` (`400`) if one is sent.
All mutations are registered `{ selfRateLimited: true }` and bill the per-source paved-road bucket
(`429 rateLimitedWorktreeOp`). `/remove` also carries the ledger's floor: a refusal returns
`200 { removed: false }` with the reason. Every body is `.strict()`, so a misspelled `action` on `/sync` is a `400` rather than a silent
default (that schema carries `z.enum([...]).default('start')`). Invariants live in
[agent-git-paved-road-contract.md](/.claude/memory/contracts/agent-git-paved-road-contract.md),
[worktree-ledger-contract.md](/.claude/memory/contracts/worktree-ledger-contract.md) and
[git-guardrails-contract.md](/.claude/memory/contracts/git-guardrails-contract.md) rule 10.

## Related

- [Session isolation](session-isolation.md) — what a worktree is, why each session gets its own, and
  how the work inside one finds its way back.
- [Git guardrails](git-guardrails.md) — the protected-branch and destructive-operation blocks that
  sit above this surface.
- [Worktree cleanup](worktree-cleanup-skill.md) — the bulk pass that retires worktrees whose work has
  already landed.
- [Orphan worktree archive](orphan-worktree-archive.md) — the background job that reclaims the disk
  folder of a worktree whose session has been gone a long time.
- [Where's My Work](wheres-my-work.md) — the read-only screen that answers what happened to a
  branch's changes.
- [CLI Control](cli-control.md) — the local server these routes belong to, and where the full-trust
  token comes from.
