fixo-cli 1.0.3 → 2.0.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.

Potentially problematic release.


This version of fixo-cli might be problematic. Click here for more details.

Files changed (222) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +18 -14
  3. package/dist/agent/agent-client.d.ts +28 -6
  4. package/dist/agent/agent-client.d.ts.map +1 -1
  5. package/dist/agent/agent-client.js +118 -39
  6. package/dist/agent/agent-client.js.map +1 -1
  7. package/dist/agent/agent-pool.d.ts +55 -6
  8. package/dist/agent/agent-pool.d.ts.map +1 -1
  9. package/dist/agent/agent-pool.js +120 -20
  10. package/dist/agent/agent-pool.js.map +1 -1
  11. package/dist/agent/auto-verifier.d.ts +55 -0
  12. package/dist/agent/auto-verifier.d.ts.map +1 -0
  13. package/dist/agent/auto-verifier.js +50 -0
  14. package/dist/agent/auto-verifier.js.map +1 -0
  15. package/dist/agent/command-parser.d.ts +37 -0
  16. package/dist/agent/command-parser.d.ts.map +1 -1
  17. package/dist/agent/command-parser.js +473 -1
  18. package/dist/agent/command-parser.js.map +1 -1
  19. package/dist/agent/context-builder.d.ts +24 -0
  20. package/dist/agent/context-builder.d.ts.map +1 -0
  21. package/dist/agent/context-builder.js +197 -0
  22. package/dist/agent/context-builder.js.map +1 -0
  23. package/dist/agent/conversation.d.ts +32 -2
  24. package/dist/agent/conversation.d.ts.map +1 -1
  25. package/dist/agent/conversation.js +84 -9
  26. package/dist/agent/conversation.js.map +1 -1
  27. package/dist/agent/duration.d.ts +24 -0
  28. package/dist/agent/duration.d.ts.map +1 -0
  29. package/dist/agent/duration.js +42 -0
  30. package/dist/agent/duration.js.map +1 -0
  31. package/dist/agent/file-writing-rules.d.ts +19 -0
  32. package/dist/agent/file-writing-rules.d.ts.map +1 -0
  33. package/dist/agent/file-writing-rules.js +31 -0
  34. package/dist/agent/file-writing-rules.js.map +1 -0
  35. package/dist/agent/mcp-bridge.js +1 -1
  36. package/dist/agent/mcp-bridge.js.map +1 -1
  37. package/dist/agent/orchestrator.d.ts +45 -0
  38. package/dist/agent/orchestrator.d.ts.map +1 -1
  39. package/dist/agent/orchestrator.js +140 -3
  40. package/dist/agent/orchestrator.js.map +1 -1
  41. package/dist/agent/parser-adapter.d.ts +17 -0
  42. package/dist/agent/parser-adapter.d.ts.map +1 -1
  43. package/dist/agent/parser-adapter.js +311 -7
  44. package/dist/agent/parser-adapter.js.map +1 -1
  45. package/dist/agent/predictive-gate.d.ts.map +1 -1
  46. package/dist/agent/predictive-gate.js +4 -1
  47. package/dist/agent/predictive-gate.js.map +1 -1
  48. package/dist/agent/provider-cooldown.d.ts.map +1 -1
  49. package/dist/agent/provider-cooldown.js +3 -2
  50. package/dist/agent/provider-cooldown.js.map +1 -1
  51. package/dist/agent/providers-manager.d.ts +5 -0
  52. package/dist/agent/providers-manager.d.ts.map +1 -1
  53. package/dist/agent/providers-manager.js +119 -8
  54. package/dist/agent/providers-manager.js.map +1 -1
  55. package/dist/agent/repo-map.d.ts +18 -1
  56. package/dist/agent/repo-map.d.ts.map +1 -1
  57. package/dist/agent/repo-map.js +144 -54
  58. package/dist/agent/repo-map.js.map +1 -1
  59. package/dist/agent/retry.js +1 -2
  60. package/dist/agent/retry.js.map +1 -1
  61. package/dist/agent/single-agent.d.ts +13 -0
  62. package/dist/agent/single-agent.d.ts.map +1 -1
  63. package/dist/agent/single-agent.js +225 -37
  64. package/dist/agent/single-agent.js.map +1 -1
  65. package/dist/agent/skills.d.ts.map +1 -1
  66. package/dist/agent/skills.js +2 -1
  67. package/dist/agent/skills.js.map +1 -1
  68. package/dist/agent/subagent.js +2 -2
  69. package/dist/agent/subagent.js.map +1 -1
  70. package/dist/agent/task-router.d.ts +46 -0
  71. package/dist/agent/task-router.d.ts.map +1 -0
  72. package/dist/agent/task-router.js +352 -0
  73. package/dist/agent/task-router.js.map +1 -0
  74. package/dist/agent/telemetry.d.ts +29 -1
  75. package/dist/agent/telemetry.d.ts.map +1 -1
  76. package/dist/agent/telemetry.js +29 -11
  77. package/dist/agent/telemetry.js.map +1 -1
  78. package/dist/agent/tool-definitions.d.ts +3 -0
  79. package/dist/agent/tool-definitions.d.ts.map +1 -0
  80. package/dist/agent/tool-definitions.js +519 -0
  81. package/dist/agent/tool-definitions.js.map +1 -0
  82. package/dist/agent/tool-executor.d.ts +6 -1
  83. package/dist/agent/tool-executor.d.ts.map +1 -1
  84. package/dist/agent/tool-executor.js +99 -553
  85. package/dist/agent/tool-executor.js.map +1 -1
  86. package/dist/agent/tools/command-tools.d.ts +6 -0
  87. package/dist/agent/tools/command-tools.d.ts.map +1 -0
  88. package/dist/agent/tools/command-tools.js +104 -0
  89. package/dist/agent/tools/command-tools.js.map +1 -0
  90. package/dist/agent/tools/file-tools.d.ts +15 -0
  91. package/dist/agent/tools/file-tools.d.ts.map +1 -0
  92. package/dist/agent/tools/file-tools.js +551 -0
  93. package/dist/agent/tools/file-tools.js.map +1 -0
  94. package/dist/agent/tools/todo-tools.d.ts +3 -0
  95. package/dist/agent/tools/todo-tools.d.ts.map +1 -0
  96. package/dist/agent/tools/todo-tools.js +70 -0
  97. package/dist/agent/tools/todo-tools.js.map +1 -0
  98. package/dist/agent/web-impl.d.ts.map +1 -1
  99. package/dist/agent/web-impl.js +45 -0
  100. package/dist/agent/web-impl.js.map +1 -1
  101. package/dist/agent/worker-agent.d.ts +3 -1
  102. package/dist/agent/worker-agent.d.ts.map +1 -1
  103. package/dist/agent/worker-agent.js +56 -16
  104. package/dist/agent/worker-agent.js.map +1 -1
  105. package/dist/config.d.ts +253 -1
  106. package/dist/config.d.ts.map +1 -1
  107. package/dist/config.js +81 -1
  108. package/dist/config.js.map +1 -1
  109. package/dist/git/git-manager.d.ts +33 -2
  110. package/dist/git/git-manager.d.ts.map +1 -1
  111. package/dist/git/git-manager.js +111 -15
  112. package/dist/git/git-manager.js.map +1 -1
  113. package/dist/git/git-ops.d.ts.map +1 -1
  114. package/dist/git/git-ops.js +2 -1
  115. package/dist/git/git-ops.js.map +1 -1
  116. package/dist/index.js +89 -8
  117. package/dist/index.js.map +1 -1
  118. package/dist/lsp/lsp-manager.js +1 -1
  119. package/dist/lsp/lsp-manager.js.map +1 -1
  120. package/dist/model-outcomes.d.ts.map +1 -1
  121. package/dist/model-outcomes.js +2 -1
  122. package/dist/model-outcomes.js.map +1 -1
  123. package/dist/planner.d.ts +0 -9
  124. package/dist/planner.d.ts.map +1 -1
  125. package/dist/planner.js +0 -9
  126. package/dist/planner.js.map +1 -1
  127. package/dist/project-memory.d.ts +12 -1
  128. package/dist/project-memory.d.ts.map +1 -1
  129. package/dist/project-memory.js +8 -6
  130. package/dist/project-memory.js.map +1 -1
  131. package/dist/runtime/loop-mitigation.d.ts +119 -0
  132. package/dist/runtime/loop-mitigation.d.ts.map +1 -0
  133. package/dist/runtime/loop-mitigation.js +192 -0
  134. package/dist/runtime/loop-mitigation.js.map +1 -0
  135. package/dist/runtime/os-sandbox.d.ts +100 -0
  136. package/dist/runtime/os-sandbox.d.ts.map +1 -0
  137. package/dist/runtime/os-sandbox.js +246 -0
  138. package/dist/runtime/os-sandbox.js.map +1 -0
  139. package/dist/runtime/run-inventory.d.ts +17 -0
  140. package/dist/runtime/run-inventory.d.ts.map +1 -0
  141. package/dist/runtime/run-inventory.js +49 -0
  142. package/dist/runtime/run-inventory.js.map +1 -0
  143. package/dist/runtime/session-snapshots.d.ts +52 -2
  144. package/dist/runtime/session-snapshots.d.ts.map +1 -1
  145. package/dist/runtime/session-snapshots.js +76 -1
  146. package/dist/runtime/session-snapshots.js.map +1 -1
  147. package/dist/runtime/staging.d.ts.map +1 -1
  148. package/dist/runtime/staging.js +4 -1
  149. package/dist/runtime/staging.js.map +1 -1
  150. package/dist/runtime/task-session.d.ts +14 -0
  151. package/dist/runtime/task-session.d.ts.map +1 -1
  152. package/dist/runtime/task-session.js +26 -0
  153. package/dist/runtime/task-session.js.map +1 -1
  154. package/dist/setup-wizard.d.ts +11 -3
  155. package/dist/setup-wizard.d.ts.map +1 -1
  156. package/dist/setup-wizard.js +113 -15
  157. package/dist/setup-wizard.js.map +1 -1
  158. package/dist/types.d.ts +8 -0
  159. package/dist/types.d.ts.map +1 -1
  160. package/dist/ui/commands/context-commands.d.ts +7 -0
  161. package/dist/ui/commands/context-commands.d.ts.map +1 -0
  162. package/dist/ui/commands/context-commands.js +241 -0
  163. package/dist/ui/commands/context-commands.js.map +1 -0
  164. package/dist/ui/commands/index.d.ts +3 -0
  165. package/dist/ui/commands/index.d.ts.map +1 -0
  166. package/dist/ui/commands/index.js +46 -0
  167. package/dist/ui/commands/index.js.map +1 -0
  168. package/dist/ui/commands/info-commands.d.ts +15 -0
  169. package/dist/ui/commands/info-commands.d.ts.map +1 -0
  170. package/dist/ui/commands/info-commands.js +122 -0
  171. package/dist/ui/commands/info-commands.js.map +1 -0
  172. package/dist/ui/commands/model-commands.d.ts +5 -0
  173. package/dist/ui/commands/model-commands.d.ts.map +1 -0
  174. package/dist/ui/commands/model-commands.js +417 -0
  175. package/dist/ui/commands/model-commands.js.map +1 -0
  176. package/dist/ui/commands/session-commands.d.ts +5 -0
  177. package/dist/ui/commands/session-commands.d.ts.map +1 -0
  178. package/dist/ui/commands/session-commands.js +154 -0
  179. package/dist/ui/commands/session-commands.js.map +1 -0
  180. package/dist/ui/commands/task-commands.d.ts +8 -0
  181. package/dist/ui/commands/task-commands.d.ts.map +1 -0
  182. package/dist/ui/commands/task-commands.js +152 -0
  183. package/dist/ui/commands/task-commands.js.map +1 -0
  184. package/dist/ui/commands/types.d.ts +46 -0
  185. package/dist/ui/commands/types.d.ts.map +1 -0
  186. package/dist/ui/commands/types.js +2 -0
  187. package/dist/ui/commands/types.js.map +1 -0
  188. package/dist/ui/commands/workspace-commands.d.ts +8 -0
  189. package/dist/ui/commands/workspace-commands.d.ts.map +1 -0
  190. package/dist/ui/commands/workspace-commands.js +131 -0
  191. package/dist/ui/commands/workspace-commands.js.map +1 -0
  192. package/dist/ui/loading-animation.d.ts +24 -0
  193. package/dist/ui/loading-animation.d.ts.map +1 -0
  194. package/dist/ui/loading-animation.js +123 -0
  195. package/dist/ui/loading-animation.js.map +1 -0
  196. package/dist/ui/markdown-stream.js +2 -2
  197. package/dist/ui/markdown-stream.js.map +1 -1
  198. package/dist/ui/prompt.d.ts +7 -0
  199. package/dist/ui/prompt.d.ts.map +1 -1
  200. package/dist/ui/prompt.js +461 -1143
  201. package/dist/ui/prompt.js.map +1 -1
  202. package/dist/ui/render-primitives.d.ts +6 -0
  203. package/dist/ui/render-primitives.d.ts.map +1 -1
  204. package/dist/ui/render-primitives.js +30 -13
  205. package/dist/ui/render-primitives.js.map +1 -1
  206. package/dist/ui/render.d.ts.map +1 -1
  207. package/dist/ui/render.js +2 -0
  208. package/dist/ui/render.js.map +1 -1
  209. package/dist/ui/session-header.d.ts +13 -0
  210. package/dist/ui/session-header.d.ts.map +1 -1
  211. package/dist/ui/session-header.js +6 -0
  212. package/dist/ui/session-header.js.map +1 -1
  213. package/package.json +22 -4
  214. package/scripts/check-vendor-wasm.js +55 -0
  215. package/vendor/tree-sitter-bash.wasm +0 -0
  216. package/vendor/tree-sitter-go.wasm +0 -0
  217. package/vendor/tree-sitter-javascript.wasm +0 -0
  218. package/vendor/tree-sitter-python.wasm +0 -0
  219. package/vendor/tree-sitter-rust.wasm +0 -0
  220. package/vendor/tree-sitter-tsx.wasm +0 -0
  221. package/vendor/tree-sitter-typescript.wasm +0 -0
  222. package/vendor/tree-sitter.wasm +0 -0
