@warlock.js/ai-workspace 5.29.0 → 5.31.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 (120) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +40 -21
  3. package/cjs/index.cjs +3295 -360
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts +1 -1
  6. package/esm/backends/local.d.mts.map +1 -1
  7. package/esm/backends/local.mjs +199 -31
  8. package/esm/backends/local.mjs.map +1 -1
  9. package/esm/backends/mock.d.mts +12 -6
  10. package/esm/backends/mock.d.mts.map +1 -1
  11. package/esm/backends/mock.mjs +45 -3
  12. package/esm/backends/mock.mjs.map +1 -1
  13. package/esm/contracts/index.d.mts +3 -3
  14. package/esm/contracts/tool-io.type.d.mts +248 -11
  15. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  16. package/esm/contracts/workspace-backend.contract.d.mts +78 -2
  17. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts +41 -16
  19. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  20. package/esm/contracts/workspace-policy.type.d.mts +142 -14
  21. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  22. package/esm/contracts/workspace.contract.d.mts +75 -22
  23. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  24. package/esm/errors.d.mts +80 -6
  25. package/esm/errors.d.mts.map +1 -1
  26. package/esm/errors.mjs +23 -2
  27. package/esm/errors.mjs.map +1 -1
  28. package/esm/index.d.mts +13 -5
  29. package/esm/index.mjs +10 -2
  30. package/esm/ops.d.mts +22 -3
  31. package/esm/ops.d.mts.map +1 -1
  32. package/esm/ops.mjs +341 -82
  33. package/esm/ops.mjs.map +1 -1
  34. package/esm/policy/jail.mjs +73 -0
  35. package/esm/policy/jail.mjs.map +1 -0
  36. package/esm/policy/policy.d.mts +26 -16
  37. package/esm/policy/policy.d.mts.map +1 -1
  38. package/esm/policy/policy.mjs +62 -86
  39. package/esm/policy/policy.mjs.map +1 -1
  40. package/esm/policy/shell-check.d.mts +64 -0
  41. package/esm/policy/shell-check.d.mts.map +1 -0
  42. package/esm/policy/shell-check.mjs +627 -0
  43. package/esm/policy/shell-check.mjs.map +1 -0
  44. package/esm/policy/shell-parser.mjs +802 -0
  45. package/esm/policy/shell-parser.mjs.map +1 -0
  46. package/esm/policy/tokenize-command.d.mts +63 -0
  47. package/esm/policy/tokenize-command.d.mts.map +1 -0
  48. package/esm/policy/tokenize-command.mjs +76 -24
  49. package/esm/policy/tokenize-command.mjs.map +1 -1
  50. package/esm/shell-jobs.d.mts +45 -0
  51. package/esm/shell-jobs.d.mts.map +1 -0
  52. package/esm/shell-jobs.mjs +247 -0
  53. package/esm/shell-jobs.mjs.map +1 -0
  54. package/esm/todos.d.mts +22 -0
  55. package/esm/todos.d.mts.map +1 -0
  56. package/esm/todos.mjs +73 -0
  57. package/esm/todos.mjs.map +1 -0
  58. package/esm/tools/edit-file.d.mts +1 -1
  59. package/esm/tools/edit-file.d.mts.map +1 -1
  60. package/esm/tools/edit-file.mjs +3 -2
  61. package/esm/tools/edit-file.mjs.map +1 -1
  62. package/esm/tools/glob.d.mts +1 -1
  63. package/esm/tools/glob.d.mts.map +1 -1
  64. package/esm/tools/glob.mjs +3 -2
  65. package/esm/tools/glob.mjs.map +1 -1
  66. package/esm/tools/grep.d.mts +1 -1
  67. package/esm/tools/grep.d.mts.map +1 -1
  68. package/esm/tools/grep.mjs +3 -2
  69. package/esm/tools/grep.mjs.map +1 -1
  70. package/esm/tools/json-schemas.mjs +175 -0
  71. package/esm/tools/json-schemas.mjs.map +1 -0
  72. package/esm/tools/list-dir.d.mts +32 -0
  73. package/esm/tools/list-dir.d.mts.map +1 -0
  74. package/esm/tools/list-dir.mjs +50 -0
  75. package/esm/tools/list-dir.mjs.map +1 -0
  76. package/esm/tools/multi-edit.d.mts +38 -0
  77. package/esm/tools/multi-edit.d.mts.map +1 -0
  78. package/esm/tools/multi-edit.mjs +121 -0
  79. package/esm/tools/multi-edit.mjs.map +1 -0
  80. package/esm/tools/read-file.d.mts +5 -5
  81. package/esm/tools/read-file.d.mts.map +1 -1
  82. package/esm/tools/read-file.mjs +10 -20
  83. package/esm/tools/read-file.mjs.map +1 -1
  84. package/esm/tools/run-shell.d.mts +39 -13
  85. package/esm/tools/run-shell.d.mts.map +1 -1
  86. package/esm/tools/run-shell.mjs +60 -47
  87. package/esm/tools/run-shell.mjs.map +1 -1
  88. package/esm/tools/run-tests.d.mts +11 -2
  89. package/esm/tools/run-tests.d.mts.map +1 -1
  90. package/esm/tools/run-tests.mjs +15 -7
  91. package/esm/tools/run-tests.mjs.map +1 -1
  92. package/esm/tools/schema.mjs +24 -19
  93. package/esm/tools/schema.mjs.map +1 -1
  94. package/esm/tools/shell-kill.d.mts +25 -0
  95. package/esm/tools/shell-kill.d.mts.map +1 -0
  96. package/esm/tools/shell-kill.mjs +36 -0
  97. package/esm/tools/shell-kill.mjs.map +1 -0
  98. package/esm/tools/shell-output.d.mts +30 -0
  99. package/esm/tools/shell-output.d.mts.map +1 -0
  100. package/esm/tools/shell-output.mjs +49 -0
  101. package/esm/tools/shell-output.mjs.map +1 -0
  102. package/esm/tools/todo.d.mts +43 -0
  103. package/esm/tools/todo.d.mts.map +1 -0
  104. package/esm/tools/todo.mjs +126 -0
  105. package/esm/tools/todo.mjs.map +1 -0
  106. package/esm/tools/write-file.d.mts +1 -1
  107. package/esm/tools/write-file.d.mts.map +1 -1
  108. package/esm/tools/write-file.mjs +3 -2
  109. package/esm/tools/write-file.mjs.map +1 -1
  110. package/esm/workspace.d.mts +12 -9
  111. package/esm/workspace.d.mts.map +1 -1
  112. package/esm/workspace.mjs +109 -28
  113. package/esm/workspace.mjs.map +1 -1
  114. package/llms-full.txt +192 -35
  115. package/llms.txt +1 -1
  116. package/package.json +5 -4
  117. package/skills/README.md +2 -2
  118. package/skills/build-loop-agent/SKILL.md +7 -7
  119. package/skills/index.md +6 -1
  120. package/skills/use-a-workspace/SKILL.md +185 -28
