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.
- package/README.md +175 -0
- package/lib/ansi.mjs +224 -0
- package/lib/cawdev.mjs +104 -0
- package/lib/code-map.mjs +164 -0
- package/lib/harness-prompt.mjs +197 -0
- package/lib/roadmap-format.mjs +453 -0
- package/lib/run-plugin.mjs +119 -0
- package/lib/secrets.mjs +290 -0
- package/lib/stage-tools.mjs +384 -0
- package/lib/tool-line.mjs +92 -0
- package/lib/tool-rules.mjs +282 -0
- package/lib/transcript-batch.mjs +88 -0
- package/lib/usage-limit.mjs +80 -0
- package/lib/usage-report.mjs +142 -0
- package/lib/usage.mjs +119 -0
- package/mcp/README.md +273 -0
- package/mcp/orchestration-smoke.mjs +267 -0
- package/mcp/server.mjs +2163 -0
- package/mcp/smoke.mjs +220 -0
- package/package.json +20 -0
- package/runner/README.md +930 -0
- package/runner/attach.mjs +2397 -0
- package/runner/banner.mjs +106 -0
- package/runner/bootstrap.mjs +501 -0
- package/runner/brand.mjs +57 -0
- package/runner/cawdev.mjs +414 -0
- package/runner/control.mjs +225 -0
- package/runner/history.mjs +91 -0
- package/runner/input.mjs +355 -0
- package/runner/macbook-laptop.json +48 -0
- package/runner/runner.mjs +7445 -0
- package/runner/scrollback.mjs +165 -0
- package/runner/select.mjs +316 -0
- package/runner/session-store.mjs +78 -0
- package/runner/sign-in.mjs +210 -0
- package/runner/stub-agent.mjs +212 -0
- package/runner/token-store.mjs +107 -0
package/runner/README.md
ADDED
|
@@ -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.
|