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
@@ -0,0 +1,20 @@
1
+ /** HOW MANY NATIVE CHILDREN ONE CARD RUN RECORDS, THE FIELD CAPS EACH
2
+ * CARRIES, AND THE STATUSES A CHILD HAS (2026-10-18, the SOLID pass). The
3
+ * server folds at most `TRACE_AGENT_CAP` children per run, clamps each field
4
+ * (`agentTraceAgents.ts`) and refuses a handed route to a child it did not
5
+ * keep, so every daemon reader that tracks a child keeps the same bound: a
6
+ * 33rd is never half-reported, admitted by the controller while the trace
7
+ * sheds its descriptor. The cap was spelled eight times across the
8
+ * controller, the trace relay, runTurn and both Codex readers (the
9
+ * codexSubagentEvents split had copied it), and nothing held it to the
10
+ * server. A leaf of its own because trace.mjs, which the web's tests import,
11
+ * reads it (mcpReportBounds.mjs argues the same); scripts/check-app-parity.mjs
12
+ * holds it to the API's `TRACE_AGENT_CAP`/`TRACE_AGENT_FIELD_CAPS` and the
13
+ * shared `SUBAGENT_STATUSES`. */
14
+
15
+ export const SUBAGENT_CAP = 32;
16
+ /** In the order the trace relay sends them. */
17
+ export const SUBAGENT_FIELD_CAPS = Object.freeze({ kind: 80, title: 200, now: 400, brief: 4_000 });
18
+ export const SUBAGENT_STATUSES = Object.freeze(['running', 'waiting', 'asked', 'stopped', 'done']);
19
+ /** A child that has finished or been stopped: the controller revives neither. */
20
+ export const SETTLED_SUBAGENT_STATUSES = Object.freeze(['done', 'stopped']);
package/bin/lib/trace.mjs CHANGED
@@ -61,12 +61,16 @@
61
61
 
62
62
  import { THINK_MARKER, scrubNativeText } from './runtimeEvents.mjs';
63
63
  import { withoutTurnDeclaration } from './turnDeclaration.mjs';
64
+ import { MAX_MCP_SERVERS, MCP_NAME_MAX, MCP_STATUSES } from './mcpReportBounds.mjs';
65
+ import { SUBAGENT_CAP, SUBAGENT_FIELD_CAPS, SUBAGENT_STATUSES } from './subagentBounds.mjs';
64
66
 
65
67
  /** One batch every two seconds — the discipline the tab's narrator keeps, for
66
68
  * the same reason: a turn emits hundreds of entries and nobody is reading them
67
69
  * faster than that. */
68
70
  export const TRACE_FLUSH_MS = 2_000;
69
- /** Entries per POST. Matches the server's own per-batch cap. */
71
+ /** Entries per POST. Matches the server's own per-batch cap
72
+ * (`agentTrace.ts.TRACE_BATCH_CAP`), which refuses a larger batch whole;
73
+ * scripts/check-app-parity.mjs rule 38 holds the two equal. */
70
74
  export const TRACE_BATCH = 40;
71
75
  /** Entries held while the uplink is down. Past this the OLDEST go: a trace is
72
76
  * scrollback, and the newest steps are the ones somebody watching wants. The
@@ -77,7 +81,7 @@ export const TRACE_BUFFER = 120;
77
81
  * (`agentTrace.ts.TRACE_PROSE_CAP`); doing it here too means a pathological line
78
82
  * never becomes the POST. The two must move together — daemon at or below the
79
83
  * server's, or the server silently does the cutting and this file's caps stop
80
- * describing what ships.
84
+ * describing what ships. scripts/check-app-parity.mjs rule 38 holds them equal.
81
85
  *
82
86
  * 300 UNTIL 2026-09-16, when it was the thing clipping sentences. A trace entry
83
87
  * used to be the humanized 160-char label the console prints, so a 300 cap could
@@ -120,6 +124,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
120
124
  * by the bare-marker collapse in `prose()` and nothing else. */
121
125
  let lastEntry = null;
122
126
  const agentReadings = new Map();
127
+ const mcpSaid = new Set();
123
128
  let dirty = orchestrationVersion === 2;
124
129
  let markerPending = dirty;
125
130
  let timer = null;
@@ -266,13 +271,13 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
266
271
  if (!update?.id) return;