@@ -0,0 +1,49 @@
1
+ import { objectSchema, optionalNumberField, stringField } from "./schema.mjs";
2
+ import { SHELL_OUTPUT_JSON_SCHEMA } from "./json-schemas.mjs";
3
+ import { tool } from "@warlock.js/ai";
4
+
5
+ //#region ../../packages/ai-workspace/src/tools/shell-output.ts
6
+ /** Default tool name exposed to the LLM. */
7
+ const DEFAULT_NAME = "shell_output";
8
+ /** Input schema for the `shell_output` tool. */
9
+ const inputSchema = objectSchema({
10
+ id: stringField(),
11
+ since: optionalNumberField(),
12
+ waitMs: optionalNumberField()
13
+ }, SHELL_OUTPUT_JSON_SCHEMA);
14
+ /**
15
+ * Build the agent-facing `shell_output` tool: the new output of a
16
+ * background command started with `run_shell({ background: true })`, plus
17
+ * its state (`running` / `exited` / `killed` / `timed-out`, `exitCode`).
18
+ *
19
+ * Without `since` it returns everything produced since the previous read;
20
+ * `cursor` in the result is the position to pass as `since` next time.
21
+ * `waitMs` (at most 30 s) waits for new output or for the command to stop
22
+ * when there is nothing new yet — the agent run's signal ends the wait
23
+ * early. Only the newest `shell.maxOutputBytes` of a command's output are
24
+ * kept; `droppedBytes` says how much the reader missed.
25
+ *
26
+ * @param ops - The shared, policy-enforced operation layer.
27
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
28
+ *
29
+ * @example
30
+ * const { data } = await makeShellOutputTool(ops).invoke({ id: "job-1", waitMs: 2_000 });
31
+ * if (data?.stdout.includes("ready")) console.log("dev server is up");
32
+ */
33
+ function makeShellOutputTool(ops, options) {
34
+ return tool({
35
+ name: options?.name ?? DEFAULT_NAME,
36
+ description: "Read new output of a background command started with run_shell (background: true) and see whether it is still running. Returns output since the last read; waitMs waits briefly for more.",
37
+ action: (input) => `Reading output of ${typeof input.id === "string" ? input.id : "a background command"}`,
38
+ input: inputSchema,
39
+ execute: (input, ctx) => ops.shellOutput(input.id, {
40
+ ...input.since !== void 0 ? { since: input.since } : {},
41
+ ...input.waitMs !== void 0 ? { waitMs: input.waitMs } : {},
42
+ ...ctx?.signal ? { signal: ctx.signal } : {}
43
+ })
44
+ });
45
+ }
46
+
47
+ //#endregion
48
+ export { makeShellOutputTool };
49
+ //# sourceMappingURL=shell-output.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-output.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/shell-output.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type { ShellOutputInput, ShellOutputResult, WorkspaceOps } from \"../contracts\";\nimport { SHELL_OUTPUT_JSON_SCHEMA } from \"./json-schemas\";\nimport { objectSchema, optionalNumberField, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"shell_output\";\n\n/** Input schema for the `shell_output` tool. */\nconst inputSchema = objectSchema<ShellOutputInput>(\n {\n id: stringField(),\n since: optionalNumberField(),\n waitMs: optionalNumberField(),\n },\n SHELL_OUTPUT_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `shell_output` tool: the new output of a\n * background command started with `run_shell({ background: true })`, plus\n * its state (`running` / `exited` / `killed` / `timed-out`, `exitCode`).\n *\n * Without `since` it returns everything produced since the previous read;\n * `cursor` in the result is the position to pass as `since` next time.\n * `waitMs` (at most 30 s) waits for new output or for the command to stop\n * when there is nothing new yet — the agent run's signal ends the wait\n * early. Only the newest `shell.maxOutputBytes` of a command's output are\n * kept; `droppedBytes` says how much the reader missed.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const { data } = await makeShellOutputTool(ops).invoke({ id: \"job-1\", waitMs: 2_000 });\n * if (data?.stdout.includes(\"ready\")) console.log(\"dev server is up\");\n */\nexport function makeShellOutputTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<ShellOutputInput, ShellOutputResult> {\n return tool<ShellOutputInput, ShellOutputResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Read new output of a background command started with run_shell \" +\n \"(background: true) and see whether it is still running. Returns \" +\n \"output since the last read; waitMs waits briefly for more.\",\n action: (input) =>\n `Reading output of ${typeof input.id === \"string\" ? input.id : \"a background command\"}`,\n input: inputSchema,\n execute: (input, ctx) =>\n ops.shellOutput(input.id, {\n ...(input.since !== undefined ? { since: input.since } : {}),\n ...(input.waitMs !== undefined ? { waitMs: input.waitMs } : {}),\n ...(ctx?.signal ? { signal: ctx.signal } : {}),\n }),\n });\n}\n"],"mappings":";;;;;;AAMA,MAAM,eAAe;;AAGrB,MAAM,cAAc,aAClB;CACE,IAAI,YAAY;CAChB,OAAO,oBAAoB;CAC3B,QAAQ,oBAAoB;AAC9B,GACA,wBACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBACd,KACA,SACmD;CACnD,OAAO,KAA0C;EAC/C,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,SAAS,UACP,qBAAqB,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK;EACjE,OAAO;EACP,UAAU,OAAO,QACf,IAAI,YAAY,MAAM,IAAI;GACxB,GAAI,MAAM,UAAU,SAAY,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;GAC1D,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;GAC7D,GAAI,KAAK,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;EAC9C,CAAC;CACL,CAAC;AACH"}
@@ -0,0 +1,43 @@
1
+ import { TodoListResult, TodoWriteInput } from "../contracts/tool-io.type.mjs";
2
+ import { TodoStore } from "../todos.mjs";
3
+ import { ToolContract } from "@warlock.js/ai";
4
+
5
+ //#region ../../packages/ai-workspace/src/tools/todo.d.ts
6
+ /**
7
+ * Build the agent-facing `todo_write` tool over a workspace's todo list:
8
+ * the model sends the **complete** updated list (each item `content` +
9
+ * `status` of `pending` / `in_progress` / `completed`, optional `id`) and it
10
+ * replaces the old one. The list lives in memory, per workspace (shared by
11
+ * its `readonly()` / `scope()` projections), for the length of the run.
12
+ *
13
+ * @param store - The workspace's todo list.
14
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
15
+ *
16
+ * @example
17
+ * await makeTodoWriteTool(store).invoke({
18
+ * todos: [
19
+ * { content: "Reproduce the failing test", status: "completed" },
20
+ * { content: "Fix the rounding bug", status: "in_progress" },
21
+ * ],
22
+ * });
23
+ */
24
+ declare function makeTodoWriteTool(store: TodoStore, options?: {
25
+ name?: string;
26
+ }): ToolContract<TodoWriteInput, TodoListResult>;
27
+ /**
28
+ * Build the agent-facing `todo_read` tool: the workspace's current todo
29
+ * list and a count per status.
30
+ *
31
+ * @param store - The workspace's todo list.
32
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
33
+ *
34
+ * @example
35
+ * const { data } = await makeTodoReadTool(store).invoke({});
36
+ * console.log(data?.counts.pending);
37
+ */
38
+ declare function makeTodoReadTool(store: TodoStore, options?: {
39
+ name?: string;
40
+ }): ToolContract<Record<string, never>, TodoListResult>;
41
+ //#endregion
42
+ export { makeTodoReadTool, makeTodoWriteTool };
43
+ //# sourceMappingURL=todo.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todo.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/todo.ts"],"mappings":";;;;;;;;AA8HA;;;;;;;;;;;;;;;iBAAgB,iBAAA,CACd,KAAA,EAAO,SAAA,EACP,OAAA;EAAY,IAAA;AAAA,IACX,YAAA,CAAa,cAAA,EAAgB,cAAA;AAwBhC;;;;;;;;;;;AAAA,iBAAgB,gBAAA,CACd,KAAA,EAAO,SAAA,EACP,OAAA;EAAY,IAAA;AAAA,IACX,YAAA,CAAa,MAAA,iBAAuB,cAAA"}
@@ -0,0 +1,126 @@
1
+ import { withJsonSchema } from "./schema.mjs";
2
+ import { TODO_READ_JSON_SCHEMA, TODO_WRITE_JSON_SCHEMA } from "./json-schemas.mjs";
3
+ import { TODO_STATUSES, todoListProblems } from "../todos.mjs";
4
+ import { tool } from "@warlock.js/ai";
5
+
6
+ //#region ../../packages/ai-workspace/src/tools/todo.ts
7
+ /** Whether `value` is a todo status. */
8
+ function isTodoStatus(value) {
9
+ return typeof value === "string" && TODO_STATUSES.includes(value);
10
+ }
11
+ /** Input schema for the `todo_write` tool. */
12
+ const todoWriteSchema = withJsonSchema({ "~standard": {
13
+ version: 1,
14
+ vendor: "ai-workspace",
15
+ validate(value) {
16
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return { issues: [{ message: "input must be an object" }] };
17
+ const raw = value.todos;
18
+ if (!Array.isArray(raw) || raw.length > 100) return { issues: [{
19
+ message: `"todos" must be an array of at most ${100} items`,
20
+ path: ["todos"]
21
+ }] };
22
+ const issues = [];
23
+ const todos = [];
24
+ raw.forEach((item, index) => {
25
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
26
+ issues.push({
27
+ message: `todos[${index}] must be an object`,
28
+ path: ["todos", index]
29
+ });
30
+ return;
31
+ }
32
+ const candidate = item;
33
+ if (typeof candidate.content !== "string") issues.push({
34
+ message: `todos[${index}].content must be a string`,
35
+ path: [
36
+ "todos",
37
+ index,
38
+ "content"
39
+ ]
40
+ });
41
+ if (!isTodoStatus(candidate.status)) issues.push({
42
+ message: `todos[${index}].status must be one of ${TODO_STATUSES.join(", ")}`,
43
+ path: [
44
+ "todos",
45
+ index,
46
+ "status"
47
+ ]
48
+ });
49
+ if (candidate.id !== void 0 && typeof candidate.id !== "string") issues.push({
50
+ message: `todos[${index}].id must be a string when provided`,
51
+ path: [
52
+ "todos",
53
+ index,
54
+ "id"
55
+ ]
56
+ });
57
+ if (typeof candidate.content === "string" && isTodoStatus(candidate.status)) todos.push({
58
+ content: candidate.content,
59
+ status: candidate.status,
60
+ ...typeof candidate.id === "string" ? { id: candidate.id } : {}
61
+ });
62
+ });
63
+ if (issues.length === 0) for (const problem of todoListProblems(todos)) issues.push({
64
+ message: problem,
65
+ path: ["todos"]
66
+ });
67
+ return issues.length > 0 ? { issues } : { value: { todos } };
68
+ }
69
+ } }, TODO_WRITE_JSON_SCHEMA);
70
+ /** Input schema for the `todo_read` tool (no arguments). */
71
+ const todoReadSchema = withJsonSchema({ "~standard": {
72
+ version: 1,
73
+ vendor: "ai-workspace",
74
+ validate: () => ({ value: {} })
75
+ } }, TODO_READ_JSON_SCHEMA);
76
+ /**
77
+ * Build the agent-facing `todo_write` tool over a workspace's todo list:
78
+ * the model sends the **complete** updated list (each item `content` +
79
+ * `status` of `pending` / `in_progress` / `completed`, optional `id`) and it
80
+ * replaces the old one. The list lives in memory, per workspace (shared by
81
+ * its `readonly()` / `scope()` projections), for the length of the run.
82
+ *
83
+ * @param store - The workspace's todo list.
84
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
85
+ *
86
+ * @example
87
+ * await makeTodoWriteTool(store).invoke({
88
+ * todos: [
89
+ * { content: "Reproduce the failing test", status: "completed" },
90
+ * { content: "Fix the rounding bug", status: "in_progress" },
91
+ * ],
92
+ * });
93
+ */
94
+ function makeTodoWriteTool(store, options) {
95
+ return tool({
96
+ name: options?.name ?? "todo_write",
97
+ description: "Replace your todo list for this task with the complete updated list (each item: content and status pending | in_progress | completed). Use it to plan multi-step work and to mark progress.",
98
+ action: "Updating the todo list",
99
+ input: todoWriteSchema,
100
+ execute: async (input) => store.write(input.todos)
101
+ });
102
+ }
103
+ /**
104
+ * Build the agent-facing `todo_read` tool: the workspace's current todo
105
+ * list and a count per status.
106
+ *
107
+ * @param store - The workspace's todo list.
108
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
109
+ *
110
+ * @example
111
+ * const { data } = await makeTodoReadTool(store).invoke({});
112
+ * console.log(data?.counts.pending);
113
+ */
114
+ function makeTodoReadTool(store, options) {
115
+ return tool({
116
+ name: options?.name ?? "todo_read",
117
+ description: "Read your current todo list for this task.",
118
+ action: "Reading the todo list",
119
+ input: todoReadSchema,
120
+ execute: async () => store.read()
121
+ });
122
+ }
123
+
124
+ //#endregion
125
+ export { makeTodoReadTool, makeTodoWriteTool };
126
+ //# sourceMappingURL=todo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todo.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/todo.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { MAX_TODOS, TODO_STATUSES, type TodoStore, todoListProblems } from \"../todos\";\nimport type {\n TodoListResult,\n TodoWriteInput,\n WorkspaceTodoInput,\n WorkspaceTodoStatus,\n} from \"../contracts\";\nimport { TODO_READ_JSON_SCHEMA, TODO_WRITE_JSON_SCHEMA } from \"./json-schemas\";\nimport { withJsonSchema } from \"./schema\";\n\n/** Whether `value` is a todo status. */\nfunction isTodoStatus(value: unknown): value is WorkspaceTodoStatus {\n return typeof value === \"string\" && (TODO_STATUSES as readonly string[]).includes(value);\n}\n\n/** Input schema for the `todo_write` tool. */\nconst todoWriteSchema = withJsonSchema<TodoWriteInput>(\n {\n \"~standard\": {\n version: 1,\n vendor: \"ai-workspace\",\n validate(value) {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const raw = (value as Record<string, unknown>).todos;\n\n if (!Array.isArray(raw) || raw.length > MAX_TODOS) {\n return {\n issues: [\n {\n message: `\"todos\" must be an array of at most ${MAX_TODOS} items`,\n path: [\"todos\"],\n },\n ],\n };\n }\n\n const issues: StandardSchemaV1.Issue[] = [];\n const todos: WorkspaceTodoInput[] = [];\n\n raw.forEach((item: unknown, index: number) => {\n if (typeof item !== \"object\" || item === null || Array.isArray(item)) {\n issues.push({ message: `todos[${index}] must be an object`, path: [\"todos\", index] });\n\n return;\n }\n\n const candidate = item as Record<string, unknown>;\n\n if (typeof candidate.content !== \"string\") {\n issues.push({\n message: `todos[${index}].content must be a string`,\n path: [\"todos\", index, \"content\"],\n });\n }\n\n if (!isTodoStatus(candidate.status)) {\n issues.push({\n message: `todos[${index}].status must be one of ${TODO_STATUSES.join(\", \")}`,\n path: [\"todos\", index, \"status\"],\n });\n }\n\n if (candidate.id !== undefined && typeof candidate.id !== \"string\") {\n issues.push({\n message: `todos[${index}].id must be a string when provided`,\n path: [\"todos\", index, \"id\"],\n });\n }\n\n if (typeof candidate.content === \"string\" && isTodoStatus(candidate.status)) {\n todos.push({\n content: candidate.content,\n status: candidate.status,\n ...(typeof candidate.id === \"string\" ? { id: candidate.id } : {}),\n });\n }\n });\n\n if (issues.length === 0) {\n for (const problem of todoListProblems(todos)) {\n issues.push({ message: problem, path: [\"todos\"] });\n }\n }\n\n return issues.length > 0 ? { issues } : { value: { todos } };\n },\n },\n },\n TODO_WRITE_JSON_SCHEMA,\n);\n\n/** Input schema for the `todo_read` tool (no arguments). */\nconst todoReadSchema = withJsonSchema<Record<string, never>>(\n {\n \"~standard\": {\n version: 1,\n vendor: \"ai-workspace\",\n validate: () => ({ value: {} }),\n },\n },\n TODO_READ_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `todo_write` tool over a workspace's todo list:\n * the model sends the **complete** updated list (each item `content` +\n * `status` of `pending` / `in_progress` / `completed`, optional `id`) and it\n * replaces the old one. The list lives in memory, per workspace (shared by\n * its `readonly()` / `scope()` projections), for the length of the run.\n *\n * @param store - The workspace's todo list.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * await makeTodoWriteTool(store).invoke({\n * todos: [\n * { content: \"Reproduce the failing test\", status: \"completed\" },\n * { content: \"Fix the rounding bug\", status: \"in_progress\" },\n * ],\n * });\n */\nexport function makeTodoWriteTool(\n store: TodoStore,\n options?: { name?: string },\n): ToolContract<TodoWriteInput, TodoListResult> {\n return tool<TodoWriteInput, TodoListResult>({\n name: options?.name ?? \"todo_write\",\n description:\n \"Replace your todo list for this task with the complete updated list \" +\n \"(each item: content and status pending | in_progress | completed). \" +\n \"Use it to plan multi-step work and to mark progress.\",\n action: \"Updating the todo list\",\n input: todoWriteSchema,\n execute: async (input) => store.write(input.todos),\n });\n}\n\n/**\n * Build the agent-facing `todo_read` tool: the workspace's current todo\n * list and a count per status.\n *\n * @param store - The workspace's todo list.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const { data } = await makeTodoReadTool(store).invoke({});\n * console.log(data?.counts.pending);\n */\nexport function makeTodoReadTool(\n store: TodoStore,\n options?: { name?: string },\n): ToolContract<Record<string, never>, TodoListResult> {\n return tool<Record<string, never>, TodoListResult>({\n name: options?.name ?? \"todo_read\",\n description: \"Read your current todo list for this task.\",\n action: \"Reading the todo list\",\n input: todoReadSchema,\n execute: async () => store.read(),\n });\n}\n"],"mappings":";;;;;;;AAaA,SAAS,aAAa,OAA8C;CAClE,OAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;;AAGA,MAAM,kBAAkB,eACtB,EACE,aAAa;CACX,SAAS;CACT,QAAQ;CACR,SAAS,OAAO;EACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;EAG5D,MAAM,MAAO,MAAkC;EAE/C,IAAI,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAI,cAC7B,OAAO,EACL,QAAQ,CACN;GACE,SAAS,2CAAiD;GAC1D,MAAM,CAAC,OAAO;EAChB,CACF,EACF;EAGF,MAAM,SAAmC,CAAC;EAC1C,MAAM,QAA8B,CAAC;EAErC,IAAI,SAAS,MAAe,UAAkB;GAC5C,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG;IACpE,OAAO,KAAK;KAAE,SAAS,SAAS,MAAM;KAAsB,MAAM,CAAC,SAAS,KAAK;IAAE,CAAC;IAEpF;GACF;GAEA,MAAM,YAAY;GAElB,IAAI,OAAO,UAAU,YAAY,UAC/B,OAAO,KAAK;IACV,SAAS,SAAS,MAAM;IACxB,MAAM;KAAC;KAAS;KAAO;IAAS;GAClC,CAAC;GAGH,IAAI,CAAC,aAAa,UAAU,MAAM,GAChC,OAAO,KAAK;IACV,SAAS,SAAS,MAAM,0BAA0B,cAAc,KAAK,IAAI;IACzE,MAAM;KAAC;KAAS;KAAO;IAAQ;GACjC,CAAC;GAGH,IAAI,UAAU,OAAO,UAAa,OAAO,UAAU,OAAO,UACxD,OAAO,KAAK;IACV,SAAS,SAAS,MAAM;IACxB,MAAM;KAAC;KAAS;KAAO;IAAI;GAC7B,CAAC;GAGH,IAAI,OAAO,UAAU,YAAY,YAAY,aAAa,UAAU,MAAM,GACxE,MAAM,KAAK;IACT,SAAS,UAAU;IACnB,QAAQ,UAAU;IAClB,GAAI,OAAO,UAAU,OAAO,WAAW,EAAE,IAAI,UAAU,GAAG,IAAI,CAAC;GACjE,CAAC;EAEL,CAAC;EAED,IAAI,OAAO,WAAW,GACpB,KAAK,MAAM,WAAW,iBAAiB,KAAK,GAC1C,OAAO,KAAK;GAAE,SAAS;GAAS,MAAM,CAAC,OAAO;EAAE,CAAC;EAIrD,OAAO,OAAO,SAAS,IAAI,EAAE,OAAO,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE;CAC7D;AACF,EACF,GACA,sBACF;;AAGA,MAAM,iBAAiB,eACrB,EACE,aAAa;CACX,SAAS;CACT,QAAQ;CACR,iBAAiB,EAAE,OAAO,CAAC,EAAE;AAC/B,EACF,GACA,qBACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,kBACd,OACA,SAC8C;CAC9C,OAAO,KAAqC;EAC1C,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,QAAQ;EACR,OAAO;EACP,SAAS,OAAO,UAAU,MAAM,MAAM,MAAM,KAAK;CACnD,CAAC;AACH;;;;;;;;;;;;AAaA,SAAgB,iBACd,OACA,SACqD;CACrD,OAAO,KAA4C;EACjD,MAAM,SAAS,QAAQ;EACvB,aAAa;EACb,QAAQ;EACR,OAAO;EACP,SAAS,YAAY,MAAM,KAAK;CAClC,CAAC;AACH"}
@@ -2,7 +2,7 @@ import { WriteFileInput, WriteFileResult } from "../contracts/tool-io.type.mjs";
2
2
  import { WorkspaceOps } from "../contracts/workspace-ops.contract.mjs";
