flowviant 0.103.0 → 0.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/bin/cli.mjs +87 -474
  2. package/bin/lib/admission.mjs +19 -3
  3. package/bin/lib/agentPlan.mjs +2 -2
  4. package/bin/lib/agentTaskKinds.mjs +130 -0
  5. package/bin/lib/agySessions.mjs +158 -0
  6. package/bin/lib/artifactContracts.mjs +153 -0
  7. package/bin/lib/artifactPolicy.mjs +45 -0
  8. package/bin/lib/artifactPreview.mjs +52 -0
  9. package/bin/lib/artifactReporter.mjs +163 -0
  10. package/bin/lib/artifacts.mjs +49 -228
  11. package/bin/lib/authReject.mjs +16 -4
  12. package/bin/lib/authproxy.mjs +47 -304
  13. package/bin/lib/boxIdentity.mjs +245 -0
  14. package/bin/lib/childEnv.mjs +11 -9
  15. package/bin/lib/claude.mjs +14 -851
  16. package/bin/lib/claudeAuth.mjs +3 -3
  17. package/bin/lib/claudePosture.mjs +370 -0
  18. package/bin/lib/claudeSessions.mjs +411 -0
  19. package/bin/lib/claudeStream.mjs +207 -0
  20. package/bin/lib/cliLimit.mjs +34 -0
  21. package/bin/lib/commandSpecs.mjs +173 -0
  22. package/bin/lib/config.mjs +11 -42
  23. package/bin/lib/credentialDate.mjs +22 -0
  24. package/bin/lib/credentials.mjs +21 -26
  25. package/bin/lib/daemonLogging.mjs +55 -0
  26. package/bin/lib/daemonState.mjs +64 -0
  27. package/bin/lib/deploy.mjs +148 -647
  28. package/bin/lib/deployCheckout.mjs +265 -0
  29. package/bin/lib/deployConfig.mjs +142 -0
  30. package/bin/lib/deployRunner.mjs +311 -0
  31. package/bin/lib/deployWire.mjs +41 -0
  32. package/bin/lib/desktopContract.mjs +56 -105
  33. package/bin/lib/env.mjs +49 -484
  34. package/bin/lib/fleet.mjs +183 -2383
  35. package/bin/lib/fleetJobs.mjs +120 -0
  36. package/bin/lib/fleetPost.mjs +40 -0
  37. package/bin/lib/fleetReports.mjs +392 -0
  38. package/bin/lib/fleetRoster.mjs +470 -0
  39. package/bin/lib/fleetStartup.mjs +196 -0
  40. package/bin/lib/fleetWake.mjs +45 -0
  41. package/bin/lib/fleetWire.mjs +46 -0
  42. package/bin/lib/git.mjs +2 -2
  43. package/bin/lib/gitMergeIdentity.mjs +51 -0
  44. package/bin/lib/grant.mjs +5 -3
  45. package/bin/lib/help.mjs +3 -125
  46. package/bin/lib/holder.mjs +482 -0
  47. package/bin/lib/instance.mjs +124 -46
  48. package/bin/lib/knowledge.mjs +16 -799
  49. package/bin/lib/knowledgeFetch.mjs +38 -0
  50. package/bin/lib/knowledgeFileSync.mjs +112 -0
  51. package/bin/lib/knowledgeFiles.mjs +489 -0
  52. package/bin/lib/knowledgeLibrary.mjs +299 -0
  53. package/bin/lib/landed.mjs +2 -1
  54. package/bin/lib/listeners.mjs +99 -141
  55. package/bin/lib/localSessions.mjs +30 -555
  56. package/bin/lib/login.mjs +3 -2
  57. package/bin/lib/loginPath.mjs +3 -5
  58. package/bin/lib/machineDisconnect.mjs +203 -0
  59. package/bin/lib/machineEnv.mjs +22 -0
  60. package/bin/lib/machineLimits.mjs +61 -0
  61. package/bin/lib/machineListing.mjs +338 -0
  62. package/bin/lib/machines.mjs +78 -455
  63. package/bin/lib/machinesCommand.mjs +214 -0
  64. package/bin/lib/mcp-cli.mjs +2 -1
  65. package/bin/lib/patchRevert.mjs +60 -0
  66. package/bin/lib/prWorkflow.mjs +157 -0
  67. package/bin/lib/preflight.mjs +2 -1
  68. package/bin/lib/pressurePolicy.mjs +104 -0
  69. package/bin/lib/preview.mjs +2 -272
  70. package/bin/lib/previewFramePolicy.mjs +112 -0
  71. package/bin/lib/previewGatePolicy.mjs +236 -0
  72. package/bin/lib/previewRegistry.mjs +235 -0
  73. package/bin/lib/printable.mjs +2 -2
  74. package/bin/lib/procRegistry.mjs +28 -18
  75. package/bin/lib/procScan.mjs +146 -0
  76. package/bin/lib/processes.mjs +29 -37
  77. package/bin/lib/projectToolRuntimes.mjs +138 -0
  78. package/bin/lib/projectTools.mjs +13 -88
  79. package/bin/lib/prompts.mjs +105 -139
  80. package/bin/lib/repoPath.mjs +26 -0
  81. package/bin/lib/resources.mjs +7 -69
  82. package/bin/lib/resumeLost.mjs +107 -0
  83. package/bin/lib/runTurn.mjs +324 -0
  84. package/bin/lib/runtimeAntigravity.mjs +230 -0
  85. package/bin/lib/runtimeCapabilities.mjs +334 -0
  86. package/bin/lib/runtimeClaude.mjs +126 -0
  87. package/bin/lib/runtimeCodex.mjs +217 -0
  88. package/bin/lib/runtimeDetection.mjs +121 -0
  89. package/bin/lib/runtimeEvents.mjs +482 -0
  90. package/bin/lib/runtimeOfferings.mjs +4 -1
  91. package/bin/lib/runtimes.mjs +42 -1402
  92. package/bin/lib/safeFileName.mjs +74 -0
  93. package/bin/lib/standDownExit.mjs +181 -0
  94. package/bin/lib/standDownGate.mjs +48 -0
  95. package/bin/lib/startProject.mjs +279 -0
  96. package/bin/lib/trace.mjs +3 -3
  97. package/bin/lib/turnLock.mjs +68 -0
  98. package/bin/lib/turnProfile.mjs +77 -0
  99. package/bin/lib/uninstall.mjs +55 -45
  100. package/bin/lib/uninstallCopies.mjs +112 -0
  101. package/bin/lib/update.mjs +30 -17
  102. package/bin/lib/uplinkScrub.mjs +265 -0
  103. package/bin/lib/vault.mjs +22 -151
  104. package/bin/lib/vaultDiff.mjs +183 -0
  105. package/bin/lib/versionOrder.mjs +42 -0
  106. package/bin/lib/views.mjs +17 -17
  107. package/bin/lib/wikiRunner.mjs +630 -0
  108. package/bin/lib/work.mjs +188 -3034
  109. package/bin/lib/workAdoptCarry.mjs +94 -0
  110. package/bin/lib/workAgentCheck.mjs +278 -0
  111. package/bin/lib/workAgentMerges.mjs +54 -142
  112. package/bin/lib/workAgentPlans.mjs +14 -12
  113. package/bin/lib/workAgentPrecheck.mjs +393 -0
  114. package/bin/lib/workAgentPublish.mjs +177 -0
  115. package/bin/lib/workAgentReview.mjs +8 -666
  116. package/bin/lib/workAgentTurnBegun.mjs +109 -0
  117. package/bin/lib/workAgentTurnExecution.mjs +604 -0
  118. package/bin/lib/workAgentTurnOutcome.mjs +219 -0
  119. package/bin/lib/workAgentTurnReports.mjs +189 -0
  120. package/bin/lib/workAgentTurns.mjs +91 -898
  121. package/bin/lib/workAttachments.mjs +93 -0
  122. package/bin/lib/workBrain.mjs +53 -0
  123. package/bin/lib/workChildren.mjs +110 -0
  124. package/bin/lib/workCommandAudit.mjs +62 -0
  125. package/bin/lib/workDiffs.mjs +2 -1
  126. package/bin/lib/workNarration.mjs +152 -0
  127. package/bin/lib/workPlaceReports.mjs +426 -0
  128. package/bin/lib/workPlaces.mjs +228 -0
  129. package/bin/lib/workPreviews.mjs +31 -9
  130. package/bin/lib/workProcesses.mjs +4 -5
  131. package/bin/lib/workPullRequests.mjs +52 -126
  132. package/bin/lib/workReportQueue.mjs +254 -0
  133. package/bin/lib/workRetire.mjs +185 -0
  134. package/bin/lib/workSessionGroups.mjs +166 -0
  135. package/bin/lib/workSessionRuntime.mjs +164 -0
  136. package/bin/lib/workSessionTokens.mjs +68 -0
  137. package/bin/lib/workSessionTurns.mjs +892 -0
  138. package/bin/lib/workShip.mjs +9 -29
  139. package/bin/lib/workToolLog.mjs +76 -0
  140. package/package.json +3 -2
  141. package/bin/lib/patch.mjs +0 -363
