@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
@@ -30,21 +30,22 @@ Paths are resolved against `cwd` with symlinks collapsed and accepted inside `cw
30
30
 
31
31
  ## The policy surface
32
32
 
33
- | Field | What it does |
34
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
35
- | `cwd` | **Absolute** jail root. Every path resolves against it and must stay inside. |
36
- | `allowPaths` | Extra readable roots outside `cwd`. |
37
- | `denyPaths` | Globs (`*`, `**`, `?`) blocked **even inside `cwd`** — e.g. `[".git/**", ".env*"]`. A bare dir name (`"node_modules"`) also blocks its contents. |
38
- | `shell.allow` / `shell.deny` | Executable **basenames** matched against every command's argv (after tokenizing — see below). **Deny wins**, case-insensitively. An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
39
- | `shell.denyPatterns` / `shell.allowCommand` | Argument rules on every command: string prefix patterns (`"git push --force"`) or RegExps, and a predicate `(argv, ctx) => true \| false \| "why"`. |
40
- | `shell.interpreter` | `"none"` (default, argv) or `"sh"` (opt-in shell syntax, every command checked). |
41
- | `shell.background` | `{ maxJobs?, timeoutMs? }` — enables background commands. Absent ⇒ refused. |
42
- | `list.ignore` / `list.maxDepth` / `list.maxEntries` | `list_dir` bounds: hidden globs (default `node_modules`, `.git`), depth cap (5), entry cap (500). |
43
- | `shell.inheritEnv` | Opt-in `process.env` keys to pass through (e.g. `["PATH"]`). **Nothing leaks in otherwise** — a command can't find `node`/`npm` without `PATH`. |
44
- | `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
45
- | `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (process tree SIGKILLed on expiry) and stdout/stderr byte cap (clipped on a character boundary). |
46
- | `read.defaultLines` / `read.maxBytes` | Default read window + a ceiling on the UTF-8 bytes of the returned `content` (line numbers included). See [Read windows and the byte ceiling](#read-windows-and-the-byte-ceiling). |
47
- | `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
33
+ | Field | What it does |
34
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `cwd` | **Absolute** jail root. Every path resolves against it and must stay inside. |
36
+ | `allowPaths` | Extra readable roots outside `cwd`. |
37
+ | `denyPaths` | Globs (`*`, `**`, `?`) blocked **even inside `cwd`** — e.g. `[".git/**", ".env*"]`. A bare dir name (`"node_modules"`) also blocks its contents. |
38
+ | `shell.allow` / `shell.deny` | Executable **basenames** matched against every command's argv (after tokenizing — see below). **Deny wins**, case-insensitively. An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
39
+ | `shell.denyPatterns` / `shell.allowCommand` | Argument rules on every command: string prefix patterns (`"git push --force"`) or RegExps, and a predicate `(argv, ctx) => true \| false \| "why"`. |
40
+ | `shell.interpreter` | `"none"` (default, argv), `"sh"`, `"bash"`, `"powershell"`, `"cmd"` or `"auto"` (opt-in shell syntax, every command checked); `shell.interpreterPath` sets the shell executable. |
41
+ | `shell.rules` / `shell.onAsk` | Ordered `{ match, effect: "allow" \| "ask" \| "deny", reason? }` rules (last match wins, a deny anywhere refuses the line); `onAsk({ command, asks, signal })` returns `{ allow, remember? }`. Refusals: `"approval-required"`, `"approval-denied"`. `ws.shell.classify(line)` lists a line's commands. |
42
+ | `shell.background` | `{ maxJobs?, timeoutMs? }` — enables background commands. Absent ⇒ refused. |
43
+ | `list.ignore` / `list.maxDepth` / `list.maxEntries` | `list_dir` bounds: hidden globs (default `node_modules`, `.git`), depth cap (5), entry cap (500). |
44
+ | `shell.inheritEnv` | Opt-in `process.env` keys to pass through (e.g. `["PATH"]`). **Nothing leaks in otherwise** — a command can't find `node`/`npm` without `PATH`. |
45
+ | `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
46
+ | `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (process tree SIGKILLed on expiry) and stdout/stderr byte cap (clipped on a character boundary). |
47
+ | `read.defaultLines` / `read.maxBytes` | Default read window + a ceiling on the UTF-8 bytes of the returned `content` (line numbers included). See [Read windows and the byte ceiling](#read-windows-and-the-byte-ceiling). |
48
+ | `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
48
49
 
49
50
  > **The most common gotcha:** `run_shell` fails to find `node`/`npm` because the env is empty. Add `shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] }`.
50
51
 
@@ -97,7 +98,7 @@ export async function commitWork(cwd: string) {
97
98
 
98
99
  - Shell mode parses the line and checks every simple command — inside `;`, `&&`, `||`, pipes, `( )`, `$( )`, backticks, here-documents and `NAME=value cmd` prefixes. Redirection targets and `cd` directories must be literal paths inside the jail (`/dev/null` is fine), re-checked with symlinks resolved.
99
100
  - It refuses what it can't parse with confidence (`reason: "unsupported-syntax"`): compound commands, arithmetic, brace expansion, unusual `${…}` forms, bash-only redirections, a command name decided at run time, `&` in a line (use `background: true`), assignments to `IFS`/`PS4`/`CDPATH`/`BASH_ENV`…; it drops shell start-up variables (`ENV`, `BASH_ENV`, `SHELLOPTS`, `BASH_FUNC_*`, …) from the environment.
100
- - `reason` values: `"metacharacter"`, `"unbalanced-quotes"`, `"denied-executable"`, `"not-allowed"`, `"denied-pattern"`, `"nested-command"`, `"unsupported-syntax"`, `"path-escape"`. `checkShellCommand(policy, line)` returns the verdict without running anything.
101
+ - `reason` values: `"metacharacter"`, `"unbalanced-quotes"`, `"denied-executable"`, `"not-allowed"`, `"denied-pattern"`, `"nested-command"`, `"unsupported-syntax"`, `"path-escape"`. `checkShellCommand(policy, line)` returns the verdict without running anything; `classifyShellCommand(line, policy?)` returns `{ commands, opaque? }` with the same parser a host approval gate can reuse; `resolveShell(policy.shell)` reports which interpreter and executable `"auto"` (or a named interpreter) resolves to on this host.
101
102
  - Pair shell mode with an exhaustive `allow`: `deny` alone is a best-effort filter.
102
103
 
103
104
  ## Two callers, one jail
@@ -218,10 +219,56 @@ A custom `WorkspaceBackend` receives the signal as `exec(command, { signal })` a
218
219
 
219
220
  ## Read-before-edit (the stale-hash guard)
220
221
 
221
- `readFile` returns a SHA-256 `hash` of the full file. Pass it to `editFile`'s `expectHash`; if the file changed since you read it, the edit is rejected as stale so you re-read before clobbering. `editFile` also requires the `oldString` to be **exact and unique** — pass `replaceAll: true` to replace every occurrence, or include more surrounding context to disambiguate.
222
+ `readFile` returns a SHA-256 `hash` of the full file. Pass it to `editFile`'s `expectHash`; if the file changed since you read it, the edit is rejected as stale so you re-read before clobbering. `editFile` also requires the `oldString` to be **found uniquely** (see "Finding the text to edit") — pass `replaceAll: true` to replace every occurrence, or include more surrounding context to disambiguate.
222
223
 
223
224
  Writes, edits and removes of the same path run one after another, even when the agent dispatches tool calls concurrently: of two edits pinned to the same `expectHash`, the second is rejected as stale instead of silently overwriting the first. `read.maxBytes` bounds what a read returns, not memory — the whole file is still read and hashed.
224
225
 
226
+ ## Finding the text to edit — `edit.matching`
227
+
228
+ By default an edit looks for `oldString` in tiers and uses the first tier that finds it: the exact text, then the text with line endings normalised (an LF `oldString` finds CRLF lines), then with look-alike punctuation folded (curly quotes, en/em dashes and non-breaking spaces match their plain forms), then line by line ignoring trailing whitespace. A result found by one of the last two tiers reports `matchTier: "unicode"` or `"whitespace"`; an exact find leaves it out. A loose find must be unique: with two candidates the edit fails with `not-unique` and writes nothing, even with `replaceAll`. Replacement text takes the file's dominant line ending, so a CRLF file stays CRLF.
229
+
230
+ ```ts
231
+ ai.workspace({
232
+ cwd,
233
+ edit: { matching: "tiered", missHintLines: 12, lineEndings: "preserve" },
234
+ });
235
+ ```
236
+
237
+ - `matching: "exact"` accepts only the text byte for byte.
238
+ - `lineEndings: "lf" | "crlf"` forces the ending of replacement text.
239
+ - When the text is not found, the `WorkspaceEditError` (`type: "not-found"`) carries a `hint`: the closest file text (at most `missHintLines` lines, 12 by default, each at most 200 characters; `0` turns it off, and it never exceeds `read.maxBytes`) or a note that `newString` is already present. A file the jail refuses never gets a hint.
240
+
241
+ ## Diagnostics after an edit — `hooks.afterEdit`
242
+
243
+ ```ts
244
+ ai.workspace({
245
+ cwd,
246
+ hooks: {
247
+ afterEdit: async ({ paths, tool }) => await typecheck(paths), // returns text or nothing
248
+ afterEditMaxChars: 4_000,
249
+ afterEditTimeoutMs: 10_000,
250
+ },
251
+ });
252
+ ```
253
+
254
+ After `write_file`, `edit_file`, `multi_edit` or `apply_patch` writes files, the hook runs once per call with the written paths (not on a patch `dryRun`, not for deleted files). Text it returns comes back on the result as `diagnostics`, between `[diagnostics]` and `[/diagnostics]` lines; output over `afterEditMaxChars` is cut with a marker. A hook that throws or does not answer within `afterEditTimeoutMs` yields `diagnostics unavailable` and never fails the edit.
255
+
256
+ ## Oversized output — `output`
257
+
258
+ ```ts
259
+ ai.workspace({
260
+ cwd,
261
+ output: {
262
+ spillDir: ".warlock/spill",
263
+ maxLines: 2_000,
264
+ maxBytes: 51_200,
265
+ retentionMs: 604_800_000,
266
+ },
267
+ });
268
+ ```
269
+
270
+ With `output.spillDir` set, output too large to return whole (`run_shell`, `shell_output`, `grep`, `glob`, and `read_file` when `read.maxBytes` cut the window) is saved in full to `spill-<time>-<random>-<label>.txt` there. The result keeps the start and end (`keep: "head+tail"`, the default for shell output; `"head"` for the rest, or set `keep` yourself) and carries a marker such as `[showing lines 1-25 and 99976-100000 of 100000; full output saved to FILE]`. The boolean `truncated` stays; `truncation` adds `{ shown: { lines }, total: { lines, bytes }, path? }` (per stream, `truncation.stdout` / `truncation.stderr`, for commands). `read_file` may open the spill directory even when it is outside the jail, so the model can page through the file with `startLine` / `limit`; nothing else outside the jail opens, and writes never reach it. Files older than `retentionMs` (7 days by default) are removed when a workspace is created, when a new file is written and on `dispose()`; `retentionMs: 0` removes the files this workspace wrote on `dispose()`. Only files named like spill files are ever removed. Without `spillDir`, output is cut at `shell.maxOutputBytes` / `read.maxBytes` as before, and `run_shell` results still report the sizes in `truncation`.
271
+
225
272
  ## Errors flow as data, not throws
226
273
 
227
274
  Policy and edit failures are typed errors extending the `@warlock.js/ai` `AIError` base. Inside a tool the `tool()` wrapper catches them and surfaces them in `{ error }`, so the agent reads the failure and self-corrects. Branch on `.type`: