@warlock.js/ai-workspace 4.5.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 (78) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +149 -0
  4. package/cjs/index.cjs +1609 -0
  5. package/cjs/index.cjs.map +1 -0
  6. package/esm/backends/local.d.mts +22 -0
  7. package/esm/backends/local.d.mts.map +1 -0
  8. package/esm/backends/local.mjs +208 -0
  9. package/esm/backends/local.mjs.map +1 -0
  10. package/esm/backends/mock.d.mts +62 -0
  11. package/esm/backends/mock.d.mts.map +1 -0
  12. package/esm/backends/mock.mjs +167 -0
  13. package/esm/backends/mock.mjs.map +1 -0
  14. package/esm/contracts/index.d.mts +5 -0
  15. package/esm/contracts/tool-io.type.d.mts +149 -0
  16. package/esm/contracts/tool-io.type.d.mts.map +1 -0
  17. package/esm/contracts/workspace-backend.contract.d.mts +69 -0
  18. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -0
  19. package/esm/contracts/workspace-ops.contract.d.mts +72 -0
  20. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -0
  21. package/esm/contracts/workspace-policy.type.d.mts +86 -0
  22. package/esm/contracts/workspace-policy.type.d.mts.map +1 -0
  23. package/esm/contracts/workspace.contract.d.mts +131 -0
  24. package/esm/contracts/workspace.contract.d.mts.map +1 -0
  25. package/esm/errors.d.mts +100 -0
  26. package/esm/errors.d.mts.map +1 -0
  27. package/esm/errors.mjs +58 -0
  28. package/esm/errors.mjs.map +1 -0
  29. package/esm/index.d.mts +20 -0
  30. package/esm/index.mjs +15 -0
  31. package/esm/ops.d.mts +25 -0
  32. package/esm/ops.d.mts.map +1 -0
  33. package/esm/ops.mjs +294 -0
  34. package/esm/ops.mjs.map +1 -0
  35. package/esm/policy/policy.d.mts +71 -0
  36. package/esm/policy/policy.d.mts.map +1 -0
  37. package/esm/policy/policy.mjs +184 -0
  38. package/esm/policy/policy.mjs.map +1 -0
  39. package/esm/tools/edit-file.d.mts +40 -0
  40. package/esm/tools/edit-file.d.mts.map +1 -0
  41. package/esm/tools/edit-file.mjs +57 -0
  42. package/esm/tools/edit-file.mjs.map +1 -0
  43. package/esm/tools/glob.d.mts +37 -0
  44. package/esm/tools/glob.d.mts.map +1 -0
  45. package/esm/tools/glob.mjs +45 -0
  46. package/esm/tools/glob.mjs.map +1 -0
  47. package/esm/tools/grep.d.mts +36 -0
  48. package/esm/tools/grep.d.mts.map +1 -0
  49. package/esm/tools/grep.mjs +51 -0
  50. package/esm/tools/grep.mjs.map +1 -0
  51. package/esm/tools/read-file.d.mts +35 -0
  52. package/esm/tools/read-file.d.mts.map +1 -0
  53. package/esm/tools/read-file.mjs +64 -0
  54. package/esm/tools/read-file.mjs.map +1 -0
  55. package/esm/tools/run-shell.d.mts +35 -0
  56. package/esm/tools/run-shell.d.mts.map +1 -0
  57. package/esm/tools/run-shell.mjs +65 -0
  58. package/esm/tools/run-shell.mjs.map +1 -0
  59. package/esm/tools/run-tests.d.mts +40 -0
  60. package/esm/tools/run-tests.d.mts.map +1 -0
  61. package/esm/tools/run-tests.mjs +67 -0
  62. package/esm/tools/run-tests.mjs.map +1 -0
  63. package/esm/tools/schema.mjs +111 -0
  64. package/esm/tools/schema.mjs.map +1 -0
  65. package/esm/tools/write-file.d.mts +33 -0
  66. package/esm/tools/write-file.d.mts.map +1 -0
  67. package/esm/tools/write-file.mjs +52 -0
  68. package/esm/tools/write-file.mjs.map +1 -0
  69. package/esm/workspace.d.mts +54 -0
  70. package/esm/workspace.d.mts.map +1 -0
  71. package/esm/workspace.mjs +210 -0
  72. package/esm/workspace.mjs.map +1 -0
  73. package/llms-full.txt +231 -0
  74. package/llms.txt +10 -0
  75. package/package.json +42 -0
  76. package/skills/README.md +13 -0
  77. package/skills/build-loop-agent/SKILL.md +100 -0
  78. package/skills/use-a-workspace/SKILL.md +117 -0