@@ -2,18 +2,23 @@
2
2
  * Tool definitions and executor for the single-agent tool-calling loop.
3
3
  * Provides: read_file, write_file, run_command, search_code, list_dir
4
4
  */
5
+ import { TOOL_DEFINITIONS } from './tool-definitions.js';
6
+ export { TOOL_DEFINITIONS };
5
7
  import fs from 'fs';
6
8
  import path from 'path';
7
9
  import { spawnSync } from 'child_process';
10
+ import { randomBytes } from 'node:crypto';
8
11
  import { colors } from '../ui/colors.js';
9
12
  import { renderToolCall, startInlineToolSpinner, } from '../ui/render-primitives.js';
10
13
  import { WorkspaceGuard } from '../workspace-guard.js';
11
14
  import { classifyCommand } from '../runtime/policy.js';
12
15
  import { checkPermission } from './permissions.js';
13
16
  import { redactedEnv, redactSecrets } from '../runtime/redaction.js';
17
+ import { runSandboxed, SandboxUnavailableError } from '../runtime/os-sandbox.js';
14
18
  import { McpManager } from './mcp-manager.js';
15
19
  import { estimateReadCost, shouldDeferRead, formatPredictiveGateDirective, DEFAULT_PREDICTIVE_BUDGET_PCT } from './predictive-gate.js';