3
3
  import { ToolContract } from "@warlock.js/ai";
4
4
 
5
- //#region ../ai-workspace/src/tools/write-file.d.ts
5
+ //#region ../../packages/ai-workspace/src/tools/write-file.d.ts
6
6
  /**
7
7
  * Build the agent-facing `write_file` tool over a workspace's policy-
8
8
  * enforced {@link WorkspaceOps}.
@@ -1 +1 @@
1
- {"version":3,"file":"write-file.d.mts","names":[],"sources":["../../../../../../../ai-workspace/src/tools/write-file.ts"],"mappings":";;;;;;;;;AAmCA;;;;;;;;;;;;;;;;;AAG+C;iBAH/B,iBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA;EAAY,IAAA;AAAA,IACX,YAAA,CAAa,cAAA,EAAgB,eAAA"}
1
+ {"version":3,"file":"write-file.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/write-file.ts"],"mappings":";;;;;;;;;AAuCA;;;;;;;;;;;;;;;;;AAG+C;iBAH/B,iBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA;EAAY,IAAA;AAAA,IACX,YAAA,CAAa,cAAA,EAAgB,eAAA"}
@@ -1,14 +1,15 @@
1
1
  import { objectSchema, stringField } from "./schema.mjs";
2
+ import { WRITE_FILE_JSON_SCHEMA } from "./json-schemas.mjs";
2
3
  import { tool } from "@warlock.js/ai";
3
4
 
4
- //#region ../ai-workspace/src/tools/write-file.ts
5
+ //#region ../../packages/ai-workspace/src/tools/write-file.ts
5
6
  /** Default tool name exposed to the LLM. */
