flowviant 0.125.5 → 0.125.7
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 +4 -5
- package/bin/lib/agentInternalWait.mjs +1 -5
- package/bin/lib/agentPlan.mjs +13 -163
- package/bin/lib/agentRepositories.mjs +3 -1
- package/bin/lib/claudeInput.mjs +89 -10
- package/bin/lib/claudeMachineMcp.mjs +85 -0
- package/bin/lib/claudePersonal.mjs +6 -57
- package/bin/lib/claudePosture.mjs +6 -15
- package/bin/lib/claudeSettingsArg.mjs +67 -0
- package/bin/lib/claudeStream.mjs +30 -1
- package/bin/lib/claudeSubagentProgress.mjs +12 -28
- package/bin/lib/claudeSubagents.mjs +4 -2
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexFencedMount.mjs +275 -0
- package/bin/lib/codexPersonal.mjs +1 -265
- package/bin/lib/codexProjectRead.mjs +5 -4
- package/bin/lib/codexResearch.mjs +9 -3
- package/bin/lib/codexSubagentEvents.mjs +11 -7
- package/bin/lib/codexSubagents.mjs +55 -52
- package/bin/lib/codexToml.mjs +328 -0
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/conversationIds.mjs +10 -0
- package/bin/lib/fleet.mjs +2 -2
- package/bin/lib/git.mjs +32 -0
- package/bin/lib/heldTool.mjs +49 -0
- package/bin/lib/hooks/mcpAllow.mjs +11 -5
- package/bin/lib/hooks/orchestrator.mjs +20 -10
- package/bin/lib/intakeDraft.mjs +1 -1
- package/bin/lib/jsonlTail.mjs +52 -0
- package/bin/lib/mcpInventory.mjs +11 -6
- package/bin/lib/mcpReasons.mjs +26 -0
- package/bin/lib/mcpReportBounds.mjs +10 -0
- package/bin/lib/mcpStdio.mjs +35 -0
- package/bin/lib/mergeBatch.mjs +6 -24
- package/bin/lib/mergeOrder.mjs +18 -0
- package/bin/lib/modelJson.mjs +173 -0
- package/bin/lib/nativeAgentTools.mjs +72 -0
- package/bin/lib/orchestrator.mjs +221 -133
- package/bin/lib/orchestratorClient.mjs +17 -21
- package/bin/lib/orchestratorHooks.mjs +21 -4
- package/bin/lib/orchestratorNative.mjs +58 -0
- package/bin/lib/orchestratorPrompt.mjs +7 -4
- package/bin/lib/orchestratorProtocol.mjs +25 -3
- package/bin/lib/postureTools.mjs +58 -13
- package/bin/lib/prWorkflow.mjs +13 -2
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectRepositories.mjs +2 -2
- package/bin/lib/projectToolRuntimes.mjs +92 -245
- package/bin/lib/projectTools.mjs +60 -19
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- package/bin/lib/repositoryDelivery.mjs +6 -4
- package/bin/lib/repositoryMerge.mjs +8 -12
- package/bin/lib/repositoryPlaces.mjs +2 -2
- package/bin/lib/repositoryPrompts.mjs +42 -0
- package/bin/lib/repositoryTool.mjs +9 -32
- package/bin/lib/researchUse.mjs +39 -0
- package/bin/lib/resumeLost.mjs +21 -1
- package/bin/lib/resumeMarker.mjs +43 -0
- package/bin/lib/runTurn.mjs +55 -52
- package/bin/lib/runtimeAntigravity.mjs +1 -1
- package/bin/lib/runtimeCapabilities.mjs +13 -18
- package/bin/lib/runtimeClaude.mjs +68 -24
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeEvents.mjs +13 -2
- package/bin/lib/runtimeOfferings.mjs +12 -5
- package/bin/lib/runtimes.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- package/bin/lib/subagentBounds.mjs +20 -0
- package/bin/lib/trace.mjs +30 -5
- package/bin/lib/turnDeclaration.mjs +1 -1
- package/bin/lib/turnDiagnostics.mjs +9 -12
- package/bin/lib/turnReceipts.mjs +64 -0
- package/bin/lib/turnSubagents.mjs +91 -0
- package/bin/lib/turnUsage.mjs +9 -6
- package/bin/lib/update.mjs +18 -4
- package/bin/lib/versionOrder.mjs +11 -1
- package/bin/lib/work.mjs +9 -9
- package/bin/lib/workAgentMerges.mjs +5 -7
- package/bin/lib/workAgentPrecheck.mjs +5 -5
- package/bin/lib/workAgentRecovery.mjs +2 -1
- package/bin/lib/workAgentResume.mjs +121 -0
- package/bin/lib/workAgentTools.mjs +111 -0
- package/bin/lib/workAgentTurnExecution.mjs +82 -246
- package/bin/lib/workAgentTurnOutcome.mjs +30 -3
- package/bin/lib/workAgentTurnReports.mjs +16 -0
- package/bin/lib/workAgentTurnShape.mjs +90 -0
- package/bin/lib/workAgentTurns.mjs +28 -12
- package/bin/lib/workArtifactReconcile.mjs +3 -2
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workMergeBatch.mjs +7 -5
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlaceReports.mjs +3 -3
- package/bin/lib/workPlaces.mjs +14 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionRuntime.mjs +2 -5
- package/bin/lib/workSessionTurns.mjs +68 -80
- package/bin/lib/workShip.mjs +8 -18
- package/bin/lib/workTurnShape.mjs +6 -4
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
package/bin/lib/runTurn.mjs
CHANGED
|
@@ -16,7 +16,6 @@ import { spawn } from 'node:child_process';
|
|
|
16
16
|
import { runtimeById } from './runtimes.mjs';
|
|
17
17
|
import { openBrowserHome, unattendedEnv } from './noWindowEnv.mjs';
|
|
18
18
|
import { resolveTurnProfile } from './turnProfile.mjs';
|
|
19
|
-
import { claudePermFor } from './claudePosture.mjs';
|
|
20
19
|
import { handleStreamLine } from './claudeStream.mjs';
|
|
21
20
|
import { never, workLanesStopped } from './standDownGate.mjs';
|
|
22
21
|
import { learnCodexLimits, recordClaudeRateLimit } from './runtimeLimits.mjs';
|
|
@@ -25,11 +24,8 @@ import { codexTurnModel } from './turnModel.mjs';
|
|
|
25
24
|
import { printableText } from './printable.mjs';
|
|
26
25
|
import { startTurnDiagnostics } from './turnDiagnostics.mjs';
|
|
27
26
|
import { createClaudeInput } from './claudeInput.mjs';
|
|
28
|
-
import { createClaudeSubagents } from './claudeSubagents.mjs';
|
|
29
|
-
import { createCodexSubagentReader } from './codexSubagents.mjs';
|
|
30
27
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
31
|
-
import {
|
|
32
|
-
import { codexSpawnResult } from './codexSubagentEvents.mjs';
|
|
28
|
+
import { createTurnSubagents } from './turnSubagents.mjs';
|
|
33
29
|
|
|
34
30
|
/** How long a Codex turn's answer may wait on its rollout's model line. */
|
|
35
31
|
const CODEX_MODEL_READ_MS = 1_500;
|
|
@@ -71,6 +67,10 @@ export function cliEnv(mcpEnv, { display = false, browserHome = null } = {}) {
|
|
|
71
67
|
// the console (a legible stream of `read …`, `grep …`, `+ node …`), assistant
|
|
72
68
|
// text is folded into the returned string for sentinel detection, and each
|
|
73
69
|
// activity is handed to `onActivity` so the caller can forward progress.
|
|
70
|
+
/** Linux's per-argument ceiling (`MAX_ARG_STRLEN`, 32 pages): one argv
|
|
71
|
+
* string past it is refused at launch with E2BIG. */
|
|
72
|
+
const MAX_ARG_BYTES = 128 * 1024;
|
|
73
|
+
|
|
74
74
|
export function runTurn(opts) {
|
|
75
75
|
// A Codex write-up runs over an isolated copy of its worktree
|
|
76
76
|
// (codexResearch.mjs). After the teardown it copies nothing either: the
|
|
@@ -172,17 +172,22 @@ function runPlainTurn(opts) {
|
|
|
172
172
|
// than sloppy. `profile` is the NAME of the posture — a promise about what
|
|
173
173
|
// must be impossible during the turn — and every runtime expresses it in its
|
|
174
174
|
// own vocabulary: Claude as an `--allowedTools` verb list, Codex as a kernel
|
|
175
|
-
// sandbox mode plus feature toggles. `perm` is
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
175
|
+
// sandbox mode plus feature toggles. `perm` is the runtime's own expression
|
|
176
|
+
// of the profile, asked of its row by the SAME name (`rt.perm`; Claude's is
|
|
177
|
+
// `claudePermFor`), so the two cannot disagree about which posture a turn
|
|
178
|
+
// is running under; a runtime that expresses postures another way has no
|
|
179
|
+
// hook. There is no precedence chain left to get wrong: the caller named
|
|
180
|
+
// one profile (turnProfile.mjs).
|
|
179
181
|
const stdinText = rt.stdin?.({ prompt, system, profile: turnProfile.adapterProfile }) ?? null;
|
|
180
182
|
const args = rt.args({
|
|
181
183
|
cardTurn: opts.cardTurn === true,
|
|
182
184
|
orchestration: opts.orchestration === true,
|
|
183
|
-
|
|
185
|
+
// The lane's own measurement when it decided to orchestrate, so the
|
|
186
|
+
// hooks and the controller cannot disagree (workAgentTurnExecution.mjs).
|
|
187
|
+
codexVersion: opts.codexVersion ?? measuredRuntimeVersion('codex')?.version,
|
|
188
|
+
claudeVersion: opts.claudeVersion ?? measuredRuntimeVersion('claude')?.version ?? null,
|
|
184
189
|
|
|
185
|
-
streamInput: stdinText !== null || (rt.
|
|
190
|
+
streamInput: stdinText !== null || (rt.liveInput === true && Boolean(opts.liveMessages)),
|
|
186
191
|
writableDirs: opts.writableDirs,
|
|
187
192
|
personal,
|
|
188
193
|
researchIsolation: opts.researchIsolation,
|
|
@@ -229,7 +234,7 @@ function runPlainTurn(opts) {
|
|
|
229
234
|
// base `.mcp.json` servers the caller mounted, each allowed by name on a
|
|
230
235
|
// profile that mounts them (`projectMcp`, turnProfile.mjs) — never on
|
|
231
236
|
// one that does not, whatever a caller passes.
|
|
232
|
-
perm:
|
|
237
|
+
perm: typeof rt.perm === 'function' ? rt.perm(turnProfile.name, knowledgeDir, vaultDir, turnProfile.projectMcp ? (opts.projectMcpServers ?? []) : []) : [],
|
|
233
238
|
// …AND THE MACHINE'S OWN MCP SERVERS (2026-10-11): the `.mcp.json`
|
|
234
239
|
// server names an agent lane disables beside them, on a profile that
|
|
235
240
|
// mounts project servers — never on one that does not.
|
|
@@ -299,12 +304,18 @@ function runPlainTurn(opts) {
|
|
|
299
304
|
display: display === true,
|
|
300
305
|
browserHome: browser?.dir,
|
|
301
306
|
});
|
|
307
|
+
// Claude Code 2.1.302 stops background agents 10 minutes after the
|
|
308
|
+
// parent goes idle with stdin closed, with no final parent turn
|
|
309
|
+
// (measured; its background shells are outside that ceiling). An
|
|
310
|
+
// orchestrated card waits for its children instead: the lane's
|
|
311
|
+
// Interrupt and the turn's expiry bound it.
|
|
312
|
+
if (rt.id === 'claude' && opts.orchestration === true) childEnv.CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS = '0';
|
|
302
313
|
finishDiagnostics = startTurnDiagnostics({ runtime, bin: rt.bin, env: childEnv,
|
|
303
314
|
cwd, args, stdinBytes: transport.text === null ? 0 : Buffer.byteLength(transport.text),
|
|
304
315
|
profile: turnProfile.name, planId: opts.diagnosticPlanId });
|
|
305
316
|
child = spawn(rt.bin, args, {
|
|
306
317
|
cwd,
|
|
307
|
-
stdio: [transport.text !== null || (rt.
|
|
318
|
+
stdio: [transport.text !== null || (rt.liveInput === true && opts.liveMessages) ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
308
319
|
/**
|
|
309
320
|
* ITS OWN PROCESS GROUP, so what the agent starts stays attributable.
|
|
310
321
|
*
|
|
@@ -340,6 +351,20 @@ function runPlainTurn(opts) {
|
|
|
340
351
|
// own browser home.
|
|
341
352
|
releaseArgs();
|
|
342
353
|
browser?.cleanup();
|
|
354
|
+
// THE COMMAND LINE'S OWN LIMIT, IN WORDS (2026-10-19). A runtime whose
|
|
355
|
+
// prompt still rides argv (Antigravity's `-p`) hits the OS's per-argument
|
|
356
|
+
// ceiling on a long message, and the turn failed as "spawn E2BIG". The
|
|
357
|
+
// refusal is the system's and stays; only its words are new.
|
|
358
|
+
// Said only when the message IS the oversized argument: E2BIG also
|
|
359
|
+
// answers an environment too large for the launch, which is not the
|
|
360
|
+
// person's message and keeps the system's own words.
|
|
361
|
+
const carrying = error?.code === 'E2BIG' && typeof prompt === 'string' && prompt
|
|
362
|
+
? args.find((a) => typeof a === 'string' && a.includes(prompt))
|
|
363
|
+
: undefined;
|
|
364
|
+
const bytes = carrying ? Buffer.byteLength(carrying) : 0;
|
|
365
|
+
if (bytes > MAX_ARG_BYTES) {
|
|
366
|
+
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' });
|
|
367
|
+
}
|
|
343
368
|
throw error;
|
|
344
369
|
}
|
|
345
370
|
if (browser) {
|
|
@@ -352,7 +377,7 @@ function runPlainTurn(opts) {
|
|
|
352
377
|
const spawnedAt = Date.now();
|
|
353
378
|
let out = '';
|
|
354
379
|
let stdinError = null;
|
|
355
|
-
const input = rt.
|
|
380
|
+
const input = rt.liveInput === true && opts.liveMessages ? createClaudeInput(child, { onTaken: opts.liveMessages.onTaken, onContext: opts.onContext ?? null }) : null;
|
|
356
381
|
const pfx = label ? `${label} ` : '';
|
|
357
382
|
/**
|
|
358
383
|
* THE ONE SINK FOR WHAT A CLI OR A MODEL WROTE (2026-10-10, the audit's
|
|
@@ -397,25 +422,11 @@ function runPlainTurn(opts) {
|
|
|
397
422
|
recordClaudeRateLimit(info);
|
|
398
423
|
onRateLimit?.(info);
|
|
399
424
|
};
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
opts.onSubagent?.(update);
|
|
405
|
-
};
|
|
406
|
-
const subagents = createClaudeSubagents(reportSubagent);
|
|
407
|
-
const claudeProgress = rt.id === 'claude' && opts.onSubagent ? createClaudeSubagentProgress({
|
|
408
|
-
cwd, since: spawnedAt, env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
409
|
-
onLine: line => handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText,
|
|
410
|
-
answerFromResult: true, subagents }),
|
|
411
|
-
}) : null;
|
|
412
|
-
const codexAgents = rt.id === 'codex' && opts.onSubagent ? createCodexSubagentReader({
|
|
413
|
-
since: spawnedAt, env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
414
|
-
onSubagent: reportSubagent, onActivity, onToolEvent,
|
|
415
|
-
onSpawnResult: opts.onSpawnResult,
|
|
416
|
-
}) : null;
|
|
425
|
+
// The native children this CLI starts, observed (turnSubagents.mjs).
|
|
426
|
+
const natives = createTurnSubagents({ runtime: rt.id, cwd, since: spawnedAt, env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
427
|
+
owned: opts.orchestration === true, onSubagent: opts.onSubagent, onSpawnResult: opts.onSpawnResult, resumeThreadId,
|
|
428
|
+
sinks: { emit, onActivity, onToolEvent, appendText } });
|
|
417
429
|
let codexThread = rt.id === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
|
|
418
|
-
if (codexThread) codexAgents?.thread(codexThread);
|
|
419
430
|
/**
|
|
420
431
|
* THE MODEL THIS SPAWN RAN ON, ONCE (2026-09-29, turnModel.mjs). Claude
|
|
421
432
|
* names it in its init event and again on every reply; the first valid
|
|
@@ -439,11 +450,13 @@ function runPlainTurn(opts) {
|
|
|
439
450
|
*/
|
|
440
451
|
const harvest = initHarvestFor(rt, args);
|
|
441
452
|
const onInitAll = (i) => {
|
|
442
|
-
|
|
453
|
+
natives.session(i?.sessionId);
|
|
443
454
|
if (harvest.skills) recordSkills(i?.skills);
|
|
444
455
|
if (harvest.mcp) recordMcpServers(i?.mcpServers, i?.tools);
|
|
445
456
|
onInit?.(i);
|
|
446
457
|
};
|
|
458
|
+
/** This turn's Claude task list, as the plan card reads it (claudeStream.mjs). */
|
|
459
|
+
const taskPlan = new Map();
|
|
447
460
|
/** One line of the child's stdout, in whichever dialect it speaks. */
|
|
448
461
|
let cliFailed = false;
|
|
449
462
|
const onLine = (line) => {
|
|
@@ -465,23 +478,23 @@ function runPlainTurn(opts) {
|
|
|
465
478
|
}
|
|
466
479
|
} catch { /* ordinary non-JSON CLI output */ }
|
|
467
480
|
if (!rt.parse)
|
|
468
|
-
return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit: onInitAll, onUsage, onRateLimit: onRate, onModel: onRanOn, subagents });
|
|
481
|
+
return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit: onInitAll, onUsage, onRateLimit: onRate, onModel: onRanOn, subagents: natives.collector, tasks: taskPlan });
|
|
469
482
|
const ev = rt.parse(line, cwd);
|
|
470
483
|
if (!ev) return;
|
|
471
484
|
// The conversation id, when the runtime announces one (codex's
|
|
472
485
|
// thread.started). Purely additive: callers that pass no onThreadId —
|
|
473
486
|
// every dispatch path — see zero behavior change.
|
|
474
487
|
if (ev.threadId) {
|
|
475
|
-
if (rt.id === 'codex') { codexThread = ev.threadId;
|
|
488
|
+
if (rt.id === 'codex') { codexThread = ev.threadId; natives.thread(ev.threadId); }
|
|
476
489
|
onThreadId?.(ev.threadId);
|
|
477
490
|
}
|
|
478
491
|
// The runtime's own token count (codex's `turn.completed`) — the same
|
|
479
492
|
// `onUsage` Claude's result event feeds, so a caller charges a turn
|
|
480
493
|
// identically whichever CLI ran it.
|
|
481
|
-
for (const update of ev.subagents ?? [])
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
494
|
+
for (const update of ev.subagents ?? []) natives.update(update);
|
|
495
|
+
// A receipt the controller cannot take never ends this line's handling,
|
|
496
|
+
// as when the lane parsed the line again itself (2026-10-18).
|
|
497
|
+
if (ev.spawnResult) try { opts.onSpawnResult?.(ev.spawnResult); } catch { /* the controller's own failure */ }
|
|
485
498
|
if (ev.toolEvent) onToolEvent?.(...ev.toolEvent);
|
|
486
499
|
if (ev.usage) onUsage?.(ev.usage);
|
|
487
500
|
// One agent MESSAGE, alone — see parseCodexLine's `answer`. Last call
|
|
@@ -529,8 +542,7 @@ function runPlainTurn(opts) {
|
|
|
529
542
|
*/
|
|
530
543
|
const onSpawnError = (e) => {
|
|
531
544
|
input?.close();
|
|
532
|
-
|
|
533
|
-
void claudeProgress?.stop();
|
|
545
|
+
natives.abandon();
|
|
534
546
|
if (opts.liveMessages) void opts.liveMessages.stop();
|
|
535
547
|
finishDiagnostics({ pid: child.pid, spawnError: typeof e.code === 'string' ? e.code : 'SPAWN_ERROR' });
|
|
536
548
|
releaseArgs();
|
|
@@ -551,17 +563,8 @@ function runPlainTurn(opts) {
|
|
|
551
563
|
finishDiagnostics({ code, signal, pid: child.pid, output: out, cliFailed, inputError: stdinError?.code });
|
|
552
564
|
const handBack = async () => {
|
|
553
565
|
if (opts.liveMessages) await opts.liveMessages.stop();
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
let progressTimer;
|
|
557
|
-
try {
|
|
558
|
-
await Promise.race([
|
|
559
|
-
Promise.all([codexAgents?.stop(), claudeProgress?.stop()]),
|
|
560
|
-
new Promise(resolve => { progressTimer = setTimeout(resolve, CODEX_MODEL_READ_MS); }),
|
|
561
|
-
]);
|
|
562
|
-
} finally { clearTimeout(progressTimer); }
|
|
563
|
-
for (const [id, status] of agentStates)
|
|
564
|
-
if (status === 'running' || status === 'waiting') reportSubagent({ id, status: 'stopped' });
|
|
566
|
+
await natives.drain(CODEX_MODEL_READ_MS);
|
|
567
|
+
natives.sweep();
|
|
565
568
|
if (stdinError) {
|
|
566
569
|
if (!workLanesStopped()) rejectTurn(stdinError);
|
|
567
570
|
return;
|
|
@@ -103,7 +103,7 @@ export const ANTIGRAVITY_RUNTIME = {
|
|
|
103
103
|
bin: 'agy',
|
|
104
104
|
install: 'see antigravity.google/docs/cli',
|
|
105
105
|
login: 'agy',
|
|
106
|
-
|
|
106
|
+
liveInput: false,
|
|
107
107
|
/** None, because it cannot reach the MCP server at all — see `blocked`. */
|
|
108
108
|
/**
|
|
109
109
|
* WIKI AND CONSULT, BUT NOT BUILD — and the split is the whole point of
|
|
@@ -19,6 +19,8 @@ import { RUNTIMES } from './runtimes.mjs';
|
|
|
19
19
|
import { unattendedEnv } from './noWindowEnv.mjs';
|
|
20
20
|
import { claudeProjectDir } from './claudeSessions.mjs';
|
|
21
21
|
import { CLAUDE_SESSION_RE } from './conversationIds.mjs';
|
|
22
|
+
import { MAX_MCP_SERVERS, MCP_NAME_MAX, MCP_STATUSES } from './mcpReportBounds.mjs';
|
|
23
|
+
import { killGroup } from './processGroup.mjs';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* WHAT THE CLI SAID IT CAN BE ASKED FOR BY NAME — the machine's skills.
|
|
@@ -105,13 +107,10 @@ export function knownSkills() {
|
|
|
105
107
|
*/
|
|
106
108
|
let mcpServersCache = null;
|
|
107
109
|
|
|
108
|
-
export
|
|
109
|
-
/** The server's own bounds (shared `MAX_MACHINE_MCP_SERVERS` and
|
|
110
|
-
* `MCP_SERVER_NAME_MAX`); scripts/check-app-parity.mjs holds them equal. */
|
|
111
|
-
export const MAX_MCP_SERVERS = 40;
|
|
112
|
-
export const MCP_NAME_MAX = 60;
|
|
110
|
+
export { MCP_STATUSES, MAX_MCP_SERVERS, MCP_NAME_MAX };
|
|
113
111
|
/** The name charset the server's Codex report parser admits
|
|
114
|
-
* (runtimeOfferings.ts `
|
|
112
|
+
* (runtimeOfferings.ts `CODEX_REPORT_NAME_RE`, held equal by
|
|
113
|
+
* check-app-parity rule 16) — Claude's report has none, and its connector
|
|
115
114
|
* names carry a space (`claude.ai Gmail`). */
|
|
116
115
|
export const CODEX_MCP_NAME = /^[A-Za-z0-9_.:-]+$/;
|
|
117
116
|
|
|
@@ -169,9 +168,14 @@ export function claudeMcpInventoryOf(list, tools) {
|
|
|
169
168
|
for (const e of list) {
|
|
170
169
|
if (!e || typeof e !== 'object' || typeof e.name !== 'string') continue;
|
|
171
170
|
const name = e.name.trim().slice(0, MCP_NAME_MAX).trim();
|
|
172
|
-
|
|
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;
|
|
173
177
|
const status = MCP_STATUSES.has(e.status) ? e.status : 'other';
|
|
174
|
-
seen.set(name, { name, status, scope
|
|
178
|
+
seen.set(name, { name, status, scope, prefix: mcpToolPrefix(e.name.trim()), tools: status === 'connected' ? [] : null });
|
|
175
179
|
}
|
|
176
180
|
// The longest prefix wins, so `a_b` never claims `a`'s tools or the reverse.
|
|
177
181
|
const byPrefix = [...seen.values()].sort((a, b) => b.prefix.length - a.prefix.length);
|
|
@@ -470,16 +474,7 @@ function runInitProbe(cwd, extraArgs, spawnChild, done = () => {}) {
|
|
|
470
474
|
settled = true;
|
|
471
475
|
clearTimeout(timer);
|
|
472
476
|
// The whole group: what the CLI started goes with it.
|
|
473
|
-
|
|
474
|
-
if (child.pid) process.kill(-child.pid, 'SIGKILL');
|
|
475
|
-
else child.kill('SIGKILL');
|
|
476
|
-
} catch {
|
|
477
|
-
try {
|
|
478
|
-
child.kill('SIGKILL');
|
|
479
|
-
} catch {
|
|
480
|
-
/* already gone */
|
|
481
|
-
}
|
|
482
|
-
}
|
|
477
|
+
killGroup(child, 'SIGKILL');
|
|
483
478
|
// AFTER the kill, and on a delay: the transcript is the CHILD's file, so
|
|
484
479
|
// deleting it while the child still lives races a recreate.
|
|
485
480
|
removeTranscriptSoon(cwd, sessionId);
|
|
@@ -14,13 +14,20 @@ import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { MODEL, USER_AGENT } from './config.mjs';
|
|
17
|
-
import { NO_SETTING_SOURCES,
|
|
18
|
-
import {
|
|
17
|
+
import { NO_SETTING_SOURCES, personalClaudeSettings, releaseTurnSettings, withPersonalSettings, writeTurnSettings } from './claudePersonal.mjs';
|
|
18
|
+
import { machineMcpServers } from './claudeMachineMcp.mjs';
|
|
19
|
+
import { claudePermFor, withMachineMcp } from './claudePosture.mjs';
|
|
20
|
+
import { withSettings } from './claudeSettingsArg.mjs';
|
|
19
21
|
import { orchestratorHooks } from './orchestratorHooks.mjs';
|
|
22
|
+
import { versionAtLeast } from './versionOrder.mjs';
|
|
20
23
|
|
|
21
|
-
/** The
|
|
22
|
-
*
|
|
23
|
-
const
|
|
24
|
+
/** The release the agent lane first measured `--system-prompt-snapshot` on
|
|
25
|
+
* (decisions.md, 2026-09-24). */
|
|
26
|
+
export const SYSTEM_PROMPT_SNAPSHOT_MIN = '2.1.281';
|
|
27
|
+
/** `--version` prints "2.1.303 (Claude Code)"; the first word is the version. */
|
|
28
|
+
export function claudeSnapshotFlagKnown(version) {
|
|
29
|
+
return versionAtLeast(String(version ?? '').trim().split(/\s+/)[0], SYSTEM_PROMPT_SNAPSHOT_MIN);
|
|
30
|
+
}
|
|
24
31
|
|
|
25
32
|
function claudeMcp(token, mcpUrl) {
|
|
26
33
|
const dir = mkdtempSync(join(tmpdir(), 'flowviant-mcp-'));
|
|
@@ -48,10 +55,10 @@ export const CLAUDE_RUNTIME = {
|
|
|
48
55
|
bin: 'claude',
|
|
49
56
|
install: 'npm i -g @anthropic-ai/claude-code',
|
|
50
57
|
login: 'claude',
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
58
|
+
/** Takes follow-up messages on stdin mid-turn (claudeInput.mjs,
|
|
59
|
+
* workLiveMessages.mjs); a runtime without it gets its follow-ups as the
|
|
60
|
+
* next turn, and its delivery is reported taken at spawn. */
|
|
61
|
+
liveInput: true,
|
|
55
62
|
/**
|
|
56
63
|
* Every profile, because every profile is DEFINED in its vocabulary: the
|
|
57
64
|
* four `--allowedTools` lists in claudePosture.mjs are what "build", "wiki",
|
|
@@ -92,17 +99,30 @@ export const CLAUDE_RUNTIME = {
|
|
|
92
99
|
* RETIRED_POSTURE_KEYS), so no spelling of it reads as one.
|
|
93
100
|
* adopt: 'fork' — a terminal conversation is forked into the tab's
|
|
94
101
|
* worktree (`--resume <id> --fork-session`).
|
|
102
|
+
* liveInput (above) — a follow-up reaches the running CLI on stdin
|
|
103
|
+
* (2026-10-18, the SOLID pass: the tab, agent and runTurn lanes spelled
|
|
104
|
+
* it `rt.id === 'claude'`, and the row's old `live` flag was read by
|
|
105
|
+
* nothing).
|
|
95
106
|
*/
|
|
96
107
|
tab: 'mcp',
|
|
97
108
|
planTurns: true,
|
|
98
109
|
adopt: 'fork',
|
|
99
110
|
mcp: claudeMcp,
|
|
111
|
+
/**
|
|
112
|
+
* THE POSTURE AS CLAUDE'S `--allowedTools` LIST (2026-10-18, the SOLID
|
|
113
|
+
* pass), looked up by the turn profile's name (claudePosture.mjs
|
|
114
|
+
* `claudePermFor`). runTurn asks the row for it, so it knows no runtime's
|
|
115
|
+
* list: a runtime that expresses postures another way — Codex's sandbox
|
|
116
|
+
* modes, agy's flags — has no hook, and Claude's table needs no stub for a
|
|
117
|
+
* posture only another CLI runs.
|
|
118
|
+
*/
|
|
119
|
+
perm: claudePermFor,
|
|
100
120
|
/**
|
|
101
121
|
* Claude takes the operating contract as a real system prompt, which is the
|
|
102
122
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
103
123
|
* above the conversation rather than inside it.
|
|
104
124
|
*/
|
|
105
|
-
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 }) {
|
|
125
|
+
args({ writableDirs = [], prompt, system, model, effort, resume, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, vaultDir, agentTools, personalSettings, machineMcp, baseMcp = [], mcpCheckout = null, streamInput = false, orchestration = false, claudeVersion = null }) {
|
|
106
126
|
// THE CARTOGRAPHER'S FENCE NAMES ITS VAULT (2026-10-10): its one write rule
|
|
107
127
|
// is the vault's absolute path (claudePosture.mjs `wikiPermFor`), so a turn
|
|
108
128
|
// with no vault, or one no rule can name, has no list — refused here in
|
|
@@ -170,7 +190,6 @@ export const CLAUDE_RUNTIME = {
|
|
|
170
190
|
* agent renders the CURRENT base snapshot, not the first turn's cache.
|
|
171
191
|
*/
|
|
172
192
|
const fenced = profile !== 'build';
|
|
173
|
-
if (orchestration && !fenced) a.push('--settings', JSON.stringify({ hooks: orchestratorHooks(), disableAllHooks: false }));
|
|
174
193
|
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
175
194
|
// A FENCED POSTURE NAMES ITS OWN MODE (2026-10-10). Its lists fence only
|
|
176
195
|
// under the CLI's `default` mode, where a tool the list does not name is
|
|
@@ -181,22 +200,35 @@ export const CLAUDE_RUNTIME = {
|
|
|
181
200
|
// states `default` itself, unless its own list names a mode (plan-mode).
|
|
182
201
|
if (fenced && !perm.includes('--permission-mode')) a.push('--permission-mode', 'default');
|
|
183
202
|
else if (agentTools) a.push('--setting-sources', 'user');
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
// EVERY TURN RENDERS ITS OWN SYSTEM PROMPT (2026-10-19). By default the
|
|
204
|
+
// CLI records a conversation's first system prompt and resends that record
|
|
205
|
+
// on every resume "even when a later launch passes different text" —
|
|
206
|
+
// measured offline on 2.1.303: a resumed turn appended BETA and the model
|
|
207
|
+
// was sent the first turn's ALPHA. A tab's appended prompt moves between
|
|
208
|
+
// turns (plan mode on and off, the knowledge and artifact paragraphs), so
|
|
209
|
+
// a tab switched out of plan mode kept being told to change nothing. An
|
|
210
|
+
// agent turn always passed this (its current base instructions). A CLI too
|
|
211
|
+
// old to name the flag refuses it, so an unmeasured version keeps the old
|
|
212
|
+
// argv.
|
|
213
|
+
if (agentTools || claudeSnapshotFlagKnown(claudeVersion)) a.push('--system-prompt-snapshot', 'off');
|
|
214
|
+
if (agentTools?.pluginDir) a.push('--plugin-dir', agentTools.pluginDir);
|
|
188
215
|
// WHICH MCP SERVERS A TURN MOUNTS. An agent's code, write-up and
|
|
189
216
|
// mockup/3D-model/deck turns (2026-10-11, the owner) mount this machine's
|
|
190
217
|
// own — its user and local servers and the account's claude.ai connectors —
|
|
191
218
|
// beside the base branch's `.mcp.json` copy the caller passes, so they are
|
|
192
219
|
// not strict; `machineMcp` names every `.mcp.json` server from the
|
|
193
220
|
// worktree up, which `withMachineMcp` disables so a worktree edit never
|
|
194
|
-
// mounts before review (claudePosture.mjs). Every
|
|
195
|
-
//
|
|
196
|
-
//
|
|
221
|
+
// mounts before review (claudePosture.mjs). Every fenced posture but plan
|
|
222
|
+
// stays strict unless the lane mounted the machine's servers — consult,
|
|
223
|
+
// wiki, and a design/research turn no lane mounted for: it never discovers
|
|
224
|
+
// a .mcp.json, nor loads the machine's servers. Keyed on the rule, not a
|
|
225
|
+
// list of names, so a posture added later is strict unless it mounts.
|
|
197
226
|
// The plan profile keeps its own principal.
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
//
|
|
228
|
+
// runTurn hands machineMcp only on a profile whose turn profile says
|
|
229
|
+
// projectMcp (runTurn.mjs), the one rule.
|
|
230
|
+
const machine = Array.isArray(machineMcp);
|
|
231
|
+
if (fenced && profile !== 'plan' && !machine && !mcp.includes('--strict-mcp-config')) a.push('--strict-mcp-config');
|
|
200
232
|
a.push(...mcp, ...resultSchemaArgs);
|
|
201
233
|
a.push('--model', model || MODEL);
|
|
202
234
|
if (effort) a.push('--effort', effort);
|
|
@@ -223,14 +255,26 @@ export const CLAUDE_RUNTIME = {
|
|
|
223
255
|
// leave the file with no turn to release it.
|
|
224
256
|
//
|
|
225
257
|
// …AND THE MACHINE'S SERVERS NO SOURCE OF THIS TURN READS (2026-10-17,
|
|
226
|
-
//
|
|
258
|
+
// claudeMachineMcp.mjs `machineMcpServers`): the user scope a fenced turn's
|
|
227
259
|
// empty source list drops, and the checkout's local scope every card turn
|
|
228
260
|
// missed. In a private file too — a server's env and headers are secrets —
|
|
229
261
|
// minus every name the base copy mounts, which speaks for the project.
|
|
230
|
-
const own = machine ? machineMcpServers({ user: fenced, checkout: mcpCheckout }) : {};
|
|
262
|
+
const own = (machine ? machineMcpServers({ user: fenced, checkout: mcpCheckout }) : null) ?? {};
|
|
231
263
|
for (const name of baseMcp) delete own[name];
|
|
232
|
-
|
|
233
|
-
|
|
264
|
+
// A file that cannot be written costs the card these servers, never the
|
|
265
|
+
// card itself: the turn runs with what its own sources load, and says so.
|
|
266
|
+
let ownArgs = [];
|
|
267
|
+
if (Object.keys(own).length) {
|
|
268
|
+
try {
|
|
269
|
+
ownArgs = ['--mcp-config', writeTurnSettings({ mcpServers: own })];
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.warn(`[agent] this machine's MCP servers are not mounted on this turn: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const mounted = machine ? withMachineMcp(perm, machineMcp) : perm;
|
|
275
|
+
// The orchestrator's hooks join that same `--settings`: the CLI applies
|
|
276
|
+
// only the last one it is handed (claudeSettingsArg.mjs `withSettings`).
|
|
277
|
+
const postured = orchestration && !fenced ? withSettings(mounted, { hooks: orchestratorHooks(), disableAllHooks: false }) : mounted;
|
|
234
278
|
let tail;
|
|
235
279
|
try {
|
|
236
280
|
tail = fenced ? withPersonalSettings(postured, personalSettings ?? personalClaudeSettings()) : postured;
|
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { ARTIFACT_DIR, FENCE_TMP_DIR } from './artifacts.mjs';
|
|
|
16
16
|
import { codexPersonalTurn } from './codexPersonal.mjs';
|
|
17
17
|
import { researchFence } from './codexResearch.mjs';
|
|
18
18
|
import { projectReadBoundary, projectReadFence } from './codexProjectRead.mjs';
|
|
19
|
-
import {
|
|
19
|
+
import { versionAtLeast } from './versionOrder.mjs';
|
|
20
20
|
import { codexOrchestratorArgs } from './orchestratorHooks.mjs';
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -27,7 +27,11 @@ import { codexOrchestratorArgs } from './orchestratorHooks.mjs';
|
|
|
27
27
|
* whose one tool carries no annotations (Flowviant's carry none): under the
|
|
28
28
|
* capture chat's read-only argv the call failed "MCP tool call requires
|
|
29
29
|
* approval, but approval policy is never" and never reached the server; under
|
|
30
|
-
* the build posture (workspace-write) the same call ran unasked
|
|
30
|
+
* the build posture (workspace-write) the same call ran unasked — no longer
|
|
31
|
+
* so on 0.163.0, where a workspace-write card turn refuses it the same way
|
|
32
|
+
* and only danger-full-access runs it (2026-10-17; a card approves the
|
|
33
|
+
* person's own servers by name, projectToolRuntimes.mjs `personalApprovals`).
|
|
34
|
+
* With
|
|
31
35
|
* `mcp_servers.flowviant.default_tools_approval_mode="approve"` beside the
|
|
32
36
|
* read-only argv it ran — and in the same turn a shell write still failed
|
|
33
37
|
* "Read-only file system" and an escalation still failed "approval policy is
|
|
@@ -88,8 +92,7 @@ export const NO_SUBAGENTS = Object.freeze(['-c', 'features.multi_agent=false', '
|
|
|
88
92
|
* and V2. Older/unmeasured binaries have no inheritance evidence here. */
|
|
89
93
|
export const CODEX_SUBAGENTS_MIN = '0.160.0';
|
|
90
94
|
export function codexSubagentsSafe(version) {
|
|
91
|
-
|
|
92
|
-
return order === 0 || order === 1;
|
|
95
|
+
return versionAtLeast(String(version ?? '').replace(/^codex-cli\s+/, ''), CODEX_SUBAGENTS_MIN);
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
/**
|
|
@@ -170,6 +173,9 @@ const codexWikiFence = (vaultDir) => [
|
|
|
170
173
|
];
|
|
171
174
|
|
|
172
175
|
function codexPrompt({ system, prompt, profile = 'build' }) {
|
|
176
|
+
// A resumed tab whose thread already holds the contract sends none
|
|
177
|
+
// (codexContract.mjs): the message alone.
|
|
178
|
+
if (!system) return prompt;
|
|
173
179
|
const readers = profile === 'consult' || profile === 'plan'
|
|
174
180
|
? '\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.'
|
|
175
181
|
: '';
|
|
@@ -183,7 +189,7 @@ export const CODEX_RUNTIME = {
|
|
|
183
189
|
bin: 'codex',
|
|
184
190
|
install: 'npm i -g @openai/codex',
|
|
185
191
|
login: 'codex login',
|
|
186
|
-
|
|
192
|
+
liveInput: false,
|
|
187
193
|
/**
|
|
188
194
|
* BUILD AND CONSULT. See `args()` below for how consult is expressed — the
|
|
189
195
|
* short version is that Codex keeps the promise at the kernel rather than in
|
|
@@ -296,7 +302,7 @@ export const CODEX_RUNTIME = {
|
|
|
296
302
|
// before it started. `-c sandbox_mode=` means the same on both.
|
|
297
303
|
if (writableDirs.length) a.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(writableDirs)}`);
|
|
298
304
|
a.push('-c', `sandbox_mode="${SAFE ? 'workspace-write' : 'danger-full-access'}"`);
|
|
299
|
-
if (cardTurn) a.push('-c', 'approval_policy="never"');
|
|
305
|
+
if (cardTurn) a.push('-c', 'approval_policy="never"', ...(agentTools?.codexApprovals ?? []));
|
|
300
306
|
a.push(...(cardTurn && profile === 'build' && codexSubagentsSafe(codexVersion)
|
|
301
307
|
? ['-c', 'features.multi_agent=true', '-c', 'features.goals=false']
|
|
302
308
|
: NO_SUBAGENTS));
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* own stream loop and calls `humanizeClaudeTool`/`toolEventOf` from it.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { codexCollabUpdates } from './codexSubagentEvents.mjs';
|
|
17
|
+
import { codexCollabUpdates, codexSpawnResult } from './codexSubagentEvents.mjs';
|
|
18
18
|
|
|
19
19
|
/** Truncate for a one-line activity label. */
|
|
20
20
|
const oneLine = (s, n = 140) =>
|
|
@@ -312,7 +312,10 @@ function humanizeCodexItem(item = {}, cwd = '') {
|
|
|
312
312
|
* threadId }` — `text` accumulates the agent's own words, because the wiki
|
|
313
313
|
* lane reads its sentinels (WIKI_DONE / REGROUND_DONE, claude.mjs
|
|
314
314
|
* `sawSentinel`) out of exactly that; `threadId` surfaces once, off the lifecycle event, for callers that
|
|
315
|
-
* need to resume THIS conversation later (runTurn's onThreadId).
|
|
315
|
+
* need to resume THIS conversation later (runTurn's onThreadId). `spawnResult`
|
|
316
|
+
* is a native spawn's receipt (codexSubagentEvents.mjs), relayed to the
|
|
317
|
+
* orchestrator; it rides any event this parser keeps, read off the same parse
|
|
318
|
+
* (2026-10-18, the SOLID pass: the spawn lane parsed every line a second time).
|
|
316
319
|
*/
|
|
317
320
|
export function parseCodexLine(line, cwd) {
|
|
318
321
|
let ev;
|
|
@@ -321,6 +324,14 @@ export function parseCodexLine(line, cwd) {
|
|
|
321
324
|
} catch {
|
|
322
325
|
return null; // not every line is JSON (warnings go to stderr, but be safe)
|
|
323
326
|
}
|
|
327
|
+
const parsed = codexEventOf(ev, cwd);
|
|
328
|
+
if (!parsed) return parsed;
|
|
329
|
+
const spawnResult = codexSpawnResult(ev.item);
|
|
330
|
+
return spawnResult ? { ...parsed, spawnResult } : parsed;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** One ThreadEvent, already parsed, in the daemon's vocabulary. */
|
|
334
|
+
function codexEventOf(ev, cwd) {
|
|
324
335
|
if (['item.started', 'item.updated', 'item.completed'].includes(ev.type) &&
|
|
325
336
|
(ev.item?.type ?? ev.item?.item_type) === 'collab_tool_call')
|
|
326
337
|
return { subagents: codexCollabUpdates(ev.item), activity: null, text: '' };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Local, bounded Codex readouts. No agent turn is started for these reports. */
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { accessSync, constants, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { basename, delimiter, isAbsolute, join } from 'node:path';
|
|
5
6
|
import { readBaseTools } from './projectTools.mjs';
|
|
6
7
|
import { mcpRefusal } from './projectToolRuntimes.mjs';
|
|
7
8
|
import { CODEX_MCP_NAME, normalizeMcpReport } from './runtimeCapabilities.mjs';
|
|
9
|
+
import { mcpCommandNotFoundReason, mcpEnvUnsetReason } from './mcpReasons.mjs';
|
|
8
10
|
import { unattendedEnv } from './noWindowEnv.mjs';
|
|
9
11
|
import { codexHome } from './codexHome.mjs';
|
|
10
12
|
|
|
@@ -15,9 +17,12 @@ let repoCached = null;
|
|
|
15
17
|
const nameOk = (name) => typeof name === 'string' && /^[A-Za-z0-9_.:-]{1,80}$/.test(name) && name !== 'flowviant';
|
|
16
18
|
// An unattended CLI child like any other (noWindowEnv.mjs, 2026-10-10): `codex
|
|
17
19
|
// mcp list` may start the person's configured servers, and none of them is
|
|
18
|
-
// handed the machine credential or a display.
|
|
20
|
+
// handed the machine credential or a display. From the home directory
|
|
21
|
+
// (2026-10-17): in a trusted checkout — the daemon's own cwd — Codex lists
|
|
22
|
+
// that checkout's `.codex/config.toml` servers as well (measured, 0.163.0),
|
|
23
|
+
// which no card mounts.
|
|
19
24
|
const run = (args, env) => execFileSync('codex', args, {
|
|
20
|
-
encoding: 'utf8', timeout: 5000, maxBuffer: 128 * 1024,
|
|
25
|
+
encoding: 'utf8', timeout: 5000, maxBuffer: 128 * 1024, cwd: homedir(),
|
|
21
26
|
stdio: ['ignore', 'pipe', 'ignore'], env: unattendedEnv(env),
|
|
22
27
|
});
|
|
23
28
|
/**
|
|
@@ -91,7 +96,9 @@ function commandFound(command, env) {
|
|
|
91
96
|
export function codexServerRows(servers, env = process.env) {
|
|
92
97
|
const rows = [];
|
|
93
98
|
for (const s of servers) {
|
|
94
|
-
|
|
99
|
+
// `mcp list` reads the person's own config; the daemon's `flowviant`
|
|
100
|
+
// rides `-c` and is never in it, so one of that name is theirs.
|
|
101
|
+
if (!s || typeof s.name !== 'string' || !(nameOk(s.name) || s.name === 'flowviant')) continue;
|
|
95
102
|
const t = s.transport && typeof s.transport === 'object' ? s.transport : {};
|
|
96
103
|
const auth = typeof s.auth_status === 'string' ? s.auth_status : '';
|
|
97
104
|
let status = 'configured';
|
|
@@ -105,10 +112,10 @@ export function codexServerRows(servers, env = process.env) {
|
|
|
105
112
|
reason = typeof s.disabled_reason === 'string' ? s.disabled_reason.slice(0, 200) : null;
|
|
106
113
|
} else if (typeof t.command === 'string' && commandFound(t.command, env) === false) {
|
|
107
114
|
status = 'failed';
|
|
108
|
-
reason =
|
|
115
|
+
reason = mcpCommandNotFoundReason(basename(t.command).slice(0, 60));
|
|
109
116
|
} else if (unset.length) {
|
|
110
117
|
status = 'failed';
|
|
111
|
-
reason =
|
|
118
|
+
reason = mcpEnvUnsetReason([...new Set(unset)].sort());
|
|
112
119
|
} else if (['needs_auth', 'not_logged_in', 'notLoggedIn'].includes(auth)) {
|
|
113
120
|
status = 'needs-auth';
|
|
114
121
|
} else if (auth === 'failed') {
|
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -131,7 +131,9 @@ export const runtimeLabel = (id) => runtimeById(id)?.label ?? String(id);
|
|
|
131
131
|
* So the restriction is gone and this is back to "installed, and this module
|
|
132
132
|
* knows how to spawn it" — which is what the registry's `live` flag always
|
|
133
133
|
* described: not which runtimes can run, but which get a session instead of a
|
|
134
|
-
* subprocess.
|
|
134
|
+
* subprocess. (That flag is gone: since 2026-10-18 the rows carry
|
|
135
|
+
* `liveInput`, a different fact — whether follow-ups reach the running CLI
|
|
136
|
+
* on stdin.)
|
|
135
137
|
*
|
|
136
138
|
* LIVE is no longer read here. That is deliberate and load-bearing: this
|
|
137
139
|
* predicate feeds both the roster report AND the claim, and if the two ever
|
package/bin/lib/startProject.mjs
CHANGED
|
@@ -208,6 +208,16 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
208
208
|
`or connect this repo to its own project with \`${terminalCommand('login')}\`.`
|
|
209
209
|
);
|
|
210
210
|
process.exit(1);
|
|
211
|
+
} else if (interactive && !CREDENTIAL.choices?.length) {
|
|
212
|
+
// NOTHING STORED AND A PERSON AT THE TERMINAL IS A FIRST CONNECTION
|
|
213
|
+
// (2026-10-19). The app's connect command ends in a bare `flowviant`
|
|
214
|
+
// and the README promises "approve the code in Flowviant → connected",
|
|
215
|
+
// yet this branch printed "no credential found" and exited 1 — so the
|
|
216
|
+
// one command every first connection runs failed after a successful
|
|
217
|
+
// install. Log in here, as the picker's connect row does; headless
|
|
218
|
+
// still refuses in the words below, because nobody can approve a code.
|
|
219
|
+
console.log('No project is connected on this machine yet — connecting one now.');
|
|
220
|
+
await reexecAfterLogin();
|
|
211
221
|
} else {
|
|
212
222
|
console.error(
|
|
213
223
|
'error: no credential found. Easiest:\n' +
|