16
20
  import { createBranch, commitChanges, pushBranch, createPullRequest } from '../git/git-ops.js';
21
+ import { GitManager } from '../git/git-manager.js';
17
22
  import { pathToFileURL } from 'url';
18
23
  import * as p from '@clack/prompts';
19
24
  import { loadConfig, saveConfig } from '../config.js';
@@ -29,6 +34,7 @@ import { recordTelemetry, telemetry } from './telemetry.js';
29
34
  import { applyModifiedArgs, fireHooks } from './hooks.js';
30
35
  import { BackgroundJobRegistry } from '../runtime/background-jobs.js';
31
36
  import { ParserFactory, languageIdFromExtension, } from './parser-adapter.js';
37
+ import { getRunInventory } from '../runtime/run-inventory.js';
32
38
  export const mcpManager = new McpManager();
33
39
  export const mcpBridgeManager = new McpBridgeManager();
34
40
  let lspManagerInstance = null;
@@ -192,524 +198,6 @@ export function classifyExecutionRole(task) {
192
198
  }
193
199
  return 'BUILD';
194
200
  }
195
- export const TOOL_DEFINITIONS = [
196
- {
197
- type: 'function',
198
- function: {
199
- name: 'read_file',
200
- description: 'Read the full text contents of a file at the given path. Use this to understand existing code before making changes. Returns the file contents as a string. Files larger than the large-file gate (15 KiB / 350 lines by default) will return a [Context-Budget Guard] synthetic directive telling you to call extract_symbols or extract_imports first.',
201
- parameters: {
202
- type: 'object',
203
- properties: {
204
- path: {
205
- type: 'string',
206
- description: 'The file path to read, relative to the workspace root or absolute.',
207
- },
208
- },
209
- required: ['path'],
210
- },
211
- },
212
- },
213
- {
214
- type: 'function',
215
- function: {
216
- name: 'extract_symbols',
217
- description: 'Extract symbol declarations (classes, functions, interfaces, types, consts) from a file. Output is capped at 100 entries. Cheaper than read_file for large files because it skips the body content.',
218
- parameters: {
219
- type: 'object',
220
- properties: {
221
- path: {
222
- type: 'string',
223
- description: 'The file path to inspect, relative to the workspace root or absolute.',
224
- },
225
- },
226
- required: ['path'],
227
- },
228
- },
229
- },
230
- {
231
- type: 'function',
232
- function: {
233
- name: 'extract_imports',
234
- description: 'Extract import statements from a file. Output is capped at 100 entries. Cheaper than read_file for large files because it skips the body content.',
235
- parameters: {
236
- type: 'object',
237
- properties: {
238
- path: {
239
- type: 'string',
240
- description: 'The file path to inspect, relative to the workspace root or absolute.',
241
- },
242
- },
243
- required: ['path'],
244
- },
245
- },
246
- },
247
- {
248
- type: 'function',
249
- function: {
250
- name: 'apply_patch',
251
- description: 'Apply a unified diff patch to files in the workspace. Prefer this over write_file for editing existing files.',
252
- parameters: {
253
- type: 'object',
254
- properties: {
255
- patch: { type: 'string', description: 'Unified diff patch text.' },
256
- },
257
- required: ['patch'],
258
- },
259
- },
260
- },
261
- {
262
- type: 'function',
263
- function: {
264
- name: 'replace_range',
265
- description: 'Replace inclusive 1-based line range in a file. Requires reading the file first.',
266
- parameters: {
267
- type: 'object',
268
- properties: {
269
- path: { type: 'string' },
270
- startLine: { type: 'string' },
271
- endLine: { type: 'string' },
272
- content: { type: 'string' },
273
- },
274
- required: ['path', 'startLine', 'endLine', 'content'],
275
- },
276
- },
277
- },
278
- {
279
- type: 'function',
280
- function: {
281
- name: 'insert_after',
282
- description: 'Insert content after the first exact anchor match in a file. Requires reading the file first.',
283
- parameters: {
284
- type: 'object',
285
- properties: {
286
- path: { type: 'string' },
287
- anchor: { type: 'string' },
288
- content: { type: 'string' },
289
- },
290
- required: ['path', 'anchor', 'content'],
291
- },
292
- },
293
- },
294
- {
295
- type: 'function',
296
- function: {
297
- name: 'rename_file',
298
- description: 'Rename or move a workspace file.',
299
- parameters: {
300
- type: 'object',
301
- properties: {
302
- from: { type: 'string' },
303
- to: { type: 'string' },
304
- },
305
- required: ['from', 'to'],
306
- },
307
- },
308
- },
309
- {
310
- type: 'function',
311
- function: {
312
- name: 'write_file',
313
- description: 'Write a complete file to disk. Use ONLY for new files or full rewrites where the prior content is irrelevant. For ANY change to an existing file (single-region edit, symbol rename, line tweak, multi-line refactor) you MUST use `str_replace` (single hunk) or `apply_patch` (multi-region) instead — both go through the same atomic staging pipeline but preserve the rest of the file. Rewriting an existing file with write_file when str_replace would do is an error: it wastes tokens, defeats LSP pre-save gating granularity, and risks losing concurrent edits. Creates parent directories if missing.',
314
- parameters: {
315
- type: 'object',
316
- properties: {
317
- path: {
318
- type: 'string',
319
- description: 'The file path to write, relative to the workspace root or absolute.',
320
- },
321
- content: {
322
- type: 'string',
323
- description: 'The full file content to write.',
324
- },
325
- },
326
- required: ['path', 'content'],
327
- },
328
- },
329
- },
330
- {
331
- type: 'function',
332
- function: {
333
- name: 'run_command',
334
- description: 'Execute a shell command and return its stdout and stderr output. Use this to run tests, build projects, install dependencies, or verify changes. Commands run in the workspace directory.',
335
- parameters: {
336
- type: 'object',
337
- properties: {
338
- command: {
339
- type: 'string',
340
- description: 'The shell command to execute.',
341
- },
342
- cwd: {
343
- type: 'string',
344
- description: 'Working directory for the command (optional, defaults to workspace root).',
345
- },
346
- },
347
- required: ['command'],
348
- },
349
- },
350
- },
351
- {
352
- type: 'function',
353
- function: {
354
- name: 'run_command_async',
355
- description: 'Spawn a long-running command in the background and return immediately with a jobId. Use poll_command_status to retrieve output and kill_command to terminate. Output streams cap at 64 KiB each; the larger totalByte counters survive the cap. Rejected in PLAN mode. Workspace-escape and sensitive-file commands are blocked at spawn time by the same command-parser used by run_command.',
356
- parameters: {
357
- type: 'object',
358
- properties: {
359
- cmd: { type: 'string', description: 'The binary to spawn.' },
360
- args: {
361
- type: 'array',
362
- items: { type: 'string' },
363
- description: 'Argument vector.',
364
- },
365
- cwd: {
366
- type: 'string',
367
- description: 'Working directory (defaults to workspace root).',
368
- },
369
- },
370
- required: ['cmd'],
371
- },
372
- },
373
- },
374
- {
375
- type: 'function',
376
- function: {
377
- name: 'poll_command_status',
378
- description: 'Read a snapshot of a background job. The snapshot includes status, exitCode, startedAt/exitedAt, and the (capped) stdout and stderr streams. tailLines truncates each stream to its last N lines; sinceBytes returns only the bytes after the given offset on stdout/stderr (delta fields).',
379
- parameters: {
380
- type: 'object',
381
- properties: {
382
- jobId: { type: 'string', description: 'The jobId returned by run_command_async.' },
383
- tailLines: { type: 'integer', description: 'Truncate each stream to last N lines.' },
384
- sinceBytes: { type: 'integer', description: 'Return only bytes after this offset.' },
385
- },
386
- required: ['jobId'],
387
- },
388
- },
389
- },
390
- {
391
- type: 'function',
392
- function: {
393
- name: 'kill_command',
394
- description: 'Send SIGTERM to a background job. No-op if the job has already exited.',
395
- parameters: {
396
- type: 'object',
397
- properties: {
398
- jobId: { type: 'string', description: 'The jobId to terminate.' },
399
- },
400
- required: ['jobId'],
401
- },
402
- },
403
- },
404
- {
405
- type: 'function',
406
- function: {
407
- name: 'search_code',
408
- description: 'Search for a text or regex pattern in workspace files. Returns matching lines with file paths and line numbers. Use this to find where functions, classes, or variables are defined or used.',
409
- parameters: {
410
- type: 'object',
411
- properties: {
412
- query: {
413
- type: 'string',
414
- description: 'The search pattern (plain text or regex).',
415
- },
416
- path: {
417
- type: 'string',
418
- description: 'Directory or file to search in (optional, defaults to workspace root).',
419
- },
420
- file_pattern: {
421
- type: 'string',
422
- description: 'Glob pattern to filter files, e.g., "*.ts" or "*.py" (optional).',
423
- },
424
- },
425
- required: ['query'],
426
- },
427
- },
428
- },
429
- {
430
- type: 'function',
431
- function: {
432
- name: 'list_dir',
433
- description: 'List files and directories at the given path. Returns names, types (file/dir), and sizes.',
434
- parameters: {
435
- type: 'object',
436
- properties: {
437
- path: {
438
- type: 'string',
439
- description: 'The directory path to list (optional, defaults to workspace root).',
440
- },
441
- },
442
- required: [],
443
- },
444
- },
445
- },
446
- {
447
- type: 'function',
448
- function: {
449
- name: 'delete_file',
450
- description: 'Delete a file at the given path from the workspace.',
451
- parameters: {
452
- type: 'object',
453
- properties: {
454
- path: {
455
- type: 'string',
456
- description: 'The file path to delete, relative to the workspace root or absolute.',
457
- },
458
- },
459
- required: ['path'],
460
- },
461
- },
462
- },
463
- {
464
- type: 'function',
465
- function: {
466
- name: 'create_branch',
467
- description: 'Create and checkout a new Git branch.',
468
- parameters: {
469
- type: 'object',
470
- properties: {
471
- branchName: { type: 'string', description: 'The name of the branch to create.' },
472
- },
473
- required: ['branchName'],
474
- },
475
- },
476
- },
477
- {
478
- type: 'function',
479
- function: {
480
- name: 'commit_changes',
481
- description: 'Stage all current changes and commit them.',
482
- parameters: {
483
- type: 'object',
484
- properties: {
485
- message: { type: 'string', description: 'The commit message.' },
486
- },
487
- required: ['message'],
488
- },
489
- },
490
- },
491
- {
492
- type: 'function',
493
- function: {
494
- name: 'push_branch',
495
- description: 'Push the current active branch to origin or custom remote.',
496
- parameters: {
497
- type: 'object',
498
- properties: {
499
- remote: { type: 'string', description: 'The remote repository name (default: origin).' },
500
- },
501
- required: [],
502
- },
503
- },
504
- },
505
- {
506
- type: 'function',
507
- function: {
508
- name: 'create_pull_request',
509
- description: 'Create a pull request on GitHub for the current branch.',
510
- parameters: {
511
- type: 'object',
512
- properties: {
513
- baseBranch: { type: 'string', description: 'The base branch to merge into (default: main).' },
514
- },
515
- required: [],
516
- },
517
- },
518
- },
519
- {
520
- type: 'function',
521
- function: {
522
- name: 'lsp_goto_definition',
523
- description: 'Find definition coordinates for a symbol at a given 0-indexed line and character position using LSP.',
524
- parameters: {
525
- type: 'object',
526
- properties: {
527
- path: { type: 'string', description: 'File path containing the symbol.' },
528
- line: { type: 'integer', description: '0-indexed line number.' },
529
- character: { type: 'integer', description: '0-indexed character offset.' },
530
- },
531
- required: ['path', 'line', 'character'],
532
- },
533
- },
534
- },
535
- {
536
- type: 'function',
537
- function: {
538
- name: 'lsp_find_references',
539
- description: 'Find all reference locations for a symbol at a given 0-indexed line and character position using LSP.',
540
- parameters: {
541
- type: 'object',
542
- properties: {
543
- path: { type: 'string', description: 'File path containing the symbol.' },
544
- line: { type: 'integer', description: '0-indexed line number.' },
545
- character: { type: 'integer', description: '0-indexed character offset.' },
546
- },
547
- required: ['path', 'line', 'character'],
548
- },
549
- },
550
- },
551
- {
552
- type: 'function',
553
- function: {
554
- name: 'lsp_hover',
555
- description: 'Retrieve type information and documentation for a symbol at a given 0-indexed line and character position using LSP.',
556
- parameters: {
557
- type: 'object',
558
- properties: {
559
- path: { type: 'string', description: 'File path containing the symbol.' },
560
- line: { type: 'integer', description: '0-indexed line number.' },
561
- character: { type: 'integer', description: '0-indexed character offset.' },
562
- },
563
- required: ['path', 'line', 'character'],
564
- },
565
- },
566
- },
567
- {
568
- type: 'function',
569
- function: {
570
- name: 'web_fetch',
571
- description: 'Fetch a webpage using an HTTP GET request and return its content converted to Markdown. Use this to read documentation or external references.',
572
- parameters: {
573
- type: 'object',
574
- properties: {
575
- url: { type: 'string', description: 'The absolute URL to fetch.' },
576
- },
577
- required: ['url'],
578
- },
579
- },
580
- },
581
- {
582
- type: 'function',
583
- function: {
584
- name: 'web_search',
585
- description: 'Perform a web search for a given query and return a list of search results as Markdown snippets. Use this to find information on the web.',
586
- parameters: {
587
- type: 'object',
588
- properties: {
589
- query: { type: 'string', description: 'The search query.' },
590
- },
591
- required: ['query'],
592
- },
593
- },
594
- },
595
- {
596
- type: 'function',
597
- function: {
598
- name: 'str_replace',
599
- description: 'Use this tool by default for any in-place edit to an existing file. Performs a surgical, atomic replacement of oldString with newString. By default, oldString must be unique within the file (expectUnique=true) — non-unique matches are rejected with a clear error so the caller can narrow the snippet. Pass replaceAll=true to substitute every occurrence. Rejected in PLAN mode. Refused for platform-locked paths. Goes through the same atomic staging pipeline as write_file and the LSP pre-save gate before any disk mutation.',
600
- parameters: {
601
- type: 'object',
602
- properties: {
603
- path: {
604
- type: 'string',
605
- description: 'The file path to edit, relative to the workspace root or absolute.',
606
- },
607
- oldString: {
608
- type: 'string',
609
- description: 'The exact substring to replace. Must appear in the file.',
610
- },
611
- newString: {
612
- type: 'string',
613
- description: 'The replacement content.',
614
- },
615
- replaceAll: {
616
- type: 'boolean',
617
- description: 'If true, replace every occurrence. Default false.',
618
- },
619
- expectUnique: {
620
- type: 'boolean',
621
- description: 'If true (default), the operation aborts when oldString is not unique. Set to false to allow non-unique matches with the first occurrence replaced.',
622
- },
623
- },
624
- required: ['path', 'oldString', 'newString'],
625
- },
626
- },
627
- },
628
- {
629
- type: 'function',
630
- function: {
631
- name: 'todo_read',
632
- description: 'Read the current project todo list from <cwd>/.fixo/todo_list.json. Returns a human-readable rendering of all items grouped into Open and Completed. A missing or unreadable file yields an empty list — by design.',
633
- parameters: {
634
- type: 'object',
635
- properties: {},
636
- required: [],
637
- },
638
- },
639
- },
640
- {
641
- type: 'function',
642
- function: {
643
- name: 'todo_write',
644
- description: 'Mutate the project todo list. Operations: add (content+blockedBy optional), set_status (id+status), remove (id), clear_done. Persisted atomically to <cwd>/.fixo/todo_list.json. Rejected in PLAN mode.',
645
- parameters: {
646
- type: 'object',
647
- properties: {
648
- op: {
649
- type: 'string',
650
- enum: ['add', 'set_status', 'remove', 'clear_done'],
651
- description: 'The mutation to apply.',
652
- },
653
- content: {
654
- type: 'string',
655
- description: 'Item content (op=add only).',
656
- },
657
- id: {
658
- type: 'string',
659
- description: 'Item id (op=set_status, op=remove).',
660
- },
661
- status: {
662
- type: 'string',
663
- enum: ['pending', 'in_progress', 'done', 'cancelled'],
664
- description: 'New status (op=set_status only).',
665
- },
666
- blockedBy: {
667
- type: 'string',
668
- description: 'Optional blocker description (op=add only).',
669
- },
670
- },
671
- required: ['op'],
672
- },
673
- },
674
- },
675
- {
676
- type: 'function',
677
- function: {
678
- name: 'glob_files',
679
- description: 'High-performance filesystem pattern matcher. Returns paths matching the given glob pattern, relative to the workspace root. Built on Node 22+ native fs.promises.glob. By default, common build/VCS directories (node_modules, .git, dist, .fixo, .fixocli) are excluded. Symlinks are not followed and hidden files are excluded unless explicitly enabled. Capped at maxResults (default 1000, hard cap 5000).',
680
- parameters: {
681
- type: 'object',
682
- properties: {
683
- pattern: {
684
- type: 'string',
685
- description: 'Glob pattern, e.g. "src/**/*.ts" or "**/package.json".',
686
- },
687
- cwd: {
688
- type: 'string',
689
- description: 'Optional directory to scope the glob. Must resolve inside the workspace. Defaults to the workspace root.',
690
- },
691
- ignore: {
692
- type: 'string',
693
- description: 'Optional extra glob pattern (or comma-separated patterns) to add to the default skip set.',
694
- },
695
- maxResults: {
696
- type: 'integer',
697
- description: 'Maximum number of results to return. Default 1000, hard cap 5000.',
698
- },
699
- includeHidden: {
700
- type: 'boolean',
701
- description: 'If true, do not exclude dotfile entries from the match. Default false.',
702
- },
703
- followSymlinks: {
704
- type: 'boolean',
705
- description: 'If true, follow symbolic links during traversal. Default false (safer).',
706
- },
707
- },
708
- required: ['pattern'],
709
- },
710
- },
711
- },
712
- ];
713
201
  /* ──────────────────────── Per-process Run ID (Pillar 2) ──────────── */
