flavor-code 1.2.8 → 1.2.10

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 (39) hide show
  1. package/README.md +47 -0
  2. package/README.zh-CN.md +42 -0
  3. package/dist/agent/loop.d.ts +4 -0
  4. package/dist/agent/types.d.ts +10 -0
  5. package/dist/{app-NL2O75LQ.js → app-USAFQVSX.js} +379 -12
  6. package/dist/{chunk-NVLYFU6P.js → chunk-F2HMO5Q2.js} +28 -11
  7. package/dist/{chunk-6X5PHLZI.js → chunk-G32MCAZA.js} +1 -1
  8. package/dist/{chunk-N2S7USST.js → chunk-RQTYHUMK.js} +2 -1
  9. package/dist/{chunk-ED4R7ZCP.js → chunk-WGYNTR4Q.js} +1820 -492
  10. package/dist/{chunk-MBLAXTUP.js → chunk-XFCJXRJ2.js} +15 -5
  11. package/dist/{claude-ink-WPWPEL5A.js → claude-ink-ORUA7GWG.js} +2 -2
  12. package/dist/cli.js +9 -8
  13. package/dist/config/protected-file.d.ts +6 -0
  14. package/dist/context/manager.d.ts +2 -0
  15. package/dist/context/workspace-instructions.d.ts +10 -0
  16. package/dist/desktop/main.js +10528 -2912
  17. package/dist/desktop/pixel-worker.js +73 -0
  18. package/dist/desktop/preload.cjs +80 -2
  19. package/dist/desktop-renderer/assets/index-BVxdOZFW.js +151 -0
  20. package/dist/desktop-renderer/assets/index-CPxt3vFY.css +1 -0
  21. package/dist/desktop-renderer/index.html +3 -4
  22. package/dist/harness/local.d.ts +4 -1
  23. package/dist/jobs/registry.d.ts +48 -0
  24. package/dist/{load-RN3WBX4C.js → load-6ZMEBHSG.js} +1 -1
  25. package/dist/permissions/engine.d.ts +7 -0
  26. package/dist/production.d.ts +13 -1
  27. package/dist/sdk/index.js +4 -4
  28. package/dist/terminal/service.d.ts +57 -0
  29. package/dist/tools/files.d.ts +10 -0
  30. package/dist/tools/jobs.d.ts +4 -0
  31. package/dist/tools/runtime.d.ts +2 -0
  32. package/dist/tools/shell.d.ts +13 -3
  33. package/dist/tools/terminal.d.ts +3 -0
  34. package/dist/tools/types.d.ts +76 -4
  35. package/dist/tools/web.d.ts +53 -0
  36. package/package.json +13 -2
  37. package//346/212/200/346/234/257/346/226/271/346/241/210/346/212/245/345/221/212.md +752 -1
  38. package/dist/desktop-renderer/assets/index-57tXtXhI.js +0 -143
  39. package/dist/desktop-renderer/assets/index-Cm_ktmXm.css +0 -1
@@ -12,7 +12,7 @@ import {
12
12
  redactConfig,
13
13
  setProjectMcpServerDisabled,
14
14
  updateProtectedFile
15
- } from "./chunk-MBLAXTUP.js";
15
+ } from "./chunk-XFCJXRJ2.js";
16
16
  import {
17
17
  AuditLogger,
18
18
  appendUsageLog,
@@ -23,7 +23,7 @@ import {
23
23
  resolveLanguage,
24
24
  setUsageSession,
25
25
  usageLogPath
26
- } from "./chunk-N2S7USST.js";
26
+ } from "./chunk-RQTYHUMK.js";
27
27
 
28
28
  // src/models/types.ts
29
29
  function modelContentText(content) {
@@ -70,11 +70,11 @@ function message(error) {
70
70
  // src/permissions/engine.ts
71
71
  import { existsSync, realpathSync } from "fs";
72
72
  import { dirname, isAbsolute, parse, relative, resolve, sep } from "path";
73
- var CONTROL_TOOLS = /* @__PURE__ */ new Set(["TaskPlan", "TaskUpdate", "AskUserQuestion", "TodoWrite", "TaskOutput"]);
73
+ var CONTROL_TOOLS = /* @__PURE__ */ new Set(["TaskPlan", "TaskUpdate", "AskUserQuestion", "TodoWrite", "TaskOutput", "JobList", "JobRead", "JobWait", "JobKill", "TerminalRead", "TerminalList"]);
74
74
  var READ_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "Search", "List", "SkillResource", "LspFindRefs", "LspHover", "LspDiagnostics", "ListRegisteredTools"]);
75
75
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "ApplyPatch", "Copy", "Mkdir", "RegisterTool"]);
76
76
  var DESTRUCTIVE_TOOLS = /* @__PURE__ */ new Set(["Delete", "Move", "RemoveTool"]);
77
- var SHELL_TOOLS = /* @__PURE__ */ new Set(["Shell", "Bash", "Command", "Exec"]);
77
+ var SHELL_TOOLS = /* @__PURE__ */ new Set(["Shell", "Bash", "Command", "Exec", "TerminalOpen", "TerminalWrite", "TerminalResize", "TerminalClose"]);
78
78
  var NETWORK_TOOLS = /* @__PURE__ */ new Set(["WebFetch", "WebSearch", "Fetch", "Network"]);
