@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.
- package/README.md +532 -5
- package/dist/index.js +3770 -0
- package/package.json +39 -6
package/README.md
CHANGED
|
@@ -1,8 +1,535 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@motir/cli` — the `motir` command-line tool
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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.
|