267
272
  const clean = (v, n) => scrubNativeText(v, n, scrub);
268
273
  const fields = {};
269
- for (const [key, cap] of Object.entries({ kind: 80, title: 200, now: 400, brief: 4_000 }))
274
+ for (const [key, cap] of Object.entries(SUBAGENT_FIELD_CAPS))
270
275
  if (typeof update[key] === 'string') fields[key] = clean(update[key], cap);
271
276
  if (typeof fields.now === 'string') fields.now = fields.now.replace(/\s+/g, ' ').trim();
272
- if (['running', 'waiting', 'asked', 'stopped', 'done'].includes(update.status)) fields.status = update.status;
277
+ if (SUBAGENT_STATUSES.includes(update.status)) fields.status = update.status;
273
278
  const previous = agentReadings.get(update.id) ?? {};
274
279
  if (agentReadings.has(update.id) && Object.entries(fields).every(([k, v]) => previous[k] === v)) return;
275
- if (!agentReadings.has(update.id) && agentReadings.size >= 32) return;
280
+ if (!agentReadings.has(update.id) && agentReadings.size >= SUBAGENT_CAP) return;
276
281
  const reading = { ...previous, ...fields };
277
282
  agentReadings.set(update.id, reading);
278
283
  // A later update repairs metadata whose first report was shed while
@@ -286,6 +291,26 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
286
291
  doing(agentId, text) {
287
292
  push({ k: 'doing', agentId: scoped(agentId), t: scrub(String(text)).slice(0, TRACE_PROSE_CAP) });
288
293
  },
294
+ /**
295
+ * WHAT THE CLI SAID OF ITS MCP SERVERS AT START (2026-10-17): Claude's
296
+ * init lists each with its status. A server that failed to start read to
297
+ * the agent and the person as "no MCP access", so each one not connected
298
+ * is said on the root stream, once per run. No list is unknown and says
299
+ * nothing; connected is good news and says nothing either.
300
+ */
301
+ mcpStatus(servers) {
302
+ if (!Array.isArray(servers)) return;
303
+ const fresh = [];
304
+ for (const s of servers.slice(0, MAX_MCP_SERVERS)) {
305
+ if (!s || typeof s.name !== 'string' || s.status === 'connected') continue;
306
+ const name = scrubNativeText(s.name, MCP_NAME_MAX, scrub).replace(/\s+/g, ' ').trim();
307
+ const status = MCP_STATUSES.has(s.status) ? s.status : 'other';
308
+ if (!name || mcpSaid.has(`${name}\0${status}`) || mcpSaid.size >= 4 * MAX_MCP_SERVERS) continue;
309
+ mcpSaid.add(`${name}\0${status}`);
310
+ fresh.push(`${name} (${status})`);
311
+ }
312
+ if (fresh.length) push({ k: 'note', t: scrub(`The CLI reported MCP servers that are not connected: ${fresh.join(', ')}.`).slice(0, TRACE_PROSE_CAP) });
313
+ },
289
314
  flush,
290
315
  /** No more entries, no more timers. The queue survives so a final flush can
291
316
  * still deliver it. */
@@ -2,7 +2,7 @@
2
2
  * normalization/caps; the unmodified answer still goes to parseTurnResult.
3
3
  * Only standalone objects and JSON/unlabelled fences count. Code examples
4
4
  * inside prose or another language's fence stay as the CLI wrote them. */
5
- import { jsonObjectClosers } from './agentPlan.mjs';
5
+ import { jsonObjectClosers } from './modelJson.mjs';
6
6
 
7
7
  const statuses = new Set(['delivered', 'blocked', 'prerequisite', 'nothing']);