package/llms-full.txt ADDED
@@ -0,0 +1,231 @@
1
+ # Warlock AI Workspace — full skills
2
+
3
+ > Package: `@warlock.js/ai-workspace`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/ai-workspace/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## build-loop-agent `@warlock.js/ai-workspace/build-loop-agent/SKILL.md`
8
+
9
+ ---
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 — `@warlock.js/ai-workspace/use-a-workspace/SKILL.md`; the agent loop / maxTrips / events themselves — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
12
+ ---
13
+
14
+ # Build a build-loop agent — read → edit → run tests, until green
15
+
16
+ The point of a workspace is to hand its tools to an agent and let it close the loop itself: read the failing spec, edit the source, run the tests, read the failure, fix again. The failure comes back as tool **data** (a non-zero `exitCode`, the captured `stderr`), not a thrown error — so the model can see what broke and self-correct.
17
+
18
+ ```ts
19
+ import "@warlock.js/ai-workspace";
20
+ import { ai } from "@warlock.js/ai";
21
+
22
+ const ws = ai.workspace({
23
+ cwd: "/srv/acme-api",
24
+ denyPaths: [".git/**", ".env*"],
25
+ shell: { allow: ["npm", "node"], inheritEnv: ["PATH"], timeoutMs: 120_000 },
26
+ });
27
+
28
+ const dev = ai.agent({
29
+ model,
30
+ tools: ws.tools.all(), // read_file, edit_file, write_file, run_shell, run_tests, grep, glob
31
+ maxTrips: 25, // give the loop room to iterate
32
+ });
33
+
34
+ await dev.execute("`npm test` fails on the cart-total suite. Make it green.");
35
+ ```
36
+
37
+ ## Why the loop works: errors as data
38
+
39
+ A failing test isn't a tool error — it's a normal result with `exitCode !== 0`. `run_tests` (and `run_shell`) return `{ exitCode, stdout, stderr, timedOut, truncated }` as the tool's `data`, so the model reads the failure on the next trip and tries again. The same is true of the policy/edit guards: a jail escape, a denied command, a non-unique edit, or a stale hash all come back as tool-error data the agent can react to, never a thrown run-killer. Give `maxTrips` enough headroom for the read → edit → test cycle to converge.
40
+
41
+ ## Give the test command a path
42
+
43
+ `run_tests` defaults to `npm test`. It runs through the **same shell policy** as `run_shell`, so the agent can't run it unless:
44
+
45
+ - the executable basename is allowed — `shell: { allow: ["npm", "node"] }`, and
46
+ - the env can find it — `inheritEnv: ["PATH"]` (the workspace never inherits `process.env` wholesale).
47
+
48
+ Override the base command per agent: `ws.tools.runTests({ command: "pnpm test" })`. A `pattern` the model supplies is appended as a filter (e.g. `pnpm test cart-total`).
49
+
50
+ ## Least-privilege variants
51
+
52
+ ### A read-only reviewer
53
+
54
+ A reviewer should never write or run a shell. Pick exactly the read tools, or hand it a `readonly()` projection (which omits the mutating tools *and* rejects the mutating direct methods):
55
+
56
+ ```ts
57
+ const reviewer = ai.agent({
58
+ model,
59
+ tools: ws.tools.pick("readFile", "grep", "glob"),
60
+ });
61
+ await reviewer.execute("Review src/cart for off-by-one bugs and summarize.");
62
+
63
+ // Equivalent projection of the same jail:
64
+ ai.agent({ model, tools: ws.readonly().tools.all() }); // read_file + grep + glob only
65
+ ```
66
+
67
+ ### Scope an agent to one package
68
+
69
+ `scope(subdir)` narrows the jail so a per-package agent can't reach outside it — one sub-agent per package, isolated blast radius:
70
+
71
+ ```ts
72
+ const apiDev = ai.agent({ model, tools: ws.scope("packages/api").tools.all() });
73
+ const webDev = ai.agent({ model, tools: ws.scope("packages/web").tools.all() });
74
+ ```
75
+
76
+ ## Scaffold from code, then hand off
77
+
78
+ You don't have to let the agent do everything. Seed structure (and install deps) with the direct methods, then hand the tools to the agent for the creative part:
79
+
80
+ ```ts
81
+ await ws.writeFile("src/routes.ts", ROUTES_BOILERPLATE);
82
+ await ws.exec("npm install zod");
83
+ await dev.execute("Implement the handlers declared in src/routes.ts.");
84
+ ```
85
+
86
+ For a purely mechanical change, skip the LLM entirely: `ws.glob(...)` + a loop of `ws.editFile(...)` under the stale-hash guard.
87
+
88
+ ## Share one workspace across a team
89
+
90
+ One workspace is one jail and one tree. Pass it to several members of a supervisor, varying only the tool slice each gets — they all read/write the **same** files:
91
+
92
+ ```ts
93
+ const ws = ai.workspace({ cwd, shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] } });
94
+
95
+ ai.supervisor({
96
+ members: [
97
+ ai.agent({ /* executor */ model, tools: ws.tools.all() }),
98
+ ai.agent({ /* reviewer */ model, tools: ws.readonly().tools.all() }),
99
+ ai.agent({ /* tester */ model, tools: ws.tools.pick("readFile", "runTests") }),
100
+ ],
101
+ });
102
+ ```
103
+
104
+ ## See also
105
+
106
+ - [`@warlock.js/ai-workspace/use-a-workspace/SKILL.md`](@warlock.js/ai-workspace/use-a-workspace/SKILL.md) — building the workspace, the policy surface, the direct methods, `readonly` / `scope`, and the typed errors.
107
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — the agent loop, `maxTrips`, and the run report.
108
+ - [`@warlock.js/ai/run-supervisor/SKILL.md`](@warlock.js/ai/run-supervisor/SKILL.md) — coordinating the members that share one workspace.
109
+
110
+
111
+ ## use-a-workspace `@warlock.js/ai-workspace/use-a-workspace/SKILL.md`
112
+
113
+ ---
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`, `read_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`; ''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''; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — `@warlock.js/ai-workspace/build-loop-agent/SKILL.md`; defining a non-filesystem custom tool — `@warlock.js/ai/define-ai-tool/SKILL.md`.'
116
+ ---
117
+
118
+ # Use a workspace — a policy jail for an agent's filesystem + shell
119
+
120
+ `ai.workspace(policy)` returns a **workspace**: a bounded place an agent reads, writes, and runs commands. One policy, one shared rule set, two ways to touch it — the agent uses `ws.tools.*`; you use the direct methods. Import the package once for its side effect so the verb is registered on the shared `ai`:
121
+
122
+ ```ts
123
+ import "@warlock.js/ai-workspace";
124
+ import { ai } from "@warlock.js/ai";
125
+
126
+ const ws = ai.workspace({
127
+ cwd: "/srv/acme-api",
128
+ denyPaths: [".git/**", ".env*"],
129
+ shell: { allow: ["npm", "node"], inheritEnv: ["PATH"], timeoutMs: 60_000 },
130
+ read: { defaultLines: 2_000 },
131
+ backend: "local", // default; use "mock" for hermetic tests
132
+ });
133
+ ```
134
+
135
+ ## A policy jail, NOT OS isolation
136
+
137
+ This is least-privilege guardrails for a **trusted** agent — not a sandbox around hostile code. It enforces a `realpath`-resolved `cwd` jail, path allow/deny, a fail-closed shell allow/deny list, timeouts, output caps, and a non-inherited environment. It does **not** contain what an *allowed* process does once it runs. Real isolation (container / microVM) is a later phase.
138
+
139
+ ## The policy surface
140
+
141
+ | Field | What it does |
142
+ |---|---|
143
+ | `cwd` | **Absolute** jail root. Every path resolves against it and must stay inside. |
144
+ | `allowPaths` | Extra readable roots outside `cwd`. |
145
+ | `denyPaths` | Globs (`*`, `**`, `?`) blocked **even inside `cwd`** — e.g. `[".git/**", ".env*"]`. A bare dir name (`"node_modules"`) also blocks its contents. |
146
+ | `shell.allow` / `shell.deny` | Executable **basenames** matched against the command's leading token. **Deny wins.** An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
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
+ | `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
149
+ | `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (SIGKILL on expiry) and stdout/stderr byte cap. |
150
+ | `read.defaultLines` / `read.maxBytes` | Default read window + a byte ceiling. |
151
+ | `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
152
+
153
+ > **The most common gotcha:** `run_shell` fails to find `node`/`npm` because the env is empty. Add `shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] }`.
154
+
155
+ ## Two callers, one jail
156
+
157
+ ### Agent-facing — `ws.tools.*`
158
+
159
+ Each tool is a `ToolContract` ready for `ai.agent({ tools })`. The seven LLM-visible tool names are `read_file`, `edit_file`, `write_file`, `run_shell`, `run_tests`, `grep`, `glob`.
160
+
161
+ ```ts
162
+ ws.tools.all(); // all seven, canonical order
163
+ ws.tools.pick("readFile", "grep", "glob"); // a least-privilege subset
164
+ ws.tools.readFile({ name: "open" }); // one tool, optionally renamed
165
+ ws.tools.runTests({ command: "pnpm test" }); // override the test base command
166
+ ```
167
+
168
+ ### Human-facing — direct methods
169
+
170
+ The same policy seam, callable from code (no LLM):
171
+
172
+ ```ts
173
+ const { content, hash, totalLines } = await ws.readFile("src/index.ts", { offset: 1, limit: 200 });
174
+ await ws.writeFile("src/new.ts", "export const x = 1;\n");
175
+ await ws.editFile({ path: "src/index.ts", oldString: "old", newString: "new", expectHash: hash });
176
+ const { exitCode, stdout, stderr, timedOut, truncated } = await ws.exec("npm test");
177
+ const { matches } = await ws.grep("TODO", { glob: "src/**/*.ts", ignoreCase: true });
178
+ const paths = await ws.glob("src/models/**/*.ts");
179
+ await ws.exists("package.json");
180
+ await ws.mkdir("src/generated");
181
+ await ws.remove("dist");
182
+ ```
183
+
184
+ ## Read-before-edit (the stale-hash guard)
185
+
186
+ `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.
187
+
188
+ ## Errors flow as data, not throws
189
+
190
+ 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`:
191
+
192
+ ```ts
193
+ import { WorkspacePolicyError, WorkspaceEditError } from "@warlock.js/ai-workspace";
194
+
195
+ try {
196
+ await ws.exec("rm -rf /");
197
+ } catch (error) {
198
+ if (error instanceof WorkspacePolicyError && error.type === "denied-command") {
199
+ console.warn(`Blocked: ${error.command}`);
200
+ }
201
+ }
202
+ ```
203
+
204
+ - `WorkspacePolicyError.type`: `"path-escape"` (jail escape / deny glob) | `"denied-command"`.
205
+ - `WorkspaceEditError.type`: `"not-found"` | `"not-unique"` | `"stale-hash"`.
206
+
207
+ ## Composition — `readonly()` and `scope()`
208
+
209
+ ```ts
210
+ const ro = ws.readonly(); // vends only read/grep/glob; mutating direct methods reject
211
+ const api = ws.scope("packages/api"); // a sub-jailed workspace rooted at the subdir
212
+ ```
213
+
214
+ `readonly()` is the least-privilege reviewer projection — `tools.all()` on it returns `read_file` + `grep` + `glob` only, and `writeFile` / `editFile` / `exec` / `mkdir` / `remove` throw a `WorkspacePolicyError`. `scope(subdir)` narrows `cwd` to `join(cwd, subdir)` (same sub-policies, same backend), so a per-package agent gets a tighter blast radius.
215
+
216
+ ## Hermetic tests with the mock backend
217
+
218
+ `backend: "mock"` swaps the local disk for an in-memory `Map` + scripted `exec` — same policy, same tool/method surface, no filesystem:
219
+
220
+ ```ts
221
+ const ws = ai.workspace({ cwd: "/srv/app", backend: "mock", shell: { allow: ["npm"] } });
222
+ // (Seed files / scripted commands via createMockBackend when constructing a backend directly.)
223
+ ```
224
+
225
+ ## See also
226
+
227
+ - [`@warlock.js/ai-workspace/build-loop-agent/SKILL.md`](@warlock.js/ai-workspace/build-loop-agent/SKILL.md) — wiring these tools into a read → edit → test agent loop.
228
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent that consumes `ws.tools.all()`.
229
+ - [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — the `tool()` / `ToolContract` seam every workspace tool is built on.
230
+
231
+
package/llms.txt ADDED
@@ -0,0 +1,10 @@
1
+ # Warlock AI Workspace
2
+
3
+ > Package: `@warlock.js/ai-workspace`
4
+
5
+ > Policy-jailed filesystem + shell workspace for @warlock.js/ai agents
6
+
7
+ ## Skills
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 — `@warlock.js/ai-workspace/use-a-workspace/SKILL.md`; the agent loop / maxTrips / events themselves — `@warlock.js/ai/run-ai-agent/SKILL.md`.
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_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`; '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'; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — `@warlock.js/ai-workspace/build-loop-agent/SKILL.md`; defining a non-filesystem custom tool — `@warlock.js/ai/define-ai-tool/SKILL.md`.
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@warlock.js/ai-workspace",
3
+ "description": "Policy-jailed filesystem + shell workspace for @warlock.js/ai agents",
4
+ "keywords": [
5
+ "warlock",
6
+ "ai",
7
+ "workspace",
8
+ "agent",
9
+ "filesystem",
10
+ "shell",
11
+ "sandbox",
12
+ "tools"
13
+ ],
14
+ "author": "Hasan Zohdy",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/warlockjs/ai-workspace"
19
+ },
20
+ "dependencies": {
21
+ "@warlock.js/fs": "4.5.0"
22
+ },
23
+ "peerDependencies": {
24
+ "@warlock.js/ai": "4.5.0"
25
+ },
26
+ "version": "4.5.0",
27
+ "main": "./cjs/index.cjs",
28
+ "module": "./esm/index.mjs",
29
+ "types": "./esm/index.d.mts",
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./esm/index.d.mts",
34
+ "default": "./esm/index.mjs"
35
+ },
36
+ "require": {
37
+ "types": "./esm/index.d.mts",
38
+ "default": "./cjs/index.cjs"
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,13 @@
1
+ # `@warlock.js/ai-workspace` — skills index
2
+
3
+ Per-task skills. All cross-references use the form `@warlock.js/<pkg>/<skill>/SKILL.md`.
4
+
5
+ ## Skills
6
+
7
+ ### [`use-a-workspace/`](./use-a-workspace/SKILL.md)
8
+
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
+
11
+ ### [`build-loop-agent/`](./build-loop-agent/SKILL.md)
12
+
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.
@@ -0,0 +1,100 @@
1
+ ---
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 — `@warlock.js/ai-workspace/use-a-workspace/SKILL.md`; the agent loop / maxTrips / events themselves — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
4
+ ---
5
+
6
+ # Build a build-loop agent — read → edit → run tests, until green
7
+
8
+ The point of a workspace is to hand its tools to an agent and let it close the loop itself: read the failing spec, edit the source, run the tests, read the failure, fix again. The failure comes back as tool **data** (a non-zero `exitCode`, the captured `stderr`), not a thrown error — so the model can see what broke and self-correct.
9
+
10
+ ```ts
11
+ import "@warlock.js/ai-workspace";
12
+ import { ai } from "@warlock.js/ai";
13
+
14
+ const ws = ai.workspace({
15
+ cwd: "/srv/acme-api",
16
+ denyPaths: [".git/**", ".env*"],
17
+ shell: { allow: ["npm", "node"], inheritEnv: ["PATH"], timeoutMs: 120_000 },
18
+ });
19
+
20
+ const dev = ai.agent({
21
+ model,
22
+ tools: ws.tools.all(), // read_file, edit_file, write_file, run_shell, run_tests, grep, glob
23
+ maxTrips: 25, // give the loop room to iterate
24
+ });
25
+
26
+ await dev.execute("`npm test` fails on the cart-total suite. Make it green.");
27
+ ```
28
+
29
+ ## Why the loop works: errors as data
30
+
31
+ A failing test isn't a tool error — it's a normal result with `exitCode !== 0`. `run_tests` (and `run_shell`) return `{ exitCode, stdout, stderr, timedOut, truncated }` as the tool's `data`, so the model reads the failure on the next trip and tries again. The same is true of the policy/edit guards: a jail escape, a denied command, a non-unique edit, or a stale hash all come back as tool-error data the agent can react to, never a thrown run-killer. Give `maxTrips` enough headroom for the read → edit → test cycle to converge.
32
+
33
+ ## Give the test command a path
34
+
35
+ `run_tests` defaults to `npm test`. It runs through the **same shell policy** as `run_shell`, so the agent can't run it unless:
36
+
37
+ - the executable basename is allowed — `shell: { allow: ["npm", "node"] }`, and
38
+ - the env can find it — `inheritEnv: ["PATH"]` (the workspace never inherits `process.env` wholesale).
39
+
40
+ Override the base command per agent: `ws.tools.runTests({ command: "pnpm test" })`. A `pattern` the model supplies is appended as a filter (e.g. `pnpm test cart-total`).
41
+
42
+ ## Least-privilege variants
43
+
44
+ ### A read-only reviewer
45
+
46
+ A reviewer should never write or run a shell. Pick exactly the read tools, or hand it a `readonly()` projection (which omits the mutating tools *and* rejects the mutating direct methods):
47
+
48
+ ```ts
49
+ const reviewer = ai.agent({
50
+ model,
51
+ tools: ws.tools.pick("readFile", "grep", "glob"),
52
+ });
53
+ await reviewer.execute("Review src/cart for off-by-one bugs and summarize.");
54
+
55
+ // Equivalent projection of the same jail:
56
+ ai.agent({ model, tools: ws.readonly().tools.all() }); // read_file + grep + glob only
57
+ ```
58
+
59
+ ### Scope an agent to one package
60
+
61
+ `scope(subdir)` narrows the jail so a per-package agent can't reach outside it — one sub-agent per package, isolated blast radius:
62
+
63
+ ```ts
64
+ const apiDev = ai.agent({ model, tools: ws.scope("packages/api").tools.all() });
65
+ const webDev = ai.agent({ model, tools: ws.scope("packages/web").tools.all() });
66
+ ```
67
+
68
+ ## Scaffold from code, then hand off
69
+
70
+ You don't have to let the agent do everything. Seed structure (and install deps) with the direct methods, then hand the tools to the agent for the creative part:
71
+
72
+ ```ts
73
+ await ws.writeFile("src/routes.ts", ROUTES_BOILERPLATE);
74
+ await ws.exec("npm install zod");
75
+ await dev.execute("Implement the handlers declared in src/routes.ts.");
76
+ ```
77
+
78
+ For a purely mechanical change, skip the LLM entirely: `ws.glob(...)` + a loop of `ws.editFile(...)` under the stale-hash guard.
79
+
80
+ ## Share one workspace across a team
81
+
82
+ One workspace is one jail and one tree. Pass it to several members of a supervisor, varying only the tool slice each gets — they all read/write the **same** files:
83
+
84
+ ```ts
85
+ const ws = ai.workspace({ cwd, shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] } });
86
+
87
+ ai.supervisor({
88
+ members: [
89
+ ai.agent({ /* executor */ model, tools: ws.tools.all() }),
90
+ ai.agent({ /* reviewer */ model, tools: ws.readonly().tools.all() }),
91
+ ai.agent({ /* tester */ model, tools: ws.tools.pick("readFile", "runTests") }),
92
+ ],
93
+ });
94
+ ```
95
+
96
+ ## See also
97
+
98
+ - [`@warlock.js/ai-workspace/use-a-workspace/SKILL.md`](@warlock.js/ai-workspace/use-a-workspace/SKILL.md) — building the workspace, the policy surface, the direct methods, `readonly` / `scope`, and the typed errors.
99
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — the agent loop, `maxTrips`, and the run report.
100
+ - [`@warlock.js/ai/run-supervisor/SKILL.md`](@warlock.js/ai/run-supervisor/SKILL.md) — coordinating the members that share one workspace.
@@ -0,0 +1,117 @@
1
+ ---
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`, `read_file`/`edit_file`/`write_file`/`run_shell`/`run_tests`; ''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''; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — `@warlock.js/ai-workspace/build-loop-agent/SKILL.md`; defining a non-filesystem custom tool — `@warlock.js/ai/define-ai-tool/SKILL.md`.'
4
+ ---
5
+
6
+ # Use a workspace — a policy jail for an agent's filesystem + shell
7
+
8
+ `ai.workspace(policy)` returns a **workspace**: a bounded place an agent reads, writes, and runs commands. One policy, one shared rule set, two ways to touch it — the agent uses `ws.tools.*`; you use the direct methods. Import the package once for its side effect so the verb is registered on the shared `ai`:
9
+
10
+ ```ts
11
+ import "@warlock.js/ai-workspace";
12
+ import { ai } from "@warlock.js/ai";
13
+
14
+ const ws = ai.workspace({
15
+ cwd: "/srv/acme-api",
16
+ denyPaths: [".git/**", ".env*"],
17
+ shell: { allow: ["npm", "node"], inheritEnv: ["PATH"], timeoutMs: 60_000 },
18
+ read: { defaultLines: 2_000 },
19
+ backend: "local", // default; use "mock" for hermetic tests
20
+ });
21
+ ```
22
+
23
+ ## A policy jail, NOT OS isolation
24
+
25
+ This is least-privilege guardrails for a **trusted** agent — not a sandbox around hostile code. It enforces a `realpath`-resolved `cwd` jail, path allow/deny, a fail-closed shell allow/deny list, timeouts, output caps, and a non-inherited environment. It does **not** contain what an *allowed* process does once it runs. Real isolation (container / microVM) is a later phase.
26
+
27
+ ## The policy surface
28
+
29
+ | Field | What it does |
30
+ |---|---|
31
+ | `cwd` | **Absolute** jail root. Every path resolves against it and must stay inside. |
32
+ | `allowPaths` | Extra readable roots outside `cwd`. |
33
+ | `denyPaths` | Globs (`*`, `**`, `?`) blocked **even inside `cwd`** — e.g. `[".git/**", ".env*"]`. A bare dir name (`"node_modules"`) also blocks its contents. |
34
+ | `shell.allow` / `shell.deny` | Executable **basenames** matched against the command's leading token. **Deny wins.** An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
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
+ | `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
37
+ | `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (SIGKILL on expiry) and stdout/stderr byte cap. |
38
+ | `read.defaultLines` / `read.maxBytes` | Default read window + a byte ceiling. |
39
+ | `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
40
+
41
+ > **The most common gotcha:** `run_shell` fails to find `node`/`npm` because the env is empty. Add `shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] }`.
42
+
43
+ ## Two callers, one jail
44
+
45
+ ### Agent-facing — `ws.tools.*`
46
+
47
+ Each tool is a `ToolContract` ready for `ai.agent({ tools })`. The seven LLM-visible tool names are `read_file`, `edit_file`, `write_file`, `run_shell`, `run_tests`, `grep`, `glob`.
48
+
49
+ ```ts
50
+ ws.tools.all(); // all seven, canonical order
51
+ ws.tools.pick("readFile", "grep", "glob"); // a least-privilege subset
52
+ ws.tools.readFile({ name: "open" }); // one tool, optionally renamed
53
+ ws.tools.runTests({ command: "pnpm test" }); // override the test base command
54
+ ```
55
+
56
+ ### Human-facing — direct methods
57
+
58
+ The same policy seam, callable from code (no LLM):
59
+
60
+ ```ts
61
+ const { content, hash, totalLines } = await ws.readFile("src/index.ts", { offset: 1, limit: 200 });
62
+ await ws.writeFile("src/new.ts", "export const x = 1;\n");
63
+ await ws.editFile({ path: "src/index.ts", oldString: "old", newString: "new", expectHash: hash });
64
+ const { exitCode, stdout, stderr, timedOut, truncated } = await ws.exec("npm test");
65
+ const { matches } = await ws.grep("TODO", { glob: "src/**/*.ts", ignoreCase: true });
66
+ const paths = await ws.glob("src/models/**/*.ts");
67
+ await ws.exists("package.json");
68
+ await ws.mkdir("src/generated");
69
+ await ws.remove("dist");
70
+ ```
71
+
72
+ ## Read-before-edit (the stale-hash guard)
73
+
74
+ `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.
75
+
76
+ ## Errors flow as data, not throws
77
+
78
+ 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`:
79
+
80
+ ```ts
81
+ import { WorkspacePolicyError, WorkspaceEditError } from "@warlock.js/ai-workspace";
82
+
83
+ try {
84
+ await ws.exec("rm -rf /");
85
+ } catch (error) {
86
+ if (error instanceof WorkspacePolicyError && error.type === "denied-command") {
87
+ console.warn(`Blocked: ${error.command}`);
88
+ }
89
+ }
90
+ ```
91
+
92
+ - `WorkspacePolicyError.type`: `"path-escape"` (jail escape / deny glob) | `"denied-command"`.
93
+ - `WorkspaceEditError.type`: `"not-found"` | `"not-unique"` | `"stale-hash"`.
94
+
95
+ ## Composition — `readonly()` and `scope()`
96
+
97
+ ```ts
98
+ const ro = ws.readonly(); // vends only read/grep/glob; mutating direct methods reject
99
+ const api = ws.scope("packages/api"); // a sub-jailed workspace rooted at the subdir
100
+ ```
101
+
102
+ `readonly()` is the least-privilege reviewer projection — `tools.all()` on it returns `read_file` + `grep` + `glob` only, and `writeFile` / `editFile` / `exec` / `mkdir` / `remove` throw a `WorkspacePolicyError`. `scope(subdir)` narrows `cwd` to `join(cwd, subdir)` (same sub-policies, same backend), so a per-package agent gets a tighter blast radius.
103
+
104
+ ## Hermetic tests with the mock backend
105
+
106
+ `backend: "mock"` swaps the local disk for an in-memory `Map` + scripted `exec` — same policy, same tool/method surface, no filesystem:
107
+
108
+ ```ts
109
+ const ws = ai.workspace({ cwd: "/srv/app", backend: "mock", shell: { allow: ["npm"] } });
110
+ // (Seed files / scripted commands via createMockBackend when constructing a backend directly.)
111
+ ```
112
+
113
+ ## See also
114
+
115
+ - [`@warlock.js/ai-workspace/build-loop-agent/SKILL.md`](@warlock.js/ai-workspace/build-loop-agent/SKILL.md) — wiring these tools into a read → edit → test agent loop.
116
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent that consumes `ws.tools.all()`.
117
+ - [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — the `tool()` / `ToolContract` seam every workspace tool is built on.