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 +38 -0
- package/lib/background-shells.mjs +108 -0
- package/lib/codex.mjs +5 -0
- package/lib/roadmap-format.mjs +52 -0
- package/mcp/server.mjs +50 -4
- package/package.json +1 -1
- package/runner/README.md +142 -19
- package/runner/attach.mjs +627 -48
- package/runner/cawdev.mjs +17 -2
- package/runner/configure.mjs +34 -0
- package/runner/input.mjs +21 -8
- package/runner/prefs.mjs +52 -0
- package/runner/runner.mjs +152 -21
- package/runner/split.mjs +109 -16
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) {
|
package/lib/roadmap-format.mjs
CHANGED
|
@@ -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
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,
|
|
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
|
|
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
|
|
363
|
-
|
|
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
|
|
375
|
-
this
|
|
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
|
-
-
|
|
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
|
|
437
|
-
|
|
438
|
-
Shipped and Declined,
|
|
439
|
-
|
|
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 (
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
455
|
-
the
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|