6
7
  const DEFAULT_NAME = "write_file";
7
8
  /** Input schema for the `write_file` tool. */
8
9
  const inputSchema = objectSchema({
9
10
  path: stringField(),
10
11
  content: stringField()
11
- });
12
+ }, WRITE_FILE_JSON_SCHEMA);
12
13
  /**
13
14
  * Build the agent-facing `write_file` tool over a workspace's policy-
14
15
  * enforced {@link WorkspaceOps}.
@@ -1 +1 @@
1
- {"version":3,"file":"write-file.mjs","names":[],"sources":["../../../../../../../ai-workspace/src/tools/write-file.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type { WorkspaceOps, WriteFileInput, WriteFileResult } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"write_file\";\n\n/** Input schema for the `write_file` tool. */\nconst inputSchema = objectSchema<WriteFileInput>({\n path: stringField(),\n content: stringField(),\n});\n\n/**\n * Build the agent-facing `write_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, content }` against a Standard Schema, then\n * delegates to `ops.writeFile`, which atomically writes the full content\n * (creating parent directories) and returns the byte count and content\n * `hash`. The tool re-attaches the workspace-relative `path` so the\n * result matches the {@link WriteFileResult} wire shape.\n *\n * **Errors flow as data.** A jail escape is thrown by `ops`; the\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field — `invoke()` never throws.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const writeTool = makeWriteFileTool(ops);\n * const { data } = await writeTool.invoke({ path: \"src/new.ts\", content: \"export {};\" });\n * console.log(data.bytesWritten, data.hash);\n */\nexport function makeWriteFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<WriteFileInput, WriteFileResult> {\n return tool<WriteFileInput, WriteFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Write full content to a workspace file, creating it (and any parent \" +\n \"directories) if absent and overwriting it otherwise. The write is \" +\n \"atomic. Returns the bytes written and the new content hash.\",\n input: inputSchema,\n async execute(input) {\n const { hash, bytesWritten } = await ops.writeFile(input.path, input.content);\n\n return { path: input.path, bytesWritten, hash };\n },\n });\n}\n"],"mappings":";;;;;AAKA,MAAM,eAAe;;AAGrB,MAAM,cAAc,aAA6B;CAC/C,MAAM,YAAY;CAClB,SAAS,YAAY;AACvB,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBD,SAAgB,kBACd,KACA,SAC+C;CAC/C,OAAO,KAAsC;EAC3C,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,MAAM,EAAE,MAAM,iBAAiB,MAAM,IAAI,UAAU,MAAM,MAAM,MAAM,OAAO;GAE5E,OAAO;IAAE,MAAM,MAAM;IAAM;IAAc;GAAK;EAChD;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"write-file.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/write-file.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport { WRITE_FILE_JSON_SCHEMA } from \"./json-schemas\";\nimport type { WorkspaceOps, WriteFileInput, WriteFileResult } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"write_file\";\n\n/** Input schema for the `write_file` tool. */\nconst inputSchema = objectSchema<WriteFileInput>(\n {\n path: stringField(),\n content: stringField(),\n },\n WRITE_FILE_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `write_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, content }` against a Standard Schema, then\n * delegates to `ops.writeFile`, which atomically writes the full content\n * (creating parent directories) and returns the byte count and content\n * `hash`. The tool re-attaches the workspace-relative `path` so the\n * result matches the {@link WriteFileResult} wire shape.\n *\n * **Errors flow as data.** A jail escape is thrown by `ops`; the\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field — `invoke()` never throws.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const writeTool = makeWriteFileTool(ops);\n * const { data } = await writeTool.invoke({ path: \"src/new.ts\", content: \"export {};\" });\n * console.log(data.bytesWritten, data.hash);\n */\nexport function makeWriteFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<WriteFileInput, WriteFileResult> {\n return tool<WriteFileInput, WriteFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Write full content to a workspace file, creating it (and any parent \" +\n \"directories) if absent and overwriting it otherwise. The write is \" +\n \"atomic. Returns the bytes written and the new content hash.\",\n input: inputSchema,\n async execute(input) {\n const { hash, bytesWritten } = await ops.writeFile(input.path, input.content);\n\n return { path: input.path, bytesWritten, hash };\n },\n });\n}\n"],"mappings":";;;;;;AAMA,MAAM,eAAe;;AAGrB,MAAM,cAAc,aAClB;CACE,MAAM,YAAY;CAClB,SAAS,YAAY;AACvB,GACA,sBACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,kBACd,KACA,SAC+C;CAC/C,OAAO,KAAsC;EAC3C,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,MAAM,EAAE,MAAM,iBAAiB,MAAM,IAAI,UAAU,MAAM,MAAM,MAAM,OAAO;GAE5E,OAAO;IAAE,MAAM,MAAM;IAAM;IAAc;GAAK;EAChD;CACF,CAAC;AACH"}
@@ -1,23 +1,26 @@
1
1
  import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