714
202
  let cachedRunId = null;
715
203
  /**
@@ -720,8 +208,10 @@ let cachedRunId = null;
720
208
  export function getOrCreateRunId() {
721
209
  if (cachedRunId)
722
210
  return cachedRunId;
723
- cachedRunId = Math.random().toString(36).slice(2, 8) +
724
- Date.now().toString(36).slice(-6);
211
+ // Use crypto.randomBytes for collision-free staging namespace IDs.
212
+ // Math.random() has a 1-in-2^30 collision chance; crypto.randomBytes
213
+ // is cryptographically secure and eliminates any collision risk.
214
+ cachedRunId = randomBytes(6).toString('hex') + Date.now().toString(36).slice(-6);
725
215
  return cachedRunId;
726
216
  }
727
217
  /** Test/utility hook — reset the cached run id. */
@@ -902,6 +392,20 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
902
392
  };
903
393
  try {
904
394
  const policy = options.policy ?? options.session?.policy ?? 'shell-confirm';
395
+ // Auto-init git repo on first mutating tool call (Finding C)
396
+ if (MUTATION_TOOL_NAMES.has(name)) {
397
+ const git = new GitManager(cwd);
398
+ if (!git.isGitRepo()) {
399
+ try {
400
+ spawnSync('git', ['init'], { cwd, encoding: 'utf-8', stdio: 'ignore' });
401
+ spawnSync('git', ['add', '.'], { cwd, encoding: 'utf-8', stdio: 'ignore' });
402
+ spawnSync('git', ['commit', '-m', 'chore: initial checkpoint by fixo'], { cwd, encoding: 'utf-8', stdio: 'ignore' });
403
+ }
404
+ catch (e) {
405
+ // Ignore if git fails (e.g. no user.name, empty directory, or git not installed)
406
+ }
407
+ }
408
+ }
905
409
  // ──── PreToolUse hooks (§3.4) ────
906
410
  // Run any user-defined pre-tool hooks. A `deny` decision
907
411
  // short-circuits the call; a `modify` decision replaces
@@ -1083,19 +587,12 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
1083
587
  }
