flowviant 0.102.2 → 0.104.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 (141) hide show
  1. package/bin/cli.mjs +87 -474
  2. package/bin/lib/admission.mjs +19 -3
  3. package/bin/lib/agentPlan.mjs +2 -2
  4. package/bin/lib/agentTaskKinds.mjs +93 -0
  5. package/bin/lib/agySessions.mjs +158 -0
  6. package/bin/lib/artifactPolicy.mjs +44 -0
  7. package/bin/lib/artifactPreview.mjs +52 -0
  8. package/bin/lib/artifactReporter.mjs +163 -0
  9. package/bin/lib/artifacts.mjs +47 -227
  10. package/bin/lib/authReject.mjs +16 -4
  11. package/bin/lib/authproxy.mjs +47 -304
  12. package/bin/lib/boxIdentity.mjs +245 -0
  13. package/bin/lib/childEnv.mjs +11 -9
  14. package/bin/lib/claude.mjs +14 -851
  15. package/bin/lib/claudeAuth.mjs +3 -3
  16. package/bin/lib/claudePosture.mjs +367 -0
  17. package/bin/lib/claudeSessions.mjs +411 -0
  18. package/bin/lib/claudeStream.mjs +207 -0
  19. package/bin/lib/cliLimit.mjs +34 -0
  20. package/bin/lib/commandSpecs.mjs +173 -0
  21. package/bin/lib/config.mjs +11 -42
  22. package/bin/lib/credentialDate.mjs +22 -0
  23. package/bin/lib/credentials.mjs +21 -26
  24. package/bin/lib/daemonLogging.mjs +55 -0
  25. package/bin/lib/daemonState.mjs +64 -0
  26. package/bin/lib/deploy.mjs +148 -647
  27. package/bin/lib/deployCheckout.mjs +265 -0
  28. package/bin/lib/deployConfig.mjs +142 -0
  29. package/bin/lib/deployRunner.mjs +311 -0
  30. package/bin/lib/deployWire.mjs +41 -0
  31. package/bin/lib/desktopContract.mjs +56 -105
  32. package/bin/lib/env.mjs +49 -484
  33. package/bin/lib/fleet.mjs +184 -2396
  34. package/bin/lib/fleetJobs.mjs +120 -0
  35. package/bin/lib/fleetPost.mjs +40 -0
  36. package/bin/lib/fleetReports.mjs +392 -0
  37. package/bin/lib/fleetRoster.mjs +470 -0
  38. package/bin/lib/fleetStartup.mjs +196 -0
  39. package/bin/lib/fleetWake.mjs +45 -0
  40. package/bin/lib/fleetWire.mjs +46 -0
  41. package/bin/lib/git.mjs +34 -3
  42. package/bin/lib/gitMergeIdentity.mjs +51 -0
  43. package/bin/lib/grant.mjs +5 -3
  44. package/bin/lib/help.mjs +3 -125
  45. package/bin/lib/holder.mjs +482 -0
  46. package/bin/lib/instance.mjs +124 -46
  47. package/bin/lib/knowledge.mjs +16 -799
  48. package/bin/lib/knowledgeFetch.mjs +38 -0
  49. package/bin/lib/knowledgeFileSync.mjs +112 -0
  50. package/bin/lib/knowledgeFiles.mjs +487 -0
  51. package/bin/lib/knowledgeLibrary.mjs +249 -0
  52. package/bin/lib/landed.mjs +2 -1
  53. package/bin/lib/listeners.mjs +99 -141
  54. package/bin/lib/localSessions.mjs +30 -555
  55. package/bin/lib/login.mjs +3 -2
  56. package/bin/lib/loginPath.mjs +3 -5
  57. package/bin/lib/machineDisconnect.mjs +203 -0
  58. package/bin/lib/machineEnv.mjs +22 -0
  59. package/bin/lib/machineLimits.mjs +61 -0
  60. package/bin/lib/machineListing.mjs +338 -0
  61. package/bin/lib/machines.mjs +78 -455
  62. package/bin/lib/machinesCommand.mjs +214 -0
  63. package/bin/lib/mcp-cli.mjs +2 -1
  64. package/bin/lib/patchRevert.mjs +60 -0
  65. package/bin/lib/prWorkflow.mjs +157 -0
  66. package/bin/lib/preflight.mjs +2 -1
  67. package/bin/lib/pressurePolicy.mjs +104 -0
  68. package/bin/lib/preview.mjs +2 -272
  69. package/bin/lib/previewFramePolicy.mjs +112 -0
  70. package/bin/lib/previewGatePolicy.mjs +236 -0
  71. package/bin/lib/previewRegistry.mjs +235 -0
  72. package/bin/lib/printable.mjs +2 -2
  73. package/bin/lib/procRegistry.mjs +28 -18
  74. package/bin/lib/procScan.mjs +146 -0
  75. package/bin/lib/processes.mjs +29 -37
  76. package/bin/lib/projectToolRuntimes.mjs +138 -0
  77. package/bin/lib/projectTools.mjs +13 -88
  78. package/bin/lib/prompts.mjs +28 -42
  79. package/bin/lib/repoPath.mjs +26 -0
  80. package/bin/lib/resources.mjs +7 -69
  81. package/bin/lib/resumeLost.mjs +107 -0
  82. package/bin/lib/runTurn.mjs +324 -0
  83. package/bin/lib/runtimeAntigravity.mjs +230 -0
  84. package/bin/lib/runtimeCapabilities.mjs +334 -0
  85. package/bin/lib/runtimeClaude.mjs +126 -0
  86. package/bin/lib/runtimeCodex.mjs +217 -0
  87. package/bin/lib/runtimeDetection.mjs +121 -0
  88. package/bin/lib/runtimeEvents.mjs +482 -0
  89. package/bin/lib/runtimeOfferings.mjs +4 -1
  90. package/bin/lib/runtimes.mjs +42 -1402
  91. package/bin/lib/safeFileName.mjs +74 -0
  92. package/bin/lib/shipMerge.mjs +33 -2
  93. package/bin/lib/standDownExit.mjs +181 -0
  94. package/bin/lib/standDownGate.mjs +48 -0
  95. package/bin/lib/startProject.mjs +279 -0
  96. package/bin/lib/trace.mjs +3 -3
  97. package/bin/lib/turnLock.mjs +68 -0
  98. package/bin/lib/turnProfile.mjs +77 -0
  99. package/bin/lib/uninstall.mjs +55 -45
  100. package/bin/lib/uninstallCopies.mjs +112 -0
  101. package/bin/lib/update.mjs +30 -17
  102. package/bin/lib/uplinkScrub.mjs +265 -0
  103. package/bin/lib/vault.mjs +22 -151
  104. package/bin/lib/vaultDiff.mjs +183 -0
  105. package/bin/lib/versionOrder.mjs +42 -0
  106. package/bin/lib/views.mjs +35 -12
  107. package/bin/lib/wikiRunner.mjs +630 -0
  108. package/bin/lib/work.mjs +188 -3037
  109. package/bin/lib/workAdoptCarry.mjs +94 -0
  110. package/bin/lib/workAgentCheck.mjs +278 -0
  111. package/bin/lib/workAgentMerges.mjs +54 -142
  112. package/bin/lib/workAgentPlans.mjs +14 -12
  113. package/bin/lib/workAgentPrecheck.mjs +393 -0
  114. package/bin/lib/workAgentPublish.mjs +177 -0
  115. package/bin/lib/workAgentReview.mjs +8 -666
  116. package/bin/lib/workAgentTurnBegun.mjs +109 -0
  117. package/bin/lib/workAgentTurnExecution.mjs +604 -0
  118. package/bin/lib/workAgentTurnOutcome.mjs +207 -0
  119. package/bin/lib/workAgentTurnReports.mjs +189 -0
  120. package/bin/lib/workAgentTurns.mjs +92 -897
  121. package/bin/lib/workAttachments.mjs +93 -0
  122. package/bin/lib/workBrain.mjs +53 -0
  123. package/bin/lib/workChildren.mjs +110 -0
  124. package/bin/lib/workCommandAudit.mjs +62 -0
  125. package/bin/lib/workDiffs.mjs +2 -1
  126. package/bin/lib/workNarration.mjs +152 -0
  127. package/bin/lib/workPlaceReports.mjs +426 -0
  128. package/bin/lib/workPlaces.mjs +228 -0
  129. package/bin/lib/workPreviews.mjs +31 -9
  130. package/bin/lib/workProcesses.mjs +4 -5
  131. package/bin/lib/workPullRequests.mjs +52 -126
  132. package/bin/lib/workReportQueue.mjs +254 -0
  133. package/bin/lib/workRetire.mjs +185 -0
  134. package/bin/lib/workSessionGroups.mjs +166 -0
  135. package/bin/lib/workSessionRuntime.mjs +164 -0
  136. package/bin/lib/workSessionTokens.mjs +68 -0
  137. package/bin/lib/workSessionTurns.mjs +892 -0
  138. package/bin/lib/workShip.mjs +9 -29
  139. package/bin/lib/workToolLog.mjs +76 -0
  140. package/package.json +3 -2
  141. package/bin/lib/patch.mjs +0 -363
