flowviant 0.116.0 → 0.117.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 +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -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 +88 -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 +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- 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/readGuard.mjs +41 -7
- 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/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -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 +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- 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/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- 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 +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +71 -139
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -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 +96 -30
- 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 +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
|
@@ -136,6 +136,16 @@ export const ANTIGRAVITY_RUNTIME = {
|
|
|
136
136
|
/** No measured ladder, and ignorance never refuses: the full list, as the
|
|
137
137
|
* server's `RUNTIME_EFFORTS.antigravity` keeps it. */
|
|
138
138
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
139
|
+
/**
|
|
140
|
+
* A TERMINAL TAB ON AGY RUNS PLAIN (audit 2026-10-10, F159; see
|
|
141
|
+
* runtimeClaude.mjs for the three facts): no MCP to mount — no credential,
|
|
142
|
+
* no cards, no streaming; the answer lands through the daemon's own report.
|
|
143
|
+
* No plan mode. Adoption MOVES the conversation (`--conversation <id>`,
|
|
144
|
+
* below): agy has no fork, so the tab continues the terminal's own store.
|
|
145
|
+
*/
|
|
146
|
+
tab: 'plain',
|
|
147
|
+
planTurns: false,
|
|
148
|
+
adopt: 'move',
|
|
139
149
|
mcp: null,
|
|
140
150
|
args({ prompt, system, model, effort, resume, resumeConversationId, profile = 'build', vaultDir, resultSchemaArgs = [], adoptResumeId }) {
|
|
141
151
|
const a = [];
|
|
@@ -158,8 +168,10 @@ export const ANTIGRAVITY_RUNTIME = {
|
|
|
158
168
|
a.push(...resultSchemaArgs);
|
|
159
169
|
|
|
160
170
|
if (profile === 'wiki') {
|
|
161
|
-
// THE CARTOGRAPHER, at the
|
|
162
|
-
// allowed, network shut.
|
|
171
|
+
// THE CARTOGRAPHER, at the bar Claude's old wiki list set — writes
|
|
172
|
+
// allowed, network shut. (Claude's list scopes the write to the vault
|
|
173
|
+
// since 2026-10-10; agy's flags cannot express "write only this
|
|
174
|
+
// directory", so this one does not.) Both halves measured on 1.1.12:
|
|
163
175
|
// • headless cannot prompt, so anything needing approval is
|
|
164
176
|
// auto-denied — including the file writes a wiki turn exists to
|
|
165
177
|
// make. `--dangerously-skip-permissions` is what grants them.
|
|
@@ -169,9 +181,8 @@ export const ANTIGRAVITY_RUNTIME = {
|
|
|
169
181
|
// auto-approving the sandbox-bypass prompt). It does NOT reproduce on
|
|
170
182
|
// 1.1.12 — tested together, the write landed and the network stayed
|
|
171
183
|
// shut. Re-check on upgrade: if it ever does cancel, this posture
|
|
172
|
-
// silently becomes "wiki turn with internet"
|
|
173
|
-
//
|
|
174
|
-
// exfil").
|
|
184
|
+
// silently becomes "wiki turn with internet" — and the vault it writes
|
|
185
|
+
// is uploaded either way.
|
|
175
186
|
if (vaultDir) a.push('--add-dir', vaultDir);
|
|
176
187
|
a.push('--sandbox', '--dangerously-skip-permissions');
|
|
177
188
|
// A sweep reads a whole repository; the 5m default would guillotine it.
|
|
@@ -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,83 @@ 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
|
+
export function recordMcpServers(list) {
|
|
145
|
+
if (!Array.isArray(list)) return;
|
|
146
|
+
mcpServersCache = normalizeMcpReport(list);
|
|
147
|
+
}
|
|
148
|
+
|
|
124
149
|
/** What to send on the roster poll as `mcp` — null until a turn has taught us. */
|
|
125
150
|
export function knownMcpServers() {
|
|
126
151
|
return mcpServersCache;
|
|
127
152
|
}
|
|
128
153
|
|
|
154
|
+
/**
|
|
155
|
+
* WHICH TURNS ARE A MEASUREMENT OF THE MACHINE (2026-10-10, the audit) —
|
|
156
|
+
* asked by runTurn.mjs, the one spawn point, of the argv the adapter built.
|
|
157
|
+
*
|
|
158
|
+
* Every report above is "last turn wins", so a turn whose CLI was told to see
|
|
159
|
+
* less than the machine has must not be recorded: a fenced turn
|
|
160
|
+
* (`--setting-sources ''` — the planner, the wiki, the capture chat, a design
|
|
161
|
+
* card) reads no personal or project skills, and an agent's code turn reads
|
|
162
|
+
* `--setting-sources user` plus the reviewed `--plugin-dir`; a
|
|
163
|
+
* `--strict-mcp-config` turn mounts only what its argv names. Recorded, each
|
|
164
|
+
* wrote its narrowed list over the machine's — a claude.ai connector that
|
|
165
|
+
* needs a sign-in vanished from the Machines page until the next tab turn put
|
|
166
|
+
* it back. ANY NAMED SOURCE LIST NARROWS MCP TOO, strict or not: the person's
|
|
167
|
+
* plugins ride the user file (claudePersonal.mjs), so an empty list drops
|
|
168
|
+
* their servers, and a project `.mcp.json` is admitted by the project and
|
|
169
|
+
* local files. The capture chat is fenced but not strict — gated on strict
|
|
170
|
+
* alone, its init still wrote over the machine's report. So skills are
|
|
171
|
+
* harvested only off a turn that names no setting source and no plugin, MCP
|
|
172
|
+
* only off one that names no setting source and is not strict, and both only
|
|
173
|
+
* off Claude Code, whose init carries them. A box that runs only agents
|
|
174
|
+
* learns from the startup probe and its Terminal tabs.
|
|
175
|
+
*/
|
|
176
|
+
export function initHarvestFor(rt, args) {
|
|
177
|
+
const argv = Array.isArray(args) ? args : [];
|
|
178
|
+
const claude = rt?.id === 'claude';
|
|
179
|
+
const sourcesNamed = argv.includes('--setting-sources');
|
|
180
|
+
return {
|
|
181
|
+
skills: claude && !sourcesNamed && !argv.includes('--plugin-dir'),
|
|
182
|
+
mcp: claude && !sourcesNamed && !argv.includes('--strict-mcp-config'),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
129
186
|
/**
|
|
130
187
|
* LEARN WHAT `/` CAN OFFER, ON A MACHINE NO TURN HAS TAUGHT.
|
|
131
188
|
*
|
|
@@ -189,10 +246,20 @@ const SKILL_PROBE_MAX_LINES = 20;
|
|
|
189
246
|
|
|
190
247
|
let skillProbeStarted = false;
|
|
191
248
|
|
|
192
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Where Claude Code keeps a transcript for `cwd`: its own directory rule,
|
|
251
|
+
* `claudeProjectDir` (claudeSessions.mjs), first.
|
|
252
|
+
*
|
|
253
|
+
* ONE RULE, NOT TWO (2026-10-10, the audit). This spelled the munge itself as
|
|
254
|
+
* `replace(/[/.]/g, '-')` — the rule claudeSessions.mjs retired on 2026-09-24
|
|
255
|
+
* because it disagrees with Claude Code for any path holding a character other
|
|
256
|
+
* than a letter, digit, `/` or `.` — so a checkout at `~/code/my_app` built the
|
|
257
|
+
* wrong primary path and every removal fell through to the scan below. The
|
|
258
|
+
* store's root is unchanged: `~/.claude`, as it was.
|
|
259
|
+
*/
|
|
193
260
|
function transcriptCandidates(cwd, sessionId) {
|
|
194
261
|
const base = join(homedir(), '.claude', 'projects');
|
|
195
|
-
const out = [join(base, cwd
|
|
262
|
+
const out = [join(base, claudeProjectDir(cwd), `${sessionId}.jsonl`)];
|
|
196
263
|
// The munge is Claude Code's, not ours, so a version that changes it must not
|
|
197
264
|
// leave the file behind: fall back to finding our own session id by name.
|
|
198
265
|
try {
|
|
@@ -207,15 +274,15 @@ function transcriptCandidates(cwd, sessionId) {
|
|
|
207
274
|
* Delete the transcript a headless `-p` turn left behind.
|
|
208
275
|
*
|
|
209
276
|
* 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
|
|
277
|
+
* the planner, the pre-review and intake each run a read-only consult turn
|
|
278
|
+
* (workCappedTurn.mjs), and every such turn has the same footprint.
|
|
279
|
+
* `claude -p` writes `~/.claude/projects/<munged-cwd>/<id>.jsonl` at startup, and
|
|
213
280
|
* `claudeSessions.mjs` offers the newest ENDED session per directory as
|
|
214
281
|
* ADOPTABLE — so anything we run for our own purposes would put a phantom
|
|
215
282
|
* untitled session in somebody's `+` menu.
|
|
216
283
|
*/
|
|
217
284
|
export function removeProbeTranscript(cwd, sessionId) {
|
|
218
|
-
if (!sessionId ||
|
|
285
|
+
if (!sessionId || !CLAUDE_SESSION_RE.test(sessionId)) return;
|
|
219
286
|
for (const f of transcriptCandidates(cwd, sessionId)) {
|
|
220
287
|
try {
|
|
221
288
|
if (existsSync(f)) {
|
|
@@ -228,6 +295,21 @@ export function removeProbeTranscript(cwd, sessionId) {
|
|
|
228
295
|
}
|
|
229
296
|
}
|
|
230
297
|
|
|
298
|
+
/**
|
|
299
|
+
* REMOVE IT SOON, NOT NOW — the one spelling of the delayed removal every
|
|
300
|
+
* headless turn of ours keeps (the skills probe here, and the capped consult
|
|
301
|
+
* turn of workCappedTurn.mjs that the planner, the pre-review and intake run).
|
|
302
|
+
*
|
|
303
|
+
* AFTER the kill and ON A DELAY: the transcript is the CHILD's file, so
|
|
304
|
+
* deleting it while the child is still dying races a recreate. Unref'd — a
|
|
305
|
+
* cleanup must never hold the process open. A turn that never named its
|
|
306
|
+
* conversation has nothing of ours to remove.
|
|
307
|
+
*/
|
|
308
|
+
export function removeTranscriptSoon(cwd, sessionId, ms = 750) {
|
|
309
|
+
if (!sessionId) return;
|
|
310
|
+
setTimeout(() => removeProbeTranscript(cwd, sessionId), ms).unref?.();
|
|
311
|
+
}
|
|
312
|
+
|
|
231
313
|
/**
|
|
232
314
|
* One line of the probe's stdout → the init event's payload, or null for "not
|
|
233
315
|
* it, keep reading".
|
|
@@ -261,25 +343,50 @@ export function parseInitLine(line) {
|
|
|
261
343
|
};
|
|
262
344
|
}
|
|
263
345
|
|
|
346
|
+
/**
|
|
347
|
+
* THE PROBE IS AN UNATTENDED CLAUDE SESSION, AND RUNS AS ONE (2026-10-10, the
|
|
348
|
+
* audit). It runs in the operator's checkout at every daemon start, and its
|
|
349
|
+
* `claude` started the person's stdio MCP servers and fired SessionStart hooks
|
|
350
|
+
* — the checkout's `.claude/settings.json` ones included, which fire even in
|
|
351
|
+
* an untrusted workspace (decisions.md, 2026-09-29) — with the daemon's whole
|
|
352
|
+
* environment, the machine credential in it. So:
|
|
353
|
+
* · its environment is every unattended child's (`unattendedEnv`): no
|
|
354
|
+
* machine credential, no display;
|
|
355
|
+
* · `disableAllHooks` rides a FLAG setting, which outranks the user and
|
|
356
|
+
* project files — and only hooks go: the probe mirrors what a Terminal tab
|
|
357
|
+
* sees, so no `--setting-sources` narrowing. MEASURED on Claude Code
|
|
358
|
+
* 2.1.296, offline: with the flag, neither a personal nor a project
|
|
359
|
+
* SessionStart hook started (no `hook_started`), and the init still named
|
|
360
|
+
* the personal and the project skill;
|
|
361
|
+
* · its own process group, killed whole when it finishes, so nothing it
|
|
362
|
+
* started — an MCP server, a backgrounded hook — outlives the probe.
|
|
363
|
+
*/
|
|
364
|
+
export const SKILL_PROBE_ARGS = Object.freeze([
|
|
365
|
+
'-p', 'x', '--model', 'haiku', '--output-format', 'stream-json', '--verbose',
|
|
366
|
+
'--settings', JSON.stringify({ disableAllHooks: true }),
|
|
367
|
+
]);
|
|
368
|
+
|
|
264
369
|
/**
|
|
265
370
|
* Kick off the one-shot probe. Returns immediately; the result lands in
|
|
266
371
|
* `skillsCache` and rides the NEXT poll, so nothing waits on it.
|
|
267
372
|
*
|
|
268
373
|
* 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.
|
|
374
|
+
* event and this one say the same thing, and the turn is free. `spawn` is a
|
|
375
|
+
* test's.
|
|
270
376
|
*/
|
|
271
|
-
export function probeSkillsOnce(cwd) {
|
|
377
|
+
export function probeSkillsOnce(cwd, { spawn: spawnChild = spawn } = {}) {
|
|
272
378
|
if (skillProbeStarted || skillsCache !== null) return;
|
|
273
379
|
skillProbeStarted = true;
|
|
274
380
|
let child;
|
|
275
381
|
try {
|
|
276
382
|
// Claude Code specifically, not the `wiki` profile's runtime pick: `skills`
|
|
277
383
|
// is Claude Code's own field and the `/` tray only renders for claude tabs.
|
|
278
|
-
child =
|
|
279
|
-
|
|
280
|
-
['
|
|
281
|
-
|
|
282
|
-
|
|
384
|
+
child = spawnChild(RUNTIMES.claude.bin, [...SKILL_PROBE_ARGS], {
|
|
385
|
+
cwd,
|
|
386
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
387
|
+
detached: true,
|
|
388
|
+
env: unattendedEnv(),
|
|
389
|
+
});
|
|
283
390
|
} catch {
|
|
284
391
|
return; // no claude on PATH — the machine simply stays unmeasured
|
|
285
392
|
}
|
|
@@ -290,14 +397,20 @@ export function probeSkillsOnce(cwd) {
|
|
|
290
397
|
if (settled) return;
|
|
291
398
|
settled = true;
|
|
292
399
|
clearTimeout(timer);
|
|
400
|
+
// The whole group: what the CLI started goes with it.
|
|
293
401
|
try {
|
|
294
|
-
child.kill('SIGKILL');
|
|
402
|
+
if (child.pid) process.kill(-child.pid, 'SIGKILL');
|
|
403
|
+
else child.kill('SIGKILL');
|
|
295
404
|
} catch {
|
|
296
|
-
|
|
405
|
+
try {
|
|
406
|
+
child.kill('SIGKILL');
|
|
407
|
+
} catch {
|
|
408
|
+
/* already gone */
|
|
409
|
+
}
|
|
297
410
|
}
|
|
298
411
|
// AFTER the kill, and on a delay: the transcript is the CHILD's file, so
|
|
299
412
|
// deleting it while the child still lives races a recreate.
|
|
300
|
-
|
|
413
|
+
removeTranscriptSoon(cwd, sessionId);
|
|
301
414
|
};
|
|
302
415
|
const timer = setTimeout(() => finish(null), SKILL_PROBE_TIMEOUT_MS);
|
|
303
416
|
timer.unref?.();
|
|
@@ -332,3 +445,10 @@ export function probeSkillsOnce(cwd) {
|
|
|
332
445
|
if (!settled && buf.length > 1_000_000) finish(null);
|
|
333
446
|
});
|
|
334
447
|
}
|
|
448
|
+
|
|
449
|
+
/** Tests only: forget what was learned, and that the probe ran. */
|
|
450
|
+
export function resetCapabilitiesForTest() {
|
|
451
|
+
skillsCache = null;
|
|
452
|
+
mcpServersCache = null;
|
|
453
|
+
skillProbeStarted = false;
|
|
454
|
+
}
|
|
@@ -14,7 +14,7 @@ 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
18
|
|
|
19
19
|
function claudeMcp(token, mcpUrl) {
|
|
20
20
|
const dir = mkdtempSync(join(tmpdir(), 'flowviant-mcp-'));
|
|
@@ -53,7 +53,8 @@ export const CLAUDE_RUNTIME = {
|
|
|
53
53
|
* contract was written, not a claim that only Claude could ever satisfy it.
|
|
54
54
|
*/
|
|
55
55
|
// `design` and `research` (0.97.0): each is an `--allowedTools` list with a
|
|
56
|
-
// PATH-SCOPED write (claudePosture.mjs). `research` is
|
|
56
|
+
// PATH-SCOPED write (claudePosture.mjs). `research` is Codex's too since
|
|
57
|
+
// 0.117.0, over an isolated copy (codexResearch.mjs);
|
|
57
58
|
// `design` is Codex's too since 0.115.0 (2026-09-29), whose permission
|
|
58
59
|
// profile was measured to write only the artifacts directory
|
|
59
60
|
// (runtimeCodex.mjs `codexDesignFence`). agy's flags cannot express "write
|
|
@@ -70,13 +71,41 @@ export const CLAUDE_RUNTIME = {
|
|
|
70
71
|
* fails when any runtime's row differs from the server's.
|
|
71
72
|
*/
|
|
72
73
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
74
|
+
/**
|
|
75
|
+
* HOW A TERMINAL TAB SPEAKS ON THIS CLI (audit 2026-10-10, F159) — the
|
|
76
|
+
* facts the session turn's shape reads (workTurnShape.mjs), declared on the
|
|
77
|
+
* row instead of spelled as an id check in the lane. Daemon-internal:
|
|
78
|
+
* runtimeDetection maps rows to fields of its own, so none of this is on
|
|
79
|
+
* the wire.
|
|
80
|
+
* tab: 'mcp' — the session tools ride a per-invocation MCP config.
|
|
81
|
+
* planTurns — a tab's plan turn can run here: `--permission-mode plan`
|
|
82
|
+
* is Claude Code's own switch (claudePosture.mjs, PLAN_MODE_PERM). Not
|
|
83
|
+
* named `planMode`, the retired runTurn switch (turnProfile.mjs,
|
|
84
|
+
* RETIRED_POSTURE_KEYS), so no spelling of it reads as one.
|
|
85
|
+
* adopt: 'fork' — a terminal conversation is forked into the tab's
|
|
86
|
+
* worktree (`--resume <id> --fork-session`).
|
|
87
|
+
*/
|
|
88
|
+
tab: 'mcp',
|
|
89
|
+
planTurns: true,
|
|
90
|
+
adopt: 'fork',
|
|
73
91
|
mcp: claudeMcp,
|
|
74
92
|
/**
|
|
75
93
|
* Claude takes the operating contract as a real system prompt, which is the
|
|
76
94
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
77
95
|
* above the conversation rather than inside it.
|
|
78
96
|
*/
|
|
79
|
-
args({ prompt, system, model, effort, resume,
|
|
97
|
+
args({ prompt, system, model, effort, resume, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, vaultDir, agentTools, personalSettings }) {
|
|
98
|
+
// THE CARTOGRAPHER'S FENCE NAMES ITS VAULT (2026-10-10): its one write rule
|
|
99
|
+
// is the vault's absolute path (claudePosture.mjs `wikiPermFor`), so a turn
|
|
100
|
+
// with no vault, or one no rule can name, has no list — refused here in
|
|
101
|
+
// words, as Codex's builder refuses an image turn with no worktree, and
|
|
102
|
+
// never run on a list it did not build.
|
|
103
|
+
if (profile === 'wiki' && !vaultDir) {
|
|
104
|
+
throw new Error("a wiki turn needs its vault to fence — none reached claude's builder");
|
|
105
|
+
}
|
|
106
|
+
if (profile === 'wiki' && !Array.isArray(perm)) {
|
|
107
|
+
throw new Error(`a wiki turn's vault must be a path a Claude Code permission rule can name — ${JSON.stringify(vaultDir)} is not`);
|
|
108
|
+
}
|
|
80
109
|
const a = [];
|
|
81
110
|
// THREE ANSWERS TO ONE QUESTION — "what conversation is this?" — and they
|
|
82
111
|
// are mutually exclusive, never combined.
|
|
@@ -132,6 +161,14 @@ export const CLAUDE_RUNTIME = {
|
|
|
132
161
|
*/
|
|
133
162
|
const fenced = profile !== 'build';
|
|
134
163
|
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
164
|
+
// A FENCED POSTURE NAMES ITS OWN MODE (2026-10-10). Its lists fence only
|
|
165
|
+
// under the CLI's `default` mode, where a tool the list does not name is
|
|
166
|
+
// refused because nobody is there to approve it. With no settings read,
|
|
167
|
+
// Claude Code 2.1.296 measured its built-in mode as `auto` (init
|
|
168
|
+
// `permissionMode: "auto"`, claudePersonal.test.mjs's real-CLI case) — a
|
|
169
|
+
// classifier that may approve what the list never named. So the posture
|
|
170
|
+
// states `default` itself, unless its own list names a mode (plan-mode).
|
|
171
|
+
if (fenced && !perm.includes('--permission-mode')) a.push('--permission-mode', 'default');
|
|
135
172
|
else if (agentTools) a.push('--setting-sources', 'user');
|
|
136
173
|
if (agentTools) {
|
|
137
174
|
a.push('--system-prompt-snapshot', 'off');
|
|
@@ -144,16 +181,29 @@ export const CLAUDE_RUNTIME = {
|
|
|
144
181
|
a.push(...mcp, ...resultSchemaArgs);
|
|
145
182
|
a.push('--model', model || MODEL);
|
|
146
183
|
if (effort) a.push('--effort', effort);
|
|
147
|
-
|
|
184
|
+
// ALWAYS THE EVENT STREAM (2026-10-10): runTurn reads every turn line by
|
|
185
|
+
// line — the activity, the init's skills and servers, the result.
|
|
186
|
+
a.push('--output-format', 'stream-json', '--verbose');
|
|
148
187
|
// The knowledge library (0.94.0) sits in the checkout, outside a
|
|
149
188
|
// worktree's cwd: named as a readable directory so a curated profile
|
|
150
189
|
// does not refuse the path the prompt just handed it. Before `perm`,
|
|
151
190
|
// because `--allowedTools` is variadic and would swallow the flag.
|
|
152
191
|
if (knowledgeDir) a.push('--add-dir', knowledgeDir);
|
|
192
|
+
// THE VAULT, likewise (2026-10-10): it lives under ~/.flowviant/vaults,
|
|
193
|
+
// outside the wiki worktree, and its `mkdir`/`rm` are path-validated to the
|
|
194
|
+
// working directories — the list's `Edit(//<vault>/**)` names the write,
|
|
195
|
+
// this names the directory. Antigravity's adapter hands it over the same way.
|
|
196
|
+
if (profile === 'wiki') a.push('--add-dir', vaultDir);
|
|
153
197
|
// `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.
|
|
198
|
+
// turn reads the person's file here, at spawn, as the CLI would. What is
|
|
199
|
+
// kept is written to a private file, never onto argv (claudePersonal.mjs)
|
|
200
|
+
// — so this is the LAST thing built: nothing after it may throw and
|
|
201
|
+
// leave the file with no turn to release it.
|
|
155
202
|
a.push(...(fenced ? withPersonalSettings(perm, personalSettings ?? personalClaudeSettings()) : perm));
|
|
156
203
|
return a;
|
|
157
204
|
},
|
|
205
|
+
/** What `args` wrote for a turn — the fenced posture's settings file —
|
|
206
|
+
* removed when runTurn's child settles. */
|
|
207
|
+
releaseArgs: releaseTurnSettings,
|
|
158
208
|
parse: null, // claudeStream.mjs owns its own stream parser (unchanged)
|
|
159
209
|
};
|