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,334 @@
1
+ /**
2
+ * WHAT THE CLI SAID IT CAN DO ON THIS MACHINE — the skills and MCP servers
3
+ * learned off Claude Code's `system.init`, and the one-shot probe that learns
4
+ * them on a machine no turn has taught.
5
+ *
6
+ * Split out of runtimes.mjs (2026-09-26, SOLID F045). These are per-process
7
+ * caches of a MACHINE REPORT (they ride the roster poll as `skills` and `mcp`),
8
+ * plus the one place the daemon spawns a CLI for its own purposes; neither is
9
+ * a fact about how a runtime is driven, and a report-shape change is not a
10
+ * reason to edit the registry. Three states throughout: null = no turn has
11
+ * taught us, [] = measured and none, a list = measured.
12
+ */
13
+
14
+ import { spawn } from 'node:child_process';
15
+ import { existsSync, readdirSync, rmSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { RUNTIMES } from './runtimes.mjs';
19
+
20
+ /**
21
+ * WHAT THE CLI SAID IT CAN BE ASKED FOR BY NAME — the machine's skills.
22
+ *
23
+ * Learned, never scanned. Claude Code's `system.init` event names its own
24
+ * resolved skill set on every stream-json turn, and the daemon already parses
25
+ * that stream (claudeStream.mjs), so this costs nothing and is authoritative: it has
26
+ * plugins, this repo's `.claude/skills`, and whatever project settings enabled
27
+ * or disabled already folded in. A `~/.claude/skills` scan of our own would be
28
+ * a second implementation of the CLI's resolution rules, and would drift.
29
+ *
30
+ * THE PRICE OF LEARNING RATHER THAN PROBING is that a machine which has not run
31
+ * a turn yet knows nothing, and says nothing. That is the honest answer: the
32
+ * app renders no menu rather than an empty one, and a slash typed into a tab
33
+ * still reaches the CLI either way — the menu is an autocomplete, never a gate.
34
+ * We do NOT probe for it: a `claude -p` run purely to populate a dropdown would
35
+ * spend the operator's quota on a UI affordance.
36
+ *
37
+ * PER MACHINE, not per session. Every session worktree is a checkout of the one
38
+ * repo this daemon serves, so project skills are identical across tabs and
39
+ * personal skills are machine-wide. Last turn wins, which is what makes a skill
40
+ * added mid-run show up on the next poll.
41
+ */
42
+ let skillsCache = null;
43
+
44
+ /** Claude Code's own names: letters, digits, dash, underscore, and the colon a
45
+ * plugin skill wears (`plugin:skill`). Anything else is not a name we could
46
+ * put after a `/` anyway, so it is dropped rather than relayed as garbage. */
47
+ const SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9_:-]{0,63}$/;
48
+
49
+ /** Record what a turn's init event reported. Bounded and sorted so the poll's
50
+ * query param has a stable length and a stable order — an unstable order would
51
+ * make the server write a "change" on every single poll. */
52
+ export function recordSkills(names) {
53
+ if (!Array.isArray(names)) return;
54
+ const clean = [...new Set(names.map((n) => String(n).trim()).filter((n) => SKILL_NAME.test(n)))]
55
+ .sort()
56
+ .slice(0, 100);
57
+ // An empty report is a FACT (a machine with no skills installed), so it is
58
+ // recorded as []. Never conflated with null, which stays "no turn has run".
59
+ skillsCache = clean;
60
+ }
61
+
62
+ /** What to send on the roster poll — null until a turn has taught us. */
63
+ export function knownSkills() {
64
+ return skillsCache;
65
+ }
66
+
67
+ /**
68
+ * WHICH MCP SERVERS AND CLAUDE.AI CONNECTORS THE CLI MOUNTED, AND HOW EACH
69
+ * STANDS (2026-09-23, 0.97.0) — learned off the same `system.init` event the
70
+ * skills are, so it costs nothing and cannot drift from what a turn reaches.
71
+ *
72
+ * MEASURED on Claude Code 2.1.281 (`claude -p hi --model haiku --output-format
73
+ * stream-json --verbose`): the init event carries `mcp_servers: [{ name,
74
+ * status, source }]` — statuses seen `connected`, `failed`, `needs-auth` and
75
+ * `pending`; a claude.ai connector arrives named `claude.ai <Name>` with
76
+ * `source: "claudeai"`, a server a turn mounts with `--mcp-config` with
77
+ * `source: "dynamic"`. A connector that needs a one-time sign-in AT THIS BOX
78
+ * reads `needs-auth` here long before a turn fails on it, and the app's
79
+ * Machines page names it.
80
+ *
81
+ * `flowviant` IS EXCLUDED: it is the server THIS daemon mounts on every tab
82
+ * turn, always present, and not something a person can act on. A status off
83
+ * the closed list is relayed as `other` rather than dropped — the CLI said
84
+ * something about the server, and silence would be the daemon deciding it did
85
+ * not. Bounded (40 entries, names cut at 60) and sorted, the `recordSkills`
86
+ * discipline: a stable param, so an unchanged report writes nothing.
87
+ *
88
+ * ONLY WHAT IS NOT CONNECTED LEAVES THE BOX (2026-09-23). The first cut
89
+ * relayed every server, connected ones included — so the operator's whole set
90
+ * of signed-in services (their mail, their brokerage, their calendar) rode the
91
+ * poll to the server and into every teammate's read of the Machines page,
92
+ * which renders only the servers that are NOT connected anyway. A connected
93
+ * connector is good news and good news is not a line; it is also nobody
94
+ * else's business. So `connected` is dropped HERE, before the cap, and what is
95
+ * sent is exactly what the page can say something about.
96
+ *
97
+ * THREE STATES, the skills split: null = no turn has taught us (the param is
98
+ * not sent), [] = nothing of the person's own needs anything (every server is
99
+ * connected, or none is mounted), a list = the ones that do. Last turn wins,
100
+ * so a connector somebody signs into leaves the list on the next turn's init,
101
+ * and an all-connected turn sends `[]`, which clears a stale list server-side.
102
+ */
103
+ let mcpServersCache = null;
104
+
105
+ export const MCP_STATUSES = new Set(['connected', 'failed', 'needs-auth', 'pending']);
106
+ export const MAX_MCP_SERVERS = 40;
107
+ export const MCP_NAME_MAX = 60;
108
+
109
+ export function recordMcpServers(list) {
110
+ if (!Array.isArray(list)) return;
111
+ const seen = new Map();
112
+ for (const e of list) {
113
+ if (!e || typeof e !== 'object' || typeof e.name !== 'string') continue;
114
+ const n = e.name.trim().slice(0, MCP_NAME_MAX).trim();
115
+ if (!n || n === 'flowviant' || seen.has(n)) continue;
116
+ if (e.status === 'connected') continue; // see the header: never relayed
117
+ seen.set(n, { n, s: MCP_STATUSES.has(e.status) ? e.status : 'other' });
118
+ }
119
+ mcpServersCache = [...seen.values()]
120
+ .sort((a, b) => (a.n < b.n ? -1 : a.n > b.n ? 1 : 0))
121
+ .slice(0, MAX_MCP_SERVERS);
122
+ }
123
+
124
+ /** What to send on the roster poll as `mcp` — null until a turn has taught us. */
125
+ export function knownMcpServers() {
126
+ return mcpServersCache;
127
+ }
128
+
129
+ /**
130
+ * LEARN WHAT `/` CAN OFFER, ON A MACHINE NO TURN HAS TAUGHT.
131
+ *
132
+ * WHY THIS EXISTS. `recordSkills` above is fed from the init event of a tab
133
+ * turn — authoritative and free, but with a hole nobody priced: THE FIRST THING
134
+ * ANYONE DOES IN A NEW TAB IS TYPE `/`, and that is by definition before that
135
+ * machine has run a turn. The menu was guaranteed empty exactly where it is
136
+ * first reached. Not a theoretical hole: checked against production on
137
+ * 2026-08-25, `agent_tokens.skills` was NULL for EVERY machine credential that
138
+ * has ever existed, because the only tab turns ever run predated the release
139
+ * that reports. The feature had never worked for anyone, once.
140
+ *
141
+ * IT COSTS ONE SMALL REQUEST, AND THAT IS THE HONEST NUMBER. This file used to
142
+ * forbid probing outright — "a `claude -p` run purely to populate a dropdown
143
+ * would spend the operator's quota on an affordance" — and that rule was
144
+ * written picturing a COMPLETED TURN. This is not one: Claude Code emits
145
+ * `system.init`, carrying its own fully resolved skill set, within ~0.5s and
146
+ * long before it finishes answering, so the child is killed the moment that
147
+ * event is read. But the request HAS gone out by then — measured on 2.1.245 by
148
+ * reading the transcript a killed probe left behind: 2 input tokens, 4 output,
149
+ * ~6k cache-creation. Two zero-request routes were tried and both failed: an
150
+ * empty prompt errors before init is emitted, and `--input-format stream-json`
151
+ * emits nothing at all until a message arrives. So the cost is one cheap turn,
152
+ * ONCE per daemon process, on the cheapest model, and only on a machine no turn
153
+ * has taught. Do not let this grow into a per-tab or per-poll probe.
154
+ *
155
+ * `--model haiku` for that reason, and it is safe by this repo's own rule: a
156
+ * name lives in AGENT_MODELS only once `claude --model <name>` is known to be
157
+ * accepted on a real install. If it were ever refused the probe simply learns
158
+ * nothing and the machine stays unmeasured — which is exactly today's state, so
159
+ * the failure mode is the status quo rather than a regression.
160
+ *
161
+ * IT SCANS FOR THE INIT EVENT, never assuming it is line 1 — and that is not
162
+ * defensive padding, it is measured. With `--model haiku` the CLI prints a
163
+ * `system/status` line FIRST and init lands on line 2; with the default model
164
+ * init is line 1. A first-line-only reader (the version this replaced) silently
165
+ * learned nothing the moment a model flag was added.
166
+ *
167
+ * IT CLEANS UP AFTER ITSELF, and this is not optional. `claude -p` writes a
168
+ * transcript to `~/.claude/projects/<munged-cwd>/<session-id>.jsonl` the moment
169
+ * it starts, and `claudeSessions.mjs` reports the newest ENDED session per
170
+ * directory to the Workbench as an ADOPTABLE row. Left behind, every daemon
171
+ * start would put a phantom untitled session in the `+` menu offering to adopt
172
+ * a conversation that never happened. The init event names its own session id,
173
+ * so the file is ours by name and is removed by it.
174
+ *
175
+ * IT IS BEST-EFFORT IN EVERY DIRECTION. No claude, no PATH, a CLI that changed
176
+ * its event shape, an unwritable home — all leave `skillsCache` exactly as it
177
+ * was (null, "nobody looked"), which is the honest answer and the one the app
178
+ * already renders correctly. Nothing here may throw, and nothing may block the
179
+ * poll it is called from.
180
+ */
181
+
182
+ /** Long enough for a cold CLI start on a slow box, short enough that a hung
183
+ * child is not left holding a session for the life of the daemon. */
184
+ const SKILL_PROBE_TIMEOUT_MS = 30_000;
185
+
186
+ /** Init arrives within the first couple of events or not at all; this is the
187
+ * guard against parsing a whole turn's output looking for it. */
188
+ const SKILL_PROBE_MAX_LINES = 20;
189
+
190
+ let skillProbeStarted = false;
191
+
192
+ /** Where Claude Code keeps a transcript for `cwd`: `/` and `.` both become `-`. */
193
+ function transcriptCandidates(cwd, sessionId) {
194
+ const base = join(homedir(), '.claude', 'projects');
195
+ const out = [join(base, cwd.replace(/[/.]/g, '-'), `${sessionId}.jsonl`)];
196
+ // The munge is Claude Code's, not ours, so a version that changes it must not
197
+ // leave the file behind: fall back to finding our own session id by name.
198
+ try {
199
+ for (const d of readdirSync(base)) out.push(join(base, d, `${sessionId}.jsonl`));
200
+ } catch {
201
+ /* no project store — nothing was written either */
202
+ }
203
+ return out;
204
+ }
205
+
206
+ /**
207
+ * Delete the transcript a headless `-p` turn left behind.
208
+ *
209
+ * EXPORTED because the skills probe is no longer the only thing that runs one:
210
+ * resolving a project's dev command is a background Claude turn too, and every
211
+ * such turn has the same footprint. `claude -p` writes
212
+ * `~/.claude/projects/<munged-cwd>/<id>.jsonl` at startup, and
213
+ * `claudeSessions.mjs` offers the newest ENDED session per directory as
214
+ * ADOPTABLE — so anything we run for our own purposes would put a phantom
215
+ * untitled session in somebody's `+` menu.
216
+ */
217
+ export function removeProbeTranscript(cwd, sessionId) {
218
+ if (!sessionId || !/^[A-Za-z0-9_-]{8,64}$/.test(sessionId)) return;
219
+ for (const f of transcriptCandidates(cwd, sessionId)) {
220
+ try {
221
+ if (existsSync(f)) {
222
+ rmSync(f, { force: true });
223
+ return;
224
+ }
225
+ } catch {
226
+ /* best-effort */
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * One line of the probe's stdout → the init event's payload, or null for "not
233
+ * it, keep reading".
234
+ *
235
+ * SEPARATE FROM THE SCAN LOOP so the thing that actually broke can be tested
236
+ * without spawning a CLI. The first version of this probe read line 1 and
237
+ * stopped, which was measured-correct with the default model and silently
238
+ * WRONG with `--model haiku`: that path prints a `system/status` line first and
239
+ * puts init on line 2, so the probe learned nothing and reported nothing, which
240
+ * is indistinguishable from the bug it was written to fix.
241
+ *
242
+ * A line that is not JSON is not a failure — a CLI warning on stdout is a line
243
+ * to skip, not a reason to abandon the probe.
244
+ */
245
+ export function parseInitLine(line) {
246
+ let ev;
247
+ try {
248
+ ev = JSON.parse(line);
249
+ } catch {
250
+ return null;
251
+ }
252
+ if (!ev || ev.type !== 'system' || ev.subtype !== 'init') return null;
253
+ return {
254
+ // An init event WITHOUT skills is still the init event: stop reading, but
255
+ // record nothing. Conflating the two would keep the probe scanning a whole
256
+ // turn's output on a CLI that does not report them.
257
+ skills: Array.isArray(ev.skills) ? ev.skills : null,
258
+ // The CLI's mounted MCP servers (0.97.0) — `recordMcpServers` normalises.
259
+ mcpServers: Array.isArray(ev.mcp_servers) ? ev.mcp_servers : null,
260
+ sessionId: typeof ev.session_id === 'string' ? ev.session_id : null,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Kick off the one-shot probe. Returns immediately; the result lands in
266
+ * `skillsCache` and rides the NEXT poll, so nothing waits on it.
267
+ *
268
+ * A no-op once a turn has taught us (`skillsCache !== null`) — a turn's init
269
+ * event and this one say the same thing, and the turn is free.
270
+ */
271
+ export function probeSkillsOnce(cwd) {
272
+ if (skillProbeStarted || skillsCache !== null) return;
273
+ skillProbeStarted = true;
274
+ let child;
275
+ try {
276
+ // Claude Code specifically, not the `wiki` profile's runtime pick: `skills`
277
+ // is Claude Code's own field and the `/` tray only renders for claude tabs.
278
+ child = spawn(
279
+ RUNTIMES.claude.bin,
280
+ ['-p', 'x', '--model', 'haiku', '--output-format', 'stream-json', '--verbose'],
281
+ { cwd, stdio: ['ignore', 'pipe', 'ignore'] }
282
+ );
283
+ } catch {
284
+ return; // no claude on PATH — the machine simply stays unmeasured
285
+ }
286
+ let buf = '';
287
+ let lines = 0;
288
+ let settled = false;
289
+ const finish = (sessionId) => {
290
+ if (settled) return;
291
+ settled = true;
292
+ clearTimeout(timer);
293
+ try {
294
+ child.kill('SIGKILL');
295
+ } catch {
296
+ /* already gone */
297
+ }
298
+ // AFTER the kill, and on a delay: the transcript is the CHILD's file, so
299
+ // deleting it while the child still lives races a recreate.
300
+ if (sessionId) setTimeout(() => removeProbeTranscript(cwd, sessionId), 750).unref?.();
301
+ };
302
+ const timer = setTimeout(() => finish(null), SKILL_PROBE_TIMEOUT_MS);
303
+ timer.unref?.();
304
+ child.on('error', () => finish(null));
305
+ child.on('exit', () => finish(null));
306
+ // UTF-8 mode on the STREAM, not a per-chunk `.toString()` — a multi-byte
307
+ // character straddling a chunk boundary decodes to mojibake (or a stray
308
+ // replacement byte) when each chunk is decoded on its own; the stream's
309
+ // own decoder holds the partial sequence over to the next chunk instead.
310
+ child.stdout.setEncoding('utf8');
311
+ child.stdout.on('data', (d) => {
312
+ if (settled) return;
313
+ buf += d;
314
+ let nl;
315
+ while (!settled && (nl = buf.indexOf('\n')) >= 0) {
316
+ const line = buf.slice(0, nl);
317
+ buf = buf.slice(nl + 1);
318
+ if (!line.trim()) continue;
319
+ if (++lines > SKILL_PROBE_MAX_LINES) {
320
+ finish(null);
321
+ return;
322
+ }
323
+ const init = parseInitLine(line);
324
+ if (!init) continue;
325
+ if (init.skills) recordSkills(init.skills);
326
+ if (init.mcpServers) recordMcpServers(init.mcpServers);
327
+ finish(init.sessionId);
328
+ return;
329
+ }
330
+ // A single line this long is not an init event; stop buffering a whole
331
+ // turn's output into memory waiting for one.
332
+ if (!settled && buf.length > 1_000_000) finish(null);
333
+ });
334
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * CLAUDE CODE, AS A RUNTIME — its registry row: argv, profiles, efforts, and
3
+ * the per-invocation MCP config file it is handed.
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, and a flag Claude Code renames is not
7
+ * a reason to open Codex's or Antigravity's builder. The registry
8
+ * (runtimes.mjs) assembles the rows and owns the rule that reads them
9
+ * (`canRun`); what the `mcp` mint must guarantee is argued there, once, for
10
+ * every vendor. Claude's own stream parser lives in claudeStream.mjs.
11
+ */
12
+
13
+ import { mkdtempSync, writeFileSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { MODEL, USER_AGENT } from './config.mjs';
17
+
18
+ function claudeMcp(token, mcpUrl) {
19
+ const dir = mkdtempSync(join(tmpdir(), 'flowviant-mcp-'));
20
+ const path = join(dir, 'mcp.json');
21
+ writeFileSync(
22
+ path,
23
+ JSON.stringify({
24
+ mcpServers: {
25
+ flowviant: {
26
+ type: 'http',
27
+ url: mcpUrl,
28
+ headers: { Authorization: `Bearer ${token}`, 'User-Agent': USER_AGENT },
29
+ },
30
+ },
31
+ }),
32
+ { mode: 0o600 }
33
+ );
34
+ return { dir, args: ['--mcp-config', path], env: {} };
35
+ }
36
+
37
+ export const CLAUDE_RUNTIME = {
38
+ id: 'claude',
39
+ label: 'Claude Code',
40
+ vendor: 'Anthropic',
41
+ bin: 'claude',
42
+ install: 'npm i -g @anthropic-ai/claude-code',
43
+ login: 'claude',
44
+ /** The Agent-SDK live session (persistent, injectable mid-task) is Claude
45
+ * only — it is an Anthropic SDK, not a CLI contract. Everything else runs
46
+ * the subprocess path. */
47
+ live: true,
48
+ /**
49
+ * Every profile, because every profile is DEFINED in its vocabulary: the
50
+ * four `--allowedTools` lists in claudePosture.mjs are what "build", "wiki",
51
+ * "consult" and "plan" currently mean. That is a statement about where the
52
+ * contract was written, not a claim that only Claude could ever satisfy it.
53
+ */
54
+ // `design` and `research` (0.97.0) are Claude's alone: each is an
55
+ // `--allowedTools` list with a PATH-SCOPED write (claudePosture.mjs), and neither
56
+ // codex's sandbox modes nor agy's flags were measured to express "write
57
+ // only this directory". A runtime that does not declare them is refused
58
+ // such a card before spawn, in words — never handed a build turn instead.
59
+ profiles: ['build', 'wiki', 'consult', 'plan', 'design', 'research'],
60
+ /**
61
+ * THE EFFORTS THIS CLI SPELLS — the daemon's copy of the server's
62
+ * `RUNTIME_EFFORTS` row, read by workBrain.mjs's `brainFor` so an effort a CLI
63
+ * would refuse at spawn is DROPPED before argv (the turn then runs on the
64
+ * CLI's own default). Claude Code's `--effort` takes all five. A copy, so
65
+ * `scripts/check-app-parity.mjs` (the release gate; appParity.test.mjs)
66
+ * fails when any runtime's row differs from the server's.
67
+ */
68
+ efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
69
+ mcp: claudeMcp,
70
+ /**
71
+ * Claude takes the operating contract as a real system prompt, which is the
72
+ * strongest form of it available anywhere: `--append-system-prompt` sits
73
+ * above the conversation rather than inside it.
74
+ */
75
+ args({ prompt, system, model, effort, resume, streamJson, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, agentTools }) {
76
+ const a = [];
77
+ // THREE ANSWERS TO ONE QUESTION — "what conversation is this?" — and they
78
+ // are mutually exclusive, never combined.
79
+ //
80
+ // ADOPTION: `--resume <id> --fork-session` finds the session globally
81
+ // (any cwd), carries its full context, and writes the FORK natively into
82
+ // THIS cwd's own store, leaving the original transcript untouched
83
+ // (measured on 2.1.234).
84
+ //
85
+ // BY ID: the conversation THIS TAB spoke under last time, learned from
86
+ // the CLI's own `system.init` event and pinned per session. It exists
87
+ // because `--continue` is CWD-KEYED, and a directory stopped being one
88
+ // tab the day tabs moved into their driver's project folder — two tabs
89
+ // sharing a directory both said `--continue` and both resumed whichever
90
+ // conversation spoke most recently there, so tab B inherited tab A's
91
+ // entire context and every turn after that ping-ponged between them.
92
+ // Exactly the failure codex's own note warns about for `resume --last`,
93
+ // arriving for Claude by a different route. An id is unambiguous wherever
94
+ // the tab is standing.
95
+ //
96
+ // `--continue` is the LAST resort, and only where nothing better is
97
+ // known.
98
+ if (adoptResumeId) a.push('--resume', adoptResumeId, '--fork-session');
99
+ else if (resumeThreadId) a.push('--resume', resumeThreadId);
100
+ else if (resume) a.push('--continue');
101
+ a.push('-p', prompt, '--append-system-prompt', system);
102
+ if (agentTools) {
103
+ // Keep personal settings, but never load the agent-editable project's
104
+ // settings, hooks, skills or CLAUDE.md discovery. A resumed agent must
105
+ // render the CURRENT base snapshot, not the first turn's prompt cache.
106
+ a.push('--setting-sources', 'user', '--system-prompt-snapshot', 'off');
107
+ if (agentTools.pluginDir) a.push('--plugin-dir', agentTools.pluginDir);
108
+ }
109
+ // These fenced postures have no project MCP, even if Claude discovers a
110
+ // .mcp.json in the agent's worktree. Build gets its reviewed base config
111
+ // explicitly from the caller; the plan profile keeps its own principal.
112
+ if (['consult', 'wiki', 'design', 'research'].includes(profile)) a.push('--strict-mcp-config');
113
+ a.push(...mcp, ...resultSchemaArgs);
114
+ a.push('--model', model || MODEL);
115
+ if (effort) a.push('--effort', effort);
116
+ if (streamJson) a.push('--output-format', 'stream-json', '--verbose');
117
+ // The knowledge library (0.94.0) sits in the checkout, outside a
118
+ // worktree's cwd: named as a readable directory so a curated profile
119
+ // does not refuse the path the prompt just handed it. Before `perm`,
120
+ // because `--allowedTools` is variadic and would swallow the flag.
121
+ if (knowledgeDir) a.push('--add-dir', knowledgeDir);
122
+ a.push(...perm);
123
+ return a;
124
+ },
125
+ parse: null, // claudeStream.mjs owns its own stream parser (unchanged)
126
+ };