@motir/cli 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +532 -5
  2. package/dist/index.js +3770 -0
  3. package/package.json +39 -6
package/README.md CHANGED
@@ -1,8 +1,535 @@
1
- # @motir/cli
1
+ # `@motir/cli` — the `motir` command-line tool
2
2
 
3
- **Motir** — AI-native project management: a full Jira/Linear-class PM tool with AI planning on the way in and AI coding on the way out.
3
+ Terminal dispatch of the Motir work loop. The CLI is an **MCP client** of the
4
+ Motir server (`/api/mcp`): every command speaks Model Context Protocol to the
5
+ tenant with a personal access token (PAT) as a bearer credential. There is no
6
+ parallel REST path — if the CLI needs a capability it lands as an MCP tool
7
+ first, then the CLI consumes it (story 7.9 architecture).
4
8
 
5
- This is a **placeholder release** to hold the package name. The real `motir` CLI is in development at [moooon-B-V/prodect-core](https://github.com/moooon-B-V/prodect-core) (`packages/cli`) and will supersede this version.
9
+ > **Status (Subtask 7.9.1):** this is the scaffold + auth + link layer. Read
10
+ > commands (`ready` / `status` / `open` — 7.9.2), single dispatch (`next` /
11
+ > `run` / `done` — 7.9.3), and the loop (`auto` / `batch` — 7.9.4+) land on the
12
+ > same `commander` program as their subtasks ship.
6
13
 
7
- - Website: [motir.co](https://motir.co)
8
- - License: GPL-3.0
14
+ ## Toolchain (the 7.9.1 evaluate-and-record decision)
15
+
16
+ - **CLI framework: [`commander`](https://github.com/tj/commander.js).** Chosen
17
+ over `yargs` for its smaller surface, first-class subcommand tree (`motir auth
18
+ login`, `motir link add`), native async actions (`parseAsync`), and built-in
19
+ `--help`/`--version`. yargs’ middleware/coercion power isn’t needed here.
20
+ - **Bundler: [`tsup`](https://tsup.egoist.dev) (esbuild under the hood).** A
21
+ zero-config TS→ESM bundler that emits a single `dist/index.js` with the
22
+ `#!/usr/bin/env node` shebang baked in (the `bin` entry). We use tsup rather
23
+ than raw esbuild only to skip hand-writing the build script; the engine is the
24
+ same esbuild the rest of the toolchain already trusts.
25
+ - **Runtime:** Node ≥ 22, ESM (`"type": "module"`).
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ npm install -g @motir/cli
31
+ motir --help
32
+ ```
33
+
34
+ `pnpm add -g @motir/cli` and `yarn global add @motir/cli` install the same
35
+ package. Runtime: **Node ≥ 22**, ESM.
36
+
37
+ **For contributors** — to run the CLI from this checkout instead of the
38
+ published package:
39
+
40
+ ```sh
41
+ pnpm --filter @motir/cli build # produces dist/index.js (the `motir` binary)
42
+ # then run it directly, or `pnpm --filter @motir/cli exec motir …`
43
+ node packages/cli/dist/index.js --help
44
+ ```
45
+
46
+ ## Commands (this subtask)
47
+
48
+ ```sh
49
+ motir auth login [--server <url>] [--token <pat>] # validate + store a PAT
50
+ motir auth status [--server <url>] # server, token prefix, owning user
51
+ motir auth logout [--server <url>] # forget the stored token
52
+
53
+ motir link [--server <url>] [--workspace <slug>] [--project <key>] [--repo <name>]
54
+ motir link add <repo> <path> # add a checkout-path override
55
+ motir link remove <repo> # remove an override
56
+
57
+ motir doctor [--agent <cmd>] [--json] # BYOK preflight (read-only)
58
+ ```
59
+
60
+ ## Reading the plan — `ready` / `status` / `sprints` / `sprint` / `show` (7.9.13 · 7.9.14 · 7.9.16)
61
+
62
+ The read surface. Every one of these is a pure read: it claims nothing, writes
63
+ nothing, and is safe to run in a loop.
64
+
65
+ ```sh
66
+ motir ready [--kinds <list>] [--assignee <id|me|unassigned>] [--json]
67
+ motir status [--json] # ready / in-flight + active sprint
68
+ motir sprints [--state <planned|active|complete>] [--json]
69
+ motir sprint [ref] [--kinds <list>] [--json] # defaults to the ACTIVE sprint
70
+ motir show <key> [--json] # one item, in full
71
+ motir open <key> [--print] # …in the browser
72
+ ```
73
+
74
+ `motir sprint`'s **`ref` resolves in order**: omitted → the active sprint; then a
75
+ sprint **id**; then an **exact name**; then a **name prefix** — each
76
+ case-insensitive, first rule yielding exactly one sprint wins. An ambiguous
77
+ prefix is an error that names the candidates rather than picking the first;
78
+ `motir sprints` (which flags the active sprint with `*`) is how you find the name
79
+ to type.
80
+
81
+ **Dependency edges render in two shapes**, chosen by the shape of the set:
82
+
83
+ - **Columns** on `ready` and `sprint` — `BLOCKS` on `ready` (a ready item has no
84
+ open blockers by definition, so the other direction would be a dead column),
85
+ `BLOCKED BY` **+** `BLOCKS` on `sprint`, which holds mixed-status work. A cell
86
+ prints at most **three** keys and collapses the rest to `+n` so it can never
87
+ wrap and break the table; `✓` marks a blocker already `done`/`cancelled` (it no
88
+ longer gates); an item with no edges renders **blank**, never `0`. **`--json`
89
+ always carries the full untruncated `dependencies` block** — the truncation is
90
+ display-only.
91
+ - **A `WAVE` build order** on `show`'s children, because one parent's children
92
+ are a closed DAG. Wave 1 is the independently-buildable set; later waves are
93
+ gated by earlier ones; `BLOCKED BY` names the edges. `↗` marks a blocker
94
+ outside the parent (named, but it forms no wave — nothing in the table can
95
+ clear it). Children in a dependency **cycle** get `—` for a wave and a
96
+ `⚠ dependency CYCLE` line: a planning bug to fix in the tree, not a CLI
97
+ failure, so `show` still exits **0** (`wave: null` in `--json`).
98
+
99
+ The reason the two differ, in one sentence: a ready set or a sprint spans many
100
+ parents so its edges are disconnected fragments, whereas a story's children are
101
+ one closed dependency graph. Full prose, with worked examples of both, lives in
102
+ [`docs/cli.md`](../../docs/cli.md#dependencies-in-the-terminal--two-renderings).
103
+
104
+ ## Dispatch — `motir next` / `motir run` / `motir done` (7.9.3)
105
+
106
+ ```sh
107
+ motir next [--kinds <list>] [--print | --agent <cmd>] [--reset]
108
+ motir run <key> [--print | --agent <cmd>] [--force]
109
+ motir done <key> [--via <status>]
110
+ motir done --session <branch>
111
+ ```
112
+
113
+ `next` picks the next ready item; `run` takes an explicit key. Both then follow
114
+ the same pipeline:
115
+
116
+ ```
117
+ select → transition_status(in_progress) → dispatch_prompt → deliver
118
+ ```
119
+
120
+ **The prompt is generated SERVER-SIDE** (`dispatch_prompt`, MOTIR-1802) and
121
+ printed byte-identical. The CLI never assembles prompt text, so every harness —
122
+ Claude Code, Codex, opencode, or a human reading it — receives the same
123
+ instruction and the grammar versions with the product.
124
+
125
+ **Two delivery modes.** `--print` (the default, and what you get whenever no
126
+ agent is configured) writes the prompt to **stdout** and everything else to
127
+ **stderr**, so `motir next --print | pbcopy` copies the prompt alone while you
128
+ still see the target repo and resolved path on screen. `--agent "<cmd>"` — or
129
+ `MOTIR_AGENT`, or `agentCommand` in the user config, in that precedence — runs
130
+ your agent on it, handing the prompt over **both** stdin **and**
131
+ `$MOTIR_PROMPT_FILE` (a 0600 temp file), streaming its output through live.
132
+
133
+ **Repo routing.** The dispatch payload names the item's repo (`targetRepo`);
134
+ the CLI maps that name to a checkout via the `.motir.json` override map or the
135
+ `<root>/<repoName>` convention, and runs the agent there — so dispatching a
136
+ `motir-ai` item while standing in `motir-core` just works. If the checkout is
137
+ **missing**, the agent runs at the workspace root so the prompt's GIT WORKFLOW
138
+ can create it, and the CLI verifies afterwards (reporting a suspect dispatch
139
+ with a `motir link add` hint if it did not appear). An item is **never** run in
140
+ some other existing checkout — an unpinned item falls back to the root.
141
+
142
+ **After the agent exits.** Exit 0 lands the item at **In Review**: a per-item-PR
143
+ item via `transition_status`, a session-lineage item via `mark_integrated` on
144
+ its inherited branch. A non-zero exit leaves the item **In Progress** (work was
145
+ started — nothing is reverted), propagates the agent's exit code, and records
146
+ the item in the session exclude list so the next `motir next` moves past it;
147
+ `--reset` clears that list.
148
+
149
+ > The exclude list is CLI **state**, not a credential, so it lives in the state
150
+ > home — `MOTIR_STATE_HOME` → `MOTIR_CONFIG_HOME` → `XDG_STATE_HOME` →
151
+ > `~/.local/state/motir` — and never beside the PAT, which the sandbox image
152
+ > mounts read-only (MOTIR-1836). It is a convenience, not a correctness
153
+ > mechanism: a failed item is already held out of the ready set by its
154
+ > `in_progress` status, so if the store cannot be written the CLI warns once and
155
+ > the run continues rather than aborting.
156
+
157
+ **Closing out.** After you merge, `motir done <key>`. The default workflow has
158
+ no direct `in_progress → done` edge, so an item dispatched with `--print` (which
159
+ never observed an agent finish) needs `motir done --via in_review <key>`; an
160
+ illegal flip surfaces the server's own allowed-targets error verbatim. A merged
161
+ **session** PR closes out in bulk with `motir done --session <branch>`.
162
+
163
+ ## The loop — `motir auto` (7.9.4)
164
+
165
+ ```sh
166
+ motir auto --agent "<cmd>" [--kinds <list>] [--max <n>] [--keep-going] [--reset] \
167
+ [--include-planning]
168
+ ```
169
+
170
+ Drain the ready set unattended. Each iteration asks the server for **exactly one**
171
+ item and runs it through the same single-dispatch pipeline as `motir next`, then
172
+ loops; the run ends when the server has nothing ready left. It never fetches the
173
+ ready list up front — the set CHANGES while the run executes, because integrating
174
+ one item unlocks its dependents, so the loop **cascades through the dependency
175
+ graph** rather than draining a snapshot. An agent is required (`--agent`,
176
+ `MOTIR_AGENT`, or the configured `agentCommand`); `--print` has nobody to paste
177
+ for and is refused with guidance.
178
+
179
+ **One session branch, one pull request.** The run opens `motir/auto-<run-id>` on
180
+ `origin` in each repo it dispatches into — lazily, on that repo's first item, and
181
+ without touching your working tree or creating a local branch. Every item's work
182
+ is integrated onto it and recorded with `mark_integrated`, which moves the item to
183
+ **In Review** (never Done — `main` does not have the work yet) and is also what
184
+ makes its dependents ready mid-run. At the end the CLI surfaces **one pull request
185
+ per repo**: the run's single human review gate. **`main` is never auto-advanced**,
186
+ by the CLI or by the prompt.
187
+
188
+ After you merge that pull request, close the whole run out with
189
+ `motir done --session motir/auto-<run-id>`. A rejected pull request leaves the
190
+ items honestly In Review — the summary names every one of them with its branch.
191
+
192
+ **What it skips, and why.** An unexpanded epic/story in the ready set is a
193
+ _planning_ item, not a dispatchable one — there is no agent prompt for "do the
194
+ planning" — so the loop skips it untouched and lists it under _needs planning_.
195
+ A `type: manual` / `executor: human` item is skipped the same way, under _needs a
196
+ human_.
197
+
198
+ ### `--include-planning` — fire the expansion instead of skipping it (7.9.8)
199
+
200
+ With the flag, an unexpanded epic/story is not skipped: the CLI submits an AI
201
+ expansion for it (the `expand_item` tool), records it, and asks for the next
202
+ ready item **immediately**.
203
+
204
+ **It triggers; it never waits.** The tool returns `{ jobId, planId }` the moment
205
+ the job is accepted, and the loop moves straight on. There is no backoff and no
206
+ poll — see the next paragraph for why waiting would be waiting on a person.
207
+
208
+ **A triggered expansion produces PROPOSALS, not work.** The job writes a plan of
209
+ proposals; **approving that plan in Motir is the only thing that turns a proposal
210
+ into a work item**, so firing it adds nothing to your tree and the epic/story
211
+ stays childless. That is exactly why the item goes on the run's exclude list —
212
+ otherwise the very next `next_ready` would hand it straight back. If you happen
213
+ to approve a plan while the run is still going, its new subtasks surface on a
214
+ later iteration like any other unlock, because the loop never pre-fetches; that
215
+ is a bonus, not something it waits for.
216
+
217
+ **End of loop.** When `next_ready` comes back empty the run ends, pending
218
+ expansions or not. The summary names each one with its plan id and a link to
219
+ review it, under _planning triggered — awaiting your approval_.
220
+
221
+ **When an expansion fails** (no credits, a refused target, a typed error) it is
222
+ **non-halting**, unlike a failed agent: nothing in the run depended on it, so the
223
+ item is named under _planning failed — still unexpanded_ and the loop continues
224
+ to the next ready item. Neither a triggered nor a failed expansion changes the
225
+ exit code, which stays a function of dispatch outcomes alone.
226
+
227
+ Expansions run on the **AI credits of the token owner**, which is why this is
228
+ opt-in rather than the default. Motir also ships a server-side equivalent — the
229
+ auto-plan cadence cron fires the same expansion for a project that opted in when
230
+ its ready set drains; this flag is the terminal-side version for a project that
231
+ has not.
232
+
233
+ **When an agent fails** the run halts by default and the item is left In Progress
234
+ with nothing reverted; `--keep-going` finishes the rest instead. Either way the
235
+ end-of-run push and pull request still happen, so a late failure never abandons
236
+ the work that already integrated. `--max <n>` caps the dispatches. Ctrl-C stops
237
+ between items (or terminates the agent mid-run), then still lands the pull
238
+ request and prints the summary.
239
+
240
+ ## The snapshot — `motir batch` (7.9.10)
241
+
242
+ ```sh
243
+ motir batch --agent "<cmd>" [--kinds <list>] [--max <n>] [--keep-going] [--reset]
244
+ ```
245
+
246
+ The snapshot complement of `motir auto`. Where `auto` follows the ready set as it
247
+ changes, `batch` **freezes it once** — it reads the whole ready set up front,
248
+ **prints the exact list it will implement**, and then runs those items one at a
249
+ time. An item that becomes ready during the run is **not** picked up; the summary
250
+ counts and names it, so you can re-run `batch` or reach for `auto`. Printing the
251
+ plan before the first agent starts is the point: you can Ctrl-C out of a run you
252
+ did not want while nothing has been touched.
253
+
254
+ **One pull request per item, and no session branch anywhere.** A strictly
255
+ main-ready snapshot is mutually independent by construction — an item is in it
256
+ only when every dependency is done **on main**, so no snapshot item can depend on
257
+ another (it would not have been ready). Each item therefore rides the `motir next`
258
+ per-item flow unchanged: its own branch off `origin/main` in its own repo, its own
259
+ pull request targeting `main`, and **In Review** once the agent's pull request is
260
+ open. Close each one out with `motir done <key>` after you merge it. Two snapshot
261
+ items in the same repo simply open two independent pull requests; `mark_integrated`
262
+ is never called and no session branch is created.
263
+
264
+ **Strict main-readiness.** An item that is ready **only** because a dependency is
265
+ integrated-awaiting-review (the 7.8.11 rule) is excluded and named: that
266
+ dependency's code is not on `main`, so a pull request of its own against `main`
267
+ could not even build. That lineage is `auto`'s territory. Unexpanded epics/stories
268
+ (_needs planning_) and `type: manual` / `executor: human` items (_needs a human_)
269
+ are excluded by the same rule `auto` uses. There is no `--include-planning` here —
270
+ an expansion's output could never join a frozen snapshot, so that flag stays
271
+ `auto`-only.
272
+
273
+ **Shared loop mechanics.** An agent is required; a failed agent halts the run by
274
+ default (the item stays In Progress, nothing reverted) and `--keep-going` finishes
275
+ the rest of the snapshot; `--kinds` and `--max` narrow it; Ctrl-C stops between
276
+ items and prints the summary so far, with any pull requests the agents already
277
+ opened standing. Snapshot items the run never reached are named so you know what a
278
+ re-run will pick up.
279
+
280
+ ### `auto` vs `batch`
281
+
282
+ | | `motir auto` | `motir batch` |
283
+ | --------------------- | ------------------------------------------------------ | -------------------------------------------------------- |
284
+ | Work list | **Live** — one `next_ready` per iteration | **Frozen** — the ready set snapshotted once, up front |
285
+ | Becomes ready mid-run | Picked up (the loop cascades the dependency graph) | **Not** picked up — counted and named in the summary |
286
+ | Git lineage | ONE session branch per repo, `motir/auto-<run-id>` | **None** — each item branches off `origin/main` |
287
+ | Pull requests | ONE per repo, opened by the CLI at the end | **One per item**, opened by the agent |
288
+ | Item close-out | `motir done --session <branch>` (bulk) | `motir done <key>` (per item) |
289
+ | Integrated-dep items | Dispatched — that lineage is the whole point | **Excluded** — their code is not on `main` |
290
+ | Unexpanded containers | Skipped, or expanded with `--include-planning` | Skipped — no `--include-planning` |
291
+ | Reach for it when | You want a long unattended run to go as deep as it can | You want a bounded, reviewable batch of independent work |
292
+
293
+ Both leave `main` untouched: it moves only through a pull request a human merges.
294
+
295
+ ## Planning — `motir plan` (7.9.9)
296
+
297
+ ```sh
298
+ motir plan # resume the PROJECT-WIDE planning conversation
299
+ motir plan MOTIR-42 [MOTIR-9] # resume the conversation ANCHORED at those items
300
+ motir plan "<what to change>" # append one turn and submit it
301
+ motir plan MOTIR-42 "<text>" --detach
302
+ ```
303
+
304
+ Where `next` / `auto` / `batch` implement the plan, `plan` **changes** it — and it
305
+ does that the way Motir actually models planning: as a **conversation**, not a
306
+ one-shot description.
307
+
308
+ **One thread, two surfaces.** A planning conversation is a persisted, resumable
309
+ thread — one per project per anchor set — and this command is a CLIENT of it, never
310
+ the owner of a parallel one. It addresses the thread by SCOPE (the project, plus any
311
+ anchor keys), which is the thread's identity, so **a turn typed in the terminal
312
+ appears in the web planning workspace and vice versa**. `motir plan` opens it,
313
+ prints every turn already on it, and then reads more.
314
+
315
+ **Appending is not submitting.** Turns ACCUMULATE. `/submit` sends them as ONE
316
+ change, framed so later turns REFINE earlier ones — "add auth to the billing epic"
317
+ then "keep them under 3 points" go out together. `/exit` leaves; **quitting can
318
+ never lose a turn**, because each one is server-side the moment you press Enter.
319
+ Ctrl-D reads as `/exit`, never as a submit.
320
+
321
+ **What comes back is PROPOSALS.** After a submit the command waits for the planner
322
+ (bounded — 10 minutes, and `--detach` skips the wait entirely), then prints the
323
+ proposed tree: indented under each proposal's proposed parent, with an op marker
324
+ (`+` add / `~` modify / `-` remove), kind/type and leaf sizing. Nothing there
325
+ exists in your tree: **approving the plan in Motir is the only thing that creates a
326
+ work item**, so the output says so and never reports "created N items". Then it
327
+ offers the loop's real next step — add another turn to refine, or open the review
328
+ URL to approve.
329
+
330
+ **Leading `KEY` arguments anchor; the rest is text.** `motir plan MOTIR-42 size
331
+ these` opens the thread anchored at MOTIR-42 and appends "size these". A different
332
+ key set is a different thread. The CLI never picks the job kind — motir-ai
333
+ classifies expand / augment / re-plan from the thread's own scope.
334
+
335
+ **A fresh project is refused, with a pointer.** The submit path reaches
336
+ `augment` / contextual, never generation: generating a first plan is the onboarding
337
+ discovery interview's job, a guided web flow. So a project with no work items is
338
+ detected **before any turn is appended** and the command exits with the
339
+ `/onboarding` URL, having appended and submitted nothing.
340
+
341
+ **Unattended.** The quoted-text form is the scriptable one; add `--detach` to get
342
+ the job/plan ids and the review URL without waiting. A non-TTY invocation with no
343
+ text is an **error naming that shorthand** — never a prompt that hangs.
344
+
345
+ ## Help + topics
346
+
347
+ `motir`, `motir help` and `motir --help` all print the same curated overview on
348
+ **stdout** and exit **0** — the front door a newcomer reads top to bottom:
349
+
350
+ ```
351
+ Usage: motir [options] [command]
352
+
353
+ SETUP COMMANDS: auth · link · doctor
354
+ READ COMMANDS: ready · status · sprints · sprint · show · open
355
+ WORK LOOP COMMANDS: next · run · auto · batch · plan · done
356
+ HELP TOPICS: help · environment · files
357
+ FLAGS: -v, --version · -h, --help
358
+ EXAMPLES: …
359
+ LEARN MORE: …
360
+ ```
361
+
362
+ Commands are grouped with commander's native `.helpGroup()` (see `src/help.ts`);
363
+ group ORDER is the registration order in `src/program.ts`. **A command
364
+ registered without an explicit group falls into `ADDITIONAL COMMANDS`**, so a
365
+ later subtask adds a command — and, with one `.helpGroup(...)` line, files it
366
+ under `SETUP` / `READ` / `WORK LOOP COMMANDS` — without ever rewriting the help
367
+ surface. (7.9.3 was the first to exercise that: `next` / `run` / `done` joined
368
+ the reserved work-loop group with one line each; `auto` / `batch` / `plan` then
369
+ did the same, and `sprints` / `sprint` / `show` joined `READ COMMANDS` the same
370
+ way.)
371
+
372
+ Two **topics** answer what a command list cannot:
373
+
374
+ ```sh
375
+ motir help environment # the 6 env vars Motir reads, and what each overrides
376
+ motir help files # ~/.config/motir/config.json + .motir.json
377
+ ```
378
+
379
+ Per-command help is unchanged (`motir help <command>`, `motir <command> --help`,
380
+ nested: `motir help auth login`, `motir link add --help`). An unknown topic
381
+ fails like every other CLI error — one line plus a hint on **stderr**, exit
382
+ **1**, no stack trace — so `motir help | head` stays clean for piping.
383
+
384
+ ## `motir doctor` — the BYOK preflight
385
+
386
+ Motir is **BYOK**: you bring your own coding agent and your own model key. So
387
+ before `motir next` / `motir auto`, `motir doctor` answers "is my setup
388
+ correct?" in one read-only pass instead of letting a missing agent or an
389
+ unsigned-in key surface halfway through a dispatch:
390
+
391
+ | Check | PASS means | FAIL means |
392
+ | ----------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------- |
393
+ | **Auth** | the stored PAT connects, lists tools, and identifies you | no token, or it is invalid/expired → `motir auth login` |
394
+ | **Project link** | `.motir.json` resolved (walking up from the cwd) | no link here or above → `motir link` |
395
+ | **Workspace + project** | the linked project is reachable _for this token_ | wrong key, or your user is not a member |
396
+ | **Repo checkouts** | every override path resolves | — (a not-yet-cloned **convention** path is fine, and only WARNs for an override) |
397
+ | **Coding agent** | the binary is on PATH and answers `--version` | it is not on PATH → the profile's install source |
398
+ | **Agent credential** | the agent's credential exists, or its key env var is set | neither → where to sign in |
399
+
400
+ It exits **non-zero** when any hard check fails, so `motir doctor && motir auto`
401
+ is a usable gate. `--json` emits the same report machine-readably. WARN rows
402
+ never fail the run — "no agent configured" is a warning, because `motir next
403
+ --print` hands you the prompt for an agent Motir never launches.
404
+
405
+ **Which agent gets checked**, in priority order: `--agent <cmd>` → the
406
+ `MOTIR_AGENT` env var → `agentCommand` in the user config. The value is a full
407
+ command line (`claude --dangerously-skip-permissions`); the first token is the
408
+ binary Motir looks for.
409
+
410
+ **It never reads your secret.** The credential check asks only whether a path
411
+ EXISTS or an env var is SET — the engine has no way to obtain a file's contents
412
+ or an env var's value, so nothing sensitive can be printed or logged. Nor does
413
+ `doctor` write anything: its only server calls are the handshake, `whoami`, and
414
+ a one-row search that proves the project is reachable.
415
+
416
+ **Known agent profiles** (the sandbox matrix, 7.9.7b). A profile is matched by
417
+ **every** binary its installer links, so the name you actually type resolves it —
418
+ Cursor's executable is `agent`, with `cursor-agent` as the legacy alias. Where a
419
+ credential can be checked honestly, `doctor` checks it: `claude` (`~/.claude`),
420
+ `codex` (`~/.codex`), `opencode` (`~/.local/share/opencode/auth.json` — the XDG
421
+ **data** dir; `~/.config/opencode` holds configuration, not the credential),
422
+ `kimi` (`~/.kimi-code`), `cursor` (`CURSOR_API_KEY`), `aider`
423
+ (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`). `antigravity` and `goose` keep their
424
+ secret in the OS **keyring**, with no portable file to look for, so they report a
425
+ WARN ("verify this one yourself") rather than a false FAIL — and no profile
426
+ tests a path that merely proves the agent is _installed_. Any other agent works
427
+ too (`--agent "<cmd> <flag>"`): its binary is still checked, its credential is
428
+ yours to confirm.
429
+
430
+ ### Config files
431
+
432
+ - **`~/.config/motir/config.json`** (XDG-respecting; override with
433
+ `MOTIR_CONFIG_HOME`) — the credential store, `chmod 600`. The PAT lives here
434
+ and **only** here. Keyed by server URL so one machine can hold tokens for
435
+ several servers.
436
+ - **`.motir.json`** at the workspace root — the project link: `{ serverUrl,
437
+ workspace, project, repos? }`. Contains **no secret**, so it is safe to
438
+ commit. Repo checkouts resolve by **convention** (`<root>/<repoName>`); the
439
+ optional `repos` map carries overrides only. Commands resolve `.motir.json`
440
+ by walking **upward** from the cwd, so any command works from inside any
441
+ checkout under the root. An **empty folder is first-class** — bind and go; the
442
+ first scaffold work items create the checkouts.
443
+
444
+ ## Tests
445
+
446
+ Two lanes, because they need different things (7.9.5):
447
+
448
+ ```sh
449
+ pnpm --filter @motir/cli test # the package suite — no Postgres, no Next app
450
+ pnpm --filter @motir/cli test:coverage # the same, with the per-file ≥90% gate
451
+ pnpm vitest run tests/cli/ # the story suite — the BUILT binary, real DB
452
+ ```
453
+
454
+ **The package suite** (`packages/cli/test/**`) covers each module in isolation.
455
+ Where a module talks to a server it is pointed at a REAL MCP server the test
456
+ starts in-process (`test/helpers/mcpTestServer.ts`) with canned tool results —
457
+ the transport, framing and error envelopes are the real ones, only the data is
458
+ scripted. `test/auto.test.ts` drives the whole `motir auto` loop against a
459
+ scripted server, agent and git, because its load-bearing properties — that it
460
+ re-queries every iteration, that an integrated item unlocks its dependents
461
+ mid-run, that `main` is never advanced, and that the pull request opens even when
462
+ the run ended badly — are not visible in any single function.
463
+
464
+ **The story suite** (`tests/cli/cli-story.test.ts`, in the root Vitest lane where
465
+ Postgres is) spawns the BUILT binary as a child process against the real
466
+ `/api/mcp` route, with a scripted fake agent and a recorded fake `gh`. It is what
467
+ proves the assembled tool works: repo routing into real checkouts, the
468
+ session-branch cascade, one pull request per touched repo, a `main` nobody
469
+ advanced, and the help surface of the binary a user actually runs.
470
+
471
+ **The gate.** `vitest.config.ts` holds per-FILE ≥90% branch/function/line
472
+ thresholds for the client core, the command modules and the pure decision layers.
473
+ Three files are ungated on purpose — `index.ts`, `program.ts` and `prompts.ts` —
474
+ each for a reason written down beside its threshold entry, and `motir batch`'s
475
+ two modules are a KNOWN GAP named in the same file (MOTIR-1829). CI runs the package
476
+ suite + gate as its own job (`CLI package`), the same shape `@motir/design-system`
477
+ uses; the story suite rides the sharded Vitest job.
478
+
479
+ ## Releasing
480
+
481
+ Releases publish to the **public npm registry** via CI — the
482
+ `.github/workflows/release-cli.yml` workflow, triggered by a package-scoped git
483
+ tag. There is no manual `npm publish` in the normal path.
484
+
485
+ 1. **Bump the version** in `packages/cli/package.json` (semver: the public
486
+ surface is the `motir` command set — its subcommands, flags and output
487
+ contracts — so removing or renaming one is a **major** bump).
488
+ 2. **Open + merge** the PR with the bump.
489
+ 3. **Tag and push** — the tag version MUST equal the `package.json` version (the
490
+ workflow guards this and fails fast otherwise):
491
+
492
+ ```bash
493
+ git tag cli-v<x.y.z> # e.g. cli-v0.1.0
494
+ git push origin cli-v<x.y.z>
495
+ ```
496
+
497
+ 4. The workflow builds, runs the package suite + coverage gate, verifies the
498
+ tarball (`dist/**` only), and publishes `@motir/cli@<x.y.z>` with public
499
+ access and npm provenance. Re-running an already-published version is a no-op
500
+ (idempotent), not a failure.
501
+
502
+ **One tag, two lanes.** `cli-v*` also fires
503
+ [`release-sandbox.yml`](../../.github/workflows/release-sandbox.yml), which
504
+ publishes the [sandbox images](./sandbox/README.md). That is deliberate: the
505
+ `motir` binary on npm and the one baked into the images are the same version by
506
+ construction, and both lanes guard the tag against this `package.json`.
507
+
508
+ **Dry run:** trigger the workflow from the Actions tab via **Run workflow**
509
+ (`workflow_dispatch`) with **dry run** checked — it builds, tests and packs the
510
+ tarball as a downloadable artifact without publishing.
511
+
512
+ **Auth:** the publish step uses **OIDC Trusted Publishing** — no `NPM_TOKEN`
513
+ secret. GitHub mints a short-lived id-token (`id-token: write`) that npm
514
+ exchanges for a one-time publish credential, gated by a Trusted Publisher
515
+ configured on npmjs.com for this package (org `moooon-B-V` / repo `motir-core` /
516
+ this workflow filename). Provenance is still requested explicitly with
517
+ `--provenance`, which needs `id-token: write` plus the `repository` field in
518
+ `package.json`.
519
+
520
+ > **Why the lane moved off `NPM_TOKEN` (MOTIR-1890).** It originally published
521
+ > with that long-lived secret (MOTIR-662). The `cli-v0.1.0` tag was its first
522
+ > CI use, and it failed:
523
+ >
524
+ > ```
525
+ > npm error code E404
526
+ > npm error 404 Not Found - PUT https://registry.npmjs.org/@motir%2fcli
527
+ > ```
528
+ >
529
+ > **A 404 on `PUT` for a scoped package that exists is npm's masked auth
530
+ > failure**, not a missing package — npm answers 404 instead of 401/403 so an
531
+ > unauthorized caller cannot probe which private scoped packages exist. Read it
532
+ > as "the credential is wrong", never as "the package is gone". Note that the
533
+ > `dry_run` path cannot catch this: it packs but skips the publish, so it
534
+ > rehearses everything except auth. OIDC removes the credential that could
535
+ > expire in the first place.