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.
Files changed (161) hide show
  1. package/README.md +66 -12
  2. package/bin/cli.mjs +50 -17
  3. package/bin/lib/admission.mjs +7 -14
  4. package/bin/lib/agentAsk.mjs +123 -0
  5. package/bin/lib/agentBrief.mjs +36 -0
  6. package/bin/lib/agentFootprint.mjs +76 -0
  7. package/bin/lib/agentInternalWait.mjs +83 -0
  8. package/bin/lib/agentPlan.mjs +134 -31
  9. package/bin/lib/agentPrerequisiteContract.mjs +43 -0
  10. package/bin/lib/agentPublish.mjs +12 -13
  11. package/bin/lib/agentTaskKinds.mjs +5 -8
  12. package/bin/lib/agySessions.mjs +3 -3
  13. package/bin/lib/artifactContracts.mjs +91 -20
  14. package/bin/lib/artifactIteration.mjs +182 -0
  15. package/bin/lib/artifactPreview.mjs +92 -18
  16. package/bin/lib/artifactRecovery.mjs +36 -0
  17. package/bin/lib/artifactReporter.mjs +194 -22
  18. package/bin/lib/artifacts.mjs +70 -63
  19. package/bin/lib/authproxy.mjs +2 -1
  20. package/bin/lib/childEnv.mjs +9 -4
  21. package/bin/lib/claude.mjs +10 -20
  22. package/bin/lib/claudeAuth.mjs +15 -5
  23. package/bin/lib/claudePersonal.mjs +71 -6
  24. package/bin/lib/claudePosture.mjs +227 -54
  25. package/bin/lib/claudeSessions.mjs +7 -7
  26. package/bin/lib/claudeStream.mjs +27 -13
  27. package/bin/lib/cliLimit.mjs +2 -2
  28. package/bin/lib/codexHome.mjs +17 -0
  29. package/bin/lib/codexPersonal.mjs +44 -3
  30. package/bin/lib/codexProjectRead.mjs +63 -0
  31. package/bin/lib/codexRelay.mjs +21 -5
  32. package/bin/lib/codexResearch.mjs +193 -0
  33. package/bin/lib/commandSpecs.mjs +6 -0
  34. package/bin/lib/config.mjs +37 -24
  35. package/bin/lib/conversationIds.mjs +51 -0
  36. package/bin/lib/credentials.mjs +2 -7
  37. package/bin/lib/daemonLocks.mjs +90 -0
  38. package/bin/lib/daemonState.mjs +5 -5
  39. package/bin/lib/deploy.mjs +6 -9
  40. package/bin/lib/deployCheckout.mjs +26 -22
  41. package/bin/lib/deployRunner.mjs +54 -18
  42. package/bin/lib/deployWire.mjs +32 -23
  43. package/bin/lib/desktopContract.mjs +6 -5
  44. package/bin/lib/discoveryContract.mjs +19 -0
  45. package/bin/lib/env.mjs +104 -35
  46. package/bin/lib/fleet.mjs +53 -13
  47. package/bin/lib/fleetPost.mjs +172 -25
  48. package/bin/lib/fleetReports.mjs +172 -171
  49. package/bin/lib/fleetStartup.mjs +6 -6
  50. package/bin/lib/fleetWire.mjs +42 -0
  51. package/bin/lib/git.mjs +111 -61
  52. package/bin/lib/gitMergeIdentity.mjs +2 -8
  53. package/bin/lib/grant.mjs +23 -7
  54. package/bin/lib/holder.mjs +108 -81
  55. package/bin/lib/hooks/mcpAllow.mjs +60 -0
  56. package/bin/lib/hooks/readGuard.mjs +54 -11
  57. package/bin/lib/imageArtifact.mjs +74 -0
  58. package/bin/lib/install.mjs +80 -33
  59. package/bin/lib/instance.mjs +36 -83
  60. package/bin/lib/intakeDraft.mjs +4 -1
  61. package/bin/lib/knowledgeFileSync.mjs +31 -0
  62. package/bin/lib/knowledgeFiles.mjs +28 -83
  63. package/bin/lib/landed.mjs +22 -29
  64. package/bin/lib/launchCommand.mjs +91 -0
  65. package/bin/lib/listeners.mjs +0 -5
  66. package/bin/lib/localSessions.mjs +52 -39
  67. package/bin/lib/login.mjs +22 -12
  68. package/bin/lib/loginPath.mjs +22 -17
  69. package/bin/lib/machineListing.mjs +6 -3
  70. package/bin/lib/maintenance.mjs +47 -0
  71. package/bin/lib/maintenanceInventory.mjs +39 -0
  72. package/bin/lib/mcp-cli.mjs +9 -10
  73. package/bin/lib/mcpInstall.mjs +163 -0
  74. package/bin/lib/mcpInventory.mjs +119 -0
  75. package/bin/lib/noWindowEnv.mjs +23 -3
  76. package/bin/lib/ownedDirs.mjs +99 -0
  77. package/bin/lib/pinnedCapture.mjs +383 -0
  78. package/bin/lib/planDependencies.mjs +70 -0
  79. package/bin/lib/postureTools.mjs +85 -0
  80. package/bin/lib/prWorkflow.mjs +94 -17
  81. package/bin/lib/preflight.mjs +8 -25
  82. package/bin/lib/preview.mjs +19 -25
  83. package/bin/lib/previewGatePolicy.mjs +34 -9
  84. package/bin/lib/previewRegistry.mjs +4 -11
  85. package/bin/lib/printable.mjs +70 -7
  86. package/bin/lib/procRegistry.mjs +33 -13
  87. package/bin/lib/procScan.mjs +2 -3
  88. package/bin/lib/procStat.mjs +76 -0
  89. package/bin/lib/processGroup.mjs +48 -0
  90. package/bin/lib/processes.mjs +10 -24
  91. package/bin/lib/projectToolRuntimes.mjs +328 -19
  92. package/bin/lib/projectTools.mjs +109 -34
  93. package/bin/lib/prompts.mjs +205 -60
  94. package/bin/lib/repoPath.mjs +19 -0
  95. package/bin/lib/repoState.mjs +17 -19
  96. package/bin/lib/repositoryBaseline.mjs +68 -0
  97. package/bin/lib/runTurn.mjs +286 -209
  98. package/bin/lib/runtimeAntigravity.mjs +16 -5
  99. package/bin/lib/runtimeCapabilities.mjs +215 -21
  100. package/bin/lib/runtimeClaude.mjs +78 -10
  101. package/bin/lib/runtimeCodex.mjs +217 -209
  102. package/bin/lib/runtimeDetection.mjs +8 -2
  103. package/bin/lib/runtimeEvents.mjs +28 -11
  104. package/bin/lib/runtimeLimits.mjs +5 -6
  105. package/bin/lib/runtimeOfferings.mjs +99 -14
  106. package/bin/lib/runtimes.mjs +10 -0
  107. package/bin/lib/shipSweep.mjs +5 -3
  108. package/bin/lib/shot.mjs +91 -17
  109. package/bin/lib/sleep.mjs +11 -0
  110. package/bin/lib/startProject.mjs +5 -5
  111. package/bin/lib/statePaths.mjs +40 -0
  112. package/bin/lib/tty.mjs +25 -15
  113. package/bin/lib/turnDiagnostics.mjs +93 -0
  114. package/bin/lib/turnLock.mjs +7 -14
  115. package/bin/lib/turnProfile.mjs +16 -4
  116. package/bin/lib/ui.mjs +0 -3
  117. package/bin/lib/uninstall.mjs +30 -26
  118. package/bin/lib/update.mjs +79 -27
  119. package/bin/lib/uplinkScrub.mjs +296 -31
  120. package/bin/lib/vault.mjs +78 -10
  121. package/bin/lib/vaultDiff.mjs +5 -2
  122. package/bin/lib/views.mjs +37 -12
  123. package/bin/lib/wikiRunner.mjs +40 -61
  124. package/bin/lib/work.mjs +44 -13
  125. package/bin/lib/workAdoptCarry.mjs +2 -4
  126. package/bin/lib/workAgentBrief.mjs +92 -0
  127. package/bin/lib/workAgentBriefRunner.mjs +59 -0
  128. package/bin/lib/workAgentCheck.mjs +104 -57
  129. package/bin/lib/workAgentMerges.mjs +62 -57
  130. package/bin/lib/workAgentPlans.mjs +82 -199
  131. package/bin/lib/workAgentPrecheck.mjs +76 -138
  132. package/bin/lib/workAgentPublish.mjs +6 -8
  133. package/bin/lib/workAgentTurnBegun.mjs +15 -11
  134. package/bin/lib/workAgentTurnExecution.mjs +131 -67
  135. package/bin/lib/workAgentTurnOutcome.mjs +11 -2
  136. package/bin/lib/workAgentTurnReports.mjs +9 -37
  137. package/bin/lib/workAgentTurns.mjs +64 -7
  138. package/bin/lib/workArtifactReconcile.mjs +79 -0
  139. package/bin/lib/workAttachments.mjs +66 -11
  140. package/bin/lib/workBrain.mjs +9 -4
  141. package/bin/lib/workCappedTurn.mjs +119 -0
  142. package/bin/lib/workChildren.mjs +5 -5
  143. package/bin/lib/workCommandAudit.mjs +18 -2
  144. package/bin/lib/workDiffs.mjs +52 -6
  145. package/bin/lib/workIntake.mjs +35 -60
  146. package/bin/lib/workPlaceReports.mjs +7 -4
  147. package/bin/lib/workPlaces.mjs +113 -31
  148. package/bin/lib/workPreviews.mjs +47 -112
  149. package/bin/lib/workProcesses.mjs +8 -35
  150. package/bin/lib/workPullRequests.mjs +37 -56
  151. package/bin/lib/workReportQueue.mjs +100 -148
  152. package/bin/lib/workRetire.mjs +2 -1
  153. package/bin/lib/workSessionRuntime.mjs +38 -37
  154. package/bin/lib/workSessionTurns.mjs +256 -199
  155. package/bin/lib/workShip.mjs +43 -23
  156. package/bin/lib/workToolLog.mjs +70 -3
  157. package/bin/lib/workTurnShape.mjs +165 -0
  158. package/npm-shrinkwrap.json +69 -0
  159. package/package.json +9 -4
  160. package/bin/lib/fleetJobs.mjs +0 -120
  161. package/bin/lib/patchRevert.mjs +0 -60
