flavor-code 1.2.7 → 1.2.9

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 (41) hide show
  1. package/README.md +405 -369
  2. package/README.zh-CN.md +387 -356
  3. package/dist/agent/loop.d.ts +4 -0
  4. package/dist/agent/types.d.ts +10 -0
  5. package/dist/{app-R3SDG2OH.js → app-USAFQVSX.js} +379 -12
  6. package/dist/{chunk-NVLYFU6P.js → chunk-F2HMO5Q2.js} +28 -11
  7. package/dist/{chunk-WN2EZV3Y.js → chunk-G32MCAZA.js} +1 -1
  8. package/dist/{chunk-N2S7USST.js → chunk-RQTYHUMK.js} +2 -1
  9. package/dist/{chunk-5RHJ3PQN.js → chunk-WGYNTR4Q.js} +1834 -495
  10. package/dist/{chunk-HFR2WS6T.js → chunk-XFCJXRJ2.js} +18 -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/config/schema.d.ts +2 -0
  15. package/dist/context/manager.d.ts +2 -0
  16. package/dist/context/workspace-instructions.d.ts +10 -0
  17. package/dist/desktop/main.js +12736 -5178
  18. package/dist/desktop/pixel-worker.js +73 -0
  19. package/dist/desktop/preload.cjs +80 -2
  20. package/dist/desktop-renderer/assets/index-D-s27J39.js +151 -0
  21. package/dist/desktop-renderer/assets/index-DMZBSGrp.css +1 -0
  22. package/dist/desktop-renderer/index.html +3 -4
  23. package/dist/harness/local.d.ts +4 -1
  24. package/dist/jobs/registry.d.ts +48 -0
  25. package/dist/{load-XC5JYYBQ.js → load-6ZMEBHSG.js} +1 -1
  26. package/dist/models/anthropic.d.ts +4 -0
  27. package/dist/permissions/engine.d.ts +7 -0
  28. package/dist/production.d.ts +13 -1
  29. package/dist/sdk/index.js +4 -4
  30. package/dist/terminal/service.d.ts +57 -0
  31. package/dist/tools/files.d.ts +10 -0
  32. package/dist/tools/jobs.d.ts +4 -0
  33. package/dist/tools/runtime.d.ts +2 -0
  34. package/dist/tools/shell.d.ts +13 -3
  35. package/dist/tools/terminal.d.ts +3 -0
  36. package/dist/tools/types.d.ts +76 -4
  37. package/dist/tools/web.d.ts +53 -0
  38. package/package.json +13 -2
  39. package//346/212/200/346/234/257/346/226/271/346/241/210/346/212/245/345/221/212.md +752 -1
  40. package/dist/desktop-renderer/assets/index-CLrVaR9H.js +0 -143
  41. 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-HFR2WS6T.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) {
@@ -7335,6 +7895,10 @@ function normalizeToolCallInput(input, maxDepth = 3) {
7335
7895
  }
7336
7896
 
7337
7897
  // src/models/anthropic.ts
7898
+ var CLAUDE_CLIENT_HEADERS = {
7899
+ "User-Agent": "claude-cli/2.1.0 (external, cli)",
7900
+ "x-app": "cli"
7901
+ };
7338
7902
  var DEFAULT_MAX_OUTPUT_TOKENS = 32768;
7339
7903
  var MAX_CACHE_MARKERS = 4;
7340
7904
  function collectCacheMarkers(system, messages) {
@@ -7368,27 +7932,27 @@ function markLastBlock(message2) {
7368
7932
  };
7369
7933
  message2.content = blocks;
7370
7934
  }
7371
- function updateInputUsage(snapshot2, usage) {
7372
- 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;
7373
7937
  if (usage?.cache_creation_input_tokens != null) {
7374
- snapshot2.cacheCreation = usage.cache_creation_input_tokens;
7938
+ snapshot4.cacheCreation = usage.cache_creation_input_tokens;
7375
7939
  }
7376
7940
  if (usage?.cache_read_input_tokens != null) {
7377
- snapshot2.cacheRead = usage.cache_read_input_tokens;
7941
+ snapshot4.cacheRead = usage.cache_read_input_tokens;
7378
7942
  }
7379
- return snapshot2.base + snapshot2.cacheCreation + snapshot2.cacheRead;
7943
+ return snapshot4.base + snapshot4.cacheCreation + snapshot4.cacheRead;
7380
7944
  }
7381
- function formatCacheUsage(model, snapshot2, shape) {
7382
- const total = snapshot2.base + snapshot2.cacheCreation + snapshot2.cacheRead;
7383
- 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;
7384
7948
  return JSON.stringify({
7385
7949
  event: "flavor-usage",
7386
7950
  sessionId: currentUsageSession(),
7387
7951
  provider: "anthropic",
7388
7952
  model,
7389
- inputTokens: snapshot2.base,
7390
- cacheReadTokens: snapshot2.cacheRead,
7391
- cacheCreationTokens: snapshot2.cacheCreation,
7953
+ inputTokens: snapshot4.base,
7954
+ cacheReadTokens: snapshot4.cacheRead,
7955
+ cacheCreationTokens: snapshot4.cacheCreation,
7392
7956
  totalInputTokens: total,
7393
7957
  cacheHitRatio: Number(hitRatio.toFixed(4)),
7394
7958
  ...shape === void 0 ? {} : { requestMessages: shape.messages, requestMarkers: shape.markers }
@@ -7407,11 +7971,12 @@ var AnthropicModelAdapter = class {
7407
7971
  this.debugUsage = options.debugUsage ?? isEnvTruthy(process.env.FLAVOR_DEBUG_USAGE);
7408
7972
  this.client = options.client ?? new Anthropic({
7409
7973
  ...options.apiKey === void 0 ? {} : { apiKey: options.apiKey },
7410
- ...options.baseURL === void 0 ? {} : { baseURL: options.baseURL }
7974
+ ...options.baseURL === void 0 ? {} : { baseURL: options.baseURL },
7975
+ ...options.headers === void 0 ? {} : { defaultHeaders: options.headers }
7411
7976
  });
7412
7977
  }
7413
- #logUsage(request, snapshot2, shape) {
7414
- const line = formatCacheUsage(request.model, snapshot2, shape);
7978
+ #logUsage(request, snapshot4, shape) {
7979
+ const line = formatCacheUsage(request.model, snapshot4, shape);
7415
7980
  if (this.debugUsage) {
7416
7981
  try {
7417
7982
  process.stderr.write(`${line}
@@ -7490,7 +8055,7 @@ var AnthropicModelAdapter = class {
7490
8055
  source: {
7491
8056
  type: "base64",
7492
8057
  media_type: block.mediaType,
7493
- data: (await readFile8(block.source.path)).toString("base64")
8058
+ data: (await readFile9(block.source.path)).toString("base64")
7494
8059
  }
7495
8060
  }));
7496
8061
  if (message2.cacheBreakpoint && Array.isArray(content) && content.length > 0) {
@@ -7672,7 +8237,7 @@ function resolveCacheProfile(input) {
7672
8237
 
7673
8238
  // src/models/openai.ts
7674
8239
  import OpenAI from "openai";
7675
- import { readFile as readFile9 } from "fs/promises";
8240
+ import { readFile as readFile10 } from "fs/promises";
7676
8241
  async function toInput(message2) {
7677
8242
  if (message2.role === "tool") {
7678
8243
  if (!message2.toolCallId) throw new Error("Tool messages require toolCallId");
@@ -7684,7 +8249,7 @@ async function toInput(message2) {
7684
8249
  }
7685
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 } : {
7686
8251
  type: "input_image",
7687
- 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")}`,
7688
8253
  detail: "auto"
7689
8254
  }));
7690
8255
  return [
@@ -7903,8 +8468,8 @@ var ModelRegistry = class {
7903
8468
  };
7904
8469
 
7905
8470
  // src/mcp/client.ts
7906
- import { createHash as createHash3 } from "crypto";
7907
- import { z as z13 } from "zod";
8471
+ import { createHash as createHash4 } from "crypto";
8472
+ import { z as z14 } from "zod";
7908
8473
  var McpManager = class {
7909
8474
  #workspace;
7910
8475
  #clientFactory;
@@ -8040,7 +8605,7 @@ async function listAllTools(client) {
8040
8605
  function createMcpToolDefinition(serverName, timeoutMs, client, remote) {
8041
8606
  if (remote.name.length === 0) throw new Error("MCP tool name must not be empty");
8042
8607
  if (!isRecord3(remote.inputSchema)) throw new Error(`MCP tool "${remote.name}" has no JSON object input schema`);
8043
- const inputSchema = z13.fromJSONSchema(
8608
+ const inputSchema = z14.fromJSONSchema(
8044
8609
  remote.inputSchema
8045
8610
  );
8046
8611
  return {
@@ -8070,7 +8635,7 @@ function mcpToolName(serverName, remoteName) {
8070
8635
  const safe = remoteName.replace(/[^A-Za-z0-9_-]/g, "_") || "tool";
8071
8636
  const direct = `${prefix}${safe}`;
8072
8637
  if (direct.length <= 64 && safe === remoteName) return direct;
8073
- const suffix = `__${createHash3("sha256").update(remoteName).digest("hex").slice(0, 8)}`;
8638
+ const suffix = `__${createHash4("sha256").update(remoteName).digest("hex").slice(0, 8)}`;
8074
8639
  return `${prefix}${safe.slice(0, Math.max(1, 64 - prefix.length - suffix.length))}${suffix}`;
8075
8640
  }
8076
8641
  function mcpErrorText(content) {
@@ -8089,7 +8654,7 @@ function isRecord3(value) {
8089
8654
  }
8090
8655
 
8091
8656
  // src/mcp/sdk.ts
8092
- import { resolve as resolve11 } from "path";
8657
+ import { resolve as resolve12 } from "path";
8093
8658
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8094
8659
  import {
8095
8660
  getDefaultEnvironment,
@@ -8111,7 +8676,7 @@ async function connectSdkMcpClient(input, dependencies = DEFAULT_DEPENDENCIES) {
8111
8676
  command: config.command,
8112
8677
  args: [...config.args],
8113
8678
  env: { ...dependencies.defaultEnvironment(), ...config.env },
8114
- cwd: resolve11(input.workspace, config.cwd ?? "."),
8679
+ cwd: resolve12(input.workspace, config.cwd ?? "."),
8115
8680
  // MCP servers frequently log startup banners to stderr. Inheriting that
8116
8681
  // stream corrupts Ink's input area, so connection errors are surfaced
8117
8682
  // through McpManager diagnostics instead.
@@ -8136,14 +8701,14 @@ async function connectSdkMcpClient(input, dependencies = DEFAULT_DEPENDENCIES) {
8136
8701
  }
8137
8702
 
8138
8703
  // src/auth/oauth.ts
8139
- import { randomBytes, createHash as createHash5 } from "crypto";
8704
+ import { randomBytes, createHash as createHash6 } from "crypto";
8140
8705
  import { createServer } from "http";
8141
8706
  import { URL as URL2, URLSearchParams } from "url";
8142
8707
 
8143
8708
  // src/auth/oauth-config.ts
8144
- import { createHash as createHash4 } from "crypto";
8709
+ import { createHash as createHash5 } from "crypto";
8145
8710
  function oauthCredentialId(tokenUrl, clientId) {
8146
- const digest = createHash4("sha256").update(`${tokenUrl}\0${clientId}`, "utf8").digest("base64url");
8711
+ const digest = createHash5("sha256").update(`${tokenUrl}\0${clientId}`, "utf8").digest("base64url");
8147
8712
  return `oauth:${digest}`;
8148
8713
  }
8149
8714
  function parseOAuthLlmConfig(value) {
@@ -8316,8 +8881,8 @@ var OAuthCallbackAuthProvider = class {
8316
8881
  async #startCallbackServer(expectedState, signal) {
8317
8882
  let serverResolve;
8318
8883
  let serverReject;
8319
- const codePromise = new Promise((resolve22, reject) => {
8320
- serverResolve = resolve22;
8884
+ const codePromise = new Promise((resolve25, reject) => {
8885
+ serverResolve = resolve25;
8321
8886
  serverReject = reject;
8322
8887
  });
8323
8888
  const server = createServer((req, res) => {
@@ -8351,7 +8916,7 @@ var OAuthCallbackAuthProvider = class {
8351
8916
  );
8352
8917
  serverResolve(code);
8353
8918
  });
8354
- return new Promise((resolve22, reject) => {
8919
+ return new Promise((resolve25, reject) => {
8355
8920
  server.once("error", (error) => {
8356
8921
  reject(new Error(`OAuth callback server failed: ${error.message}`));
8357
8922
  });
@@ -8368,7 +8933,7 @@ var OAuthCallbackAuthProvider = class {
8368
8933
  signal.addEventListener("abort", () => {
8369
8934
  serverReject(new Error("OAuth flow aborted"));
8370
8935
  }, { once: true });
8371
- resolve22({ redirectUri, promise: codePromise });
8936
+ resolve25({ redirectUri, promise: codePromise });
8372
8937
  });
8373
8938
  });
8374
8939
  }
@@ -8400,7 +8965,7 @@ var OAuthCallbackAuthProvider = class {
8400
8965
  return randomBytes(32).toString("base64url");
8401
8966
  }
8402
8967
  #computeCodeChallenge(verifier) {
8403
- return createHash5("sha256").update(verifier).digest("base64url");
8968
+ return createHash6("sha256").update(verifier).digest("base64url");
8404
8969
  }
8405
8970
  #generateState() {
8406
8971
  return randomBytes(32).toString("base64url");
@@ -8450,13 +9015,13 @@ function escapeHtml(text) {
8450
9015
  }
8451
9016
 
8452
9017
  // src/auth/store.ts
8453
- import { dirname as dirname9 } from "path";
9018
+ import { dirname as dirname10 } from "path";
8454
9019
  function createFileTokenStore(filePath) {
8455
9020
  let baseline = {};
8456
9021
  const store = {
8457
9022
  tokens: {},
8458
9023
  async load() {
8459
- const key = await loadOrCreateConfigKey(dirname9(filePath));
9024
+ const key = await loadOrCreateConfigKey(dirname10(filePath));
8460
9025
  const result = await readRecoverableFile(filePath, (raw) => parseTokens(decryptDocument(raw, key)));
8461
9026
  const tokens = result?.value ?? {};
8462
9027
  baseline = { ...tokens };
@@ -8464,7 +9029,7 @@ function createFileTokenStore(filePath) {
8464
9029
  return tokens;
8465
9030
  },
8466
9031
  async save(tokens) {
8467
- const key = await loadOrCreateConfigKey(dirname9(filePath));
9032
+ const key = await loadOrCreateConfigKey(dirname10(filePath));
8468
9033
  const removals = Object.keys(baseline).filter((providerId) => !(providerId in tokens));
8469
9034
  const upserts = Object.fromEntries(Object.entries(tokens).filter(
8470
9035
  ([providerId, token]) => !sameToken(baseline[providerId], token)
@@ -8630,16 +9195,16 @@ function data(value) {
8630
9195
 
8631
9196
  // src/plugins/host.ts
8632
9197
  import { lstatSync, realpathSync as realpathSync2 } from "fs";
8633
- import { lstat as lstat5, open as open5, readdir as readdir4, realpath as realpath3, stat as stat3, writeFile as writeFile6 } from "fs/promises";
8634
- 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";
8635
9200
  import { pathToFileURL } from "url";
8636
9201
 
8637
9202
  // src/plugins/types.ts
8638
- import { z as z14 } from "zod";
9203
+ import { z as z15 } from "zod";
8639
9204
  var PLUGIN_NAME = /^(?:@[a-z0-9][a-z0-9._-]{0,63}\/)?[a-z0-9][a-z0-9._-]{0,63}$/;
8640
9205
  var CONTRIBUTION_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
8641
- var entry = z14.object({ name: z14.string().regex(CONTRIBUTION_NAME) }).strict();
8642
- 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) => {
8643
9208
  const seen = /* @__PURE__ */ new Set();
8644
9209
  for (const [index, item] of items.entries()) {
8645
9210
  const key = identify(item);
@@ -8647,17 +9212,17 @@ var uniqueArray = (schema, label, identify = (item) => String(item)) => z14.arra
8647
9212
  seen.add(key);
8648
9213
  }
8649
9214
  });
8650
- var PluginManifestSchema = z14.object({
8651
- name: z14.string().regex(PLUGIN_NAME),
8652
- version: z14.string().min(1).max(64),
8653
- apiVersion: z14.literal("1"),
8654
- main: z14.string().min(1).max(512),
8655
- permissions: uniqueArray(z14.enum(["filesystem:read", "filesystem:write"]), "permission").max(16),
8656
- 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({
8657
9222
  commands: uniqueArray(entry, "command contribution", (item) => item.name),
8658
9223
  tools: uniqueArray(entry, "tool contribution", (item) => item.name),
8659
- hooks: uniqueArray(z14.object({ name: z14.enum(HOOK_EVENT_NAMES) }).strict(), "hook contribution", (item) => item.name),
8660
- 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),
8661
9226
  modelAdapters: uniqueArray(entry, "model-adapter contribution", (item) => item.name)
8662
9227
  }).strict()
8663
9228
  }).strict();
@@ -8749,7 +9314,7 @@ var PluginHost = class {
8749
9314
  const roots = [];
8750
9315
  for (const directory of directories) {
8751
9316
  try {
8752
- 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));
8753
9318
  } catch (error) {
8754
9319
  this.#diagnose(directory, error, directory);
8755
9320
  }
@@ -8779,10 +9344,10 @@ var PluginHost = class {
8779
9344
  async #readCandidate(inputRoot, source, label = inputRoot) {
8780
9345
  let diagnosticLabel = label;
8781
9346
  try {
8782
- const rootInfo = await lstat5(inputRoot);
9347
+ const rootInfo = await lstat6(inputRoot);
8783
9348
  if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) throw new Error("Plugin root must be a static directory");
8784
- const root = await realpath3(inputRoot);
8785
- const rawManifest = await readManifest(resolve12(root, MANIFEST));
9349
+ const root = await realpath4(inputRoot);
9350
+ const rawManifest = await readManifest(resolve13(root, MANIFEST));
8786
9351
  if (typeof rawManifest === "object" && rawManifest !== null && "name" in rawManifest && typeof rawManifest.name === "string") {
8787
9352
  diagnosticLabel = rawManifest.name;
8788
9353
  }
@@ -8881,7 +9446,7 @@ var PluginHost = class {
8881
9446
  assertActive();
8882
9447
  const permission = `filesystem:${operation}`;
8883
9448
  if (!manifest.permissions.includes(permission)) throw new Error(`Plugin ${manifest.name} lacks ${permission} permission`);
8884
- const path = resolve12(rawPath);
9449
+ const path = resolve13(rawPath);
8885
9450
  const authorized = await this.#options.authorizeFilesystem?.({ plugin: manifest.name, operation, path });
8886
9451
  assertActive();
8887
9452
  if (authorized !== true) throw new Error(`Filesystem ${operation} denied for plugin ${manifest.name}`);
@@ -8922,8 +9487,8 @@ var PluginHost = class {
8922
9487
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`Skill root "${name}" must be a static directory`);
8923
9488
  const physical = realpathSync2.native(registered);
8924
9489
  assertContained(candidate.root, physical, "Skill root");
8925
- const snapshot2 = fromStats(info);
8926
- 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)) });
8927
9492
  return register("skillRoot", name, [capability]);
8928
9493
  },
8929
9494
  registerModelAdapter: (name, value) => register("modelAdapter", name, [value])
@@ -8949,7 +9514,7 @@ var PluginHost = class {
8949
9514
  }
8950
9515
  };
8951
9516
  async function readManifest(path) {
8952
- const lexical = await lstat5(path, { bigint: true });
9517
+ const lexical = await lstat6(path, { bigint: true });
8953
9518
  if (!lexical.isFile() || lexical.isSymbolicLink()) throw new Error("Plugin manifest must be a static regular file");
8954
9519
  const handle = await open5(path, "r");
8955
9520
  try {
@@ -8966,7 +9531,7 @@ async function readManifest(path) {
8966
9531
  if (bytesRead > MAX_MANIFEST_BYTES2) throw new Error(`Plugin manifest exceeds ${MAX_MANIFEST_BYTES2} bytes`);
8967
9532
  const after = fromStats(await handle.stat({ bigint: true }));
8968
9533
  assertSnapshot(before, after, "Plugin manifest");
8969
- const current = fromStats(await lstat5(path, { bigint: true }));
9534
+ const current = fromStats(await lstat6(path, { bigint: true }));
8970
9535
  assertSnapshot(before, current, "Plugin manifest path");
8971
9536
  return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead)));
8972
9537
  } finally {
@@ -8974,9 +9539,9 @@ async function readManifest(path) {
8974
9539
  }
8975
9540
  }
8976
9541
  async function snapshotEntry(root, entry2) {
8977
- const lexicalStats = await lstat5(entry2, { bigint: true });
9542
+ const lexicalStats = await lstat6(entry2, { bigint: true });
8978
9543
  if (!lexicalStats.isFile() && !lexicalStats.isSymbolicLink()) throw new Error("Plugin entry must be a file");
8979
- const physicalPath = await realpath3(entry2);
9544
+ const physicalPath = await realpath4(entry2);
8980
9545
  assertContained(root, physicalPath, "Plugin entry symlink");
8981
9546
  const physicalStats = await stat3(physicalPath, { bigint: true });
8982
9547
  if (!physicalStats.isFile()) throw new Error("Plugin entry must resolve to a regular file");
@@ -9005,21 +9570,21 @@ function contributionNames(manifest, kind) {
9005
9570
  function fromStats(stats) {
9006
9571
  return { dev: stats.dev, ino: stats.ino, mode: stats.mode, size: stats.size, mtimeNs: stats.mtimeNs, ctimeNs: stats.ctimeNs };
9007
9572
  }
9008
- function pickIdentity(snapshot2) {
9009
- 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 };
9010
9575
  }
9011
9576
  function assertSnapshot(expected, actual, label) {
9012
9577
  for (const key of ["dev", "ino", "mode", "size", "mtimeNs", "ctimeNs"]) if (expected[key] !== actual[key]) throw new Error(`${label} changed during validation`);
9013
9578
  }
9014
9579
  function resolveContained(root, value, label) {
9015
- if (isAbsolute5(value)) throw new Error(`${label} must be relative to the plugin root`);
9016
- 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);
9017
9582
  assertContained(root, candidate, label);
9018
9583
  return candidate;
9019
9584
  }
9020
9585
  function assertContained(root, candidate, label) {
9021
- const delta = relative10(root, candidate);
9022
- 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`);
9023
9588
  }
9024
9589
  function scopedLogger(logger, plugin) {
9025
9590
  const prefix = `[plugin:${plugin}] `;
@@ -9063,8 +9628,8 @@ function compare(left, right) {
9063
9628
 
9064
9629
  // src/skills/registry.ts
9065
9630
  import { constants as constants3 } from "fs";
9066
- import { lstat as lstat6, open as defaultOpen, readdir as readdir5, realpath as realpath4 } from "fs/promises";
9067
- 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";
9068
9633
  import { marked } from "marked";
9069
9634
  import { parseDocument } from "yaml";
9070
9635
  var SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -9189,13 +9754,13 @@ var SkillRegistry = class {
9189
9754
  const record = await this.#recordFor(skill);
9190
9755
  const normalized = normalizeReference(reference);
9191
9756
  const parts = normalized.split("/");
9192
- 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] ?? "")) {
9193
9758
  throw new Error(`Resource reference escapes the skill root or is not a direct resource: ${reference}`);
9194
9759
  }
9195
9760
  const body = await this.loadBody(record.metadata);
9196
9761
  const references = extractResourceReferences(body);
9197
9762
  if (!references.has(normalized)) throw new Error(`Skill resource is not directly referenced: ${reference}`);
9198
- const candidate = resolve13(record.metadata.root, ...parts);
9763
+ const candidate = resolve14(record.metadata.root, ...parts);
9199
9764
  const file = await openVerifiedFile(candidate, record.metadata.root, "Resource", this.#options.openFile);
9200
9765
  try {
9201
9766
  if (file.snapshot.size > BigInt(this.#options.maxResourceBytes)) throw new Error(`Skill resource is too large: ${reference}`);
@@ -9224,13 +9789,13 @@ var SkillRegistry = class {
9224
9789
  async #discoverSource(roots, source) {
9225
9790
  const found = [];
9226
9791
  for (const configuredRoot of roots) {
9227
- const root = resolve13(configuredRoot);
9792
+ const root = resolve14(configuredRoot);
9228
9793
  let entries;
9229
9794
  let physicalRoot = root;
9230
9795
  try {
9231
- const rootInfo = await lstat6(root);
9796
+ const rootInfo = await lstat7(root);
9232
9797
  if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) throw new Error("Skill root must be a real directory");
9233
- physicalRoot = await realpath4(root);
9798
+ physicalRoot = await realpath5(root);
9234
9799
  entries = await readdir5(root, { withFileTypes: true });
9235
9800
  } catch (error) {
9236
9801
  if (isMissing2(error)) continue;
@@ -9239,13 +9804,13 @@ var SkillRegistry = class {
9239
9804
  }
9240
9805
  entries.sort((left, right) => compareCodePoints(left.name, right.name));
9241
9806
  for (const entry2 of entries) {
9242
- const skillRoot = resolve13(root, entry2.name);
9807
+ const skillRoot = resolve14(root, entry2.name);
9243
9808
  try {
9244
9809
  if (entry2.isSymbolicLink()) throw new Error("Symlinked skill directories are not allowed");
9245
9810
  if (!entry2.isDirectory()) continue;
9246
- const physicalSkillRoot = await realpath4(skillRoot);
9811
+ const physicalSkillRoot = await realpath5(skillRoot);
9247
9812
  if (!isWithin3(physicalRoot, physicalSkillRoot)) throw new Error("Skill directory escapes its registry root");
9248
- const skillFile = resolve13(skillRoot, "SKILL.md");
9813
+ const skillFile = resolve14(skillRoot, "SKILL.md");
9249
9814
  const parsed = await readFrontmatterFile(
9250
9815
  skillFile,
9251
9816
  physicalSkillRoot,
@@ -9382,10 +9947,10 @@ async function openFile(path, flags) {
9382
9947
  return defaultOpen(path, flags);
9383
9948
  }
9384
9949
  async function openVerifiedFile(path, root, label, opener, expected) {
9385
- const initial = await lstat6(path, { bigint: true });
9950
+ const initial = await lstat7(path, { bigint: true });
9386
9951
  if (initial.isSymbolicLink()) throw new Error(`${label} symlinks are not allowed`);
9387
9952
  assertRegular(initial, label);
9388
- const canonical3 = await realpath4(path);
9953
+ const canonical3 = await realpath5(path);
9389
9954
  if (!isWithin3(root, canonical3)) throw new Error(`${label} escapes the skill root`);
9390
9955
  const noFollow = typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
9391
9956
  const handle = await opener(canonical3, constants3.O_RDONLY | noFollow);
@@ -9446,8 +10011,8 @@ function decodeUtf82(content, label) {
9446
10011
  }
9447
10012
  }
9448
10013
  function isWithin3(root, candidate) {
9449
- const delta = relative11(root, candidate);
9450
- return delta === "" || delta !== ".." && !delta.startsWith(`..${sep10}`) && !isAbsolute6(delta);
10014
+ const delta = relative12(root, candidate);
10015
+ return delta === "" || delta !== ".." && !delta.startsWith(`..${sep11}`) && !isAbsolute7(delta);
9451
10016
  }
9452
10017
  function isMissing2(error) {
9453
10018
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
@@ -9494,10 +10059,10 @@ function score(metadata, query) {
9494
10059
  }
9495
10060
 
9496
10061
  // src/skills/tool.ts
9497
- import { z as z15 } from "zod";
9498
- var SkillResourceInput = z15.object({
9499
- skill: z15.string().min(1),
9500
- 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)
9501
10066
  }).strict();
9502
10067
  function createSkillResourceTool(registry) {
9503
10068
  return {
@@ -9524,97 +10089,97 @@ function createSkillResourceTool(registry) {
9524
10089
 
9525
10090
  // src/session/store.ts
9526
10091
  import { constants as constants4 } from "fs";
9527
- 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";
9528
- 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";
9529
10094
  import { randomUUID as randomUUID6 } from "crypto";
9530
- import { z as z16 } from "zod";
10095
+ import { z as z17 } from "zod";
9531
10096
  var SESSION_VERSION = 3;
9532
10097
  var DEFAULT_MAX_SESSION_BYTES = 5 * 1024 * 1024;
9533
- var SessionIdSchema = z16.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/, "Invalid session id");
9534
- var IsoDateSchema = z16.string().datetime({ offset: true });
9535
- var ToolCallSchema = z16.object({
9536
- id: z16.string().min(1).max(256),
9537
- name: z16.string().min(1).max(256),
9538
- 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()
9539
10104
  }).strict();
9540
- var TextContentBlockSchema = z16.object({
9541
- type: z16.literal("text"),
9542
- 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)
9543
10108
  }).strict();
9544
- var ImageContentBlockSchema = z16.object({
9545
- type: z16.literal("image"),
9546
- source: z16.object({ type: z16.literal("file"), path: z16.string().min(1).max(32768) }).strict(),
9547
- mediaType: z16.enum(["image/png", "image/jpeg", "image/webp"]),
9548
- sha256: z16.string().regex(/^[a-f0-9]{64}$/),
9549
- bytes: z16.number().int().positive().max(20 * 1024 * 1024),
9550
- 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()
9551
10116
  }).strict();
9552
- var UserContentSchema = z16.union([
9553
- z16.string().max(DEFAULT_MAX_SESSION_BYTES),
9554
- 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)
9555
10120
  ]);
9556
10121
  var MessageMetadataShape = {
9557
- toolCallId: z16.string().min(1).max(256).optional(),
9558
- toolCalls: z16.array(ToolCallSchema).max(1e3).optional()
10122
+ toolCallId: z17.string().min(1).max(256).optional(),
10123
+ toolCalls: z17.array(ToolCallSchema).max(1e3).optional()
9559
10124
  };
9560
- var MessageSchema = z16.discriminatedUnion("role", [
9561
- z16.object({ role: z16.literal("user"), content: UserContentSchema, ...MessageMetadataShape }).strict(),
9562
- z16.object({ role: z16.literal("assistant"), content: z16.string().max(DEFAULT_MAX_SESSION_BYTES), ...MessageMetadataShape }).strict(),
9563
- 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()
9564
10129
  ]);
9565
- var LegacySummarySchema = z16.object({ role: z16.literal("system"), content: z16.string().max(DEFAULT_MAX_SESSION_BYTES) }).strict();
9566
- var CompactBoundarySchema = z16.object({
9567
- 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),
9568
10133
  compactedAt: IsoDateSchema
9569
10134
  }).strict();
9570
- var StateSchema = z16.enum(["pending", "running", "completed", "failed", "blocked", "cancelled"]);
9571
- var MemoryLifecycleSchema = z16.object({
9572
- status: z16.enum(["active", "completed", "abandoned"]),
9573
- taskId: z16.string().regex(/^memory-[A-Za-z0-9._-]{1,120}$/).optional(),
9574
- 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(),
9575
10140
  finalizedAt: IsoDateSchema.optional(),
9576
- transcriptHash: z16.string().regex(/^[a-f0-9]{64}$/).optional()
10141
+ transcriptHash: z17.string().regex(/^[a-f0-9]{64}$/).optional()
9577
10142
  }).strict();
9578
- var SessionBaseSchema = z16.object({
10143
+ var SessionBaseSchema = z17.object({
9579
10144
  sessionId: SessionIdSchema,
9580
10145
  createdAt: IsoDateSchema,
9581
10146
  updatedAt: IsoDateSchema,
9582
- workspace: z16.object({ path: z16.string().min(1).max(32768) }).strict(),
9583
- tasks: z16.object({
10147
+ workspace: z17.object({ path: z17.string().min(1).max(32768) }).strict(),
10148
+ tasks: z17.object({
9584
10149
  plan: TaskPlanSchema.optional(),
9585
10150
  graph: TaskGraphSchema.optional(),
9586
- states: z16.record(z16.string(), StateSchema),
9587
- results: z16.record(z16.string(), SubagentResultSchema)
10151
+ states: z17.record(z17.string(), StateSchema),
10152
+ results: z17.record(z17.string(), SubagentResultSchema)
9588
10153
  }).strict(),
9589
- 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(),
9590
10155
  permissionMode: PermissionModeSchema,
9591
10156
  memory: MemoryLifecycleSchema.optional()
9592
10157
  }).strict();
9593
10158
  var SessionDocumentV1Schema = SessionBaseSchema.extend({
9594
- version: z16.literal(1),
9595
- conversation: z16.object({
10159
+ version: z17.literal(1),
10160
+ conversation: z17.object({
9596
10161
  summary: LegacySummarySchema.optional(),
9597
- messages: z16.array(MessageSchema).max(5e4)
10162
+ messages: z17.array(MessageSchema).max(5e4)
9598
10163
  }).strict()
9599
10164
  }).strict();
9600
- var ConversationSchema = z16.object({
10165
+ var ConversationSchema = z17.object({
9601
10166
  compact: CompactBoundarySchema.optional(),
9602
- messages: z16.array(MessageSchema).max(5e4)
10167
+ messages: z17.array(MessageSchema).max(5e4)
9603
10168
  }).strict();
9604
10169
  var SessionDocumentV2Schema = SessionBaseSchema.extend({
9605
- version: z16.literal(2),
10170
+ version: z17.literal(2),
9606
10171
  conversation: ConversationSchema
9607
10172
  }).strict();
9608
- var TranscriptStateSchema = z16.custom(isTranscriptState, "Invalid transcript state");
9609
- var TimelineSchema = z16.object({
9610
- version: z16.literal(1),
10173
+ var TranscriptStateSchema = z17.custom(isTranscriptState, "Invalid transcript state");
10174
+ var TimelineSchema = z17.object({
10175
+ version: z17.literal(1),
9611
10176
  state: TranscriptStateSchema
9612
10177
  }).strict();
9613
10178
  var SessionDocumentSchema = SessionBaseSchema.extend({
9614
- version: z16.literal(SESSION_VERSION),
9615
- conversation: z16.object({
10179
+ version: z17.literal(SESSION_VERSION),
10180
+ conversation: z17.object({
9616
10181
  compact: CompactBoundarySchema.optional(),
9617
- messages: z16.array(MessageSchema).max(5e4)
10182
+ messages: z17.array(MessageSchema).max(5e4)
9618
10183
  }).strict(),
9619
10184
  timeline: TimelineSchema
9620
10185
  }).strict();
@@ -9624,7 +10189,7 @@ var SessionStore = class {
9624
10189
  #maxBytes;
9625
10190
  #maxSessions;
9626
10191
  constructor(options) {
9627
- this.#workspace = resolve14(options.workspace);
10192
+ this.#workspace = resolve15(options.workspace);
9628
10193
  this.#sessions = join12(this.#workspace, ".flavor", "sessions");
9629
10194
  this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_SESSION_BYTES;
9630
10195
  if (!Number.isSafeInteger(this.#maxBytes) || this.#maxBytes < 256) throw new Error("maxBytes must be an integer of at least 256");
@@ -9761,7 +10326,7 @@ var SessionStore = class {
9761
10326
  }
9762
10327
  const path = this.#path(sessionId);
9763
10328
  try {
9764
- const metadata = await lstat7(path);
10329
+ const metadata = await lstat8(path);
9765
10330
  if (!metadata.isFile()) throw new Error("Session path is not a regular file");
9766
10331
  await rm4(path);
9767
10332
  } catch (error) {
@@ -9770,7 +10335,7 @@ var SessionStore = class {
9770
10335
  }
9771
10336
  #path(sessionId) {
9772
10337
  SessionIdSchema.parse(sessionId);
9773
- const path = resolve14(this.#sessions, `${sessionId}.jsonl`);
10338
+ const path = resolve15(this.#sessions, `${sessionId}.jsonl`);
9774
10339
  if (!isWithin4(this.#sessions, path)) throw new Error("Invalid session id");
9775
10340
  return path;
9776
10341
  }
@@ -9790,14 +10355,14 @@ var SessionStore = class {
9790
10355
  }
9791
10356
  }
9792
10357
  async #prepareDirectory() {
9793
- await assertNoSymlink3(this.#workspace, dirname10(this.#sessions));
10358
+ await assertNoSymlink3(this.#workspace, dirname11(this.#sessions));
9794
10359
  await mkdir9(this.#sessions, { recursive: true, mode: 448 });
9795
10360
  await this.#assertSafeExistingDirectory();
9796
10361
  }
9797
10362
  async #assertSafeExistingDirectory() {
9798
10363
  await assertNoSymlink3(this.#workspace, this.#sessions);
9799
- const canonicalRoot = await realpath5(this.#workspace);
9800
- const canonicalSessions = await realpath5(this.#sessions);
10364
+ const canonicalRoot = await realpath6(this.#workspace);
10365
+ const canonicalSessions = await realpath6(this.#sessions);
9801
10366
  if (!isWithin4(canonicalRoot, canonicalSessions)) throw new Error("Session directory escapes the workspace");
9802
10367
  }
9803
10368
  async #quarantine(path) {
@@ -9810,7 +10375,7 @@ async function boundedRead(path, maxBytes) {
9810
10375
  const metadata = await stat4(path);
9811
10376
  if (!metadata.isFile()) throw new Error("Session path is not a regular file");
9812
10377
  if (metadata.size > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
9813
- const value = await readFile10(path);
10378
+ const value = await readFile11(path);
9814
10379
  if (value.byteLength > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
9815
10380
  return value.toString("utf8");
9816
10381
  }
@@ -9948,31 +10513,31 @@ function isRecord5(value) {
9948
10513
  return typeof value === "object" && value !== null && !Array.isArray(value);
9949
10514
  }
9950
10515
  async function assertNoSymlink3(root, target) {
9951
- let cursor = resolve14(target);
9952
- const stop = resolve14(root);
10516
+ let cursor = resolve15(target);
10517
+ const stop = resolve15(root);
9953
10518
  if (!isWithin4(stop, cursor)) throw new Error("Session path escapes the workspace");
9954
10519
  const chain = [];
9955
10520
  while (cursor !== stop) {
9956
10521
  chain.push(cursor);
9957
- cursor = dirname10(cursor);
10522
+ cursor = dirname11(cursor);
9958
10523
  }
9959
10524
  for (const path of chain.reverse()) {
9960
10525
  try {
9961
- 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}`);
9962
10527
  } catch (error) {
9963
10528
  if (!isCode6(error, "ENOENT")) throw error;
9964
10529
  }
9965
10530
  }
9966
10531
  }
9967
10532
  function isWithin4(root, path) {
9968
- const value = relative12(resolve14(root), resolve14(path));
9969
- 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(":");
9970
10535
  }
9971
10536
  async function canonical2(path) {
9972
10537
  try {
9973
- return await realpath5(resolve14(path));
10538
+ return await realpath6(resolve15(path));
9974
10539
  } catch {
9975
- return resolve14(path);
10540
+ return resolve15(path);
9976
10541
  }
9977
10542
  }
9978
10543
  function isCode6(error, code) {
@@ -9999,45 +10564,45 @@ function sanitize2(input, seen = /* @__PURE__ */ new WeakSet()) {
9999
10564
  // src/sleep/organizer.ts
10000
10565
  import { constants as constants5 } from "fs";
10001
10566
  import { randomUUID as randomUUID7 } from "crypto";
10002
- 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";
10003
- import { isAbsolute as isAbsolute7, join as join13, relative as relative13, resolve as resolve15, sep as sep12 } from "path";
10004
- import { z as z17 } from "zod";
10005
- var DateKeySchema = z17.string().regex(/^\d{4}-\d{2}-\d{2}$/);
10006
- var ReviewSchema = z17.object({
10007
- title: z17.string().min(1).max(120),
10008
- taskSummary: z17.array(z17.string().min(1)).min(1).max(20),
10009
- executionReflection: z17.array(z17.string().min(1)).min(1).max(20),
10010
- decisionsAndLearnings: z17.array(z17.string().min(1)).max(20),
10011
- openQuestionsAndRisks: z17.array(z17.string().min(1)).max(20),
10012
- tomorrowPlan: z17.array(z17.string().min(1)).min(1).max(20),
10013
- toolUsage: z17.object({
10014
- totalCalls: z17.number().int().min(0),
10015
- shell: z17.number().int().min(0),
10016
- fileRead: z17.number().int().min(0),
10017
- fileWrite: z17.number().int().min(0),
10018
- search: z17.number().int().min(0),
10019
- lsp: z17.number().int().min(0),
10020
- subagent: z17.number().int().min(0),
10021
- 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)
10022
10587
  }).strict(),
10023
- tokenEstimate: z17.object({
10024
- estimatedInput: z17.number().int().min(0),
10025
- estimatedOutput: z17.number().int().min(0),
10026
- 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)
10027
10592
  }).strict(),
10028
- humanIntervention: z17.object({
10029
- approvalRequests: z17.number().int().min(0),
10030
- questionsAsked: z17.number().int().min(0),
10031
- 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)
10032
10597
  }).strict(),
10033
- qualityIndicators: z17.object({
10034
- hallucinationAlerts: z17.array(z17.string().min(1)).max(10),
10035
- failuresAndRetries: z17.array(z17.string().min(1)).max(10),
10036
- codeChangeSummary: z17.string().max(500),
10037
- 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)
10038
10603
  }).strict(),
10039
- knowledgeDeposits: z17.object({
10040
- worthRemembering: z17.array(z17.string().min(1)).max(10)
10604
+ knowledgeDeposits: z18.object({
10605
+ worthRemembering: z18.array(z18.string().min(1)).max(10)
10041
10606
  }).strict()
10042
10607
  }).strict();
10043
10608
  function localDateKey(date) {
@@ -10054,7 +10619,7 @@ var ProjectSleepOrganizer = class {
10054
10619
  #sessions;
10055
10620
  #generate;
10056
10621
  constructor(options) {
10057
- this.#workspace = resolve15(options.workspace);
10622
+ this.#workspace = resolve16(options.workspace);
10058
10623
  this.#sleepDirectory = join13(this.#workspace, ".flavor", "sleep");
10059
10624
  this.#sessions = options.sessions ?? new SessionStore({ workspace: this.#workspace });
10060
10625
  this.#generate = options.generate;
@@ -10146,8 +10711,8 @@ Previous output was invalid. Repair it and return strict JSON only.`;
10146
10711
  await mkdir10(flavorDirectory, { recursive: true, mode: 448 });
10147
10712
  await assertNotSymlink(this.#sleepDirectory);
10148
10713
  await mkdir10(this.#sleepDirectory, { recursive: true, mode: 448 });
10149
- const root = await realpath6(this.#workspace);
10150
- const target = await realpath6(this.#sleepDirectory);
10714
+ const root = await realpath7(this.#workspace);
10715
+ const target = await realpath7(this.#sleepDirectory);
10151
10716
  if (!isWithin5(root, target)) throw new Error("Sleep directory escapes the workspace");
10152
10717
  }
10153
10718
  async #hasReport(date) {
@@ -10471,16 +11036,16 @@ function assertDateKey(value) {
10471
11036
  }
10472
11037
  async function assertNotSymlink(path) {
10473
11038
  try {
10474
- 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}`);
10475
11040
  } catch (error) {
10476
11041
  if (!hasCode(error, "ENOENT")) throw error;
10477
11042
  }
10478
11043
  }
10479
11044
  function isWithin5(root, candidate) {
10480
- const path = relative13(root, candidate);
11045
+ const path = relative14(root, candidate);
10481
11046
  if (path === "") return true;
10482
- if (path === ".." || path.startsWith(`..${sep12}`) || isAbsolute7(path)) return false;
10483
- 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}`);
10484
11049
  }
10485
11050
  function hasCode(error, code) {
10486
11051
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
@@ -10529,10 +11094,10 @@ function computeSessionStats(documents) {
10529
11094
 
10530
11095
  // src/tools/files.ts
10531
11096
  import { constants as constants6 } from "fs";
10532
- 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";
10533
- 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";
10534
11099
  import { randomUUID as randomUUID8 } from "crypto";
10535
- import { z as z18 } from "zod";
11100
+ import { z as z19 } from "zod";
10536
11101
 
10537
11102
  // src/tools/file-diff.ts
10538
11103
  var MAX_DIFF_PREVIEW_LINES = 120;
@@ -10638,18 +11203,34 @@ function truncateLines(lines) {
10638
11203
 
10639
11204
  // src/tools/files.ts
10640
11205
  var MAX_READ_BYTES = 1048576;
10641
- var ReadInput = z18.object({
10642
- path: z18.string().min(1),
10643
- maxBytes: z18.coerce.number().int().positive().max(MAX_READ_BYTES).nullable().optional(),
10644
- startLine: z18.coerce.number().int().positive().nullable().optional(),
10645
- 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(),
10646
11211
  // Accepts booleans and their string forms (weak-typed models emit "true");
10647
11212
  // kept transform-free so the schema converts to JSON Schema for providers.
10648
- 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()
10649
11214
  });
10650
- var WriteInput = z18.object({ path: z18.string().min(1), content: z18.string() });
10651
- var EditInput = z18.object({ path: z18.string().min(1), oldText: z18.string().min(1), newText: z18.string() });
10652
- 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
+ };
10653
11234
  function createReadTool(workspace, options = {}) {
10654
11235
  const guard = createPathGuard(workspace);
10655
11236
  const openFile2 = options.openFile ?? ((path) => open8(path, constants6.O_RDONLY));
@@ -10657,7 +11238,7 @@ function createReadTool(workspace, options = {}) {
10657
11238
  return {
10658
11239
  name: "Read",
10659
11240
  description: "Read a UTF-8 text file, optionally restricted to a line range with startLine/endLine",
10660
- inputSchema: ReadInput,
11241
+ inputSchema: ReadInput2,
10661
11242
  paths: (input) => [guard.lexical(input.path)],
10662
11243
  execute: async (input, signal) => {
10663
11244
  abortIfNeeded(signal);
@@ -10679,6 +11260,7 @@ function createReadTool(workspace, options = {}) {
10679
11260
  const textEnd = utf8SafeEnd(contents, windowBytes);
10680
11261
  const truncated = contents.length > windowBytes;
10681
11262
  const text = contents.subarray(0, textEnd).toString("utf8").replaceAll("\r\n", "\n");
11263
+ options.observations?.set(path, versionOfStat(info));
10682
11264
  const lines = text.split("\n");
10683
11265
  if (lines.at(-1) === "") lines.pop();
10684
11266
  const availableLines = lines.length;
@@ -10725,6 +11307,7 @@ function createWriteTool(workspace, options = {}) {
10725
11307
  execute: async (input, signal) => {
10726
11308
  abortIfNeeded(signal);
10727
11309
  const path = await guard.destination(input.path);
11310
+ const expected = await expectedMutationVersion(path, options.observations);
10728
11311
  const previous = await readOptionalPresentationText(path);
10729
11312
  if (!previous.exists || previous.text !== void 0) {
10730
11313
  await options.beforeCommit?.({
@@ -10734,7 +11317,9 @@ function createWriteTool(workspace, options = {}) {
10734
11317
  kind: previous.exists ? "update" : "create"
10735
11318
  }, signal);
10736
11319
  }
10737
- 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);
10738
11323
  const output = { path, bytes: Buffer.byteLength(input.content) };
10739
11324
  if (previous.exists && previous.text === void 0) return output;
10740
11325
  return withToolPresentation(output, buildFileChangePresentation(
@@ -10757,6 +11342,7 @@ function createEditTool(workspace, options = {}) {
10757
11342
  abortIfNeeded(signal);
10758
11343
  const displayPath = guard.lexical(input.path);
10759
11344
  const path = await guard.existing(input.path);
11345
+ const expected = await expectedMutationVersion(path, options.observations);
10760
11346
  const contents = await readText(path);
10761
11347
  const hasCRLF = contents.includes("\r\n");
10762
11348
  const norm = (s) => hasCRLF ? s.replace(/\r\n/g, "\n") : s;
@@ -10772,7 +11358,9 @@ function createEditTool(workspace, options = {}) {
10772
11358
  const updatedLF = contentsLF.slice(0, first) + newTextLF + contentsLF.slice(first + oldTextLF.length);
10773
11359
  const updated = hasCRLF ? updatedLF.replace(/\n/g, "\r\n") : updatedLF;
10774
11360
  await options.beforeCommit?.({ path: displayPath, before: contents, after: updated, kind: "update" }, signal);
10775
- 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);
10776
11364
  return withToolPresentation(
10777
11365
  { path: displayPath, replacements: 1 },
10778
11366
  buildFileChangePresentation(displayPath, contents, updated, "update")
@@ -10793,10 +11381,12 @@ function createApplyPatchTool(workspace, options = {}) {
10793
11381
  const prepared = [];
10794
11382
  for (const change of changes) {
10795
11383
  const path = await guard.destination(change.path);
11384
+ const expected = await expectedMutationVersion(path, options.observations);
10796
11385
  const original = change.created ? await requireAbsent(guard, change.path) : await readText(await guard.existing(change.path));
10797
11386
  const applied = applyHunks(original, change.hunks);
10798
- prepared.push({ path, content: applied.content, change, hunks: applied.hunks });
11387
+ prepared.push({ path, content: applied.content, change, hunks: applied.hunks, expected });
10799
11388
  }
11389
+ for (const change of prepared) await assertMutationVersion(change.path, change.expected, options.observations);
10800
11390
  for (const change of prepared) {
10801
11391
  const before = change.change.created ? "" : await readText(change.path);
10802
11392
  await options.beforeCommit?.({
@@ -10805,43 +11395,49 @@ function createApplyPatchTool(workspace, options = {}) {
10805
11395
  after: change.content,
10806
11396
  kind: change.change.created ? "create" : "update"
10807
11397
  }, signal);
10808
- 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);
10809
11403
  }
10810
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));
10811
11407
  return withToolPresentation(
10812
11408
  { files: prepared.map((change) => change.path) },
10813
- buildPatchPresentation(first.path, first.change.created, first.hunks)
11409
+ relatedChanges.length === 0 ? primaryPresentation : { ...primaryPresentation, relatedChanges }
10814
11410
  );
10815
11411
  }
10816
11412
  };
10817
11413
  }
10818
11414
  function createPathGuard(workspace) {
10819
- const root = resolve16(workspace);
11415
+ const root = resolve17(workspace);
10820
11416
  const lexical = (input) => {
10821
- const candidate = resolve16(root, input);
10822
- 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");
10823
11419
  return candidate;
10824
11420
  };
10825
11421
  return {
10826
11422
  lexical,
10827
11423
  existing: async (input) => {
10828
11424
  const candidate = lexical(input);
10829
- const physical = await realpath7(candidate);
10830
- 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");
10831
11427
  return physical;
10832
11428
  },
10833
11429
  destination: async (input) => {
10834
11430
  const candidate = lexical(input);
10835
- const physicalRoot = await realpath7(root);
11431
+ const physicalRoot = await realpath8(root);
10836
11432
  let ancestor = candidate;
10837
11433
  while (true) {
10838
11434
  try {
10839
- const physical = await realpath7(ancestor);
10840
- 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");
10841
11437
  break;
10842
11438
  } catch (error) {
10843
11439
  if (!isMissing3(error)) throw error;
10844
- const parent = dirname11(ancestor);
11440
+ const parent = dirname12(ancestor);
10845
11441
  if (parent === ancestor) throw error;
10846
11442
  ancestor = parent;
10847
11443
  }
@@ -10850,9 +11446,9 @@ function createPathGuard(workspace) {
10850
11446
  }
10851
11447
  };
10852
11448
  }
10853
- async function atomicWrite(path, content, signal) {
11449
+ async function atomicWrite(path, content, signal, expectedVersion) {
10854
11450
  abortIfNeeded(signal);
10855
- await mkdir11(dirname11(path), { recursive: true });
11451
+ await mkdir11(dirname12(path), { recursive: true });
10856
11452
  const temporary = `${path}.${randomUUID8()}.tmp`;
10857
11453
  try {
10858
11454
  const handle = await open8(temporary, constants6.O_CREAT | constants6.O_EXCL | constants6.O_WRONLY, 384);
@@ -10863,19 +11459,46 @@ async function atomicWrite(path, content, signal) {
10863
11459
  await handle.close();
10864
11460
  }
10865
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
+ }
10866
11465
  await rename7(temporary, path);
10867
11466
  } finally {
10868
11467
  await rm6(temporary, { force: true });
10869
11468
  }
10870
11469
  }
10871
11470
  async function readText(path) {
10872
- const contents = await readFile11(path);
11471
+ const contents = await readFile12(path);
10873
11472
  if (isBinary(contents)) throw new Error("Cannot edit binary file as text");
10874
11473
  return contents.toString("utf8");
10875
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
+ }
10876
11499
  async function readOptionalPresentationText(path) {
10877
11500
  try {
10878
- const contents = await readFile11(path);
11501
+ const contents = await readFile12(path);
10879
11502
  return isBinary(contents) ? { exists: true } : { exists: true, text: contents.toString("utf8") };
10880
11503
  } catch (error) {
10881
11504
  return isMissing3(error) ? { exists: false } : { exists: true };
@@ -10959,9 +11582,9 @@ async function readBounded2(path, maxBytes, signal, openFile2) {
10959
11582
  await handle.close();
10960
11583
  }
10961
11584
  }
10962
- function within(root, candidate) {
10963
- const delta = relative14(root, candidate);
10964
- 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);
10965
11588
  }
10966
11589
  function forceFlag(value) {
10967
11590
  return value === true || value === "true";
@@ -11182,13 +11805,321 @@ function matchesLines(source, start, expected) {
11182
11805
  return start >= 0 && start + expected.length <= source.length && expected.every((line, index) => source[start + index] === line);
11183
11806
  }
11184
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
+
11185
12116
  // src/tools/lsp.ts
11186
12117
  import { spawn as spawn5 } from "child_process";
11187
12118
  import { existsSync as existsSync2 } from "fs";
11188
- import { access, constants as constants7, readFile as readFile12 } from "fs/promises";
11189
- 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";
11190
12121
  import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "url";
11191
- import { z as z19 } from "zod";
12122
+ import { z as z21 } from "zod";
11192
12123
  var LSP_REQUEST_TIMEOUT_MS = 15e3;
11193
12124
  var LspConnection = class {
11194
12125
  #process;
@@ -11285,8 +12216,8 @@ var LspConnection = class {
11285
12216
  const id = this.#nextId++;
11286
12217
  const request = { jsonrpc: "2.0", id, method, params };
11287
12218
  const timeout = AbortSignal.timeout(LSP_REQUEST_TIMEOUT_MS);
11288
- const promise = new Promise((resolve22, reject) => {
11289
- this.#pending.set(id, { resolve: resolve22, reject });
12219
+ const promise = new Promise((resolve25, reject) => {
12220
+ this.#pending.set(id, { resolve: resolve25, reject });
11290
12221
  timeout.addEventListener("abort", () => {
11291
12222
  if (this.#pending.has(id)) {
11292
12223
  this.#pending.delete(id);
@@ -11351,7 +12282,7 @@ var LspConnection = class {
11351
12282
  const url = new URL(uri);
11352
12283
  if (url.protocol !== "file:") return;
11353
12284
  const filePath = fileURLToPath3(url);
11354
- const text = await readFile12(filePath, "utf8");
12285
+ const text = await readFile13(filePath, "utf8");
11355
12286
  const current = this.#documents.get(uri);
11356
12287
  if (current === void 0) {
11357
12288
  this.#documents.set(uri, { text, version: 1 });
@@ -11402,8 +12333,8 @@ var LspBuffer = class {
11402
12333
  }
11403
12334
  }
11404
12335
  };
11405
- var TYPESCRIPT_CLI = resolve17(
11406
- dirname12(fileURLToPath3(import.meta.resolve("typescript/package.json"))),
12336
+ var TYPESCRIPT_CLI = resolve18(
12337
+ dirname13(fileURLToPath3(import.meta.resolve("typescript/package.json"))),
11407
12338
  "lib",
11408
12339
  "tsc.js"
11409
12340
  );
@@ -11444,7 +12375,7 @@ var RealLspManager = class {
11444
12375
  #serverConfigs;
11445
12376
  #onStatus;
11446
12377
  constructor(options) {
11447
- this.#workspace = resolve17(options.workspace);
12378
+ this.#workspace = resolve18(options.workspace);
11448
12379
  this.#serverConfigs = options.serverConfigs ?? KNOWN_SERVERS;
11449
12380
  this.#onStatus = options.onStatus;
11450
12381
  }
@@ -11493,7 +12424,7 @@ var RealLspManager = class {
11493
12424
  if (pending !== void 0) return pending;
11494
12425
  const config = this.#serverConfigs.find((c) => c.language === language);
11495
12426
  if (config === void 0) return void 0;
11496
- const hasRoot = config.rootFiles.some((file) => existsSync2(resolve17(this.#workspace, file)));
12427
+ const hasRoot = config.rootFiles.some((file) => existsSync2(resolve18(this.#workspace, file)));
11497
12428
  if (!hasRoot) return void 0;
11498
12429
  const startPromise = this.#startServer(config);
11499
12430
  this.#pendingStarts.set(language, startPromise);
@@ -11522,24 +12453,24 @@ var RealLspManager = class {
11522
12453
  return connection;
11523
12454
  }
11524
12455
  };
11525
- var LspFindRefsInput = z19.object({
11526
- file: z19.string().min(1),
11527
- line: z19.coerce.number().int().nonnegative(),
11528
- 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()
11529
12460
  });
11530
- var LspHoverInput = z19.object({
11531
- file: z19.string().min(1),
11532
- line: z19.coerce.number().int().nonnegative(),
11533
- 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()
11534
12465
  });
11535
- var LspDiagnosticsInput = z19.object({
11536
- file: z19.string().min(1)
12466
+ var LspDiagnosticsInput = z21.object({
12467
+ file: z21.string().min(1)
11537
12468
  });
11538
12469
  function createLspTools(workspace, options = {}) {
11539
- const root = resolve17(workspace);
12470
+ const root = resolve18(workspace);
11540
12471
  const manager = options.manager ?? new RealLspManager({ workspace: root, onStatus: options.onStatus });
11541
12472
  const guard = (input) => {
11542
- const candidate = resolve17(root, input);
12473
+ const candidate = resolve18(root, input);
11543
12474
  if (!isWithin6(root, candidate)) throw new Error("Path is outside the workspace");
11544
12475
  return candidate;
11545
12476
  };
@@ -11615,8 +12546,8 @@ function languageIdForPath(filePath) {
11615
12546
  }
11616
12547
  }
11617
12548
  function isWithin6(root, candidate) {
11618
- const delta = relative15(root, candidate);
11619
- return delta === "" || !delta.startsWith(`..${sep14}`) && delta !== ".." && !isAbsolute9(delta);
12549
+ const delta = relative16(root, candidate);
12550
+ return delta === "" || !delta.startsWith(`..${sep15}`) && delta !== ".." && !isAbsolute10(delta);
11620
12551
  }
11621
12552
  function uriToPath(uri) {
11622
12553
  try {
@@ -11648,18 +12579,18 @@ function severityLabel(severity) {
11648
12579
  }
11649
12580
 
11650
12581
  // src/tools/ask-user-question.ts
11651
- import { z as z20 } from "zod";
11652
- var QuestionOptionSchema = z20.object({
11653
- label: z20.string().trim().min(1),
11654
- 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)
11655
12586
  });
11656
- var QuestionSchema = z20.object({
11657
- question: z20.string().trim().min(1),
11658
- header: z20.string().trim().min(1),
11659
- 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)
11660
12591
  });
11661
- var AskUserQuestionInput = z20.object({
11662
- questions: z20.array(QuestionSchema).min(1).max(4)
12592
+ var AskUserQuestionInput = z22.object({
12593
+ questions: z22.array(QuestionSchema).min(1).max(4)
11663
12594
  });
11664
12595
  function hookAnswersFromUpdatedInput(updatedInput, questions) {
11665
12596
  if (typeof updatedInput !== "object" || updatedInput === null) return void 0;
@@ -11688,7 +12619,7 @@ var QuestionBridge = class {
11688
12619
  ask(questions, signal) {
11689
12620
  if (this.#pending !== void 0) return Promise.reject(new Error("A question is already pending"));
11690
12621
  if (signal.aborted) return Promise.reject(signal.reason);
11691
- return new Promise((resolve22, reject) => {
12622
+ return new Promise((resolve25, reject) => {
11692
12623
  const onAbort = () => {
11693
12624
  this.#pending = void 0;
11694
12625
  reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
@@ -11700,7 +12631,7 @@ var QuestionBridge = class {
11700
12631
  signal.removeEventListener("abort", onAbort);
11701
12632
  this.#pending = void 0;
11702
12633
  this.#onChange?.();
11703
- resolve22(answers);
12634
+ resolve25(answers);
11704
12635
  },
11705
12636
  reject: (error) => {
11706
12637
  signal.removeEventListener("abort", onAbort);
@@ -11743,20 +12674,20 @@ function createAskUserQuestionTool(handler) {
11743
12674
  }
11744
12675
 
11745
12676
  // src/tools/todo-write.ts
11746
- import { z as z21 } from "zod";
11747
- var TodoStatusSchema = z21.enum([
12677
+ import { z as z23 } from "zod";
12678
+ var TodoStatusSchema = z23.enum([
11748
12679
  "pending",
11749
12680
  "in_progress",
11750
12681
  "completed",
11751
12682
  "cancelled"
11752
12683
  ]);
11753
- var TodoItemSchema = z21.object({
11754
- content: z21.string().trim().min(1),
12684
+ var TodoItemSchema = z23.object({
12685
+ content: z23.string().trim().min(1),
11755
12686
  status: TodoStatusSchema,
11756
- activeForm: z21.string().trim().min(1)
12687
+ activeForm: z23.string().trim().min(1)
11757
12688
  });
11758
- var TodoWriteInput = z21.object({
11759
- todos: z21.array(TodoItemSchema).min(1).max(50)
12689
+ var TodoWriteInput = z23.object({
12690
+ todos: z23.array(TodoItemSchema).min(1).max(50)
11760
12691
  });
11761
12692
  function createTodoWriteTool() {
11762
12693
  return {
@@ -11791,38 +12722,38 @@ function createTodoWriteTool() {
11791
12722
 
11792
12723
  // src/tools/managed.ts
11793
12724
  import { randomUUID as randomUUID9 } from "crypto";
11794
- import { link, lstat as lstat9, mkdir as mkdir12, readFile as readFile13, readdir as readdir8, unlink, writeFile as writeFile7 } from "fs/promises";
11795
- import { dirname as dirname13, join as join14, resolve as resolve18 } from "path";
11796
- 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";
11797
12728
  var MANAGED_TOOL_VERSION = 1;
11798
12729
  var MAX_RECORD_BYTES = 256 * 1024;
11799
12730
  var TOOL_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
11800
- var JsonSchemaObject = z22.record(z22.string(), z22.unknown()).superRefine((schema, context) => {
12731
+ var JsonSchemaObject = z24.record(z24.string(), z24.unknown()).superRefine((schema, context) => {
11801
12732
  if (schema.type !== "object") {
11802
12733
  context.addIssue({ code: "custom", message: "Tool input JSON Schema must have type object" });
11803
12734
  }
11804
12735
  });
11805
- var RegisterManagedToolInputSchema = z22.object({
11806
- name: z22.string().regex(TOOL_NAME).describe("Unique callable tool name using letters, digits, and underscores"),
11807
- 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"),
11808
12739
  inputSchema: JsonSchemaObject.describe("Provider-facing JSON Schema with an object at the root"),
11809
- implementation: z22.string().min(1).max(64 * 1024).describe(
12740
+ implementation: z24.string().min(1).max(64 * 1024).describe(
11810
12741
  "Async JavaScript function body, or complete function/arrow expression, receiving input, signal, and context; return the tool result"
11811
12742
  ),
11812
- scope: z22.enum(["project", "global"]).default("project").describe(
12743
+ scope: z24.enum(["project", "global"]).default("project").describe(
11813
12744
  "Project persists in the current workspace; global persists for every workspace"
11814
12745
  ),
11815
- 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(
11816
12747
  "Agent roles allowed to call the generated tool; omit to allow both"
11817
12748
  )
11818
12749
  }).strict();
11819
- var RemoveManagedToolInputSchema = z22.object({
11820
- name: z22.string().regex(TOOL_NAME).describe("Managed tool name to remove"),
11821
- 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")
11822
12753
  }).strict();
11823
12754
  var ManagedToolRecordSchema = RegisterManagedToolInputSchema.extend({
11824
- version: z22.literal(MANAGED_TOOL_VERSION),
11825
- createdAt: z22.string().datetime()
12755
+ version: z24.literal(MANAGED_TOOL_VERSION),
12756
+ createdAt: z24.string().datetime()
11826
12757
  }).strict();
11827
12758
  var AsyncFunction = Object.getPrototypeOf(async function() {
11828
12759
  }).constructor;
@@ -11834,9 +12765,9 @@ var ManagedToolStore = class {
11834
12765
  #entries = /* @__PURE__ */ new Map();
11835
12766
  #diagnostics = [];
11836
12767
  constructor(options) {
11837
- this.#workspace = resolve18(options.workspace);
12768
+ this.#workspace = resolve19(options.workspace);
11838
12769
  this.#projectRoot = join14(this.#workspace, ".flavor", "tools");
11839
- this.#globalRoot = join14(resolve18(options.home), ".flavor-code", "tools");
12770
+ this.#globalRoot = join14(resolve19(options.home), ".flavor-code", "tools");
11840
12771
  this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
11841
12772
  }
11842
12773
  get diagnostics() {
@@ -11897,7 +12828,7 @@ var ManagedToolStore = class {
11897
12828
  if (matches.length === 0) throw new Error(`Managed tool "${input.name}" was not found`);
11898
12829
  if (matches.length > 1) throw new Error(`Managed tool "${input.name}" exists in multiple scopes; specify scope`);
11899
12830
  const entry2 = matches[0];
11900
- const info = await lstat9(entry2.path);
12831
+ const info = await lstat10(entry2.path);
11901
12832
  if (!info.isFile() || info.isSymbolicLink()) throw new Error(`Managed tool path is no longer a regular file: ${entry2.path}`);
11902
12833
  await unlink(entry2.path);
11903
12834
  this.#entries.delete(scopedKey(entry2.scope, entry2.record.name));
@@ -11923,10 +12854,10 @@ var ManagedToolStore = class {
11923
12854
  for (const name of names) {
11924
12855
  const path = join14(root, name);
11925
12856
  try {
11926
- const info = await lstat9(path);
12857
+ const info = await lstat10(path);
11927
12858
  if (!info.isFile() || info.isSymbolicLink()) throw new Error("Managed tool record must be a regular file");
11928
12859
  if (info.size > MAX_RECORD_BYTES) throw new Error(`Managed tool record exceeds ${MAX_RECORD_BYTES} bytes`);
11929
- const record = ManagedToolRecordSchema.parse(JSON.parse(await readFile13(path, "utf8")));
12860
+ const record = ManagedToolRecordSchema.parse(JSON.parse(await readFile14(path, "utf8")));
11930
12861
  if (record.scope !== scope2) throw new Error(`Managed tool record scope must match its ${scope2} directory`);
11931
12862
  const key = scopedKey(scope2, record.name);
11932
12863
  if (this.#entries.has(key)) throw new Error(`Duplicate managed tool name in ${scope2} scope: ${record.name}`);
@@ -12009,7 +12940,7 @@ function createManagedToolManagementTools(options) {
12009
12940
  const listTools = {
12010
12941
  name: "ListRegisteredTools",
12011
12942
  description: "List durable tools created through RegisterTool, including scope, path, and active precedence",
12012
- inputSchema: z22.object({}).strict(),
12943
+ inputSchema: z24.object({}).strict(),
12013
12944
  agents: mainOnly,
12014
12945
  paths: () => [],
12015
12946
  execute: async (_input, signal) => {
@@ -12022,7 +12953,7 @@ function createManagedToolManagementTools(options) {
12022
12953
  function compileDefinition(record, scope2, workspace) {
12023
12954
  let inputSchema;
12024
12955
  try {
12025
- inputSchema = z22.fromJSONSchema(record.inputSchema);
12956
+ inputSchema = z24.fromJSONSchema(record.inputSchema);
12026
12957
  } catch (error) {
12027
12958
  throw new Error(`Invalid input schema for managed tool "${record.name}": ${message(error)}`);
12028
12959
  }
@@ -12076,7 +13007,7 @@ ${sourceUrl}`
12076
13007
  }
12077
13008
  }
12078
13009
  async function writeExclusive(path, content) {
12079
- await mkdir12(dirname13(path), { recursive: true });
13010
+ await mkdir12(dirname14(path), { recursive: true });
12080
13011
  const temporary = `${path}.${randomUUID9()}.tmp`;
12081
13012
  try {
12082
13013
  await writeFile7(temporary, content, { encoding: "utf8", flag: "wx", mode: 384 });
@@ -12122,7 +13053,7 @@ function isMissing4(error) {
12122
13053
  }
12123
13054
 
12124
13055
  // src/hallucination/confidence.ts
12125
- import { z as z23 } from "zod";
13056
+ import { z as z25 } from "zod";
12126
13057
 
12127
13058
  // src/hallucination/types.ts
12128
13059
  var DEFAULT_SLIDING_WINDOW_SIZE = 20;
@@ -12135,12 +13066,12 @@ var DEFAULT_EVALUATION_TIMEOUT_MS = 2e3;
12135
13066
  var MAX_QUERY_CHARS = 5e3;
12136
13067
  var MAX_OUTPUT_CHARS = 1e4;
12137
13068
  var MAX_EVIDENCE_CHARS = 6e3;
12138
- var ConfidenceSchema = z23.object({
12139
- taskAlignment: z23.number(),
12140
- evidenceGrounding: z23.number(),
12141
- processReliability: z23.number(),
12142
- reason: z23.string(),
12143
- 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)
12144
13075
  });
12145
13076
  var PROMPT = `You are a fast, evidence-aware task-completion verifier.
12146
13077
 
@@ -12467,7 +13398,7 @@ function buildWarningMessages(report, languageTag) {
12467
13398
  }
12468
13399
 
12469
13400
  // src/hallucination/sliding-window.ts
12470
- import { createHash as createHash6 } from "crypto";
13401
+ import { createHash as createHash7 } from "crypto";
12471
13402
  var SlidingWindow = class {
12472
13403
  #windowSize;
12473
13404
  #threshold;
@@ -12483,7 +13414,7 @@ var SlidingWindow = class {
12483
13414
  }
12484
13415
  hash(toolName, params) {
12485
13416
  const sorted = JSON.stringify(params, sortedKeys(params));
12486
- return createHash6("sha256").update(`${toolName}:${sorted}`).digest("hex");
13417
+ return createHash7("sha256").update(`${toolName}:${sorted}`).digest("hex");
12487
13418
  }
12488
13419
  push(toolName, params) {
12489
13420
  const hash = this.hash(toolName, params);
@@ -12838,9 +13769,9 @@ ${total}`;
12838
13769
  }
12839
13770
 
12840
13771
  // src/memory/store.ts
12841
- import { createHash as createHash7 } from "crypto";
12842
- import { readFile as readFile14, rm as rm7 } from "fs/promises";
12843
- 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";
12844
13775
 
12845
13776
  // src/memory/similarity.ts
12846
13777
  function normalizeForSimilarity(value) {
@@ -12942,7 +13873,7 @@ var MemoryStore = class {
12942
13873
  constructor(options) {
12943
13874
  if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 1) throw new Error("maxEntries must be an integer of at least 1");
12944
13875
  if (!Number.isSafeInteger(options.maxEntryChars) || options.maxEntryChars < 1) throw new Error("maxEntryChars must be an integer of at least 1");
12945
- this.workspace = resolve19(options.workspace);
13876
+ this.workspace = resolve20(options.workspace);
12946
13877
  this.#memoryRoot = join15(this.workspace, ".flavor", "memory");
12947
13878
  this.path = join15(this.#memoryRoot, "MEMORY.md");
12948
13879
  this.#behaviorPath = join15(this.#memoryRoot, "behavior.json");
@@ -13163,14 +14094,14 @@ var MemoryStore = class {
13163
14094
  });
13164
14095
  }
13165
14096
  async #readTaskItem(reference) {
13166
- const document = decodeTask(await readFile14(this.#resolveContentPath(reference.contentPath), "utf8"));
14097
+ const document = decodeTask(await readFile15(this.#resolveContentPath(reference.contentPath), "utf8"));
13167
14098
  return document.items.find((item) => item.id === reference.id)?.content ?? reference.summary;
13168
14099
  }
13169
14100
  #resolveContentPath(contentPath) {
13170
14101
  if (!/^tasks\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(contentPath)) throw new Error("Invalid memory content path");
13171
- const path = resolve19(this.#memoryRoot, ...contentPath.split("/"));
13172
- const rel = relative16(this.#memoryRoot, path);
13173
- 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");
13174
14105
  return path;
13175
14106
  }
13176
14107
  async #updateIndex(update) {
@@ -13324,7 +14255,7 @@ function sanitizeTopicKey(value) {
13324
14255
  return value.normalize("NFKC").toLocaleLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 128);
13325
14256
  }
13326
14257
  function memoryId(type, content) {
13327
- 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);
13328
14259
  }
13329
14260
  function assertTaskId(taskId) {
13330
14261
  if (!TASK_ID.test(taskId)) throw new Error(`Invalid memory task id: ${taskId}`);
@@ -13639,8 +14570,8 @@ function createExecutionEnvironment(workspace, config) {
13639
14570
  }
13640
14571
 
13641
14572
  // src/ide/client.ts
13642
- import { readFile as readFile15, readdir as readdir9, stat as stat6 } from "fs/promises";
13643
- 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";
13644
14575
  var FlavorIdeClient = class {
13645
14576
  #workspace;
13646
14577
  #home;
@@ -13651,8 +14582,8 @@ var FlavorIdeClient = class {
13651
14582
  #sessionId;
13652
14583
  #eventTail = Promise.resolve();
13653
14584
  constructor(options) {
13654
- this.#workspace = resolve20(options.workspace);
13655
- this.#home = resolve20(options.home);
14585
+ this.#workspace = resolve21(options.workspace);
14586
+ this.#home = resolve21(options.home);
13656
14587
  this.#environment = options.environment ?? process.env;
13657
14588
  this.#fetch = options.fetch ?? fetch;
13658
14589
  }
@@ -13746,7 +14677,7 @@ var FlavorIdeClient = class {
13746
14677
  const candidates = (await Promise.all(entries.map(async (name) => {
13747
14678
  const path = join16(directory, name);
13748
14679
  try {
13749
- const [contents, info] = await Promise.all([readFile15(path, "utf8"), stat6(path)]);
14680
+ const [contents, info] = await Promise.all([readFile16(path, "utf8"), stat6(path)]);
13750
14681
  const lock = parseLockfile(contents);
13751
14682
  return lock === void 0 ? void 0 : { lock, modified: info.mtimeMs };
13752
14683
  } catch {
@@ -13850,19 +14781,385 @@ function parsePort(value) {
13850
14781
  return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
13851
14782
  }
13852
14783
  function isWithin7(parent, child) {
13853
- const base = normalize(resolve20(parent));
13854
- const target = normalize(resolve20(child));
13855
- 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}`);
13856
14787
  }
13857
14788
  function normalize(path) {
13858
14789
  const normalized = path.normalize("NFC");
13859
14790
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
13860
14791
  }
13861
14792
  function relativeDisplay(workspace, filePath) {
13862
- const path = relative17(workspace, filePath).replaceAll("\\", "/");
14793
+ const path = relative18(workspace, filePath).replaceAll("\\", "/");
13863
14794
  return path && !path.startsWith("../") ? path : basename5(filePath);
13864
14795
  }
13865
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
+
13866
15163
  // src/production.ts
13867
15164
  var INTERRUPTED_TASK_PLAN_CONTEXT = [
13868
15165
  "The previous turn's task plan was cancelled and archived, so it is no longer active.",
@@ -13893,7 +15190,7 @@ var ApprovalBridge = class {
13893
15190
  request(request, signal = new AbortController().signal) {
13894
15191
  if (this.#settle !== void 0) return Promise.resolve("deny");
13895
15192
  if (signal.aborted) return Promise.resolve("deny");
13896
- this.#pending = { id: randomUUID10(), ...request };
15193
+ this.#pending = { id: randomUUID12(), ...request };
13897
15194
  this.#onChange?.();
13898
15195
  return new Promise((resolvePromise) => {
13899
15196
  this.#settle = resolvePromise;
@@ -13914,8 +15211,8 @@ var ApprovalBridge = class {
13914
15211
  }
13915
15212
  };
13916
15213
  async function createProductionRuntime(options) {
13917
- const workspace = resolve21(options.workspace ?? process.cwd());
13918
- const home = resolve21(options.home ?? homedir());
15214
+ const workspace = resolve24(options.workspace ?? process.cwd());
15215
+ const home = resolve24(options.home ?? homedir());
13919
15216
  const environment = options.environment ?? process.env;
13920
15217
  const ide = new FlavorIdeClient({ workspace, home, environment });
13921
15218
  const ideReady = ide.initialize();
@@ -14048,17 +15345,29 @@ async function createProductionRuntime(options) {
14048
15345
  return questions.ask(qs, signal);
14049
15346
  };
14050
15347
  const executionEnvironment = createExecutionEnvironment(workspace, config.execution);
14051
- 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
+ };
14052
15356
  const tools = [
14053
- createReadTool(workspace),
15357
+ createReadTool(workspace, { observations }),
14054
15358
  createWriteTool(workspace, fileMutationOptions),
14055
15359
  createEditTool(workspace, fileMutationOptions),
14056
15360
  createApplyPatchTool(workspace, fileMutationOptions),
14057
15361
  createGlobTool(workspace),
14058
15362
  createGrepTool(workspace),
14059
15363
  createShellTool(workspace, {
15364
+ jobs,
14060
15365
  ...executionEnvironment === void 0 ? {} : { executionEnvironment }
14061
15366
  }),
15367
+ createWebFetchTool(),
15368
+ createWebSearchTool(),
15369
+ ...createJobTools(jobs),
15370
+ ...createTerminalTools(terminals, workspace),
14062
15371
  ...createLspTools(workspace, {
14063
15372
  onStatus: (message2) => emitOutput({ type: "notice", message: message2 })
14064
15373
  }),
@@ -14190,7 +15499,9 @@ async function createProductionRuntime(options) {
14190
15499
  const skillsReady = skills.discover();
14191
15500
  skillsReady.catch(() => void 0);
14192
15501
  tools.push(createSkillResourceTool(skills));
15502
+ if (options.extraTools !== void 0) tools.push(...options.extraTools);
14193
15503
  const flavor = await optionalText2(join17(workspace, "FLAVOR.md"));
15504
+ const instructionBaseline = await workspaceInstructions.baseline();
14194
15505
  let memoryContext;
14195
15506
  if (memoryStore !== void 0) {
14196
15507
  try {
@@ -14213,7 +15524,7 @@ async function createProductionRuntime(options) {
14213
15524
  let taskResults = { ...recovered?.tasks.results ?? {} };
14214
15525
  const subagentStartedAt = {};
14215
15526
  const subagentElapsedMs = {};
14216
- 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)}`;
14217
15528
  setUsageSession(sessionId);
14218
15529
  ideSessionId = sessionId;
14219
15530
  await ideReady;
@@ -14383,6 +15694,7 @@ async function createProductionRuntime(options) {
14383
15694
  })
14384
15695
  ],
14385
15696
  ...flavor === void 0 ? {} : { flavor },
15697
+ ...instructionBaseline === "" ? {} : { workspaceInstructions: instructionBaseline },
14386
15698
  ...memoryContext === void 0 ? {} : { memory: memoryContext },
14387
15699
  ...taskState === void 0 ? {} : { taskState },
14388
15700
  userMemory: () => userMemoryContext ?? "",
@@ -14420,6 +15732,7 @@ async function createProductionRuntime(options) {
14420
15732
  maxIterationsMain: config.maxIterations.main,
14421
15733
  maxIterationsSubagent: config.maxIterations.subagent,
14422
15734
  hasActiveProgress,
15735
+ afterToolSuccess: async (_tool, paths, _input, _output, context) => workspaceInstructions.discover(paths, context.ownerId ?? context.agent),
14423
15736
  approve: resolveToolApproval
14424
15737
  });
14425
15738
  harnessCreated = true;
@@ -14520,7 +15833,7 @@ async function createProductionRuntime(options) {
14520
15833
  const prompt = String(event.payload.prompt);
14521
15834
  const reassessInterruptedPlan = interruptedTaskPlanNeedsReassessment && !prompt.startsWith("/");
14522
15835
  timelineState = transcriptReducer(timelineState, { type: "submit", prompt });
14523
- automaticMemoryTask = !prompt.startsWith("/");
15836
+ automaticMemoryTask = !prompt.startsWith("/") && harness.permissionProfile !== "d2c";
14524
15837
  if (automaticMemoryTask) memoryReviews.dismissAll();
14525
15838
  if (!prompt.startsWith("/") && memoryLifecycle.status === "completed") {
14526
15839
  memoryLifecycle = {
@@ -14645,16 +15958,23 @@ async function createProductionRuntime(options) {
14645
15958
  return;
14646
15959
  }
14647
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 };
14648
15964
  const loopTools = [
14649
- createReadTool(input.workspace),
14650
- createWriteTool(input.workspace),
14651
- createEditTool(input.workspace),
14652
- createApplyPatchTool(input.workspace),
15965
+ createReadTool(input.workspace, { observations: loopObservations }),
15966
+ createWriteTool(input.workspace, loopMutationOptions),
15967
+ createEditTool(input.workspace, loopMutationOptions),
15968
+ createApplyPatchTool(input.workspace, loopMutationOptions),
14653
15969
  createGlobTool(input.workspace),
14654
15970
  createGrepTool(input.workspace),
14655
15971
  createShellTool(input.workspace, {
15972
+ jobs,
14656
15973
  ...loopExecutionEnvironment === void 0 ? {} : { executionEnvironment: loopExecutionEnvironment }
14657
15974
  }),
15975
+ createWebFetchTool(),
15976
+ createWebSearchTool(),
15977
+ ...createJobTools(jobs),
14658
15978
  ...createLspTools(input.workspace, {
14659
15979
  onStatus: (status) => emitOutput({ type: "notice", message: status })
14660
15980
  }),
@@ -14662,6 +15982,7 @@ async function createProductionRuntime(options) {
14662
15982
  ...mcpTools
14663
15983
  ];
14664
15984
  const loopFlavor = await optionalText2(join17(input.workspace, "FLAVOR.md"));
15985
+ const loopInstructionBaseline = await loopInstructions.baseline();
14665
15986
  const loopEnvironment = createPromptEnvironment({
14666
15987
  now: /* @__PURE__ */ new Date(),
14667
15988
  platform: process.platform,
@@ -14691,6 +16012,7 @@ async function createProductionRuntime(options) {
14691
16012
  })
14692
16013
  ],
14693
16014
  ...loopFlavor === void 0 ? {} : { flavor: loopFlavor },
16015
+ ...loopInstructionBaseline === "" ? {} : { workspaceInstructions: loopInstructionBaseline },
14694
16016
  ...memoryContext === void 0 ? {} : { memory: memoryContext },
14695
16017
  userMemory: () => userMemoryContext ?? "",
14696
16018
  ...compactAtChars === void 0 ? {} : { compactAtChars },
@@ -14723,6 +16045,7 @@ async function createProductionRuntime(options) {
14723
16045
  maxIterationsMain: config.maxIterations.main,
14724
16046
  maxIterationsSubagent: config.maxIterations.subagent,
14725
16047
  loopMode: true,
16048
+ afterToolSuccess: async (_tool, paths, _input, _output, context) => loopInstructions.discover(paths, context.ownerId ?? context.agent),
14726
16049
  approve: resolveToolApproval
14727
16050
  });
14728
16051
  try {
@@ -14774,7 +16097,7 @@ async function createProductionRuntime(options) {
14774
16097
  return answers[0] === "Continue" ? "approved" : "rejected";
14775
16098
  },
14776
16099
  fingerprint: workspaceFingerprint,
14777
- 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)}`
14778
16101
  });
14779
16102
  const goalStore = new GoalStore({ workspace });
14780
16103
  const goalOrchestrator = new GoalOrchestrator({
@@ -14787,13 +16110,13 @@ async function createProductionRuntime(options) {
14787
16110
  maxStallStreak: 2,
14788
16111
  persistence: goalStore,
14789
16112
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
14790
- 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)}`,
14791
16114
  runWorker: ({ workspace: goalWorkspace, prompt, signal }) => runLoopWorker({ workspace: goalWorkspace, prompt, signal })
14792
16115
  });
14793
16116
  let sessionHistory = await SessionHistory.open({
14794
16117
  workspace,
14795
16118
  sessionId,
14796
- restoreContext: (snapshot2) => harness.main.context.restore(snapshot2)
16119
+ restoreContext: (snapshot4) => harness.main.context.restore(snapshot4)
14797
16120
  });
14798
16121
  const services = {
14799
16122
  hooks,
@@ -14905,7 +16228,7 @@ async function createProductionRuntime(options) {
14905
16228
  tasks: () => ({ plan: taskPlan, graph: taskGraph, states: taskStates, results: taskResults }),
14906
16229
  audit: async (toolFilter) => {
14907
16230
  try {
14908
- const raw = await readFile16(auditLogger.path, "utf8");
16231
+ const raw = await readFile17(auditLogger.path, "utf8");
14909
16232
  const lines = raw.trim().split("\n").filter((line) => line.length > 0);
14910
16233
  const entries = lines.map((line) => {
14911
16234
  try {
@@ -14944,7 +16267,7 @@ ${summary2}`;
14944
16267
  },
14945
16268
  usage: async () => {
14946
16269
  try {
14947
- const raw = await readFile16(usageLogPath(), "utf8");
16270
+ const raw = await readFile17(usageLogPath(), "utf8");
14948
16271
  return formatUsageSummary(summarizeUsage(parseUsageEntries(raw)));
14949
16272
  } catch (error) {
14950
16273
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
@@ -14972,7 +16295,7 @@ ${summary2}`;
14972
16295
  for (const key of Object.keys(subagentStartedAt)) delete subagentStartedAt[key];
14973
16296
  for (const key of Object.keys(subagentElapsedMs)) delete subagentElapsedMs[key];
14974
16297
  const previousIdeSessionId = ideSessionId;
14975
- 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)}`;
14976
16299
  setUsageSession(sessionId);
14977
16300
  ideSessionId = sessionId;
14978
16301
  if (previousIdeSessionId !== void 0) await ide.endSession(previousIdeSessionId);
@@ -14981,7 +16304,7 @@ ${summary2}`;
14981
16304
  sessionHistory = await SessionHistory.open({
14982
16305
  workspace,
14983
16306
  sessionId,
14984
- restoreContext: (snapshot2) => harness.main.context.restore(snapshot2)
16307
+ restoreContext: (snapshot4) => harness.main.context.restore(snapshot4)
14985
16308
  });
14986
16309
  memoryLifecycle = { status: "active", taskId: createMemoryTaskId(), messageStart: 0 };
14987
16310
  timelineState = createTranscriptState();
@@ -15087,7 +16410,10 @@ ${renderMemoryDocument(entries)}`;
15087
16410
  baseURL: next.baseURL,
15088
16411
  ...next.maxOutputTokens === void 0 ? {} : { maxOutputTokens: next.maxOutputTokens }
15089
16412
  };
15090
- registry.register(next.providerId, next.apiType === "anthropic" ? new AnthropicModelAdapter(adapterOptions) : new OpenAIModelAdapter(adapterOptions));
16413
+ registry.register(next.providerId, next.apiType === "anthropic" ? new AnthropicModelAdapter({
16414
+ ...adapterOptions,
16415
+ ...providerConfig?.claudeClient === true ? { headers: CLAUDE_CLIENT_HEADERS } : {}
16416
+ }) : new OpenAIModelAdapter(adapterOptions));
15091
16417
  effectiveLlm = next;
15092
16418
  mainModel = `${next.providerId}:${next.defaultModel}`;
15093
16419
  childModel = `${next.providerId}:${next.cheapModel}`;
@@ -15127,13 +16453,19 @@ ${renderMemoryDocument(entries)}`;
15127
16453
  sleepScheduler.start();
15128
16454
  }
15129
16455
  const session = new FlavorSession(services);
16456
+ const authorization = {
16457
+ permissionProfile: () => harness.permissionProfile,
16458
+ setPermissionProfile: (profile) => harness.setPermissionProfile(profile)
16459
+ };
15130
16460
  let disposed = false;
15131
16461
  return {
15132
16462
  session,
15133
16463
  services,
16464
+ authorization,
15134
16465
  approvals,
15135
16466
  memoryReviews,
15136
16467
  restoredTranscript,
16468
+ jobs: { list: () => jobs.list(), subscribe: (listener) => jobs.subscribe(listener) },
15137
16469
  get sessionId() {
15138
16470
  return sessionId;
15139
16471
  },
@@ -15150,6 +16482,8 @@ ${renderMemoryDocument(entries)}`;
15150
16482
  await persistTail;
15151
16483
  if (ideSessionId !== void 0) await ide.endSession(ideSessionId);
15152
16484
  await executionEnvironment?.dispose();
16485
+ terminals.dispose();
16486
+ await jobs.dispose();
15153
16487
  auditLogger.close();
15154
16488
  memoryReviews.dispose();
15155
16489
  await cleanupProduction(approvals, questions, pluginHost, mcpManager, harness);
@@ -15162,6 +16496,8 @@ ${renderMemoryDocument(entries)}`;
15162
16496
  if (ideSessionId !== void 0) await ide.endSession(ideSessionId);
15163
16497
  await sleepScheduler?.dispose();
15164
16498
  await executionEnvironment?.dispose();
16499
+ terminals.dispose();
16500
+ await jobs.dispose();
15165
16501
  await cleanupProduction(approvals, questions, pluginHost, mcpManager, harnessCreated ? harness : void 0);
15166
16502
  } catch (cleanupError) {
15167
16503
  attachCleanupError(primaryError, cleanupError);
@@ -15276,7 +16612,7 @@ function terminalProgressState(status) {
15276
16612
  return "failed";
15277
16613
  }
15278
16614
  async function workspaceFingerprint(workspace) {
15279
- const hash = createHash8("sha256");
16615
+ const hash = createHash9("sha256");
15280
16616
  const diff = await execFileNoThrow(
15281
16617
  "git",
15282
16618
  ["-C", workspace, "diff", "--no-ext-diff", "--binary", "HEAD"],
@@ -15292,13 +16628,13 @@ async function workspaceFingerprint(workspace) {
15292
16628
  let remaining = 5 * 1024 * 1024;
15293
16629
  for (const name of untracked.stdout.split("\0").filter(Boolean).sort()) {
15294
16630
  if (name === ".flavor/loops" || name.startsWith(".flavor/loops/")) continue;
15295
- const path = resolve21(workspace, name);
15296
- const relativePath = relative18(resolve21(workspace), path);
15297
- 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;
15298
16634
  hash.update(name);
15299
16635
  if (remaining <= 0) continue;
15300
16636
  try {
15301
- const content = await readFile16(path);
16637
+ const content = await readFile17(path);
15302
16638
  const slice = content.subarray(0, remaining);
15303
16639
  hash.update(slice);
15304
16640
  remaining -= slice.length;
@@ -15495,7 +16831,10 @@ async function registerConfiguredAdapters(providers, registry, environment, diag
15495
16831
  ...runtimeProvider.baseURL === void 0 ? {} : { baseURL: runtimeProvider.baseURL },
15496
16832
  ...runtimeProvider.maxOutputTokens === void 0 ? {} : { maxOutputTokens: runtimeProvider.maxOutputTokens }
15497
16833
  };
15498
- const adapter = apiProtocol === "anthropic" ? new AnthropicModelAdapter(adapterOptions) : new OpenAIModelAdapter(adapterOptions);
16834
+ const adapter = apiProtocol === "anthropic" ? new AnthropicModelAdapter({
16835
+ ...adapterOptions,
16836
+ ...runtimeProvider.claudeClient === true ? { headers: CLAUDE_CLIENT_HEADERS } : {}
16837
+ }) : new OpenAIModelAdapter(adapterOptions);
15499
16838
  const cacheProfile = resolveCacheProfile({ apiType: apiProtocol, baseURL: runtimeProvider.baseURL });
15500
16839
  if (apiProtocol === "openai" && isDashScopeBaseURL(runtimeProvider.baseURL)) {
15501
16840
  diagnostics.push(
@@ -15687,7 +17026,7 @@ async function generateSleepReview(registry, modelId, prompt, signal) {
15687
17026
  }
15688
17027
  async function optionalText2(path) {
15689
17028
  try {
15690
- return await readFile16(path, "utf8");
17029
+ return await readFile17(path, "utf8");
15691
17030
  } catch (error) {
15692
17031
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return void 0;
15693
17032
  throw error;
@@ -15700,10 +17039,10 @@ function remove(items, item) {
15700
17039
  function sameToolName(left, right) {
15701
17040
  return left.toLowerCase() === right.toLowerCase();
15702
17041
  }
15703
- function storedConversation(snapshot2) {
17042
+ function storedConversation(snapshot4) {
15704
17043
  return {
15705
- ...snapshot2.compact === void 0 ? {} : { compact: snapshot2.compact },
15706
- messages: snapshot2.messages.flatMap((message2) => {
17044
+ ...snapshot4.compact === void 0 ? {} : { compact: snapshot4.compact },
17045
+ messages: snapshot4.messages.flatMap((message2) => {
15707
17046
  if (message2.role === "system") return [];
15708
17047
  const metadata = {
15709
17048
  ...message2.toolCallId === void 0 ? {} : { toolCallId: message2.toolCallId },
@@ -15826,10 +17165,10 @@ async function* runGoalSession(orchestrator, goal, signal) {
15826
17165
  }
15827
17166
  function memoryTranscriptHash(messages) {
15828
17167
  const visible = messages.filter((item) => item.role === "user" || item.role === "assistant").map((item) => `${item.role}\0${modelContentText(item.content).trim()}`).join("\n");
15829
- return createHash8("sha256").update(visible, "utf8").digest("hex");
17168
+ return createHash9("sha256").update(visible, "utf8").digest("hex");
15830
17169
  }
15831
17170
  function createMemoryTaskId() {
15832
- return `memory-${randomUUID10()}`;
17171
+ return `memory-${randomUUID12()}`;
15833
17172
  }
15834
17173
 
15835
17174
  export {