@warlock.js/ai-workspace 5.43.0 → 5.44.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 (84) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +14 -1
  3. package/cjs/index.cjs +1781 -141
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts.map +1 -1
  6. package/esm/backends/local.mjs +36 -22
  7. package/esm/backends/local.mjs.map +1 -1
  8. package/esm/contracts/index.d.mts +5 -1
  9. package/esm/contracts/shell-approval.type.d.mts +78 -0
  10. package/esm/contracts/shell-approval.type.d.mts.map +1 -0
  11. package/esm/contracts/tool-io.type.d.mts +35 -1
  12. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  13. package/esm/contracts/workspace-backend.contract.d.mts +11 -1
  14. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  15. package/esm/contracts/workspace-edit.type.d.mts +66 -0
  16. package/esm/contracts/workspace-edit.type.d.mts.map +1 -0
  17. package/esm/contracts/workspace-ops.contract.d.mts +7 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  19. package/esm/contracts/workspace-output.type.d.mts +57 -0
  20. package/esm/contracts/workspace-output.type.d.mts.map +1 -0
  21. package/esm/contracts/workspace-policy.type.d.mts +38 -1
  22. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  23. package/esm/contracts/workspace.contract.d.mts +11 -0
  24. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  25. package/esm/edit/apply-replacement.mjs +58 -0
  26. package/esm/edit/apply-replacement.mjs.map +1 -0
  27. package/esm/edit/match.d.mts +16 -0
  28. package/esm/edit/match.d.mts.map +1 -0
  29. package/esm/edit/match.mjs +214 -0
  30. package/esm/edit/match.mjs.map +1 -0
  31. package/esm/errors.d.mts +12 -1
  32. package/esm/errors.d.mts.map +1 -1
  33. package/esm/errors.mjs +1 -0
  34. package/esm/errors.mjs.map +1 -1
  35. package/esm/hooks/after-edit.mjs +36 -0
  36. package/esm/hooks/after-edit.mjs.map +1 -0
  37. package/esm/index.d.mts +7 -2
  38. package/esm/index.mjs +3 -2
  39. package/esm/ops.d.mts +3 -1
  40. package/esm/ops.d.mts.map +1 -1
  41. package/esm/ops.mjs +196 -75
  42. package/esm/ops.mjs.map +1 -1
  43. package/esm/output/spill-dir.mjs +15 -0
  44. package/esm/output/spill-dir.mjs.map +1 -0
  45. package/esm/output/spill.mjs +220 -0
  46. package/esm/output/spill.mjs.map +1 -0
  47. package/esm/policy/policy.d.mts +3 -1
  48. package/esm/policy/policy.d.mts.map +1 -1
  49. package/esm/policy/policy.mjs +12 -4
  50. package/esm/policy/policy.mjs.map +1 -1
  51. package/esm/policy/powershell-aliases.mjs +275 -0
  52. package/esm/policy/powershell-aliases.mjs.map +1 -0
  53. package/esm/policy/powershell-parser.mjs +418 -0
  54. package/esm/policy/powershell-parser.mjs.map +1 -0
  55. package/esm/policy/shell-check.d.mts +32 -2
  56. package/esm/policy/shell-check.d.mts.map +1 -1
  57. package/esm/policy/shell-check.mjs +183 -31
  58. package/esm/policy/shell-check.mjs.map +1 -1
  59. package/esm/policy/shell-interpreter.d.mts +45 -0
  60. package/esm/policy/shell-interpreter.d.mts.map +1 -0
  61. package/esm/policy/shell-interpreter.mjs +109 -0
  62. package/esm/policy/shell-interpreter.mjs.map +1 -0
  63. package/esm/policy/shell-parser.d.mts +31 -0
  64. package/esm/policy/shell-parser.d.mts.map +1 -0
  65. package/esm/policy/shell-parser.mjs +3 -0
  66. package/esm/policy/shell-parser.mjs.map +1 -1
  67. package/esm/tools/glob.mjs +1 -0
  68. package/esm/tools/glob.mjs.map +1 -1
  69. package/esm/tools/run-shell.d.mts +10 -0
  70. package/esm/tools/run-shell.d.mts.map +1 -1
  71. package/esm/tools/run-shell.mjs +18 -1
  72. package/esm/tools/run-shell.mjs.map +1 -1
  73. package/esm/tools/run-tests.d.mts +7 -0
  74. package/esm/tools/run-tests.d.mts.map +1 -1
  75. package/esm/tools/run-tests.mjs +5 -4
  76. package/esm/tools/run-tests.mjs.map +1 -1
  77. package/esm/tools/write-file.mjs +3 -2
  78. package/esm/tools/write-file.mjs.map +1 -1
  79. package/esm/workspace.d.mts.map +1 -1
  80. package/esm/workspace.mjs +16 -5
  81. package/esm/workspace.mjs.map +1 -1
  82. package/llms-full.txt +64 -17
  83. package/package.json +3 -3
  84. package/skills/use-a-workspace/SKILL.md +64 -17
