@ikarmic/forge-core 5.0.0 → 5.1.1

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 (163) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/agent.d.ts +8 -0
  3. package/dist/agent/agent.js +148 -24
  4. package/dist/agent/background.d.ts +58 -0
  5. package/dist/agent/background.js +139 -0
  6. package/dist/agent/enhance-prompt.d.ts +3 -1
  7. package/dist/agent/enhance-prompt.js +6 -2
  8. package/dist/agent/goal.d.ts +54 -0
  9. package/dist/agent/goal.js +65 -0
  10. package/dist/agent/prompts.d.ts +25 -1
  11. package/dist/agent/prompts.js +61 -17
  12. package/dist/agents/agent-files.d.ts +43 -0
  13. package/dist/agents/agent-files.js +109 -0
  14. package/dist/checkpoints/store.js +5 -1
  15. package/dist/commands/custom-commands.d.ts +9 -4
  16. package/dist/commands/custom-commands.js +13 -2
  17. package/dist/config-file.d.ts +43 -0
  18. package/dist/config-file.js +249 -5
  19. package/dist/config-merge.d.ts +13 -0
  20. package/dist/config-merge.js +67 -0
  21. package/dist/config-schema.d.ts +340 -0
  22. package/dist/config-schema.js +126 -0
  23. package/dist/config.js +2 -1
  24. package/dist/context/compaction.js +4 -0
  25. package/dist/context/mask.js +1 -1
  26. package/dist/custom-providers.d.ts +95 -0
  27. package/dist/custom-providers.js +120 -0
  28. package/dist/git/auto-commit.d.ts +2 -2
  29. package/dist/git/auto-commit.js +5 -4
  30. package/dist/github/github-tools.d.ts +2 -2
  31. package/dist/hooks/runner.js +3 -2
  32. package/dist/import/claude-code.d.ts +39 -0
  33. package/dist/import/claude-code.js +254 -0
  34. package/dist/index.d.ts +34 -5
  35. package/dist/index.js +27 -4
  36. package/dist/journey/answers-block.d.ts +17 -0
  37. package/dist/journey/answers-block.js +15 -0
  38. package/dist/journey/index.d.ts +9 -0
  39. package/dist/journey/index.js +8 -0
  40. package/dist/journey/journey.d.ts +13 -0
  41. package/dist/journey/journey.js +40 -0
  42. package/dist/journey/offer.d.ts +25 -0
  43. package/dist/journey/offer.js +48 -0
  44. package/dist/journey/questions.d.ts +23 -0
  45. package/dist/journey/questions.js +49 -0
  46. package/dist/journey/stages.d.ts +13 -0
  47. package/dist/journey/stages.js +107 -0
  48. package/dist/journey/survey.d.ts +10 -0
  49. package/dist/journey/survey.js +203 -0
  50. package/dist/journey/trigger.d.ts +19 -0
  51. package/dist/journey/trigger.js +79 -0
  52. package/dist/journey/types.d.ts +72 -0
  53. package/dist/journey/types.js +1 -0
  54. package/dist/lsp/client.d.ts +5 -0
  55. package/dist/lsp/client.js +39 -3
  56. package/dist/mcp/index.d.ts +5 -1
  57. package/dist/mcp/index.js +3 -1
  58. package/dist/mcp/manager.d.ts +17 -3
  59. package/dist/mcp/manager.js +27 -2
  60. package/dist/mcp/marketplace.d.ts +2 -0
  61. package/dist/mcp/marketplace.js +10 -4
  62. package/dist/mcp/mcp-client.d.ts +12 -2
  63. package/dist/mcp/mcp-client.js +74 -17
  64. package/dist/mcp/mcp-config.d.ts +2 -0
  65. package/dist/mcp/mcp-config.js +1 -0
  66. package/dist/mcp/oauth.d.ts +55 -0
  67. package/dist/mcp/oauth.js +197 -0
  68. package/dist/mcp/resource-tools.d.ts +8 -0
  69. package/dist/mcp/resource-tools.js +56 -0
  70. package/dist/memory/session-digest.d.ts +12 -0
  71. package/dist/memory/session-digest.js +47 -0
  72. package/dist/memory/stale.js +2 -1
  73. package/dist/model-alias.d.ts +18 -2
  74. package/dist/model-alias.js +27 -3
  75. package/dist/model-catalog.js +0 -1
  76. package/dist/model-list.js +2 -1
  77. package/dist/model-overrides.d.ts +30 -0
  78. package/dist/model-overrides.js +48 -0
  79. package/dist/orchestrator/specialists-registry.js +2 -3
  80. package/dist/orchestrator/specialists.js +4 -4
  81. package/dist/policy/ca-certs.d.ts +8 -0
  82. package/dist/policy/ca-certs.js +30 -0
  83. package/dist/policy/proxy.d.ts +10 -3
  84. package/dist/policy/proxy.js +33 -6
  85. package/dist/pricing.js +4 -0
  86. package/dist/providers.d.ts +7 -0
  87. package/dist/providers.js +39 -5
  88. package/dist/semantic/embedder.d.ts +15 -0
  89. package/dist/semantic/embedder.js +25 -0
  90. package/dist/semantic/semantic-index.d.ts +10 -2
  91. package/dist/semantic/semantic-index.js +16 -7
  92. package/dist/sessions/session-store.js +2 -1
  93. package/dist/skills/builtin.d.ts +2 -0
  94. package/dist/skills/builtin.js +68 -0
  95. package/dist/skills/index.d.ts +2 -0
  96. package/dist/skills/index.js +2 -0
  97. package/dist/skills/loader.d.ts +10 -3
  98. package/dist/skills/loader.js +20 -7
  99. package/dist/skills/marketplace.js +2 -1
  100. package/dist/skills/remote.d.ts +14 -0
  101. package/dist/skills/remote.js +131 -0
  102. package/dist/skills/types.d.ts +10 -0
  103. package/dist/specs/generate.d.ts +18 -9
  104. package/dist/specs/generate.js +174 -14
  105. package/dist/specs/index.d.ts +2 -2
  106. package/dist/specs/index.js +1 -1
  107. package/dist/specs/store.d.ts +14 -2
  108. package/dist/specs/store.js +26 -1
  109. package/dist/specs/types.d.ts +11 -0
  110. package/dist/steering/extra-instructions.d.ts +8 -0
  111. package/dist/steering/extra-instructions.js +104 -0
  112. package/dist/steering/folder-instructions.d.ts +8 -0
  113. package/dist/steering/folder-instructions.js +28 -0
  114. package/dist/steering/select.js +2 -1
  115. package/dist/timed-fetch.d.ts +12 -0
  116. package/dist/timed-fetch.js +83 -0
  117. package/dist/tools/apply-patch.d.ts +52 -0
  118. package/dist/tools/apply-patch.js +197 -0
  119. package/dist/tools/ask-user.d.ts +27 -0
  120. package/dist/tools/ask-user.js +60 -0
  121. package/dist/tools/browser-tools.d.ts +6 -6
  122. package/dist/tools/browser-tools.js +3 -13
  123. package/dist/tools/codebase-search.d.ts +4 -2
  124. package/dist/tools/codebase-search.js +2 -2
  125. package/dist/tools/edit-file.js +5 -3
  126. package/dist/tools/fetch-url.d.ts +7 -0
  127. package/dist/tools/fetch-url.js +52 -19
  128. package/dist/tools/find-symbol.d.ts +2 -2
  129. package/dist/tools/ignore-rules.js +2 -1
  130. package/dist/tools/index.d.ts +5 -1
  131. package/dist/tools/index.js +10 -1
  132. package/dist/tools/memory.d.ts +6 -6
  133. package/dist/tools/notebook.d.ts +2 -2
  134. package/dist/tools/permission-rules.d.ts +32 -0
  135. package/dist/tools/permission-rules.js +182 -0
  136. package/dist/tools/read-file.d.ts +5 -0
  137. package/dist/tools/read-file.js +12 -2
  138. package/dist/tools/read-only.d.ts +1 -1
  139. package/dist/tools/read-only.js +2 -0
  140. package/dist/tools/run-command.d.ts +4 -2
  141. package/dist/tools/run-command.js +12 -8
  142. package/dist/tools/sandbox-loader.js +3 -13
  143. package/dist/tools/search-files.d.ts +8 -2
  144. package/dist/tools/search-files.js +43 -0
  145. package/dist/tools/shared.d.ts +5 -0
  146. package/dist/tools/shared.js +30 -2
  147. package/dist/tools/shell-env.d.ts +7 -0
  148. package/dist/tools/shell-env.js +9 -0
  149. package/dist/tools/sub-agent-wait.d.ts +29 -0
  150. package/dist/tools/sub-agent-wait.js +60 -0
  151. package/dist/tools/sub-agent.d.ts +50 -4
  152. package/dist/tools/sub-agent.js +213 -125
  153. package/dist/tools/text-encoding.d.ts +10 -0
  154. package/dist/tools/text-encoding.js +40 -0
  155. package/dist/tools/todo.d.ts +36 -0
  156. package/dist/tools/todo.js +42 -0
  157. package/dist/tools/write-file.js +15 -9
  158. package/dist/tools/write-tools.d.ts +12 -0
  159. package/dist/tools/write-tools.js +18 -0
  160. package/dist/types.d.ts +98 -0
  161. package/dist/worktree/worktree.d.ts +3 -0
  162. package/dist/worktree/worktree.js +22 -0
  163. package/package.json +1 -1
