flowviant 0.125.0 → 0.125.1

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.
@@ -84,7 +84,7 @@ function runPlainTurn(opts) {
84
84
  * Both answers stay pending instead, as `process.exit` left them.
85
85
  */
86
86
  if (workLanesStopped()) return never();
87
- return new Promise((resolveTurn) => {
87
+ return new Promise((resolveTurn, rejectTurn) => {
88
88
  const resolve = (v) => {
89
89
  if (!workLanesStopped()) resolveTurn(v);
90
90
  };
@@ -167,8 +167,9 @@ function runPlainTurn(opts) {
167
167
  // up by the SAME name (`claudePermFor`), so the two cannot disagree about
168
168
  // which posture a turn is running under. There is no precedence chain left
169
169
  // to get wrong: the caller named one profile (turnProfile.mjs).
170
+ const stdinText = rt.stdin?.({ prompt, system, profile: turnProfile.adapterProfile }) ?? null;
170
171
  const args = rt.args({
171
- streamInput: rt.id === 'claude' && Boolean(opts.liveMessages),
172
+ streamInput: stdinText !== null || (rt.id === 'claude' && Boolean(opts.liveMessages)),
172
173
  writableDirs: opts.writableDirs,
173
174
  personal,
174
175
  researchIsolation: opts.researchIsolation,
@@ -277,10 +278,11 @@ function runPlainTurn(opts) {
277
278
  browserHome: browser?.dir,
278
279
  });
279
280
  finishDiagnostics = startTurnDiagnostics({ runtime, bin: rt.bin, env: childEnv,
280
- cwd, args, profile: turnProfile.name, planId: opts.diagnosticPlanId });
281
+ cwd, args, stdinBytes: stdinText === null ? 0 : Buffer.byteLength(stdinText),
282
+ profile: turnProfile.name, planId: opts.diagnosticPlanId });
281
283
  child = spawn(rt.bin, args, {
282
284
  cwd,
283
- stdio: [rt.id === 'claude' && opts.liveMessages ? 'pipe' : 'ignore', 'pipe', 'pipe'],
285
+ stdio: [stdinText !== null || (rt.id === 'claude' && opts.liveMessages) ? 'pipe' : 'ignore', 'pipe', 'pipe'],
284
286
  /**
285
287
  * ITS OWN PROCESS GROUP, so what the agent starts stays attributable.
286
288
  *
@@ -327,6 +329,7 @@ function runPlainTurn(opts) {
327
329
  // than this is a previous turn's (turnModel.mjs).
328
330
  const spawnedAt = Date.now();
329
331
  let out = '';
332
+ let stdinError = null;
330
333
  const input = rt.id === 'claude' && opts.liveMessages ? createClaudeInput(child, { onTaken: opts.liveMessages.onTaken }) : null;
331
334
  const pfx = label ? `${label} ` : '';
332
335
  /**
@@ -497,9 +500,13 @@ function runPlainTurn(opts) {
497
500
  releaseArgs();
498
501
  opts.onExit?.(code, signal);
499
502
  if (buf.trim()) onLine(buf);
500
- finishDiagnostics({ code, signal, pid: child.pid, output: out, cliFailed });
503
+ finishDiagnostics({ code, signal, pid: child.pid, output: out, cliFailed, inputError: stdinError?.code });
501
504
  const handBack = async () => {
502
505
  if (opts.liveMessages) await opts.liveMessages.stop();
506
+ if (stdinError) {
507
+ if (!workLanesStopped()) rejectTurn(stdinError);
508
+ return;
509
+ }
503
510
  resolve(out);
504
511
  // After the answer is handed back, never before it: see `onRate`.
505
512
  if (codexThread) {
@@ -530,6 +537,17 @@ function runPlainTurn(opts) {
530
537
  }
531
538
  handBack();
532
539
  });
540
+ if (stdinText !== null) {
541
+ // end() buffers with backpressure and closes at EOF; Codex reads the
542
+ // entire prompt before starting. An early close must not accept output
543
+ // from a turn whose complete authorized input never reached the CLI.
544
+ child.stdin.on('error', (error) => {
545
+ stdinError = Object.assign(new Error(`${rt.label} could not receive the complete turn input (${error.code || 'PIPE_ERROR'}).`), { code: error.code || 'PIPE_ERROR' });
546
+ opts.onCliFailure?.();
547
+ child.kill('SIGTERM');
548
+ });
549
+ child.stdin.end(stdinText, 'utf8');
550
+ }
533
551
  if (input) {
534
552
  const initial = opts.liveMessages.initialPrompt(prompt, resumeThreadId, resume, opts.liveContinuation === true);
535
553
  input.send(opts.liveMessages.initialId || opts.liveMessages.turnId, initial);
@@ -158,6 +158,13 @@ const codexWikiFence = (vaultDir) => [
158
158
  ...fencedTail(),
159
159
  ];
160
160
 
161
+ function codexPrompt({ system, prompt, profile = 'build' }) {
162
+ const readers = profile === 'consult' || profile === 'plan'
163
+ ? '\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.'
164
+ : '';
165
+ return `${system}${readers}\n\n---\n\n${prompt}`;
166
+ }
167
+
161
168
  export const CODEX_RUNTIME = {
162
169
  id: 'codex',
163
170
  label: 'Codex',
@@ -227,13 +234,12 @@ export const CODEX_RUNTIME = {
227
234
  * can diff. A write-up reads a COPY with no `.git` and never resumes
228
235
  * (codexResearch.mjs), so its fence passes it.
229
236
  *
230
- * THE PROMPT IS LAST, and that is load-bearing rather than tidy. Codex takes
231
- * it as a trailing POSITIONAL, so every flag — including the two `-c` MCP
232
- * overrides, which the caller hands in rather than appending — has to be
233
- * placed before it. Appending them after the positional is the kind of argv
234
- * that parses today and stops parsing on some future clap upgrade.
237
+ * The prompt positional stays last; runTurn uses `-` and sends the complete
238
+ * framed text through stdin. A whole batch can exceed Linux's single-argument
239
+ * limit even when its argv plus environment is far below ARG_MAX. Both exec
240
+ * and exec resume support `-` (verified on Codex 0.160.0, 2026-10-16).
235
241
  */
236
- args({ writableDirs = [], prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation, knowledgeDir, projectReads }) {
242
+ args({ streamInput = false, writableDirs = [], prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation, knowledgeDir, projectReads }) {
237
243
  // Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
238
244
  // agy moves) — codex has no adoptable store wired yet. Reaching here
239
245
  // with an adopt id is a wiring mistake upstream, and it fails loudly on
@@ -294,12 +300,11 @@ export const CODEX_RUNTIME = {
294
300
  if (a.includes('--ignore-user-config')) a.push(...(personal?.args ?? []));
295
301
 
296
302
  a.push(...mcp, ...resultSchemaArgs);
297
- const readers = profile === 'consult' || profile === 'plan'
298
- ? '\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.'
299
- : '';
300
- a.push(`${system}${readers}\n\n---\n\n${prompt}`);
303
+ a.push(streamInput ? '-' : codexPrompt({ system, prompt, profile }));
301
304
  return a;
302
305
  },
306
+ /** One-shot UTF-8 input, closed at EOF; distinct from Claude's live JSONL. */
307
+ stdin: codexPrompt,
303
308
  /**
304
309
  * THE PERSON'S LOGIN FOR A FENCED TURN, read once per spawn by runTurn from
305
310
  * the turn's own environment (its CODEX_HOME — an agent's own home when the
@@ -10,6 +10,7 @@ import { AUTH_ENV_VARS, claudeAuthContext, claudeCredentialPath } from './claude
10
10
  import { scrub } from './uplinkScrub.mjs';
11
11
  import { printableText } from './printable.mjs';
12
12
  import { measuredRuntimeVersion } from './runtimeDetection.mjs';
13
+ import { VERSION } from './config.mjs';
13
14
 
14
15
  let active = 0;
15
16
  const authNames = (env) => AUTH_ENV_VARS.filter((name) =>
@@ -40,7 +41,7 @@ export function startTurnDiagnostics(options) {
40
41
  }
41
42
 
42
43
  function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile,
43
- planId = null, args = [], version, write = (row) => console.info(`[cli-turn] ${JSON.stringify(row)}`) }) {
44
+ planId = null, args = [], stdinBytes = 0, version, write = (row) => console.info(`[cli-turn] ${JSON.stringify(row)}`) }) {
44
45
  const startedAt = Date.now();
45
46
  const turnId = randomUUID();
46
47
  const executable = executableFor(bin, env, cwd);
@@ -58,7 +59,14 @@ function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile
58
59
  } catch { /* CLI settings unavailable */ }
59
60
  }
60
61
  }
61
- const base = { turnId, runtime, profile, planId, cwd, daemonPid: process.pid,
62
+ // Only byte counts, including each terminating NUL. No values or hashes.
63
+ const argSizes = [bin, ...args].map(value => Buffer.byteLength(value) + 1);
64
+ const envSizes = Object.entries(env).filter(([, value]) => value !== undefined)
65
+ .map(([key, value]) => Buffer.byteLength(`${key}=${value}`) + 1);
66
+ const sum = values => values.reduce((a, b) => a + b, 0);
67
+ const launch = { argvBytes: sum(argSizes), maxArgBytes: Math.max(0, ...argSizes),
68
+ envBytes: sum(envSizes), maxEnvEntryBytes: Math.max(0, ...envSizes), stdinBytes };
69
+ const base = { turnId, runtime, profile, planId, cwd, daemonPid: process.pid, daemonVersion: VERSION, launch,
62
70
  uid: process.getuid?.() ?? null, platform: process.platform,
63
71
  nodeVersion: process.version, executable, version: version ?? null,
64
72
  runtimeMeasurement: measuredRuntimeVersion(runtime) };
@@ -79,15 +87,15 @@ function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile
79
87
  active++;
80
88
  emit({ ...base, phase: 'start', at: new Date(startedAt).toISOString(), activeTurns: active, auth: auth() });
81
89
  let finished = false;
82
- return ({ code = null, signal = null, pid = null, output = '', spawnError = null, cliFailed = false } = {}) => {
90
+ return ({ code = null, signal = null, pid = null, output = '', spawnError = null, inputError = null, cliFailed = false } = {}) => {
83
91
  if (finished) return;
84
92
  finished = true;
85
93
  active--;
86
94
  const failed = cliFailed || (code !== null && code !== 0);
87
95
  emit({ ...base, phase: 'end', at: new Date().toISOString(), durationMs: Date.now() - startedAt,
88
- pid, code, signal, spawnError, activeTurns: active, auth: auth(),
96
+ pid, code, signal, spawnError, inputError, activeTurns: active, auth: auth(),
89
97
  failureKind: failed && /OAuth session expired and could not be refreshed/i.test(output)
90
98
  ? 'oauth_refresh' : failed && /Failed to authenticate/i.test(output) ? 'authentication'
91
- : spawnError ? 'spawn' : code !== null && code !== 0 ? 'cli_exit' : null });
99
+ : spawnError ? 'spawn' : inputError ? 'input' : code !== null && code !== 0 ? 'cli_exit' : null });
92
100
  };
93
101
  }
package/bin/lib/work.mjs CHANGED
@@ -54,7 +54,8 @@
54
54
  * turnLock.mjs tab pid locks; workLiveMessages.mjs — live input; workDelivery.mjs — queue
55
55
  * (and, split earlier: workDiffs, workPreviews, workPullRequests, workShip,
56
56
  * workAgentPlans, workAgentCheck, workAgentPrecheck, workAgentReview,
57
- * workAgentTurns, workAgentRecovery, workAgentMerges, workProcesses; and out of workAgentTurns,
57
+ * workAgentTurns, workAgentRecovery, workAgentMerges, workMergeBatch,
58
+ * workProcesses; and out of workAgentTurns,
58
59
  * SOLID F036: workAgentTurnReports, workAgentTurnExecution, workAgentTurnBegun,
59
60
  * workAgentTurnOutcome; repositoryPlaces + agentRepositories: multi-repo places; the live brief: workAgentBrief, workAgentBriefRunner)
60
61
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.125.0",
3
+ "version": "0.125.1",
4
4
  "description": "Run your own coding CLIs as build agents for Flowviant — Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
5
5
  "type": "module",
6
6
  "bin": {