flowviant 0.125.7 → 0.125.12
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/README.md +80 -1
- package/bin/cli.mjs +7 -1
- package/bin/lib/activityFeed.mjs +69 -0
- package/bin/lib/agentMergeReports.mjs +46 -0
- package/bin/lib/agentMergeResolution.mjs +48 -0
- package/bin/lib/agentPlan.mjs +14 -2
- package/bin/lib/agentTaskTypes.mjs +11 -7
- package/bin/lib/artifactContracts.mjs +8 -0
- package/bin/lib/artifactIteration.mjs +18 -0
- package/bin/lib/artifactReporter.mjs +16 -3
- package/bin/lib/claudePosture.mjs +21 -0
- package/bin/lib/claudeStream.mjs +25 -4
- package/bin/lib/codexResearch.mjs +35 -5
- package/bin/lib/commandSpecs.mjs +8 -5
- package/bin/lib/daemonJournal.mjs +102 -0
- package/bin/lib/daemonLogging.mjs +22 -18
- package/bin/lib/fleet.mjs +16 -6
- package/bin/lib/fleetReports.mjs +12 -5
- package/bin/lib/hooks/orchestrator.mjs +3 -1
- package/bin/lib/logReplay.mjs +133 -0
- package/bin/lib/nativeAgentTools.mjs +20 -3
- package/bin/lib/observedLines.mjs +63 -0
- package/bin/lib/orchestrator.mjs +40 -7
- package/bin/lib/orchestratorNative.mjs +133 -3
- package/bin/lib/orchestratorPrompt.mjs +51 -4
- package/bin/lib/postureTools.mjs +14 -6
- package/bin/lib/printable.mjs +1 -1
- package/bin/lib/prompts.mjs +122 -7
- package/bin/lib/repositoryDelivery.mjs +1 -1
- package/bin/lib/repositoryMerge.mjs +1 -1
- package/bin/lib/researchHandoff.mjs +144 -0
- package/bin/lib/researchHandoffText.mjs +3 -0
- package/bin/lib/researchKnowledge.mjs +60 -0
- package/bin/lib/researchRestoration.mjs +38 -0
- package/bin/lib/resultInputs.mjs +83 -0
- package/bin/lib/runTurn.mjs +76 -43
- package/bin/lib/runtimeClaude.mjs +9 -4
- package/bin/lib/runtimeCodex.mjs +51 -22
- package/bin/lib/runtimeEvents.mjs +17 -7
- package/bin/lib/testFindings.mjs +60 -0
- package/bin/lib/testFindingsContract.mjs +2 -0
- package/bin/lib/turnDiagnostics.mjs +2 -1
- package/bin/lib/turnOutput.mjs +63 -0
- package/bin/lib/turnRecord.mjs +11 -0
- package/bin/lib/turnSubagents.mjs +13 -3
- package/bin/lib/ui.mjs +2 -2
- package/bin/lib/uplinkScrub.mjs +49 -0
- package/bin/lib/views.mjs +49 -58
- package/bin/lib/wikiRunner.mjs +2 -2
- package/bin/lib/work.mjs +2 -2
- package/bin/lib/workActivity.mjs +65 -0
- package/bin/lib/workAgentBriefRunner.mjs +2 -2
- package/bin/lib/workAgentMerges.mjs +57 -52
- package/bin/lib/workAgentPlans.mjs +76 -27
- package/bin/lib/workAgentPrecheck.mjs +1 -1
- package/bin/lib/workAgentTurnExecution.mjs +55 -9
- package/bin/lib/workAgentTurnOutcome.mjs +12 -3
- package/bin/lib/workAgentTurnReports.mjs +17 -3
- package/bin/lib/workAgentTurnShape.mjs +8 -5
- package/bin/lib/workAgentTurns.mjs +8 -6
- package/bin/lib/workIntake.mjs +8 -2
- package/bin/lib/workMergeBatch.mjs +7 -1
- package/bin/lib/workPlanRecovery.mjs +50 -0
- package/bin/lib/workSessionTurns.mjs +7 -2
- package/package.json +1 -1
package/bin/lib/runTurn.mjs
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { promptTransport } from './promptTransport.mjs';
|
|
14
|
+
import { prepareResearchLaunch } from './researchHandoff.mjs';
|
|
15
|
+
import { runResearchWithReceipt } from './researchRestoration.mjs';
|
|
14
16
|
import { runIsolatedResearch } from './codexResearch.mjs';
|
|
15
17
|
import { spawn } from 'node:child_process';
|
|
16
18
|
import { runtimeById } from './runtimes.mjs';
|
|
@@ -21,7 +23,8 @@ import { never, workLanesStopped } from './standDownGate.mjs';
|
|
|
21
23
|
import { learnCodexLimits, recordClaudeRateLimit } from './runtimeLimits.mjs';
|
|
22
24
|
import { initHarvestFor, recordMcpServers, recordSkills } from './runtimeCapabilities.mjs';
|
|
23
25
|
import { codexTurnModel } from './turnModel.mjs';
|
|
24
|
-
import {
|
|
26
|
+
import { createTurnOutput } from './turnOutput.mjs';
|
|
27
|
+
import { createObservedLines } from './observedLines.mjs';
|
|
25
28
|
import { startTurnDiagnostics } from './turnDiagnostics.mjs';
|
|
26
29
|
import { createClaudeInput } from './claudeInput.mjs';
|
|
27
30
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
@@ -29,6 +32,8 @@ import { createTurnSubagents } from './turnSubagents.mjs';
|
|
|
29
32
|
|
|
30
33
|
/** How long a Codex turn's answer may wait on its rollout's model line. */
|
|
31
34
|
const CODEX_MODEL_READ_MS = 1_500;
|
|
35
|
+
/** How long an exited CLI's turn waits for its streams to close (see the `exit` handler). */
|
|
36
|
+
export const EXIT_CLOSE_GRACE_MS = 2_000;
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* THE ENVIRONMENT A CLI TURN IS SPAWNED WITH — the daemon's own, plus the
|
|
@@ -61,7 +66,7 @@ export function cliEnv(mcpEnv, { display = false, browserHome = null } = {}) {
|
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
// One CLI turn. Output is captured (for sentinel detection) and streamed
|
|
64
|
-
// through
|
|
69
|
+
// through as complete timestamped records carrying work and turn identity.
|
|
65
70
|
//
|
|
66
71
|
// The event stream is parsed, always: only the humanized tool activity reaches
|
|
67
72
|
// the console (a legible stream of `read …`, `grep …`, `+ node …`), assistant
|
|
@@ -76,12 +81,14 @@ export function runTurn(opts) {
|
|
|
76
81
|
// (codexResearch.mjs). After the teardown it copies nothing either: the
|
|
77
82
|
// stand-down gate below is asked before the copy, not only before the spawn.
|
|
78
83
|
if (opts.runtime === 'codex' && opts.profile === 'research')
|
|
79
|
-
return workLanesStopped() ? never() : runIsolatedResearch(opts, runPlainTurn);
|
|
84
|
+
return workLanesStopped() ? never() : runIsolatedResearch(opts, prepared => runResearchWithReceipt(prepared, runPlainTurn));
|
|
85
|
+
if (opts.profile === 'research' && opts.researchHandoff)
|
|
86
|
+
return prepareResearchLaunch(opts).then(prepared => runResearchWithReceipt(prepared, runPlainTurn));
|
|
80
87
|
return runPlainTurn(opts);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
function runPlainTurn(opts) {
|
|
84
|
-
const { prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, agentTools, runtime = 'claude',
|
|
91
|
+
const { prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, agentTools, runtime = 'claude', onSpawn, answerFromResult, onActivity, onToolEvent: callerOnToolEvent, onInit, onUsage, onRateLimit, onModel, onThreadId, onAnswer, vaultDir, knowledgeDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId, display, browserHome } = opts;
|
|
85
92
|
/**
|
|
86
93
|
* AFTER THE TEARDOWN, NO TURN STARTS AND NO TURN ENDS (standDownGate.mjs).
|
|
87
94
|
* A stand-down that drains a deploy keeps this process alive; without this
|
|
@@ -90,11 +97,19 @@ function runPlainTurn(opts) {
|
|
|
90
97
|
* Both answers stay pending instead, as `process.exit` left them.
|
|
91
98
|
*/
|
|
92
99
|
if (workLanesStopped()) return never();
|
|
100
|
+
const output = createTurnOutput(opts);
|
|
101
|
+
const emit = output.emit;
|
|
102
|
+
const onToolEvent = (name, input, meta, observedAt) => {
|
|
103
|
+
output.tool(name, input, meta, observedAt);
|
|
104
|
+
callerOnToolEvent?.(name, input, meta);
|
|
105
|
+
};
|
|
93
106
|
return new Promise((resolveTurn, rejectTurn) => {
|
|
94
107
|
const resolve = (v) => {
|
|
108
|
+
output.close();
|
|
95
109
|
if (!workLanesStopped()) resolveTurn(v);
|
|
96
110
|
};
|
|
97
111
|
const reject = (error) => {
|
|
112
|
+
output.close();
|
|
98
113
|
if (!workLanesStopped()) rejectTurn(error);
|
|
99
114
|
};
|
|
100
115
|
const rt = runtimeById(runtime);
|
|
@@ -105,7 +120,7 @@ function runPlainTurn(opts) {
|
|
|
105
120
|
// lane's `unsupported` check). Here the daemon log names the id and the
|
|
106
121
|
// turn answers nothing — never Claude wearing the other runtime's name.
|
|
107
122
|
if (!rt) {
|
|
108
|
-
|
|
123
|
+
output.error(`error: cannot run '${runtime}' — this daemon does not know that runtime; update flowviant`);
|
|
109
124
|
resolve('');
|
|
110
125
|
return;
|
|
111
126
|
}
|
|
@@ -116,7 +131,7 @@ function runPlainTurn(opts) {
|
|
|
116
131
|
*/
|
|
117
132
|
const turnProfile = resolveTurnProfile(opts);
|
|
118
133
|
if (turnProfile.error) {
|
|
119
|
-
|
|
134
|
+
output.error(`error: ${turnProfile.error}`);
|
|
120
135
|
resolve('');
|
|
121
136
|
return;
|
|
122
137
|
}
|
|
@@ -131,7 +146,7 @@ function runPlainTurn(opts) {
|
|
|
131
146
|
// adapter learns to express one this opens by itself.
|
|
132
147
|
const offRuntime = turnProfile.onlyOn?.(rt);
|
|
133
148
|
if (offRuntime) {
|
|
134
|
-
|
|
149
|
+
output.error(`error: ${offRuntime}`);
|
|
135
150
|
resolve('');
|
|
136
151
|
return;
|
|
137
152
|
}
|
|
@@ -140,7 +155,7 @@ function runPlainTurn(opts) {
|
|
|
140
155
|
// drive. Fail as a turn with no sentinel — the loop already treats that as
|
|
141
156
|
// "the protocol did not complete" and retries, rather than inventing a
|
|
142
157
|
// completion for work that never started.
|
|
143
|
-
|
|
158
|
+
output.error(`error: cannot run '${rt.label}' — ${rt.blocked}`);
|
|
144
159
|
resolve('');
|
|
145
160
|
return;
|
|
146
161
|
}
|
|
@@ -371,28 +386,13 @@ function runPlainTurn(opts) {
|
|
|
371
386
|
child.once('close', browser.cleanup);
|
|
372
387
|
child.once('error', browser.cleanup);
|
|
373
388
|
}
|
|
374
|
-
if (child.pid) onSpawn?.(child);
|
|
389
|
+
if (child.pid) { output.event(`${rt.label} started`, 'start'); onSpawn?.(child); }
|
|
375
390
|
// When this turn began, for the Codex rollout read: a `turn_context` older
|
|
376
391
|
// than this is a previous turn's (turnModel.mjs).
|
|
377
392
|
const spawnedAt = Date.now();
|
|
378
393
|
let out = '';
|
|
379
394
|
let stdinError = null;
|
|
380
395
|
const input = rt.liveInput === true && opts.liveMessages ? createClaudeInput(child, { onTaken: opts.liveMessages.onTaken, onContext: opts.onContext ?? null }) : null;
|
|
381
|
-
const pfx = label ? `${label} ` : '';
|
|
382
|
-
/**
|
|
383
|
-
* THE ONE SINK FOR WHAT A CLI OR A MODEL WROTE (2026-10-10, the audit's
|
|
384
|
-
* F133): activity labels — a Bash command, a Codex `say`, a file path —
|
|
385
|
-
* and the CLI's raw stderr. A prompt-injected agent can put an OSC 52 in
|
|
386
|
-
* a tool command as easily as in prose, and this wrote it to the
|
|
387
|
-
* operator's terminal as-is. Scrubbed HERE, before the daemon's own
|
|
388
|
-
* coloured `label` is added, so the prefix keeps its colour and nothing
|
|
389
|
-
* the model wrote keeps an escape sequence (printable.mjs, `printableText`).
|
|
390
|
-
*/
|
|
391
|
-
const emit = (s) => {
|
|
392
|
-
const text = printableText(String(s));
|
|
393
|
-
process.stdout.write(pfx ? text.replace(/\n/g, `\n${pfx}`) : text);
|
|
394
|
-
};
|
|
395
|
-
|
|
396
396
|
// EVERY TURN IS LINE-PARSED (2026-10-10, the audit). Claude's adapter
|
|
397
397
|
// always asks for `--output-format stream-json` and every other runtime
|
|
398
398
|
// brings its own `parse` — for Codex the JSONL stream is the only output
|
|
@@ -401,7 +401,6 @@ function runPlainTurn(opts) {
|
|
|
401
401
|
// already drifted once (it called process.exit(1)); both are gone. A line
|
|
402
402
|
// that is not JSON is still kept as text (claudeStream.mjs), and stderr
|
|
403
403
|
// still lands in the haystack the sentinels read.
|
|
404
|
-
let buf = '';
|
|
405
404
|
const appendText = (t) => {
|
|
406
405
|
out += t;
|
|
407
406
|
};
|
|
@@ -419,13 +418,15 @@ function runPlainTurn(opts) {
|
|
|
419
418
|
* waits on a file read. See runtimeLimits.mjs.
|
|
420
419
|
*/
|
|
421
420
|
const onRate = (info) => {
|
|
421
|
+
if (info?.status === 'rejected' || info?.status === 'allowed_warning')
|
|
422
|
+
output.event(`${rt.label} plan limit: ${info.status}${typeof info.rateLimitType === 'string' ? ` (${info.rateLimitType})` : ''}`, 'limit');
|
|
422
423
|
recordClaudeRateLimit(info);
|
|
423
424
|
onRateLimit?.(info);
|
|
424
425
|
};
|
|
425
426
|
// The native children this CLI starts, observed (turnSubagents.mjs).
|
|
426
427
|
const natives = createTurnSubagents({ runtime: rt.id, cwd, since: spawnedAt, env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
427
428
|
owned: opts.orchestration === true, onSubagent: opts.onSubagent, onSpawnResult: opts.onSpawnResult, resumeThreadId,
|
|
428
|
-
sinks: { emit, onActivity, onToolEvent, appendText } });
|
|
429
|
+
sinks: { emit, onActivity, onToolEvent, appendText, onDetail: output.detail } });
|
|
429
430
|
let codexThread = rt.id === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
|
|
430
431
|
/**
|
|
431
432
|
* THE MODEL THIS SPAWN RAN ON, ONCE (2026-09-29, turnModel.mjs). Claude
|
|
@@ -459,10 +460,13 @@ function runPlainTurn(opts) {
|
|
|
459
460
|
const taskPlan = new Map();
|
|
460
461
|
/** One line of the child's stdout, in whichever dialect it speaks. */
|
|
461
462
|
let cliFailed = false;
|
|
462
|
-
const onLine = (line) => {
|
|
463
|
+
const onLine = (line, observedAt) => {
|
|
464
|
+
const emitObserved = (text, meta) => emit(text, { ...meta, observedAt });
|
|
465
|
+
const toolObserved = (name, input, meta) => onToolEvent(name, input, meta, observedAt);
|
|
463
466
|
try {
|
|
464
467
|
const event = JSON.parse(line);
|
|
465
468
|
const parentEvent = event.parent_tool_use_id == null && !event.isSidechain;
|
|
469
|
+
opts.researchReceipt?.event(event);
|
|
466
470
|
if (input && parentEvent) {
|
|
467
471
|
input.event(event);
|
|
468
472
|
if (event.type === 'system' && event.subtype === 'init' && event.session_id) opts.liveMessages.onInit(event.session_id);
|
|
@@ -478,7 +482,7 @@ function runPlainTurn(opts) {
|
|
|
478
482
|
}
|
|
479
483
|
} catch { /* ordinary non-JSON CLI output */ }
|
|
480
484
|
if (!rt.parse)
|
|
481
|
-
return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit: onInitAll, onUsage, onRateLimit: onRate, onModel: onRanOn, subagents: natives.collector, tasks: taskPlan });
|
|
485
|
+
return handleStreamLine(line, { cwd, emit: emitObserved, onActivity, onToolEvent: toolObserved, appendText, answerFromResult, onInit: onInitAll, onUsage, onRateLimit: onRate, onModel: onRanOn, subagents: natives.collector, tasks: taskPlan, onDetail: (text, meta) => output.detail(text, { ...meta, observedAt }) });
|
|
482
486
|
const ev = rt.parse(line, cwd);
|
|
483
487
|
if (!ev) return;
|
|
484
488
|
// The conversation id, when the runtime announces one (codex's
|
|
@@ -495,14 +499,17 @@ function runPlainTurn(opts) {
|
|
|
495
499
|
// A receipt the controller cannot take never ends this line's handling,
|
|
496
500
|
// as when the lane parsed the line again itself (2026-10-18).
|
|
497
501
|
if (ev.spawnResult) try { opts.onSpawnResult?.(ev.spawnResult); } catch { /* the controller's own failure */ }
|
|
498
|
-
if (ev.toolEvent)
|
|
502
|
+
if (ev.toolEvent) toolObserved(...ev.toolEvent);
|
|
503
|
+
for (const tool of ev.toolEvents ?? []) toolObserved(...tool);
|
|
499
504
|
if (ev.usage) onUsage?.(ev.usage);
|
|
500
505
|
// One agent MESSAGE, alone — see parseCodexLine's `answer`. Last call
|
|
501
506
|
// wins at the caller, which is what "the final answer" means.
|
|
502
507
|
if (typeof ev.answer === 'string') onAnswer?.(ev.answer);
|
|
508
|
+
if (ev.detail) output.detail(ev.detail.text, { source: ev.detail.source, observedAt });
|
|
503
509
|
if (ev.text) appendText(ev.text);
|
|
504
510
|
if (ev.activity) {
|
|
505
|
-
|
|
511
|
+
emitObserved(ev.activity.kind === 'error' ? ev.activity.full ?? ev.activity.label : ev.activity.label + '\n', ev.activity);
|
|
512
|
+
if (ev.activity.error) emitObserved(ev.activity.error, { ...ev.activity, kind: 'error', full: ev.activity.error });
|
|
506
513
|
onActivity?.(ev.activity);
|
|
507
514
|
}
|
|
508
515
|
};
|
|
@@ -513,19 +520,18 @@ function runPlainTurn(opts) {
|
|
|
513
520
|
// `setEncoding` holds the partial bytes over to the next chunk.
|
|
514
521
|
child.stdout.setEncoding('utf8');
|
|
515
522
|
child.stderr.setEncoding('utf8');
|
|
523
|
+
let ended = false;
|
|
524
|
+
const stdoutLines = createObservedLines((line, at) => { if (line.trim()) onLine(line, at); });
|
|
525
|
+
const stderrLines = createObservedLines((line, observedAt) => emit(line + '\n', { observedAt, source: 'stderr' }));
|
|
516
526
|
child.stdout.on('data', (d) => {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
520
|
-
const line = buf.slice(0, nl);
|
|
521
|
-
buf = buf.slice(nl + 1);
|
|
522
|
-
if (line.trim()) onLine(line);
|
|
523
|
-
}
|
|
527
|
+
if (ended) return;
|
|
528
|
+
stdoutLines.push(d);
|
|
524
529
|
});
|
|
525
530
|
// stderr is not JSON (warnings/errors) — pass through and keep for sentinels.
|
|
526
531
|
child.stderr.on('data', (s) => {
|
|
532
|
+
if (ended) return;
|
|
527
533
|
out += s;
|
|
528
|
-
|
|
534
|
+
stderrLines.push(s);
|
|
529
535
|
});
|
|
530
536
|
/**
|
|
531
537
|
* A SPAWN THAT FAILS FAILS THE TURN, NOT THE DAEMON — the one copy of the
|
|
@@ -541,32 +547,42 @@ function runPlainTurn(opts) {
|
|
|
541
547
|
* this turn with the native reason; the lane records a failed report.
|
|
542
548
|
*/
|
|
543
549
|
const onSpawnError = (e) => {
|
|
550
|
+
opts.researchReceipt?.fail();
|
|
544
551
|
input?.close();
|
|
552
|
+
stdoutLines.end();
|
|
553
|
+
stderrLines.end();
|
|
545
554
|
natives.abandon();
|
|
546
555
|
if (opts.liveMessages) void opts.liveMessages.stop();
|
|
547
556
|
finishDiagnostics({ pid: child.pid, spawnError: typeof e.code === 'string' ? e.code : 'SPAWN_ERROR' });
|
|
548
557
|
releaseArgs();
|
|
549
558
|
if (e.code === 'ENOENT') {
|
|
550
|
-
|
|
559
|
+
output.error(`error: '${rt.bin}' CLI not found on PATH. Install ${rt.label} first: ${rt.install}`);
|
|
551
560
|
resolve('');
|
|
552
561
|
return;
|
|
553
562
|
}
|
|
554
|
-
|
|
563
|
+
output.error(e);
|
|
555
564
|
reject(e);
|
|
556
565
|
};
|
|
557
566
|
child.on('error', onSpawnError);
|
|
558
|
-
|
|
567
|
+
const onClose = (code, signal) => {
|
|
568
|
+
if (ended) return;
|
|
569
|
+
ended = true;
|
|
559
570
|
input?.close();
|
|
560
571
|
releaseArgs();
|
|
561
572
|
opts.onExit?.(code, signal);
|
|
562
|
-
|
|
573
|
+
stdoutLines.end();
|
|
574
|
+
stderrLines.end();
|
|
575
|
+
if (code !== 0 || signal) opts.researchReceipt?.fail();
|
|
563
576
|
finishDiagnostics({ code, signal, pid: child.pid, output: out, cliFailed, inputError: stdinError?.code });
|
|
564
577
|
const handBack = async () => {
|
|
565
578
|
if (opts.liveMessages) await opts.liveMessages.stop();
|
|
566
579
|
await natives.drain(CODEX_MODEL_READ_MS);
|
|
567
580
|
natives.sweep();
|
|
581
|
+
if (signal) output.event(`${rt.label} interrupted (${signal})`, 'interrupted');
|
|
582
|
+
else if (code !== 0) output.event(`${rt.label} exited with code ${code}`, 'error');
|
|
568
583
|
if (stdinError) {
|
|
569
|
-
|
|
584
|
+
output.error(stdinError);
|
|
585
|
+
reject(stdinError);
|
|
570
586
|
return;
|
|
571
587
|
}
|
|
572
588
|
resolve(out);
|
|
@@ -598,12 +614,29 @@ function runPlainTurn(opts) {
|
|
|
598
614
|
return;
|
|
599
615
|
}
|
|
600
616
|
handBack();
|
|
617
|
+
};
|
|
618
|
+
child.on('close', onClose);
|
|
619
|
+
/**
|
|
620
|
+
* THE CLI'S EXIT ENDS ITS TURN (2026-10-19). `close` waits for every
|
|
621
|
+
* stdio stream, and anything the CLI started that inherited them holds
|
|
622
|
+
* them open after the CLI is gone. A New task chat's Codex aborted its
|
|
623
|
+
* turn and exited; its turn stayed pending under a renewed lease for six
|
|
624
|
+
* and a half hours, until the machine restarted. An exited CLI says
|
|
625
|
+
* nothing more, so a turn its streams still hold settles with what it
|
|
626
|
+
* said. The streams keep draining, since the holder is not this turn's
|
|
627
|
+
* to break (`interruptThenKill`): a later line is dropped, never parsed
|
|
628
|
+
* into a settled turn.
|
|
629
|
+
*/
|
|
630
|
+
child.once('exit', (code, signal) => {
|
|
631
|
+
const timer = setTimeout(() => onClose(code, signal), EXIT_CLOSE_GRACE_MS);
|
|
632
|
+
child.once('close', () => clearTimeout(timer));
|
|
601
633
|
});
|
|
602
634
|
if (transport.text !== null) {
|
|
603
635
|
// end() buffers with backpressure and closes at EOF; Codex reads the
|
|
604
636
|
// entire prompt before starting. An early close must not accept output
|
|
605
637
|
// from a turn whose complete authorized input never reached the CLI.
|
|
606
638
|
child.stdin.on('error', (error) => {
|
|
639
|
+
opts.researchReceipt?.fail('input_error');
|
|
607
640
|
stdinError = Object.assign(new Error(`${rt.label} could not receive the complete turn input (${error.code || 'PIPE_ERROR'}).`), { code: error.code || 'PIPE_ERROR' });
|
|
608
641
|
opts.onCliFailure?.();
|
|
609
642
|
child.kill('SIGTERM');
|
|
@@ -16,9 +16,10 @@ import { join } from 'node:path';
|
|
|
16
16
|
import { MODEL, USER_AGENT } from './config.mjs';
|
|
17
17
|
import { NO_SETTING_SOURCES, personalClaudeSettings, releaseTurnSettings, withPersonalSettings, writeTurnSettings } from './claudePersonal.mjs';
|
|
18
18
|
import { machineMcpServers } from './claudeMachineMcp.mjs';
|
|
19
|
-
import { claudePermFor, withMachineMcp } from './claudePosture.mjs';
|
|
19
|
+
import { claudePermFor, withMachineMcp, withNativeChildren } from './claudePosture.mjs';
|
|
20
20
|
import { withSettings } from './claudeSettingsArg.mjs';
|
|
21
21
|
import { orchestratorHooks } from './orchestratorHooks.mjs';
|
|
22
|
+
import { orchestratesPosture } from './orchestratorPrompt.mjs';
|
|
22
23
|
import { versionAtLeast } from './versionOrder.mjs';
|
|
23
24
|
|
|
24
25
|
/** The release the agent lane first measured `--system-prompt-snapshot` on
|
|
@@ -122,7 +123,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
122
123
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
123
124
|
* above the conversation rather than inside it.
|
|
124
125
|
*/
|
|
125
|
-
args({ writableDirs = [], prompt, system, model, effort, resume, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, vaultDir, agentTools, personalSettings, machineMcp, baseMcp = [], mcpCheckout = null, streamInput = false, orchestration = false, claudeVersion = null }) {
|
|
126
|
+
args({ writableDirs = [], prompt, system, model, effort, resume, perm: postureList, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, vaultDir, agentTools, personalSettings, machineMcp, baseMcp = [], mcpCheckout = null, streamInput = false, orchestration = false, claudeVersion = null }) {
|
|
126
127
|
// THE CARTOGRAPHER'S FENCE NAMES ITS VAULT (2026-10-10): its one write rule
|
|
127
128
|
// is the vault's absolute path (claudePosture.mjs `wikiPermFor`), so a turn
|
|
128
129
|
// with no vault, or one no rule can name, has no list — refused here in
|
|
@@ -131,7 +132,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
131
132
|
if (profile === 'wiki' && !vaultDir) {
|
|
132
133
|
throw new Error("a wiki turn needs its vault to fence — none reached claude's builder");
|
|
133
134
|
}
|
|
134
|
-
if (profile === 'wiki' && !Array.isArray(
|
|
135
|
+
if (profile === 'wiki' && !Array.isArray(postureList)) {
|
|
135
136
|
throw new Error(`a wiki turn's vault must be a path a Claude Code permission rule can name — ${JSON.stringify(vaultDir)} is not`);
|
|
136
137
|
}
|
|
137
138
|
const a = [];
|
|
@@ -190,6 +191,10 @@ export const CLAUDE_RUNTIME = {
|
|
|
190
191
|
* agent renders the CURRENT base snapshot, not the first turn's cache.
|
|
191
192
|
*/
|
|
192
193
|
const fenced = profile !== 'build';
|
|
194
|
+
// THE GATE RIDES A CODE OR RESEARCH CARD (2026-10-19, orchestratorPrompt.mjs);
|
|
195
|
+
// a research card's list then admits the children its fence denied.
|
|
196
|
+
const gated = orchestration && orchestratesPosture(profile);
|
|
197
|
+
const perm = gated && fenced ? withNativeChildren(postureList) : postureList;
|
|
193
198
|
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
194
199
|
// A FENCED POSTURE NAMES ITS OWN MODE (2026-10-10). Its lists fence only
|
|
195
200
|
// under the CLI's `default` mode, where a tool the list does not name is
|
|
@@ -274,7 +279,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
274
279
|
const mounted = machine ? withMachineMcp(perm, machineMcp) : perm;
|
|
275
280
|
// The orchestrator's hooks join that same `--settings`: the CLI applies
|
|
276
281
|
// only the last one it is handed (claudeSettingsArg.mjs `withSettings`).
|
|
277
|
-
const postured =
|
|
282
|
+
const postured = gated ? withSettings(mounted, { hooks: orchestratorHooks(), disableAllHooks: false }) : mounted;
|
|
278
283
|
let tail;
|
|
279
284
|
try {
|
|
280
285
|
tail = fenced ? withPersonalSettings(postured, personalSettings ?? personalClaudeSettings()) : postured;
|
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { researchFence } from './codexResearch.mjs';
|
|
|
18
18
|
import { projectReadBoundary, projectReadFence } from './codexProjectRead.mjs';
|
|
19
19
|
import { versionAtLeast } from './versionOrder.mjs';
|
|
20
20
|
import { codexOrchestratorArgs } from './orchestratorHooks.mjs';
|
|
21
|
+
import { orchestratesPosture } from './orchestratorPrompt.mjs';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* FLOWVIANT'S OWN TOOLS NEED NO APPROVAL FROM A CHAT NOBODY CAN ANSWER
|
|
@@ -84,8 +85,31 @@ export const NO_REACH_OUT = Object.freeze([
|
|
|
84
85
|
*/
|
|
85
86
|
export const NO_WEB = Object.freeze(['-c', 'tools.web_search=false', '-c', 'web_search="disabled"']);
|
|
86
87
|
|
|
87
|
-
/** Fenced lanes and non-card turns never delegate
|
|
88
|
-
|
|
88
|
+
/** Fenced lanes and non-card turns never delegate — but an orchestrated
|
|
89
|
+
* research card, whose children read under the gate (2026-10-19).
|
|
90
|
+
* `features.multi_agent_v2=false` closes a person's own V2 switch, which
|
|
91
|
+
* beats every other key (measured on 0.163; harmless on 0.144). */
|
|
92
|
+
export const NO_SUBAGENTS = Object.freeze(['-c', 'features.multi_agent=false', '-c', 'features.goals=false', '-c', 'features.multi_agent_v2=false']);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* …AND ON A CATALOG V2 MODEL, `agents.enabled=false` (2026-10-19). Measured on
|
|
96
|
+
* codex-cli 0.163.0, offline: `features.multi_agent=false` did not remove
|
|
97
|
+
* native subagents on gpt-6.1-sol, gpt-6-luna or gpt-5.6-sol, whose catalog
|
|
98
|
+
* entry names multi-agent V2 — a stock research turn listed `collaboration`
|
|
99
|
+
* and a spawn really started a child no gate saw (core/src/config/mod.rs:
|
|
100
|
+
* V2 if `features.multi_agent_v2`, else disabled only by `agents.enabled =
|
|
101
|
+
* false`, else the model's catalog version, and only then the feature).
|
|
102
|
+
* With the pair the namespace is gone and the call fails "unsupported call".
|
|
103
|
+
* `agents.enabled` first exists in 0.145.0: 0.144.x reads `[agents]` as a role
|
|
104
|
+
* map and refuses to start ("invalid type: boolean `false`, expected struct
|
|
105
|
+
* AgentRoleToml"), and there the catalog does not outrank the feature yet.
|
|
106
|
+
* So it rides only a CLI measured at 0.145.0 or later.
|
|
107
|
+
*/
|
|
108
|
+
export const AGENTS_ENABLED_MIN = '0.145.0';
|
|
109
|
+
export const noSubagents = (codexVersion) => [...NO_SUBAGENTS,
|
|
110
|
+
...(versionAtLeast(String(codexVersion ?? '').replace(/^codex-cli\s+/, ''), AGENTS_ENABLED_MIN) ? ['-c', 'agents.enabled=false'] : [])];
|
|
111
|
+
/** A card turn whose children the gate admits: native multi-agent on, goals off. */
|
|
112
|
+
export const CARD_SUBAGENTS = Object.freeze(['-c', 'features.multi_agent=true', '-c', 'features.goals=false']);
|
|
89
113
|
|
|
90
114
|
/** rust-v0.160.0 core/src/agent/child_config.rs applies the invoking turn's
|
|
91
115
|
* approval policy, permission profile and cwd AFTER role overrides, for V1
|
|
@@ -111,10 +135,11 @@ export const HERMETIC = Object.freeze(['--ignore-user-config', '--ignore-rules']
|
|
|
111
135
|
* needs the isolated research read fence because broad-read tools can read
|
|
112
136
|
* host credentials. Feature flags do not prove account/model entitlement.
|
|
113
137
|
*/
|
|
114
|
-
export const fencedTail = ({ imageGeneration = false, web = false } = {}) => [
|
|
138
|
+
export const fencedTail = ({ imageGeneration = false, web = false, subagents = false, codexVersion } = {}) => [
|
|
115
139
|
// Native web is paired with the copied, root-denied read fence.
|
|
116
140
|
...(web ? [] : NO_WEB),
|
|
117
|
-
|
|
141
|
+
// Only an orchestrated research card's fence admits children (CODEX_FENCES).
|
|
142
|
+
...(subagents ? CARD_SUBAGENTS : noSubagents(codexVersion)),
|
|
118
143
|
...NO_REACH_OUT,
|
|
119
144
|
imageGeneration ? '--enable' : '--disable',
|
|
120
145
|
'image_generation',
|
|
@@ -165,11 +190,11 @@ export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
|
|
|
165
190
|
* config: `codex exec` (0.156.1) has no `-P`, so every Codex wiki turn was
|
|
166
191
|
* refused before it started (0.114.0).
|
|
167
192
|
*/
|
|
168
|
-
const codexWikiFence = (vaultDir) => [
|
|
193
|
+
const codexWikiFence = (vaultDir, codexVersion) => [
|
|
169
194
|
'-c', 'permissions.flowviantwiki.extends=":read-only"',
|
|
170
195
|
'-c', `permissions.flowviantwiki.filesystem={${JSON.stringify(vaultDir)}="write"}`,
|
|
171
196
|
'-c', 'default_permissions="flowviantwiki"',
|
|
172
|
-
...fencedTail(),
|
|
197
|
+
...fencedTail({ codexVersion }),
|
|
173
198
|
];
|
|
174
199
|
|
|
175
200
|
function codexPrompt({ system, prompt, profile = 'build' }) {
|
|
@@ -287,8 +312,11 @@ export const CODEX_RUNTIME = {
|
|
|
287
312
|
// THE FENCE, picked by the same keys as `codexIgnoresUserConfig`: a fenced
|
|
288
313
|
// posture builds its own (CODEX_FENCES below); anything else is a build.
|
|
289
314
|
const fenced = codexIgnoresUserConfig({ profile, vaultDir });
|
|
315
|
+
// THE GATE RIDES A CODE OR RESEARCH CARD on a CLI whose children inherit
|
|
316
|
+
// the parent's sandbox (2026-10-19, orchestratorPrompt.mjs).
|
|
317
|
+
const gated = orchestration && orchestratesPosture(fenced ? profile : 'build') && codexSubagentsSafe(codexVersion);
|
|
290
318
|
if (fenced) {
|
|
291
|
-
a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation, knowledgeDir, projectReads }));
|
|
319
|
+
a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation, knowledgeDir, projectReads, subagents: gated, codexVersion }));
|
|
292
320
|
} else {
|
|
293
321
|
// The daemon's build posture, mapped: SAFE keeps writes inside the
|
|
294
322
|
// workspace, the default lets the agent run its own tests and git
|
|
@@ -303,9 +331,7 @@ export const CODEX_RUNTIME = {
|
|
|
303
331
|
if (writableDirs.length) a.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(writableDirs)}`);
|
|
304
332
|
a.push('-c', `sandbox_mode="${SAFE ? 'workspace-write' : 'danger-full-access'}"`);
|
|
305
333
|
if (cardTurn) a.push('-c', 'approval_policy="never"', ...(agentTools?.codexApprovals ?? []));
|
|
306
|
-
a.push(...(cardTurn && profile === 'build' && codexSubagentsSafe(codexVersion)
|
|
307
|
-
? ['-c', 'features.multi_agent=true', '-c', 'features.goals=false']
|
|
308
|
-
: NO_SUBAGENTS));
|
|
334
|
+
a.push(...(cardTurn && profile === 'build' && codexSubagentsSafe(codexVersion) ? CARD_SUBAGENTS : noSubagents(codexVersion)));
|
|
309
335
|
}
|
|
310
336
|
|
|
311
337
|
/**
|
|
@@ -321,7 +347,7 @@ export const CODEX_RUNTIME = {
|
|
|
321
347
|
if (a.includes('--ignore-user-config')) a.push(...(personal?.args ?? []));
|
|
322
348
|
|
|
323
349
|
a.push(...mcp, ...resultSchemaArgs);
|
|
324
|
-
if (
|
|
350
|
+
if (gated) a.push(...codexOrchestratorArgs());
|
|
325
351
|
a.push(streamInput ? '-' : codexPrompt({ system, prompt, profile }));
|
|
326
352
|
return a;
|
|
327
353
|
},
|
|
@@ -401,7 +427,7 @@ export const CODEX_RUNTIME = {
|
|
|
401
427
|
* Claude's research list was fenced to prevent. Codex research (0.117.0) runs
|
|
402
428
|
* under its own fence instead, over a copied view (CODEX_FENCES `research`).
|
|
403
429
|
*/
|
|
404
|
-
export function codexArtifactFence(cwd, { name, imageGeneration = false, what }) {
|
|
430
|
+
export function codexArtifactFence(cwd, { name, imageGeneration = false, what, codexVersion }) {
|
|
405
431
|
if (!cwd || !isAbsolute(cwd)) {
|
|
406
432
|
throw new Error(`${what} needs its worktree to fence — none reached codex's builder`);
|
|
407
433
|
}
|
|
@@ -423,14 +449,14 @@ export function codexArtifactFence(cwd, { name, imageGeneration = false, what })
|
|
|
423
449
|
'-c', `default_permissions="${name}"`,
|
|
424
450
|
// Nobody is on this end of the pipe to approve an escalation.
|
|
425
451
|
'-c', 'approval_policy="never"',
|
|
426
|
-
...fencedTail({ imageGeneration }),
|
|
452
|
+
...fencedTail({ imageGeneration, codexVersion }),
|
|
427
453
|
];
|
|
428
454
|
}
|
|
429
455
|
|
|
430
456
|
/** The design card's fence — `codexArtifactFence` under its own profile name,
|
|
431
457
|
* with native image generation available for supporting assets. */
|
|
432
|
-
export const codexDesignFence = (cwd) =>
|
|
433
|
-
codexArtifactFence(cwd, { name: 'flowviantdesign', imageGeneration: true, what: 'a design turn' });
|
|
458
|
+
export const codexDesignFence = (cwd, codexVersion) =>
|
|
459
|
+
codexArtifactFence(cwd, { name: 'flowviantdesign', imageGeneration: true, what: 'a design turn', codexVersion });
|
|
434
460
|
|
|
435
461
|
/**
|
|
436
462
|
* EVERY FENCED POSTURE'S ARGV, by the keys of CODEX_FENCED (a test holds the
|
|
@@ -440,7 +466,7 @@ const CODEX_FENCES = {
|
|
|
440
466
|
// A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — a different
|
|
441
467
|
// shape from Claude's verb allowlist, which is the whole reason `profile` is
|
|
442
468
|
// a promise rather than a flag list. See codexProjectRead.mjs.
|
|
443
|
-
consult: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail()],
|
|
469
|
+
consult: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail({ codexVersion: opts.codexVersion })],
|
|
444
470
|
// A PLANNING SESSION. Read-only on the filesystem, exactly like a consult —
|
|
445
471
|
// the writes it makes go through the control plane, not through this box —
|
|
446
472
|
// and for the same reason: its prompt is steered by anything a project
|
|
@@ -449,8 +475,8 @@ const CODEX_FENCES = {
|
|
|
449
475
|
// turn's scoped token. The plan token has the planning tools; a capture
|
|
450
476
|
// token has only staging and transcript tools. The server refuses
|
|
451
477
|
// everything outside that scope.
|
|
452
|
-
plan: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail()],
|
|
453
|
-
wiki: ({ vaultDir }) => codexWikiFence(vaultDir),
|
|
478
|
+
plan: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail({ codexVersion: opts.codexVersion })],
|
|
479
|
+
wiki: ({ vaultDir, codexVersion }) => codexWikiFence(vaultDir, codexVersion),
|
|
454
480
|
// AN IMAGE CARD (0.114.0): read the repo, write ONLY the worktree's
|
|
455
481
|
// `.flowviant/artifacts/`, generate pictures, and nothing else. MEASURED on
|
|
456
482
|
// codex-cli 0.156.1 (Linux, bubblewrap), with no model turn spent: under
|
|
@@ -477,18 +503,21 @@ const CODEX_FENCES = {
|
|
|
477
503
|
//
|
|
478
504
|
// Native images support every artifact kind; this profile still delivers
|
|
479
505
|
// PNG/WebP, while design/model/deck still deliver their self-contained page.
|
|
480
|
-
image: ({ cwd }) => codexArtifactFence(cwd, { name: 'flowviantimage', imageGeneration: true, what: 'an image turn' }),
|
|
506
|
+
image: ({ cwd, codexVersion }) => codexArtifactFence(cwd, { name: 'flowviantimage', imageGeneration: true, what: 'an image turn', codexVersion }),
|
|
481
507
|
// A MOCKUP, 3D-MODEL OR DECK CARD (0.115.0): the fence is
|
|
482
508
|
// `codexDesignFence`'s, argued and measured at `codexArtifactFence` above.
|
|
483
|
-
design: ({ cwd }) => codexDesignFence(cwd),
|
|
509
|
+
design: ({ cwd, codexVersion }) => codexDesignFence(cwd, codexVersion),
|
|
484
510
|
// A WRITE-UP CARD (0.117.0): live web over a COPIED view of the worktree,
|
|
485
511
|
// never the checkout or the login store, under a root-denied tool fence
|
|
486
512
|
// (codexResearch.mjs `researchFence`, built by runTurn's
|
|
487
513
|
// `runIsolatedResearch`). It is the one fence that keeps the web, so its
|
|
488
514
|
// tail leaves NO_WEB out and closes everything else.
|
|
489
|
-
research
|
|
515
|
+
// An orchestrated research card's children (2026-10-19) inherit this same
|
|
516
|
+
// permission profile, approval policy and cwd (0.160.0 child_config.rs);
|
|
517
|
+
// the gate refuses their writes.
|
|
518
|
+
research: ({ researchIsolation, subagents = false, codexVersion }) => {
|
|
490
519
|
if (!researchIsolation) throw new Error('Codex research requires its isolated input copy.');
|
|
491
|
-
return [...researchFence(researchIsolation), ...fencedTail({ web: true, imageGeneration: true })];
|
|
520
|
+
return [...researchFence(researchIsolation), ...fencedTail({ web: true, imageGeneration: true, subagents, codexVersion })];
|
|
492
521
|
},
|
|
493
522
|
};
|
|
494
523
|
/** The fenced profile names — for the test that holds both tables to one set. */
|
|
@@ -29,12 +29,13 @@ const oneLine = (s, n = 140) =>
|
|
|
29
29
|
* terminal console and a one-line pulse that is overwritten every two seconds,
|
|
30
30
|
* and neither can show a paragraph. The turn TRACE is a third consumer with the
|
|
31
31
|
* opposite need: it is scrollback, and clipping a sentence at 140 characters
|
|
32
|
-
* there is the product summarizing its own agent.
|
|
32
|
+
* there is the product summarizing its own agent. Errors also use `full` in
|
|
33
|
+
* the native daemon console (2026-10-19), preserving their words and lines.
|
|
33
34
|
*
|
|
34
35
|
* So a prose activity may ALSO carry `full`: the text EXACTLY as the CLI
|
|
35
|
-
* emitted it, uncollapsed and unclipped. Nothing derives it
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* emitted it, uncollapsed and unclipped. Nothing derives it. The trace and
|
|
37
|
+
* the console's errors read it; routine console activity and the pulse still
|
|
38
|
+
* read `label`. Absent means there was no fuller text than the
|
|
38
39
|
* label (a tool line, a bare thinking marker), and the trace falls back to the
|
|
39
40
|
* label — which is what every pre-0.87.0 daemon does for everything.
|
|
40
41
|
*
|
|
@@ -367,7 +368,14 @@ function codexEventOf(ev, cwd) {
|
|
|
367
368
|
exitCode: item.exit_code, isError: item.status === 'failed',
|
|
368
369
|
output: item.aggregated_output ?? '',
|
|
369
370
|
}] : null;
|
|
370
|
-
|
|
371
|
+
const mcp = (item.item_type ?? item.type) === 'mcp_tool_call';
|
|
372
|
+
const toolEvents = mcp ? [
|
|
373
|
+
...(item.arguments !== undefined ? [[`${item.server ?? 'mcp'}.${item.tool ?? ''}`, item.arguments, { id: item.id, runtime: 'codex' }]] : []),
|
|
374
|
+
[null, {}, { id: item.id, result: true, runtime: 'codex', isError: item.status === 'failed',
|
|
375
|
+
output: item.error?.message ?? (item.result === undefined ? '' : JSON.stringify(item.result)),
|
|
376
|
+
detail: item.error?.message ?? item.result ?? '' }],
|
|
377
|
+
] : [];
|
|
378
|
+
return { activity, text, ...(toolEvent ? { toolEvent } : {}), ...(toolEvents.length ? { toolEvents } : {}), ...(isMessage ? { answer: String(item.text ?? item.message ?? '') } : {}) };
|
|
371
379
|
}
|
|
372
380
|
// THE TURN'S OWN TOKEN COUNT (2026-09-24). `codex exec --json` closes a
|
|
373
381
|
// turn with `turn.completed` carrying `usage` — the codex twin of Claude's
|
|
@@ -500,18 +508,19 @@ export function parseAgyLine(line, cwd) {
|
|
|
500
508
|
const ti = su.tool_info;
|
|
501
509
|
if (!ti) return null;
|
|
502
510
|
const err = ti.error?.message;
|
|
511
|
+
const toolEvent = ti.name && ti.parameters !== undefined ? [ti.name, ti.parameters, { runtime: 'antigravity', ...(ti.id ? { id: ti.id } : {}) }] : null;
|
|
503
512
|
// Same rule as codex's error lines: the label is the console's, `full` is
|
|
504
513
|
// the trace's, and a tool error's message is exactly the kind of sentence a
|
|
505
514
|
// 140-char clamp cuts the cause out of.
|
|
506
515
|
if (err)
|
|
507
516
|
return {
|
|
508
517
|
activity: { kind: 'error', label: oneLine(err), full: String(err) },
|
|
509
|
-
text: '',
|
|
518
|
+
...(toolEvent ? { toolEvent } : {}), text: '',
|
|
510
519
|
};
|
|
511
520
|
// Each tool is reported twice — once ACTIVE, once DONE — so only the
|
|
512
521
|
// terminal state emits, otherwise every action appears in the thread twice.
|
|
513
522
|
if (su.state && su.state !== 'DONE') return null;
|
|
514
|
-
return { activity: humanizeAgyTool(ti.name, ti.parameters ?? {}, cwd), text: '' };
|
|
523
|
+
return { activity: humanizeAgyTool(ti.name, ti.parameters ?? {}, cwd), ...(toolEvent ? { toolEvent } : {}), text: '' };
|
|
515
524
|
}
|
|
516
525
|
if (ev.event === 'result') {
|
|
517
526
|
const r = ev.result ?? {};
|
|
@@ -521,6 +530,7 @@ export function parseAgyLine(line, cwd) {
|
|
|
521
530
|
activity: r.error
|
|
522
531
|
? { kind: 'error', label: oneLine(r.error), full: String(r.error) }
|
|
523
532
|
: null,
|
|
533
|
+
...(typeof r.response === 'string' && r.response ? { detail: { source: 'answer', text: r.response } } : {}),
|
|
524
534
|
text: `${r.response ?? ''}${r.error ? `\n${r.error}` : ''}\n`,
|
|
525
535
|
};
|
|
526
536
|
}
|