8
8
  function declaration(body) {
@@ -4,9 +4,10 @@
4
4
  * Logging is best effort and rides the daemon's existing bounded private log.
5
5
  */
6
6
  import { randomUUID } from 'node:crypto';
7
- import { accessSync, constants, readFileSync, realpathSync, statSync } from 'node:fs';
7
+ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
8
8
  import { delimiter, isAbsolute, resolve } from 'node:path';
9
9
  import { AUTH_ENV_VARS, claudeAuthContext, claudeCredentialPath } from './claudeAuth.mjs';
10
+ import { settingsAt } from './claudeSettingsArg.mjs';
10
11
  import { scrub } from './uplinkScrub.mjs';
11
12
  import { printableText } from './printable.mjs';
12
13
  import { measuredRuntimeVersion } from './runtimeDetection.mjs';
@@ -47,17 +48,13 @@ function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile
47
48
  const executable = executableFor(bin, env, cwd);
48
49
  let settingsEnv = {};
49
50
  if (runtime === 'claude') {
50
- const at = args.indexOf('--settings');
51
- if (at >= 0) {
52
- try {
53
- const input = args[at + 1];
54
- const settings = JSON.parse(input.startsWith('{') ? input : readFileSync(input, 'utf8'));
55
- if (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env)) {
56
- for (const [key, value] of Object.entries(settings.env))
57
- if (typeof value === 'string') settingsEnv[key] = value;
58
- }
59
- } catch { /* CLI settings unavailable */ }
60
- }
51
+ try {
52
+ const { settings } = settingsAt(args);
53
+ if (settings?.env && typeof settings.env === 'object' && !Array.isArray(settings.env)) {
54
+ for (const [key, value] of Object.entries(settings.env))
55
+ if (typeof value === 'string') settingsEnv[key] = value;
56
+ }
57
+ } catch { /* CLI settings unavailable */ }
61
58
  }
62
59
  // Only byte counts, including each terminating NUL. No values or hashes.
