flowviant 0.103.0 → 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.
- package/bin/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +93 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactPolicy.mjs +44 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +47 -227
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +367 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +183 -2383
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +2 -2
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +487 -0
- package/bin/lib/knowledgeLibrary.mjs +249 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +28 -42
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +17 -17
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3034
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +207 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +91 -898
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- 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
|
+
};
|