cawdev-cli 1.0.11-beta → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/lib/finish-check.mjs +34 -1
- package/lib/harness-prompt.mjs +6 -0
- package/lib/order.mjs +7 -0
- package/lib/protected.mjs +225 -0
- package/lib/roadmap-format.mjs +25 -10
- package/lib/runner-config.mjs +9 -0
- package/lib/secrets.mjs +2 -2
- package/lib/sonar-analysis.mjs +52 -0
- package/lib/sonarqube-mcp.mjs +50 -0
- package/lib/stage-tools.mjs +54 -1
- package/mcp/README.md +48 -36
- package/mcp/server.mjs +263 -72
- package/package.json +1 -1
- package/runner/README.md +101 -25
- package/runner/attach.mjs +46 -1
- package/runner/cawdev.mjs +20 -2
- package/runner/configure.mjs +62 -0
- package/runner/git-host.mjs +298 -5
- package/runner/pause-note.mjs +104 -0
- package/runner/push-guard.mjs +333 -0
- package/runner/runner.mjs +675 -177
- package/runner/split.mjs +1 -1
package/runner/README.md
CHANGED
|
@@ -158,8 +158,9 @@ served like any added project, and the rest of the console's setup walk —
|
|
|
158
158
|
rules, members, the CTO Interview — is linked at
|
|
159
159
|
`<url>/projects/<slug>/setup`.
|
|
160
160
|
|
|
161
|
-
**What takes effect when** (R294).
|
|
162
|
-
without a restart — `agentCommands`, `grantable`, `acceptsRulesFromConsole
|
|
161
|
+
**What takes effect when** (R294). Four settings a running daemon takes
|
|
162
|
+
without a restart — `agentCommands`, `grantable`, `acceptsRulesFromConsole`
|
|
163
|
+
and `autoWorkspaces`,
|
|
163
164
|
the `HOT_SWAPPABLE` list in `tools/lib/runner-config.mjs`, spelled once —
|
|
164
165
|
because the daemon reads them fresh on every use. Everything else in the file
|
|
165
166
|
(`projects` and their workspaces, `url`, `name`, the token, `browser`, the
|
|
@@ -197,7 +198,7 @@ and its copy all work exactly as they always have, and scrolling up reaches the
|
|
|
197
198
|
start of the session. The only thing pinned is the footer at the bottom, and it
|
|
198
199
|
never scrolls away: which cawdev, who you are signed in as, this machine's
|
|
199
200
|
runner, the per-project session counts against their checkouts and the machine's
|
|
200
|
-
total
|
|
201
|
+
total — plus the run you are watching and the keys that
|
|
201
202
|
work right now. Anything *stopping* a session sits above all of that, because it
|
|
202
203
|
is the only thing in there waiting on a person.
|
|
203
204
|
|
|
@@ -283,7 +284,7 @@ the shape of the decision, and the whole value of asking a person is that they
|
|
|
283
284
|
can say the thing that was not on the list — so getting there costs one key, and
|
|
284
285
|
`esc` from it comes back to the list rather than abandoning the answer. A chosen
|
|
285
286
|
option and a typed sentence resolve the same question the same way, through the
|
|
286
|
-
same endpoint the
|
|
287
|
+
same endpoint the Waiting on you page and the run page post to: one record, the same
|
|
287
288
|
`answeredBy`, and the badge clears at once.
|
|
288
289
|
|
|
289
290
|
A permission request is the same widget — R60's lengths of yes as rows, with the
|
|
@@ -694,7 +695,7 @@ it ends.
|
|
|
694
695
|
A bare path means one workspace, which is what every config meant before this
|
|
695
696
|
existed — nothing changes for a machine that serves one checkout per project.
|
|
696
697
|
|
|
697
|
-
How many coding runs go at once is
|
|
698
|
+
How many coding runs go at once is the number of workspaces. A run that
|
|
698
699
|
waits now says **"no free workspace in cawdev (2 here, all busy)"** instead of
|
|
699
700
|
"that project already has a run here", which was a proxy for it.
|
|
700
701
|
|
|
@@ -717,18 +718,16 @@ says about knowing it.
|
|
|
717
718
|
|
|
718
719
|
**This number caps coding and nothing else** (R70). `ASK`, `ROADMAP`, `AUDIT`
|
|
719
720
|
and `SCOPE` runs take no workspace, so a project whose checkouts are all busy
|
|
720
|
-
still starts a question,
|
|
721
|
-
|
|
721
|
+
still starts a question, a card-writing run, an audit and a scoping
|
|
722
|
+
run at once. `INTERVIEW`, `MERGE`
|
|
722
723
|
and `RELEASE` do take one — each commits something — and count against this
|
|
723
724
|
number; what each may *write* in it is far narrower than a coding run's and is
|
|
724
725
|
decided by the profile, not by the prompt: an interview writes `docs/brief/`,
|
|
725
726
|
a merge writes the files git put conflict markers in, and a release (R187)
|
|
726
727
|
writes exactly the version files the platform names on the claim, with `git`,
|
|
727
728
|
`gh pr create`, `gh pr view` and `node` for the exports — no build tools, no
|
|
728
|
-
permission prompt, no `gh pr merge`. What holds
|
|
729
|
-
is
|
|
730
|
-
(every profile counts)"** is the other thing a waiting run can say, and the log
|
|
731
|
-
always says which of the two it was.
|
|
729
|
+
permission prompt, no `gh pr merge`. What holds a coding run back
|
|
730
|
+
is a free workspace, and the log says so (R109 removed `maxSessions`).
|
|
732
731
|
|
|
733
732
|
**One thing this gate cannot express, and does not try to.** It serialises on a
|
|
734
733
|
checkout, so two runs on *the same branch* in two checkouts are, to this daemon,
|
|
@@ -797,8 +796,8 @@ whether the browser is allowed. Those five answer nearly every "why did that
|
|
|
797
796
|
not happen", and they used to be spread across a config file and a shrug.
|
|
798
797
|
|
|
799
798
|
Attaching adds a second bar row: the URL, each project as **coding sessions over
|
|
800
|
-
checkouts** (`cawdev 1/2`), and the machine's total
|
|
801
|
-
|
|
799
|
+
checkouts** (`cawdev 1/2`), and the machine's total.
|
|
800
|
+
The gate, on screen, counting what it actually bounds — a question is in the
|
|
802
801
|
total on the right and not in any project's figure, because it took no checkout.
|
|
803
802
|
A run that is waiting is explained by the bar
|
|
804
803
|
above it rather than by reading the source. A narrow terminal drops projects
|
|
@@ -849,11 +848,11 @@ than twenty-six. A machine that wants it unattended puts that string in its own
|
|
|
849
848
|
### What a person can ask of a checkout
|
|
850
849
|
|
|
851
850
|
R57. The daemon polls `workspace-requests/claim` every few seconds and does one
|
|
852
|
-
of **
|
|
851
|
+
of **eleven** things to a checkout it serves. It said six for two entries, and
|
|
853
852
|
`HANDOFF` had been missing from the list since R148 — a table that quietly does
|
|
854
853
|
not describe one of the things a person can ask for is worse than no table. So
|
|
855
854
|
count the rows below against that number before you trust either. `CUT_BRANCH`
|
|
856
|
-
is the ninth, R260's; `OPEN_PR` the tenth, R261's.
|
|
855
|
+
is the ninth, R260's; `OPEN_PR` the tenth, R261's, `CREATE_BRANCH` the eleventh, R379's.
|
|
857
856
|
|
|
858
857
|
| | |
|
|
859
858
|
|---|---|
|
|
@@ -867,6 +866,7 @@ is the ninth, R260's; `OPEN_PR` the tenth, R261's.
|
|
|
867
866
|
| `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. |
|
|
868
867
|
| `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. |
|
|
869
868
|
| `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. |
|
|
869
|
+
| `CREATE_BRANCH` | R379's New branch from the console: pushes `origin/<fromBranch>` to `refs/heads/<name>` for the name in `message`. Unlike `CUT_BRANCH` a name already on origin is a **refusal**, because the person asked for a new branch and the existing one may be somebody else's work. Touches no working tree; a daemon that knows the kind says `createBranch: true` in its hello. |
|
|
870
870
|
|
|
871
871
|
**A `MERGE` that fails also says which KIND of failure it was** — R155, in one
|
|
872
872
|
of five words beside `gh`'s own unchanged text:
|
|
@@ -970,6 +970,15 @@ files at all. R47 removes the need for this by giving each run a workspace.
|
|
|
970
970
|
— the branch rewrote the same lines — the run fails **naming the stash and
|
|
971
971
|
the command to recover it**, because work parked somewhere nobody was told
|
|
972
972
|
about is work lost.
|
|
973
|
+
|
|
974
|
+
**A refused move to RUNNING is never the run's failure** (i267). The move
|
|
975
|
+
can be refused only when the platform no longer holds this claim: the run
|
|
976
|
+
was cancelled while the checkout was prepared, or put back in the queue. The
|
|
977
|
+
daemon then logs `the platform did not let this run start`, posts nothing,
|
|
978
|
+
and leaves the run exactly as the platform has it; if it is QUEUED, the next
|
|
979
|
+
poll offers it again and it is claimed properly. Only 400, 404, 409 and 410
|
|
980
|
+
count as a refusal; any other status (401, 403, 408, 429), a network error or
|
|
981
|
+
a 5xx still fails the run.
|
|
973
982
|
3. **Spawns the agent** in that directory, in its own process group, with the
|
|
974
983
|
run token in its environment and an `.mcp.json` pointing at cawdev's MCP
|
|
975
984
|
server. **Your own token never reaches the child.**
|
|
@@ -1167,6 +1176,63 @@ Before this, R325's run finished with a file modified and nothing pushed, and
|
|
|
1167
1176
|
the next run in that checkout refused to start on top of it. A checkout with no
|
|
1168
1177
|
remote commits what is left and asks nothing of a push.
|
|
1169
1178
|
|
|
1179
|
+
## Git hosts: GitHub, GitLab and Bitbucket — and what the agent may push (R565)
|
|
1180
|
+
|
|
1181
|
+
Branches, pull requests and merges are opened **from cawdev** and performed by
|
|
1182
|
+
this daemon, with credentials that live on this machine. The platform never
|
|
1183
|
+
holds one. A project says which host it is on (`/projects/*/settings`, "Git
|
|
1184
|
+
host"), or the daemon works it out from `origin`.
|
|
1185
|
+
|
|
1186
|
+
| Host | How the daemon talks to it |
|
|
1187
|
+
|---|---|
|
|
1188
|
+
| GitHub | `gh`, signed in (`gh auth login`) |
|
|
1189
|
+
| GitLab | `glab`, signed in (`glab auth login --hostname <host>`) |
|
|
1190
|
+
| Bitbucket Cloud (bitbucket.org) | its REST API, with a token this machine keeps |
|
|
1191
|
+
|
|
1192
|
+
Bitbucket Server / Data Center is not supported.
|
|
1193
|
+
|
|
1194
|
+
**Bitbucket's token** goes in the environment, and the config names the
|
|
1195
|
+
variable — never the token itself:
|
|
1196
|
+
|
|
1197
|
+
```json
|
|
1198
|
+
{
|
|
1199
|
+
"bitbucket": { "email": "you@example.com", "tokenEnv": "BITBUCKET_TOKEN" }
|
|
1200
|
+
}
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
With `email`, the token is an Atlassian API token and is sent as Basic
|
|
1204
|
+
`email:token`. Without it, it is a workspace or repository access token, sent
|
|
1205
|
+
as a Bearer token. `tokenEnv` defaults to `BITBUCKET_TOKEN`. The daemon logs,
|
|
1206
|
+
once at boot, where it reads the token from (or that it is missing). That
|
|
1207
|
+
variable is **removed from the environment of every agent session** it starts;
|
|
1208
|
+
`GH_TOKEN` and `GITLAB_TOKEN` are not, because `git push` may authenticate
|
|
1209
|
+
through them.
|
|
1210
|
+
|
|
1211
|
+
**After updating, restart the daemon.** A daemon that predates this does not
|
|
1212
|
+
advertise `bitbucket` in its `gitHosts`, and cawdev will not address a
|
|
1213
|
+
Bitbucket project's pull request or merge to it: the button says "No machine of
|
|
1214
|
+
yours runs a cawdev runner that speaks Bitbucket. Restart the daemon to update
|
|
1215
|
+
it."
|
|
1216
|
+
|
|
1217
|
+
### What the agent may push
|
|
1218
|
+
|
|
1219
|
+
A coding session may commit on its card's branch and push **that branch and
|
|
1220
|
+
nothing else**. For Claude Code the daemon passes a `PreToolUse` hook
|
|
1221
|
+
(`push-guard.mjs`) and disallows `gh` and `glab`: a host CLI, a force, a delete,
|
|
1222
|
+
`--mirror`, `--all`, a `+` refspec, or a push to any other branch is refused
|
|
1223
|
+
with a reason the session reads. A release session may also push its own tag.
|
|
1224
|
+
|
|
1225
|
+
After **any** writing session, for Claude, Codex and Antigravity alike, the
|
|
1226
|
+
daemon compares the remote with what it held before and, if the session pushed a
|
|
1227
|
+
ref other than its branch, says so on the run: *"This session pushed `<ref>`,
|
|
1228
|
+
which is outside its branch …"*. Deletions cannot be attributed to a session and
|
|
1229
|
+
are not flagged.
|
|
1230
|
+
|
|
1231
|
+
**This is a guard against mistakes, not a security boundary.** An agent with a
|
|
1232
|
+
shell and your credentials can get round a pattern match (`git -c alias.x='!…'`,
|
|
1233
|
+
a host CLI called through a script). **The real wall is branch protection on the
|
|
1234
|
+
host: protect the default branch and require a pull request.** Do that.
|
|
1235
|
+
|
|
1170
1236
|
## What a project's rules can ask of it (R40)
|
|
1171
1237
|
|
|
1172
1238
|
A project may decide that a finished run **pushes its branch**, **opens a pull
|
|
@@ -1257,12 +1323,9 @@ rest queue.
|
|
|
1257
1323
|
|
|
1258
1324
|
**Questions are not serialised.** An `ASK` run prepares nothing and writes
|
|
1259
1325
|
nothing, so it runs alongside whatever else is going on — you can ask about a
|
|
1260
|
-
project while an agent is working in it.
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
this daemon has claimed, not children it has spawned: a claim takes a second or
|
|
1264
|
-
two to become a process, and counting processes let one pass of the loop claim
|
|
1265
|
-
the whole queue.
|
|
1326
|
+
project while an agent is working in it. Nothing bounds a question, a roadmap session or an audit but the platform's
|
|
1327
|
+
own queue: `maxSessions` was removed in R109, and the workspace gate above is
|
|
1328
|
+
the only one.
|
|
1266
1329
|
|
|
1267
1330
|
## If the daemon dies mid-run
|
|
1268
1331
|
|
|
@@ -1343,6 +1406,19 @@ bound to it state the numbers first. There is nothing to configure here and
|
|
|
1343
1406
|
nothing to upgrade: the platform sets the same `paused` flag a person's click
|
|
1344
1407
|
sets, and this daemon already obeys it on the next beat.
|
|
1345
1408
|
|
|
1409
|
+
**A queued run says why a paused machine leaves it queued (R571).** The daemon
|
|
1410
|
+
logs and shows one of two lines, from `pause-note.mjs` (pure, tested):
|
|
1411
|
+
`this machine is paused from the console (resume it on Settings → Machines, or
|
|
1412
|
+
press c)` for a person's pause, and `cawdev paused this machine for usage: its
|
|
1413
|
+
five-hour window is at 92%, back in 1h 20m — it resumes by itself` for the
|
|
1414
|
+
platform's. The attached terminal's `c` menu has a **Pause this machine** /
|
|
1415
|
+
**Resume this machine** row, which makes the first line's "press c" true: it
|
|
1416
|
+
writes the platform through `POST /api/runners/{id}/pause` (`setPaused` in
|
|
1417
|
+
`configure.mjs`), never the config file, and the daemon reads it off its next
|
|
1418
|
+
heartbeat. A running daemon needs a **restart** to show these lines and to tell
|
|
1419
|
+
attached terminals about a pause; an older one keeps saying *paused from the
|
|
1420
|
+
console* and has no pause row in `c`.
|
|
1421
|
+
|
|
1346
1422
|
## Trying it without spending Claude usage
|
|
1347
1423
|
|
|
1348
1424
|
`stub-agent.mjs` stands in for `claude`. It is spawned the same way, talks to
|
|
@@ -1391,7 +1467,7 @@ permission stops forever.
|
|
|
1391
1467
|
|
|
1392
1468
|
- `--permission-mode acceptEdits` lets it write files.
|
|
1393
1469
|
- `--allowedTools mcp__cawdev__…` lets it use the cawdev tools. **Without this
|
|
1394
|
-
the entire loop is unreachable** — no reading the task, no moving the
|
|
1470
|
+
the entire loop is unreachable** — no reading the task, no moving the card,
|
|
1395
1471
|
no reporting, no asking. A real session found this by being denied
|
|
1396
1472
|
`task_current` and stopping rather than guessing, which was the right call
|
|
1397
1473
|
and a good sign for the prompt.
|
|
@@ -1417,7 +1493,7 @@ never asks you anything — an unlisted tool is refused, not raised (i138).
|
|
|
1417
1493
|
|
|
1418
1494
|
**Anything else stops and asks you** (R51). Rather than being denied in
|
|
1419
1495
|
silence, a session that needs `mvn` or `npm` posts a request that appears in
|
|
1420
|
-
your
|
|
1496
|
+
your Waiting on you page and on the run, and waits for an answer. Allow it once, or allow it
|
|
1421
1497
|
always here — which writes a rule on the project that every runner serving it
|
|
1422
1498
|
inherits.
|
|
1423
1499
|
|
|
@@ -1548,8 +1624,8 @@ after `--mcp-config` fails with `ENAMETOOLONG`, which names neither the flag nor
|
|
|
1548
1624
|
the prompt.
|
|
1549
1625
|
|
|
1550
1626
|
The prompt deliberately teaches the **method**, not the task. The task is in the
|
|
1551
|
-
roadmap
|
|
1552
|
-
in the prompt too would be a second copy that can disagree with the
|
|
1627
|
+
roadmap card, which the agent reads for itself with `task_current` — putting it
|
|
1628
|
+
in the prompt too would be a second copy that can disagree with the card.
|
|
1553
1629
|
|
|
1554
1630
|
### Codex CLI
|
|
1555
1631
|
|
package/runner/attach.mjs
CHANGED
|
@@ -52,7 +52,7 @@ import { basename, dirname } from 'node:path';
|
|
|
52
52
|
import { listSockets, socketPathFor } from './control.mjs';
|
|
53
53
|
import {
|
|
54
54
|
RESTART_FROM_ATTACH, addProject, addWorkspace, agentsOf, pathsOf, readConfigFile, reloadIfRunning,
|
|
55
|
-
removeProject, setAcceptsRulesFromConsole, setAgentEnabled, setQuality, slugsOf,
|
|
55
|
+
removeProject, setAcceptsRulesFromConsole, setAgentEnabled, setMaxPlanningRuns, setPaused, setQuality, slugsOf,
|
|
56
56
|
} from './configure.mjs';
|
|
57
57
|
import {
|
|
58
58
|
TYPE_A_PATH, absolute, completePath, describe, inspectPath, listDirectories,
|
|
@@ -779,6 +779,25 @@ export function configMenu(file, runner = {}, gone = new Set()) {
|
|
|
779
779
|
rulesOn
|
|
780
780
|
? { id: 'rules', label: 'Stop accepting permission rules from the console', hint: 'on now' }
|
|
781
781
|
: { id: 'rules', label: 'Accept permission rules from the console', hint: 'off now' },
|
|
782
|
+
// R577: a number on the platform, not a switch in the file — the hello
|
|
783
|
+
// carries what the daemon was last told, so the row reads the live value.
|
|
784
|
+
// R571: the row that makes a queued line's "or press c" true. R288: the
|
|
785
|
+
// label is the change, the hint the current value.
|
|
786
|
+
{
|
|
787
|
+
id: 'pause',
|
|
788
|
+
// A usage pause has no toggle: the sweep would pause it again within a minute.
|
|
789
|
+
label: runner.pausedByUsage
|
|
790
|
+
? 'Paused by cawdev for usage'
|
|
791
|
+
: runner.paused ? 'Resume this machine — claim new runs again' : 'Pause this machine — claim nothing new',
|
|
792
|
+
hint: runner.pausedByUsage
|
|
793
|
+
? 'resumes by itself when the window resets; change the threshold on Settings → Machines'
|
|
794
|
+
: runner.paused ? 'paused now' : 'claiming now',
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
id: 'planning-limit',
|
|
798
|
+
label: 'Limit planning runs at once',
|
|
799
|
+
hint: runner.maxPlanningRuns ? `${runner.maxPlanningRuns} at once now` : 'no limit now',
|
|
800
|
+
},
|
|
782
801
|
// R300: one row per served project, saying whether its branches are
|
|
783
802
|
// scanned here now — the state the row would change, R288's rule.
|
|
784
803
|
...slugs.map((slug) => (qualityOf(file.projects[slug])
|
|
@@ -2573,6 +2592,32 @@ export class Attached {
|
|
|
2573
2592
|
this.planned = this.planned.filter((card) => card.slug !== slug);
|
|
2574
2593
|
return this.reloadDaemon(file, next);
|
|
2575
2594
|
}
|
|
2595
|
+
if (chosen.id === 'pause') {
|
|
2596
|
+
if (this.runner.pausedByUsage) {
|
|
2597
|
+
this.say(` ${ink.muted('cawdev paused this machine for usage; it resumes by itself. Change the threshold on Settings → Machines.')}`);
|
|
2598
|
+
return this.note('');
|
|
2599
|
+
}
|
|
2600
|
+
// Writes the platform, which only a person may do, and not the file.
|
|
2601
|
+
if (!this.requireSignIn('pause or resume this machine')) return undefined;
|
|
2602
|
+
const runner = await setPaused({
|
|
2603
|
+
session: this.session, runnerId: this.runner.id, paused: this.runner.paused !== true, say, ink,
|
|
2604
|
+
});
|
|
2605
|
+
this.runner.paused = runner?.paused === true;
|
|
2606
|
+
this.runner.pausedByUsage = runner?.pausedByUsage === true;
|
|
2607
|
+
return this.note('');
|
|
2608
|
+
}
|
|
2609
|
+
if (chosen.id === 'planning-limit') {
|
|
2610
|
+
// Writes the platform, which only a person may do, and not the file — so
|
|
2611
|
+
// no reload: the daemon reads the number off its next heartbeat.
|
|
2612
|
+
if (!this.requireSignIn('limit planning runs at once')) return undefined;
|
|
2613
|
+
const answer = (await ask.line(' How many at once (0 for no limit)? ')).trim();
|
|
2614
|
+
const limit = answer === '' ? NaN : Number(answer);
|
|
2615
|
+
const runner = await setMaxPlanningRuns({
|
|
2616
|
+
session: this.session, runnerId: this.runner.id, limit, say, ink,
|
|
2617
|
+
});
|
|
2618
|
+
this.runner.maxPlanningRuns = runner?.maxPlanningRuns ?? null;
|
|
2619
|
+
return this.note('');
|
|
2620
|
+
}
|
|
2576
2621
|
if (chosen.id === 'rules') {
|
|
2577
2622
|
const next = await setAcceptsRulesFromConsole({
|
|
2578
2623
|
file, configPath, ask, say, ink, enabled: file.acceptsRulesFromConsole !== true,
|
package/runner/cawdev.mjs
CHANGED
|
@@ -30,9 +30,9 @@ import { asker, askOne, mintForThisMachine, setUpThisMachine } from './bootstrap
|
|
|
30
30
|
import { liveSockets, probeSocket, socketPathFor } from './control.mjs';
|
|
31
31
|
import {
|
|
32
32
|
addProject, addWorkspace, readConfigFile, reloadIfRunning, removeProject, setAcceptsRulesFromConsole,
|
|
33
|
-
setQuality, setAgentEnabled,
|
|
33
|
+
setMaxPlanningRuns, setQuality, setAgentEnabled, thisRunnerId,
|
|
34
34
|
} from './configure.mjs';
|
|
35
|
-
import { restartNeededFor } from '../lib/runner-config.mjs';
|
|
35
|
+
import { restartNeededFor, runnerNameOf } from '../lib/runner-config.mjs';
|
|
36
36
|
import { Select } from './select.mjs';
|
|
37
37
|
import { storedSession } from './sign-in.mjs';
|
|
38
38
|
import { loadToken } from './token-store.mjs';
|
|
@@ -69,6 +69,7 @@ const USAGE = `
|
|
|
69
69
|
cawdev config rules on|off accept permission rules from the console
|
|
70
70
|
cawdev config quality on|off <slug> scan that project's branches with
|
|
71
71
|
SonarQube after a run's TEST stage
|
|
72
|
+
cawdev config planning-limit <n> how many planning runs (PLAN, SCOPE, AUDIT, REVIEW) run here at once; 0 is no limit
|
|
72
73
|
cawdev config show this machine's config, token redacted
|
|
73
74
|
|
|
74
75
|
On a machine with no config, cawdev sets one up: it signs you in through the
|
|
@@ -473,6 +474,7 @@ export async function pickAction(ask, say, pick = askOne) {
|
|
|
473
474
|
{ id: 'rules-off', label: 'Stop accepting rules from the console' },
|
|
474
475
|
{ id: 'quality-on', label: 'Scan a project\'s branches with SonarQube after TEST' },
|
|
475
476
|
{ id: 'quality-off', label: 'Stop scanning a project\'s branches' },
|
|
477
|
+
{ id: 'planning-limit', label: 'Limit planning runs at once' },
|
|
476
478
|
{ id: 'show', label: 'Show this machine\'s config' },
|
|
477
479
|
],
|
|
478
480
|
}));
|
|
@@ -484,6 +486,9 @@ export async function pickAction(ask, say, pick = askOne) {
|
|
|
484
486
|
if (id === 'remove-project') {
|
|
485
487
|
return ['remove-project', (await ask.line(' Which project (slug)? ')).trim()];
|
|
486
488
|
}
|
|
489
|
+
if (id === 'planning-limit') {
|
|
490
|
+
return ['planning-limit', (await ask.line(' How many at once (0 for no limit)? ')).trim()];
|
|
491
|
+
}
|
|
487
492
|
if (id === 'rules-on') return ['rules', 'on'];
|
|
488
493
|
if (id === 'rules-off') return ['rules', 'off'];
|
|
489
494
|
if (id === 'quality-on' || id === 'quality-off') {
|
|
@@ -547,6 +552,19 @@ export async function runAction([verb, ...rest], { configPath, file, url, sessio
|
|
|
547
552
|
await reload(await setQuality({ file, configPath, ask, say, ink, slug, enabled: sub === 'on' }));
|
|
548
553
|
return;
|
|
549
554
|
}
|
|
555
|
+
if (verb === 'planning-limit') {
|
|
556
|
+
// R577. Written to the PLATFORM, not the file, so no `reload`: there is no
|
|
557
|
+
// key for a daemon to re-read, and it learns the number from its heartbeat.
|
|
558
|
+
const [raw] = rest;
|
|
559
|
+
const limit = raw === undefined || String(raw).trim() === '' ? NaN : Number(raw);
|
|
560
|
+
if (!Number.isInteger(limit) || limit < 0 || limit > 50) {
|
|
561
|
+
throw new Error('Name a number: cawdev config planning-limit <0-50>');
|
|
562
|
+
}
|
|
563
|
+
await setMaxPlanningRuns({
|
|
564
|
+
session, runnerId: await thisRunnerId(session, runnerNameOf(file)), limit, say, ink,
|
|
565
|
+
});
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
550
568
|
throw new Error(`Not a cawdev config command: ${verb}. See cawdev --help.`);
|
|
551
569
|
}
|
|
552
570
|
|
package/runner/configure.mjs
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
// the hello — applied, or these keys need a restart — never this file's guess
|
|
19
19
|
// about which keys the daemon can take.
|
|
20
20
|
//
|
|
21
|
+
// R577: `setMaxPlanningRuns` is the one setting here that writes the PLATFORM
|
|
22
|
+
// and not the file — a limit on planning runs is a per-machine value the
|
|
23
|
+
// console also edits, so it lives where both can reach it. It therefore never
|
|
24
|
+
// goes through `reloadIfRunning`: the daemon reads it off its next heartbeat.
|
|
25
|
+
//
|
|
21
26
|
// R288: the same functions have a SECOND door — the attached terminal's `c`
|
|
22
27
|
// key — and that is why every question here comes in through a parameter.
|
|
23
28
|
// `ask.line` and `pick` are the two shapes a question takes (a line, or one
|
|
@@ -457,6 +462,63 @@ export async function removeProject({
|
|
|
457
462
|
return next;
|
|
458
463
|
}
|
|
459
464
|
|
|
465
|
+
/**
|
|
466
|
+
* The id the platform knows this machine by — R577.
|
|
467
|
+
*
|
|
468
|
+
* Looked up by the name the daemon registers under (`runnerNameOf`), among the
|
|
469
|
+
* machines that are yours: the config file does not hold the id, and two
|
|
470
|
+
* people's laptops may both be called `laptop`.
|
|
471
|
+
*/
|
|
472
|
+
export async function thisRunnerId(session, name) {
|
|
473
|
+
const runners = await session.request('/api/runners');
|
|
474
|
+
const mine = (Array.isArray(runners) ? runners : [])
|
|
475
|
+
.filter((each) => each.yours && each.name === name);
|
|
476
|
+
if (mine.length === 0) {
|
|
477
|
+
throw new Error(`cawdev has no machine of yours called "${name}" — has its daemon run once?`);
|
|
478
|
+
}
|
|
479
|
+
if (mine.length > 1) {
|
|
480
|
+
throw new Error(`More than one machine of yours is called "${name}" (${mine.map((each) => each.id).join(', ')}) — set it on the Runners page.`);
|
|
481
|
+
}
|
|
482
|
+
return mine[0].id;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* How many planning runs this machine runs at once — R577. 0 is no limit.
|
|
487
|
+
*
|
|
488
|
+
* Posted to the owner-only pause endpoint, whose "absent leaves it alone"
|
|
489
|
+
* means nothing else about the machine moves. Refused before any request when
|
|
490
|
+
* the number is not one, so a typo costs no round trip.
|
|
491
|
+
*/
|
|
492
|
+
export async function setMaxPlanningRuns({ session, runnerId, limit, say, ink = painter(3) }) {
|
|
493
|
+
if (!Number.isInteger(limit) || limit < 0 || limit > 50) {
|
|
494
|
+
throw new Error('A planning limit is a whole number from 0 (no limit) to 50.');
|
|
495
|
+
}
|
|
496
|
+
const runner = await session.request(`/api/runners/${runnerId}/pause`, {
|
|
497
|
+
method: 'POST',
|
|
498
|
+
body: { maxPlanningRuns: limit },
|
|
499
|
+
});
|
|
500
|
+
say(` ${ink.success('✓')} ${ink.muted('planning runs at once:')} ${ink.text(limit ? String(limit) : 'no limit')}`
|
|
501
|
+
+ ` ${ink.muted('— the daemon applies it on its next heartbeat')}`);
|
|
502
|
+
return runner;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Pause this machine or resume it — R73, offered in the attached terminal's `c`
|
|
507
|
+
* menu by R571 so the queued line's "press c" is true. Writes the platform,
|
|
508
|
+
* like `setMaxPlanningRuns`, through the call Settings → Machines makes; the
|
|
509
|
+
* daemon reads it off its next heartbeat and no file is touched.
|
|
510
|
+
*/
|
|
511
|
+
export async function setPaused({ session, runnerId, paused, say, ink = painter(3) }) {
|
|
512
|
+
const runner = await session.request(`/api/runners/${runnerId}/pause`, {
|
|
513
|
+
method: 'POST',
|
|
514
|
+
body: { paused: paused === true },
|
|
515
|
+
});
|
|
516
|
+
say(` ${ink.success('✓')} ${ink.muted(paused ? 'paused:' : 'resumed:')} `
|
|
517
|
+
+ `${ink.text(paused ? 'claiming nothing new' : 'claiming again')}`
|
|
518
|
+
+ ` ${ink.muted('— the daemon applies it on its next heartbeat')}`);
|
|
519
|
+
return runner;
|
|
520
|
+
}
|
|
521
|
+
|
|
460
522
|
/** Turn `acceptsRulesFromConsole` on or off — R126. */
|
|
461
523
|
export async function setAcceptsRulesFromConsole({
|
|
462
524
|
file, configPath, enabled,
|