@@ -0,0 +1,324 @@
1
+ /**
2
+ * ONE HEADLESS CLI TURN — the argv the runtime's adapter builds, the child it
3
+ * spawns (its own process group, the daemon's environment minus the machine
4
+ * credential), and the supervision of its output until it closes.
5
+ *
6
+ * Split out of claude.mjs (SOLID 2026-09-26) because process lifetime — spawn,
7
+ * decode, error, close — changes for its own reasons. The posture it hands the
8
+ * adapter comes from claudePosture.mjs by the profile's name; Claude's stream
9
+ * is read by claudeStream.mjs; every other runtime brings its own `parse`
10
+ * (runtimeEvents.mjs).
11
+ */
12
+
13
+ import { spawn } from 'node:child_process';
14
+ import { runtimeById } from './runtimes.mjs';
15
+ import { withoutMachineCredentials } from './machineEnv.mjs';
16
+ import { resolveTurnProfile } from './turnProfile.mjs';
17
+ import { claudePermFor } from './claudePosture.mjs';
18
+ import { handleStreamLine } from './claudeStream.mjs';
19
+ import { never, workLanesStopped } from './standDownGate.mjs';
20
+
21
+ /**
22
+ * THE ENVIRONMENT A CLI TURN IS SPAWNED WITH — the daemon's own, plus the
23
+ * turn's MCP token, MINUS the machine credential (2026-09-24, the audit).
24
+ *
25
+ * A turn cannot be handed a curated environment the way `childEnv` builds one
26
+ * for a deploy: the CLI's own sign-in lives in this environment, and stripping
27
+ * it signs the CLI out. But on a headless box started as
28
+ * `FLOWVIANT_MACHINE_TOKEN=… npx flowviant` the MACHINE CREDENTIAL lives here
29
+ * too, and every turn — and every dev server and script a turn starts, which
30
+ * inherit it — could read it with one `env`. Nothing a CLI or its children do
31
+ * needs it: the daemon authenticates to the server itself, and a turn that
32
+ * needs the project's tools gets its own per-turn token through `mcpEnv` or
33
+ * the MCP config file. So those two names, and only those, are removed —
34
+ * by the one rule in machineEnv.mjs, which the project check obeys too.
35
+ */
36
+ export function cliEnv(mcpEnv) {
37
+ // After the merge, so an mcpEnv can never smuggle one back in.
38
+ return withoutMachineCredentials({ ...process.env, ...(mcpEnv ?? {}) });
39
+ }
40
+
41
+ // One Claude Code turn. Output is captured (for sentinel detection) and streamed
42
+ // through, line-prefixed with the worker label so a fleet stays legible.
43
+ //
44
+ // `streamJson` switches to `--output-format stream-json` and parses the event
45
+ // stream: only the humanized tool activity reaches the console (a legible
46
+ // stream of `read …`, `grep …`, `+ node …`), assistant text is folded into the
47
+ // returned string for sentinel detection, and each activity is handed to
48
+ // `onActivity` so the caller can forward progress. Build-agent turns leave it
49
+ // off and keep the raw text passthrough + line sentinels.
50
+ export function runTurn(opts) {
51
+ const { prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, agentTools, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onToolEvent, onInit, onUsage, onThreadId, onAnswer, vaultDir, knowledgeDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId } = opts;
52
+ /**
53
+ * AFTER THE TEARDOWN, NO TURN STARTS AND NO TURN ENDS (standDownGate.mjs).
54
+ * A stand-down that drains a deploy keeps this process alive; without this
55
+ * a turn the teardown killed would hand its empty output back to its lane,
56
+ * which would settle it as `nothing` or spawn a fresh retry nobody kills.
57
+ * Both answers stay pending instead, as `process.exit` left them.
58
+ */
59
+ if (workLanesStopped()) return never();
60
+ return new Promise((resolveTurn) => {
61
+ const resolve = (v) => {
62
+ if (!workLanesStopped()) resolveTurn(v);
63
+ };
64
+ const rt = runtimeById(runtime);
65
+ // AN UNKNOWN RUNTIME IS REFUSED, NEVER RUN AS CLAUDE (2026-09-26). Asked
66
+ // before anything is spawned. This is the belt, not the relay: the words
67
+ // a person sees come from the callers' own gates, asked earlier (the agent
68
+ // lane's `canRun` answers "this machine cannot run <id>", the session
69
+ // lane's `unsupported` check). Here the daemon log names the id and the
70
+ // turn answers nothing — never Claude wearing the other runtime's name.
71
+ if (!rt) {
72
+ console.error(`\nerror: cannot run '${runtime}' — this daemon does not know that runtime; update flowviant`);
73
+ resolve('');
74
+ return;
75
+ }
76
+ /**
77
+ * ONE PROFILE, NAMED BY THE CALLER (turnProfile.mjs). An unknown name or a
78
+ * retired posture switch fails the turn in words — never the build
79
+ * posture by fall-through.
80
+ */
81
+ const turnProfile = resolveTurnProfile(opts);
82
+ if (turnProfile.error) {
83
+ console.error(`\nerror: ${turnProfile.error}`);
84
+ resolve('');
85
+ return;
86
+ }
87
+ // A PROFILE SOME RUNTIMES CANNOT EXPRESS FAILS THE TURN — plan mode off
88
+ // Claude, a design or research posture on a runtime that does not declare
89
+ // it. The callers refuse first, in words (the tab lane; the agent lane's
90
+ // `canRun`); this is the belt no caller can skip. The other adapters build
91
+ // argv from the profile name and never read `perm`, so the only
92
+ // alternative is their build branch — for codex `--sandbox
93
+ // danger-full-access`: a research card's words with the whole machine to
94
+ // act on. Asked against the runtime's own declared profiles, so the day an
95
+ // adapter learns to express one this opens by itself.
96
+ const offRuntime = turnProfile.onlyOn?.(rt);
97
+ if (offRuntime) {
98
+ console.error(`\nerror: ${offRuntime}`);
99
+ resolve('');
100
+ return;
101
+ }
102
+ if (!rt.args) {
103
+ // Reached only if a brief names a runtime this daemon declares but cannot
104
+ // drive. Fail as a turn with no sentinel — the loop already treats that as
105
+ // "the protocol did not complete" and retries, rather than inventing a
106
+ // completion for work that never started.
107
+ console.error(`\nerror: cannot run '${rt.label}' — ${rt.blocked}`);
108
+ resolve('');
109
+ return;
110
+ }
111
+ // Pin the model — never inherit the user's global default (which for Claude
112
+ // may be a 1M/long-context tier their subscription can't bill autonomous
113
+ // work on). A per-task override (chosen in the app, validated server-side
114
+ // against a fixed list before it ever reaches this argv) wins over the
115
+ // machine pin; absent, the pin stands. Effort has no machine-level pin at
116
+ // all: unset means the CLI's own default, the honest resting state.
117
+ //
118
+ // TWO FORMS OF THE SAME DECISION, and the redundancy is deliberate rather
119
+ // than sloppy. `profile` is the NAME of the posture — a promise about what
120
+ // must be impossible during the turn — and every runtime expresses it in its
121
+ // own vocabulary: Claude as an `--allowedTools` verb list, Codex as a kernel
122
+ // sandbox mode plus feature toggles. `perm` is Claude's expression, looked
123
+ // up by the SAME name (`claudePermFor`), so the two cannot disagree about
124
+ // which posture a turn is running under. There is no precedence chain left
125
+ // to get wrong: the caller named one profile (turnProfile.mjs).
126
+ const args = rt.args({
127
+ prompt,
128
+ system,
129
+ model,
130
+ effort,
131
+ resume,
132
+ streamJson,
133
+ profile: turnProfile.adapterProfile,
134
+ agentTools,
135
+ // Adopting a terminal session (workSessionTurns.mjs): Claude turns it into
136
+ // `--resume <id> --fork-session` (a FORK — the original is untouched);
137
+ // agy turns it into `--conversation <id>` (a MOVE — agy has no fork, the
138
+ // tab continues the terminal conversation itself). Codex THROWS on it,
139
+ // so a mis-wired adoption fails as a loud turn error rather than a
140
+ // silent fresh conversation wearing an adopted session's name.
141
+ adoptResumeId,
142
+ // Only the wiki profile uses it, but it is passed unconditionally: a
143
+ // runtime that can path-scope its writes needs to know WHERE the vault is,
144
+ // and Claude — which cannot — simply ignores it.
145
+ vaultDir,
146
+ // THE PROJECT'S KNOWLEDGE LIBRARY (0.94.0), when this box holds one. It
147
+ // lives in the CHECKOUT and a turn usually runs in a worktree, so the
148
+ // prompt hands an absolute path OUTSIDE the cwd — and a curated Claude
149
+ // profile (the capture chat's read-only list, `FLOWVIANT_SAFE=1`) may
150
+ // refuse a read there. `--add-dir` says the directory is one it may read.
151
+ // Only Claude's adapter uses it; codex's sandboxes read the filesystem
152
+ // already, and agy's `--add-dir` is spent on the wiki vault.
153
+ knowledgeDir,
154
+ // Structured-output flags for the MEDIATED path. Handed to the adapter
155
+ // rather than appended here for the same reason `mcp` is: Codex takes its
156
+ // prompt as a trailing positional, so a flag after it is in the wrong
157
+ // place.
158
+ resultSchemaArgs,
159
+ // PLAN MODE REPLACES THE POSTURE, never joins it: beside
160
+ // `--dangerously-skip-permissions` the bypass wins silently (measured —
161
+ // see PLAN_MODE_PERM).
162
+ perm: claudePermFor(turnProfile.name, knowledgeDir),
163
+ // Handed to the adapter rather than appended here, because WHERE these go
164
+ // is a property of the CLI: Codex reads its prompt as a trailing
165
+ // positional, so a flag after it is a flag in the wrong place.
166
+ // Wiki-vault turns are pure file work and pass neither — no MCP at all.
167
+ mcp: turnProfile.strictMcp ? ['--strict-mcp-config'] : mcpConfig ? ['--mcp-config', mcpConfig] : (mcpArgs ?? []),
168
+ // Resuming a SPECIFIC held conversation by its own id (workSessionTurns.mjs, codex
169
+ // sessions). Runtimes without a by-id resume ignore it and keep their
170
+ // `resume` behavior unchanged.
171
+ resumeThreadId,
172
+ // agy's by-id resume (workSessionTurns.mjs, antigravity sessions): the conversation
173
+ // id learned from the adopt hint or the cwd registry after a turn.
174
+ resumeConversationId,
175
+ });
176
+ // Whatever this machine is signed in with, we use. We do NOT pick.
177
+ //
178
+ // This used to delete ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN to force
179
+ // the subscription path, which was right when the daemon ran on a
180
+ // developer's laptop: a key left in their shell would silently bill every
181
+ // turn as raw API usage instead of the plan they were already paying for.
182
+ // On a machine the project leaves running, an inherited org key is the
183
+ // POINT — deleting it is the daemon overriding the credential its operator
184
+ // deliberately configured.
185
+ //
186
+ // Which credential is correct, and whether an account may be shared, is
187
+ // between the operator and the vendor. Flowviant does not detect it and does
188
+ // not enforce it; it runs the CLI the ordinary way and relays what happens.
189
+ const child = spawn(rt.bin, args, {
190
+ cwd,
191
+ stdio: ['ignore', 'pipe', 'pipe'],
192
+ /**
193
+ * ITS OWN PROCESS GROUP, so what the agent starts stays attributable.
194
+ *
195
+ * Everything the CLI spawns inherits this pgid and KEEPS it through
196
+ * `nohup` and `setsid` — which is exactly when attribution by descendancy
197
+ * fails, because reparenting to init breaks the ppid chain the moment a
198
+ * process becomes long-running. `processes.mjs` reads the group; the
199
+ * Workbench renders it.
200
+ *
201
+ * TEARDOWN IS DELIBERATELY UNCHANGED: `shutdownWork` still SIGTERMs this
202
+ * CHILD and never the group. Signalling the group would kill the dev
203
+ * server the driver started every time the daemon restarts — including
204
+ * on an ordinary auto-update, unattended — which is the outcome the
205
+ * deleted dev-run supervisor spent a whole registry avoiding. Flowviant
206
+ * does not manage those processes; it reports them.
207
+ *
208
+ * Not `unref`'d: the daemon must still wait on this turn.
209
+ */
210
+ detached: true,
211
+ // Only ADDS to the environment (the worker token, for runtimes that read
212
+ // it from there). Never replaces it: the CLI's own credentials live in
213
+ // this environment, and handing it a curated one signs it out. The one
214
+ // thing it REMOVES is this daemon's machine credential — see `cliEnv`.
215
+ env: cliEnv(mcpEnv),
216
+ });
217
+ onSpawn?.(child);
218
+ let out = '';
219
+ const pfx = label ? `${label} ` : '';
220
+ const emit = (s) => process.stdout.write(pfx ? s.replace(/\n/g, `\n${pfx}`) : s);
221
+
222
+ // A runtime with its own parser is ALWAYS line-parsed — for Codex the JSONL
223
+ // stream is the only output there is, so treating it as raw text would print
224
+ // event objects at the operator and, worse, hand the sentinel matcher a
225
+ // string containing every word the model reasoned about.
226
+ const lineParsed = streamJson || Boolean(rt.parse);
227
+ if (lineParsed) {
228
+ let buf = '';
229
+ const appendText = (t) => {
230
+ out += t;
231
+ };
232
+ /** One line of the child's stdout, in whichever dialect it speaks. */
233
+ const onLine = (line) => {
234
+ if (!rt.parse)
235
+ return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit, onUsage });
236
+ const ev = rt.parse(line, cwd);
237
+ if (!ev) return;
238
+ // The conversation id, when the runtime announces one (codex's
239
+ // thread.started). Purely additive: callers that pass no onThreadId —
240
+ // every dispatch path — see zero behavior change.
241
+ if (ev.threadId) onThreadId?.(ev.threadId);
242
+ // The runtime's own token count (codex's `turn.completed`) — the same
243
+ // `onUsage` Claude's result event feeds, so a caller charges a turn
244
+ // identically whichever CLI ran it.
245
+ if (ev.usage) onUsage?.(ev.usage);
246
+ // One agent MESSAGE, alone — see parseCodexLine's `answer`. Last call
247
+ // wins at the caller, which is what "the final answer" means.
248
+ if (typeof ev.answer === 'string') onAnswer?.(ev.answer);
249
+ if (ev.text) appendText(ev.text);
250
+ if (ev.activity) {
251
+ emit(`${ev.activity.label}\n`);
252
+ onActivity?.(ev.activity);
253
+ }
254
+ };
255
+ // DECODED AS A STREAM, never chunk by chunk (2026-09-24, the audit): a
256
+ // pipe read can end inside a multi-byte character — the em dash models
257
+ // write constantly — and `d.toString()` per chunk turned each half into
258
+ // U+FFFD, which JSON.parse accepts and the reply then carried verbatim.
259
+ // `setEncoding` holds the partial bytes over to the next chunk.
260
+ child.stdout.setEncoding('utf8');
261
+ child.stderr.setEncoding('utf8');
262
+ child.stdout.on('data', (d) => {
263
+ buf += d;
264
+ let nl;
265
+ while ((nl = buf.indexOf('\n')) >= 0) {
266
+ const line = buf.slice(0, nl);
267
+ buf = buf.slice(nl + 1);
268
+ if (line.trim()) onLine(line);
269
+ }
270
+ });
271
+ // stderr is not JSON (warnings/errors) — pass through and keep for sentinels.
272
+ child.stderr.on('data', (s) => {
273
+ out += s;
274
+ emit(s);
275
+ });
276
+ child.on('error', (e) => {
277
+ if (e.code === 'ENOENT') {
278
+ // A MISSING CLI FAILS THE TURN, NOT THE DAEMON. This called
279
+ // process.exit(1), which was defensible while `claude` was the only
280
+ // runtime and preflight refused to start without it — the process
281
+ // could not reach here. Both halves of that are gone: preflight is now
282
+ // fatal only when NOTHING is drivable, so a Codex-only machine starts
283
+ // legitimately, and the wiki/plan-check/consult turns still ask for
284
+ // Claude by default. On such a machine the first wiki sweep would have
285
+ // killed the whole daemon, taking every in-flight build with it,
286
+ // because one background job could not find one binary.
287
+ console.error(`\nerror: '${rt.bin}' CLI not found on PATH. Install ${rt.label} first: ${rt.install}`);
288
+ resolve('');
289
+ return;
290
+ }
291
+ console.error(e);
292
+ resolve(out);
293
+ });
294
+ child.on('close', () => {
295
+ if (buf.trim()) onLine(buf);
296
+ resolve(out);
297
+ });
298
+ return;
299
+ }
300
+
301
+ const onChunk = (s) => {
302
+ out += s;
303
+ emit(s);
304
+ };
305
+ // Stream-decoded for the same reason as the line-parsed path above.
306
+ child.stdout.setEncoding('utf8');
307
+ child.stderr.setEncoding('utf8');
308
+ child.stdout.on('data', onChunk);
309
+ child.stderr.on('data', onChunk);
310
+ child.on('error', (e) => {
311
+ if (e.code === 'ENOENT') {
312
+ // A MISSING CLI FAILS THE TURN, NOT THE DAEMON — same fix as the
313
+ // line-parsed path above; this raw-output duplicate used to
314
+ // process.exit(1) and take every in-flight worker down with it.
315
+ console.error(`\nerror: '${rt.bin}' CLI not found on PATH. Install ${rt.label} first: ${rt.install}`);
316
+ resolve('');
317
+ return;
318
+ }
319
+ console.error(e);
320
+ resolve(out);
321
+ });
322
+ child.on('close', () => resolve(out));
323
+ });
324
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * ANTIGRAVITY, AS A RUNTIME — its registry row, and the measured record of why
3
+ * it cannot be handed a per-lane MCP config.
4
+ *
5
+ * Split out of runtimes.mjs (2026-09-26, SOLID F045). Each vendor's argv moves
6
+ * on its own vendor's release schedule; the registry (runtimes.mjs) assembles
7
+ * the rows and owns the rule that reads them (`canRun`, `mediated`), and the
8
+ * stream-json parser this row hands out as `parse` lives with the other stream
9
+ * parsers in runtimeEvents.mjs.
10
+ */
11
+
12
+ import { parseAgyLine } from './runtimeEvents.mjs';
13
+
14
+ /**
15
+ * DECLARED, NOT DRIVABLE — and this is now MEASURED rather than argued.
16
+ *
17
+ * The reason went wrong twice before it went right, so the evidence is written
18
+ * down here in full. First it cited upstream antigravity-cli#60, which is about
19
+ * `.antigravitycli/mcp_config.json` — the project-DISCOVERY folder — while
20
+ * claiming it was about `.agents/`, the workspace-CUSTOMIZATION folder. Then,
21
+ * on reading the docs (antigravity.google/docs/mcp describes `.agents/`, and
22
+ * Google's own codelab creates it), this comment swung the other way and said
23
+ * the blocker looked wrong. Both were reasoning from paperwork.
24
+ *
25
+ * THE TEST, run against agy 1.1.12, signed in, in a real git repo:
26
+ * the SAME minimal stdio MCP server, declared two ways.
27
+ * • workspace `<worktree>/.agents/mcp_config.json` → the server process is
28
+ * NEVER SPAWNED. Not connected-and-failed: never launched. The model
29
+ * answers "NO_MCP".
30
+ * • global `~/.gemini/config/mcp_config.json` → spawns immediately and
31
+ * handshakes: server/discover, initialize, notifications/initialized,
32
+ * tools/list.
33
+ * stdio deliberately, to remove every confound — no bearer token to reject,
34
+ * no network, no TLS. The difference is the config LOCATION and nothing else.
35
+ *
36
+ * AND IT SURVIVES THE TRUST VARIABLE, which was the obvious objection: agy
37
+ * keeps a `trustedWorkspaces` list in settings.json, a fresh per-task worktree
38
+ * is not on it, and an untrusted folder demonstrably changes behaviour (the
39
+ * model stops treating cwd as its workspace and works out of its own scratch
40
+ * dir). Adding the worktree to `trustedWorkspaces` and re-running changed
41
+ * nothing: the server still never spawned, the model still answered NO_MCP.
42
+ * So the workspace config is not trust-gated, it is simply not read.
43
+ *
44
+ * So Antigravity's MCP config is machine-wide IN PRACTICE, and the original
45
+ * blocker's conclusion stands even though its cited reason never did: every
46
+ * lane on the box would share one worker token, which is exactly the blast
47
+ * radius the per-lane token exists to prevent. That is why this stays
48
+ * undrivable, and it is a property of the CLI rather than something we can
49
+ * work around from here.
50
+ *
51
+ * A TRAP FOR WHOEVER RE-TESTS THIS: agy exposes MCP through a single generic
52
+ * `call_mcp_tool` dispatcher, so per-server tools NEVER appear in the `init`
53
+ * event's tools array even when a server is loaded correctly. Reading that
54
+ * array tells you nothing. Watch the server process instead, or ask the model.
55
+ *
56
+ * FOUR MORE FACTS from the same session, each an independent obstacle:
57
+ * 1. AUTH IS INTERACTIVE OAUTH (bubbletea TUI; needs a real /dev/tty), with
58
+ * no headless credential path. A signed-out `agy -p` prints a consent URL
59
+ * and then SITS until `--print-timeout` (default 5m) before erroring — so
60
+ * a lane on a signed-out agy burns five minutes per turn looking like a
61
+ * hang. Preflight cannot see it: `--version` succeeds while signed out.
62
+ * 2. `--sandbox` IS A BOOLEAN ("terminal restrictions enabled"), not a mode
63
+ * selector — but a CONSULT POSTURE IS STILL EXPRESSIBLE, and this was
64
+ * recorded backwards here for a while. It does not come from `--sandbox`
65
+ * at all; it comes from headless mode's default. Any tool needing a
66
+ * permission that cannot be prompted for is AUTO-DENIED:
67
+ * "User denied permission to run command: <cmd>"
68
+ * "a tool required the 'command' permission that headless mode cannot
69
+ * prompt for, so it was auto-denied."
70
+ * Observed repeatedly, including for an entirely benign `pwd && ls -la`,
71
+ * and a local listener confirmed no egress in any run. Reads (list_dir,
72
+ * file reads) work throughout. So: NOT passing
73
+ * `--dangerously-skip-permissions` IS the consult posture, and passing it
74
+ * is the build posture — both per invocation, both harness-enforced
75
+ * rather than model-instructed. `--mode plan` is a separate, WEAKER thing:
76
+ * it steers behaviour and blocks workspace writes, but it is not what
77
+ * stops command execution.
78
+ * RESIDUAL RISK, and it has no fix from here: `permissions.allow` in the
79
+ * machine-wide settings.json is inherited, so a user who has allowed
80
+ * `command(...)` widens every consult on that box. Codex has
81
+ * `--ignore-user-config` for exactly this; agy has no equivalent.
82
+ * 3. No `mcp` subcommand; `/mcp` is interactive-only.
83
+ * 4. It attempts to INSTALL PLAYWRIGHT at startup (observed failing 404
84
+ * against playwright.azureedge.net). A daemon runtime that downloads and
85
+ * runs a browser driver is worth knowing before it goes on a machine the
86
+ * project leaves running.
87
+ *
88
+ * There is no npm package. `antigravity-cli` (0.0.1, "placeholder") and `agy`
89
+ * (0.0.0, empty) on npm are SQUATS by unrelated accounts; the real channel is
90
+ * the install script at antigravity.google, which is why `install` below says
91
+ * to see the docs rather than naming an npm command.
92
+ *
93
+ * WHAT WOULD UNBLOCK IT: a per-invocation MCP flag or env var, or workspace
94
+ * configs actually being honoured. Re-test with the two-location stdio probe
95
+ * above — it is five minutes and it answers the question outright. Pin any
96
+ * wiring to >= 1.1.10 (`--model`/`--effort` were ignored in headless before it;
97
+ * `--output-format` arrived in 1.1.8).
98
+ */
99
+ export const ANTIGRAVITY_RUNTIME = {
100
+ id: 'antigravity',
101
+ label: 'Antigravity',
102
+ vendor: 'Google',
103
+ bin: 'agy',
104
+ install: 'see antigravity.google/docs/cli',
105
+ login: 'agy',
106
+ live: false,
107
+ /** None, because it cannot reach the MCP server at all — see `blocked`. */
108
+ /**
109
+ * WIKI AND CONSULT, BUT NOT BUILD — and the split is the whole point of
110
+ * making drivability per-profile rather than one verdict.
111
+ *
112
+ * Only a BUILD needs the MCP control plane: claiming, reporting a blocker,
113
+ * attaching a PR, completing. A wiki turn writes markdown files that the
114
+ * daemon syncs afterwards, and a consult answers a question in prose; BOTH
115
+ * already run with no MCP at all on every runtime, Claude included. Gating
116
+ * them on an MCP capability they never use was a test of the wrong thing,
117
+ * and it is what kept Antigravity at zero for months.
118
+ *
119
+ * Build returns here when the mediated adapter lands (the daemon holds the
120
+ * MCP connection and the CLI just returns schema-enforced JSON via
121
+ * `--json-schema`), which needs no per-invocation MCP config from the vendor
122
+ * at all.
123
+ *
124
+ * PLAN IS ABSENT ON PURPOSE, and NOT because a planning turn is beyond it —
125
+ * it reads code as well as anything here. A plan session is a conversation
126
+ * that makes many control-plane calls as it goes (spawn a slice, re-shape
127
+ * it, drop it, rewrite the spec), and mediation turns a turn into ONE
128
+ * schema-enforced form the daemon then applies. That shape fits a build,
129
+ * whose outcome is a single structured result; it does not yet fit an
130
+ * argument. Note that `canRun` would otherwise say yes via `mediated()` and
131
+ * hand this runtime a job it cannot finish — declaring the profile is the
132
+ * only thing standing between here and that. Mediated planning is a real
133
+ * design (the session returns its writes as a batch), just not a built one.
134
+ */
135
+ profiles: ['build', 'wiki', 'consult'],
136
+ /** No measured ladder, and ignorance never refuses: the full list, as the
137
+ * server's `RUNTIME_EFFORTS.antigravity` keeps it. */
138
+ efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
139
+ mcp: null,
140
+ args({ prompt, system, model, effort, resume, resumeConversationId, profile = 'build', vaultDir, resultSchemaArgs = [], adoptResumeId }) {
141
+ const a = [];
142
+ // WHICH CONVERSATION IS THIS — one decision, answered one way (the same
143
+ // rule as Claude's --resume/--continue). By id when the caller knows:
144
+ // `--conversation <id>` resumes globally, any cwd (measured on 1.1.12).
145
+ // Adoption is the SAME argv because agy has no fork — the id in the db
146
+ // is the identity (a renamed copy fails "trajectory not found",
147
+ // measured), so adopting MOVES the conversation: the tab continues the
148
+ // terminal session itself, appending to its one store. `--continue` is
149
+ // the cwd-keyed fallback for a session whose id was never learned.
150
+ if (adoptResumeId || resumeConversationId) a.push('--conversation', adoptResumeId || resumeConversationId);
151
+ else if (resume) a.push('--continue');
152
+ // No system-prompt flag, same weakening as Codex: the contract rides in
153
+ // the prompt, fenced and first.
154
+ a.push('-p', `${system}\n\n---\n\n${prompt}`);
155
+ a.push('--output-format', 'stream-json');
156
+ if (model) a.push('--model', model);
157
+ if (effort) a.push('--effort', effort);
158
+ a.push(...resultSchemaArgs);
159
+
160
+ 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:
163
+ // • headless cannot prompt, so anything needing approval is
164
+ // auto-denied — including the file writes a wiki turn exists to
165
+ // make. `--dangerously-skip-permissions` is what grants them.
166
+ // • `--sandbox` blocks egress: with it, curl returned 400 and a local
167
+ // listener saw nothing; without it, 200 and the request arrived.
168
+ // Upstream #36 warns these two cancel out (skip-permissions
169
+ // auto-approving the sandbox-bypass prompt). It does NOT reproduce on
170
+ // 1.1.12 — tested together, the write landed and the network stayed
171
+ // 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").
175
+ if (vaultDir) a.push('--add-dir', vaultDir);
176
+ a.push('--sandbox', '--dangerously-skip-permissions');
177
+ // A sweep reads a whole repository; the 5m default would guillotine it.
178
+ a.push('--print-timeout', '60m');
179
+ } else if (profile === 'build') {
180
+ // A BUILD WRITES, and headless auto-denies anything needing approval —
181
+ // so without this every edit comes back "User denied permission" and the
182
+ // turn reports failure having touched nothing. Caught only because an
183
+ // end-to-end test had to add the flag by hand to work.
184
+ //
185
+ // NOT `--sandbox` here, unlike wiki: a build has to `git push`, so it
186
+ // needs the network by definition.
187
+ //
188
+ // AND NOTHING ELSE CONTAINS IT EITHER. This comment used to end "the
189
+ // containment is the worktree, as it is for every runtime", which was
190
+ // false and is the kind of false that stops people looking: `cwd` is a
191
+ // starting directory, not a jail. A build turn runs with the operator's
192
+ // full user permissions — it can write outside the worktree, to their
193
+ // home directory, to their other checkouts. Claude gets
194
+ // --dangerously-skip-permissions, codex gets --sandbox
195
+ // danger-full-access, and agy gets no sandbox flag at all. The only
196
+ // real boundary today is that the person driving the tab is trusted.
197
+ //
198
+ // FLOWVIANT_SAFE HAS NO EXPRESSION ON THIS RUNTIME. Claude narrows to an
199
+ // allowlist and Codex to `workspace-write`; agy's only per-invocation
200
+ // control is this boolean, and its allow/deny engine is machine-wide.
201
+ // So a SAFE-mode operator gets an agy build that is not actually
202
+ // narrowed — which is why `mediatedSafeGap` is surfaced rather than
203
+ // quietly ignored.
204
+ a.push('--dangerously-skip-permissions');
205
+ a.push('--print-timeout', '60m');
206
+ } else if (profile === 'consult') {
207
+ // NOTHING GRANTED, deliberately. The headless default IS the consult
208
+ // posture: `run_command` comes back "User denied permission… headless
209
+ // mode cannot prompt for it, so it was auto-denied", observed even for a
210
+ // benign `pwd && ls -la`, with no egress in any run. Reads keep working,
211
+ // which is all a consult needs. Passing --dangerously-skip-permissions
212
+ // here would hand a question ANY project editor can type a shell.
213
+ a.push('--print-timeout', '10m');
214
+ }
215
+ return a;
216
+ },
217
+ parse: parseAgyLine,
218
+ /**
219
+ * `--json-schema` enforces the shape of the final result — verified against
220
+ * 1.1.12, which returned exactly the object asked for. This is what makes a
221
+ * BUILD possible on a runtime that cannot be handed an MCP config: the CLI
222
+ * stops needing to CALL anything and just returns a filled-in form, and the
223
+ * daemon makes the control-plane calls on its behalf with the lane's own
224
+ * per-lane token. Strictly better than parsing markers out of prose, which a
225
+ * model can wrap in a code fence, truncate or hallucinate.
226
+ */
227
+ resultSchema: (path) => ['--json-schema', path],
228
+ blocked:
229
+ 'its MCP config is machine-wide — a workspace .agents/mcp_config.json is never loaded (measured), so every lane would share one token',
230
+ };