63
60
  const argSizes = [bin, ...args].map(value => Buffer.byteLength(value) + 1);
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A TURN'S COMMIT RECEIPTS — every sha that landed on its branch while it ran
3
+ * (2026-10-18, the SOLID pass).
4
+ *
5
+ * The rule was spelled three times: the agent run's `commitsBetween`, the
6
+ * lane's catch (which settles a turn whose run threw, and computed the same
7
+ * receipts over what the run had measured), and a multi-repository place's
8
+ * per-repository log (repositoryPlaces.mjs). Two incidents live in it — the
9
+ * base commits reported as a turn's own, and the six-hour re-run below — so a
10
+ * fix to one copy that missed another was the risk. A leaf: it imports only
11
+ * git.mjs's guarded read.
12
+ *
13
+ * Every sha that landed on this branch between two points. Measured, never
14
+ * asserted — this is the whole reason an agent is not asked for its own.
15
+ *
16
+ * `--not <base>` and `--no-merges`, because `from..HEAD` alone reports
17
+ * BASE'S commits as this turn's receipts the moment anything folds base in
18
+ * — which the stale path does on purpose before a merge. A receipt naming
19
+ * somebody else's commit is worse than a missing one.
20
+ *
21
+ * IT CANNOT THROW, and that guard is the whole point of it being here.
22
+ *
23
+ * `git()` throws on a non-zero exit, and `baseRef()` is built from the
24
+ * project's Base branch setting — free text an owner types, never verified
25
+ * against the remote. Point it at a branch with no tracking ref (`develop`
26
+ * on a repo whose remote branch is `dev`) and this exits "fatal: ambiguous
27
+ * argument". The throw escaped `runAgentTurn` AFTER the CLI had already run
28
+ * the card, so `postAgentTurn` was never reached, the server never settled
29
+ * the turn, and the next poll handed back the identical turn — the same
30
+ * card re-run every poll for six hours, on the operator's shared account,
31
+ * piling commits onto the review branch, silently.
32
+ *
33
+ * An unreadable range means we cannot MEASURE the receipts, which is a
34
+ * smaller failure than not settling: the turn still reports, and ship-time
35
+ * reconciliation books whatever no card claimed. Missing beats fabricated
36
+ * and both beat a stall.
37
+ *
38
+ * THE CAP IS THE WIRE'S: agent-turn-done takes `commits` up to 50
39
+ * (machineAgentTurns.routes.ts), so a longer list is a settle refused.
40
+ */
41
+ import { gitOrNull } from './git.mjs';
42
+
43
+ /** The most receipts one settle carries — the API's `commits` max. */
44
+ export const TURN_COMMITS_MAX = 50;
45
+
46
+ /** The shas on `wt`'s HEAD since `from` and not on `base`, merges left out,
47
+ * newest first and capped; `[]` for a range git cannot read. */
48
+ export function commitsSince(wt, from, base) {
49
+ const out = gitOrNull(['log', '--format=%H', '--no-merges', `${from}..HEAD`, '--not', base], wt);
50
+ return out?.split('\n').map((x) => x.trim()).filter(Boolean).slice(0, TURN_COMMITS_MAX) ?? [];
51
+ }
52
+
53
+ /**
54
+ * The receipts one agent turn settles with: a multi-repository card's own
55
+ * (`repositoryReceipts()`, every writable repository's, a sha once), else
56
+ * this worktree's since `before`. `baseRef` is the lane's function, read only
57
+ * when there is a range to read; no `before` (no worktree, or a branch with
58
+ * no commits yet) is none.
59
+ */
60
+ export function turnCommits({ repositoryReceipts, wt, before, baseRef }) {
61
+ return repositoryReceipts
62
+ ? [...new Set(repositoryReceipts().map((r) => r.sha))].slice(0, TURN_COMMITS_MAX)
63
+ : before ? commitsSince(wt, before, baseRef()) : [];
64
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * THE NATIVE CHILDREN ONE CLI TURN STARTS, OBSERVED — the readers that follow
3
+ * them, the bound a turn with no orchestrator keeps on them, and its exit
4
+ * sweep (2026-10-18, the SOLID pass).
5
+ *
6
+ * Split out of runTurn.mjs, the one spawn point every lane uses, because
7
+ * watching a CLI's own subagents is a second vocabulary with its own reasons
8
+ * to change: Claude's background agents (its stream, and the agent_progress
9
+ * rows only its transcript keeps) and Codex's collab children (their
10
+ * rollouts) arrived in four commits in three days, each another runtime-id
11
+ * arm inside the spawn function. runTurn keeps the process and its parent
12
+ * stream; this keeps the children, and a third CLI with native subagents is
13
+ * an arm here. The Codex thread's close-time reads (its plan windows, its
14
+ * model) stay in runTurn: they are the turn's, not a child's.
15
+ *
16
+ * Two rules ride with it, each argued where it lives below: an orchestrator
17
+ * bounds and settles its own records, so only an unowned turn is counted
18
+ * here; and native transcript reads are readouts, so the drain at close is
19
+ * bounded and never holds the card's answer.
20
+ */
21
+
22
+ import { handleStreamLine } from './claudeStream.mjs';
23
+ import { createClaudeSubagents } from './claudeSubagents.mjs';
24
+ import { createClaudeSubagentProgress } from './claudeSubagentProgress.mjs';
25
+ import { createCodexSubagentReader } from './codexSubagents.mjs';
26
+ import { SUBAGENT_CAP } from './subagentBounds.mjs';
27
+
28
+ /**
29
+ * `sinks` are the turn's own: its console, activity, tool-event and text
30
+ * sinks, which a Claude child's transcript rows feed as its stream does.
31
+ */
32
+ export function createTurnSubagents({ runtime, cwd, since, env, owned, onSubagent, onSpawnResult, resumeThreadId,
33
+ sinks: { emit, onActivity, onToolEvent, appendText } }) {
34
+ // An orchestrator bounds its own records and settles them at close
35
+ // (orchestrator.mjs). Counted here by stream id, refused retries used up
36
+ // the 32 and later children's updates were dropped; the exit sweep
37
+ // reported 'stopped' off stream state the controller had already settled.
38
+ const agentStates = new Map();
39
+ const reportSubagent = (update) => {
40
+ if (!update?.id) return;
41
+ if (!owned) {
42
+ if (!agentStates.has(update.id) && agentStates.size >= SUBAGENT_CAP) return;
43
+ agentStates.set(update.id, update.status ?? agentStates.get(update.id) ?? 'running');
44
+ }
45
+ onSubagent?.(update);
46
+ };
47
+ const subagents = createClaudeSubagents(reportSubagent);
48
+ const claudeProgress = runtime === 'claude' && onSubagent ? createClaudeSubagentProgress({
49
+ cwd, since, env,
50
+ onLine: line => handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText,
51
+ answerFromResult: true, subagents }),
52
+ }) : null;
53
+ const codexAgents = runtime === 'codex' && onSubagent ? createCodexSubagentReader({
54
+ since, env,
55
+ onSubagent: reportSubagent, onActivity, onToolEvent,
56
+ onSpawnResult,
57
+ }) : null;
58
+ const resumed = runtime === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
59
+ if (resumed) codexAgents?.thread(resumed);
60
+ return {
61
+ /** Claude's child bookkeeping, handed to the parent stream's reader. */
62
+ collector: subagents,
63
+ /** Claude's session, off its init: whose transcript to follow. */
64
+ session: (id) => claudeProgress?.session(id),
65
+ /** Codex's thread, off `thread.started`: whose rollouts to follow. */
66
+ thread: (id) => codexAgents?.thread(id),
67
+ /** A child update the Codex exec stream carried. */
68
+ update: (update) => codexAgents?.update(update),
69
+ /** The spawn failed: the readers stop, unawaited. */
70
+ abandon() {
71
+ void codexAgents?.stop();
72
+ void claudeProgress?.stop();
73
+ },
74
+ async drain(capMs) {
75
+ // Native transcript reads are readouts too: a stuck filesystem must
76
+ // not hold the card's answer. Readers stop scheduling before draining.
77
+ let progressTimer;
78
+ try {
79
+ await Promise.race([
80
+ Promise.all([codexAgents?.stop(), claudeProgress?.stop()]),
81
+ new Promise(resolve => { progressTimer = setTimeout(resolve, capMs); }),
82
+ ]);
83
+ } finally { clearTimeout(progressTimer); }
84
+ },
85
+ /** After the drain: a child an unowned turn still reads as live ended with it. */
86
+ sweep() {
87
+ for (const [id, status] of agentStates)
88
+ if (status === 'running' || status === 'waiting') reportSubagent({ id, status: 'stopped' });
89
+ },
90
+ };
91
+ }
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * ONE TURN'S TOKENS, ADDED UP ACROSS THE SPAWNS THAT MADE IT (2026-09-28).
3
3
  *