2
2
  import { Workspace } from "./contracts/workspace.contract.mjs";
3
- //#region ../ai-workspace/src/workspace.d.ts
3
+ //#region ../../packages/ai-workspace/src/workspace.d.ts
4
4
  /**
5
5
  * Build a {@link Workspace} — the integrator that wires a
6
6
  * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops
7
- * layer, and the seven agent-facing tool factories.
7
+ * layer, and the agent-facing tool factories.
8
8
  *
9
9
  * The backend is chosen from `policy.backend`: `"mock"` runs in memory
10
10
  * (hermetic tests); the `"local"` default (and any absent value) runs
11
11
  * over the real disk via `@warlock.js/fs` + `node:child_process`. The
12
12
  * returned workspace exposes:
13
13
  *
14
- * - **`tools.*`** — `readFile` / `editFile` / `writeFile` / `runShell` /
15
- * `runTests` / `grep` / `glob`, plus `all()` (every tool) and
16
- * `pick(...)` (a least-privilege subset).
17
- * - **direct methods** — `readFile` / `writeFile` / `editFile` / `exec` /
18
- * `grep` / `glob` / `exists` / `mkdir` / `remove`, each delegating 1:1
19
- * to the shared ops layer.
20
- * - **`readonly()`** — a projection that vends only read/grep/glob and
14
+ * - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /
15
+ * `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /
16
+ * `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)
17
+ * and `pick(...)` (a least-privilege subset).
18
+ * - **direct methods** — `readFile` / `writeFile` / `editFile` /
19
+ * `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /
20
+ * `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /
21
+ * `remove` / `readTodos` / `writeTodos`, each delegating to the shared
22
+ * ops layer, and `dispose()`.
23
+ * - **`readonly()`** — a projection that vends only the read-side tools and
21
24
  * rejects every mutating direct method.
22
25
  * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.
23
26
  *
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.mts","names":[],"sources":["../../../../../../ai-workspace/src/workspace.ts"],"mappings":";;;;;;;AA6RA;;;;;;;;AAA6D;AAE5D;;;;;;;;;;;AAWgD;;;;;;;;;;;iBAbjC,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,SAAS;;;;;;;;YAWjD,EAAA;;IAER,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,SAAS;EAAA;AAAA"}
1
+ {"version":3,"file":"workspace.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"mappings":";;;;;;;AAuZA;;;;;;;;AAA6D;AAE5D;;;;;;;;;;;AAWgD;;;;;;;;;;;;;;iBAbjC,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,SAAS;;;;;;;;YAWjD,EAAA;;IAER,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,SAAS;EAAA;AAAA"}
package/esm/workspace.mjs CHANGED
@@ -1,18 +1,25 @@
1
1
  import { WorkspacePolicyError } from "./errors.mjs";
2
+ import { ShellJobs } from "./shell-jobs.mjs";
2
3
  import { createOps } from "./ops.mjs";
3
4
  import { createLocalBackend } from "./backends/local.mjs";
4
5
  import { createMockBackend } from "./backends/mock.mjs";
5
6
  import { makeEditFileTool } from "./tools/edit-file.mjs";
6
7
  import { makeGlobTool } from "./tools/glob.mjs";
7
8
  import { makeGrepTool } from "./tools/grep.mjs";
9
+ import { makeListDirTool } from "./tools/list-dir.mjs";
10
+ import { makeMultiEditTool } from "./tools/multi-edit.mjs";
8
11
  import { makeReadFileTool } from "./tools/read-file.mjs";
9
12
  import { makeRunShellTool } from "./tools/run-shell.mjs";
10
13
  import { makeRunTestsTool } from "./tools/run-tests.mjs";
14
+ import { makeShellKillTool } from "./tools/shell-kill.mjs";
15
+ import { makeShellOutputTool } from "./tools/shell-output.mjs";
16
+ import { TodoStore } from "./todos.mjs";
17
+ import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
11
18
  import { makeWriteFileTool } from "./tools/write-file.mjs";
12
19
  import { ai } from "@warlock.js/ai";
13
20
  import path from "node:path";
14
21
 
15
- //#region ../ai-workspace/src/workspace.ts
22
+ //#region ../../packages/ai-workspace/src/workspace.ts
16
23
  /**
17
24
  * The full set of tool names a writable workspace vends, in a stable
18
25
  * canonical order so `tools.all()` is deterministic.
@@ -20,23 +27,35 @@ import path from "node:path";
20
27
  const ALL_TOOL_NAMES = [
21
28
  "readFile",
22
29
  "editFile",
30
+ "multiEdit",
23
31
  "writeFile",
32
+ "listDir",
24
33
  "runShell",
34
+ "shellOutput",
35
+ "shellKill",
25
36
  "runTests",
26
37
  "grep",
27
- "glob"
38
+ "glob",
39
+ "todoWrite",
40
+ "todoRead"
28
41
  ];
29
42
  /**
30
43
  * The subset a {@link Workspace.readonly} projection exposes — the
31
- * non-mutating tools only. `editFile` / `writeFile` / `runShell` /
32
- * `runTests` are deliberately omitted so a reviewer agent has no path to
33
- * change the tree.
44
+ * tools that cannot change the tree. `editFile` / `multiEdit` /
45
+ * `writeFile` / the shell tools / `runTests` are deliberately omitted so a
46
+ * reviewer agent has no path to change it. The todo tools stay: they only
47
+ * touch the in-memory plan.
34
48
  */
