flowviant 0.116.0 → 0.124.0
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 +66 -12
- package/bin/cli.mjs +50 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentAsk.mjs +123 -0
- package/bin/lib/agentBrief.mjs +36 -0
- package/bin/lib/agentFootprint.mjs +76 -0
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +134 -31
- package/bin/lib/agentPrerequisiteContract.mjs +43 -0
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +5 -8
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactContracts.mjs +91 -20
- package/bin/lib/artifactIteration.mjs +182 -0
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactRecovery.mjs +36 -0
- package/bin/lib/artifactReporter.mjs +194 -22
- package/bin/lib/artifacts.mjs +70 -63
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudeAuth.mjs +15 -5
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +227 -54
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +27 -13
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexProjectRead.mjs +63 -0
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +193 -0
- package/bin/lib/commandSpecs.mjs +6 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +90 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +6 -5
- package/bin/lib/discoveryContract.mjs +19 -0
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +53 -13
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +172 -171
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/mcpAllow.mjs +60 -0
- package/bin/lib/hooks/readGuard.mjs +54 -11
- package/bin/lib/imageArtifact.mjs +74 -0
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/maintenance.mjs +47 -0
- package/bin/lib/maintenanceInventory.mjs +39 -0
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/mcpInstall.mjs +163 -0
- package/bin/lib/mcpInventory.mjs +119 -0
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/planDependencies.mjs +70 -0
- package/bin/lib/postureTools.mjs +85 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +328 -19
- package/bin/lib/projectTools.mjs +109 -34
- package/bin/lib/prompts.mjs +205 -60
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/repositoryBaseline.mjs +68 -0
- package/bin/lib/runTurn.mjs +286 -209
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +215 -21
- package/bin/lib/runtimeClaude.mjs +78 -10
- package/bin/lib/runtimeCodex.mjs +217 -209
- package/bin/lib/runtimeDetection.mjs +8 -2
- package/bin/lib/runtimeEvents.mjs +28 -11
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +99 -14
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnDiagnostics.mjs +93 -0
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +16 -4
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +44 -13
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentBrief.mjs +92 -0
- package/bin/lib/workAgentBriefRunner.mjs +59 -0
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +82 -199
- package/bin/lib/workAgentPrecheck.mjs +76 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +131 -67
- package/bin/lib/workAgentTurnOutcome.mjs +11 -2
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +64 -7
- package/bin/lib/workArtifactReconcile.mjs +79 -0
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +119 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +18 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +113 -31
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +256 -199
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workToolLog.mjs +70 -3
- package/bin/lib/workTurnShape.mjs +165 -0
- package/npm-shrinkwrap.json +69 -0
- package/package.json +9 -4
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
package/bin/lib/runTurn.mjs
CHANGED
|
@@ -10,16 +10,19 @@
|
|
|
10
10
|
* (runtimeEvents.mjs).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { runIsolatedResearch } from './codexResearch.mjs';
|
|
13
14
|
import { spawn } from 'node:child_process';
|
|
14
15
|
import { runtimeById } from './runtimes.mjs';
|
|
15
|
-
import {
|
|
16
|
-
import { openBrowserHome, withoutWindows } from './noWindowEnv.mjs';
|
|
16
|
+
import { openBrowserHome, unattendedEnv } from './noWindowEnv.mjs';
|
|
17
17
|
import { resolveTurnProfile } from './turnProfile.mjs';
|
|
18
18
|
import { claudePermFor } from './claudePosture.mjs';
|
|
19
19
|
import { handleStreamLine } from './claudeStream.mjs';
|
|
20
20
|
import { never, workLanesStopped } from './standDownGate.mjs';
|
|
21
21
|
import { learnCodexLimits, recordClaudeRateLimit } from './runtimeLimits.mjs';
|
|
22
|
+
import { initHarvestFor, recordMcpServers, recordSkills } from './runtimeCapabilities.mjs';
|
|
22
23
|
import { codexTurnModel } from './turnModel.mjs';
|
|
24
|
+
import { printableText } from './printable.mjs';
|
|
25
|
+
import { startTurnDiagnostics } from './turnDiagnostics.mjs';
|
|
23
26
|
|
|
24
27
|
/** How long a Codex turn's answer may wait on its rollout's model line. */
|
|
25
28
|
const CODEX_MODEL_READ_MS = 1_500;
|
|
@@ -37,7 +40,9 @@ const CODEX_MODEL_READ_MS = 1_500;
|
|
|
37
40
|
* needs it: the daemon authenticates to the server itself, and a turn that
|
|
38
41
|
* needs the project's tools gets its own per-turn token through `mcpEnv` or
|
|
39
42
|
* the MCP config file. So those two names, and only those, are removed —
|
|
40
|
-
* by the one rule in machineEnv.mjs,
|
|
43
|
+
* by the one rule in machineEnv.mjs, composed for every unattended CLI child
|
|
44
|
+
* by `unattendedEnv` (noWindowEnv.mjs), which the project check, the skills
|
|
45
|
+
* probe and the Codex offerings reads obey too.
|
|
41
46
|
*/
|
|
42
47
|
/**
|
|
43
48
|
* …AND, UNLESS A PERSON IS AT THE KEYBOARD, NO WAY TO A WINDOW (2026-09-29).
|
|
@@ -49,21 +54,27 @@ const CODEX_MODEL_READ_MS = 1_500;
|
|
|
49
54
|
*/
|
|
50
55
|
export function cliEnv(mcpEnv, { display = false, browserHome = null } = {}) {
|
|
51
56
|
// After the merge, so an mcpEnv can never smuggle one back in.
|
|
52
|
-
|
|
53
|
-
return display === true ? env : withoutWindows(env, browserHome);
|
|
57
|
+
return unattendedEnv({ ...process.env, ...(mcpEnv ?? {}) }, { display, browserHome });
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
// One
|
|
60
|
+
// One CLI turn. Output is captured (for sentinel detection) and streamed
|
|
57
61
|
// through, line-prefixed with the worker label so a fleet stays legible.
|
|
58
62
|
//
|
|
59
|
-
//
|
|
60
|
-
// stream
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// `onActivity` so the caller can forward progress. Build-agent turns leave it
|
|
64
|
-
// off and keep the raw text passthrough + line sentinels.
|
|
63
|
+
// The event stream is parsed, always: only the humanized tool activity reaches
|
|
64
|
+
// the console (a legible stream of `read …`, `grep …`, `+ node …`), assistant
|
|
65
|
+
// text is folded into the returned string for sentinel detection, and each
|
|
66
|
+
// activity is handed to `onActivity` so the caller can forward progress.
|
|
65
67
|
export function runTurn(opts) {
|
|
66
|
-
|
|
68
|
+
// A Codex write-up runs over an isolated copy of its worktree
|
|
69
|
+
// (codexResearch.mjs). After the teardown it copies nothing either: the
|
|
70
|
+
// stand-down gate below is asked before the copy, not only before the spawn.
|
|
71
|
+
if (opts.runtime === 'codex' && opts.profile === 'research')
|
|
72
|
+
return workLanesStopped() ? never() : runIsolatedResearch(opts, runPlainTurn);
|
|
73
|
+
return runPlainTurn(opts);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runPlainTurn(opts) {
|
|
77
|
+
const { prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, agentTools, runtime = 'claude', label, onSpawn, answerFromResult, onActivity, onToolEvent, onInit, onUsage, onRateLimit, onModel, onThreadId, onAnswer, vaultDir, knowledgeDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId, display, browserHome } = opts;
|
|
67
78
|
/**
|
|
68
79
|
* AFTER THE TEARDOWN, NO TURN STARTS AND NO TURN ENDS (standDownGate.mjs).
|
|
69
80
|
* A stand-down that drains a deploy keeps this process alive; without this
|
|
@@ -157,12 +168,12 @@ export function runTurn(opts) {
|
|
|
157
168
|
// to get wrong: the caller named one profile (turnProfile.mjs).
|
|
158
169
|
const args = rt.args({
|
|
159
170
|
personal,
|
|
171
|
+
researchIsolation: opts.researchIsolation,
|
|
160
172
|
prompt,
|
|
161
173
|
system,
|
|
162
174
|
model,
|
|
163
175
|
effort,
|
|
164
176
|
resume,
|
|
165
|
-
streamJson,
|
|
166
177
|
profile: turnProfile.adapterProfile,
|
|
167
178
|
agentTools,
|
|
168
179
|
// Adopting a terminal session (workSessionTurns.mjs): Claude turns it into
|
|
@@ -172,9 +183,9 @@ export function runTurn(opts) {
|
|
|
172
183
|
// so a mis-wired adoption fails as a loud turn error rather than a
|
|
173
184
|
// silent fresh conversation wearing an adopted session's name.
|
|
174
185
|
adoptResumeId,
|
|
175
|
-
// Only the wiki profile uses it, but it is passed unconditionally:
|
|
176
|
-
// runtime that
|
|
177
|
-
// and
|
|
186
|
+
// Only the wiki profile uses it, but it is passed unconditionally: every
|
|
187
|
+
// runtime that runs a cartographer path-scopes its writes to the vault
|
|
188
|
+
// and needs to know WHERE it is — Claude's list names it too (2026-10-10).
|
|
178
189
|
vaultDir,
|
|
179
190
|
// The turn's own directory, for a runtime that fences its writes to a
|
|
180
191
|
// path under it: Codex's image profile makes `<cwd>/.flowviant/artifacts`
|
|
@@ -185,8 +196,8 @@ export function runTurn(opts) {
|
|
|
185
196
|
// prompt hands an absolute path OUTSIDE the cwd — and a curated Claude
|
|
186
197
|
// profile (the capture chat's read-only list, `FLOWVIANT_SAFE=1`) may
|
|
187
198
|
// refuse a read there. `--add-dir` says the directory is one it may read.
|
|
188
|
-
//
|
|
189
|
-
//
|
|
199
|
+
// Claude adds it as a directory; Codex includes only this library in
|
|
200
|
+
// its project read boundary; agy's `--add-dir` is spent on the vault.
|
|
190
201
|
knowledgeDir,
|
|
191
202
|
// Structured-output flags for the MEDIATED path. Handed to the adapter
|
|
192
203
|
// rather than appended here for the same reason `mcp` is: Codex takes its
|
|
@@ -196,7 +207,16 @@ export function runTurn(opts) {
|
|
|
196
207
|
// PLAN MODE REPLACES THE POSTURE, never joins it: beside
|
|
197
208
|
// `--dangerously-skip-permissions` the bypass wins silently (measured —
|
|
198
209
|
// see PLAN_MODE_PERM).
|
|
199
|
-
|
|
210
|
+
//
|
|
211
|
+
// …AND THE PROJECT SERVERS IT MAY CALL (2026-10-10): the names of the
|
|
212
|
+
// base `.mcp.json` servers the caller mounted, each allowed by name on a
|
|
213
|
+
// profile that mounts them (`projectMcp`, turnProfile.mjs) — never on
|
|
214
|
+
// one that does not, whatever a caller passes.
|
|
215
|
+
perm: claudePermFor(turnProfile.name, knowledgeDir, vaultDir, turnProfile.projectMcp ? (opts.projectMcpServers ?? []) : []),
|
|
216
|
+
// …AND THE MACHINE'S OWN MCP SERVERS (2026-10-11): the `.mcp.json`
|
|
217
|
+
// server names an agent lane disables beside them, on a profile that
|
|
218
|
+
// mounts project servers — never on one that does not.
|
|
219
|
+
machineMcp: turnProfile.projectMcp ? opts.machineMcp : undefined,
|
|
200
220
|
// Handed to the adapter rather than appended here, because WHERE these go
|
|
201
221
|
// is a property of the CLI: Codex reads its prompt as a trailing
|
|
202
222
|
// positional, so a flag after it is a flag in the wrong place.
|
|
@@ -223,45 +243,78 @@ export function runTurn(opts) {
|
|
|
223
243
|
// Which credential is correct, and whether an account may be shared, is
|
|
224
244
|
// between the operator and the vendor. Flowviant does not detect it and does
|
|
225
245
|
// not enforce it; it runs the CLI the ordinary way and relays what happens.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// Only ADDS to the environment (the worker token, for runtimes that read
|
|
254
|
-
// it from there). Never replaces it: the CLI's own credentials live in
|
|
255
|
-
// this environment, and handing it a curated one signs it out. What it
|
|
256
|
-
// REMOVES is this daemon's machine credential and, for a turn nobody is
|
|
257
|
-
// sitting at, every way to a window — see `cliEnv`.
|
|
258
|
-
// The login's literal secrets first, so nothing of the lane's (its MCP
|
|
259
|
-
// token, its CODEX_HOME) can be overwritten by one.
|
|
260
|
-
env: cliEnv(personal?.env ? { ...personal.env, ...(mcpEnv ?? {}) } : mcpEnv, {
|
|
246
|
+
/**
|
|
247
|
+
* WHAT THE ADAPTER WROTE FOR THIS ARGV goes when the turn settles — once,
|
|
248
|
+
* on close or on a spawn error: Claude's fenced `--settings` file, which
|
|
249
|
+
* holds the person's login settings (claudePersonal.mjs, 2026-10-10).
|
|
250
|
+
* Everything between the argv and the spawn sits inside the `try` below,
|
|
251
|
+
* so no throw on the way leaves that file behind: the sweep takes only a
|
|
252
|
+
* dead process's leftovers, and this one names the daemon's own pid.
|
|
253
|
+
*/
|
|
254
|
+
let argsReleased = false;
|
|
255
|
+
const releaseArgs = () => {
|
|
256
|
+
if (argsReleased) return;
|
|
257
|
+
argsReleased = true;
|
|
258
|
+
try {
|
|
259
|
+
rt.releaseArgs?.(args);
|
|
260
|
+
} catch {
|
|
261
|
+
/* best effort — a later write sweeps it */
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
let browser = null;
|
|
265
|
+
let child;
|
|
266
|
+
let finishDiagnostics = () => {};
|
|
267
|
+
try {
|
|
268
|
+
// A turn nobody is sitting at keeps its browsers in a home of its own —
|
|
269
|
+
// the lane's kept one (an agent's), else this spawn's, removed when the
|
|
270
|
+
// CLI closes. See `cliEnv`.
|
|
271
|
+
browser = display === true ? null : openBrowserHome(browserHome);
|
|
272
|
+
const childEnv = cliEnv(personal?.env ? { ...personal.env, ...(mcpEnv ?? {}) } : mcpEnv, {
|
|
261
273
|
display: display === true,
|
|
262
274
|
browserHome: browser?.dir,
|
|
263
|
-
})
|
|
264
|
-
|
|
275
|
+
});
|
|
276
|
+
finishDiagnostics = startTurnDiagnostics({ runtime, bin: rt.bin, env: childEnv,
|
|
277
|
+
cwd, args, profile: turnProfile.name, planId: opts.diagnosticPlanId });
|
|
278
|
+
child = spawn(rt.bin, args, {
|
|
279
|
+
cwd,
|
|
280
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
281
|
+
/**
|
|
282
|
+
* ITS OWN PROCESS GROUP, so what the agent starts stays attributable.
|
|
283
|
+
*
|
|
284
|
+
* Everything the CLI spawns inherits this pgid and KEEPS it through
|
|
285
|
+
* `nohup` and `setsid` — which is exactly when attribution by descendancy
|
|
286
|
+
* fails, because reparenting to init breaks the ppid chain the moment a
|
|
287
|
+
* process becomes long-running. `processes.mjs` reads the group; the
|
|
288
|
+
* Workbench renders it.
|
|
289
|
+
*
|
|
290
|
+
* TEARDOWN IS DELIBERATELY UNCHANGED: `shutdownWork` still SIGTERMs this
|
|
291
|
+
* CHILD and never the group. Signalling the group would kill the dev
|
|
292
|
+
* server the driver started every time the daemon restarts — including
|
|
293
|
+
* on an ordinary auto-update, unattended — which is the outcome the
|
|
294
|
+
* deleted dev-run supervisor spent a whole registry avoiding. Flowviant
|
|
295
|
+
* does not manage those processes; it reports them.
|
|
296
|
+
*
|
|
297
|
+
* Not `unref`'d: the daemon must still wait on this turn.
|
|
298
|
+
*/
|
|
299
|
+
detached: true,
|
|
300
|
+
// Only ADDS to the environment (the worker token, for runtimes that read
|
|
301
|
+
// it from there). Never replaces it: the CLI's own credentials live in
|
|
302
|
+
// this environment, and handing it a curated one signs it out. What it
|
|
303
|
+
// REMOVES is this daemon's machine credential and, for a turn nobody is
|
|
304
|
+
// sitting at, every way to a window — see `cliEnv`.
|
|
305
|
+
// The login's literal secrets first, so nothing of the lane's (its MCP
|
|
306
|
+
// token, its CODEX_HOME) can be overwritten by one.
|
|
307
|
+
env: childEnv,
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
finishDiagnostics({ spawnError: typeof error?.code === 'string' ? error.code : 'SPAWN_THROW' });
|
|
311
|
+
// A spawn that throws (an argument it refuses), or anything on the way
|
|
312
|
+
// to it, still takes what the adapter wrote with it — and this spawn's
|
|
313
|
+
// own browser home.
|
|
314
|
+
releaseArgs();
|
|
315
|
+
browser?.cleanup();
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
265
318
|
if (browser) {
|
|
266
319
|
child.once('close', browser.cleanup);
|
|
267
320
|
child.once('error', browser.cleanup);
|
|
@@ -272,170 +325,194 @@ export function runTurn(opts) {
|
|
|
272
325
|
const spawnedAt = Date.now();
|
|
273
326
|
let out = '';
|
|
274
327
|
const pfx = label ? `${label} ` : '';
|
|
275
|
-
|
|
328
|
+
/**
|
|
329
|
+
* THE ONE SINK FOR WHAT A CLI OR A MODEL WROTE (2026-10-10, the audit's
|
|
330
|
+
* F133): activity labels — a Bash command, a Codex `say`, a file path —
|
|
331
|
+
* and the CLI's raw stderr. A prompt-injected agent can put an OSC 52 in
|
|
332
|
+
* a tool command as easily as in prose, and this wrote it to the
|
|
333
|
+
* operator's terminal as-is. Scrubbed HERE, before the daemon's own
|
|
334
|
+
* coloured `label` is added, so the prefix keeps its colour and nothing
|
|
335
|
+
* the model wrote keeps an escape sequence (printable.mjs, `printableText`).
|
|
336
|
+
*/
|
|
337
|
+
const emit = (s) => {
|
|
338
|
+
const text = printableText(String(s));
|
|
339
|
+
process.stdout.write(pfx ? text.replace(/\n/g, `\n${pfx}`) : text);
|
|
340
|
+
};
|
|
276
341
|
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// DECODED AS A STREAM, never chunk by chunk (2026-09-24, the audit): a
|
|
344
|
-
// pipe read can end inside a multi-byte character — the em dash models
|
|
345
|
-
// write constantly — and `d.toString()` per chunk turned each half into
|
|
346
|
-
// U+FFFD, which JSON.parse accepts and the reply then carried verbatim.
|
|
347
|
-
// `setEncoding` holds the partial bytes over to the next chunk.
|
|
348
|
-
child.stdout.setEncoding('utf8');
|
|
349
|
-
child.stderr.setEncoding('utf8');
|
|
350
|
-
child.stdout.on('data', (d) => {
|
|
351
|
-
buf += d;
|
|
352
|
-
let nl;
|
|
353
|
-
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
354
|
-
const line = buf.slice(0, nl);
|
|
355
|
-
buf = buf.slice(nl + 1);
|
|
356
|
-
if (line.trim()) onLine(line);
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
// stderr is not JSON (warnings/errors) — pass through and keep for sentinels.
|
|
360
|
-
child.stderr.on('data', (s) => {
|
|
361
|
-
out += s;
|
|
362
|
-
emit(s);
|
|
363
|
-
});
|
|
364
|
-
child.on('error', (e) => {
|
|
365
|
-
if (e.code === 'ENOENT') {
|
|
366
|
-
// A MISSING CLI FAILS THE TURN, NOT THE DAEMON. This called
|
|
367
|
-
// process.exit(1), which was defensible while `claude` was the only
|
|
368
|
-
// runtime and preflight refused to start without it — the process
|
|
369
|
-
// could not reach here. Both halves of that are gone: preflight is now
|
|
370
|
-
// fatal only when NOTHING is drivable, so a Codex-only machine starts
|
|
371
|
-
// legitimately, and the wiki/plan-check/consult turns still ask for
|
|
372
|
-
// Claude by default. On such a machine the first wiki sweep would have
|
|
373
|
-
// killed the whole daemon, taking every in-flight build with it,
|
|
374
|
-
// because one background job could not find one binary.
|
|
375
|
-
console.error(`\nerror: '${rt.bin}' CLI not found on PATH. Install ${rt.label} first: ${rt.install}`);
|
|
376
|
-
resolve('');
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
console.error(e);
|
|
380
|
-
resolve(out);
|
|
381
|
-
});
|
|
382
|
-
child.on('close', () => {
|
|
383
|
-
if (buf.trim()) onLine(buf);
|
|
384
|
-
const handBack = () => {
|
|
385
|
-
resolve(out);
|
|
386
|
-
// After the answer is handed back, never before it: see `onRate`.
|
|
387
|
-
if (codexThread) {
|
|
388
|
-
const thread = codexThread;
|
|
389
|
-
setImmediate(() => void learnCodexLimits(thread));
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
/**
|
|
393
|
-
* A CODEX TURN'S MODEL IS IN ITS ROLLOUT, so a caller that asked for
|
|
394
|
-
* it waits for one bounded read before the answer goes back — the
|
|
395
|
-
* settle it rides is built from that answer. Only then: a caller that
|
|
396
|
-
* passes no `onModel` hands back exactly as before. A read that fails
|
|
397
|
-
* or overruns reports nothing (never a guess).
|
|
398
|
-
*/
|
|
399
|
-
if (codexThread && onModel && !ranOn) {
|
|
400
|
-
const thread = codexThread;
|
|
401
|
-
let timer = null;
|
|
402
|
-
const cap = new Promise((res) => {
|
|
403
|
-
timer = setTimeout(() => res(null), CODEX_MODEL_READ_MS);
|
|
404
|
-
});
|
|
405
|
-
void Promise.race([codexTurnModel(thread, { since: spawnedAt }), cap])
|
|
406
|
-
.then(onRanOn, () => {})
|
|
407
|
-
.finally(() => {
|
|
408
|
-
clearTimeout(timer);
|
|
409
|
-
handBack();
|
|
410
|
-
});
|
|
411
|
-
return;
|
|
342
|
+
// EVERY TURN IS LINE-PARSED (2026-10-10, the audit). Claude's adapter
|
|
343
|
+
// always asks for `--output-format stream-json` and every other runtime
|
|
344
|
+
// brings its own `parse` — for Codex the JSONL stream is the only output
|
|
345
|
+
// there is. A raw-text path survived for a `streamJson: false` no caller
|
|
346
|
+
// passed any more, with a second copy of the spawn-error rule that had
|
|
347
|
+
// already drifted once (it called process.exit(1)); both are gone. A line
|
|
348
|
+
// that is not JSON is still kept as text (claudeStream.mjs), and stderr
|
|
349
|
+
// still lands in the haystack the sentinels read.
|
|
350
|
+
let buf = '';
|
|
351
|
+
const appendText = (t) => {
|
|
352
|
+
out += t;
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* THE PLAN'S WINDOWS, LEARNED OFF EVERY TURN THIS DAEMON RUNS (0.109.0).
|
|
356
|
+
*
|
|
357
|
+
* This is the one spawn point — agent turns, Terminal tabs, capture
|
|
358
|
+
* chats, the planner, the pre-review and the wiki all come through here
|
|
359
|
+
* — so it is the one place that sees every CLI's word on its plan.
|
|
360
|
+
* Claude says it on the stream (`rate_limit_event`), recorded as it
|
|
361
|
+
* arrives; a caller's own `onRateLimit` still hears it. Codex writes it
|
|
362
|
+
* into the thread's rollout file instead, so the thread this turn spoke
|
|
363
|
+
* under (announced on `thread.started`, else the one it resumed) is read
|
|
364
|
+
* AFTER the child closes, on the next tick — the turn's answer never
|
|
365
|
+
* waits on a file read. See runtimeLimits.mjs.
|
|
366
|
+
*/
|
|
367
|
+
const onRate = (info) => {
|
|
368
|
+
recordClaudeRateLimit(info);
|
|
369
|
+
onRateLimit?.(info);
|
|
370
|
+
};
|
|
371
|
+
let codexThread = rt.id === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
|
|
372
|
+
/**
|
|
373
|
+
* THE MODEL THIS SPAWN RAN ON, ONCE (2026-09-29, turnModel.mjs). Claude
|
|
374
|
+
* names it in its init event and again on every reply; the first valid
|
|
375
|
+
* one is the answer, so the init wins and the first reply is the
|
|
376
|
+
* fallback. Codex names it only in its rollout, read at close below.
|
|
377
|
+
*/
|
|
378
|
+
let ranOn = null;
|
|
379
|
+
const onRanOn = (m) => {
|
|
380
|
+
if (ranOn || !m) return;
|
|
381
|
+
ranOn = m;
|
|
382
|
+
onModel?.(m);
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* WHAT THE CLI SAID IT HAS — its skills and MCP servers — recorded HERE
|
|
386
|
+
* for every lane (2026-10-10, the audit), beside the plan's windows: four
|
|
387
|
+
* lanes each kept a copy of the harvest, and they drifted until a fenced
|
|
388
|
+
* turn's narrowed list overwrote the machine's report. Only a turn whose
|
|
389
|
+
* argv narrows neither is recorded (`initHarvestFor`,
|
|
390
|
+
* runtimeCapabilities.mjs); a caller's own `onInit` still hears every
|
|
391
|
+
* init.
|
|
392
|
+
*/
|
|
393
|
+
const harvest = initHarvestFor(rt, args);
|
|
394
|
+
const onInitAll = (i) => {
|
|
395
|
+
if (harvest.skills) recordSkills(i?.skills);
|
|
396
|
+
if (harvest.mcp) recordMcpServers(i?.mcpServers, i?.tools);
|
|
397
|
+
onInit?.(i);
|
|
398
|
+
};
|
|
399
|
+
/** One line of the child's stdout, in whichever dialect it speaks. */
|
|
400
|
+
let cliFailed = false;
|
|
401
|
+
const onLine = (line) => {
|
|
402
|
+
try {
|
|
403
|
+
const event = JSON.parse(line);
|
|
404
|
+
if ((event.type === 'result' && event.is_error === true) ||
|
|
405
|
+
event.type === 'error' || event.type === 'turn.failed') {
|
|
406
|
+
cliFailed = true;
|
|
407
|
+
opts.onCliFailure?.();
|
|
412
408
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
409
|
+
} catch { /* ordinary non-JSON CLI output */ }
|
|
410
|
+
if (!rt.parse)
|
|
411
|
+
return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit: onInitAll, onUsage, onRateLimit: onRate, onModel: onRanOn });
|
|
412
|
+
const ev = rt.parse(line, cwd);
|
|
413
|
+
if (!ev) return;
|
|
414
|
+
// The conversation id, when the runtime announces one (codex's
|
|
415
|
+
// thread.started). Purely additive: callers that pass no onThreadId —
|
|
416
|
+
// every dispatch path — see zero behavior change.
|
|
417
|
+
if (ev.threadId) {
|
|
418
|
+
if (rt.id === 'codex') codexThread = ev.threadId;
|
|
419
|
+
onThreadId?.(ev.threadId);
|
|
420
|
+
}
|
|
421
|
+
// The runtime's own token count (codex's `turn.completed`) — the same
|
|
422
|
+
// `onUsage` Claude's result event feeds, so a caller charges a turn
|
|
423
|
+
// identically whichever CLI ran it.
|
|
424
|
+
if (ev.toolEvent) onToolEvent?.(...ev.toolEvent);
|
|
425
|
+
if (ev.usage) onUsage?.(ev.usage);
|
|
426
|
+
// One agent MESSAGE, alone — see parseCodexLine's `answer`. Last call
|
|
427
|
+
// wins at the caller, which is what "the final answer" means.
|
|
428
|
+
if (typeof ev.answer === 'string') onAnswer?.(ev.answer);
|
|
429
|
+
if (ev.text) appendText(ev.text);
|
|
430
|
+
if (ev.activity) {
|
|
431
|
+
emit(`${ev.activity.label}\n`);
|
|
432
|
+
onActivity?.(ev.activity);
|
|
433
|
+
}
|
|
421
434
|
};
|
|
422
|
-
//
|
|
435
|
+
// DECODED AS A STREAM, never chunk by chunk (2026-09-24, the audit): a
|
|
436
|
+
// pipe read can end inside a multi-byte character — the em dash models
|
|
437
|
+
// write constantly — and `d.toString()` per chunk turned each half into
|
|
438
|
+
// U+FFFD, which JSON.parse accepts and the reply then carried verbatim.
|
|
439
|
+
// `setEncoding` holds the partial bytes over to the next chunk.
|
|
423
440
|
child.stdout.setEncoding('utf8');
|
|
424
441
|
child.stderr.setEncoding('utf8');
|
|
425
|
-
child.stdout.on('data',
|
|
426
|
-
|
|
427
|
-
|
|
442
|
+
child.stdout.on('data', (d) => {
|
|
443
|
+
buf += d;
|
|
444
|
+
let nl;
|
|
445
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
446
|
+
const line = buf.slice(0, nl);
|
|
447
|
+
buf = buf.slice(nl + 1);
|
|
448
|
+
if (line.trim()) onLine(line);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
// stderr is not JSON (warnings/errors) — pass through and keep for sentinels.
|
|
452
|
+
child.stderr.on('data', (s) => {
|
|
453
|
+
out += s;
|
|
454
|
+
emit(s);
|
|
455
|
+
});
|
|
456
|
+
/**
|
|
457
|
+
* A SPAWN THAT FAILS FAILS THE TURN, NOT THE DAEMON — the one copy of the
|
|
458
|
+
* rule. A missing CLI resolves '' with the install line; this called
|
|
459
|
+
* process.exit(1), which was defensible while `claude` was the only
|
|
460
|
+
* runtime and preflight refused to start without it — the process could
|
|
461
|
+
* not reach here. Both halves of that are gone: preflight is now fatal
|
|
462
|
+
* only when NOTHING is drivable, so a Codex-only machine starts
|
|
463
|
+
* legitimately, and the wiki/plan-check/consult turns still ask for Claude
|
|
464
|
+
* by default. On such a machine the first wiki sweep would have killed the
|
|
465
|
+
* whole daemon, taking every in-flight build with it, because one
|
|
466
|
+
* background job could not find one binary. Any other spawn error
|
|
467
|
+
* resolves what was captured so far.
|
|
468
|
+
*/
|
|
469
|
+
const onSpawnError = (e) => {
|
|
470
|
+
finishDiagnostics({ pid: child.pid, spawnError: typeof e.code === 'string' ? e.code : 'SPAWN_ERROR' });
|
|
471
|
+
releaseArgs();
|
|
428
472
|
if (e.code === 'ENOENT') {
|
|
429
|
-
// A MISSING CLI FAILS THE TURN, NOT THE DAEMON — same fix as the
|
|
430
|
-
// line-parsed path above; this raw-output duplicate used to
|
|
431
|
-
// process.exit(1) and take every in-flight worker down with it.
|
|
432
473
|
console.error(`\nerror: '${rt.bin}' CLI not found on PATH. Install ${rt.label} first: ${rt.install}`);
|
|
433
474
|
resolve('');
|
|
434
475
|
return;
|
|
435
476
|
}
|
|
436
477
|
console.error(e);
|
|
437
478
|
resolve(out);
|
|
479
|
+
};
|
|
480
|
+
child.on('error', onSpawnError);
|
|
481
|
+
child.on('close', (code, signal) => {
|
|
482
|
+
releaseArgs();
|
|
483
|
+
opts.onExit?.(code, signal);
|
|
484
|
+
if (buf.trim()) onLine(buf);
|
|
485
|
+
finishDiagnostics({ code, signal, pid: child.pid, output: out, cliFailed });
|
|
486
|
+
const handBack = () => {
|
|
487
|
+
resolve(out);
|
|
488
|
+
// After the answer is handed back, never before it: see `onRate`.
|
|
489
|
+
if (codexThread) {
|
|
490
|
+
const thread = codexThread;
|
|
491
|
+
setImmediate(() => void learnCodexLimits(thread));
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
/**
|
|
495
|
+
* A CODEX TURN'S MODEL IS IN ITS ROLLOUT, so a caller that asked for
|
|
496
|
+
* it waits for one bounded read before the answer goes back — the
|
|
497
|
+
* settle it rides is built from that answer. Only then: a caller that
|
|
498
|
+
* passes no `onModel` hands back exactly as before. A read that fails
|
|
499
|
+
* or overruns reports nothing (never a guess).
|
|
500
|
+
*/
|
|
501
|
+
if (codexThread && onModel && !ranOn) {
|
|
502
|
+
const thread = codexThread;
|
|
503
|
+
let timer = null;
|
|
504
|
+
const cap = new Promise((res) => {
|
|
505
|
+
timer = setTimeout(() => res(null), CODEX_MODEL_READ_MS);
|
|
506
|
+
});
|
|
507
|
+
void Promise.race([codexTurnModel(thread, { since: spawnedAt }), cap])
|
|
508
|
+
.then(onRanOn, () => {})
|
|
509
|
+
.finally(() => {
|
|
510
|
+
clearTimeout(timer);
|
|
511
|
+
handBack();
|
|
512
|
+
});
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
handBack();
|
|
438
516
|
});
|
|
439
|
-
child.on('close', () => resolve(out));
|
|
440
517
|
});
|
|
441
518
|
}
|
|
@@ -136,6 +136,16 @@ export const ANTIGRAVITY_RUNTIME = {
|
|
|
136
136
|
/** No measured ladder, and ignorance never refuses: the full list, as the
|
|
137
137
|
* server's `RUNTIME_EFFORTS.antigravity` keeps it. */
|
|
138
138
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
139
|
+
/**
|
|
140
|
+
* A TERMINAL TAB ON AGY RUNS PLAIN (audit 2026-10-10, F159; see
|
|
141
|
+
* runtimeClaude.mjs for the three facts): no MCP to mount — no credential,
|
|
142
|
+
* no cards, no streaming; the answer lands through the daemon's own report.
|
|
143
|
+
* No plan mode. Adoption MOVES the conversation (`--conversation <id>`,
|
|
144
|
+
* below): agy has no fork, so the tab continues the terminal's own store.
|
|
145
|
+
*/
|
|
146
|
+
tab: 'plain',
|
|
147
|
+
planTurns: false,
|
|
148
|
+
adopt: 'move',
|
|
139
149
|
mcp: null,
|
|
140
150
|
args({ prompt, system, model, effort, resume, resumeConversationId, profile = 'build', vaultDir, resultSchemaArgs = [], adoptResumeId }) {
|
|
141
151
|
const a = [];
|
|
@@ -158,8 +168,10 @@ export const ANTIGRAVITY_RUNTIME = {
|
|
|
158
168
|
a.push(...resultSchemaArgs);
|
|
159
169
|
|
|
160
170
|
if (profile === 'wiki') {
|
|
161
|
-
// THE CARTOGRAPHER, at the
|
|
162
|
-
// allowed, network shut.
|
|
171
|
+
// THE CARTOGRAPHER, at the bar Claude's old wiki list set — writes
|
|
172
|
+
// allowed, network shut. (Claude's list scopes the write to the vault
|
|
173
|
+
// since 2026-10-10; agy's flags cannot express "write only this
|
|
174
|
+
// directory", so this one does not.) Both halves measured on 1.1.12:
|
|
163
175
|
// • headless cannot prompt, so anything needing approval is
|
|
164
176
|
// auto-denied — including the file writes a wiki turn exists to
|
|
165
177
|
// make. `--dangerously-skip-permissions` is what grants them.
|
|
@@ -169,9 +181,8 @@ export const ANTIGRAVITY_RUNTIME = {
|
|
|
169
181
|
// auto-approving the sandbox-bypass prompt). It does NOT reproduce on
|
|
170
182
|
// 1.1.12 — tested together, the write landed and the network stayed
|
|
171
183
|
// shut. Re-check on upgrade: if it ever does cancel, this posture
|
|
172
|
-
// silently becomes "wiki turn with internet"
|
|
173
|
-
//
|
|
174
|
-
// exfil").
|
|
184
|
+
// silently becomes "wiki turn with internet" — and the vault it writes
|
|
185
|
+
// is uploaded either way.
|
|
175
186
|
if (vaultDir) a.push('--add-dir', vaultDir);
|
|
176
187
|
a.push('--sandbox', '--dangerously-skip-permissions');
|
|
177
188
|
// A sweep reads a whole repository; the 5m default would guillotine it.
|