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.
Files changed (161) hide show
  1. package/README.md +66 -12
  2. package/bin/cli.mjs +50 -17
  3. package/bin/lib/admission.mjs +7 -14
  4. package/bin/lib/agentAsk.mjs +123 -0
  5. package/bin/lib/agentBrief.mjs +36 -0
  6. package/bin/lib/agentFootprint.mjs +76 -0
  7. package/bin/lib/agentInternalWait.mjs +83 -0
  8. package/bin/lib/agentPlan.mjs +134 -31
  9. package/bin/lib/agentPrerequisiteContract.mjs +43 -0
  10. package/bin/lib/agentPublish.mjs +12 -13
  11. package/bin/lib/agentTaskKinds.mjs +5 -8
  12. package/bin/lib/agySessions.mjs +3 -3
  13. package/bin/lib/artifactContracts.mjs +91 -20
  14. package/bin/lib/artifactIteration.mjs +182 -0
  15. package/bin/lib/artifactPreview.mjs +92 -18
  16. package/bin/lib/artifactRecovery.mjs +36 -0
  17. package/bin/lib/artifactReporter.mjs +194 -22
  18. package/bin/lib/artifacts.mjs +70 -63
  19. package/bin/lib/authproxy.mjs +2 -1
  20. package/bin/lib/childEnv.mjs +9 -4
  21. package/bin/lib/claude.mjs +10 -20
  22. package/bin/lib/claudeAuth.mjs +15 -5
  23. package/bin/lib/claudePersonal.mjs +71 -6
  24. package/bin/lib/claudePosture.mjs +227 -54
  25. package/bin/lib/claudeSessions.mjs +7 -7
  26. package/bin/lib/claudeStream.mjs +27 -13
  27. package/bin/lib/cliLimit.mjs +2 -2
  28. package/bin/lib/codexHome.mjs +17 -0
  29. package/bin/lib/codexPersonal.mjs +44 -3
  30. package/bin/lib/codexProjectRead.mjs +63 -0
  31. package/bin/lib/codexRelay.mjs +21 -5
  32. package/bin/lib/codexResearch.mjs +193 -0
  33. package/bin/lib/commandSpecs.mjs +6 -0
  34. package/bin/lib/config.mjs +37 -24
  35. package/bin/lib/conversationIds.mjs +51 -0
  36. package/bin/lib/credentials.mjs +2 -7
  37. package/bin/lib/daemonLocks.mjs +90 -0
  38. package/bin/lib/daemonState.mjs +5 -5
  39. package/bin/lib/deploy.mjs +6 -9
  40. package/bin/lib/deployCheckout.mjs +26 -22
  41. package/bin/lib/deployRunner.mjs +54 -18
  42. package/bin/lib/deployWire.mjs +32 -23
  43. package/bin/lib/desktopContract.mjs +6 -5
  44. package/bin/lib/discoveryContract.mjs +19 -0
  45. package/bin/lib/env.mjs +104 -35
  46. package/bin/lib/fleet.mjs +53 -13
  47. package/bin/lib/fleetPost.mjs +172 -25
  48. package/bin/lib/fleetReports.mjs +172 -171
  49. package/bin/lib/fleetStartup.mjs +6 -6
  50. package/bin/lib/fleetWire.mjs +42 -0
  51. package/bin/lib/git.mjs +111 -61
  52. package/bin/lib/gitMergeIdentity.mjs +2 -8
  53. package/bin/lib/grant.mjs +23 -7
  54. package/bin/lib/holder.mjs +108 -81
  55. package/bin/lib/hooks/mcpAllow.mjs +60 -0
  56. package/bin/lib/hooks/readGuard.mjs +54 -11
  57. package/bin/lib/imageArtifact.mjs +74 -0
  58. package/bin/lib/install.mjs +80 -33
  59. package/bin/lib/instance.mjs +36 -83
  60. package/bin/lib/intakeDraft.mjs +4 -1
  61. package/bin/lib/knowledgeFileSync.mjs +31 -0
  62. package/bin/lib/knowledgeFiles.mjs +28 -83
  63. package/bin/lib/landed.mjs +22 -29
  64. package/bin/lib/launchCommand.mjs +91 -0
  65. package/bin/lib/listeners.mjs +0 -5
  66. package/bin/lib/localSessions.mjs +52 -39
  67. package/bin/lib/login.mjs +22 -12
  68. package/bin/lib/loginPath.mjs +22 -17
  69. package/bin/lib/machineListing.mjs +6 -3
  70. package/bin/lib/maintenance.mjs +47 -0
  71. package/bin/lib/maintenanceInventory.mjs +39 -0
  72. package/bin/lib/mcp-cli.mjs +9 -10
  73. package/bin/lib/mcpInstall.mjs +163 -0
  74. package/bin/lib/mcpInventory.mjs +119 -0
  75. package/bin/lib/noWindowEnv.mjs +23 -3
  76. package/bin/lib/ownedDirs.mjs +99 -0
  77. package/bin/lib/pinnedCapture.mjs +383 -0
  78. package/bin/lib/planDependencies.mjs +70 -0
  79. package/bin/lib/postureTools.mjs +85 -0
  80. package/bin/lib/prWorkflow.mjs +94 -17
  81. package/bin/lib/preflight.mjs +8 -25
  82. package/bin/lib/preview.mjs +19 -25
  83. package/bin/lib/previewGatePolicy.mjs +34 -9
  84. package/bin/lib/previewRegistry.mjs +4 -11
  85. package/bin/lib/printable.mjs +70 -7
  86. package/bin/lib/procRegistry.mjs +33 -13
  87. package/bin/lib/procScan.mjs +2 -3
  88. package/bin/lib/procStat.mjs +76 -0
  89. package/bin/lib/processGroup.mjs +48 -0
  90. package/bin/lib/processes.mjs +10 -24
  91. package/bin/lib/projectToolRuntimes.mjs +328 -19
  92. package/bin/lib/projectTools.mjs +109 -34
  93. package/bin/lib/prompts.mjs +205 -60
  94. package/bin/lib/repoPath.mjs +19 -0
  95. package/bin/lib/repoState.mjs +17 -19
  96. package/bin/lib/repositoryBaseline.mjs +68 -0
  97. package/bin/lib/runTurn.mjs +286 -209
  98. package/bin/lib/runtimeAntigravity.mjs +16 -5
  99. package/bin/lib/runtimeCapabilities.mjs +215 -21
  100. package/bin/lib/runtimeClaude.mjs +78 -10
  101. package/bin/lib/runtimeCodex.mjs +217 -209
  102. package/bin/lib/runtimeDetection.mjs +8 -2
  103. package/bin/lib/runtimeEvents.mjs +28 -11
  104. package/bin/lib/runtimeLimits.mjs +5 -6
  105. package/bin/lib/runtimeOfferings.mjs +99 -14
  106. package/bin/lib/runtimes.mjs +10 -0
  107. package/bin/lib/shipSweep.mjs +5 -3
  108. package/bin/lib/shot.mjs +91 -17
  109. package/bin/lib/sleep.mjs +11 -0
  110. package/bin/lib/startProject.mjs +5 -5
  111. package/bin/lib/statePaths.mjs +40 -0
  112. package/bin/lib/tty.mjs +25 -15
  113. package/bin/lib/turnDiagnostics.mjs +93 -0
  114. package/bin/lib/turnLock.mjs +7 -14
  115. package/bin/lib/turnProfile.mjs +16 -4
  116. package/bin/lib/ui.mjs +0 -3
  117. package/bin/lib/uninstall.mjs +30 -26
  118. package/bin/lib/update.mjs +79 -27
  119. package/bin/lib/uplinkScrub.mjs +296 -31
  120. package/bin/lib/vault.mjs +78 -10
  121. package/bin/lib/vaultDiff.mjs +5 -2
  122. package/bin/lib/views.mjs +37 -12
  123. package/bin/lib/wikiRunner.mjs +40 -61
  124. package/bin/lib/work.mjs +44 -13
  125. package/bin/lib/workAdoptCarry.mjs +2 -4
  126. package/bin/lib/workAgentBrief.mjs +92 -0
  127. package/bin/lib/workAgentBriefRunner.mjs +59 -0
  128. package/bin/lib/workAgentCheck.mjs +104 -57
  129. package/bin/lib/workAgentMerges.mjs +62 -57
  130. package/bin/lib/workAgentPlans.mjs +82 -199
  131. package/bin/lib/workAgentPrecheck.mjs +76 -138
  132. package/bin/lib/workAgentPublish.mjs +6 -8
  133. package/bin/lib/workAgentTurnBegun.mjs +15 -11
  134. package/bin/lib/workAgentTurnExecution.mjs +131 -67
  135. package/bin/lib/workAgentTurnOutcome.mjs +11 -2
  136. package/bin/lib/workAgentTurnReports.mjs +9 -37
  137. package/bin/lib/workAgentTurns.mjs +64 -7
  138. package/bin/lib/workArtifactReconcile.mjs +79 -0
  139. package/bin/lib/workAttachments.mjs +66 -11
  140. package/bin/lib/workBrain.mjs +9 -4
  141. package/bin/lib/workCappedTurn.mjs +119 -0
  142. package/bin/lib/workChildren.mjs +5 -5
  143. package/bin/lib/workCommandAudit.mjs +18 -2
  144. package/bin/lib/workDiffs.mjs +52 -6
  145. package/bin/lib/workIntake.mjs +35 -60
  146. package/bin/lib/workPlaceReports.mjs +7 -4
  147. package/bin/lib/workPlaces.mjs +113 -31
  148. package/bin/lib/workPreviews.mjs +47 -112
  149. package/bin/lib/workProcesses.mjs +8 -35
  150. package/bin/lib/workPullRequests.mjs +37 -56
  151. package/bin/lib/workReportQueue.mjs +100 -148
  152. package/bin/lib/workRetire.mjs +2 -1
  153. package/bin/lib/workSessionRuntime.mjs +38 -37
  154. package/bin/lib/workSessionTurns.mjs +256 -199
  155. package/bin/lib/workShip.mjs +43 -23
  156. package/bin/lib/workToolLog.mjs +70 -3
  157. package/bin/lib/workTurnShape.mjs +165 -0
  158. package/npm-shrinkwrap.json +69 -0
  159. package/package.json +9 -4
  160. package/bin/lib/fleetJobs.mjs +0 -120
  161. package/bin/lib/patchRevert.mjs +0 -60
