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/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). Three settings a running daemon takes
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 against `maxSessions` — plus the run you are watching and the keys that
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 inbox and the run page post to: one record, the same
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 `min(workspaces, maxSessions)`. A run that
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, an entry-writing session, an audit and a scoping
721
- session at once. `INTERVIEW`, `MERGE`
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 the rest back
729
- is `maxSessions`, which counts every profile — **"at 4 sessions on this machine
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 against `maxSessions`.
801
- Both gates, on screen, counting what each actually bounds — a question is in the
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 **ten** things to a checkout it serves. It said six for two entries, and
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. `maxSessions` (4 by default) bounds how
1261
- many agent processes this machine will host at once, and it is the **only**
1262
- thing bounding a question, a roadmap session or an audit (R70). It counts runs
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 entry,
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 inbox and on the run, and waits for an answer. Allow it once, or allow it
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 entry, which the agent reads for itself with `task_current` — putting it
1552
- in the prompt too would be a second copy that can disagree with the entry.
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
 
@@ -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,