cawdev-cli 1.0.4-beta → 1.0.5-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 +27 -0
- package/lib/ansi.mjs +2 -0
- package/lib/codex.mjs +214 -0
- package/lib/harness-prompt.mjs +14 -6
- package/lib/runner-config.mjs +3 -2
- package/mcp/server.mjs +3 -0
- package/package.json +1 -1
- package/runner/README.md +264 -9
- package/runner/attach.mjs +1251 -23
- package/runner/auto-workspaces.mjs +106 -0
- package/runner/bootstrap.mjs +37 -5
- package/runner/cawdev.mjs +28 -11
- package/runner/changed-lines.mjs +87 -0
- package/runner/configure.mjs +21 -5
- package/runner/runner.mjs +1111 -133
- package/runner/select.mjs +31 -4
- package/runner/split.mjs +830 -0
package/lib/agents.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The coding agents a machine can spawn, spelled once.
|
|
3
|
+
*
|
|
4
|
+
* <p>Three CLIs, each a command on the PATH: Claude Code, Antigravity and
|
|
5
|
+
* Codex. The setup walk, `cawdev config agent`, the config screen and the
|
|
6
|
+
* daemon all ask "which agent is this command" and "what is it called", and
|
|
7
|
+
* the answer was a `=== 'agy' ? … : 'Claude Code'` in five places — right for
|
|
8
|
+
* two agents and silently wrong for a third.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Every agent, in the order they are offered. */
|
|
12
|
+
export const KNOWN_AGENTS = ['claude', 'agy', 'codex'];
|
|
13
|
+
|
|
14
|
+
const LABELS = { claude: 'Claude Code', agy: 'Antigravity CLI', codex: 'Codex CLI' };
|
|
15
|
+
|
|
16
|
+
/** Which agent a configured command is — by its name, the rule `agentCommands` always used. */
|
|
17
|
+
export function agentOfCommand(command) {
|
|
18
|
+
const name = String(command ?? '').split('/').pop();
|
|
19
|
+
if (name.endsWith('agy')) return 'agy';
|
|
20
|
+
if (name.endsWith('codex')) return 'codex';
|
|
21
|
+
return 'claude';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What a person calls the agent behind a command. */
|
|
25
|
+
export function agentLabel(command) {
|
|
26
|
+
return LABELS[agentOfCommand(command)];
|
|
27
|
+
}
|
package/lib/ansi.mjs
CHANGED
|
@@ -102,6 +102,8 @@ export function painter(depth = colourDepth()) {
|
|
|
102
102
|
text: ink([0xd4, 0xd0, 0xcc], 37),
|
|
103
103
|
|
|
104
104
|
bold: wrap('\x1b[1m'),
|
|
105
|
+
/** Bold AND underlined — a fact the eye should find first (advanced mode's agent · model). */
|
|
106
|
+
strong: wrap('\x1b[1;4m'),
|
|
105
107
|
dim: wrap('\x1b[2m'),
|
|
106
108
|
reverse: wrap('\x1b[7m'),
|
|
107
109
|
/** Plain, for a value that should not compete with the label beside it. */
|
package/lib/codex.mjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI's Codex CLI as a third agent — the arguments a spawn is given and the
|
|
3
|
+
* events it prints, and nothing that touches a process.
|
|
4
|
+
*
|
|
5
|
+
* In lib/ for `code-map.mjs`'s reason: the hard parts are a command line and a
|
|
6
|
+
* stream, both testable without a daemon, a platform or a model. The spawn
|
|
7
|
+
* itself is `spawnCodex` in runner.mjs, beside `spawnAntigravity`.
|
|
8
|
+
*
|
|
9
|
+
* <p>Every flag and field here was checked against the real CLI (0.155) by
|
|
10
|
+
* hand rather than read off documentation:
|
|
11
|
+
*
|
|
12
|
+
* <ul>
|
|
13
|
+
* <li>`codex exec --json` prints JSONL: `thread.started` (carries the id
|
|
14
|
+
* `resume` takes), `item.started`/`item.completed` for `agent_message`,
|
|
15
|
+
* `command_execution`, `mcp_tool_call`, `file_change`…, then
|
|
16
|
+
* `turn.completed` with `usage` or `turn.failed` with `error`.
|
|
17
|
+
* <li>In `exec` the approval policy is NEVER, so an MCP call is refused
|
|
18
|
+
* outright ("requires approval, but approval policy is never") unless its
|
|
19
|
+
* server says `default_tools_approval_mode = "approve"`. With it, cawdev's
|
|
20
|
+
* own server is reached even under `--sandbox read-only`, network included:
|
|
21
|
+
* the sandbox is the SHELL's, not the MCP servers'.
|
|
22
|
+
* <li>`enabled_tools` really narrows a server — a tool left out is not offered
|
|
23
|
+
* at all — and `disabled_tools` takes one away.
|
|
24
|
+
* <li>`env_vars` forwards named variables from Codex's own environment to a
|
|
25
|
+
* server, which keeps the run's token off the command line, where `ps`
|
|
26
|
+
* would show it to anybody on the machine.
|
|
27
|
+
* <li>`shell_environment_policy.set` blanks a variable for every command the
|
|
28
|
+
* model runs — and `exclude` does NOT (tried: `echo $CAWDEV_TOKEN` still
|
|
29
|
+
* printed it). The token reaches cawdev's server through `env_vars`, which
|
|
30
|
+
* reads Codex's own environment and not the shell's, so blanking it for the
|
|
31
|
+
* shell costs the server nothing.
|
|
32
|
+
* <li>`resume` takes the exec-level options BEFORE the subcommand:
|
|
33
|
+
* `codex exec --json -s read-only resume <id> -`; after it, `-s` is refused.
|
|
34
|
+
* <li>`--strict-config` refuses a field it does not know, so a key misspelled
|
|
35
|
+
* here fails the spawn out loud instead of being ignored.
|
|
36
|
+
* </ul>
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** The `mcp__cawdev__` prefix off a Claude Code tool list, keeping cawdev's own names only. */
|
|
40
|
+
export function cawdevToolNames(tools) {
|
|
41
|
+
const prefix = 'mcp__cawdev__';
|
|
42
|
+
return [...new Set((Array.isArray(tools) ? tools : [])
|
|
43
|
+
.filter((tool) => typeof tool === 'string' && tool.startsWith(prefix))
|
|
44
|
+
.map((tool) => tool.slice(prefix.length)))];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A value as TOML, which is what `-c key=value` parses.
|
|
49
|
+
*
|
|
50
|
+
* <p>A JSON string is a TOML basic string for everything JSON escapes; DEL is
|
|
51
|
+
* the one control character JSON leaves alone and TOML refuses.
|
|
52
|
+
*/
|
|
53
|
+
export function tomlValue(value) {
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return `[${value.map(tomlValue).join(', ')}]`;
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
if (value && typeof value === 'object') {
|
|
61
|
+
const entries = Object.entries(value).map(([key, each]) => `${tomlKey(key)} = ${tomlValue(each)}`);
|
|
62
|
+
return `{ ${entries.join(', ')} }`;
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify(String(value ?? '')).replace(/\u007f/g, '\\u007f');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function tomlKey(key) {
|
|
68
|
+
return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlValue(String(key));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The command line for one Codex turn. The prompt goes on stdin (`-`).
|
|
73
|
+
*
|
|
74
|
+
* @param options.sandbox `read-only` — the OS refuses every write the shell
|
|
75
|
+
* attempts — or `bypass`, which is `--dangerously-bypass-approvals-and-sandbox`
|
|
76
|
+
* and is only ever passed where the machine has allowed everything.
|
|
77
|
+
* @param options.servers `[{ name, command, args, env, envVars, enabledTools,
|
|
78
|
+
* disabledTools }]`, each approved, since nobody is there to approve a call.
|
|
79
|
+
* @param options.hideFromShell variables blanked for the model's own commands.
|
|
80
|
+
*/
|
|
81
|
+
export function codexArgs({
|
|
82
|
+
model = null, effort = null, sandbox, resumeId = null, servers = [], hideFromShell = [],
|
|
83
|
+
}) {
|
|
84
|
+
if (sandbox !== 'read-only' && sandbox !== 'bypass') {
|
|
85
|
+
throw new Error(`codexArgs: sandbox must be read-only or bypass, not ${sandbox}`);
|
|
86
|
+
}
|
|
87
|
+
// `--ignore-user-config` is `--setting-sources ''` for this CLI: the
|
|
88
|
+
// operator's own ~/.codex/config.toml can add MCP servers, hooks and an
|
|
89
|
+
// approval reviewer, and none of that may widen what a run was given. The
|
|
90
|
+
// sign-in is untouched — it is read from CODEX_HOME either way.
|
|
91
|
+
const args = ['exec', '--json', '--strict-config', '--ignore-user-config'];
|
|
92
|
+
args.push(...(sandbox === 'bypass'
|
|
93
|
+
? ['--dangerously-bypass-approvals-and-sandbox']
|
|
94
|
+
: ['--sandbox', 'read-only']));
|
|
95
|
+
if (model) {
|
|
96
|
+
args.push('--model', model);
|
|
97
|
+
}
|
|
98
|
+
if (effort) {
|
|
99
|
+
args.push('-c', `model_reasoning_effort=${tomlValue(effort)}`);
|
|
100
|
+
}
|
|
101
|
+
if (hideFromShell.length) {
|
|
102
|
+
args.push('-c', `shell_environment_policy.set=${tomlValue(
|
|
103
|
+
Object.fromEntries(hideFromShell.map((name) => [name, ''])))}`);
|
|
104
|
+
}
|
|
105
|
+
for (const server of servers) {
|
|
106
|
+
const key = `mcp_servers.${tomlKey(server.name)}`;
|
|
107
|
+
args.push('-c', `${key}.command=${tomlValue(server.command)}`);
|
|
108
|
+
if (server.args?.length) {
|
|
109
|
+
args.push('-c', `${key}.args=${tomlValue(server.args)}`);
|
|
110
|
+
}
|
|
111
|
+
if (server.env && Object.keys(server.env).length) {
|
|
112
|
+
args.push('-c', `${key}.env=${tomlValue(server.env)}`);
|
|
113
|
+
}
|
|
114
|
+
if (server.envVars?.length) {
|
|
115
|
+
args.push('-c', `${key}.env_vars=${tomlValue(server.envVars)}`);
|
|
116
|
+
}
|
|
117
|
+
if (server.enabledTools) {
|
|
118
|
+
args.push('-c', `${key}.enabled_tools=${tomlValue(server.enabledTools)}`);
|
|
119
|
+
}
|
|
120
|
+
if (server.disabledTools?.length) {
|
|
121
|
+
args.push('-c', `${key}.disabled_tools=${tomlValue(server.disabledTools)}`);
|
|
122
|
+
}
|
|
123
|
+
args.push('-c', `${key}.default_tools_approval_mode="approve"`);
|
|
124
|
+
}
|
|
125
|
+
if (resumeId) {
|
|
126
|
+
args.push('resume', resumeId);
|
|
127
|
+
}
|
|
128
|
+
args.push('-');
|
|
129
|
+
return args;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A tool result's text, whatever shape the MCP server gave it. */
|
|
133
|
+
function resultText(result) {
|
|
134
|
+
const content = result?.content;
|
|
135
|
+
if (Array.isArray(content)) {
|
|
136
|
+
const text = content.map((each) => each?.text ?? '').filter(Boolean).join('\n');
|
|
137
|
+
if (text) return text;
|
|
138
|
+
}
|
|
139
|
+
return result == null ? '(no output)' : JSON.stringify(result);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* One line of `codex exec --json`, as what the transcript and the run need.
|
|
144
|
+
*
|
|
145
|
+
* <p>Returns `{ sessionId?, lines: [{ kind, body }], usage?, failed?, error?, ended? }`.
|
|
146
|
+
* `ended` is true on `turn.completed` — the turn ending, which is the whole of
|
|
147
|
+
* what a stage is judged by. `failed` is `turn.failed`'s sentence; `error` a
|
|
148
|
+
* top-level error's, which may be passing. An event this version does not know
|
|
149
|
+
* yields nothing.
|
|
150
|
+
*/
|
|
151
|
+
export function translateCodexEvent(event) {
|
|
152
|
+
const out = { lines: [] };
|
|
153
|
+
const item = event?.item ?? {};
|
|
154
|
+
switch (event?.type) {
|
|
155
|
+
case 'thread.started':
|
|
156
|
+
out.sessionId = event.thread_id ?? null;
|
|
157
|
+
break;
|
|
158
|
+
case 'item.started':
|
|
159
|
+
if (item.type === 'command_execution') {
|
|
160
|
+
out.lines.push({ kind: 'TOOL', body: `Bash(${item.command ?? ''})` });
|
|
161
|
+
} else if (item.type === 'mcp_tool_call') {
|
|
162
|
+
out.lines.push({
|
|
163
|
+
kind: 'TOOL',
|
|
164
|
+
body: `mcp__${item.server}__${item.tool}(${JSON.stringify(item.arguments ?? {})})`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
case 'item.completed':
|
|
169
|
+
if (item.type === 'agent_message' && item.text) {
|
|
170
|
+
out.lines.push({ kind: 'ASSISTANT', body: item.text });
|
|
171
|
+
} else if (item.type === 'command_execution') {
|
|
172
|
+
const output = String(item.aggregated_output ?? '').slice(0, 4000);
|
|
173
|
+
out.lines.push({
|
|
174
|
+
kind: 'TOOL_RESULT',
|
|
175
|
+
body: output || `(exit ${item.exit_code ?? '?'})`,
|
|
176
|
+
});
|
|
177
|
+
} else if (item.type === 'mcp_tool_call') {
|
|
178
|
+
out.lines.push({
|
|
179
|
+
kind: 'TOOL_RESULT',
|
|
180
|
+
body: item.error
|
|
181
|
+
? `failed: ${item.error.message ?? JSON.stringify(item.error)}`
|
|
182
|
+
: resultText(item.result).slice(0, 4000),
|
|
183
|
+
});
|
|
184
|
+
} else if (item.type === 'file_change') {
|
|
185
|
+
const changes = (item.changes ?? [])
|
|
186
|
+
.map((each) => `${each.kind ?? 'update'} ${each.path ?? ''}`.trim());
|
|
187
|
+
out.lines.push({ kind: 'TOOL', body: `Edit(${changes.join(', ') || 'files'})` });
|
|
188
|
+
} else if (item.type === 'error' && item.message) {
|
|
189
|
+
out.lines.push({ kind: 'SYSTEM', body: item.message });
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case 'turn.completed':
|
|
193
|
+
out.ended = true;
|
|
194
|
+
out.usage = {
|
|
195
|
+
input: Number(event.usage?.input_tokens) || 0,
|
|
196
|
+
output: Number(event.usage?.output_tokens) || 0,
|
|
197
|
+
};
|
|
198
|
+
break;
|
|
199
|
+
case 'turn.failed':
|
|
200
|
+
out.failed = event.error?.message ?? 'Codex reported the turn failed.';
|
|
201
|
+
out.lines.push({ kind: 'SYSTEM', body: out.failed });
|
|
202
|
+
break;
|
|
203
|
+
case 'error':
|
|
204
|
+
// NOT `failed`: a top-level error is also how a retried stream says
|
|
205
|
+
// "Reconnecting… 2/5". Only `turn.failed` ends a turn badly; this is
|
|
206
|
+
// kept as the last thing it said, for when nothing better arrives.
|
|
207
|
+
out.error = event.message ?? 'Codex reported an error.';
|
|
208
|
+
out.lines.push({ kind: 'SYSTEM', body: out.error });
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
package/lib/harness-prompt.mjs
CHANGED
|
@@ -48,10 +48,13 @@ export function harnessPrompt({
|
|
|
48
48
|
//
|
|
49
49
|
// R161. A REQUIRED one is named FIRST and in its own block. Two lists rather
|
|
50
50
|
// than one annotated list, because a `(required)` suffix on a line in a list
|
|
51
|
-
// of twelve is a detail; a heading is an instruction.
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
51
|
+
// of twelve is a detail; a heading is an instruction. When a project
|
|
52
|
+
// requires nothing at all — every project until somebody says otherwise —
|
|
53
|
+
// this function still returns exactly what it returned before the entry:
|
|
54
|
+
// the AS_NEEDED wording below has since been extended once, by i257, but a
|
|
55
|
+
// project with no `mustExperts`/`mustSkills`/`mustServers` never reaches
|
|
56
|
+
// either block, so the claim is about that project, not about the wording
|
|
57
|
+
// itself staying fixed forever.
|
|
55
58
|
const reach = [];
|
|
56
59
|
const requiredOf = (list) => (list ?? []).filter((each) => each.required);
|
|
57
60
|
const optionalOf = (list) => (list ?? []).filter((each) => !each.required);
|
|
@@ -63,12 +66,17 @@ export function harnessPrompt({
|
|
|
63
66
|
if (mustExperts.length) {
|
|
64
67
|
reach.push('**Experts you MUST use** — this project requires each of these. Delegate '
|
|
65
68
|
+ 'with the `Agent` tool, `subagent_type` exactly as written, before this stage is '
|
|
66
|
-
+
|
|
69
|
+
+ 'finished. This stage is one process and ends when your turn does: a backgrounded '
|
|
70
|
+
+ '`Agent` call that has not reported back by then is lost, and so is anything gated on '
|
|
71
|
+
+ 'it. Pass `run_in_background: false` and wait for each of these in the same turn — do '
|
|
72
|
+
+ 'not say "I\'ll report back" and stop.\n'
|
|
73
|
+
+ `${lines(mustExperts)}`);
|
|
67
74
|
}
|
|
68
75
|
if (mayExperts.length) {
|
|
69
76
|
reach.push('**Experts** — delegate with the `Agent` tool, `subagent_type` exactly as '
|
|
70
77
|
+ 'written. Use one whenever its description fits the step you are on; they were '
|
|
71
|
-
+ 'chosen for this project
|
|
78
|
+
+ 'chosen for this project. The same rule applies: a call backgrounded past the end of '
|
|
79
|
+
+ 'your turn never comes back.\n'
|
|
72
80
|
+ lines(mayExperts));
|
|
73
81
|
} else if (!mustExperts.length && cannotDelegate?.length) {
|
|
74
82
|
reach.push(`**Experts** — ${cannotDelegate.join(' ')}`);
|
package/lib/runner-config.mjs
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* Adding a key that IS captured would make the daemon claim it applied
|
|
26
26
|
* something it did not, which is worse than saying restart.
|
|
27
27
|
*/
|
|
28
|
-
export const HOT_SWAPPABLE = Object.freeze(['agentCommands', 'grantable', 'acceptsRulesFromConsole'
|
|
28
|
+
export const HOT_SWAPPABLE = Object.freeze(['agentCommands', 'grantable', 'acceptsRulesFromConsole',
|
|
29
|
+
'autoWorkspaces']);
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The legacy spelling of one key, read as the current one — so a config that
|
|
@@ -60,7 +61,7 @@ export function restartNeededFor(booted, file) {
|
|
|
60
61
|
return [...keys]
|
|
61
62
|
.filter((key) => !HOT_SWAPPABLE.includes(key))
|
|
62
63
|
.filter((key) => canonical(before[key]) !== canonical(after[key]))
|
|
63
|
-
.sort();
|
|
64
|
+
.sort((a, b) => a.localeCompare(b));
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/** Applied, or would be on a SIGHUP: the hot-swappable keys that differ. */
|
package/mcp/server.mjs
CHANGED
|
@@ -2011,6 +2011,9 @@ function formatQualityReading(reading, branch) {
|
|
|
2011
2011
|
`new coverage ${percent(row.newCoverage)}`,
|
|
2012
2012
|
];
|
|
2013
2013
|
if (row.analysedAs === 'MAIN') parts.push('analysed as the main branch, not under its own name');
|
|
2014
|
+
if (row.analysedAs === 'PROJECT') {
|
|
2015
|
+
parts.push('analysed as its own SonarQube project; new = on lines this branch changed; no gate');
|
|
2016
|
+
}
|
|
2014
2017
|
if (row.headSha) parts.push(`at ${row.headSha.slice(0, 7)}`);
|
|
2015
2018
|
if (row.dashboardUrl) parts.push(row.dashboardUrl);
|
|
2016
2019
|
lines.push(`Branch ${asked.branch}: ${parts.join(' · ')}`);
|
package/package.json
CHANGED
package/runner/README.md
CHANGED
|
@@ -101,7 +101,7 @@ 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
|
|
104
|
+
claude", "Enable codex"), the console-rules switch, and the config itself with the
|
|
105
105
|
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
|
|
@@ -126,7 +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 agent enable|disable <claude|agy>
|
|
129
|
+
cawdev config agent enable|disable <claude|agy|codex>
|
|
130
130
|
cawdev config rules on|off accept permission rules from the console — R126
|
|
131
131
|
cawdev config show this machine's config, token redacted
|
|
132
132
|
```
|
|
@@ -331,6 +331,166 @@ browser anywhere and the terminal collects the session when you approve.
|
|
|
331
331
|
error — it is what an expired session looks like, and the answer is the same as
|
|
332
332
|
having none.
|
|
333
333
|
|
|
334
|
+
### Advanced mode: one window per session
|
|
335
|
+
|
|
336
|
+
`/advanced-mode` splits the terminal the way emacs does: **one window per
|
|
337
|
+
session** this machine is running — or ran since you attached — above **the
|
|
338
|
+
prompt, which stays at the bottom** the way a shell's does. Each window's title says what kind of
|
|
339
|
+
session it is (`plan`, `dev`, `review`, `ask`…), where it stands, the card and
|
|
340
|
+
the project, and its body is that session's output as it arrives. A frame's
|
|
341
|
+
colour is its session's state — **red while it runs, green once it finished**
|
|
342
|
+
(a plan planned, a dev developed), amber if it stopped any other way — and
|
|
343
|
+
the focused window is drawn in heavy lines; `tab` and `shift+tab` move the focus, `1`–`9`
|
|
344
|
+
jump to a window, and `enter` prompts the focused session exactly as it does
|
|
345
|
+
outside the mode. `/advanced-mode` again (or `/advanced-mode off`) leaves.
|
|
346
|
+
|
|
347
|
+
It is drawn on the terminal's **alternate screen**, like nano or emacs, and
|
|
348
|
+
holds still the way they do: auto-wrap is off, so nothing ever scrolls the
|
|
349
|
+
screen, and the wheel is sent to the program as arrow keys instead of sliding
|
|
350
|
+
the terminal up. The normal screen and its scrollback are exactly as you left
|
|
351
|
+
them — R81's reason
|
|
352
|
+
for printing into the scrollback still holds, and this mode is something you
|
|
353
|
+
ask for rather than the default. On the way out, what the watched session said
|
|
354
|
+
meanwhile is laid into the scrollback, so nothing was lost by watching it here.
|
|
355
|
+
|
|
356
|
+
**The PLANNED cards run across the top, grouped by project** — a line with
|
|
357
|
+
each project's name over its cards and a rule between two projects, every
|
|
358
|
+
project this machine serves included ("nothing planned" is an answer) — one
|
|
359
|
+
small box each, titled with the card and its status — `R3 (Planned)` — the cards of
|
|
360
|
+
every project this machine serves that are ready to be developed, read again
|
|
361
|
+
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
|
+
|
|
365
|
+
**Four windows at most.** Past four, the rest are named on one line above the
|
|
366
|
+
prompt — number, kind, state, card, in the state's colour — and `tab` (or the
|
|
367
|
+
number) onto one brings it up in place of another. The focused session always
|
|
368
|
+
has a window, then the ones still running, then the ended ones, each most
|
|
369
|
+
recently looked at first.
|
|
370
|
+
|
|
371
|
+
A window stays up when its session ends, and its last line says what can be
|
|
372
|
+
done next:
|
|
373
|
+
|
|
374
|
+
- **`/develop`** on a finished plan starts the development of its card, on
|
|
375
|
+
this machine (the branch is the card's own).
|
|
376
|
+
- **`/pr`** on a finished session that wrote a branch opens its pull request
|
|
377
|
+
(`POST runs/{id}/pull-request` — through the card's work item, or from the
|
|
378
|
+
run itself for a session started from a sentence): this machine pushes the
|
|
379
|
+
branch if origin does not have it and runs `gh pr create` (so `gh` has to be
|
|
380
|
+
installed and signed in here). The POST answers with the workspace REQUEST,
|
|
381
|
+
and the terminal follows it (`GET /api/runners/{id}/workspace-requests/{id}`)
|
|
382
|
+
to its result: the URL — which the daemon confirms with `gh pr view` after
|
|
383
|
+
`gh pr create`, so "made" is the host's word and not gh's — or why there is
|
|
384
|
+
none, printed in full (no `gh` comes with its install line and the compare
|
|
385
|
+
link to open it by hand). It watched the run's `prUrl` before, which a
|
|
386
|
+
failure never writes, and said "asked for…" for as long as it was open.
|
|
387
|
+
A checkout cloned on demand is served like any other: the set of paths a
|
|
388
|
+
request may name is read per request, not taken at boot.
|
|
389
|
+
- **`/continue`** on a failed or stopped session is the console's *Carry on*,
|
|
390
|
+
on this machine (`POST runs/{id}/carry-on` with this runner): the same run
|
|
391
|
+
re-queued, its window coming back to life when the daemon takes it. A
|
|
392
|
+
session that stopped BEFORE this terminal attached — the daemon forgets a
|
|
393
|
+
run as it ends, so after a restart nothing here knew of it — is found on
|
|
394
|
+
attach and on sign-in (`findStopped`: the first page of each served
|
|
395
|
+
project's runs, kept when `carriesOnHere` — yours, `canCarryOn`, and its
|
|
396
|
+
checkout on this machine or none), listed in the scrollback, and given a
|
|
397
|
+
window that opens on why it stopped.
|
|
398
|
+
- **`/open`** opens the pull request in your browser.
|
|
399
|
+
- **`/drop`** deletes the checkout the session was cloned into, when it was one
|
|
400
|
+
made on demand (below): asked first, naming the path, and refused while a
|
|
401
|
+
run is in it or holds it, with uncommitted files, or with a commit origin
|
|
402
|
+
does not have. The checkouts in the config are never offered — a project
|
|
403
|
+
always keeps them.
|
|
404
|
+
- **`/close`** takes the window away.
|
|
405
|
+
|
|
406
|
+
**`/new`** starts a session from the prompt, aimed at this machine so its
|
|
407
|
+
window opens here: `/new plan R3`, `/new dev R3`, `/new ask <question>` — or a
|
|
408
|
+
task in words with no card: `/new plan add a CSV export of the invoices` writes
|
|
409
|
+
its card first and plans it (R150), and `/new dev fix the sort on the client
|
|
410
|
+
list` writes its card too (`cardTitleFor`: the sentence, cut between words
|
|
411
|
+
near a hundred characters, the whole text in the body) and then develops it
|
|
412
|
+
exactly as `/new dev R12` would — a number, a work item on the development
|
|
413
|
+
board, a branch of its own, and the card session's instruction to commit. It
|
|
414
|
+
was a manual session on a `task-…` branch with no card, which never reached
|
|
415
|
+
the board and was never told to commit. A card reference on its own is that card; anything
|
|
416
|
+
else is the task in words. **The project is always asked** — a list of what
|
|
417
|
+
this machine serves — unless the command names it with `@project`, or
|
|
418
|
+
advanced mode is narrowed to ONE project (`/projects`), which is a choice
|
|
419
|
+
already made rather than a guess. Never guessed from the focused window, nor
|
|
420
|
+
from a machine serving just one, since a session on the wrong project is a
|
|
421
|
+
card planned or a branch cut in the wrong repository. `/new dev` alone and `d` list the planned cards of every project,
|
|
422
|
+
each with its own, so they ask nothing more. A usage window over this machine's threshold is refused in a
|
|
423
|
+
sentence; `--anyway` starts it regardless, the same decision the console's
|
|
424
|
+
panel asks for. All four commands need you signed in (`/login`), and they work
|
|
425
|
+
outside advanced mode too, on the session you are watching.
|
|
426
|
+
|
|
427
|
+
**Which projects it shows is yours to pick.** `/advanced-mode` on a machine
|
|
428
|
+
serving several projects opens a picker with every one ticked — space (or a
|
|
429
|
+
digit) ticks and unticks, `a` all or none, enter confirms — and only the
|
|
430
|
+
projects ticked are drawn: their windows, their roadmap on top, and the ones
|
|
431
|
+
`/new` asks between. `/advanced-mode @saasng @ngsign` names them and asks
|
|
432
|
+
nothing; `/projects` asks again from inside the mode; `/reload` keeps the
|
|
433
|
+
pick. A prompt never talks to a session of a project that is not shown.
|
|
434
|
+
|
|
435
|
+
**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
|
|
440
|
+
third of the screen, and gone before the windows are on a short terminal.
|
|
441
|
+
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.
|
|
445
|
+
|
|
446
|
+
**A window says which agent and which model** — `Claude · Opus 5.5`,
|
|
447
|
+
`Codex · GPT-5-codex` — bold and underlined at the head of its second line
|
|
448
|
+
(`agentModelFor`, `modelLabel`). A live session's is the daemon's word, what
|
|
449
|
+
it actually spawned: the model asked for, replaced by the one Claude Code
|
|
450
|
+
announces on `init`, and per STAGE on a walked run, since each stage is its
|
|
451
|
+
own process. An ended one's is the platform's. No model named reads
|
|
452
|
+
`default model` — the CLI's own choice — rather than a guess.
|
|
453
|
+
|
|
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.
|
|
458
|
+
|
|
459
|
+
**The prompt shows what is open, not what was.** A question is the prompt's
|
|
460
|
+
banner alone, whole and wrapped between words, and it goes the moment it is
|
|
461
|
+
answered — here or in the console. A permission request's lines are tagged
|
|
462
|
+
with it (`sayAbout`) and leave when it is decided.
|
|
463
|
+
|
|
464
|
+
### Checkouts made on demand: `autoWorkspaces`
|
|
465
|
+
|
|
466
|
+
`"autoWorkspaces": 3` in the machine's config lets the daemon CLONE up to three
|
|
467
|
+
more checkouts of a project by itself, when a coding run would otherwise wait
|
|
468
|
+
for "no free workspace" — so `/new dev R4` starts while R3 is still being
|
|
469
|
+
developed. Absent or 0 is none, as it has always been. The clone sits beside
|
|
470
|
+
the project's first checkout as `<first>-auto-2`, `-auto-3`…; it is a local
|
|
471
|
+
clone of the first (no credential, no download) with `origin` put back to the
|
|
472
|
+
project's remote. Nothing is written to the config: the daemon finds these by
|
|
473
|
+
their name at boot and on SIGHUP, so deleting one — `/drop`, or by hand — is
|
|
474
|
+
the whole of removing it. A clone and not a `git worktree`, on purpose: the
|
|
475
|
+
daemon's reset checks the default branch out, which git refuses in a second
|
|
476
|
+
worktree. Changed without a restart, like `agentCommands`.
|
|
477
|
+
|
|
478
|
+
### `/reload`
|
|
479
|
+
|
|
480
|
+
Quitting and starting again, in one word — for when the client's code changed
|
|
481
|
+
on disk. The same command line runs again in a fresh process. When nothing is
|
|
482
|
+
running here the runner is stopped first (as `q` does), so the relaunch starts
|
|
483
|
+
it on the new code too; when sessions are live it is kept, and the terminal
|
|
484
|
+
says so — restarting it would end them. Under `runner.mjs --attach` the runner
|
|
485
|
+
is the terminal's own process, so `/reload` says to quit and start again.
|
|
486
|
+
|
|
487
|
+
It comes back **as you left it**: advanced mode if it was on, the same window
|
|
488
|
+
focused and the same session watched, the daemon's log shown or not, and the
|
|
489
|
+
windows of sessions that had ended — with their `/develop` and `/pr` — except
|
|
490
|
+
the ones you closed. The state travels to the new process in
|
|
491
|
+
`CAWDEV_RELOAD_STATE`, which that process reads once and removes, so nothing it
|
|
492
|
+
spawns inherits it.
|
|
493
|
+
|
|
334
494
|
### Where the socket is
|
|
335
495
|
|
|
336
496
|
`~/.cawdev/run/<runner name>.sock`, in a `0700` directory, removed when the
|
|
@@ -370,6 +530,23 @@ How many coding runs go at once is `min(workspaces, maxSessions)`. A run that
|
|
|
370
530
|
waits now says **"no free workspace in cawdev (2 here, all busy)"** instead of
|
|
371
531
|
"that project already has a run here", which was a proxy for it.
|
|
372
532
|
|
|
533
|
+
**A free checkout with uncommitted work in it is not preferred over a clean
|
|
534
|
+
one** (R71/i258). A coding run with no pinned checkout and no free preferred
|
|
535
|
+
one now takes the first free checkout whose tree reads clean, checking each
|
|
536
|
+
free checkout in `workspaces` order until one does. It only falls back to a
|
|
537
|
+
free checkout somebody stranded work in — and today's *Start anyway* flow —
|
|
538
|
+
when every free checkout of that project is dirty; the log says so when it
|
|
539
|
+
happens. A *preferred* checkout (R212) is still taken whether it is clean or
|
|
540
|
+
not: it is the checkout the branch last ran in, so its uncommitted work is
|
|
541
|
+
plausibly that branch's own.
|
|
542
|
+
|
|
543
|
+
**Unless the run asked to wait for a clean one instead** (`waitForClean`,
|
|
544
|
+
R270). Then a preferred checkout is taken only when it reads clean, and when
|
|
545
|
+
nothing free reads clean the run is left queued — with a reason saying so —
|
|
546
|
+
rather than falling back to the first free checkout regardless. See "Prepares
|
|
547
|
+
the working copy" below for where that flag comes from and what the daemon
|
|
548
|
+
says about knowing it.
|
|
549
|
+
|
|
373
550
|
**This number caps coding and nothing else** (R70). `ASK`, `ROADMAP`, `AUDIT`
|
|
374
551
|
and `SCOPE` runs take no workspace, so a project whose checkouts are all busy
|
|
375
552
|
still starts a question, an entry-writing session, an audit and a scoping
|
|
@@ -599,7 +776,24 @@ files at all. R47 removes the need for this by giving each run a workspace.
|
|
|
599
776
|
uncommitted before the run is started, and the decision arrives with the
|
|
600
777
|
claim as `allowDirty`. Without it the runner still refuses, with the file
|
|
601
778
|
list, and fails the run saying so: a run that arrives with no flag is one
|
|
602
|
-
nobody was warned about.
|
|
779
|
+
nobody was warned about. **Except when the dirt is the branch's own**: the
|
|
780
|
+
platform NAMED the checkout (a carried-on run's held workspace, R80, or a
|
|
781
|
+
pinned work item's next run, R86) and it is already on the run's branch.
|
|
782
|
+
Then the work is kept and not cleaned — refusing it made every *Carry on*
|
|
783
|
+
a second failure about work the run itself had written.
|
|
784
|
+
|
|
785
|
+
**Or the run may have said, before it got this far, to wait instead**
|
|
786
|
+
(`waitForClean`, R270) — the console's third way out of the dirty-checkout
|
|
787
|
+
panel, alongside *Start anyway* and *Cancel*. That is decided earlier, in
|
|
788
|
+
`pickWorkspace`, at poll time: with `waitForClean` set, the preferred
|
|
789
|
+
checkout is taken only when it reads clean, and when nothing free reads
|
|
790
|
+
clean the run is left queued with a reason rather than claimed at all — so
|
|
791
|
+
by the time a claim reaches this step, the checkout it names is already
|
|
792
|
+
clean and this whole paragraph does not apply. The daemon says
|
|
793
|
+
`waitForClean: true` in its capabilities, the same way it says
|
|
794
|
+
`baseBranches: true`, and an older daemon that does not is not offered such
|
|
795
|
+
a run — the platform tells it apart rather than letting it take a dirty
|
|
796
|
+
checkout the person explicitly asked it not to.
|
|
603
797
|
|
|
604
798
|
With it, the edits are **carried onto the branch** — stashed, checked out,
|
|
605
799
|
popped (R57). `git checkout` refuses outright when a locally modified file
|
|
@@ -729,12 +923,34 @@ that reaches the server by another name than the platform does (a LAN
|
|
|
729
923
|
hostname), and `timeoutSeconds` (600) bounding the scan and the wait.
|
|
730
924
|
|
|
731
925
|
**Community Edition.** A server that refuses `sonar.branch.name` — every
|
|
732
|
-
Community Edition — is *skipped by default*: the
|
|
733
|
-
saying so, and nothing is analysed.
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
926
|
+
Community Edition without the plugin below — is *skipped by default*: the
|
|
927
|
+
reading carries an error saying so, and nothing is analysed.
|
|
928
|
+
`"whenBranchesRefused"` in the block opts into one of two fallbacks; `skip`
|
|
929
|
+
stays the default and nothing opts in for you.
|
|
930
|
+
|
|
931
|
+
- `"main"` re-scans the branch as the project's **main** analysis and marks
|
|
932
|
+
the reading `MAIN` with a warning. Understand what that trades: the
|
|
933
|
+
project's own dashboard, and the platform's project reading with it, then
|
|
934
|
+
describe whichever branch scanned last.
|
|
935
|
+
- `"project"` scans the branch as its **own scratch SonarQube project**
|
|
936
|
+
instead — `<key>:branch:<branch>-<hash>`, overwritten on each run of the
|
|
937
|
+
branch, and never touching the project's own dashboard. A fresh project has
|
|
938
|
+
no new-code period, so "new" is redefined as *on a line this branch
|
|
939
|
+
changed*; the reading is marked `PROJECT`, its gate is always `NONE`
|
|
940
|
+
(nothing to gate a scratch project on), and there is no new-code coverage.
|
|
941
|
+
This needs the scanner's token to hold **Create Projects** on the server —
|
|
942
|
+
check its default project visibility too, since *public* would publish the
|
|
943
|
+
code. cawdev never deletes these scratch projects; cleaning them up is the
|
|
944
|
+
operator's own housekeeping.
|
|
945
|
+
|
|
946
|
+
The project itself can say which of these it needs: its Settings →
|
|
947
|
+
SonarQube card offers *Developer Edition or above (or Community with the
|
|
948
|
+
[sonarqube-community-branch-plugin](https://github.com/mc1arke/sonarqube-community-branch-plugin)
|
|
949
|
+
installed)*, *Community Edition*, or *Not sure — find out on the first scan*.
|
|
950
|
+
Set correctly, the daemon skips the doomed branch attempt and goes straight
|
|
951
|
+
to the chosen fallback; left at "not sure", behaviour is exactly as above.
|
|
952
|
+
**Restart the daemon after upgrading it to use `"project"`**: an older
|
|
953
|
+
runner does not know the value and refuses to boot with it in its config.
|
|
738
954
|
|
|
739
955
|
**A failed scan never fails a run.** A machine with no block scans nothing and
|
|
740
956
|
says nothing. A machine with a block and a project with no key logs `not
|
|
@@ -1096,3 +1312,42 @@ the prompt.
|
|
|
1096
1312
|
The prompt deliberately teaches the **method**, not the task. The task is in the
|
|
1097
1313
|
roadmap entry, which the agent reads for itself with `task_current` — putting it
|
|
1098
1314
|
in the prompt too would be a second copy that can disagree with the entry.
|
|
1315
|
+
|
|
1316
|
+
### Codex CLI
|
|
1317
|
+
|
|
1318
|
+
`codex` in `agentCommands` (`cawdev config agent enable codex`) lets a run name
|
|
1319
|
+
`codex` as its agent. It is spawned by `spawnCodex`, one `codex exec --json`
|
|
1320
|
+
per turn like Antigravity, and every flag it is given is in
|
|
1321
|
+
`../lib/codex.mjs` with what was checked against the real CLI (0.155). Sign in
|
|
1322
|
+
once with `codex login`; the run uses that sign-in and **not** your
|
|
1323
|
+
`~/.codex/config.toml` (`--ignore-user-config`, this CLI's `--setting-sources
|
|
1324
|
+
''`), so a model or MCP server you configured for yourself does not reach it.
|
|
1325
|
+
|
|
1326
|
+
Codex has a sandbox the operating system enforces, and that decides what it
|
|
1327
|
+
may do:
|
|
1328
|
+
|
|
1329
|
+
- **A run that must change nothing** — ASK, AUDIT, SCOPE, REVIEW, PLAN, and the
|
|
1330
|
+
PLAN, VERIFY and MEMORY stages of any run — gets `--sandbox read-only`. Its
|
|
1331
|
+
shell reads and cannot write, and it needs nothing from the machine. Only
|
|
1332
|
+
cawdev's server is attached, narrowed with `enabled_tools` to the list Claude
|
|
1333
|
+
Code would be handed, and no stage ever holds `report`.
|
|
1334
|
+
- **A run that writes** — CODE, INTERVIEW, MERGE, RELEASE and their IMPLEMENT
|
|
1335
|
+
and TEST — must commit and push, which the sandbox refuses, and Codex has no
|
|
1336
|
+
allow-list for its shell. So it runs with
|
|
1337
|
+
`--dangerously-bypass-approvals-and-sandbox`, and **only on a machine that
|
|
1338
|
+
allows everything** (R126); anywhere else the run fails before anything is
|
|
1339
|
+
spawned, saying so.
|
|
1340
|
+
|
|
1341
|
+
Every MCP server is marked `default_tools_approval_mode = "approve"` — in `exec`
|
|
1342
|
+
the approval policy is *never*, and without it every call to cawdev is refused.
|
|
1343
|
+
The run's token reaches cawdev's server through `env_vars`, never the command
|
|
1344
|
+
line, and is blanked for the model's own commands
|
|
1345
|
+
(`shell_environment_policy.set`).
|
|
1346
|
+
|
|
1347
|
+
Not reached on Codex: experts (the session is told it cannot delegate), a
|
|
1348
|
+
project's tool rules and R51's permission prompt (both are Claude Code's
|
|
1349
|
+
`--allowedTools` grammar and its `approve` handler), the command half of R110's
|
|
1350
|
+
shield (the transcript half — secrets taken out of a result — runs in the daemon
|
|
1351
|
+
and applies), and live prompting (the process ends with its turn; a follow-up is
|
|
1352
|
+
`codex exec resume`). Skills are inlined whole, as for Antigravity. No usage
|
|
1353
|
+
windows are read: Codex has no `-p /usage`.
|