@warlock.js/ai-workspace 5.28.0 → 5.30.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.
- package/CHANGELOG.md +30 -0
- package/README.md +6 -0
- package/cjs/index.cjs +201 -73
- package/cjs/index.cjs.map +1 -1
- package/esm/backends/local.d.mts +1 -1
- package/esm/backends/local.d.mts.map +1 -1
- package/esm/backends/local.mjs +38 -8
- package/esm/backends/local.mjs.map +1 -1
- package/esm/backends/mock.d.mts +1 -1
- package/esm/backends/mock.d.mts.map +1 -1
- package/esm/backends/mock.mjs +11 -3
- package/esm/backends/mock.mjs.map +1 -1
- package/esm/contracts/index.d.mts +1 -1
- package/esm/contracts/tool-io.type.d.mts +47 -8
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-backend.contract.d.mts +17 -1
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +10 -14
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-policy.type.d.mts +9 -2
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +13 -14
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +1 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +1 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/ops.d.mts +1 -1
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +121 -28
- package/esm/ops.mjs.map +1 -1
- package/esm/policy/policy.d.mts +1 -1
- package/esm/policy/policy.d.mts.map +1 -1
- package/esm/policy/policy.mjs +1 -1
- package/esm/policy/policy.mjs.map +1 -1
- package/esm/policy/tokenize-command.mjs +1 -1
- package/esm/policy/tokenize-command.mjs.map +1 -1
- package/esm/tools/edit-file.d.mts +1 -1
- package/esm/tools/edit-file.d.mts.map +1 -1
- package/esm/tools/edit-file.mjs +1 -1
- package/esm/tools/edit-file.mjs.map +1 -1
- package/esm/tools/glob.d.mts +1 -1
- package/esm/tools/glob.d.mts.map +1 -1
- package/esm/tools/glob.mjs +1 -1
- package/esm/tools/glob.mjs.map +1 -1
- package/esm/tools/grep.d.mts +1 -1
- package/esm/tools/grep.d.mts.map +1 -1
- package/esm/tools/grep.mjs +1 -1
- package/esm/tools/grep.mjs.map +1 -1
- package/esm/tools/read-file.d.mts +5 -5
- package/esm/tools/read-file.d.mts.map +1 -1
- package/esm/tools/read-file.mjs +8 -19
- package/esm/tools/read-file.mjs.map +1 -1
- package/esm/tools/run-shell.d.mts +5 -1
- package/esm/tools/run-shell.d.mts.map +1 -1
- package/esm/tools/run-shell.mjs +9 -2
- package/esm/tools/run-shell.mjs.map +1 -1
- package/esm/tools/run-tests.d.mts +3 -2
- package/esm/tools/run-tests.d.mts.map +1 -1
- package/esm/tools/run-tests.mjs +5 -4
- package/esm/tools/run-tests.mjs.map +1 -1
- package/esm/tools/schema.mjs +1 -1
- package/esm/tools/schema.mjs.map +1 -1
- package/esm/tools/write-file.d.mts +1 -1
- package/esm/tools/write-file.d.mts.map +1 -1
- package/esm/tools/write-file.mjs +1 -1
- package/esm/tools/write-file.mjs.map +1 -1
- package/esm/workspace.d.mts +1 -1
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +1 -1
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +48 -12
- package/llms.txt +2 -2
- package/package.json +5 -4
- package/skills/README.md +3 -3
- package/skills/build-loop-agent/SKILL.md +4 -4
- package/skills/use-a-workspace/SKILL.md +44 -8
package/llms-full.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: build-loop-agent
|
|
11
|
-
description: 'Wire a @warlock.js/ai-workspace into a coding agent that closes the read → edit → run-tests loop on its own. Triggers: `ws.tools.all()`, `ai.agent({ tools: ws.tools.all() })`, `maxTrips`, `run_tests`, `run_shell`, ''agent that fixes a failing test'', ''make the suite green'', ''coding agent that edits a repo'', ''build-loop agent'', ''self-correcting agent'', ''read-only code reviewer'', ''scaffold then hand off to an agent'', ''share one workspace across a supervisor'', ''least-privilege agent tools'', ''errors as tool data''; typical wiring `const ws = ai.workspace({ cwd, shell }); ai.agent({ model, tools: ws.tools.all(), maxTrips })`. Skip: constructing the workspace / its policy / direct methods / readonly / scope —
|
|
11
|
+
description: 'Wire a @warlock.js/ai-workspace into a coding agent that closes the read → edit → run-tests loop on its own. Triggers: `ws.tools.all()`, `ai.agent({ tools: ws.tools.all() })`, `maxTrips`, `run_tests`, `run_shell`, ''agent that fixes a failing test'', ''make the suite green'', ''coding agent that edits a repo'', ''build-loop agent'', ''self-correcting agent'', ''read-only code reviewer'', ''scaffold then hand off to an agent'', ''share one workspace across a supervisor'', ''least-privilege agent tools'', ''errors as tool data''; typical wiring `const ws = ai.workspace({ cwd, shell }); ai.agent({ model, tools: ws.tools.all(), maxTrips })`. Skip: constructing the workspace / its policy / direct methods / readonly / scope — the `use-a-workspace` topic; the agent loop / maxTrips / events themselves — the `run-ai-agent` topic of the `warlock-js-ai` skill.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Build a build-loop agent — read → edit → run tests, until green
|
|
@@ -103,16 +103,16 @@ ai.supervisor({
|
|
|
103
103
|
|
|
104
104
|
## See also
|
|
105
105
|
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
106
|
+
- The `use-a-workspace` topic — building the workspace, the policy surface, the direct methods, `readonly` / `scope`, and the typed errors.
|
|
107
|
+
- The `run-ai-agent` topic of the `warlock-js-ai` skill — the agent loop, `maxTrips`, and the run report.
|
|
108
|
+
- The `run-supervisor` topic of the `warlock-js-ai` skill — coordinating the members that share one workspace.
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
## use-a-workspace `@warlock.js/ai-workspace/use-a-workspace/SKILL.md`
|
|
112
112
|
|
|
113
113
|
---
|
|
114
114
|
name: use-a-workspace
|
|
115
|
-
description: 'Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `WorkspacePolicyError`, `WorkspaceEditError`, `denyPaths`, `inheritEnv`, `expectHash`, `tokenizeCommand`, `read_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`/`grep`; ''give an agent file tools'', ''jail an agent to a directory'', ''let an agent read/write/grep a repo'', ''read-only workspace for a reviewer'', ''scope an agent to a subdirectory'', ''run a shell command under policy'', ''edit a file with a stale-hash guard'', ''command injection guard for an agent shell tool'', ''ReDoS-safe grep for an agent''; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools —
|
|
115
|
+
description: 'Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `WorkspacePolicyError`, `WorkspaceEditError`, `denyPaths`, `inheritEnv`, `expectHash`, `read.maxBytes`, `bytesCapped`, `cancelled`, `ExecOptions`, `tokenizeCommand`, `read_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`/`grep`; ''give an agent file tools'', ''jail an agent to a directory'', ''let an agent read/write/grep a repo'', ''read-only workspace for a reviewer'', ''scope an agent to a subdirectory'', ''run a shell command under policy'', ''edit a file with a stale-hash guard'', ''command injection guard for an agent shell tool'', ''ReDoS-safe grep for an agent'', ''cap how many bytes a read returns'', ''cancel a running workspace command''; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — the `build-loop-agent` topic; defining a non-filesystem custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
|
|
116
116
|
---
|
|
117
117
|
|
|
118
118
|
# Use a workspace — a policy jail for an agent's filesystem + shell
|
|
@@ -146,8 +146,8 @@ This is least-privilege guardrails for a **trusted** agent — not a sandbox aro
|
|
|
146
146
|
| `shell.allow` / `shell.deny` | Executable **basenames** matched against the command's leading argv token (after tokenizing — see below). **Deny wins.** An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
|
|
147
147
|
| `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`. |
|
|
148
148
|
| `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
|
|
149
|
-
| `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (
|
|
150
|
-
| `read.defaultLines` / `read.maxBytes` | Default read window + a byte ceiling. |
|
|
149
|
+
| `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (process tree SIGKILLed on expiry) and stdout/stderr byte cap (clipped on a character boundary). |
|
|
150
|
+
| `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). |
|
|
151
151
|
| `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
|
|
152
152
|
|
|
153
153
|
> **The most common gotcha:** `run_shell` fails to find `node`/`npm` because the env is empty. Add `shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] }`.
|
|
@@ -185,10 +185,10 @@ ws.tools.runTests({ command: "vitest run" }); // override the test base command
|
|
|
185
185
|
The same policy seam, callable from code (no LLM):
|
|
186
186
|
|
|
187
187
|
```ts
|
|
188
|
-
const { content, hash, totalLines } = await ws.readFile("src/index.ts", { offset: 1, limit: 200 });
|
|
188
|
+
const { content, hash, totalLines, endLine, truncated, bytesCapped } = await ws.readFile("src/index.ts", { offset: 1, limit: 200 });
|
|
189
189
|
await ws.writeFile("src/new.ts", "export const x = 1;\n");
|
|
190
190
|
await ws.editFile({ path: "src/index.ts", oldString: "old", newString: "new", expectHash: hash });
|
|
191
|
-
const { exitCode, stdout, stderr, timedOut, truncated } = await ws.exec("npm test");
|
|
191
|
+
const { exitCode, stdout, stderr, timedOut, cancelled, truncated } = await ws.exec("npm test");
|
|
192
192
|
const { matches } = await ws.grep("TODO", { glob: "src/**/*.ts", ignoreCase: true });
|
|
193
193
|
const paths = await ws.glob("src/models/**/*.ts");
|
|
194
194
|
await ws.exists("package.json");
|
|
@@ -200,10 +200,46 @@ await ws.remove("dist");
|
|
|
200
200
|
|
|
201
201
|
The model supplies `pattern` freely, so `grep` treats it as untrusted input before compiling it into a `RegExp`: patterns over 200 characters and patterns matching a nested-quantifier shape (`(x+)+`, `(x*)*`, `(x+)*`, `(x*)+`-style groups — the classic catastrophic-backtracking shape) are rejected up front as a `WorkspacePolicyError` `type: "unsafe-pattern"`, before any regex is compiled. Any scanned line longer than 2000 characters is skipped (not tested), bounding the worst-case backtracking cost of any single call regardless of pattern shape.
|
|
202
202
|
|
|
203
|
+
## Read windows and the byte ceiling
|
|
204
|
+
|
|
205
|
+
`readFile()` and the `read_file` tool return the same result for the same arguments: a `cat -n` numbered window plus `{ startLine, endLine, totalLines, totalBytes, truncated, bytesCapped, hash }`.
|
|
206
|
+
|
|
207
|
+
- The window starts at `offset` (`startLine` on the tool) and holds at most `limit` lines (default `read.defaultLines`, else 2000).
|
|
208
|
+
- `read.maxBytes` caps the UTF-8 bytes of the returned `content` itself — line-number prefixes and newlines count. Whole lines are kept while they fit; the first line that doesn't is cut on a **character boundary** (never mid-character), so a single very long line still comes back bounded. `bytesCapped` is `true` whenever the cap cut anything, and `endLine` is the last line included, possibly partially.
|
|
209
|
+
- `truncated` is `true` when anything after `content` was withheld — later lines (by `limit`, the default window or the cap) or the tail of `endLine`.
|
|
210
|
+
- `hash` is always the SHA-256 of the **whole file**, not the window, so it is a valid `expectHash` whatever was returned. `totalBytes` is the whole file's size.
|
|
211
|
+
- No `maxBytes` ⇒ no byte cap (only the line window applies).
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const ws = ai.workspace({ cwd, read: { maxBytes: 64_000, defaultLines: 400 } });
|
|
215
|
+
const page = await ws.readFile("dist/bundle.js");
|
|
216
|
+
if (page.bytesCapped) console.warn(`Only part of line ${page.endLine} fit in the read budget.`);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Cancelling a command
|
|
220
|
+
|
|
221
|
+
`exec()` accepts a per-call `signal`; `run_shell` and `run_tests` pass the agent run's signal (the dispatch `ctx.signal`) automatically, so cancelling the agent run stops the command it is waiting on.
|
|
222
|
+
|
|
223
|
+
- Already aborted ⇒ the command is **not started** (`exitCode: 130`).
|
|
224
|
+
- Aborted mid-run ⇒ the whole process tree is SIGKILLed, as on a timeout.
|
|
225
|
+
- Either way the result says `cancelled: true` — it resolves, it doesn't throw — and `cancelled` and `timedOut` are never both `true`. Branch on those flags, not on the exit code.
|
|
226
|
+
- The signal belongs to that one call; the workspace never stores it, so one workspace can serve many runs. The abort listener is removed once the command settles.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const controller = new AbortController();
|
|
230
|
+
const pending = ws.exec("npm test", { signal: controller.signal, timeoutMs: 120_000 });
|
|
231
|
+
controller.abort();
|
|
232
|
+
const { cancelled } = await pending; // true
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
A custom `WorkspaceBackend` receives the signal as `exec(command, { signal })` and should follow the same rules, reporting `cancelled: true` in its result.
|
|
236
|
+
|
|
203
237
|
## Read-before-edit (the stale-hash guard)
|
|
204
238
|
|
|
205
239
|
`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.
|
|
206
240
|
|
|
241
|
+
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.
|
|
242
|
+
|
|
207
243
|
## Errors flow as data, not throws
|
|
208
244
|
|
|
209
245
|
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`:
|
|
@@ -243,8 +279,8 @@ const ws = ai.workspace({ cwd: "/srv/app", backend: "mock", shell: { allow: ["np
|
|
|
243
279
|
|
|
244
280
|
## See also
|
|
245
281
|
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
282
|
+
- The `build-loop-agent` topic — wiring these tools into a read → edit → test agent loop.
|
|
283
|
+
- The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent that consumes `ws.tools.all()`.
|
|
284
|
+
- The `define-ai-tool` topic of the `warlock-js-ai` skill — the `tool()` / `ToolContract` seam every workspace tool is built on.
|
|
249
285
|
|
|
250
286
|
|
package/llms.txt
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [build-loop-agent](@warlock.js/ai-workspace/build-loop-agent/SKILL.md): Wire a @warlock.js/ai-workspace into a coding agent that closes the read → edit → run-tests loop on its own. Triggers: `ws.tools.all()`, `ai.agent({ tools: ws.tools.all() })`, `maxTrips`, `run_tests`, `run_shell`, 'agent that fixes a failing test', 'make the suite green', 'coding agent that edits a repo', 'build-loop agent', 'self-correcting agent', 'read-only code reviewer', 'scaffold then hand off to an agent', 'share one workspace across a supervisor', 'least-privilege agent tools', 'errors as tool data'; typical wiring `const ws = ai.workspace({ cwd, shell }); ai.agent({ model, tools: ws.tools.all(), maxTrips })`. Skip: constructing the workspace / its policy / direct methods / readonly / scope —
|
|
10
|
-
- [use-a-workspace](@warlock.js/ai-workspace/use-a-workspace/SKILL.md): Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `WorkspacePolicyError`, `WorkspaceEditError`, `denyPaths`, `inheritEnv`, `expectHash`, `tokenizeCommand`, `read_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`/`grep`; 'give an agent file tools', 'jail an agent to a directory', 'let an agent read/write/grep a repo', 'read-only workspace for a reviewer', 'scope an agent to a subdirectory', 'run a shell command under policy', 'edit a file with a stale-hash guard', 'command injection guard for an agent shell tool', 'ReDoS-safe grep for an agent'; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools —
|
|
9
|
+
- [build-loop-agent](@warlock.js/ai-workspace/build-loop-agent/SKILL.md): Wire a @warlock.js/ai-workspace into a coding agent that closes the read → edit → run-tests loop on its own. Triggers: `ws.tools.all()`, `ai.agent({ tools: ws.tools.all() })`, `maxTrips`, `run_tests`, `run_shell`, 'agent that fixes a failing test', 'make the suite green', 'coding agent that edits a repo', 'build-loop agent', 'self-correcting agent', 'read-only code reviewer', 'scaffold then hand off to an agent', 'share one workspace across a supervisor', 'least-privilege agent tools', 'errors as tool data'; typical wiring `const ws = ai.workspace({ cwd, shell }); ai.agent({ model, tools: ws.tools.all(), maxTrips })`. Skip: constructing the workspace / its policy / direct methods / readonly / scope — the `use-a-workspace` topic; the agent loop / maxTrips / events themselves — the `run-ai-agent` topic of the `warlock-js-ai` skill.
|
|
10
|
+
- [use-a-workspace](@warlock.js/ai-workspace/use-a-workspace/SKILL.md): Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `WorkspacePolicyError`, `WorkspaceEditError`, `denyPaths`, `inheritEnv`, `expectHash`, `read.maxBytes`, `bytesCapped`, `cancelled`, `ExecOptions`, `tokenizeCommand`, `read_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`/`grep`; 'give an agent file tools', 'jail an agent to a directory', 'let an agent read/write/grep a repo', 'read-only workspace for a reviewer', 'scope an agent to a subdirectory', 'run a shell command under policy', 'edit a file with a stale-hash guard', 'command injection guard for an agent shell tool', 'ReDoS-safe grep for an agent', 'cap how many bytes a read returns', 'cancel a running workspace command'; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — the `build-loop-agent` topic; defining a non-filesystem custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.
|
package/package.json
CHANGED
|
@@ -15,16 +15,17 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/warlockjs/
|
|
18
|
+
"url": "git+https://github.com/warlockjs/framework.git",
|
|
19
|
+
"directory": "packages/ai-workspace"
|
|
19
20
|
},
|
|
20
21
|
"peerDependencies": {
|
|
21
|
-
"@warlock.js/ai": "5.
|
|
22
|
+
"@warlock.js/ai": "5.30.0"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@standard-schema/spec": "^1.0.0",
|
|
25
|
-
"@warlock.js/fs": "5.
|
|
26
|
+
"@warlock.js/fs": "5.30.0"
|
|
26
27
|
},
|
|
27
|
-
"version": "5.
|
|
28
|
+
"version": "5.30.0",
|
|
28
29
|
"main": "./cjs/index.cjs",
|
|
29
30
|
"module": "./esm/index.mjs",
|
|
30
31
|
"types": "./esm/index.d.mts",
|
package/skills/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# `@warlock.js/ai-workspace` — skills index
|
|
2
2
|
|
|
3
|
-
Per-task skills.
|
|
3
|
+
Per-task skills. Cross-references name the topic ("the `<topic>` topic"), and for another package add its skill ("the `<topic>` topic of the `warlock-js-<pkg>` skill").
|
|
4
4
|
|
|
5
5
|
## Skills
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### `use-a-workspace`
|
|
8
8
|
|
|
9
9
|
Build a policy-jailed filesystem + shell workspace with `ai.workspace(policy)` and operate it from both ends — the agent-facing `ws.tools.*` (the seven tools + `all()` / `pick(...)`) and the human-facing direct methods (`readFile` / `writeFile` / `editFile` / `exec` / `grep` / `glob` / `exists` / `mkdir` / `remove`). Covers the policy surface (the `cwd` jail, `allowPaths` / `denyPaths`, the fail-closed shell allow/deny + `inheritEnv`, read caps), the local vs mock backend, the read-before-edit stale-hash guard, the typed `WorkspacePolicyError` / `WorkspaceEditError`, and the `readonly()` / `scope(subdir)` projections. Load when constructing a workspace, choosing a policy, vending tools to an agent, or driving the filesystem programmatically.
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### `build-loop-agent`
|
|
12
12
|
|
|
13
13
|
Wire a workspace into a coding agent that closes the read → edit → run-tests loop on its own — `ai.agent({ tools: ws.tools.all() })` so a failing suite comes back as tool *data* and the model self-corrects until green. Covers the minimal build-loop wiring, why errors-as-data drives the loop, the shell policy a test command needs (`shell.allow` + `inheritEnv: ["PATH"]`), the read-only reviewer via `tools.pick` / `readonly()`, scaffolding from code before handing off, and sharing one jail across a supervisor's members. Load when building an agent that edits a repo, runs tests, reviews code, or scaffolds then hands off.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-loop-agent
|
|
3
|
-
description: 'Wire a @warlock.js/ai-workspace into a coding agent that closes the read → edit → run-tests loop on its own. Triggers: `ws.tools.all()`, `ai.agent({ tools: ws.tools.all() })`, `maxTrips`, `run_tests`, `run_shell`, ''agent that fixes a failing test'', ''make the suite green'', ''coding agent that edits a repo'', ''build-loop agent'', ''self-correcting agent'', ''read-only code reviewer'', ''scaffold then hand off to an agent'', ''share one workspace across a supervisor'', ''least-privilege agent tools'', ''errors as tool data''; typical wiring `const ws = ai.workspace({ cwd, shell }); ai.agent({ model, tools: ws.tools.all(), maxTrips })`. Skip: constructing the workspace / its policy / direct methods / readonly / scope —
|
|
3
|
+
description: 'Wire a @warlock.js/ai-workspace into a coding agent that closes the read → edit → run-tests loop on its own. Triggers: `ws.tools.all()`, `ai.agent({ tools: ws.tools.all() })`, `maxTrips`, `run_tests`, `run_shell`, ''agent that fixes a failing test'', ''make the suite green'', ''coding agent that edits a repo'', ''build-loop agent'', ''self-correcting agent'', ''read-only code reviewer'', ''scaffold then hand off to an agent'', ''share one workspace across a supervisor'', ''least-privilege agent tools'', ''errors as tool data''; typical wiring `const ws = ai.workspace({ cwd, shell }); ai.agent({ model, tools: ws.tools.all(), maxTrips })`. Skip: constructing the workspace / its policy / direct methods / readonly / scope — the `use-a-workspace` topic; the agent loop / maxTrips / events themselves — the `run-ai-agent` topic of the `warlock-js-ai` skill.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Build a build-loop agent — read → edit → run tests, until green
|
|
@@ -95,6 +95,6 @@ ai.supervisor({
|
|
|
95
95
|
|
|
96
96
|
## See also
|
|
97
97
|
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
98
|
+
- The `use-a-workspace` topic — building the workspace, the policy surface, the direct methods, `readonly` / `scope`, and the typed errors.
|
|
99
|
+
- The `run-ai-agent` topic of the `warlock-js-ai` skill — the agent loop, `maxTrips`, and the run report.
|
|
100
|
+
- The `run-supervisor` topic of the `warlock-js-ai` skill — coordinating the members that share one workspace.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: use-a-workspace
|
|
3
|
-
description: 'Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `WorkspacePolicyError`, `WorkspaceEditError`, `denyPaths`, `inheritEnv`, `expectHash`, `tokenizeCommand`, `read_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`/`grep`; ''give an agent file tools'', ''jail an agent to a directory'', ''let an agent read/write/grep a repo'', ''read-only workspace for a reviewer'', ''scope an agent to a subdirectory'', ''run a shell command under policy'', ''edit a file with a stale-hash guard'', ''command injection guard for an agent shell tool'', ''ReDoS-safe grep for an agent''; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools —
|
|
3
|
+
description: 'Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `WorkspacePolicyError`, `WorkspaceEditError`, `denyPaths`, `inheritEnv`, `expectHash`, `read.maxBytes`, `bytesCapped`, `cancelled`, `ExecOptions`, `tokenizeCommand`, `read_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`/`grep`; ''give an agent file tools'', ''jail an agent to a directory'', ''let an agent read/write/grep a repo'', ''read-only workspace for a reviewer'', ''scope an agent to a subdirectory'', ''run a shell command under policy'', ''edit a file with a stale-hash guard'', ''command injection guard for an agent shell tool'', ''ReDoS-safe grep for an agent'', ''cap how many bytes a read returns'', ''cancel a running workspace command''; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — the `build-loop-agent` topic; defining a non-filesystem custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use a workspace — a policy jail for an agent's filesystem + shell
|
|
@@ -34,8 +34,8 @@ This is least-privilege guardrails for a **trusted** agent — not a sandbox aro
|
|
|
34
34
|
| `shell.allow` / `shell.deny` | Executable **basenames** matched against the command's leading argv token (after tokenizing — see below). **Deny wins.** An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
|
|
35
35
|
| `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`. |
|
|
36
36
|
| `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
|
|
37
|
-
| `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (
|
|
38
|
-
| `read.defaultLines` / `read.maxBytes` | Default read window + a byte ceiling. |
|
|
37
|
+
| `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (process tree SIGKILLed on expiry) and stdout/stderr byte cap (clipped on a character boundary). |
|
|
38
|
+
| `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). |
|
|
39
39
|
| `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
|
|
40
40
|
|
|
41
41
|
> **The most common gotcha:** `run_shell` fails to find `node`/`npm` because the env is empty. Add `shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] }`.
|
|
@@ -73,10 +73,10 @@ ws.tools.runTests({ command: "vitest run" }); // override the test base command
|
|
|
73
73
|
The same policy seam, callable from code (no LLM):
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
|
-
const { content, hash, totalLines } = await ws.readFile("src/index.ts", { offset: 1, limit: 200 });
|
|
76
|
+
const { content, hash, totalLines, endLine, truncated, bytesCapped } = await ws.readFile("src/index.ts", { offset: 1, limit: 200 });
|
|
77
77
|
await ws.writeFile("src/new.ts", "export const x = 1;\n");
|
|
78
78
|
await ws.editFile({ path: "src/index.ts", oldString: "old", newString: "new", expectHash: hash });
|
|
79
|
-
const { exitCode, stdout, stderr, timedOut, truncated } = await ws.exec("npm test");
|
|
79
|
+
const { exitCode, stdout, stderr, timedOut, cancelled, truncated } = await ws.exec("npm test");
|
|
80
80
|
const { matches } = await ws.grep("TODO", { glob: "src/**/*.ts", ignoreCase: true });
|
|
81
81
|
const paths = await ws.glob("src/models/**/*.ts");
|
|
82
82
|
await ws.exists("package.json");
|
|
@@ -88,10 +88,46 @@ await ws.remove("dist");
|
|
|
88
88
|
|
|
89
89
|
The model supplies `pattern` freely, so `grep` treats it as untrusted input before compiling it into a `RegExp`: patterns over 200 characters and patterns matching a nested-quantifier shape (`(x+)+`, `(x*)*`, `(x+)*`, `(x*)+`-style groups — the classic catastrophic-backtracking shape) are rejected up front as a `WorkspacePolicyError` `type: "unsafe-pattern"`, before any regex is compiled. Any scanned line longer than 2000 characters is skipped (not tested), bounding the worst-case backtracking cost of any single call regardless of pattern shape.
|
|
90
90
|
|
|
91
|
+
## Read windows and the byte ceiling
|
|
92
|
+
|
|
93
|
+
`readFile()` and the `read_file` tool return the same result for the same arguments: a `cat -n` numbered window plus `{ startLine, endLine, totalLines, totalBytes, truncated, bytesCapped, hash }`.
|
|
94
|
+
|
|
95
|
+
- The window starts at `offset` (`startLine` on the tool) and holds at most `limit` lines (default `read.defaultLines`, else 2000).
|
|
96
|
+
- `read.maxBytes` caps the UTF-8 bytes of the returned `content` itself — line-number prefixes and newlines count. Whole lines are kept while they fit; the first line that doesn't is cut on a **character boundary** (never mid-character), so a single very long line still comes back bounded. `bytesCapped` is `true` whenever the cap cut anything, and `endLine` is the last line included, possibly partially.
|
|
97
|
+
- `truncated` is `true` when anything after `content` was withheld — later lines (by `limit`, the default window or the cap) or the tail of `endLine`.
|
|
98
|
+
- `hash` is always the SHA-256 of the **whole file**, not the window, so it is a valid `expectHash` whatever was returned. `totalBytes` is the whole file's size.
|
|
99
|
+
- No `maxBytes` ⇒ no byte cap (only the line window applies).
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const ws = ai.workspace({ cwd, read: { maxBytes: 64_000, defaultLines: 400 } });
|
|
103
|
+
const page = await ws.readFile("dist/bundle.js");
|
|
104
|
+
if (page.bytesCapped) console.warn(`Only part of line ${page.endLine} fit in the read budget.`);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Cancelling a command
|
|
108
|
+
|
|
109
|
+
`exec()` accepts a per-call `signal`; `run_shell` and `run_tests` pass the agent run's signal (the dispatch `ctx.signal`) automatically, so cancelling the agent run stops the command it is waiting on.
|
|
110
|
+
|
|
111
|
+
- Already aborted ⇒ the command is **not started** (`exitCode: 130`).
|
|
112
|
+
- Aborted mid-run ⇒ the whole process tree is SIGKILLed, as on a timeout.
|
|
113
|
+
- Either way the result says `cancelled: true` — it resolves, it doesn't throw — and `cancelled` and `timedOut` are never both `true`. Branch on those flags, not on the exit code.
|
|
114
|
+
- The signal belongs to that one call; the workspace never stores it, so one workspace can serve many runs. The abort listener is removed once the command settles.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const pending = ws.exec("npm test", { signal: controller.signal, timeoutMs: 120_000 });
|
|
119
|
+
controller.abort();
|
|
120
|
+
const { cancelled } = await pending; // true
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A custom `WorkspaceBackend` receives the signal as `exec(command, { signal })` and should follow the same rules, reporting `cancelled: true` in its result.
|
|
124
|
+
|
|
91
125
|
## Read-before-edit (the stale-hash guard)
|
|
92
126
|
|
|
93
127
|
`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.
|
|
94
128
|
|
|
129
|
+
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.
|
|
130
|
+
|
|
95
131
|
## Errors flow as data, not throws
|
|
96
132
|
|
|
97
133
|
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`:
|
|
@@ -131,6 +167,6 @@ const ws = ai.workspace({ cwd: "/srv/app", backend: "mock", shell: { allow: ["np
|
|
|
131
167
|
|
|
132
168
|
## See also
|
|
133
169
|
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
170
|
+
- The `build-loop-agent` topic — wiring these tools into a read → edit → test agent loop.
|
|
171
|
+
- The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent that consumes `ws.tools.all()`.
|
|
172
|
+
- The `define-ai-tool` topic of the `warlock-js-ai` skill — the `tool()` / `ToolContract` seam every workspace tool is built on.
|