@@ -0,0 +1,138 @@
1
+ /**
2
+ * WHAT EACH CLI TAKES OF THE REPO'S TOOLS, AND HOW A TURN IS PREPARED FOR IT
3
+ * (2026-09-26, SOLID F064).
4
+ *
5
+ * One row per runtime: whether it can take MCP servers at all, which MCP
6
+ * configurations it refuses (Codex takes no SSE), whether it takes the base
7
+ * branch's skills, and the runner-specific filesystem work a turn needs
8
+ * (Claude: a reviewed plugin carrying the base skills; Codex: an isolated
9
+ * CODEX_HOME). Split out of projectTools.mjs, where a capability table sat
10
+ * beside `runner === 'codex'` / `runner === 'claude'` branches in
11
+ * prepareAgentTools and a second copy of the SSE rule in toolReadout, so a
12
+ * new runtime or a transport change needed coordinated edits in three places.
13
+ * prepareAgentTools, toolReadout and runtimeOfferings.mjs (the Codex MCP
14
+ * report) all ask this table; the base-branch snapshot reader stays in
15
+ * projectTools.mjs. projectTools.test.mjs pins every transport word to here.
16
+ */
17
+
18
+ import { existsSync, mkdirSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from 'node:fs';
19
+ import { join, resolve } from 'node:path';
20
+ import { homedir } from 'node:os';
21
+
22
+ export const toml = (value) => JSON.stringify(String(value));
23
+
24
+ /** Keep personal Codex files, but do not copy its project trust grants. An
25
+ * untrusted worktree's .codex/config.toml is ignored by Codex 0.156.1. */
26
+ function isolatedCodexHome(dir, env, worktree) {
27
+ const source = resolve(env.CODEX_HOME || join(homedir(), '.codex'));
28
+ const dest = join(dir, 'codex-home');
29
+ mkdirSync(dest, { mode: 0o700 });
30
+ if (existsSync(source)) {
31
+ for (const name of readdirSync(source)) {
32
+ if (name === 'config.toml') continue;
33
+ symlinkSync(join(source, name), join(dest, name));
34
+ }
35
+ }
36
+ const configPath = join(source, 'config.toml');
37
+ const sourceConfig = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
38
+ const topLevel = sourceConfig.split(/^\s*\[/m, 1)[0];
39
+ const personalDeveloperInstructions = /^\s*developer_instructions\s*=/m.test(topLevel);
40
+ // These sections alone grant a checkout permission to supply project
41
+ // settings. Keep every other personal setting, skill and session store.
42
+ const lines = sourceConfig.split('\n');
43
+ let inProject = false;
44
+ let config = lines.filter((line) => {
45
+ const header = /^\s*(?:\[\[([^\]]+)\]\]|\[([^\]]+)\])\s*(?:#.*)?$/.exec(line);
46
+ if (header) {
47
+ const section = header[1] ?? header[2];
48
+ inProject = section === 'projects' || section.startsWith('projects.');
49
+ }
50
+ return !inProject;
51
+ }).join('\n');
52
+ if (worktree) {
53
+ // Codex discovers these even in an untrusted project. Disable exactly the
54
+ // worktree skill paths; the person's ~/.codex/skills and config remain.
55
+ const worktreeSkills = [];
56
+ for (const sourceDir of ['.agents/skills', '.codex/skills', '.claude/skills']) {
57
+ const skillsDir = join(worktree, sourceDir);
58
+ if (!existsSync(skillsDir)) continue;
59
+ for (const name of readdirSync(skillsDir)) {
60
+ const skill = join(skillsDir, name, 'SKILL.md');
61
+ if (existsSync(skill)) worktreeSkills.push(skill);
62
+ }
63
+ }
64
+ if (worktreeSkills.length && /^\s*skills\.config\s*=/m.test(config))
65
+ throw new Error('Cannot isolate Codex project skills with inline skills.config');
66
+ for (const skill of worktreeSkills) config += `\n[[skills.config]]\npath = ${toml(skill)}\nenabled = false\n`;
67
+ }
68
+ writeFileSync(join(dest, 'config.toml'), config, { mode: 0o600 });
69
+ return { path: dest, personalDeveloperInstructions };
70
+ }
71
+
72
+ /** Claude takes the base branch's skills as a plugin written for this turn. */
73
+ function reviewedClaudePlugin(dir, snapshot) {
74
+ if (!snapshot.skillFiles.length) return null;
75
+ const pluginDir = join(dir, 'reviewed-plugin');
76
+ mkdirSync(join(pluginDir, '.claude-plugin'), { recursive: true });
77
+ writeFileSync(join(pluginDir, '.claude-plugin/plugin.json'), JSON.stringify({ name: 'flowviant-reviewed', version: '1.0.0', description: 'Base-branch agent skills' }), { mode: 0o600 });
78
+ for (const { path, body } of snapshot.skillFiles) {
79
+ const dest = join(pluginDir, path.replace(/^\.claude\//, ''));
80
+ mkdirSync(join(dest, '..'), { recursive: true });
81
+ writeFileSync(dest, body, { mode: 0o600 });
82
+ }
83
+ return pluginDir;
84
+ }
85
+
86
+ /**
87
+ * The table. `refuseMcp(config)` names why this runtime cannot take one MCP
88
+ * configuration it otherwise could; `prepare(dir, snapshot, env, worktree)`
89
+ * returns the runner-specific part of prepareAgentTools' result.
90
+ */
91
+ const TOOL_RUNTIMES = {
92
+ claude: {
93
+ mcp: true,
94
+ skills: true,
95
+ refuseMcp: () => null,
96
+ prepare: (dir, snapshot) => ({ pluginDir: reviewedClaudePlugin(dir, snapshot) }),
97
+ },
98
+ codex: {
99
+ mcp: true,
100
+ skills: true,
101
+ refuseMcp: (config) => (config.type === 'sse' ? 'Codex cannot take SSE MCP' : null),
102
+ prepare: (dir, _snapshot, env, worktree) => {
103
+ const isolated = isolatedCodexHome(dir, env, worktree);
104
+ return { codexHome: isolated.path, personalDeveloperInstructions: isolated.personalDeveloperInstructions };
105
+ },
106
+ },
107
+ antigravity: { mcp: false, skills: false },
108
+ };
109
+
110
+ /** An unknown runner (or 'none', when no CLI is installed) takes nothing. */
111
+ const takesNothing = { mcp: false, skills: false };
112
+ const rowFor = (runner) => (Object.hasOwn(TOOL_RUNTIMES, runner) ? TOOL_RUNTIMES[runner] : takesNothing);
113
+
114
+ /** Pure mapping used before a future tool-attach gesture is offered. */
115
+ export function runnerToolCapabilities(runner) {
116
+ const { mcp, skills } = rowFor(runner);
117
+ return { mcp, skills };
118
+ }
119
+
120
+ /** Why `runner` cannot take this MCP configuration, or null when it can. The
121
+ * one answer both preparation (which drops it) and the readout (which says it)
122
+ * read. */
123
+ export function mcpRefusal(runner, config) {
124
+ const row = rowFor(runner);
125
+ if (!row.mcp) return runner === 'none' ? 'no installed CLI can take MCP' : `${runner} cannot take MCP`;
126
+ return row.refuseMcp?.(config) ?? null;
127
+ }
128
+
129
+ /** Why `runner` cannot take the base branch's skills, or null when it can. */
130
+ export function skillsRefusal(runner) {
131
+ if (rowFor(runner).skills) return null;
132
+ return runner === 'none' ? 'no installed CLI can take instructions' : `${runner} cannot take instructions`;
133
+ }
134
+
135
+ /** The runner-specific files a turn needs, written under `dir`. */
136
+ export function prepareForRunner(runner, dir, snapshot, env, worktree) {
137
+ return { pluginDir: null, codexHome: null, personalDeveloperInstructions: false, ...(rowFor(runner).prepare?.(dir, snapshot, env, worktree) ?? {}) };
138
+ }
@@ -1,15 +1,15 @@
1
1
  /** Repo-owned tool configuration, read from the base ref for every agent turn. */
2
2
  import { execFileSync } from 'node:child_process';
3
- import { accessSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
- import { delimiter, isAbsolute, join, resolve } from 'node:path';
5
- import { homedir, tmpdir } from 'node:os';
3
+ import { accessSync, constants, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { delimiter, isAbsolute, join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { mcpRefusal, prepareForRunner, skillsRefusal, toml } from './projectToolRuntimes.mjs';
6
7
 
7
8
  const MAX_TOOLS = 40;
8
9
  const MAX_REPORT_TOOLS = 80; // 40 MCP servers plus 40 instruction directories
9
10
  const MAX_INSTRUCTION_FILES = 80;
10
11
  const MAX_INSTRUCTION_BYTES = 512 * 1024;
11
12
  const nameOk = (name) => /^[A-Za-z0-9_-]{1,80}$/.test(name) && name !== 'flowviant';
12
- const toml = (value) => JSON.stringify(String(value));
13
13
  const show = (root, ref, path) => {
14
14
  try {
15
15
  return execFileSync('git', ['show', `${ref}:${path}`], {
@@ -39,12 +39,6 @@ function referenced(value) {
39
39
  return [...String(value).matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]);
40
40
  }
41
41
 
42
- /** Pure mapping used before a future tool-attach gesture is offered. */
43
- export function runnerToolCapabilities(runner) {
44
- return ({ claude: { mcp: true, skills: true }, codex: { mcp: true, skills: true }, antigravity: { mcp: false, skills: false } })[runner]
45
- ?? { mcp: false, skills: false };
46
- }
47
-
48
42
  export function readBaseTools(root, ref, env = process.env) {
49
43
  // Resolve once: a base branch may advance while the turn is being prepared.
50
44
  const commit = execFileSync('git', ['rev-parse', '--verify', ref], {
@@ -105,59 +99,10 @@ export function readBaseTools(root, ref, env = process.env) {
105
99
 
106
100
  const expand = (value, env) => String(value).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, key) => env[key] ?? '');
107
101
 
108
- /** Keep personal Codex files, but do not copy its project trust grants. An
109
- * untrusted worktree's .codex/config.toml is ignored by Codex 0.156.1. */
110
- function isolatedCodexHome(dir, env, worktree) {
111
- const source = resolve(env.CODEX_HOME || join(homedir(), '.codex'));
112
- const dest = join(dir, 'codex-home');
113
- mkdirSync(dest, { mode: 0o700 });
114
- if (existsSync(source)) {
115
- for (const name of readdirSync(source)) {
116
- if (name === 'config.toml') continue;
117
- symlinkSync(join(source, name), join(dest, name));
118
- }
119
- }
120
- const configPath = join(source, 'config.toml');
121
- const sourceConfig = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
122
- const topLevel = sourceConfig.split(/^\s*\[/m, 1)[0];
123
- const personalDeveloperInstructions = /^\s*developer_instructions\s*=/m.test(topLevel);
124
- // These sections alone grant a checkout permission to supply project
125
- // settings. Keep every other personal setting, skill and session store.
126
- const lines = sourceConfig.split('\n');
127
- let inProject = false;
128
- let config = lines.filter((line) => {
129
- const header = /^\s*(?:\[\[([^\]]+)\]\]|\[([^\]]+)\])\s*(?:#.*)?$/.exec(line);
130
- if (header) {
131
- const section = header[1] ?? header[2];
132
- inProject = section === 'projects' || section.startsWith('projects.');
133
- }
134
- return !inProject;
135
- }).join('\n');
136
- if (worktree) {
137
- // Codex discovers these even in an untrusted project. Disable exactly the
138
- // worktree skill paths; the person's ~/.codex/skills and config remain.
139
- const worktreeSkills = [];
140
- for (const sourceDir of ['.agents/skills', '.codex/skills', '.claude/skills']) {
141
- const skillsDir = join(worktree, sourceDir);
142
- if (!existsSync(skillsDir)) continue;
143
- for (const name of readdirSync(skillsDir)) {
144
- const skill = join(skillsDir, name, 'SKILL.md');
145
- if (existsSync(skill)) worktreeSkills.push(skill);
146
- }
147
- }
148
- if (worktreeSkills.length && /^\s*skills\.config\s*=/m.test(config))
149
- throw new Error('Cannot isolate Codex project skills with inline skills.config');
150
- for (const skill of worktreeSkills) config += `\n[[skills.config]]\npath = ${toml(skill)}\nenabled = false\n`;
151
- }
152
- writeFileSync(join(dest, 'config.toml'), config, { mode: 0o600 });
153
- return { path: dest, personalDeveloperInstructions };
154
- }
155
-
156
102
  /** Temporary per-turn files are removed after the last retry. No personal CLI config is written. */
157
103
  export function prepareAgentTools(snapshot, runner, env = process.env, worktree = null) {
158
104
  const dir = mkdtempSync(join(tmpdir(), 'flowviant-agent-tools-'));
159
- const caps = runnerToolCapabilities(runner);
160
- const usable = snapshot.tools.filter((t) => t.state === 'ready' && caps.mcp && !(runner === 'codex' && t.config.type === 'sse'));
105
+ const usable = snapshot.tools.filter((t) => t.state === 'ready' && mcpRefusal(runner, t.config) == null);
161
106
  const mcpServers = {};
162
107
  const codexArgs = [];
163
108
  for (const { name, config } of usable) {
@@ -185,7 +130,7 @@ export function prepareAgentTools(snapshot, runner, env = process.env, worktree
185
130
  const mcpPath = join(dir, 'mcp.json');
186
131
  writeFileSync(mcpPath, JSON.stringify({ mcpServers }), { mode: 0o600 });
187
132
  const pointers = [];
188
- for (const { path, body } of [...snapshot.instructions, ...(caps.skills ? snapshot.skillFiles : [])]) {
133
+ for (const { path, body } of [...snapshot.instructions, ...(skillsRefusal(runner) == null ? snapshot.skillFiles : [])]) {
189
134
  const dest = join(dir, path);
190
135
  mkdirSync(join(dest, '..'), { recursive: true });
191
136
  writeFileSync(dest, body, { mode: 0o600 });
@@ -195,28 +140,14 @@ export function prepareAgentTools(snapshot, runner, env = process.env, worktree
195
140
  const instructions = [baseInstructions,
196
141
  pointers.length ? `Base-branch instruction and skill copies (read the relevant nested files and skill files before acting):\n${pointers.join('\n')}` : '',
197
142
  ].filter(Boolean).join('\n\n');
198
- const pluginDir = runner === 'claude' && snapshot.skillFiles.length ? join(dir, 'reviewed-plugin') : null;
199
- if (pluginDir) {
200
- mkdirSync(join(pluginDir, '.claude-plugin'), { recursive: true });
201
- writeFileSync(join(pluginDir, '.claude-plugin/plugin.json'), JSON.stringify({ name: 'flowviant-reviewed', version: '1.0.0', description: 'Base-branch agent skills' }), { mode: 0o600 });
202
- for (const { path, body } of snapshot.skillFiles) {
203
- const dest = join(pluginDir, path.replace(/^\.claude\//, ''));
204
- mkdirSync(join(dest, '..'), { recursive: true });
205
- writeFileSync(dest, body, { mode: 0o600 });
206
- }
207
- }
208
- let codexHome = null;
209
- let personalDeveloperInstructions = false;
143
+ let prepared;
210
144
  try {
211
- if (runner === 'codex') {
212
- const isolated = isolatedCodexHome(dir, env, worktree);
213
- codexHome = isolated.path;
214
- personalDeveloperInstructions = isolated.personalDeveloperInstructions;
215
- }
145
+ prepared = prepareForRunner(runner, dir, snapshot, env, worktree);
216
146
  } catch (error) {
217
147
  rmSync(dir, { recursive: true, force: true });
218
148
  throw error;
219
149
  }
150
+ const { pluginDir, codexHome, personalDeveloperInstructions } = prepared;
220
151
  return {
221
152
  mcpPath,
222
153
  codexArgs,
@@ -229,20 +160,14 @@ export function prepareAgentTools(snapshot, runner, env = process.env, worktree
229
160
  }
230
161
 
231
162
  export function toolReadout(snapshot, runner) {
232
- const caps = runnerToolCapabilities(runner);
233
163
  const short = (reason) => reason == null ? null : String(reason).slice(0, 200);
234
164
  const mcp = snapshot.tools.map(({ name, config, state, reason }) => {
235
- const unsupported = runner === 'codex' && config.type === 'sse';
236
- return {
237
- name, kind: 'mcp',
238
- state: caps.mcp && !unsupported ? state : 'missing',
239
- reason: short(!caps.mcp ? runner === 'none' ? 'no installed CLI can take MCP' : `${runner} cannot take MCP`
240
- : unsupported ? 'Codex cannot take SSE MCP' : reason),
241
- };
165
+ const refused = mcpRefusal(runner, config);
166
+ return { name, kind: 'mcp', state: refused ? 'missing' : state, reason: short(refused ?? reason) };
242
167
  });
168
+ const skillsRefused = skillsRefusal(runner);
243
169
  const instructions = snapshot.skills.map(({ path }) => ({
244
- name: path.split('/')[2], kind: 'instruction', state: caps.skills ? 'ready' : 'missing',
245
- reason: short(caps.skills ? null : runner === 'none' ? 'no installed CLI can take instructions' : `${runner} cannot take instructions`),
170
+ name: path.split('/')[2], kind: 'instruction', state: skillsRefused ? 'missing' : 'ready', reason: short(skillsRefused),
246
171
  }));
247
172
  return [...mcp, ...instructions].slice(0, MAX_REPORT_TOOLS);
248
173
  }