35
49
  const READONLY_TOOL_NAMES = [
36
50
  "readFile",
51
+ "listDir",
37
52
  "grep",
38
- "glob"
53
+ "glob",
54
+ "todoWrite",
55
+ "todoRead"
39
56
  ];
57
+ /** Tools vended only when the policy enables background commands. */
58
+ const BACKGROUND_TOOL_NAMES = new Set(["shellOutput", "shellKill"]);
40
59
  /**
41
60
  * Choose the dumb IO executor for a policy. `"mock"` selects the
42
61
  * in-memory backend (hermetic tests); anything else — including the
@@ -61,11 +80,15 @@ function selectBackend(policy) {
61
80
  * Constructed via {@link workspace}; the class itself is internal.
62
81
  */
63
82
  var WorkspaceImpl = class WorkspaceImpl {
64
- constructor(policy, allowedTools = ALL_TOOL_NAMES) {
83
+ constructor(policy, allowedTools = ALL_TOOL_NAMES, state) {
65
84
  this.policy = policy;
66
85
  this.allowedTools = new Set(allowedTools);
86
+ this.state = state ?? {
87
+ jobs: new ShellJobs(),
88
+ todos: new TodoStore()
89
+ };
67
90
  const backend = selectBackend(policy);
68
- this.ops = createOps(backend, policy);
91
+ this.ops = createOps(backend, policy, { jobs: this.state.jobs });
69
92
  this.tools = this.buildTools();
70
93
  }
71
94
  /**
@@ -77,26 +100,49 @@ var WorkspaceImpl = class WorkspaceImpl {
77
100
  */
78
101
  buildTools() {
79
102
  const erase = (contract) => contract;
103
+ const shell = this.policy.shell;
104
+ const background = shell?.background !== void 0;
105
+ const interpreter = shell?.interpreter ?? "none";
80
106
  const factories = {
81
107
  readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),
82
108
  editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),
109
+ multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),
83
110
  writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),
