flowviant 0.125.4 → 0.125.6
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/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 +28 -9
- package/bin/lib/claudeMachineMcp.mjs +85 -0
- package/bin/lib/claudePersonal.mjs +6 -6
- package/bin/lib/claudePosture.mjs +6 -15
- package/bin/lib/claudeSettingsArg.mjs +67 -0
- package/bin/lib/claudeSubagentProgress.mjs +12 -28
- package/bin/lib/claudeSubagents.mjs +4 -2
- 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/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 +9 -5
- package/bin/lib/mcpReasons.mjs +20 -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 +212 -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/projectRepositories.mjs +2 -2
- package/bin/lib/projectToolRuntimes.mjs +92 -245
- package/bin/lib/projectTools.mjs +59 -19
- 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/resumeMarker.mjs +43 -0
- package/bin/lib/runTurn.mjs +38 -52
- package/bin/lib/runtimeAntigravity.mjs +1 -1
- package/bin/lib/runtimeCapabilities.mjs +6 -16
- package/bin/lib/runtimeClaude.mjs +62 -19
- package/bin/lib/runtimeCodex.mjs +9 -6
- package/bin/lib/runtimeEvents.mjs +13 -2
- package/bin/lib/runtimeOfferings.mjs +9 -4
- package/bin/lib/runtimes.mjs +3 -1
- 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/versionOrder.mjs +11 -1
- package/bin/lib/work.mjs +6 -6
- 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 +62 -246
- package/bin/lib/workAgentTurnOutcome.mjs +30 -3
- package/bin/lib/workAgentTurnShape.mjs +90 -0
- package/bin/lib/workAgentTurns.mjs +24 -10
- package/bin/lib/workArtifactReconcile.mjs +3 -2
- package/bin/lib/workMergeBatch.mjs +7 -5
- package/bin/lib/workPlaceReports.mjs +3 -3
- package/bin/lib/workPlaces.mjs +14 -0
- package/bin/lib/workSessionRuntime.mjs +2 -5
- package/bin/lib/workSessionTurns.mjs +12 -63
- package/bin/lib/workShip.mjs +8 -18
- package/bin/lib/workTurnShape.mjs +6 -4
- package/package.json +1 -1
|
@@ -14,14 +14,12 @@ 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, personalClaudeSettings, releaseTurnSettings, withPersonalSettings } from './claudePersonal.mjs';
|
|
18
|
-
import {
|
|
17
|
+
import { NO_SETTING_SOURCES, personalClaudeSettings, releaseTurnSettings, withPersonalSettings, writeTurnSettings } from './claudePersonal.mjs';
|
|
18
|
+
import { machineMcpServers } from './claudeMachineMcp.mjs';
|
|
19
|
+
import { claudePermFor, withMachineMcp } from './claudePosture.mjs';
|
|
20
|
+
import { withSettings } from './claudeSettingsArg.mjs';
|
|
19
21
|
import { orchestratorHooks } from './orchestratorHooks.mjs';
|
|
20
22
|
|
|
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
|
-
|
|
25
23
|
function claudeMcp(token, mcpUrl) {
|
|
26
24
|
const dir = mkdtempSync(join(tmpdir(), 'flowviant-mcp-'));
|
|
27
25
|
const path = join(dir, 'mcp.json');
|
|
@@ -48,10 +46,10 @@ export const CLAUDE_RUNTIME = {
|
|
|
48
46
|
bin: 'claude',
|
|
49
47
|
install: 'npm i -g @anthropic-ai/claude-code',
|
|
50
48
|
login: 'claude',
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
49
|
+
/** Takes follow-up messages on stdin mid-turn (claudeInput.mjs,
|
|
50
|
+
* workLiveMessages.mjs); a runtime without it gets its follow-ups as the
|
|
51
|
+
* next turn, and its delivery is reported taken at spawn. */
|
|
52
|
+
liveInput: true,
|
|
55
53
|
/**
|
|
56
54
|
* Every profile, because every profile is DEFINED in its vocabulary: the
|
|
57
55
|
* four `--allowedTools` lists in claudePosture.mjs are what "build", "wiki",
|
|
@@ -92,17 +90,30 @@ export const CLAUDE_RUNTIME = {
|
|
|
92
90
|
* RETIRED_POSTURE_KEYS), so no spelling of it reads as one.
|
|
93
91
|
* adopt: 'fork' — a terminal conversation is forked into the tab's
|
|
94
92
|
* worktree (`--resume <id> --fork-session`).
|
|
93
|
+
* liveInput (above) — a follow-up reaches the running CLI on stdin
|
|
94
|
+
* (2026-10-18, the SOLID pass: the tab, agent and runTurn lanes spelled
|
|
95
|
+
* it `rt.id === 'claude'`, and the row's old `live` flag was read by
|
|
96
|
+
* nothing).
|
|
95
97
|
*/
|
|
96
98
|
tab: 'mcp',
|
|
97
99
|
planTurns: true,
|
|
98
100
|
adopt: 'fork',
|
|
99
101
|
mcp: claudeMcp,
|
|
102
|
+
/**
|
|
103
|
+
* THE POSTURE AS CLAUDE'S `--allowedTools` LIST (2026-10-18, the SOLID
|
|
104
|
+
* pass), looked up by the turn profile's name (claudePosture.mjs
|
|
105
|
+
* `claudePermFor`). runTurn asks the row for it, so it knows no runtime's
|
|
106
|
+
* list: a runtime that expresses postures another way — Codex's sandbox
|
|
107
|
+
* modes, agy's flags — has no hook, and Claude's table needs no stub for a
|
|
108
|
+
* posture only another CLI runs.
|
|
109
|
+
*/
|
|
110
|
+
perm: claudePermFor,
|
|
100
111
|
/**
|
|
101
112
|
* Claude takes the operating contract as a real system prompt, which is the
|
|
102
113
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
103
114
|
* above the conversation rather than inside it.
|
|
104
115
|
*/
|
|
105
|
-
args({ writableDirs = [], prompt, system, model, effort, resume, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, vaultDir, agentTools, personalSettings, machineMcp, streamInput = false, orchestration = false }) {
|
|
116
|
+
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 }) {
|
|
106
117
|
// THE CARTOGRAPHER'S FENCE NAMES ITS VAULT (2026-10-10): its one write rule
|
|
107
118
|
// is the vault's absolute path (claudePosture.mjs `wikiPermFor`), so a turn
|
|
108
119
|
// with no vault, or one no rule can name, has no list — refused here in
|
|
@@ -170,7 +181,6 @@ export const CLAUDE_RUNTIME = {
|
|
|
170
181
|
* agent renders the CURRENT base snapshot, not the first turn's cache.
|
|
171
182
|
*/
|
|
172
183
|
const fenced = profile !== 'build';
|
|
173
|
-
if (orchestration && !fenced) a.push('--settings', JSON.stringify({ hooks: orchestratorHooks(), disableAllHooks: false }));
|
|
174
184
|
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
175
185
|
// A FENCED POSTURE NAMES ITS OWN MODE (2026-10-10). Its lists fence only
|
|
176
186
|
// under the CLI's `default` mode, where a tool the list does not name is
|
|
@@ -191,12 +201,17 @@ export const CLAUDE_RUNTIME = {
|
|
|
191
201
|
// beside the base branch's `.mcp.json` copy the caller passes, so they are
|
|
192
202
|
// not strict; `machineMcp` names every `.mcp.json` server from the
|
|
193
203
|
// worktree up, which `withMachineMcp` disables so a worktree edit never
|
|
194
|
-
// mounts before review (claudePosture.mjs). Every
|
|
195
|
-
//
|
|
196
|
-
//
|
|
204
|
+
// mounts before review (claudePosture.mjs). Every fenced posture but plan
|
|
205
|
+
// stays strict unless the lane mounted the machine's servers — consult,
|
|
206
|
+
// wiki, and a design/research turn no lane mounted for: it never discovers
|
|
207
|
+
// a .mcp.json, nor loads the machine's servers. Keyed on the rule, not a
|
|
208
|
+
// list of names, so a posture added later is strict unless it mounts.
|
|
197
209
|
// The plan profile keeps its own principal.
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
//
|
|
211
|
+
// runTurn hands machineMcp only on a profile whose turn profile says
|
|
212
|
+
// projectMcp (runTurn.mjs), the one rule.
|
|
213
|
+
const machine = Array.isArray(machineMcp);
|
|
214
|
+
if (fenced && profile !== 'plan' && !machine && !mcp.includes('--strict-mcp-config')) a.push('--strict-mcp-config');
|
|
200
215
|
a.push(...mcp, ...resultSchemaArgs);
|
|
201
216
|
a.push('--model', model || MODEL);
|
|
202
217
|
if (effort) a.push('--effort', effort);
|
|
@@ -221,8 +236,36 @@ export const CLAUDE_RUNTIME = {
|
|
|
221
236
|
// kept is written to a private file, never onto argv (claudePersonal.mjs)
|
|
222
237
|
// — so this is the LAST thing built: nothing after it may throw and
|
|
223
238
|
// leave the file with no turn to release it.
|
|
224
|
-
|
|
225
|
-
|
|
239
|
+
//
|
|
240
|
+
// …AND THE MACHINE'S SERVERS NO SOURCE OF THIS TURN READS (2026-10-17,
|
|
241
|
+
// claudeMachineMcp.mjs `machineMcpServers`): the user scope a fenced turn's
|
|
242
|
+
// empty source list drops, and the checkout's local scope every card turn
|
|
243
|
+
// missed. In a private file too — a server's env and headers are secrets —
|
|
244
|
+
// minus every name the base copy mounts, which speaks for the project.
|
|
245
|
+
const own = (machine ? machineMcpServers({ user: fenced, checkout: mcpCheckout }) : null) ?? {};
|
|
246
|
+
for (const name of baseMcp) delete own[name];
|
|
247
|
+
// A file that cannot be written costs the card these servers, never the
|
|
248
|
+
// card itself: the turn runs with what its own sources load, and says so.
|
|
249
|
+
let ownArgs = [];
|
|
250
|
+
if (Object.keys(own).length) {
|
|
251
|
+
try {
|
|
252
|
+
ownArgs = ['--mcp-config', writeTurnSettings({ mcpServers: own })];
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.warn(`[agent] this machine's MCP servers are not mounted on this turn: ${error.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const mounted = machine ? withMachineMcp(perm, machineMcp) : perm;
|
|
258
|
+
// The orchestrator's hooks join that same `--settings`: the CLI applies
|
|
259
|
+
// only the last one it is handed (claudeSettingsArg.mjs `withSettings`).
|
|
260
|
+
const postured = orchestration && !fenced ? withSettings(mounted, { hooks: orchestratorHooks(), disableAllHooks: false }) : mounted;
|
|
261
|
+
let tail;
|
|
262
|
+
try {
|
|
263
|
+
tail = fenced ? withPersonalSettings(postured, personalSettings ?? personalClaudeSettings()) : postured;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
releaseTurnSettings(ownArgs);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
a.push(...ownArgs, ...tail);
|
|
226
269
|
return a;
|
|
227
270
|
},
|
|
228
271
|
/** What `args` wrote for a turn — the fenced posture's settings file —
|
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { ARTIFACT_DIR, FENCE_TMP_DIR } from './artifacts.mjs';
|
|
|
16
16
|
import { codexPersonalTurn } from './codexPersonal.mjs';
|
|
17
17
|
import { researchFence } from './codexResearch.mjs';
|
|
18
18
|
import { projectReadBoundary, projectReadFence } from './codexProjectRead.mjs';
|
|
19
|
-
import {
|
|
19
|
+
import { versionAtLeast } from './versionOrder.mjs';
|
|
20
20
|
import { codexOrchestratorArgs } from './orchestratorHooks.mjs';
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -27,7 +27,11 @@ import { codexOrchestratorArgs } from './orchestratorHooks.mjs';
|
|
|
27
27
|
* whose one tool carries no annotations (Flowviant's carry none): under the
|
|
28
28
|
* capture chat's read-only argv the call failed "MCP tool call requires
|
|
29
29
|
* approval, but approval policy is never" and never reached the server; under
|
|
30
|
-
* the build posture (workspace-write) the same call ran unasked
|
|
30
|
+
* the build posture (workspace-write) the same call ran unasked — no longer
|
|
31
|
+
* so on 0.163.0, where a workspace-write card turn refuses it the same way
|
|
32
|
+
* and only danger-full-access runs it (2026-10-17; a card approves the
|
|
33
|
+
* person's own servers by name, projectToolRuntimes.mjs `personalApprovals`).
|
|
34
|
+
* With
|
|
31
35
|
* `mcp_servers.flowviant.default_tools_approval_mode="approve"` beside the
|
|
32
36
|
* read-only argv it ran — and in the same turn a shell write still failed
|
|
33
37
|
* "Read-only file system" and an escalation still failed "approval policy is
|
|
@@ -88,8 +92,7 @@ export const NO_SUBAGENTS = Object.freeze(['-c', 'features.multi_agent=false', '
|
|
|
88
92
|
* and V2. Older/unmeasured binaries have no inheritance evidence here. */
|
|
89
93
|
export const CODEX_SUBAGENTS_MIN = '0.160.0';
|
|
90
94
|
export function codexSubagentsSafe(version) {
|
|
91
|
-
|
|
92
|
-
return order === 0 || order === 1;
|
|
95
|
+
return versionAtLeast(String(version ?? '').replace(/^codex-cli\s+/, ''), CODEX_SUBAGENTS_MIN);
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
/**
|
|
@@ -183,7 +186,7 @@ export const CODEX_RUNTIME = {
|
|
|
183
186
|
bin: 'codex',
|
|
184
187
|
install: 'npm i -g @openai/codex',
|
|
185
188
|
login: 'codex login',
|
|
186
|
-
|
|
189
|
+
liveInput: false,
|
|
187
190
|
/**
|
|
188
191
|
* BUILD AND CONSULT. See `args()` below for how consult is expressed — the
|
|
189
192
|
* short version is that Codex keeps the promise at the kernel rather than in
|
|
@@ -296,7 +299,7 @@ export const CODEX_RUNTIME = {
|
|
|
296
299
|
// before it started. `-c sandbox_mode=` means the same on both.
|
|
297
300
|
if (writableDirs.length) a.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(writableDirs)}`);
|
|
298
301
|
a.push('-c', `sandbox_mode="${SAFE ? 'workspace-write' : 'danger-full-access'}"`);
|
|
299
|
-
if (cardTurn) a.push('-c', 'approval_policy="never"');
|
|
302
|
+
if (cardTurn) a.push('-c', 'approval_policy="never"', ...(agentTools?.codexApprovals ?? []));
|
|
300
303
|
a.push(...(cardTurn && profile === 'build' && codexSubagentsSafe(codexVersion)
|
|
301
304
|
? ['-c', 'features.multi_agent=true', '-c', 'features.goals=false']
|
|
302
305
|
: 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
|
/**
|
|
@@ -105,10 +110,10 @@ export function codexServerRows(servers, env = process.env) {
|
|
|
105
110
|
reason = typeof s.disabled_reason === 'string' ? s.disabled_reason.slice(0, 200) : null;
|
|
106
111
|
} else if (typeof t.command === 'string' && commandFound(t.command, env) === false) {
|
|
107
112
|
status = 'failed';
|
|
108
|
-
reason =
|
|
113
|
+
reason = mcpCommandNotFoundReason(basename(t.command).slice(0, 60));
|
|
109
114
|
} else if (unset.length) {
|
|
110
115
|
status = 'failed';
|
|
111
|
-
reason =
|
|
116
|
+
reason = mcpEnvUnsetReason([...new Set(unset)].sort());
|
|
112
117
|
} else if (['needs_auth', 'not_logged_in', 'notLoggedIn'].includes(auth)) {
|
|
113
118
|
status = 'needs-auth';
|
|
114
119
|
} else if (auth === 'failed') {
|
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -131,7 +131,9 @@ export const runtimeLabel = (id) => runtimeById(id)?.label ?? String(id);
|
|
|
131
131
|
* So the restriction is gone and this is back to "installed, and this module
|
|
132
132
|
* knows how to spawn it" — which is what the registry's `live` flag always
|
|
133
133
|
* described: not which runtimes can run, but which get a session instead of a
|
|
134
|
-
* subprocess.
|
|
134
|
+
* subprocess. (That flag is gone: since 2026-10-18 the rows carry
|
|
135
|
+
* `liveInput`, a different fact — whether follow-ups reach the running CLI
|
|
136
|
+
* on stdin.)
|
|
135
137
|
*
|
|
136
138
|
* LIVE is no longer read here. That is deliberate and load-bearing: this
|
|
137
139
|
* predicate feeds both the roster report AND the claim, and if the two ever
|
|
@@ -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/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
|
+
};
|