orbitmap
v0.20.0
Published
Project management CLI for AI coding agents — works with Gemini CLI, GPT Codex, Claude Code, and any agent with shell access.
Readme
OrbitMap CLI
Project management for AI coding agents — without MCP.
What is this?
OrbitMap CLI gives AI agents (Gemini CLI, Codex, Claude Code without MCP, etc.) access to OrbitMap project management through simple shell commands. It's the non-MCP alternative to orbitmap-mcp, and tracks the same Agent API contract — the exact schema_version it is pinned to lives in tests/contract/fixtures.ts (PINNED_SCHEMA_VERSION) and is bumped by the contract re-vendor, so it is stated in one place instead of drifting here.
Use cases:
- AI agents that can run shell commands but don't support MCP (Gemini CLI, Codex CLI)
- Environments where MCP configuration is impractical
- Quick task management from your terminal
Quick Start
# 1. Run the setup wizard — it sets up an Orbitmap.ai account (cloud) by default.
# Local files, no account: `orbitmap init --local` (see "Local mode" below).
npx orbitmap init
# 2. Enter your credentials
# - API Key (from OrbitMap dashboard)
# - Area ID (optional — auto-detected if you belong to a single area)
# 3. Confirm where the agent skills go — `init` installs them in the same run for
# Claude Code AND Codex (and Cursor when it is installed), without asking
# which agent you use, creating
# CLAUDE.md / AGENTS.md if there is none yet. `--agent <type>` installs for one
# agent only; `--no-skills` opts out.
# 4. Start working
npx orbitmap startinit is the whole install: config, credentials and the agent skills. There is no
mandatory second command.
Local mode
The CLI runs in one of two modes. orbitmap init sets up cloud mode by default — the
account holder has already made that choice by signing up — and local-files mode is not
retired, only reached deliberately, via orbitmap init --local:
| | Cloud mode | Local mode | |---|---|---| | Storage | Orbitmap.ai workspace (Agent API) | Markdown files with YAML frontmatter on your disk | | Account | required | none | | Web UI, team, multiple agents | yes | no — solo developer | | Commands | all of them | all of them, minus the limitations below |
Both modes speak the same commands with the same output — orbitmap start, orbitmap log,
orbitmap tasks --json behave identically, and agent skills installed by orbitmap init never
need to know which mode is active.
Set up local mode
--local is the only way in — there is no interactive mode question anymore, so --local
always takes what you pass and falls back to sensible defaults (workspace directory
./.orbitmap, workspace name, first area = current directory name) rather than prompting:
npx orbitmap init --local \
--workspace ~/orbits/personal \ # where the files live (default: ./.orbitmap)
--workspace-name "My Orbit" \ # default: current directory name
--area "My App" \ # first area (default: current directory name)
--no-skills # skip installing agent skillsIf the workspace directory is outside the project, init writes a pointer at
<project>/.orbitmap/link.json ({"workspace": "…", "area": "…"}) and adds
.orbitmap/link.json and .orbitmap/docs/ to .gitignore (not the whole .orbitmap/
directory — .orbitmap/config.json, see Configuration, is meant to be
committed). The pointer is found by walking up from the current directory, like .git, so
commands work from any subdirectory. If the workspace is ./.orbitmap, nothing is gitignored —
the data is meant to be committable.
Resolution order for the workspace directory: --workspace → ORBITMAP_WORKSPACE →
.orbitmap/link.json → ./.orbitmap/workspace.yml → workspace_path in
~/.orbitmap/config.json. The mode itself is resolved per-project, first match wins: --workspace
(explicit → local) → ORBITMAP_MODE=local|cloud → nearest .orbitmap/config.json mode field →
auto-detect (walk up from cwd looking for .orbitmap/workspace.yml or .orbitmap/link.json, same
as git looking for .git → local) → mode in the global config file, defaulting to cloud when
absent. The auto-detect rung (and the project config rung above it) is what lets one project stay
local and another stay cloud on the same machine — init never writes mode to the global config
for the local path.
Where the data lives
<workspace-dir>/
workspace.yml # workspace header, default area + entity-number counters
missions/MS-<num>.md
intents/IN-<num>.md
ideas/ID-<num>.md
docs/<slug>.md # one copy of each document, same path and format as the
# cloud doc cache (`<project>/.orbitmap/docs/`)
issues/IS-<num>.md # workspace-level issues (no area)
areas/
<area-slug>/
area.yml # area header (git url, default branch, …)
tasks/TS-<num>.md
issues/IS-<num>.md # area-scoped issues
vibes/VB-<num>.mdEvery entity is one Markdown file: the YAML frontmatter holds the fields, the body holds the long
text (## Description, and for tasks an append-only ## Work Log). Relations are stored as entity
numbers and slugs, so the files stay readable and diff cleanly in git.
The CLI is the only writer. Editing these files by hand (or letting an agent do it) bypasses
validation and the counters — always go through orbitmap commands.
Current limitations
Local mode is a proof of concept. Not supported yet:
- No import/export between a local workspace and the cloud (planned next; entity-number format and response shapes are already identical so the future import is a data copy).
- No orbits —
orbit *commands fail withNOT_SUPPORTED_IN_LOCAL_MODEand exit 1. - No document section-patching, sharing or presigned uploads —
doc-patch,doc-share,doc-unshare,doc-visibilityfail the same way.docs,doc,doc-import,doc-update,doc-metaanddoc-statuswork. - No document history —
doc-statusanddoc-metarewrite the file's frontmatter and drop--changelog; the file and git are the audit trail. - No document scope — a local workspace keeps documents in one flat set with no
document-to-area links, so
doc-import --scopeis refused rather than ignored. - No OAuth (
login/logout) — meaningless without a server. - No human team members: exactly one implicit user (
me) and one implicit agent (local-agent).membersreturns them;assignaccepts only them. - No sync or merge between machines — you may sync the workspace directory with git yourself, the CLI does not mediate conflicts.
See docs/architecture.md for the adapter split and
docs/adr/0001-local-mode.md for why local storage exists at all.
Object model
OrbitMap organises work on several levels. The CLI mirrors them 1:1:
| Level | Prefix | What it is |
|-------|--------|-----------|
| Workspace | — | Top-level container that owns areas, orbits, intents and missions |
| Area | — | A single product/repo/domain (formerly called project) — the default scope of most commands |
| Orbit | — | Cross-area grouping of work (a theme, an initiative); items of any type can be attached |
| Intent | IN- | A desired outcome at workspace level; tasks and issues can be linked to it |
| Mission | MS- | A time-boxed workspace focus; only one mission can be active |
| Task | TS- | The unit of work agents execute |
| Issue | IS- | A reported problem |
| Idea | ID- | A future possibility |
| Vibe | VB- | Ad-hoc work context outside the task lifecycle |
Project → Area rename. What used to be called a project is now an area. The global flag is
--area, the env var isORBITMAP_AREA, and the config key isarea. The legacyORBITMAP_PROJECT_IDenv var andproject_idconfig key were read-only fallbacks for one release and were removed by ADR 0002 — they had the same cross-project leak as the machine-wideareathey aliased. Setareain your project's.orbitmap/config.jsoninstead.
Commands
Task Management
| Command | Description |
|---------|-------------|
| orbitmap tasks | List tasks across every area you reach and every assignee (--area, --status, --priority, --orbit, --intent, --mission, --agent, --unassigned, --limit, --all) |
| orbitmap task <id-or-number> | Show full task details |
| orbitmap start [id-or-number] | Start working on a task (auto-picks next TODO if no ID) |
| orbitmap status <id-or-number> <status> | Update task status (--force to move a parent to done with unfinished subtasks) |
| orbitmap create <title> | Create a new task (--from-issue, --intent, --mission — mutually exclusive with --intent, --orbit) |
| orbitmap subtask <parent-id-or-number> <title> | Create a subtask |
| orbitmap assign <id-or-number> [agent-id] | Assign a task, issue, idea, or intent to an agent or user |
| orbitmap get <prefixed-id> | Show any object by prefixed ID (TS-, IS-, ID-, VB-, IN-, MS-) |
| orbitmap dep add <id-or-number> <target-id-or-number> | Add a dependency between tasks |
| orbitmap dep remove <id-or-number> <target-id-or-number> | Remove a dependency |
| orbitmap task update <id-or-number> | Edit a task after creation: title, description, agent instructions, priority, estimated effort, branch name (the old task-edit-content still works) |
Valid statuses: backlog, todo, in_progress, in_review, review_changes, done, blocked, cancelled
What a list shows, and what it hides
Every list — tasks, issues, ideas, vibes, docs, intents, missions — spans
every area your key reaches and every assignee. It is not scoped to your own work, and it
is not scoped to the area your directory maps to: that area is the context for writes and
for single-object reads, and the server deliberately ignores it when listing.
Narrowing is explicit, and always visible in the output:
orbitmap tasks # everything you can reach
orbitmap tasks --area demo # one area
orbitmap tasks --area demo,web # two areas (comma-separated, no spaces)
orbitmap tasks --area all # cancel an ORBITMAP_AREA narrowing for this call
orbitmap tasks --agent me # only your own tasks — the pre-9.0.0 default
orbitmap tasks --agent jakub-agent # a colleague's
orbitmap tasks --unassigned # the free backlog
orbitmap tasks --intent IN-nahetc # everything under one intent, in one call
orbitmap tasks --mission MS-abc123
orbitmap tasks --status all # every status, on every listLists are paged: 30 rows by default, 100 at most per page. The page is not the answer —
--all is:
orbitmap tasks --limit 100 # one bigger page
orbitmap tasks --all # follow every page and print all 143 rowsUnder every result the CLI prints what produced it, whether the list was empty, truncated or complete:
30 of 143 shown across 7 areas — run with --all for every row.
Scope: all 7 reachable areas, status todo,in_progress (46 hidden, add --status all), assignee any.An empty list carries the same line, so "nothing exists" and "you did not ask for it" are never the same sentence:
No tasks. Scope: areas demo (1 of 7), status todo,in_progress (46 hidden, add --status all), assignee unassigned.--json carries the whole meta block: total, next_cursor, per_area
({slug, shown, total} per area), applied_filters, scope and workspace, plus each row's
own area, agent_id and assigned_user_id.
--area names one area everywhere else (create, log, status, doc-import, …) — that
is the area a write lands in, so a comma-separated --area is refused there rather than
silently taking the first. --area all is refused on the document writes for the same
reason: it clears the list filter and nothing else, so on a write it would quietly mean
"whichever area this directory maps to". Say where a new document belongs with
doc-import --scope; every other document command reaches its document by id or slug
across the whole workspace.
Referring to objects: <id-or-number>
Every command that takes an object reference accepts all three forms interchangeably:
- the prefixed display ID the CLI prints —
TS-uw9yh5,IS-4k2p9q,IN-6htga8 - the bare 6-character code —
uw9yh5 - the UUID
You can therefore paste any identifier straight out of orbitmap tasks, orbitmap context or
the message orbitmap create prints. This was previously true of orbitmap task and
orbitmap get only: status, log, subtask, start, assign, task update and
dep add/dep remove rejected prefixed IDs with a 404, including identifiers the CLI had
just printed itself.
The Agent API resolves a prefixed ID inline for issues, ideas, intents and missions, but not
for tasks — a task reference that is not a UUID needs a /tasks/by-number/{n} lookup first.
The CLI now does that once, in the adapter layer, so every task command behaves the same and a
UUID still costs exactly one request. See src/id-resolve.ts.
assign auto-detects the object type from the prefix (IS-… → issue, IN-… → intent, …);
pass --type task|issue|idea|intent for bare UUIDs and bare codes.
Work Logging
| Command | Description |
|---------|-------------|
| orbitmap log <id-or-number> <message> | Log work on a task, intent, mission or issue |
The target follows from the reference: TS-… writes against the task, IN-… against the
intent, MS-… against the mission, IS-… against the issue. A bare UUID or bare code means
a task unless --target says otherwise.
Options:
--type <type>— Log type:note(default),code_change,decision,blocker. Fordiscovery,explorationandfixuseorbitmap vibe log— those are vibe types, not work-log types.status_changeis written by the server on a status change and cannot be authored by a client.--target <kind>— Which object a bare UUID names:task(default),intent,mission,issue--meta <json>— JSON metadata, e.g.'{"files":["src/app.ts"]}'
Examples:
orbitmap log abc123 "Implemented user auth endpoint" --type code_change
orbitmap log abc123 "Using JWT over sessions for stateless auth" --type decision
orbitmap log abc123 "Blocked on missing API credentials" --type blocker
orbitmap log IN-a1b2c3 "Chose 3 FKs + a CHECK over a polymorphic target" --type decision
orbitmap log MS-a1b2c3 "Mission scope trimmed to the two gating intents" --type note
orbitmap log IS-a1b2c3 "Root cause is the stale cache, not the query" --type decisionWork log on an issue is cloud-only for now — local mode has no ## Work Log section on
issue files and refuses orbitmap log IS-… with NOT_SUPPORTED_IN_LOCAL_MODE.
Work log entries on a status change
intent update, mission update and mission status take a repeatable --log
<type>:<content>, sent as one batch with the update. The content is split on the FIRST
colon only, so it may contain colons of its own.
orbitmap intent update IN-a1b2c3 --status plan \
--log decision:"chose 3 FKs + CHECK" \
--log note:"resume: CLI next"The API requires at least one entry when the status changes and writes nothing at all
when there is none — so a rejected transition is re-run with --log, not retried as-is.
Documents
| Command | Description |
|---------|-------------|
| orbitmap docs | List documents across every area you reach, plus workspace-level ones (--area, --type, --search, --status — default published, CSV or all — --limit, --all) |
| orbitmap doc <id-or-slug> | Show document (TOC by default, --full, --section) |
| orbitmap doc-import <title> | Import a markdown file (--scope workspace\|area, --workspace-id for which workspace, --context to set when-to-use hint, --status draft to import as a draft) |
| orbitmap doc-update <id-or-slug> | Replace a document from a file (--changelog, --context to rewrite the when-to-use hint) |
| orbitmap doc-patch <id-or-slug> | Patch a document with section operations (--changelog, --context, --dry-run) |
| orbitmap doc-meta <id-or-slug> | Change a document's title, type or context — never its content (the slug and the version stay as they are; --base-version, --changelog) |
| orbitmap doc-status <id-or-slug> <draft\|published\|archived> | Archive, restore or publish a document without touching its content, version or history (--base-version, --changelog) |
| orbitmap doc-share <doc-id> <area-id> | Link a document to another area |
| orbitmap doc-unshare <doc-id> <area-id> | Unlink a document from an area (--confirm-workspace-scope to detach the last one) |
| orbitmap doc-visibility <doc-id> <area-id> | Restrict a share to specific agents (or allow all) |
| orbitmap docs pull [slug] | Download document(s) to local cache |
| orbitmap docs list-cache | Show locally cached documents |
| orbitmap docs clean | Remove stale cached documents |
Examples:
# List all API docs
orbitmap docs --type api
# Search documents by title
orbitmap docs --search "auth"
# Read cheaply: TOC → one section → full content
orbitmap doc api-specification
orbitmap doc api-specification --section "## Authentication"
orbitmap doc api-specification --full
# Import a new document into the current area
orbitmap doc-import "API Spec" --type api --file ./docs/api-spec.md
# Import one at workspace level: linked to no area, reachable from every one
orbitmap doc-import "Coding Standards" --type guide --file ./docs/standards.md --scope workspace
# Update existing document
orbitmap doc-update api-specification --file ./docs/api-spec.md --changelog "Added auth section"
# Update the content AND the when-to-use hint shown in `orbitmap docs`
orbitmap doc-update api-specification --file ./docs/api-spec.md \
--context "Reference when implementing or debugging authentication flows"
# Patch a single section (optimistic locking via --base-version)
orbitmap doc-patch api-specification --base-version 7 \
--replace-section "## Authentication" --content "Bearer tokens only." \
--changelog "Reworded auth"
# Relabel a mislabelled document — columns only: no upload, no version bump, same slug
orbitmap doc-meta api-specification --title "Agent API Specification" --type api
# Share with another area
orbitmap doc-share <doc-uuid> <area-uuid> --agents agent1-uuid,agent2-uuid
# Detach the last area — the document becomes workspace-level, so it must be confirmed
orbitmap doc-unshare <doc-uuid> <area-uuid> --confirm-workspace-scopeScope. A document belongs to a workspace and is linked to any number of areas; zero
links means workspace scope. --scope at import says where a new one lands; doc-share /
doc-unshare move an existing one between the two. A reference (slug or UUID) resolves
across the whole workspace on every path, so a refusal names the real cause — the document
is unreachable at your scope, or not writable by your role — rather than reading as a wrong
slug.
Area & Agent Info
| Command | Description |
|---------|-------------|
| orbitmap context | Session matching material: agent identity, area/workspace, missions and intents with TL;DRs, mission workitems, areas, document metadata |
| orbitmap areas | List all areas assigned to this agent |
| orbitmap create-area <name> | Create a new area (--desc, --slug, --workspace-id) |
| orbitmap members <area-id> | List agents and users in an area (accepts slug) |
| orbitmap overview [area-id] | Area overview with task statistics (uses default area) |
Issues, Ideas & Vibes
| Command | Description |
|---------|-------------|
| orbitmap issues | List open issues across every area you reach (--area, --status, --severity, --intent, --limit, --all); status shows as resolved · on_branch once a delivery is set |
| orbitmap issue show <id> | Show issue details (UUID or number), including its work log (oldest-first) and delivery beside the status |
| orbitmap issue register | Register a new issue (--intent to link it to an intent) |
| orbitmap issue edit <id> | Edit issue content (--title, --description, --severity) |
| orbitmap issue status <id> <status> | Update issue status (--delivery <on_branch\|merged\|released>, only with a resulting status of resolved; --resolved-task accepts a UUID or a TS- display number) |
| orbitmap issue resolve <id> | Resolve an issue (same --delivery/--resolved-task options as issue status) |
| orbitmap ideas | List ideas across every area you reach (--area, --status, --priority, --limit, --all) |
| orbitmap idea add | Add a new idea — into this directory's area, or at workspace level from the workspace root (--workspace forces it, --area names one) |
| orbitmap idea show <id> | Show idea details (UUID or number) |
| orbitmap idea update <id> | Correct an idea after filing it (--title, --content, --priority) |
| orbitmap idea status <id> | Update idea status |
| orbitmap vibes | List vibes across every area you reach (--area, --type, --since, --limit, --all) |
| orbitmap vibe log | Log a new vibe |
| orbitmap vibe show <id> | Show vibe details |
| orbitmap vibe update <id> | Update a vibe |
| orbitmap vibe delete <id> | Delete a vibe permanently — asks first, --yes confirms non-interactively (--json deletes without prompting) |
Intents
Workspace-level desired outcomes. Tasks and issues hang off an intent; an intent can sit in an orbit.
| Command | Description |
|---------|-------------|
| orbitmap intents | List intents — workspace-wide (--status, --mission, --limit, --all) |
| orbitmap intent show <id-or-number> | Show an intent (UUID or IN-xxxxxx) — embeds the context bundle (--no-bundle strips it, --no-logs skips the work-log tail, --slim is the freshness probe) |
| orbitmap intent create <title> | Create an intent (--outcome, --design, --plan, --status, --areas <slug>,<slug>, --log <type>:<content> repeatable, --orbit) |
| orbitmap intent update <id> | Update title, outcome, design, plan, or status (--roadmap-priority <lane> sets the roadmap lane; --areas <slug>,<slug> declares its areas; --log <type>:<content>, repeatable) |
| orbitmap intent link <id> --type task\|issue --id <id-or-number> | Link a task or issue (UUID or display number) |
| orbitmap intent unlink <id> --type task\|issue --id <id-or-number> | Unlink a task or issue |
| orbitmap intent orbit <id> [orbit-id] | Set the intent's orbit (--clear to remove) |
Statuses: new, design, plan, build, done, cancelled
Roadmap lanes (--roadmap-priority, quote the two-word ones): parking lot, candidate,
roadmap, mission, done
orbitmap intent create "Cut onboarding time in half" \
--outcome "New users reach first value in under 5 minutes" \
--design "Two-step wizard replacing the current five-step form." \
--plan "Phase 1: wizard skeleton. Phase 2: inline validation." \
--status design --orbit growth-q3
orbitmap intent link IN-a1b2c3 --type task --id TS-d4e5f6
# A status change carries its reason — the API rejects it (422) without at least one entry.
orbitmap intent update IN-a1b2c3 --status plan \
--log decision:"chose 3 FKs + CHECK" \
--log note:"resume: CLI next"intent create --orbit is create-then-attach. If the attach step fails the intent is still created — the CLI reports the failure and does not retry.
The context bundle. intent show prints a Context bundle: block after the intent's own outcome/design/plan and before the work-log tail: the mission handle with its outcome TL;DR, the documents that surround the intent (listed by their when_to_use, with the slug to pass to orbitmap doc), sibling intent handles, and the intent's own tasks and issues. It is handle tier — TL;DRs and titles, never bodies. --no-bundle strips it when you only want the intent itself.
The freshness probe. --slim replaces the whole body with a ~150-token probe: handle, the outcome/design/plan TL;DRs, declared areas, mission, task counts per status, and the updated_at + last_log_id pair. Hold that pair from a full fetch; run the probe to see whether the intent moved. Unchanged means your copy is current — do not re-fetch. A work log never touches updated_at, so both halves have to be compared. --slim wins over --no-bundle and --no-logs.
orbitmap intent show IN-a1b2c3 # intent + context bundle + work-log tail
orbitmap intent show IN-a1b2c3 --no-bundle # just the intent and its logs
orbitmap intent show IN-a1b2c3 --slim # freshness probe: TL;DRs + updated_at/last_log_idMissions
Time-boxed workspace focus. Only one mission can be active at a time.
| Command | Description |
|---------|-------------|
| orbitmap missions | List missions — workspace-wide (--status, --limit, --all) |
| orbitmap mission show <id-or-number> | Show a mission (UUID or MS-xxxxxx) |
| orbitmap mission create <title> | Create a mission (--desc, --outcome, --end-date) |
| orbitmap mission update <id> | Update title, description, outcome, or end date (--log <type>:<content>, repeatable) |
| orbitmap mission status <id> <status> | Set lifecycle status (--move-to to relocate unfinished work; --log required by the API) |
| orbitmap mission link <id> --type <t> --id <id-or-number> | Link a task, issue, idea, or intent (UUID or display number) |
| orbitmap mission unlink <id> --type <t> --id <id-or-number> | Unlink a directly-linked item |
Statuses: created as new; then active, completed, cancelled.
orbitmap mission create "Ship v2 beta" --outcome "Beta live for 50 customers" --end-date 2026-09-30
orbitmap mission status MS-a1b2c3 active
orbitmap mission status MS-a1b2c3 completed --move-to MS-d4e5f6Linking a task or issue to a mission clears its intent_id — an item belongs either to an intent or directly to a mission.
Orbits
| Command | Description |
|---------|-------------|
| orbitmap orbit list | List orbits (workspace-scoped from the area context, --workspace) |
| orbitmap orbit show <id-or-slug> | Show orbit details (areas, task counts, resources) |
| orbitmap orbit create <name> | Create an orbit (--desc, --color, --areas) |
| orbitmap orbit update <id-or-slug> | Update name, description, status, or color |
| orbitmap orbit items <id-or-slug> | List items in an orbit (--type) |
| orbitmap orbit attach <id-or-slug> | Attach an object to an orbit |
| orbitmap orbit detach <id-or-slug> | Detach an object from an orbit |
| orbitmap orbit area link <id-or-slug> <area-id> | Link an area to an orbit |
| orbitmap orbit area unlink <id-or-slug> <area-id> | Unlink an area from an orbit |
| orbitmap orbit dep <id-or-slug> <target-orbit-id> | Add an orbit-to-orbit dependency |
Item types: task, issue, vibe, idea, document
Orbit statuses: active, completed, archived
orbitmap orbit create "Growth Q3" --color "#5B8DEF" --areas <area-uuid>,<area-uuid>
orbitmap orbit attach growth-q3 --type task --id <task-uuid> --note "core flow"
orbitmap orbit items growth-q3 --type taskThere is no orbit delete and no dependency remove in the API — archive via orbit update --status archived.
Delivery Status
Track code delivery lifecycle on tasks:
orbitmap status abc123 done --delivery on_branch
orbitmap status abc123 done --delivery merged
orbitmap status abc123 done --delivery releasedSetup helpers
| Command | Description |
|---------|-------------|
| orbitmap init | Full setup — mode, then the workspace + its area directories (cloud) or a local workspace, then the agent skills (--key, --workspace-id, --new-workspace [name], --area, --allow-nested, --advanced, --global; --local, --workspace, --workspace-name; --agent <type>, --scope <user|project>, --no-skills, --skip-update-check) |
| orbitmap off / orbitmap on | Switch OrbitMap off for this folder (and its sub-folders), or back on. off writes a short "OrbitMap is off in this folder" block into the folder's CLAUDE.md / AGENTS.md and "disabled": true into .orbitmap/config.json. Every command except on, off, init, uninstall, setup-agent and help then exits 1 with OrbitMap is off in this folder. Run `orbitmap on` to turn it back on. on (from the folder or any sub-folder) removes both. No network call |
| orbitmap login / orbitmap logout | Browser-based OAuth login / token revocation |
| orbitmap setup-agent | Install or repair the instructions for Claude Code and Codex, and Cursor when it is installed — init installs them once already, and orbitmap context refreshes them after a CLI upgrade by itself. --agent <type> handles one agent only, e.g. Cursor, Gemini, Windsurf or generic (--scope, --clean-project, --skip-update-check) |
Global Options
| Option | Description |
|--------|-------------|
| --json | Output raw JSON (for scripting/piping) |
| --area <id> | Override area context (UUID or slug) |
| --workspace [path] | Override the local workspace directory (local mode only). The path is optional only so the bare flag can reach idea add, where it means "file at workspace level"; with no path it never selects a directory or a mode |
Configuration
See docs/adr/0002-project-scoped-config.md for the full design and rationale (this
replaced a single global config file — breaking for scripted init callers that relied on
that, see the ADR's Consequences section).
Setup
Run npx orbitmap init for interactive setup. Cloud setup is workspace-first: the key is
validated with GET /areas, you pick the workspace (--workspace-id in a script), and init
works out which directory holds which area — by directory name, by the origin git remote
matched against the area's repository URL, by the slugified name, or by asking. It writes
<root>/.orbitmap/config.json (committable, no secrets) by default; pass --global to write
~/.orbitmap/config.json instead (a machine-wide default, like git config --global). The API
key always goes to ~/.orbitmap/credentials.json, under a named agent profile — never into
a config file, either way. Re-running init merges, so running it inside a newly cloned area
adds that area to the workspace map that already exists above it.
A second project needs no key paste (Agent API 10.22.0). Without --key, init first checks the
keys already saved in ~/.orbitmap/credentials.json with GET /me and lists every one that works on
this server, labelled (all workspaces · <first workspace>) or (workspaces: a, b, c +N) so keys of
different accounts can be told apart. One match is offered in the key prompt (Enter = use it, or
paste a new key); several give a numbered pick where Enter means [k], paste a new key; none falls
back to the key prompt. A scripted run reuses nothing, and piped input reuses only a single
all-workspaces key. When that agent may create workspaces, the workspace question also offers
Create a new workspace beside one Use existing: <name> (areas: …) line per workspace. It is the
default for a folder outside every connected workspace, asks for a name that has no default (three
empty answers stop the run with nothing written), and is created with POST /workspaces only after
you confirm the write manifest. In a script, pass
--new-workspace [name] (default name: the folder's). An older server answers GET /me with 404,
and init then works exactly as before.
Project config file — <project>/.orbitmap/config.json
{
"schema_version": 1,
"mode": "cloud",
"agent": "orbitmap-app-bot",
"workspace": "my-workspace",
"areas": { "orbitmap-cli": "orbitmap-cli", "packages/api": "api" }
}mode is "cloud" (the default when the key is absent) or "local". agent names a
profile in ~/.orbitmap/credentials.json — the project says which agent to use, the
credentials file (below) holds that agent's key. workspace is a workspace slug or UUID,
never a path. areas maps directories (relative to this file's parent; "." is that
directory) to area slugs, which is how one root can hold several areas; each mapped
subdirectory also gets a marker config of its own holding nothing but {"area": "<slug>"}.
The older single area field is still read for back-compat. This file is meant to be
committed: a team shares the workspace, the agent name and the area layout through version
control, and each member's own copy of credentials.json supplies their own agent's key.
Areas living outside the tree cannot be expressed portably, so they go to the gitignored
<root>/.orbitmap/link.json instead ("areas": { "<slug>": "/abs/path" }).
Global config file — ~/.orbitmap/config.json
Same shape, used as the fallback for any field a project's own config doesn't set, and as
the only place workspace_path (the default local workspace directory) is read from. Only
written by orbitmap init --global, or left over from before ADR 0002 — in which case a
lingering area is still honoured but prints a one-time deprecation warning telling you to
move it into a project's own .orbitmap/config.json.
Credentials file — ~/.orbitmap/credentials.json
Never committed, never leaves the machine. Also holds the OAuth session used by
orbitmap login/orbitmap logout (unrelated top-level fields, unchanged shape):
{
"agents": {
"orbitmap-app-bot": { "api_key": "orbitmap_..." },
"orbitmap-cli-bot": { "api_key": "orbitmap_..." }
}
}An agent name that resolves to nothing here (nothing named agent anywhere, including the
global config) falls back to the "default" profile — which is also where a pre-ADR-0002
global api_key is migrated to. That migration runs inside loadConfig(), i.e. on the
first command of any kind after upgrading, not only on orbitmap init; it rewrites
~/.orbitmap/config.json without the key (preserving every other key in the file) and
prints a one-time notice. If the write fails — read-only $HOME, a credentials.json owned
by another user — it says so and keeps using the key from the config file, so the CLI never
locks you out over a key that is right there on disk.
~/.orbitmap itself can be relocated with ORBITMAP_CONFIG_HOME or the standard
XDG_CONFIG_HOME (as $XDG_CONFIG_HOME/orbitmap); reads fall back to the legacy
~/.orbitmap location when the new one has nothing yet.
Environment Variables
Environment variables take precedence over every config file:
| Variable | Description |
|----------|-------------|
| ORBITMAP_API_KEY | Agent API key (bypasses agent-profile lookup entirely) |
| ORBITMAP_API_URL | API base URL — the only remaining override; there is no config-file equivalent any more (see the ADR) |
| ORBITMAP_AGENT | Name of the agent profile in credentials.json to use |
| ORBITMAP_AREA | Area UUID or slug |
| ORBITMAP_MODE | cloud or local — overrides the config mode |
| ORBITMAP_WORKSPACE | Local workspace directory (local mode) |
| ORBITMAP_CONFIG_HOME / XDG_CONFIG_HOME | Relocate ~/.orbitmap |
| ORBITMAP_NO_UPDATE_NOTIFIER | 1 silences the once-a-day "newer CLI available" notice (it is also skipped when CI is set) |
Resolution Priority
Area (first match wins):
- Explicit
--areaflag ORBITMAP_AREAenv.orbitmap/link.json(area) found by walking up from the current directory — local mode- Nearest
.orbitmap/config.jsonthat setsarea, found the same way. Resolution is per field: apackages/api/.orbitmap/config.jsonthat sets onlyagentdoes not hide anareapinned by the repository root default_areain the workspace'sworkspace.yml— local mode; written byorbitmap initfor the first area, so a workspace outranks the machine-wide default of an unrelated project- Global config
area(deprecated — prints a warning when hit) - Defaults (local mode: the only area in the workspace, when there is exactly one)
Agent / API key (first match wins):
ORBITMAP_API_KEYenv — used directly, no profile lookup- Otherwise resolve an agent name (
ORBITMAP_AGENTenv → nearest.orbitmap/config.jsonthat setsagent→ global configagent→"default") and look up its key incredentials.json
Mode (first match wins): --workspace (explicit → local) → ORBITMAP_MODE → nearest
.orbitmap/config.json that sets mode → local auto-detect (.orbitmap/workspace.yml or
.orbitmap/link.json found by walking up) → mode in the global config, defaulting to
cloud.
Workspace directory (local mode, first match wins): --workspace → ORBITMAP_WORKSPACE
→ .orbitmap/link.json (workspace) → nearest .orbitmap/workspace.yml → the .orbitmap/
directory of the nearest config that sets mode: "local", when it is itself a workspace →
workspace_path in the global config, which must be an absolute path (a relative one
would mean a different directory in every project, so it is ignored with a warning).
Authentication
In cloud mode, OrbitMap CLI uses a single Agent API Key for authentication (Authorization: Bearer header). Local mode has no authentication at all — access to the workspace directory is the only credential. Each agent has its own key, named by an agent profile in ~/.orbitmap/credentials.json — get the key itself from the OrbitMap dashboard. Area context travels in the X-Orbitmap-Area header.
Optionally, set a default Area ID if your agent is assigned to multiple areas.
Error handling
API errors are printed as Error [CODE] (status): message. Alongside the regular envelope, the client normalises the flat billing/usage shape returned by 402/429 responses into:
| Code | When |
|------|------|
| PAYMENT_REQUIRED | HTTP 402 — billing action needed |
| RATE_LIMITED | HTTP 429 — too many requests |
| USAGE_LIMIT | Other flat-error responses |
| TIMEOUT | No response within 30s |
For AI Agents
orbitmap init already teaches your agent about OrbitMap — the skills are its last step, in
both cloud and local mode. It installs them for Claude Code and Codex together, and for
Cursor when it is installed, without asking which agent you use, and creates the agent config
file when there is none:
npx orbitmap init # skills for Claude Code and Codex (+ Cursor when installed)
npx orbitmap init --agent claude # one agent only; --no-skills opts outWhy both: they cost a handful of small files, while asking a new user which coding agent they
work with was the step most likely to lose them. Nothing about the directory is inspected, and
the install does not require either agent to be present — if neither Claude Code nor Codex is
on the machine, the run ends with this notice (and cloud init skips its "Next" block):
No Claude Code or Codex found on this machine. OrbitMap works through one of them —
install either and it picks up the OrbitMap skills installed just now:
Claude (desktop app with the Code tab): https://claude.com/download
Codex (in the ChatGPT desktop app): https://chatgpt.com/codexWhen at least one is found, cloud init closes with a "Next" block that names only the places
this machine can open the folder from — a Terminal: line (cd <folder> && claude, or codex)
and/or a Desktop: line (Claude app → Code tab, ChatGPT app → Codex), plus a Cursor:
line when Cursor is installed — and tells you to ask "what should we build first?". A machine
with only Cursor gets the "Next" block and no notice.
setup-agent does the same install on its own — for repairing an install, and for adding one of
the four agents init does not install by default (after a CLI upgrade the skills refresh by
themselves, see "Staying current" below):
npx orbitmap setup-agent # Claude Code and Codex (+ Cursor when installed), like init
npx orbitmap setup-agent --agent gemini # one agent only: claude, codex, gemini, cursor, windsurf, genericGemini, Windsurf and generic have no skill ecosystem, so installing them means inlining
~86 KB of instructions into one file; that is why they are reached deliberately with --agent
rather than by default.
Cursor loads skills too (Cursor 2.4+). init and a bare setup-agent install it alongside the
default pair when Cursor is on the machine — the cursor / cursor-agent command, the Cursor
app, or Cursor's app-data folder (~/.cursor alone does not count: OrbitMap creates it). Install
it explicitly for Cursor Cloud Agents on a machine without Cursor, or to commit the skills to a
repository:
npx orbitmap setup-agent --agent cursor # ~/.cursor/skills/ + ~/.cursor/rules/orbitmap.mdc
npx orbitmap setup-agent --agent cursor --scope project # ./.cursor/skills/ + ./.cursor/rules/orbitmap.mdcCloud Agents see only ~/.cursor/skills/ after you turn on Settings → Agents → Sync Skills, or
skills committed to the repository (.cursor/). ~/.claude/skills/ never reaches them. The install
at project scope removes the OrbitMap block an older CLI inlined into .cursorrules, and
orbitmap context keeps a Cursor skills install current after an upgrade, like the other two.
Cursor also reads ~/.claude/skills/ and ~/.codex/skills/, so on a machine with all three
installs it may list each OrbitMap skill more than once.
Staying current
- Skills refresh themselves.
orbitmap contextrefreshes the skills, instructions block and hook entry that OrbitMap wrote earlier — Claude Code, Codex and Cursor, user and project scope — when their<!-- ORBITMAP_START v<ver> -->marker is older than the CLI you run, and prints one stderr line:[orbitmap] Updated the OrbitMap skills to v<new> (were v<old>): ~/.claude, ~/.codex. Nosetup-agentrun is needed after an upgrade. - Daily notice. At most once per calendar day the CLI ends a command with one stderr line:
[orbitmap] A newer CLI is available: <latest> (you run <current>). Update: <command>(npm i -g orbitmap@latestfor a global install,npx orbitmap@latestfor npx). The registry is asked at most once per 24 hours, for at most 1.5 seconds, and silently skipped offline. SetORBITMAP_NO_UPDATE_NOTIFIER=1to turn it off; it is also skipped whenCIis set. The installed skills tell the agent to relay[orbitmap]notices once per session. initandsetup-agentrefuse a stale CLI. They check the registry live; when a newer version exists they printThis is orbitmap <cur>; the latest is <latest>.,Update first: <command>andContinue with <cur> anyway? [y/N]— Enter stops with nothing written. A non-interactive run (--key, no terminal) exits with code 1 and suggests--skip-update-check, which skips the check.uninstallnever checks.- Server minimum. The CLI sends
X-Orbitmap-Cli-Versionon every Agent API request. A server that sets a minimum version answers an older CLI with426 CLI_UPGRADE_REQUIRED; the CLI printsError [CLI_UPGRADE_REQUIRED] (426): <message>andUpdate: <command>. The gate is off until the server sets one.
Where it installs: --scope user (global) vs --scope project
Run interactively, both commands ask once where the skills should go — one answer for every agent being installed:
Where should the OrbitMap skills be installed?
[1] Global (~/.claude and ~/.codex) — every session and subagent sees them, in any directory;
writes into your home directory (recommended)
[2] Project (this dir) — only sessions started in this directory can load themGlobal is recommended and is what Enter picks; --scope skips the question; a scripted run takes
global without prompting and says so in its output.
Global (user) scope means ~/.claude/skills/ and ~/.claude/CLAUDE.md for Claude Code,
~/.codex/skills/ and ~/.codex/AGENTS.md for Codex, and ~/.gemini/GEMINI.md for Gemini. Agent
skills do not traverse directories: a project-level .claude/skills/ is invisible to a session
started in a sibling or child repository, while CLAUDE.md is inherited downwards. A user-scope
install is the only one that is consistent everywhere — and it matters now that one workspace
routinely spans several area directories, where a single project install would cover just one.
A user-scope install writes nothing into your project directory.
npx orbitmap setup-agent --scope project # this directory only
npx orbitmap init --agent claude --scope projectAt project scope each agent's skills go where that agent reads them: ./.claude/skills/ and
./CLAUDE.md for Claude Code; ./.agents/skills/, ./AGENTS.md and ./.codex/hooks.json for
Codex. The hook says so explicitly, so an agent that inherits CLAUDE.md / AGENTS.md from a
parent directory but cannot load the skills knows why, and knows to re-run with --scope user.
CODEX_HOME. Codex's personal directory is ~/.codex unless CODEX_HOME is set to an
absolute path, in which case Codex reads $CODEX_HOME/skills and $CODEX_HOME/AGENTS.md instead
— and so does the installer: a user-scope Codex install, its hook file (hooks.json) and the
refresh checks all follow CODEX_HOME. A relative value is ignored.
The Codex hook needs one approval. Both agents get a PostToolUse hook that carries the server's
Instructions blocks to the agent. Codex never runs a freshly written hook until you trust it,
so on a machine where Codex is present the install ends its Codex part with:
Codex: approve the OrbitMap hook once — run /hooks inside Codex and trust `orbitmap emit-instructions`.That is the one manual step; Claude Code needs nothing.
When Claude Code is found and Codex is not, the install prints Codex not found (looked for codex
on PATH, ~/.codex, Codex.app and ChatGPT.app). If you use Codex, its skills are installed anyway —
run /hooks inside Codex and trust orbitmap emit-instructions. instead. From inside an agent,
orbitmap context also reports (once per day) a missing skills directory, instructions block or
hook entry for the agent that called it, and a Codex hook that was never approved.
Coming from a pre-skills Codex install? Those wrote all six skill bodies inlined into
~/.codex/AGENTS.md — around 86 KB of markdown carried in every turn. Re-running the install at
user scope replaces that block in place, between its <!-- ORBITMAP_START/END --> markers, with
the thin hook plus real skill files under ~/.codex/skills/ — nothing else to run:
npx orbitmap setup-agentThe shrink needs no manual cleanup.
Upgrading from an older setup? If you already have skills in ./.claude/skills/ (or
./.agents/skills/ for Codex), that project copy silently wins in this directory. A user-scope
install detects it and names the paths; it offers to clean up when run interactively, and
otherwise tells you to run:
npx orbitmap setup-agent --scope user --clean-projectCleanup removes only the six OrbitMap skill directories and OrbitMap's own marker block and hook
entry — never your own skills, never unrelated CLAUDE.md content, never a whole .claude
directory.
Either way it installs two layers, mirroring the OrbitMap MCP server:
- A thin always-on hook (~10 lines) in your agent config (
CLAUDE.md,AGENTS.md, …) so the agent always knows the project uses OrbitMap and knows the workflow skeleton. - The deep, on-demand instructions — full parity with the MCP
fullprofile (plusorbitmap-brainstormandorbitmap-design, CLI-native, no MCP counterpart). The agents that can load skills — Claude Code, Codex and Cursor — get them as six loaded-on-demand skills (orbitmap,orbitmap-execute,orbitmap-plan,orbitmap-brainstorm,orbitmap-design,orbitmap-docs, zero per-turn context cost) under~/.claude/skills/,~/.codex/skills/and~/.cursor/skills/respectively — or./.claude/skills/,./.agents/skills/and./.cursor/skills/at project scope ($CODEX_HOME/skills/whenCODEX_HOMEis set). Gemini, Windsurf and generic have no skill ecosystem, so the same content is inlined into their config file as a markdown fallback.
A skill is not always a single file: orbitmap-execute ships SKILL.md plus
references/worker-contract.md (the rules a dispatched worker subagent follows). init and
setup-agent install every file a skill carries, and refresh them all on a re-run.
The instructions are mode-agnostic: they reference CLI commands only, so the same skills drive an agent whether the data goes to the cloud or to local files.
Executing work: two flows
The installed orbitmap-execute skill covers two ways of working, chosen by what you ask for:
- "work on TS-x" runs a single tracked task — start or resume it, implement, log, and
finish with the correct status (
in_reviewby default, so a human verifies before closing). - "execute IN-x" delivers a whole planned intent in the background: tasks are built by
parallel workers in dependency-ordered waves, each task gated by its own tests, then the
intent-level test and an impact pass diffed against a test baseline captured before the run,
with a goal-based fix loop (no retry cap) until the gates are green. A stuck task ends
blockedwith a logged blocker while the rest of the intent completes. The run ends with every completed taskin_reviewon anintent/<number>-<slug>branch — merging is left to you: the run never merges and never pushes to the default branch.
Both flows follow a test policy: one test per guarantee, existing tests first, and tests a change made redundant are pruned; each finished task records the tests it reused, added, changed or removed.
See Agent Instructions for details.
Requirements
- Node.js 18+
License
MIT
