flowviant 0.125.30 → 0.125.31
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/bin/lib/agentPlan.mjs +8 -0
- package/bin/lib/agentQueueContext.mjs +6 -0
- package/bin/lib/agentSplit.mjs +90 -0
- package/bin/lib/architectureContract.mjs +57 -0
- package/bin/lib/architectureReport.mjs +38 -0
- package/bin/lib/artifactContracts.mjs +21 -2
- package/bin/lib/claudeInput.mjs +1 -1
- package/bin/lib/claudeSubagents.mjs +46 -6
- package/bin/lib/claudeWorkflows.mjs +94 -0
- package/bin/lib/cliLimit.mjs +5 -0
- package/bin/lib/codexInput.mjs +191 -0
- package/bin/lib/codexSubagentEvents.mjs +3 -1
- package/bin/lib/codexSubagents.mjs +34 -9
- package/bin/lib/conversationOutput.mjs +14 -0
- package/bin/lib/crossCliChild.mjs +327 -0
- package/bin/lib/crossCliLaunch.mjs +92 -0
- package/bin/lib/crossCliPosture.mjs +55 -0
- package/bin/lib/crossCliResearch.mjs +19 -0
- package/bin/lib/fixtures/workflow-ultracode.jsonl +22 -0
- package/bin/lib/nativeAgentTools.mjs +12 -5
- package/bin/lib/orchestrator.mjs +24 -7
- package/bin/lib/orchestratorClient.mjs +8 -2
- package/bin/lib/orchestratorPrompt.mjs +13 -5
- package/bin/lib/orchestratorProtocol.mjs +18 -0
- package/bin/lib/outsideCli.mjs +124 -0
- package/bin/lib/prompts.mjs +7 -5
- package/bin/lib/runTurn.mjs +30 -8
- package/bin/lib/runtimeClaude.mjs +7 -3
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeLimits.mjs +59 -1
- package/bin/lib/splitGate.mjs +115 -0
- package/bin/lib/subagentBounds.mjs +26 -0
- package/bin/lib/subagentLedger.mjs +110 -0
- package/bin/lib/trace.mjs +134 -6
- package/bin/lib/turnDeclaration.mjs +4 -4
- package/bin/lib/turnGaps.mjs +6 -2
- package/bin/lib/turnSubagents.mjs +23 -9
- package/bin/lib/wikiDraft.mjs +9 -0
- package/bin/lib/workAgentResume.mjs +11 -2
- package/bin/lib/workAgentTurnExecution.mjs +31 -7
- package/bin/lib/workAgentTurnOutcome.mjs +11 -0
- package/bin/lib/workAgentTurnReports.mjs +5 -2
- package/package.json +1 -1
package/bin/lib/orchestrator.mjs
CHANGED
|
@@ -11,9 +11,11 @@ import { scrub, learnExtraSecrets, forgetExtraSecrets } from './uplinkScrub.mjs'
|
|
|
11
11
|
import { OrchestratorInputTooLarge, readOrchestratorInput } from './orchestratorInput.mjs';
|
|
12
12
|
import { SAFE_POINT_AFTER_EDIT as AFTER_EDIT } from './orchestratorProtocol.mjs';
|
|
13
13
|
import { MACHINE_MESSAGE_ROUTE_STATES, MESSAGE_ROUTE_FIELD_MAX, OPEN_MESSAGE_ROUTE_STATES, ORCHESTRATOR_SERVER } from './orchestratorProtocol.mjs';
|
|
14
|
-
import { captureToolName, isChildWriteTool, isClaudeStopTool, isDiscoveryTool, isFollowupTool, isPersonQuestionTool, isShellTool, isSpawnTool } from './nativeAgentTools.mjs';
|
|
14
|
+
import { captureToolName, crossCliToolName, isChildWriteTool, isClaudeStopTool, isDiscoveryTool, isFollowupTool, isPersonQuestionTool, isShellTool, isSpawnTool } from './nativeAgentTools.mjs';
|
|
15
15
|
import { childBrain, childChoice, followupTarget, readOnlyShell, readSpawn, spawnReceipt, withChildContract } from './orchestratorNative.mjs';
|
|
16
16
|
import { SETTLED_SUBAGENT_STATUSES, SUBAGENT_CAP, SUBAGENT_FIELD_CAPS } from './subagentBounds.mjs';
|
|
17
|
+
import { readOnlyLookup, splitHold } from './splitGate.mjs';
|
|
18
|
+
import { createCrossCliChildren, CROSS_CLI_ACTIONS, CROSS_CLI_RESULT_WAIT_MAX_S } from './crossCliChild.mjs';
|
|
17
19
|
|
|
18
20
|
const WAIT = 'Waiting for a free slot on the machine';
|
|
19
21
|
// A research card's children read inside its fence; the orchestrator alone writes and delivers.
|
|
@@ -35,7 +37,7 @@ const text = (s, n) => scrubNativeText(s, n, scrub);
|
|
|
35
37
|
* on Codex, only for a model in `catalog`, the measured spawnable models.
|
|
36
38
|
*/
|
|
37
39
|
export async function createOrchestrator({ turnId, runtime, trace, admit, request = fleetRequest, notify = () => false, taken = () => {},
|
|
38
|
-
interval = 500, hold = 60_000, clock = Date.now, scrubText = text, children = {}, readOnlyRun = null, signal, rootTools = null, assignmentContext = () => '' } = {}) {
|
|
40
|
+
interval = 500, hold = 60_000, clock = Date.now, scrubText = text, children = {}, readOnlyRun = null, signal, rootTools = null, assignmentContext = () => '', crossCli = null, listen = listenHeldTool } = {}) {
|
|
39
41
|
// Consults belong to a held attempt, never to a card turn. This capability
|
|
40
42
|
// is entirely local; it cannot report routes or borrow an agent principal.
|
|
41
43
|
if (readOnlyRun && (!['plan', 'capture'].includes(readOnlyRun.scope) || !filled(readOnlyRun.id) ||
|
|
@@ -73,11 +75,16 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
73
75
|
const emit = update => {
|
|
74
76
|
const id = resolve(update.id), prior = records.get(id) ?? { id };
|
|
75
77
|
const reading = { ...prior, ...update, id };
|
|
78
|
+
// A nested child's parent is named natively; the trace knows its record.
|
|
79
|
+
if (typeof update.parent === 'string' && update.parent !== 'lead') reading.parent = resolve(update.parent);
|
|
76
80
|
if (reading.namedKind) reading.kind = reading.namedKind;
|
|
77
81
|
if (reading.namedTitle) reading.title = reading.namedTitle;
|
|
78
82
|
records.set(id, reading); trace.agent(reading); return reading;
|
|
79
83
|
};
|
|
80
84
|
const release = id => { const a = records.get(resolve(id)); a?.release?.(); if (a) a.release = null; };
|
|
85
|
+
// Subagents on another CLI (crossCliChild.mjs): a code card's lead only.
|
|
86
|
+
const others = crossCli && !readOnlyRun ? createCrossCliChildren({ lead: runtime, ...crossCli, access, card, split: children.split ?? null,
|
|
87
|
+
emit, count: () => records.size, admit, announce: m => announce(m), interval }) : null;
|
|
81
88
|
// A wake the root CLI did not take (stdin closed; Codex always) waits for
|
|
82
89
|
// the root's next tool hook, where both CLIs read additionalContext.
|
|
83
90
|
const announce = message => {
|
|
@@ -152,6 +159,9 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
152
159
|
const spawn = readSpawn(event);
|
|
153
160
|
if (!spawn) return { deny: 'Give this native spawn a text prompt or message so the run can gate it.' };
|
|
154
161
|
const { callId, brief } = spawn;
|
|
162
|
+
// A child that may edit files waits for the person's yes (splitGate.mjs).
|
|
163
|
+
const held = splitHold(children.split ?? null, event, spawn, { runtime, access });
|
|
164
|
+
if (held) return { deny: held };
|
|
155
165
|
// Above the card's model or effort is refused before any record or slot.
|
|
156
166
|
const chosen = childBrain(event, spawn, card);
|
|
157
167
|
if (chosen.deny) return { deny: chosen.deny };
|
|
@@ -166,6 +176,8 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
166
176
|
if (!slot(id)) { a.denied = resumed ? 'resume' : 'spawn'; return { deny: `${WAIT}. Agent id: ${id}. Keep working yourself; you will be told when this spawn can be retried. Do not wait for a child that has not started.` }; }
|
|
167
177
|
// Claude's SendMessage addresses a child by its Agent `name`, Codex V2 by its task name.
|
|
168
178
|
for (const name of spawn.names) if (filled(name) && name.length <= 160) aliases.set(name, id);
|
|
179
|
+
// Admitted unasked as a read-only lookup: its file writes are refused below.
|
|
180
|
+
if (readOnlyLookup(children.split ?? null, event, spawn, access)) a.readOnly = true;
|
|
169
181
|
start(a, a.title);
|
|
170
182
|
// A cheaper model or lower effort than the card's is relayed on the child's
|
|
171
183
|
// stream, and the parent is asked to say why in its own update.
|
|
@@ -303,11 +315,13 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
303
315
|
if (!records.has(id)) return { deny: sealedRun ? `That names no agent in this run.${childContract()}` : 'Use the agent record id in your prompt.' };
|
|
304
316
|
bind(child, id);
|
|
305
317
|
}
|
|
318
|
+
if (child && crossCliToolName(ev.tool_name)) return { deny: 'Only the card’s lead starts or continues a subagent on another CLI. Return your work to it.' };
|
|
306
319
|
// A sealed child that missed (or never got) its start's words reads
|
|
307
320
|
// them here, whole: an earlier run's follow-up, or a CLI without the hook.
|
|
308
321
|
if (child && !records.has(resolve(child)) && !isDiscoveryTool(ev.tool_name))
|
|
309
322
|
return { deny: sealedRun ? `Register with your orchestrator before working.${childContract()}` : 'Before working, call card_orchestrator.register_agent with the agent record id in your prompt. Tool discovery is allowed.' };
|
|
310
323
|
if ((child && access === 'read' || readOnlyRun) && isChildWriteTool(ev.tool_name)) return { deny: readOnlyRun ? 'This attempt only reads: no file writes.' : READ_ONLY_REFUSAL };
|
|
324
|
+
if (child && records.get(resolve(child))?.readOnly && isChildWriteTool(ev.tool_name)) return { deny: 'You started as a read-only lookup: no file writes. Return your findings to the orchestrator.' };
|
|
311
325
|
if (rootTools && captureToolName(ev.tool_name)) {
|
|
312
326
|
if (child || !rootSession || ev.session_id !== rootSession) return { deny: 'Only the top-level capture coordinator may call capture tools. Return findings to it.' };
|
|
313
327
|
return rootTools.authorize(ev);
|
|
@@ -340,6 +354,7 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
340
354
|
return held.length ? { ...result, context: [result.context, ...held].filter(Boolean).join('\n\n') } : result;
|
|
341
355
|
}
|
|
342
356
|
if (action === 'route_message') return route(args);
|
|
357
|
+
if (CROSS_CLI_ACTIONS.includes(action)) { if (!others) throw new Error('This run cannot start a subagent on another CLI.'); return others.handle(action, args); }
|
|
343
358
|
const id = named(args.agentId);
|
|
344
359
|
// A person’s answer starts a new CLI turn. The parent carries the earlier
|
|
345
360
|
// question and native identity in its own conversation; relay that known
|
|
@@ -396,13 +411,13 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
396
411
|
};
|
|
397
412
|
let listener;
|
|
398
413
|
try {
|
|
399
|
-
listener = await
|
|
414
|
+
listener = await listen({ envPrefix: 'FLOWVIANT_ORCHESTRATOR', verb: '__orchestrator-mcp', requestTimeoutMs: others ? (CROSS_CLI_RESULT_WAIT_MAX_S + 30) * 1000 : 5_000,
|
|
400
415
|
capability, isClosed: ended, respond });
|
|
401
416
|
} catch (error) { forgetExtraSecrets(secretKey); throw error; }
|
|
402
417
|
const { server, env: heldEnv, args: argv } = listener;
|
|
403
418
|
// The hook reads it to refuse a research child's write while this is down.
|
|
404
419
|
const env = { ...heldEnv, FLOWVIANT_ORCHESTRATOR_CHILDREN: access,
|
|
405
|
-
...(readOnlyRun ? { FLOWVIANT_ORCHESTRATOR_READ_ONLY: '1' } : {}) };
|
|
420
|
+
...(readOnlyRun ? { FLOWVIANT_ORCHESTRATOR_READ_ONLY: '1' } : {}), ...(others?.offered().length ? { FLOWVIANT_ORCHESTRATOR_CROSS_CLI: others.offered().join(',') } : {}) };
|
|
406
421
|
const timer = setInterval(() => {
|
|
407
422
|
if (ended() || polling) return;
|
|
408
423
|
polling = true;
|
|
@@ -426,7 +441,9 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
426
441
|
} finally { polling = false; }
|
|
427
442
|
}, interval);
|
|
428
443
|
timer.unref?.();
|
|
429
|
-
|
|
444
|
+
// Codex's own MCP tool timeout sits above a result call's longest wait.
|
|
445
|
+
return { env, config: { command: process.execPath, args: argv, env, ...(others ? { toolTimeoutSec: CROSS_CLI_RESULT_WAIT_MAX_S + 30 } : {}) }, handle,
|
|
446
|
+
crossCli: () => others?.offered() ?? [],
|
|
430
447
|
root(id) { if (typeof id === 'string') rootSession = id; },
|
|
431
448
|
resolve,
|
|
432
449
|
spawnResult(result) {
|
|
@@ -460,10 +477,10 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
460
477
|
async close(stopped = false) {
|
|
461
478
|
if (closed) return;
|
|
462
479
|
closed = true; clearInterval(timer);
|
|
463
|
-
rootTools?.close();
|
|
480
|
+
rootTools?.close(); others?.close();
|
|
464
481
|
// Only an escalated question waits for the person; any other asked child ended with the run.
|
|
465
482
|
for (const a of records.values()) { release(a.id); const q = questions.get(a.id);
|
|
466
|
-
if (!terminal(a.status) && !(a.status === 'asked' && !readOnlyRun && !stopped && q?.escalated && !q.answer)) emit({ id: a.id, status: 'stopped', now: stopped ? 'Stopped with the run' : 'Run ended' }); }
|
|
483
|
+
if (!terminal(a.status) && !(!stopped && others?.asking(a.id)) && !(a.status === 'asked' && !readOnlyRun && !stopped && q?.escalated && !q.answer)) emit({ id: a.id, status: 'stopped', now: stopped ? 'Stopped with the run' : 'Run ended' }); }
|
|
467
484
|
server.close(); server.closeAllConnections();
|
|
468
485
|
forgetExtraSecrets(secretKey);
|
|
469
486
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ORCHESTRATOR_TOOLS } from './orchestratorProtocol.mjs';
|
|
1
|
+
import { CROSS_CLI_TOOLS, ORCHESTRATOR_TOOLS } from './orchestratorProtocol.mjs';
|
|
2
2
|
import { ORCHESTRATOR_REQUEST_BYTES, ORCHESTRATOR_INPUT_TOO_LARGE, OrchestratorInputTooLarge } from './orchestratorInput.mjs';
|
|
3
3
|
import { serveMcpStdio } from './mcpStdio.mjs';
|
|
4
4
|
|
|
@@ -25,6 +25,12 @@ export async function callOrchestrator(action, args, timeoutMs = ORCHESTRATOR_CL
|
|
|
25
25
|
return result;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** A result call waits up to its own waitSeconds (crossCliChild.mjs), past the usual bound. */
|
|
29
|
+
export const callTimeoutFor = (action, args) => action === 'cli_agent_result'
|
|
30
|
+
? (Math.min(Math.max(Number(args?.waitSeconds) || 60, 0), 300) + 15) * 1000 : ORCHESTRATOR_CLIENT_TIMEOUT_MS;
|
|
31
|
+
|
|
28
32
|
export function orchestratorMcpMain() {
|
|
29
|
-
|
|
33
|
+
// Includes stop_cli_agent; all four child actions relay through this capability.
|
|
34
|
+
const tools = process.env.FLOWVIANT_ORCHESTRATOR_CROSS_CLI ? [...ORCHESTRATOR_TOOLS, ...CROSS_CLI_TOOLS] : ORCHESTRATOR_TOOLS;
|
|
35
|
+
return serveMcpStdio({ serverInfo: { name: 'card-orchestrator', version: '2' }, tools, call: (action, args) => callOrchestrator(action, args, callTimeoutFor(action, args)) });
|
|
30
36
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TESTS_SCOPE } from './testFindingsContract.mjs';
|
|
2
2
|
import { SAFE_POINT_AFTER_EDIT } from './orchestratorProtocol.mjs';
|
|
3
|
+
import { splitContract } from './splitGate.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* THE POSTURES A MARKED CARD IS ORCHESTRATED IN, and what their children may
|
|
@@ -49,7 +50,10 @@ const CHILD_SPELLING = {
|
|
|
49
50
|
const atCard = (model, effort) =>
|
|
50
51
|
`This card runs on ${model ? model : 'its CLI’s default model'} at ${effort ? `${effort} effort` : 'its CLI’s default effort'}.`;
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
/** Another CLI's children (E7, crossCliChild.mjs), where the controller offers them. */
|
|
54
|
+
const CROSS_CLI_SPELLING = 'start_cli_agent starts a child on another CLI, at a model and effort of that CLI no higher than the card\'s effort; use it only for a batch the person put on that CLI';
|
|
55
|
+
|
|
56
|
+
const childBrain = ({ runtime, model, effort, crossCli = [] }) => `CHILD MODEL AND EFFORT. ${atCard(model, effort)} By default every child runs at the card's model and effort${model || effort ? '; the gate names them when your spawn names neither, so a child never falls back to the CLI\'s own global default' : ', which it inherits from this run when your spawn names neither'}. For a simple, well-bounded child — a search, a sweep of file reads, a mechanical edit — you may choose a cheaper model or a lower effort. Keep the card's model and effort for hard or judgment-heavy work. Never choose a model or effort above the card's, and only models of this card's own CLI${crossCli.length ? ` for a native child (${CROSS_CLI_SPELLING})` : ''}; the gate refuses an effort above the card's. ${CHILD_SPELLING[runtime] ?? ''} When you choose anything other than the card's model or effort for a child, name the choice and why in your update.`.replace(' ', ' ');
|
|
53
57
|
|
|
54
58
|
/** Investigators do not propose separate plans. Only this coordinator sees
|
|
55
59
|
* and reconciles the complete selection before the existing JSON validator. */
|
|
@@ -126,15 +130,19 @@ ${childBrain(brain).replaceAll('card', 'capture turn')}`;
|
|
|
126
130
|
* rules, the card's own model and effort, then the gate, routing and child
|
|
127
131
|
* prompt rules every orchestrated card shares.
|
|
128
132
|
*/
|
|
129
|
-
export function orchestratorContract({ posture = 'build', taskType = null, runtime = 'claude', model = null, effort = null } = {}) {
|
|
133
|
+
export function orchestratorContract({ posture = 'build', taskType = null, runtime = 'claude', model = null, effort = null, split = null, crossCli = [] } = {}) {
|
|
130
134
|
const reads = ORCHESTRATED_POSTURES[posture] === 'read';
|
|
135
|
+
// The person's yes before children edit files (splitGate.mjs): a research
|
|
136
|
+
// card's children only read, so nothing there waits or is proposed.
|
|
137
|
+
const asks = reads ? '' : splitContract(split, { runtime, model, effort, crossCli });
|
|
131
138
|
return `CARD ORCHESTRATOR — this applies to this card's one CLI and worktree.
|
|
132
139
|
You are the model the person chose and remain responsible for the card and its final JSON.
|
|
133
140
|
When an agent queue snapshot is supplied, use it across resumed and fresh turns. Execute only the active card. A person's instruction keeps its stated scope across later cards; its recorded card is provenance, not an implicit target. Give each child a focused assignment with relevant prior findings, decisions and exact prerequisite references. Upcoming work never expands a child's assignment or the active card's product/write contract.
|
|
134
|
-
${SPLIT}${reads ? '' : `\n${SHARED_FIRST}`}${taskType === 'tests' ? `\n${TESTS_SCOPE} Give every child this Tests scope explicitly.` : ''}${reads ? `\n${RESEARCH}` : ''}
|
|
135
|
-
${childBrain({ runtime, model, effort })}
|
|
141
|
+
${SPLIT}${reads ? '' : `\n${SHARED_FIRST}`}${asks ? `\n${asks}` : ''}${taskType === 'tests' ? `\n${TESTS_SCOPE} Give every child this Tests scope explicitly.` : ''}${reads ? `\n${RESEARCH}` : ''}
|
|
142
|
+
${childBrain({ runtime, model, effort, crossCli })}
|
|
143
|
+
${crossCli.length ? 'SUBAGENTS ON ANOTHER CLI. Use start_cli_agent for another CLI, cli_agent_result to read it, followup_cli_agent to continue by its agentId or key even in a later turn, and stop_cli_agent to interrupt just that child. A lost native session refuses in words. An asking result carries a question and options: answer with followup_cli_agent from known facts, or end your own turn through the existing person-question path. Never route a child question directly to the person. Research children only read and return findings; they never edit files.' : ''}
|
|
136
144
|
|
|
137
|
-
The daemon gates every native spawn against this project's Turns at once pool, shared with its other turns. Claude children run in the background so you remain available for messages and their questions; Codex spawns are already asynchronous. A refused spawn with a waiting id has not started. Keep working yourself; do not wait for it. When notified of a free slot, retry its native spawn (the same or a reworded prompt) or its native follow-up; the slot is held for about a minute, then offered again later. Never bypass this gate with another CLI process. The orchestrator itself also holds a slot. If no slot becomes available, finish the work yourself.
|
|
145
|
+
The daemon gates every native spawn against this project's Turns at once pool, shared with its other turns. Claude children run in the background so you remain available for messages and their questions; Codex spawns are already asynchronous. A refused spawn with a waiting id has not started. Keep working yourself; do not wait for it. When notified of a free slot, retry its native spawn (the same or a reworded prompt) or its native follow-up; the slot is held for about a minute, then offered again later. Never bypass this gate with another CLI process${crossCli.length ? '; start_cli_agent is not one: its child takes a slot from this same pool and waits while none is free' : ''}. The orchestrator itself also holds a slot. If no slot becomes available, finish the work yourself.
|
|
138
146
|
On Claude you may end your turn while native children run: Claude keeps this process alive and wakes you on each child's completion and on the person's notes. Follow-ups and answers to a Claude child go through SendMessage to its agent id. Write the card's final JSON only after every child has finished or been stopped; your last answer is the card's answer.
|
|
139
147
|
|
|
140
148
|
The card_orchestrator MCP tools report routing and child questions. A live person’s note has a Message id. Report each note using route_message:
|
|
@@ -34,4 +34,22 @@ export const ORCHESTRATOR_TOOLS = [
|
|
|
34
34
|
{ name: 'answer_agent', description: 'Answer a child question from known facts, or mark it for escalation to the person. An escalation returns the attribution to put on the existing blocked question card.',
|
|
35
35
|
inputSchema: { type: 'object', properties: { agentId: { type: 'string' }, answer: { type: 'string' }, escalate: { type: 'boolean' },
|
|
36
36
|
question: { type: 'string' }, nativeAgentId: { type: 'string' }, kind: { type: 'string' }, title: { type: 'string' } }, required: ['agentId'], additionalProperties: false } },
|
|
37
|
+
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** SUBAGENTS ON ANOTHER CLI (2026-11-06, D21, E7; crossCliChild.mjs): listed
|
|
41
|
+
* only on a run that may start one (`FLOWVIANT_ORCHESTRATOR_CROSS_CLI`, the
|
|
42
|
+
* CLIs offered). The daemon starts and fences the child itself. */
|
|
43
|
+
export const CROSS_CLI_TOOLS = [
|
|
44
|
+
{ name: 'start_cli_agent', description: 'Start a subagent on another CLI than yours (claude or codex) in this card\'s worktree, for a part of the card the person approved for that CLI. Returns its agentId at once; it works in the background. Read its outcome with cli_agent_result. The daemon fences it with the card\'s posture for that CLI; it cannot start subagents of its own.',
|
|
45
|
+
inputSchema: { type: 'object', properties: { cli: { enum: ['claude', 'codex'] }, title: { type: 'string' }, kind: { type: 'string', description: 'Backend, UI, Tests or the area of work.' },
|
|
46
|
+
brief: { type: 'string', description: 'The whole assignment: the part, the files it owns, the shared code it must not change, and what to return.' },
|
|
47
|
+
model: { type: 'string', description: 'A model of that CLI; leave it out for the person\'s batch choice or the default.' }, effort: { type: 'string' } },
|
|
48
|
+
required: ['cli', 'title', 'kind', 'brief'], additionalProperties: false } },
|
|
49
|
+
{ name: 'followup_cli_agent', description: 'Send a finished subagent on another CLI a follow-up in its own conversation. Use the same agentId or stable key across turns. Refused while it is still working; a missing native session refuses in words. Returns at once; read it with cli_agent_result.',
|
|
50
|
+
inputSchema: { type: 'object', properties: { agentId: { type: 'string' }, message: { type: 'string' } }, required: ['agentId', 'message'], additionalProperties: false } },
|
|
51
|
+
{ name: 'cli_agent_result', description: 'Wait up to waitSeconds (at most 300) for a subagent on another CLI, then return its status (waiting, running, done, asking, stopped or limited), the model and effort its own session names, its final message as result, the files it changed and the CLI\'s own words on a failure or plan limit.',
|
|
52
|
+
inputSchema: { type: 'object', properties: { agentId: { type: 'string' }, waitSeconds: { type: 'number', minimum: 0, maximum: 300 } }, required: ['agentId'], additionalProperties: false } },
|
|
53
|
+
{ name: 'stop_cli_agent', description: 'Interrupt only this child, then kill its process group after the grace period if needed. Records stopped and releases its slot. Its files and conversation remain.',
|
|
54
|
+
inputSchema: { type: 'object', properties: { agentId: { type: 'string' } }, required: ['agentId'], additionalProperties: false } },
|
|
37
55
|
];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A LEAD THAT SHELLS OUT TO ANOTHER CLI IS SHOWN, TAGGED "OUTSIDE" (D23,
|
|
3
|
+
* 2026-11-06, S4.6). `codex exec …` or `claude -p …` from a card's shell is
|
|
4
|
+
* a second model working on the card, and the person sitting at the keyboard
|
|
5
|
+
* would see it start. It is not one of the CLI's own subagents: it is never
|
|
6
|
+
* blocked, never counted toward the 32 and never given a subagent record.
|
|
7
|
+
* Each one is an `outside` row on the turn's trace (trace.mjs `outside`).
|
|
8
|
+
*
|
|
9
|
+
* THE MATCHER IS SMALL AND CONSERVATIVE ON PURPOSE. It reads only the first
|
|
10
|
+
* command of the line, after leading `cd … &&` steps, environment
|
|
11
|
+
* assignments and an `env`, inside one `bash -lc '…'` wrapper (Codex's exec
|
|
12
|
+
* spelling): its first word is `codex` with `exec`/`e`, `claude` with
|
|
13
|
+
* `-p`/`--print`, or `gemini`, directly or through `npx`/`bunx`/`pnpm dlx`/
|
|
14
|
+
* `npm exec` of that CLI's package. A pipeline, a loop or a script that
|
|
15
|
+
* starts one is not recognized: an unmatched line is an ordinary command,
|
|
16
|
+
* which the lead's stream relays as it always did. The asked model is what
|
|
17
|
+
* the command names (`--model`/`-m`); what ran is that CLI's business.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const PACKAGES = Object.freeze({
|
|
21
|
+
'@openai/codex': 'codex',
|
|
22
|
+
'@anthropic-ai/claude-code': 'claude',
|
|
23
|
+
'@google/gemini-cli': 'gemini',
|
|
24
|
+
});
|
|
25
|
+
const RUNNERS = new Set(['npx', 'bunx']);
|
|
26
|
+
const MODEL_RE = /^[A-Za-z0-9._:\-/]{1,80}$/;
|
|
27
|
+
|
|
28
|
+
/** Shell words, with quotes honored and no expansion. */
|
|
29
|
+
function words(line) {
|
|
30
|
+
const out = [];
|
|
31
|
+
let word = '', quote = null, any = false;
|
|
32
|
+
for (let i = 0; i < line.length; i++) {
|
|
33
|
+
const ch = line[i];
|
|
34
|
+
if (quote) {
|
|
35
|
+
if (ch === quote) quote = null;
|
|
36
|
+
else if (ch === '\\' && quote === '"' && i + 1 < line.length) word += line[++i];
|
|
37
|
+
else word += ch;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (ch === "'" || ch === '"') { quote = ch; any = true; continue; }
|
|
41
|
+
if (ch === '\\' && i + 1 < line.length) { word += line[++i]; any = true; continue; }
|
|
42
|
+
if (/\s/.test(ch)) { if (word || any) out.push(word); word = ''; any = false; continue; }
|
|
43
|
+
word += ch;
|
|
44
|
+
}
|
|
45
|
+
if (word || any) out.push(word);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The first command of a line, past `cd … &&`/`;` steps, as words. */
|
|
50
|
+
function firstCommand(line) {
|
|
51
|
+
let text = String(line ?? '').trim();
|
|
52
|
+
const wrapped = text.match(/^(?:\S*\/)?(?:bash|sh|zsh)\s+-l?c\s+(['"])([\s\S]*)\1$/);
|
|
53
|
+
if (wrapped) text = wrapped[2].trim();
|
|
54
|
+
for (const step of text.split(/\s*(?:&&|;)\s*/)) {
|
|
55
|
+
const w = words(step);
|
|
56
|
+
if (!w.length || w[0] === 'cd' || w[0] === 'pushd') continue;
|
|
57
|
+
let i = 0;
|
|
58
|
+
if (w[i] === 'env') i++;
|
|
59
|
+
while (i < w.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(w[i])) i++;
|
|
60
|
+
return w.slice(i);
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const base = word => String(word ?? '').split('/').at(-1);
|
|
66
|
+
|
|
67
|
+
/** `{ cli, model? }` when the command starts another CLI, else null. */
|
|
68
|
+
export function matchOutsideCli(command) {
|
|
69
|
+
let w = firstCommand(Array.isArray(command) ? command.join(' ') : command);
|
|
70
|
+
// A package runner names the CLI by its package.
|
|
71
|
+
const runner = base(w[0]);
|
|
72
|
+
const dlx = (runner === 'pnpm' && w[1] === 'dlx') || (runner === 'npm' && w[1] === 'exec');
|
|
73
|
+
let cli = null;
|
|
74
|
+
if (RUNNERS.has(runner) || dlx) {
|
|
75
|
+
let i = dlx ? 2 : 1;
|
|
76
|
+
while (i < w.length && w[i].startsWith('-')) i++;
|
|
77
|
+
cli = PACKAGES[String(w[i] ?? '').replace(/(?<=.)@[^/]*$/, '')] ?? null;
|
|
78
|
+
if (!cli) return null;
|
|
79
|
+
w = [cli, ...w.slice(i + 1).filter(x => x !== '--')];
|
|
80
|
+
} else cli = ['codex', 'claude', 'gemini'].includes(base(w[0])) ? base(w[0]) : null;
|
|
81
|
+
if (!cli) return null;
|
|
82
|
+
const args = w.slice(1);
|
|
83
|
+
if (cli === 'codex' && !['exec', 'e'].includes(args[0])) return null;
|
|
84
|
+
if (cli === 'claude' && !args.some(a => a === '-p' || a === '--print')) return null;
|
|
85
|
+
let model = null;
|
|
86
|
+
for (let i = 0; i < args.length; i++) {
|
|
87
|
+
const a = args[i];
|
|
88
|
+
if (a === '--model' || a === '-m') model = args[i + 1] ?? null;
|
|
89
|
+
else if (a.startsWith('--model=')) model = a.slice(8);
|
|
90
|
+
}
|
|
91
|
+
return { cli, ...(model && MODEL_RE.test(model) ? { model } : {}) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The lead's tool events, watched for Outside rows. `observe` takes the
|
|
96
|
+
* same (name, input, meta) the lane's tool sink does: a shell call opens a
|
|
97
|
+
* row by its call id, its result settles it. A background shell's result is
|
|
98
|
+
* only its launch, so its row keeps no status it cannot know.
|
|
99
|
+
*/
|
|
100
|
+
export function createOutsideRuns(trace) {
|
|
101
|
+
const open = new Map();
|
|
102
|
+
return {
|
|
103
|
+
observe(name, input, meta) {
|
|
104
|
+
if (!meta?.id) return;
|
|
105
|
+
if (meta.result) {
|
|
106
|
+
if (!open.has(meta.id)) return;
|
|
107
|
+
const background = open.get(meta.id);
|
|
108
|
+
open.delete(meta.id);
|
|
109
|
+
if (background) return;
|
|
110
|
+
const failed = meta.isError === true || (Number.isInteger(meta.exitCode) && meta.exitCode !== 0);
|
|
111
|
+
trace.outside(meta.id, { status: failed ? 'failed' : 'done' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (name !== 'Bash' || typeof input?.command !== 'string' && !Array.isArray(input?.command)) return;
|
|
115
|
+
const match = matchOutsideCli(input.command);
|
|
116
|
+
if (!match) return;
|
|
117
|
+
const background = input.run_in_background === true;
|
|
118
|
+
open.set(meta.id, background);
|
|
119
|
+
if (open.size > 64) open.delete(open.keys().next().value);
|
|
120
|
+
trace.outside(meta.id, { ...match, command: Array.isArray(input.command) ? input.command.join(' ') : input.command,
|
|
121
|
+
...(background ? {} : { status: 'running' }) });
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
package/bin/lib/prompts.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CONVERSATION_OUTPUT } from './conversationOutput.mjs';
|
|
1
2
|
import { TESTS_SCOPE, TESTS_REPORT } from './testFindingsContract.mjs';
|
|
2
3
|
import { randomBytes } from 'node:crypto';
|
|
3
4
|
import { REGRESSION_INVESTIGATION } from './discoveryContract.mjs';
|
|
@@ -28,13 +29,14 @@ import { ARTIFACT_CONTRACT_TAIL, HELP_THEM_DECIDE, LOOK_HEADLESS, SYSTEM_AGENT_D
|
|
|
28
29
|
import { isLibraryReference } from './knowledgeLibrary.mjs';
|
|
29
30
|
import { readCardThread } from './cardThread.mjs';
|
|
30
31
|
import { AGENT_FILE_LINE_MAX, AGENT_FILES_PER_MESSAGE_MAX, CARD_FILES_PER_TURN_MAX } from './agentFiles.mjs';
|
|
32
|
+
import { SYSTEM_ARCHITECTURE_CONTRACT } from './architectureContract.mjs';
|
|
31
33
|
|
|
32
34
|
// Wiki-gen turn: the local Claude READS the repo (cwd) and writes/maintains the
|
|
33
35
|
// knowledge VAULT — a plain directory of markdown files with [[wikilinks]]
|
|
34
36
|
// (Obsidian-style). No MCP tools involved: the vault is just files, and the
|
|
35
37
|
// daemon hash-diff syncs them to Flowviant after the turn. The repo itself is
|
|
36
38
|
// strictly read-only.
|
|
37
|
-
const WIKI_MAINTENANCE =
|
|
39
|
+
const WIKI_MAINTENANCE = `${SYSTEM_ARCHITECTURE_CONTRACT}
|
|
38
40
|
OWNERSHIP AND PROVENANCE — this directory is a disposable draft of GENERATED
|
|
39
41
|
output, separate from project Knowledge, uploaded/reference vault snapshots and
|
|
40
42
|
human standing Instructions. Never write to any original source. In this draft,
|
|
@@ -2037,12 +2039,12 @@ XHR, sockets, forms and popups are all closed. Never commit these files.`;
|
|
|
2037
2039
|
/**
|
|
2038
2040
|
* The system prompt a turn actually runs under: the contract it picked, plus
|
|
2039
2041
|
* whatever project context this box holds. `knowledgeDir` null or absent and
|
|
2040
|
-
* `artifacts` false
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2042
|
+
* `artifacts` false omit their paragraphs. The shared conversation output guide
|
|
2043
|
+
* always follows the base contract. Knowledge then says what to READ, and the
|
|
2044
|
+
* artifact paragraph says where to WRITE when that surface accepts files.
|
|
2043
2045
|
*/
|
|
2044
2046
|
export const withProjectContext = (system, { knowledgeDir, artifacts = false } = {}) => {
|
|
2045
|
-
let out = system
|
|
2047
|
+
let out = `${system}\n\n${CONVERSATION_OUTPUT}`;
|
|
2046
2048
|
if (knowledgeDir) out = `${out}\n\n${KNOWLEDGE_PARAGRAPH(knowledgeDir)}`;
|
|
2047
2049
|
if (artifacts) out = `${out}\n\n${ARTIFACTS_PARAGRAPH}`;
|
|
2048
2050
|
return out;
|
package/bin/lib/runTurn.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { createTurnOutput } from './turnOutput.mjs';
|
|
|
27
27
|
import { createObservedLines } from './observedLines.mjs';
|
|
28
28
|
import { startTurnDiagnostics } from './turnDiagnostics.mjs';
|
|
29
29
|
import { createClaudeInput } from './claudeInput.mjs';
|
|
30
|
+
import { codexAppArgs, createCodexInput } from './codexInput.mjs';
|
|
30
31
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
31
32
|
import { createTurnSubagents } from './turnSubagents.mjs';
|
|
32
33
|
import { withPublicReference } from './publicReferenceRuntime.mjs';
|
|
@@ -79,7 +80,7 @@ const MAX_ARG_BYTES = 128 * 1024;
|
|
|
79
80
|
|
|
80
81
|
export function runTurn(opts) {
|
|
81
82
|
if (workLanesStopped()) return never();
|
|
82
|
-
if ((opts.profile === 'research' || opts.researchIdentity) && ['claude', 'codex'].includes(opts.runtime ?? 'claude'))
|
|
83
|
+
if (!opts.crossCliChild && (opts.profile === 'research' || opts.researchIdentity) && ['claude', 'codex'].includes(opts.runtime ?? 'claude'))
|
|
83
84
|
return withPublicReference(opts, runProfileTurn);
|
|
84
85
|
return runProfileTurn(opts);
|
|
85
86
|
}
|
|
@@ -88,7 +89,7 @@ function runProfileTurn(opts) {
|
|
|
88
89
|
// A Codex write-up runs over an isolated copy of its worktree
|
|
89
90
|
// (codexResearch.mjs). After the teardown it copies nothing either: the
|
|
90
91
|
// stand-down gate below is asked before the copy, not only before the spawn.
|
|
91
|
-
if (opts.runtime === 'codex' && opts.profile === 'research')
|
|
92
|
+
if (!opts.crossCliChild && opts.runtime === 'codex' && opts.profile === 'research')
|
|
92
93
|
return workLanesStopped() ? never() : runIsolatedResearch(opts, prepared => runResearchWithReceipt(prepared, runPlainTurn));
|
|
93
94
|
if (opts.profile === 'research' && opts.researchHandoff)
|
|
94
95
|
return prepareResearchLaunch(opts).then(prepared => runResearchWithReceipt(prepared, runPlainTurn));
|
|
@@ -208,6 +209,8 @@ function runPlainTurn(opts) {
|
|
|
208
209
|
verifiedWorktree: opts.verifiedWorktree === true,
|
|
209
210
|
orchestration: opts.orchestration === true,
|
|
210
211
|
readOnlyCoordination: opts.readOnlyCoordination === true,
|
|
212
|
+
// A subagent on another CLI's card (crossCliChild.mjs, crossCliPosture.mjs).
|
|
213
|
+
crossCliChild: opts.crossCliChild === true,
|
|
211
214
|
// The lane's own measurement when it decided to orchestrate, so the
|
|
212
215
|
// hooks and the controller cannot disagree (workAgentTurnExecution.mjs).
|
|
213
216
|
codexVersion: opts.codexVersion ?? measuredRuntimeVersion('codex')?.version,
|
|
@@ -303,8 +306,13 @@ function runPlainTurn(opts) {
|
|
|
303
306
|
* so no throw on the way leaves that file behind: the sweep takes only a
|
|
304
307
|
* dead process's leftovers, and this one names the daemon's own pid.
|
|
305
308
|
*/
|
|
309
|
+
const nativeVersion = opts.codexVersion ?? measuredRuntimeVersion('codex')?.version;
|
|
310
|
+
// Only a measured native protocol switches transport. Unknown/older CLIs
|
|
311
|
+
// still receive the original prompt and durable next-turn queue via exec.
|
|
312
|
+
const appArgs = rt.id === 'codex' && opts.liveMessages && nativeVersion ? codexAppArgs(args, nativeVersion) : null;
|
|
313
|
+
const codexApp = Boolean(appArgs);
|
|
306
314
|
let transport;
|
|
307
|
-
try { transport = promptTransport(rt.id, args, stdinText); }
|
|
315
|
+
try { transport = appArgs ? { args: appArgs, text: null, cleanup() {} } : promptTransport(rt.id, args, stdinText); }
|
|
308
316
|
catch (error) { rt.releaseArgs?.(args); throw error; }
|
|
309
317
|
args.splice(0, args.length, ...transport.args);
|
|
310
318
|
let argsReleased = false;
|
|
@@ -403,7 +411,13 @@ function runPlainTurn(opts) {
|
|
|
403
411
|
const spawnedAt = Date.now();
|
|
404
412
|
let out = '';
|
|
405
413
|
let stdinError = null;
|
|
406
|
-
const input =
|
|
414
|
+
const input = codexApp ? createCodexInput(child, { cwd, threadId: resumeThreadId,
|
|
415
|
+
onTaken: id => { if (!id.startsWith('recovery-')) opts.liveMessages.onTaken(id); },
|
|
416
|
+
onThread: id => opts.liveMessages.onInit(id),
|
|
417
|
+
onEvent: event => onLine(JSON.stringify(event), Date.now(), true),
|
|
418
|
+
onFailure: () => { cliFailed = true; opts.onCliFailure?.(); },
|
|
419
|
+
onContext: opts.onContext ?? null,
|
|
420
|
+
}) : !rt.parse && rt.liveInput === true && opts.liveMessages ? createClaudeInput(child, { onTaken: opts.liveMessages.onTaken, onContext: opts.onContext ?? null }) : null;
|
|
407
421
|
// EVERY TURN IS LINE-PARSED (2026-10-10, the audit). Claude's adapter
|
|
408
422
|
// always asks for `--output-format stream-json` and every other runtime
|
|
409
423
|
// brings its own `parse` — for Codex the JSONL stream is the only output
|
|
@@ -436,7 +450,7 @@ function runPlainTurn(opts) {
|
|
|
436
450
|
};
|
|
437
451
|
// The native children this CLI starts, observed (turnSubagents.mjs).
|
|
438
452
|
const natives = createTurnSubagents({ runtime: rt.id, cwd, since: spawnedAt, env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
439
|
-
owned: opts.orchestration === true, onSubagent: opts.onSubagent, onSpawnResult: opts.onSpawnResult, resumeThreadId,
|
|
453
|
+
owned: opts.orchestration === true, onSubagent: opts.onSubagent, onWorkflow: opts.onWorkflow, onSpawnResult: opts.onSpawnResult, resumeThreadId, knownSubagents: opts.knownSubagents,
|
|
440
454
|
sinks: { emit, onActivity, onToolEvent, appendText, onDetail: output.detail } });
|
|
441
455
|
let codexThread = rt.id === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
|
|
442
456
|
/**
|
|
@@ -471,14 +485,18 @@ function runPlainTurn(opts) {
|
|
|
471
485
|
const taskPlan = new Map();
|
|
472
486
|
/** One line of the child's stdout, in whichever dialect it speaks. */
|
|
473
487
|
let cliFailed = false;
|
|
474
|
-
const onLine = (line, observedAt) => {
|
|
488
|
+
const onLine = (line, observedAt, nativeEvent = false) => {
|
|
489
|
+
if (codexApp && !nativeEvent) {
|
|
490
|
+
try { input.event(JSON.parse(line)); } catch { /* non-protocol stdout */ }
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
475
493
|
const emitObserved = (text, meta) => emit(text, { ...meta, observedAt });
|
|
476
494
|
const toolObserved = (name, input, meta) => onToolEvent(name, input, meta, observedAt);
|
|
477
495
|
try {
|
|
478
496
|
const event = JSON.parse(line);
|
|
479
497
|
const parentEvent = event.parent_tool_use_id == null && !event.isSidechain;
|
|
480
498
|
opts.researchReceipt?.event(event);
|
|
481
|
-
if (input && parentEvent) {
|
|
499
|
+
if (input && !codexApp && parentEvent) {
|
|
482
500
|
input.event(event);
|
|
483
501
|
if (event.type === 'system' && event.subtype === 'init' && event.session_id) opts.liveMessages.onInit(event.session_id);
|
|
484
502
|
// A message written just before result may become a second native
|
|
@@ -486,6 +504,7 @@ function runPlainTurn(opts) {
|
|
|
486
504
|
// structured objects joined together; every result still reports spend.
|
|
487
505
|
if (event.type === 'result') out = '';
|
|
488
506
|
}
|
|
507
|
+
if (codexApp && event.type === 'turn.started') out = '';
|
|
489
508
|
if (parentEvent && ((event.type === 'result' && event.is_error === true) ||
|
|
490
509
|
event.type === 'error' || event.type === 'turn.failed')) {
|
|
491
510
|
cliFailed = true;
|
|
@@ -500,7 +519,9 @@ function runPlainTurn(opts) {
|
|
|
500
519
|
// thread.started). Purely additive: callers that pass no onThreadId —
|
|
501
520
|
// every dispatch path — see zero behavior change.
|
|
502
521
|
if (ev.threadId) {
|
|
503
|
-
if (rt.id === 'codex') { codexThread = ev.threadId; natives.thread(ev.threadId);
|
|
522
|
+
if (rt.id === 'codex') { codexThread = ev.threadId; natives.thread(ev.threadId);
|
|
523
|
+
if (!input && opts.liveMessages) opts.liveMessages.onTaken(opts.liveMessages.turnId);
|
|
524
|
+
}
|
|
504
525
|
onThreadId?.(ev.threadId);
|
|
505
526
|
}
|
|
506
527
|
// The runtime's own token count (codex's `turn.completed`) — the same
|
|
@@ -657,6 +678,7 @@ function runPlainTurn(opts) {
|
|
|
657
678
|
if (input) {
|
|
658
679
|
const initial = opts.liveMessages.initialPrompt(prompt, resumeThreadId, resume, opts.liveContinuation === true);
|
|
659
680
|
input.send(opts.liveMessages.initialId || opts.liveMessages.turnId, initial);
|
|
681
|
+
if (codexApp) input.start();
|
|
660
682
|
opts.liveMessages.connect(input.send);
|
|
661
683
|
}
|
|
662
684
|
});
|
|
@@ -21,6 +21,7 @@ import { withSettings } from './claudeSettingsArg.mjs';
|
|
|
21
21
|
import { orchestratorHooks, claudeOrchestrationSafe } from './orchestratorHooks.mjs';
|
|
22
22
|
import { orchestratesPosture } from './orchestratorPrompt.mjs';
|
|
23
23
|
import { versionAtLeast } from './versionOrder.mjs';
|
|
24
|
+
import { crossCliClaudePerm } from './crossCliPosture.mjs';
|
|
24
25
|
|
|
25
26
|
/** The release the agent lane first measured `--system-prompt-snapshot` on
|
|
26
27
|
* (decisions.md, 2026-09-24). */
|
|
@@ -123,7 +124,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
123
124
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
124
125
|
* above the conversation rather than inside it.
|
|
125
126
|
*/
|
|
126
|
-
args({ managedBrowser = false, 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, readOnlyCoordination = false, claudeVersion = null }) {
|
|
127
|
+
args({ managedBrowser = false, 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, readOnlyCoordination = false, claudeVersion = null, crossCliChild = false, cwd }) {
|
|
127
128
|
// THE CARTOGRAPHER'S FENCE NAMES ITS VAULT (2026-10-10): its one write rule
|
|
128
129
|
// is the vault's absolute path (claudePosture.mjs `wikiPermFor`), so a turn
|
|
129
130
|
// with no vault, or one no rule can name, has no list — refused here in
|
|
@@ -195,7 +196,10 @@ export const CLAUDE_RUNTIME = {
|
|
|
195
196
|
// a research card's list then admits the children its fence denied.
|
|
196
197
|
const readCoordinator = orchestration && readOnlyCoordination && ['consult', 'plan'].includes(profile) && claudeOrchestrationSafe(claudeVersion);
|
|
197
198
|
const gated = readCoordinator || orchestration && orchestratesPosture(profile);
|
|
198
|
-
|
|
199
|
+
// A subagent on another CLI's card runs the build fenced to its worktree,
|
|
200
|
+
// with no children of its own (crossCliPosture.mjs).
|
|
201
|
+
const perm = readCoordinator ? withReadOnlyCoordination(postureList) : gated && fenced ? withNativeChildren(postureList)
|
|
202
|
+
: crossCliChild ? crossCliClaudePerm(postureList, cwd, profile) : postureList;
|
|
199
203
|
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
200
204
|
// A FENCED POSTURE NAMES ITS OWN MODE (2026-10-10). Its lists fence only
|
|
201
205
|
// under the CLI's `default` mode, where a tool the list does not name is
|
|
@@ -205,7 +209,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
205
209
|
// classifier that may approve what the list never named. So the posture
|
|
206
210
|
// states `default` itself, unless its own list names a mode (plan-mode).
|
|
207
211
|
if (fenced && !perm.includes('--permission-mode')) a.push('--permission-mode', 'default');
|
|
208
|
-
else if (agentTools) a.push('--setting-sources', 'user');
|
|
212
|
+
else if (agentTools || crossCliChild) a.push('--setting-sources', 'user');
|
|
209
213
|
// EVERY TURN RENDERS ITS OWN SYSTEM PROMPT (2026-10-19). By default the
|
|
210
214
|
// CLI records a conversation's first system prompt and resends that record
|
|
211
215
|
// on every resume "even when a later launch passes different text" —
|
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { projectReadBoundary, projectReadFence } from './codexProjectRead.mjs';
|
|
|
19
19
|
import { versionAtLeast } from './versionOrder.mjs';
|
|
20
20
|
import { codexOrchestratorArgs } from './orchestratorHooks.mjs';
|
|
21
21
|
import { orchestratesPosture } from './orchestratorPrompt.mjs';
|
|
22
|
+
import { CROSS_CLI_TMP_FENCE } from './crossCliPosture.mjs';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* FLOWVIANT'S OWN TOOLS NEED NO APPROVAL FROM A CHAT NOBODY CAN ANSWER
|
|
@@ -215,7 +216,7 @@ export const CODEX_RUNTIME = {
|
|
|
215
216
|
bin: 'codex',
|
|
216
217
|
install: 'npm i -g @openai/codex',
|
|
217
218
|
login: 'codex login',
|
|
218
|
-
liveInput:
|
|
219
|
+
liveInput: true,
|
|
219
220
|
/**
|
|
220
221
|
* BUILD AND CONSULT. See `args()` below for how consult is expressed — the
|
|
221
222
|
* short version is that Codex keeps the promise at the kernel rather than in
|
|
@@ -281,7 +282,7 @@ export const CODEX_RUNTIME = {
|
|
|
281
282
|
* limit even when its argv plus environment is far below ARG_MAX. Both exec
|
|
282
283
|
* and exec resume support `-` (verified on Codex 0.160.0, 2026-10-16).
|
|
283
284
|
*/
|
|
284
|
-
args({ managedBrowser = false, streamInput = false, writableDirs = [], prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation, knowledgeDir, projectReads, cardTurn = false, verifiedWorktree = false, codexVersion, orchestration = false, readOnlyCoordination = false }) {
|
|
285
|
+
args({ managedBrowser = false, streamInput = false, writableDirs = [], prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation, knowledgeDir, projectReads, cardTurn = false, verifiedWorktree = false, codexVersion, orchestration = false, readOnlyCoordination = false, crossCliChild = false }) {
|
|
285
286
|
// Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
|
|
286
287
|
// agy moves) — codex has no adoptable store wired yet. Reaching here
|
|
287
288
|
// with an adopt id is a wiring mistake upstream, and it fails loudly on
|
|
@@ -319,7 +320,7 @@ export const CODEX_RUNTIME = {
|
|
|
319
320
|
// the parent's sandbox (2026-10-19, orchestratorPrompt.mjs).
|
|
320
321
|
const gated = orchestration && (orchestratesPosture(fenced ? profile : 'build') || readOnlyCoordination && ['consult', 'plan'].includes(profile)) && codexSubagentsSafe(codexVersion);
|
|
321
322
|
if (fenced) {
|
|
322
|
-
a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation, knowledgeDir, projectReads, subagents: gated, codexVersion }));
|
|
323
|
+
a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation, knowledgeDir, projectReads, subagents: gated, codexVersion, crossCliChild }));
|
|
323
324
|
} else {
|
|
324
325
|
// The daemon's build posture, mapped: SAFE keeps writes inside the
|
|
325
326
|
// workspace, the default lets the agent run its own tests and git
|
|
@@ -334,7 +335,9 @@ export const CODEX_RUNTIME = {
|
|
|
334
335
|
if (writableDirs.length) a.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(writableDirs)}`);
|
|
335
336
|
a.push('-c', `sandbox_mode="${SAFE ? 'workspace-write' : 'danger-full-access'}"`);
|
|
336
337
|
if (cardTurn) a.push('-c', 'approval_policy="never"', ...(agentTools?.codexApprovals ?? []));
|
|
337
|
-
|
|
338
|
+
// A subagent on another CLI's card has none of its own (crossCliPosture.mjs).
|
|
339
|
+
a.push(...(crossCliChild ? [...noSubagents(codexVersion), ...(SAFE ? CROSS_CLI_TMP_FENCE : [])]
|
|
340
|
+
: cardTurn && profile === 'build' && codexSubagentsSafe(codexVersion) ? CARD_SUBAGENTS : noSubagents(codexVersion)));
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
/**
|
|
@@ -521,9 +524,12 @@ const CODEX_FENCES = {
|
|
|
521
524
|
// An orchestrated research card's children (2026-10-19) inherit this same
|
|
522
525
|
// permission profile, approval policy and cwd (0.160.0 child_config.rs);
|
|
523
526
|
// the gate refuses their writes.
|
|
524
|
-
research: ({ researchIsolation, subagents = false, codexVersion }) => {
|
|
527
|
+
research: ({ researchIsolation, subagents = false, codexVersion, crossCliChild = false }) => {
|
|
525
528
|
if (!researchIsolation) throw new Error('Codex research requires its isolated input copy.');
|
|
526
|
-
|
|
529
|
+
const fence = researchFence(researchIsolation);
|
|
530
|
+
// A child returns findings to its lead: even the artifact/scratch roots read only.
|
|
531
|
+
return [...(crossCliChild ? fence.map(a => a.startsWith('permissions.flowviantresearch.filesystem=') ? a.replaceAll('="write"', '="read"') : a) : fence),
|
|
532
|
+
...fencedTail({ web: true, imageGeneration: !crossCliChild, subagents: !crossCliChild && subagents, codexVersion })];
|
|
527
533
|
},
|
|
528
534
|
};
|
|
529
535
|
/** The fenced profile names — for the test that holds both tables to one set. */
|