package/esm/index.d.mts CHANGED
@@ -1,4 +1,8 @@
1
+ import { WorkspaceEditMatchTier, WorkspaceEditMatching } from "./edit/match.mjs";
2
+ import { WorkspaceOutputKeep, WorkspaceOutputPolicy, WorkspaceTruncation } from "./contracts/workspace-output.type.mjs";
1
3
  import { ApplyPatchFileResult, ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./contracts/tool-io.type.mjs";
4
+ import { WorkspaceCommandDecision, WorkspaceCommandRule, WorkspaceShellAsk, WorkspaceShellAskDecision, WorkspaceShellAskReason, WorkspaceShellAskRequest, WorkspaceShellClassification, WorkspaceShellOnAsk } from "./contracts/shell-approval.type.mjs";
5
+ import { WorkspaceAfterEditEvent, WorkspaceEditLineEndings, WorkspaceEditPolicy, WorkspaceEditTool, WorkspaceHooks } from "./contracts/workspace-edit.type.mjs";
2
6
  import { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy } from "./contracts/workspace-policy.type.mjs";
3
7
  import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceOutputStream } from "./contracts/workspace-backend.contract.mjs";
4
8
  import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
@@ -6,7 +10,8 @@ import { Workspace, WorkspaceTools } from "./contracts/workspace.contract.mjs";
6
10
  import "./contracts/index.mjs";
7
11
  import { WorkspaceEditError, WorkspaceEditErrorOptions, WorkspaceEditFailure, WorkspaceJobError, WorkspaceJobErrorOptions, WorkspaceJobFailure, WorkspacePolicyError, WorkspacePolicyErrorOptions, WorkspacePolicyViolation, WorkspaceShellRefusalReason } from "./errors.mjs";
