cawdev-cli 0.9.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.
@@ -0,0 +1,930 @@
1
+ # The runner daemon
2
+
3
+ The piece that lives on your machine. It holds the repositories and the Claude
4
+ Code login; **the platform holds neither.**
5
+
6
+ It connects **outbound** and polls, so there is no inbound port and NAT and
7
+ firewalls are not anybody's problem. See R19 in the roadmap for the server-side
8
+ alternative that was declined, and why.
9
+
10
+ Plain Node, zero dependencies. You are about to let it spawn agent sessions
11
+ against your working copies — it should be a file you can read first.
12
+
13
+ ## Setting it up
14
+
15
+ Write the config — what this machine serves, and where:
16
+
17
+ ```json
18
+ {
19
+ "url": "http://localhost:4200",
20
+ "name": "my-laptop",
21
+ "projects": {
22
+ "cawdev": "/Users/you/code/cawdev",
23
+ "dycrypt": "/Users/you/code/dycrypt"
24
+ }
25
+ }
26
+ ```
27
+
28
+ Then run `cawdev` (below). Finding no token it can use, it signs you in through
29
+ your browser and mints a `runner:operate` one scoped to exactly those projects,
30
+ storing it in `~/.cawdev/token.json`, mode 0600. **Nothing is typed or pasted**,
31
+ and you can still revoke it in the console under Agent tokens, which stops the
32
+ machine.
33
+
34
+ There is deliberately no `"token"` in the config above. This file names working
35
+ copies and permissions, so it is the kind people keep beside their code and
36
+ commit — `macbook-laptop.json` in this directory is tracked, and
37
+ `configs.test.mjs` fails if one of them ever grows a credential. A token in a
38
+ config is still *read*, because R93's generated `~/.cawdev/runner.config.json`
39
+ is that shape and lives under a home directory.
40
+
41
+ Driving the daemon directly, without the `cawdev` command, means supplying the
42
+ credential yourself — it does not open browsers:
43
+
44
+ ```sh
45
+ CAWDEV_TOKEN=cawd_… node tools/runner/runner.mjs --config runner.config.json
46
+ ```
47
+
48
+ `readConfig` takes the token from `CAWDEV_TOKEN`, then the config, then
49
+ `~/.cawdev/token.json` — the store last, because it is the one nobody typed.
50
+
51
+ The name is how you will recognise it in the console's runner picker.
52
+ Registering is idempotent by (owner, name), so restarting the daemon is the same
53
+ runner rather than a third entry in the list.
54
+
55
+ ## `cawdev` — the terminal, in one word
56
+
57
+ ```sh
58
+ cawdev
59
+ ```
60
+
61
+ That is the whole of it. Install the command once — `npm i -g ./tools`, or
62
+ `npm link` from `tools/` while you are working on it — and typing `cawdev` gets
63
+ you a working machine: it looks for a daemon here, **starts one if it finds
64
+ none**, and drops you into the UI.
65
+
66
+ Signing in happens **in your browser**. The first launch opens cawdev's sign-in
67
+ page and waits for you to approve a code; after that it is remembered, and
68
+ `/login` does it again on demand. No password is ever typed into the terminal —
69
+ that belongs on a page your browser has told you the origin of.
70
+
71
+ ```
72
+ cawdev the runner here, starting one if there is none
73
+ cawdev --runner <name> when this machine runs more than one
74
+ cawdev --url <url> which cawdev to sign in to (or CAWDEV_URL)
75
+ cawdev --config <path> the runner config to start a daemon from
76
+ cawdev --no-start attach only; never launch a daemon
77
+ cawdev --watch-only do not sign in; watch without being able to act
78
+ cawdev --leave-running leave the daemon running when you quit
79
+ ```
80
+
81
+ **Quitting stops the daemon** — R123. One word starts the machine and the
82
+ window; one key ends both, and it asks twice while sessions are running because
83
+ they go with it. A background process you did not know you started is the cost
84
+ of one word doing all this, and the earlier answer to that — naming the `kill`
85
+ on the way out — left the chore with the person rather than doing it.
86
+
87
+ `cawdev --leave-running` is the old behaviour for a machine that should keep
88
+ claiming work after the window closes; there the goodbye names the runner and
89
+ the command that stops it.
90
+
91
+ The other two ways in still work and are not deprecated. `node runner.mjs
92
+ --config macbook-laptop.json --attach` runs the daemon and the UI in **one
93
+ process and one terminal**, which is what you want when the daemon should die
94
+ with the window — there `q` stops it, and asks first when sessions are live.
95
+ `node runner.mjs attach` joins a daemon started elsewhere.
96
+
97
+ ### What it shows you
98
+
99
+ The console shows you a session. This shows you **the machine** — and the
100
+ difference is the runs that are *not* moving. The daemon knows why the fifth run
101
+ is waiting ("cawdev already has a run here", "at 4 sessions"); nothing else
102
+ does, and that reason exists nowhere but here.
103
+
104
+ **The transcript is in your terminal's own scrollback.** Session output is
105
+ printed rather than painted, so the wheel, `shift+PgUp`, your terminal's search
106
+ and its copy all work exactly as they always have, and scrolling up reaches the
107
+ start of the session. The only thing pinned is the footer at the bottom, and it
108
+ never scrolls away: which cawdev, who you are signed in as, this machine's
109
+ runner, the per-project session counts against their checkouts and the machine's
110
+ total against `maxSessions` — plus the run you are watching and the keys that
111
+ work right now. Anything *stopping* a session sits above all of that, because it
112
+ is the only thing in there waiting on a person.
113
+
114
+ `L` lists the runs as an overlay: every one this machine is driving, claiming or
115
+ leaving queued, with the reason each waiting one waits. Arrows move, `enter`
116
+ opens that run — laying its history into the scrollback so you are not staring
117
+ at a blank terminal — and `esc` leaves without changing anything.
118
+
119
+ A **status line** sits with the footer while something is running: which run, how
120
+ long it has been going, and the key that stops it. It is absent when nothing is.
121
+
122
+ | Key | |
123
+ |---|---|
124
+ | `enter`, `i` | prompt the session you are watching |
125
+ | `/` | a command — the list filters as you type |
126
+ | `L` | the run list; arrows, `enter` to open, `esc` to leave |
127
+ | `1`–`9` | jump straight to a run |
128
+ | `a` | answer the question it stopped on — if it is yours (R58) |
129
+ | `y` / `s` / `n` | a permission request: allow once / for the rest of this run / refuse (R51, R60) |
130
+ | `Y` | allow always, here — writes a project rule |
131
+ | `x`, twice | cancel the session |
132
+ | `g` | print the daemon's own log instead of the transcript |
133
+ | `q` | stop the runner and leave (`--leave-running` keeps it up) |
134
+ | `esc` | close whatever is open, without ending the session |
135
+ | `ctrl+c`, twice | the same, and then leave |
136
+
137
+ In any list: arrows move, `1`–`9` pick straight away, `enter` chooses, `esc`
138
+ leaves. While typing: `↑`/`↓` walk your history, `tab` completes, and the arrows
139
+ move through the command list while one is open.
140
+
141
+ | Command | |
142
+ |---|---|
143
+ | `/help` | the list |
144
+ | `/login` | sign in through the browser |
145
+ | `/logout` | forget the stored session on this machine |
146
+ | `/runs` | the run list — the same as `L` |
147
+ | `/cancel` | cancel the session you are watching |
148
+ | `/log` | the daemon's own log, on or off |
149
+ | `/quit` | leave |
150
+
151
+ **Typing `/` filters that list as you go**, each row with its description; arrows
152
+ and `enter` pick one and `tab` completes as far as the matches agree. Guessing a
153
+ command name and being told `no such command` is a step, and it is the step this
154
+ removes.
155
+
156
+ **`↑` recalls what you last sent**, kept in `~/.cawdev/history.json` at mode
157
+ `0600` and keyed by URL like the session beside it — so it survives quitting.
158
+ `/logout` forgets it along with the session.
159
+
160
+ **A paste stays one line.** Paste four hundred lines and the input shows
161
+ `[pasted, 342 lines]`; all of it is sent. A paste that scrolled the transcript
162
+ away would bury the thing this program exists to keep.
163
+
164
+ Under `NO_COLOR` it is the same terminal without the colour: the picker still
165
+ moves, and the `❯`, the numbers and the words carry what the colour did — colour
166
+ and cursor are two different questions. Through a pipe or on a dumb terminal
167
+ there is no cursor at all, so a picker becomes a **numbered list read from
168
+ stdin** — type the number, or for a question type the answer itself. The fixed
169
+ answers are printed when they change and the escape codes are stripped; a log
170
+ file full of `ESC[32m` is not legible, whatever else it is.
171
+
172
+ ### Answering is picking, not retyping
173
+
174
+ The agent has usually already worked out the two or three answers it can act on
175
+ — `ask_user` has carried `options` since R10 — and until R83 this was the one
176
+ surface that threw them away and asked you to retype one of them, spelled
177
+ correctly.
178
+
179
+ Now a question with options arrives as a list: the question prints into the
180
+ transcript, the options become a live selection, arrows or a digit choose one,
181
+ and that is the answer. A question with no options goes straight to the line, as
182
+ it always did.
183
+
184
+ **The last row is always "write my own answer"**, and it opens a real line to
185
+ type on with the question still on screen. The options are the agent's *guess* at
186
+ the shape of the decision, and the whole value of asking a person is that they
187
+ can say the thing that was not on the list — so getting there costs one key, and
188
+ `esc` from it comes back to the list rather than abandoning the answer. A chosen
189
+ option and a typed sentence resolve the same question the same way, through the
190
+ same endpoint the inbox and the run page post to: one record, the same
191
+ `answeredBy`, and the badge clears at once.
192
+
193
+ A permission request is the same widget — R60's lengths of yes as rows, with the
194
+ tool and its arguments printed above them so you are deciding about something you
195
+ can read. The single keys keep working for anybody who has learned them.
196
+
197
+ R58 is unchanged by any of this: a question that is not yours is shown with the
198
+ name of the person it is waiting on, and no picker is offered.
199
+
200
+ ### A question on this machine is not necessarily yours
201
+
202
+ Since R58 a question belongs to the person who **started the run**, and only
203
+ they — or somebody they hand it to — may answer it. The banner has two shapes
204
+ because of that: `a answer` when it is yours, and `waiting on alice@…` when it
205
+ is not.
206
+
207
+ It shows you the question either way. This program exists to answer "why is that
208
+ run not moving", and on a machine serving a team the answer is often a name. What
209
+ it will not do is offer you a key that the platform would then refuse, because a
210
+ terminal that takes an answer and hands back a 403 reads as cawdev being broken
211
+ rather than as the question belonging to a colleague.
212
+
213
+ If the person it is waiting on cannot be reached, a project owner can take the
214
+ question over from the console — the run page has the button, and the takeover
215
+ is recorded on the question rather than appearing as an unexplained answer.
216
+
217
+ `i` is refused while a session is asking, and says so: a run blocked inside
218
+ `ask_user` cannot read a prompt, and the words typed into one queue behind the
219
+ answer they were meant to be. R78 put that refusal in the API, so this client,
220
+ the run page and the home composer cannot disagree about it.
221
+
222
+ ### Three lengths of yes
223
+
224
+ A permission request has R60's three answers here as well as in the console, as
225
+ three rows of a list and as three keys. `y` is this call; `s` is the rest of this
226
+ run and no longer; `Y` writes a project rule that outlives the session, the
227
+ person and the reason they said yes. Refusing asks why.
228
+
229
+ `s` names what it covers — `Bash(mvn *)` when the server could render a rule for
230
+ the command, and `every Bash` when it could not, because those are two different
231
+ promises and a banner that said the same words for both would be lying about one
232
+ of them. `Y` is offered only when there is a rule to write: a compound command
233
+ like `cd backend && ./mvnw test` cannot be settled by a pattern about its first
234
+ word, and a key that quietly became an allow-once would be worse than no key.
235
+
236
+ ### Watching is free; acting means signing in
237
+
238
+ Everything on the socket is readable without signing in — `--watch-only` skips
239
+ the browser entirely, and watching is the larger half of what this is for.
240
+
241
+ Anything that *changes* something goes to the platform over HTTP as you, not
242
+ through the daemon. That is not fussiness: prompting a session, cancelling one,
243
+ answering a question and deciding a permission request all refuse an agent
244
+ token (R51), so a socket that could do them would either lend the daemon's own
245
+ credential to a guard built to prevent exactly that, or keep yours.
246
+
247
+ **Signing in is a browser round trip, and no password reaches this process.**
248
+ `cawdev` asks the platform for a code, opens R55's sign-in page, and waits.
249
+ You approve the code there; the page names the machine that asked, and the code
250
+ is on both screens so you can check they match. What comes back is *your*
251
+ session, stored in `~/.cawdev/session.json` at mode `0600` and keyed by URL — so
252
+ one machine can hold sessions for two different cawdevs without either
253
+ pretending to be the other.
254
+
255
+ Over ssh, where there is no browser to open, the URL is printed: carry it to a
256
+ browser anywhere and the terminal collects the session when you approve.
257
+
258
+ `/logout` forgets it. A stored session the platform no longer honours is not an
259
+ error — it is what an expired session looks like, and the answer is the same as
260
+ having none.
261
+
262
+ ### Where the socket is
263
+
264
+ `~/.cawdev/run/<runner name>.sock`, in a `0700` directory, removed when the
265
+ daemon stops. **Permission to read it is permission to read this machine's
266
+ transcripts** — which is why it is under your home directory and shows only this
267
+ machine's own work.
268
+
269
+ More than one daemon here? `cawdev --runner <name>`. One is chosen for you.
270
+
271
+ A named runner is never started for you: naming one is a claim that it is there,
272
+ and launching a *different* daemon under that name because the first was not
273
+ answering is not what was asked.
274
+
275
+ A killed daemon leaves its socket file behind, and a stale file is
276
+ indistinguishable from a live one until you try it — so `cawdev` connects before
277
+ it believes one, which is what stops it attaching to nothing instead of starting
278
+ a daemon. `CAWDEV_RUN_DIR` moves the directory, which is mostly for tests.
279
+
280
+ ## Workspaces: more than one run at a time
281
+
282
+ A project can offer several checkouts. A run takes one, and gives it back when
283
+ it ends.
284
+
285
+ ```json
286
+ {
287
+ "projects": {
288
+ "dycrypt": "/Users/you/code/dycrypt",
289
+ "cawdev": { "workspaces": ["/Users/you/code/cawdev-1", "/Users/you/code/cawdev-2"] }
290
+ }
291
+ }
292
+ ```
293
+
294
+ A bare path means one workspace, which is what every config meant before this
295
+ existed — nothing changes for a machine that serves one checkout per project.
296
+
297
+ How many coding runs go at once is `min(workspaces, maxSessions)`. A run that
298
+ waits now says **"no free workspace in cawdev (2 here, all busy)"** instead of
299
+ "that project already has a run here", which was a proxy for it.
300
+
301
+ **This number caps coding and nothing else** (R70). `ASK`, `ROADMAP`, `AUDIT`
302
+ and `SCOPE` runs take no workspace, so a project whose checkouts are all busy
303
+ still starts a question, an entry-writing session, an audit and a scoping
304
+ session at once. `INTERVIEW`, `MERGE`
305
+ and `RELEASE` do take one — each commits something — and count against this
306
+ number; what each may *write* in it is far narrower than a coding run's and is
307
+ decided by the profile, not by the prompt: an interview writes `docs/brief/`,
308
+ a merge writes the files git put conflict markers in, and a release (R187)
309
+ writes exactly the version files the platform names on the claim, with `git`,
310
+ `gh pr create`, `gh pr view` and `node` for the exports — no build tools, no
311
+ permission prompt, no `gh pr merge`. What holds the rest back
312
+ is `maxSessions`, which counts every profile — **"at 4 sessions on this machine
313
+ (every profile counts)"** is the other thing a waiting run can say, and the log
314
+ always says which of the two it was.
315
+
316
+ **One thing this gate cannot express, and does not try to.** It serialises on a
317
+ checkout, so two runs on *the same branch* in two checkouts are, to this daemon,
318
+ two free workspaces and two runs to take. That is right for a branch each and
319
+ wrong for one branch shared — so R67's *one branch, in order* is held by the
320
+ platform instead: a run that follows another is simply not offered here until
321
+ that one has ended. Nothing in this file changes for it, and that is the point.
322
+ Do not add a branch check to the gate; it would be a second, weaker copy of a
323
+ rule that already exists where it can see every machine rather than one.
324
+
325
+ ### A workspace belongs to the daemon
326
+
327
+ Before each run it is cleared with `git clean -fd` — **without `-x`**, so
328
+ `.env`, `node_modules` and `target` survive and only what the last session left
329
+ lying about is removed. What goes is always logged.
330
+
331
+ **Do not list a directory you work in by hand.** That clean deletes untracked
332
+ files. It is skipped when a run was deliberately started on top of uncommitted
333
+ work, but the rule stands: a workspace is the machine's, not yours.
334
+
335
+ **A held workspace is not cleaned and not offered** (R80). When a code run
336
+ fails, the platform marks its checkout *held* — the uncommitted work is still
337
+ in it — and tells this daemon so on every heartbeat (`heldWorkspaces`). The
338
+ daemon counts it as busy: a new run does not land there, and `git clean` does
339
+ not run there, until somebody on the run page either **carries on** (the run
340
+ re-queues onto this machine and picks up its own session in the same
341
+ directory) or **discards** it, which is the only thing that frees it. The
342
+ runners page lists what each machine is holding, so a workspace nobody
343
+ remembers cannot quietly sit taken.
344
+
345
+ **A branch goes back where it was, until it does not have to** (R86, R212). A
346
+ branch's later runs are offered the checkout it last ran in; while that
347
+ checkout holds commits origin does not — or a failed run's tree — it is the
348
+ only answer and the run waits, saying so (*waiting for …, which is busy — the
349
+ branch has work only that checkout holds*). Once the last run pushed and
350
+ nothing is writing the branch, the platform only *prefers* it: a busy preferred
351
+ checkout means any free one, on this machine or another, and a stale local ref
352
+ is fast-forwarded to `origin` before the session starts.
353
+
354
+ **A cancelled run's uncommitted work is stashed if the person cancelling asked
355
+ for it** (R217). A cancelled run releases its checkout — nothing can come back
356
+ for it — so the run page, when the daemon's last look counted uncommitted
357
+ files, offers *Cancel and stash the changes* beside *Cancel, leave them in the
358
+ checkout*. The stash is this daemon's, taken in the child's close handler once
359
+ the agent process is dead and never a moment earlier: `git stash push
360
+ --include-untracked -m "cawdev: stashed when <run> was cancelled"`. The
361
+ transcript then ends with what was parked and the `git -C <path> stash pop`
362
+ that recovers it, or that there was nothing to park, or why it could not be —
363
+ never a second failure, since the run is already over. A run that took no
364
+ checkout of its own (an ASK standing in your working copy) is refused the
365
+ option by the platform.
366
+
367
+ Provision them however you like — `git clone`, then whatever the project needs
368
+ to build. R48 makes them cheap by cloning a golden checkout per run; until then
369
+ they are yours to create, and two or three is plenty.
370
+
371
+ Nothing is written down about which workspace is busy, so a killed daemon leaks
372
+ nothing: restarting frees them all.
373
+
374
+ ### What it looks like
375
+
376
+ R62. Starting the daemon prints the mark and then the five settings that decide
377
+ what it will actually do — the platform it registered against, this machine's
378
+ name, every project with **how many checkouts it has**, the session cap, and
379
+ whether the browser is allowed. Those five answer nearly every "why did that
380
+ not happen", and they used to be spread across a config file and a shrug.
381
+
382
+ Attaching adds a second bar row: the URL, each project as **coding sessions over
383
+ checkouts** (`cawdev 1/2`), and the machine's total against `maxSessions`.
384
+ Both gates, on screen, counting what each actually bounds — a question is in the
385
+ total on the right and not in any project's figure, because it took no checkout.
386
+ A run that is waiting is explained by the bar
387
+ above it rather than by reading the source. A narrow terminal drops projects
388
+ from the end (with an `…`) and never the total, because on a machine at its cap
389
+ the total is the number that answers the question.
390
+
391
+ Colour is `tools/lib/ansi.mjs`: truecolor where the terminal says so, the
392
+ 256-colour cube where it does not, and **nothing at all** under `NO_COLOR`,
393
+ through a pipe, or on a dumb terminal. Nothing carries meaning in colour alone
394
+ — every state that has a colour also has a word — so a piped log reads exactly
395
+ as it always did.
396
+
397
+ One thing worth knowing about the tinting: a line matching `skipped` is muted
398
+ *before* anything matches `failed`. `fetch skipped: git fetch --prune origin
399
+ failed: no origin` is a repository with no remote, which happens on every
400
+ survey of every scratch checkout and is fine. Painting it red teaches people
401
+ that red means nothing.
402
+
403
+ ### Letting a run drive the browser
404
+
405
+ R61. `--chrome` connects a session to Claude in Chrome — verified in print mode
406
+ against 2.1.252, with no terminal and no settings sources: the tools are there
407
+ and a call reaches the extension.
408
+
409
+ **Off by default, and this machine has the last word.**
410
+
411
+ ```json
412
+ { "browser": true, "projects": { "cawdev": { "path": "…", "browser": false } } }
413
+ ```
414
+
415
+ It reaches the extension in **your own Chrome**: your logged-in sessions, your
416
+ cookies, your mail. That is a different kind of permission from `Bash(mvn *)`,
417
+ and it must not be reachable by writing a roadmap card in a project this machine
418
+ happens to serve — so the platform records what was asked for and the config
419
+ decides whether it happens. A per-project `browser` overrides the machine's
420
+ answer in either direction.
421
+
422
+ A run that asks and is refused is **not failed**. It runs without a browser and
423
+ says so on its own transcript, because a capability withheld and a broken run
424
+ are different things.
425
+
426
+ **Turning it on does not pre-allow it.** The tools become available; the first
427
+ call still stops and asks. `mcp__claude-in-chrome` — the server with no tool
428
+ after it — covers every tool on it, so one answer settles the session rather
429
+ than twenty-six. A machine that wants it unattended puts that string in its own
430
+ `allowedTools`.
431
+
432
+ ### What a person can ask of a checkout
433
+
434
+ R57. The daemon polls `workspace-requests/claim` every few seconds and does one
435
+ of **ten** things to a checkout it serves. It said six for two entries, and
436
+ `HANDOFF` had been missing from the list since R148 — a table that quietly does
437
+ not describe one of the things a person can ask for is worse than no table. So
438
+ count the rows below against that number before you trust either. `CUT_BRANCH`
439
+ is the ninth, R260's; `OPEN_PR` the tenth, R261's.
440
+
441
+ | | |
442
+ |---|---|
443
+ | `SHOW` | `git status --porcelain`, the untracked list, and `git diff HEAD`. Reads only. |
444
+ | `STASH` | `git stash push --include-untracked`, and reports the ref to recover it by. |
445
+ | `COMMIT` | `git add -A` and commit. `--no-verify` is not passed — a repository's hooks are its own business. |
446
+ | `INDEX` | R77's map button: refreshes the code map, then builds the project's skill index if one is turned on. No session, no branch. |
447
+ | `RESET` | R87's start-over, and the destructive one: `reset --hard`, `clean -fd`, then back to `origin/<branch>` — or, for a branch never pushed, onto the default branch with the branch deleted. |
448
+ | `MERGE` | R134's Merge button on the development board: `gh pr merge <url> --squash --delete-branch` for the branch in `message`. The only kind whose effect is **not** in the checkout — it lands a branch on the host and touches no working tree, which is why several branches merge safely from one clone. The URL is reported on the result's first line, and the platform reads that line as the evidence to put on the card. **The platform hands a project's merges to machines one at a time, in the order they were asked for** (R188), so a claim may come back without a merge that is plainly queued — it is offered on a later poll, once the one ahead of it is over. **A claim with `mergeMethod: "MERGE_COMMIT"` runs `--merge` instead of `--squash`** — R261, and only a sprint's branch is asked for that way: nine card squashes onto it stay nine commits on the default. The second line of the result says which happened ("merged with a merge commit" / "squashed and merged"), and the platform quotes it on every card of the sprint — so a daemon older than R261, which squashes whatever the claim says, is noticed on nine cards rather than nowhere. Restart the daemon after updating. |
449
+ | `HANDOFF` | R148's hand-off, and the only kind addressed to a **run** rather than to a directory alone: the branch is pushed, whatever was uncommitted is packaged as a patch on its base sha, and the checkout is put back on the default branch. The next claim can then go to any machine. |
450
+ | `TAG` | R156's release: `git ls-remote --tags origin refs/tags/<version>` for the version in `message`. **The remote, not this clone** — a local tag nobody pushed is exactly the state a release is trying to rule out, and `git tag --list` cannot tell the two apart. A read; it fetches nothing and moves no ref. The sha is reported on the result's first line, the way `MERGE` reports its URL, because the platform will not confirm a release on `ok: true` alone. **A tag that is not there yet is `ok: false` with a sentence, and that is a normal answer** — the release procedure pushes the tag last, so the first check correctly finds nothing. Addressed to a **release** rather than to a directory alone, which is why it is asked for from the roadmap board and refused on this channel. |
451
+ | `CUT_BRANCH` | R260's sprint branch: `git push origin origin/<default>:refs/heads/<name>` for the branch in `message`, after a fetch — so the sprint's cards are cut from what everyone else has, not from whatever this checkout was left on. The second kind after `MERGE` whose effect is **not** in the checkout: the push names two refs and this clone's HEAD is not one of them, so any live checkout of the project will do and nothing local moves. **A branch already on origin is `ok: true`** ("already on origin at <sha>") — the sprint wanted one to exist, and one does. The sha is reported on the result's first line, the way `TAG` reports its. Addressed to a **sprint** and asked for from the sprint page by an OWNER, for `MERGE`'s reason — it reaches the remote — and refused on this channel. A `MERGE` of a card's pull request into a sprint branch still runs `--squash --delete-branch`: the card's branch goes, the sprint's stays. Landing the sprint branch itself is `OPEN_PR` then `MERGE` below. |
452
+ | `OPEN_PR` | R261's sprint merge, first half: `gh pr create --head <branch> --base <base> --title <title>` for the branch in `message` — the sprint's — to `baseBranch ?? defaultBranch`, titled as the claim's `title` (*S1 Notifications*). **An existing pull request is `ok: true` with its URL** — the same `gh pr view` lookup the `auto_pr` rule makes, and the reason the platform can ask this every time: it holds no git-host credential and cannot know whether one exists. The URL is reported on the result's first line, and the platform queues the `MERGE` to this same checkout when it reads one. Nothing is pushed: the branch is on origin already, cut there by `CUT_BRANCH` and merged into by every card. Addressed to a **work item** — the sprint's — and asked for by Merge on the development board or *Merge sprint* on the sprint page, by an OWNER; refused on this channel for `MERGE`'s reason. |
453
+
454
+ **A `MERGE` that fails also says which KIND of failure it was** — R155, in one
455
+ of five words beside `gh`'s own unchanged text:
456
+
457
+ | | |
458
+ |---|---|
459
+ | `CONFLICT` | `gh pr view --json mergeable` says `CONFLICTING`. The one an agent can do something about, and the only failure the console offers **Merge with an agent** on. |
460
+ | `NO_PULL_REQUEST` | There is nothing on the host to merge. R134 already named this case; it now has a word. |
461
+ | `NOT_PERMITTED` | 403, a protected branch, a required review or a required status check. The host said no, and it will say no again. |
462
+ | `UNREACHABLE` | No `gh`, or the host could not be reached at all. Nothing is known about the pull request. |
463
+ | `OTHER` | Anything else — **including `mergeable: UNKNOWN`**, which GitHub answers when it has not computed mergeability yet. Unknown is not "no conflict", and reading it as one is the single way this feature would disappear silently. |
464
+
465
+ The classification is made **here**, on the machine, and never by the platform:
466
+ the platform has no `gh` and parsing this text would make it a platform with an
467
+ opinion about a git version it does not run. A daemon older than R155 sends no
468
+ word at all, which the platform stores as null and the console reads as
469
+ *unknown* — offering the agent merge anyway, because being wrong there costs one
470
+ `git fetch`.
471
+
472
+ Two guards, and they are in different places on purpose. The platform checks
473
+ that the runner is **yours**; only this process knows which directories it was
474
+ actually given, so **a request naming a path this daemon does not serve is
475
+ refused rather than run**. Results are capped, and say in the text that they
476
+ were capped: a diff that silently stops halfway is one somebody reads to the
477
+ end and then acts on.
478
+
479
+ ## The daemon's tooling is frozen when it starts
480
+
481
+ The runner spawns the cawdev MCP server from this repository — and **cawdev is
482
+ its own first project**, so a run working on cawdev checks this very directory
483
+ out onto another branch. The session would then be handed whichever MCP server
484
+ happened to be on the branch it is working on.
485
+
486
+ That is not hypothetical: a run on a branch cut from `main` was given a server
487
+ with no `approve` tool while holding a `--permission-prompt-tool` flag naming
488
+ it, and died on its first tool call.
489
+
490
+ So the server is **copied to a temp directory at startup** and sessions are
491
+ pointed at the copy. To pick up changes to it, restart the daemon — which is
492
+ when its own code reloads anyway, so the two cannot disagree about what exists.
493
+
494
+ **If you drive cawdev with cawdev, run the daemon from a separate worktree:**
495
+
496
+ ```sh
497
+ git worktree add ~/code/cawdev-runner main
498
+ cd ~/code/cawdev-runner/tools/runner
499
+ node runner.mjs --config ~/code/cawdev/tools/runner/macbook-laptop.json --attach
500
+ ```
501
+
502
+ Then a run switching branches in your working copy cannot reach the daemon's own
503
+ files at all. R47 removes the need for this by giving each run a workspace.
504
+
505
+ ## What it does with a run
506
+
507
+ 1. **Claims it.** The claim response carries the run's own `cawdr_` token and
508
+ the project's default branch — the runner never touches the project API,
509
+ which is session-only.
510
+
511
+ **Base branches** (R260). The claim may also carry `baseBranch`: the
512
+ branch of the sprint the card is in, copied onto the work item when it was
513
+ opened. When it is there, cutting, the daemon's own merge-in (R155), the
514
+ pull request's `--base` and the merge check's git fallback use it; a
515
+ reset, the code map and the brief go on reading `defaultBranch`. When it
516
+ is not — every run until a sprint has a branch — every git and `gh` call
517
+ is byte for byte what it was. A base that is not on origin **fails the
518
+ run** saying so, rather than falling back to the default under the
519
+ sprint-branch's name. The daemon says `baseBranches: true` in its
520
+ capabilities, and that key is how the platform knows not to offer a based
521
+ run to an older daemon.
522
+ 2. **Prepares the working copy**: fetch, then branch off the default — or
523
+ off `origin/<baseBranch>` when the claim carried one.
524
+ **A dirty tree stops it unless somebody said otherwise.** An agent let loose
525
+ in a checkout with uncommitted work will at best confuse itself and at worst
526
+ commit somebody's half-finished thoughts — so the console shows what is
527
+ uncommitted before the run is started, and the decision arrives with the
528
+ claim as `allowDirty`. Without it the runner still refuses, with the file
529
+ list, and fails the run saying so: a run that arrives with no flag is one
530
+ nobody was warned about.
531
+
532
+ With it, the edits are **carried onto the branch** — stashed, checked out,
533
+ popped (R57). `git checkout` refuses outright when a locally modified file
534
+ differs between the two commits, which is what made *Start anyway* fail one
535
+ step later than the refusal it was meant to replace. If the pop cannot apply
536
+ — the branch rewrote the same lines — the run fails **naming the stash and
537
+ the command to recover it**, because work parked somewhere nobody was told
538
+ about is work lost.
539
+ 3. **Spawns the agent** in that directory, in its own process group, with the
540
+ run token in its environment and an `.mcp.json` pointing at cawdev's MCP
541
+ server. **Your own token never reaches the child.**
542
+
543
+ When the claim carries a `resume` (R69), it spawns `--resume <session-id>`
544
+ and writes the **follow-up** to stdin instead of the opening prompt — the
545
+ session is being handed back its own transcript, so it already has the
546
+ question, and re-sending it would be a repeat wearing a resume's clothes.
547
+ Nothing else changes: the same profile, so the same permissions. Being
548
+ started a second time is not a reason to be allowed to write files.
549
+ 4. **Streams the transcript.** Every stream-json event the session emits is
550
+ summarised into a line and batched to the platform, which is what the
551
+ console's live terminal reads. It summarises rather than forwards — the
552
+ `init` event alone is kilobytes of tool inventory nobody reads.
553
+
554
+ Two things are kept out of that event rather than summarised away: the model,
555
+ and — since R69 — the **session id**, which is reported to the platform as
556
+ the handle `claude --resume` takes. It is reported **every** time an `init`
557
+ arrives, because a resumed session announces itself again and the id a
558
+ *further* resume needs is the most recent one.
559
+ 5. **Delivers prompts.** It long-polls for prompts typed in the console and
560
+ writes them into the session's stdin, which stays **open** for exactly this
561
+ reason (`--input-format stream-json`). One process, one session, many turns.
562
+ A prompt is acknowledged only after the write, so one that never landed is
563
+ retried rather than lost.
564
+ 6. **Says what its checkouts look like**, on every heartbeat: for each project
565
+ it serves, the porcelain status, capped at twenty paths but counting them
566
+ all. This is the only way the console can know a checkout is dirty before a
567
+ run is started in it — the platform cannot see your machine. A checkout it
568
+ cannot read is reported as unreadable rather than omitted, because "I could
569
+ not look" and "it was clean" are different answers. One entry **per
570
+ workspace**, in this daemon's own `workspaces` order and carrying the run
571
+ holding each: R71 has the console pick the first unheld entry to decide which
572
+ checkout a run is headed for, so re-ordering this list would move a warning
573
+ onto a checkout nothing is going to touch.
574
+ 7. **Reads each repository for the project's Git tab**, on a slow timer of its
575
+ own — `gitSurveySeconds`, five minutes by default, plus once at startup. Per
576
+ project: `git fetch --prune`, the tail of the default branch's history, and
577
+ every remote branch with whether it is merged, plus any local branch whose
578
+ upstream is `[gone]`. The platform holds no git credentials, so this machine
579
+ is the only thing that can answer; the console shows every reading with when
580
+ it was taken and says **stale** when it has aged.
581
+
582
+ Its own timer rather than the heartbeat's, because it pays for a network
583
+ round trip per project and the heartbeat runs every thirty seconds. The
584
+ `fetch` is **skipped while an agent is working in that checkout** — it is the
585
+ only part that writes anything, and taking the ref lock out from under a
586
+ session to refresh a background page is a bad trade. Nothing in *this* pass
587
+ writes to git: no merging, no branch deletion, no pushing. (A project's own
588
+ rules can ask for all three — see below — but that is a queued action with a
589
+ name on it, not something the survey does in passing.)
590
+ 8. **Reports the lifecycle back** and, if the session dies without saying
591
+ anything, ends the run rather than leaving it `RUNNING` forever.
592
+
593
+ Cancelling a run reaches the daemon on its next poll and takes down the child's
594
+ whole process group — an agent that started a build should not leave it running.
595
+
596
+ ## What a project's rules can ask of it (R40)
597
+
598
+ A project may decide that a finished run **pushes its branch**, **opens a pull
599
+ request**, and — if somebody with `OWNER` deliberately turned it on — **merges
600
+ it with nobody reading the diff**. The rules are set in the console, ride along
601
+ with the claim, and the daemon logs them when it takes a run, so you find out
602
+ what is going to happen before it happens rather than afterwards. `auto_merge`
603
+ gets a shouted line of its own.
604
+
605
+ The daemon does not decide any of this. The platform queues `PUSH`, `OPEN_PR`
606
+ and `MERGE` on the same `run_action` queue the console's *Commit* button uses,
607
+ and the daemon performs them in `settleActions` — one pass right after the
608
+ session ends, because the working-copy watcher stopped with the child and the
609
+ rules queue their work at exactly that moment.
610
+
611
+ **cawdev still holds no git-host credential.** This machine does, which is why
612
+ the work happens here and why R19 and R25's division is unchanged. All that is
613
+ new is that the platform can ask.
614
+
615
+ What it actually runs:
616
+
617
+ - `git push --set-upstream <remote> <branch>`. Never `--force`: a rule that
618
+ pushes must never be a rule that overwrites somebody else's commits.
619
+ - `gh pr create --head <branch>`, titled with the run's label, after pushing if
620
+ the branch is not out yet. An existing pull request is a **success**, not a
621
+ conflict — the rule wanted one to exist and one does.
622
+ - `gh pr merge <url> --squash --delete-branch`, and only if a real pull request
623
+ is found. A `/compare/` URL is not one, and it refuses rather than guessing.
624
+
625
+ Every one of them reports a result or a reason, and a machine with no `gh`, no
626
+ remote or no credentials simply says so. **A rule is a request, not a grant** —
627
+ the same asymmetry `grantable` gives the R51 tool rules.
628
+
629
+ ## What it does after a run is over
630
+
631
+ A merge happens **after** the run ends, by definition: somebody reviews the pull
632
+ request and merges it. So the one event worth recording is the one event a live
633
+ session can never be present for, and R25's reading — taken on the runner's last
634
+ reporting pass — always stops one question short. `PUSHED` is not `MERGED`.
635
+
636
+ Every ten minutes, on the loop it already has, the daemon asks
637
+ `GET /api/runners/{id}/branches` for the finished runs worth re-checking, looks,
638
+ and posts the answers back in one batch. The platform names the branches; the
639
+ runner says what happened to them. **cawdev holds no git-host credential** and
640
+ could not answer this itself — that division is R19 and R25's, and this keeps it.
641
+
642
+ How it answers, in order:
643
+
644
+ 1. **`gh pr view`**, preferring the recorded pull request URL over the branch
645
+ name. `gh pr view <branch>` stops finding anything once the branch is deleted,
646
+ which is exactly when the question gets interesting; a URL keeps answering.
647
+ This is also the only thing that can see a **squash merge**.
648
+ 2. **`git merge-base --is-ancestor <head> origin/<default>`**, which works for
649
+ any remote at all. By SHA, not by branch name: a merged branch is usually
650
+ deleted, but its last commit stays reachable from the default branch for ever,
651
+ which is why `git branch --merged` is not what is used here.
652
+ 3. **`git ls-remote`** — if ancestry says no and the branch is still on the
653
+ remote, it is genuinely open and waiting.
654
+
655
+ Anything else is **`UNKNOWN`**, and that is a real answer rather than a missing
656
+ one. Proving a merge is possible; disproving one is not — a squashed branch that
657
+ was then deleted looks exactly like an abandoned one — so the daemon says it
658
+ cannot tell instead of guessing. A project it serves whose checkout it can no
659
+ longer read also answers `UNKNOWN`; a project it does **not** serve it says
660
+ nothing about at all, so it cannot blank another machine's good answer.
661
+
662
+ The pass is bounded and rotating, longest-unchecked first, and a run already
663
+ recorded as merged is never offered again: **merged is permanent.** A merged
664
+ branch is usually deleted within seconds, so the very next pass often cannot
665
+ prove anything — and letting that overwrite the record would mean forgetting the
666
+ one fact worth learning, minutes after learning it.
667
+
668
+ **One run at a time per working copy** — for runs that use one. Coding runs
669
+ share a checkout, so a second in the same directory would fight the first; the
670
+ rest queue.
671
+
672
+ **Questions are not serialised.** An `ASK` run prepares nothing and writes
673
+ nothing, so it runs alongside whatever else is going on — you can ask about a
674
+ project while an agent is working in it. `maxSessions` (4 by default) bounds how
675
+ many agent processes this machine will host at once, and it is the **only**
676
+ thing bounding a question, a roadmap session or an audit (R70). It counts runs
677
+ this daemon has claimed, not children it has spawned: a claim takes a second or
678
+ two to become a process, and counting processes let one pass of the loop claim
679
+ the whole queue.
680
+
681
+ ## If the daemon dies mid-run
682
+
683
+ The platform notices. `StaleRunSweeper` fails a run whose runner has stopped
684
+ heartbeating for five minutes, with `failureReason: RUNNER_VANISHED` and a
685
+ summary saying so — otherwise the run would sit `RUNNING` forever and block the
686
+ project from starting anything else. The workspace is **kept**, not reclaimed:
687
+ when the daemon comes back, the run page offers *Carry on*, which re-queues the
688
+ run onto this machine and resumes its own session in the same checkout, with
689
+ whatever it had written still there. Nothing is lost by a crash that a person
690
+ does not choose to discard.
691
+
692
+ A run **`WAITING_ON_USER` is never swept.** That is the one state where nothing
693
+ happening is correct: it is stalled on a person, who may reasonably take a day.
694
+ Nor is a `PAUSED` or `USAGE_LIMITED` one (below) — there is no process to lose.
695
+
696
+ ## When the usage limit hits (R73)
697
+
698
+ Claude Code stops with a message naming the window — the five-hour one or the
699
+ weekly one — and when it resets. The daemon reads that off the session's last
700
+ output (`tools/lib/usage-limit.mjs`, tested) and reports the run as
701
+ **`USAGE_LIMITED`** with the window and the reset time, rather than `FAILED`
702
+ with a stack of text. The run page says *Usage limit — resumes after …*; the
703
+ card stays where it was; the workspace stays taken. It also posts the reading
704
+ to `POST /api/runners/{id}/limits`, so the runners page shows what this machine
705
+ has used of each window.
706
+
707
+ **Pause** is the same state with a person's hand on it: the run page's *Pause*
708
+ button moves a `RUNNING` run to `PAUSED`, and the runners page's *Pause* switch
709
+ stops a machine claiming anything new without stopping what it is driving.
710
+ Both are yours to undo. *Carry on* re-queues the run onto its runner, which
711
+ resumes the session it already had.
712
+
713
+ **Auto-resume** is per machine, off by default, on the runners page. With it
714
+ on, the platform re-queues a `USAGE_LIMITED` run the minute its window opens,
715
+ onto the same machine. It never touches a `PAUSED` run: a person stopped that,
716
+ and only a person starts it. The runner has no say in any of this beyond
717
+ obeying its heartbeat — `paused` and `heldWorkspaces` come from the platform,
718
+ and `runner.mjs` reads them rather than deciding them.
719
+
720
+ **The thresholds (R158) are set in the console, on the runners page, not in
721
+ `runner.config.json`.** *Stop claiming at %* pauses this machine when one of its
722
+ whole windows crosses the line, and *Ask before starting at %* makes a start
723
+ bound to it state the numbers first. There is nothing to configure here and
724
+ nothing to upgrade: the platform sets the same `paused` flag a person's click
725
+ sets, and this daemon already obeys it on the next beat.
726
+
727
+ ## Trying it without spending Claude usage
728
+
729
+ `stub-agent.mjs` stands in for `claude`. It is spawned the same way, talks to
730
+ the platform through the same API, and follows a fixed script instead of
731
+ thinking:
732
+
733
+ Point `agentCommand` straight at it — an **absolute path, with no `agentArgs`**:
734
+
735
+ ```json
736
+ { "agentCommand": "/…/cawdev/tools/runner/stub-agent.mjs", "agentArgs": [] }
737
+ ```
738
+
739
+ ```sh
740
+ CAWDEV_STUB_SCRIPT=ask-then-finish \
741
+ CAWDEV_TOKEN=cawd_… \
742
+ node tools/runner/runner.mjs --config runner.config.json
743
+ ```
744
+
745
+ Not `"agentCommand": "node"` with the script in `agentArgs`: the runner puts
746
+ `--mcp-config` first, so node gets a flag it does not know and exits with `bad
747
+ option` before the script runs. The shebang avoids that, and the flags land in
748
+ the stub's argv where it ignores them. Absolute, because the child's cwd is the
749
+ working copy rather than this repository.
750
+
751
+ | `CAWDEV_STUB_SCRIPT` | What it does |
752
+ |---|---|
753
+ | `report-and-finish` | progress, then done (default) |
754
+ | `ask-then-finish` | asks a question, waits for the answer, then done |
755
+ | `crash` | exits non-zero without reporting |
756
+ | `hang` | never exits — for testing cancellation |
757
+
758
+ ## The agent command
759
+
760
+ `agentCommand` and `agentArgs` are configuration, not code. That is what makes
761
+ the stub possible, and it is also how R17's second CLI would arrive.
762
+
763
+ The defaults are for Claude Code, verified against 2.1.247:
764
+
765
+ See `DEFAULTS` in `runner.mjs` for the full list — it is long because the
766
+ cawdev MCP tools are named individually.
767
+
768
+ ### About permissions
769
+
770
+ **A spawned agent has no terminal**, so anything that stops to ask a human for
771
+ permission stops forever.
772
+
773
+ - `--permission-mode acceptEdits` lets it write files.
774
+ - `--allowedTools mcp__cawdev__…` lets it use the cawdev tools. **Without this
775
+ the entire loop is unreachable** — no reading the task, no moving the entry,
776
+ no reporting, no asking. A real session found this by being denied
777
+ `task_current` and stopping rather than guessing, which was the right call
778
+ and a good sign for the prompt.
779
+
780
+ They are named explicitly rather than reached with `bypassPermissions`, because
781
+ nothing here should imply the agent may run arbitrary commands.
782
+
783
+ Both lines above are the **coding** defaults, and only a session that writes
784
+ code is spawned with them. A profile that does not — `ASK`, `PLAN`, `REVIEW`,
785
+ `AUDIT`, `SCOPE` (R227 — an audit's list, name for name, with a prompt that
786
+ asks it to cut an idea into cards rather than to look for what is wrong), and
787
+ the rest — and a read-only stage of a coding run — `PLAN`,
788
+ `VERIFY`, `MEMORY` — get their own `--allowedTools` and neither a permission
789
+ mode nor a permission prompt: the list is the whole permission. That is what
790
+ makes "cannot write" true rather than asked for (`acceptEdits` on its own lets
791
+ a session write a file its list does not name), and it is why such a session
792
+ never asks you anything — an unlisted tool is refused, not raised (i138).
793
+
794
+ - `Bash(git *)` lets it commit. The prompt tells it to commit its work, so the
795
+ default has to allow that — a default configuration that forbids what the
796
+ default prompt asks for is a broken default. A real session wrote the file,
797
+ could not commit, and reported `blocked`: correct behaviour, avoidable cause.
798
+
799
+ **Anything else stops and asks you** (R51). Rather than being denied in
800
+ silence, a session that needs `mvn` or `npm` posts a request that appears in
801
+ your inbox and on the run, and waits for an answer. Allow it once, or allow it
802
+ always here — which writes a rule on the project that every runner serving it
803
+ inherits.
804
+
805
+ Nobody answering is a decision too: after fifteen minutes the request expires
806
+ and the session is told to report blocked rather than wait for ever.
807
+
808
+ You can still say in advance what a project may do, and for anything a session
809
+ needs on every run that is the better answer — with `allowedTools`, which
810
+ **adds to** the defaults rather than replacing them:
811
+
812
+ ```json
813
+ {
814
+ "allowedTools": ["Bash(node *)"],
815
+ "projects": {
816
+ "dycrypt": {
817
+ "path": "/Users/you/code/dycrypt",
818
+ "allowedTools": ["Bash(mvn *)", "Bash(gh *)", "mcp__roadmap"]
819
+ },
820
+ "medymo": "/Users/you/code/medymo"
821
+ }
822
+ }
823
+ ```
824
+
825
+ A project is a path, or a path with permissions of its own; both forms work, so
826
+ the one project that runs Maven does not force the long form on the rest.
827
+
828
+ Do **not** reach for `agentArgs` to add a permission: it replaces the whole
829
+ default list, so you would have to repeat all sixteen MCP tool names to add one
830
+ `Bash` pattern.
831
+
832
+ #### Skills need no configuration here — R76
833
+
834
+ A skill is a capability a project turns on in cawdev, and the runner attaches it
835
+ to the session as an MCP server. **There is nothing to configure on the
836
+ machine**: turn CodeGraph on for a project in the console and the next run in
837
+ that project has it.
838
+
839
+ That is deliberate, and narrower than it sounds. A skill's command is not
840
+ something anybody types — the `skill` table is seeded by migration and has no
841
+ create endpoint, so enabling one runs a command cawdev itself shipped. An
842
+ allowlist on every machine would have been guarding against a project owner
843
+ switching on a vetted skill, which is friction rather than a boundary.
844
+
845
+ **The machine's consent did not go away; it moved to where it was already
846
+ being asked.** The skill's tools are not added to `--allowedTools`, so the
847
+ session's first call stops and asks a person (R51), and *allow `mcp__codegraph`
848
+ for this session* (R60) is the answer that fits. A machine that wants it
849
+ unattended says so in its own `allowedTools` — the same place every other
850
+ standing permission lives, rather than a second list that only skills use.
851
+
852
+ **The index lives outside the checkout.** CodeGraph parses the repository into
853
+ a graph beside it; the runner builds that once per repository, keeps it in
854
+ `~/.cawdev/skills/<skill>/<project>`, and copies it into each workspace — so
855
+ R47's several checkouts of one project do not each pay for a parse. The pidfile
856
+ and socket are never copied: they name a live process, and in another workspace
857
+ they point at a daemon serving another tree. The index directory is added to
858
+ that checkout's `.git/info/exclude`, so it neither shows up as a dirty tree nor
859
+ gets deleted by the reset between runs.
860
+
861
+ #### `grantable`: what this machine lets a saved rule cover
862
+
863
+ A rule stored on the platform applies to sessions **nobody is watching**, so the
864
+ machine's owner has the last word on it. `grantable` is that word:
865
+
866
+ ```json
867
+ {
868
+ "grantable": ["Bash(mvn *)"],
869
+ "projects": {
870
+ "dycrypt": {
871
+ "path": "/Users/you/code/dycrypt",
872
+ "grantable": ["Bash(npm *)"]
873
+ }
874
+ }
875
+ }
876
+ ```
877
+
878
+ A project rule outside the ceiling is **dropped**, and the drop is reported onto
879
+ the run so nobody is left wondering why allowing something changed nothing.
880
+
881
+ **Empty by default, and that is safe rather than timid**: a machine that has
882
+ declared nothing still works, it just asks every time. Widening it is a real
883
+ decision about what an unattended agent may run in your checkout, so it is yours
884
+ to make rather than a default you inherit without noticing.
885
+
886
+ The ceiling limits *saved rules only*. A person allowing one call in the moment
887
+ is present and looking at the command, and needs no ceiling.
888
+
889
+ #### Two flags worth knowing about
890
+
891
+ - `--permission-prompt-tool mcp__cawdev__approve` is what makes the asking
892
+ possible. It is **hidden from `claude --help`** on 2.1.251 but accepted; the
893
+ daemon probes for it at startup and warns loudly if the CLI it was pointed at
894
+ does not take it.
895
+ - `--setting-sources ''` stops a run inheriting **your own**
896
+ `~/.claude/settings.json`. Without it, what a session may do depends on an
897
+ invisible file on whichever machine claimed the run — two laptops, two answers
898
+ — and the ceiling above means nothing.
899
+
900
+ ### The model
901
+
902
+ A run may name one, chosen when it is started. The runner passes it through as
903
+ `--model`, **before** `--allowedTools` — that option is variadic and would
904
+ swallow it otherwise. A run that names no model is spawned exactly as it was
905
+ before R23, so `agentArgs` may still pin one for every run on this machine.
906
+
907
+ ### The project's own MCP servers
908
+
909
+ A repository that ships its own `.mcp.json` means it. Those servers are
910
+ **passed through** into the config the runner generates, because a server
911
+ merely *discovered* in a repository is project-scoped — Claude Code asks whether
912
+ you trust it, and a spawned session has no terminal to answer with, so it
913
+ auto-denies and the repository's own tooling is silently missing.
914
+
915
+ Their tools still need naming in `allowedTools`, as `mcp__<server>__<tool>`.
916
+ cawdev's own entry is written last, so a project cannot shadow it with a server
917
+ of the same name and intercept the run's token. On a machine
918
+ dedicated to this, `bypassPermissions` covers everything — but that is a real
919
+ decision about what an unattended agent may do in your checkout, and it should
920
+ be yours to make rather than a default you inherit without noticing.
921
+
922
+ **Both `--mcp-config` and `--allowedTools` are variadic**, so whatever follows
923
+ them is swallowed as another value. The runner puts `--mcp-config` first and the
924
+ prompt on **stdin** for exactly this reason — passing the prompt as an argument
925
+ after `--mcp-config` fails with `ENAMETOOLONG`, which names neither the flag nor
926
+ the prompt.
927
+
928
+ The prompt deliberately teaches the **method**, not the task. The task is in the
929
+ roadmap entry, which the agent reads for itself with `task_current` — putting it
930
+ in the prompt too would be a second copy that can disagree with the entry.