@@ -14,6 +14,8 @@ import { SAFE } from './config.mjs';
14
14
  import { parseCodexLine } from './runtimeEvents.mjs';
15
15
  import { ARTIFACT_DIR, FENCE_TMP_DIR } from './artifacts.mjs';
16
16
  import { codexPersonalTurn } from './codexPersonal.mjs';
17
+ import { researchFence } from './codexResearch.mjs';
18
+ import { projectReadBoundary, projectReadFence } from './codexProjectRead.mjs';
17
19
 
18
20
  /**
19
21
  * FLOWVIANT'S OWN TOOLS NEED NO APPROVAL FROM A CHAT NOBODY CAN ANSWER
@@ -51,16 +53,6 @@ function codexMcp(token, mcpUrl) {
51
53
  };
52
54
  }
53
55
 
54
- /**
55
- * THE POSTURES THAT READ NONE OF THE PERSON'S CONFIG — the branches of `args()`
56
- * below that pass `--ignore-user-config`, said once so the adapter and its
57
- * `personal` hook cannot disagree about which turns carry the person's login
58
- * back (codexPersonal.mjs). A wiki turn with no vault is a build turn there,
59
- * and so here.
60
- */
61
- export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
62
- profile === 'consult' || profile === 'plan' || profile === 'image' || profile === 'design' || (profile === 'wiki' && Boolean(vaultDir));
63
-
64
56
  /**
65
57
  * WHAT A FENCED CODEX TURN MAY NOT REACH FOR (0.114.0). codex-cli 0.156.1
66
58
  * ships `browser_use` (and its external and full-CDP forms), an in-app
@@ -69,12 +61,101 @@ export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
69
61
  * a browser or the desktop is a way around a filesystem fence. A build turn
70
62
  * keeps the person's own config.
71
63
  */