@@ -1,7 +1,10 @@
1
1
  import { z } from 'zod';
2
+ import { registeredAgent, registeredAgents } from '../agents/agent-files.js';
2
3
  import { Agent } from '../agent/agent.js';
4
+ import { applyResolved, resolveModelAlias } from '../model-alias.js';
3
5
  import { WorktreeError, WorktreeManager } from '../worktree/index.js';
4
6
  import { BUILTIN_TOOLS } from './index.js';
7
+ import { READ_ONLY_TOOLS } from './read-only.js';
5
8
  import { resolveAndCheckPath } from './shared.js';
6
9
  const params = z.object({
7
10
  /** The task for the sub-agent to perform. Free-form text. */
@@ -10,6 +13,13 @@ const params = z.object({
10
13
  .min(1)
11
14
  .max(20_000)
12
15
  .describe('Plain-English task for the sub-agent to perform.'),
16
+ /** Name of an agent file (`.forge/agents/<name>.md`) to run as: its prompt, tools, model and step cap. */
17
+ agent: z
18
+ .string()
19
+ .min(1)
20
+ .max(64)
21
+ .optional()
22
+ .describe('Run as the named agent defined in .forge/agents (its prompt, tools and model). Omit for a general helper.'),
13
23
  /** Hard cap on iterations for the sub-agent. Default 10. */
14
24
  max_iterations: z
15
25
  .number()
@@ -81,6 +91,14 @@ const params = z.object({
81
91
  .boolean()
82
92
  .optional()
83
93
  .describe('When use_worktree is true: also delete the auto-created branch alongside the worktree (skipped if another worktree still uses it). Default true. v2.14.'),
94
+ /**
95
+ * Start the child and return at once. The child keeps working while the parent continues; collect its answer with
96
+ * `sub_agent_wait`. Only where the surface can show background runs; elsewhere the child runs to completion as usual.
97
+ */
98
+ background: z
99
+ .boolean()
100
+ .optional()
101
+ .describe('Start the sub-agent and return immediately with an id; keep working, then collect the answer with sub_agent_wait. Use it for independent work that does not block your next step. Default false.'),
84
102
  });
85
103
  const DEFAULT_MAX_ITERATIONS = 10;
86
104
  /**
@@ -89,7 +107,7 @@ const DEFAULT_MAX_ITERATIONS = 10;
89
107
  * passing `allowSubAgent: true` via context if you really want it)
90
108
  * - nothing else: the sub-agent gets the full built-in toolset.
91
109
  */
92
- const FORBIDDEN_TOOLS = new Set(['sub_agent']);
110
+ const FORBIDDEN_TOOLS = new Set(['sub_agent', 'sub_agent_wait']);
93
111
  /**
94
112
  * Delegate a task to a child agent. The child runs synchronously and its
95
113
  * final text + run statistics are returned to the parent. Useful for
@@ -122,140 +140,210 @@ export const subAgentTool = {
122
140
  throw new Error(`Invalid sub_agent arguments: ${parsed.error.message}`);
123
141
  }
124
142
  const valid = parsed.data;
125
- const maxIterations = Math.min(valid.max_iterations ?? DEFAULT_MAX_ITERATIONS, 50);
126
- // Security: `cwd` becomes the CHILD agent's own sandbox root: an
127
- // unvalidated override would let a caller point the child anywhere on
128
- // disk (e.g. cwd: "C:\\Users\\victim\\Documents") with none of the
129
- // approval gating `run_command`/`sub_agent` itself otherwise gets.
130
- // resolveAndCheckPath resolves relative to the parent's own `ctx.cwd`
131
- // and throws if the result escapes it (or violates `ctx.policy`),
132
- // which matches this tool's documented intent: "operate on a
133
- // subdirectory": while still accepting the common case.
134
- const baseCwd = valid.cwd ? resolveAndCheckPath(valid.cwd, ctx) : ctx.cwd;
135
- const auto = valid.auto ?? false;
136
- // Worktree setup. We do this *before* constructing the child Agent
137
- // so the child runs with the worktree path as its cwd. On any
138
- // error during creation, we fall back to plain cwd mode with a
139
- // stderr warning: the user gets a working sub-agent instead of
140
- // an opaque failure.
141
- let worktreeInfo;
142
- let manager;
143
- if (valid.use_worktree) {
144
- const repoRoot = await WorktreeManager.resolveRepoRoot(baseCwd);
145
- if (!repoRoot) {
146
- // Not a git repo: warn but proceed without isolation. Better
147
- // than failing outright; the user can fix the repo separately.
148
- ctx.onProgress?.(`sub_agent: use_worktree requested but ${baseCwd} is not a git repo; running without isolation`);
143
+ const agentDef = valid.agent ? registeredAgent(valid.agent) : undefined;
144
+ if (valid.agent && !agentDef) {
145
+ const known = registeredAgents().map((a) => a.name);
146
+ throw new Error(`Unknown agent "${valid.agent}". ${known.length ? `Known agents: ${known.join(', ')}.` : 'No agents are defined in .forge/agents.'}`);
147
+ }
148
+ if (valid.background && ctx.background) {
149
+ // The child gets its own abort signal (the person stops it from the strip) and reports to the strip, not to the
150
+ // run that started it, which may be over by the time the child speaks.
151
+ const { abortSignal: _parentSignal, onChildProgress: _progress, onProgress: _line, ...rest } = ctx;
152
+ const started = ctx.background.start({
153
+ task: valid.task,
154
+ agent: valid.agent,
155
+ run: (signal) => runSubAgent(valid, agentDef, { ...rest, abortSignal: signal }),
156
+ });
157
+ return {
158
+ started: true,
159
+ id: started.id,
160
+ task: valid.task,
161
+ message: `Started background agent ${started.id}. Carry on with other work; call sub_agent_wait to collect its answer. Do not start it again.`,
162
+ };
163
+ }
164
+ return runSubAgent(valid, agentDef, ctx);
165
+ },
166
+ };
167
+ /** One child run, to completion: worktree setup, the child agent, cleanup, and the result. */
168
+ async function runSubAgent(valid, agentDef, ctx) {
169
+ // Security: `cwd` becomes the CHILD agent's own sandbox root: an
170
+ // unvalidated override would let a caller point the child anywhere on
171
+ // disk (e.g. cwd: "C:\\Users\\victim\\Documents") with none of the
172
+ // approval gating `run_command`/`sub_agent` itself otherwise gets.
173
+ // resolveAndCheckPath resolves relative to the parent's own `ctx.cwd`
174
+ // and throws if the result escapes it (or violates `ctx.policy`),
175
+ // which matches this tool's documented intent: "operate on a
176
+ // subdirectory": while still accepting the common case.
177
+ const baseCwd = valid.cwd ? resolveAndCheckPath(valid.cwd, ctx) : ctx.cwd;
178
+ const auto = valid.auto ?? false;
179
+ // Worktree setup. We do this *before* constructing the child Agent
180
+ // so the child runs with the worktree path as its cwd. On any
181
+ // error during creation, we fall back to plain cwd mode with a
182
+ // stderr warning: the user gets a working sub-agent instead of
183
+ // an opaque failure.
184
+ let worktreeInfo;
185
+ let manager;
186
+ if (valid.use_worktree) {
187
+ const repoRoot = await WorktreeManager.resolveRepoRoot(baseCwd);
188
+ if (!repoRoot) {
189
+ // Not a git repo: warn but proceed without isolation. Better
190
+ // than failing outright; the user can fix the repo separately.
191
+ ctx.onProgress?.(`sub_agent: use_worktree requested but ${baseCwd} is not a git repo; running without isolation`);
192
+ }
193
+ else {
194
+ try {
195
+ manager = new WorktreeManager(repoRoot);
196
+ // Clean up any leftover worktrees from a previous crashed
197
+ // session before we allocate a new one. Best-effort.
198
+ await manager.pruneStale();
199
+ worktreeInfo = await manager.create({
200
+ ...(valid.worktree_label ? { label: valid.worktree_label } : {}),
201
+ });
202
+ ctx.onProgress?.(`sub_agent: isolated in worktree ${worktreeInfo.path} (branch ${worktreeInfo.branch})`);
149
203
  }
150
- else {
151
- try {
152
- manager = new WorktreeManager(repoRoot);
153
- // Clean up any leftover worktrees from a previous crashed
154
- // session before we allocate a new one. Best-effort.
155
- await manager.pruneStale();
156
- worktreeInfo = await manager.create({
157
- ...(valid.worktree_label ? { label: valid.worktree_label } : {}),
158
- });
159
- ctx.onProgress?.(`sub_agent: isolated in worktree ${worktreeInfo.path} (branch ${worktreeInfo.branch})`);
204
+ catch (err) {
205
+ if (err instanceof WorktreeError) {
206
+ ctx.onProgress?.(`sub_agent: worktree setup failed (${err.message}); running without isolation`);
160
207
  }
161
- catch (err) {
162
- if (err instanceof WorktreeError) {
163
- ctx.onProgress?.(`sub_agent: worktree setup failed (${err.message}); running without isolation`);
164
- }
165
- else {
166
- throw err;
167
- }
208
+ else {
209
+ throw err;
168
210
  }
169
211
  }
170
212
  }
171
- const cwd = worktreeInfo ? worktreeInfo.path : baseCwd;
172
- const toolsAllowed = BUILTIN_TOOLS.filter((t) => !FORBIDDEN_TOOLS.has(t.name));
173
- // The sub-agent inherits our system prompt by default; users can override
174
- // via the AGENT_CONFIG system prompt if needed. For the v1 we keep the
175
- // child on the same system prompt so its behavior matches the parent's.
176
- const child = new Agent({
177
- model: ctx.parentModel ?? fallbackModel(),
178
- cwd,
179
- auto,
180
- maxIterations,
181
- disableBuiltins: false,
182
- // The child's toolset is the standard built-in set (without sub_agent).
183
- extraTools: toolsAllowed,
213
+ }
214
+ const cwd = worktreeInfo ? worktreeInfo.path : baseCwd;
215
+ const setup = childSetupFor(agentDef, BUILTIN_TOOLS, ctx, valid.max_iterations);
216
+ const maxIterations = setup.maxIterations;
217
+ // The sub-agent inherits our system prompt by default; users can override
218
+ // via the AGENT_CONFIG system prompt if needed. For the v1 we keep the
219
+ // child on the same system prompt so its behavior matches the parent's.
220
+ const child = new Agent(childAgentConfig({ ...setup, maxIterations }, ctx, cwd, auto));
221
+ const toolsUsed = [];
222
+ const started = Date.now();
223
+ let result;
224
+ try {
225
+ result = await child.run({
226
+ task: valid.task,
227
+ ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
228
+ onEvent: (event) => {
229
+ if (event.type === 'tool_call') {
230
+ toolsUsed.push(event.name);
231
+ }
232
+ // Stream a compact progress line through the parent's context so the
233
+ // user can see the child is making progress in interactive mode.
234
+ ctx.onChildProgress?.({
235
+ tool: 'sub_agent',
236
+ task: valid.task,
237
+ event,
238
+ });
239
+ },
184
240
  });
185
- const toolsUsed = [];
186
- const started = Date.now();
187
- let result;
188
- try {
189
- result = await child.run({
190
- task: valid.task,
191
- onEvent: (event) => {
192
- if (event.type === 'tool_call') {
193
- toolsUsed.push(event.name);
194
- }
195
- // Stream a compact progress line through the parent's context so the
196
- // user can see the child is making progress in interactive mode.
197
- ctx.onChildProgress?.({
198
- tool: 'sub_agent',
199
- task: valid.task,
200
- event,
201
- });
202
- },
203
- });
204
- }
205
- finally {
206
- // Best-effort cleanup. We always try to remove the worktree,
207
- // even on error / abort, so a crashed sub-agent doesn't leak a
208
- // directory. Force=true so uncommitted changes don't block us.
209
- //
210
- // v2.14: also delete the auto-created `forge/<label>-<sha>`
211
- // branch unless the caller explicitly turned it off. Most
212
- // sub-agents finish their work cleanly, so leaving the branch
213
- // behind is pure clutter. The branch-deletion logic in
214
- // `WorktreeManager.remove` is safety-checked: it refuses to
215
- // delete a branch that another worktree still uses, so two
216
- // concurrent sub-agents on the same branch won't collide.
217
- if (manager && worktreeInfo) {
218
- // Optional with z.boolean().optional() → default true when
219
- // the caller omits the key. Using `.default(true)` on the
220
- // schema would mismatch ToolDefinition<TArgs>'s expected
221
- // input type (the schema's input side stays `boolean |
222
- // undefined` while the inferred output becomes `boolean`),
223
- // so we keep the default in code.
224
- const cleanupBranch = valid.cleanup_branch ?? true;
225
- try {
226
- const result = await manager.remove(worktreeInfo.path, {
227
- force: true,
228
- deleteBranch: cleanupBranch,
229
- });
230
- if (cleanupBranch && result.branchDeleted && ctx.onProgress) {
231
- ctx.onProgress(`sub_agent: removed worktree and branch ${result.branch}`);
232
- }
233
- else if (cleanupBranch && !result.branchDeleted && ctx.onProgress) {
234
- ctx.onProgress(`sub_agent: removed worktree (kept branch ${result.branch ?? '(detached)'}: still in use)`);
235
- }
241
+ }
242
+ finally {
243
+ // Best-effort cleanup. We always try to remove the worktree,
244
+ // even on error / abort, so a crashed sub-agent doesn't leak a
245
+ // directory. Force=true so uncommitted changes don't block us.
246
+ //
247
+ // v2.14: also delete the auto-created `forge/<label>-<sha>`
248
+ // branch unless the caller explicitly turned it off. Most
249
+ // sub-agents finish their work cleanly, so leaving the branch
250
+ // behind is pure clutter. The branch-deletion logic in
251
+ // `WorktreeManager.remove` is safety-checked: it refuses to
252
+ // delete a branch that another worktree still uses, so two
253
+ // concurrent sub-agents on the same branch won't collide.
254
+ if (manager && worktreeInfo) {
255
+ // Optional with z.boolean().optional() → default true when
256
+ // the caller omits the key. Using `.default(true)` on the
257
+ // schema would mismatch ToolDefinition<TArgs>'s expected
258
+ // input type (the schema's input side stays `boolean |
259
+ // undefined` while the inferred output becomes `boolean`),
260
+ // so we keep the default in code.
261
+ const cleanupBranch = valid.cleanup_branch ?? true;
262
+ try {
263
+ const result = await manager.remove(worktreeInfo.path, {
264
+ force: true,
265
+ deleteBranch: cleanupBranch,
266
+ });
267
+ if (cleanupBranch && result.branchDeleted && ctx.onProgress) {
268
+ ctx.onProgress(`sub_agent: removed worktree and branch ${result.branch}`);
236
269
  }
237
- catch (cleanupErr) {
238
- // Don't mask the agent's actual error with a cleanup error.
239
- // Just surface it through onProgress so the user can see it.
240
- ctx.onProgress?.(`sub_agent: warning: failed to remove worktree ${worktreeInfo.path}: ${cleanupErr.message}`);
270
+ else if (cleanupBranch && !result.branchDeleted && ctx.onProgress) {
271
+ ctx.onProgress(`sub_agent: removed worktree (kept branch ${result.branch ?? '(detached)'}: still in use)`);
241
272
  }
242
273
  }
274
+ catch (cleanupErr) {
275
+ // Don't mask the agent's actual error with a cleanup error.
276
+ // Just surface it through onProgress so the user can see it.
277
+ ctx.onProgress?.(`sub_agent: warning: failed to remove worktree ${worktreeInfo.path}: ${cleanupErr.message}`);
278
+ }
243
279
  }
244
- return {
245
- task: valid.task,
246
- finalText: result.finalText,
247
- iterations: result.iterations,
248
- reason: result.reason,
249
- inputTokens: result.inputTokens,
250
- outputTokens: result.outputTokens,
251
- durationMs: Date.now() - started,
252
- toolsUsed,
253
- ...(worktreeInfo
254
- ? { worktreePath: worktreeInfo.path, worktreeBranch: worktreeInfo.branch }
255
- : {}),
256
- };
257
- },
258
- };
280
+ }
281
+ return {
282
+ task: valid.task,
283
+ finalText: result.finalText,
284
+ iterations: result.iterations,
285
+ reason: result.reason,
286
+ inputTokens: result.inputTokens,
287
+ outputTokens: result.outputTokens,
288
+ durationMs: Date.now() - started,
289
+ toolsUsed,
290
+ ...(worktreeInfo
291
+ ? { worktreePath: worktreeInfo.path, worktreeBranch: worktreeInfo.branch }
292
+ : {}),
293
+ };
294
+ }
295
+ /**
296
+ * What a child runs with: the tools it may see, its model, step cap and system prompt. Without an agent file this is
297
+ * the old behaviour (every tool but `sub_agent`, the sub-agent or parent model, default cap). `sub_agent` is never
298
+ * offered to a child, whatever the file says.
299
+ */
300
+ export function childSetupFor(def, tools, ctx, requestedIterations) {
301
+ const denied = new Set([...FORBIDDEN_TOOLS, ...(def?.deny ?? [])]);
302
+ const allow = def && def.tools.length > 0 ? new Set(def.tools) : undefined;
303
+ const base = childModelFor(ctx);
304
+ let model = base;
305
+ if (def?.model)
306
+ model = applyResolved(base, resolveModelAlias(def.model, undefined, base.provider));
307
+ if (def?.temperature !== undefined)
308
+ model = { ...model, temperature: def.temperature };
309
+ return {
310
+ tools: tools.filter((t) => !denied.has(t.name) && (!allow || allow.has(t.name))),
311
+ model,
312
+ maxIterations: Math.min(requestedIterations ?? def?.steps ?? DEFAULT_MAX_ITERATIONS, 50),
313
+ ...(def?.prompt ? { systemPrompt: def.prompt } : {}),
314
+ };
315
+ }
316
+ /**
317
+ * The child's Agent configuration. It carries the limits the person set for the parent (permission rules, tool
318
+ * sandbox): a child started with `auto: true` must not be a way around an `edit: deny` rule or a denied tool.
319
+ */
320
+ export function childAgentConfig(setup, ctx, cwd, auto) {
321
+ return {
322
+ model: setup.model,
323
+ cwd,
324
+ auto,
325
+ maxIterations: setup.maxIterations,
326
+ ...(setup.systemPrompt ? { systemPrompt: setup.systemPrompt } : {}),
327
+ // A child has no one to ask, so without this every tool that needs approval is refused, even reading a folder.
328
+ autoApproveTools: childAutoApprove(ctx),
329
+ ...(ctx.permissionRules ? { permissionRules: ctx.permissionRules } : {}),
330
+ ...(ctx.toolSandbox ? { toolSandbox: ctx.toolSandbox } : {}),
331
+ // The same budget object, not a copy: a child that started its own four
332
+ // would be a way around the limit the parent is under (RB-14).
333
+ ...(ctx.askBudget ? { askBudget: ctx.askBudget } : {}),
334
+ // The child's toolset is the built-in set minus sub_agent, narrowed by the agent file when one is named.
335
+ disableBuiltins: true,
336
+ extraTools: setup.tools,
337
+ };
338
+ }
339
+ /** What a child runs without asking: what the parent does, else the read-only tools. Destructive tools still never run unasked. */
340
+ export function childAutoApprove(ctx) {
341
+ return ctx.autoApproveTools ? [...ctx.autoApproveTools] : [...READ_ONLY_TOOLS];
342
+ }
343
+ /** The configured sub-agent model, else the parent's, else a last-resort default. */
344
+ export function childModelFor(ctx) {
345
+ return ctx.subAgentModel ?? ctx.parentModel ?? fallbackModel();
346
+ }
259
347
  function fallbackModel() {
260
348
  // Best-effort: use whatever the SDK resolves from env. The tool requires
261
349
  // a model config to construct an Agent; in practice the parent's config
@@ -0,0 +1,10 @@
1
+ export type TextEncoding = 'utf8' | 'utf8-bom' | 'utf16le' | 'utf16be';
2
+ /**
3
+ * Decode a file's bytes by its byte-order mark. Without one the file is read as UTF-8. The mark itself is not part
4
+ * of the text, and `encodeText` puts it back, so an edit never changes the encoding of a file.
5
+ */
6
+ export declare function decodeText(buf: Buffer): {
7
+ text: string;
8
+ encoding: TextEncoding;
9
+ };
10
+ export declare function encodeText(text: string, encoding: TextEncoding): Buffer;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Decode a file's bytes by its byte-order mark. Without one the file is read as UTF-8. The mark itself is not part
3
+ * of the text, and `encodeText` puts it back, so an edit never changes the encoding of a file.
4
+ */
5
+ export function decodeText(buf) {
6
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf)
7
+ return { text: buf.subarray(3).toString('utf8'), encoding: 'utf8-bom' };
8
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe)
9
+ return { text: buf.subarray(2).toString('utf16le'), encoding: 'utf16le' };
10
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
11
+ const body = Buffer.from(buf.subarray(2));
12
+ // Swap each byte pair to little endian, then decode.
13
+ for (let i = 0; i + 1 < body.length; i += 2) {
14
+ const a = body[i];
15
+ body[i] = body[i + 1];
16
+ body[i + 1] = a;
17
+ }
18
+ return { text: body.toString('utf16le'), encoding: 'utf16be' };
19
+ }
20
+ return { text: buf.toString('utf8'), encoding: 'utf8' };
21
+ }
22
+ export function encodeText(text, encoding) {
23
+ switch (encoding) {
24
+ case 'utf8-bom':
25
+ return Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(text, 'utf8')]);
26
+ case 'utf16le':
27
+ return Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(text, 'utf16le')]);
28
+ case 'utf16be': {
29
+ const body = Buffer.from(text, 'utf16le');
30
+ for (let i = 0; i + 1 < body.length; i += 2) {
31
+ const a = body[i];
32
+ body[i] = body[i + 1];
33
+ body[i + 1] = a;
34
+ }
35
+ return Buffer.concat([Buffer.from([0xfe, 0xff]), body]);
36
+ }
37
+ default:
38
+ return Buffer.from(text, 'utf8');
39
+ }
40
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import type { TodoItem, ToolDefinition } from '../types.js';
3
+ /**
4
+ * The agent's own checklist for a multi-step task. `todo_write` replaces the whole list (one call per update, as
5
+ * in Kilo), `todo_read` returns it. Neither touches the workspace, so neither asks for approval.
6
+ */
7
+ declare const writeParams: z.ZodObject<{
8
+ todos: z.ZodArray<z.ZodObject<{
9
+ content: z.ZodString;
10
+ status: z.ZodEnum<["pending", "in_progress", "completed"]>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ status: "pending" | "in_progress" | "completed";
13
+ content: string;
14
+ }, {
15
+ status: "pending" | "in_progress" | "completed";
16
+ content: string;
17
+ }>, "many">;
18
+ }, "strip", z.ZodTypeAny, {
19
+ todos: {
20
+ status: "pending" | "in_progress" | "completed";
21
+ content: string;
22
+ }[];
23
+ }, {
24
+ todos: {
25
+ status: "pending" | "in_progress" | "completed";
26
+ content: string;
27
+ }[];
28
+ }>;
29
+ declare const readParams: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
30
+ export declare const todoWriteTool: ToolDefinition<z.infer<typeof writeParams>, {
31
+ todos: TodoItem[];
32
+ }>;
33
+ export declare const todoReadTool: ToolDefinition<z.infer<typeof readParams>, {
34
+ todos: TodoItem[];
35
+ }>;
36
+ export {};
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The agent's own checklist for a multi-step task. `todo_write` replaces the whole list (one call per update, as
4
+ * in Kilo), `todo_read` returns it. Neither touches the workspace, so neither asks for approval.
5
+ */
6
+ const writeParams = z.object({
7
+ todos: z
8
+ .array(z.object({
9
+ content: z.string().min(1).max(500).describe('What this step is, in the imperative: "Add the README".'),
10
+ status: z.enum(['pending', 'in_progress', 'completed']).describe('Exactly one item may be in_progress.'),
11
+ }))
12
+ .max(50)
13
+ .describe('The full list, in order. Replaces the previous list; an empty array clears it.'),
14
+ });
15
+ const readParams = z.object({});
16
+ function store(ctx) {
17
+ if (!ctx.todos)
18
+ throw new Error('The todo list is not available in this run.');
19
+ return ctx.todos;
20
+ }
21
+ export const todoWriteTool = {
22
+ name: 'todo_write',
23
+ description: 'Create or update the checklist for a task with several steps. Send the complete list every time; mark one step in_progress and completed steps as you finish them. Skip it for simple one-step tasks.',
24
+ parameters: writeParams,
25
+ noApproval: true,
26
+ execute: async (args, ctx) => {
27
+ const list = store(ctx);
28
+ if (args.todos.filter((t) => t.status === 'in_progress').length > 1) {
29
+ throw new Error('Only one item can be in_progress at a time.');
30
+ }
31
+ const todos = args.todos.map((t, i) => ({ id: `todo-${i + 1}`, content: t.content, status: t.status }));
32
+ list.set(todos);
33
+ return { todos };
34
+ },
35
+ };
36
+ export const todoReadTool = {
37
+ name: 'todo_read',
38
+ description: 'Read the current checklist for this task.',
39
+ parameters: readParams,
40
+ noApproval: true,
41
+ execute: async (_args, ctx) => ({ todos: [...store(ctx).get()] }),
42
+ };
@@ -1,7 +1,8 @@
1
- import { mkdir, stat, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import { resolveAndCheckPath } from './shared.js';
5
+ import { decodeText, encodeText } from './text-encoding.js';
5
6
  const params = z.object({
6
7
  path: z.string().describe('Path of the file to write, relative to the working directory or absolute.'),
7
8
  content: z.string().describe('Full file content to write.'),
@@ -13,17 +14,22 @@ export const writeFileTool = {
13
14
  destructive: false,
14
15
  execute: async (args, ctx) => {
15
16
  const resolved = resolveAndCheckPath(args.path, ctx);
16
- const existedBefore = await stat(resolved).then(() => true, (err) => {
17
- if (err.code === 'ENOENT')
18
- return false;
19
- throw err;
20
- });
17
+ // Replacing a file keeps its encoding (a BOM, UTF-16), as edit_file and apply_patch do; a new file is UTF-8.
18
+ let encoding;
19
+ try {
20
+ encoding = decodeText(await readFile(resolved)).encoding;
21
+ }
22
+ catch (err) {
23
+ if (err.code !== 'ENOENT')
24
+ throw err;
25
+ }
21
26
  await mkdir(path.dirname(resolved), { recursive: true });
22
- await writeFile(resolved, args.content, 'utf8');
27
+ const bytes = encodeText(args.content, encoding ?? 'utf8');
28
+ await writeFile(resolved, bytes);
23
29
  return {
24
30
  path: resolved,
25
- bytes: Buffer.byteLength(args.content, 'utf8'),
26
- created: !existedBefore,
31
+ bytes: bytes.length,
32
+ created: encoding === undefined,
27
33
  };
28
34
  },
29
35
  };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The tools that change files. Anything that lists "the write tools" (read-only modes, plan and ask modes, dry
3
+ * runs, checkpoints, permission rules) must include every one of these; `tests/write-tools-guard.test.ts` scans
4
+ * the code for lists that do not. Add a new file-changing tool here and in that scan's failures.
5
+ */
6
+ export declare const FILE_WRITE_TOOL_NAMES: readonly ["write_file", "edit_file", "edit_notebook", "apply_patch"];
7
+ /**
8
+ * The files a tool call works on: every file of an `apply_patch` (its paths are inside the patch text), else the
9
+ * call's `path`. Anything that reacts to "a file was written or read" (auto-lint, steering, changed-file lists) uses
10
+ * this, never `args.path` alone.
11
+ */
12
+ export declare function filesTouchedBy(toolName: string, args: unknown): string[];
@@ -0,0 +1,18 @@
1
+ import { patchedPaths } from './apply-patch.js';
2
+ /**
3
+ * The tools that change files. Anything that lists "the write tools" (read-only modes, plan and ask modes, dry
4
+ * runs, checkpoints, permission rules) must include every one of these; `tests/write-tools-guard.test.ts` scans
5
+ * the code for lists that do not. Add a new file-changing tool here and in that scan's failures.
6
+ */
7
+ export const FILE_WRITE_TOOL_NAMES = ['write_file', 'edit_file', 'edit_notebook', 'apply_patch'];
8
+ /**
9
+ * The files a tool call works on: every file of an `apply_patch` (its paths are inside the patch text), else the
10
+ * call's `path`. Anything that reacts to "a file was written or read" (auto-lint, steering, changed-file lists) uses
11
+ * this, never `args.path` alone.
12
+ */
13
+ export function filesTouchedBy(toolName, args) {
14
+ const a = (args ?? {});
15
+ if (toolName === 'apply_patch')
16
+ return typeof a.patch === 'string' ? patchedPaths(a.patch) : [];
17
+ return typeof a.path === 'string' ? [a.path] : [];
18
+ }