84
- runShell: (opts) => erase(makeRunShellTool(this.ops, opts)),
85
- runTests: (opts) => erase(makeRunTestsTool(this.ops, opts)),
111
+ listDir: (opts) => erase(makeListDirTool(this.ops, opts)),
112
+ runShell: (opts) => erase(makeRunShellTool(this.ops, {
113
+ ...opts?.name !== void 0 ? { name: opts.name } : {},
114
+ ...shell?.timeoutMs !== void 0 ? { maxTimeoutMs: shell.timeoutMs } : {},
115
+ background
116
+ })),
117
+ shellOutput: (opts) => erase(makeShellOutputTool(this.ops, opts)),
118
+ shellKill: (opts) => erase(makeShellKillTool(this.ops, opts)),
119
+ runTests: (opts) => erase(makeRunTestsTool(this.ops, {
120
+ ...opts,
121
+ interpreter
122
+ })),
86
123
  grep: (opts) => erase(makeGrepTool(this.ops, opts)),
87
- glob: (opts) => erase(makeGlobTool(this.ops, opts))
124
+ glob: (opts) => erase(makeGlobTool(this.ops, opts)),
125
+ todoWrite: (opts) => erase(makeTodoWriteTool(this.state.todos, opts)),
126
+ todoRead: (opts) => erase(makeTodoReadTool(this.state.todos, opts))
88
127
  };
