@motir/cli 0.0.1 → 0.1.1

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