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.
- 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 +184 -2396
- 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 +34 -3
- 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/shipMerge.mjs +33 -2
- 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 +35 -12
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3037
- 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 +92 -897
- 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,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
|
+
};
|