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.
Files changed (101) hide show
  1. package/README.md +4 -5
  2. package/bin/lib/agentInternalWait.mjs +1 -5
  3. package/bin/lib/agentPlan.mjs +13 -163
  4. package/bin/lib/agentRepositories.mjs +3 -1
  5. package/bin/lib/claudeInput.mjs +89 -10
  6. package/bin/lib/claudeMachineMcp.mjs +85 -0
  7. package/bin/lib/claudePersonal.mjs +6 -57
  8. package/bin/lib/claudePosture.mjs +6 -15
  9. package/bin/lib/claudeSettingsArg.mjs +67 -0
  10. package/bin/lib/claudeStream.mjs +30 -1
  11. package/bin/lib/claudeSubagentProgress.mjs +12 -28
  12. package/bin/lib/claudeSubagents.mjs +4 -2
  13. package/bin/lib/codexContract.mjs +52 -0
  14. package/bin/lib/codexFencedMount.mjs +275 -0
  15. package/bin/lib/codexPersonal.mjs +1 -265
  16. package/bin/lib/codexProjectRead.mjs +5 -4
  17. package/bin/lib/codexResearch.mjs +9 -3
  18. package/bin/lib/codexSubagentEvents.mjs +11 -7
  19. package/bin/lib/codexSubagents.mjs +55 -52
  20. package/bin/lib/codexToml.mjs +328 -0
  21. package/bin/lib/config.mjs +22 -1
  22. package/bin/lib/conversationIds.mjs +10 -0
  23. package/bin/lib/fleet.mjs +2 -2
  24. package/bin/lib/git.mjs +32 -0
  25. package/bin/lib/heldTool.mjs +49 -0
  26. package/bin/lib/hooks/mcpAllow.mjs +11 -5
  27. package/bin/lib/hooks/orchestrator.mjs +20 -10
  28. package/bin/lib/intakeDraft.mjs +1 -1
  29. package/bin/lib/jsonlTail.mjs +52 -0
  30. package/bin/lib/mcpInventory.mjs +11 -6
  31. package/bin/lib/mcpReasons.mjs +26 -0
  32. package/bin/lib/mcpReportBounds.mjs +10 -0
  33. package/bin/lib/mcpStdio.mjs +35 -0
  34. package/bin/lib/mergeBatch.mjs +6 -24
  35. package/bin/lib/mergeOrder.mjs +18 -0
  36. package/bin/lib/modelJson.mjs +173 -0
  37. package/bin/lib/nativeAgentTools.mjs +72 -0
  38. package/bin/lib/orchestrator.mjs +221 -133
  39. package/bin/lib/orchestratorClient.mjs +17 -21
  40. package/bin/lib/orchestratorHooks.mjs +21 -4
  41. package/bin/lib/orchestratorNative.mjs +58 -0
  42. package/bin/lib/orchestratorPrompt.mjs +7 -4
  43. package/bin/lib/orchestratorProtocol.mjs +25 -3
  44. package/bin/lib/postureTools.mjs +58 -13
  45. package/bin/lib/prWorkflow.mjs +13 -2
  46. package/bin/lib/processGroup.mjs +41 -0
  47. package/bin/lib/projectRepositories.mjs +2 -2
  48. package/bin/lib/projectToolRuntimes.mjs +92 -245
  49. package/bin/lib/projectTools.mjs +60 -19
  50. package/bin/lib/prompts.mjs +47 -18
  51. package/bin/lib/reportJournal.mjs +61 -0
  52. package/bin/lib/repositoryDelivery.mjs +6 -4
  53. package/bin/lib/repositoryMerge.mjs +8 -12
  54. package/bin/lib/repositoryPlaces.mjs +2 -2
  55. package/bin/lib/repositoryPrompts.mjs +42 -0
  56. package/bin/lib/repositoryTool.mjs +9 -32
  57. package/bin/lib/researchUse.mjs +39 -0
  58. package/bin/lib/resumeLost.mjs +21 -1
  59. package/bin/lib/resumeMarker.mjs +43 -0
  60. package/bin/lib/runTurn.mjs +55 -52
  61. package/bin/lib/runtimeAntigravity.mjs +1 -1
  62. package/bin/lib/runtimeCapabilities.mjs +13 -18
  63. package/bin/lib/runtimeClaude.mjs +68 -24
  64. package/bin/lib/runtimeCodex.mjs +12 -6
  65. package/bin/lib/runtimeEvents.mjs +13 -2
  66. package/bin/lib/runtimeOfferings.mjs +12 -5
  67. package/bin/lib/runtimes.mjs +3 -1
  68. package/bin/lib/startProject.mjs +10 -0
  69. package/bin/lib/subagentBounds.mjs +20 -0
  70. package/bin/lib/trace.mjs +30 -5
  71. package/bin/lib/turnDeclaration.mjs +1 -1
  72. package/bin/lib/turnDiagnostics.mjs +9 -12
  73. package/bin/lib/turnReceipts.mjs +64 -0
  74. package/bin/lib/turnSubagents.mjs +91 -0
  75. package/bin/lib/turnUsage.mjs +9 -6
  76. package/bin/lib/update.mjs +18 -4
  77. package/bin/lib/versionOrder.mjs +11 -1
  78. package/bin/lib/work.mjs +9 -9
  79. package/bin/lib/workAgentMerges.mjs +5 -7
  80. package/bin/lib/workAgentPrecheck.mjs +5 -5
  81. package/bin/lib/workAgentRecovery.mjs +2 -1
  82. package/bin/lib/workAgentResume.mjs +121 -0
  83. package/bin/lib/workAgentTools.mjs +111 -0
  84. package/bin/lib/workAgentTurnExecution.mjs +82 -246
  85. package/bin/lib/workAgentTurnOutcome.mjs +30 -3
  86. package/bin/lib/workAgentTurnReports.mjs +16 -0
  87. package/bin/lib/workAgentTurnShape.mjs +90 -0
  88. package/bin/lib/workAgentTurns.mjs +28 -12
  89. package/bin/lib/workArtifactReconcile.mjs +3 -2
  90. package/bin/lib/workAttachments.mjs +27 -5
  91. package/bin/lib/workMergeBatch.mjs +7 -5
  92. package/bin/lib/workNarration.mjs +32 -0
  93. package/bin/lib/workPlaceReports.mjs +3 -3
  94. package/bin/lib/workPlaces.mjs +14 -0
  95. package/bin/lib/workReportQueue.mjs +18 -1
  96. package/bin/lib/workSessionRuntime.mjs +2 -5
  97. package/bin/lib/workSessionTurns.mjs +68 -80
  98. package/bin/lib/workShip.mjs +8 -18
  99. package/bin/lib/workTurnShape.mjs +6 -4
  100. package/bin/lib/worktreeDiff.mjs +29 -5
  101. package/package.json +1 -1
