---
url: https://docs.repoos.org/cli.md
---
# CLI reference

Run `repoos` with no arguments to see this list in your terminal.

## Setup

### `repoos init [name]`

Scaffolds RepoOS files in the current repo, plus an `inbox` starter task so the
board isn't empty — a suggestion for you, not work to auto-run, promoted with
`repoos mv <id> ready`. The starter body follows the repo's content: an
effectively empty repo (no meaningful source yet) gets the product-vision task,
otherwise the read-the-codebase one; override with
`repoos init --starter vision|codebase`. By default files go under a `repoos/`
subdirectory; interactive prompts let you choose a different location or `/` for
the repo root. `repoos.toml` and `AGENTS.md` always stay at the root. Run outside
a git repo, it starts a guided new-project flow instead, which can launch the web
console for you. On an existing repo, an interactive run also offers to seed the
task-area vocabulary (and commented preview-target stubs for it) — skippable, and
skipped automatically once `[[areas]]` is declared. See
[Configuration](/configuration#areas).

`--docs-from` copies an existing doc set into `docsDir` (a directory, a single
file, or a `.zip`); `--force` overwrites collisions. The guided flow also prompts
for a path, then offers to scaffold starter docs. Nothing is imported or
scaffolded unless you pass the flag or answer yes. See `repoos docs` below.

#### Non-interactive new-project mode (for agents and scripts)

Outside a git repo with no TTY, `repoos init <name>` can't prompt, so it prints
the exact command to run and exits non-zero. Pass `--new` (or `--yes`) to run
the same guided flow with answers from flags and defaults — no pty tricks:

```bash
repoos init myproject --new \
  --description "A tiny social app for book clubs" \
  --areas web,api \
  --no-launch
```

| Flag | Meaning |
| --- | --- |
| `--new`, `--yes` | Run the new-project flow non-interactively. Required without a TTY. |
| `--description "<text>"` | Project description seeded into the starter task. |
| `--description-file <path>` | Read the description from a file; `-` reads stdin (multi-line markdown). |
| `--areas web,api` | Seed the area vocabulary (same comma-separated rule as the area picker). |
| `--layout <ns>` | `repoos` (default), `/` for the repo root, or a namespace path. |
| `--commit` / `--no-commit` | Make the initial commit of the scaffold (default: commit). |
| `--launch` / `--no-launch` | Start the web console when done (default: no launch without a TTY). |
| `--preview-stub` / `--no-preview-stub` | Scaffold commented `[[preview.targets]]` stubs for the areas. |
| `--dir <path>` | Where to create the project; alternative to the positional name. |
| `--force` | Scaffold into an existing non-empty directory. |
| `--json` | Print `{ root, tasks: [...], created: [...] }` instead of the human summary. |
| `--help` | Documented flags, and which apply to new vs existing repos. |

`--new` seeds the **new-project** starter ("Flesh out the product vision and
initial architecture"). Do **not** work around a missing TTY by running
`git init` first: inside a git repo, `repoos init` seeds the *existing-codebase*
starter ("Read this codebase…"), which is the wrong route for a brand-new
project.

When the repo has no check plan yet, init inspects its stack and writes a
starter plan to an uncommitted `repoos.check-plan.proposed.toml` for review;
interactive init offers to move it into `repoos.toml`. See
[Checks before merge](/check).

### `repoos upgrade [--channel beta|canary|rc]`

Self-updates a standalone (curl-installed) RepoOS to the latest release.
Tracks stable by default; `--channel` follows a prerelease line. Package-manager
installs are not modified: RepoOS detects the usual npm, Bun, pnpm, Homebrew,
and legacy mise paths and prints the matching update command instead. `--channel`
applies only to the standalone curl install.

See [Updating RepoOS](/getting-started#updating-repoos) for every install method.

## The board

### `repoos new "<title>"`

Creates a task. When the repo is a git checkout, RepoOS writes the task file and
attempts an initial commit of that file only (`docs(<id>): add task …`). A failed
commit leaves the file on disk uncommitted and prints a warning — set git
identity in config or via `GIT_AUTHOR_*` / `GIT_COMMITTER_*` env vars.

```bash
repoos new "Fix the login redirect loop" --type bug --area web --priority p1
repoos new "Redesign the onboarding flow" --area "web, onboarding"
repoos new "Build the settings screen" --depends-on 0542,0538
repoos new "Wire up the export endpoint" --paths src/server/export.ts,src/ui-app/src/views/ExportView.vue --hold true
```

`repoos new --help` lists every accepted flag with a one-line description.
Both that help and the usage text printed on a bad flag are generated from the
command's flag table, so a flag cannot be added without appearing in `--help`.

| Flag | Values |
| --- | --- |
| `--type` | `feature`, `bug`, `chore`, `spec`, `refactor` |
| `--priority` | `p0`, `p1`, `p2`, `p3` |
| `--area` | Free text, comma-separated for several (`--area web, core`). Areas a repo declares (`[[areas]]` in repoos.toml) appear in the task drawer's area picker; anything outside it stays allowed. |
| `--story` | Tag the task as part of a story (a delivery slice). |
| `--depends-on` | Comma-separated task ids that must be completed and merged into `main` first. |
| `--hold` | `true` or `false` — hold the task out of the auto-engineering picker. |
| `--paths` | Comma-separated files the task touches, e.g. `src/a.ts,src/b.ts`. |
| `--needs-input` | `true` or `false` — flag the task as needing human input. |
| `--questions` | Open questions for the human, newline-separated or a JSON list. |
| `--assigned-to` | `ai` or `human` — who owns the task. Any other value is refused. |
| `--ai` | Shorthand for `--assigned-to ai` |
| `--human` | Shorthand for `--assigned-to human` |
| `--body` | Task body; pass `-` to read from stdin |
| `--human-step-override` | A reason that clears a `needs-human-step` keyword match on an AI task (see below). |
| `--shots` | Raw JSON list declaring the `## Shots` section; pass `-` to read from stdin. |
| `--screenshot` | Attach a local image file to the task's `## Screenshots` section. Repeatable (`--screenshot a.png --screenshot b.png`). |
| `--caption` | Alt text for a `--screenshot` image. Repeatable; the Nth caption labels the Nth screenshot. |

`--priority` and `--type` are validated on every write: a value outside the
sets above is rejected with the field, the bad value and the full list of valid
values (e.g. `priority 'medium' is not valid; use one of p0, p1, p2, p3`), and
is never silently defaulted. The same check applies to `repoos update` and the
HTTP API. Task files that already carry a legacy value (`priority: high`,
`type: ux`) still load and display as-is — only new writes are checked.

A task assigned to a human is created with a `## Human task` note saying so, and
skips the agent task-body checks: `## Notes for AI` is optional and the task is
not flagged `underspecified` or `needs-human-step`. This is the one-command way
to put human work on the board:

```bash
repoos new "Flush the Divot DNS cache" --assigned-to human
```

Human-assigned tasks are inert board work: move them between Inbox, Ready,
Active, Review and Done by dragging, using the drawer status control, or with
`repoos mv <id> <status>`. These moves only change status; they never start an
agent, run checks, create a worktree or invoke review/close-out. `hold` is not
needed for human assignment. Mark done in the drawer or run
`repoos mv <id> done --note "what was completed"`; RepoOS records who closed it
and the optional note. An AI agent may move a human task only when the human
explicitly asked it to update that task's board status.

Pass at most one of `--assigned-to`, `--ai` and `--human`; combining them is an
error.

On an AI task, acceptance criteria that mention a device, account, or
credentials raise `needs-human-step`. When that is a keyword false positive,
clear it deliberately with a reason — on creation or with `repoos update`. The
reason is stored on the task and written to its activity log, so the clearance
is on the record rather than a silent rewording:

```bash
repoos new "Reduce the retry backoff" --human-step-override "the API-key mention is a fixture"
repoos update 0815 --human-step-override "no real credentials are involved"
```

The override only clears the `needs-human-step` flag; the matched keywords are
still listed in `needs_input_detail` until the override is set. Pass an empty
value (`--human-step-override ""`) to remove it.

### `repoos list [status]`

Shows the board, or one column: `inbox`, `ready`, `active`, `review`, `done`.
Archived tasks are hidden by default (a count is printed at the bottom);
`repoos list archived` shows them with their reason.

### `repoos show <id>`

Prints a task's full spec — metadata, body, and activity log. An archived task
shows an `archived` row with its reason.

### `repoos mv <id> <status>`

Moves a task to a new status. Takes `--note "..."` to record why.

`--note` only adds an activity log entry; it does not send feedback to the
engineer. Use `repoos send-back` when returning work with instructions.

```bash
repoos mv 0001 ready --note "spec is settled, ready to hand over"
```

`review` is the one status with special behaviour, because reaching it means
running the checks, not just editing a field. From your terminal the move goes
through the same handoff finalization the UI uses — commit the branch, run
`repoos check`, pass the commit guard, then land in `review`. The task stays
`active` while that runs and stays `active` with the reason if the check fails;
the command itself still exits 0. See
[Review and close-out](/review-and-close-out).

Run from inside a RepoOS-managed agent session for that same task,
`repoos mv <id> review` records a **handoff request** instead of moving anything: the
runner picks it up when the turn ends and finalizes then. That is deliberate —
an agent moving its own task out of `active` would have its turn killed
mid-flight.

`done` is refused when the task's branch still exists and is not merged into
`main`; `repoos mv done` only flips the flag, it never merges. Use
`repoos done <id>` (the real close-out pipeline), **Move to done** in the UI,
or merge the branch yourself first.

For server-backed actions (start, pause, sync, handoff, close-out, previews, config,
runners, running agents, board stats), use the **Control plane** commands below
instead of hand-rolled `curl` — they authenticate once (loopback token or stored
session, with transparent dev-login re-auth on 401).

An archived task is refused too: archiving is orthogonal to status, and the
only way back is to unarchive it (from the Archived list in the UI, or
`POST /api/tasks/:id/unarchive`).

## Control plane (running server)

These commands talk to the live RepoOS server (`repoos serve`). They reuse the
loopback CLI token when auth is on, or a stored session after dev-login, and
support `--json` on every command. Long operations accept `--wait` (review and
done wait by default in human mode).

| Command | API |
| --- | --- |
| `repoos start <id> [--fresh] [--force]` | `POST /api/tasks/:id/start` — `--force` starts a held task (see hold below) |
| `repoos pause <id>` | `POST /api/tasks/:id/pause` — stop the current run and keep the task paused (a hold does more; see below) |
| `repoos hold <id> --reason "…" [--until <date>]` | `POST /api/tasks/:id/hold` — stop any running engineer and make the task inert for ALL automation |
| `repoos unhold <id> [--start]` | `POST /api/tasks/:id/unhold` — release the hold; `--start` re-launches the engineer on the now-released task |
| `repoos sync <id>` | `POST /api/tasks/:id/sync` — merge main into the task branch (lists conflicting paths and exits non-zero on conflict) |
| `repoos message <id> "…" [--now]` | `POST /api/tasks/:id/message` (`now: true` interrupts a busy agent) |
| `repoos send-back <id> --message "…"` or `--message-file <file|->` | `POST /api/tasks/:id/send-back` — move to active, start the engineer, then queue the message; `-` reads stdin |
| `repoos review <id> [--wait]` | `PATCH /api/tasks/:id` (`status: review`) |
| `repoos review cancel <id>` | `PATCH /api/tasks/:id` (`status: active`, cancels an in-flight handoff/review) |
| `repoos review again <id>` | `POST /api/tasks/:id/review/again` |
| `repoos done <id> [--commit-dirty] [--wait]` | `POST /api/tasks/:id/done` |
| `repoos done <id> --cancel` (`repoos abort <id>`) | `POST /api/tasks/:id/done/cancel` |
| `repoos api <METHOD> /api/<path> [--json body] [--port N]` | Any `/api/` route |
| `repoos landable <id>` | `GET /api/tasks/:id/landable` — read-only merge readiness checks; exits non-zero unless the task is ready to merge (`--json` for a stable checklist shape) |
| `repoos handoff-status <id>` | `GET /api/tasks/:id/handoff-status` — non-blocking latest handoff state (`not_requested`, `running`, `passed`, `failed`, `deferred`), step and reason; never starts or waits for a handoff |
| `repoos override <id> --cli X --model Y` | `PATCH /api/tasks/:id` |
| `repoos preview <id> [--stop]` | `POST /api/tasks/:id/preview` (+ `/stop`) |
| `repoos dev [list]` · `repoos dev <name> start\|stop\|restart\|status\|logs` | `GET`/`POST /api/dev/targets` |
| `repoos config get\|set <key> [value]` | `GET` / `PATCH /api/config` |
| `repoos runners [--probe]` | remote-validation status + test |
| `repoos runners check [--json]` | engineer CLI install/auth matrix per declared host (#0915); exits non-zero when a cell is red |
| `repoos runners setup <host> [--login <cli>]` | install missing declared CLIs; `--login` opens interactive SSH into the CLI login prompt |
| `repoos integration list\|pipeline\|get <id>\|retry <id>\|refresh-install <id>` | Integration job inspection and retry routes |
| `repoos attention list\|dismiss <id>` | `GET /api/attention` and needs-input dismissal |
| `repoos agents` | `GET /api/agents/running` |
| `repoos agents pause\|resume <agent> [--model M] [--until <date>] [--reason "…"]` | `PATCH /api/config` — pause/resume an allowlist agent or model (#0850) |
| `repoos stats` | `GET /api/stats/board` + `GET /api/system` — board spend, pressure, and top CPU/memory processes labeled RepoOS vs other (`--json` includes both under `system`) |
| `repoos decisions` | `GET /api/decisions` — CTO escalation digest with cause, evidence, and actions |
| `repoos driver brief` | `GET /api/driver/brief` — the CTO board brief from live state (#0731) |

Pass `--port N` when the server is not on the default port. If the server is
down, the CLI exits with a clear message instead of a generic fetch error.
`repoos api` is an authenticated escape hatch for a route that has no dedicated
CLI command yet. Prefer the named command when one exists. API parity is
enforced by `src/cli/api-route-parity.ts`; routes intentionally unavailable to
drivers must have a documented exemption there.

### Hold / unhold a task out of ALL automation (#0960)

Existing `hold: true` tasks and tasks tagged `hold` gain the same CTO/watchdog
protection automatically; no task-format migration or new status is required.

A **hold** is a single, durable, documented state that makes a task inert for
every automation RepoOS runs — auto-dispatch, the CTO monitor (idle nudges and
stalled-agent restarts), the watchdog's auto-resume, crash recovery, and
queued-message delivery that would otherwise start an engineer. A held task
keeps its `status` (a held `active` task stops its engineer but stays `active`; a
held `ready` one simply never auto-dispatches); it is never started by anything
but an explicit human or driver command that clears the hold. This is broader
than `pause`, which only stops the current run — the CTO monitor could still
restart a paused task. Use hold when you want a task to sit completely still.

```bash
repoos hold 0960 --reason "waiting on design sign-off" --until 2026-12-01T00:00:00Z
repoos unhold 0960            # release it
repoos unhold 0960 --start     # release it and re-launch the engineer
```

`--reason` is optional free text on the Hold dialog and on
`POST /api/tasks/:id/hold` (a hold with no reason omits the reason row). The
`repoos hold` command still requires `--reason`. `--until` is an optional ISO-8601
UTC auto-release time — past that moment the hold is inert and the task behaves
as if never held (so a calendar date works as a "resume after…"). The hold is
written into the task's frontmatter (`hold`, `hold_reason`, `hold_until`,
`hold_by`, `hold_at`) alongside its `status`, so it survives a server restart and
is visible on the board card and in the driver brief (`repoos driver brief`
gains a **Held** section). A held task also does not count toward
`maxActiveTasks`.

`repoos start <id> --force` starts a held task by overriding the hold; this is
the explicit escape hatch for a human or driver who has decided to proceed. A
normal `repoos start` on a held task is refused with a clear message naming the
reason, and tells you to `repoos unhold` (or `--force`) first. Held tasks do not
appear in the auto-engineering picker, and a held `ready` task is never
selected. A `hold` tag on a task is treated the same as the `hold` flag, so you
can park work without editing frontmatter directly.

### `repoos bundle` (easter-eggs bundles)

Surgical edits to an open **Easter eggs bundle:** task without replacing the
whole body. Writes go through `POST /api/tasks/<id>/bundle`; bundle task
`status` is untouched.

```bash
repoos bundle add 0822 --title "Hub find bar" --area web --fix "keep find below title" --source "#0795"
repoos bundle edit 0822 E2 --section "Desired UX" --section-body "New UX text."
repoos bundle done 0822 E1
repoos bundle undo 0822 E1
```

`add` appends one `## Items` checklist line and a matching `## E<n> · Title`
section (next free id). `edit` changes one `###` subsection inside an item.
`done` / `undo` tick or clear that item's `- [x]` checkbox in `## Items` only.

Refuses tasks whose title does not start with `Easter eggs bundle:` unless
`--force`. Pass `--port N` when the control plane is not on the default port.

### `repoos update <id>`

Edits a task's metadata or body: `--title`, `--area`, `--story`,
`--depends-on`, `--priority`, `--type`, `--body`, `--branch`, `--assigned-to`,
`--human-step-override`, `--needs-input`, `--needs-merge`, `--hold`, `--paths`,
`--questions`, `--clear-questions`, `--shots`, `--section` / `--section-body`,
`--force`, and per-role agent pins (`--agent`, `--cli`, `--model`, plus `--pm-*`
and `--review-*` variants). Run `repoos update --help` for the full, generated
list.
These write the same fields as `PATCH /api/tasks/<id>` and are what
engineer/reviewer runs read.
`POST /api/tasks/<id>/start` and `/message` reject override fields in the
request body (HTTP 400); PM chat routes still accept `cliOverride` /
`modelOverride` for a single turn. See [Running with agents](/running-with-agents#2-choose-agents-per-role).

```bash
repoos update 0615 --depends-on 0542,0538
repoos update 0615 --depends-on ""
repoos update 0316 --needs-input false
repoos update 0397 --needs-merge false
```

The `--needs-input` / `--needs-merge` flags take `true` or `false`, and clearing
the flag also clears the reason/detail recorded alongside it.

Dependencies are satisfied only when the upstream task is `done` and Git
confirms its merged commit is an ancestor of `main`. A blocked task cannot be
started manually unless you explicitly confirm the override; missing or
unverifiable upstream tasks are marked as needing a human. Unknown ids,
self-dependencies, and cycles are rejected when written.

To declare evidence screenshots, pass raw JSON to `--shots`. The CLI validates
every entry and writes the fenced `## Shots` section itself, leaving the rest of
the body (including the Activity log) untouched. `repoos new` accepts it too;
`-` reads the JSON from stdin:

```
repoos update 0612 --shots '[{"target":"default","route":"/agents?tab=detected","label":"Detected tab","highlight":".detect-row"}]'
```

### Attaching screenshots (`--screenshot`)

When you have the actual image files — an agent handed a human's screenshots in
chat, for example — pass each one to `--screenshot` (repeatable) on `repoos new`
or `repoos update`. The command validates the file against the same allowlist
and size limit the UI upload uses (`.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`,
`.avif`, `.bmp`; max 10 MB), stores it under the gitignored
`work/.attachments/<id>/`, assigns an opaque `att_<uuid>.<ext>` storage name
(stable in committed Markdown), and writes or
extends the task's `## Screenshots` section (kept before `## Activity`). It
prints the resulting attachment URLs:

```bash
repoos new "Fix the login redirect loop" --screenshot before.png --screenshot after.png
repoos update 0763 --screenshot repro.png --caption "crash on save"
```

`--caption` sets alt text for `--screenshot` images by position: repeat both flags
(`--screenshot a.png --caption "before" --screenshot b.png --caption "after"`).
Fewer captions than screenshots is fine (the rest keep default alt text). More
captions than screenshots is an error that names both counts. A missing,
unsupported, empty, or oversize file is rejected with a clear error before any
change is written. Nothing under `work/` is staged — the images are gitignored
and served by the running server from disk; only the `## Screenshots` markdown
lands in the task file.

To replace any other single `## Section` (create it if absent), use
`--section "<heading>" --section-body "..."`. A `Shots` section written this way
must be a fenced JSON list, and malformed content is rejected rather than
silently falling back to a `/` capture. `--shots '[]'` (an empty JSON list) writes
an empty shot list (`## Shots\n```json\n[]`), which clears all declared shots and
lets the task fall back to automatic captures. To remove the entire `## Shots`
section instead, use `repoos update <id> --section "Shots" --section-body ""`.
`--shots` cannot be combined with `--section` or `--body`.

A full `--body` replace that would drop Problem / Desired UX / Acceptance
criteria / Notes for AI is refused unless `--force` is passed; the error points
at the `--section` form.

Editing the body of an **active** task — `--body`, `--section`, the API `PATCH`,
or the drawer's **Update requirements** action — records one activity entry
naming the changed sections and sends the engineer one message carrying the same
summary (with the new text of added items). The engineer's worktree copy of the
task file is deliberately not written: the notice tells it what moved, and
merging main (or the next handoff) brings the file in line. A body edit on a task
that is **not** active sends no engineer notice.

### `repoos note <id> "<text>"`

Appends a free-form note to the task's activity log. Useful for leaving context
an agent should read before picking the task up.

### `repoos new-doc "<description>"`

Creates a document from a description, via the Product Manager agent.

### `repoos docs import <dir|file|.zip> [--force] [--dry-run]`

Copies an existing doc set into `docsDir`, preserving folder structure. The
source may be a directory, a single file, or a `.zip` archive (common for a
browser download): an archive is extracted to a temp directory first, a lone
top-level folder is unwrapped so you don't get an extra level, and macOS junk
(`__MACOSX/`, `.DS_Store`, `._*`) is ignored. Archives that try to escape the
extraction directory (zip-slip), contain symlinks, or are absurdly large or
numerous are refused. Existing files are left untouched and reported unless
`--force` is passed; `--dry-run` prints the plan and writes nothing.

### `repoos docs scaffold`

Writes a minimal, opt-in starter skeleton into `docsDir`: an index `README.md`
with a reading order and an "if you learn something durable, write it here"
line, plus short `product.md`, `architecture.md`, `conventions.md` and
`glossary.md` stubs with prompts. It never overwrites an existing file, and
nothing is created unless you run the command (or answer yes to the scaffold
prompt in the guided `repoos init` flow).

## Stories

### `repoos story new|list|show|update`

Stories are an optional grouping over tasks (see
[Stories](/configuration#stories)). The **New story** flow in the UI always
hands the description to the PM agent, which writes up the scope and tags any
existing untagged tasks that belong. When an agent or script already has the
finished story, `repoos story` writes it verbatim instead: no PM, no tagging.

````bash
repoos story new "Project updates email" --body "Why this slice exists, scope, non-goals."
repoos story new "Checkout rework" --body -            # body from stdin
repoos story list                                       # numbers + paths
repoos story list --json
repoos story show 0007                                  # definition + member tasks with status
repoos story show "Project updates email" --json
repoos story update 0007 --body "Rewritten scope."      # keeps the stable number
repoos story update 0007 --name "Project update emails" # rename keeps the number

### `repoos schedule list|run|enable|disable|new|update|preview|delete`

Manage **scheduled tasks** — markdown definitions under `schedules/` that create
ordinary `work/*.md` tasks on a cadence (the same files the Agents → Scheduled
Tasks UI edits). Writes land on the board root and commit when git tracks the
file.

```bash
repoos schedule list
repoos schedule list --json
repoos schedule preview --cadence weekly --timezone UTC --time-of-day 09:00 --day-of-week 1 --title "Doc audit" --task-title "Audit docs"
repoos schedule new --title "Doc audit" --task-title "Audit docs" --cadence weekly --timezone UTC --time-of-day 09:00 --day-of-week 1 --body -
repoos schedule update 0001 --enabled false
repoos schedule enable 0001
repoos schedule disable 0001
repoos schedule run 0001
repoos schedule delete 0001 --yes
````

| Subcommand | API equivalent |
| --- | --- |
| `list` | `GET /api/schedules` |
| `new` | `POST /api/schedules` |
| `update`, `enable`, `disable` | `PATCH /api/schedules/:id` |
| `delete` | `DELETE /api/schedules/:id` |
| `run` | `POST /api/schedules/:id/run` |
| `preview` | `POST /api/schedules/preview` |

````

`new` and `update` write and commit the definition under `storiesDir`, exactly
like `repoos new` commits a task file, and print the story's number and path.
`--body -` reads the body from stdin. `show` resolves the story by number,
name, or key, prints the definition, and lists every member task with its
status; a tag-only story (tasks tagged but no file under `stories/`) shows with
`(tag-only, no definition)`. `list` and `show` accept `--json`.

These commands are board-rooted like `repoos new`: run from inside a task
worktree, they still write the MAIN checkout's `stories/`, which is the only
copy the live board reads. `repoos new --story "…"` and
`repoos update <id> --story "…"` tag tasks with a story and are unchanged.

The same write is on the HTTP API for a script that prefers it:
`POST /api/stories { name?, body }` and `PATCH /api/stories/:key { name?, body? }`.

## Project secrets

### `repoos secrets`

Manage explicitly registered project environments without printing saved
values. Use `list` to view key names and safe metadata; `add`, `replace`, and
`delete` read new values from stdin or a hidden terminal prompt. The API and CLI
use a content version for each write, so a concurrent edit returns a conflict
and must be retried after reloading. Keys inherited only from an earlier source
are read-only through the API, CLI, and Secrets page; neither replace nor delete
silently creates an override.

```bash
repoos secrets list dev
printf '%s' 'fake-value' | repoos secrets add dev SERVICE_TOKEN
repoos secrets replace dev SERVICE_TOKEN   # hidden prompt in a terminal
repoos secrets delete dev SERVICE_TOKEN
repoos secrets bind dev:web dev-nick DATABASE_URL STRIPE_SECRET_KEY
repoos secrets bindings
````

Bindings select one environment and an explicit key allowlist for a local
consumer. They never fall back to another environment. See
[Environment and secrets](/environment-and-secrets) for storage, permissions,
and exposure limits.

## Running it

### `repoos serve [--port N] [--host <address>]`

Starts the local server — web UI, API, and live event stream. Without `--port`
it uses `servePort` from `repoos.toml`, or a stable port derived from the repo's
path so separate repos never collide.

By default the listener binds to **127.0.0.1** (localhost only). When RepoOS
detects Tailscale on this machine, it widens the bind to **0.0.0.0** so other
tailnet devices can reach the server while localhost still works. The startup
banner names the bound interface. Pass **`--host 127.0.0.1`** to force
localhost-only even when Tailscale is running.

### `repoos service` / `repoos service <subcommand>`

Manages a background service for this repo (install, start, stop, restart,
enable/disable login auto-start, list all services). With no subcommand, shows
this repo's service status — the usual way to run an unattended control plane
instead of keeping `repoos serve` in a terminal.

### `repoos uninstall`

Removes a **standalone** install created by the curl installer (`install.sh`).
Does not remove source checkouts or package-manager installs; those are
uninstalled through npm, Homebrew, or your package manager. Project data and
user configuration are left in place.

### `repoos --version` / `repoos version`

Prints the installed RepoOS version and build metadata.

### `repoos stop [--port N]`

Stops this repo's server, identified by its own lockfile. It will not touch a
server belonging to a different repo.

### `repoos servers [--json] [--prune]`

Lists every control-plane `repoos serve` that registered itself under
`~/.repoos/servers/` (one `<port>.json` per listener). Shows port, project
name, repo root, PID, uptime, live engineer count, and whether the entry still
looks healthy (PID alive and `/api/health` reports the same root).

`--prune` removes stale registry files (dead PID, or a live PID whose
`/api/health` is unreachable or reports a different root), then lists the rest.
`--json` prints the same data for scripts. Preview servers and task worktree
serves never register. The Hub uses the same registry to discover local servers.

### `repoos status [--json]`

One-screen health snapshot: server, build freshness, board counts, worktrees,
tunnel, and git state.

The `server` line names the *running server's* own RepoOS version and build
(`v0.5.66 (build abc1234)`), and when that build differs from the installed CLI's
it prints a warning — `server is older than the installed CLI … restart to pick
up fixes` — with the command that restarts it (`repoos service restart` for a
managed service, `repoos serve` for a hand-run process). The `build` line
describes this checkout, so in a project repo it reads `no RepoOS build` rather
than implying the repo is a RepoOS source checkout. `--json` exposes `cli` and
`server.buildState` (`same`/`stale`/`unknown`) so scripts can detect a stale
server without parsing text.

### `repoos doctor [--json] [--verbose] [--probe <cli> [--model <id>]] [--binary <path>]`

A read-only readiness preflight for a real project. It checks the repository
identity (root, git, linked worktree), parses and validates `repoos.toml`,
verifies the configured layout and existing task frontmatter, detects the
required runtimes and enabled agent CLIs, explains whether a meaningful
`repoos check` plan is configured, and reports server, auth and credential
readiness — each with a stable finding id, a severity (`pass` / `warn` / `fail`)
and a concrete next step. It also reports each enabled harness's compatibility
contract status (verified, upgrade recommended, newer than verified, unsupported,
or not yet probed).

It also runs an advisory **docs-wiring** check and warns when `AGENTS.md` never
points at the docs index (`docsDir/README.md`), when docs exist that the index
doesn't link to, or when the docs directory is empty while tasks exist. These are
always warnings with a one-line fix hint — they never change the exit code. See
[Give the project docs](/getting-started#give-the-project-docs).

```bash
repoos doctor            # warnings/failures, summary and next steps
repoos doctor --verbose  # every check, including the passing ones
repoos doctor --json     # the same findings, machine-readable
```

By default the report **collapses passing checks** so a single failure isn't
buried under ~25 green lines: only sections with warnings or failures are listed,
followed by the summary and the next steps (failures first), and a closing hint
that `--verbose` shows the whole checklist. `--json` is unaffected — it always
emits every finding. Output is laid out for the terminal width and wraps cleanly
when piped, so the default run never breaks its own indentation. The default run
never initializes, rewrites config, installs, logs in, contacts a model provider,
kills a process or mutates git, and it works offline. It exits non-zero when any
finding is a failure, so it is usable in a script. Paste `repoos doctor` output
into an issue to report a setup problem.

```bash
repoos doctor --probe opencode --yes   # run the live adapter-contract probe
```

`--probe <cli>` is a separate, explicitly **opt-in** mode: it runs the named
harness's adapter-contract suite against the installed binary inside an isolated
temporary directory, then cleans up. Unlike the default report it starts real
harness runs, so it **may use provider credentials and spend tokens** (the
one-shot, resume and cancellation seams each start a run). It refuses to run
headless without `--yes`, and it never reads task files, prompts or project
content. `--model <id>` optionally pins the model used for the probe's runs (e.g.
`--probe pi --model openrouter/deepseek/deepseek-v4.1-flash`), for harnesses
whose default model depends on your environment; it is ignored by harnesses
that have no model flag. See [Coding harness compatibility](./coding-harness-compatibility.md).

### `repoos support bundle` / `repoos support inspect`

Creates a small, inspectable, **redacted** diagnostic archive to attach to an
issue when RepoOS fails on a real-world project. It is built locally and never
uploaded; opening a browser, copying to the clipboard or filing an issue are all
separate, explicit actions you take yourself.

```bash
repoos support bundle                   # write .repoos/support/repoos-support-<ts>.tar.gz
repoos support bundle --dry-run         # show exactly what would be included; write nothing
repoos support bundle --out /tmp/x.tar.gz
repoos support inspect <bundle.tar.gz>  # list every file inside an existing bundle
```

The bundle contains a versioned structured report: RepoOS version/build metadata,
platform/runtime versions, the effective configuration *shape* (flags, counts,
schema versions — never secret values or free-form fields), agent/tool detection
and versions, the resolved check plan, the sanitized `repoos doctor` findings,
the classified result of the latest doctor run, server/health/lifecycle
diagnostics, bounded recent error messages, and a machine-readable
`manifest.json` listing every file, its size and hash, the collection time and
the redaction rules/version.

It never includes `.env`, environment values, API tokens, passwords, cookies,
SSH keys, session material, prompts, transcripts, task bodies, source code,
diffs, attachments or raw logs. Home directories and the repo root are minimized
to `~` and `<repo>`. A final scan verifies no known secret shape or private path
survived; a miss **aborts** the write rather than packaging it. A down server, a
missing agent CLI or an unparseable `repoos.toml` degrade that one section into
an explicit omission with the reason, and the rest of the bundle is still
written. The default output lands under the cache directory (gitignored in most
repos); if that path is inside the repo and not gitignored, both the CLI and the
UI warn that the archive could be committed and suggest `--out` or a
`.gitignore` entry. In the web UI it is on **Settings → Support**, and a
**Create a redacted support bundle** action also sits on the failed
Move-to-done / check panel — the moment you're most likely to need it.

## Quality and maintenance

### `repoos check`

The definition-of-done gate. It runs the check plan your repo declares in
`repoos.toml` — for any stack (see [Checks before merge](/check)). Exits
non-zero on any failure, so it works in CI as well as locally.

```bash
repoos check --profile full      # every declared step, including slow ones
repoos check --changed main      # fast pre-review pass over changed paths
repoos check --print-plan        # print the resolved plan as [[check.steps]]
repoos check --fix               # run each format step's fixer before its check
repoos check --step tests        # run only the named step(s), to re-check one
```

Every run ends with a short failed-steps summary naming each failed step and the
`repoos check --step <name>` command that reruns it. `--fix` never applies at
close-out, so an unformatted committed tree still fails the merge gate.

### `repoos outline <file>`

Prints a compact map of a source file — top-level and exported functions,
classes, interfaces, types, enums and constants with their `start-end` line
numbers; for `.vue`, the template/script/style block ranges plus script symbols;
for `.css`, its top-level selectors and at-rules. Run it once before reading a
large file, then read only the range you need (`offset`/`limit`) instead of the
whole file.

Supported: `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.vue`, `.css`.
Anything else prints one clear line saying so. `--json` emits the same data as
JSON.

```bash
repoos outline src/server/server.ts
repoos outline src/ui-app/src/components/TaskDrawer.vue
repoos outline src/ui-app/src/style.css --json
```

### `repoos shot`

Captures screenshots of a task's managed preview so a UI change leaves visual
evidence a reviewer or human can actually look at, instead of "it looked fine".
Run it from the task's worktree; it asks the running `repoos serve` to start the
task's preview (never starting a server itself), drives the same optional
Playwright/WebKit path the UI check uses, and stores PNGs under
`work/.attachments/<taskId>/shots/` — gitignored, never referenced from the task
body, and shown as the **UI changes** sub-tab of the drawer's Changes tab. This
is the default local attachment storage; an opt-in cloud provider is configured
under [`storage.provider`](/configuration#attachment-storage).

`repoos shot` follows the
[`remoteValidation.browserCaptureMode`](/configuration#remote-validation)
setting. With `local` (the default) it drives Playwright/WebKit on this machine;
`runner-available` captures on a capable runner and falls back locally with a
visible note; `runner-only` returns the runner's structured failure instead of
capturing locally. An arbitrary absolute URL, or an explicit `--viewport`,
`--wait`, or `--full-page`, stays local with a note in runner-available mode
(and fails with the reason in runner-only, because the remote single-shot path
cannot reproduce it). Command capture targets always run locally on this
machine.

```bash
repoos shot                          # capture the preview root for this task
repoos shot /repo/commits/abc123     # capture one route
repoos shot --target "Docs site"     # force a target (skip path/area resolution)
repoos shot --selector ".sidebar"    # capture one element instead of the page
repoos shot --wait 2000              # wait longer before capturing (default 900ms)
repoos shot https://example.test/x   # capture an arbitrary URL
repoos shot --target "macOS app"     # run a command capture target (non-web app)
```

### `repoos handoff-status`

Prints the latest handoff attempt for a task and returns immediately: `not
requested`, `running`, `passed`, `failed` or `deferred`, with the current step
and any failure/deferral reason. It never starts, waits for, or changes a
handoff, so it is safe to call while a finalization is running — including from
an engineer that is unsure whether its request reached the server. After a
pass, fail or deferral, RepoOS also queues one message with the outcome for the
engineer's next turn, so request-then-end-turn needs no polling.

```bash
repoos handoff-status 1028
repoos handoff-status 1028 --json
```

### `repoos review again`

Start a fresh reviewer run on a task that is already in `review` — the same
action as **Review again** in the task drawer. Use this after
`repoos verify <id>` refreshes stale evidence, or whenever you want another
review pass without moving the task back to `active`. Refuses when an engineer
turn, close-out, or another review is already in progress.

```bash
repoos review again 0832
repoos review again 0832 --json
```

### `repoos verify`

Refresh the current review evidence without starting an engineer turn. This
re-runs the declared browser and command captures and UI verification against
the task worktree's current HEAD. The command waits for the capture to finish.

`repoos verify` refreshes through the server, so the
[`remoteValidation.browserCaptureMode`](/configuration#remote-validation)
policy applies exactly as it does to the automatic handoff capture:
`runner-available` prefers a capable runner and falls back locally with a
visible note, while `runner-only` reports the runner's failure instead of
capturing on this machine.

```bash
repoos verify 0766
repoos verify 0766 --json
```

The refresh is available only for a clean task in review with no agent run or
close-out in progress. Captures and the verification record are stored in the
gitignored evidence directories. A failed or unavailable capture is reported
in the response and verification record.

A capture command that writes a tracked file (ignoring `REPOOS_SHOT_DIR`) does
not leave edits behind: the refresh restores tracked files and removes
untracked files the capture created, reports what was restored (`restored` in
the JSON response, and per-path lines in the human output), and the worktree is
porcelain-clean again afterwards. A HEAD that moved under the capture still
fails the refresh — the record can only be tied to the tree it was captured
on — but the tracked damage is restored even then.

A repo whose product is a native macOS app, a mobile app or a desktop app
declares a **command capture target** instead of (or as well as) a dev-server
`[[preview.targets]]` row — a `[[shot.targets]]` command that renders image
files offscreen. `repoos shot --target "<name>"` runs that command and stores
the images it writes into `REPOOS_SHOT_DIR`, and the automatic handoff capture
runs it too. See
[Previews and shots](../docs/previews.md#evidence-shots-for-non-web-targets) for
the config shape and platform recipes.

After the page loads, `repoos shot` waits for network quiet (best-effort) and a
short settle before capturing, so a dev server's client-mount spinner is not
what gets recorded. Raise `--wait <ms>` for a page that renders slower than
that.

Which target(s) to capture is decided from the files the task changed, matched
against `[[preview.targets]].paths` globs in `repoos.toml` — not the task's
up-front `area:`. The default (main-app) preview can declare its own globs as
`paths` under `[preview]` (this repo: `src/ui-app/**`), so an app diff resolves
it too and a mixed app+docs diff captures both. If nothing matches, it falls
back to area resolution; `--target` overrides. Each shot records its resolved
target name. The drawer warns whenever the changed paths touch a target the
task's `area` does not resolve to, so a mislabeled area becomes visible rather
than silently screenshotting the wrong app. Only one preview runs per task, so
a multi-target capture restarts the preview between targets (stopping one you
may be watching) — the command prints a note when it will do that.

#### The task's `## Shots` list

A change inside a drawer, modal or filled form is not visible from `/`, and you
are the one who knows which page/state shows it. Declare it in the task body —
a `## Shots` section holding a fenced JSON list, one entry per shot:

```json
[
  {
    "target": "default",
    "route": "/",
    "label": "New-task drawer open",
    "highlight": "form.new-task .title-input",
    "steps": [
      { "click": "button[data-test-id='new-task']" },
      { "waitMs": 300 }
    ]
  }
]
```

Each entry takes `target` (a resolved preview target name; omitted means the
default target), `route` (default `/`), an optional `selector` (capture one
element), a human `label` shown as the shot's caption in the Changes tab, an
optional `highlight` CSS selector — every element it matches is outlined in
the capture, pointing the reviewer at what changed — and optional ordered
`steps` — `{ "click": "<selector>" }`, `{ "fill": "<selector>", "text":
"..." }`, `{ "waitFor": "<selector>" }`, `{ "hover": "<selector>" }` or
`{ "waitMs": 300 }` — using plain
CSS selectors or test ids, no framework knowledge. Optional `viewportWidth`
and `viewportHeight` (px, same bounds as the CLI `--viewport` flag) pin the
capture to a specific window size — use two heights on the same route when
layout must be checked at tall and short viewports (#0951). Say what changed:
a label naming the change and a highlight on the changed element are the
difference between a reviewer reading "New-task drawer open" and squinting at
a full page.

`repoos shot` captures this list, and the SERVER does too: declared shots are
captured automatically when a task moves to review, with targets resolved the
same way the CLI resolves them — the diff's changed paths first, then the task's
`area`, then the default `[preview] command`. The automatic capture is
deliberately conservative when nothing is declared (#0603): a fallback
shot's caption records the glob that matched (`auto: matched src/ui-app/**`),
a declared shot's caption records its label (`declared: New-task drawer
open`); a diff touching only tests or task notes touches a glob but is not a
UI change, so nothing is captured without a declaration (test files are not
UI evidence); and a docs target matched only by content files is skipped
unless a declared shot names a route — the docs home page does not show a
wording edit. (Repos whose UI is itself markdown-driven: a genuine UI change
in a `.md` file counts as content, so declare the shot with its route.) Skips
and failures are recorded in the task log and activity as
`shots: skipped — <reason>` / `shots: failed — <reason>`, never as a failed
handoff; skips that ride alongside a captured shot appear in the same note.
An engineer-made capture pre-empts the automatic one.

Typing a route (or passing `--selector`) on the command line replaces the
declared list for that run, and applies to every resolved target — a typed
route is captured, not deferred to the `/` fallback (#0610). The declared
entries are not re-pointed at it: each one carries `steps`, a `highlight` and
a `label` written for its own route, so moving entry #2 to a route you just
typed would click selectors that may not exist there. A route you name also
counts as the justification the docs-content skip asks for, so
`repoos shot /configuration --target "Docs site"` captures the page you asked
for instead of standing down. The command prints the plan it will shoot
(`plan: default /settings – …`) before it starts a browser.

Playwright/WebKit is optional (a dev dependency). When it is missing, `repoos
shot` prints install advice and exits non-zero without touching anything:
`bun add -d @playwright/test && bunx playwright install webkit`. A screenshot
is the one sanctioned use of the managed preview by an engineer agent, and only
for UI-visible changes — see [Review and close-out](/review-and-close-out).

### `repoos index [--json]`

Rebuilds the derived index cache. The cache is disposable — the markdown files
are the source of truth — so this is safe to run any time.

### `repoos gc [--yes|--dry-run]`

Reclaims leaked task worktrees and branches, using the same automatic and
fail-closed policy as the server's boot/close-out sweep. It only removes
throwaway close-out candidates and worktrees for resolved tasks whose content
is provably already in main; active, human-assigned, unmerged, and uniquely
dirty work is kept and reported with a reason. Worktrees outside RepoOS's
managed directory are listed as **external** and never touched. If the task
index or the durable close-out-job records cannot be read, cleanup is skipped
and the error is reported rather than guessing that work is inactive.

Ignored files are inspected too: a worktree holding a local `.env`, an
attachment tree, or other non-generated ignored data is retained even though
`git status` would call it clean. Regenerable output (`dist/`, `node_modules/`,
coverage, `.repoos/` cache) does not block cleanup, and an inherited `.env`
symlink into the main checkout is safe to drop because the canonical file
stays. Retained entries report commits ahead, changed source paths, dirty
paths, and ignored paths so the reason is actionable.

`--dry-run` shows the candidate / retained / external breakdown without
changing anything. `--yes` is accepted as a compatibility alias; the default
run already applies the safe candidates.

### `repoos tunnel <subcommand>`

Publishes local apps through Cloudflare Tunnel + Zero Trust:
`setup`, `create`, `allow`, `deny`, `start`, `install`, `stop`, `list`, `status`.