1084
588
  catch (err) {
1085
589
  if (verbose) {
1086
- console.error('Failed to run AST command safety check, falling back to regex safety check:', err.message);
1087
- }
1088
- const trimmed = args.command.trim();
1089
- const { DANGEROUS_COMMANDS } = await import('../runtime/policy.js');
1090
- for (const pattern of DANGEROUS_COMMANDS) {
1091
- if (pattern.test(trimmed)) {
1092
- safetyResult = {
1093
- safe: false,
1094
- reason: `Regex security match: Command contains potentially unsafe pattern/metacharacter: ${pattern.toString()}`
1095
- };
1096
- break;
1097
- }
590
+ console.error('Failed to run AST command safety check, failing closed for security:', err.message);
1098
591
  }
592
+ safetyResult = {
593
+ safe: false,
594
+ reason: `Error: AST command safety check failed and regex fallback is disabled for security reasons: ${err.message}`
595
+ };
1099
596
  }
1100
597
  if (!safetyResult.safe) {
1101
598
  const allowed = await askUnsafeCommandPermission(args.command, safetyResult.reason, options.allowWithoutPrompt);
@@ -1104,7 +601,7 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
1104
601
  break;
1105
602
  }
1106
603
  }
1107
- event.result = executeRunCommand(args.command, args.cwd || cwd, cwd, options.session);
604
+ event.result = executeRunCommand(args.command, args.cwd || cwd, cwd, options.session, options.safety?.sandboxMode);
1108
605
  break;