79
79
  var PATH_REQUIRED_TOOLS = /* @__PURE__ */ new Set([
80
80
  "Read",
@@ -107,18 +107,26 @@ var PermissionEngine = class {
107
107
  #lexicalWorkspace;
108
108
  #workspace;
109
109
  #mode;
110
+ #profile;
110
111
  constructor(options) {
111
112
  const root = resolve(options.workspace);
112
113
  this.#lexicalWorkspace = root;
113
114
  this.#workspace = existsSync(root) ? realpathSync.native(root) : root;
114
115
  this.#mode = canonicalMode(options.mode ?? "default");
116
+ this.#profile = options.profile ?? "standard";
115
117
  }
116
118
  get mode() {
117
119
  return this.#mode;
118
120
  }
121
+ get profile() {
122
+ return this.#profile;
123
+ }
119
124
  setMode(mode) {
120
125
  this.#mode = canonicalMode(mode);
121
126
  }
127
+ setProfile(profile) {
128
+ this.#profile = profile;
129
+ }
122
130
  decide(request) {
123
131
  if (request.tool === "Task") return request.agent === "main" ? { decision: "allow" } : { decision: "deny", reason: "Task delegation is restricted to the main agent" };
124
132
  if (CONTROL_TOOLS.has(request.tool)) return { decision: "allow" };
@@ -137,6 +145,7 @@ var PermissionEngine = class {
137
145
  }
138
146
  }
139
147
  const inside = paths.every((path) => classifyPath(this.#lexicalWorkspace, this.#workspace, path).inside);
148
+ if (this.#profile === "d2c") return this.#d2cDecision(request, inside);
140
149
  if (this.#mode === "plan") {
141
150
  return READ_TOOLS.has(request.tool) ? { decision: "allow" } : { decision: "deny", reason: "Plan mode is read-only" };
142
151
  }
@@ -159,6 +168,44 @@ var PermissionEngine = class {
159
168
  }
160
169
  return ask(this.#mode, `Unknown tool: ${request.tool}`);
161
170
  }
171
+ #d2cDecision(request, inside) {
172
+ if ((request.paths?.length ?? 0) > 0 && !inside) {
173
+ return { decision: "deny", reason: "D2C tools must remain in the workspace" };
174
+ }
175
+ if (READ_TOOLS.has(request.tool)) return { decision: "allow" };
176
+ if (isDeletionTool(request.tool)) {
177
+ return { decision: "ask", reason: "Deletion requires approval", allowAlways: false };
178
+ }
179
+ if (DESTRUCTIVE_TOOLS.has(request.tool)) {
180
+ return request.tool === "Move" ? { decision: "allow" } : { decision: "ask", reason: "Deletion requires approval", allowAlways: false };
181
+ }
182
+ if (WRITE_TOOLS.has(request.tool)) return { decision: "allow" };
183
+ if (SHELL_TOOLS.has(request.tool)) return this.#d2cShellDecision(request);
184
+ if (isNetworkTool(request.tool)) return { decision: "allow" };
185
+ return { decision: "allow" };
186
+ }
187
+ #d2cShellDecision(request) {
188
+ const analysis = request.args === void 0 ? analyzeCommand(request.command ?? "") : analyzeArgumentCommand(request.command ?? "", request.args);
189
+ const cwd = request.cwd ?? this.#workspace;
190
+ const cwdClassification = classifyPath(this.#lexicalWorkspace, this.#workspace, cwd);
191
+ if (cwdClassification.escape || !cwdClassification.inside) {
192
+ return { decision: "deny", reason: "D2C shell cwd must remain in the workspace" };
193
+ }
194
+ if (analysis.catastrophic) {
195
+ return { decision: "deny", reason: "Explicitly forbidden system-level command" };
196
+ }
197
+ if (isD2cManagedPreviewCommand(analysis.command)) {
198
+ return {
199
+ decision: "deny",
200
+ reason: "D2C preview lifecycle is managed by D2cCompare; pass the project directory directly instead of starting npm/Vite manually"
201
+ };
202
+ }
203
+ if (assessD2cCommandPaths(analysis.command, cwd, this.#workspace, this.#lexicalWorkspace) === "deny") {
204
+ return { decision: "deny", reason: "D2C command arguments escape the workspace" };
205
+ }
206
+ if (analysis.deletion) return { decision: "ask", reason: "Deletion requires approval", allowAlways: false };
207
+ return { decision: "allow" };
208
+ }
162
209
  #shellDecision(request) {
163
210
  const analysis = request.args === void 0 ? analyzeCommand(request.command ?? "") : analyzeArgumentCommand(request.command ?? "", request.args);
164
211
  if (request.agent === "subagent") {
@@ -201,25 +248,29 @@ function ask(mode, reason) {
201
248
  function isNetworkTool(name) {
202
249
  return NETWORK_TOOLS.has(name) || name.startsWith("mcp__");
203
250
  }
251
+ function isDeletionTool(name) {
252
+ if (name === "Delete" || name === "RemoveTool") return true;
253
+ return /(?:^|[_-])(delete|remove|unlink|destroy)(?:[_-]|$)/i.test(name);
254
+ }
204
255
  function analyzeArgumentCommand(executable, args) {
205
256
  const name = parse(executable.replace(/\.exe$/i, "")).name.toLowerCase();
206
257
  const command = { executable: name, args: [...args], raw: [executable, ...args].join(" ").toLowerCase() };
207
258
  if (["sh", "bash", "zsh"].includes(name)) {
208
259
  const flag = args.findIndex((arg) => arg.toLowerCase() === "-c");
209
- if (flag < 0 || args[flag + 1] === void 0) return { command, destructive: false, opaque: true, wrapped: true };
260
+ if (flag < 0 || args[flag + 1] === void 0) return commandAnalysis(command, true, true);
210
261
  return { ...analyzeCommand(args[flag + 1]), wrapped: true };
211
262
  }
212
263
  if (name === "cmd") {
213
264
  const flag = args.findIndex((arg) => arg.toLowerCase() === "/c");
214
- if (flag < 0 || args[flag + 1] === void 0) return { command, destructive: false, opaque: true, wrapped: true };
265
+ if (flag < 0 || args[flag + 1] === void 0) return commandAnalysis(command, true, true);
215
266
  return { ...analyzeCommand(args.slice(flag + 1).join(" ")), wrapped: true };
216
267
  }
217
268
  if (["powershell", "pwsh"].includes(name)) {
218
269
  const flag = args.findIndex((arg) => ["-command", "-c"].includes(arg.toLowerCase()));
219
- if (flag < 0 || args[flag + 1] === void 0) return { command, destructive: false, opaque: true, wrapped: true };
270
+ if (flag < 0 || args[flag + 1] === void 0) return commandAnalysis(command, true, true);
220
271
  return { ...analyzeCommand(args.slice(flag + 1).join(" ")), wrapped: true };
221
272
  }
222
- return { command, destructive: isForbiddenCommand(command), opaque: false, wrapped: false };
273
+ return commandAnalysis(command, false, false);
223
274
  }
224
275
  function classifyPath(lexicalWorkspace, physicalWorkspace, input) {
225
276
  const candidate = resolve(lexicalWorkspace, input);
@@ -256,33 +307,60 @@ function isWithin(root, candidate) {
256
307
  const delta = relative(root, candidate);
257
308
  return delta === "" || !delta.startsWith(`..${sep}`) && delta !== ".." && !isAbsolute(delta);
258
309
  }
310
+ function isD2cManagedPreviewCommand(command) {
311
+ const executable = command.executable.replace(/\.cmd$/i, "");
312
+ const args = command.args.map((arg) => arg.toLowerCase());
313
+ if (executable === "npm") {
314
+ if (args[0] === "start") return true;
315
+ if (args[0] === "run" && ["dev", "start", "serve", "preview"].includes(args[1] ?? "")) return true;
316
+ if (args[0] === "exec" && args.some((arg) => arg === "vite")) return true;
317
+ }
318
+ if (["pnpm", "yarn", "bun"].includes(executable)) {
319
+ const script = args[0] === "run" ? args[1] : args[0];
320
+ if (["dev", "start", "serve", "preview"].includes(script ?? "")) return true;
321
+ }
322
+ if (executable === "npx" && args.some((arg) => arg === "vite")) return true;
323
+ if (executable === "vite") return true;
324
+ if (executable === "node" && args.some((arg) => /(?:^|[\\/])vite[\\/]bin[\\/]vite(?:\.js)?$/i.test(arg))) return true;
325
+ return /(?:^|[\s;&|])(?:npm(?:\.cmd)?\s+(?:run\s+)?(?:dev|start|serve|preview)|(?:pnpm|yarn|bun)(?:\.cmd)?\s+(?:run\s+)?(?:dev|start|serve|preview)|npx(?:\.cmd)?\s+(?:[^\s]+\s+)*vite|npm(?:\.cmd)?\s+exec\s+(?:--\s+)?vite|vite(?:\.cmd)?|node(?:\.exe)?\s+[^\s]*vite[\\/]bin[\\/]vite(?:\.js)?)(?:\s|$)/i.test(command.raw);
326
+ }
327
+ function commandAnalysis(command, opaque, wrapped) {
328
+ return {
329
+ command,
330
+ destructive: isForbiddenCommand(command),
331
+ deletion: isDeletionCommand(command),
332
+ catastrophic: isCatastrophicCommand(command),
333
+ opaque,
334
+ wrapped
335
+ };
336
+ }
259
337
  function analyzeCommand(raw, depth = 0) {
260
338
  const command = parseCommand(raw);
261
- if (depth > 4) return { command, destructive: false, opaque: true, wrapped: depth > 0 };
339
+ if (depth > 4) return commandAnalysis(command, true, depth > 0);
262
340
  const wrapper = command.executable;
263
341
  if (["sh", "bash", "zsh"].includes(wrapper)) {
264
342
  const flag = command.args.findIndex((arg) => arg.toLowerCase() === "-c");
265
- if (flag < 0 || command.args[flag + 1] === void 0) return { command, destructive: false, opaque: true, wrapped: true };
343
+ if (flag < 0 || command.args[flag + 1] === void 0) return commandAnalysis(command, true, true);
266
344
  return { ...analyzeCommand(command.args.slice(flag + 1).join(" "), depth + 1), wrapped: true };
267
345
  }
268
346
  if (wrapper === "cmd") {
269
347
  const flag = command.args.findIndex((arg) => arg.toLowerCase() === "/c");
270
- if (flag < 0 || command.args[flag + 1] === void 0) return { command, destructive: false, opaque: true, wrapped: true };
348
+ if (flag < 0 || command.args[flag + 1] === void 0) return commandAnalysis(command, true, true);
271
349
  return { ...analyzeCommand(command.args.slice(flag + 1).join(" "), depth + 1), wrapped: true };
272
350
  }
273
351
  if (["powershell", "pwsh"].includes(wrapper)) {
274
352
  const fileFlag = command.args.some((arg) => ["-file", "-f"].includes(arg.toLowerCase()));
275
- if (fileFlag) return { command, destructive: false, opaque: true, wrapped: true };
353
+ if (fileFlag) return commandAnalysis(command, true, true);
276
354
  const flag = command.args.findIndex((arg) => ["-command", "-c"].includes(arg.toLowerCase()));
277
- if (flag < 0 || command.args[flag + 1] === void 0) return { command, destructive: false, opaque: true, wrapped: true };
355
+ if (flag < 0 || command.args[flag + 1] === void 0) return commandAnalysis(command, true, true);
278
356
  return { ...analyzeCommand(command.args.slice(flag + 1).join(" "), depth + 1), wrapped: true };
279
357
  }
280
358
  if (wrapper === "call") {
281
- if (command.args.length === 0) return { command, destructive: false, opaque: true, wrapped: true };
359
+ if (command.args.length === 0) return commandAnalysis(command, true, true);
282
360
  return { ...analyzeCommand(command.args.join(" "), depth + 1), wrapped: true };
283
361
  }
284
362
  const opaque = ["start", "for", "if"].includes(wrapper) || /[%!()]/.test(command.raw);
285
- return { command, destructive: isForbiddenCommand(command), opaque, wrapped: depth > 0 };
363
+ return commandAnalysis(command, opaque, depth > 0);
286
364
  }
287
365
  function parseCommand(raw) {
288
366
  const tokens = raw.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((token) => token.replace(/^(?:"(.*)"|'(.*)')$/, "$1$2")) ?? [];
@@ -290,15 +368,26 @@ function parseCommand(raw) {
290
368
  return { executable: parse(executable).name.toLowerCase(), args: tokens, raw: raw.toLowerCase() };
291
369
  }
292
370
  function isForbiddenCommand(command) {
371
+ return isCatastrophicCommand(command) || /(^|\s)(git\s+reset\s+--hard|git\s+clean\s+-[^\s]*f)/.test(command.raw);
372
+ }
373
+ function isCatastrophicCommand(command) {
293
374
  if (["shutdown", "reboot", "halt", "mkfs", "diskpart", "format"].includes(command.executable)) return true;
375
+ if (/(^|[;&|]\s*)(shutdown|reboot|halt|mkfs|diskpart|format)(?:\.exe)?(?:\s|$)/i.test(command.raw)) return true;
294
376
  if (command.executable === "rm" && isDestructiveRm(command.args)) return true;
295
377
  for (const match of command.raw.matchAll(/\brm(?:\.exe)?\s+([^;&|]+)/gi)) {
296
378
  const nested = parseCommand(`rm ${match[1] ?? ""}`);
297
379
  if (isDestructiveRm(nested.args)) return true;
298
380
  }
299
- if (command.executable === "remove-item" && command.args.some((arg) => /^-(recurse|r)$/i.test(arg)) && command.args.some(isFilesystemRoot)) return true;
300
- if (command.executable === "dd" && command.args.some((arg) => /^of=\/dev\//i.test(arg))) return true;
301
- return /(^|\s)(git\s+reset\s+--hard|git\s+clean\s+-[^\s]*f)/.test(command.raw);
381
+ if (/\b(remove-item|ri)\b/i.test(command.raw)) {
382
+ const recursive = /\s-(recurse|r)(?:\s|$)/i.test(command.raw);
383
+ const root = command.args.some(isFilesystemRoot) || /\s(?:[a-z]:[\\/]?|\/)(?:\s|$)/i.test(command.raw);
384
+ if (recursive && root) return true;
385
+ }
386
+ return command.executable === "dd" && command.args.some((arg) => /^of=\/dev\//i.test(arg));
387
+ }
388
+ function isDeletionCommand(command) {
389
+ if (["rm", "rmdir", "unlink", "del", "erase", "remove-item", "ri"].includes(command.executable)) return true;
390
+ return /(^|[;&|]\s*)(rm|rmdir|unlink|del|erase|remove-item|ri)(?:\.exe)?(?:\s|$)/i.test(command.raw) || /(^|\s)git\s+(rm|clean)(?:\s|$)/i.test(command.raw) || /(^|\s)git\s+reset\s+--hard(?:\s|$)/i.test(command.raw) || /(^|\s)find\s+[^;&|]*(?:-delete|-exec\s+rm)(?:\s|$)/i.test(command.raw);
302
391
  }
303
392
  function isDestructiveRm(args) {
304
393
  const shortFlags = args.filter((arg) => /^-[^-]/.test(arg)).join("").toLowerCase();
@@ -387,6 +476,16 @@ function assessArgumentPath(value, cwd, lexicalWorkspace, physicalWorkspace) {
387
476
  function looksLikePath(value) {
388
477
  return isAbsolute(value) || value === "." || value === ".." || value.startsWith("./") || value.startsWith(".\\") || value.startsWith("../") || value.startsWith("..\\") || value.includes("/") || value.includes("\\");
389
478
  }
479
+ function assessD2cCommandPaths(command, cwd, workspace, lexicalWorkspace) {
480
+ for (const raw of command.args) {
481
+ const value = raw.includes("=") ? raw.slice(raw.indexOf("=") + 1) : raw;
482
+ if (process.platform === "win32" && /^\/[a-z?]+$/i.test(value)) continue;
483
+ if (!(isAbsolute(value) || value === ".." || value.startsWith("../") || value.startsWith("..\\"))) continue;
484
+ const classification = classifyPath(lexicalWorkspace, workspace, resolve(cwd, value));
485
+ if (classification.escape || !classification.inside) return "deny";
486
+ }
487
+ return "allow";
488
+ }
390
489
 
391
490
  // src/utils/redact.ts
392
491
  function redactErrorText(text) {
@@ -996,22 +1095,32 @@ function transcriptReducer(state, action) {
996
1095
  state: "running",
997
1096
  text: `${event.name}${event.label ? ` ${event.label}` : ""}`,
998
1097
  ...event.hint === void 0 ? {} : { hint: event.hint },
1098
+ ...event.presentation === void 0 ? {} : { presentation: event.presentation },
999
1099
  tool: { name: event.name, input: event.input }
1000
1100
  });
1001
1101
  if (event.type === "tool-end") {
1002
1102
  const cancelled = !event.result.ok && event.result.error?.code === "cancelled";
1003
1103
  const previous = state.active.blocks.find((block) => block.kind === "status" && block.id === `tool:${event.id}`);
1004
1104
  const input = previous?.kind === "status" && previous.tool !== void 0 ? previous.tool.input : null;
1105
+ const presentation = event.result.ok ? event.result.presentation ?? (previous?.kind === "status" ? previous.presentation : void 0) : void 0;
1005
1106
  return upsertStatus(state, {
1006
1107
  kind: "status",
1007
1108
  id: `tool:${event.id}`,
1008
1109
  state: event.result.ok ? "completed" : cancelled ? "cancelled" : "failed",
1009
1110
  text: `${event.result.ok ? "\u2713" : "\xD7"} ${event.name}${event.label ? ` ${event.label}` : ""}`,
1010
1111
  ...event.hint === void 0 ? {} : { hint: event.hint },
1011
- ...event.result.ok && event.result.presentation !== void 0 ? { presentation: event.result.presentation } : {},
1112
+ ...presentation === void 0 ? {} : { presentation },
1012
1113
  tool: { name: event.name, input, result: event.result }
1013
1114
  });
1014
1115
  }
1116
+ if (event.type === "deliverables") return upsertStatus(state, {
1117
+ kind: "status",
1118
+ id: `deliverables:${state.active.id}`,
1119
+ state: "completed",
1120
+ text: `Changed ${event.files.length} file${event.files.length === 1 ? "" : "s"}`,
1121
+ details: event.files.map((file) => `${file.operation} ${file.path} (+${file.added} -${file.removed})`).join("\n"),
1122
+ presentation: { kind: "changeset", files: event.files }
1123
+ });
1015
1124
  if (event.type === "notice") return upsertStatus(state, {
1016
1125
  kind: "status",
1017
1126
  id: `notice:${state.active.blocks.length}`,
@@ -1114,12 +1223,12 @@ function hydrateHistory(messages, compact) {
1114
1223
  function transcriptFromLegacyConversation(messages, compact) {
1115
1224
  return hydrateHistory(messages, compact);
1116
1225
  }
1117
- function applyTaskSnapshot(turn, snapshot2, includeTerminal = true) {
1226
+ function applyTaskSnapshot(turn, snapshot4, includeTerminal = true) {
1118
1227
  const taskBlocks = [];
1119
1228
  const previous = new Map(turn.blocks.filter((block) => block.kind === "status").map((block) => [block.id, block]));
1120
1229
  const now = Date.now();
1121
1230
  const suppressed = new Set(turn.suppressedTaskIds ?? []);
1122
- for (const task of snapshot2.plan?.tasks ?? []) {
1231
+ for (const task of snapshot4.plan?.tasks ?? []) {
1123
1232
  const id = `task:${task.id}`;
1124
1233
  const terminal = ["completed", "failed", "blocked", "cancelled"].includes(task.status);
1125
1234
  if (!terminal) suppressed.delete(id);
@@ -1139,16 +1248,16 @@ function applyTaskSnapshot(turn, snapshot2, includeTerminal = true) {
1139
1248
  ...elapsedMs === void 0 ? {} : { elapsedMs }
1140
1249
  });
1141
1250
  }
1142
- for (const node of snapshot2.subagents.graph?.nodes ?? []) {
1143
- const status = snapshot2.subagents.states[node.id] ?? "pending";
1251
+ for (const node of snapshot4.subagents.graph?.nodes ?? []) {
1252
+ const status = snapshot4.subagents.states[node.id] ?? "pending";
1144
1253
  const id = `subagent:${node.id}`;
1145
1254
  const terminal = ["completed", "failed", "blocked", "cancelled"].includes(status);
1146
1255
  if (!terminal) suppressed.delete(id);
1147
1256
  if ((!includeTerminal || suppressed.has(id)) && terminal) continue;
1148
1257
  const state = status === "running" ? "running" : status === "completed" ? "completed" : status === "cancelled" ? "cancelled" : status === "failed" || status === "blocked" ? "failed" : "info";
1149
1258
  const prior = previous.get(id);
1150
- const snapshotStartedAt = snapshot2.subagents.startedAt?.[node.id];
1151
- const snapshotElapsedMs = snapshot2.subagents.elapsedMs?.[node.id];
1259
+ const snapshotStartedAt = snapshot4.subagents.startedAt?.[node.id];
1260
+ const snapshotElapsedMs = snapshot4.subagents.elapsedMs?.[node.id];
1152
1261
  const startedAt = state === "running" ? snapshotStartedAt ?? prior?.startedAt ?? now : prior?.startedAt ?? snapshotStartedAt;
1153
1262
  const elapsedMs = state !== "running" && startedAt !== void 0 ? prior?.elapsedMs ?? snapshotElapsedMs ?? Math.max(0, now - startedAt) : prior?.elapsedMs;
1154
1263
  taskBlocks.push({
@@ -1174,15 +1283,15 @@ function applyTaskSnapshot(turn, snapshot2, includeTerminal = true) {
1174
1283
  }
1175
1284
  return {
1176
1285
  ...turn,
1177
- taskSnapshot: snapshot2,
1286
+ taskSnapshot: snapshot4,
1178
1287
  suppressedTaskIds: [...suppressed],
1179
1288
  blocks,
1180
1289
  statusLines: blocks.filter((block) => block.kind === "status").map((block) => block.text)
1181
1290
  };
1182
1291
  }
1183
- function terminalTaskIds(snapshot2) {
1184
- const ids = (snapshot2.plan?.tasks ?? []).filter((task) => ["completed", "failed", "blocked", "cancelled"].includes(task.status)).map((task) => `task:${task.id}`);
1185
- for (const [id, status] of Object.entries(snapshot2.subagents.states)) {
1292
+ function terminalTaskIds(snapshot4) {
1293
+ const ids = (snapshot4.plan?.tasks ?? []).filter((task) => ["completed", "failed", "blocked", "cancelled"].includes(task.status)).map((task) => `task:${task.id}`);
1294
+ for (const [id, status] of Object.entries(snapshot4.subagents.states)) {
1186
1295
  if (["completed", "failed", "blocked", "cancelled"].includes(status)) ids.push(`subagent:${id}`);
1187
1296
  }
1188
1297
  return ids;
@@ -1466,7 +1575,8 @@ async function gitFiles(workspace) {
1466
1575
  execFile("git", ["-C", workspace, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
1467
1576
  encoding: "buffer",
1468
1577
  windowsHide: true,
1469
- maxBuffer: 16 * 1024 * 1024
1578
+ maxBuffer: 16 * 1024 * 1024,
1579
+ timeout: 3e4
1470
1580
  }, (error, stdout, stderr) => {
1471
1581
  if (error !== null) {
1472
1582
  reject(new Error(`Cannot enumerate checkpoint files with git: ${stderr.toString("utf8").trim() || error.message}`));
@@ -2490,10 +2600,10 @@ var AgentMessageQueue = class {
2490
2600
  return { steering: [...this.#steering], followUp: [...this.#followUp] };
2491
2601
  }
2492
2602
  clear() {
2493
- const snapshot2 = this.snapshot();
2603
+ const snapshot4 = this.snapshot();
2494
2604
  this.#steering.length = 0;
2495
2605
  this.#followUp.length = 0;
2496
- return snapshot2;
2606
+ return snapshot4;
2497
2607
  }
2498
2608
  #items(kind) {
2499
2609
  return kind === "steer" ? this.#steering : this.#followUp;
@@ -2862,10 +2972,10 @@ function runInvocation(invocation, signal, timeoutMs) {
2862
2972
  }
2863
2973
 
2864
2974
  // src/production.ts
2865
- import { readFile as readFile16 } from "fs/promises";
2866
- import { createHash as createHash8, randomUUID as randomUUID10 } from "crypto";
2975
+ import { readFile as readFile17 } from "fs/promises";
2976
+ import { createHash as createHash9, randomUUID as randomUUID12 } from "crypto";
2867
2977
  import { homedir, release as osRelease, version as osVersion } from "os";
2868
- import { join as join17, relative as relative18, resolve as resolve21, sep as sep17 } from "path";
2978
+ import { join as join17, relative as relative20, resolve as resolve24, sep as sep19 } from "path";
2869
2979
 
2870
2980
  // src/agent/subagents.ts
2871
2981
  import { z as z4 } from "zod";
@@ -3037,13 +3147,13 @@ function hasCycle(nodes) {
3037
3147
  // src/utils/async.ts
3038
3148
  function awaitWithSignal(promise, signal) {
3039
3149
  signal.throwIfAborted();
3040
- return new Promise((resolve22, reject) => {
3150
+ return new Promise((resolve25, reject) => {
3041
3151
  const onAbort = () => reject(signal.reason);
3042
3152
  signal.addEventListener("abort", onAbort, { once: true });
3043
3153
  promise.then(
3044
3154
  (value) => {
3045
3155
  signal.removeEventListener("abort", onAbort);
3046
- resolve22(value);
3156
+ resolve25(value);
3047
3157
  },
3048
3158
  (error) => {
3049
3159
  signal.removeEventListener("abort", onAbort);
@@ -3591,6 +3701,7 @@ var ContextManager = class _ContextManager {
3591
3701
  #system;
3592
3702
  #volatileSystem;
3593
3703
  #flavor;
3704
+ #workspaceInstructions;
3594
3705
  #memory;
3595
3706
  #userMemory;
3596
3707
  #compactAtChars;
@@ -3624,6 +3735,7 @@ var ContextManager = class _ContextManager {
3624
3735
  this.#system = options.system;
3625
3736
  this.#volatileSystem = options.volatileSystem;
3626
3737
  this.#flavor = options.flavor;
3738
+ this.#workspaceInstructions = options.workspaceInstructions;
3627
3739
  this.#memory = options.memory;
3628
3740
  this.#userMemory = options.userMemory;
3629
3741
  this.#taskState = options.taskState;
@@ -3657,6 +3769,7 @@ var ContextManager = class _ContextManager {
3657
3769
  system: resolveSystemSections(this.#system),
3658
3770
  ...this.#volatileSystem === void 0 ? {} : { volatileSystem: resolveSystemSections(this.#volatileSystem) },
3659
3771
  ...this.#flavor === void 0 ? {} : { flavor: this.#flavor },
3772
+ ...this.#workspaceInstructions === void 0 ? {} : { workspaceInstructions: this.#workspaceInstructions },
3660
3773
  ...this.#memory === void 0 ? {} : { memory: this.#memory },
3661
3774
  ...this.#taskState === void 0 ? {} : { taskState: this.#taskState },
3662
3775
  ...userMemory === void 0 ? {} : { userMemory },
@@ -3698,10 +3811,10 @@ var ContextManager = class _ContextManager {
3698
3811
  messages: providerValidMessages(this.#messages)
3699
3812
  };
3700
3813
  }
3701
- restore(snapshot2) {
3702
- const messages = providerValidMessages(snapshot2.messages);
3703
- const legacySummary = snapshot2.summary?.role === "system" && snapshot2.summary.content.startsWith("Conversation summary\n") ? snapshot2.summary.content.slice("Conversation summary\n".length) : void 0;
3704
- this.#compact = snapshot2.compact === void 0 ? legacySummary === void 0 ? void 0 : { summary: legacySummary, compactedAt: (/* @__PURE__ */ new Date(0)).toISOString() } : { ...snapshot2.compact };
3814
+ restore(snapshot4) {
3815
+ const messages = providerValidMessages(snapshot4.messages);
3816
+ const legacySummary = snapshot4.summary?.role === "system" && snapshot4.summary.content.startsWith("Conversation summary\n") ? snapshot4.summary.content.slice("Conversation summary\n".length) : void 0;
3817
+ this.#compact = snapshot4.compact === void 0 ? legacySummary === void 0 ? void 0 : { summary: legacySummary, compactedAt: (/* @__PURE__ */ new Date(0)).toISOString() } : { ...snapshot4.compact };
3705
3818
  this.#messages = messages.map((message2) => message2.role === "tool" ? { ...message2, content: truncateToolOutput(message2.content, this.#toolOutputChars) } : cloneMessage2(message2));
3706
3819
  this.#lastRecordedInputTokens = void 0;
3707
3820
  this.#estimatedTokensAtLastRecordedUsage = void 0;
@@ -3867,6 +3980,12 @@ var ContextManager = class _ContextManager {
3867
3980
  ${this.#flavor}`,
3868
3981
  cacheBreakpoint: true
3869
3982
  }],
3983
+ ...this.#workspaceInstructions === void 0 ? [] : [{
3984
+ role: "system",
3985
+ content: `Workspace instructions
3986
+ ${this.#workspaceInstructions}`,
3987
+ cacheBreakpoint: true
3988
+ }],
3870
3989
  // Stable user preferences close the cacheable prefix.
3871
3990
  ...userMemory === void 0 ? [] : [{ role: "system", content: `User memory
3872
3991
  ${userMemory}` }]
@@ -3969,6 +4088,125 @@ function providerValidMessages(input) {
3969
4088
  return output;
3970
4089
  }
3971
4090
 
4091
+ // src/context/workspace-instructions.ts
4092
+ import { createHash as createHash2 } from "crypto";
4093
+ import { lstat as lstat3, readFile as readFile4, realpath as realpath2 } from "fs/promises";
4094
+ import { dirname as dirname7, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve6, sep as sep6 } from "path";
4095
+ var NAMES = ["AGENTS.md", "CLAUDE.md", "AGENTS.local.md", "CLAUDE.local.md"];
4096
+ var ROOT_NAMES = ["AGENTS.md", "CLAUDE.md", "AGENTS.local.md", "CLAUDE.local.md"];
4097
+ var WorkspaceInstructions = class {
4098
+ #root;
4099
+ #maxFileBytes;
4100
+ #maxTotalBytes;
4101
+ #seen = /* @__PURE__ */ new Map();
4102
+ constructor(workspace, options = {}) {
4103
+ this.#root = resolve6(workspace);
4104
+ this.#maxFileBytes = options.maxFileBytes ?? 64 * 1024;
4105
+ this.#maxTotalBytes = options.maxTotalBytes ?? 256 * 1024;
4106
+ }
4107
+ async baseline(scope2 = "main") {
4108
+ const documents = await this.#readDirectories([{ path: this.#root, depth: 0 }], ROOT_NAMES);
4109
+ return this.#format(this.#newOrChanged(documents, scope2));
4110
+ }
4111
+ async discover(paths, scope2 = "main") {
4112
+ const directories = /* @__PURE__ */ new Map();
4113
+ for (const path of paths) {
4114
+ const absolute = resolve6(this.#root, path);
4115
+ if (!within(this.#root, absolute)) continue;
4116
+ const targetDirectory = await directoryForPath(absolute);
4117
+ let current = targetDirectory;
4118
+ while (within(this.#root, current)) {
4119
+ const depth = depthOf(this.#root, current);
4120
+ directories.set(current, depth);
4121
+ if (current === this.#root) break;
4122
+ current = dirname7(current);
4123
+ }
4124
+ }
4125
+ const ordered = [...directories].map(([path, depth]) => ({ path, depth })).sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path));
4126
+ const changed = this.#newOrChanged(await this.#readDirectories(ordered, NAMES), scope2);
4127
+ const formatted = this.#format(changed);
4128
+ return formatted === "" ? [] : [formatted];
4129
+ }
4130
+ #newOrChanged(documents, scope2) {
4131
+ const seen = this.#seen.get(scope2) ?? /* @__PURE__ */ new Map();
4132
+ this.#seen.set(scope2, seen);
4133
+ const changed = [];
4134
+ for (const document of documents) {
4135
+ if (seen.get(document.path) === document.digest) continue;
4136
+ seen.set(document.path, document.digest);
4137
+ changed.push(document);
4138
+ }
4139
+ return changed;
4140
+ }
4141
+ async #readDirectories(directories, names) {
4142
+ const result = [];
4143
+ let physicalRoot;
4144
+ try {
4145
+ physicalRoot = await realpath2(this.#root);
4146
+ } catch {
4147
+ return result;
4148
+ }
4149
+ for (const directory of directories) {
4150
+ for (const name of names) {
4151
+ const path = resolve6(directory.path, name);
4152
+ try {
4153
+ const info = await lstat3(path);
4154
+ if (!info.isFile() && !info.isSymbolicLink()) continue;
4155
+ const physical = await realpath2(path);
4156
+ if (!within(physicalRoot, physical)) continue;
4157
+ const bytes = await readFile4(path);
4158
+ const clipped = bytes.subarray(0, this.#maxFileBytes);
4159
+ const suffix = bytes.length > clipped.length ? `
4160
+
4161
+ [Instruction truncated at ${this.#maxFileBytes} bytes]` : "";
4162
+ const content = clipped.toString("utf8") + suffix;
4163
+ result.push({
4164
+ path,
4165
+ depth: directory.depth,
4166
+ content,
4167
+ digest: createHash2("sha256").update(bytes).digest("hex")
4168
+ });
4169
+ } catch {
4170
+ }
4171
+ }
4172
+ }
4173
+ return result;
4174
+ }
4175
+ #format(documents) {
4176
+ if (documents.length === 0) return "";
4177
+ const selected = [];
4178
+ let bytes = 0;
4179
+ for (const document of [...documents].sort((a, b) => b.depth - a.depth)) {
4180
+ const size = Buffer.byteLength(document.content);
4181
+ if (bytes + size > this.#maxTotalBytes) continue;
4182
+ bytes += size;
4183
+ selected.push(document);
4184
+ }
4185
+ selected.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path));
4186
+ return selected.map((document) => {
4187
+ const display = relative6(this.#root, document.path) || document.path;
4188
+ return `<workspace-instructions path="${display}">
4189
+ ${document.content}
4190
+ </workspace-instructions>`;
4191
+ }).join("\n\n");
4192
+ }
4193
+ };
4194
+ async function directoryForPath(path) {
4195
+ try {
4196
+ return (await lstat3(path)).isDirectory() ? path : dirname7(path);
4197
+ } catch {
4198
+ return dirname7(path);
4199
+ }
4200
+ }
4201
+ function depthOf(root, path) {
4202
+ const delta = relative6(root, path);
4203
+ return delta === "" ? 0 : delta.split(sep6).length;
4204
+ }
4205
+ function within(root, candidate) {
4206
+ const delta = relative6(root, candidate);
4207
+ return delta === "" || !delta.startsWith(`..${sep6}`) && delta !== ".." && !isAbsolute4(delta);
4208
+ }
4209
+
3972
4210
  // src/context/summarizer.ts
3973
4211
  async function summarizeWithModel(options) {
3974
4212
  const maxAttempts = options.maxPromptTooLongAttempts ?? 3;
@@ -4244,10 +4482,10 @@ function sanitize(message2, secrets) {
4244
4482
  }
4245
4483
  function waitForRetry(delayMs, signal) {
4246
4484
  if (signal?.aborted) return Promise.reject(signal.reason);
4247
- return new Promise((resolve22, reject) => {
4485
+ return new Promise((resolve25, reject) => {
4248
4486
  const timer = setTimeout(() => {
4249
4487
  signal?.removeEventListener("abort", onAbort);
4250
- resolve22();
4488
+ resolve25();
4251
4489
  }, delayMs);
4252
4490
  const onAbort = () => {
4253
4491
  clearTimeout(timer);
@@ -4260,6 +4498,7 @@ function waitForRetry(delayMs, signal) {
4260
4498
 
4261
4499
  // src/agent/loop.ts
4262
4500
  var DEFAULT_MAX_ITERATIONS = 40;
4501
+ var D2C_MAX_ITERATION_EXTENSIONS = 10;
4263
4502
  var DEFAULT_MODEL_ATTEMPTS = 3;
4264
4503
  var FALLBACK_MODEL_ATTEMPTS = 2;
4265
4504
  var RETRY_BASE_DELAY_MS = 1e3;
@@ -4278,6 +4517,7 @@ function envMaxIterations() {
4278
4517
  }
4279
4518
  var AgentLoop = class {
4280
4519
  #options;
4520
+ #iterationLimitMode = "standard";
4281
4521
  constructor(options) {
4282
4522
  const envOverride = envMaxIterations();
4283
4523
  const maxIterations = envOverride ?? options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
@@ -4300,6 +4540,9 @@ var AgentLoop = class {
4300
4540
  get modelId() {
4301
4541
  return this.#options.modelId;
4302
4542
  }
4543
+ get iterationLimitMode() {
4544
+ return this.#iterationLimitMode;
4545
+ }
4303
4546
  setModel(modelId) {
4304
4547
  this.#options.registry.get(modelId);
4305
4548
  this.#options.modelId = modelId;
@@ -4308,6 +4551,10 @@ var AgentLoop = class {
4308
4551
  this.#options.registry.get(modelId);
4309
4552
  this.#options.fallbackModelId = modelId;
4310
4553
  }
4554
+ /** D2C keeps a hard cap, but may automatically expand it up to ten times. */
4555
+ setIterationLimitMode(mode) {
4556
+ this.#iterationLimitMode = mode;
4557
+ }
4311
4558
  async *run(request) {
4312
4559
  this.#options.context.append(request.initialUserMessage ?? { role: "user", content: request.prompt });
4313
4560
  let totalInputTokens = 0;
@@ -4317,7 +4564,10 @@ var AgentLoop = class {
4317
4564
  let hasCacheData = false;
4318
4565
  let accumulatedText = "";
4319
4566
  let modelInvocation = 0;
4567
+ const deliverables = /* @__PURE__ */ new Map();
4320
4568
  let maxIterations = this.#options.maxIterations;
4569
+ const iterationLimitMode = this.#iterationLimitMode;
4570
+ const maxExtensions = iterationLimitMode === "d2c" ? D2C_MAX_ITERATION_EXTENSIONS : this.#options.maxExtensions;
4321
4571
  let extensions = 0;
4322
4572
  let warned = false;
4323
4573
  let iteration = 0;
@@ -4332,11 +4582,12 @@ var AgentLoop = class {
4332
4582
  const remaining = maxIterations - iteration;
4333
4583
  yield {
4334
4584
  type: "warning",
4335
- message: `Approaching iteration limit: ${iteration}/${maxIterations} rounds used. ${remaining} rounds remaining before automatic circuit breaker.`
4585
+ message: iterationLimitMode === "d2c" ? `Approaching D2C base iteration limit: ${iteration}/${maxIterations} rounds used. ${remaining} rounds remain before automatic expansion (up to ${maxExtensions} expansions).` : `Approaching iteration limit: ${iteration}/${maxIterations} rounds used. ${remaining} rounds remaining before automatic circuit breaker.`
4336
4586
  };
4337
4587
  }
4338
4588
  if (iteration >= maxIterations) {
4339
- if (this.#options.hasActiveProgress?.() && extensions < this.#options.maxExtensions) {
4589
+ const canAutoExtend = extensions < maxExtensions && (iterationLimitMode === "d2c" || this.#options.hasActiveProgress?.() === true);
4590
+ if (canAutoExtend) {
4340
4591
  const previous = maxIterations;
4341
4592
  maxIterations += this.#options.extendIterations;
4342
4593
  extensions += 1;
@@ -4348,7 +4599,7 @@ var AgentLoop = class {
4348
4599
  };
4349
4600
  yield {
4350
4601
  type: "warning",
4351
- message: `Iteration limit ${previous} reached but task progress is active. Auto-extending by ${this.#options.extendIterations} rounds (extension ${extensions}/${this.#options.maxExtensions}).`
4602
+ message: iterationLimitMode === "d2c" ? `D2C iteration limit ${previous} reached. Auto-expanding by ${this.#options.extendIterations} rounds (expansion ${extensions}/${maxExtensions}).` : `Iteration limit ${previous} reached but task progress is active. Auto-extending by ${this.#options.extendIterations} rounds (extension ${extensions}/${maxExtensions}).`
4352
4603
  };
4353
4604
  continue;
4354
4605
  }
@@ -4680,6 +4931,7 @@ var AgentLoop = class {
4680
4931
  } catch {
4681
4932
  }
4682
4933
  }
4934
+ if (deliverables.size > 0) yield { type: "deliverables", files: [...deliverables.values()] };
4683
4935
  yield {
4684
4936
  type: "done",
4685
4937
  usage: {
@@ -4708,13 +4960,15 @@ var AgentLoop = class {
4708
4960
  for (const call of batch) {
4709
4961
  const label = this.#options.runtime.label(call);
4710
4962
  const hint = this.#options.runtime.hint(call);
4963
+ const presentation = this.#options.runtime.callPresentation(call);
4711
4964
  this.#options.hallucinationGuard?.recordToolCall(call.name, call.input, call.id);
4712
- yield { type: "tool-start", id: call.id, name: call.name, input: call.input, ...label === void 0 ? {} : { label }, ...hint === void 0 ? {} : { hint } };
4965
+ yield { type: "tool-start", id: call.id, name: call.name, input: call.input, ...label === void 0 ? {} : { label }, ...hint === void 0 ? {} : { hint }, ...presentation === void 0 ? {} : { presentation } };
4713
4966
  }
4714
4967
  const results = await Promise.all(batch.map(async (call) => ({
4715
4968
  call,
4716
4969
  result: await this.#options.runtime.execute(call, {
4717
4970
  agent: this.#options.agent,
4971
+ ...this.#options.ownerId === void 0 ? {} : { ownerId: this.#options.ownerId },
4718
4972
  ...request.signal === void 0 ? {} : { signal: request.signal }
4719
4973
  })
4720
4974
  })));
@@ -4751,7 +5005,14 @@ var AgentLoop = class {
4751
5005
  const endLabel = this.#options.runtime.label(call);
4752
5006
  const endHint = this.#options.runtime.hint(call);
4753
5007
  this.#options.hallucinationGuard?.recordToolResult(call.name, result, call.id);
5008
+ if (result.ok && result.presentation?.kind === "file-change") {
5009
+ for (const change of [result.presentation, ...result.presentation.relatedChanges ?? []]) {
5010
+ const current = deliverables.get(change.path);
5011
+ deliverables.set(change.path, current === void 0 ? { path: change.path, operation: change.operation, added: change.added, removed: change.removed } : { ...current, operation: change.operation, added: current.added + change.added, removed: current.removed + change.removed });
5012
+ }
5013
+ }
4754
5014
  yield { type: "tool-end", id: call.id, name: call.name, result, ...endLabel === void 0 ? {} : { label: endLabel }, ...endHint === void 0 ? {} : { hint: endHint } };
5015
+ for (const context of result.additionalContext ?? []) this.#options.context.append({ role: "system", content: context });
4755
5016
  }
4756
5017
  if (turnError !== void 0) {
4757
5018
  yield { type: "error", error: turnError };
@@ -4781,7 +5042,7 @@ function parallelReadBatchEnd(calls, start) {
4781
5042
  return end;
4782
5043
  }
4783
5044
  function toolResultMessage(toolCallId, result) {
4784
- return { role: "tool", toolCallId, content: JSON.stringify(result.ok ? result.output : { error: result.error }) ?? "null" };
5045
+ return { role: "tool", toolCallId, content: result.ok && result.content !== void 0 ? result.content : JSON.stringify(result.ok ? result.output : { error: result.error }) ?? "null" };
4785
5046
  }
4786
5047
  function abortMessage(signal) {
4787
5048
  return signal.reason instanceof Error ? signal.reason.message : "Agent run cancelled";
@@ -4795,10 +5056,10 @@ function serializeToolInput(input) {
4795
5056
  }
4796
5057
  function waitForRetry2(delayMs, signal) {
4797
5058
  if (signal?.aborted) return Promise.reject(signal.reason);
4798
- return new Promise((resolve22, reject) => {
5059
+ return new Promise((resolve25, reject) => {
4799
5060
  const timer = setTimeout(() => {
4800
5061
  signal?.removeEventListener("abort", onAbort);
4801
- resolve22();
5062
+ resolve25();
4802
5063
  }, delayMs);
4803
5064
  const onAbort = () => {
4804
5065
  clearTimeout(timer);
@@ -4876,7 +5137,7 @@ function redactClassifierText(value) {
4876
5137
  // src/tools/runtime.ts
4877
5138
  import { randomUUID as randomUUID3 } from "crypto";
4878
5139
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
4879
- import { basename as basename2, join as join5, resolve as resolve6 } from "path";
5140
+ import { basename as basename2, join as join5, resolve as resolve7 } from "path";
4880
5141
  import { z as z8 } from "zod";
4881
5142
 
4882
5143
  // src/tools/types.ts
@@ -4914,6 +5175,7 @@ var ToolRuntime = class {
4914
5175
  #disposeSchemas;
4915
5176
  #workspace;
4916
5177
  #outputLimits;
5178
+ #afterSuccess;
4917
5179
  #turnOutputChars = 0;
4918
5180
  #disposed = false;
4919
5181
  constructor(options) {
@@ -4923,8 +5185,9 @@ var ToolRuntime = class {
4923
5185
  this.#approve = options.approve;
4924
5186
  this.#classify = options.classify;
4925
5187
  this.#alwaysAllowed = new Set(options.alwaysAllowed ?? []);
4926
- this.#workspace = resolve6(options.workspace ?? process.cwd());
5188
+ this.#workspace = resolve7(options.workspace ?? process.cwd());
4927
5189
  this.#outputLimits = { ...DEFAULT_TOOL_OUTPUT_LIMITS, ...options.outputLimits };
5190
+ this.#afterSuccess = options.afterSuccess;
4928
5191
  validateOutputLimit("perToolChars", this.#outputLimits.perToolChars);
4929
5192
  validateOutputLimit("perTurnChars", this.#outputLimits.perTurnChars);
4930
5193
  this.#disposeSchemas = [
@@ -5034,7 +5297,7 @@ var ToolRuntime = class {
5034
5297
  if (permission.decision === "deny") {
5035
5298
  return this.#fail(tool.name, input, context.agent, "permission_denied", permission.reason ?? "Tool use denied");
5036
5299
  }
5037
- if (permission.decision === "ask" && context.agent === "main" && this.#permissions.mode === "auto" && this.#classify !== void 0) {
5300
+ if (permission.decision === "ask" && context.agent === "main" && this.#permissions.mode === "auto" && this.#permissions.profile === "standard" && this.#classify !== void 0) {
5038
5301
  try {
5039
5302
  const classified = await this.#classify(request, signal);
5040
5303
  if (classified.decision === "deny") {
@@ -5051,6 +5314,7 @@ var ToolRuntime = class {
5051
5314
  const sessionAllowed = this.#alwaysAllowed.has(getToolCategory(tool.name));
5052
5315
  if (!sessionAllowed && (pre.decision === "ask" || permission.decision === "ask")) {
5053
5316
  const reason = [pre.reason, permission.reason].filter((value) => value !== void 0).join("\n") || "Approval required";
5317
+ const allowAlways = permission.allowAlways !== false;
5054
5318
  const requestDecision = await this.#hooks.emit({
5055
5319
  version: 1,
5056
5320
  type: "PermissionRequest",
@@ -5066,7 +5330,7 @@ var ToolRuntime = class {
5066
5330
  const ctx = requestDecision.additionalContext ?? "";
5067
5331
  if (ctx.includes("codeisland:allow-all")) {
5068
5332
  const category = getToolCategory(tool.name);
5069
- if (category !== "destructive") this.#alwaysAllowed.add(category);
5333
+ if (allowAlways && category !== "destructive") this.#alwaysAllowed.add(category);
5070
5334
  }
5071
5335
  } else if (context.agent !== "main" && this.#permissions.mode !== "bubble") {
5072
5336
  return this.#fail(tool.name, input, context.agent, "approval_required", reason);
@@ -5074,29 +5338,46 @@ var ToolRuntime = class {
5074
5338
  throw signal.reason;
5075
5339
  } else {
5076
5340
  const category = getToolCategory(tool.name);
5077
- if (this.#approve === void 0) {
5341
+ if (allowAlways && this.#alwaysAllowed.has(category)) {
5342
+ } else if (this.#approve === void 0) {
5078
5343
  return this.#fail(tool.name, input, context.agent, "permission_denied", reason);
5079
5344
  } else {
5080
- const decision = await this.#approve({ ...request, reason }, signal);
5345
+ const decision = await this.#approve({ ...request, reason, ...allowAlways ? {} : { allowAlways: false } }, signal);
5081
5346
  if (decision === "deny") {
5082
5347
  return this.#fail(tool.name, input, context.agent, "user_denied", reason);
5083
5348
  }
5084
- if (decision === "always" && category !== "destructive") {
5349
+ if (decision === "always" && allowAlways && category !== "destructive") {
5085
5350
  this.#alwaysAllowed.add(category);
5086
5351
  }
5087
5352
  }
5088
5353
  }
5089
5354
  }
5090
5355
  if (signal.aborted) throw signal.reason;
5091
- const rawOutput = await tool.execute(input, signal);
5092
- const presentation = getToolPresentation(rawOutput);
5093
- const output = await this.#limitOutput(tool.name, rawOutput);
5356
+ const executed = await tool.execute(input, signal, context);
5357
+ let rawOutput;
5358
+ try {
5359
+ rawOutput = tool.outputSchema === void 0 ? executed : tool.outputSchema.parse(executed);
5360
+ } catch (error) {
5361
+ return this.#fail(tool.name, input, context.agent, "invalid_output", message(error));
5362
+ }
5363
+ const declaredPresentation = tool.presentResult?.(rawOutput, input);
5364
+ const presentation = declaredPresentation ?? getToolPresentation(rawOutput);
5365
+ const rendered = tool.renderForModel?.(rawOutput, input);
5366
+ const output = rendered === void 0 ? await this.#limitOutput(tool.name, rawOutput) : rawOutput;
5367
+ const content = rendered === void 0 ? void 0 : await this.#limitText(tool.name, rendered);
5368
+ const additionalContext = await this.#afterSuccess?.(tool.name, tool.paths(input), input, rawOutput, context);
5094
5369
  await this.#hooks.emit({
5095
5370
  version: 1,
5096
5371
  type: "PostToolUse",
5097
5372
  payload: { tool: tool.name, input, agent: context.agent, output }
5098
5373
  });
5099
- return { ok: true, output, ...presentation === void 0 ? {} : { presentation } };
5374
+ return {
5375
+ ok: true,
5376
+ output,
5377
+ ...content === void 0 ? {} : { content },
5378
+ ...presentation === void 0 ? {} : { presentation },
5379
+ ...additionalContext === void 0 || additionalContext.length === 0 ? {} : { additionalContext }
5380
+ };
5100
5381
  } catch (error) {
5101
5382
  return this.#fail(tool.name, input, context.agent, "tool_error", message(error));
5102
5383
  }
@@ -5133,6 +5414,19 @@ var ToolRuntime = class {
5133
5414
  savedTo
5134
5415
  };
5135
5416
  }
5417
+ callPresentation(call) {
5418
+ const tool = this.#tools.get(call.name);
5419
+ if (tool?.presentCall === void 0) return void 0;
5420
+ try {
5421
+ return tool.presentCall(tool.inputSchema.parse(call.input));
5422
+ } catch {
5423
+ return void 0;
5424
+ }
5425
+ }
5426
+ async #limitText(tool, content) {
5427
+ const limited = await this.#limitOutput(tool, content);
5428
+ return typeof limited === "string" ? limited : serializeToolOutput(limited);
5429
+ }
5136
5430
  async #fail(tool, input, agent, code, errorMessage) {
5137
5431
  const result = { ok: false, error: { code, message: errorMessage } };
5138
5432
  try {
@@ -5169,6 +5463,7 @@ var LocalHarness = class {
5169
5463
  #options;
5170
5464
  #subagentModelId;
5171
5465
  #mainPermissions;
5466
+ #permissionProfile = "standard";
5172
5467
  #contexts = /* @__PURE__ */ new WeakSet();
5173
5468
  #children = /* @__PURE__ */ new Set();
5174
5469
  #mainDefinitions;
@@ -5199,6 +5494,9 @@ var LocalHarness = class {
5199
5494
  get permissionMode() {
5200
5495
  return this.#mainPermissions.mode;
5201
5496
  }
5497
+ get permissionProfile() {
5498
+ return this.#permissionProfile;
5499
+ }
5202
5500
  setModel(role, modelId) {
5203
5501
  this.#options.registry.get(modelId);
5204
5502
  if (role === "main") this.main.loop.setModel(modelId);
@@ -5210,6 +5508,12 @@ var LocalHarness = class {
5210
5508
  setPermissionMode(mode) {
5211
5509
  this.#mainPermissions.setMode(mode);
5212
5510
  }
5511
+ setPermissionProfile(profile) {
5512
+ this.#permissionProfile = profile;
5513
+ this.#mainPermissions.setProfile(profile);
5514
+ this.main.loop.setIterationLimitMode(profile === "d2c" ? "d2c" : "standard");
5515
+ for (const child of this.#children) child.loop.setIterationLimitMode(profile === "d2c" ? "d2c" : "standard");
5516
+ }
5213
5517
  replaceMainTools(definitions) {
5214
5518
  if (this.#disposed) throw new Error("LocalHarness is disposed");
5215
5519
  const nextDefinitions = definitions.filter((tool) => tool.agents === void 0 || tool.agents.includes("main"));
@@ -5224,7 +5528,7 @@ var LocalHarness = class {
5224
5528
  const tools = this.#toolsForAgent("subagent").filter((tool) => !MAIN_TASK_TOOL_NAMES.has(tool.name));
5225
5529
  const context = this.#options.createContext("subagent", tools, this.#subagentModelId, parentContext);
5226
5530
  this.#claimContext(context);
5227
- const profile = this.#createProfile(this.#subagentModelId, tools, "subagent", context, this.#options.approve);
5531
+ const profile = this.#createProfile(this.#subagentModelId, tools, "subagent", context, this.#options.approve, void 0, `subagent:${task.id}`);
5228
5532
  let disposed = false;
5229
5533
  const child = {
5230
5534
  ...profile,
@@ -5270,9 +5574,11 @@ var LocalHarness = class {
5270
5574
  #toolsForAgent(agent) {
5271
5575
  return this.#options.tools.filter((tool) => tool.agents === void 0 || tool.agents.includes(agent));
5272
5576
  }
5273
- #createProfile(modelId, definitions, agent, context, approve, fallbackModelId) {
5577
+ #createProfile(modelId, definitions, agent, context, approve, fallbackModelId, ownerId = agent) {
5274
5578
  const permissions = new PermissionEngine({
5275
5579
  workspace: this.#options.workspace,
5580
+ ...this.#options.afterToolSuccess === void 0 ? {} : { afterSuccess: this.#options.afterToolSuccess },
5581
+ profile: this.#permissionProfile,
5276
5582
  mode: this.#options.loopMode ? "bypassPermissions" : agent === "subagent" ? this.#mainPermissions.mode === "plan" ? "plan" : "bubble" : this.#options.permissionMode ?? "default"
5277
5583
  });
5278
5584
  if (agent === "main") this.#mainPermissions = permissions;
@@ -5302,10 +5608,12 @@ var LocalHarness = class {
5302
5608
  hooks: this.#options.hooks,
5303
5609
  tools,
5304
5610
  agent,
5611
+ ownerId,
5305
5612
  ...maxIterations === void 0 ? {} : { maxIterations },
5306
5613
  ...isMain && this.#options.hasActiveProgress !== void 0 ? { hasActiveProgress: this.#options.hasActiveProgress } : {},
5307
5614
  ...isMain && this.#options.hallucinationGuard !== void 0 ? { hallucinationGuard: this.#options.hallucinationGuard } : {}
5308
5615
  });
5616
+ loop.setIterationLimitMode(this.#permissionProfile === "d2c" ? "d2c" : "standard");
5309
5617
  return { get modelId() {
5310
5618
  return loop.modelId;
5311
5619
  }, context, runtime, tools, loop };
@@ -5475,7 +5783,7 @@ var HookBus = class {
5475
5783
  }
5476
5784
  };
5477
5785
  function runShellHandler(descriptor, event, signal) {
5478
- return new Promise((resolve22, reject) => {
5786
+ return new Promise((resolve25, reject) => {
5479
5787
  const child = spawn3(descriptor.command, [...descriptor.args ?? []], {
5480
5788
  env: { ...process.env, ...descriptor.env },
5481
5789
  shell: false,
@@ -5518,7 +5826,7 @@ function runShellHandler(descriptor, event, signal) {
5518
5826
  return;
5519
5827
  }
5520
5828
  try {
5521
- resolve22(HookDecisionSchema.parse(JSON.parse(stdout)));
5829
+ resolve25(HookDecisionSchema.parse(JSON.parse(stdout)));
5522
5830
  } catch (error) {
5523
5831
  reject(error);
5524
5832
  }
@@ -6211,9 +6519,9 @@ function extractJson(text) {
6211
6519
  }
6212
6520
 
6213
6521
  // src/goal/classifier.ts
6214
- import { createHash as createHash2 } from "crypto";
6215
- import { readFile as readFile4, readdir as readdir3 } from "fs/promises";
6216
- import { join as join6, relative as relative6 } from "path";
6522
+ import { createHash as createHash3 } from "crypto";
6523
+ import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
6524
+ import { join as join6, relative as relative7 } from "path";
6217
6525
  function buildSkepticPrompt(evidence, plan, index) {
6218
6526
  const criteriaList = plan.criteria.map((c) => ` ${c.id}. [${c.type}] ${c.description}`).join("\n");
6219
6527
  return [
@@ -6377,19 +6685,19 @@ function deduplicateGaps(gaps) {
6377
6685
  }
6378
6686
  function computeFingerprint(gaps) {
6379
6687
  const content = gaps.map((g) => `${g.criterion}:${g.description}`).sort().join("\n");
6380
- return createHash2("sha256").update(content).digest("hex").slice(0, 16);
6688
+ return createHash3("sha256").update(content).digest("hex").slice(0, 16);
6381
6689
  }
6382
6690
  async function collectEvidence(workspace, objective, finalResponse, priorGaps) {
6383
6691
  const changedFiles = await collectChangedFiles(workspace);
6384
6692
  const planPath = join6(workspace, ".flavor", "goal-plan.md");
6385
6693
  let planFile = null;
6386
6694
  try {
6387
- planFile = await readFile4(planPath, "utf8");
6695
+ planFile = await readFile5(planPath, "utf8");
6388
6696
  } catch {
6389
6697
  }
6390
6698
  return {
6391
6699
  objective,
6392
- changedFiles: changedFiles.map((f) => relative6(workspace, f)),
6700
+ changedFiles: changedFiles.map((f) => relative7(workspace, f)),
6393
6701
  planFile,
6394
6702
  finalResponse,
6395
6703
  priorGaps
@@ -6407,7 +6715,7 @@ async function collectChangedFiles(workspace) {
6407
6715
  }
6408
6716
  for (const entry2 of entries) {
6409
6717
  const full = join6(dir, entry2.name);
6410
- const rel = relative6(workspace, full);
6718
+ const rel = relative7(workspace, full);
6411
6719
  if (entry2.name === ".git" || entry2.name === "node_modules" || entry2.name === ".flavor" || entry2.name.startsWith(".")) continue;
6412
6720
  if (entry2.isDirectory()) {
6413
6721
  await walk(full);
@@ -6681,21 +6989,21 @@ function formatPriorGaps(gaps) {
6681
6989
  // src/goal/store.ts
6682
6990
  import { constants } from "fs";
6683
6991
  import { randomUUID as randomUUID4 } from "crypto";
6684
- import { lstat as lstat3, mkdir as mkdir6, open as open3, rename as rename3, rm as rm2 } from "fs/promises";
6685
- import { dirname as dirname7, join as join8, relative as relative7, resolve as resolve7, sep as sep6 } from "path";
6992
+ import { lstat as lstat4, mkdir as mkdir6, open as open3, rename as rename3, rm as rm2 } from "fs/promises";
6993
+ import { dirname as dirname8, join as join8, relative as relative8, resolve as resolve8, sep as sep7 } from "path";
6686
6994
  var DEFAULT_MAX_BYTES = 1024 * 1024;
6687
6995
  var GoalStore = class {
6688
6996
  #workspace;
6689
6997
  #root;
6690
6998
  #maxBytes;
6691
6999
  constructor(options) {
6692
- this.#workspace = resolve7(options.workspace);
7000
+ this.#workspace = resolve8(options.workspace);
6693
7001
  this.#root = join8(this.#workspace, ".flavor", "goals");
6694
7002
  this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
6695
7003
  }
6696
7004
  async save(input) {
6697
7005
  const state = GoalStateSchema.parse(input);
6698
- await assertNoSymlink(this.#workspace, dirname7(this.#root));
7006
+ await assertNoSymlink(this.#workspace, dirname8(this.#root));
6699
7007
  await mkdir6(this.#root, { recursive: true, mode: 448 });
6700
7008
  await assertNoSymlink(this.#workspace, this.#root);
6701
7009
  const target = join8(this.#root, `${state.id}.json`);
@@ -6720,20 +7028,20 @@ var GoalStore = class {
6720
7028
  }
6721
7029
  };
6722
7030
  async function assertNoSymlink(root, target) {
6723
- let cursor = resolve7(target);
6724
- const stop = resolve7(root);
6725
- const value = relative7(stop, cursor);
6726
- if (value === ".." || value.startsWith(`..${sep6}`) || value.includes(":")) {
7031
+ let cursor = resolve8(target);
7032
+ const stop = resolve8(root);
7033
+ const value = relative8(stop, cursor);
7034
+ if (value === ".." || value.startsWith(`..${sep7}`) || value.includes(":")) {
6727
7035
  throw new Error("Goal path escapes workspace");
6728
7036
  }
6729
7037
  const chain = [];
6730
7038
  while (cursor !== stop) {
6731
7039
  chain.push(cursor);
6732
- cursor = dirname7(cursor);
7040
+ cursor = dirname8(cursor);
6733
7041
  }
6734
7042
  for (const path of chain.reverse()) {
6735
7043
  try {
6736
- if ((await lstat3(path)).isSymbolicLink()) throw new Error(`Goal path contains a symbolic link: ${path}`);
7044
+ if ((await lstat4(path)).isSymbolicLink()) throw new Error(`Goal path contains a symbolic link: ${path}`);
6737
7045
  } catch (error) {
6738
7046
  if (!isCode3(error, "ENOENT")) throw error;
6739
7047
  }
@@ -6744,8 +7052,8 @@ function isCode3(error, code) {
6744
7052
  }
6745
7053
 
6746
7054
  // src/loop/isolation.ts
6747
- import { appendFile, mkdir as mkdir7, readFile as readFile5 } from "fs/promises";
6748
- import { join as join9, resolve as resolve8 } from "path";
7055
+ import { appendFile, mkdir as mkdir7, readFile as readFile6 } from "fs/promises";
7056
+ import { join as join9, resolve as resolve9 } from "path";
6749
7057
  var CODE_CHANGE = /\b(?:fix|implement|add|update|change|modify|refactor|build|create|remove|delete|write|migrate)\b|修复|实现|添加|更新|修改|重构|创建|删除|迁移/i;
6750
7058
  var READ_ONLY = /\b(?:review|analy[sz]e|inspect|explain|research|investigate|audit|summari[sz]e|report)\b|调研|分析|审查|检查|解释|研究|总结|报告/i;
6751
7059
  function inferGoalIntent(goal) {
@@ -6755,7 +7063,7 @@ function inferGoalIntent(goal) {
6755
7063
  }
6756
7064
  async function prepareLoopWorkspace(options) {
6757
7065
  options.signal?.throwIfAborted();
6758
- const root = resolve8(options.root);
7066
+ const root = resolve9(options.root);
6759
7067
  const intent = inferGoalIntent(options.goal);
6760
7068
  if (intent === "read_only") return { kind: "ready", workspace: { root, mode: "current" } };
6761
7069
  const inside = await git(root, ["rev-parse", "--is-inside-work-tree"]);
@@ -6798,13 +7106,13 @@ async function ensureWorktreeDirectoryIgnored(root) {
6798
7106
  if (ignored2.code === 0) return;
6799
7107
  const commonDirectory = await git(root, ["rev-parse", "--git-common-dir"]);
6800
7108
  if (commonDirectory.code !== 0) throw new Error(commonDirectory.stderr.trim() || "Git common directory is unavailable");
6801
- const gitDirectory = resolve8(root, commonDirectory.stdout.trim());
7109
+ const gitDirectory = resolve9(root, commonDirectory.stdout.trim());
6802
7110
  const infoDirectory = join9(gitDirectory, "info");
6803
7111
  const excludePath = join9(infoDirectory, "exclude");
6804
7112
  await mkdir7(infoDirectory, { recursive: true });
6805
7113
  let existing = "";
6806
7114
  try {
6807
- existing = await readFile5(excludePath, "utf8");
7115
+ existing = await readFile6(excludePath, "utf8");
6808
7116
  } catch (error) {
6809
7117
  if (!(typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")) throw error;
6810
7118
  }
@@ -6815,8 +7123,8 @@ async function ensureWorktreeDirectoryIgnored(root) {
6815
7123
 
6816
7124
  // src/loop/store.ts
6817
7125
  import { constants as constants2 } from "fs";
6818
- import { lstat as lstat4, mkdir as mkdir8, open as open4, readFile as readFile6, realpath as realpath2, rename as rename4, rm as rm3, stat as stat2 } from "fs/promises";
6819
- import { dirname as dirname8, join as join10, relative as relative8, resolve as resolve9, sep as sep7 } from "path";
7126
+ import { lstat as lstat5, mkdir as mkdir8, open as open4, readFile as readFile7, realpath as realpath3, rename as rename4, rm as rm3, stat as stat2 } from "fs/promises";
7127
+ import { dirname as dirname9, join as join10, relative as relative9, resolve as resolve10, sep as sep8 } from "path";
6820
7128
  import { randomUUID as randomUUID5 } from "crypto";
6821
7129
  var LOOP_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
6822
7130
  var DEFAULT_MAX_BYTES2 = 5 * 1024 * 1024;
@@ -6826,7 +7134,7 @@ var LoopStore = class {
6826
7134
  #maxBytes;
6827
7135
  #appendTails = /* @__PURE__ */ new Map();
6828
7136
  constructor(options) {
6829
- this.#workspace = resolve9(options.workspace);
7137
+ this.#workspace = resolve10(options.workspace);
6830
7138
  this.#root = join10(this.#workspace, ".flavor", "loops");
6831
7139
  this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES2;
6832
7140
  }
@@ -6868,7 +7176,7 @@ var LoopStore = class {
6868
7176
  try {
6869
7177
  const metadata = await stat2(target);
6870
7178
  if (!metadata.isFile() || metadata.size > this.#maxBytes) throw new Error("Loop state is not a bounded regular file");
6871
- const raw = await readFile6(target, "utf8");
7179
+ const raw = await readFile7(target, "utf8");
6872
7180
  const state = LoopStateSchema.parse(JSON.parse(raw));
6873
7181
  await this.#assertWorkspace(state.workspace);
6874
7182
  return state;
@@ -6898,7 +7206,7 @@ var LoopStore = class {
6898
7206
  }
6899
7207
  async #prepare(loopId) {
6900
7208
  validateId(loopId);
6901
- await assertNoSymlink2(this.#workspace, dirname8(this.#root));
7209
+ await assertNoSymlink2(this.#workspace, dirname9(this.#root));
6902
7210
  await mkdir8(this.#root, { recursive: true, mode: 448 });
6903
7211
  await assertNoSymlink2(this.#workspace, this.#root);
6904
7212
  const directory = join10(this.#root, loopId);
@@ -6925,31 +7233,31 @@ function validateId(loopId) {
6925
7233
  if (!LOOP_ID.test(loopId)) throw new Error("Invalid loop id");
6926
7234
  }
6927
7235
  async function assertNoSymlink2(root, target) {
6928
- let cursor = resolve9(target);
6929
- const stop = resolve9(root);
7236
+ let cursor = resolve10(target);
7237
+ const stop = resolve10(root);
6930
7238
  if (!isWithin2(stop, cursor)) throw new Error("Loop path escapes workspace");
6931
7239
  const chain = [];
6932
7240
  while (cursor !== stop) {
6933
7241
  chain.push(cursor);
6934
- cursor = dirname8(cursor);
7242
+ cursor = dirname9(cursor);
6935
7243
  }
6936
7244
  for (const path of chain.reverse()) {
6937
7245
  try {
6938
- if ((await lstat4(path)).isSymbolicLink()) throw new Error(`Loop path contains a symbolic link: ${path}`);
7246
+ if ((await lstat5(path)).isSymbolicLink()) throw new Error(`Loop path contains a symbolic link: ${path}`);
6939
7247
  } catch (error) {
6940
7248
  if (!isCode4(error, "ENOENT")) throw error;
6941
7249
  }
6942
7250
  }
6943
7251
  }
6944
7252
  function isWithin2(root, path) {
6945
- const value = relative8(resolve9(root), resolve9(path));
6946
- return value === "" || !value.startsWith(`..${sep7}`) && value !== ".." && !value.includes(":");
7253
+ const value = relative9(resolve10(root), resolve10(path));
7254
+ return value === "" || !value.startsWith(`..${sep8}`) && value !== ".." && !value.includes(":");
6947
7255
  }
6948
7256
  async function canonical(path) {
6949
7257
  try {
6950
- return await realpath2(resolve9(path));
7258
+ return await realpath3(resolve10(path));
6951
7259
  } catch {
6952
- return resolve9(path);
7260
+ return resolve10(path);
6953
7261
  }
6954
7262
  }
6955
7263
  function isCode4(error, code) {
@@ -6957,46 +7265,191 @@ function isCode4(error, code) {
6957
7265
  }
6958
7266
 
6959
7267
  // src/loop/verifier.ts
6960
- import { readFile as readFile7 } from "fs/promises";
7268
+ import { readFile as readFile8 } from "fs/promises";
6961
7269
  import { join as join11 } from "path";
6962
7270
 
6963
7271
  // src/tools/shell.ts
6964
7272
  import { spawn as spawn4 } from "child_process";
6965
- import { isAbsolute as isAbsolute4, relative as relative9, resolve as resolve10, sep as sep8 } from "path";
7273
+ import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve11, sep as sep9 } from "path";
7274
+ import { z as z13 } from "zod";
7275
+
7276
+ // src/tools/jobs.ts
6966
7277
  import { z as z12 } from "zod";
7278
+ var JobId = z12.string().regex(/^job-[a-f0-9-]+$/);
7279
+ var ReadInput = z12.object({ id: JobId, cursor: z12.coerce.number().int().min(0).optional() });
7280
+ var IdInput = z12.object({ id: JobId });
7281
+ function createJobTools(registry) {
7282
+ const tools = [
7283
+ {
7284
+ name: "JobList",
7285
+ description: "List background jobs owned by this agent",
7286
+ inputSchema: z12.object({}),
7287
+ paths: () => [],
7288
+ execute: async (_input, _signal, context) => registry.list(owner(context)),
7289
+ presentResult: (output) => jobListPresentation(output)
7290
+ },
7291
+ {
7292
+ name: "JobRead",
7293
+ description: "Read incremental output from an owned background job",
7294
+ inputSchema: ReadInput,
7295
+ paths: () => [],
7296
+ execute: async (input, _signal, context) => registry.read(input.id, owner(context), input.cursor ?? 0),
7297
+ renderForModel: (output) => JSON.stringify(output),
7298
+ presentResult: (output) => jobPresentation("read", output)
7299
+ },
7300
+ {
7301
+ name: "JobWait",
7302
+ description: "Wait for an owned background job to finish",
7303
+ inputSchema: IdInput,
7304
+ paths: () => [],
7305
+ execute: async (input, signal, context) => registry.wait(input.id, owner(context), signal),
7306
+ presentResult: (output) => jobPresentation("wait", output)
7307
+ },
7308
+ {
7309
+ name: "JobKill",
7310
+ description: "Cancel an owned background job and its process tree",
7311
+ inputSchema: IdInput,
7312
+ paths: () => [],
7313
+ execute: async (input, _signal, context) => {
7314
+ registry.kill(input.id, owner(context));
7315
+ return registry.read(input.id, owner(context));
7316
+ },
7317
+ presentResult: (output) => jobPresentation("kill", output)
7318
+ }
7319
+ ];
7320
+ return tools;
7321
+ }
7322
+ function jobPresentation(action, job) {
7323
+ const read = "output" in job ? job : void 0;
7324
+ return {
7325
+ kind: "job",
7326
+ action,
7327
+ id: job.id,
7328
+ jobKind: job.kind,
7329
+ label: job.label,
7330
+ state: job.state,
7331
+ ...job.exitCode === void 0 ? {} : { exitCode: job.exitCode },
7332
+ ...job.error === void 0 ? {} : { error: job.error },
7333
+ ...read === void 0 ? {} : { output: read.output, cursor: read.cursor },
7334
+ ...job.truncated ? { truncated: true } : {}
7335
+ };
7336
+ }
7337
+ function jobListPresentation(jobs) {
7338
+ return {
7339
+ kind: "job",
7340
+ action: "list",
7341
+ jobs: jobs.map((job) => ({
7342
+ id: job.id,
7343
+ kind: job.kind,
7344
+ label: job.label,
7345
+ state: job.state,
7346
+ ...job.exitCode === void 0 ? {} : { exitCode: job.exitCode }
7347
+ }))
7348
+ };
7349
+ }
7350
+ function owner(context) {
7351
+ return context?.ownerId ?? context?.agent ?? "main";
7352
+ }
7353
+
7354
+ // src/tools/shell.ts
6967
7355
  var DEFAULT_MAX_OUTPUT_BYTES2 = 1048576;
6968
- var ELLIPSIS = Buffer.from("\u2026");
7356
+ var DEFAULT_SHELL_TIMEOUT_MS = 10 * 6e4;
7357
+ var ELLIPSIS = "\u2026";
6969
7358
  var TERMINATION_GRACE_MS = 250;
6970
7359
  var TERMINATION_FAILURE_MS = 5e3;
6971
- var ShellInput = z12.object({
6972
- command: z12.string().min(1),
6973
- args: z12.array(z12.string()),
6974
- cwd: z12.string().min(1).nullable().optional(),
6975
- timeoutMs: z12.coerce.number().int().positive().max(864e5).nullable().optional()
7360
+ var STREAM_CLOSE_GRACE_MS = 500;
7361
+ var ShellInput = z13.object({
7362
+ command: z13.string().min(1),
7363
+ args: z13.array(z13.string()),
7364
+ cwd: z13.string().min(1).nullable().optional(),
7365
+ timeoutMs: z13.coerce.number().int().positive().max(864e5).nullable().optional(),
7366
+ background: z13.boolean().optional()
6976
7367
  });
6977
7368
  function createShellTool(workspace, options = {}) {
6978
- const root = resolve10(workspace);
7369
+ const root = resolve11(workspace);
6979
7370
  const maxBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES2;
7371
+ const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
6980
7372
  if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new Error("maxOutputBytes must be a positive integer");
7373
+ if (!Number.isSafeInteger(defaultTimeoutMs) || defaultTimeoutMs <= 0 || defaultTimeoutMs > 864e5) {
7374
+ throw new Error("defaultTimeoutMs must be a positive integer no greater than 86400000");
7375
+ }
6981
7376
  return {
6982
7377
  name: "Shell",
6983
- description: "Run a command with an argument array inside the workspace",
7378
+ description: "Run a bounded command with an argument array inside the workspace; background=true returns a managed job id",
6984
7379
  inputSchema: ShellInput,
6985
7380
  paths: (input) => [workingDirectory(root, input.cwd ?? void 0)],
6986
7381
  summarize: (input) => [input.command, ...input.args].join(" "),
7382
+ presentCall: (input) => ({ kind: "terminal", variant: "command", title: input.background ? "Starting background command" : "Running command", command: [input.command, ...input.args].join(" "), state: "running" }),
6987
7383
  permissions: (input) => ({
6988
7384
  paths: [workingDirectory(root, input.cwd ?? void 0)],
6989
7385
  command: input.command,
6990
7386
  args: input.args,
6991
7387
  cwd: workingDirectory(root, input.cwd ?? void 0)
6992
7388
  }),
6993
- execute: async (input, signal) => {
6994
- if (options.executionEnvironment === void 0) return executeShell(root, input, signal, maxBytes);
7389
+ presentResult: (output, input) => output && "jobId" in output ? {
7390
+ kind: "job",
7391
+ action: "start",
7392
+ id: output.jobId,
7393
+ jobKind: "shell",
7394
+ label: [input.command, ...input.args].join(" "),
7395
+ state: "running"
7396
+ } : { kind: "terminal", variant: "command", title: [input.command, ...input.args].join(" "), command: [input.command, ...input.args].join(" "), stdout: output.stdout, stderr: output.stderr, exitCode: output.exitCode, state: output.terminationReason === "cancelled" ? "cancelled" : output.exitCode === 0 ? "completed" : "failed", truncated: output.truncated },
7397
+ execute: async (input, signal, context) => {
7398
+ const timeoutMs = input.timeoutMs ?? defaultTimeoutMs;
7399
+ if (input.background === true) {
7400
+ if (options.jobs === void 0) throw new Error("Background shell requires a JobRegistry");
7401
+ const cancellation = new AbortController();
7402
+ const job = options.jobs.create({
7403
+ kind: "shell",
7404
+ owner: owner(context),
7405
+ label: [input.command, ...input.args].join(" "),
7406
+ cancel: () => cancellation.abort(new Error("Background job cancelled"))
7407
+ });
7408
+ void (async () => {
7409
+ try {
7410
+ let result2;
7411
+ if (options.executionEnvironment === void 0) {
7412
+ result2 = await executeShell(root, { ...input, timeoutMs }, cancellation.signal, maxBytes, (stream, text) => job.append(stream === "stderr" ? `[stderr] ${text}` : text));
7413
+ } else {
7414
+ const raw = await options.executionEnvironment.exec({
7415
+ command: input.command,
7416
+ args: input.args,
7417
+ cwd: workingDirectory(root, input.cwd ?? void 0),
7418
+ timeoutMs
7419
+ }, cancellation.signal);
7420
+ job.append(raw.stdout);
7421
+ if (raw.stderr !== "") job.append(`[stderr] ${raw.stderr}`);
7422
+ result2 = { ...raw, truncated: false, truncation: { stdout: { truncated: false, originalBytes: Buffer.byteLength(raw.stdout), limitBytes: maxBytes }, stderr: { truncated: false, originalBytes: Buffer.byteLength(raw.stderr), limitBytes: maxBytes } } };
7423
+ }
7424
+ job.complete({ exitCode: result2.exitCode });
7425
+ } catch (error) {
7426
+ job.fail(error);
7427
+ }
7428
+ })();
7429
+ return {
7430
+ jobId: job.id,
7431
+ state: "running",
7432
+ command: [input.command, ...input.args].join(" "),
7433
+ exitCode: null,
7434
+ signal: null,
7435
+ stdout: "",
7436
+ stderr: "",
7437
+ truncated: false,
7438
+ truncation: {
7439
+ stdout: { truncated: false, originalBytes: 0, limitBytes: maxBytes },
7440
+ stderr: { truncated: false, originalBytes: 0, limitBytes: maxBytes }
7441
+ },
7442
+ terminationReason: null
7443
+ };
7444
+ }
7445
+ if (options.executionEnvironment === void 0) {
7446
+ return executeShell(root, { ...input, timeoutMs }, signal, maxBytes);
7447
+ }
6995
7448
  const result = await options.executionEnvironment.exec({
6996
7449
  command: input.command,
6997
7450
  args: input.args,
6998
7451
  cwd: workingDirectory(root, input.cwd ?? void 0),
6999
- ...input.timeoutMs == null ? {} : { timeoutMs: input.timeoutMs }
7452
+ timeoutMs
7000
7453
  }, signal);
7001
7454
  const stdout = new BoundedOutput(maxBytes);
7002
7455
  const stderr = new BoundedOutput(maxBytes);
@@ -7015,7 +7468,7 @@ function createShellTool(workspace, options = {}) {
7015
7468
  }
7016
7469
  };
7017
7470
  }
7018
- async function executeShell(root, input, cancellation, maxBytes) {
7471
+ async function executeShell(root, input, cancellation, maxBytes, onOutput) {
7019
7472
  const cwd = workingDirectory(root, input.cwd ?? void 0);
7020
7473
  const stdout = new BoundedOutput(maxBytes);
7021
7474
  const stderr = new BoundedOutput(maxBytes);
@@ -7027,27 +7480,37 @@ async function executeShell(root, input, cancellation, maxBytes) {
7027
7480
  const quoteArg = (s) => s.includes(" ") ? `"${s.replace(/"/g, '""')}"` : s;
7028
7481
  const quoted = input.args.map(quoteArg).join(" ");
7029
7482
  const commandLine = quoted.length > 0 ? `${quoteArg(input.command)} ${quoted}` : quoteArg(input.command);
7030
- const child = spawn4(commandLine, {
7483
+ const shellCommandLine = process.platform === "win32" ? `chcp 65001>nul & ${commandLine}` : commandLine;
7484
+ const child = spawn4(shellCommandLine, {
7031
7485
  cwd,
7032
7486
  shell: true,
7033
7487
  windowsHide: true,
7034
7488
  detached: process.platform !== "win32"
7035
7489
  });
7036
7490
  let exitObserved = false;
7491
+ let observedExitCode = null;
7492
+ let observedSignal = null;
7037
7493
  let settled = false;
7038
7494
  let terminationReason = null;
7039
7495
  let termination;
7040
7496
  let terminalTimer;
7497
+ let streamCloseTimer;
7498
+ const stdoutDecoder = onOutput === void 0 ? void 0 : new AdaptiveOutputDecoder();
7499
+ const stderrDecoder = onOutput === void 0 ? void 0 : new AdaptiveOutputDecoder();
7041
7500
  const terminate = (reason) => {
7042
- if (termination !== void 0 || exitObserved || child.exitCode !== null || child.signalCode !== null) {
7501
+ if (termination !== void 0 || settled) return;
7502
+ terminationReason = reason;
7503
+ if (exitObserved || child.exitCode !== null || child.signalCode !== null) {
7504
+ child.stdout.destroy();
7505
+ child.stderr.destroy();
7506
+ void finishResolve(observedExitCode ?? child.exitCode, observedSignal ?? child.signalCode);
7043
7507
  return;
7044
7508
  }
7045
- terminationReason = reason;
7046
7509
  termination = terminateTree(child.pid);
7047
7510
  terminalTimer = setTimeout(() => finishReject(new Error(`Process did not close after ${reason} termination`)), TERMINATION_FAILURE_MS);
7048
7511
  terminalTimer.unref();
7049
7512
  };
7050
- const timeoutMs = input.timeoutMs ?? void 0;
7513
+ const timeoutMs = input.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
7051
7514
  let timer;
7052
7515
  child.once("spawn", () => {
7053
7516
  if (timeoutMs === void 0 || exitObserved || settled) return;
@@ -7056,30 +7519,57 @@ async function executeShell(root, input, cancellation, maxBytes) {
7056
7519
  });
7057
7520
  const onCancel = () => terminate("cancelled");
7058
7521
  cancellation.addEventListener("abort", onCancel, { once: true });
7059
- child.stdout.on("data", (chunk) => stdout.add(chunk));
7060
- child.stderr.on("data", (chunk) => stderr.add(chunk));
7061
- child.once("exit", () => {
7522
+ child.stdout.on("data", (chunk) => {
7523
+ stdout.add(chunk);
7524
+ if (stdoutDecoder !== void 0) {
7525
+ const text = stdoutDecoder.write(chunk);
7526
+ if (text !== "") onOutput?.("stdout", text);
7527
+ }
7528
+ });
7529
+ child.stderr.on("data", (chunk) => {
7530
+ stderr.add(chunk);
7531
+ if (stderrDecoder !== void 0) {
7532
+ const text = stderrDecoder.write(chunk);
7533
+ if (text !== "") onOutput?.("stderr", text);
7534
+ }
7535
+ });
7536
+ child.once("exit", (exitCode, exitSignal) => {
7062
7537
  exitObserved = true;
7538
+ observedExitCode = exitCode;
7539
+ observedSignal = exitSignal;
7063
7540
  if (timer !== void 0) clearTimeout(timer);
7541
+ streamCloseTimer = setTimeout(() => {
7542
+ child.stdout.destroy();
7543
+ child.stderr.destroy();
7544
+ void finishResolve(observedExitCode, observedSignal);
7545
+ }, STREAM_CLOSE_GRACE_MS);
7546
+ streamCloseTimer.unref();
7064
7547
  });
7065
7548
  child.once("error", (error) => {
7066
7549
  finishReject(error);
7067
7550
  });
7068
- child.once("close", async (exitCode, signal) => {
7069
- if (termination !== void 0) await termination;
7551
+ child.once("close", (exitCode, signal) => {
7552
+ void finishResolve(exitCode, signal);
7553
+ });
7554
+ async function finishResolve(exitCode, exitSignal) {
7070
7555
  if (settled) return;
7071
7556
  settled = true;
7072
7557
  cleanup();
7558
+ if (termination !== void 0) await termination;
7559
+ const stdoutTail = stdoutDecoder?.end() ?? "";
7560
+ const stderrTail = stderrDecoder?.end() ?? "";
7561
+ if (stdoutTail !== "") onOutput?.("stdout", stdoutTail);
7562
+ if (stderrTail !== "") onOutput?.("stderr", stderrTail);
7073
7563
  resolvePromise({
7074
7564
  exitCode,
7075
- signal,
7565
+ signal: exitSignal,
7076
7566
  stdout: stdout.text(),
7077
7567
  stderr: stderr.text(),
7078
7568
  truncated: stdout.truncated || stderr.truncated,
7079
7569
  truncation: { stdout: stdout.metadata(), stderr: stderr.metadata() },
7080
7570
  terminationReason
7081
7571
  });
7082
- });
7572
+ }
7083
7573
  function finishReject(error) {
7084
7574
  if (settled) return;
7085
7575
  settled = true;
@@ -7089,6 +7579,7 @@ async function executeShell(root, input, cancellation, maxBytes) {
7089
7579
  function cleanup() {
7090
7580
  if (timer !== void 0) clearTimeout(timer);
7091
7581
  if (terminalTimer !== void 0) clearTimeout(terminalTimer);
7582
+ if (streamCloseTimer !== void 0) clearTimeout(streamCloseTimer);
7092
7583
  cancellation.removeEventListener("abort", onCancel);
7093
7584
  }
7094
7585
  });
@@ -7124,14 +7615,41 @@ var BoundedOutput = class {
7124
7615
  return { truncated: this.truncated, originalBytes: this.#bytes, limitBytes: this.#limit };
7125
7616
  }
7126
7617
  text() {
7127
- if (!this.truncated) return this.#complete.toString("utf8");
7128
- return Buffer.concat([utf8Prefix(this.#head), ELLIPSIS, utf8Suffix(this.#tail)]).toString("utf8");
7618
+ if (!this.truncated) return decodeOutput(this.#complete);
7619
+ const encoding = classifyUtf8(this.#complete) === "invalid" && process.platform === "win32" ? "gb18030" : "utf-8";
7620
+ return `${decodePrefix(this.#head, encoding)}${ELLIPSIS}${decodeSuffix(this.#tail, encoding)}`;
7621
+ }
7622
+ };
7623
+ var AdaptiveOutputDecoder = class {
7624
+ #pending = Buffer.alloc(0);
7625
+ #decoder;
7626
+ write(chunk) {
7627
+ if (this.#decoder !== void 0) return this.#decoder.decode(chunk, { stream: true });
7628
+ this.#pending = Buffer.concat([this.#pending, chunk]);
7629
+ if (this.#pending.every((byte) => byte < 128)) {
7630
+ const ascii = this.#pending.toString("ascii");
7631
+ this.#pending = Buffer.alloc(0);
7632
+ return ascii;
7633
+ }
7634
+ const classification = classifyUtf8(this.#pending);
7635
+ if (classification === "incomplete") return "";
7636
+ const encoding = classification === "valid" || process.platform !== "win32" ? "utf-8" : "gb18030";
7637
+ this.#decoder = new TextDecoder(encoding);
7638
+ const text = this.#decoder.decode(this.#pending, { stream: true });
7639
+ this.#pending = Buffer.alloc(0);
7640
+ return text;
7641
+ }
7642
+ end() {
7643
+ if (this.#decoder !== void 0) return this.#decoder.decode();
7644
+ const text = decodeOutput(this.#pending);
7645
+ this.#pending = Buffer.alloc(0);
7646
+ return text;
7129
7647
  }
7130
7648
  };
7131
7649
  function workingDirectory(root, cwd = ".") {
7132
- const candidate = resolve10(root, cwd);
7133
- const delta = relative9(root, candidate);
7134
- if (delta === ".." || delta.startsWith(`..${sep8}`) || isAbsolute4(delta)) throw new Error("Working directory is outside the workspace");
7650
+ const candidate = resolve11(root, cwd);
7651
+ const delta = relative10(root, candidate);
7652
+ if (delta === ".." || delta.startsWith(`..${sep9}`) || isAbsolute5(delta)) throw new Error("Working directory is outside the workspace");
7135
7653
  return candidate;
7136
7654
  }
7137
7655
  async function terminateTree(pid) {
@@ -7164,34 +7682,76 @@ async function terminateTree(pid) {
7164
7682
  }
7165
7683
  function waitForProcess(child) {
7166
7684
  return new Promise((resolvePromise) => {
7167
- child.once("error", () => resolvePromise());
7168
- child.once("close", () => resolvePromise());
7685
+ let settled = false;
7686
+ const finish = () => {
7687
+ if (settled) return;
7688
+ settled = true;
7689
+ clearTimeout(timer);
7690
+ resolvePromise();
7691
+ };
7692
+ const timer = setTimeout(finish, TERMINATION_FAILURE_MS);
7693
+ timer.unref();
7694
+ child.once("error", finish);
7695
+ child.once("close", finish);
7169
7696
  });
7170
7697
  }
7171
7698
  function delay(milliseconds) {
7172
7699
  return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
7173
7700
  }
7174
- function utf8Prefix(buffer) {
7701
+ function decodeOutput(buffer) {
7702
+ const encoding = classifyUtf8(buffer) === "invalid" && process.platform === "win32" ? "gb18030" : "utf-8";
7703
+ return new TextDecoder(encoding).decode(buffer);
7704
+ }
7705
+ function decodePrefix(buffer, encoding) {
7175
7706
  for (let end = buffer.length; end >= Math.max(0, buffer.length - 3); end -= 1) {
7176
7707
  const candidate = buffer.subarray(0, end);
7177
7708
  try {
7178
- new TextDecoder("utf-8", { fatal: true }).decode(candidate);
7179
- return candidate;
7709
+ return new TextDecoder(encoding, { fatal: true }).decode(candidate);
7180
7710
  } catch {
7181
7711
  }
7182
7712
  }
7183
- return Buffer.from(buffer.toString("utf8"));
7713
+ return new TextDecoder(encoding).decode(buffer);
7184
7714
  }
7185
- function utf8Suffix(buffer) {
7715
+ function decodeSuffix(buffer, encoding) {
7186
7716
  for (let start = 0; start <= Math.min(3, buffer.length); start += 1) {
7187
7717
  const candidate = buffer.subarray(start);
7188
7718
  try {
7189
- new TextDecoder("utf-8", { fatal: true }).decode(candidate);
7190
- return candidate;
7719
+ return new TextDecoder(encoding, { fatal: true }).decode(candidate);
7191
7720
  } catch {
7192
7721
  }
7193
7722
  }
7194
- return Buffer.from(buffer.toString("utf8"));
7723
+ return new TextDecoder(encoding).decode(buffer);
7724
+ }
7725
+ function classifyUtf8(buffer) {
7726
+ try {
7727
+ new TextDecoder("utf-8", { fatal: true }).decode(buffer);
7728
+ return "valid";
7729
+ } catch {
7730
+ const suffixLength = incompleteUtf8SuffixLength(buffer);
7731
+ if (suffixLength === 0) return "invalid";
7732
+ try {
7733
+ new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, buffer.length - suffixLength));
7734
+ return "incomplete";
7735
+ } catch {
7736
+ return "invalid";
7737
+ }
7738
+ }
7739
+ }
7740
+ function incompleteUtf8SuffixLength(buffer) {
7741
+ for (let start = Math.max(0, buffer.length - 3); start < buffer.length; start += 1) {
7742
+ const lead = buffer[start];
7743
+ const expected = lead >= 194 && lead <= 223 ? 2 : lead >= 224 && lead <= 239 ? 3 : lead >= 240 && lead <= 244 ? 4 : 0;
7744
+ if (expected === 0 || buffer.length - start >= expected) continue;
7745
+ let continuation = true;
7746
+ for (let index = start + 1; index < buffer.length; index += 1) {
7747
+ if ((buffer[index] & 192) !== 128) {
7748
+ continuation = false;
7749
+ break;
7750
+ }
7751
+ }
7752
+ if (continuation) return buffer.length - start;
7753
+ }
7754
+ return 0;
7195
7755
  }
7196
7756
 
7197
7757
  // src/loop/verifier.ts
@@ -7272,7 +7832,7 @@ function isUnconditionalPassThrough(script) {
7272
7832
  }
7273
7833
  async function optionalText(path) {
7274
7834
  try {
7275
- return await readFile7(path, "utf8");
7835
+ return await readFile8(path, "utf8");
7276
7836
  } catch (error) {
7277
7837
  if (isCode5(error, "ENOENT")) return void 0;
7278
7838
  throw error;
@@ -7293,7 +7853,7 @@ function isCode5(error, code) {
7293
7853
 
7294
7854
  // src/models/anthropic.ts
7295
7855
  import Anthropic from "@anthropic-ai/sdk";
7296
- import { readFile as readFile8 } from "fs/promises";
7856
+ import { readFile as readFile9 } from "fs/promises";
7297
7857
 
7298
7858
  // src/utils/json.ts
7299
7859
  function safeParseJson(input) {
@@ -7372,27 +7932,27 @@ function markLastBlock(message2) {
7372
7932
  };
7373
7933
  message2.content = blocks;
7374
7934
  }
7375
- function updateInputUsage(snapshot2, usage) {
7376
- if (usage?.input_tokens != null) snapshot2.base = usage.input_tokens;
7935
+ function updateInputUsage(snapshot4, usage) {
7936
+ if (usage?.input_tokens != null) snapshot4.base = usage.input_tokens;
7377
7937
  if (usage?.cache_creation_input_tokens != null) {
7378
- snapshot2.cacheCreation = usage.cache_creation_input_tokens;
7938
+ snapshot4.cacheCreation = usage.cache_creation_input_tokens;
7379
7939
  }
7380
7940
  if (usage?.cache_read_input_tokens != null) {
7381
- snapshot2.cacheRead = usage.cache_read_input_tokens;
7941
+ snapshot4.cacheRead = usage.cache_read_input_tokens;
7382
7942
  }
7383
- return snapshot2.base + snapshot2.cacheCreation + snapshot2.cacheRead;
7943
+ return snapshot4.base + snapshot4.cacheCreation + snapshot4.cacheRead;
7384
7944
  }
7385
- function formatCacheUsage(model, snapshot2, shape) {
7386
- const total = snapshot2.base + snapshot2.cacheCreation + snapshot2.cacheRead;
7387
- const hitRatio = total > 0 ? snapshot2.cacheRead / total : 0;
7945
+ function formatCacheUsage(model, snapshot4, shape) {
7946
+ const total = snapshot4.base + snapshot4.cacheCreation + snapshot4.cacheRead;
7947
+ const hitRatio = total > 0 ? snapshot4.cacheRead / total : 0;
7388
7948
  return JSON.stringify({
7389
7949
  event: "flavor-usage",
7390
7950
  sessionId: currentUsageSession(),
7391
7951
  provider: "anthropic",
7392
7952
  model,
7393
- inputTokens: snapshot2.base,
7394
- cacheReadTokens: snapshot2.cacheRead,
7395
- cacheCreationTokens: snapshot2.cacheCreation,
7953
+ inputTokens: snapshot4.base,
7954
+ cacheReadTokens: snapshot4.cacheRead,
7955
+ cacheCreationTokens: snapshot4.cacheCreation,
7396
7956
  totalInputTokens: total,
7397
7957
  cacheHitRatio: Number(hitRatio.toFixed(4)),
7398
7958
  ...shape === void 0 ? {} : { requestMessages: shape.messages, requestMarkers: shape.markers }
@@ -7415,8 +7975,8 @@ var AnthropicModelAdapter = class {
7415
7975
  ...options.headers === void 0 ? {} : { defaultHeaders: options.headers }
7416
7976
  });
7417
7977
  }
7418
- #logUsage(request, snapshot2, shape) {
7419
- const line = formatCacheUsage(request.model, snapshot2, shape);
7978
+ #logUsage(request, snapshot4, shape) {
7979
+ const line = formatCacheUsage(request.model, snapshot4, shape);
7420
7980
  if (this.debugUsage) {
7421
7981
  try {
7422
7982
  process.stderr.write(`${line}
@@ -7495,7 +8055,7 @@ var AnthropicModelAdapter = class {
7495
8055
  source: {
7496
8056
  type: "base64",
7497
8057
  media_type: block.mediaType,
7498
- data: (await readFile8(block.source.path)).toString("base64")
8058
+ data: (await readFile9(block.source.path)).toString("base64")
7499
8059
  }
7500
8060
  }));
7501
8061
  if (message2.cacheBreakpoint && Array.isArray(content) && content.length > 0) {
@@ -7677,7 +8237,7 @@ function resolveCacheProfile(input) {
7677
8237
 
7678
8238
  // src/models/openai.ts
7679
8239
  import OpenAI from "openai";
7680
- import { readFile as readFile9 } from "fs/promises";
8240
+ import { readFile as readFile10 } from "fs/promises";
7681
8241
  async function toInput(message2) {
7682
8242
  if (message2.role === "tool") {
7683
8243
  if (!message2.toolCallId) throw new Error("Tool messages require toolCallId");
@@ -7689,7 +8249,7 @@ async function toInput(message2) {
7689
8249
  }
7690
8250
  const content = typeof message2.content === "string" ? message2.content : await Promise.all(message2.content.map(async (block) => block.type === "text" ? { type: "input_text", text: block.text } : {
7691
8251
  type: "input_image",
7692
- image_url: `data:${block.mediaType};base64,${(await readFile9(block.source.path)).toString("base64")}`,
8252
+ image_url: `data:${block.mediaType};base64,${(await readFile10(block.source.path)).toString("base64")}`,
7693
8253
  detail: "auto"
7694
8254
  }));
7695
8255
  return [
@@ -7908,8 +8468,8 @@ var ModelRegistry = class {
7908
8468
  };
7909
8469
 
7910
8470
  // src/mcp/client.ts
7911
- import { createHash as createHash3 } from "crypto";
7912
- import { z as z13 } from "zod";
8471
+ import { createHash as createHash4 } from "crypto";
8472
+ import { z as z14 } from "zod";
7913
8473
  var McpManager = class {
7914
8474
  #workspace;
7915
8475
  #clientFactory;
@@ -8045,7 +8605,7 @@ async function listAllTools(client) {
8045
8605
  function createMcpToolDefinition(serverName, timeoutMs, client, remote) {
8046
8606
  if (remote.name.length === 0) throw new Error("MCP tool name must not be empty");
8047
8607
  if (!isRecord3(remote.inputSchema)) throw new Error(`MCP tool "${remote.name}" has no JSON object input schema`);
8048
- const inputSchema = z13.fromJSONSchema(
8608
+ const inputSchema = z14.fromJSONSchema(
8049
8609
  remote.inputSchema
8050
8610
  );
8051
8611
  return {
@@ -8075,7 +8635,7 @@ function mcpToolName(serverName, remoteName) {
8075
8635
  const safe = remoteName.replace(/[^A-Za-z0-9_-]/g, "_") || "tool";
8076
8636
  const direct = `${prefix}${safe}`;
8077
8637
  if (direct.length <= 64 && safe === remoteName) return direct;
8078
- const suffix = `__${createHash3("sha256").update(remoteName).digest("hex").slice(0, 8)}`;
8638
+ const suffix = `__${createHash4("sha256").update(remoteName).digest("hex").slice(0, 8)}`;
8079
8639
  return `${prefix}${safe.slice(0, Math.max(1, 64 - prefix.length - suffix.length))}${suffix}`;
8080
8640
  }
8081
8641
  function mcpErrorText(content) {
@@ -8094,7 +8654,7 @@ function isRecord3(value) {
8094
8654
  }
8095
8655
 
8096
8656
  // src/mcp/sdk.ts
8097
- import { resolve as resolve11 } from "path";
8657
+ import { resolve as resolve12 } from "path";
8098
8658
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8099
8659
  import {
8100
8660
  getDefaultEnvironment,
@@ -8116,7 +8676,7 @@ async function connectSdkMcpClient(input, dependencies = DEFAULT_DEPENDENCIES) {
8116
8676
  command: config.command,
8117
8677
  args: [...config.args],
8118
8678
  env: { ...dependencies.defaultEnvironment(), ...config.env },
8119
- cwd: resolve11(input.workspace, config.cwd ?? "."),
8679
+ cwd: resolve12(input.workspace, config.cwd ?? "."),
8120
8680
  // MCP servers frequently log startup banners to stderr. Inheriting that
8121
8681
  // stream corrupts Ink's input area, so connection errors are surfaced
8122
8682
  // through McpManager diagnostics instead.
@@ -8141,14 +8701,14 @@ async function connectSdkMcpClient(input, dependencies = DEFAULT_DEPENDENCIES) {
8141
8701
  }
8142
8702
 
8143
8703
  // src/auth/oauth.ts
8144
- import { randomBytes, createHash as createHash5 } from "crypto";
8704
+ import { randomBytes, createHash as createHash6 } from "crypto";
8145
8705
  import { createServer } from "http";
8146
8706
  import { URL as URL2, URLSearchParams } from "url";
8147
8707
 
8148
8708
  // src/auth/oauth-config.ts
8149
- import { createHash as createHash4 } from "crypto";
8709
+ import { createHash as createHash5 } from "crypto";
8150
8710
  function oauthCredentialId(tokenUrl, clientId) {
8151
- const digest = createHash4("sha256").update(`${tokenUrl}\0${clientId}`, "utf8").digest("base64url");
8711
+ const digest = createHash5("sha256").update(`${tokenUrl}\0${clientId}`, "utf8").digest("base64url");
8152
8712
  return `oauth:${digest}`;
8153
8713
  }
8154
8714
  function parseOAuthLlmConfig(value) {
@@ -8321,8 +8881,8 @@ var OAuthCallbackAuthProvider = class {
8321
8881
  async #startCallbackServer(expectedState, signal) {
8322
8882
  let serverResolve;
8323
8883
  let serverReject;
8324
- const codePromise = new Promise((resolve22, reject) => {
8325
- serverResolve = resolve22;
8884
+ const codePromise = new Promise((resolve25, reject) => {
8885
+ serverResolve = resolve25;
8326
8886
  serverReject = reject;
8327
8887
  });
8328
8888
  const server = createServer((req, res) => {
@@ -8356,7 +8916,7 @@ var OAuthCallbackAuthProvider = class {
8356
8916
  );
8357
8917
  serverResolve(code);
8358
8918
  });
8359
- return new Promise((resolve22, reject) => {
8919
+ return new Promise((resolve25, reject) => {
8360
8920
  server.once("error", (error) => {
8361
8921
  reject(new Error(`OAuth callback server failed: ${error.message}`));
8362
8922
  });
@@ -8373,7 +8933,7 @@ var OAuthCallbackAuthProvider = class {
8373
8933
  signal.addEventListener("abort", () => {
8374
8934
  serverReject(new Error("OAuth flow aborted"));
8375
8935
  }, { once: true });
8376
- resolve22({ redirectUri, promise: codePromise });
8936
+ resolve25({ redirectUri, promise: codePromise });
8377
8937
  });
8378
8938
  });
8379
8939
  }
@@ -8405,7 +8965,7 @@ var OAuthCallbackAuthProvider = class {
8405
8965
  return randomBytes(32).toString("base64url");
8406
8966
  }
8407
8967
  #computeCodeChallenge(verifier) {
8408
- return createHash5("sha256").update(verifier).digest("base64url");
8968
+ return createHash6("sha256").update(verifier).digest("base64url");
8409
8969
  }
8410
8970
  #generateState() {
8411
8971
  return randomBytes(32).toString("base64url");
@@ -8455,13 +9015,13 @@ function escapeHtml(text) {
8455
9015
  }
8456
9016
 
8457
9017
  // src/auth/store.ts
8458
- import { dirname as dirname9 } from "path";
9018
+ import { dirname as dirname10 } from "path";
8459
9019
  function createFileTokenStore(filePath) {
8460
9020
  let baseline = {};
8461
9021
  const store = {
8462
9022
  tokens: {},
8463
9023
  async load() {
8464
- const key = await loadOrCreateConfigKey(dirname9(filePath));
9024
+ const key = await loadOrCreateConfigKey(dirname10(filePath));
8465
9025
  const result = await readRecoverableFile(filePath, (raw) => parseTokens(decryptDocument(raw, key)));
8466
9026
  const tokens = result?.value ?? {};
8467
9027
  baseline = { ...tokens };
@@ -8469,7 +9029,7 @@ function createFileTokenStore(filePath) {
8469
9029
  return tokens;
8470
9030
  },
8471
9031
  async save(tokens) {
8472
- const key = await loadOrCreateConfigKey(dirname9(filePath));
9032
+ const key = await loadOrCreateConfigKey(dirname10(filePath));
8473
9033
  const removals = Object.keys(baseline).filter((providerId) => !(providerId in tokens));
8474
9034
  const upserts = Object.fromEntries(Object.entries(tokens).filter(
8475
9035
  ([providerId, token]) => !sameToken(baseline[providerId], token)
@@ -8635,16 +9195,16 @@ function data(value) {
8635
9195
 
8636
9196
  // src/plugins/host.ts
8637
9197
  import { lstatSync, realpathSync as realpathSync2 } from "fs";
8638
- import { lstat as lstat5, open as open5, readdir as readdir4, realpath as realpath3, stat as stat3, writeFile as writeFile6 } from "fs/promises";
8639
- import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve12, sep as sep9 } from "path";
9198
+ import { lstat as lstat6, open as open5, readdir as readdir4, realpath as realpath4, stat as stat3, writeFile as writeFile6 } from "fs/promises";
9199
+ import { isAbsolute as isAbsolute6, relative as relative11, resolve as resolve13, sep as sep10 } from "path";
8640
9200
  import { pathToFileURL } from "url";
8641
9201
 
8642
9202
  // src/plugins/types.ts
8643
- import { z as z14 } from "zod";
9203
+ import { z as z15 } from "zod";
8644
9204
  var PLUGIN_NAME = /^(?:@[a-z0-9][a-z0-9._-]{0,63}\/)?[a-z0-9][a-z0-9._-]{0,63}$/;
8645
9205
  var CONTRIBUTION_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
8646
- var entry = z14.object({ name: z14.string().regex(CONTRIBUTION_NAME) }).strict();
8647
- var uniqueArray = (schema, label, identify = (item) => String(item)) => z14.array(schema).max(256).superRefine((items, context) => {
9206
+ var entry = z15.object({ name: z15.string().regex(CONTRIBUTION_NAME) }).strict();
9207
+ var uniqueArray = (schema, label, identify = (item) => String(item)) => z15.array(schema).max(256).superRefine((items, context) => {
8648
9208
  const seen = /* @__PURE__ */ new Set();
8649
9209
  for (const [index, item] of items.entries()) {
8650
9210
  const key = identify(item);
@@ -8652,17 +9212,17 @@ var uniqueArray = (schema, label, identify = (item) => String(item)) => z14.arra
8652
9212
  seen.add(key);
8653
9213
  }
8654
9214
  });
8655
- var PluginManifestSchema = z14.object({
8656
- name: z14.string().regex(PLUGIN_NAME),
8657
- version: z14.string().min(1).max(64),
8658
- apiVersion: z14.literal("1"),
8659
- main: z14.string().min(1).max(512),
8660
- permissions: uniqueArray(z14.enum(["filesystem:read", "filesystem:write"]), "permission").max(16),
8661
- contributes: z14.object({
9215
+ var PluginManifestSchema = z15.object({
9216
+ name: z15.string().regex(PLUGIN_NAME),
9217
+ version: z15.string().min(1).max(64),
9218
+ apiVersion: z15.literal("1"),
9219
+ main: z15.string().min(1).max(512),
9220
+ permissions: uniqueArray(z15.enum(["filesystem:read", "filesystem:write"]), "permission").max(16),
9221
+ contributes: z15.object({
8662
9222
  commands: uniqueArray(entry, "command contribution", (item) => item.name),
8663
9223
  tools: uniqueArray(entry, "tool contribution", (item) => item.name),
8664
- hooks: uniqueArray(z14.object({ name: z14.enum(HOOK_EVENT_NAMES) }).strict(), "hook contribution", (item) => item.name),
8665
- skillRoots: uniqueArray(entry.extend({ path: z14.string().min(1).max(512) }).strict(), "skill-root contribution", (item) => item.name),
9224
+ hooks: uniqueArray(z15.object({ name: z15.enum(HOOK_EVENT_NAMES) }).strict(), "hook contribution", (item) => item.name),
9225
+ skillRoots: uniqueArray(entry.extend({ path: z15.string().min(1).max(512) }).strict(), "skill-root contribution", (item) => item.name),
8666
9226
  modelAdapters: uniqueArray(entry, "model-adapter contribution", (item) => item.name)
8667
9227
  }).strict()
8668
9228
  }).strict();
@@ -8754,7 +9314,7 @@ var PluginHost = class {
8754
9314
  const roots = [];
8755
9315
  for (const directory of directories) {
8756
9316
  try {
8757
- for (const entry2 of await readdir4(directory, { withFileTypes: true })) if (entry2.isDirectory()) roots.push(resolve12(directory, entry2.name));
9317
+ for (const entry2 of await readdir4(directory, { withFileTypes: true })) if (entry2.isDirectory()) roots.push(resolve13(directory, entry2.name));
8758
9318
  } catch (error) {
8759
9319
  this.#diagnose(directory, error, directory);
8760
9320
  }
@@ -8784,10 +9344,10 @@ var PluginHost = class {
8784
9344
  async #readCandidate(inputRoot, source, label = inputRoot) {
8785
9345
  let diagnosticLabel = label;
8786
9346
  try {
8787
- const rootInfo = await lstat5(inputRoot);
9347
+ const rootInfo = await lstat6(inputRoot);
8788
9348
  if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) throw new Error("Plugin root must be a static directory");
8789
- const root = await realpath3(inputRoot);
8790
- const rawManifest = await readManifest(resolve12(root, MANIFEST));
9349
+ const root = await realpath4(inputRoot);
9350
+ const rawManifest = await readManifest(resolve13(root, MANIFEST));
8791
9351
  if (typeof rawManifest === "object" && rawManifest !== null && "name" in rawManifest && typeof rawManifest.name === "string") {
8792
9352
  diagnosticLabel = rawManifest.name;
8793
9353
  }
@@ -8886,7 +9446,7 @@ var PluginHost = class {
8886
9446
  assertActive();
8887
9447
  const permission = `filesystem:${operation}`;
8888
9448
  if (!manifest.permissions.includes(permission)) throw new Error(`Plugin ${manifest.name} lacks ${permission} permission`);
8889
- const path = resolve12(rawPath);
9449
+ const path = resolve13(rawPath);
8890
9450
  const authorized = await this.#options.authorizeFilesystem?.({ plugin: manifest.name, operation, path });
8891
9451
  assertActive();
8892
9452
  if (authorized !== true) throw new Error(`Filesystem ${operation} denied for plugin ${manifest.name}`);
@@ -8927,8 +9487,8 @@ var PluginHost = class {
8927
9487
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`Skill root "${name}" must be a static directory`);
8928
9488
  const physical = realpathSync2.native(registered);
8929
9489
  assertContained(candidate.root, physical, "Skill root");
8930
- const snapshot2 = fromStats(info);
8931
- const capability = Object.freeze({ path: physical, identity: Object.freeze(pickIdentity(snapshot2)) });
9490
+ const snapshot4 = fromStats(info);
9491
+ const capability = Object.freeze({ path: physical, identity: Object.freeze(pickIdentity(snapshot4)) });
8932
9492
  return register("skillRoot", name, [capability]);
8933
9493
  },
8934
9494
  registerModelAdapter: (name, value) => register("modelAdapter", name, [value])
@@ -8954,7 +9514,7 @@ var PluginHost = class {
8954
9514
  }
8955
9515
  };
8956
9516
  async function readManifest(path) {
8957
- const lexical = await lstat5(path, { bigint: true });
9517
+ const lexical = await lstat6(path, { bigint: true });
8958
9518
  if (!lexical.isFile() || lexical.isSymbolicLink()) throw new Error("Plugin manifest must be a static regular file");
8959
9519
  const handle = await open5(path, "r");
8960
9520
  try {
@@ -8971,7 +9531,7 @@ async function readManifest(path) {
8971
9531
  if (bytesRead > MAX_MANIFEST_BYTES2) throw new Error(`Plugin manifest exceeds ${MAX_MANIFEST_BYTES2} bytes`);
8972
9532
  const after = fromStats(await handle.stat({ bigint: true }));
8973
9533
  assertSnapshot(before, after, "Plugin manifest");
8974
- const current = fromStats(await lstat5(path, { bigint: true }));
9534
+ const current = fromStats(await lstat6(path, { bigint: true }));
8975
9535
  assertSnapshot(before, current, "Plugin manifest path");
8976
9536
  return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead)));
8977
9537
  } finally {
@@ -8979,9 +9539,9 @@ async function readManifest(path) {
8979
9539
  }
8980
9540
  }
8981
9541
  async function snapshotEntry(root, entry2) {
8982
- const lexicalStats = await lstat5(entry2, { bigint: true });
9542
+ const lexicalStats = await lstat6(entry2, { bigint: true });
8983
9543
  if (!lexicalStats.isFile() && !lexicalStats.isSymbolicLink()) throw new Error("Plugin entry must be a file");
8984
- const physicalPath = await realpath3(entry2);
9544
+ const physicalPath = await realpath4(entry2);
8985
9545
  assertContained(root, physicalPath, "Plugin entry symlink");
8986
9546
  const physicalStats = await stat3(physicalPath, { bigint: true });
8987
9547
  if (!physicalStats.isFile()) throw new Error("Plugin entry must resolve to a regular file");
@@ -9010,21 +9570,21 @@ function contributionNames(manifest, kind) {
9010
9570
  function fromStats(stats) {
9011
9571
  return { dev: stats.dev, ino: stats.ino, mode: stats.mode, size: stats.size, mtimeNs: stats.mtimeNs, ctimeNs: stats.ctimeNs };
9012
9572
  }
9013
- function pickIdentity(snapshot2) {
9014
- return { dev: snapshot2.dev, ino: snapshot2.ino, mtimeNs: snapshot2.mtimeNs, ctimeNs: snapshot2.ctimeNs };
9573
+ function pickIdentity(snapshot4) {
9574
+ return { dev: snapshot4.dev, ino: snapshot4.ino, mtimeNs: snapshot4.mtimeNs, ctimeNs: snapshot4.ctimeNs };
9015
9575
  }
9016
9576
  function assertSnapshot(expected, actual, label) {
9017
9577
  for (const key of ["dev", "ino", "mode", "size", "mtimeNs", "ctimeNs"]) if (expected[key] !== actual[key]) throw new Error(`${label} changed during validation`);
9018
9578
  }
9019
9579
  function resolveContained(root, value, label) {
9020
- if (isAbsolute5(value)) throw new Error(`${label} must be relative to the plugin root`);
9021
- const candidate = resolve12(root, value);
9580
+ if (isAbsolute6(value)) throw new Error(`${label} must be relative to the plugin root`);
9581
+ const candidate = resolve13(root, value);
9022
9582
  assertContained(root, candidate, label);
9023
9583
  return candidate;
9024
9584
  }
9025
9585
  function assertContained(root, candidate, label) {
9026
- const delta = relative10(root, candidate);
9027
- if (delta === ".." || delta.startsWith(`..${sep9}`) || isAbsolute5(delta)) throw new Error(`${label} escapes the plugin root`);
9586
+ const delta = relative11(root, candidate);
9587
+ if (delta === ".." || delta.startsWith(`..${sep10}`) || isAbsolute6(delta)) throw new Error(`${label} escapes the plugin root`);
9028
9588
  }
9029
9589
  function scopedLogger(logger, plugin) {
9030
9590
  const prefix = `[plugin:${plugin}] `;
@@ -9068,8 +9628,8 @@ function compare(left, right) {
9068
9628
 
9069
9629
  // src/skills/registry.ts
9070
9630
  import { constants as constants3 } from "fs";
9071
- import { lstat as lstat6, open as defaultOpen, readdir as readdir5, realpath as realpath4 } from "fs/promises";
9072
- import { isAbsolute as isAbsolute6, relative as relative11, resolve as resolve13, sep as sep10 } from "path";
9631
+ import { lstat as lstat7, open as defaultOpen, readdir as readdir5, realpath as realpath5 } from "fs/promises";
9632
+ import { isAbsolute as isAbsolute7, relative as relative12, resolve as resolve14, sep as sep11 } from "path";
9073
9633
  import { marked } from "marked";
9074
9634
  import { parseDocument } from "yaml";
9075
9635
  var SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -9194,13 +9754,13 @@ var SkillRegistry = class {
9194
9754
  const record = await this.#recordFor(skill);
9195
9755
  const normalized = normalizeReference(reference);
9196
9756
  const parts = normalized.split("/");
9197
- if (isAbsolute6(reference) || parts.includes("..") || parts.length !== 2 || !RESOURCE_DIRECTORIES.has(parts[0] ?? "") || !RESOURCE_NAME.test(parts[1] ?? "")) {
9757
+ if (isAbsolute7(reference) || parts.includes("..") || parts.length !== 2 || !RESOURCE_DIRECTORIES.has(parts[0] ?? "") || !RESOURCE_NAME.test(parts[1] ?? "")) {
9198
9758
  throw new Error(`Resource reference escapes the skill root or is not a direct resource: ${reference}`);
9199
9759
  }
9200
9760
  const body = await this.loadBody(record.metadata);
9201
9761
  const references = extractResourceReferences(body);
9202
9762
  if (!references.has(normalized)) throw new Error(`Skill resource is not directly referenced: ${reference}`);
9203
- const candidate = resolve13(record.metadata.root, ...parts);
9763
+ const candidate = resolve14(record.metadata.root, ...parts);
9204
9764
  const file = await openVerifiedFile(candidate, record.metadata.root, "Resource", this.#options.openFile);
9205
9765
  try {
9206
9766
  if (file.snapshot.size > BigInt(this.#options.maxResourceBytes)) throw new Error(`Skill resource is too large: ${reference}`);
@@ -9229,13 +9789,13 @@ var SkillRegistry = class {
9229
9789
  async #discoverSource(roots, source) {
9230
9790
  const found = [];
9231
9791
  for (const configuredRoot of roots) {
9232
- const root = resolve13(configuredRoot);
9792
+ const root = resolve14(configuredRoot);
9233
9793
  let entries;
9234
9794
  let physicalRoot = root;
9235
9795
  try {
9236
- const rootInfo = await lstat6(root);
9796
+ const rootInfo = await lstat7(root);
9237
9797
  if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) throw new Error("Skill root must be a real directory");
9238
- physicalRoot = await realpath4(root);
9798
+ physicalRoot = await realpath5(root);
9239
9799
  entries = await readdir5(root, { withFileTypes: true });
9240
9800
  } catch (error) {
9241
9801
  if (isMissing2(error)) continue;
@@ -9244,13 +9804,13 @@ var SkillRegistry = class {
9244
9804
  }
9245
9805
  entries.sort((left, right) => compareCodePoints(left.name, right.name));
9246
9806
  for (const entry2 of entries) {
9247
- const skillRoot = resolve13(root, entry2.name);
9807
+ const skillRoot = resolve14(root, entry2.name);
9248
9808
  try {
9249
9809
  if (entry2.isSymbolicLink()) throw new Error("Symlinked skill directories are not allowed");
9250
9810
  if (!entry2.isDirectory()) continue;
9251
- const physicalSkillRoot = await realpath4(skillRoot);
9811
+ const physicalSkillRoot = await realpath5(skillRoot);
9252
9812
  if (!isWithin3(physicalRoot, physicalSkillRoot)) throw new Error("Skill directory escapes its registry root");
9253
- const skillFile = resolve13(skillRoot, "SKILL.md");
9813
+ const skillFile = resolve14(skillRoot, "SKILL.md");
9254
9814
  const parsed = await readFrontmatterFile(
9255
9815
  skillFile,
9256
9816
  physicalSkillRoot,
@@ -9387,10 +9947,10 @@ async function openFile(path, flags) {
9387
9947
  return defaultOpen(path, flags);
9388
9948
  }
9389
9949
  async function openVerifiedFile(path, root, label, opener, expected) {
9390
- const initial = await lstat6(path, { bigint: true });
9950
+ const initial = await lstat7(path, { bigint: true });
9391
9951
  if (initial.isSymbolicLink()) throw new Error(`${label} symlinks are not allowed`);
9392
9952
  assertRegular(initial, label);
9393
- const canonical3 = await realpath4(path);
9953
+ const canonical3 = await realpath5(path);
9394
9954
  if (!isWithin3(root, canonical3)) throw new Error(`${label} escapes the skill root`);
9395
9955
  const noFollow = typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
9396
9956
  const handle = await opener(canonical3, constants3.O_RDONLY | noFollow);
@@ -9451,8 +10011,8 @@ function decodeUtf82(content, label) {
9451
10011
  }
9452
10012
  }
9453
10013
  function isWithin3(root, candidate) {
9454
- const delta = relative11(root, candidate);
9455
- return delta === "" || delta !== ".." && !delta.startsWith(`..${sep10}`) && !isAbsolute6(delta);
10014
+ const delta = relative12(root, candidate);
10015
+ return delta === "" || delta !== ".." && !delta.startsWith(`..${sep11}`) && !isAbsolute7(delta);
9456
10016
  }
9457
10017
  function isMissing2(error) {
9458
10018
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
@@ -9499,10 +10059,10 @@ function score(metadata, query) {
9499
10059
  }
9500
10060
 
9501
10061
  // src/skills/tool.ts
9502
- import { z as z15 } from "zod";
9503
- var SkillResourceInput = z15.object({
9504
- skill: z15.string().min(1),
9505
- reference: z15.string().min(1)
10062
+ import { z as z16 } from "zod";
10063
+ var SkillResourceInput = z16.object({
10064
+ skill: z16.string().min(1),
10065
+ reference: z16.string().min(1)
9506
10066
  }).strict();
9507
10067
  function createSkillResourceTool(registry) {
9508
10068
  return {
@@ -9529,97 +10089,97 @@ function createSkillResourceTool(registry) {
9529
10089
 
9530
10090
  // src/session/store.ts
9531
10091
  import { constants as constants4 } from "fs";
9532
- import { lstat as lstat7, mkdir as mkdir9, open as open6, readFile as readFile10, readdir as readdir6, realpath as realpath5, rename as rename5, rm as rm4, stat as stat4 } from "fs/promises";
9533
- import { basename as basename3, dirname as dirname10, join as join12, relative as relative12, resolve as resolve14, sep as sep11 } from "path";
10092
+ import { lstat as lstat8, mkdir as mkdir9, open as open6, readFile as readFile11, readdir as readdir6, realpath as realpath6, rename as rename5, rm as rm4, stat as stat4 } from "fs/promises";
10093
+ import { basename as basename3, dirname as dirname11, join as join12, relative as relative13, resolve as resolve15, sep as sep12 } from "path";
9534
10094
  import { randomUUID as randomUUID6 } from "crypto";
9535
- import { z as z16 } from "zod";
10095
+ import { z as z17 } from "zod";
9536
10096
  var SESSION_VERSION = 3;
9537
10097
  var DEFAULT_MAX_SESSION_BYTES = 5 * 1024 * 1024;
9538
- var SessionIdSchema = z16.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/, "Invalid session id");
9539
- var IsoDateSchema = z16.string().datetime({ offset: true });
9540
- var ToolCallSchema = z16.object({
9541
- id: z16.string().min(1).max(256),
9542
- name: z16.string().min(1).max(256),
9543
- input: z16.unknown()
10098
+ var SessionIdSchema = z17.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/, "Invalid session id");
10099
+ var IsoDateSchema = z17.string().datetime({ offset: true });
10100
+ var ToolCallSchema = z17.object({
10101
+ id: z17.string().min(1).max(256),
10102
+ name: z17.string().min(1).max(256),
10103
+ input: z17.unknown()
9544
10104
  }).strict();
9545
- var TextContentBlockSchema = z16.object({
9546
- type: z16.literal("text"),
9547
- text: z16.string().max(DEFAULT_MAX_SESSION_BYTES)
10105
+ var TextContentBlockSchema = z17.object({
10106
+ type: z17.literal("text"),
10107
+ text: z17.string().max(DEFAULT_MAX_SESSION_BYTES)
9548
10108
  }).strict();
9549
- var ImageContentBlockSchema = z16.object({
9550
- type: z16.literal("image"),
9551
- source: z16.object({ type: z16.literal("file"), path: z16.string().min(1).max(32768) }).strict(),
9552
- mediaType: z16.enum(["image/png", "image/jpeg", "image/webp"]),
9553
- sha256: z16.string().regex(/^[a-f0-9]{64}$/),
9554
- bytes: z16.number().int().positive().max(20 * 1024 * 1024),
9555
- name: z16.string().min(1).max(255).optional()
10109
+ var ImageContentBlockSchema = z17.object({
10110
+ type: z17.literal("image"),
10111
+ source: z17.object({ type: z17.literal("file"), path: z17.string().min(1).max(32768) }).strict(),
10112
+ mediaType: z17.enum(["image/png", "image/jpeg", "image/webp"]),
10113
+ sha256: z17.string().regex(/^[a-f0-9]{64}$/),
10114
+ bytes: z17.number().int().positive().max(20 * 1024 * 1024),
10115
+ name: z17.string().min(1).max(255).optional()
9556
10116
  }).strict();
9557
- var UserContentSchema = z16.union([
9558
- z16.string().max(DEFAULT_MAX_SESSION_BYTES),
9559
- z16.array(z16.discriminatedUnion("type", [TextContentBlockSchema, ImageContentBlockSchema])).min(1).max(100)
10117
+ var UserContentSchema = z17.union([
10118
+ z17.string().max(DEFAULT_MAX_SESSION_BYTES),
10119
+ z17.array(z17.discriminatedUnion("type", [TextContentBlockSchema, ImageContentBlockSchema])).min(1).max(100)
9560
10120
  ]);
9561
10121
  var MessageMetadataShape = {
9562
- toolCallId: z16.string().min(1).max(256).optional(),
9563
- toolCalls: z16.array(ToolCallSchema).max(1e3).optional()
10122
+ toolCallId: z17.string().min(1).max(256).optional(),
10123
+ toolCalls: z17.array(ToolCallSchema).max(1e3).optional()
9564
10124
  };
9565
- var MessageSchema = z16.discriminatedUnion("role", [
9566
- z16.object({ role: z16.literal("user"), content: UserContentSchema, ...MessageMetadataShape }).strict(),
9567
- z16.object({ role: z16.literal("assistant"), content: z16.string().max(DEFAULT_MAX_SESSION_BYTES), ...MessageMetadataShape }).strict(),
9568
- z16.object({ role: z16.literal("tool"), content: z16.string().max(DEFAULT_MAX_SESSION_BYTES), ...MessageMetadataShape }).strict()
10125
+ var MessageSchema = z17.discriminatedUnion("role", [
10126
+ z17.object({ role: z17.literal("user"), content: UserContentSchema, ...MessageMetadataShape }).strict(),
10127
+ z17.object({ role: z17.literal("assistant"), content: z17.string().max(DEFAULT_MAX_SESSION_BYTES), ...MessageMetadataShape }).strict(),
10128
+ z17.object({ role: z17.literal("tool"), content: z17.string().max(DEFAULT_MAX_SESSION_BYTES), ...MessageMetadataShape }).strict()
9569
10129
  ]);
9570
- var LegacySummarySchema = z16.object({ role: z16.literal("system"), content: z16.string().max(DEFAULT_MAX_SESSION_BYTES) }).strict();
9571
- var CompactBoundarySchema = z16.object({
9572
- summary: z16.string().min(1).max(DEFAULT_MAX_SESSION_BYTES),
10130
+ var LegacySummarySchema = z17.object({ role: z17.literal("system"), content: z17.string().max(DEFAULT_MAX_SESSION_BYTES) }).strict();
10131
+ var CompactBoundarySchema = z17.object({
10132
+ summary: z17.string().min(1).max(DEFAULT_MAX_SESSION_BYTES),
9573
10133
  compactedAt: IsoDateSchema
9574
10134
  }).strict();
9575
- var StateSchema = z16.enum(["pending", "running", "completed", "failed", "blocked", "cancelled"]);
9576
- var MemoryLifecycleSchema = z16.object({
9577
- status: z16.enum(["active", "completed", "abandoned"]),
9578
- taskId: z16.string().regex(/^memory-[A-Za-z0-9._-]{1,120}$/).optional(),
9579
- messageStart: z16.number().int().min(0).max(5e4).optional(),
10135
+ var StateSchema = z17.enum(["pending", "running", "completed", "failed", "blocked", "cancelled"]);
10136
+ var MemoryLifecycleSchema = z17.object({
10137
+ status: z17.enum(["active", "completed", "abandoned"]),
10138
+ taskId: z17.string().regex(/^memory-[A-Za-z0-9._-]{1,120}$/).optional(),
10139
+ messageStart: z17.number().int().min(0).max(5e4).optional(),
9580
10140
  finalizedAt: IsoDateSchema.optional(),
9581
- transcriptHash: z16.string().regex(/^[a-f0-9]{64}$/).optional()
10141
+ transcriptHash: z17.string().regex(/^[a-f0-9]{64}$/).optional()
9582
10142
  }).strict();
9583
- var SessionBaseSchema = z16.object({
10143
+ var SessionBaseSchema = z17.object({
9584
10144
  sessionId: SessionIdSchema,
9585
10145
  createdAt: IsoDateSchema,
9586
10146
  updatedAt: IsoDateSchema,
9587
- workspace: z16.object({ path: z16.string().min(1).max(32768) }).strict(),
9588
- tasks: z16.object({
10147
+ workspace: z17.object({ path: z17.string().min(1).max(32768) }).strict(),
10148
+ tasks: z17.object({
9589
10149
  plan: TaskPlanSchema.optional(),
9590
10150
  graph: TaskGraphSchema.optional(),
9591
- states: z16.record(z16.string(), StateSchema),
9592
- results: z16.record(z16.string(), SubagentResultSchema)
10151
+ states: z17.record(z17.string(), StateSchema),
10152
+ results: z17.record(z17.string(), SubagentResultSchema)
9593
10153
  }).strict(),
9594
- models: z16.object({ main: z16.string().min(1).max(1024), subagent: z16.string().min(1).max(1024) }).strict(),
10154
+ models: z17.object({ main: z17.string().min(1).max(1024), subagent: z17.string().min(1).max(1024) }).strict(),
9595
10155
  permissionMode: PermissionModeSchema,
9596
10156
  memory: MemoryLifecycleSchema.optional()
9597
10157
  }).strict();
9598
10158
  var SessionDocumentV1Schema = SessionBaseSchema.extend({
9599
- version: z16.literal(1),
9600
- conversation: z16.object({
10159
+ version: z17.literal(1),
10160
+ conversation: z17.object({
9601
10161
  summary: LegacySummarySchema.optional(),
9602
- messages: z16.array(MessageSchema).max(5e4)
10162
+ messages: z17.array(MessageSchema).max(5e4)
9603
10163
  }).strict()
9604
10164
  }).strict();
9605
- var ConversationSchema = z16.object({
10165
+ var ConversationSchema = z17.object({
9606
10166
  compact: CompactBoundarySchema.optional(),
9607
- messages: z16.array(MessageSchema).max(5e4)
10167
+ messages: z17.array(MessageSchema).max(5e4)
9608
10168
  }).strict();
9609
10169
  var SessionDocumentV2Schema = SessionBaseSchema.extend({
9610
- version: z16.literal(2),
10170
+ version: z17.literal(2),
9611
10171
  conversation: ConversationSchema
9612
10172
  }).strict();
9613
- var TranscriptStateSchema = z16.custom(isTranscriptState, "Invalid transcript state");
9614
- var TimelineSchema = z16.object({
9615
- version: z16.literal(1),
10173
+ var TranscriptStateSchema = z17.custom(isTranscriptState, "Invalid transcript state");
10174
+ var TimelineSchema = z17.object({
10175
+ version: z17.literal(1),
9616
10176
  state: TranscriptStateSchema
9617
10177
  }).strict();
9618
10178
  var SessionDocumentSchema = SessionBaseSchema.extend({
9619
- version: z16.literal(SESSION_VERSION),
9620
- conversation: z16.object({
10179
+ version: z17.literal(SESSION_VERSION),
10180
+ conversation: z17.object({
9621
10181
  compact: CompactBoundarySchema.optional(),
9622
- messages: z16.array(MessageSchema).max(5e4)
10182
+ messages: z17.array(MessageSchema).max(5e4)
9623
10183
  }).strict(),
9624
10184
  timeline: TimelineSchema
9625
10185
  }).strict();
@@ -9629,7 +10189,7 @@ var SessionStore = class {
9629
10189
  #maxBytes;
9630
10190
  #maxSessions;
9631
10191
  constructor(options) {
9632
- this.#workspace = resolve14(options.workspace);
10192
+ this.#workspace = resolve15(options.workspace);
9633
10193
  this.#sessions = join12(this.#workspace, ".flavor", "sessions");
9634
10194
  this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_SESSION_BYTES;
9635
10195
  if (!Number.isSafeInteger(this.#maxBytes) || this.#maxBytes < 256) throw new Error("maxBytes must be an integer of at least 256");
@@ -9766,7 +10326,7 @@ var SessionStore = class {
9766
10326
  }
9767
10327
  const path = this.#path(sessionId);
9768
10328
  try {
9769
- const metadata = await lstat7(path);
10329
+ const metadata = await lstat8(path);
9770
10330
  if (!metadata.isFile()) throw new Error("Session path is not a regular file");
9771
10331
  await rm4(path);
9772
10332
  } catch (error) {
@@ -9775,7 +10335,7 @@ var SessionStore = class {
9775
10335
  }
9776
10336
  #path(sessionId) {
9777
10337
  SessionIdSchema.parse(sessionId);
9778
- const path = resolve14(this.#sessions, `${sessionId}.jsonl`);
10338
+ const path = resolve15(this.#sessions, `${sessionId}.jsonl`);
9779
10339
  if (!isWithin4(this.#sessions, path)) throw new Error("Invalid session id");
9780
10340
  return path;
9781
10341
  }
@@ -9795,14 +10355,14 @@ var SessionStore = class {
9795
10355
  }
9796
10356
  }
9797
10357
  async #prepareDirectory() {
9798
- await assertNoSymlink3(this.#workspace, dirname10(this.#sessions));
10358
+ await assertNoSymlink3(this.#workspace, dirname11(this.#sessions));
9799
10359
  await mkdir9(this.#sessions, { recursive: true, mode: 448 });
9800
10360
  await this.#assertSafeExistingDirectory();
9801
10361
  }
9802
10362
  async #assertSafeExistingDirectory() {
9803
10363
  await assertNoSymlink3(this.#workspace, this.#sessions);
9804
- const canonicalRoot = await realpath5(this.#workspace);
9805
- const canonicalSessions = await realpath5(this.#sessions);
10364
+ const canonicalRoot = await realpath6(this.#workspace);
10365
+ const canonicalSessions = await realpath6(this.#sessions);
9806
10366
  if (!isWithin4(canonicalRoot, canonicalSessions)) throw new Error("Session directory escapes the workspace");
9807
10367
  }
9808
10368
  async #quarantine(path) {
@@ -9815,7 +10375,7 @@ async function boundedRead(path, maxBytes) {
9815
10375
  const metadata = await stat4(path);
9816
10376
  if (!metadata.isFile()) throw new Error("Session path is not a regular file");
9817
10377
  if (metadata.size > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
9818
- const value = await readFile10(path);
10378
+ const value = await readFile11(path);
9819
10379
  if (value.byteLength > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
9820
10380
  return value.toString("utf8");
9821
10381
  }
@@ -9953,31 +10513,31 @@ function isRecord5(value) {
9953
10513
  return typeof value === "object" && value !== null && !Array.isArray(value);
9954
10514
  }
9955
10515
  async function assertNoSymlink3(root, target) {
9956
- let cursor = resolve14(target);
9957
- const stop = resolve14(root);
10516
+ let cursor = resolve15(target);
10517
+ const stop = resolve15(root);
9958
10518
  if (!isWithin4(stop, cursor)) throw new Error("Session path escapes the workspace");
9959
10519
  const chain = [];
9960
10520
  while (cursor !== stop) {
9961
10521
  chain.push(cursor);
9962
- cursor = dirname10(cursor);
10522
+ cursor = dirname11(cursor);
9963
10523
  }
9964
10524
  for (const path of chain.reverse()) {
9965
10525
  try {
9966
- if ((await lstat7(path)).isSymbolicLink()) throw new Error(`Session path contains a symbolic link: ${path}`);
10526
+ if ((await lstat8(path)).isSymbolicLink()) throw new Error(`Session path contains a symbolic link: ${path}`);
9967
10527
  } catch (error) {
9968
10528
  if (!isCode6(error, "ENOENT")) throw error;
9969
10529
  }
9970
10530
  }
9971
10531
  }
9972
10532
  function isWithin4(root, path) {
9973
- const value = relative12(resolve14(root), resolve14(path));
9974
- return value === "" || !value.startsWith(`..${sep11}`) && value !== ".." && !value.includes(":");
10533
+ const value = relative13(resolve15(root), resolve15(path));
10534
+ return value === "" || !value.startsWith(`..${sep12}`) && value !== ".." && !value.includes(":");
9975
10535
  }
9976
10536
  async function canonical2(path) {
9977
10537
  try {
9978
- return await realpath5(resolve14(path));
10538
+ return await realpath6(resolve15(path));
9979
10539
  } catch {
9980
- return resolve14(path);
10540
+ return resolve15(path);
9981
10541
  }
9982
10542
  }
9983
10543
  function isCode6(error, code) {
@@ -10004,45 +10564,45 @@ function sanitize2(input, seen = /* @__PURE__ */ new WeakSet()) {
10004
10564
  // src/sleep/organizer.ts
10005
10565
  import { constants as constants5 } from "fs";
10006
10566
  import { randomUUID as randomUUID7 } from "crypto";
10007
- import { lstat as lstat8, mkdir as mkdir10, open as open7, readdir as readdir7, realpath as realpath6, rename as rename6, rm as rm5 } from "fs/promises";
10008
- import { isAbsolute as isAbsolute7, join as join13, relative as relative13, resolve as resolve15, sep as sep12 } from "path";
10009
- import { z as z17 } from "zod";
10010
- var DateKeySchema = z17.string().regex(/^\d{4}-\d{2}-\d{2}$/);
10011
- var ReviewSchema = z17.object({
10012
- title: z17.string().min(1).max(120),
10013
- taskSummary: z17.array(z17.string().min(1)).min(1).max(20),
10014
- executionReflection: z17.array(z17.string().min(1)).min(1).max(20),
10015
- decisionsAndLearnings: z17.array(z17.string().min(1)).max(20),
10016
- openQuestionsAndRisks: z17.array(z17.string().min(1)).max(20),
10017
- tomorrowPlan: z17.array(z17.string().min(1)).min(1).max(20),
10018
- toolUsage: z17.object({
10019
- totalCalls: z17.number().int().min(0),
10020
- shell: z17.number().int().min(0),
10021
- fileRead: z17.number().int().min(0),
10022
- fileWrite: z17.number().int().min(0),
10023
- search: z17.number().int().min(0),
10024
- lsp: z17.number().int().min(0),
10025
- subagent: z17.number().int().min(0),
10026
- other: z17.number().int().min(0)
10567
+ import { lstat as lstat9, mkdir as mkdir10, open as open7, readdir as readdir7, realpath as realpath7, rename as rename6, rm as rm5 } from "fs/promises";
10568
+ import { isAbsolute as isAbsolute8, join as join13, relative as relative14, resolve as resolve16, sep as sep13 } from "path";
10569
+ import { z as z18 } from "zod";
10570
+ var DateKeySchema = z18.string().regex(/^\d{4}-\d{2}-\d{2}$/);
10571
+ var ReviewSchema = z18.object({
10572
+ title: z18.string().min(1).max(120),
10573
+ taskSummary: z18.array(z18.string().min(1)).min(1).max(20),
10574
+ executionReflection: z18.array(z18.string().min(1)).min(1).max(20),
10575
+ decisionsAndLearnings: z18.array(z18.string().min(1)).max(20),
10576
+ openQuestionsAndRisks: z18.array(z18.string().min(1)).max(20),
10577
+ tomorrowPlan: z18.array(z18.string().min(1)).min(1).max(20),
10578
+ toolUsage: z18.object({
10579
+ totalCalls: z18.number().int().min(0),
10580
+ shell: z18.number().int().min(0),
10581
+ fileRead: z18.number().int().min(0),
10582
+ fileWrite: z18.number().int().min(0),
10583
+ search: z18.number().int().min(0),
10584
+ lsp: z18.number().int().min(0),
10585
+ subagent: z18.number().int().min(0),
10586
+ other: z18.number().int().min(0)
10027
10587
  }).strict(),
10028
- tokenEstimate: z17.object({
10029
- estimatedInput: z17.number().int().min(0),
10030
- estimatedOutput: z17.number().int().min(0),
10031
- notes: z17.string().max(200)
10588
+ tokenEstimate: z18.object({
10589
+ estimatedInput: z18.number().int().min(0),
10590
+ estimatedOutput: z18.number().int().min(0),
10591
+ notes: z18.string().max(200)
10032
10592
  }).strict(),
10033
- humanIntervention: z17.object({
10034
- approvalRequests: z17.number().int().min(0),
10035
- questionsAsked: z17.number().int().min(0),
10036
- summary: z17.string().max(300)
10593
+ humanIntervention: z18.object({
10594
+ approvalRequests: z18.number().int().min(0),
10595
+ questionsAsked: z18.number().int().min(0),
10596
+ summary: z18.string().max(300)
10037
10597
  }).strict(),
10038
- qualityIndicators: z17.object({
10039
- hallucinationAlerts: z17.array(z17.string().min(1)).max(10),
10040
- failuresAndRetries: z17.array(z17.string().min(1)).max(10),
10041
- codeChangeSummary: z17.string().max(500),
10042
- overallAssessment: z17.string().max(200)
10598
+ qualityIndicators: z18.object({
10599
+ hallucinationAlerts: z18.array(z18.string().min(1)).max(10),
10600
+ failuresAndRetries: z18.array(z18.string().min(1)).max(10),
10601
+ codeChangeSummary: z18.string().max(500),
10602
+ overallAssessment: z18.string().max(200)
10043
10603
  }).strict(),
10044
- knowledgeDeposits: z17.object({
10045
- worthRemembering: z17.array(z17.string().min(1)).max(10)
10604
+ knowledgeDeposits: z18.object({
10605
+ worthRemembering: z18.array(z18.string().min(1)).max(10)
10046
10606
  }).strict()
10047
10607
  }).strict();
10048
10608
  function localDateKey(date) {
@@ -10059,7 +10619,7 @@ var ProjectSleepOrganizer = class {
10059
10619
  #sessions;
10060
10620
  #generate;
10061
10621
  constructor(options) {
10062
- this.#workspace = resolve15(options.workspace);
10622
+ this.#workspace = resolve16(options.workspace);
10063
10623
  this.#sleepDirectory = join13(this.#workspace, ".flavor", "sleep");
10064
10624
  this.#sessions = options.sessions ?? new SessionStore({ workspace: this.#workspace });
10065
10625
  this.#generate = options.generate;
@@ -10151,8 +10711,8 @@ Previous output was invalid. Repair it and return strict JSON only.`;
10151
10711
  await mkdir10(flavorDirectory, { recursive: true, mode: 448 });
10152
10712
  await assertNotSymlink(this.#sleepDirectory);
10153
10713
  await mkdir10(this.#sleepDirectory, { recursive: true, mode: 448 });
10154
- const root = await realpath6(this.#workspace);
10155
- const target = await realpath6(this.#sleepDirectory);
10714
+ const root = await realpath7(this.#workspace);
10715
+ const target = await realpath7(this.#sleepDirectory);
10156
10716
  if (!isWithin5(root, target)) throw new Error("Sleep directory escapes the workspace");
10157
10717
  }
10158
10718
  async #hasReport(date) {
@@ -10476,16 +11036,16 @@ function assertDateKey(value) {
10476
11036
  }
10477
11037
  async function assertNotSymlink(path) {
10478
11038
  try {
10479
- if ((await lstat8(path)).isSymbolicLink()) throw new Error(`Refusing to use symbolic link: ${path}`);
11039
+ if ((await lstat9(path)).isSymbolicLink()) throw new Error(`Refusing to use symbolic link: ${path}`);
10480
11040
  } catch (error) {
10481
11041
  if (!hasCode(error, "ENOENT")) throw error;
10482
11042
  }
10483
11043
  }
10484
11044
  function isWithin5(root, candidate) {
10485
- const path = relative13(root, candidate);
11045
+ const path = relative14(root, candidate);
10486
11046
  if (path === "") return true;
10487
- if (path === ".." || path.startsWith(`..${sep12}`) || isAbsolute7(path)) return false;
10488
- return process.platform !== "win32" || resolve15(root, path).toLowerCase().startsWith(`${resolve15(root).toLowerCase()}${sep12}`);
11047
+ if (path === ".." || path.startsWith(`..${sep13}`) || isAbsolute8(path)) return false;
11048
+ return process.platform !== "win32" || resolve16(root, path).toLowerCase().startsWith(`${resolve16(root).toLowerCase()}${sep13}`);
10489
11049
  }
10490
11050
  function hasCode(error, code) {
10491
11051
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
@@ -10534,10 +11094,10 @@ function computeSessionStats(documents) {
10534
11094
 
10535
11095
  // src/tools/files.ts
10536
11096
  import { constants as constants6 } from "fs";
10537
- import { mkdir as mkdir11, open as open8, readFile as readFile11, realpath as realpath7, rename as rename7, rm as rm6, stat as stat5 } from "fs/promises";
10538
- import { dirname as dirname11, isAbsolute as isAbsolute8, relative as relative14, resolve as resolve16, sep as sep13 } from "path";
11097
+ import { mkdir as mkdir11, open as open8, readFile as readFile12, realpath as realpath8, rename as rename7, rm as rm6, stat as stat5 } from "fs/promises";
11098
+ import { dirname as dirname12, isAbsolute as isAbsolute9, relative as relative15, resolve as resolve17, sep as sep14 } from "path";
10539
11099
  import { randomUUID as randomUUID8 } from "crypto";
10540
- import { z as z18 } from "zod";
11100
+ import { z as z19 } from "zod";
10541
11101
 
10542
11102
  // src/tools/file-diff.ts
10543
11103
  var MAX_DIFF_PREVIEW_LINES = 120;
@@ -10643,18 +11203,34 @@ function truncateLines(lines) {
10643
11203
 
10644
11204
  // src/tools/files.ts
10645
11205
  var MAX_READ_BYTES = 1048576;
10646
- var ReadInput = z18.object({
10647
- path: z18.string().min(1),
10648
- maxBytes: z18.coerce.number().int().positive().max(MAX_READ_BYTES).nullable().optional(),
10649
- startLine: z18.coerce.number().int().positive().nullable().optional(),
10650
- endLine: z18.coerce.number().int().positive().nullable().optional(),
11206
+ var ReadInput2 = z19.object({
11207
+ path: z19.string().min(1),
11208
+ maxBytes: z19.coerce.number().int().positive().max(MAX_READ_BYTES).nullable().optional(),
11209
+ startLine: z19.coerce.number().int().positive().nullable().optional(),
11210
+ endLine: z19.coerce.number().int().positive().nullable().optional(),
10651
11211
  // Accepts booleans and their string forms (weak-typed models emit "true");
10652
11212
  // kept transform-free so the schema converts to JSON Schema for providers.
10653
- force: z18.union([z18.boolean(), z18.string().refine((value) => value === "true" || value === "false")]).nullable().optional()
11213
+ force: z19.union([z19.boolean(), z19.string().refine((value) => value === "true" || value === "false")]).nullable().optional()
10654
11214
  });
10655
- var WriteInput = z18.object({ path: z18.string().min(1), content: z18.string() });
10656
- var EditInput = z18.object({ path: z18.string().min(1), oldText: z18.string().min(1), newText: z18.string() });
10657
- var ApplyPatchInput = z18.object({ patch: z18.string().min(1) });
11215
+ var WriteInput = z19.object({ path: z19.string().min(1), content: z19.string() });
11216
+ var EditInput = z19.object({ path: z19.string().min(1), oldText: z19.string().min(1), newText: z19.string() });
11217
+ var ApplyPatchInput = z19.object({ patch: z19.string().min(1) });
11218
+ var FileObservationStore = class {
11219
+ #versions = /* @__PURE__ */ new Map();
11220
+ get(path) {
11221
+ return this.#versions.get(path);
11222
+ }
11223
+ set(path, version) {
11224
+ this.#versions.set(path, version);
11225
+ }
11226
+ async refresh(path) {
11227
+ this.set(path, await fileVersion(path));
11228
+ }
11229
+ async assertCurrent(path, expected) {
11230
+ const current = await fileVersion(path);
11231
+ if (current !== expected) throw new Error(`Stale file: ${path} changed since it was read; read it again before writing`);
11232
+ }
11233
+ };
10658
11234
  function createReadTool(workspace, options = {}) {
10659
11235
  const guard = createPathGuard(workspace);
10660
11236
  const openFile2 = options.openFile ?? ((path) => open8(path, constants6.O_RDONLY));
@@ -10662,7 +11238,7 @@ function createReadTool(workspace, options = {}) {
10662
11238
  return {
10663
11239
  name: "Read",
10664
11240
  description: "Read a UTF-8 text file, optionally restricted to a line range with startLine/endLine",
10665
- inputSchema: ReadInput,
11241
+ inputSchema: ReadInput2,
10666
11242
  paths: (input) => [guard.lexical(input.path)],
10667
11243
  execute: async (input, signal) => {
10668
11244
  abortIfNeeded(signal);
@@ -10684,6 +11260,7 @@ function createReadTool(workspace, options = {}) {
10684
11260
  const textEnd = utf8SafeEnd(contents, windowBytes);
10685
11261
  const truncated = contents.length > windowBytes;
10686
11262
  const text = contents.subarray(0, textEnd).toString("utf8").replaceAll("\r\n", "\n");
11263
+ options.observations?.set(path, versionOfStat(info));
10687
11264
  const lines = text.split("\n");
10688
11265
  if (lines.at(-1) === "") lines.pop();
10689
11266
  const availableLines = lines.length;
@@ -10730,6 +11307,7 @@ function createWriteTool(workspace, options = {}) {
10730
11307
  execute: async (input, signal) => {
10731
11308
  abortIfNeeded(signal);
10732
11309
  const path = await guard.destination(input.path);
11310
+ const expected = await expectedMutationVersion(path, options.observations);
10733
11311
  const previous = await readOptionalPresentationText(path);
10734
11312
  if (!previous.exists || previous.text !== void 0) {
10735
11313
  await options.beforeCommit?.({
@@ -10739,7 +11317,9 @@ function createWriteTool(workspace, options = {}) {
10739
11317
  kind: previous.exists ? "update" : "create"
10740
11318
  }, signal);
10741
11319
  }
10742
- await atomicWrite(path, input.content, signal);
11320
+ await assertMutationVersion(path, expected, options.observations);
11321
+ await atomicWrite(path, input.content, signal, expected);
11322
+ await options.observations?.refresh(path);
10743
11323
  const output = { path, bytes: Buffer.byteLength(input.content) };
10744
11324
  if (previous.exists && previous.text === void 0) return output;
10745
11325
  return withToolPresentation(output, buildFileChangePresentation(
@@ -10762,6 +11342,7 @@ function createEditTool(workspace, options = {}) {
10762
11342
  abortIfNeeded(signal);
10763
11343
  const displayPath = guard.lexical(input.path);
10764
11344
  const path = await guard.existing(input.path);
11345
+ const expected = await expectedMutationVersion(path, options.observations);
10765
11346
  const contents = await readText(path);
10766
11347
  const hasCRLF = contents.includes("\r\n");
10767
11348
  const norm = (s) => hasCRLF ? s.replace(/\r\n/g, "\n") : s;
@@ -10777,7 +11358,9 @@ function createEditTool(workspace, options = {}) {
10777
11358
  const updatedLF = contentsLF.slice(0, first) + newTextLF + contentsLF.slice(first + oldTextLF.length);
10778
11359
  const updated = hasCRLF ? updatedLF.replace(/\n/g, "\r\n") : updatedLF;
10779
11360
  await options.beforeCommit?.({ path: displayPath, before: contents, after: updated, kind: "update" }, signal);
10780
- await atomicWrite(path, updated, signal);
11361
+ await assertMutationVersion(path, expected, options.observations);
11362
+ await atomicWrite(path, updated, signal, expected);
11363
+ await options.observations?.refresh(path);
10781
11364
  return withToolPresentation(
10782
11365
  { path: displayPath, replacements: 1 },
10783
11366
  buildFileChangePresentation(displayPath, contents, updated, "update")
@@ -10798,10 +11381,12 @@ function createApplyPatchTool(workspace, options = {}) {
10798
11381
  const prepared = [];
10799
11382
  for (const change of changes) {
10800
11383
  const path = await guard.destination(change.path);
11384
+ const expected = await expectedMutationVersion(path, options.observations);
10801
11385
  const original = change.created ? await requireAbsent(guard, change.path) : await readText(await guard.existing(change.path));
10802
11386
  const applied = applyHunks(original, change.hunks);
10803
- prepared.push({ path, content: applied.content, change, hunks: applied.hunks });
11387
+ prepared.push({ path, content: applied.content, change, hunks: applied.hunks, expected });
10804
11388
  }
11389
+ for (const change of prepared) await assertMutationVersion(change.path, change.expected, options.observations);
10805
11390
  for (const change of prepared) {
10806
11391
  const before = change.change.created ? "" : await readText(change.path);
10807
11392
  await options.beforeCommit?.({
@@ -10810,43 +11395,49 @@ function createApplyPatchTool(workspace, options = {}) {
10810
11395
  after: change.content,
10811
11396
  kind: change.change.created ? "create" : "update"
10812
11397
  }, signal);
10813
- await atomicWrite(change.path, change.content, signal);
11398
+ }
11399
+ for (const change of prepared) await assertMutationVersion(change.path, change.expected, options.observations);
11400
+ for (const change of prepared) {
11401
+ await atomicWrite(change.path, change.content, signal, change.expected);
11402
+ await options.observations?.refresh(change.path);
10814
11403
  }
10815
11404
  const first = prepared[0];
11405
+ const primaryPresentation = buildPatchPresentation(first.path, first.change.created, first.hunks);
11406
+ const relatedChanges = prepared.slice(1).map((change) => buildPatchPresentation(change.path, change.change.created, change.hunks));
10816
11407
  return withToolPresentation(
10817
11408
  { files: prepared.map((change) => change.path) },
10818
- buildPatchPresentation(first.path, first.change.created, first.hunks)
11409
+ relatedChanges.length === 0 ? primaryPresentation : { ...primaryPresentation, relatedChanges }
10819
11410
  );
10820
11411
  }
10821
11412
  };
10822
11413
  }
10823
11414
  function createPathGuard(workspace) {
10824
- const root = resolve16(workspace);
11415
+ const root = resolve17(workspace);
10825
11416
  const lexical = (input) => {
10826
- const candidate = resolve16(root, input);
10827
- if (!within(root, candidate)) throw new Error("Path is outside the workspace");
11417
+ const candidate = resolve17(root, input);
11418
+ if (!within2(root, candidate)) throw new Error("Path is outside the workspace");
10828
11419
  return candidate;
10829
11420
  };
10830
11421
  return {
10831
11422
  lexical,
10832
11423
  existing: async (input) => {
10833
11424
  const candidate = lexical(input);
10834
- const physical = await realpath7(candidate);
10835
- if (!within(await realpath7(root), physical)) throw new Error("Path escapes the workspace through a symlink");
11425
+ const physical = await realpath8(candidate);
11426
+ if (!within2(await realpath8(root), physical)) throw new Error("Path escapes the workspace through a symlink");
10836
11427
  return physical;
10837
11428
  },
10838
11429
  destination: async (input) => {
10839
11430
  const candidate = lexical(input);
10840
- const physicalRoot = await realpath7(root);
11431
+ const physicalRoot = await realpath8(root);
10841
11432
  let ancestor = candidate;
10842
11433
  while (true) {
10843
11434
  try {
10844
- const physical = await realpath7(ancestor);
10845
- if (!within(physicalRoot, physical)) throw new Error("Path escapes the workspace through a symlink");
11435
+ const physical = await realpath8(ancestor);
11436
+ if (!within2(physicalRoot, physical)) throw new Error("Path escapes the workspace through a symlink");
10846
11437
  break;
10847
11438
  } catch (error) {
10848
11439
  if (!isMissing3(error)) throw error;
10849
- const parent = dirname11(ancestor);
11440
+ const parent = dirname12(ancestor);
10850
11441
  if (parent === ancestor) throw error;
10851
11442
  ancestor = parent;
10852
11443
  }
@@ -10855,9 +11446,9 @@ function createPathGuard(workspace) {
10855
11446
  }
10856
11447
  };
10857
11448
  }
10858
- async function atomicWrite(path, content, signal) {
11449
+ async function atomicWrite(path, content, signal, expectedVersion) {
10859
11450
  abortIfNeeded(signal);
10860
- await mkdir11(dirname11(path), { recursive: true });
11451
+ await mkdir11(dirname12(path), { recursive: true });
10861
11452
  const temporary = `${path}.${randomUUID8()}.tmp`;
10862
11453
  try {
10863
11454
  const handle = await open8(temporary, constants6.O_CREAT | constants6.O_EXCL | constants6.O_WRONLY, 384);
@@ -10868,19 +11459,46 @@ async function atomicWrite(path, content, signal) {
10868
11459
  await handle.close();
10869
11460
  }
10870
11461
  abortIfNeeded(signal);
11462
+ if (expectedVersion !== void 0 && await fileVersion(path) !== expectedVersion) {
11463
+ throw new Error(`Stale file: ${path} changed while the replacement was being prepared`);
11464
+ }
10871
11465
  await rename7(temporary, path);
10872
11466
  } finally {
10873
11467
  await rm6(temporary, { force: true });
10874
11468
  }
10875
11469
  }
10876
11470
  async function readText(path) {
10877
- const contents = await readFile11(path);
11471
+ const contents = await readFile12(path);
10878
11472
  if (isBinary(contents)) throw new Error("Cannot edit binary file as text");
10879
11473
  return contents.toString("utf8");
10880
11474
  }
11475
+ var ABSENT_VERSION = "absent";
11476
+ async function expectedMutationVersion(path, observations) {
11477
+ const current = await fileVersion(path);
11478
+ const observed = observations?.get(path);
11479
+ if (observed !== void 0 && observed !== current) {
11480
+ throw new Error(`Stale file: ${path} changed since it was read; read it again before writing`);
11481
+ }
11482
+ return current;
11483
+ }
11484
+ async function assertMutationVersion(path, expected, observations) {
11485
+ if (observations !== void 0) await observations.assertCurrent(path, expected);
11486
+ else if (await fileVersion(path) !== expected) throw new Error(`Stale file: ${path} changed since mutation preparation`);
11487
+ }
11488
+ async function fileVersion(path) {
11489
+ try {
11490
+ return versionOfStat(await stat5(path));
11491
+ } catch (error) {
11492
+ if (isMissing3(error)) return ABSENT_VERSION;
11493
+ throw error;
11494
+ }
11495
+ }
11496
+ function versionOfStat(info) {
11497
+ return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
11498
+ }
10881
11499
  async function readOptionalPresentationText(path) {
10882
11500
  try {
10883
- const contents = await readFile11(path);
11501
+ const contents = await readFile12(path);
10884
11502
  return isBinary(contents) ? { exists: true } : { exists: true, text: contents.toString("utf8") };
10885
11503
  } catch (error) {
10886
11504
  return isMissing3(error) ? { exists: false } : { exists: true };
@@ -10964,9 +11582,9 @@ async function readBounded2(path, maxBytes, signal, openFile2) {
10964
11582
  await handle.close();
10965
11583
  }
10966
11584
  }
10967
- function within(root, candidate) {
10968
- const delta = relative14(root, candidate);
10969
- return delta === "" || !delta.startsWith(`..${sep13}`) && delta !== ".." && !isAbsolute8(delta);
11585
+ function within2(root, candidate) {
11586
+ const delta = relative15(root, candidate);
11587
+ return delta === "" || !delta.startsWith(`..${sep14}`) && delta !== ".." && !isAbsolute9(delta);
10970
11588
  }
10971
11589
  function forceFlag(value) {
10972
11590
  return value === true || value === "true";
@@ -11187,13 +11805,321 @@ function matchesLines(source, start, expected) {
11187
11805
  return start >= 0 && start + expected.length <= source.length && expected.every((line, index) => source[start + index] === line);
11188
11806
  }
11189
11807
 
11808
+ // src/tools/web.ts
11809
+ import { lookup } from "dns/promises";
11810
+ import { isIP } from "net";
11811
+ import { request as httpRequest } from "http";
11812
+ import { request as httpsRequest } from "https";
11813
+ import { z as z20 } from "zod";
11814
+ var WebFetchInput = z20.object({
11815
+ url: z20.string().url().max(8192),
11816
+ timeoutMs: z20.coerce.number().int().positive().max(12e4).optional(),
11817
+ maxBytes: z20.coerce.number().int().positive().max(10 * 1024 * 1024).optional()
11818
+ });
11819
+ var WebSearchInput = z20.object({
11820
+ query: z20.string().trim().min(1).max(1e3),
11821
+ maxResults: z20.coerce.number().int().min(1).max(20).optional()
11822
+ });
11823
+ function createWebFetchTool() {
11824
+ return {
11825
+ name: "WebFetch",
11826
+ description: "Fetch a public HTTP(S) page with SSRF protection and return readable text",
11827
+ inputSchema: WebFetchInput,
11828
+ outputSchema: z20.object({ url: z20.string(), status: z20.number(), contentType: z20.string(), content: z20.string(), truncated: z20.boolean() }),
11829
+ paths: () => [],
11830
+ summarize: (input) => input.url,
11831
+ presentCall: (input) => ({ kind: "web", title: `Fetching ${new URL(input.url).hostname}`, url: input.url }),
11832
+ permissions: () => ({ paths: [] }),
11833
+ execute: async (input, signal) => fetchPublic(input.url, {
11834
+ signal,
11835
+ timeoutMs: input.timeoutMs ?? 3e4,
11836
+ maxBytes: input.maxBytes ?? 2 * 1024 * 1024
11837
+ }),
11838
+ renderForModel: (output) => [
11839
+ `URL: ${output.url}`,
11840
+ `Status: ${output.status}`,
11841
+ `Content-Type: ${output.contentType}`,
11842
+ output.truncated ? "[Response truncated]" : "",
11843
+ "",
11844
+ output.content
11845
+ ].filter((line, index) => line !== "" || index >= 4).join("\n"),
11846
+ presentResult: (output) => ({ kind: "web", title: `Fetched ${new URL(output.url).hostname}`, url: output.url, summary: `${output.status} \xB7 ${output.contentType}` })
11847
+ };
11848
+ }
11849
+ function createWebSearchTool(provider = createDefaultWebSearchProvider()) {
11850
+ return {
11851
+ name: "WebSearch",
11852
+ description: "Search the public web and return titles, URLs, and snippets",
11853
+ inputSchema: WebSearchInput,
11854
+ outputSchema: z20.object({ query: z20.string(), results: z20.array(z20.object({ title: z20.string(), url: z20.string(), snippet: z20.string() })) }),
11855
+ paths: () => [],
11856
+ summarize: (input) => input.query,
11857
+ presentCall: (input) => ({ kind: "web", title: `Searching: ${input.query}` }),
11858
+ permissions: () => ({ paths: [] }),
11859
+ execute: async (input, signal) => ({ query: input.query, results: [...await provider.search(input.query, input.maxResults ?? 8, signal)] }),
11860
+ renderForModel: (output) => output.results.length === 0 ? `No web results found for: ${output.query}` : output.results.map((item, index) => `${index + 1}. ${item.title}
11861
+ ${item.url}
11862
+ ${item.snippet}`).join("\n\n"),
11863
+ presentResult: (output) => ({ kind: "web", title: `Search: ${output.query}`, summary: `${output.results.length} results`, items: output.results })
11864
+ };
11865
+ }
11866
+ function createDefaultWebSearchProvider(fetcher = fetchPublic) {
11867
+ return new MultiSourceWebSearchProvider(fetcher);
11868
+ }
11869
+ var MultiSourceWebSearchProvider = class {
11870
+ constructor(fetcher) {
11871
+ this.fetcher = fetcher;
11872
+ }
11873
+ fetcher;
11874
+ async search(query, maxResults, signal) {
11875
+ const sources = [
11876
+ { name: "DuckDuckGo Lite", url: `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, parse: parseDuckDuckGo },
11877
+ { name: "Bing", url: `https://www.bing.com/search?q=${encodeURIComponent(query)}`, parse: parseBing }
11878
+ ];
11879
+ const failures = [];
11880
+ for (const source of sources) {
11881
+ try {
11882
+ const response = await this.fetcher(source.url, {
11883
+ signal,
11884
+ timeoutMs: 3e4,
11885
+ maxBytes: 2 * 1024 * 1024,
11886
+ rawHtml: true
11887
+ });
11888
+ if (response.status < 200 || response.status >= 300) {
11889
+ failures.push(`${source.name}: HTTP ${response.status}`);
11890
+ continue;
11891
+ }
11892
+ const results = source.parse(response.content);
11893
+ if (results.length > 0) return results.slice(0, maxResults);
11894
+ failures.push(`${source.name}: no results parsed`);
11895
+ } catch (error) {
11896
+ if (signal.aborted) throw error;
11897
+ failures.push(`${source.name}: ${error instanceof Error ? error.message : String(error)}`);
11898
+ }
11899
+ }
11900
+ throw new Error(`Web search providers failed (${failures.join("; ")})`);
11901
+ }
11902
+ };
11903
+ async function fetchPublic(initialUrl, options) {
11904
+ let current = new URL(initialUrl);
11905
+ for (let redirects = 0; redirects <= 5; redirects += 1) {
11906
+ validateUrl(current);
11907
+ const addresses = await lookup(normalizedHostname(current.hostname), { all: true, verbatim: true });
11908
+ if (addresses.length === 0) throw new Error("Host did not resolve");
11909
+ if (addresses.some((entry2) => isBlockedResolvedAddress(entry2.address, current.hostname))) {
11910
+ throw new Error("Blocked private or special network destination");
11911
+ }
11912
+ const response = await requestAddress(current, addresses[0].address, options);
11913
+ if (response.redirect !== void 0) {
11914
+ if (redirects === 5) throw new Error("Too many redirects");
11915
+ current = new URL(response.redirect, current);
11916
+ continue;
11917
+ }
11918
+ const contentType = response.contentType;
11919
+ const decoded = new TextDecoder(charset(contentType), { fatal: false }).decode(response.body);
11920
+ const content = !options.rawHtml && /text\/html|application\/xhtml\+xml/i.test(contentType) ? htmlToText(decoded) : decoded;
11921
+ return { url: current.href, status: response.status, contentType, content, truncated: response.truncated };
11922
+ }
11923
+ throw new Error("Too many redirects");
11924
+ }
11925
+ async function requestAddress(url, address, options) {
11926
+ return new Promise((resolvePromise, reject) => {
11927
+ let settled = false;
11928
+ const finish = (value) => {
11929
+ if (settled) return;
11930
+ settled = true;
11931
+ clearTimeout(totalTimer);
11932
+ resolvePromise(value);
11933
+ };
11934
+ const fail = (error) => {
11935
+ if (settled) return;
11936
+ settled = true;
11937
+ clearTimeout(totalTimer);
11938
+ reject(error);
11939
+ };
11940
+ const secure = url.protocol === "https:";
11941
+ const request = (secure ? httpsRequest : httpRequest)({
11942
+ protocol: url.protocol,
11943
+ hostname: address,
11944
+ port: url.port || (secure ? 443 : 80),
11945
+ method: "GET",
11946
+ path: `${url.pathname}${url.search}`,
11947
+ headers: { host: url.host, accept: "text/html,application/json,text/plain;q=0.9,*/*;q=0.5", "accept-encoding": "identity", "user-agent": "Flavor-Code/1" },
11948
+ ...secure ? { servername: normalizedHostname(url.hostname) } : {},
11949
+ signal: options.signal
11950
+ }, (response) => {
11951
+ const status = response.statusCode ?? 0;
11952
+ const location = response.headers.location;
11953
+ if (location !== void 0 && [301, 302, 303, 307, 308].includes(status)) {
11954
+ response.resume();
11955
+ finish({ status, contentType: "", body: new Uint8Array(), truncated: false, redirect: location });
11956
+ return;
11957
+ }
11958
+ const chunks = [];
11959
+ let bytes = 0;
11960
+ let truncated = false;
11961
+ response.on("data", (chunk) => {
11962
+ if (bytes >= options.maxBytes) {
11963
+ truncated = true;
11964
+ finish({ status, contentType: String(response.headers["content-type"] ?? "application/octet-stream"), body: Buffer.concat(chunks), truncated });
11965
+ response.destroy();
11966
+ return;
11967
+ }
11968
+ const accepted = chunk.subarray(0, options.maxBytes - bytes);
11969
+ chunks.push(accepted);
11970
+ bytes += accepted.length;
11971
+ if (accepted.length < chunk.length) truncated = true;
11972
+ });
11973
+ response.once("end", () => finish({
11974
+ status,
11975
+ contentType: String(response.headers["content-type"] ?? "application/octet-stream"),
11976
+ body: Buffer.concat(chunks),
11977
+ truncated
11978
+ }));
11979
+ response.once("error", fail);
11980
+ });
11981
+ const totalTimer = setTimeout(() => request.destroy(new Error(`Web request timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
11982
+ totalTimer.unref();
11983
+ request.once("error", fail);
11984
+ request.end();
11985
+ });
11986
+ }
11987
+ function validateUrl(url) {
11988
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("Only HTTP(S) URLs are supported");
11989
+ if (url.username !== "" || url.password !== "") throw new Error("URL credentials are not allowed");
11990
+ const hostname = normalizedHostname(url.hostname).toLowerCase();
11991
+ if (hostname === "localhost" || hostname.endsWith(".localhost")) throw new Error("Blocked private destination");
11992
+ const port = url.port === "" ? url.protocol === "https:" ? 443 : 80 : Number(url.port);
11993
+ if (![80, 443].includes(port)) throw new Error(`Blocked network port: ${port}`);
11994
+ if (isIP(hostname) && isBlockedAddress(hostname)) throw new Error("Blocked private or special network destination");
11995
+ }
11996
+ function isBlockedResolvedAddress(input, hostname) {
11997
+ return isBlockedAddress(input, { allowBenchmarkCarrier: isIP(normalizedHostname(hostname)) === 0 });
11998
+ }
11999
+ function isBlockedAddress(input, options = {}) {
12000
+ const address = input.toLowerCase().replace(/^\[|\]$/g, "").split("%")[0];
12001
+ if (address === "::" || address === "::1") return true;
12002
+ const mappedIpv4 = extractMappedIpv4(address);
12003
+ if (mappedIpv4 !== void 0) return isBlockedAddress(mappedIpv4, options);
12004
+ if (address.includes(":")) {
12005
+ return address.startsWith("fc") || address.startsWith("fd") || /^fe[89ab]/.test(address) || address.startsWith("ff") || address.startsWith("2001:db8:");
12006
+ }
12007
+ const parts = address.split(".").map(Number);
12008
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return true;
12009
+ const [a, b, c] = parts;
12010
+ return a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 && (c === 0 || c === 2) || a === 192 && b === 88 && c === 99 || !options.allowBenchmarkCarrier && a === 198 && (b === 18 || b === 19) || a === 198 && b === 51 && c === 100 || a === 203 && b === 0 && c === 113;
12011
+ }
12012
+ function extractMappedIpv4(address) {
12013
+ if (isIP(address) !== 6) return void 0;
12014
+ let canonical3;
12015
+ try {
12016
+ canonical3 = new URL(`http://[${address}]/`).hostname.replace(/^\[|\]$/g, "");
12017
+ } catch {
12018
+ return void 0;
12019
+ }
12020
+ const match = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(canonical3);
12021
+ if (match === null) return void 0;
12022
+ const high = Number.parseInt(match[1], 16);
12023
+ const low = Number.parseInt(match[2], 16);
12024
+ return `${high >>> 8}.${high & 255}.${low >>> 8}.${low & 255}`;
12025
+ }
12026
+ function normalizedHostname(hostname) {
12027
+ return hostname.replace(/^\[|\]$/g, "");
12028
+ }
12029
+ function htmlToText(html) {
12030
+ let text = html.replace(/<!--[\s\S]*?-->/g, "").replace(/<(script|style|noscript|svg)\b[^>]*>[\s\S]*?<\/\1>/gi, "").replace(/<h1\b[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n").replace(/<h2\b[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n").replace(/<h3\b[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n").replace(/<li\b[^>]*>/gi, "\n- ").replace(/<(p|div|section|article|main|header|footer|ul|ol|table|tr)\b[^>]*>/gi, "\n").replace(/<\/(p|div|section|article|main|header|footer|li|ul|ol|table|tr)>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "");
12031
+ text = decodeEntities(text).replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").replace(/\n[ \t]+/g, "\n").replace(/\n{3,}/g, "\n\n");
12032
+ return text.trim();
12033
+ }
12034
+ function parseDuckDuckGo(html) {
12035
+ const results = [];
12036
+ const pattern = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
12037
+ for (const match of html.matchAll(pattern)) {
12038
+ const url = unwrapDuckDuckGoUrl(match[1]);
12039
+ if (url !== void 0) results.push({ title: htmlToText(match[2]), url, snippet: htmlToText(match[3]) });
12040
+ }
12041
+ const litePattern = /<a\b[^>]*href=(["'])(.*?)\1[^>]*class=(["'])[^"']*\bresult-link\b[^"']*\3[^>]*>([\s\S]*?)<\/a>[\s\S]*?<td\b[^>]*class=(["'])[^"']*\bresult-snippet\b[^"']*\5[^>]*>([\s\S]*?)<\/td>/gi;
12042
+ for (const match of html.matchAll(litePattern)) {
12043
+ const url = unwrapDuckDuckGoUrl(match[2]);
12044
+ if (url !== void 0) results.push({ title: htmlToText(match[4]), url, snippet: htmlToText(match[6]) });
12045
+ }
12046
+ return uniqueResults(results);
12047
+ }
12048
+ function parseBing(html) {
12049
+ const results = [];
12050
+ const itemPattern = /<li\b[^>]*class=(["'])[^"']*\bb_algo\b[^"']*\1[^>]*>([\s\S]*?)<\/li>/gi;
12051
+ for (const itemMatch of html.matchAll(itemPattern)) {
12052
+ const item = itemMatch[2];
12053
+ const titleMatch = /<h2\b[^>]*>[\s\S]*?<a\b([^>]*)>([\s\S]*?)<\/a>[\s\S]*?<\/h2>/i.exec(item);
12054
+ const snippetMatch = /<div\b[^>]*class=(["'])[^"']*\bb_caption\b[^"']*\1[^>]*>[\s\S]*?<p\b[^>]*>([\s\S]*?)<\/p>/i.exec(item);
12055
+ if (titleMatch === null) continue;
12056
+ const href = readHtmlAttribute(titleMatch[1], "href");
12057
+ const url = href === void 0 ? void 0 : unwrapBingUrl(href);
12058
+ if (url === void 0) continue;
12059
+ results.push({ title: htmlToText(titleMatch[2]), url, snippet: snippetMatch === null ? "" : htmlToText(snippetMatch[2]) });
12060
+ }
12061
+ return uniqueResults(results);
12062
+ }
12063
+ function unwrapDuckDuckGoUrl(input) {
12064
+ try {
12065
+ const parsed = new URL(decodeEntities(input), "https://lite.duckduckgo.com");
12066
+ return publicResultUrl(parsed.searchParams.get("uddg") ?? parsed.href);
12067
+ } catch {
12068
+ return void 0;
12069
+ }
12070
+ }
12071
+ function unwrapBingUrl(input) {
12072
+ try {
12073
+ const parsed = new URL(decodeEntities(input), "https://www.bing.com");
12074
+ const encoded = parsed.hostname.endsWith("bing.com") && parsed.pathname === "/ck/a" ? parsed.searchParams.get("u") : null;
12075
+ if (encoded?.startsWith("a1")) {
12076
+ const target = Buffer.from(encoded.slice(2), "base64url").toString("utf8");
12077
+ return publicResultUrl(target);
12078
+ }
12079
+ return publicResultUrl(parsed.href);
12080
+ } catch {
12081
+ return void 0;
12082
+ }
12083
+ }
12084
+ function publicResultUrl(input) {
12085
+ try {
12086
+ const parsed = new URL(input);
12087
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : void 0;
12088
+ } catch {
12089
+ return void 0;
12090
+ }
12091
+ }
12092
+ function readHtmlAttribute(attributes, name) {
12093
+ const match = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i").exec(attributes);
12094
+ return match?.[1] ?? match?.[2];
12095
+ }
12096
+ function uniqueResults(results) {
12097
+ const seen = /* @__PURE__ */ new Set();
12098
+ return results.filter((result) => {
12099
+ if (seen.has(result.url)) return false;
12100
+ seen.add(result.url);
12101
+ return true;
12102
+ });
12103
+ }
12104
+ function decodeEntities(text) {
12105
+ const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
12106
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (_all, entity) => {
12107
+ if (entity.startsWith("#x")) return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
12108
+ if (entity.startsWith("#")) return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
12109
+ return named[entity.toLowerCase()] ?? `&${entity};`;
12110
+ });
12111
+ }
12112
+ function charset(contentType) {
12113
+ return /charset\s*=\s*(?:utf-16|utf-16le)/i.test(contentType) ? "utf-16le" : "utf-8";
12114
+ }
12115
+
11190
12116
  // src/tools/lsp.ts
11191
12117
  import { spawn as spawn5 } from "child_process";
11192
12118
  import { existsSync as existsSync2 } from "fs";
11193
- import { access, constants as constants7, readFile as readFile12 } from "fs/promises";
11194
- import { basename as basename4, dirname as dirname12, extname, isAbsolute as isAbsolute9, relative as relative15, resolve as resolve17, sep as sep14 } from "path";
12119
+ import { access, constants as constants7, readFile as readFile13 } from "fs/promises";
12120
+ import { basename as basename4, dirname as dirname13, extname, isAbsolute as isAbsolute10, relative as relative16, resolve as resolve18, sep as sep15 } from "path";
11195
12121
  import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "url";
11196
- import { z as z19 } from "zod";
12122
+ import { z as z21 } from "zod";
11197
12123
  var LSP_REQUEST_TIMEOUT_MS = 15e3;
11198
12124
  var LspConnection = class {
11199
12125
  #process;
@@ -11290,8 +12216,8 @@ var LspConnection = class {
11290
12216
  const id = this.#nextId++;
11291
12217
  const request = { jsonrpc: "2.0", id, method, params };
11292
12218
  const timeout = AbortSignal.timeout(LSP_REQUEST_TIMEOUT_MS);
11293
- const promise = new Promise((resolve22, reject) => {
11294
- this.#pending.set(id, { resolve: resolve22, reject });
12219
+ const promise = new Promise((resolve25, reject) => {
12220
+ this.#pending.set(id, { resolve: resolve25, reject });
11295
12221
  timeout.addEventListener("abort", () => {
11296
12222
  if (this.#pending.has(id)) {
11297
12223
  this.#pending.delete(id);
@@ -11356,7 +12282,7 @@ var LspConnection = class {
11356
12282
  const url = new URL(uri);
11357
12283
  if (url.protocol !== "file:") return;
11358
12284
  const filePath = fileURLToPath3(url);
11359
- const text = await readFile12(filePath, "utf8");
12285
+ const text = await readFile13(filePath, "utf8");
11360
12286
  const current = this.#documents.get(uri);
11361
12287
  if (current === void 0) {
11362
12288
  this.#documents.set(uri, { text, version: 1 });
@@ -11407,8 +12333,8 @@ var LspBuffer = class {
11407
12333
  }
11408
12334
  }
11409
12335
  };
11410
- var TYPESCRIPT_CLI = resolve17(
11411
- dirname12(fileURLToPath3(import.meta.resolve("typescript/package.json"))),
12336
+ var TYPESCRIPT_CLI = resolve18(
12337
+ dirname13(fileURLToPath3(import.meta.resolve("typescript/package.json"))),
11412
12338
  "lib",
11413
12339
  "tsc.js"
11414
12340
  );
@@ -11449,7 +12375,7 @@ var RealLspManager = class {
11449
12375
  #serverConfigs;
11450
12376
  #onStatus;
11451
12377
  constructor(options) {
11452
- this.#workspace = resolve17(options.workspace);
12378
+ this.#workspace = resolve18(options.workspace);
11453
12379
  this.#serverConfigs = options.serverConfigs ?? KNOWN_SERVERS;
11454
12380
  this.#onStatus = options.onStatus;
11455
12381
  }
@@ -11498,7 +12424,7 @@ var RealLspManager = class {
11498
12424
  if (pending !== void 0) return pending;
11499
12425
  const config = this.#serverConfigs.find((c) => c.language === language);
11500
12426
  if (config === void 0) return void 0;
11501
- const hasRoot = config.rootFiles.some((file) => existsSync2(resolve17(this.#workspace, file)));
12427
+ const hasRoot = config.rootFiles.some((file) => existsSync2(resolve18(this.#workspace, file)));
11502
12428
  if (!hasRoot) return void 0;
11503
12429
  const startPromise = this.#startServer(config);
11504
12430
  this.#pendingStarts.set(language, startPromise);
@@ -11527,24 +12453,24 @@ var RealLspManager = class {
11527
12453
  return connection;
11528
12454
  }
11529
12455
  };
11530
- var LspFindRefsInput = z19.object({
11531
- file: z19.string().min(1),
11532
- line: z19.coerce.number().int().nonnegative(),
11533
- character: z19.coerce.number().int().nonnegative()
12456
+ var LspFindRefsInput = z21.object({
12457
+ file: z21.string().min(1),
12458
+ line: z21.coerce.number().int().nonnegative(),
12459
+ character: z21.coerce.number().int().nonnegative()
11534
12460
  });
11535
- var LspHoverInput = z19.object({
11536
- file: z19.string().min(1),
11537
- line: z19.coerce.number().int().nonnegative(),
11538
- character: z19.coerce.number().int().nonnegative()
12461
+ var LspHoverInput = z21.object({
12462
+ file: z21.string().min(1),
12463
+ line: z21.coerce.number().int().nonnegative(),
12464
+ character: z21.coerce.number().int().nonnegative()
11539
12465
  });
11540
- var LspDiagnosticsInput = z19.object({
11541
- file: z19.string().min(1)
12466
+ var LspDiagnosticsInput = z21.object({
12467
+ file: z21.string().min(1)
11542
12468
  });
11543
12469
  function createLspTools(workspace, options = {}) {
11544
- const root = resolve17(workspace);
12470
+ const root = resolve18(workspace);
11545
12471
  const manager = options.manager ?? new RealLspManager({ workspace: root, onStatus: options.onStatus });
11546
12472
  const guard = (input) => {
11547
- const candidate = resolve17(root, input);
12473
+ const candidate = resolve18(root, input);
11548
12474
  if (!isWithin6(root, candidate)) throw new Error("Path is outside the workspace");
11549
12475
  return candidate;
11550
12476
  };
@@ -11620,8 +12546,8 @@ function languageIdForPath(filePath) {
11620
12546
  }
11621
12547
  }
11622
12548
  function isWithin6(root, candidate) {
11623
- const delta = relative15(root, candidate);
11624
- return delta === "" || !delta.startsWith(`..${sep14}`) && delta !== ".." && !isAbsolute9(delta);
12549
+ const delta = relative16(root, candidate);
12550
+ return delta === "" || !delta.startsWith(`..${sep15}`) && delta !== ".." && !isAbsolute10(delta);
11625
12551
  }
11626
12552
  function uriToPath(uri) {
11627
12553
  try {
@@ -11653,18 +12579,18 @@ function severityLabel(severity) {
11653
12579
  }
11654
12580
 
11655
12581
  // src/tools/ask-user-question.ts
11656
- import { z as z20 } from "zod";
11657
- var QuestionOptionSchema = z20.object({
11658
- label: z20.string().trim().min(1),
11659
- description: z20.string().trim().min(1)
12582
+ import { z as z22 } from "zod";
12583
+ var QuestionOptionSchema = z22.object({
12584
+ label: z22.string().trim().min(1),
12585
+ description: z22.string().trim().min(1)
11660
12586
  });
11661
- var QuestionSchema = z20.object({
11662
- question: z20.string().trim().min(1),
11663
- header: z20.string().trim().min(1),
11664
- options: z20.array(QuestionOptionSchema).min(1).max(4)
12587
+ var QuestionSchema = z22.object({
12588
+ question: z22.string().trim().min(1),
12589
+ header: z22.string().trim().min(1),
12590
+ options: z22.array(QuestionOptionSchema).min(1).max(4)
11665
12591
  });
11666
- var AskUserQuestionInput = z20.object({
11667
- questions: z20.array(QuestionSchema).min(1).max(4)
12592
+ var AskUserQuestionInput = z22.object({
12593
+ questions: z22.array(QuestionSchema).min(1).max(4)
11668
12594
  });
11669
12595
  function hookAnswersFromUpdatedInput(updatedInput, questions) {
11670
12596
  if (typeof updatedInput !== "object" || updatedInput === null) return void 0;
@@ -11693,7 +12619,7 @@ var QuestionBridge = class {
11693
12619
  ask(questions, signal) {
11694
12620
  if (this.#pending !== void 0) return Promise.reject(new Error("A question is already pending"));
11695
12621
  if (signal.aborted) return Promise.reject(signal.reason);
11696
- return new Promise((resolve22, reject) => {
12622
+ return new Promise((resolve25, reject) => {
11697
12623
  const onAbort = () => {
11698
12624
  this.#pending = void 0;
11699
12625
  reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
@@ -11705,7 +12631,7 @@ var QuestionBridge = class {
11705
12631
  signal.removeEventListener("abort", onAbort);
11706
12632
  this.#pending = void 0;
11707
12633
  this.#onChange?.();
11708
- resolve22(answers);
12634
+ resolve25(answers);
11709
12635
  },
11710
12636
  reject: (error) => {
11711
12637
  signal.removeEventListener("abort", onAbort);
@@ -11748,20 +12674,20 @@ function createAskUserQuestionTool(handler) {
11748
12674
  }
11749
12675
 
11750
12676
  // src/tools/todo-write.ts
11751
- import { z as z21 } from "zod";
11752
- var TodoStatusSchema = z21.enum([
12677
+ import { z as z23 } from "zod";
12678
+ var TodoStatusSchema = z23.enum([
11753
12679
  "pending",
11754
12680
  "in_progress",
11755
12681
  "completed",
11756
12682
  "cancelled"
11757
12683
  ]);
11758
- var TodoItemSchema = z21.object({
11759
- content: z21.string().trim().min(1),
12684
+ var TodoItemSchema = z23.object({
12685
+ content: z23.string().trim().min(1),
11760
12686
  status: TodoStatusSchema,
11761
- activeForm: z21.string().trim().min(1)
12687
+ activeForm: z23.string().trim().min(1)
11762
12688
  });
11763
- var TodoWriteInput = z21.object({
11764
- todos: z21.array(TodoItemSchema).min(1).max(50)
12689
+ var TodoWriteInput = z23.object({
12690
+ todos: z23.array(TodoItemSchema).min(1).max(50)
11765
12691
  });
11766
12692
  function createTodoWriteTool() {
11767
12693
  return {
@@ -11796,38 +12722,38 @@ function createTodoWriteTool() {
11796
12722
 
11797
12723
  // src/tools/managed.ts
11798
12724
  import { randomUUID as randomUUID9 } from "crypto";
11799
- import { link, lstat as lstat9, mkdir as mkdir12, readFile as readFile13, readdir as readdir8, unlink, writeFile as writeFile7 } from "fs/promises";
11800
- import { dirname as dirname13, join as join14, resolve as resolve18 } from "path";
11801
- import { z as z22 } from "zod";
12725
+ import { link, lstat as lstat10, mkdir as mkdir12, readFile as readFile14, readdir as readdir8, unlink, writeFile as writeFile7 } from "fs/promises";
12726
+ import { dirname as dirname14, join as join14, resolve as resolve19 } from "path";
12727
+ import { z as z24 } from "zod";
11802
12728
  var MANAGED_TOOL_VERSION = 1;
11803
12729
  var MAX_RECORD_BYTES = 256 * 1024;
11804
12730
  var TOOL_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
11805
- var JsonSchemaObject = z22.record(z22.string(), z22.unknown()).superRefine((schema, context) => {
12731
+ var JsonSchemaObject = z24.record(z24.string(), z24.unknown()).superRefine((schema, context) => {
11806
12732
  if (schema.type !== "object") {
11807
12733
  context.addIssue({ code: "custom", message: "Tool input JSON Schema must have type object" });
11808
12734
  }
11809
12735
  });
11810
- var RegisterManagedToolInputSchema = z22.object({
11811
- name: z22.string().regex(TOOL_NAME).describe("Unique callable tool name using letters, digits, and underscores"),
11812
- description: z22.string().min(1).max(4096).describe("Purpose and appropriate use of the generated tool"),
12736
+ var RegisterManagedToolInputSchema = z24.object({
12737
+ name: z24.string().regex(TOOL_NAME).describe("Unique callable tool name using letters, digits, and underscores"),
12738
+ description: z24.string().min(1).max(4096).describe("Purpose and appropriate use of the generated tool"),
11813
12739
  inputSchema: JsonSchemaObject.describe("Provider-facing JSON Schema with an object at the root"),
11814
- implementation: z22.string().min(1).max(64 * 1024).describe(
12740
+ implementation: z24.string().min(1).max(64 * 1024).describe(
11815
12741
  "Async JavaScript function body, or complete function/arrow expression, receiving input, signal, and context; return the tool result"
11816
12742
  ),
11817
- scope: z22.enum(["project", "global"]).default("project").describe(
12743
+ scope: z24.enum(["project", "global"]).default("project").describe(
11818
12744
  "Project persists in the current workspace; global persists for every workspace"
11819
12745
  ),
11820
- agents: z22.array(z22.enum(["main", "subagent"])).min(1).max(2).optional().describe(
12746
+ agents: z24.array(z24.enum(["main", "subagent"])).min(1).max(2).optional().describe(
11821
12747
  "Agent roles allowed to call the generated tool; omit to allow both"
11822
12748
  )
11823
12749
  }).strict();
11824
- var RemoveManagedToolInputSchema = z22.object({
11825
- name: z22.string().regex(TOOL_NAME).describe("Managed tool name to remove"),
11826
- scope: z22.enum(["project", "global"]).optional().describe("Required only when the same name exists in both scopes")
12750
+ var RemoveManagedToolInputSchema = z24.object({
12751
+ name: z24.string().regex(TOOL_NAME).describe("Managed tool name to remove"),
12752
+ scope: z24.enum(["project", "global"]).optional().describe("Required only when the same name exists in both scopes")
11827
12753
  }).strict();
11828
12754
  var ManagedToolRecordSchema = RegisterManagedToolInputSchema.extend({
11829
- version: z22.literal(MANAGED_TOOL_VERSION),
11830
- createdAt: z22.string().datetime()
12755
+ version: z24.literal(MANAGED_TOOL_VERSION),
12756
+ createdAt: z24.string().datetime()
11831
12757
  }).strict();
11832
12758
  var AsyncFunction = Object.getPrototypeOf(async function() {
11833
12759
  }).constructor;
@@ -11839,9 +12765,9 @@ var ManagedToolStore = class {
11839
12765
  #entries = /* @__PURE__ */ new Map();
11840
12766
  #diagnostics = [];
11841
12767
  constructor(options) {
11842
- this.#workspace = resolve18(options.workspace);
12768
+ this.#workspace = resolve19(options.workspace);
11843
12769
  this.#projectRoot = join14(this.#workspace, ".flavor", "tools");
11844
- this.#globalRoot = join14(resolve18(options.home), ".flavor-code", "tools");
12770
+ this.#globalRoot = join14(resolve19(options.home), ".flavor-code", "tools");
11845
12771
  this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
11846
12772
  }
11847
12773
  get diagnostics() {
@@ -11902,7 +12828,7 @@ var ManagedToolStore = class {
11902
12828
  if (matches.length === 0) throw new Error(`Managed tool "${input.name}" was not found`);
11903
12829
  if (matches.length > 1) throw new Error(`Managed tool "${input.name}" exists in multiple scopes; specify scope`);
11904
12830
  const entry2 = matches[0];
11905
- const info = await lstat9(entry2.path);
12831
+ const info = await lstat10(entry2.path);
11906
12832
  if (!info.isFile() || info.isSymbolicLink()) throw new Error(`Managed tool path is no longer a regular file: ${entry2.path}`);
11907
12833
  await unlink(entry2.path);
11908
12834
  this.#entries.delete(scopedKey(entry2.scope, entry2.record.name));
@@ -11928,10 +12854,10 @@ var ManagedToolStore = class {
11928
12854
  for (const name of names) {
11929
12855
  const path = join14(root, name);
11930
12856
  try {
11931
- const info = await lstat9(path);
12857
+ const info = await lstat10(path);
11932
12858
  if (!info.isFile() || info.isSymbolicLink()) throw new Error("Managed tool record must be a regular file");
11933
12859
  if (info.size > MAX_RECORD_BYTES) throw new Error(`Managed tool record exceeds ${MAX_RECORD_BYTES} bytes`);
11934
- const record = ManagedToolRecordSchema.parse(JSON.parse(await readFile13(path, "utf8")));
12860
+ const record = ManagedToolRecordSchema.parse(JSON.parse(await readFile14(path, "utf8")));
11935
12861
  if (record.scope !== scope2) throw new Error(`Managed tool record scope must match its ${scope2} directory`);
11936
12862
  const key = scopedKey(scope2, record.name);
11937
12863
  if (this.#entries.has(key)) throw new Error(`Duplicate managed tool name in ${scope2} scope: ${record.name}`);
@@ -12014,7 +12940,7 @@ function createManagedToolManagementTools(options) {
12014
12940
  const listTools = {
12015
12941
  name: "ListRegisteredTools",
12016
12942
  description: "List durable tools created through RegisterTool, including scope, path, and active precedence",
12017
- inputSchema: z22.object({}).strict(),
12943
+ inputSchema: z24.object({}).strict(),
12018
12944
  agents: mainOnly,
12019
12945
  paths: () => [],
12020
12946
  execute: async (_input, signal) => {
@@ -12027,7 +12953,7 @@ function createManagedToolManagementTools(options) {
12027
12953
  function compileDefinition(record, scope2, workspace) {
12028
12954
  let inputSchema;
12029
12955
  try {
12030
- inputSchema = z22.fromJSONSchema(record.inputSchema);
12956
+ inputSchema = z24.fromJSONSchema(record.inputSchema);
12031
12957
  } catch (error) {
12032
12958
  throw new Error(`Invalid input schema for managed tool "${record.name}": ${message(error)}`);
12033
12959
  }
@@ -12081,7 +13007,7 @@ ${sourceUrl}`
12081
13007
  }
12082
13008
  }
12083
13009
  async function writeExclusive(path, content) {
12084
- await mkdir12(dirname13(path), { recursive: true });
13010
+ await mkdir12(dirname14(path), { recursive: true });
12085
13011
  const temporary = `${path}.${randomUUID9()}.tmp`;
12086
13012
  try {
12087
13013
  await writeFile7(temporary, content, { encoding: "utf8", flag: "wx", mode: 384 });
@@ -12127,7 +13053,7 @@ function isMissing4(error) {
12127
13053
  }
12128
13054
 
12129
13055
  // src/hallucination/confidence.ts
12130
- import { z as z23 } from "zod";
13056
+ import { z as z25 } from "zod";
12131
13057
 
12132
13058
  // src/hallucination/types.ts
12133
13059
  var DEFAULT_SLIDING_WINDOW_SIZE = 20;
@@ -12140,12 +13066,12 @@ var DEFAULT_EVALUATION_TIMEOUT_MS = 2e3;
12140
13066
  var MAX_QUERY_CHARS = 5e3;
12141
13067
  var MAX_OUTPUT_CHARS = 1e4;
12142
13068
  var MAX_EVIDENCE_CHARS = 6e3;
12143
- var ConfidenceSchema = z23.object({
12144
- taskAlignment: z23.number(),
12145
- evidenceGrounding: z23.number(),
12146
- processReliability: z23.number(),
12147
- reason: z23.string(),
12148
- unsupportedClaims: z23.array(z23.string()).max(3)
13069
+ var ConfidenceSchema = z25.object({
13070
+ taskAlignment: z25.number(),
13071
+ evidenceGrounding: z25.number(),
13072
+ processReliability: z25.number(),
13073
+ reason: z25.string(),
13074
+ unsupportedClaims: z25.array(z25.string()).max(3)
12149
13075
  });
12150
13076
  var PROMPT = `You are a fast, evidence-aware task-completion verifier.
12151
13077
 
@@ -12472,7 +13398,7 @@ function buildWarningMessages(report, languageTag) {
12472
13398
  }
12473
13399
 
12474
13400
  // src/hallucination/sliding-window.ts
12475
- import { createHash as createHash6 } from "crypto";
13401
+ import { createHash as createHash7 } from "crypto";
12476
13402
  var SlidingWindow = class {
12477
13403
  #windowSize;
12478
13404
  #threshold;
@@ -12488,7 +13414,7 @@ var SlidingWindow = class {
12488
13414
  }
12489
13415
  hash(toolName, params) {
12490
13416
  const sorted = JSON.stringify(params, sortedKeys(params));
12491
- return createHash6("sha256").update(`${toolName}:${sorted}`).digest("hex");
13417
+ return createHash7("sha256").update(`${toolName}:${sorted}`).digest("hex");
12492
13418
  }
12493
13419
  push(toolName, params) {
12494
13420
  const hash = this.hash(toolName, params);
@@ -12843,9 +13769,9 @@ ${total}`;
12843
13769
  }
12844
13770
 
12845
13771
  // src/memory/store.ts
12846
- import { createHash as createHash7 } from "crypto";
12847
- import { readFile as readFile14, rm as rm7 } from "fs/promises";
12848
- import { resolve as resolve19, join as join15, relative as relative16, sep as sep15 } from "path";
13772
+ import { createHash as createHash8 } from "crypto";
13773
+ import { readFile as readFile15, rm as rm7 } from "fs/promises";
13774
+ import { resolve as resolve20, join as join15, relative as relative17, sep as sep16 } from "path";
12849
13775
 
12850
13776
  // src/memory/similarity.ts
12851
13777
  function normalizeForSimilarity(value) {
@@ -12947,7 +13873,7 @@ var MemoryStore = class {
12947
13873
  constructor(options) {
12948
13874
  if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 1) throw new Error("maxEntries must be an integer of at least 1");
12949
13875
  if (!Number.isSafeInteger(options.maxEntryChars) || options.maxEntryChars < 1) throw new Error("maxEntryChars must be an integer of at least 1");
12950
- this.workspace = resolve19(options.workspace);
13876
+ this.workspace = resolve20(options.workspace);
12951
13877
  this.#memoryRoot = join15(this.workspace, ".flavor", "memory");
12952
13878
  this.path = join15(this.#memoryRoot, "MEMORY.md");
12953
13879
  this.#behaviorPath = join15(this.#memoryRoot, "behavior.json");
@@ -13168,14 +14094,14 @@ var MemoryStore = class {
13168
14094
  });
13169
14095
  }
13170
14096
  async #readTaskItem(reference) {
13171
- const document = decodeTask(await readFile14(this.#resolveContentPath(reference.contentPath), "utf8"));
14097
+ const document = decodeTask(await readFile15(this.#resolveContentPath(reference.contentPath), "utf8"));
13172
14098
  return document.items.find((item) => item.id === reference.id)?.content ?? reference.summary;
13173
14099
  }
13174
14100
  #resolveContentPath(contentPath) {
13175
14101
  if (!/^tasks\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(contentPath)) throw new Error("Invalid memory content path");
13176
- const path = resolve19(this.#memoryRoot, ...contentPath.split("/"));
13177
- const rel = relative16(this.#memoryRoot, path);
13178
- if (rel === ".." || rel.startsWith(`..${sep15}`)) throw new Error("Memory content path escapes the memory directory");
14102
+ const path = resolve20(this.#memoryRoot, ...contentPath.split("/"));
14103
+ const rel = relative17(this.#memoryRoot, path);
14104
+ if (rel === ".." || rel.startsWith(`..${sep16}`)) throw new Error("Memory content path escapes the memory directory");
13179
14105
  return path;
13180
14106
  }
13181
14107
  async #updateIndex(update) {
@@ -13329,7 +14255,7 @@ function sanitizeTopicKey(value) {
13329
14255
  return value.normalize("NFKC").toLocaleLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 128);
13330
14256
  }
13331
14257
  function memoryId(type, content) {
13332
- return createHash7("sha256").update(`${type}\0${normalizeMemoryContent(content).toLocaleLowerCase()}`, "utf8").digest("hex").slice(0, 12);
14258
+ return createHash8("sha256").update(`${type}\0${normalizeMemoryContent(content).toLocaleLowerCase()}`, "utf8").digest("hex").slice(0, 12);
13333
14259
  }
13334
14260
  function assertTaskId(taskId) {
13335
14261
  if (!TASK_ID.test(taskId)) throw new Error(`Invalid memory task id: ${taskId}`);
@@ -13644,8 +14570,8 @@ function createExecutionEnvironment(workspace, config) {
13644
14570
  }
13645
14571
 
13646
14572
  // src/ide/client.ts
13647
- import { readFile as readFile15, readdir as readdir9, stat as stat6 } from "fs/promises";
13648
- import { basename as basename5, join as join16, relative as relative17, resolve as resolve20, sep as sep16 } from "path";
14573
+ import { readFile as readFile16, readdir as readdir9, stat as stat6 } from "fs/promises";
14574
+ import { basename as basename5, join as join16, relative as relative18, resolve as resolve21, sep as sep17 } from "path";
13649
14575
  var FlavorIdeClient = class {
13650
14576
  #workspace;
13651
14577
  #home;
@@ -13656,8 +14582,8 @@ var FlavorIdeClient = class {
13656
14582
  #sessionId;
13657
14583
  #eventTail = Promise.resolve();
13658
14584
  constructor(options) {
13659
- this.#workspace = resolve20(options.workspace);
13660
- this.#home = resolve20(options.home);
14585
+ this.#workspace = resolve21(options.workspace);
14586
+ this.#home = resolve21(options.home);
13661
14587
  this.#environment = options.environment ?? process.env;
13662
14588
  this.#fetch = options.fetch ?? fetch;
13663
14589
  }
@@ -13751,7 +14677,7 @@ var FlavorIdeClient = class {
13751
14677
  const candidates = (await Promise.all(entries.map(async (name) => {
13752
14678
  const path = join16(directory, name);
13753
14679
  try {
13754
- const [contents, info] = await Promise.all([readFile15(path, "utf8"), stat6(path)]);
14680
+ const [contents, info] = await Promise.all([readFile16(path, "utf8"), stat6(path)]);
13755
14681
  const lock = parseLockfile(contents);
13756
14682
  return lock === void 0 ? void 0 : { lock, modified: info.mtimeMs };
13757
14683
  } catch {
@@ -13855,19 +14781,385 @@ function parsePort(value) {
13855
14781
  return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
13856
14782
  }
13857
14783
  function isWithin7(parent, child) {
13858
- const base = normalize(resolve20(parent));
13859
- const target = normalize(resolve20(child));
13860
- return target === base || target.startsWith(`${base}${sep16}`);
14784
+ const base = normalize(resolve21(parent));
14785
+ const target = normalize(resolve21(child));
14786
+ return target === base || target.startsWith(`${base}${sep17}`);
13861
14787
  }
13862
14788
  function normalize(path) {
13863
14789
  const normalized = path.normalize("NFC");
13864
14790
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
13865
14791
  }
13866
14792
  function relativeDisplay(workspace, filePath) {
13867
- const path = relative17(workspace, filePath).replaceAll("\\", "/");
14793
+ const path = relative18(workspace, filePath).replaceAll("\\", "/");
13868
14794
  return path && !path.startsWith("../") ? path : basename5(filePath);
13869
14795
  }
13870
14796
 
14797
+ // src/jobs/registry.ts
14798
+ import { randomUUID as randomUUID10 } from "crypto";
14799
+ var JobRegistry = class {
14800
+ #jobs = /* @__PURE__ */ new Map();
14801
+ #maxOutputChars;
14802
+ #maxJobs;
14803
+ #listeners = /* @__PURE__ */ new Set();
14804
+ constructor(options = {}) {
14805
+ this.#maxOutputChars = options.maxOutputChars ?? 2e5;
14806
+ this.#maxJobs = options.maxJobs ?? 100;
14807
+ if (!Number.isSafeInteger(this.#maxOutputChars) || this.#maxOutputChars <= 0) throw new Error("maxOutputChars must be positive");
14808
+ if (!Number.isSafeInteger(this.#maxJobs) || this.#maxJobs <= 0) throw new Error("maxJobs must be positive");
14809
+ }
14810
+ create(input) {
14811
+ this.#pruneCompleted();
14812
+ const id = `job-${randomUUID10().slice(0, 12)}`;
14813
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14814
+ let settle;
14815
+ const done = new Promise((resolve25) => {
14816
+ settle = resolve25;
14817
+ });
14818
+ const record = {
14819
+ id,
14820
+ ...input,
14821
+ state: "running",
14822
+ createdAt: now,
14823
+ updatedAt: now,
14824
+ outputChars: 0,
14825
+ truncated: false,
14826
+ output: "",
14827
+ outputStart: 0,
14828
+ settle,
14829
+ done
14830
+ };
14831
+ this.#jobs.set(id, record);
14832
+ this.#publish();
14833
+ return {
14834
+ id,
14835
+ append: (text) => {
14836
+ if (record.state !== "running" || text === "") return;
14837
+ record.output += text;
14838
+ record.outputChars += text.length;
14839
+ if (record.output.length > this.#maxOutputChars) {
14840
+ const removed = record.output.length - this.#maxOutputChars;
14841
+ record.output = record.output.slice(removed);
14842
+ record.outputStart += removed;
14843
+ record.truncated = true;
14844
+ }
14845
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
14846
+ this.#publish();
14847
+ },
14848
+ complete: (result = {}) => this.#finish(
14849
+ record,
14850
+ result.exitCode !== void 0 && result.exitCode !== null && result.exitCode !== 0 ? "failed" : "completed",
14851
+ result
14852
+ ),
14853
+ fail: (error) => this.#finish(record, "failed", { error: error instanceof Error ? error.message : String(error) }),
14854
+ cancel: () => this.#finish(record, "cancelled", {})
14855
+ };
14856
+ }
14857
+ list(owner2) {
14858
+ return [...this.#jobs.values()].filter((job) => owner2 === void 0 || job.owner === owner2).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).map(snapshot2);
14859
+ }
14860
+ read(id, owner2, cursor = 0) {
14861
+ const job = this.#owned(id, owner2);
14862
+ const effective = Math.max(cursor, job.outputStart);
14863
+ return { ...snapshot2(job), output: job.output.slice(effective - job.outputStart), cursor: job.outputStart + job.output.length };
14864
+ }
14865
+ async wait(id, owner2, signal) {
14866
+ const job = this.#owned(id, owner2);
14867
+ if (job.state !== "running") return snapshot2(job);
14868
+ if (signal === void 0) return job.done;
14869
+ return Promise.race([job.done, new Promise((_resolve, reject) => {
14870
+ if (signal.aborted) {
14871
+ reject(signal.reason);
14872
+ return;
14873
+ }
14874
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
14875
+ })]);
14876
+ }
14877
+ kill(id, owner2) {
14878
+ const job = this.#owned(id, owner2);
14879
+ if (job.state !== "running") return;
14880
+ void job.cancel?.();
14881
+ this.#finish(job, "cancelled", {});
14882
+ }
14883
+ subscribe(listener) {
14884
+ this.#listeners.add(listener);
14885
+ return () => this.#listeners.delete(listener);
14886
+ }
14887
+ async dispose() {
14888
+ const running = [...this.#jobs.values()].filter((job) => job.state === "running");
14889
+ await Promise.allSettled(running.map(async (job) => job.cancel?.()));
14890
+ for (const job of running) this.#finish(job, "cancelled", {});
14891
+ }
14892
+ #owned(id, owner2) {
14893
+ const job = this.#jobs.get(id);
14894
+ if (job === void 0) throw new Error(`Unknown job: ${id}`);
14895
+ if (job.owner !== owner2) throw new Error("Job owner mismatch");
14896
+ return job;
14897
+ }
14898
+ #finish(record, state, detail) {
14899
+ if (record.state !== "running") return;
14900
+ record.state = state;
14901
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
14902
+ if (detail.exitCode !== void 0) record.exitCode = detail.exitCode;
14903
+ if (detail.error !== void 0) record.error = detail.error;
14904
+ const value = snapshot2(record);
14905
+ record.settle(value);
14906
+ this.#publish();
14907
+ }
14908
+ #publish() {
14909
+ const value = this.list();
14910
+ for (const listener of this.#listeners) listener(value);
14911
+ }
14912
+ #pruneCompleted() {
14913
+ if (this.#jobs.size < this.#maxJobs) return;
14914
+ const removable = [...this.#jobs.values()].filter((job) => job.state !== "running").sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
14915
+ for (const job of removable) {
14916
+ if (this.#jobs.size < this.#maxJobs) break;
14917
+ this.#jobs.delete(job.id);
14918
+ }
14919
+ }
14920
+ };
14921
+ function snapshot2(job) {
14922
+ return {
14923
+ id: job.id,
14924
+ kind: job.kind,
14925
+ owner: job.owner,
14926
+ label: job.label,
14927
+ state: job.state,
14928
+ createdAt: job.createdAt,
14929
+ updatedAt: job.updatedAt,
14930
+ outputChars: job.outputChars,
14931
+ truncated: job.truncated,
14932
+ ...job.exitCode === void 0 ? {} : { exitCode: job.exitCode },
14933
+ ...job.error === void 0 ? {} : { error: job.error }
14934
+ };
14935
+ }
14936
+
14937
+ // src/terminal/service.ts
14938
+ import { randomUUID as randomUUID11 } from "crypto";
14939
+ import { createRequire as nodeCreateRequire } from "module";
14940
+ import { isAbsolute as isAbsolute11, relative as relative19, resolve as resolve22, sep as sep18 } from "path";
14941
+ var TerminalService = class {
14942
+ #root;
14943
+ #factory;
14944
+ #jobs;
14945
+ #maxOutputChars;
14946
+ #sessions = /* @__PURE__ */ new Map();
14947
+ constructor(workspace, options = {}) {
14948
+ this.#root = resolve22(workspace);
14949
+ this.#factory = options.factory ?? defaultFactory;
14950
+ this.#jobs = options.jobs;
14951
+ this.#maxOutputChars = options.maxOutputChars ?? 2e5;
14952
+ }
14953
+ open(input) {
14954
+ const cwd = workspacePath(this.#root, input.cwd ?? ".");
14955
+ const shell = input.shell ?? defaultShell();
14956
+ const id = `term-${randomUUID11().slice(0, 12)}`;
14957
+ const pty = this.#factory(shell, input.args ?? [], {
14958
+ cwd,
14959
+ cols: input.columns ?? 100,
14960
+ rows: input.rows ?? 30,
14961
+ env: cleanEnvironment(process.env)
14962
+ });
14963
+ let jobId;
14964
+ const record = {
14965
+ id,
14966
+ owner: input.owner,
14967
+ shell,
14968
+ cwd,
14969
+ state: "running",
14970
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
14971
+ pty,
14972
+ output: "",
14973
+ outputStart: 0,
14974
+ outputChars: 0,
14975
+ truncated: false,
14976
+ disposables: []
14977
+ };
14978
+ const job = this.#jobs?.create({ kind: "terminal", owner: input.owner, label: shell, cancel: () => this.close(id, input.owner) });
14979
+ if (job !== void 0) {
14980
+ jobId = job.id;
14981
+ record.jobId = job.id;
14982
+ record.job = job;
14983
+ }
14984
+ record.disposables.push(pty.onData((data2) => {
14985
+ record.output += data2;
14986
+ record.outputChars += data2.length;
14987
+ if (record.output.length > this.#maxOutputChars) {
14988
+ const removed = record.output.length - this.#maxOutputChars;
14989
+ record.output = record.output.slice(removed);
14990
+ record.outputStart += removed;
14991
+ record.truncated = true;
14992
+ }
14993
+ job?.append(data2);
14994
+ }));
14995
+ record.disposables.push(pty.onExit(({ exitCode }) => {
14996
+ if (record.state === "running") record.state = "exited";
14997
+ record.exitCode = exitCode;
14998
+ job?.complete({ exitCode });
14999
+ }));
15000
+ this.#sessions.set(id, record);
15001
+ return { ...snapshot3(record), ...jobId === void 0 ? {} : { jobId } };
15002
+ }
15003
+ write(id, owner2, data2) {
15004
+ this.#owned(id, owner2).pty.write(data2);
15005
+ }
15006
+ resize(id, owner2, columns, rows) {
15007
+ this.#owned(id, owner2).pty.resize(columns, rows);
15008
+ }
15009
+ read(id, owner2, cursor = 0) {
15010
+ const record = this.#owned(id, owner2);
15011
+ const effective = Math.max(cursor, record.outputStart);
15012
+ return { ...snapshot3(record), output: record.output.slice(effective - record.outputStart), cursor: record.outputStart + record.output.length, truncated: record.truncated };
15013
+ }
15014
+ list(owner2) {
15015
+ return [...this.#sessions.values()].filter((item) => item.owner === owner2).map(snapshot3);
15016
+ }
15017
+ close(id, owner2) {
15018
+ const record = this.#owned(id, owner2);
15019
+ if (record.state === "running") record.pty.kill();
15020
+ record.state = "closed";
15021
+ record.job?.cancel();
15022
+ for (const disposable of record.disposables) disposable.dispose();
15023
+ }
15024
+ dispose() {
15025
+ for (const record of this.#sessions.values()) {
15026
+ if (record.state === "running") record.pty.kill();
15027
+ for (const disposable of record.disposables) disposable.dispose();
15028
+ record.state = "closed";
15029
+ record.job?.cancel();
15030
+ }
15031
+ }
15032
+ #owned(id, owner2) {
15033
+ const record = this.#sessions.get(id);
15034
+ if (record === void 0) throw new Error(`Unknown terminal: ${id}`);
15035
+ if (record.owner !== owner2) throw new Error("Terminal owner mismatch");
15036
+ return record;
15037
+ }
15038
+ };
15039
+ function snapshot3(record) {
15040
+ return {
15041
+ id: record.id,
15042
+ owner: record.owner,
15043
+ shell: record.shell,
15044
+ cwd: record.cwd,
15045
+ state: record.state,
15046
+ createdAt: record.createdAt,
15047
+ ...record.exitCode === void 0 ? {} : { exitCode: record.exitCode },
15048
+ ...record.jobId === void 0 ? {} : { jobId: record.jobId }
15049
+ };
15050
+ }
15051
+ function defaultFactory(shell, args, options) {
15052
+ try {
15053
+ const pty = nodeCreateRequire(import.meta.url)("node-pty");
15054
+ return pty.spawn(shell, args, { ...options, name: "xterm-256color" });
15055
+ } catch (error) {
15056
+ throw new Error(`Pseudo-terminal backend is unavailable: ${error instanceof Error ? error.message : String(error)}`);
15057
+ }
15058
+ }
15059
+ function defaultShell() {
15060
+ return process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : process.env.SHELL ?? "/bin/sh";
15061
+ }
15062
+ function cleanEnvironment(environment) {
15063
+ return Object.fromEntries(Object.entries(environment).filter((entry2) => entry2[1] !== void 0));
15064
+ }
15065
+ function workspacePath(root, input) {
15066
+ const path = resolve22(root, input);
15067
+ const delta = relative19(root, path);
15068
+ if (delta === ".." || delta.startsWith(`..${sep18}`) || isAbsolute11(delta)) throw new Error("Terminal cwd is outside the workspace");
15069
+ return path;
15070
+ }
15071
+
15072
+ // src/tools/terminal.ts
15073
+ import { z as z26 } from "zod";
15074
+ import { resolve as resolve23 } from "path";
15075
+ var Id = z26.string().regex(/^term-[a-f0-9-]+$/);
15076
+ var OpenInput = z26.object({ cwd: z26.string().optional(), shell: z26.string().optional(), args: z26.array(z26.string()).optional(), columns: z26.coerce.number().int().min(10).max(500).optional(), rows: z26.coerce.number().int().min(2).max(300).optional() });
15077
+ var WriteInput2 = z26.object({ id: Id, data: z26.string().max(1e5), enter: z26.boolean().optional() });
15078
+ var ReadInput3 = z26.object({ id: Id, cursor: z26.coerce.number().int().min(0).optional() });
15079
+ var ResizeInput = z26.object({ id: Id, columns: z26.coerce.number().int().min(10).max(500), rows: z26.coerce.number().int().min(2).max(300) });
15080
+ var CloseInput = z26.object({ id: Id });
15081
+ function createTerminalTools(terminals, workspace = process.cwd()) {
15082
+ const tools = [
15083
+ {
15084
+ name: "TerminalOpen",
15085
+ description: "Open a persistent interactive pseudo-terminal in the workspace",
15086
+ inputSchema: OpenInput,
15087
+ paths: (input) => [resolve23(workspace, input.cwd ?? ".")],
15088
+ permissions: (input) => ({ paths: [resolve23(workspace, input.cwd ?? ".")], command: input.shell ?? "system-shell", args: input.args ?? [], cwd: resolve23(workspace, input.cwd ?? ".") }),
15089
+ execute: async (input, _signal, context) => terminals.open({
15090
+ owner: owner(context),
15091
+ ...input.cwd === void 0 ? {} : { cwd: input.cwd },
15092
+ ...input.shell === void 0 ? {} : { shell: input.shell },
15093
+ ...input.args === void 0 ? {} : { args: input.args },
15094
+ ...input.columns === void 0 ? {} : { columns: input.columns },
15095
+ ...input.rows === void 0 ? {} : { rows: input.rows }
15096
+ }),
15097
+ presentResult: (output) => ({ kind: "terminal", variant: "terminal", title: `Terminal ${output.id}`, state: "running" })
15098
+ },
15099
+ {
15100
+ name: "TerminalWrite",
15101
+ description: "Write text or a command to a persistent terminal",
15102
+ inputSchema: WriteInput2,
15103
+ paths: () => [],
15104
+ permissions: (input) => ({ paths: [], command: input.data, args: [] }),
15105
+ execute: async (input, _signal, context) => {
15106
+ terminals.write(input.id, owner(context), input.data + (input.enter ? "\r" : ""));
15107
+ return { id: input.id, written: input.data.length + (input.enter ? 1 : 0) };
15108
+ }
15109
+ },
15110
+ {
15111
+ name: "TerminalRead",
15112
+ description: "Read output produced by a persistent terminal since a cursor",
15113
+ inputSchema: ReadInput3,
15114
+ paths: () => [],
15115
+ execute: async (input, _signal, context) => terminals.read(input.id, owner(context), input.cursor ?? 0),
15116
+ renderForModel: (output) => output.output,
15117
+ presentResult: (output) => {
15118
+ const result = output;
15119
+ return {
15120
+ kind: "terminal",
15121
+ variant: "terminal",
15122
+ title: `Terminal ${result.id}`,
15123
+ stdout: result.output,
15124
+ truncated: result.truncated,
15125
+ ...result.exitCode === void 0 ? { state: result.state === "closed" ? "cancelled" : "running" } : {
15126
+ exitCode: result.exitCode,
15127
+ state: result.exitCode === 0 ? "completed" : "failed"
15128
+ }
15129
+ };
15130
+ }
15131
+ },
15132
+ {
15133
+ name: "TerminalResize",
15134
+ description: "Resize a persistent terminal",
15135
+ inputSchema: ResizeInput,
15136
+ paths: () => [],
15137
+ execute: async (input, _signal, context) => {
15138
+ terminals.resize(input.id, owner(context), input.columns, input.rows);
15139
+ return { id: input.id, columns: input.columns, rows: input.rows };
15140
+ }
15141
+ },
15142
+ {
15143
+ name: "TerminalClose",
15144
+ description: "Close a persistent terminal",
15145
+ inputSchema: CloseInput,
15146
+ paths: () => [],
15147
+ execute: async (input, _signal, context) => {
15148
+ terminals.close(input.id, owner(context));
15149
+ return { id: input.id, closed: true };
15150
+ }
15151
+ },
15152
+ {
15153
+ name: "TerminalList",
15154
+ description: "List persistent terminals owned by this agent",
15155
+ inputSchema: z26.object({}),
15156
+ paths: () => [],
15157
+ execute: async (_input, _signal, context) => terminals.list(owner(context))
15158
+ }
15159
+ ];
15160
+ return tools;
15161
+ }
15162
+
13871
15163
  // src/production.ts
13872
15164
  var INTERRUPTED_TASK_PLAN_CONTEXT = [
13873
15165
  "The previous turn's task plan was cancelled and archived, so it is no longer active.",
@@ -13898,7 +15190,7 @@ var ApprovalBridge = class {
13898
15190
  request(request, signal = new AbortController().signal) {
13899
15191
  if (this.#settle !== void 0) return Promise.resolve("deny");
13900
15192
  if (signal.aborted) return Promise.resolve("deny");
13901
- this.#pending = { id: randomUUID10(), ...request };
15193
+ this.#pending = { id: randomUUID12(), ...request };
13902
15194
  this.#onChange?.();
13903
15195
  return new Promise((resolvePromise) => {
13904
15196
  this.#settle = resolvePromise;
@@ -13919,8 +15211,8 @@ var ApprovalBridge = class {
13919
15211
  }
13920
15212
  };
13921
15213
  async function createProductionRuntime(options) {
13922
- const workspace = resolve21(options.workspace ?? process.cwd());
13923
- const home = resolve21(options.home ?? homedir());
15214
+ const workspace = resolve24(options.workspace ?? process.cwd());
15215
+ const home = resolve24(options.home ?? homedir());
13924
15216
  const environment = options.environment ?? process.env;
13925
15217
  const ide = new FlavorIdeClient({ workspace, home, environment });
13926
15218
  const ideReady = ide.initialize();
@@ -14053,17 +15345,29 @@ async function createProductionRuntime(options) {
14053
15345
  return questions.ask(qs, signal);
14054
15346
  };
14055
15347
  const executionEnvironment = createExecutionEnvironment(workspace, config.execution);
14056
- const fileMutationOptions = options.beforeFileCommit === void 0 ? {} : { beforeCommit: options.beforeFileCommit };
15348
+ const jobs = new JobRegistry();
15349
+ const terminals = new TerminalService(workspace, { jobs });
15350
+ const observations = new FileObservationStore();
15351
+ const workspaceInstructions = new WorkspaceInstructions(workspace);
15352
+ const fileMutationOptions = {
15353
+ observations,
15354
+ ...options.beforeFileCommit === void 0 ? {} : { beforeCommit: options.beforeFileCommit }
15355
+ };
14057
15356
  const tools = [
14058
- createReadTool(workspace),
15357
+ createReadTool(workspace, { observations }),
14059
15358
  createWriteTool(workspace, fileMutationOptions),
14060
15359
  createEditTool(workspace, fileMutationOptions),
14061
15360
  createApplyPatchTool(workspace, fileMutationOptions),
14062
15361
  createGlobTool(workspace),
14063
15362
  createGrepTool(workspace),
14064
15363
  createShellTool(workspace, {
15364
+ jobs,
14065
15365
  ...executionEnvironment === void 0 ? {} : { executionEnvironment }
14066
15366
  }),
15367
+ createWebFetchTool(),
15368
+ createWebSearchTool(),
15369
+ ...createJobTools(jobs),
15370
+ ...createTerminalTools(terminals, workspace),
14067
15371
  ...createLspTools(workspace, {
14068
15372
  onStatus: (message2) => emitOutput({ type: "notice", message: message2 })
14069
15373
  }),
@@ -14195,7 +15499,9 @@ async function createProductionRuntime(options) {
14195
15499
  const skillsReady = skills.discover();
14196
15500
  skillsReady.catch(() => void 0);
14197
15501
  tools.push(createSkillResourceTool(skills));
15502
+ if (options.extraTools !== void 0) tools.push(...options.extraTools);
14198
15503
  const flavor = await optionalText2(join17(workspace, "FLAVOR.md"));
15504
+ const instructionBaseline = await workspaceInstructions.baseline();
14199
15505
  let memoryContext;
14200
15506
  if (memoryStore !== void 0) {
14201
15507
  try {
@@ -14218,7 +15524,7 @@ async function createProductionRuntime(options) {
14218
15524
  let taskResults = { ...recovered?.tasks.results ?? {} };
14219
15525
  const subagentStartedAt = {};
14220
15526
  const subagentElapsedMs = {};
14221
- let sessionId = recovered?.sessionId ?? `session-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID10().slice(0, 8)}`;
15527
+ let sessionId = recovered?.sessionId ?? `session-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID12().slice(0, 8)}`;
14222
15528
  setUsageSession(sessionId);
14223
15529
  ideSessionId = sessionId;
14224
15530
  await ideReady;
@@ -14388,6 +15694,7 @@ async function createProductionRuntime(options) {
14388
15694
  })
14389
15695
  ],
14390
15696
  ...flavor === void 0 ? {} : { flavor },
15697
+ ...instructionBaseline === "" ? {} : { workspaceInstructions: instructionBaseline },
14391
15698
  ...memoryContext === void 0 ? {} : { memory: memoryContext },
14392
15699
  ...taskState === void 0 ? {} : { taskState },
14393
15700
  userMemory: () => userMemoryContext ?? "",
@@ -14425,6 +15732,7 @@ async function createProductionRuntime(options) {
14425
15732
  maxIterationsMain: config.maxIterations.main,
14426
15733
  maxIterationsSubagent: config.maxIterations.subagent,
14427
15734
  hasActiveProgress,
15735
+ afterToolSuccess: async (_tool, paths, _input, _output, context) => workspaceInstructions.discover(paths, context.ownerId ?? context.agent),
14428
15736
  approve: resolveToolApproval
14429
15737
  });
14430
15738
  harnessCreated = true;
@@ -14525,7 +15833,7 @@ async function createProductionRuntime(options) {
14525
15833
  const prompt = String(event.payload.prompt);
14526
15834
  const reassessInterruptedPlan = interruptedTaskPlanNeedsReassessment && !prompt.startsWith("/");
14527
15835
  timelineState = transcriptReducer(timelineState, { type: "submit", prompt });
14528
- automaticMemoryTask = !prompt.startsWith("/");
15836
+ automaticMemoryTask = !prompt.startsWith("/") && harness.permissionProfile !== "d2c";
14529
15837
  if (automaticMemoryTask) memoryReviews.dismissAll();
14530
15838
  if (!prompt.startsWith("/") && memoryLifecycle.status === "completed") {
14531
15839
  memoryLifecycle = {
@@ -14650,16 +15958,23 @@ async function createProductionRuntime(options) {
14650
15958
  return;
14651
15959
  }
14652
15960
  const loopExecutionEnvironment = createExecutionEnvironment(input.workspace, config.execution);
15961
+ const loopObservations = new FileObservationStore();
15962
+ const loopInstructions = new WorkspaceInstructions(input.workspace);
15963
+ const loopMutationOptions = { observations: loopObservations };
14653
15964
  const loopTools = [
14654
- createReadTool(input.workspace),
14655
- createWriteTool(input.workspace),
14656
- createEditTool(input.workspace),
14657
- createApplyPatchTool(input.workspace),
15965
+ createReadTool(input.workspace, { observations: loopObservations }),
15966
+ createWriteTool(input.workspace, loopMutationOptions),
15967
+ createEditTool(input.workspace, loopMutationOptions),
15968
+ createApplyPatchTool(input.workspace, loopMutationOptions),
14658
15969
  createGlobTool(input.workspace),
14659
15970
  createGrepTool(input.workspace),
14660
15971
  createShellTool(input.workspace, {
15972
+ jobs,
14661
15973
  ...loopExecutionEnvironment === void 0 ? {} : { executionEnvironment: loopExecutionEnvironment }
14662
15974
  }),
15975
+ createWebFetchTool(),
15976
+ createWebSearchTool(),
15977
+ ...createJobTools(jobs),
14663
15978
  ...createLspTools(input.workspace, {
14664
15979
  onStatus: (status) => emitOutput({ type: "notice", message: status })
14665
15980
  }),
@@ -14667,6 +15982,7 @@ async function createProductionRuntime(options) {
14667
15982
  ...mcpTools
14668
15983
  ];
14669
15984
  const loopFlavor = await optionalText2(join17(input.workspace, "FLAVOR.md"));
15985
+ const loopInstructionBaseline = await loopInstructions.baseline();
14670
15986
  const loopEnvironment = createPromptEnvironment({
14671
15987
  now: /* @__PURE__ */ new Date(),
14672
15988
  platform: process.platform,
@@ -14696,6 +16012,7 @@ async function createProductionRuntime(options) {
14696
16012
  })
14697
16013
  ],
14698
16014
  ...loopFlavor === void 0 ? {} : { flavor: loopFlavor },
16015
+ ...loopInstructionBaseline === "" ? {} : { workspaceInstructions: loopInstructionBaseline },
14699
16016
  ...memoryContext === void 0 ? {} : { memory: memoryContext },
14700
16017
  userMemory: () => userMemoryContext ?? "",
14701
16018
  ...compactAtChars === void 0 ? {} : { compactAtChars },
@@ -14728,6 +16045,7 @@ async function createProductionRuntime(options) {
14728
16045
  maxIterationsMain: config.maxIterations.main,
14729
16046
  maxIterationsSubagent: config.maxIterations.subagent,
14730
16047
  loopMode: true,
16048
+ afterToolSuccess: async (_tool, paths, _input, _output, context) => loopInstructions.discover(paths, context.ownerId ?? context.agent),
14731
16049
  approve: resolveToolApproval
14732
16050
  });
14733
16051
  try {
@@ -14779,7 +16097,7 @@ async function createProductionRuntime(options) {
14779
16097
  return answers[0] === "Continue" ? "approved" : "rejected";
14780
16098
  },
14781
16099
  fingerprint: workspaceFingerprint,
14782
- idFactory: () => `loop-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID10().slice(0, 8)}`
16100
+ idFactory: () => `loop-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID12().slice(0, 8)}`
14783
16101
  });
14784
16102
  const goalStore = new GoalStore({ workspace });
14785
16103
  const goalOrchestrator = new GoalOrchestrator({
@@ -14792,13 +16110,13 @@ async function createProductionRuntime(options) {
14792
16110
  maxStallStreak: 2,
14793
16111
  persistence: goalStore,
14794
16112
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
14795
- idFactory: () => `goal-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID10().slice(0, 8)}`,
16113
+ idFactory: () => `goal-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID12().slice(0, 8)}`,
14796
16114
  runWorker: ({ workspace: goalWorkspace, prompt, signal }) => runLoopWorker({ workspace: goalWorkspace, prompt, signal })
14797
16115
  });
14798
16116
  let sessionHistory = await SessionHistory.open({
14799
16117
  workspace,
14800
16118
  sessionId,
14801
- restoreContext: (snapshot2) => harness.main.context.restore(snapshot2)
16119
+ restoreContext: (snapshot4) => harness.main.context.restore(snapshot4)
14802
16120
  });
14803
16121
  const services = {
14804
16122
  hooks,
@@ -14910,7 +16228,7 @@ async function createProductionRuntime(options) {
14910
16228
  tasks: () => ({ plan: taskPlan, graph: taskGraph, states: taskStates, results: taskResults }),
14911
16229
  audit: async (toolFilter) => {
14912
16230
  try {
14913
- const raw = await readFile16(auditLogger.path, "utf8");
16231
+ const raw = await readFile17(auditLogger.path, "utf8");
14914
16232
  const lines = raw.trim().split("\n").filter((line) => line.length > 0);
14915
16233
  const entries = lines.map((line) => {
14916
16234
  try {
@@ -14949,7 +16267,7 @@ ${summary2}`;
14949
16267
  },
14950
16268
  usage: async () => {
14951
16269
  try {
14952
- const raw = await readFile16(usageLogPath(), "utf8");
16270
+ const raw = await readFile17(usageLogPath(), "utf8");
14953
16271
  return formatUsageSummary(summarizeUsage(parseUsageEntries(raw)));
14954
16272
  } catch (error) {
14955
16273
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
@@ -14977,7 +16295,7 @@ ${summary2}`;
14977
16295
  for (const key of Object.keys(subagentStartedAt)) delete subagentStartedAt[key];
14978
16296
  for (const key of Object.keys(subagentElapsedMs)) delete subagentElapsedMs[key];
14979
16297
  const previousIdeSessionId = ideSessionId;
14980
- sessionId = `session-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID10().slice(0, 8)}`;
16298
+ sessionId = `session-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID12().slice(0, 8)}`;
14981
16299
  setUsageSession(sessionId);
14982
16300
  ideSessionId = sessionId;
14983
16301
  if (previousIdeSessionId !== void 0) await ide.endSession(previousIdeSessionId);
@@ -14986,7 +16304,7 @@ ${summary2}`;
14986
16304
  sessionHistory = await SessionHistory.open({
14987
16305
  workspace,
14988
16306
  sessionId,
14989
- restoreContext: (snapshot2) => harness.main.context.restore(snapshot2)
16307
+ restoreContext: (snapshot4) => harness.main.context.restore(snapshot4)
14990
16308
  });
14991
16309
  memoryLifecycle = { status: "active", taskId: createMemoryTaskId(), messageStart: 0 };
14992
16310
  timelineState = createTranscriptState();
@@ -15135,13 +16453,19 @@ ${renderMemoryDocument(entries)}`;
15135
16453
  sleepScheduler.start();
15136
16454
  }
15137
16455
  const session = new FlavorSession(services);
16456
+ const authorization = {
16457
+ permissionProfile: () => harness.permissionProfile,
16458
+ setPermissionProfile: (profile) => harness.setPermissionProfile(profile)
16459
+ };
15138
16460
  let disposed = false;
15139
16461
  return {
15140
16462
  session,
15141
16463
  services,
16464
+ authorization,
15142
16465
  approvals,
15143
16466
  memoryReviews,
15144
16467
  restoredTranscript,
16468
+ jobs: { list: () => jobs.list(), subscribe: (listener) => jobs.subscribe(listener) },
15145
16469
  get sessionId() {
15146
16470
  return sessionId;
15147
16471
  },
@@ -15158,6 +16482,8 @@ ${renderMemoryDocument(entries)}`;
15158
16482
  await persistTail;
15159
16483
  if (ideSessionId !== void 0) await ide.endSession(ideSessionId);
15160
16484
  await executionEnvironment?.dispose();
16485
+ terminals.dispose();
16486
+ await jobs.dispose();
15161
16487
  auditLogger.close();
15162
16488
  memoryReviews.dispose();
15163
16489
  await cleanupProduction(approvals, questions, pluginHost, mcpManager, harness);
@@ -15170,6 +16496,8 @@ ${renderMemoryDocument(entries)}`;
15170
16496
  if (ideSessionId !== void 0) await ide.endSession(ideSessionId);
15171
16497
  await sleepScheduler?.dispose();
15172
16498
  await executionEnvironment?.dispose();
16499
+ terminals.dispose();
16500
+ await jobs.dispose();
15173
16501
  await cleanupProduction(approvals, questions, pluginHost, mcpManager, harnessCreated ? harness : void 0);
15174
16502
  } catch (cleanupError) {
15175
16503
  attachCleanupError(primaryError, cleanupError);
@@ -15284,7 +16612,7 @@ function terminalProgressState(status) {
15284
16612
  return "failed";
15285
16613
  }
15286
16614
  async function workspaceFingerprint(workspace) {
15287
- const hash = createHash8("sha256");
16615
+ const hash = createHash9("sha256");
15288
16616
  const diff = await execFileNoThrow(
15289
16617
  "git",
15290
16618
  ["-C", workspace, "diff", "--no-ext-diff", "--binary", "HEAD"],
@@ -15300,13 +16628,13 @@ async function workspaceFingerprint(workspace) {
15300
16628
  let remaining = 5 * 1024 * 1024;
15301
16629
  for (const name of untracked.stdout.split("\0").filter(Boolean).sort()) {
15302
16630
  if (name === ".flavor/loops" || name.startsWith(".flavor/loops/")) continue;
15303
- const path = resolve21(workspace, name);
15304
- const relativePath = relative18(resolve21(workspace), path);
15305
- if (relativePath === ".." || relativePath.startsWith(`..${sep17}`)) continue;
16631
+ const path = resolve24(workspace, name);
16632
+ const relativePath = relative20(resolve24(workspace), path);
16633
+ if (relativePath === ".." || relativePath.startsWith(`..${sep19}`)) continue;
15306
16634
  hash.update(name);
15307
16635
  if (remaining <= 0) continue;
15308
16636
  try {
15309
- const content = await readFile16(path);
16637
+ const content = await readFile17(path);
15310
16638
  const slice = content.subarray(0, remaining);
15311
16639
  hash.update(slice);
15312
16640
  remaining -= slice.length;
@@ -15698,7 +17026,7 @@ async function generateSleepReview(registry, modelId, prompt, signal) {
15698
17026
  }
15699
17027
  async function optionalText2(path) {
15700
17028
  try {
15701
- return await readFile16(path, "utf8");
17029
+ return await readFile17(path, "utf8");
15702
17030
  } catch (error) {
15703
17031
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return void 0;
15704
17032
  throw error;
@@ -15711,10 +17039,10 @@ function remove(items, item) {
15711
17039
  function sameToolName(left, right) {
15712
17040
  return left.toLowerCase() === right.toLowerCase();
15713
17041
  }
15714
- function storedConversation(snapshot2) {
17042
+ function storedConversation(snapshot4) {
15715
17043
  return {
15716
- ...snapshot2.compact === void 0 ? {} : { compact: snapshot2.compact },
15717
- messages: snapshot2.messages.flatMap((message2) => {
17044
+ ...snapshot4.compact === void 0 ? {} : { compact: snapshot4.compact },
17045
+ messages: snapshot4.messages.flatMap((message2) => {
15718
17046
  if (message2.role === "system") return [];
15719
17047
  const metadata = {
15720
17048
  ...message2.toolCallId === void 0 ? {} : { toolCallId: message2.toolCallId },
@@ -15837,10 +17165,10 @@ async function* runGoalSession(orchestrator, goal, signal) {
15837
17165
  }
15838
17166
  function memoryTranscriptHash(messages) {
15839
17167
  const visible = messages.filter((item) => item.role === "user" || item.role === "assistant").map((item) => `${item.role}\0${modelContentText(item.content).trim()}`).join("\n");
15840
- return createHash8("sha256").update(visible, "utf8").digest("hex");
17168
+ return createHash9("sha256").update(visible, "utf8").digest("hex");
15841
17169
  }
15842
17170
  function createMemoryTaskId() {
15843
- return `memory-${randomUUID10()}`;
17171
+ return `memory-${randomUUID12()}`;
15844
17172
  }
15845
17173
 
15846
17174
  export {