@@ -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 { withoutMachineCredentials } from './machineEnv.mjs';
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, which the project check obeys too.
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
- const env = withoutMachineCredentials({ ...process.env, ...(mcpEnv ?? {}) });
53
- return display === true ? env : withoutWindows(env, browserHome);
57
+ return unattendedEnv({ ...process.env, ...(mcpEnv ?? {}) }, { display, browserHome });
54
58
  }
55
59
 
56
- // One Claude Code turn. Output is captured (for sentinel detection) and streamed
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
- // `streamJson` switches to `--output-format stream-json` and parses the event
60
- // stream: only the humanized tool activity reaches the console (a legible
61
- // stream of `read …`, `grep …`, `+ node …`), assistant text is folded into the
62
- // returned string for sentinel detection, and each activity is handed to
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
- const { prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, agentTools, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onToolEvent, onInit, onUsage, onRateLimit, onModel, onThreadId, onAnswer, vaultDir, knowledgeDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId, display, browserHome } = opts;
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: a
176
- // runtime that can path-scope its writes needs to know WHERE the vault is,
177
- // and Claude — which cannot — simply ignores it.
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
- // Only Claude's adapter uses it; codex's sandboxes read the filesystem
189
- // already, and agy's `--add-dir` is spent on the wiki vault.
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
- perm: claudePermFor(turnProfile.name, knowledgeDir),
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
- // A turn nobody is sitting at keeps its browsers in a home of its own —
228
- // the lane's kept one (an agent's), else this spawn's, removed when the
229
- // CLI closes. See `cliEnv`.
230
- const browser = display === true ? null : openBrowserHome(browserHome);
231
- const child = spawn(rt.bin, args, {
232
- cwd,
233
- stdio: ['ignore', 'pipe', 'pipe'],
234
- /**
235
- * ITS OWN PROCESS GROUP, so what the agent starts stays attributable.
236
- *
237
- * Everything the CLI spawns inherits this pgid and KEEPS it through
238
- * `nohup` and `setsid` — which is exactly when attribution by descendancy
239
- * fails, because reparenting to init breaks the ppid chain the moment a
240
- * process becomes long-running. `processes.mjs` reads the group; the
241
- * Workbench renders it.
242
- *
243
- * TEARDOWN IS DELIBERATELY UNCHANGED: `shutdownWork` still SIGTERMs this
244
- * CHILD and never the group. Signalling the group would kill the dev
245
- * server the driver started every time the daemon restarts — including
246
- * on an ordinary auto-update, unattended — which is the outcome the
247
- * deleted dev-run supervisor spent a whole registry avoiding. Flowviant
248
- * does not manage those processes; it reports them.
249
- *
250
- * Not `unref`'d: the daemon must still wait on this turn.
251
- */
252
- detached: true,
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
- const emit = (s) => process.stdout.write(pfx ? s.replace(/\n/g, `\n${pfx}`) : s);
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
- // A runtime with its own parser is ALWAYS line-parsed — for Codex the JSONL
278
- // stream is the only output there is, so treating it as raw text would print
279
- // event objects at the operator and, worse, hand the sentinel matcher a
280
- // string containing every word the model reasoned about.
281
- const lineParsed = streamJson || Boolean(rt.parse);
282
- if (lineParsed) {
283
- let buf = '';
284
- const appendText = (t) => {
285
- out += t;
286
- };
287
- /**
288
- * THE PLAN'S WINDOWS, LEARNED OFF EVERY TURN THIS DAEMON RUNS (0.109.0).
289
- *
290
- * This is the one spawn point — agent turns, Terminal tabs, capture
291
- * chats, the planner, the pre-review and the wiki all come through here
292
- * — so it is the one place that sees every CLI's word on its plan.
293
- * Claude says it on the stream (`rate_limit_event`), recorded as it
294
- * arrives; a caller's own `onRateLimit` still hears it. Codex writes it
295
- * into the thread's rollout file instead, so the thread this turn spoke
296
- * under (announced on `thread.started`, else the one it resumed) is read
297
- * AFTER the child closes, on the next tick — the turn's answer never
298
- * waits on a file read. See runtimeLimits.mjs.
299
- */
300
- const onRate = (info) => {
301
- recordClaudeRateLimit(info);
302
- onRateLimit?.(info);
303
- };
304
- let codexThread = rt.id === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
305
- /**
306
- * THE MODEL THIS SPAWN RAN ON, ONCE (2026-09-29, turnModel.mjs). Claude
307
- * names it in its init event and again on every reply; the first valid
308
- * one is the answer, so the init wins and the first reply is the
309
- * fallback. Codex names it only in its rollout, read at close below.
310
- */
311
- let ranOn = null;
312
- const onRanOn = (m) => {
313
- if (ranOn || !m) return;
314
- ranOn = m;
315
- onModel?.(m);
316
- };
317
- /** One line of the child's stdout, in whichever dialect it speaks. */
318
- const onLine = (line) => {
319
- if (!rt.parse)
320
- return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit, onUsage, onRateLimit: onRate, onModel: onRanOn });
321
- const ev = rt.parse(line, cwd);
322
- if (!ev) return;
323
- // The conversation id, when the runtime announces one (codex's
324
- // thread.started). Purely additive: callers that pass no onThreadId —
325
- // every dispatch path — see zero behavior change.
326
- if (ev.threadId) {
327
- if (rt.id === 'codex') codexThread = ev.threadId;
328
- onThreadId?.(ev.threadId);
329
- }
330
- // The runtime's own token count (codex's `turn.completed`) — the same
331
- // `onUsage` Claude's result event feeds, so a caller charges a turn
332
- // identically whichever CLI ran it.
333
- if (ev.usage) onUsage?.(ev.usage);
334
- // One agent MESSAGE, alone — see parseCodexLine's `answer`. Last call
335
- // wins at the caller, which is what "the final answer" means.
336
- if (typeof ev.answer === 'string') onAnswer?.(ev.answer);
337
- if (ev.text) appendText(ev.text);
338
- if (ev.activity) {
339
- emit(`${ev.activity.label}\n`);
340
- onActivity?.(ev.activity);
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
- handBack();
414
- });
415
- return;
416
- }
417
-
418
- const onChunk = (s) => {
419
- out += s;
420
- emit(s);
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
- // Stream-decoded for the same reason as the line-parsed path above.
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', onChunk);
426
- child.stderr.on('data', onChunk);
427
- child.on('error', (e) => {
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 same bar WIKI_PERM sets for Claude — writes
162
- // allowed, network shut. Both halves measured on 1.1.12:
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", which is the one line
173
- // WIKI_PERM draws ("Command execution is the line: it enables network
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.