1109
606
  case 'search_code':
1110
607
  setSpinner({ kind: 'search', name: 'Search', detail: `"${truncate(args.query, 40)}" in ${args.path ?? '.'}` });
@@ -1294,6 +791,16 @@ function executeReadFile(filePath, cwd, session, largeFileGateBytes = 15 * 1024,
1294
791
  if (!fs.existsSync(resolved)) {
1295
792
  return `Error: File not found: ${filePath}`;
1296
793
  }
794
+ const baseName = path.basename(resolved).toLowerCase();
795
+ const lowerPath = resolved.toLowerCase();
796
+ if (baseName === '.env' ||
797
+ baseName.startsWith('.env.') ||
798
+ baseName === 'id_rsa' ||
799
+ baseName === 'providers.json' ||
800
+ lowerPath.includes('/.ssh/') ||
801
+ lowerPath.endsWith('.pem')) {
802
+ return `Error: Access to sensitive file "${filePath}" is blocked for security reasons.`;
803
+ }
1297
804
  const stat = fs.statSync(resolved);
1298
805
  if (stat.isDirectory()) {
1299
806
  return `Error: "${filePath}" is a directory, not a file. Use list_dir instead.`;
@@ -1328,7 +835,7 @@ function executeReadFile(filePath, cwd, session, largeFileGateBytes = 15 * 1024,
1328
835
  */
1329
836
  function countLines(filePath) {
1330
837
  let count = 0;
1331
- let sawNewline = true;
838
+ let lastCharWasNewline = true;
1332
839
  const stream = fs.openSync(filePath, 'r');
1333
840
  try {
1334
841
  const buf = Buffer.allocUnsafe(64 * 1024);
@@ -1337,14 +844,14 @@ function countLines(filePath) {
1337
844
  for (let i = 0; i < bytesRead; i++) {
1338
845
  if (buf[i] === 0x0a) {
1339
846
  count++;
1340
- sawNewline = true;
847
+ lastCharWasNewline = true;
1341
848
  }
1342
849
  else {
1343
- sawNewline = false;
850
+ lastCharWasNewline = false;
1344
851
  }
1345
852
  }
1346
853
  }
1347
- if (!sawNewline)
854
+ if (!lastCharWasNewline)
1348
855
  count++;
1349
856
  }
1350
857
  finally {
@@ -1419,6 +926,16 @@ async function executeExtractImports(filePath, cwd, session) {
1419
926
  function executeWriteFile(filePath, content, cwd, options = {}) {
1420
927
  const guard = new WorkspaceGuard(cwd);
1421
928
  const resolved = guard.resolve(filePath, 'file');
929
+ const baseName = path.basename(resolved).toLowerCase();
930
+ const lowerPath = resolved.toLowerCase();
931
+ if (baseName === '.env' ||
932
+ baseName.startsWith('.env.') ||
933
+ baseName === 'id_rsa' ||
934
+ baseName === 'providers.json' ||
935
+ lowerPath.includes('/.ssh/') ||
936
+ lowerPath.endsWith('.pem')) {
937
+ return Promise.resolve(`Error: Access to sensitive file "${filePath}" is blocked for security reasons.`);
938
+ }
1422
939
  // Pillar 5 / Protection 1 — refuse to mutate the platform's
1423
940
  // own runtime. This is the guard that prevents an autonomous
1424
941
  // agent from corrupting `src/agent/tool-executor.ts` and
@@ -1466,27 +983,53 @@ function executeWriteFile(filePath, content, cwd, options = {}) {
1466
983
  return `File updated: ${filePath}`;
1467
984
  });
1468
985
  }
1469
- function executeRunCommand(command, requestedCwd, workspaceRoot, session) {
986
+ function executeRunCommand(command, requestedCwd, workspaceRoot, session, sandboxMode) {
1470
987
  const guard = new WorkspaceGuard(workspaceRoot);
1471
988
  const commandCwd = guard.resolve(requestedCwd, 'command cwd');
1472
989
  try {
1473
- const result = spawnSync(command, {
1474
- shell: true,
1475
- cwd: commandCwd,
1476
- encoding: 'utf-8',
1477
- timeout: 60_000, // 60 second timeout
1478
- maxBuffer: 1024 * 1024, // 1MB max output
1479
- env: redactedEnv(),
1480
- });
990
+ let result;
991
+ if (sandboxMode === 'os-sandbox') {
992
+ // Opt-in OS-level sandbox. The regex command-parser layer that
993
+ // ran upstream is left in place — this is defence in depth.
994
+ // If the platform binary is missing we surface a structured
995
+ // error instead of silently downgrading to unsandboxed exec.
996
+ try {
997
+ result = runSandboxed(command, {
998
+ cwd: commandCwd,
999
+ allowedWritePaths: [workspaceRoot],
1000
+ allowNetwork: true,
1001
+ timeout: 60_000,
1002
+ maxBuffer: 1024 * 1024,
1003
+ env: redactedEnv(),
1004
+ });
1005
+ }
1006
+ catch (sandboxErr) {
1007
+ if (sandboxErr instanceof SandboxUnavailableError) {
1008
+ return `Error: OS sandbox mode is enabled but cannot be applied — ${sandboxErr.message}. Either install the platform binary or set preferences.safety.sandboxMode to 'guard'.`;
1009
+ }
1010
+ throw sandboxErr;
1011
+ }
1012
+ }
1013
+ else {
1014
+ result = spawnSync(command, {
1015
+ shell: true,
1016
+ cwd: commandCwd,
1017
+ encoding: 'utf-8',
1018
+ timeout: 60_000, // 60 second timeout
1019
+ maxBuffer: 1024 * 1024, // 1MB max output
1020
+ env: redactedEnv(),
1021
+ });
1022
+ }
1481
1023
  const output = redactSecrets([result.stdout ?? '', result.stderr ?? ''].filter(Boolean).join('\n'));
1482
1024
  const status = result.status ?? 0;
1483
1025
  session?.record('command_finished', { command, cwd: guard.relative(commandCwd), status, output: truncate(output, 4000) });
1484
1026
  return output || `(command completed with code ${status})`;
1485
1027
  }
1486
1028
  catch (error) {
1487
- const stdout = error.stdout ?? '';
1488
- const stderr = error.stderr ?? '';
1489
- const code = error.status ?? 'unknown';
1029
+ const err = error;
1030
+ const stdout = err.stdout ?? '';
1031
+ const stderr = err.stderr ?? '';
1032
+ const code = err.status ?? 'unknown';
1490
1033
  return redactSecrets(`Command exited with code ${code}\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`.trim());
1491
1034
  }
1492
1035
  }
@@ -1503,7 +1046,8 @@ function executeSearchCode(query, searchPath, filePattern, cwd) {
1503
1046
  }
1504
1047
  catch (error) {
1505
1048
  if (process.env.DEBUG || process.env.VERBOSE || process.argv.includes('--verbose')) {
1506
- console.warn(`[Debug Warning] Failed to determine if ripgrep (rg) is installed: ${error.message || error}`);
1049
+ const msg = error instanceof Error ? error.message : String(error);
1050
+ console.warn(`[Debug Warning] Failed to determine if ripgrep (rg) is installed: ${msg}`);
1507
1051
  }
1508
1052
  }
1509
1053
  let output = '';
@@ -1560,7 +1104,8 @@ function executeListDir(dirPath, cwd) {
1560
1104
  }
1561
1105
  let entries;
1562
1106
  try {
1563
- entries = fs.readdirSync(resolved, { withFileTypes: true });
1107
+ const inv = getRunInventory(getOrCreateRunId());
1108
+ entries = inv.listDir(resolved);
1564
1109
  }
1565
1110
  catch (error) {
1566
1111
  return `Error: Cannot read directory: ${error instanceof Error ? error.message : String(error)}`;
@@ -1575,6 +1120,7 @@ function executeListDir(dirPath, cwd) {
1575
1120
  return a.name.localeCompare(b.name);
1576
1121
  });
1577
1122
  const lines = [];
1123
+ const inv = getRunInventory(getOrCreateRunId());
1578
1124
  for (const entry of filtered) {
1579
1125
  if (entry.isDirectory()) {
1580
1126
  lines.push(`📁 ${entry.name}/`);
@@ -1582,7 +1128,7 @@ function executeListDir(dirPath, cwd) {
1582
1128
  else {
1583
1129
  let size = '';
1584
1130
  try {
1585
- const s = fs.statSync(path.join(resolved, entry.name));
1131
+ const s = inv.fileStats(path.join(resolved, entry.name));
1586
1132
  size = formatSize(s.size);
1587
1133
  }
1588
1134
  catch {
@@ -1898,7 +1444,7 @@ export async function executeTodoWrite(args, cwd, options = {}) {
1898
1444
  * dispatch is hot-path-friendly and test-friendly (tests inject
1899
1445
  * a custom registry via {@link setBackgroundJobRegistry}).
1900
1446
  */
1901
- let globalBackgroundRegistry = null;
1447
+ const globalBackgroundRegistry = null;
1902
1448
  const backgroundRegistries = new Map();
1903
1449
  export function getBackgroundJobRegistry(cwd) {
1904
1450
  let reg = backgroundRegistries.get(cwd);