89
128
  const build = (name, opts) => factories[name](opts);
129
+ const vendedByAll = (name) => this.allowedTools.has(name) && (background || !BACKGROUND_TOOL_NAMES.has(name));
90
130
  return {
91
- all: () => ALL_TOOL_NAMES.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),
131
+ all: () => ALL_TOOL_NAMES.filter(vendedByAll).map((name) => build(name)),
92
132
  pick: (...names) => names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),
93
133
  readFile: (opts) => build("readFile", opts),
94
134
  editFile: (opts) => build("editFile", opts),
135
+ multiEdit: (opts) => build("multiEdit", opts),
95
136
  writeFile: (opts) => build("writeFile", opts),
137
+ listDir: (opts) => build("listDir", opts),
96
138
  runShell: (opts) => build("runShell", opts),
139
+ shellOutput: (opts) => build("shellOutput", opts),
140
+ shellKill: (opts) => build("shellKill", opts),
97
141
  runTests: (opts) => build("runTests", opts),
98
142
  grep: (opts) => build("grep", opts),
99
- glob: (opts) => build("glob", opts)
143
+ glob: (opts) => build("glob", opts),
144
+ todoWrite: (opts) => build("todoWrite", opts),
145
+ todoRead: (opts) => build("todoRead", opts)
100
146
  };
101
147
  }
102
148
  /**
@@ -122,10 +168,40 @@ var WorkspaceImpl = class WorkspaceImpl {
122
168
  this.assertWritable("editFile");
123
169
  return this.ops.editFile(input);
124
170
  }
171
+ async multiEdit(input) {
172
+ this.assertWritable("multiEdit");
173
+ return this.ops.multiEdit(input);
174
+ }
175
+ listDir(dirPath, opts) {
176
+ return this.ops.listDir(dirPath, opts);
177
+ }
125
178
  async exec(command, opts) {
126
179
  this.assertWritable("exec");
127
180
  return this.ops.exec(command, opts);
128
181
  }
182
+ async startShell(command, opts) {
183
+ this.assertWritable("startShell");
184
+ return this.ops.startShell(command, opts);
185
+ }
186
+ shellOutput(id, opts) {
187
+ return this.ops.shellOutput(id, opts);
188
+ }
189
+ async shellKill(id) {
190
+ this.assertWritable("shellKill");
191
+ return this.ops.shellKill(id);
192
+ }
193
+ listShells() {
194
+ return this.ops.listShells();
195
+ }
196
+ readTodos() {
197
+ return this.state.todos.read();
198
+ }
199
+ writeTodos(todos) {
200
+ return this.state.todos.write(todos);
201
+ }
202
+ dispose() {
203
+ return this.ops.dispose();
204
+ }
129
205
  grep(pattern, opts) {
130
206
  return this.ops.grep(pattern, opts);
131
207
  }
@@ -144,44 +220,49 @@ var WorkspaceImpl = class WorkspaceImpl {
144
220
  return this.ops.remove(filePath);
145
221
  }
146
222
  /**
147
- * A read-only projection over the SAME policy — only the read/grep/glob
148
- * tools are vended and every mutating direct method rejects with a
149
- * {@link WorkspacePolicyError}. A fresh ops/backend is built from the
150
- * identical policy, so the projection sees the same jailed tree.
223
+ * A read-only projection over the SAME policy — only the read / list /
224
+ * grep / glob / todo tools are vended and every mutating direct method
225
+ * rejects with a {@link WorkspacePolicyError}. A fresh ops/backend is
226
+ * built from the identical policy, so the projection sees the same jailed
227
+ * tree; it shares this workspace's background commands and todo list.
151
228
  */
152
229
  readonly() {
153
- return new WorkspaceImpl(this.policy, READONLY_TOOL_NAMES);
230
+ return new WorkspaceImpl(this.policy, READONLY_TOOL_NAMES.filter((name) => this.allowedTools.has(name)), this.state);
154
231
  }
155
232
  /**
156
233
  * A sub-jailed view rooted at `subdir` (relative to this workspace's
157
234
  * `cwd`). Returns a brand-new workspace whose policy is this policy
158
235
  * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,
159
- * same allow/deny/shell/read sub-policies, but a tighter jail root.
236
+ * same allow/deny/shell/read sub-policies, but a tighter jail root. It
237
+ * shares this workspace's background commands and todo list.
160
238
  */
161
239
  scope(subdir) {
162
240
  return new WorkspaceImpl({
163
241
  ...this.policy,
164
242
  cwd: path.join(this.policy.cwd, subdir)
165
- }, [...this.allowedTools]);
243
+ }, [...this.allowedTools], this.state);
166
244
  }
167
245
  };
168
246
  /**
169
247
  * Build a {@link Workspace} — the integrator that wires a
170
248
  * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops
171
- * layer, and the seven agent-facing tool factories.
249
+ * layer, and the agent-facing tool factories.
172
250
  *
173
251
  * The backend is chosen from `policy.backend`: `"mock"` runs in memory
174
252
  * (hermetic tests); the `"local"` default (and any absent value) runs
175
253
  * over the real disk via `@warlock.js/fs` + `node:child_process`. The
176
254
  * returned workspace exposes:
177
255
  *
178
- * - **`tools.*`** — `readFile` / `editFile` / `writeFile` / `runShell` /
179
- * `runTests` / `grep` / `glob`, plus `all()` (every tool) and
180
- * `pick(...)` (a least-privilege subset).
181
- * - **direct methods** — `readFile` / `writeFile` / `editFile` / `exec` /
182
- * `grep` / `glob` / `exists` / `mkdir` / `remove`, each delegating 1:1
183
- * to the shared ops layer.
184
- * - **`readonly()`** — a projection that vends only read/grep/glob and
256
+ * - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /
257
+ * `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /
258
+ * `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)
259
+ * and `pick(...)` (a least-privilege subset).
260
+ * - **direct methods** — `readFile` / `writeFile` / `editFile` /
261
+ * `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /
262
+ * `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /
263
+ * `remove` / `readTodos` / `writeTodos`, each delegating to the shared
264
+ * ops layer, and `dispose()`.
265
+ * - **`readonly()`** — a projection that vends only the read-side tools and
185
266
  * rejects every mutating direct method.
186
267
  * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.
187
268
  *