flowviant 0.125.5 → 0.125.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/bin/lib/agentInternalWait.mjs +1 -5
- package/bin/lib/agentPlan.mjs +13 -163
- package/bin/lib/agentRepositories.mjs +3 -1
- package/bin/lib/claudeInput.mjs +89 -10
- package/bin/lib/claudeMachineMcp.mjs +85 -0
- package/bin/lib/claudePersonal.mjs +6 -57
- package/bin/lib/claudePosture.mjs +6 -15
- package/bin/lib/claudeSettingsArg.mjs +67 -0
- package/bin/lib/claudeStream.mjs +30 -1
- package/bin/lib/claudeSubagentProgress.mjs +12 -28
- package/bin/lib/claudeSubagents.mjs +4 -2
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexFencedMount.mjs +275 -0
- package/bin/lib/codexPersonal.mjs +1 -265
- package/bin/lib/codexProjectRead.mjs +5 -4
- package/bin/lib/codexResearch.mjs +9 -3
- package/bin/lib/codexSubagentEvents.mjs +11 -7
- package/bin/lib/codexSubagents.mjs +55 -52
- package/bin/lib/codexToml.mjs +328 -0
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/conversationIds.mjs +10 -0
- package/bin/lib/fleet.mjs +2 -2
- package/bin/lib/git.mjs +32 -0
- package/bin/lib/heldTool.mjs +49 -0
- package/bin/lib/hooks/mcpAllow.mjs +11 -5
- package/bin/lib/hooks/orchestrator.mjs +20 -10
- package/bin/lib/intakeDraft.mjs +1 -1
- package/bin/lib/jsonlTail.mjs +52 -0
- package/bin/lib/mcpInventory.mjs +11 -6
- package/bin/lib/mcpReasons.mjs +26 -0
- package/bin/lib/mcpReportBounds.mjs +10 -0
- package/bin/lib/mcpStdio.mjs +35 -0
- package/bin/lib/mergeBatch.mjs +6 -24
- package/bin/lib/mergeOrder.mjs +18 -0
- package/bin/lib/modelJson.mjs +173 -0
- package/bin/lib/nativeAgentTools.mjs +72 -0
- package/bin/lib/orchestrator.mjs +221 -133
- package/bin/lib/orchestratorClient.mjs +17 -21
- package/bin/lib/orchestratorHooks.mjs +21 -4
- package/bin/lib/orchestratorNative.mjs +58 -0
- package/bin/lib/orchestratorPrompt.mjs +7 -4
- package/bin/lib/orchestratorProtocol.mjs +25 -3
- package/bin/lib/postureTools.mjs +58 -13
- package/bin/lib/prWorkflow.mjs +13 -2
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectRepositories.mjs +2 -2
- package/bin/lib/projectToolRuntimes.mjs +92 -245
- package/bin/lib/projectTools.mjs +60 -19
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- package/bin/lib/repositoryDelivery.mjs +6 -4
- package/bin/lib/repositoryMerge.mjs +8 -12
- package/bin/lib/repositoryPlaces.mjs +2 -2
- package/bin/lib/repositoryPrompts.mjs +42 -0
- package/bin/lib/repositoryTool.mjs +9 -32
- package/bin/lib/researchUse.mjs +39 -0
- package/bin/lib/resumeLost.mjs +21 -1
- package/bin/lib/resumeMarker.mjs +43 -0
- package/bin/lib/runTurn.mjs +55 -52
- package/bin/lib/runtimeAntigravity.mjs +1 -1
- package/bin/lib/runtimeCapabilities.mjs +13 -18
- package/bin/lib/runtimeClaude.mjs +68 -24
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeEvents.mjs +13 -2
- package/bin/lib/runtimeOfferings.mjs +12 -5
- package/bin/lib/runtimes.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- package/bin/lib/subagentBounds.mjs +20 -0
- package/bin/lib/trace.mjs +30 -5
- package/bin/lib/turnDeclaration.mjs +1 -1
- package/bin/lib/turnDiagnostics.mjs +9 -12
- package/bin/lib/turnReceipts.mjs +64 -0
- package/bin/lib/turnSubagents.mjs +91 -0
- package/bin/lib/turnUsage.mjs +9 -6
- package/bin/lib/update.mjs +18 -4
- package/bin/lib/versionOrder.mjs +11 -1
- package/bin/lib/work.mjs +9 -9
- package/bin/lib/workAgentMerges.mjs +5 -7
- package/bin/lib/workAgentPrecheck.mjs +5 -5
- package/bin/lib/workAgentRecovery.mjs +2 -1
- package/bin/lib/workAgentResume.mjs +121 -0
- package/bin/lib/workAgentTools.mjs +111 -0
- package/bin/lib/workAgentTurnExecution.mjs +82 -246
- package/bin/lib/workAgentTurnOutcome.mjs +30 -3
- package/bin/lib/workAgentTurnReports.mjs +16 -0
- package/bin/lib/workAgentTurnShape.mjs +90 -0
- package/bin/lib/workAgentTurns.mjs +28 -12
- package/bin/lib/workArtifactReconcile.mjs +3 -2
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workMergeBatch.mjs +7 -5
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlaceReports.mjs +3 -3
- package/bin/lib/workPlaces.mjs +14 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionRuntime.mjs +2 -5
- package/bin/lib/workSessionTurns.mjs +68 -80
- package/bin/lib/workShip.mjs +8 -18
- package/bin/lib/workTurnShape.mjs +6 -4
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
|
@@ -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(
|
|
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 (
|
|
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 >=
|
|
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 './
|
|
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,
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
package/bin/lib/turnUsage.mjs
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
package/bin/lib/update.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
227
|
-
|
|
228
|
-
|
|
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
|
/**
|
package/bin/lib/versionOrder.mjs
CHANGED
|
@@ -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)
|
|
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;
|
|
59
|
-
* SOLID
|
|
60
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
31
|
+
return relayedText(output || String(error?.message || error)).slice(0, 2000);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
export function createWorkAgentMerges({
|
|
36
|
-
|
|
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:
|
|
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 `
|
|
110
|
-
* trailer ids are what let the prompt say "N earlier
|
|
111
|
-
* this box" honestly: the difference between the
|
|
112
|
-
* the specs this disk holds is a fact, and a box
|
|
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 !==
|
|
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);
|