8
12
  import { ResolvedPath, buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
9
- import { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand } from "./policy/shell-check.mjs";
13
+ import { ResolvedShell, ResolvedShellInterpreter, ShellHost, resolveShell } from "./policy/shell-interpreter.mjs";
14
+ import { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand, classifyShellCommand } from "./policy/shell-check.mjs";
10
15
  import { ArgvAnalysis, analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
11
16
  import { applyPatchWithOps } from "./patch/apply-patch-generic.mjs";
12
17
  import { FilePatch, FilePatchKind, PatchHunk, PatchLine, parsePatch } from "./patch/parse-patch.mjs";
@@ -29,5 +34,5 @@ import { TodoStore } from "./todos.mjs";
29
34
  import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
30
35
  import { makeWriteFileTool } from "./tools/write-file.mjs";
31
36
  import { workspace } from "./workspace.mjs";
32
- export { type ApplyPatchFileResult, type ApplyPatchInput, type ApplyPatchResult, type ArgvAnalysis, type EditFileInput, type EditFileResult, type ExecOptions, type FilePatch, type FilePatchKind, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type ListDirEntry, type ListDirEntryType, type ListDirInput, type ListDirOptions, type ListDirResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type MultiEditInput, type MultiEditOperation, type MultiEditResult, type PatchHunk, type PatchLine, type ReadFileInput, type ReadFileResult, type ResolvedPath, type RunShellBackgroundResult, type RunShellInput, type RunShellResult, type RunTestsInput, type ShellJobInfo, type ShellJobStatus, type ShellKillInput, type ShellOutputInput, type ShellOutputOptions, type ShellOutputResult, type StartShellOptions, type TodoListResult, TodoStore, type TodoWriteInput, type Workspace, type WorkspaceAllowCommand, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendProcess, type WorkspaceBackendStartOptions, type WorkspaceBackendStat, type WorkspaceBackendType, type WorkspaceBackgroundPolicy, type WorkspaceCommandCheckContext, type WorkspaceCommandPattern, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, WorkspaceJobError, type WorkspaceJobErrorOptions, type WorkspaceJobFailure, type WorkspaceListPolicy, type WorkspaceOps, type WorkspaceOutputStream, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellCheck, type WorkspaceShellInterpreter, type WorkspaceShellPath, type WorkspaceShellPathAccess, type WorkspaceShellPolicy, type WorkspaceShellRefusalReason, type WorkspaceTodo, type WorkspaceTodoInput, type WorkspaceTodoStatus, type WorkspaceToolName, type WorkspaceTools, type WriteFileInput, type WriteFileResult, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, tokenizeCommand, workspace };
37
+ export { type ApplyPatchFileResult, type ApplyPatchInput, type ApplyPatchResult, type ArgvAnalysis, type EditFileInput, type EditFileResult, type ExecOptions, type FilePatch, type FilePatchKind, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type ListDirEntry, type ListDirEntryType, type ListDirInput, type ListDirOptions, type ListDirResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type MultiEditInput, type MultiEditOperation, type MultiEditResult, type PatchHunk, type PatchLine, type ReadFileInput, type ReadFileResult, type ResolvedPath, type ResolvedShell, type ResolvedShellInterpreter, type RunShellBackgroundResult, type RunShellInput, type RunShellResult, type RunTestsInput, type ShellHost, type ShellJobInfo, type ShellJobStatus, type ShellKillInput, type ShellOutputInput, type ShellOutputOptions, type ShellOutputResult, type StartShellOptions, type TodoListResult, TodoStore, type TodoWriteInput, type Workspace, type WorkspaceAfterEditEvent, type WorkspaceAllowCommand, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendProcess, type WorkspaceBackendStartOptions, type WorkspaceBackendStat, type WorkspaceBackendType, type WorkspaceBackgroundPolicy, type WorkspaceCommandCheckContext, type WorkspaceCommandDecision, type WorkspaceCommandPattern, type WorkspaceCommandRule, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, type WorkspaceEditLineEndings, type WorkspaceEditMatchTier, type WorkspaceEditMatching, type WorkspaceEditPolicy, type WorkspaceEditTool, type WorkspaceHooks, WorkspaceJobError, type WorkspaceJobErrorOptions, type WorkspaceJobFailure, type WorkspaceListPolicy, type WorkspaceOps, type WorkspaceOutputKeep, type WorkspaceOutputPolicy, type WorkspaceOutputStream, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellAsk, type WorkspaceShellAskDecision, type WorkspaceShellAskReason, type WorkspaceShellAskRequest, type WorkspaceShellCheck, type WorkspaceShellClassification, type WorkspaceShellInterpreter, type WorkspaceShellOnAsk, type WorkspaceShellPath, type WorkspaceShellPathAccess, type WorkspaceShellPolicy, type WorkspaceShellRefusalReason, type WorkspaceTodo, type WorkspaceTodoInput, type WorkspaceTodoStatus, type WorkspaceToolName, type WorkspaceTools, type WorkspaceTruncation, type WriteFileInput, type WriteFileResult, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, classifyShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, resolveShell, tokenizeCommand, workspace };
33
38
  import "./workspace.mjs";
package/esm/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError } from "./errors.mjs";
2
+ import { resolveShell } from "./policy/shell-interpreter.mjs";
2
3
  import { analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
3
- import { checkShellCommand } from "./policy/shell-check.mjs";
4
+ import { checkShellCommand, classifyShellCommand } from "./policy/shell-check.mjs";
4
5
  import { buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
5
6
  import { applyHunks } from "./patch/apply-hunks.mjs";
6
7
  import { parsePatch } from "./patch/parse-patch.mjs";
@@ -24,4 +25,4 @@ import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
24
25
  import { makeWriteFileTool } from "./tools/write-file.mjs";
25
26
  import { workspace } from "./workspace.mjs";
26
27
 
27
- export { TodoStore, WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, tokenizeCommand, workspace };
28
+ export { TodoStore, WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, classifyShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, resolveShell, tokenizeCommand, workspace };
package/esm/ops.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
1
+ import { WorkspaceCommandPattern, WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
2
2
  import { WorkspaceBackend } from "./contracts/workspace-backend.contract.mjs";
3
3
  import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
4
4
  import "./contracts/index.mjs";
@@ -18,6 +18,8 @@ import { ShellJobs } from "./shell-jobs.mjs";
18
18
  export interface OpsRuntime {
19
19
  /** The workspace's background commands. */
20
20
  jobs: ShellJobs;
21
+ /** Patterns the host approved with `remember`, shared by every view of the workspace. */
22
+ approvals?: WorkspaceCommandPattern[];
21
23
  }
22
24
  /**
23
25
  * Create the policy-enforced operation layer over a backend.
package/esm/ops.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ops.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAiMiB;;EAEf,MAAM;;;;;;;;;;;;;;;;;;;;;wBAouBQ,UACd,SAAS,kBACT,QAAQ,iBACR,UAAU,aACT"}
1
+ {"version":3,"file":"ops.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"mappings":";;;;;;;;;;;;;;;;;iBA0MiB;;EAEf,MAAM;;EAEN,YAAY;;;;;;;;;;;;;;;;;;;;;wBA23BE,UACd,SAAS,kBACT,QAAQ,iBACR,UAAU,aACT"}
package/esm/ops.mjs CHANGED
@@ -1,8 +1,12 @@
1
1
  import { WorkspaceEditError, WorkspacePolicyError } from "./errors.mjs";
2
2
  import { globToRegExp, isInside, matchesAnyGlob } from "./policy/jail.mjs";
3
+ import { resolveShell } from "./policy/shell-interpreter.mjs";
3
4
  import { checkShellCommand } from "./policy/shell-check.mjs";
4
5
  import { buildCommandEnv, resolveInJail } from "./policy/policy.mjs";
5
6
  import { executePatch } from "./patch/execute-patch.mjs";
7
+ import { applyReplacement } from "./edit/apply-replacement.mjs";
8
+ import { runAfterEdit } from "./hooks/after-edit.mjs";
9
+ import { OutputSpiller, splitLines } from "./output/spill.mjs";
6
10
  import { ShellJobs } from "./shell-jobs.mjs";
7
11
  import path from "node:path";
8
12
  import { fs } from "@warlock.js/fs";
@@ -139,6 +143,26 @@ var Ops = class {
139
143
  this.policy = policy;
140
144
  this.runtime = runtime;
141
145
  this.pathLocks = /* @__PURE__ */ new Map();
146
+ this.resolvedShell = resolveShell(policy.shell);
147
+ this.approvals = runtime.approvals ?? [];
148
+ this.spiller = new OutputSpiller(backend, policy);
149
+ }
150
+ /** The edit-policy settings applied to every replacement. */
151
+ editOptions() {
152
+ const edit = this.policy.edit;
153
+ return {
154
+ matching: edit?.matching ?? "tiered",
155
+ missHintLines: edit?.missHintLines ?? 12,
156
+ lineEndings: edit?.lineEndings ?? "preserve",
157
+ ...this.policy.read?.maxBytes !== void 0 ? { hintMaxBytes: this.policy.read.maxBytes } : {}
158
+ };
159
+ }
160
+ /** Run `afterEdit` for a call that wrote `paths`; `undefined` when there is no output. */
161
+ diagnosticsFor(tool, paths) {
162
+ return runAfterEdit(this.policy, {
163
+ paths,
164
+ tool
165
+ });
142
166
  }
143
167
  /** Run `task` after every earlier mutation of `absolutePath` has settled. */
144
168
  async withPathLock(absolutePath, task) {
@@ -183,7 +207,7 @@ var Ops = class {
183
207
  }
184
208
  }
185
209
  async readFile(inputPath, opts) {
186
- const { absolutePath } = await resolveInJail(this.policy, inputPath);
210
+ const { absolutePath } = await resolveInJail(this.policy, inputPath, "read");
187
211
  const raw = await this.backend.readFile(absolutePath);
188
212
  const hash = fs.hash.string(raw);
189
213
  const lines = raw.split("\n");
@@ -192,10 +216,29 @@ var Ops = class {
192
216
  const limit = opts?.limit ?? this.policy.read?.defaultLines ?? DEFAULT_READ_LINES;
193
217
  const maxBytes = this.policy.read?.maxBytes;
194
218
  const startIndex = offset - 1;
195
- const { content, returnedLines, bytesCapped } = numberWindow(lines.slice(startIndex, startIndex + Math.max(0, limit)), offset, maxBytes === void 0 ? void 0 : Math.max(0, Math.floor(maxBytes)));
219
+ const window = lines.slice(startIndex, startIndex + Math.max(0, limit));
220
+ const { content, returnedLines, bytesCapped } = numberWindow(window, offset, maxBytes === void 0 ? void 0 : Math.max(0, Math.floor(maxBytes)));
196
221
  const endLine = offset + returnedLines - 1;
222
+ const spillDir = this.spiller.spillDir;
223
+ let spilled;
224
+ if (bytesCapped && spillDir !== void 0 && !isInside(absolutePath, spillDir)) {
225
+ const full = numberWindow(window, offset, void 0).content;
226
+ const file = await this.spiller.save("read_file", full);
227
+ if (file !== void 0) spilled = {
228
+ content: `${content}\n[showing lines 1-${returnedLines} of ${window.length}; full output saved to ${file}]`,
229
+ truncation: {
230
+ shown: { lines: returnedLines },
231
+ total: {
232
+ lines: window.length,
233
+ bytes: Buffer.byteLength(full, "utf8")
234
+ },
235
+ path: file
236
+ }
237
+ };
238
+ }
197
239
  return {
198
- content,
240
+ content: spilled?.content ?? content,
241
+ ...spilled !== void 0 ? { truncation: spilled.truncation } : {},
199
242
  startLine: offset,
200
243
  endLine,
201
244
  totalLines,
@@ -206,8 +249,8 @@ var Ops = class {
206
249
  };
207
250
  }
208
251
  async writeFile(inputPath, content) {
209
- const { absolutePath } = await resolveInJail(this.policy, inputPath);
210
- return this.withPathLock(absolutePath, async () => {
252
+ const { absolutePath, relativePath } = await resolveInJail(this.policy, inputPath);
253
+ const written = await this.withPathLock(absolutePath, async () => {
211
254
  const parent = path.dirname(absolutePath);
212
255
  await this.backend.mkdir(parent);
213
256
  await this.backend.writeFile(absolutePath, content);
@@ -216,10 +259,20 @@ var Ops = class {
216
259
  bytesWritten: Buffer.byteLength(content, "utf8")
217
260
  };
218
261
  });
262
+ const diagnostics = await this.diagnosticsFor("write_file", [relativePath || inputPath]);
263
+ return {
264
+ ...written,
265
+ ...diagnostics !== void 0 ? { diagnostics } : {}
266
+ };
219
267
  }
220
268
  async editFile(input) {
221
269
  const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);
222
- return this.withPathLock(absolutePath, () => this.applyEdit(input, absolutePath, relativePath));
270
+ const result = await this.withPathLock(absolutePath, () => this.applyEdit(input, absolutePath, relativePath));
271
+ const diagnostics = await this.diagnosticsFor("edit_file", [result.path]);
272
+ return diagnostics === void 0 ? result : {
273
+ ...result,
274
+ diagnostics
275
+ };
223
276
  }
224
277
  /** The read-check-replace-write of {@link editFile}, run under its path lock. */
225
278
  async applyEdit(input, absolutePath, relativePath) {
@@ -231,19 +284,20 @@ var Ops = class {
231
284
  expectedHash: input.expectHash,
232
285
  actualHash: currentHash
233
286
  });
234
- const { content: updated, replacements } = applyReplacement(current, input, input.path, relativePath || input.path);
287
+ const { content: updated, replacements, matchTier } = applyReplacement(current, input, input.path, relativePath || input.path, this.editOptions());
235
288
  await this.backend.writeFile(absolutePath, updated);
236
289
  return {
237
290
  path: relativePath || input.path,
238
291
  replacements,
239
- hash: fs.hash.string(updated)
292
+ hash: fs.hash.string(updated),
293
+ ...matchTier !== void 0 ? { matchTier } : {}
240
294
  };
241
295
  }
242
296
  async multiEdit(input) {
243
297
  const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);
244
298
  const shownPath = relativePath || input.path;
245
299
  if (input.edits.length === 0 || input.edits.length > 100) throw new RangeError(`A multi-edit needs between 1 and ${100} edits (got ${input.edits.length}).`);
246
- return this.withPathLock(absolutePath, async () => {
300
+ const result = await this.withPathLock(absolutePath, async () => {
247
301
  const current = await this.backend.readFile(absolutePath);
248
302
  const currentHash = fs.hash.string(current);
249
303
  if (input.expectHash !== void 0 && input.expectHash !== currentHash) throw new WorkspaceEditError(`File "${input.path}" changed since it was read; none of the edits were applied.`, {
@@ -254,21 +308,40 @@ var Ops = class {
254
308
  });
255
309
  let content = current;
256
310
  let replacements = 0;
311
+ let matchTier;
312
+ const options = this.editOptions();
257
313
  input.edits.forEach((edit, editIndex) => {
258
- const applied = applyReplacement(content, edit, input.path, shownPath, editIndex);
314
+ const applied = applyReplacement(content, edit, input.path, shownPath, options, editIndex);
259
315
  content = applied.content;
260
316
  replacements += applied.replacements;
317
+ if (applied.matchTier === "whitespace" || matchTier === void 0) matchTier = applied.matchTier ?? matchTier;
261
318
  });
262
319
  await this.backend.writeFile(absolutePath, content);
263
320
  return {
264
321
  path: shownPath,
265
322
  edits: input.edits.length,
266
323
  replacements,
267
- hash: fs.hash.string(content)
324
+ hash: fs.hash.string(content),
325
+ ...matchTier !== void 0 ? { matchTier } : {}
268
326
  };
269
327
  });
328
+ const diagnostics = await this.diagnosticsFor("multi_edit", [shownPath]);
329
+ return diagnostics === void 0 ? result : {
330
+ ...result,
331
+ diagnostics
332
+ };
270
333
  }
271
334
  async applyPatch(input) {
335
+ const result = await this.runPatch(input);
336
+ if (result.dryRun) return result;
337
+ const paths = result.files.filter((file) => file.action !== "deleted").map((file) => file.path);
338
+ const diagnostics = await this.diagnosticsFor("apply_patch", paths);
339
+ return diagnostics === void 0 ? result : {
340
+ ...result,
341
+ diagnostics
342
+ };
343
+ }
344
+ runPatch(input) {
272
345
  return executePatch(input, {
273
346
  resolve: (inputPath) => resolveInJail(this.policy, inputPath),
274
347
  exists: (absolutePath) => this.backend.exists(absolutePath),
@@ -288,9 +361,14 @@ var Ops = class {
288
361
  * (argv or shell mode), then — shell mode — every redirection target and
289
362
  * `cd` directory against the jail with symlinks resolved.
290
363
  */
291
- async assertCommandAllowed(command) {
292
- const verdict = checkShellCommand(this.policy, command);
293
- if (!verdict.allowed) throw new WorkspacePolicyError(verdict.message ?? `Command is not permitted by the workspace shell policy: ${command}`, {
364
+ async assertCommandAllowed(command, signal) {
365
+ const verdict = checkShellCommand(this.policy, command, {
366
+ remembered: this.approvals,
367
+ resolved: this.resolvedShell
368
+ });
369
+ const onAsk = this.policy.shell?.onAsk;
370
+ const needsApproval = !verdict.allowed && verdict.reason === "approval-required" && verdict.asks.length > 0;
371
+ if (!verdict.allowed && !(needsApproval && onAsk !== void 0)) throw new WorkspacePolicyError(verdict.message ?? `Command is not permitted by the workspace shell policy: ${command}`, {
294
372
  type: "denied-command",
295
373
  command,
296
374
  ...verdict.reason !== void 0 ? { reason: verdict.reason } : {},
@@ -313,9 +391,29 @@ var Ops = class {
313
391
  reason: "not-allowed",
314
392
  command
315
393
  });
394
+ if (needsApproval && onAsk !== void 0) await this.askForApproval(command, verdict.asks, onAsk, signal);
395
+ }
396
+ /** Put the commands that need approval to `shell.onAsk`; refuse the line unless it says yes. */
397
+ async askForApproval(command, asks, onAsk, signal) {
398
+ let decision;
399
+ try {
400
+ decision = await onAsk({
401
+ command,
402
+ asks,
403
+ ...signal ? { signal } : {}
404
+ });
405
+ } catch {
406
+ decision = void 0;
407
+ }
408
+ if (decision?.allow !== true) throw new WorkspacePolicyError(`Running ${asks.map((ask) => `\`${ask.argv.join(" ")}\``).join(", ")} was not approved.`, {
409
+ type: "denied-command",
410
+ reason: "approval-denied",
411
+ command
412
+ });
413
+ if (decision.remember !== void 0 && !this.approvals.includes(decision.remember)) this.approvals.push(decision.remember);
316
414
  }
317
415
  async exec(command, opts) {
318
- await this.assertCommandAllowed(command);
416
+ await this.assertCommandAllowed(command, opts?.signal);
319
417
  const signal = opts?.signal;
320
418
  if (signal?.aborted) return {
321
419
  exitCode: CANCELLED_BEFORE_START_EXIT_CODE,
@@ -328,24 +426,27 @@ var Ops = class {
328
426
  const shell = this.policy.shell;
329
427
  const timeoutMs = opts?.timeoutMs ?? shell?.timeoutMs;
330
428
  const maxOutputBytes = shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
331
- const interpreter = shell?.interpreter ?? "none";
429
+ const interpreter = this.resolvedShell.interpreter;
332
430
  const result = await this.backend.exec(command, {
333
431
  cwd: this.policy.cwd,
334
432
  timeoutMs,
335
433
  env: buildCommandEnv(this.policy),
336
434
  ...signal ? { signal } : {},
337
- ...interpreter !== "none" ? { interpreter } : {}
435
+ ...interpreter !== "none" ? { interpreter } : {},
436
+ ...interpreter !== "none" && this.resolvedShell.file !== void 0 ? { interpreterPath: this.resolvedShell.file } : {}
338
437
  });
339
- const stdout = capOutput(result.stdout, maxOutputBytes);
340
- const stderr = capOutput(result.stderr, maxOutputBytes);
438
+ const stdout = await this.fitStream("run_shell-stdout", result.stdout, maxOutputBytes);
439
+ const stderr = await this.fitStream("run_shell-stderr", result.stderr, maxOutputBytes);
341
440
  const cancelled = result.cancelled === true;
441
+ const truncation = streamTruncation(stdout.truncation, stderr.truncation);
342
442
  return {
343
443
  exitCode: result.exitCode,
344
444
  stdout: stdout.value,
345
445
  stderr: stderr.value,
346
446
  truncated: stdout.truncated || stderr.truncated,
347
447
  timedOut: result.timedOut && !cancelled,
348
- cancelled
448
+ cancelled,
449
+ ...truncation !== void 0 ? { truncation } : {}
349
450
  };
350
451
  }
351
452
  async startShell(command, opts) {
@@ -363,7 +464,8 @@ var Ops = class {
363
464
  command
364
465
  });
365
466
  const shell = this.policy.shell;
366
- const interpreter = shell?.interpreter ?? "none";
467
+ const interpreter = this.resolvedShell.interpreter;
468
+ const interpreterPath = interpreter !== "none" ? this.resolvedShell.file : void 0;
367
469
  const timeoutMs = lowerOf(background.timeoutMs, opts?.timeoutMs);
368
470
  const env = buildCommandEnv(this.policy);
369
471
  return this.runtime.jobs.start({
@@ -375,12 +477,51 @@ var Ops = class {
375
477
  cwd: this.policy.cwd,
376
478
  env,
377
479
  ...interpreter !== "none" ? { interpreter } : {},
480
+ ...interpreterPath !== void 0 ? { interpreterPath } : {},
378
481
  onOutput
379
482
  })
380
483
  });
381
484
  }
382
- shellOutput(id, opts) {
383
- return this.runtime.jobs.output(id, opts);
485
+ async shellOutput(id, opts) {
486
+ const result = await this.runtime.jobs.output(id, opts);
487
+ if (!this.spiller.enabled) return result;
488
+ const stdout = await this.spiller.text(`shell_output-${id}-stdout`, result.stdout, "head+tail");
489
+ const stderr = await this.spiller.text(`shell_output-${id}-stderr`, result.stderr, "head+tail");
490
+ const truncation = streamTruncation(stdout.truncation, stderr.truncation);
491
+ return truncation === void 0 ? result : {
492
+ ...result,
493
+ stdout: stdout.value,
494
+ stderr: stderr.value,
495
+ truncated: true,
496
+ truncation
497
+ };
498
+ }
499
+ /**
500
+ * Fit one captured stream to the limits: spilled to a file when
501
+ * `output.spillDir` is set, otherwise clipped at `maxBytes` as always.
502
+ */
503
+ async fitStream(label, value, maxBytes) {
504
+ if (this.spiller.enabled) {
505
+ const cap = this.policy.shell?.maxOutputBytes;
506
+ const spilled = await this.spiller.text(label, value, "head+tail", cap);
507
+ if (spilled.truncation !== void 0) return {
508
+ value: spilled.value,
509
+ truncated: true,
510
+ truncation: spilled.truncation
511
+ };
512
+ }
513
+ const capped = capOutput(value, maxBytes);
514
+ if (!capped.truncated) return capped;
515
+ return {
516
+ ...capped,
517
+ truncation: {
518
+ shown: { lines: splitLines(capped.value).length },
519
+ total: {
520
+ lines: splitLines(value).length,
521
+ bytes: Buffer.byteLength(value, "utf8")
522
+ }
523
+ }
524
+ };
384
525
  }
385
526
  shellKill(id) {
386
527
  return this.runtime.jobs.kill(id);
@@ -388,8 +529,9 @@ var Ops = class {
388
529
  listShells() {
389
530
  return this.runtime.jobs.list();
390
531
  }
391
- dispose() {
392
- return this.runtime.jobs.dispose();
532
+ async dispose() {
533
+ await this.runtime.jobs.dispose();
534
+ await this.spiller.dispose();
393
535
  }
394
536
  async listDir(inputPath = ".", opts) {
395
537
  const { absolutePath: root, relativePath: rootRelative } = await resolveInJail(this.policy, inputPath);
@@ -493,16 +635,32 @@ var Ops = class {
493
635
  line: index + 1,
494
636
  text: line
495
637
  });
496
- if (matches.length >= DEFAULT_MAX_GREP_MATCHES) return {
497
- matches,
498
- total: matches.length
499
- };
638
+ if (matches.length >= DEFAULT_MAX_GREP_MATCHES) return this.fitMatches(matches);
500
639
  }
501
640
  }
502
641
  }
503
- return {
642
+ return this.fitMatches(matches);
643
+ }
644
+ /** Apply the `output` limits to a finished match list. */
645
+ async fitMatches(matches) {
646
+ const fitted = await this.spiller.list("grep", matches, (match) => `${match.path}:${match.line}:${match.text}`, "head");
647
+ return fitted.truncation === void 0 ? {
504
648
  matches,
505
649
  total: matches.length
650
+ } : {
651
+ matches: fitted.items,
652
+ total: matches.length,
653
+ truncated: true,
654
+ truncation: fitted.truncation
655
+ };
656
+ }
657
+ async globResult(pattern) {
658
+ const all = await this.glob(pattern);
659
+ const fitted = await this.spiller.list("glob", all, (entry) => entry, "head");
660
+ return fitted.truncation === void 0 ? { paths: all } : {
661
+ paths: fitted.items,
662
+ truncated: true,
663
+ truncation: fitted.truncation
506
664
  };
507
665
  }
508
666
  async glob(pattern) {
@@ -535,57 +693,20 @@ var Ops = class {
535
693
  return denyPaths !== void 0 && denyPaths.length > 0 && matchesAnyGlob(relativePath, denyPaths);
536
694
  }
537
695
  };
696
+ /** Combine per-stream truncation metadata; `undefined` when neither stream was clipped. */
697
+ function streamTruncation(stdout, stderr) {
698
+ if (stdout === void 0 && stderr === void 0) return;
699
+ return {
700
+ ...stdout !== void 0 ? { stdout } : {},
701
+ ...stderr !== void 0 ? { stderr } : {}
702
+ };
703
+ }
538
704
  /** The lower of two optional limits (either may be absent). */
539
705
  function lowerOf(policyLimit, requested) {
540
706
  if (requested === void 0 || requested <= 0) return policyLimit;
541
707
  return policyLimit === void 0 ? requested : Math.min(policyLimit, requested);
542
708
  }
543
709
  /**
544
- * Apply one exact-string replacement to `content` under the edit rules:
545
- * `oldString` must occur, and exactly once unless `replaceAll`.
546
- */
547
- function applyReplacement(content, edit, inputPath, shownPath, editIndex) {
548
- const occurrences = countOccurrences(content, edit.oldString);
549
- const which = editIndex === void 0 ? "" : `Edit ${editIndex + 1}: `;
550
- const nothingApplied = editIndex === void 0 ? "" : " None of the edits were applied.";
551
- const indexOption = editIndex === void 0 ? {} : { editIndex };
552
- if (occurrences === 0) throw new WorkspaceEditError(`${which}The text to replace was not found in "${inputPath}".${nothingApplied}`, {
553
- type: "not-found",
554
- path: shownPath,
555
- matches: 0,
556
- ...indexOption
557
- });
558
- if (occurrences > 1 && !edit.replaceAll) throw new WorkspaceEditError(`${which}The text to replace is not unique in "${inputPath}" (${occurrences} matches); pass replaceAll or include more surrounding context.${nothingApplied}`, {
559
- type: "not-unique",
560
- path: shownPath,
561
- matches: occurrences,
562
- ...indexOption
563
- });
564
- return {
565
- content: edit.replaceAll ? content.split(edit.oldString).join(edit.newString) : replaceFirst(content, edit.oldString, edit.newString),
566
- replacements: edit.replaceAll ? occurrences : 1
567
- };
568
- }
569
- /** Count non-overlapping occurrences of `needle` in `haystack`. */
570
- function countOccurrences(haystack, needle) {
571
- if (needle === "") return 0;
572
- let count = 0;
573
- let from = 0;
574
- while (true) {
575
- const index = haystack.indexOf(needle, from);
576
- if (index === -1) break;
577
- count++;
578
- from = index + needle.length;
579
- }
580
- return count;
581
- }
582
- /** Replace the first occurrence of `needle` with `replacement`. */
583
- function replaceFirst(haystack, needle, replacement) {
584
- const index = haystack.indexOf(needle);
585
- if (index === -1) return haystack;
586
- return haystack.slice(0, index) + replacement + haystack.slice(index + needle.length);
587
- }
588
- /**
589
710
  * Create the policy-enforced operation layer over a backend.
590
711
  *
591
712
  * The returned {@link WorkspaceOps} is the single seam both the