@@ -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 { createClaudeSubagentProgress } from './claudeSubagentProgress.mjs';
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 Claude's expression, looked
176
- // up by the SAME name (`claudePermFor`), so the two cannot disagree about
177
- // which posture a turn is running under. There is no precedence chain left
178
- // to get wrong: the caller named one profile (turnProfile.mjs).
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
- codexVersion: measuredRuntimeVersion('codex')?.version,
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.id === 'claude' && Boolean(opts.liveMessages)),
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: claudePermFor(turnProfile.name, knowledgeDir, vaultDir, turnProfile.projectMcp ? (opts.projectMcpServers ?? []) : []),
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.id === 'claude' && opts.liveMessages) ? 'pipe' : 'ignore', 'pipe', 'pipe'],
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.id === 'claude' && opts.liveMessages ? createClaudeInput(child, { onTaken: opts.liveMessages.onTaken }) : null;
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
- const agentStates = new Map();
401
- const reportSubagent = (update) => {
402
- if (!update?.id || (!agentStates.has(update.id) && agentStates.size >= 32)) return;
403
- agentStates.set(update.id, update.status ?? agentStates.get(update.id) ?? 'running');
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
- claudeProgress?.session(i?.sessionId);
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; codexAgents?.thread(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 ?? []) codexAgents?.update(update);
482
- if (rt.id === 'codex') {
483
- try { const item = JSON.parse(line)?.item; const result = codexSpawnResult(item); if (result) opts.onSpawnResult?.(result); } catch { /* non-JSON */ }
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
- void codexAgents?.stop();
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
- // Native transcript reads are readouts too: a stuck filesystem must
555
- // not hold the card's answer. Readers stop scheduling before draining.
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
- live: false,
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 const MCP_STATUSES = new Set(['connected', 'failed', 'needs-auth', 'pending']);
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 `name`) — Claude's report has none, and its connector
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
- if (!name || name === 'flowviant' || seen.has(name)) continue;
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: typeof e.scope === 'string' ? e.scope : typeof e.source === 'string' ? e.source : null, prefix: mcpToolPrefix(e.name.trim()), tools: status === 'connected' ? [] : null });
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
- try {
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, machineMcpServers, personalClaudeSettings, releaseTurnSettings, withPersonalSettings, writeTurnSettings } from './claudePersonal.mjs';
18
- import { withMachineMcp } from './claudePosture.mjs';
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 profiles an agent lane mounts the machine's MCP servers on (2026-10-11):
22
- * the ones whose turn profile says `projectMcp` and that Claude declares. */
23
- const MACHINE_MCP_PROFILES = Object.freeze(['build', 'design', 'research']);
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
- /** The Agent-SDK live session (persistent, injectable mid-task) is Claude
52
- * only — it is an Anthropic SDK, not a CLI contract. Everything else runs
53
- * the subprocess path. */
54
- live: true,
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
- if (agentTools) {
185
- a.push('--system-prompt-snapshot', 'off');
186
- if (agentTools.pluginDir) a.push('--plugin-dir', agentTools.pluginDir);
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 other fenced posture —
195
- // consult, wiki, and a design/research turn no lane mounted for — stays
196
- // strict: it never discovers a .mcp.json, nor loads the machine's servers.
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
- const machine = Array.isArray(machineMcp) && MACHINE_MCP_PROFILES.includes(profile);
199
- if (['consult', 'wiki', 'design', 'research'].includes(profile) && !machine && !mcp.includes('--strict-mcp-config')) a.push('--strict-mcp-config');
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
- // claudePersonal.mjs `machineMcpServers`): the user scope a fenced turn's
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
- const ownArgs = Object.keys(own).length ? ['--mcp-config', writeTurnSettings({ mcpServers: own })] : [];
233
- const postured = machine ? withMachineMcp(perm, machineMcp) : perm;
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;
@@ -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 { compareVersions } from './versionOrder.mjs';
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. With
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
- const order = compareVersions(String(version ?? '').replace(/^codex-cli\s+/, ''), CODEX_SUBAGENTS_MIN);
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
- live: false,
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
- if (!s || typeof s.name !== 'string' || !nameOk(s.name)) continue;
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 = `command ${basename(t.command).slice(0, 60)} not found`;
115
+ reason = mcpCommandNotFoundReason(basename(t.command).slice(0, 60));
109
116
  } else if (unset.length) {
110
117
  status = 'failed';
111
- reason = `env ${[...new Set(unset)].sort().join(', ')} unset`;
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') {
@@ -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
@@ -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' +