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.
Files changed (135) hide show
  1. package/README.md +31 -12
  2. package/bin/cli.mjs +13 -17
  3. package/bin/lib/admission.mjs +7 -14
  4. package/bin/lib/agentInternalWait.mjs +83 -0
  5. package/bin/lib/agentPlan.mjs +56 -24
  6. package/bin/lib/agentPublish.mjs +12 -13
  7. package/bin/lib/agentTaskKinds.mjs +4 -7
  8. package/bin/lib/agySessions.mjs +3 -3
  9. package/bin/lib/artifactPreview.mjs +92 -18
  10. package/bin/lib/artifactReporter.mjs +16 -8
  11. package/bin/lib/artifacts.mjs +64 -60
  12. package/bin/lib/authproxy.mjs +2 -1
  13. package/bin/lib/childEnv.mjs +9 -4
  14. package/bin/lib/claude.mjs +10 -20
  15. package/bin/lib/claudePersonal.mjs +71 -6
  16. package/bin/lib/claudePosture.mjs +119 -42
  17. package/bin/lib/claudeSessions.mjs +7 -7
  18. package/bin/lib/claudeStream.mjs +7 -11
  19. package/bin/lib/cliLimit.mjs +2 -2
  20. package/bin/lib/codexHome.mjs +17 -0
  21. package/bin/lib/codexPersonal.mjs +44 -3
  22. package/bin/lib/codexRelay.mjs +21 -5
  23. package/bin/lib/codexResearch.mjs +180 -0
  24. package/bin/lib/config.mjs +37 -24
  25. package/bin/lib/conversationIds.mjs +51 -0
  26. package/bin/lib/credentials.mjs +2 -7
  27. package/bin/lib/daemonLocks.mjs +88 -0
  28. package/bin/lib/daemonState.mjs +5 -5
  29. package/bin/lib/deploy.mjs +6 -9
  30. package/bin/lib/deployCheckout.mjs +26 -22
  31. package/bin/lib/deployRunner.mjs +54 -18
  32. package/bin/lib/deployWire.mjs +32 -23
  33. package/bin/lib/desktopContract.mjs +3 -5
  34. package/bin/lib/env.mjs +104 -35
  35. package/bin/lib/fleet.mjs +7 -9
  36. package/bin/lib/fleetPost.mjs +172 -25
  37. package/bin/lib/fleetReports.mjs +130 -170
  38. package/bin/lib/fleetStartup.mjs +6 -6
  39. package/bin/lib/fleetWire.mjs +42 -0
  40. package/bin/lib/git.mjs +111 -61
  41. package/bin/lib/gitMergeIdentity.mjs +2 -8
  42. package/bin/lib/grant.mjs +23 -7
  43. package/bin/lib/holder.mjs +108 -81
  44. package/bin/lib/hooks/readGuard.mjs +41 -7
  45. package/bin/lib/install.mjs +80 -33
  46. package/bin/lib/instance.mjs +36 -83
  47. package/bin/lib/intakeDraft.mjs +4 -1
  48. package/bin/lib/knowledgeFileSync.mjs +31 -0
  49. package/bin/lib/knowledgeFiles.mjs +28 -83
  50. package/bin/lib/landed.mjs +22 -29
  51. package/bin/lib/launchCommand.mjs +91 -0
  52. package/bin/lib/listeners.mjs +0 -5
  53. package/bin/lib/localSessions.mjs +52 -39
  54. package/bin/lib/login.mjs +22 -12
  55. package/bin/lib/loginPath.mjs +22 -17
  56. package/bin/lib/machineListing.mjs +6 -3
  57. package/bin/lib/mcp-cli.mjs +9 -10
  58. package/bin/lib/noWindowEnv.mjs +23 -3
  59. package/bin/lib/ownedDirs.mjs +99 -0
  60. package/bin/lib/pinnedCapture.mjs +383 -0
  61. package/bin/lib/prWorkflow.mjs +94 -17
  62. package/bin/lib/preflight.mjs +8 -25
  63. package/bin/lib/preview.mjs +19 -25
  64. package/bin/lib/previewGatePolicy.mjs +34 -9
  65. package/bin/lib/previewRegistry.mjs +4 -11
  66. package/bin/lib/printable.mjs +70 -7
  67. package/bin/lib/procRegistry.mjs +33 -13
  68. package/bin/lib/procScan.mjs +2 -3
  69. package/bin/lib/procStat.mjs +76 -0
  70. package/bin/lib/processGroup.mjs +48 -0
  71. package/bin/lib/processes.mjs +10 -24
  72. package/bin/lib/projectToolRuntimes.mjs +101 -16
  73. package/bin/lib/projectTools.mjs +50 -32
  74. package/bin/lib/prompts.mjs +41 -19
  75. package/bin/lib/repoPath.mjs +19 -0
  76. package/bin/lib/repoState.mjs +17 -19
  77. package/bin/lib/runTurn.mjs +268 -210
  78. package/bin/lib/runtimeAntigravity.mjs +16 -5
  79. package/bin/lib/runtimeCapabilities.mjs +140 -20
  80. package/bin/lib/runtimeClaude.mjs +55 -5
  81. package/bin/lib/runtimeCodex.mjs +245 -205
  82. package/bin/lib/runtimeDetection.mjs +2 -2
  83. package/bin/lib/runtimeEvents.mjs +16 -10
  84. package/bin/lib/runtimeLimits.mjs +5 -6
  85. package/bin/lib/runtimeOfferings.mjs +24 -11
  86. package/bin/lib/runtimes.mjs +10 -0
  87. package/bin/lib/shipSweep.mjs +5 -3
  88. package/bin/lib/shot.mjs +91 -17
  89. package/bin/lib/sleep.mjs +11 -0
  90. package/bin/lib/startProject.mjs +5 -5
  91. package/bin/lib/statePaths.mjs +40 -0
  92. package/bin/lib/tty.mjs +25 -15
  93. package/bin/lib/turnLock.mjs +7 -14
  94. package/bin/lib/turnProfile.mjs +1 -1
  95. package/bin/lib/ui.mjs +0 -3
  96. package/bin/lib/uninstall.mjs +30 -26
  97. package/bin/lib/update.mjs +79 -27
  98. package/bin/lib/uplinkScrub.mjs +296 -31
  99. package/bin/lib/vault.mjs +78 -10
  100. package/bin/lib/vaultDiff.mjs +5 -2
  101. package/bin/lib/views.mjs +37 -12
  102. package/bin/lib/wikiRunner.mjs +40 -61
  103. package/bin/lib/work.mjs +17 -7
  104. package/bin/lib/workAdoptCarry.mjs +2 -4
  105. package/bin/lib/workAgentCheck.mjs +104 -57
  106. package/bin/lib/workAgentMerges.mjs +62 -57
  107. package/bin/lib/workAgentPlans.mjs +71 -139
  108. package/bin/lib/workAgentPrecheck.mjs +71 -138
  109. package/bin/lib/workAgentPublish.mjs +6 -8
  110. package/bin/lib/workAgentTurnBegun.mjs +15 -11
  111. package/bin/lib/workAgentTurnExecution.mjs +46 -53
  112. package/bin/lib/workAgentTurnReports.mjs +9 -37
  113. package/bin/lib/workAgentTurns.mjs +4 -3
  114. package/bin/lib/workAttachments.mjs +66 -11
  115. package/bin/lib/workBrain.mjs +9 -4
  116. package/bin/lib/workCappedTurn.mjs +109 -0
  117. package/bin/lib/workChildren.mjs +5 -5
  118. package/bin/lib/workCommandAudit.mjs +11 -2
  119. package/bin/lib/workDiffs.mjs +52 -6
  120. package/bin/lib/workIntake.mjs +35 -60
  121. package/bin/lib/workPlaceReports.mjs +7 -4
  122. package/bin/lib/workPlaces.mjs +96 -30
  123. package/bin/lib/workPreviews.mjs +47 -112
  124. package/bin/lib/workProcesses.mjs +8 -35
  125. package/bin/lib/workPullRequests.mjs +37 -56
  126. package/bin/lib/workReportQueue.mjs +100 -148
  127. package/bin/lib/workRetire.mjs +2 -1
  128. package/bin/lib/workSessionRuntime.mjs +38 -37
  129. package/bin/lib/workSessionTurns.mjs +111 -189
  130. package/bin/lib/workShip.mjs +43 -23
  131. package/bin/lib/workTurnShape.mjs +163 -0
  132. package/npm-shrinkwrap.json +59 -0
  133. package/package.json +3 -2
  134. package/bin/lib/fleetJobs.mjs +0 -120
  135. 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 same bar WIKI_PERM sets for Claude — writes
162
- // allowed, network shut. Both halves measured on 1.1.12:
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", which is the one line
173
- // WIKI_PERM draws ("Command execution is the line: it enables network
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
- export function recordMcpServers(list) {
110
- if (!Array.isArray(list)) return;
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 list) {
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
- mcpServersCache = [...seen.values()]
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
- /** Where Claude Code keeps a transcript for `cwd`: `/` and `.` both become `-`. */
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.replace(/[/.]/g, '-'), `${sessionId}.jsonl`)];
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
- * 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
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 || !/^[A-Za-z0-9_-]{8,64}$/.test(sessionId)) return;
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 = spawn(
279
- RUNTIMES.claude.bin,
280
- ['-p', 'x', '--model', 'haiku', '--output-format', 'stream-json', '--verbose'],
281
- { cwd, stdio: ['ignore', 'pipe', 'ignore'] }
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
- /* already gone */
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
- if (sessionId) setTimeout(() => removeProbeTranscript(cwd, sessionId), 750).unref?.();
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 Claude's alone;
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, streamJson, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, agentTools, personalSettings }) {
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
- if (streamJson) a.push('--output-format', 'stream-json', '--verbose');
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
  };