cawdev-cli 1.0.5-beta → 1.0.6-beta

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/lib/agents.mjs CHANGED
@@ -25,3 +25,41 @@ export function agentOfCommand(command) {
25
25
  export function agentLabel(command) {
26
26
  return LABELS[agentOfCommand(command)];
27
27
  }
28
+
29
+ /**
30
+ * Why an agent that needs the whole machine was refused, and what to do about
31
+ * it — in steps, for THIS machine. Codex and Antigravity have no per-command
32
+ * allow-list, so a session that writes can only run with everything allowed;
33
+ * the refusal used to say "turn on allow everything" and nothing about where,
34
+ * or that the machine must first agree to take rules from the console.
35
+ *
36
+ * Pure, so the wording is read in a test rather than found in a failed run.
37
+ *
38
+ * @param agent `codex` or `agy`
39
+ * @param machine `{ name, acceptsConsoleRules, configPath, consoleUrl }`
40
+ * @param run `{ entryRef, profile }` — what to start again, and how
41
+ * @param readsWithout whether a read-only session of this agent needs nothing
42
+ */
43
+ export function needsEverything(agent, machine = {}, run = {}, { readsWithout = true } = {}) {
44
+ const who = LABELS[agent] ?? agent;
45
+ const short = agent === 'agy' ? 'agy' : agent;
46
+ const again = run.entryRef ? `/new ${run.profile === 'PLAN' ? 'plan' : 'dev'} ${run.entryRef}` : '/new …';
47
+ const lines = [
48
+ `${who} cannot ${readsWithout ? 'write code' : 'run'} on this machine yet: it has no per-command `
49
+ + 'permissions, so a session that commits and pushes must be allowed to run anything — and '
50
+ + 'this machine has not allowed everything.',
51
+ '',
52
+ 'To allow it:',
53
+ machine.acceptsConsoleRules
54
+ ? ' 1. ✓ This machine already accepts permission rules from the console.'
55
+ : ' 1. On this machine: in the cawdev terminal press c → "Accept permission rules from the '
56
+ + `console"${machine.configPath ? ` (or "acceptsRulesFromConsole": true in ${machine.configPath})` : ''}.`,
57
+ ` 2. On cawdev: ${machine.consoleUrl ? `${machine.consoleUrl.replace(/\/+$/, '')}/settings/runners` : 'Settings → Runners'}`
58
+ + ` → ${machine.name ?? 'this machine'} → tick "Allow everything".`,
59
+ ` 3. Start it again: ${again} --${short}`,
60
+ '',
61
+ 'Allowing everything lets every unattended session on this machine run any command in its checkouts.',
62
+ `Or, without it: ${again} --claude${readsWithout && agent === 'codex' && run.entryRef ? ` — or keep ${who} for plans (/new plan ${run.entryRef} --codex)` : ''}.`,
63
+ ];
64
+ return lines.join('\n');
65
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The shell commands a Claude Code session started in the BACKGROUND and never
3
+ * saw the end of — read off its stream-json events.
4
+ *
5
+ * A lifecycle stage is one turn (R112): the daemon closes the session the
6
+ * moment the turn ends. A stage that ran `npm test` with `run_in_background`
7
+ * and ended its turn to wait for it loses the run — the result never comes
8
+ * back, nothing is recorded, and the walk marched on to MEMORY and FINISHED
9
+ * with the work unproven and uncommitted (R15, saasng). So the walk asks this,
10
+ * at the stage's end, whether anything it started is unfinished or unread;
11
+ * `walkLifecycle` then fails the stage instead of calling it done, which is
12
+ * what lets `/continue` pick the run back up at that stage.
13
+ *
14
+ * Pure: events in, a list out. Three tool shapes, by name:
15
+ * - `Bash` with `run_in_background: true` starts one; its result names the id
16
+ * ("… running in background with ID: <id>");
17
+ * - `BashOutput` (or `TaskOutput`) reads one by `bash_id`/`shell_id`/`task_id`;
18
+ * the command is over only when a read says a status other than running;
19
+ * - `KillShell`/`KillBash`/`TaskStop` ends one.
20
+ * A command whose id cannot be read off its result stays unfinished: failing
21
+ * loudly and re-running a stage costs a turn, calling unproven work proven
22
+ * costs somebody's afternoon.
23
+ *
24
+ * @author Claude Opus 5.5 (Claude Code)
25
+ * @author Moez Ben MBarka
26
+ */
27
+
28
+ const READS = new Set(['BashOutput', 'TaskOutput']);
29
+ const KILLS = new Set(['KillShell', 'KillBash', 'TaskStop']);
30
+
31
+ /** The text of a tool_result's content, whichever of its two shapes it came in. */
32
+ function textOf(content) {
33
+ if (typeof content === 'string') return content;
34
+ if (Array.isArray(content)) {
35
+ return content.map((part) => (typeof part?.text === 'string' ? part.text : '')).join('\n');
36
+ }
37
+ return '';
38
+ }
39
+
40
+ function shellIdOf(input) {
41
+ const id = input?.bash_id ?? input?.shell_id ?? input?.task_id ?? input?.id;
42
+ return typeof id === 'string' && id ? id : null;
43
+ }
44
+
45
+ export class BackgroundShells {
46
+ constructor() {
47
+ /** tool_use id of each background `Bash` → { command, shellId, done }. */
48
+ this.started = new Map();
49
+ /** tool_use id of each read → the shell id it read. */
50
+ this.reads = new Map();
51
+ }
52
+
53
+ /** One stream-json event. Anything that is not about a shell is ignored. */
54
+ observe(event) {
55
+ const parts = event?.message?.content;
56
+ if (!Array.isArray(parts)) return;
57
+ if (event.type === 'assistant') {
58
+ for (const part of parts) {
59
+ if (part?.type !== 'tool_use') continue;
60
+ if (part.name === 'Bash' && part.input?.run_in_background === true) {
61
+ this.started.set(part.id, {
62
+ command: String(part.input.command ?? '').trim(),
63
+ shellId: null,
64
+ done: false,
65
+ });
66
+ } else if (READS.has(part.name)) {
67
+ const id = shellIdOf(part.input);
68
+ if (id) this.reads.set(part.id, id);
69
+ } else if (KILLS.has(part.name)) {
70
+ this.finish(shellIdOf(part.input));
71
+ }
72
+ }
73
+ return;
74
+ }
75
+ if (event.type === 'user') {
76
+ for (const part of parts) {
77
+ if (part?.type !== 'tool_result') continue;
78
+ const text = textOf(part.content);
79
+ const launched = this.started.get(part.tool_use_id);
80
+ if (launched && !launched.shellId) {
81
+ const id = /\bID:?\s*([A-Za-z0-9_-]+)/.exec(text);
82
+ if (id) launched.shellId = id[1];
83
+ continue;
84
+ }
85
+ const read = this.reads.get(part.tool_use_id);
86
+ if (read) {
87
+ // Over only when the read says so; a read of a command still going
88
+ // is the command still going.
89
+ if (!/<status>\s*running\s*<\/status>|\bstatus:\s*running\b/i.test(text)) {
90
+ this.finish(read);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ finish(shellId) {
98
+ if (!shellId) return;
99
+ for (const each of this.started.values()) {
100
+ if (each.shellId === shellId) each.done = true;
101
+ }
102
+ }
103
+
104
+ /** The commands started in the background whose end this session never saw. */
105
+ unfinished() {
106
+ return [...this.started.values()].filter((each) => !each.done).map((each) => each.command);
107
+ }
108
+ }
package/lib/codex.mjs CHANGED
@@ -120,6 +120,11 @@ export function codexArgs({
120
120
  if (server.disabledTools?.length) {
121
121
  args.push('-c', `${key}.disabled_tools=${tomlValue(server.disabledTools)}`);
122
122
  }
123
+ // How long one call may take before Codex gives up on it — 60 s unless
124
+ // set, which is shorter than a person answering a question.
125
+ if (server.toolTimeoutSec) {
126
+ args.push('-c', `${key}.tool_timeout_sec=${tomlValue(server.toolTimeoutSec)}`);
127
+ }
123
128
  args.push('-c', `${key}.default_tools_approval_mode="approve"`);
124
129
  }
125
130
  if (resumeId) {
@@ -255,6 +255,23 @@ export function parseRoadmap(text) {
255
255
  entry.afterRefs = tokens.map(({ ref }) => ref);
256
256
  continue;
257
257
  }
258
+ const tags = /^Tags:\s*(.+)$/.exec(line.trim());
259
+ if (tags) {
260
+ // R315. Already normalised by the server (lowercase, no `#`) by the
261
+ // time it reaches the file, so a plain split is the whole rule.
262
+ entry.tags = tags[1].split(/[,\s]+/).map((t) => t.trim()).filter(Boolean);
263
+ continue;
264
+ }
265
+ // R315. Computed, never stored, and never sent by import.mjs — the same
266
+ // arrangement as `Development:` above. Kept as the exact text so a file
267
+ // parsed and re-rendered without asking the API again is still a fixed
268
+ // point; `renderEntry` recomputes the same text from a live `origin`
269
+ // object when there is one.
270
+ const origin = /^Origin:\s*(.+)$/.exec(line.trim());
271
+ if (origin) {
272
+ entry.origin = origin[1].trim();
273
+ continue;
274
+ }
258
275
  const sprint = /^Sprint:\s*S(\d+)\s*(.*)$/.exec(line.trim());
259
276
  if (sprint) {
260
277
  // R257: `Sprint: S1 Notifications` — the sprint's number and its name
@@ -360,6 +377,33 @@ export function renderRoadmap(entries, { preamble, sectionOrder = [] }) {
360
377
  return parts.join('\n').replace(/\n+$/, '') + '\n';
361
378
  }
362
379
 
380
+ /**
381
+ * The word `Origin:` writes — R315. `entry.origin` is a plain string once it
382
+ * has round-tripped through the file (see the parser above); freshly off the
383
+ * API it is `{kind, label, url}`, and this is the one place that turns the
384
+ * second into the first, so the two paths render identically.
385
+ */
386
+ function originWord(origin) {
387
+ if (typeof origin === 'string') {
388
+ return origin;
389
+ }
390
+ if (!origin || origin.kind === 'NONE') {
391
+ return null;
392
+ }
393
+ switch (origin.kind) {
394
+ case 'SONARQUBE':
395
+ return 'SonarQube';
396
+ case 'FEEDBACK':
397
+ return 'Feedback';
398
+ case 'APPLICATION':
399
+ return origin.label ? `An application (${origin.label})` : 'An application';
400
+ case 'SESSION':
401
+ return origin.label ? `A session (${origin.label})` : 'A session';
402
+ default:
403
+ return null;
404
+ }
405
+ }
406
+
363
407
  function renderEntry(entry) {
364
408
  const lines = [`### ${entryRef(entry)} — ${entry.title}`, ''];
365
409
 
@@ -425,6 +469,14 @@ function renderEntry(entry) {
425
469
  .map(({ ref }) => ref);
426
470
  lines.push(`After: ${written.join(', ')}`);
427
471
  }
472
+ if (entry.tags?.length) {
473
+ // R315. Sorted, so the export does not churn on the set's own order.
474
+ lines.push(`Tags: ${[...entry.tags].sort().join(', ')}`);
475
+ }
476
+ const origin = originWord(entry.origin);
477
+ if (origin) {
478
+ lines.push(`Origin: ${origin}`);
479
+ }
428
480
  if (entry.sprint) {
429
481
  // R257. A line on the card and never a heading: the section is where the
430
482
  // file groups, and a sprint is a set a person opened across sections. The
package/mcp/server.mjs CHANGED
@@ -665,6 +665,13 @@ const TOOLS = [
665
665
  'Cards this one starts coding after — it waits in the queue until every one of ' +
666
666
  'them is MERGED or SHIPPED. Refs, as `related`. Same project, must exist, never itself.',
667
667
  },
668
+ tags: {
669
+ type: 'array',
670
+ items: { type: 'string' },
671
+ description:
672
+ 'Free words — lowercase, letters/digits/./_/-, up to 32 characters, at least one ' +
673
+ 'letter, at most 12. The leading # is optional and stripped.',
674
+ },
668
675
  },
669
676
  required: ['title'],
670
677
  },
@@ -674,7 +681,7 @@ const TOOLS = [
674
681
  method: 'POST',
675
682
  body: pick(args, [
676
683
  'title', 'body', 'status', 'branch', 'merge', 'version', 'reason', 'section', 'related',
677
- 'after', 'sprint',
684
+ 'after', 'sprint', 'tags',
678
685
  ]),
679
686
  });
680
687
  return `Created ${refOf(entry)} in ${slug}.\n\n${formatEntry(entry, {})}`;
@@ -734,6 +741,13 @@ const TOOLS = [
734
741
  items: { type: ['string', 'integer'] },
735
742
  description: 'Refs — R12, i15; a bare number is a roadmap card (R221).',
736
743
  },
744
+ tags: {
745
+ type: 'array',
746
+ items: { type: 'string' },
747
+ description:
748
+ 'Free words — lowercase, letters/digits/./_/-, up to 32 characters, at least one ' +
749
+ 'letter, at most 12. The leading # is optional and stripped.',
750
+ },
737
751
  },
738
752
  required: ['title', 'severity'],
739
753
  },
@@ -741,7 +755,7 @@ const TOOLS = [
741
755
  const slug = await resolveProject(config, args.project);
742
756
  const issue = await api(config, `/api/projects/${slug}/issues`, {
743
757
  method: 'POST',
744
- body: pick(args, ['title', 'body', 'severity', 'status', 'related']),
758
+ body: pick(args, ['title', 'body', 'severity', 'status', 'related', 'tags']),
745
759
  });
746
760
  return `Filed ${refOf(issue)} in ${slug}.\n\n${formatEntry(issue, {})}`;
747
761
  },
@@ -779,7 +793,7 @@ const TOOLS = [
779
793
  {
780
794
  name: 'roadmap_update',
781
795
  description:
782
- 'Edit an entry\'s title, body, section, sprint, related refs or the cards it starts ' +
796
+ 'Edit an entry\'s title, body, section, sprint, tags, related refs or the cards it starts ' +
783
797
  'after. Use roadmap_set_status to move it.',
784
798
  inputSchema: {
785
799
  type: 'object',
@@ -813,6 +827,12 @@ const TOOLS = [
813
827
  'them is MERGED or SHIPPED. Refs, as `related`. Same project, must exist, never ' +
814
828
  'itself. [] clears it.',
815
829
  },
830
+ tags: {
831
+ type: 'array',
832
+ items: { type: 'string' },
833
+ description:
834
+ 'Free words — same rule as roadmap_create. Leave out to leave them alone; [] clears them.',
835
+ },
816
836
  },
817
837
  required: ['number'],
818
838
  },
@@ -821,7 +841,7 @@ const TOOLS = [
821
841
  const entry = await api(config, `/api/projects/${slug}/roadmap/${args.number}`, {
822
842
  method: 'PATCH',
823
843
  // pick keeps a present 0, which is how a card leaves its sprint.
824
- body: pick(args, ['title', 'body', 'section', 'related', 'after', 'sprint']),
844
+ body: pick(args, ['title', 'body', 'section', 'related', 'after', 'sprint', 'tags']),
825
845
  });
826
846
  return `Updated ${refOf(entry)}.\n\n${formatEntry(entry, {})}`;
827
847
  },
@@ -2065,6 +2085,23 @@ function refOf(entry) {
2065
2085
  return entry.ref ?? ((entry.kind === 'ISSUE' ? 'i' : 'R') + entry.number);
2066
2086
  }
2067
2087
 
2088
+ /**
2089
+ * The word `formatEntry` prints for a card's origin — R315. Never stored,
2090
+ * always the object shape `{kind, label, url}` off the API — unlike
2091
+ * `roadmap-format.mjs`'s own copy, which also has to read one back out of a
2092
+ * file.
2093
+ */
2094
+ function originWord(origin) {
2095
+ if (!origin || origin.kind === 'NONE') return null;
2096
+ switch (origin.kind) {
2097
+ case 'SONARQUBE': return 'SonarQube';
2098
+ case 'FEEDBACK': return 'Feedback';
2099
+ case 'APPLICATION': return origin.label ? `an application (${origin.label})` : 'an application';
2100
+ case 'SESSION': return origin.label ? `a session (${origin.label})` : 'a session';
2101
+ default: return null;
2102
+ }
2103
+ }
2104
+
2068
2105
  function formatEntry(entry, { brief, comments, plan }) {
2069
2106
  const lines = [`${refOf(entry)} — ${entry.title}`, ` status: ${entry.statusDisplay}`];
2070
2107
  if (entry.branch) lines.push(` branch: ${entry.branch}`);
@@ -2072,6 +2109,9 @@ function formatEntry(entry, { brief, comments, plan }) {
2072
2109
  if (entry.version) lines.push(` version: ${entry.version}`);
2073
2110
  if (entry.declinedReason) lines.push(` declined because: ${entry.declinedReason}`);
2074
2111
  if (entry.section) lines.push(` section: ${entry.section}`);
2112
+ // R315. Sorted for the same reason the export is: a reader's shape must not
2113
+ // depend on the set's own insertion order.
2114
+ if (entry.tags?.length) lines.push(` tags: ${[...entry.tags].sort().map((t) => `#${t}`).join(' ')}`);
2075
2115
  // R257. Always printed when the card has one, so a reader's shape does not
2076
2116
  // depend on which tool printed the card; a closed sprint says so.
2077
2117
  if (entry.sprint) {
@@ -2108,6 +2148,12 @@ function formatEntry(entry, { brief, comments, plan }) {
2108
2148
  lines.push(` written by: a ${entry.createdBy.profile} session, `
2109
2149
  + `under ${entry.createdBy.startedByEmail}`);
2110
2150
  }
2151
+ // R315. Never null on the wire — NONE is a person at the console — so only
2152
+ // ever printed when it says something. Beside `written by` rather than
2153
+ // instead of it: origin says the DOOR, and the session line above already
2154
+ // says who and what kind, when the door was a session.
2155
+ const origin = originWord(entry.origin);
2156
+ if (origin) lines.push(` origin: ${origin}`);
2111
2157
  if (!brief && entry.body) lines.push('', entry.body);
2112
2158
  // R124, between the body and the discussion — the order they are read in:
2113
2159
  // what we want, how we mean to get it, and the argument about both.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cawdev-cli",
3
- "version": "1.0.5-beta",
3
+ "version": "1.0.6-beta",
4
4
  "description": "cawdev's terminal client and runner daemon — zero dependencies, on purpose",
5
5
  "type": "module",
6
6
  "bin": {
package/runner/README.md CHANGED
@@ -101,8 +101,8 @@ with the window — there `q` stops it, and asks first when sessions are live.
101
101
  machine is configured, most changes are smaller than that. **Inside the
102
102
  terminal, press `c`** (or type `/config`): a picker over what this machine does
103
103
  — add a project, add a workspace, each agent with its current state ("Disable
104
- claude", "Enable codex"), the console-rules switch, and the config itself with the
105
- token cut short. Every row is one of the shell commands below, driven through
104
+ claude", "Enable codex"), the console-rules switch, **Stop serving** each
105
+ project, and the config itself with the token cut short. Every row is one of the shell commands below, driven through
106
106
  the terminal's own line editor and picker; `esc` anywhere in a step changes
107
107
  nothing. The screen edits the file the daemon says it booted from (the hello
108
108
  carries `configPath` since R288), so an older daemon is told to restart rather
@@ -126,6 +126,7 @@ is for scripts and for a machine with no daemon to attach to:
126
126
  cawdev config a menu, the same Select widget as everywhere else
127
127
  cawdev config add-project serve one more project you can write to
128
128
  cawdev config add-workspace another checkout for a project already served
129
+ cawdev config remove-project <slug> stop serving a project; its checkouts stay on disk
129
130
  cawdev config agent enable|disable <claude|agy|codex>
130
131
  cawdev config rules on|off accept permission rules from the console — R126
131
132
  cawdev config show this machine's config, token redacted
@@ -230,7 +231,8 @@ removes.
230
231
  `/logout` forgets it along with the session.
231
232
 
232
233
  **A paste stays one line.** Paste four hundred lines and the input shows
233
- `[pasted, 342 lines]`; all of it is sent. A paste that scrolled the transcript
234
+ `[pasted, 342 lines]`; all of it is sent — into a prompt or into a command alike
235
+ (`/new plan <paste>`, `/card <paste>`), its line breaks kept (`commandWords`). A paste that scrolled the transcript
234
236
  away would bury the thing this program exists to keep.
235
237
 
236
238
  Under `NO_COLOR` it is the same terminal without the colour: the picker still
@@ -359,8 +361,17 @@ project this machine serves included ("nothing planned" is an answer) — one
359
361
  small box each, titled with the card and its status — `R3 (Planned)` — the cards of
360
362
  every project this machine serves that are ready to be developed, read again
361
363
  every thirty seconds. **`d`** lists them and develops the one you pick, on this
362
- machine and on the card's own branch; `/new dev` with nothing after it does the
363
- same anywhere, and `/new dev R3` goes straight to one.
364
+ machine and on the card's own branch — except on a finished plan's window, where
365
+ it is that plan's `/develop` (below) with no list; `/new dev` with nothing after it does the
366
+ same anywhere, and `/new dev R3` goes straight to one. A full-width rule
367
+ under the strip separates the board from the session windows; it comes out of
368
+ the strip's own rows, never the windows'.
369
+
370
+ **`/refresh`** reads the cards again now rather than on the next tick, and
371
+ says what it found per project — `saasng: 5 considering · 2 planned · 1 in
372
+ development` — and which project could not be read, and why. A project that
373
+ cannot be read keeps its last cards and no other's: one unreadable slug used
374
+ to leave every column of every project at 0, silently.
364
375
 
365
376
  **Four windows at most.** Past four, the rest are named on one line above the
366
377
  prompt — number, kind, state, card, in the state's colour — and `tab` (or the
@@ -371,8 +382,15 @@ recently looked at first.
371
382
  A window stays up when its session ends, and its last line says what can be
372
383
  done next:
373
384
 
374
- - **`/develop`** on a finished plan starts the development of its card, on
375
- this machine (the branch is the card's own).
385
+ - **`d`** or **`/develop`** on a finished plan — the window says `d develops R2
386
+ here, in place of this plan` — starts the development of its card, on
387
+ this machine (the branch is the card's own). `/develop R30` names another
388
+ card, `@slug` its project, and `--codex`/`--claude`/`--agy` the agent —
389
+ otherwise the agent that PLANNED the card when its finished plan has a
390
+ window here, else `/agent`'s choice, exactly as `/new dev R30` does, because
391
+ it IS that command. Once the development has started the plan's window
392
+ closes, and the development's window takes its focus when this machine
393
+ takes the run; a start the platform refused leaves the plan's window up.
376
394
  - **`/pr`** on a finished session that wrote a branch opens its pull request
377
395
  (`POST runs/{id}/pull-request` — through the card's work item, or from the
378
396
  run itself for a session started from a sentence): this machine pushes the
@@ -395,7 +413,18 @@ done next:
395
413
  project's runs, kept when `carriesOnHere` — yours, `canCarryOn`, and its
396
414
  checkout on this machine or none), listed in the scrollback, and given a
397
415
  window that opens on why it stopped.
398
- - **`/open`** opens the pull request in your browser.
416
+ - **Finished, having built nothing** — a dev session that ended FINISHED
417
+ with no commit (IMPLEMENT stopped on a plan it would not follow, say) —
418
+ reads `finished · nothing built`, in amber rather than green, offers no
419
+ `/pr` but the re-plan instead, and its window shows its summary, where it
420
+ said why (`builtNothing`; the terminal reads the run's commits once it
421
+ ends, and claims nothing until it has).
422
+ - **`/open`** opens the pull request in your browser. It reads the run again
423
+ first — the runner records the link after the run is FINISHED, so the copy
424
+ taken at that moment has none — and falls back to the card's work item.
425
+ On a finished PLAN, which opens no pull request, it opens the card on cawdev
426
+ instead — `roadmap/R2?show=plan` (`issues/…` for an issue) — with the plan
427
+ of record unfolded and scrolled to: that plan is what the session produced.
399
428
  - **`/drop`** deletes the checkout the session was cloned into, when it was one
400
429
  made on demand (below): asked first, naming the path, and refused while a
401
430
  run is in it or holds it, with uncommitted files, or with a commit origin
@@ -424,6 +453,21 @@ sentence; `--anyway` starts it regardless, the same decision the console's
424
453
  panel asks for. All four commands need you signed in (`/login`), and they work
425
454
  outside advanced mode too, on the session you are watching.
426
455
 
456
+ **The prompt says which window it acts on.** In advanced mode the line at the
457
+ bottom leads with the focused window's number — `[2] cawdev ▸ /pr`, `[2]
458
+ enter prompts “…”` — and a command's answer keeps the number of the window it
459
+ was about (`[2] A session on … is running`), so a note left on screen after
460
+ the focus moved still says whose it was.
461
+
462
+ **A project deleted in the console is not offered.** The config on this
463
+ machine still names it, and nothing on the platform can change that file, so
464
+ the terminal asks: a served project whose roadmap read answers 404 — deleted,
465
+ or your access went — is left out of the picker, the strip, `/new` and `/card`,
466
+ and one line says so, once, after the mode is on screen. `c` → **Stop serving
467
+ ngsign** (`cawdev config remove-project ngsign`) takes it out of the config;
468
+ the checkouts stay where they are, and the daemon stops polling for it at its
469
+ next restart, because `projects` is read at boot.
470
+
427
471
  **Which projects it shows is yours to pick.** `/advanced-mode` on a machine
428
472
  serving several projects opens a picker with every one ticked — space (or a
429
473
  digit) ticks and unticks, `a` all or none, enter confirms — and only the
@@ -432,16 +476,73 @@ projects ticked are drawn: their windows, their roadmap on top, and the ones
432
476
  nothing; `/projects` asks again from inside the mode; `/reload` keeps the
433
477
  pick. A prompt never talks to a session of a project that is not shown.
434
478
 
479
+ **↑ is history, as in a shell.** On a line nothing has been typed on yet —
480
+ empty, or the bare `/` a command line opens with — ↑ and ↓ walk what was
481
+ typed before (kept across launches, `history.mjs`); a command line walks only
482
+ the commands. Once something is typed, the arrows move in the list of
483
+ commands instead, and enter sends a recalled line as it is. Outside advanced
484
+ mode ↑ with no line open recalls the last entry onto the line it belongs to;
485
+ in advanced mode the wheel arrives as arrows, so there it is `/` or enter,
486
+ then ↑.
487
+
488
+ **`/view`** opens the focused session's page on cawdev — transcript, stages,
489
+ card — in the browser, on the console this terminal signed in to.
490
+
491
+ **`/view R29`** prints a card here instead: its status, section, tags, branch,
492
+ work item and pull request, who is coding or reviewing it, what it is related
493
+ to, its page on the web, and the first thirty lines of its body (`cardLines`,
494
+ pure). `/view <words>` finds it by its title among the cards of the projects on
495
+ screen and offers a list when several match; `@slug` names the project;
496
+ **`/view card`** is the focused session's own card.
497
+
498
+ **Which agent `/new` asks for.** `--claude`, `--codex` or `--agy` on a `/new`
499
+ names that session's agent (`/new dev R12 --codex`); `/agent codex` makes it
500
+ the terminal's choice for every `/new` after it, kept across launches
501
+ (`prefs.mjs`, `~/.cawdev/terminal.json`) and shown in advanced mode's prompt
502
+ title; `/agent default` hands the choice back to the project. A flag beats
503
+ `/agent`, and with neither nothing is named — the project's default decides,
504
+ as it always has. A name no runner spawns is refused, by `/agent` and by the
505
+ platform alike, rather than spawned as Claude Code; `/agent codex` on a
506
+ machine that does not list codex says so (`c` → Enable codex). The window's
507
+ `Codex · GPT-5-codex` line says which one actually ran.
508
+
509
+ **A stage asks rather than stops.** A decision that is the person's — a scope
510
+ the card leaves open, a plan the code has moved away from — is asked with
511
+ `ask_user`, and the stage carries on with the answer; PLAN asks before it
512
+ writes, so a plan never hands IMPLEMENT an open question, and IMPLEMENT asks
513
+ when its plan is wrong or incomplete instead of stopping (`ASK_DONT_STOP`).
514
+ Codex is given the time to: cawdev's MCP server gets `tool_timeout_sec` 660 on
515
+ its side, longer than `ask_user`'s ten-minute wait — Codex's own default of
516
+ 60 s cut every question off before anybody could answer it.
517
+
518
+ **Codex and Antigravity write only on a machine that allows everything.**
519
+ Neither has per-command permissions — Codex's sandbox is read-only, blocks the
520
+ network (no push) or is off — so a session of theirs that writes is refused,
521
+ unspawned, until the machine accepts console rules AND "Allow everything" is
522
+ ticked on it. The refusal says so in steps, for this machine
523
+ (`needsEverything`, lib/agents.mjs): which of the two is done, the Runners page
524
+ on this console, the command to start it again — and it is written on the
525
+ run's transcript as well as its summary, so the window shows it; a failed
526
+ window prints its run's summary (`sayWhyItFailed`) rather than just "failed".
527
+
528
+ **`/card <the idea in words>`** writes a card in **Considering** and starts
529
+ nothing — no session, no branch, nothing spent: the roadmap's first column
530
+ is where an idea waits to be decided on, and writing one down does not need
531
+ a model. Titled by its words (`cardTitleFor`), the whole text in its body;
532
+ the project chosen exactly as `/new` chooses it (`@project`, else advanced
533
+ mode narrowed to one, else asked).
534
+
435
535
  **Across the top, the roadmap** (`roadmapStrip`): every project this machine
436
- serves as a small board — Considering, Planned, In development, Merged side
437
- by side, one line per card, `●` on a card with a session running now;
438
- Shipped and Declined, which only grow, are counts beside the project's name,
439
- as on the console's board. As tall as the fullest column, never more than a
536
+ serves as a small board — Considering, Planned and In development side by
537
+ side, one line per card, `●` on a card with a session running now; Merged,
538
+ Shipped and Declined — work that is over, and only grows — are counts beside
539
+ the project's name (`1 merged · 2 shipped`), as the console's board folds its
540
+ own last columns. As tall as the fullest column, never more than a
440
541
  third of the screen, and gone before the windows are on a short terminal.
441
542
  When the room runs out it is SAID: a column ends in `+N more`, empty columns
442
- go first when the width does, then whole columns (Merged, then Considering,
443
- then Planned — In development stays longest), named in the header as
444
- `merged 3 hidden`, then projects, counted as `+N projects`. Read every 30 s.
543
+ go first when the width does, then whole columns (Considering, then Planned —
544
+ In development stays longest), named in the header as `considering 3
545
+ hidden`, then projects, counted as `+N projects`. Read every 30 s.
445
546
 
446
547
  **A window says which agent and which model** — `Claude · Opus 5.5`,
447
548
  `Codex · GPT-5-codex` — bold and underlined at the head of its second line
@@ -451,16 +552,25 @@ announces on `init`, and per STAGE on a walked run, since each stage is its
451
552
  own process. An ended one's is the platform's. No model named reads
452
553
  `default model` — the CLI's own choice — rather than a guess.
453
554
 
454
- **A dev window says where it is working**: its second line is the project and
455
- the absolute path of the checkout (`subtitleFor`) — on a machine with
456
- `saasng` and its `-auto-N` clones, the one directory to open. Cut from the
457
- left when narrow, so the end of the path survives.
555
+ **A window's project has a line of its own**, the second, under the title
556
+ (`subtitleFor`) — at the end of the title a long card name pushed it out of
557
+ sight. **A dev window adds where it is working**: the project and the absolute
558
+ path of the checkout — on a machine with `saasng` and its `-auto-N` clones, the
559
+ one directory to open. Cut from the left when narrow, so the end of the path
560
+ survives.
458
561
 
459
562
  **The prompt shows what is open, not what was.** A question is the prompt's
460
563
  banner alone, whole and wrapped between words, and it goes the moment it is
461
564
  answered — here or in the console. A permission request's lines are tagged
462
565
  with it (`sayAbout`) and leave when it is decided.
463
566
 
567
+ **A question says which window it came from.** With four sessions on screen,
568
+ "waiting for an answer" is not enough to know whose. A question or a
569
+ permission request names its session (`sessionTag`) — `2 · dev · R3 Client
570
+ mail… — saasng`: the window's number, its kind, its card and its project — in
571
+ the line announcing it, in the title of the picker that answers it, and above
572
+ the banner. The card is cut to 32 columns so the question itself still fits.
573
+
464
574
  ### Checkouts made on demand: `autoWorkspaces`
465
575
 
466
576
  `"autoWorkspaces": 3` in the machine's config lets the daemon CLONE up to three
@@ -853,6 +963,19 @@ files at all. R47 removes the need for this by giving each run a workspace.
853
963
  writes to git: no merging, no branch deletion, no pushing. (A project's own
854
964
  rules can ask for all three — see below — but that is a queued action with a
855
965
  name on it, not something the survey does in passing.)
966
+ 7b. **A stage is not done while its commands are still running.** A stage is
967
+ one turn, closed the moment the turn ends, so a test suite started with
968
+ `run_in_background` and never waited for is a result lost. The daemon
969
+ follows the session's background shells (`lib/background-shells.mjs`:
970
+ `Bash` with `run_in_background`, `BashOutput`, `KillShell`); a stage whose
971
+ turn ends with one still running, or finished and never read, is reported
972
+ FAILED with the commands named, and the run fails with it — keeping its
973
+ checkout and its session (R80), so **`/continue` picks it up**. Where it
974
+ resumes is the platform's (R263: the claim's lifecycle starts at the first
975
+ stage not DONE, with what the DONE ones said); what the daemon adds is WHY
976
+ the stage stopped — the re-run is handed that and told to run its commands
977
+ in the foreground. IMPLEMENT and TEST are also told up front to
978
+ wait for every command, and TEST to commit — no stage after it writes code.
856
979
  8. **Reports the lifecycle back** and, if the session dies without saying
857
980
  anything, ends the run rather than leaving it `RUNNING` forever.
858
981