4
- * The agent lane SETS its usage — one spawn, one `result` — and leaves the
5
- * adding-up to the server. The Terminal tab, the capture chat and the wiki
6
- * are different: a tab turn whose resume came back empty runs a fresh retry
7
- * in the same turn, and BOTH spawns spent real tokens; reporting only the
8
- * second would under-count exactly the turns that went wrong. So those lanes
9
- * sum what each spawn reported before the one settle carries it.
4
+ * A turn can be more than one spawn: a tab turn whose resume came back empty
5
+ * runs a fresh retry in the same turn, and an agent turn resumes its CLI
6
+ * after an internal wait (agentInternalWait.mjs) or to commit a repository's
7
+ * work (repositoryDelivery.mjs). Every spawn spent real tokens; reporting
8
+ * only the last would under-count exactly the turns that went wrong. So the
9
+ * Terminal tab, the capture chat, the planner, the wiki and the agent lane
10
+ * (since 2026-10-18, the SOLID pass) sum what each spawn reported through
11
+ * this, and the server still charges once, behind the one settle that
12
+ * carries it.
10
13
  *
11
14
  * The wire shape is the agent lane's, unchanged: `{input, output,
12
15
  * cacheCreate, cacheRead, runtime}`. Null-safe on both sides — a spawn that
@@ -39,7 +39,7 @@ import { platform, arch } from 'node:process';
39
39
  import { pipeline } from 'node:stream/promises';
40
40
  import { Readable } from 'node:stream';
41
41
  import { relaunchSelf, runningCompiledBinary, runningViaNpx, selfArgv, terminalCommand } from './launchCommand.mjs';
42
- import { VERSION } from './config.mjs';
42
+ import { DAEMON_INSTANCE, VERSION } from './config.mjs';
43
43
  import { compareVersions, versionBelow } from './versionOrder.mjs';
44
44
  import { note, ok, warn } from './ui.mjs';
45
45
 
@@ -194,6 +194,9 @@ export function reexec(teardown, { viaNpx = false, target = null } = {}, deps =
194
194
  // as a rival and the new daemon SIGTERMed this proxy. instance.mjs adopts
195
195
  // the lock from this pid when it is really an ancestor of the successor.
196
196
  FLOWVIANT_REEXEC_FROM: String(process.pid),
197
+ // WHO THE SUCCESSOR IS, to the server: this process's own instance, so
198
+ // the idle tabs' session leases are already its own (config.mjs).
199
+ FLOWVIANT_DAEMON_INSTANCE: DAEMON_INSTANCE,
197
200
  // WHAT WE RESTARTED IN ORDER TO BECOME. The successor compares its own
198
201
  // VERSION against this: if it came back still short, the update did not
199
202
  // take (a registry serving a stale `latest`, an npx cache that refused to
@@ -223,9 +226,20 @@ export function updateRestartFailed(target) {
223
226
  return nowVsAttempted === -1 && targetVsAttempted !== null && targetVsAttempted <= 0;
224
227
  }
225
228
 
226
- /** Install @latest globally. Throws on failure (EACCES without sudo, offline…). */
227
- function installLatest() {
228
- execFileSync('npm', ['install', '-g', 'flowviant@latest'], { ...npmOutsideCheckout(), stdio: 'inherit' });
229
+ /**
230
+ * HOW LONG A GLOBAL INSTALL MAY TAKE (2026-10-19). The install is synchronous
231
+ * — it holds the event loop for as long as npm runs — and it had no bound, so
232
+ * a registry or a lock that hung npm hung the whole daemon: no poll, no wake,
233
+ * no turn, on a machine that read as connected. Five minutes is far past a
234
+ * real install; past it npm is killed and the failure is the usual one,
235
+ * retried in fifteen minutes.
236
+ */
237
+ export const INSTALL_TIMEOUT_MS = 5 * 60_000;
238
+
239
+ /** Install @latest globally. Throws on failure (EACCES without sudo, offline,
240
+ * past `INSTALL_TIMEOUT_MS`…). `exec` and `timeoutMs` are the test seams. */
241
+ export function installLatest({ exec = execFileSync, timeoutMs = INSTALL_TIMEOUT_MS } = {}) {
242
+ exec('npm', ['install', '-g', 'flowviant@latest'], { ...npmOutsideCheckout(), stdio: 'inherit', timeout: timeoutMs, killSignal: 'SIGKILL' });
229
243
  }
230
244
 
231
245
  /**
@@ -2,7 +2,11 @@
2
2
  * VERSION ORDER — the one parser and comparator every version decision on this
3
3
  * box uses: the takeover downgrade guard (instance.mjs), the self-update
4
4
  * signals (update.mjs, fleet.mjs), the behind marks in the read-only views
5
- * (views.mjs) and the newest nvm Node (loginPath.mjs).
5
+ * (views.mjs), the newest nvm Node (loginPath.mjs), and the measured CLI
6
+ * floors — Claude's and Codex's orchestration (orchestratorHooks.mjs,
7
+ * runtimeCodex.mjs) and the Codex sandbox helper's (codexResearch.mjs
8
+ * `CODEX_HELPER_MIN`, read by codexProjectRead.mjs too; 2026-10-18, the SOLID
9
+ * pass: both kept a hand-rolled reducer and the same spelled-out floor).
6
10
  *
7
11
  * Split out (SOLID F005) because four hand-written rules disagreed on a
8
12
  * version they could not read: one called it EQUAL, one called each
@@ -40,3 +44,9 @@ export function compareVersions(a, b) {
40
44
 
41
45
  /** True only when BOTH parse and `a` is strictly older. Unknown is not older. */
42
46
  export const versionBelow = (a, b) => compareVersions(a, b) === -1;
47
+
48
+ /** True only when BOTH parse and `a` is at or above `b`. Unknown is not at least. */
49
+ export const versionAtLeast = (a, b) => {
50
+ const o = compareVersions(a, b);
51
+ return o === 0 || o === 1;
52
+ };
package/bin/lib/work.mjs CHANGED
@@ -55,9 +55,9 @@
55
55
  * (and, split earlier: workDiffs, workPreviews, workPullRequests, workShip,
56
56
  * workAgentPlans, workAgentCheck, workAgentPrecheck, workAgentReview,
57
57
  * workAgentTurns, workAgentRecovery, workAgentMerges, workMergeBatch,
58
- * workProcesses; and out of workAgentTurns,
59
- * SOLID F036: workAgentTurnReports, workAgentTurnExecution, workAgentTurnBegun,
60
- * workAgentTurnOutcome; repositoryPlaces + agentRepositories: multi-repo places; the live brief: workAgentBrief, workAgentBriefRunner)
58
+ * workProcesses; out of workAgentTurns, SOLID F036: workAgentTurnReports,
59
+ * workAgentTurnExecution, workAgentTurnBegun, workAgentTurnOutcome; out of that run (SOLID, 2026-10-18): workAgentTools, workAgentTurnShape, workAgentResume;
60
+ * repositoryPlaces + agentRepositories: multi-repo places; the live brief: workAgentBrief, workAgentBriefRunner)
61
61
  */
62
62
 
63
63
  import { trackedRepositorySetup } from './repositorySetup.mjs';
@@ -65,6 +65,7 @@ import { createRepositoryPlaces } from './repositoryPlaces.mjs';
65
65
  import { createAgentRepositories } from './agentRepositories.mjs';
66
66
  import { createWorkProcesses } from './workProcesses.mjs';
67
67
  import { createWorkAgentMerges } from './workAgentMerges.mjs';
68
+ import { createWorkMergeBatch } from './workMergeBatch.mjs';
68
69
  import { createWorkAgentTurns } from './workAgentTurns.mjs';
69
70
  import { createAgentRecovery } from './workAgentRecovery.mjs';
70
71
  import { createWorkAgentCheck } from './workAgentCheck.mjs';
@@ -249,7 +250,7 @@ export function createWorkManager({
249
250
  flushWorkReports,
250
251
  sweepMergedSessionBranch,
251
252
  } = createWorkReportQueue({
252
- repoRoot,
253
+ repoRoot, journalDir: baseDir,
253
254
  baseRef,
254
255
  workAttempts,
255
256
  artifacts,
@@ -260,7 +261,7 @@ export function createWorkManager({
260
261
  });
261
262
  const { workTokens, mintWorkToken } = createWorkSessionTokens({ getLeaseTtl });
262
263
  const { makeNarrator, sayTurnDeferred, lastDeferSaid } = createWorkNarration();
263
- const { fetchAttachments, fetchAgentFiles } = createWorkAttachments();
264
+ const { fetchTabFiles, fetchAgentFiles } = createWorkAttachments();
264
265
  const { sessionRuntime, agyRegistryLookup } = createWorkSessionRuntime({ sessionMetaPath });
265
266
  const { carryDirtyState } = createAdoptCarry({ sessionMetaPath });
266
267
  const { processWorkTurns, interruptWorkTurns, workAnswering } = createWorkSessionTurns({
@@ -286,7 +287,7 @@ export function createWorkManager({
286
287
  sessionRuntime,
287
288
  agyRegistryLookup,
288
289
  carryDirtyState,
289
- fetchAttachments,
290
+ fetchTabFiles,
290
291
  makeNarrator,
291
292
  sayTurnDeferred,
292
293
  lastDeferSaid,
@@ -391,9 +392,8 @@ export function createWorkManager({
391
392
  fetchAgentFiles,
392
393
  });
393
394
  const { processAgentMergeJobs, agentMerges } = createWorkAgentMerges({
394
- runCheck, workChildren, groupKillChildren, admit,
395
- repositoryPlaces,
396
- repoRoot,
395
+ runBatch: createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPlace, runCheck, workChildren, groupKillChildren, admit, onRepoChanged, landed }),
396
+ repositoryPlaces, repoRoot,
397
397
  inPlace,
398
398
  baseDir,
399
399
  gitNet,
@@ -1,13 +1,12 @@
1
- import { createWorkMergeBatch } from './workMergeBatch.mjs';
2
1
  import { mergeRepositoryPlace } from './repositoryMerge.mjs';
3
2
  import { fleetRequest } from './fleetPost.mjs';
4
3
  /** Agent merge claims, pull-request approval, and branch cleanup. */
5
- import { ensureOpenPr, ghReady, mergeAndVerifyTip, PR_URL_RE, relayedFailure } from './prWorkflow.mjs';
4
+ import { ensureOpenPr, ghReady, mergeAndVerifyTip, PR_URL_RE, relayedFailure, relayedText } from './prWorkflow.mjs';
6
5
  import { existsSync } from 'node:fs';
7
6
  import { execFileSync } from 'node:child_process';
8
7
  import { join } from 'node:path';
9
8
  import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
10
- import { git, gitOrNull, gitNet as gitNetIn, baseBranchName, isSafePathSegment, sessionBranch, stripUrlUserinfo } from './git.mjs';
9
+ import { git, gitOrNull, gitNet as gitNetIn, baseBranchName, isSafePathSegment, sessionBranch } from './git.mjs';
11
10
  import { isPublishRef, publishPushArgs, publishDeleteArgs, publishErrorText } from './agentPublish.mjs';
12
11
  import { mergeOutward as shipMergeOutward } from './shipMerge.mjs';
13
12
  import { gitMergeIn } from './gitMergeIdentity.mjs';
@@ -29,11 +28,11 @@ export function agentMergeFailureDetail(error) {
29
28
  .map((value) => String(value ?? '').trim())
30
29
  .filter(Boolean)
31
30
  .join('\n');
32
- return envScrub(stripUrlUserinfo(output || String(error?.message || error))).slice(0, 2000);
31
+ return relayedText(output || String(error?.message || error)).slice(0, 2000);
33
32
  }
34
33
 
35
34
  export function createWorkAgentMerges({
36
- runCheck, workChildren = new Map(), groupKillChildren = new Set(), admit,
35
+ runBatch,
37
36
  repositoryPlaces,
38
37
  repoRoot,
39
38
  inPlace,
@@ -116,7 +115,7 @@ export function createWorkAgentMerges({
116
115
  const report = async (body) => {
117
116
  reported = true;
118
117
  if (body?.ok === true) landedRef = job.publishedRef ?? null;
119
- await postAgentMerge({ ...body, ...(body.detail ? { detail: envScrub(stripUrlUserinfo(String(body.detail))).slice(0, 2000) } : {}) });
118
+ await postAgentMerge({ ...body, ...(body.detail ? { detail: relayedText(String(body.detail)).slice(0, 2000) } : {}) });
120
119
  };
121
120
 
122
121
  // A WRITER on the place, exactly as a ship is. It folds base in and pushes
@@ -436,7 +435,6 @@ export function createWorkAgentMerges({
436
435
  }
437
436
  };
438
437
 
439
- const runBatch = createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPlace, runCheck, workChildren, groupKillChildren, admit, onRepoChanged, landed });
440
438
  const processAgentMergeJobs = (jobs) => {
441
439
  if (!Array.isArray(jobs) || jobs.length === 0) return;
442
440
  for (const job of jobs.slice(0, 2)) {
@@ -106,11 +106,11 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
106
106
  /**
107
107
  * THE BRANCH'S OWN COMMITS, subject + trailers, and the card ids they name.
108
108
  *
109
- * MEASURED, never asserted — the same reason `commitsBetween` exists. The
110
- * trailer ids are what let the prompt say "N earlier cards' specs are not on
111
- * this box" honestly: the difference between the cards this branch claims and
112
- * the specs this disk holds is a fact, and a box that adopted the agent
113
- * mid-run is exactly the case where it is non-zero.
109
+ * MEASURED, never asserted — the same reason `commitsSince` exists
110
+ * (turnReceipts.mjs). The trailer ids are what let the prompt say "N earlier
111
+ * cards' specs are not on this box" honestly: the difference between the
112
+ * cards this branch claims and the specs this disk holds is a fact, and a box
113
+ * that adopted the agent mid-run is exactly the case where it is non-zero.
114
114
  *
115
115
  * IT CANNOT THROW. `baseRef()` is free text an owner typed and may name no
116
116
  * ref at all; an unreadable range costs the reviewer its context, never the
@@ -4,6 +4,7 @@ import { isSafePathSegment, sessionBranchRef, gitNetAsync } from './git.mjs';
4
4
  import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
5
5
  import { fleetEndpoint } from './fleetWire.mjs';
6
6
  import { fleetRequest } from './fleetPost.mjs';
7
+ import { AGENT_PLACE_PREFIX } from './workPlaces.mjs';
7
8
 
8
9
  /** Inspect before any cut or CLI. A failed request waits for the next poll. */
9
10
  export function createAgentRecovery({
@@ -32,7 +33,7 @@ export function createAgentRecovery({
32
33
  for (const job of [...jobs.slice(next), ...jobs.slice(0, next)]) {
33
34
  if (!job || !isSafePathSegment(job.agentId) || !isSafePathSegment(job.placeId) ||
34
35
  sessionBranchRef(job.placeId) === null || typeof job.sourceKey !== 'string' ||
35
- !job.sourceBoxId || job.sourceBoxId === id || job.placeId !== `a-${job.agentId}`) continue;
36
+ !job.sourceBoxId || job.sourceBoxId === id || job.placeId !== `${AGENT_PLACE_PREFIX}${job.agentId}`) continue;
36
37
  const key = `${job.sourceBoxId}:${job.sourceKey}`;
37
38
  if (ready.get(job.agentId) === key) { ready.delete(job.agentId); continue; }
38
39
  blocked.add(job.agentId);