72
- const NO_REACH_OUT = [
64
+ export const NO_REACH_OUT = Object.freeze([
73
65
  '-c', 'features.browser_use=false',
74
66
  '-c', 'features.browser_use_external=false',
75
67
  '-c', 'features.in_app_browser=false',
76
68
  '-c', 'features.computer_use=false',
77
69
  '-c', 'features.apps=false',
70
+ ]);
71
+
72
+ /**
73
+ * THE HOLE NO SANDBOX COVERS. `web_search` ships in `codex exec`'s default
74
+ * tool list even without --search, and it executes SERVER-SIDE at OpenAI — no
75
+ * local sandbox touches it. An injected turn could pack what it read into a
76
+ * query and egress it straight past every filesystem and network fence. Both
77
+ * spellings, because the two config systems disagree about which one is live.
78
+ */
79
+ export const NO_WEB = Object.freeze(['-c', 'tools.web_search=false', '-c', 'web_search="disabled"']);
80
+
81
+ /** Sub-agents would be a second turn whose posture nobody here chose. */
82
+ export const NO_SUBAGENTS = Object.freeze(['-c', 'features.multi_agent=false', '-c', 'features.goals=false']);
83
+
84
+ /**
85
+ * HERMETIC. Without these a user's ~/.codex/config.toml, a project `.rules`
86
+ * execpolicy file, or their own MCP servers can widen a posture we are
87
+ * asserting on their behalf — silently, and on the turns whose prompt comes
88
+ * from someone else's typing. Last in every fence: the person's login rides
89
+ * back right after `--ignore-user-config` (see `args()`).
90
+ */
91
+ export const HERMETIC = Object.freeze(['--ignore-user-config', '--ignore-rules']);
92
+
93
+ /**
94
+ * Every fenced posture keeps config isolation, desktop/app closure and the
95
+ * delegation policy. Artifact postures explicitly enable native images as a
96
+ * supporting capability; consult, plan and wiki keep them off. Native web
97
+ * needs the isolated research read fence because broad-read tools can read
98
+ * host credentials. Feature flags do not prove account/model entitlement.
99
+ */
100
+ export const fencedTail = ({ imageGeneration = false, web = false } = {}) => [
101
+ // Native web is paired with the copied, root-denied read fence.
102
+ ...(web ? [] : NO_WEB),
103
+ ...NO_SUBAGENTS,
104
+ ...NO_REACH_OUT,
105
+ imageGeneration ? '--enable' : '--disable',
106
+ 'image_generation',
107
+ ...HERMETIC,
108
+ ];
109
+
110
+ /**
111
+ * THE FENCED POSTURES, AS A PREDICATE (2026-10-10, the audit) — which turns
112
+ * run under a fence of Flowviant's (and so read none of the person's config),
113
+ * answered WITHOUT building argv: the `personal` hook asks with only the
114
+ * profile and the vault, and the image and design builders need a worktree.
115
+ * `args()` picks its fence builder by the same keys (CODEX_FENCES below), so
116
+ * the hook that carries the person's login back and the argv that ignores
117
+ * their file cannot disagree. A wiki turn with no vault has no fence to build
118
+ * and is a build turn, here and in `args()`.
119
+ */
120
+ export const CODEX_FENCED = Object.freeze({
121
+ consult: () => true,
122
+ plan: () => true,
123
+ wiki: ({ vaultDir } = {}) => Boolean(vaultDir),
124
+ image: () => true,
125
+ design: () => true,
126
+ research: () => true,
127
+ });
128
+
129
+ export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
130
+ Object.hasOwn(CODEX_FENCED, profile) && CODEX_FENCED[profile]({ vaultDir });
131
+
132
+ /** Consult/capture use root-denied tool reads over the actual project,
133
+ * knowledge and Git metadata. Scrubbing remains supplemental. */
134
+ /**
135
+ * THE CARTOGRAPHER: read the repo, write ONLY the vault. Codex's permission
136
+ * profiles take a per-path filesystem map, so the rule is enforced by the
137
+ * kernel. Measured: repo readable, vault writable, repo writes fail
138
+ * "Read-only file system", and curl returns 000 — the network is off. The
139
+ * vault is an uplink (every page is synced to the project's wiki), so web
140
+ * search and sub-agents are shut here as on every other fence (2026-10-10:
141
+ * this copy had drifted without them, and no ruling ever granted them). The
142
+ * write covers the vault's own `.git` too, which is why the daemon's git
143
+ * there runs nothing the vault's config names (vault.mjs `vaultGit`).
144
+ *
145
+ * The table is set WHOLE rather than as a dotted key, and that is not style:
146
+ * `-c permissions.wiki.filesystem."<path>"="write"` splits the dotted path on
147
+ * the dots INSIDE the path, and every real vault lives under
148
+ * `~/.flowviant/vaults/…`. It fails with "filesystem path must be absolute",
149
+ * which reads like a path problem and is a parsing one. The path is a TOML
150
+ * basic string (JSON's escapes), as the artifact fences' are. Selected by
151
+ * config: `codex exec` (0.156.1) has no `-P`, so every Codex wiki turn was
152
+ * refused before it started (0.114.0).
153
+ */
154
+ const codexWikiFence = (vaultDir) => [
155
+ '-c', 'permissions.flowviantwiki.extends=":read-only"',
156
+ '-c', `permissions.flowviantwiki.filesystem={${JSON.stringify(vaultDir)}="write"}`,
157
+ '-c', 'default_permissions="flowviantwiki"',
158
+ ...fencedTail(),
78
159
  ];
79
160
 
80
161
  export const CODEX_RUNTIME = {
@@ -91,12 +172,12 @@ export const CODEX_RUNTIME = {
91
172
  * a verb allowlist, and for a consult's actual threat (exfiltration driven
92
173
  * by an injected question) that is sufficient and arguably stronger.
93
174
  *
94
- * ALL THREE, and `wiki` is the one worth pausing on: Codex expresses it
95
- * MORE strictly than Claude does. A cartographer must read the repo and
96
- * write only the vault; Claude's WIKI_PERM cannot path-scope Write and says
97
- * so, leaning on the worktree reset as a backstop. Codex's permission
98
- * profiles enforce the same rule in the kernel — measured: repo readable,
99
- * vault writable, repo writes refused, network off.
175
+ * ALL THREE, and `wiki` is the one worth pausing on: a cartographer must
176
+ * read the repo and write only the vault. Claude's list says so in rules
177
+ * (`wikiPermFor`, probed 2026-10-10); Codex's permission profiles enforce
178
+ * the write half in the kernel — measured: repo readable, vault writable,
179
+ * repo writes refused, network off — while reading the whole box, as every
180
+ * Codex fence does (see `codexArtifactFence`).
100
181
  *
101
182
  * Enforcement was verified on Linux (bubblewrap + seccomp). macOS Seatbelt
102
183
  * and Windows are UNTESTED; if this daemon starts running there, re-verify
@@ -107,14 +188,23 @@ export const CODEX_RUNTIME = {
107
188
  * Claude's, because the pictures come from Codex's own image tool.
108
189
  *
109
190
  * AND `design` (0.115.0), shared with Claude: the mockup, 3D-model and deck
110
- * cards' fence, expressed as `codexDesignFence` below. Research is NOT
111
- * declared: its web and Codex's whole-box reads cannot be fenced apart
112
- * (see that function).
191
+ * cards' fence, expressed as `codexDesignFence` below.
192
+ * AND `research` (0.117.0): live web over a copied view under a root-denied
193
+ * Linux tool fence (`codexResearch.mjs`), outside the login and checkout.
113
194
  */
114
- profiles: ['build', 'consult', 'wiki', 'plan', 'image', 'design'],
195
+ profiles: ['build', 'consult', 'wiki', 'plan', 'image', 'design', 'research'],
115
196
  /** `model_reasoning_effort` tops out at `xhigh` — there is no `max`, and a
116
197
  * queued or skewed job naming it would be an argument Codex refuses. */
117
198
  efforts: ['low', 'medium', 'high', 'xhigh'],
199
+ /**
200
+ * A TERMINAL TAB ON CODEX (audit 2026-10-10, F159; see runtimeClaude.mjs
201
+ * for the three facts): an MCP tab like Claude's, with no plan mode (there
202
+ * is no flag to spell it) and no adoption — Codex has no adoptable
203
+ * terminal store yet, and `args()` backstops an adopt id with a throw.
204
+ */
205
+ tab: 'mcp',
206
+ planTurns: false,
207
+ adopt: null,
118
208
  mcp: codexMcp,
119
209
  /**
120
210
  * Codex has NO system-prompt flag. The contract therefore rides inside the
@@ -131,9 +221,11 @@ export const CODEX_RUNTIME = {
131
221
  * the prompt always carries them too. Workbench tabs keep their normal
132
222
  * checkout discovery.
133
223
  *
134
- * `--skip-git-repo-check` is deliberately NOT passed: a task always builds
135
- * in a git worktree, and if it somehow is not one, failing loudly beats
136
- * silently editing files nobody can diff.
224
+ * `--skip-git-repo-check` is deliberately NOT passed by the build or any
225
+ * fence but research's: a task always builds in a git worktree, and if it
226
+ * somehow is not one, failing loudly beats silently editing files nobody
227
+ * can diff. A write-up reads a COPY with no `.git` and never resumes
228
+ * (codexResearch.mjs), so its fence passes it.
137
229
  *
138
230
  * THE PROMPT IS LAST, and that is load-bearing rather than tidy. Codex takes
139
231
  * it as a trailing POSITIONAL, so every flag — including the two `-c` MCP
@@ -141,7 +233,7 @@ export const CODEX_RUNTIME = {
141
233
  * placed before it. Appending them after the positional is the kind of argv
142
234
  * that parses today and stops parsing on some future clap upgrade.
143
235
  */
144
- args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null }) {
236
+ args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation, knowledgeDir, projectReads }) {
145
237
  // Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
146
238
  // agy moves) — codex has no adoptable store wired yet. Reaching here
147
239
  // with an adopt id is a wiring mistake upstream, and it fails loudly on
@@ -169,166 +261,11 @@ export const CODEX_RUNTIME = {
169
261
  // Effort is a config value on Codex rather than a flag.
170
262
  if (effort) a.push('-c', `model_reasoning_effort="${effort}"`);
171
263
 
172
- if (profile === 'consult') {
173
- // A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — and it is a
174
- // different shape from Claude's, which is the whole reason `profile` is
175
- // a promise rather than a flag list.
176
- //
177
- // Claude gets a VERB allowlist: Read, Grep, Glob and a handful of
178
- // read-only Bash forms, with nothing that reaches the network. Codex has
179
- // no such thing — it has no file-read tool at all, so reading the repo
180
- // IS command execution (`cat`, `rg`). Removing the shell leaves the
181
- // model with exactly ["update_plan","request_user_input"], which cannot
182
- // answer a question about a codebase. You get both capabilities or
183
- // neither.
184
- //
185
- // So the promise is kept one layer down instead. `read-only` is
186
- // kernel-enforced (bubblewrap + seccomp on Linux): writes fail, and a
187
- // direct-IP connect fails with "Operation not permitted" — socket() is
188
- // denied, not merely DNS. An injected command still RUNS and still
189
- // cannot take the repository anywhere, which is the threat a consult
190
- // actually has: its prompt is steered by a question any project editor
191
- // can type. Arguably a stronger guarantee than the allowlist, being
192
- // below the agent rather than inside it.
193
- a.push('-c', 'sandbox_mode="read-only"');
194
-
195
- // THE HOLE THE SANDBOX DOES NOT COVER. `web_search` ships in `codex
196
- // exec`'s default tool list even without --search, and it executes
197
- // SERVER-SIDE at OpenAI — no local sandbox touches it. An injected turn
198
- // could pack repo contents into a query and egress them straight past
199
- // everything above. Both spellings, because the two config systems
200
- // disagree about which one is live.
201
- a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
202
-
203
- // Sub-agents would be a second turn whose posture nobody here chose.
204
- a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
205
- // Image generation ships ON in codex-cli 0.156.1 even under
206
- // --ignore-user-config (measured 2026-09-29): a turn that asked for no
207
- // picture is handed no tool that spends image credits.
208
- a.push('--disable', 'image_generation');
209
-
210
- // HERMETIC. Without these a user's ~/.codex/config.toml, a project
211
- // `.rules` execpolicy file, or their own MCP servers can widen a posture
212
- // we are asserting on their behalf — silently, and on the one turn whose
213
- // prompt comes from someone else's typing.
214
- a.push('--ignore-user-config', '--ignore-rules');
215
- } else if (profile === 'plan') {
216
- // A PLANNING SESSION. Read-only on the filesystem, exactly like a
217
- // consult — the writes it makes go through the control plane, not
218
- // through this box — so the kernel sandbox is the same one, and for the
219
- // same reason: this turn's prompt is steered by anything a project
220
- // editor can type.
221
- //
222
- // Everything the consult branch above closes stays closed, and the
223
- // reasoning is unchanged, so it is not restated: web_search egresses
224
- // server-side at OpenAI where no local sandbox reaches it, sub-agents
225
- // would be a turn whose posture nobody here chose, and a user's own
226
- // config or MCP servers must not widen a posture we are asserting on
227
- // their behalf.
228
- //
229
- // What differs from a consult is the ONE thing this profile exists for:
230
- // an MCP config IS passed, carrying this turn's scoped token. The plan
231
- // token has the planning tools; a capture token has only staging and
232
- // transcript tools. The server refuses everything outside that scope.
233
- a.push('-c', 'sandbox_mode="read-only"');
234
- a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
235
- a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
236
- // Image generation ships ON in codex-cli 0.156.1 even under
237
- // --ignore-user-config (measured 2026-09-29): a turn that asked for no
238
- // picture is handed no tool that spends image credits.
239
- a.push('--disable', 'image_generation');
240
- a.push('--ignore-user-config', '--ignore-rules');
241
- } else if (profile === 'wiki' && vaultDir) {
242
- // THE CARTOGRAPHER, AND THIS ONE IS STRICTER THAN CLAUDE'S.
243
- //
244
- // A wiki turn reads the whole repo and writes ONLY the vault. Claude
245
- // cannot actually express that: WIKI_PERM hands it Write/Edit and its own
246
- // comment admits "Write/Edit can't be path-scoped here; the worktree
247
- // reset is the backstop" — i.e. the cartographer CAN scribble on the
248
- // checkout and we clean up afterwards.
249
- //
250
- // Codex's permission profiles take a per-path filesystem map, so the
251
- // rule is enforced by the kernel instead of apologised for. Measured:
252
- // repo readable, vault writable, repo writes fail "Read-only file
253
- // system", and curl returns 000 — the network is off, which is the half
254
- // WIKI_PERM was really protecting (its comment: "Command execution is
255
- // the line: it enables network exfil").
256
- //
257
- // The table is set WHOLE rather than as a dotted key, and that is not
258
- // style: `-c permissions.wiki.filesystem."<path>"="write"` splits the
259
- // dotted path on the dots INSIDE the path, and every real vault lives
260
- // under `~/.flowviant/vaults/…`. It fails with "filesystem path must be
261
- // absolute", which reads like a path problem and is a parsing one.
262
- a.push('-c', 'permissions.flowviantwiki.extends=":read-only"');
263
- a.push('-c', `permissions.flowviantwiki.filesystem={"${vaultDir}"="write"}`);
264
- // Selected by config: `codex exec` (0.156.1) has no `-P`, so every Codex
265
- // wiki turn was refused before it started (0.114.0).
266
- a.push('-c', 'default_permissions="flowviantwiki"');
267
- a.push('--disable', 'image_generation');
268
- a.push(...NO_REACH_OUT);
269
- a.push('--ignore-user-config', '--ignore-rules');
270
- } else if (profile === 'image') {
271
- // AN IMAGE CARD (0.114.0): read the repo, write ONLY the worktree's
272
- // `.flowviant/artifacts/`, generate pictures, and nothing else. The
273
- // cartographer's shape — a permission profile whose filesystem map the
274
- // kernel enforces — pointed at the artifacts directory instead of a
275
- // vault. MEASURED on codex-cli 0.156.1 (Linux, bubblewrap), with no model
276
- // turn spent: under this profile a `cp` from a CODEX_HOME-shaped folder
277
- // into the artifacts directory landed, a write to a repo file failed
278
- // "Read-only file system", and curl got no network; `codex debug
279
- // prompt-input` with these overrides renders a managed profile of
280
- // `write <worktree>/.flowviant/artifacts` + `read :root`, "Approval
281
- // policy is currently never".
282
- //
283
- // SELECTED BY `default_permissions`, NOT `-P`: `codex exec` (and `exec
284
- // resume`) on 0.156.1 refuse `-P` as an unexpected argument — only
285
- // `codex sandbox` takes it — while `-c default_permissions="<name>"`
286
- // resolves the named profile in both. The directory must EXIST before
287
- // spawn (measured: a missing one cannot be created from inside the
288
- // fence), which the agent lane sees to (`ensureArtifactDir`).
289
- //
290
- // WHERE THE PICTURES COME FROM. Codex's own `image_gen` tool saves each
291
- // one under `$CODEX_HOME/generated_images/…` and says the path (the
292
- // binary's own words: "Generated images are saved to … by default. If
293
- // you need to use a generated image at another path, copy it"). An agent
294
- // turn's CODEX_HOME is the agent's own isolated home (projectToolRuntimes,
295
- // kept per agent since 2026-09-29); `:root` reads admit it, so the agent
296
- // copies what it keeps into the one writable directory, where the
297
- // artifact scan finds it.
298
- //
299
- // IMAGE GENERATION IS SWITCHED ON HERE AND ONLY HERE — a decision. This
300
- // is the one turn whose card asks for pictures, so it must not depend
301
- // on whatever the person's config says about the feature. A BUILD turn
302
- // keeps the person's own config (not hermetic, by design — see below),
303
- // and the other fenced postures (consult, plan, wiki) never asked for a
304
- // picture, so none of them is handed a tool that spends image credits.
305
- if (!cwd || !isAbsolute(cwd)) {
306
- throw new Error("an image turn needs its worktree to fence — none reached codex's builder");
307
- }
308
- const artifacts = join(cwd, ARTIFACT_DIR);
309
- a.push('-c', 'permissions.flowviantimage.extends=":read-only"');
310
- // The table WHOLE (the wiki branch's reason: a dotted key splits on the
311
- // dots inside the path), the path as a TOML basic string.
312
- // …and the fence's own scratch (2026-09-29, the design fence's
313
- // measurement: a `cat > x <<EOF` needs a temp file, and /tmp is
314
- // read-only in here), named for the shells through TMPDIR/TMPPREFIX.
315
- const scratch = join(cwd, FENCE_TMP_DIR);
316
- a.push('-c', `permissions.flowviantimage.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`);
317
- a.push('-c', `shell_environment_policy.set={TMPDIR=${JSON.stringify(scratch)},TMPPREFIX=${JSON.stringify(join(scratch, 'zsh'))}}`);
318
- a.push('-c', 'default_permissions="flowviantimage"');
319
- a.push(...NO_REACH_OUT);
320
- // Nobody is on this end of the pipe to approve an escalation.
321
- a.push('-c', 'approval_policy="never"');
322
- a.push('--enable', 'image_generation');
323
- // No web: the consult branch's two spellings, for its reason — web
324
- // search runs server-side where no sandbox reaches it.
325
- a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
326
- a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
327
- a.push('--ignore-user-config', '--ignore-rules');
328
- } else if (profile === 'design') {
329
- // A MOCKUP, 3D-MODEL OR DECK CARD (0.115.0): the fence is
330
- // `codexDesignFence`'s, argued and measured at its definition below.
331
- a.push(...codexDesignFence(cwd));
264
+ // THE FENCE, picked by the same keys as `codexIgnoresUserConfig`: a fenced
265
+ // posture builds its own (CODEX_FENCES below); anything else is a build.
266
+ const fenced = codexIgnoresUserConfig({ profile, vaultDir });
267
+ if (fenced) {
268
+ a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation, knowledgeDir, projectReads }));
332
269
  } else {
333
270
  // The daemon's build posture, mapped: SAFE keeps writes inside the
334
271
  // workspace, the default lets the agent run its own tests and git
@@ -356,7 +293,10 @@ export const CODEX_RUNTIME = {
356
293
  if (a.includes('--ignore-user-config')) a.push(...(personal?.args ?? []));
357
294
 
358
295
  a.push(...mcp, ...resultSchemaArgs);
359
- a.push(`${system}\n\n---\n\n${prompt}`);
296
+ const readers = profile === 'consult' || profile === 'plan'
297
+ ? '\nUse exec_command with shell="/bin/bash" and login=false for project discovery. Project files, local Git history and the project knowledge directory are readable; host files are outside this turn’s read boundary.'
298
+ : '';
299
+ a.push(`${system}${readers}\n\n---\n\n${prompt}`);
360
300
  return a;
361
301
  },
362
302
  /**
@@ -376,16 +316,19 @@ export const CODEX_RUNTIME = {
376
316
  };
377
317
 
378
318
  /**
379
- * THE MOCKUP, 3D-MODEL AND DECK CARDS' FENCE ON CODEX (0.115.0; 2026-09-29, the
380
- * owner: "its saying 3d models run on claude only but thats not true").
319
+ * THE ARTIFACT CARDS' FENCE ON CODEX — the image card's (0.114.0, the image
320
+ * tool switched on) and the mockup, 3D-model and deck cards' (0.115.0;
321
+ * 2026-09-29, the owner: "its saying 3d models run on claude only but thats
322
+ * not true"), one builder under each card's own profile name since 2026-10-10.
381
323
  *
382
- * The image fence's shape without the image tool: a permission profile that
383
- * reads the repo and writes ONLY `<worktree>/.flowviant/artifacts/`, selected
324
+ * Design and image share supporting native image generation and a
325
+ * permission profile that reads the repo and writes ONLY
326
+ * `<worktree>/.flowviant/artifacts/` (and the turn's scratch), selected
384
327
  * by `default_permissions` (`codex exec` and `exec resume` refuse `-P` on
385
328
  * 0.156.1), no network, no web search, no sub-agents, no browser, computer or
386
- * apps, nobody's config or rules, and no approvals to wait on. Nothing about
387
- * the card's contract needs more: every one of the three is one HTML page, and
388
- * a page's CDN scripts load in the viewer, never on this machine.
329
+ * apps, nobody's config or rules, and no approvals to wait on. Needed image
330
+ * generation supports the page without changing its end product. CDN scripts
331
+ * load in the viewer, never on this machine.
389
332
  *
390
333
  * MEASURED on codex-cli 0.156.1 (Linux, bubblewrap), with gpt-6-luna at low
391
334
  * effort, in a scratch git repository (2026-09-29), with exactly this argv:
@@ -423,15 +366,16 @@ export const CODEX_RUNTIME = {
423
366
  * descriptor"). With the network and the web shut, the one road out is the
424
367
  * artifact itself, which is scrubbed of the machine's known secret values and
425
368
  * of any Flowviant credential by shape on the way up — the image fence's
426
- * standing (0.114.0), and why RESEARCH is not declared here: a write-up needs
427
- * the web, and a turn that can read `.env` and `~/.flowviant/credentials.json`
428
- * while holding `web_search` (both measured readable under this profile with
429
- * `web_search="live"`) is the exfiltration Claude's research list was fenced
430
- * to prevent.
369
+ * standing (0.114.0), and why RESEARCH does not use this fence: a write-up
370
+ * needs the web, and a turn that can read `.env` and
371
+ * `~/.flowviant/credentials.json` while holding `web_search` (both measured
372
+ * readable under this profile with `web_search="live"`) is the exfiltration
373
+ * Claude's research list was fenced to prevent. Codex research (0.117.0) runs
374
+ * under its own fence instead, over a copied view (CODEX_FENCES `research`).
431
375
  */
432
- export function codexDesignFence(cwd) {
376
+ export function codexArtifactFence(cwd, { name, imageGeneration = false, what }) {
433
377
  if (!cwd || !isAbsolute(cwd)) {
434
- throw new Error("a design turn needs its worktree to fence — none reached codex's builder");
378
+ throw new Error(`${what} needs its worktree to fence — none reached codex's builder`);
435
379
  }
436
380
  const artifacts = join(cwd, ARTIFACT_DIR);
437
381
  // THE TURN'S OWN SCRATCH (artifacts.mjs `ensureFenceScratch`, made empty by
@@ -443,17 +387,81 @@ export function codexDesignFence(cwd) {
443
387
  // `shell_environment_policy.set` — measured to reach the sandboxed shell.
444
388
  const scratch = join(cwd, FENCE_TMP_DIR);
445
389
  return [
446
- '-c', 'permissions.flowviantdesign.extends=":read-only"',
447
- // The table WHOLE and the path a TOML basic string — the wiki branch's
390
+ '-c', `permissions.${name}.extends=":read-only"`,
391
+ // The table WHOLE and the path a TOML basic string — the wiki fence's
448
392
  // reason: a dotted key splits on the dots inside the path.
449
- '-c', `permissions.flowviantdesign.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`,
393
+ '-c', `permissions.${name}.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`,
450
394
  '-c', `shell_environment_policy.set={TMPDIR=${JSON.stringify(scratch)},TMPPREFIX=${JSON.stringify(join(scratch, 'zsh'))}}`,
451
- '-c', 'default_permissions="flowviantdesign"',
395
+ '-c', `default_permissions="${name}"`,
396
+ // Nobody is on this end of the pipe to approve an escalation.
452
397
  '-c', 'approval_policy="never"',
453
- '--disable', 'image_generation',
454
- '-c', 'tools.web_search=false', '-c', 'web_search="disabled"',
455
- '-c', 'features.multi_agent=false', '-c', 'features.goals=false',
456
- ...NO_REACH_OUT,
457
- '--ignore-user-config', '--ignore-rules',
398
+ ...fencedTail({ imageGeneration }),
458
399
  ];
459
400
  }
401
+
402
+ /** The design card's fence — `codexArtifactFence` under its own profile name,
403
+ * with native image generation available for supporting assets. */
404
+ export const codexDesignFence = (cwd) =>
405
+ codexArtifactFence(cwd, { name: 'flowviantdesign', imageGeneration: true, what: 'a design turn' });
406
+
407
+ /**
408
+ * EVERY FENCED POSTURE'S ARGV, by the keys of CODEX_FENCED (a test holds the
409
+ * two tables to one key set), each built from the shared pieces above.
410
+ */
411
+ const CODEX_FENCES = {
412
+ // A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — a different
413
+ // shape from Claude's verb allowlist, which is the whole reason `profile` is
414
+ // a promise rather than a flag list. See codexProjectRead.mjs.
415
+ consult: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail()],
416
+ // A PLANNING SESSION. Read-only on the filesystem, exactly like a consult —
417
+ // the writes it makes go through the control plane, not through this box —
418
+ // and for the same reason: its prompt is steered by anything a project
419
+ // editor can type. What differs is the ONE thing this profile exists for:
420
+ // an MCP config IS passed (by the caller, after the fence), carrying this
421
+ // turn's scoped token. The plan token has the planning tools; a capture
422
+ // token has only staging and transcript tools. The server refuses
423
+ // everything outside that scope.
424
+ plan: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail()],
425
+ wiki: ({ vaultDir }) => codexWikiFence(vaultDir),
426
+ // AN IMAGE CARD (0.114.0): read the repo, write ONLY the worktree's
427
+ // `.flowviant/artifacts/`, generate pictures, and nothing else. MEASURED on
428
+ // codex-cli 0.156.1 (Linux, bubblewrap), with no model turn spent: under
429
+ // this profile a `cp` from a CODEX_HOME-shaped folder into the artifacts
430
+ // directory landed, a write to a repo file failed "Read-only file system",
431
+ // and curl got no network; `codex debug prompt-input` with these overrides
432
+ // renders a managed profile of `write <worktree>/.flowviant/artifacts` +
433
+ // `read :root`, "Approval policy is currently never".
434
+ //
435
+ // SELECTED BY `default_permissions`, NOT `-P`: `codex exec` (and `exec
436
+ // resume`) on 0.156.1 refuse `-P` as an unexpected argument — only `codex
437
+ // sandbox` takes it — while `-c default_permissions="<name>"` resolves the
438
+ // named profile in both. The directory must EXIST before spawn (measured: a
439
+ // missing one cannot be created from inside the fence), which the agent lane
440
+ // sees to (`ensureArtifactDir`).
441
+ //
442
+ // WHERE THE PICTURES COME FROM. Codex's own `image_gen` tool saves each one
443
+ // under `$CODEX_HOME/generated_images/…` and says the path (the binary's own
444
+ // words: "Generated images are saved to … by default. If you need to use a
445
+ // generated image at another path, copy it"). An agent turn's CODEX_HOME is
446
+ // the agent's own isolated home (projectToolRuntimes, kept per agent since
447
+ // 2026-09-29); `:root` reads admit it, so the agent copies what it keeps
448
+ // into the one writable directory, where the artifact scan finds it.
449
+ //
450
+ // Native images support every artifact kind; this profile still delivers
451
+ // PNG/WebP, while design/model/deck still deliver their self-contained page.
452
+ image: ({ cwd }) => codexArtifactFence(cwd, { name: 'flowviantimage', imageGeneration: true, what: 'an image turn' }),
453
+ // A MOCKUP, 3D-MODEL OR DECK CARD (0.115.0): the fence is
454
+ // `codexDesignFence`'s, argued and measured at `codexArtifactFence` above.
455
+ design: ({ cwd }) => codexDesignFence(cwd),
456
+ // A WRITE-UP CARD (0.117.0): live web over a COPIED view of the worktree,
457
+ // never the checkout or the login store, under a root-denied tool fence
458
+ // (codexResearch.mjs `researchFence`, built by runTurn's
459
+ // `runIsolatedResearch`). It is the one fence that keeps the web, so its
460
+ // tail leaves NO_WEB out and closes everything else.
461
+ research: ({ researchIsolation }) => {
462
+ if (!researchIsolation) throw new Error('Codex research requires its isolated input copy.');
463
+ return [...researchFence(researchIsolation), ...fencedTail({ web: true, imageGeneration: true })];
464
+ },
465
+ };
466
+ /** The fenced profile names — for the test that holds both tables to one set. */
467
+ export const CODEX_FENCE_PROFILES = Object.freeze(Object.keys(CODEX_FENCES));
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import { execFileSync } from 'node:child_process';
17
- import { RUNTIMES, canRun, drivableHere } from './runtimes.mjs';
17
+ import { RUNTIMES, canRun, drivableHere, runtimeById } from './runtimes.mjs';
18
18
 
19
19
  /**
20
20
  * WHICH RUNTIME RUNS A JOB THAT NOBODY @MENTIONED.
@@ -41,7 +41,7 @@ import { RUNTIMES, canRun, drivableHere } from './runtimes.mjs';
41
41
  export function pickRuntimeFor(profile, { detected } = {}) {
42
42
  const rows = detected ?? detectRuntimes();
43
43
  const ok = (id) =>
44
- canRun(RUNTIMES[id], profile) && Boolean(rows.find((d) => d.id === id)?.installed);
44
+ canRun(runtimeById(id), profile) && Boolean(rows.find((d) => d.id === id)?.installed);
45
45
  if (ok('claude')) return 'claude';
46
46
  return Object.keys(RUNTIMES).find(ok) ?? null;
47
47
  }
@@ -86,6 +86,12 @@ export function runtimesReport(detected) {
86
86
  .join(',');
87
87
  }
88
88
 
89
+ /** Existing probe evidence only; diagnostics never spawn another CLI. */
90
+ export function measuredRuntimeVersion(id) {
91
+ const row = detectedCache?.find((runtime) => runtime.id === id);
92
+ return row ? { version: row.version, measuredAt: new Date(detectedAt).toISOString() } : null;
93
+ }
94
+
89
95
  export function detectRuntimes({ refresh = false } = {}) {
90
96
  if (detectedCache && !refresh && Date.now() - detectedAt < DETECT_TTL_MS) return detectedCache;
91
97
  detectedAt = Date.now();
@@ -86,14 +86,20 @@ export function humanizeClaudeTool(name, input = {}, cwd = '') {
86
86
  case 'LS':
87
87
  return { kind: 'list', label: `ls ${shortPath(input.path ?? '.', cwd)}` };
88
88
  case 'Bash':
89
- // `command` rides beside the display label, VERBATIM (capped): the label
90
- // is a 60-char readout for a console and the rail, and truncation is
91
- // fine there — but the admin's command audit exists to answer "what
89
+ // `command` rides beside the display label, VERBATIM (windowed): the
90
+ // label is a 60-char readout for a console and the rail, and truncation
91
+ // is fine there — but the admin's command audit exists to answer "what
92
92
  // actually ran on our box", and an ellipsis is exactly where the part
93
93
  // that matters would hide.
94
+ //
95
+ // WINDOWED, NOT CAPPED (2026-10-10, the audit's F128). This cut at the
96
+ // server's 2000 before the audit scrubbed it — the cap-then-scrub order
97
+ // `toolEventOf` below records as a leak: a secret straddling character
98
+ // 2000 kept its prefix. The window only bounds memory; the audit
99
+ // scrubs, THEN cuts to the server's cap (workCommandAudit.mjs).
94
100
  return {
95
101
  kind: 'bash',
96
- command: String(input.command ?? '').slice(0, 2000),
102
+ command: String(input.command ?? '').slice(0, SCRUB_WINDOW),
97
103
  label: `$ ${oneLine(input.command, 60)}`,
98
104
  };
99
105
  case 'TodoWrite': {
@@ -265,7 +271,7 @@ function humanizeCodexItem(item = {}, cwd = '') {
265
271
  case 'command_execution':
266
272
  return {
267
273
  kind: 'bash',
268
- command: String(item.command ?? '').slice(0, 2000),
274
+ command: String(item.command ?? '').slice(0, SCRUB_WINDOW), // windowed, not capped — see Claude's Bash
269
275
  label: `$ ${oneLine(item.command, 60)}`,
270
276
  };
271
277
  case 'file_change': {
@@ -295,9 +301,9 @@ function humanizeCodexItem(item = {}, cwd = '') {
295
301
 
296
302
  /**
297
303
  * Codex `--json` emits JSONL of ThreadEvents. Returns `{ activity, text,
298
- * threadId }` — `text` accumulates the agent's own words, because the turn
299
- * loop reads its sentinels (NOTHING / BLOCKED:<id> / DONE) out of exactly
300
- * that; `threadId` surfaces once, off the lifecycle event, for callers that
304
+ * threadId }` — `text` accumulates the agent's own words, because the wiki
305
+ * lane reads its sentinels (WIKI_DONE / REGROUND_DONE, claude.mjs
306
+ * `sawSentinel`) out of exactly that; `threadId` surfaces once, off the lifecycle event, for callers that
301
307
  * need to resume THIS conversation later (runTurn's onThreadId).
302
308
  */
303
309
  export function parseCodexLine(line, cwd) {
@@ -316,11 +322,16 @@ export function parseCodexLine(line, cwd) {
316
322
  // text: nothing here is the model speaking.
317
323
  case 'thread.started':
318
324
  return { activity: null, text: '', threadId: String(ev.thread_id ?? '') || null };
325
+ case 'item.started': {
326
+ const item = ev.item ?? {};
327
+ if ((item.item_type ?? item.type) !== 'command_execution') return null;
328
+ return { toolEvent: ['Bash', { command: item.command }, { id: item.id, runtime: 'codex' }] };
329
+ }
319
330
  case 'item.completed': {
320
331
  const item = ev.item ?? {};
321
332
  const activity = humanizeCodexItem(item, cwd);
322
333
  // Only the agent's MESSAGES are sentinel-bearing text. Reasoning is not:
323
- // a model that muses "I could output NOTHING here" must not end the turn.
334
+ // a model that muses "I could print WIKI_DONE now" must not end the turn.
324
335
  const isMessage = (item.item_type ?? item.type) === 'agent_message';
325
336
  const text = isMessage ? `${item.text ?? item.message ?? ''}\n` : '';
326
337
  // `answer` is the SAME message, surfaced alone: `text` accumulates every
@@ -328,7 +339,13 @@ export function parseCodexLine(line, cwd) {
328
339
  // the agent said LAST" (the agent lane's final JSON object) cannot recover
329
340
  // it from `out`. Claude's stream has this for free under
330
341
  // `answerFromResult`; codex needed it said.
331
- return { activity, text, ...(isMessage ? { answer: String(item.text ?? item.message ?? '') } : {}) };
342
+ const command = (item.item_type ?? item.type) === 'command_execution';
343
+ const toolEvent = command ? ['Bash', { command: item.command }, {
344
+ id: item.id, result: true, runtime: 'codex',
345
+ exitCode: item.exit_code, isError: item.status === 'failed',
346
+ output: item.aggregated_output ?? '',
347
+ }] : null;
348
+ return { activity, text, ...(toolEvent ? { toolEvent } : {}), ...(isMessage ? { answer: String(item.text ?? item.message ?? '') } : {}) };
332
349
  }
333
350
  // THE TURN'S OWN TOKEN COUNT (2026-09-24). `codex exec --json` closes a
334
351
  // turn with `turn.completed` carrying `usage` — the codex twin of Claude's
@@ -439,7 +456,7 @@ function humanizeAgyTool(name, p = {}, cwd = '') {
439
456
  case 'run_command':
440
457
  return {
441
458
  kind: 'bash',
442
- command: String(p.CommandLine ?? '').slice(0, 2000),
459
+ command: String(p.CommandLine ?? '').slice(0, SCRUB_WINDOW), // windowed, not capped — see Claude's Bash
443
460
  label: `$ ${oneLine(p.CommandLine, 60)}`,
444
461
  };
445
462
  case 'call_mcp_tool':