flowviant 0.116.0 → 0.124.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -12
- package/bin/cli.mjs +50 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentAsk.mjs +123 -0
- package/bin/lib/agentBrief.mjs +36 -0
- package/bin/lib/agentFootprint.mjs +76 -0
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +134 -31
- package/bin/lib/agentPrerequisiteContract.mjs +43 -0
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +5 -8
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactContracts.mjs +91 -20
- package/bin/lib/artifactIteration.mjs +182 -0
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactRecovery.mjs +36 -0
- package/bin/lib/artifactReporter.mjs +194 -22
- package/bin/lib/artifacts.mjs +70 -63
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudeAuth.mjs +15 -5
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +227 -54
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +27 -13
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexProjectRead.mjs +63 -0
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +193 -0
- package/bin/lib/commandSpecs.mjs +6 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +90 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +6 -5
- package/bin/lib/discoveryContract.mjs +19 -0
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +53 -13
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +172 -171
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/mcpAllow.mjs +60 -0
- package/bin/lib/hooks/readGuard.mjs +54 -11
- package/bin/lib/imageArtifact.mjs +74 -0
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/maintenance.mjs +47 -0
- package/bin/lib/maintenanceInventory.mjs +39 -0
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/mcpInstall.mjs +163 -0
- package/bin/lib/mcpInventory.mjs +119 -0
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/planDependencies.mjs +70 -0
- package/bin/lib/postureTools.mjs +85 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +328 -19
- package/bin/lib/projectTools.mjs +109 -34
- package/bin/lib/prompts.mjs +205 -60
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/repositoryBaseline.mjs +68 -0
- package/bin/lib/runTurn.mjs +286 -209
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +215 -21
- package/bin/lib/runtimeClaude.mjs +78 -10
- package/bin/lib/runtimeCodex.mjs +217 -209
- package/bin/lib/runtimeDetection.mjs +8 -2
- package/bin/lib/runtimeEvents.mjs +28 -11
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +99 -14
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnDiagnostics.mjs +93 -0
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +16 -4
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +44 -13
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentBrief.mjs +92 -0
- package/bin/lib/workAgentBriefRunner.mjs +59 -0
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +82 -199
- package/bin/lib/workAgentPrecheck.mjs +76 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +131 -67
- package/bin/lib/workAgentTurnOutcome.mjs +11 -2
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +64 -7
- package/bin/lib/workArtifactReconcile.mjs +79 -0
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +119 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +18 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +113 -31
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +256 -199
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workToolLog.mjs +70 -3
- package/bin/lib/workTurnShape.mjs +165 -0
- package/npm-shrinkwrap.json +69 -0
- package/package.json +9 -4
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
|
@@ -16,6 +16,9 @@ import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { RUNTIMES } from './runtimes.mjs';
|
|
19
|
+
import { unattendedEnv } from './noWindowEnv.mjs';
|
|
20
|
+
import { claudeProjectDir } from './claudeSessions.mjs';
|
|
21
|
+
import { CLAUDE_SESSION_RE } from './conversationIds.mjs';
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* WHAT THE CLI SAID IT CAN BE ASKED FOR BY NAME — the machine's skills.
|
|
@@ -103,29 +106,134 @@ export function knownSkills() {
|
|
|
103
106
|
let mcpServersCache = null;
|
|
104
107
|
|
|
105
108
|
export const MCP_STATUSES = new Set(['connected', 'failed', 'needs-auth', 'pending']);
|
|
109
|
+
/** The server's own bounds (shared `MAX_MACHINE_MCP_SERVERS` and
|
|
110
|
+
* `MCP_SERVER_NAME_MAX`); scripts/check-app-parity.mjs holds them equal. */
|
|
106
111
|
export const MAX_MCP_SERVERS = 40;
|
|
107
112
|
export const MCP_NAME_MAX = 60;
|
|
113
|
+
/** The name charset the server's Codex report parser admits
|
|
114
|
+
* (runtimeOfferings.ts `name`) — Claude's report has none, and its connector
|
|
115
|
+
* names carry a space (`claude.ai Gmail`). */
|
|
116
|
+
export const CODEX_MCP_NAME = /^[A-Za-z0-9_.:-]+$/;
|
|
108
117
|
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
/**
|
|
119
|
+
* ONE SHAPE FOR EVERY MCP REPORT THAT LEAVES THE BOX (2026-10-10, the audit)
|
|
120
|
+
* — Claude's `mcp` off a turn's init, and Codex's `rtp.codex`
|
|
121
|
+
* (runtimeOfferings.mjs) — bounded exactly as the server re-applies it, so
|
|
122
|
+
* nothing is cut silently at the boundary: names trimmed and CLIPPED at
|
|
123
|
+
* MCP_NAME_MAX (never dropped for length), `charset` when the server's parser
|
|
124
|
+
* has one, this daemon's own `flowviant` and every `connected` server dropped
|
|
125
|
+
* (see the header), the first of a name kept, sorted, at most
|
|
126
|
+
* MAX_MCP_SERVERS. A status off the closed list is `other`. `entries` are
|
|
127
|
+
* `{ name, status }`.
|
|
128
|
+
*/
|
|
129
|
+
export function normalizeMcpReport(entries, { charset = null } = {}) {
|
|
111
130
|
const seen = new Map();
|
|
112
|
-
for (const e of
|
|
131
|
+
for (const e of Array.isArray(entries) ? entries : []) {
|
|
113
132
|
if (!e || typeof e !== 'object' || typeof e.name !== 'string') continue;
|
|
114
133
|
const n = e.name.trim().slice(0, MCP_NAME_MAX).trim();
|
|
115
134
|
if (!n || n === 'flowviant' || seen.has(n)) continue;
|
|
135
|
+
if (charset && !charset.test(n)) continue;
|
|
116
136
|
if (e.status === 'connected') continue; // see the header: never relayed
|
|
117
137
|
seen.set(n, { n, s: MCP_STATUSES.has(e.status) ? e.status : 'other' });
|
|
118
138
|
}
|
|
119
|
-
|
|
139
|
+
return [...seen.values()]
|
|
120
140
|
.sort((a, b) => (a.n < b.n ? -1 : a.n > b.n ? 1 : 0))
|
|
121
141
|
.slice(0, MAX_MCP_SERVERS);
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
/**
|
|
145
|
+
* EVERY SERVER CLAUDE CODE REPORTED, CONNECTED ONES INCLUDED, WITH ITS SOURCE
|
|
146
|
+
* AND — WHEN CONNECTED — ITS TOOLS' NAMES (2026-10-13, the owner: name every
|
|
147
|
+
* MCP server and capability each CLI gives the project). Learned off the same
|
|
148
|
+
* init as the list above, under the same harvest rule; it leaves the box only
|
|
149
|
+
* on this box's own `tools-report` (mcpInventory.mjs), never on the poll, and
|
|
150
|
+
* carries names, statuses and sources alone. The header's "only what is not
|
|
151
|
+
* connected leaves the box" still holds for the poll's `mcp`, which an older
|
|
152
|
+
* app reads.
|
|
153
|
+
*
|
|
154
|
+
* `scope` is the CLI's own word (measured on Claude Code 2.1.296's scope list:
|
|
155
|
+
* local, user, project, dynamic, enterprise, claudeai, managed, agent);
|
|
156
|
+
* mcpInventory.mjs maps it to a source. Tool names come from the init's
|
|
157
|
+
* `tools`, where a server's tools are `mcp__<server>__<tool>` and the server
|
|
158
|
+
* part is its name with every character outside `[A-Za-z0-9_-]` made `_`
|
|
159
|
+
* (`claude.ai Gmail` → `claude_ai_Gmail`, measured 2026-10-11).
|
|
160
|
+
*/
|
|
161
|
+
let mcpInventoryCache = null;
|
|
162
|
+
const MAX_INVENTORY_SERVERS = 80;
|
|
163
|
+
const mcpToolPrefix = (name) => `mcp__${name.replace(/[^A-Za-z0-9_-]/g, '_')}__`;
|
|
164
|
+
|
|
165
|
+
/** The init's servers and tools → `[{ name, status, scope, tools }]`. */
|
|
166
|
+
export function claudeMcpInventoryOf(list, tools) {
|
|
167
|
+
const names = Array.isArray(tools) ? tools.filter((t) => typeof t === 'string' && t.startsWith('mcp__')) : [];
|
|
168
|
+
const seen = new Map();
|
|
169
|
+
for (const e of list) {
|
|
170
|
+
if (!e || typeof e !== 'object' || typeof e.name !== 'string') continue;
|
|
171
|
+
const name = e.name.trim().slice(0, MCP_NAME_MAX).trim();
|
|
172
|
+
if (!name || name === 'flowviant' || seen.has(name)) continue;
|
|
173
|
+
const status = MCP_STATUSES.has(e.status) ? e.status : 'other';
|
|
174
|
+
seen.set(name, { name, status, scope: typeof e.scope === 'string' ? e.scope : typeof e.source === 'string' ? e.source : null, prefix: mcpToolPrefix(e.name.trim()), tools: status === 'connected' ? [] : null });
|
|
175
|
+
}
|
|
176
|
+
// The longest prefix wins, so `a_b` never claims `a`'s tools or the reverse.
|
|
177
|
+
const byPrefix = [...seen.values()].sort((a, b) => b.prefix.length - a.prefix.length);
|
|
178
|
+
for (const tool of names) {
|
|
179
|
+
const owner = byPrefix.find((s) => tool.startsWith(s.prefix));
|
|
180
|
+
if (owner?.tools) owner.tools.push(tool.slice(owner.prefix.length));
|
|
181
|
+
}
|
|
182
|
+
return [...seen.values()]
|
|
183
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
184
|
+
.slice(0, MAX_INVENTORY_SERVERS)
|
|
185
|
+
.map(({ prefix, ...server }) => server);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function recordMcpServers(list, tools) {
|
|
189
|
+
if (!Array.isArray(list)) return;
|
|
190
|
+
mcpServersCache = normalizeMcpReport(list);
|
|
191
|
+
mcpInventoryCache = claudeMcpInventoryOf(list, tools);
|
|
192
|
+
}
|
|
193
|
+
|
|
124
194
|
/** What to send on the roster poll as `mcp` — null until a turn has taught us. */
|
|
125
195
|
export function knownMcpServers() {
|
|
126
196
|
return mcpServersCache;
|
|
127
197
|
}
|
|
128
198
|
|
|
199
|
+
/** Claude Code's whole server list for the box's inventory — null until a
|
|
200
|
+
* turn (or the probe) has taught us. */
|
|
201
|
+
export function knownClaudeMcpInventory() {
|
|
202
|
+
return mcpInventoryCache;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* WHICH TURNS ARE A MEASUREMENT OF THE MACHINE (2026-10-10, the audit) —
|
|
207
|
+
* asked by runTurn.mjs, the one spawn point, of the argv the adapter built.
|
|
208
|
+
*
|
|
209
|
+
* Every report above is "last turn wins", so a turn whose CLI was told to see
|
|
210
|
+
* less than the machine has must not be recorded: a fenced turn
|
|
211
|
+
* (`--setting-sources ''` — the planner, the wiki, the capture chat, a design
|
|
212
|
+
* card) reads no personal or project skills, and an agent's code turn reads
|
|
213
|
+
* `--setting-sources user` plus the reviewed `--plugin-dir`; a
|
|
214
|
+
* `--strict-mcp-config` turn mounts only what its argv names. Recorded, each
|
|
215
|
+
* wrote its narrowed list over the machine's — a claude.ai connector that
|
|
216
|
+
* needs a sign-in vanished from the Machines page until the next tab turn put
|
|
217
|
+
* it back. ANY NAMED SOURCE LIST NARROWS MCP TOO, strict or not: the person's
|
|
218
|
+
* plugins ride the user file (claudePersonal.mjs), so an empty list drops
|
|
219
|
+
* their servers, and a project `.mcp.json` is admitted by the project and
|
|
220
|
+
* local files. The capture chat is fenced but not strict — gated on strict
|
|
221
|
+
* alone, its init still wrote over the machine's report. So skills are
|
|
222
|
+
* harvested only off a turn that names no setting source and no plugin, MCP
|
|
223
|
+
* only off one that names no setting source and is not strict, and both only
|
|
224
|
+
* off Claude Code, whose init carries them. A box that runs only agents
|
|
225
|
+
* learns from the startup probe and its Terminal tabs.
|
|
226
|
+
*/
|
|
227
|
+
export function initHarvestFor(rt, args) {
|
|
228
|
+
const argv = Array.isArray(args) ? args : [];
|
|
229
|
+
const claude = rt?.id === 'claude';
|
|
230
|
+
const sourcesNamed = argv.includes('--setting-sources');
|
|
231
|
+
return {
|
|
232
|
+
skills: claude && !sourcesNamed && !argv.includes('--plugin-dir'),
|
|
233
|
+
mcp: claude && !sourcesNamed && !argv.includes('--strict-mcp-config'),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
129
237
|
/**
|
|
130
238
|
* LEARN WHAT `/` CAN OFFER, ON A MACHINE NO TURN HAS TAUGHT.
|
|
131
239
|
*
|
|
@@ -189,10 +297,20 @@ const SKILL_PROBE_MAX_LINES = 20;
|
|
|
189
297
|
|
|
190
298
|
let skillProbeStarted = false;
|
|
191
299
|
|
|
192
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* Where Claude Code keeps a transcript for `cwd`: its own directory rule,
|
|
302
|
+
* `claudeProjectDir` (claudeSessions.mjs), first.
|
|
303
|
+
*
|
|
304
|
+
* ONE RULE, NOT TWO (2026-10-10, the audit). This spelled the munge itself as
|
|
305
|
+
* `replace(/[/.]/g, '-')` — the rule claudeSessions.mjs retired on 2026-09-24
|
|
306
|
+
* because it disagrees with Claude Code for any path holding a character other
|
|
307
|
+
* than a letter, digit, `/` or `.` — so a checkout at `~/code/my_app` built the
|
|
308
|
+
* wrong primary path and every removal fell through to the scan below. The
|
|
309
|
+
* store's root is unchanged: `~/.claude`, as it was.
|
|
310
|
+
*/
|
|
193
311
|
function transcriptCandidates(cwd, sessionId) {
|
|
194
312
|
const base = join(homedir(), '.claude', 'projects');
|
|
195
|
-
const out = [join(base, cwd
|
|
313
|
+
const out = [join(base, claudeProjectDir(cwd), `${sessionId}.jsonl`)];
|
|
196
314
|
// The munge is Claude Code's, not ours, so a version that changes it must not
|
|
197
315
|
// leave the file behind: fall back to finding our own session id by name.
|
|
198
316
|
try {
|
|
@@ -207,15 +325,15 @@ function transcriptCandidates(cwd, sessionId) {
|
|
|
207
325
|
* Delete the transcript a headless `-p` turn left behind.
|
|
208
326
|
*
|
|
209
327
|
* EXPORTED because the skills probe is no longer the only thing that runs one:
|
|
210
|
-
*
|
|
211
|
-
* such turn has the same footprint.
|
|
212
|
-
* `~/.claude/projects/<munged-cwd>/<id>.jsonl` at startup, and
|
|
328
|
+
* the planner, the pre-review and intake each run a read-only consult turn
|
|
329
|
+
* (workCappedTurn.mjs), and every such turn has the same footprint.
|
|
330
|
+
* `claude -p` writes `~/.claude/projects/<munged-cwd>/<id>.jsonl` at startup, and
|
|
213
331
|
* `claudeSessions.mjs` offers the newest ENDED session per directory as
|
|
214
332
|
* ADOPTABLE — so anything we run for our own purposes would put a phantom
|
|
215
333
|
* untitled session in somebody's `+` menu.
|
|
216
334
|
*/
|
|
217
335
|
export function removeProbeTranscript(cwd, sessionId) {
|
|
218
|
-
if (!sessionId ||
|
|
336
|
+
if (!sessionId || !CLAUDE_SESSION_RE.test(sessionId)) return;
|
|
219
337
|
for (const f of transcriptCandidates(cwd, sessionId)) {
|
|
220
338
|
try {
|
|
221
339
|
if (existsSync(f)) {
|
|
@@ -228,6 +346,21 @@ export function removeProbeTranscript(cwd, sessionId) {
|
|
|
228
346
|
}
|
|
229
347
|
}
|
|
230
348
|
|
|
349
|
+
/**
|
|
350
|
+
* REMOVE IT SOON, NOT NOW — the one spelling of the delayed removal every
|
|
351
|
+
* headless turn of ours keeps (the skills probe here, and the capped consult
|
|
352
|
+
* turn of workCappedTurn.mjs that the planner, the pre-review and intake run).
|
|
353
|
+
*
|
|
354
|
+
* AFTER the kill and ON A DELAY: the transcript is the CHILD's file, so
|
|
355
|
+
* deleting it while the child is still dying races a recreate. Unref'd — a
|
|
356
|
+
* cleanup must never hold the process open. A turn that never named its
|
|
357
|
+
* conversation has nothing of ours to remove.
|
|
358
|
+
*/
|
|
359
|
+
export function removeTranscriptSoon(cwd, sessionId, ms = 750) {
|
|
360
|
+
if (!sessionId) return;
|
|
361
|
+
setTimeout(() => removeProbeTranscript(cwd, sessionId), ms).unref?.();
|
|
362
|
+
}
|
|
363
|
+
|
|
231
364
|
/**
|
|
232
365
|
* One line of the probe's stdout → the init event's payload, or null for "not
|
|
233
366
|
* it, keep reading".
|
|
@@ -257,30 +390,76 @@ export function parseInitLine(line) {
|
|
|
257
390
|
skills: Array.isArray(ev.skills) ? ev.skills : null,
|
|
258
391
|
// The CLI's mounted MCP servers (0.97.0) — `recordMcpServers` normalises.
|
|
259
392
|
mcpServers: Array.isArray(ev.mcp_servers) ? ev.mcp_servers : null,
|
|
393
|
+
// Its tool names, for the inventory's per-server lists (2026-10-13).
|
|
394
|
+
tools: Array.isArray(ev.tools) ? ev.tools : null,
|
|
260
395
|
sessionId: typeof ev.session_id === 'string' ? ev.session_id : null,
|
|
261
396
|
};
|
|
262
397
|
}
|
|
263
398
|
|
|
399
|
+
/**
|
|
400
|
+
* THE PROBE IS AN UNATTENDED CLAUDE SESSION, AND RUNS AS ONE (2026-10-10, the
|
|
401
|
+
* audit). It runs in the operator's checkout at every daemon start, and its
|
|
402
|
+
* `claude` started the person's stdio MCP servers and fired SessionStart hooks
|
|
403
|
+
* — the checkout's `.claude/settings.json` ones included, which fire even in
|
|
404
|
+
* an untrusted workspace (decisions.md, 2026-09-29) — with the daemon's whole
|
|
405
|
+
* environment, the machine credential in it. So:
|
|
406
|
+
* · its environment is every unattended child's (`unattendedEnv`): no
|
|
407
|
+
* machine credential, no display;
|
|
408
|
+
* · `disableAllHooks` rides a FLAG setting, which outranks the user and
|
|
409
|
+
* project files — and only hooks go: the probe mirrors what a Terminal tab
|
|
410
|
+
* sees, so no `--setting-sources` narrowing. MEASURED on Claude Code
|
|
411
|
+
* 2.1.296, offline: with the flag, neither a personal nor a project
|
|
412
|
+
* SessionStart hook started (no `hook_started`), and the init still named
|
|
413
|
+
* the personal and the project skill;
|
|
414
|
+
* · its own process group, killed whole when it finishes, so nothing it
|
|
415
|
+
* started — an MCP server, a backgrounded hook — outlives the probe.
|
|
416
|
+
*/
|
|
417
|
+
export const SKILL_PROBE_ARGS = Object.freeze([
|
|
418
|
+
'-p', 'x', '--model', 'haiku', '--output-format', 'stream-json', '--verbose',
|
|
419
|
+
'--settings', JSON.stringify({ disableAllHooks: true }),
|
|
420
|
+
]);
|
|
421
|
+
|
|
264
422
|
/**
|
|
265
423
|
* Kick off the one-shot probe. Returns immediately; the result lands in
|
|
266
424
|
* `skillsCache` and rides the NEXT poll, so nothing waits on it.
|
|
267
425
|
*
|
|
268
426
|
* 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.
|
|
427
|
+
* event and this one say the same thing, and the turn is free. `spawn` is a
|
|
428
|
+
* test's.
|
|
270
429
|
*/
|
|
271
|
-
export function probeSkillsOnce(cwd) {
|
|
430
|
+
export function probeSkillsOnce(cwd, { spawn: spawnChild = spawn } = {}) {
|
|
272
431
|
if (skillProbeStarted || skillsCache !== null) return;
|
|
273
432
|
skillProbeStarted = true;
|
|
433
|
+
runInitProbe(cwd, [], spawnChild);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* LOOK AGAIN, NOW, WITH THE BASE BRANCH'S SERVERS MOUNTED (2026-10-13) — the
|
|
438
|
+
* re-check after a confirmed MCP install (mcpInstall.mjs). The same cheap
|
|
439
|
+
* probe, same rules (hooks off, unattended, its group killed), with the base
|
|
440
|
+
* copy added through `--mcp-config` so Claude Code says whether each project
|
|
441
|
+
* server now starts; that is what clears a machine's checklist on the Machines
|
|
442
|
+
* page. A person pressed Install for this, so it is not the once-per-process
|
|
443
|
+
* probe: it runs every time it is asked, which is once per finished install.
|
|
444
|
+
* Resolves when the probe has finished (or given up).
|
|
445
|
+
*/
|
|
446
|
+
export function probeMcpNow(cwd, mcpConfigPath, { spawn: spawnChild = spawn } = {}) {
|
|
447
|
+
return new Promise((resolve) => runInitProbe(cwd, mcpConfigPath ? ['--mcp-config', mcpConfigPath] : [], spawnChild, resolve));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function runInitProbe(cwd, extraArgs, spawnChild, done = () => {}) {
|
|
274
451
|
let child;
|
|
275
452
|
try {
|
|
276
453
|
// Claude Code specifically, not the `wiki` profile's runtime pick: `skills`
|
|
277
454
|
// is Claude Code's own field and the `/` tray only renders for claude tabs.
|
|
278
|
-
child =
|
|
279
|
-
|
|
280
|
-
['
|
|
281
|
-
|
|
282
|
-
|
|
455
|
+
child = spawnChild(RUNTIMES.claude.bin, [...SKILL_PROBE_ARGS, ...extraArgs], {
|
|
456
|
+
cwd,
|
|
457
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
458
|
+
detached: true,
|
|
459
|
+
env: unattendedEnv(),
|
|
460
|
+
});
|
|
283
461
|
} catch {
|
|
462
|
+
done();
|
|
284
463
|
return; // no claude on PATH — the machine simply stays unmeasured
|
|
285
464
|
}
|
|
286
465
|
let buf = '';
|
|
@@ -290,14 +469,21 @@ export function probeSkillsOnce(cwd) {
|
|
|
290
469
|
if (settled) return;
|
|
291
470
|
settled = true;
|
|
292
471
|
clearTimeout(timer);
|
|
472
|
+
// The whole group: what the CLI started goes with it.
|
|
293
473
|
try {
|
|
294
|
-
child.kill('SIGKILL');
|
|
474
|
+
if (child.pid) process.kill(-child.pid, 'SIGKILL');
|
|
475
|
+
else child.kill('SIGKILL');
|
|
295
476
|
} catch {
|
|
296
|
-
|
|
477
|
+
try {
|
|
478
|
+
child.kill('SIGKILL');
|
|
479
|
+
} catch {
|
|
480
|
+
/* already gone */
|
|
481
|
+
}
|
|
297
482
|
}
|
|
298
483
|
// AFTER the kill, and on a delay: the transcript is the CHILD's file, so
|
|
299
484
|
// deleting it while the child still lives races a recreate.
|
|
300
|
-
|
|
485
|
+
removeTranscriptSoon(cwd, sessionId);
|
|
486
|
+
done();
|
|
301
487
|
};
|
|
302
488
|
const timer = setTimeout(() => finish(null), SKILL_PROBE_TIMEOUT_MS);
|
|
303
489
|
timer.unref?.();
|
|
@@ -323,7 +509,7 @@ export function probeSkillsOnce(cwd) {
|
|
|
323
509
|
const init = parseInitLine(line);
|
|
324
510
|
if (!init) continue;
|
|
325
511
|
if (init.skills) recordSkills(init.skills);
|
|
326
|
-
if (init.mcpServers) recordMcpServers(init.mcpServers);
|
|
512
|
+
if (init.mcpServers) recordMcpServers(init.mcpServers, init.tools);
|
|
327
513
|
finish(init.sessionId);
|
|
328
514
|
return;
|
|
329
515
|
}
|
|
@@ -332,3 +518,11 @@ export function probeSkillsOnce(cwd) {
|
|
|
332
518
|
if (!settled && buf.length > 1_000_000) finish(null);
|
|
333
519
|
});
|
|
334
520
|
}
|
|
521
|
+
|
|
522
|
+
/** Tests only: forget what was learned, and that the probe ran. */
|
|
523
|
+
export function resetCapabilitiesForTest() {
|
|
524
|
+
skillsCache = null;
|
|
525
|
+
mcpServersCache = null;
|
|
526
|
+
mcpInventoryCache = null;
|
|
527
|
+
skillProbeStarted = false;
|
|
528
|
+
}
|
|
@@ -14,7 +14,12 @@ import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { MODEL, USER_AGENT } from './config.mjs';
|
|
17
|
-
import { NO_SETTING_SOURCES, personalClaudeSettings, withPersonalSettings } from './claudePersonal.mjs';
|
|
17
|
+
import { NO_SETTING_SOURCES, personalClaudeSettings, releaseTurnSettings, withPersonalSettings } from './claudePersonal.mjs';
|
|
18
|
+
import { withMachineMcp } from './claudePosture.mjs';
|
|
19
|
+
|
|
20
|
+
/** The profiles an agent lane mounts the machine's MCP servers on (2026-10-11):
|
|
21
|
+
* the ones whose turn profile says `projectMcp` and that Claude declares. */
|
|
22
|
+
const MACHINE_MCP_PROFILES = Object.freeze(['build', 'design', 'research']);
|
|
18
23
|
|
|
19
24
|
function claudeMcp(token, mcpUrl) {
|
|
20
25
|
const dir = mkdtempSync(join(tmpdir(), 'flowviant-mcp-'));
|
|
@@ -53,7 +58,10 @@ export const CLAUDE_RUNTIME = {
|
|
|
53
58
|
* contract was written, not a claim that only Claude could ever satisfy it.
|
|
54
59
|
*/
|
|
55
60
|
// `design` and `research` (0.97.0): each is an `--allowedTools` list with a
|
|
56
|
-
// PATH-SCOPED write (claudePosture.mjs)
|
|
61
|
+
// PATH-SCOPED write (claudePosture.mjs); an agent's turn mounts the
|
|
62
|
+
// project's base MCP servers (2026-10-10) and this machine's own
|
|
63
|
+
// (2026-10-11). `research` is Codex's too since 0.117.0,
|
|
64
|
+
// over an isolated copy (codexResearch.mjs);
|
|
57
65
|
// `design` is Codex's too since 0.115.0 (2026-09-29), whose permission
|
|
58
66
|
// profile was measured to write only the artifacts directory
|
|
59
67
|
// (runtimeCodex.mjs `codexDesignFence`). agy's flags cannot express "write
|
|
@@ -70,13 +78,41 @@ export const CLAUDE_RUNTIME = {
|
|
|
70
78
|
* fails when any runtime's row differs from the server's.
|
|
71
79
|
*/
|
|
72
80
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
81
|
+
/**
|
|
82
|
+
* HOW A TERMINAL TAB SPEAKS ON THIS CLI (audit 2026-10-10, F159) — the
|
|
83
|
+
* facts the session turn's shape reads (workTurnShape.mjs), declared on the
|
|
84
|
+
* row instead of spelled as an id check in the lane. Daemon-internal:
|
|
85
|
+
* runtimeDetection maps rows to fields of its own, so none of this is on
|
|
86
|
+
* the wire.
|
|
87
|
+
* tab: 'mcp' — the session tools ride a per-invocation MCP config.
|
|
88
|
+
* planTurns — a tab's plan turn can run here: `--permission-mode plan`
|
|
89
|
+
* is Claude Code's own switch (claudePosture.mjs, PLAN_MODE_PERM). Not
|
|
90
|
+
* named `planMode`, the retired runTurn switch (turnProfile.mjs,
|
|
91
|
+
* RETIRED_POSTURE_KEYS), so no spelling of it reads as one.
|
|
92
|
+
* adopt: 'fork' — a terminal conversation is forked into the tab's
|
|
93
|
+
* worktree (`--resume <id> --fork-session`).
|
|
94
|
+
*/
|
|
95
|
+
tab: 'mcp',
|
|
96
|
+
planTurns: true,
|
|
97
|
+
adopt: 'fork',
|
|
73
98
|
mcp: claudeMcp,
|
|
74
99
|
/**
|
|
75
100
|
* Claude takes the operating contract as a real system prompt, which is the
|
|
76
101
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
77
102
|
* above the conversation rather than inside it.
|
|
78
103
|
*/
|
|
79
|
-
args({ prompt, system, model, effort, resume,
|
|
104
|
+
args({ prompt, system, model, effort, resume, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, vaultDir, agentTools, personalSettings, machineMcp }) {
|
|
105
|
+
// THE CARTOGRAPHER'S FENCE NAMES ITS VAULT (2026-10-10): its one write rule
|
|
106
|
+
// is the vault's absolute path (claudePosture.mjs `wikiPermFor`), so a turn
|
|
107
|
+
// with no vault, or one no rule can name, has no list — refused here in
|
|
108
|
+
// words, as Codex's builder refuses an image turn with no worktree, and
|
|
109
|
+
// never run on a list it did not build.
|
|
110
|
+
if (profile === 'wiki' && !vaultDir) {
|
|
111
|
+
throw new Error("a wiki turn needs its vault to fence — none reached claude's builder");
|
|
112
|
+
}
|
|
113
|
+
if (profile === 'wiki' && !Array.isArray(perm)) {
|
|
114
|
+
throw new Error(`a wiki turn's vault must be a path a Claude Code permission rule can name — ${JSON.stringify(vaultDir)} is not`);
|
|
115
|
+
}
|
|
80
116
|
const a = [];
|
|
81
117
|
// THREE ANSWERS TO ONE QUESTION — "what conversation is this?" — and they
|
|
82
118
|
// are mutually exclusive, never combined.
|
|
@@ -132,28 +168,60 @@ export const CLAUDE_RUNTIME = {
|
|
|
132
168
|
*/
|
|
133
169
|
const fenced = profile !== 'build';
|
|
134
170
|
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
171
|
+
// A FENCED POSTURE NAMES ITS OWN MODE (2026-10-10). Its lists fence only
|
|
172
|
+
// under the CLI's `default` mode, where a tool the list does not name is
|
|
173
|
+
// refused because nobody is there to approve it. With no settings read,
|
|
174
|
+
// Claude Code 2.1.296 measured its built-in mode as `auto` (init
|
|
175
|
+
// `permissionMode: "auto"`, claudePersonal.test.mjs's real-CLI case) — a
|
|
176
|
+
// classifier that may approve what the list never named. So the posture
|
|
177
|
+
// states `default` itself, unless its own list names a mode (plan-mode).
|
|
178
|
+
if (fenced && !perm.includes('--permission-mode')) a.push('--permission-mode', 'default');
|
|
135
179
|
else if (agentTools) a.push('--setting-sources', 'user');
|
|
136
180
|
if (agentTools) {
|
|
137
181
|
a.push('--system-prompt-snapshot', 'off');
|
|
138
182
|
if (agentTools.pluginDir) a.push('--plugin-dir', agentTools.pluginDir);
|
|
139
183
|
}
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
184
|
+
// WHICH MCP SERVERS A TURN MOUNTS. An agent's code, write-up and
|
|
185
|
+
// mockup/3D-model/deck turns (2026-10-11, the owner) mount this machine's
|
|
186
|
+
// own — its user and local servers and the account's claude.ai connectors —
|
|
187
|
+
// beside the base branch's `.mcp.json` copy the caller passes, so they are
|
|
188
|
+
// not strict; `machineMcp` names every `.mcp.json` server from the
|
|
189
|
+
// worktree up, which `withMachineMcp` disables so a worktree edit never
|
|
190
|
+
// mounts before review (claudePosture.mjs). Every other fenced posture —
|
|
191
|
+
// consult, wiki, and a design/research turn no lane mounted for — stays
|
|
192
|
+
// strict: it never discovers a .mcp.json, nor loads the machine's servers.
|
|
193
|
+
// The plan profile keeps its own principal.
|
|
194
|
+
const machine = Array.isArray(machineMcp) && MACHINE_MCP_PROFILES.includes(profile);
|
|
195
|
+
if (['consult', 'wiki', 'design', 'research'].includes(profile) && !machine && !mcp.includes('--strict-mcp-config')) a.push('--strict-mcp-config');
|
|
144
196
|
a.push(...mcp, ...resultSchemaArgs);
|
|
145
197
|
a.push('--model', model || MODEL);
|
|
146
198
|
if (effort) a.push('--effort', effort);
|
|
147
|
-
|
|
199
|
+
// ALWAYS THE EVENT STREAM (2026-10-10): runTurn reads every turn line by
|
|
200
|
+
// line — the activity, the init's skills and servers, the result.
|
|
201
|
+
// A completed thinking block can arrive minutes after reasoning began.
|
|
202
|
+
// Ask for its start too, so both live surfaces can relay that fact.
|
|
203
|
+
a.push('--output-format', 'stream-json', '--verbose', '--include-partial-messages');
|
|
148
204
|
// The knowledge library (0.94.0) sits in the checkout, outside a
|
|
149
205
|
// worktree's cwd: named as a readable directory so a curated profile
|
|
150
206
|
// does not refuse the path the prompt just handed it. Before `perm`,
|
|
151
207
|
// because `--allowedTools` is variadic and would swallow the flag.
|
|
152
208
|
if (knowledgeDir) a.push('--add-dir', knowledgeDir);
|
|
209
|
+
// THE VAULT, likewise (2026-10-10): it lives under ~/.flowviant/vaults,
|
|
210
|
+
// outside the wiki worktree, and its `mkdir`/`rm` are path-validated to the
|
|
211
|
+
// working directories — the list's `Edit(//<vault>/**)` names the write,
|
|
212
|
+
// this names the directory. Antigravity's adapter hands it over the same way.
|
|
213
|
+
if (profile === 'wiki') a.push('--add-dir', vaultDir);
|
|
153
214
|
// `personalSettings` is the caller's copy when it has one (a test); a
|
|
154
|
-
// turn reads the person's file here, at spawn, as the CLI would.
|
|
155
|
-
a
|
|
215
|
+
// turn reads the person's file here, at spawn, as the CLI would. What is
|
|
216
|
+
// kept is written to a private file, never onto argv (claudePersonal.mjs)
|
|
217
|
+
// — so this is the LAST thing built: nothing after it may throw and
|
|
218
|
+
// leave the file with no turn to release it.
|
|
219
|
+
const postured = machine ? withMachineMcp(perm, machineMcp) : perm;
|
|
220
|
+
a.push(...(fenced ? withPersonalSettings(postured, personalSettings ?? personalClaudeSettings()) : postured));
|
|
156
221
|
return a;
|
|
157
222
|
},
|
|
223
|
+
/** What `args` wrote for a turn — the fenced posture's settings file —
|
|
224
|
+
* removed when runTurn's child settles. */
|
|
225
|
+
releaseArgs: releaseTurnSettings,
|
|
158
226
|
parse: null, // claudeStream.mjs owns its own stream parser (unchanged)
|
|
159
227
|
};
|