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/runner/cawdev.mjs
CHANGED
|
@@ -29,8 +29,8 @@ import { attach, urlFrom, valueOf } from './attach.mjs';
|
|
|
29
29
|
import { asker, askOne, mintForThisMachine, setUpThisMachine } from './bootstrap.mjs';
|
|
30
30
|
import { liveSockets, probeSocket, socketPathFor } from './control.mjs';
|
|
31
31
|
import {
|
|
32
|
-
addProject, addWorkspace, readConfigFile, reloadIfRunning,
|
|
33
|
-
setAgentEnabled,
|
|
32
|
+
addProject, addWorkspace, readConfigFile, reloadIfRunning, removeProject, setAcceptsRulesFromConsole,
|
|
33
|
+
setQuality, setAgentEnabled,
|
|
34
34
|
} from './configure.mjs';
|
|
35
35
|
import { restartNeededFor } from '../lib/runner-config.mjs';
|
|
36
36
|
import { Select } from './select.mjs';
|
|
@@ -63,6 +63,8 @@ const USAGE = `
|
|
|
63
63
|
cawdev config add-project serve one more project you can write to
|
|
64
64
|
cawdev config add-workspace another checkout for a project you
|
|
65
65
|
already serve — R47's concurrency
|
|
66
|
+
cawdev config remove-project <slug> stop serving a project (its checkouts
|
|
67
|
+
stay on disk)
|
|
66
68
|
cawdev config agent enable|disable <claude|agy|codex>
|
|
67
69
|
cawdev config rules on|off accept permission rules from the console
|
|
68
70
|
cawdev config quality on|off <slug> scan that project's branches with
|
|
@@ -422,6 +424,7 @@ async function runVersion(url, ink) {
|
|
|
422
424
|
/**
|
|
423
425
|
* `cawdev config` — R283's granular half of the setup walk, on a machine
|
|
424
426
|
* already configured. A direct subcommand (`add-project`, `add-workspace`,
|
|
427
|
+
* `remove-project <slug>`,
|
|
425
428
|
* `agent enable|disable <command>`, `rules on|off`, `quality on|off <slug>`,
|
|
426
429
|
* `show`) or, with none, a
|
|
427
430
|
* menu — the same `Select` widget everything that offers a choice uses.
|
|
@@ -463,6 +466,7 @@ export async function pickAction(ask, say, pick = askOne) {
|
|
|
463
466
|
rows: [
|
|
464
467
|
{ id: 'add-project', label: 'Add a project' },
|
|
465
468
|
{ id: 'add-workspace', label: 'Add a workspace to a project' },
|
|
469
|
+
{ id: 'remove-project', label: 'Stop serving a project' },
|
|
466
470
|
{ id: 'agent-enable', label: `Enable an agent (${KNOWN_AGENTS.join(' / ')})` },
|
|
467
471
|
{ id: 'agent-disable', label: 'Disable an agent' },
|
|
468
472
|
{ id: 'rules-on', label: 'Accept permission rules from the console' },
|
|
@@ -477,6 +481,9 @@ export async function pickAction(ask, say, pick = askOne) {
|
|
|
477
481
|
const command = (await ask.line(` Which agent — ${KNOWN_AGENTS.join(', ')}? `)).trim();
|
|
478
482
|
return ['agent', id === 'agent-enable' ? 'enable' : 'disable', command];
|
|
479
483
|
}
|
|
484
|
+
if (id === 'remove-project') {
|
|
485
|
+
return ['remove-project', (await ask.line(' Which project (slug)? ')).trim()];
|
|
486
|
+
}
|
|
480
487
|
if (id === 'rules-on') return ['rules', 'on'];
|
|
481
488
|
if (id === 'rules-off') return ['rules', 'off'];
|
|
482
489
|
if (id === 'quality-on' || id === 'quality-off') {
|
|
@@ -507,6 +514,14 @@ export async function runAction([verb, ...rest], { configPath, file, url, sessio
|
|
|
507
514
|
await reload(await addWorkspace({ configPath, file, url, session, ask, say, ink }));
|
|
508
515
|
return;
|
|
509
516
|
}
|
|
517
|
+
if (verb === 'remove-project') {
|
|
518
|
+
const [slug] = rest;
|
|
519
|
+
if (!slug) {
|
|
520
|
+
throw new Error('Name a project: cawdev config remove-project <slug>');
|
|
521
|
+
}
|
|
522
|
+
await reload(await removeProject({ file, configPath, ask, say, ink, slug }));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
510
525
|
if (verb === 'agent') {
|
|
511
526
|
const [sub, command] = rest;
|
|
512
527
|
if (!KNOWN_AGENTS.includes(command)) {
|
package/runner/configure.mjs
CHANGED
|
@@ -345,6 +345,40 @@ export async function setAgentEnabled({
|
|
|
345
345
|
return next;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Stop serving a project: its entry leaves the config, and nothing else moves.
|
|
350
|
+
* The checkouts stay on disk — they are somebody's working copies, and
|
|
351
|
+
* deleting a directory is not a config change — and the machine's token keeps
|
|
352
|
+
* its grant, which reaches nothing once the project is gone and reaches it
|
|
353
|
+
* again if it is added back. `projects` is read at boot, so the daemon serves
|
|
354
|
+
* it until a restart; the reload after this says so.
|
|
355
|
+
*
|
|
356
|
+
* The case it exists for: a project deleted in the console is still in this
|
|
357
|
+
* file, so the terminal kept offering it and the daemon kept polling for it.
|
|
358
|
+
*/
|
|
359
|
+
export async function removeProject({
|
|
360
|
+
file, configPath, slug,
|
|
361
|
+
ask, say, ink = painter(3),
|
|
362
|
+
write = writeRunnerConfig,
|
|
363
|
+
} = {}) {
|
|
364
|
+
const projects = file.projects ?? {};
|
|
365
|
+
if (!slug || !(slug in projects)) {
|
|
366
|
+
throw new Error(`This machine does not serve ${slug || 'that project'}. `
|
|
367
|
+
+ `It serves: ${Object.keys(projects).join(', ') || 'nothing yet'}.`);
|
|
368
|
+
}
|
|
369
|
+
const paths = pathsOf(projects[slug]);
|
|
370
|
+
const kept = paths.length ? ` Its checkout${paths.length === 1 ? '' : 's'} stay${paths.length === 1 ? 's' : ''} on disk: ${paths.join(', ')}.` : '';
|
|
371
|
+
if (!await confirm(ask, `Stop serving ${slug} on this machine?${kept}`)) {
|
|
372
|
+
throw new Error('Nothing changed.');
|
|
373
|
+
}
|
|
374
|
+
const { [slug]: _removed, ...rest } = projects;
|
|
375
|
+
const next = { ...file, projects: rest };
|
|
376
|
+
await write(configPath, next);
|
|
377
|
+
say(` ${ink.success('✓')} ${ink.muted(`This machine no longer serves ${slug}.`)}`
|
|
378
|
+
+ (paths.length ? ` ${ink.muted(`Delete ${paths.length === 1 ? 'its checkout' : 'its checkouts'} yourself if you no longer want ${paths.length === 1 ? 'it' : 'them'}.`)}` : ''));
|
|
379
|
+
return next;
|
|
380
|
+
}
|
|
381
|
+
|
|
348
382
|
/** Turn `acceptsRulesFromConsole` on or off — R126. */
|
|
349
383
|
export async function setAcceptsRulesFromConsole({
|
|
350
384
|
file, configPath, enabled,
|
package/runner/input.mjs
CHANGED
|
@@ -136,25 +136,38 @@ export class History {
|
|
|
136
136
|
this.draft = '';
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
/**
|
|
140
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Older. Returns null when there is nothing older, so the line is left alone.
|
|
141
|
+
*
|
|
142
|
+
* @param keep which entries this line walks — a command line walks the
|
|
143
|
+
* commands and skips the prompts sent to sessions, which it cannot send
|
|
144
|
+
*/
|
|
145
|
+
back(current = '', keep = () => true) {
|
|
141
146
|
if (this.at === this.entries.length) {
|
|
142
147
|
this.draft = current;
|
|
143
148
|
}
|
|
144
|
-
|
|
149
|
+
let at = this.at;
|
|
150
|
+
do {
|
|
151
|
+
at -= 1;
|
|
152
|
+
} while (at >= 0 && !keep(this.entries[at]));
|
|
153
|
+
if (at < 0) {
|
|
145
154
|
return null;
|
|
146
155
|
}
|
|
147
|
-
this.at
|
|
148
|
-
return this.entries[
|
|
156
|
+
this.at = at;
|
|
157
|
+
return this.entries[at];
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
/** Newer, ending at the draft you were writing. */
|
|
152
|
-
forward() {
|
|
161
|
+
forward(keep = () => true) {
|
|
153
162
|
if (this.at >= this.entries.length) {
|
|
154
163
|
return null;
|
|
155
164
|
}
|
|
156
|
-
this.at
|
|
157
|
-
|
|
165
|
+
let at = this.at;
|
|
166
|
+
do {
|
|
167
|
+
at += 1;
|
|
168
|
+
} while (at < this.entries.length && !keep(this.entries[at]));
|
|
169
|
+
this.at = at;
|
|
170
|
+
return at === this.entries.length ? this.draft : this.entries[at];
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
reset() {
|
package/runner/prefs.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this terminal remembers about how you like to work — beside history.mjs
|
|
3
|
+
* and in its shape: one file under ~/.cawdev, keyed by the platform's URL, 0600.
|
|
4
|
+
*
|
|
5
|
+
* One preference so far: the agent `/new` asks for when a command names none
|
|
6
|
+
* (`/agent codex`). Kept here and not on the platform, because it is a choice
|
|
7
|
+
* about THIS terminal — the project's default and your account's live in the
|
|
8
|
+
* console, and this sits in front of them without changing either.
|
|
9
|
+
*
|
|
10
|
+
* @author Claude Opus 5.5 (Claude Code)
|
|
11
|
+
* @author Moez Ben MBarka
|
|
12
|
+
*/
|
|
13
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
export function prefsFile() {
|
|
18
|
+
return process.env.CAWDEV_PREFS_FILE ?? join(homedir(), '.cawdev', 'terminal.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function key(url) {
|
|
22
|
+
return String(url).replace(/\/+$/, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readAll() {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(await readFile(prefsFile(), 'utf8'));
|
|
28
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** This platform's preferences, `{}` when there are none or the file is unreadable. */
|
|
35
|
+
export async function loadPrefs(url) {
|
|
36
|
+
const mine = (await readAll())[key(url)];
|
|
37
|
+
return mine && typeof mine === 'object' ? mine : {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Merges `changes` into this platform's preferences; a null value removes the key. */
|
|
41
|
+
export async function savePrefs(url, changes) {
|
|
42
|
+
const all = await readAll();
|
|
43
|
+
const mine = { ...(all[key(url)] ?? {}) };
|
|
44
|
+
for (const [name, value] of Object.entries(changes)) {
|
|
45
|
+
if (value === null || value === undefined) delete mine[name];
|
|
46
|
+
else mine[name] = value;
|
|
47
|
+
}
|
|
48
|
+
all[key(url)] = mine;
|
|
49
|
+
await mkdir(dirname(prefsFile()), { recursive: true, mode: 0o700 });
|
|
50
|
+
await writeFile(prefsFile(), `${JSON.stringify(all, null, 2)}\n`, { mode: 0o600 });
|
|
51
|
+
await chmod(prefsFile(), 0o600).catch(() => undefined);
|
|
52
|
+
}
|
package/runner/runner.mjs
CHANGED
|
@@ -40,12 +40,13 @@ import {
|
|
|
40
40
|
} from '../lib/stage-tools.mjs';
|
|
41
41
|
import { capabilityIn, describeCall } from '../lib/tool-line.mjs';
|
|
42
42
|
import { findSecret } from '../lib/secrets.mjs';
|
|
43
|
+
import { BackgroundShells } from '../lib/background-shells.mjs';
|
|
43
44
|
import { cliVersion, isBelow } from '../lib/version.mjs';
|
|
44
45
|
import { reloadableFor, restartNeededFor } from '../lib/runner-config.mjs';
|
|
45
46
|
import {
|
|
46
47
|
createAutoWorkspace, discoverAutoWorkspaces, isAutoWorkspace,
|
|
47
48
|
} from './auto-workspaces.mjs';
|
|
48
|
-
import { agentOfCommand } from '../lib/agents.mjs';
|
|
49
|
+
import { agentOfCommand, needsEverything } from '../lib/agents.mjs';
|
|
49
50
|
import { cawdevToolNames, codexArgs, translateCodexEvent } from '../lib/codex.mjs';
|
|
50
51
|
|
|
51
52
|
// --- configuration -----------------------------------------------------------
|
|
@@ -5182,21 +5183,60 @@ function argsForProfile(agentArgs, profile, run, expertAgents = [], skills = [])
|
|
|
5182
5183
|
* re-derived the plan from a précis would be planning again, which is the one
|
|
5183
5184
|
* thing the gate exists to stop it doing after somebody approved the first one.
|
|
5184
5185
|
*/
|
|
5185
|
-
|
|
5186
|
+
/** Longer than ask_user's own ten-minute wait — see the cawdev server of spawnCodex. */
|
|
5187
|
+
const CAWDEV_TOOL_TIMEOUT_SEC = 660;
|
|
5188
|
+
|
|
5189
|
+
/**
|
|
5190
|
+
* A decision that is the person's is ASKED, and the stage goes on with the
|
|
5191
|
+
* answer. R19 and R24 (Codex) each met one — a plan the code had moved away
|
|
5192
|
+
* from, a plan whose review left the scope open — and ended the stage saying
|
|
5193
|
+
* so, the run FINISHED with nothing built and the question in a summary
|
|
5194
|
+
* nobody was asked. Claude asked; the wording let Codex stop instead.
|
|
5195
|
+
*/
|
|
5196
|
+
const ASK_DONT_STOP = 'If a decision is genuinely the person\'s — a scope the card leaves open, '
|
|
5197
|
+
+ 'a plan that no longer matches the code, a trade-off with no right answer — ASK it with '
|
|
5198
|
+
+ '`ask_user` (a round of questions if there are several), wait for the answer, and carry on '
|
|
5199
|
+
+ 'with it. Never end this stage on an open question: a question left in your report is one '
|
|
5200
|
+
+ 'nobody was asked.';
|
|
5201
|
+
|
|
5202
|
+
/**
|
|
5203
|
+
* A stage is ONE turn, and it ends the moment the turn does — so a command left
|
|
5204
|
+
* running in the background is a result lost (background-shells.mjs).
|
|
5205
|
+
*/
|
|
5206
|
+
const WAIT_FOR_COMMANDS = 'This stage ends when your turn ends: run every command in the '
|
|
5207
|
+
+ 'foreground and wait for it to finish — a build or a test suite left running in the '
|
|
5208
|
+
+ 'background is lost, and the stage is run again.';
|
|
5209
|
+
|
|
5210
|
+
/**
|
|
5211
|
+
* @param hasPlan whether the card has a plan of record — the same test
|
|
5212
|
+
* harnessPrompt uses to draw "The plan for this card" (`plan?.body`), so the
|
|
5213
|
+
* instruction and the block it points at cannot disagree
|
|
5214
|
+
*/
|
|
5215
|
+
function stagePrompt(stage, carried, hasPlan = true) {
|
|
5186
5216
|
const asked = {
|
|
5187
5217
|
PLAN: 'Work out what to do and WRITE THE PLAN. Do not change anything — you have no '
|
|
5188
5218
|
+ 'tool that can, so do not spend turns discovering that. Read what you need, then say, '
|
|
5189
5219
|
+ 'concretely: which files, what change in each, and how you will know it worked. '
|
|
5190
5220
|
+ 'What you report becomes the card\'s plan, and a DIFFERENT session — another '
|
|
5191
5221
|
+ 'process, with none of your context — will be handed it and told to carry it out. '
|
|
5192
|
-
+ 'Write it for them.',
|
|
5222
|
+
+ 'Write it for them — with every decision in it already made. ' + ASK_DONT_STOP,
|
|
5193
5223
|
VERIFY: 'Check the plan against what is actually there. Does every file it names exist? '
|
|
5194
5224
|
+ 'Does every interface it assumes still look like that? You cannot change anything — '
|
|
5195
5225
|
+ 'say what is wrong with the plan, or say plainly that it holds.',
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5226
|
+
// With a plan, the plan is the task. Without one — `/new dev R21` on a
|
|
5227
|
+
// card nobody planned — the CARD is: the old wording pointed at a plan
|
|
5228
|
+
// section that was not there, and a careful session read "stop rather than
|
|
5229
|
+
// improvise" as "stop", ending R21 with nothing built and nothing to push.
|
|
5230
|
+
IMPLEMENT: (hasPlan
|
|
5231
|
+
? 'Do the work in the plan — it is under "The plan for this card" above, '
|
|
5232
|
+
+ 'written by the plan phase and agreed. Do not quietly improvise a different change: '
|
|
5233
|
+
+ 'somebody approved that plan. If it turns out to be wrong or incomplete, ask what to '
|
|
5234
|
+
+ 'do instead of stopping — and stop only if the answer is to stop. '
|
|
5235
|
+
: 'This card has no plan, and it does not need one: it was started straight into '
|
|
5236
|
+
+ 'development, so the CARD\'s text is the task. Work out what it asks — read the code '
|
|
5237
|
+
+ 'it touches first — build it, and commit it on this branch. Never stop because a plan '
|
|
5238
|
+
+ 'is missing. ')
|
|
5239
|
+
+ ASK_DONT_STOP + ' ' + WAIT_FOR_COMMANDS,
|
|
5200
5240
|
// R129. Two honest ways to do this stage, and the project chose one. The
|
|
5201
5241
|
// TESTBOOK wording says "you have no tool that could" for the PLAN
|
|
5202
5242
|
// prompt's reason: a session that has to discover a refusal spends turns
|
|
@@ -5215,7 +5255,8 @@ function stagePrompt(stage, carried) {
|
|
|
5215
5255
|
+ 'failing test is a result, not a failure of this stage. The branch\'s SonarQube gate '
|
|
5216
5256
|
+ 'is one call away — `quality_get`, which reads this run\'s branch by default; a red '
|
|
5217
5257
|
+ 'gate with new vulnerabilities is a finding to report with the output, not a failure '
|
|
5218
|
-
+ 'to hide.'
|
|
5258
|
+
+ 'to hide. ' + WAIT_FOR_COMMANDS + ' Then commit the branch\'s changes — no stage '
|
|
5259
|
+
+ 'after this one writes code, and work left uncommitted is work nothing can push.',
|
|
5219
5260
|
MEMORY: 'Write what the NEXT session on this branch needs to know: what was done, what was '
|
|
5220
5261
|
+ 'tried and rejected and why, and anything that turned out to matter and would not be '
|
|
5221
5262
|
+ 'obvious from the diff. Be brief and concrete. This is saved as this branch\'s '
|
|
@@ -6556,8 +6597,23 @@ async function walkLifecycle(config, run, runToken, cwd, baseCommit, workspace,
|
|
|
6556
6597
|
return { tokensIn: Number(row?.tokensIn) || 0, tokensOut: Number(row?.tokensOut) || 0 };
|
|
6557
6598
|
};
|
|
6558
6599
|
|
|
6600
|
+
const storedRow = (name) => (Array.isArray(stored)
|
|
6601
|
+
? stored.find((each) => each?.stage === name) : null);
|
|
6602
|
+
|
|
6559
6603
|
for (let at = 0; at < lifecycle.length; at++) {
|
|
6560
6604
|
const stage = lifecycle[at];
|
|
6605
|
+
// Where a carried-on run resumes, and what its DONE stages said, is the
|
|
6606
|
+
// platform's (R263, `WorkflowService.resumeFrom`): the claim's lifecycle
|
|
6607
|
+
// starts at the first stage not DONE. What only the daemon knows is WHY
|
|
6608
|
+
// the stage about to run stopped last time — so that is handed to it.
|
|
6609
|
+
const before = storedRow(stage.stage);
|
|
6610
|
+
if (before?.state === 'FAILED' && before.outcome) {
|
|
6611
|
+
// The attempt that stopped, handed to the one about to run.
|
|
6612
|
+
carried[`${stage.stage} (the attempt that stopped)`] = `${before.outcome}\n\n`
|
|
6613
|
+
+ 'This stage is being run again to finish that. Run the commands it left running '
|
|
6614
|
+
+ 'again IN THE FOREGROUND — not with run_in_background — wait for each one to end '
|
|
6615
|
+
+ 'and read its output, then finish the stage properly.';
|
|
6616
|
+
}
|
|
6561
6617
|
// R305. What THIS stage's processes have used, across its attempts. Outside
|
|
6562
6618
|
// the attempt loop on purpose: `run_stage` is one row per stage and keeps
|
|
6563
6619
|
// the larger reading, so a second attempt after a refused gate must report
|
|
@@ -6668,6 +6724,21 @@ async function walkLifecycle(config, run, runToken, cwd, baseCommit, workspace,
|
|
|
6668
6724
|
return;
|
|
6669
6725
|
}
|
|
6670
6726
|
|
|
6727
|
+
// The turn ended, but with commands it started in the background still
|
|
6728
|
+
// running, or finished and never read. Not done: the result is what the
|
|
6729
|
+
// stage was for. FAILED keeps the checkout and the session (R80), so
|
|
6730
|
+
// /continue picks the run up HERE — the stages behind it stay pending.
|
|
6731
|
+
if (last?.unfinishedShells?.length) {
|
|
6732
|
+
const listed = last.unfinishedShells.map((command) => `\`${command.slice(0, 120)}\``).join(', ');
|
|
6733
|
+
const why = `The ${stage.stage} stage ended with ${last.unfinishedShells.length} command(s) `
|
|
6734
|
+
+ `still running in the background, or finished and never read: ${listed}. `
|
|
6735
|
+
+ 'Nothing they proved was recorded — /continue runs this stage again and waits for them.';
|
|
6736
|
+
log(` ${why}`);
|
|
6737
|
+
await reportStage(config, run, stage.stage, 'report', { state: 'FAILED', outcome: why });
|
|
6738
|
+
await finish(config, run, 'FAILED', `${why}\n\n${last.text ?? ''}`.trim());
|
|
6739
|
+
return;
|
|
6740
|
+
}
|
|
6741
|
+
|
|
6671
6742
|
await reportStage(config, run, stage.stage, 'report', {
|
|
6672
6743
|
state: 'DONE',
|
|
6673
6744
|
// The PLAN's text IS the plan — the artefact R109 exists to store, and
|
|
@@ -7300,7 +7371,7 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
7300
7371
|
// sending it again is a repeat wearing a resume's clothes — R69's rule about
|
|
7301
7372
|
// the opening prompt, applied to the thing that now travels with it.
|
|
7302
7373
|
writeUserMessage(child, resume?.prompt
|
|
7303
|
-
?? (promptFor(run) + harness + (stage ? stagePrompt(stage, carried) : '')));
|
|
7374
|
+
?? (promptFor(run) + harness + (stage ? stagePrompt(stage, carried, Boolean(plan?.body)) : '')));
|
|
7304
7375
|
|
|
7305
7376
|
let lastText = '';
|
|
7306
7377
|
// R305. This child's own last reading, handed back to the walk so the next
|
|
@@ -7326,6 +7397,10 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
7326
7397
|
// wait for it, which is exactly the shape that loses it when `endSession`
|
|
7327
7398
|
// closes stdin below.
|
|
7328
7399
|
const backgrounded = new Set();
|
|
7400
|
+
// Shell commands this child started in the background and never saw end
|
|
7401
|
+
// — the walk will not call a stage done with any of them left (see
|
|
7402
|
+
// background-shells.mjs).
|
|
7403
|
+
const shells = new BackgroundShells();
|
|
7329
7404
|
// The last of what the CLI said on stderr, for the exit decision below.
|
|
7330
7405
|
// The usage-limit notice is written there, and a decision made on stdout
|
|
7331
7406
|
// alone would call every window a crash.
|
|
@@ -7487,6 +7562,7 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
7487
7562
|
buffer = buffer.slice(newline + 1);
|
|
7488
7563
|
if (!line) continue;
|
|
7489
7564
|
const event = safeJson(line);
|
|
7565
|
+
if (event) shells.observe(event);
|
|
7490
7566
|
|
|
7491
7567
|
// i257. Read off the RAW event, before `linesOf` flattens it to a
|
|
7492
7568
|
// string: `describeCall` (`../lib/tool-line.mjs`) never carries
|
|
@@ -7893,7 +7969,8 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
7893
7969
|
// gate on it, and carry it into the next stage. A run with no lifecycle
|
|
7894
7970
|
// resolves with nothing, exactly as it always did.
|
|
7895
7971
|
resolvePromise(stage
|
|
7896
|
-
? { code, signal, text: lastText, turnEnded, usage: childTotals
|
|
7972
|
+
? { code, signal, text: lastText, turnEnded, usage: childTotals,
|
|
7973
|
+
unfinishedShells: shells.unfinished() }
|
|
7897
7974
|
: undefined);
|
|
7898
7975
|
});
|
|
7899
7976
|
});
|
|
@@ -8188,14 +8265,36 @@ function inlinedSkillsFor(skills) {
|
|
|
8188
8265
|
* tool a session delegates through — confirmed end-to-end by hand, a probe
|
|
8189
8266
|
* agent installed, invoked by name, and its answer returned correctly.
|
|
8190
8267
|
*/
|
|
8268
|
+
/** This machine, as a refusal names it: what it is called, where its config and console are. */
|
|
8269
|
+
function thisMachine(config) {
|
|
8270
|
+
return {
|
|
8271
|
+
name: config.name,
|
|
8272
|
+
acceptsConsoleRules: config.acceptsRulesFromConsole === true,
|
|
8273
|
+
configPath: config.configPath ?? null,
|
|
8274
|
+
consoleUrl: config.url,
|
|
8275
|
+
};
|
|
8276
|
+
}
|
|
8277
|
+
|
|
8278
|
+
/**
|
|
8279
|
+
* A refusal said on the run's own transcript, where the terminal and the run
|
|
8280
|
+
* page show it — the run's summary alone left a window reading "failed" and
|
|
8281
|
+
* nothing else, with the reason one page away.
|
|
8282
|
+
*/
|
|
8283
|
+
async function sayOnTheRun(config, run, text) {
|
|
8284
|
+
await api(config, `/api/projects/${run.projectSlug}/runs/${run.id}/output`, {
|
|
8285
|
+
method: 'POST',
|
|
8286
|
+
body: { lines: text.split('\n').filter((body) => body.trim()).map((body) => ({ kind: 'ERROR', body })) },
|
|
8287
|
+
}).catch((failure) => log(` could not say it on the run: ${failure.message}`));
|
|
8288
|
+
}
|
|
8289
|
+
|
|
8191
8290
|
async function spawnAntigravity(config, run, runToken, cwd, baseCommit, resume, projectServers,
|
|
8192
8291
|
skills, stageExperts, cannotDelegate, instincts, briefing, plan, lifecycle, stage, carried,
|
|
8193
8292
|
agyCmd, granted, usage = null) {
|
|
8194
8293
|
if (!granted.everything) {
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8294
|
+
// Antigravity needs it even to read: no per-tool allow-list exists on it.
|
|
8295
|
+
const summary = needsEverything('agy', thisMachine(config), run, { readsWithout: false });
|
|
8296
|
+
log(` refusing to spawn Antigravity: ${summary.split('\n')[0]}`);
|
|
8297
|
+
await sayOnTheRun(config, run, summary);
|
|
8199
8298
|
await finish(config, run, 'FAILED', summary);
|
|
8200
8299
|
return stage ? { code: -1, signal: null, text: summary, turnEnded: false } : undefined;
|
|
8201
8300
|
}
|
|
@@ -8285,7 +8384,7 @@ async function spawnAntigravity(config, run, runToken, cwd, baseCommit, resume,
|
|
|
8285
8384
|
const promptText = resuming
|
|
8286
8385
|
? resume.prompt
|
|
8287
8386
|
: (promptFor(run) + harness + skillsBlock + expertsBlock
|
|
8288
|
-
+ (stage ? stagePrompt(stage, carried) : ''));
|
|
8387
|
+
+ (stage ? stagePrompt(stage, carried, Boolean(plan?.body)) : ''));
|
|
8289
8388
|
|
|
8290
8389
|
const args = ['-p', promptText, '--output-format', 'stream-json', '--dangerously-skip-permissions'];
|
|
8291
8390
|
if (run.model) {
|
|
@@ -8334,6 +8433,12 @@ async function spawnAntigravity(config, run, runToken, cwd, baseCommit, resume,
|
|
|
8334
8433
|
// Code one does, with no code of its own.
|
|
8335
8434
|
running.set(run.id, child);
|
|
8336
8435
|
|
|
8436
|
+
// What the console can see of the checkout, and the commits as they land —
|
|
8437
|
+
// the same watcher as the Claude Code path. Without it the platform never
|
|
8438
|
+
// hears of this run's commits, so the finish rules (R40) see a run that
|
|
8439
|
+
// wrote nothing and queue no push and no pull request.
|
|
8440
|
+
const workingCopy = watchWorkingCopy(config, run, cwd, baseCommit);
|
|
8441
|
+
|
|
8337
8442
|
// stream-json arrives in chunks that split mid-line, same as Claude
|
|
8338
8443
|
// Code's — buffer until a full line rather than assuming one chunk is
|
|
8339
8444
|
// one event.
|
|
@@ -8430,6 +8535,7 @@ async function spawnAntigravity(config, run, runToken, cwd, baseCommit, resume,
|
|
|
8430
8535
|
});
|
|
8431
8536
|
|
|
8432
8537
|
child.on('error', async (failure) => {
|
|
8538
|
+
workingCopy.stop();
|
|
8433
8539
|
running.delete(run.id);
|
|
8434
8540
|
const summary = `Could not spawn Antigravity: ${failure.message}`;
|
|
8435
8541
|
await transcript.flush();
|
|
@@ -8439,6 +8545,7 @@ async function spawnAntigravity(config, run, runToken, cwd, baseCommit, resume,
|
|
|
8439
8545
|
});
|
|
8440
8546
|
|
|
8441
8547
|
child.on('close', async (code) => {
|
|
8548
|
+
workingCopy.stop();
|
|
8442
8549
|
running.delete(run.id);
|
|
8443
8550
|
await transcript.flush();
|
|
8444
8551
|
const text = result?.response
|
|
@@ -8466,6 +8573,10 @@ async function spawnAntigravity(config, run, runToken, cwd, baseCommit, resume,
|
|
|
8466
8573
|
if (!stage) {
|
|
8467
8574
|
await settleActions(config, run, cwd);
|
|
8468
8575
|
}
|
|
8576
|
+
// The last reading, as on the Claude Code path: after the agent has
|
|
8577
|
+
// stopped and after anything the rules did.
|
|
8578
|
+
await reportCommits(config, run, cwd, baseCommit);
|
|
8579
|
+
|
|
8469
8580
|
resolvePromise(stage
|
|
8470
8581
|
? { code, signal: null, text, turnEnded, usage: childTotals }
|
|
8471
8582
|
: undefined);
|
|
@@ -8535,17 +8646,18 @@ async function spawnCodex(config, run, runToken, cwd, baseCommit, resume, projec
|
|
|
8535
8646
|
skills, stageExperts, readOnlyStage, instincts, briefing, plan, lifecycle, stage, carried,
|
|
8536
8647
|
codexCmd, granted, shield, usage = null) {
|
|
8537
8648
|
const failedBeforeSpawn = async (summary) => {
|
|
8538
|
-
log(` ${summary}`);
|
|
8649
|
+
log(` ${summary.split('\n')[0]}`);
|
|
8650
|
+
await sayOnTheRun(config, run, summary);
|
|
8539
8651
|
await finish(config, run, 'FAILED', summary);
|
|
8540
8652
|
return stage ? { code: -1, signal: null, text: summary, turnEnded: false } : undefined;
|
|
8541
8653
|
};
|
|
8542
8654
|
|
|
8543
8655
|
const mayWrite = writesCodeProfile(run) && !readOnlyStage;
|
|
8544
8656
|
if (mayWrite && !granted.everything) {
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8657
|
+
// It must commit and push, which its sandbox refuses, and there is no
|
|
8658
|
+
// allow-list for its shell — so everything, or nothing. Read-only runs
|
|
8659
|
+
// and stages need nothing.
|
|
8660
|
+
return failedBeforeSpawn(needsEverything('codex', thisMachine(config), run));
|
|
8549
8661
|
}
|
|
8550
8662
|
if (mayWrite) {
|
|
8551
8663
|
log(' !! this machine allows EVERYTHING: this Codex session runs without a sandbox');
|
|
@@ -8591,6 +8703,12 @@ async function spawnCodex(config, run, runToken, cwd, baseCommit, resume, projec
|
|
|
8591
8703
|
args: [serverPath],
|
|
8592
8704
|
envVars: Object.keys(cawdevEnv),
|
|
8593
8705
|
enabledTools: codexCawdevTools(run, stage, mayWrite),
|
|
8706
|
+
// Codex cuts an MCP call off after 60 s unless told otherwise, and
|
|
8707
|
+
// `ask_user` waits up to ten minutes for a person: a question asked from
|
|
8708
|
+
// Codex died before anybody could answer it, so it did not ask — it
|
|
8709
|
+
// stopped. Longer than ask_user's own wait (mcp/server.mjs), so the
|
|
8710
|
+
// question is answered or handed back as pending, never cut off.
|
|
8711
|
+
toolTimeoutSec: CAWDEV_TOOL_TIMEOUT_SEC,
|
|
8594
8712
|
});
|
|
8595
8713
|
|
|
8596
8714
|
const cannotDelegate = stageExperts.length
|
|
@@ -8612,7 +8730,7 @@ async function spawnCodex(config, run, runToken, cwd, baseCommit, resume, projec
|
|
|
8612
8730
|
})
|
|
8613
8731
|
+ inlinedSkillsFor(skills)
|
|
8614
8732
|
+ expertsBlockFor([], cannotDelegate)
|
|
8615
|
-
+ (stage ? stagePrompt(stage, carried) : ''));
|
|
8733
|
+
+ (stage ? stagePrompt(stage, carried, Boolean(plan?.body)) : ''));
|
|
8616
8734
|
|
|
8617
8735
|
const model = stage?.model ?? run.model;
|
|
8618
8736
|
const args = codexArgs({
|
|
@@ -8651,6 +8769,13 @@ async function spawnCodex(config, run, runToken, cwd, baseCommit, resume, projec
|
|
|
8651
8769
|
child.cawdevModel = model ?? null;
|
|
8652
8770
|
// `reapCancelled` SIGTERMs whatever is here, whichever CLI it is.
|
|
8653
8771
|
running.set(run.id, child);
|
|
8772
|
+
|
|
8773
|
+
// What the console can see of the checkout, and the commits as they land —
|
|
8774
|
+
// the same watcher as the Claude Code path. Without it the platform never
|
|
8775
|
+
// hears of this run's commits, so the finish rules (R40) see a run that
|
|
8776
|
+
// wrote nothing and queue no push and no pull request.
|
|
8777
|
+
const workingCopy = watchWorkingCopy(config, run, cwd, baseCommit);
|
|
8778
|
+
|
|
8654
8779
|
child.stdin.on('error', () => {
|
|
8655
8780
|
// A CLI that died before reading its prompt says why on stderr.
|
|
8656
8781
|
});
|
|
@@ -8728,6 +8853,7 @@ async function spawnCodex(config, run, runToken, cwd, baseCommit, resume, projec
|
|
|
8728
8853
|
});
|
|
8729
8854
|
|
|
8730
8855
|
child.on('error', async (failure) => {
|
|
8856
|
+
workingCopy.stop();
|
|
8731
8857
|
running.delete(run.id);
|
|
8732
8858
|
const summary = `Could not spawn Codex: ${failure.message}`;
|
|
8733
8859
|
await transcript.flush();
|
|
@@ -8736,6 +8862,7 @@ async function spawnCodex(config, run, runToken, cwd, baseCommit, resume, projec
|
|
|
8736
8862
|
});
|
|
8737
8863
|
|
|
8738
8864
|
child.on('close', async (code) => {
|
|
8865
|
+
workingCopy.stop();
|
|
8739
8866
|
running.delete(run.id);
|
|
8740
8867
|
await transcript.flush();
|
|
8741
8868
|
const text = failed ?? lastSaid ?? lastError ?? (err.trim() || 'Codex produced no output.');
|
|
@@ -8750,6 +8877,10 @@ async function spawnCodex(config, run, runToken, cwd, baseCommit, resume, projec
|
|
|
8750
8877
|
if (!stage) {
|
|
8751
8878
|
await settleActions(config, run, cwd);
|
|
8752
8879
|
}
|
|
8880
|
+
// The last reading, as on the Claude Code path: after the agent has
|
|
8881
|
+
// stopped and after anything the rules did.
|
|
8882
|
+
await reportCommits(config, run, cwd, baseCommit);
|
|
8883
|
+
|
|
8753
8884
|
resolvePromise(stage
|
|
8754
8885
|
? { code, signal: null, text, turnEnded: ended && !failed, usage: childTotals }
|
|
8755
8886
|
: undefined);
|