flowviant 0.125.6 → 0.125.9
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 +84 -6
- 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 +44 -0
- package/bin/lib/agentPlan.mjs +6 -2
- package/bin/lib/claudeInput.mjs +61 -1
- package/bin/lib/claudePosture.mjs +21 -0
- package/bin/lib/claudeStream.mjs +54 -4
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexResearch.mjs +33 -5
- package/bin/lib/commandSpecs.mjs +8 -5
- package/bin/lib/config.mjs +22 -1
- 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/mcpInventory.mjs +2 -1
- package/bin/lib/mcpReasons.mjs +6 -0
- package/bin/lib/nativeAgentTools.mjs +20 -3
- package/bin/lib/observedLines.mjs +63 -0
- package/bin/lib/orchestrator.mjs +49 -7
- package/bin/lib/orchestratorNative.mjs +133 -3
- package/bin/lib/orchestratorPrompt.mjs +50 -4
- package/bin/lib/postureTools.mjs +14 -6
- package/bin/lib/printable.mjs +1 -1
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectTools.mjs +3 -2
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- 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/resumeLost.mjs +21 -1
- package/bin/lib/runTurn.mjs +98 -44
- package/bin/lib/runtimeCapabilities.mjs +7 -2
- package/bin/lib/runtimeClaude.mjs +30 -8
- package/bin/lib/runtimeCodex.mjs +54 -22
- package/bin/lib/runtimeEvents.mjs +17 -7
- package/bin/lib/runtimeOfferings.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -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/update.mjs +18 -4
- 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 +5 -5
- 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 +62 -8
- package/bin/lib/workAgentTurnReports.mjs +33 -3
- package/bin/lib/workAgentTurnShape.mjs +6 -4
- package/bin/lib/workAgentTurns.mjs +12 -8
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workIntake.mjs +8 -2
- package/bin/lib/workMergeBatch.mjs +7 -1
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlanRecovery.mjs +50 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionTurns.mjs +64 -22
- package/bin/lib/worktreeDiff.mjs +29 -5
- 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,23 +66,29 @@ 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
|
|
68
73
|
// text is folded into the returned string for sentinel detection, and each
|
|
69
74
|
// activity is handed to `onActivity` so the caller can forward progress.
|
|
75
|
+
/** Linux's per-argument ceiling (`MAX_ARG_STRLEN`, 32 pages): one argv
|
|
76
|
+
* string past it is refused at launch with E2BIG. */
|
|
77
|
+
const MAX_ARG_BYTES = 128 * 1024;
|
|
78
|
+
|
|
70
79
|
export function runTurn(opts) {
|
|
71
80
|
// A Codex write-up runs over an isolated copy of its worktree
|
|
72
81
|
// (codexResearch.mjs). After the teardown it copies nothing either: the
|
|
73
82
|
// stand-down gate below is asked before the copy, not only before the spawn.
|
|
74
83
|
if (opts.runtime === 'codex' && opts.profile === 'research')
|
|
75
|
-
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));
|
|
76
87
|
return runPlainTurn(opts);
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
function runPlainTurn(opts) {
|
|
80
|
-
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;
|
|
81
92
|
/**
|
|
82
93
|
* AFTER THE TEARDOWN, NO TURN STARTS AND NO TURN ENDS (standDownGate.mjs).
|
|
83
94
|
* A stand-down that drains a deploy keeps this process alive; without this
|
|
@@ -86,11 +97,19 @@ function runPlainTurn(opts) {
|
|
|
86
97
|
* Both answers stay pending instead, as `process.exit` left them.
|
|
87
98
|
*/
|
|
88
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
|
+
};
|
|
89
106
|
return new Promise((resolveTurn, rejectTurn) => {
|
|
90
107
|
const resolve = (v) => {
|
|
108
|
+
output.close();
|
|
91
109
|
if (!workLanesStopped()) resolveTurn(v);
|
|
92
110
|
};
|
|
93
111
|
const reject = (error) => {
|
|
112
|
+
output.close();
|
|
94
113
|
if (!workLanesStopped()) rejectTurn(error);
|
|
95
114
|
};
|
|
96
115
|
const rt = runtimeById(runtime);
|
|
@@ -101,7 +120,7 @@ function runPlainTurn(opts) {
|
|
|
101
120
|
// lane's `unsupported` check). Here the daemon log names the id and the
|
|
102
121
|
// turn answers nothing — never Claude wearing the other runtime's name.
|
|
103
122
|
if (!rt) {
|
|
104
|
-
|
|
123
|
+
output.error(`error: cannot run '${runtime}' — this daemon does not know that runtime; update flowviant`);
|
|
105
124
|
resolve('');
|
|
106
125
|
return;
|
|
107
126
|
}
|
|
@@ -112,7 +131,7 @@ function runPlainTurn(opts) {
|
|
|
112
131
|
*/
|
|
113
132
|
const turnProfile = resolveTurnProfile(opts);
|
|
114
133
|
if (turnProfile.error) {
|
|
115
|
-
|
|
134
|
+
output.error(`error: ${turnProfile.error}`);
|
|
116
135
|
resolve('');
|
|
117
136
|
return;
|
|
118
137
|
}
|
|
@@ -127,7 +146,7 @@ function runPlainTurn(opts) {
|
|
|
127
146
|
// adapter learns to express one this opens by itself.
|
|
128
147
|
const offRuntime = turnProfile.onlyOn?.(rt);
|
|
129
148
|
if (offRuntime) {
|
|
130
|
-
|
|
149
|
+
output.error(`error: ${offRuntime}`);
|
|
131
150
|
resolve('');
|
|
132
151
|
return;
|
|
133
152
|
}
|
|
@@ -136,7 +155,7 @@ function runPlainTurn(opts) {
|
|
|
136
155
|
// drive. Fail as a turn with no sentinel — the loop already treats that as
|
|
137
156
|
// "the protocol did not complete" and retries, rather than inventing a
|
|
138
157
|
// completion for work that never started.
|
|
139
|
-
|
|
158
|
+
output.error(`error: cannot run '${rt.label}' — ${rt.blocked}`);
|
|
140
159
|
resolve('');
|
|
141
160
|
return;
|
|
142
161
|
}
|
|
@@ -181,6 +200,7 @@ function runPlainTurn(opts) {
|
|
|
181
200
|
// The lane's own measurement when it decided to orchestrate, so the
|
|
182
201
|
// hooks and the controller cannot disagree (workAgentTurnExecution.mjs).
|
|
183
202
|
codexVersion: opts.codexVersion ?? measuredRuntimeVersion('codex')?.version,
|
|
203
|
+
claudeVersion: opts.claudeVersion ?? measuredRuntimeVersion('claude')?.version ?? null,
|
|
184
204
|
|
|
185
205
|
streamInput: stdinText !== null || (rt.liveInput === true && Boolean(opts.liveMessages)),
|
|
186
206
|
writableDirs: opts.writableDirs,
|
|
@@ -346,34 +366,33 @@ function runPlainTurn(opts) {
|
|
|
346
366
|
// own browser home.
|
|
347
367
|
releaseArgs();
|
|
348
368
|
browser?.cleanup();
|
|
369
|
+
// THE COMMAND LINE'S OWN LIMIT, IN WORDS (2026-10-19). A runtime whose
|
|
370
|
+
// prompt still rides argv (Antigravity's `-p`) hits the OS's per-argument
|
|
371
|
+
// ceiling on a long message, and the turn failed as "spawn E2BIG". The
|
|
372
|
+
// refusal is the system's and stays; only its words are new.
|
|
373
|
+
// Said only when the message IS the oversized argument: E2BIG also
|
|
374
|
+
// answers an environment too large for the launch, which is not the
|
|
375
|
+
// person's message and keeps the system's own words.
|
|
376
|
+
const carrying = error?.code === 'E2BIG' && typeof prompt === 'string' && prompt
|
|
377
|
+
? args.find((a) => typeof a === 'string' && a.includes(prompt))
|
|
378
|
+
: undefined;
|
|
379
|
+
const bytes = carrying ? Buffer.byteLength(carrying) : 0;
|
|
380
|
+
if (bytes > MAX_ARG_BYTES) {
|
|
381
|
+
throw Object.assign(new Error(`${rt.label} takes its message on the command line, and this one is too long for it (${Math.ceil(bytes / 1024)} KB; the system allows ${MAX_ARG_BYTES / 1024} KB per argument, E2BIG). Send it in shorter parts, or attach the long text as a file.`), { code: 'E2BIG' });
|
|
382
|
+
}
|
|
349
383
|
throw error;
|
|
350
384
|
}
|
|
351
385
|
if (browser) {
|
|
352
386
|
child.once('close', browser.cleanup);
|
|
353
387
|
child.once('error', browser.cleanup);
|
|
354
388
|
}
|
|
355
|
-
if (child.pid) onSpawn?.(child);
|
|
389
|
+
if (child.pid) { output.event(`${rt.label} started`, 'start'); onSpawn?.(child); }
|
|
356
390
|
// When this turn began, for the Codex rollout read: a `turn_context` older
|
|
357
391
|
// than this is a previous turn's (turnModel.mjs).
|
|
358
392
|
const spawnedAt = Date.now();
|
|
359
393
|
let out = '';
|
|
360
394
|
let stdinError = null;
|
|
361
|
-
const input = rt.liveInput === true && opts.liveMessages ? createClaudeInput(child, { onTaken: opts.liveMessages.onTaken }) : null;
|
|
362
|
-
const pfx = label ? `${label} ` : '';
|
|
363
|
-
/**
|
|
364
|
-
* THE ONE SINK FOR WHAT A CLI OR A MODEL WROTE (2026-10-10, the audit's
|
|
365
|
-
* F133): activity labels — a Bash command, a Codex `say`, a file path —
|
|
366
|
-
* and the CLI's raw stderr. A prompt-injected agent can put an OSC 52 in
|
|
367
|
-
* a tool command as easily as in prose, and this wrote it to the
|
|
368
|
-
* operator's terminal as-is. Scrubbed HERE, before the daemon's own
|
|
369
|
-
* coloured `label` is added, so the prefix keeps its colour and nothing
|
|
370
|
-
* the model wrote keeps an escape sequence (printable.mjs, `printableText`).
|
|
371
|
-
*/
|
|
372
|
-
const emit = (s) => {
|
|
373
|
-
const text = printableText(String(s));
|
|
374
|
-
process.stdout.write(pfx ? text.replace(/\n/g, `\n${pfx}`) : text);
|
|
375
|
-
};
|
|
376
|
-
|
|
395
|
+
const input = rt.liveInput === true && opts.liveMessages ? createClaudeInput(child, { onTaken: opts.liveMessages.onTaken, onContext: opts.onContext ?? null }) : null;
|
|
377
396
|
// EVERY TURN IS LINE-PARSED (2026-10-10, the audit). Claude's adapter
|
|
378
397
|
// always asks for `--output-format stream-json` and every other runtime
|
|
379
398
|
// brings its own `parse` — for Codex the JSONL stream is the only output
|
|
@@ -382,7 +401,6 @@ function runPlainTurn(opts) {
|
|
|
382
401
|
// already drifted once (it called process.exit(1)); both are gone. A line
|
|
383
402
|
// that is not JSON is still kept as text (claudeStream.mjs), and stderr
|
|
384
403
|
// still lands in the haystack the sentinels read.
|
|
385
|
-
let buf = '';
|
|
386
404
|
const appendText = (t) => {
|
|
387
405
|
out += t;
|
|
388
406
|
};
|
|
@@ -400,13 +418,15 @@ function runPlainTurn(opts) {
|
|
|
400
418
|
* waits on a file read. See runtimeLimits.mjs.
|
|
401
419
|
*/
|
|
402
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');
|
|
403
423
|
recordClaudeRateLimit(info);
|
|
404
424
|
onRateLimit?.(info);
|
|
405
425
|
};
|
|
406
426
|
// The native children this CLI starts, observed (turnSubagents.mjs).
|
|
407
427
|
const natives = createTurnSubagents({ runtime: rt.id, cwd, since: spawnedAt, env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
408
428
|
owned: opts.orchestration === true, onSubagent: opts.onSubagent, onSpawnResult: opts.onSpawnResult, resumeThreadId,
|
|
409
|
-
sinks: { emit, onActivity, onToolEvent, appendText } });
|
|
429
|
+
sinks: { emit, onActivity, onToolEvent, appendText, onDetail: output.detail } });
|
|
410
430
|
let codexThread = rt.id === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
|
|
411
431
|
/**
|
|
412
432
|
* THE MODEL THIS SPAWN RAN ON, ONCE (2026-09-29, turnModel.mjs). Claude
|
|
@@ -436,12 +456,17 @@ function runPlainTurn(opts) {
|
|
|
436
456
|
if (harvest.mcp) recordMcpServers(i?.mcpServers, i?.tools);
|
|
437
457
|
onInit?.(i);
|
|
438
458
|
};
|
|
459
|
+
/** This turn's Claude task list, as the plan card reads it (claudeStream.mjs). */
|
|
460
|
+
const taskPlan = new Map();
|
|
439
461
|
/** One line of the child's stdout, in whichever dialect it speaks. */
|
|
440
462
|
let cliFailed = false;
|
|
441
|
-
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);
|
|
442
466
|
try {
|
|
443
467
|
const event = JSON.parse(line);
|
|
444
468
|
const parentEvent = event.parent_tool_use_id == null && !event.isSidechain;
|
|
469
|
+
opts.researchReceipt?.event(event);
|
|
445
470
|
if (input && parentEvent) {
|
|
446
471
|
input.event(event);
|
|
447
472
|
if (event.type === 'system' && event.subtype === 'init' && event.session_id) opts.liveMessages.onInit(event.session_id);
|
|
@@ -457,7 +482,7 @@ function runPlainTurn(opts) {
|
|
|
457
482
|
}
|
|
458
483
|
} catch { /* ordinary non-JSON CLI output */ }
|
|
459
484
|
if (!rt.parse)
|
|
460
|
-
return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit: onInitAll, onUsage, onRateLimit: onRate, onModel: onRanOn, subagents: natives.collector });
|
|
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 }) });
|
|
461
486
|
const ev = rt.parse(line, cwd);
|
|
462
487
|
if (!ev) return;
|
|
463
488
|
// The conversation id, when the runtime announces one (codex's
|
|
@@ -474,14 +499,17 @@ function runPlainTurn(opts) {
|
|
|
474
499
|
// A receipt the controller cannot take never ends this line's handling,
|
|
475
500
|
// as when the lane parsed the line again itself (2026-10-18).
|
|
476
501
|
if (ev.spawnResult) try { opts.onSpawnResult?.(ev.spawnResult); } catch { /* the controller's own failure */ }
|
|
477
|
-
if (ev.toolEvent)
|
|
502
|
+
if (ev.toolEvent) toolObserved(...ev.toolEvent);
|
|
503
|
+
for (const tool of ev.toolEvents ?? []) toolObserved(...tool);
|
|
478
504
|
if (ev.usage) onUsage?.(ev.usage);
|
|
479
505
|
// One agent MESSAGE, alone — see parseCodexLine's `answer`. Last call
|
|
480
506
|
// wins at the caller, which is what "the final answer" means.
|
|
481
507
|
if (typeof ev.answer === 'string') onAnswer?.(ev.answer);
|
|
508
|
+
if (ev.detail) output.detail(ev.detail.text, { source: ev.detail.source, observedAt });
|
|
482
509
|
if (ev.text) appendText(ev.text);
|
|
483
510
|
if (ev.activity) {
|
|
484
|
-
|
|
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 });
|
|
485
513
|
onActivity?.(ev.activity);
|
|
486
514
|
}
|
|
487
515
|
};
|
|
@@ -492,19 +520,18 @@ function runPlainTurn(opts) {
|
|
|
492
520
|
// `setEncoding` holds the partial bytes over to the next chunk.
|
|
493
521
|
child.stdout.setEncoding('utf8');
|
|
494
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' }));
|
|
495
526
|
child.stdout.on('data', (d) => {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
499
|
-
const line = buf.slice(0, nl);
|
|
500
|
-
buf = buf.slice(nl + 1);
|
|
501
|
-
if (line.trim()) onLine(line);
|
|
502
|
-
}
|
|
527
|
+
if (ended) return;
|
|
528
|
+
stdoutLines.push(d);
|
|
503
529
|
});
|
|
504
530
|
// stderr is not JSON (warnings/errors) — pass through and keep for sentinels.
|
|
505
531
|
child.stderr.on('data', (s) => {
|
|
532
|
+
if (ended) return;
|
|
506
533
|
out += s;
|
|
507
|
-
|
|
534
|
+
stderrLines.push(s);
|
|
508
535
|
});
|
|
509
536
|
/**
|
|
510
537
|
* A SPAWN THAT FAILS FAILS THE TURN, NOT THE DAEMON — the one copy of the
|
|
@@ -520,32 +547,42 @@ function runPlainTurn(opts) {
|
|
|
520
547
|
* this turn with the native reason; the lane records a failed report.
|
|
521
548
|
*/
|
|
522
549
|
const onSpawnError = (e) => {
|
|
550
|
+
opts.researchReceipt?.fail();
|
|
523
551
|
input?.close();
|
|
552
|
+
stdoutLines.end();
|
|
553
|
+
stderrLines.end();
|
|
524
554
|
natives.abandon();
|
|
525
555
|
if (opts.liveMessages) void opts.liveMessages.stop();
|
|
526
556
|
finishDiagnostics({ pid: child.pid, spawnError: typeof e.code === 'string' ? e.code : 'SPAWN_ERROR' });
|
|
527
557
|
releaseArgs();
|
|
528
558
|
if (e.code === 'ENOENT') {
|
|
529
|
-
|
|
559
|
+
output.error(`error: '${rt.bin}' CLI not found on PATH. Install ${rt.label} first: ${rt.install}`);
|
|
530
560
|
resolve('');
|
|
531
561
|
return;
|
|
532
562
|
}
|
|
533
|
-
|
|
563
|
+
output.error(e);
|
|
534
564
|
reject(e);
|
|
535
565
|
};
|
|
536
566
|
child.on('error', onSpawnError);
|
|
537
|
-
|
|
567
|
+
const onClose = (code, signal) => {
|
|
568
|
+
if (ended) return;
|
|
569
|
+
ended = true;
|
|
538
570
|
input?.close();
|
|
539
571
|
releaseArgs();
|
|
540
572
|
opts.onExit?.(code, signal);
|
|
541
|
-
|
|
573
|
+
stdoutLines.end();
|
|
574
|
+
stderrLines.end();
|
|
575
|
+
if (code !== 0 || signal) opts.researchReceipt?.fail();
|
|
542
576
|
finishDiagnostics({ code, signal, pid: child.pid, output: out, cliFailed, inputError: stdinError?.code });
|
|
543
577
|
const handBack = async () => {
|
|
544
578
|
if (opts.liveMessages) await opts.liveMessages.stop();
|
|
545
579
|
await natives.drain(CODEX_MODEL_READ_MS);
|
|
546
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');
|
|
547
583
|
if (stdinError) {
|
|
548
|
-
|
|
584
|
+
output.error(stdinError);
|
|
585
|
+
reject(stdinError);
|
|
549
586
|
return;
|
|
550
587
|
}
|
|
551
588
|
resolve(out);
|
|
@@ -577,12 +614,29 @@ function runPlainTurn(opts) {
|
|
|
577
614
|
return;
|
|
578
615
|
}
|
|
579
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));
|
|
580
633
|
});
|
|
581
634
|
if (transport.text !== null) {
|
|
582
635
|
// end() buffers with backpressure and closes at EOF; Codex reads the
|
|
583
636
|
// entire prompt before starting. An early close must not accept output
|
|
584
637
|
// from a turn whose complete authorized input never reached the CLI.
|
|
585
638
|
child.stdin.on('error', (error) => {
|
|
639
|
+
opts.researchReceipt?.fail('input_error');
|
|
586
640
|
stdinError = Object.assign(new Error(`${rt.label} could not receive the complete turn input (${error.code || 'PIPE_ERROR'}).`), { code: error.code || 'PIPE_ERROR' });
|
|
587
641
|
opts.onCliFailure?.();
|
|
588
642
|
child.kill('SIGTERM');
|
|
@@ -168,9 +168,14 @@ export function claudeMcpInventoryOf(list, tools) {
|
|
|
168
168
|
for (const e of list) {
|
|
169
169
|
if (!e || typeof e !== 'object' || typeof e.name !== 'string') continue;
|
|
170
170
|
const name = e.name.trim().slice(0, MCP_NAME_MAX).trim();
|
|
171
|
-
|
|
171
|
+
const scope = typeof e.scope === 'string' ? e.scope : typeof e.source === 'string' ? e.source : null;
|
|
172
|
+
// The daemon's own `flowviant` rides `--mcp-config`, reported `dynamic`.
|
|
173
|
+
// One in the person's own config is theirs (2026-10-17, the owner) and is
|
|
174
|
+
// listed, so a revoked token there reads failed (2026-10-19). An
|
|
175
|
+
// unscoped report cannot say which, and stays hidden.
|
|
176
|
+
if (!name || name === 'flowviant' && (scope === 'dynamic' || scope == null) || seen.has(name)) continue;
|
|
172
177
|
const status = MCP_STATUSES.has(e.status) ? e.status : 'other';
|
|
173
|
-
seen.set(name, { name, status, scope
|
|
178
|
+
seen.set(name, { name, status, scope, prefix: mcpToolPrefix(e.name.trim()), tools: status === 'connected' ? [] : null });
|
|
174
179
|
}
|
|
175
180
|
// The longest prefix wins, so `a_b` never claims `a`'s tools or the reverse.
|
|
176
181
|
const byPrefix = [...seen.values()].sort((a, b) => b.prefix.length - a.prefix.length);
|
|
@@ -16,9 +16,19 @@ 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';
|
|
23
|
+
import { versionAtLeast } from './versionOrder.mjs';
|
|
24
|
+
|
|
25
|
+
/** The release the agent lane first measured `--system-prompt-snapshot` on
|
|
26
|
+
* (decisions.md, 2026-09-24). */
|
|
27
|
+
export const SYSTEM_PROMPT_SNAPSHOT_MIN = '2.1.281';
|
|
28
|
+
/** `--version` prints "2.1.303 (Claude Code)"; the first word is the version. */
|
|
29
|
+
export function claudeSnapshotFlagKnown(version) {
|
|
30
|
+
return versionAtLeast(String(version ?? '').trim().split(/\s+/)[0], SYSTEM_PROMPT_SNAPSHOT_MIN);
|
|
31
|
+
}
|
|
22
32
|
|
|
23
33
|
function claudeMcp(token, mcpUrl) {
|
|
24
34
|
const dir = mkdtempSync(join(tmpdir(), 'flowviant-mcp-'));
|
|
@@ -113,7 +123,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
113
123
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
114
124
|
* above the conversation rather than inside it.
|
|
115
125
|
*/
|
|
116
|
-
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 }) {
|
|
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 }) {
|
|
117
127
|
// THE CARTOGRAPHER'S FENCE NAMES ITS VAULT (2026-10-10): its one write rule
|
|
118
128
|
// is the vault's absolute path (claudePosture.mjs `wikiPermFor`), so a turn
|
|
119
129
|
// with no vault, or one no rule can name, has no list — refused here in
|
|
@@ -122,7 +132,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
122
132
|
if (profile === 'wiki' && !vaultDir) {
|
|
123
133
|
throw new Error("a wiki turn needs its vault to fence — none reached claude's builder");
|
|
124
134
|
}
|
|
125
|
-
if (profile === 'wiki' && !Array.isArray(
|
|
135
|
+
if (profile === 'wiki' && !Array.isArray(postureList)) {
|
|
126
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`);
|
|
127
137
|
}
|
|
128
138
|
const a = [];
|
|
@@ -181,6 +191,10 @@ export const CLAUDE_RUNTIME = {
|
|
|
181
191
|
* agent renders the CURRENT base snapshot, not the first turn's cache.
|
|
182
192
|
*/
|
|
183
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;
|
|
184
198
|
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
185
199
|
// A FENCED POSTURE NAMES ITS OWN MODE (2026-10-10). Its lists fence only
|
|
186
200
|
// under the CLI's `default` mode, where a tool the list does not name is
|
|
@@ -191,10 +205,18 @@ export const CLAUDE_RUNTIME = {
|
|
|
191
205
|
// states `default` itself, unless its own list names a mode (plan-mode).
|
|
192
206
|
if (fenced && !perm.includes('--permission-mode')) a.push('--permission-mode', 'default');
|
|
193
207
|
else if (agentTools) a.push('--setting-sources', 'user');
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
// EVERY TURN RENDERS ITS OWN SYSTEM PROMPT (2026-10-19). By default the
|
|
209
|
+
// CLI records a conversation's first system prompt and resends that record
|
|
210
|
+
// on every resume "even when a later launch passes different text" —
|
|
211
|
+
// measured offline on 2.1.303: a resumed turn appended BETA and the model
|
|
212
|
+
// was sent the first turn's ALPHA. A tab's appended prompt moves between
|
|
213
|
+
// turns (plan mode on and off, the knowledge and artifact paragraphs), so
|
|
214
|
+
// a tab switched out of plan mode kept being told to change nothing. An
|
|
215
|
+
// agent turn always passed this (its current base instructions). A CLI too
|
|
216
|
+
// old to name the flag refuses it, so an unmeasured version keeps the old
|
|
217
|
+
// argv.
|
|
218
|
+
if (agentTools || claudeSnapshotFlagKnown(claudeVersion)) a.push('--system-prompt-snapshot', 'off');
|
|
219
|
+
if (agentTools?.pluginDir) a.push('--plugin-dir', agentTools.pluginDir);
|
|
198
220
|
// WHICH MCP SERVERS A TURN MOUNTS. An agent's code, write-up and
|
|
199
221
|
// mockup/3D-model/deck turns (2026-10-11, the owner) mount this machine's
|
|
200
222
|
// own — its user and local servers and the account's claude.ai connectors —
|
|
@@ -257,7 +279,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
257
279
|
const mounted = machine ? withMachineMcp(perm, machineMcp) : perm;
|
|
258
280
|
// The orchestrator's hooks join that same `--settings`: the CLI applies
|
|
259
281
|
// only the last one it is handed (claudeSettingsArg.mjs `withSettings`).
|
|
260
|
-
const postured =
|
|
282
|
+
const postured = gated ? withSettings(mounted, { hooks: orchestratorHooks(), disableAllHooks: false }) : mounted;
|
|
261
283
|
let tail;
|
|
262
284
|
try {
|
|
263
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,14 +190,17 @@ 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' }) {
|
|
201
|
+
// A resumed tab whose thread already holds the contract sends none
|
|
202
|
+
// (codexContract.mjs): the message alone.
|
|
203
|
+
if (!system) return prompt;
|
|
176
204
|
const readers = profile === 'consult' || profile === 'plan'
|
|
177
205
|
? '\nUse exec_command with shell="/bin/bash" and login=false for project discovery. Project files, local Git history and the project knowledge directory are readable; host files are outside this turn’s read boundary.'
|
|
178
206
|
: '';
|
|
@@ -284,8 +312,11 @@ export const CODEX_RUNTIME = {
|
|
|
284
312
|
// THE FENCE, picked by the same keys as `codexIgnoresUserConfig`: a fenced
|
|
285
313
|
// posture builds its own (CODEX_FENCES below); anything else is a build.
|
|
286
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);
|
|
287
318
|
if (fenced) {
|
|
288
|
-
a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation, knowledgeDir, projectReads }));
|
|
319
|
+
a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation, knowledgeDir, projectReads, subagents: gated, codexVersion }));
|
|
289
320
|
} else {
|
|
290
321
|
// The daemon's build posture, mapped: SAFE keeps writes inside the
|
|
291
322
|
// workspace, the default lets the agent run its own tests and git
|
|
@@ -300,9 +331,7 @@ export const CODEX_RUNTIME = {
|
|
|
300
331
|
if (writableDirs.length) a.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(writableDirs)}`);
|
|
301
332
|
a.push('-c', `sandbox_mode="${SAFE ? 'workspace-write' : 'danger-full-access'}"`);
|
|
302
333
|
if (cardTurn) a.push('-c', 'approval_policy="never"', ...(agentTools?.codexApprovals ?? []));
|
|
303
|
-
a.push(...(cardTurn && profile === 'build' && codexSubagentsSafe(codexVersion)
|
|
304
|
-
? ['-c', 'features.multi_agent=true', '-c', 'features.goals=false']
|
|
305
|
-
: NO_SUBAGENTS));
|
|
334
|
+
a.push(...(cardTurn && profile === 'build' && codexSubagentsSafe(codexVersion) ? CARD_SUBAGENTS : noSubagents(codexVersion)));
|
|
306
335
|
}
|
|
307
336
|
|
|
308
337
|
/**
|
|
@@ -318,7 +347,7 @@ export const CODEX_RUNTIME = {
|
|
|
318
347
|
if (a.includes('--ignore-user-config')) a.push(...(personal?.args ?? []));
|
|
319
348
|
|
|
320
349
|
a.push(...mcp, ...resultSchemaArgs);
|
|
321
|
-
if (
|
|
350
|
+
if (gated) a.push(...codexOrchestratorArgs());
|
|
322
351
|
a.push(streamInput ? '-' : codexPrompt({ system, prompt, profile }));
|
|
323
352
|
return a;
|
|
324
353
|
},
|
|
@@ -398,7 +427,7 @@ export const CODEX_RUNTIME = {
|
|
|
398
427
|
* Claude's research list was fenced to prevent. Codex research (0.117.0) runs
|
|
399
428
|
* under its own fence instead, over a copied view (CODEX_FENCES `research`).
|
|
400
429
|
*/
|
|
401
|
-
export function codexArtifactFence(cwd, { name, imageGeneration = false, what }) {
|
|
430
|
+
export function codexArtifactFence(cwd, { name, imageGeneration = false, what, codexVersion }) {
|
|
402
431
|
if (!cwd || !isAbsolute(cwd)) {
|
|
403
432
|
throw new Error(`${what} needs its worktree to fence — none reached codex's builder`);
|
|
404
433
|
}
|
|
@@ -420,14 +449,14 @@ export function codexArtifactFence(cwd, { name, imageGeneration = false, what })
|
|
|
420
449
|
'-c', `default_permissions="${name}"`,
|
|
421
450
|
// Nobody is on this end of the pipe to approve an escalation.
|
|
422
451
|
'-c', 'approval_policy="never"',
|
|
423
|
-
...fencedTail({ imageGeneration }),
|
|
452
|
+
...fencedTail({ imageGeneration, codexVersion }),
|
|
424
453
|
];
|
|
425
454
|
}
|
|
426
455
|
|
|
427
456
|
/** The design card's fence — `codexArtifactFence` under its own profile name,
|
|
428
457
|
* with native image generation available for supporting assets. */
|
|
429
|
-
export const codexDesignFence = (cwd) =>
|
|
430
|
-
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 });
|
|
431
460
|
|
|
432
461
|
/**
|
|
433
462
|
* EVERY FENCED POSTURE'S ARGV, by the keys of CODEX_FENCED (a test holds the
|
|
@@ -437,7 +466,7 @@ const CODEX_FENCES = {
|
|
|
437
466
|
// A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — a different
|
|
438
467
|
// shape from Claude's verb allowlist, which is the whole reason `profile` is
|
|
439
468
|
// a promise rather than a flag list. See codexProjectRead.mjs.
|
|
440
|
-
consult: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail()],
|
|
469
|
+
consult: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail({ codexVersion: opts.codexVersion })],
|
|
441
470
|
// A PLANNING SESSION. Read-only on the filesystem, exactly like a consult —
|
|
442
471
|
// the writes it makes go through the control plane, not through this box —
|
|
443
472
|
// and for the same reason: its prompt is steered by anything a project
|
|
@@ -446,8 +475,8 @@ const CODEX_FENCES = {
|
|
|
446
475
|
// turn's scoped token. The plan token has the planning tools; a capture
|
|
447
476
|
// token has only staging and transcript tools. The server refuses
|
|
448
477
|
// everything outside that scope.
|
|
449
|
-
plan: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail()],
|
|
450
|
-
wiki: ({ vaultDir }) => codexWikiFence(vaultDir),
|
|
478
|
+
plan: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail({ codexVersion: opts.codexVersion })],
|
|
479
|
+
wiki: ({ vaultDir, codexVersion }) => codexWikiFence(vaultDir, codexVersion),
|
|
451
480
|
// AN IMAGE CARD (0.114.0): read the repo, write ONLY the worktree's
|
|
452
481
|
// `.flowviant/artifacts/`, generate pictures, and nothing else. MEASURED on
|
|
453
482
|
// codex-cli 0.156.1 (Linux, bubblewrap), with no model turn spent: under
|
|
@@ -474,18 +503,21 @@ const CODEX_FENCES = {
|
|
|
474
503
|
//
|
|
475
504
|
// Native images support every artifact kind; this profile still delivers
|
|
476
505
|
// PNG/WebP, while design/model/deck still deliver their self-contained page.
|
|
477
|
-
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 }),
|
|
478
507
|
// A MOCKUP, 3D-MODEL OR DECK CARD (0.115.0): the fence is
|
|
479
508
|
// `codexDesignFence`'s, argued and measured at `codexArtifactFence` above.
|
|
480
|
-
design: ({ cwd }) => codexDesignFence(cwd),
|
|
509
|
+
design: ({ cwd, codexVersion }) => codexDesignFence(cwd, codexVersion),
|
|
481
510
|
// A WRITE-UP CARD (0.117.0): live web over a COPIED view of the worktree,
|
|
482
511
|
// never the checkout or the login store, under a root-denied tool fence
|
|
483
512
|
// (codexResearch.mjs `researchFence`, built by runTurn's
|
|
484
513
|
// `runIsolatedResearch`). It is the one fence that keeps the web, so its
|
|
485
514
|
// tail leaves NO_WEB out and closes everything else.
|
|
486
|
-
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 }) => {
|
|
487
519
|
if (!researchIsolation) throw new Error('Codex research requires its isolated input copy.');
|
|
488
|
-
return [...researchFence(researchIsolation), ...fencedTail({ web: true, imageGeneration: true })];
|
|
520
|
+
return [...researchFence(researchIsolation), ...fencedTail({ web: true, imageGeneration: true, subagents, codexVersion })];
|
|
489
521
|
},
|
|
490
522
|
};
|
|
491
523
|
/** The fenced profile names — for the test that holds both tables to one set. */
|