@wrongstack/core 0.289.0 → 0.291.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/dist/boot.d.ts.map +1 -1
  2. package/dist/chronicle/domain-adapter.d.ts.map +1 -1
  3. package/dist/chronicle/index.d.ts +1 -1
  4. package/dist/chronicle/index.d.ts.map +1 -1
  5. package/dist/chronicle/index.js +964 -245
  6. package/dist/chronicle/index.js.map +3 -3
  7. package/dist/chronicle/journal.d.ts +35 -3
  8. package/dist/chronicle/journal.d.ts.map +1 -1
  9. package/dist/chronicle/query.d.ts +8 -5
  10. package/dist/chronicle/query.d.ts.map +1 -1
  11. package/dist/chronicle/tool-adapter.d.ts.map +1 -1
  12. package/dist/coordination/agent-subagent-runner.d.ts +6 -0
  13. package/dist/coordination/agent-subagent-runner.d.ts.map +1 -1
  14. package/dist/coordination/agents/phase6-domain.d.ts.map +1 -1
  15. package/dist/coordination/brain.d.ts +2 -1
  16. package/dist/coordination/brain.d.ts.map +1 -1
  17. package/dist/coordination/collab-debug.d.ts +0 -9
  18. package/dist/coordination/collab-debug.d.ts.map +1 -1
  19. package/dist/coordination/coordinator/error-classifier.d.ts.map +1 -1
  20. package/dist/coordination/dep-watcher.d.ts +2 -1
  21. package/dist/coordination/dep-watcher.d.ts.map +1 -1
  22. package/dist/coordination/director/director-collab.d.ts +1 -1
  23. package/dist/coordination/director/director-collab.d.ts.map +1 -1
  24. package/dist/coordination/director-tools.d.ts.map +1 -1
  25. package/dist/coordination/director.d.ts +19 -1
  26. package/dist/coordination/director.d.ts.map +1 -1
  27. package/dist/coordination/fleet-event-validation.d.ts +0 -7
  28. package/dist/coordination/fleet-event-validation.d.ts.map +1 -1
  29. package/dist/coordination/fleet-spawn.d.ts.map +1 -1
  30. package/dist/coordination/fleet-supervisor.d.ts +1 -1
  31. package/dist/coordination/fleet-supervisor.d.ts.map +1 -1
  32. package/dist/coordination/fleet.d.ts +0 -44
  33. package/dist/coordination/fleet.d.ts.map +1 -1
  34. package/dist/coordination/index.d.ts +2 -2
  35. package/dist/coordination/index.d.ts.map +1 -1
  36. package/dist/coordination/index.js +733 -111
  37. package/dist/coordination/index.js.map +4 -4
  38. package/dist/coordination/knowledge-graph.d.ts +2 -1
  39. package/dist/coordination/knowledge-graph.d.ts.map +1 -1
  40. package/dist/coordination/large-answer-store.d.ts +0 -6
  41. package/dist/coordination/large-answer-store.d.ts.map +1 -1
  42. package/dist/coordination/mail-tools.d.ts.map +1 -1
  43. package/dist/coordination/mailbox-http-router.d.ts.map +1 -1
  44. package/dist/coordination/mailbox-message-codec.d.ts +35 -0
  45. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
  46. package/dist/coordination/mailbox-tool.d.ts +1 -1
  47. package/dist/coordination/mailbox-tool.d.ts.map +1 -1
  48. package/dist/coordination/mailbox-types.d.ts +151 -0
  49. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  50. package/dist/coordination/provider-status-tracker.d.ts +27 -2
  51. package/dist/coordination/provider-status-tracker.d.ts.map +1 -1
  52. package/dist/coordination/subagent-budget.d.ts +2 -1
  53. package/dist/coordination/subagent-budget.d.ts.map +1 -1
  54. package/dist/core/agent-response.d.ts +2 -1
  55. package/dist/core/agent-response.d.ts.map +1 -1
  56. package/dist/core/agent-tools.d.ts.map +1 -1
  57. package/dist/core/context.d.ts +51 -0
  58. package/dist/core/context.d.ts.map +1 -1
  59. package/dist/core/fallback-model.d.ts +1 -1
  60. package/dist/core/fallback-model.d.ts.map +1 -1
  61. package/dist/core/fallback-profile-manager.d.ts +1 -1
  62. package/dist/core/fallback-profile-manager.d.ts.map +1 -1
  63. package/dist/core/fleet-pulse.d.ts +2 -1
  64. package/dist/core/fleet-pulse.d.ts.map +1 -1
  65. package/dist/core/iteration-limit.d.ts +2 -1
  66. package/dist/core/iteration-limit.d.ts.map +1 -1
  67. package/dist/core/mailbox-loop.d.ts +11 -1
  68. package/dist/core/mailbox-loop.d.ts.map +1 -1
  69. package/dist/core/model-availability-calendar.d.ts +27 -0
  70. package/dist/core/model-availability-calendar.d.ts.map +1 -0
  71. package/dist/core/system-prompt-builder.d.ts +2 -1
  72. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  73. package/dist/defaults/index.js +1459 -560
  74. package/dist/defaults/index.js.map +4 -4
  75. package/dist/execution/auto-compaction-middleware.d.ts +3 -2
  76. package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
  77. package/dist/execution/enhance-recovery.d.ts +8 -0
  78. package/dist/execution/enhance-recovery.d.ts.map +1 -1
  79. package/dist/execution/error-handler.d.ts +2 -2
  80. package/dist/execution/error-handler.d.ts.map +1 -1
  81. package/dist/execution/index.js +643 -134
  82. package/dist/execution/index.js.map +4 -4
  83. package/dist/execution/intelligent-compactor.d.ts +0 -16
  84. package/dist/execution/intelligent-compactor.d.ts.map +1 -1
  85. package/dist/execution/one-shot-llm.d.ts.map +1 -1
  86. package/dist/execution/retry-policy.d.ts +0 -13
  87. package/dist/execution/retry-policy.d.ts.map +1 -1
  88. package/dist/execution/skill-loader.d.ts.map +1 -1
  89. package/dist/execution/tool-executor.d.ts.map +1 -1
  90. package/dist/hq/auth-store.d.ts.map +1 -1
  91. package/dist/hq/index.js +39 -1
  92. package/dist/hq/index.js.map +3 -3
  93. package/dist/index.d.ts +32 -30
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +22263 -19999
  96. package/dist/index.js.map +4 -4
  97. package/dist/infrastructure/index.js +59 -5
  98. package/dist/infrastructure/index.js.map +2 -2
  99. package/dist/kernel/events/agent-events.d.ts +2 -0
  100. package/dist/kernel/events/agent-events.d.ts.map +1 -1
  101. package/dist/kernel/events/file-events.d.ts +46 -0
  102. package/dist/kernel/events/file-events.d.ts.map +1 -1
  103. package/dist/kernel/events/memory-events.d.ts +9 -0
  104. package/dist/kernel/events/memory-events.d.ts.map +1 -1
  105. package/dist/kernel/events/provider-events.d.ts +2 -1
  106. package/dist/kernel/events/provider-events.d.ts.map +1 -1
  107. package/dist/kernel/events/tool-events.d.ts +25 -0
  108. package/dist/kernel/events/tool-events.d.ts.map +1 -1
  109. package/dist/mailbox-attach.d.ts.map +1 -1
  110. package/dist/plugins/auto-review-plugin.d.ts +10 -0
  111. package/dist/plugins/auto-review-plugin.d.ts.map +1 -1
  112. package/dist/plugins/chimera-plugin.d.ts +5 -0
  113. package/dist/plugins/chimera-plugin.d.ts.map +1 -1
  114. package/dist/security/index.d.ts +1 -0
  115. package/dist/security/index.d.ts.map +1 -1
  116. package/dist/security/index.js +317 -9
  117. package/dist/security/index.js.map +4 -4
  118. package/dist/security/kanban-boundary.d.ts +10 -0
  119. package/dist/security/kanban-boundary.d.ts.map +1 -0
  120. package/dist/security/permission-policy.d.ts +8 -1
  121. package/dist/security/permission-policy.d.ts.map +1 -1
  122. package/dist/storage/config-loader.d.ts +15 -0
  123. package/dist/storage/config-loader.d.ts.map +1 -1
  124. package/dist/storage/index.d.ts +2 -2
  125. package/dist/storage/index.d.ts.map +1 -1
  126. package/dist/storage/index.js +627 -428
  127. package/dist/storage/index.js.map +4 -4
  128. package/dist/storage/memory-consolidator.d.ts +49 -4
  129. package/dist/storage/memory-consolidator.d.ts.map +1 -1
  130. package/dist/storage/memory-graph-backend.d.ts +11 -0
  131. package/dist/storage/memory-graph-backend.d.ts.map +1 -1
  132. package/dist/storage/provider-config-watcher.d.ts +17 -0
  133. package/dist/storage/provider-config-watcher.d.ts.map +1 -1
  134. package/dist/storage/session-event-bridge.d.ts +2 -2
  135. package/dist/storage/session-event-bridge.d.ts.map +1 -1
  136. package/dist/tools/fallback-manage-tools.d.ts.map +1 -1
  137. package/dist/tools/index.js +166 -26
  138. package/dist/tools/index.js.map +3 -3
  139. package/dist/types/config.d.ts +12 -4
  140. package/dist/types/config.d.ts.map +1 -1
  141. package/dist/types/file-event-record.d.ts +68 -0
  142. package/dist/types/file-event-record.d.ts.map +1 -0
  143. package/dist/types/index.d.ts +1 -0
  144. package/dist/types/index.d.ts.map +1 -1
  145. package/dist/types/index.js +13 -8
  146. package/dist/types/index.js.map +3 -3
  147. package/dist/types/permission.d.ts +38 -0
  148. package/dist/types/permission.d.ts.map +1 -1
  149. package/dist/types/provider.d.ts +1 -1
  150. package/dist/types/provider.d.ts.map +1 -1
  151. package/dist/types/session.d.ts +27 -0
  152. package/dist/types/session.d.ts.map +1 -1
  153. package/dist/types/tool-executor.d.ts +2 -0
  154. package/dist/types/tool-executor.d.ts.map +1 -1
  155. package/dist/utils/config-backup.d.ts +20 -0
  156. package/dist/utils/config-backup.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +12 -11
  158. package/dist/utils/index.d.ts.map +1 -1
  159. package/dist/utils/index.js +436 -290
  160. package/dist/utils/index.js.map +4 -4
  161. package/dist/utils/message-invariants.d.ts +12 -0
  162. package/dist/utils/message-invariants.d.ts.map +1 -1
  163. package/dist/utils/safe-json.d.ts +1 -1
  164. package/dist/utils/safe-json.d.ts.map +1 -1
  165. package/dist/utils/term.d.ts +6 -0
  166. package/dist/utils/term.d.ts.map +1 -1
  167. package/dist/utils/tool-wire-compact.d.ts +9 -0
  168. package/dist/utils/tool-wire-compact.d.ts.map +1 -1
  169. package/dist/utils/wstack-paths.d.ts +21 -1
  170. package/dist/utils/wstack-paths.d.ts.map +1 -1
  171. package/instructions/agents/ios.md +113 -0
  172. package/instructions/llm/memory-consolidator.md +4 -12
  173. package/instructions/system.md +4 -1
  174. package/package.json +3 -3
  175. package/skills/auto-review/SKILL.md +176 -0
  176. package/skills/mnemosyne/SKILL.md +156 -0
  177. package/skills/mnemosyne/instructions/agent-prompt.md +200 -0
@@ -36,8 +36,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
36
36
  }
37
37
  let mode;
38
38
  try {
39
- const stat12 = await fs.stat(targetPath);
40
- mode = stat12.mode & 511;
39
+ const stat13 = await fs.stat(targetPath);
40
+ mode = stat13.mode & 511;
41
41
  } catch {
42
42
  mode = opts.mode;
43
43
  }
@@ -90,8 +90,8 @@ async function withFileLock(targetPath, fn, opts = {}) {
90
90
  }
91
91
  if (code !== "EEXIST" && code !== "EPERM") throw err;
92
92
  try {
93
- const stat12 = await fs.stat(lockPath);
94
- if (Date.now() - stat12.mtimeMs > staleMs) {
93
+ const stat13 = await fs.stat(lockPath);
94
+ if (Date.now() - stat13.mtimeMs > staleMs) {
95
95
  await fs.unlink(lockPath);
96
96
  continue;
97
97
  }
@@ -127,13 +127,13 @@ async function waitForLockRelease(lockPath, remainingMs) {
127
127
  const parentDir = path.dirname(lockPath);
128
128
  const lockName = path.basename(lockPath);
129
129
  const intervalMs = Math.min(remainingMs, 100);
130
- return new Promise((resolve12) => {
130
+ return new Promise((resolve14) => {
131
131
  let settled = false;
132
132
  let watcher = null;
133
133
  const timer = setTimeout(() => {
134
134
  settled = true;
135
135
  watcher?.close();
136
- resolve12();
136
+ resolve14();
137
137
  }, intervalMs);
138
138
  try {
139
139
  watcher = watchDir(parentDir, (eventType, filename) => {
@@ -142,14 +142,14 @@ async function waitForLockRelease(lockPath, remainingMs) {
142
142
  settled = true;
143
143
  clearTimeout(timer);
144
144
  watcher?.close();
145
- resolve12();
145
+ resolve14();
146
146
  }
147
147
  });
148
148
  } catch {
149
149
  clearTimeout(timer);
150
150
  if (!settled) {
151
151
  settled = true;
152
- setTimeout(resolve12, Math.min(remainingMs, 25));
152
+ setTimeout(resolve14, Math.min(remainingMs, 25));
153
153
  }
154
154
  return;
155
155
  }
@@ -161,7 +161,7 @@ async function waitForLockRelease(lockPath, remainingMs) {
161
161
  settled = true;
162
162
  clearTimeout(timer);
163
163
  watcher?.close();
164
- resolve12();
164
+ resolve14();
165
165
  }
166
166
  }
167
167
  );
@@ -185,7 +185,7 @@ async function renameWithRetry(from, to) {
185
185
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
186
186
  throw err;
187
187
  }
188
- await new Promise((resolve12) => setTimeout(resolve12, delays[i]));
188
+ await new Promise((resolve14) => setTimeout(resolve14, delays[i]));
189
189
  }
190
190
  }
191
191
  throw lastErr;
@@ -382,6 +382,37 @@ var color = {
382
382
  bgGreen: wrap("42", "49")
383
383
  };
384
384
 
385
+ // src/utils/config-backup.ts
386
+ import * as fs2 from "node:fs/promises";
387
+ import * as path2 from "node:path";
388
+ function configHistoryDir(globalRoot) {
389
+ return path2.join(globalRoot, "config-history");
390
+ }
391
+ function configSlug(absolutePath, globalRoot) {
392
+ const rel = path2.relative(globalRoot, absolutePath);
393
+ const normalized = rel.replace(/\\/g, "/").replace(/\.json$/i, "");
394
+ return normalized.replace(/\//g, "-");
395
+ }
396
+ async function backupConfigFile(filePath, paths) {
397
+ let currentContent;
398
+ try {
399
+ currentContent = await fs2.readFile(filePath, "utf8");
400
+ if (!currentContent.trim()) return;
401
+ } catch {
402
+ return;
403
+ }
404
+ const now = /* @__PURE__ */ new Date();
405
+ const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
406
+ const slug = configSlug(filePath, paths.globalRoot);
407
+ const backupDir = configHistoryDir(paths.globalRoot);
408
+ const backupFile = path2.join(backupDir, `${slug}-${ts}.json`);
409
+ try {
410
+ await fs2.mkdir(backupDir, { recursive: true });
411
+ await fs2.writeFile(backupFile, currentContent, { mode: 384, encoding: "utf8" });
412
+ } catch {
413
+ }
414
+ }
415
+
385
416
  // src/utils/context-evidence.ts
386
417
  var MAX_DIGEST_CHARS = 4e3;
387
418
  function createContextEvidenceState() {
@@ -631,8 +662,8 @@ async function expandGlob(pattern) {
631
662
  for (const e of entries) {
632
663
  const full = `${dir}${SEP}${e}`;
633
664
  try {
634
- const stat12 = await fsp.stat(full);
635
- if (stat12.isDirectory()) await walk3(full, rest);
665
+ const stat13 = await fsp.stat(full);
666
+ if (stat13.isDirectory()) await walk3(full, rest);
636
667
  } catch {
637
668
  }
638
669
  }
@@ -649,8 +680,8 @@ async function expandGlob(pattern) {
649
680
  if (entries.includes(seg)) {
650
681
  const full = `${dir}${SEP}${seg}`;
651
682
  try {
652
- const stat12 = await fsp.stat(full);
653
- if (stat12.isDirectory()) await walk3(full, rest);
683
+ const stat13 = await fsp.stat(full);
684
+ if (stat13.isDirectory()) await walk3(full, rest);
654
685
  } catch {
655
686
  }
656
687
  }
@@ -841,10 +872,10 @@ function validateAgainstSchema(value, schema) {
841
872
  return { ok: errors.length === 0, errors };
842
873
  }
843
874
  var MAX_SCHEMA_DEPTH = 64;
844
- function walk(value, schema, path28, errors, depth) {
875
+ function walk(value, schema, path30, errors, depth) {
845
876
  if (depth > MAX_SCHEMA_DEPTH) {
846
877
  errors.push({
847
- path: path28 || "<root>",
878
+ path: path30 || "<root>",
848
879
  message: `schema nesting exceeds maximum depth (${MAX_SCHEMA_DEPTH})`
849
880
  });
850
881
  return;
@@ -852,7 +883,7 @@ function walk(value, schema, path28, errors, depth) {
852
883
  if (schema.enum !== void 0) {
853
884
  if (!enumIncludes(schema.enum, value)) {
854
885
  errors.push({
855
- path: path28 || "<root>",
886
+ path: path30 || "<root>",
856
887
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
857
888
  });
858
889
  return;
@@ -861,7 +892,7 @@ function walk(value, schema, path28, errors, depth) {
861
892
  if (typeof schema.type === "string") {
862
893
  if (!checkType(value, schema.type)) {
863
894
  errors.push({
864
- path: path28 || "<root>",
895
+ path: path30 || "<root>",
865
896
  message: `expected ${schema.type}, got ${describeType(value)} (${previewValue(value)})`
866
897
  });
867
898
  return;
@@ -873,7 +904,7 @@ function walk(value, schema, path28, errors, depth) {
873
904
  if (!(req in obj)) {
874
905
  const expected = schema.properties?.[req]?.type;
875
906
  errors.push({
876
- path: joinPath(path28, req),
907
+ path: joinPath(path30, req),
877
908
  message: `required property missing${typeof expected === "string" ? ` (expected ${expected})` : ""}`
878
909
  });
879
910
  }
@@ -881,14 +912,14 @@ function walk(value, schema, path28, errors, depth) {
881
912
  if (schema.properties) {
882
913
  for (const [key, subSchema] of Object.entries(schema.properties)) {
883
914
  if (key in obj) {
884
- walk(obj[key], subSchema, joinPath(path28, key), errors, depth + 1);
915
+ walk(obj[key], subSchema, joinPath(path30, key), errors, depth + 1);
885
916
  }
886
917
  }
887
918
  }
888
919
  }
889
920
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
890
921
  for (let i = 0; i < value.length; i++) {
891
- walk(value[i], schema.items, `${path28}[${i}]`, errors, depth + 1);
922
+ walk(value[i], schema.items, `${path30}[${i}]`, errors, depth + 1);
892
923
  }
893
924
  }
894
925
  }
@@ -1168,9 +1199,23 @@ function mapContent(msg, fn) {
1168
1199
  }
1169
1200
  return { ...msg, content: next };
1170
1201
  }
1202
+ function hasMeaningfulContent(content) {
1203
+ if (typeof content === "string") return content.trim().length > 0;
1204
+ for (const block of content) {
1205
+ if (block.type === "text") {
1206
+ if (block.text.trim().length > 0) return true;
1207
+ continue;
1208
+ }
1209
+ if (block.type === "thinking") {
1210
+ if (block.thinking.trim().length > 0 || block.signature) return true;
1211
+ continue;
1212
+ }
1213
+ return true;
1214
+ }
1215
+ return false;
1216
+ }
1171
1217
  function isEmptyMessage(msg) {
1172
- if (typeof msg.content === "string") return msg.content.trim().length === 0;
1173
- return msg.content.length === 0;
1218
+ return !hasMeaningfulContent(msg.content);
1174
1219
  }
1175
1220
 
1176
1221
  // src/utils/regex-guard.ts
@@ -1211,7 +1256,7 @@ function compileUserRegex(pattern, flags) {
1211
1256
 
1212
1257
  // src/utils/safe-json.ts
1213
1258
  function safeParse(input, maxBytes = 5e6) {
1214
- if (input.length > maxBytes) {
1259
+ if (Buffer.byteLength(input, "utf8") > maxBytes) {
1215
1260
  return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
1216
1261
  }
1217
1262
  try {
@@ -1246,14 +1291,14 @@ function safeStringify(value, pretty = false) {
1246
1291
  }
1247
1292
 
1248
1293
  // src/utils/session-scoped-path.ts
1249
- import * as path2 from "node:path";
1294
+ import * as path3 from "node:path";
1250
1295
  function sessionScopedPath(dir, sessionId, suffix) {
1251
1296
  if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
1252
1297
  throw invalid(sessionId);
1253
1298
  }
1254
- const resolved = path2.resolve(dir, `${sessionId}${suffix}`);
1255
- const rel = path2.relative(path2.resolve(dir), resolved);
1256
- if (rel.startsWith("..") || path2.isAbsolute(rel)) {
1299
+ const resolved = path3.resolve(dir, `${sessionId}${suffix}`);
1300
+ const rel = path3.relative(path3.resolve(dir), resolved);
1301
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
1257
1302
  throw invalid(sessionId);
1258
1303
  }
1259
1304
  return resolved;
@@ -1267,84 +1312,21 @@ function invalid(sessionId) {
1267
1312
  });
1268
1313
  }
1269
1314
 
1315
+ // src/utils/sleep.ts
1316
+ function sleep(ms) {
1317
+ return new Promise((resolve14) => setTimeout(resolve14, ms));
1318
+ }
1319
+
1270
1320
  // src/utils/slug.ts
1271
1321
  function slugify(name, fallback = "prompt", maxLen = 64) {
1272
1322
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/g, "") || fallback;
1273
1323
  }
1274
1324
 
1275
- // src/utils/ulid.ts
1276
- import { randomBytes as randomBytes2 } from "node:crypto";
1277
- var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1278
- var ENCODING_LEN = ENCODING.length;
1279
- var TIME_LEN = 10;
1280
- var RANDOM_LEN = 16;
1281
- function encodeTime(now, len) {
1282
- let mod;
1283
- let str = "";
1284
- for (let i = len - 1; i >= 0; i--) {
1285
- mod = now % ENCODING_LEN;
1286
- str = ENCODING[mod] + str;
1287
- now = (now - mod) / ENCODING_LEN;
1288
- }
1289
- return str;
1290
- }
1291
- function encodeRandom(len) {
1292
- const bytes = randomBytes2(len);
1293
- let str = "";
1294
- for (let i = 0; i < len; i++) {
1295
- str += ENCODING[bytes[i] % ENCODING_LEN];
1296
- }
1297
- return str;
1298
- }
1299
- function ulid(seedTime = Date.now()) {
1300
- return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
1301
- }
1302
-
1303
- // src/utils/sleep.ts
1304
- function sleep(ms) {
1305
- return new Promise((resolve12) => setTimeout(resolve12, ms));
1306
- }
1307
-
1308
1325
  // src/utils/string.ts
1309
1326
  function truncate(s, max) {
1310
1327
  return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1311
1328
  }
1312
1329
 
1313
- // src/utils/tool-subject.ts
1314
- var GLOB_METACHARACTERS = /[*?[\]]/g;
1315
- function escapeGlobSubject(value) {
1316
- return value.replace(GLOB_METACHARACTERS, (char) => `\\${char}`);
1317
- }
1318
- function normalizePathSubject(value) {
1319
- return escapeGlobSubject(value.replace(/\\/g, "/"));
1320
- }
1321
- function isPathSubjectKey(subjectKey) {
1322
- return subjectKey === "path" || subjectKey === "file" || subjectKey === "files";
1323
- }
1324
- function subjectForToolInput(toolName, input, subjectKey) {
1325
- if (!input || typeof input !== "object") return void 0;
1326
- const obj = input;
1327
- if (subjectKey) {
1328
- const value = obj[subjectKey];
1329
- if (typeof value === "string") {
1330
- return isPathSubjectKey(subjectKey) ? normalizePathSubject(value) : escapeGlobSubject(value);
1331
- }
1332
- }
1333
- if (toolName === "bash" && typeof obj.command === "string") {
1334
- return escapeGlobSubject(obj.command);
1335
- }
1336
- if (typeof obj.path === "string") {
1337
- return normalizePathSubject(obj.path);
1338
- }
1339
- if (typeof obj.url === "string") {
1340
- return escapeGlobSubject(obj.url);
1341
- }
1342
- if (typeof obj.name === "string") {
1343
- return escapeGlobSubject(obj.name);
1344
- }
1345
- return void 0;
1346
- }
1347
-
1348
1330
  // src/utils/tool-wire-compact.ts
1349
1331
  var TOOL_DESCRIPTION_MAX_CHARS = 400;
1350
1332
  var SCHEMA_DESCRIPTION_MAX_CHARS = 120;
@@ -1361,9 +1343,11 @@ function compactToolDefinitionForWire(tool, opts = {}) {
1361
1343
  tool.description ?? "",
1362
1344
  opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
1363
1345
  ),
1364
- inputSchema: compactSchemaDescriptions(
1365
- tool.inputSchema,
1366
- opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
1346
+ inputSchema: normalizeTopLevelToolSchema(
1347
+ compactSchemaDescriptions(
1348
+ tool.inputSchema,
1349
+ opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
1350
+ )
1367
1351
  )
1368
1352
  };
1369
1353
  if (useDefaultOptions && typeof tool === "object" && tool !== null) {
@@ -1371,6 +1355,44 @@ function compactToolDefinitionForWire(tool, opts = {}) {
1371
1355
  }
1372
1356
  return compact;
1373
1357
  }
1358
+ function normalizeTopLevelToolSchema(schema) {
1359
+ const combinators = ["oneOf", "anyOf", "allOf"].map((keyword) => ({ keyword, branches: schema[keyword] })).filter(
1360
+ (entry) => Array.isArray(entry.branches)
1361
+ );
1362
+ if (combinators.length === 0) return schema;
1363
+ const out = { ...schema, type: "object" };
1364
+ delete out["oneOf"];
1365
+ delete out["anyOf"];
1366
+ delete out["allOf"];
1367
+ const properties = isRecord(schema["properties"]) ? { ...schema["properties"] } : {};
1368
+ let required = stringSet(schema["required"]);
1369
+ for (const { keyword, branches } of combinators) {
1370
+ const objectBranches = branches.filter(isRecord);
1371
+ for (const branch of objectBranches) {
1372
+ if (isRecord(branch["properties"])) Object.assign(properties, branch["properties"]);
1373
+ }
1374
+ const branchRequired = objectBranches.map((branch) => stringSet(branch["required"]));
1375
+ if (keyword === "allOf") {
1376
+ for (const fields of branchRequired) for (const field of fields) required.add(field);
1377
+ } else if (branchRequired.length > 0) {
1378
+ const common = new Set(
1379
+ [...branchRequired[0]].filter(
1380
+ (field) => branchRequired.slice(1).every((fields) => fields.has(field))
1381
+ )
1382
+ );
1383
+ required = /* @__PURE__ */ new Set([...required, ...common]);
1384
+ }
1385
+ }
1386
+ out["properties"] = properties;
1387
+ if (required.size > 0) out["required"] = [...required];
1388
+ else delete out["required"];
1389
+ return out;
1390
+ }
1391
+ function stringSet(value) {
1392
+ return new Set(
1393
+ Array.isArray(value) ? value.filter((item) => typeof item === "string") : []
1394
+ );
1395
+ }
1374
1396
  function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
1375
1397
  const compact = compactSchemaNode(schema, maxDescriptionChars);
1376
1398
  return isRecord(compact) ? compact : { type: "object", properties: {} };
@@ -2265,25 +2287,88 @@ function resolveToolResultRenderMode(modes, toolName) {
2265
2287
  return normalizeToolResultRenderMode(modes?.[toolName]) ?? DEFAULT_TOOL_RESULT_RENDER_MODE;
2266
2288
  }
2267
2289
 
2290
+ // src/utils/tool-subject.ts
2291
+ var GLOB_METACHARACTERS = /[*?[\]]/g;
2292
+ function escapeGlobSubject(value) {
2293
+ return value.replace(GLOB_METACHARACTERS, (char) => `\\${char}`);
2294
+ }
2295
+ function normalizePathSubject(value) {
2296
+ return escapeGlobSubject(value.replace(/\\/g, "/"));
2297
+ }
2298
+ function isPathSubjectKey(subjectKey) {
2299
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files";
2300
+ }
2301
+ function subjectForToolInput(toolName, input, subjectKey) {
2302
+ if (!input || typeof input !== "object") return void 0;
2303
+ const obj = input;
2304
+ if (subjectKey) {
2305
+ const value = obj[subjectKey];
2306
+ if (typeof value === "string") {
2307
+ return isPathSubjectKey(subjectKey) ? normalizePathSubject(value) : escapeGlobSubject(value);
2308
+ }
2309
+ }
2310
+ if (toolName === "bash" && typeof obj.command === "string") {
2311
+ return escapeGlobSubject(obj.command);
2312
+ }
2313
+ if (typeof obj.path === "string") {
2314
+ return normalizePathSubject(obj.path);
2315
+ }
2316
+ if (typeof obj.url === "string") {
2317
+ return escapeGlobSubject(obj.url);
2318
+ }
2319
+ if (typeof obj.name === "string") {
2320
+ return escapeGlobSubject(obj.name);
2321
+ }
2322
+ return void 0;
2323
+ }
2324
+
2325
+ // src/utils/ulid.ts
2326
+ import { randomBytes as randomBytes2 } from "node:crypto";
2327
+ var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2328
+ var ENCODING_LEN = ENCODING.length;
2329
+ var TIME_LEN = 10;
2330
+ var RANDOM_LEN = 16;
2331
+ function encodeTime(now, len) {
2332
+ let mod;
2333
+ let str = "";
2334
+ for (let i = len - 1; i >= 0; i--) {
2335
+ mod = now % ENCODING_LEN;
2336
+ str = ENCODING[mod] + str;
2337
+ now = (now - mod) / ENCODING_LEN;
2338
+ }
2339
+ return str;
2340
+ }
2341
+ function encodeRandom(len) {
2342
+ const bytes = randomBytes2(len);
2343
+ let str = "";
2344
+ for (let i = 0; i < len; i++) {
2345
+ str += ENCODING[bytes[i] % ENCODING_LEN];
2346
+ }
2347
+ return str;
2348
+ }
2349
+ function ulid(seedTime = Date.now()) {
2350
+ return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
2351
+ }
2352
+
2268
2353
  // src/utils/wstack-paths.ts
2269
2354
  import { createHash } from "node:crypto";
2270
- import * as fs2 from "node:fs";
2355
+ import * as fs3 from "node:fs";
2271
2356
  import * as os from "node:os";
2272
- import * as path3 from "node:path";
2357
+ import * as path4 from "node:path";
2273
2358
  function canonicalProjectRoot(absRoot) {
2274
- const checkoutRoot = path3.resolve(absRoot);
2275
- const dotGit = path3.join(checkoutRoot, ".git");
2359
+ const checkoutRoot = path4.resolve(absRoot);
2360
+ const dotGit = path4.join(checkoutRoot, ".git");
2276
2361
  try {
2277
- if (!fs2.statSync(dotGit).isFile()) return checkoutRoot;
2278
- const gitDirLine = fs2.readFileSync(dotGit, "utf8").trim();
2362
+ if (!fs3.statSync(dotGit).isFile()) return checkoutRoot;
2363
+ const gitDirLine = fs3.readFileSync(dotGit, "utf8").trim();
2279
2364
  const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
2280
2365
  if (!match?.[1]) return checkoutRoot;
2281
- const gitDir = path3.resolve(checkoutRoot, match[1].trim());
2282
- const commonDirFile = path3.join(gitDir, "commondir");
2283
- if (!fs2.statSync(commonDirFile).isFile()) return checkoutRoot;
2284
- const commonDir = path3.resolve(gitDir, fs2.readFileSync(commonDirFile, "utf8").trim());
2285
- if (path3.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2286
- return path3.dirname(commonDir);
2366
+ const gitDir = path4.resolve(checkoutRoot, match[1].trim());
2367
+ const commonDirFile = path4.join(gitDir, "commondir");
2368
+ if (!fs3.statSync(commonDirFile).isFile()) return checkoutRoot;
2369
+ const commonDir = path4.resolve(gitDir, fs3.readFileSync(commonDirFile, "utf8").trim());
2370
+ if (path4.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2371
+ return path4.dirname(commonDir);
2287
2372
  } catch {
2288
2373
  return checkoutRoot;
2289
2374
  }
@@ -2293,7 +2378,7 @@ function projectHash(absRoot) {
2293
2378
  }
2294
2379
  function projectSlug(absRoot) {
2295
2380
  const identityRoot = canonicalProjectRoot(absRoot);
2296
- const base = slugify2(path3.basename(identityRoot));
2381
+ const base = slugify2(path4.basename(identityRoot));
2297
2382
  const hash2 = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
2298
2383
  return `${base}-${hash2}`;
2299
2384
  }
@@ -2302,61 +2387,83 @@ function slugify2(name) {
2302
2387
  }
2303
2388
  function wstackGlobalRoot() {
2304
2389
  const fromEnv = process.env["WRONGSTACK_HOME"];
2305
- if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
2306
- return path3.join(os.homedir(), ".wrongstack");
2390
+ if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
2391
+ return path4.join(os.homedir(), ".wrongstack");
2307
2392
  }
2308
2393
  function resolveWstackPaths(opts) {
2309
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path3.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2394
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path4.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2310
2395
  const homeDir = opts.userHome ?? os.homedir();
2311
2396
  const hash2 = projectHash(opts.projectRoot);
2312
2397
  const slug = projectSlug(opts.projectRoot);
2313
- const projectDir = path3.join(globalRoot, "projects", slug);
2398
+ const projectDir = path4.join(globalRoot, "projects", slug);
2314
2399
  return {
2315
2400
  globalRoot,
2316
2401
  projectRoot: opts.projectRoot,
2317
2402
  homeDir,
2318
2403
  configDir: globalRoot,
2319
- globalConfig: path3.join(globalRoot, "config.json"),
2320
- secretsKey: path3.join(globalRoot, ".key"),
2321
- globalMemory: path3.join(globalRoot, "memory.md"),
2322
- globalSkills: path3.join(globalRoot, "skills"),
2323
- globalClaudeSkills: path3.join(homeDir, ".claude", "skills"),
2324
- globalDesignKits: path3.join(globalRoot, "design-kits"),
2325
- globalPrompts: path3.join(globalRoot, "prompts"),
2326
- globalInstructions: path3.join(globalRoot, "instructions"),
2327
- promptUsage: path3.join(globalRoot, "prompt-usage.json"),
2328
- cacheDir: path3.join(globalRoot, "cache"),
2329
- modelsCache: path3.join(globalRoot, "cache", "models.dev.json"),
2330
- modelsOverlayCache: path3.join(globalRoot, "cache", "models-overlay.json"),
2331
- historyFile: path3.join(globalRoot, "history"),
2332
- logFile: path3.join(globalRoot, "logs", "wrongstack.log"),
2404
+ globalConfig: path4.join(globalRoot, "config.json"),
2405
+ profilesDir: path4.join(globalRoot, "profiles"),
2406
+ profileConfig: (name) => {
2407
+ const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
2408
+ return path4.join(globalRoot, "profiles", safe || "default", "config.json");
2409
+ },
2410
+ profileStatuslineConfig: (name) => {
2411
+ const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
2412
+ return path4.join(globalRoot, "profiles", safe || "default", "statusline.json");
2413
+ },
2414
+ profileModeConfig: (name) => {
2415
+ const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
2416
+ return path4.join(globalRoot, "profiles", safe || "default", "mode.json");
2417
+ },
2418
+ profileProviderStatus: (name) => {
2419
+ const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
2420
+ return path4.join(globalRoot, "profiles", safe || "default", "provider-status.json");
2421
+ },
2422
+ profileUpdateCache: (name) => {
2423
+ const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
2424
+ return path4.join(globalRoot, "profiles", safe || "default", "update-cache.json");
2425
+ },
2426
+ secretsKey: path4.join(globalRoot, ".key"),
2427
+ globalMemory: path4.join(globalRoot, "memory.md"),
2428
+ globalSkills: path4.join(globalRoot, "skills"),
2429
+ globalClaudeSkills: path4.join(homeDir, ".claude", "skills"),
2430
+ globalDesignKits: path4.join(globalRoot, "design-kits"),
2431
+ globalPrompts: path4.join(globalRoot, "prompts"),
2432
+ globalInstructions: path4.join(globalRoot, "instructions"),
2433
+ promptUsage: path4.join(globalRoot, "prompt-usage.json"),
2434
+ cacheDir: path4.join(globalRoot, "cache"),
2435
+ modelsCache: path4.join(globalRoot, "cache", "models.dev.json"),
2436
+ modelsOverlayCache: path4.join(globalRoot, "cache", "models-overlay.json"),
2437
+ historyFile: path4.join(globalRoot, "history"),
2438
+ logFile: path4.join(globalRoot, "logs", "wrongstack.log"),
2333
2439
  projectDir,
2334
- projectCodebaseIndex: path3.join(projectDir, "codebase-index"),
2335
- projectMemory: path3.join(projectDir, "memory.md"),
2336
- projectSessions: path3.join(projectDir, "sessions"),
2337
- projectTrust: path3.join(projectDir, "trust.json"),
2338
- projectMeta: path3.join(projectDir, "meta.json"),
2339
- projectLocalConfig: path3.join(projectDir, "config.local.json"),
2340
- inProjectConfig: path3.join(opts.projectRoot, ".wrongstack", "config.json"),
2341
- inProjectAgentsFile: path3.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2342
- inProjectSkills: path3.join(opts.projectRoot, ".wrongstack", "skills"),
2343
- inProjectClaudeSkills: path3.join(opts.projectRoot, ".claude", "skills"),
2344
- inProjectPrompts: path3.join(opts.projectRoot, ".wrongstack", "prompts"),
2345
- inProjectInstructions: path3.join(opts.projectRoot, ".wrongstack", "instructions"),
2346
- inProjectDesignKits: path3.join(opts.projectRoot, ".wrongstack", "design-kits"),
2347
- inProjectWorktrees: path3.join(opts.projectRoot, ".wrongstack", "worktrees"),
2440
+ projectCodebaseIndex: path4.join(projectDir, "codebase-index"),
2441
+ projectMemory: path4.join(projectDir, "memory.md"),
2442
+ projectSessions: path4.join(projectDir, "sessions"),
2443
+ projectTrust: path4.join(projectDir, "trust.json"),
2444
+ projectMeta: path4.join(projectDir, "meta.json"),
2445
+ projectLocalConfig: path4.join(projectDir, "config.local.json"),
2446
+ inProjectConfig: path4.join(opts.projectRoot, ".wrongstack", "config.json"),
2447
+ inProjectAgentsFile: path4.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2448
+ inProjectSkills: path4.join(opts.projectRoot, ".wrongstack", "skills"),
2449
+ inProjectClaudeSkills: path4.join(opts.projectRoot, ".claude", "skills"),
2450
+ inProjectPrompts: path4.join(opts.projectRoot, ".wrongstack", "prompts"),
2451
+ inProjectInstructions: path4.join(opts.projectRoot, ".wrongstack", "instructions"),
2452
+ inProjectDesignKits: path4.join(opts.projectRoot, ".wrongstack", "design-kits"),
2453
+ inProjectWorktrees: path4.join(opts.projectRoot, ".wrongstack", "worktrees"),
2348
2454
  projectHash: hash2,
2349
2455
  projectSlug: slug,
2350
- projectGoal: path3.join(projectDir, "goal.json"),
2351
- projectInputHistory: path3.join(projectDir, "input-history.json"),
2352
- projectSpecs: path3.join(projectDir, "specs"),
2353
- projectTaskGraphs: path3.join(projectDir, "task-graphs"),
2354
- projectSddSession: path3.join(projectDir, "sdd-session.json"),
2355
- projectPlan: path3.join(projectDir, "plan.json"),
2356
- projectAutophase: path3.join(projectDir, "autophase"),
2357
- projectSddBoards: path3.join(projectDir, "sdd-boards"),
2358
- syncConfig: path3.join(globalRoot, "sync.json"),
2359
- projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
2456
+ projectGoal: path4.join(projectDir, "goal.json"),
2457
+ projectInputHistory: path4.join(projectDir, "input-history.json"),
2458
+ projectSpecs: path4.join(projectDir, "specs"),
2459
+ projectTaskGraphs: path4.join(projectDir, "task-graphs"),
2460
+ projectSddSession: path4.join(projectDir, "sdd-session.json"),
2461
+ projectPlan: path4.join(projectDir, "plan.json"),
2462
+ projectAutophase: path4.join(projectDir, "autophase"),
2463
+ projectSddBoards: path4.join(projectDir, "sdd-boards"),
2464
+ syncConfig: path4.join(globalRoot, "sync.json"),
2465
+ configHistoryDir: path4.join(globalRoot, "config-history"),
2466
+ projectStatus: (projectHash2) => path4.join(globalRoot, "projects", projectHash2, "status.json")
2360
2467
  };
2361
2468
  }
2362
2469
 
@@ -2649,7 +2756,7 @@ var InMemoryAgentBridge = class {
2649
2756
  });
2650
2757
  }
2651
2758
  this.inflightGuards.add(correlationId);
2652
- return new Promise((resolve12, reject) => {
2759
+ return new Promise((resolve14, reject) => {
2653
2760
  const timer = setTimeout(() => {
2654
2761
  this.inflightGuards.delete(correlationId);
2655
2762
  this.pendingRequests.delete(correlationId);
@@ -2668,7 +2775,7 @@ var InMemoryAgentBridge = class {
2668
2775
  return;
2669
2776
  }
2670
2777
  this.pendingRequests.set(correlationId, {
2671
- resolve: resolve12,
2778
+ resolve: resolve14,
2672
2779
  reject,
2673
2780
  timer
2674
2781
  });
@@ -3017,13 +3124,13 @@ var SubagentBudget = class _SubagentBudget {
3017
3124
  if (!bus?.hasListenerFor("budget.threshold_reached")) {
3018
3125
  return Promise.resolve("stop");
3019
3126
  }
3020
- return new Promise((resolve12) => {
3127
+ return new Promise((resolve14) => {
3021
3128
  let resolved = false;
3022
3129
  const respond = (d) => {
3023
3130
  if (resolved) return;
3024
3131
  resolved = true;
3025
3132
  clearTimeout(fallback);
3026
- resolve12(d);
3133
+ resolve14(d);
3027
3134
  };
3028
3135
  const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
3029
3136
  const sessionId = this.currentSessionId();
@@ -3348,8 +3455,34 @@ function makeAgentSubagentRunner(opts) {
3348
3455
  const taskStartedAt = Date.now();
3349
3456
  const factoryResult = await opts.factory(ctx.config, task);
3350
3457
  const { agent, events } = factoryResult;
3458
+ const taskContext = task.context ?? {};
3459
+ const hasExplicitTelemetryTask = typeof taskContext["telemetryTaskId"] === "string";
3460
+ const telemetryTaskId = hasExplicitTelemetryTask ? taskContext["telemetryTaskId"] : task.id;
3461
+ const telemetryRunId = typeof taskContext["telemetryRunId"] === "string" ? taskContext["telemetryRunId"] : void 0;
3462
+ const telemetryBoardId = typeof taskContext["telemetryBoardId"] === "string" ? taskContext["telemetryBoardId"] : void 0;
3351
3463
  const agentContext = agent.ctx;
3352
- if (agentContext?.meta) agentContext.meta["subagentTaskId"] = task.id;
3464
+ if (agentContext?.meta) {
3465
+ agentContext.meta["subagentTaskId"] = task.id;
3466
+ const kanbanContext = taskContext["kanban"];
3467
+ if (kanbanContext && typeof kanbanContext === "object") {
3468
+ agentContext.meta["kanban"] = kanbanContext;
3469
+ }
3470
+ if (hasExplicitTelemetryTask) {
3471
+ agentContext.meta["telemetryTaskId"] = telemetryTaskId;
3472
+ if (telemetryRunId) agentContext.meta["telemetryRunId"] = telemetryRunId;
3473
+ }
3474
+ }
3475
+ if (hasExplicitTelemetryTask) {
3476
+ agentContext?.setCurrentKanbanTask?.(telemetryTaskId, telemetryBoardId);
3477
+ } else {
3478
+ const kanbanContext = taskContext["kanban"];
3479
+ if (kanbanContext && typeof kanbanContext === "object") {
3480
+ const kanban = kanbanContext;
3481
+ const boardId = typeof kanban["boardId"] === "string" ? kanban["boardId"] : void 0;
3482
+ const taskId = typeof kanban["taskId"] === "string" ? kanban["taskId"] : void 0;
3483
+ if (taskId) agentContext?.setCurrentKanbanTask?.(taskId, boardId);
3484
+ }
3485
+ }
3353
3486
  const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
3354
3487
  const aborter = new AbortController();
3355
3488
  ctx.budget._events = events;
@@ -3374,6 +3507,27 @@ function makeAgentSubagentRunner(opts) {
3374
3507
  const unsub = [];
3375
3508
  unsub.push(
3376
3509
  events.on("tool.executed", (e) => {
3510
+ if (hasExplicitTelemetryTask) {
3511
+ opts.hostEvents?.emit("subagent.tool_executed", {
3512
+ sessionId: e.sessionId,
3513
+ subagentId: ctx.subagentId,
3514
+ agentSessionId: e.sessionId,
3515
+ agentName: e.agentName ?? ctx.config.name,
3516
+ taskId: telemetryTaskId,
3517
+ runId: telemetryRunId,
3518
+ traceId: e.traceId,
3519
+ id: e.id,
3520
+ name: e.name,
3521
+ durationMs: e.durationMs,
3522
+ ok: e.ok,
3523
+ // Raw tool inputs may contain credentials or sensitive arguments.
3524
+ // Task telemetry crosses from the isolated worker bus to the shared
3525
+ // host bus, so expose only execution metadata at this boundary.
3526
+ outputBytes: e.outputBytes,
3527
+ outputTokens: e.outputTokens,
3528
+ outputLines: e.outputLines
3529
+ });
3530
+ }
3377
3531
  try {
3378
3532
  ctx.budget.recordToolCall();
3379
3533
  } catch (eb) {
@@ -3385,6 +3539,17 @@ function makeAgentSubagentRunner(opts) {
3385
3539
  lastToolFailed = null;
3386
3540
  }
3387
3541
  }),
3542
+ events.on("file.event", (e) => {
3543
+ if (!hasExplicitTelemetryTask) return;
3544
+ if (e.runId === telemetryRunId && e.taskId === telemetryTaskId) return;
3545
+ opts.hostEvents?.emit("file.event", {
3546
+ ...e,
3547
+ scope: "task",
3548
+ taskId: telemetryTaskId,
3549
+ boardId: telemetryBoardId,
3550
+ runId: telemetryRunId
3551
+ });
3552
+ }),
3388
3553
  events.on("provider.response", (e) => {
3389
3554
  try {
3390
3555
  ctx.budget.recordUsage(e.usage);
@@ -3607,7 +3772,7 @@ var TOOLS = {
3607
3772
  // src/coordination/agents/agent-prompts.ts
3608
3773
  import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
3609
3774
  import * as os2 from "node:os";
3610
- import * as path4 from "node:path";
3775
+ import * as path5 from "node:path";
3611
3776
  import { fileURLToPath } from "node:url";
3612
3777
  var promptCache = /* @__PURE__ */ new Map();
3613
3778
  var candidateCache = /* @__PURE__ */ new Map();
@@ -3620,7 +3785,7 @@ function agentPrompt(id) {
3620
3785
  let resolved = "";
3621
3786
  for (const dir of agentPromptDirCandidates(envDir)) {
3622
3787
  try {
3623
- resolved = readFileSync2(path4.join(dir, fileName), "utf8").trimEnd();
3788
+ resolved = readFileSync2(path5.join(dir, fileName), "utf8").trimEnd();
3624
3789
  break;
3625
3790
  } catch {
3626
3791
  }
@@ -3629,20 +3794,20 @@ function agentPrompt(id) {
3629
3794
  return resolved;
3630
3795
  }
3631
3796
  function agentPromptDirCandidates(envDir) {
3632
- const globalRoot = process.env["WRONGSTACK_HOME"] || path4.join(os2.homedir(), ".wrongstack");
3797
+ const globalRoot = process.env["WRONGSTACK_HOME"] || path5.join(os2.homedir(), ".wrongstack");
3633
3798
  const candKey = `${envDir}\0${globalRoot}`;
3634
3799
  const cached = candidateCache.get(candKey);
3635
3800
  if (cached !== void 0) return cached;
3636
- const here = path4.dirname(fileURLToPath(import.meta.url));
3801
+ const here = path5.dirname(fileURLToPath(import.meta.url));
3637
3802
  const explicitDir = envDir || void 0;
3638
3803
  const candidates = [
3639
- ...explicitDir ? [path4.resolve(explicitDir)] : [],
3640
- path4.join(globalRoot, "instructions", "agents"),
3641
- path4.resolve(here, "../../../../instructions/agents"),
3642
- path4.resolve(here, "../../../instructions/agents"),
3643
- path4.resolve(here, "../../instructions/agents"),
3644
- path4.resolve(here, "../instructions/agents"),
3645
- path4.resolve(here, "instructions/agents")
3804
+ ...explicitDir ? [path5.resolve(explicitDir)] : [],
3805
+ path5.join(globalRoot, "instructions", "agents"),
3806
+ path5.resolve(here, "../../../../instructions/agents"),
3807
+ path5.resolve(here, "../../../instructions/agents"),
3808
+ path5.resolve(here, "../../instructions/agents"),
3809
+ path5.resolve(here, "../instructions/agents"),
3810
+ path5.resolve(here, "instructions/agents")
3646
3811
  ];
3647
3812
  const ordered = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
3648
3813
  candidateCache.set(candKey, ordered);
@@ -4678,6 +4843,40 @@ var DOMAIN_AGENTS = [
4678
4843
  "information architecture"
4679
4844
  ]
4680
4845
  }
4846
+ },
4847
+ {
4848
+ config: {
4849
+ id: "ios",
4850
+ name: "iOS",
4851
+ role: "ios",
4852
+ tools: [...TOOLS.build, "fetch"],
4853
+ prompt: agentPrompt("ios")
4854
+ },
4855
+ budget: HEAVY_BUDGET,
4856
+ capability: {
4857
+ phase: "domain",
4858
+ summary: "Apple-platform app development in Swift against the latest Xcode/iOS-SDK line: SwiftUI, UIKit, SwiftData, concurrency, accessibility, and App Store submission.",
4859
+ keywords: [
4860
+ "ios",
4861
+ "iphone",
4862
+ "ipad",
4863
+ "ipados",
4864
+ "watchos",
4865
+ "tvos",
4866
+ "visionos",
4867
+ "macos",
4868
+ "swift",
4869
+ "swiftui",
4870
+ "uikit",
4871
+ "xcode",
4872
+ "swiftdata",
4873
+ "app store",
4874
+ "app intents",
4875
+ "swift package manager",
4876
+ "cocoapods",
4877
+ "foundation models"
4878
+ ]
4879
+ }
4681
4880
  }
4682
4881
  ];
4683
4882
 
@@ -5211,7 +5410,7 @@ function attachAutoExtend(events, policy = {}) {
5211
5410
 
5212
5411
  // src/coordination/delegate-tool.ts
5213
5412
  import * as fsp2 from "node:fs/promises";
5214
- import * as path5 from "node:path";
5413
+ import * as path6 from "node:path";
5215
5414
  import { randomUUID as randomUUID2 } from "node:crypto";
5216
5415
 
5217
5416
  // src/coordination/fleet.ts
@@ -5659,7 +5858,7 @@ function createDelegateTool(opts) {
5659
5858
  };
5660
5859
  }
5661
5860
  async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
5662
- return new Promise((resolve12) => {
5861
+ return new Promise((resolve14) => {
5663
5862
  let settled = false;
5664
5863
  let timer;
5665
5864
  let offAbort = () => {
@@ -5672,7 +5871,7 @@ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abo
5672
5871
  offIter();
5673
5872
  offProgress();
5674
5873
  offAbort();
5675
- resolve12(value);
5874
+ resolve14(value);
5676
5875
  };
5677
5876
  const arm = () => {
5678
5877
  if (timer) clearTimeout(timer);
@@ -5832,13 +6031,13 @@ async function readSubagentPartial(opts, subagentId) {
5832
6031
  if (!opts.sessionsRoot) return void 0;
5833
6032
  const candidates = [];
5834
6033
  if (opts.directorRunId) {
5835
- candidates.push(path5.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
6034
+ candidates.push(path6.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
5836
6035
  } else {
5837
6036
  try {
5838
6037
  const entries = await fsp2.readdir(opts.sessionsRoot, { withFileTypes: true });
5839
6038
  for (const entry of entries) {
5840
6039
  if (entry.isDirectory()) {
5841
- candidates.push(path5.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
6040
+ candidates.push(path6.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
5842
6041
  }
5843
6042
  }
5844
6043
  } catch {
@@ -5885,7 +6084,7 @@ async function readSubagentPartial(opts, subagentId) {
5885
6084
  // src/coordination/director.ts
5886
6085
  import { randomUUID as randomUUID6 } from "node:crypto";
5887
6086
  import * as fsp5 from "node:fs/promises";
5888
- import * as path7 from "node:path";
6087
+ import * as path8 from "node:path";
5889
6088
 
5890
6089
  // src/storage/director-state.ts
5891
6090
  import * as fsp3 from "node:fs/promises";
@@ -6368,7 +6567,7 @@ var CollabSession = class extends EventEmitter {
6368
6567
  }
6369
6568
  for (const filePath of allFiles) {
6370
6569
  try {
6371
- const [content, stat12] = await Promise.all([
6570
+ const [content, stat13] = await Promise.all([
6372
6571
  fsp4.readFile(filePath, "utf8"),
6373
6572
  fsp4.stat(filePath)
6374
6573
  ]);
@@ -6378,8 +6577,8 @@ var CollabSession = class extends EventEmitter {
6378
6577
  path: filePath,
6379
6578
  content,
6380
6579
  language,
6381
- snapshotMtimeMs: stat12.mtimeMs,
6382
- snapshotSizeBytes: stat12.size
6580
+ snapshotMtimeMs: stat13.mtimeMs,
6581
+ snapshotSizeBytes: stat13.size
6383
6582
  });
6384
6583
  } catch {
6385
6584
  this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
@@ -6798,9 +6997,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
6798
6997
  for (const file of this.snapshot.files) {
6799
6998
  if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
6800
6999
  try {
6801
- const stat12 = await fsp4.stat(file.path);
6802
- const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat12.mtimeMs > file.snapshotMtimeMs + 1;
6803
- const sizeChanged = file.snapshotSizeBytes !== void 0 && stat12.size !== file.snapshotSizeBytes;
7000
+ const stat13 = await fsp4.stat(file.path);
7001
+ const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat13.mtimeMs > file.snapshotMtimeMs + 1;
7002
+ const sizeChanged = file.snapshotSizeBytes !== void 0 && stat13.size !== file.snapshotSizeBytes;
6804
7003
  if (mtimeChanged || sizeChanged) {
6805
7004
  warnings.push(`${file.path} changed after the collab snapshot was captured.`);
6806
7005
  }
@@ -6955,7 +7154,7 @@ var DirectorBtwNotes = class {
6955
7154
 
6956
7155
  // src/utils/instruction-file.ts
6957
7156
  import { readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
6958
- import * as path6 from "node:path";
7157
+ import * as path7 from "node:path";
6959
7158
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6960
7159
  var textCache = /* @__PURE__ */ new Map();
6961
7160
  var rootCandidates;
@@ -6965,7 +7164,7 @@ function readBundledInstructionText(relativePath) {
6965
7164
  let resolved = "";
6966
7165
  for (const root of instructionRootCandidates()) {
6967
7166
  try {
6968
- resolved = readFileSync3(path6.join(root, relativePath), "utf8").trimEnd();
7167
+ resolved = readFileSync3(path7.join(root, relativePath), "utf8").trimEnd();
6969
7168
  break;
6970
7169
  } catch {
6971
7170
  }
@@ -6981,11 +7180,11 @@ function renderInstructionTemplate(template, values) {
6981
7180
  }
6982
7181
  function instructionRootCandidates() {
6983
7182
  if (rootCandidates !== void 0) return rootCandidates;
6984
- const here = path6.dirname(fileURLToPath2(import.meta.url));
7183
+ const here = path7.dirname(fileURLToPath2(import.meta.url));
6985
7184
  const candidates = [
6986
- path6.resolve(here, "../../instructions"),
6987
- path6.resolve(here, "../instructions"),
6988
- path6.resolve(here, "instructions")
7185
+ path7.resolve(here, "../../instructions"),
7186
+ path7.resolve(here, "../instructions"),
7187
+ path7.resolve(here, "instructions")
6989
7188
  ];
6990
7189
  rootCandidates = candidates.sort((a, b) => Number(!isDirectory2(a)) - Number(!isDirectory2(b)));
6991
7190
  return rootCandidates;
@@ -7063,6 +7262,8 @@ function rosterSummaryFromConfigs(roster) {
7063
7262
  import { randomUUID as randomUUID4 } from "node:crypto";
7064
7263
  import {
7065
7264
  claimReadyTask,
7265
+ describeKanbanBoundary,
7266
+ heartbeatTaskAssignment,
7066
7267
  listReadyTasks,
7067
7268
  updateTaskAssignment
7068
7269
  } from "@wrongstack/kanban";
@@ -7817,13 +8018,19 @@ function makeKanbanQueueTool(director, roster) {
7817
8018
  const projectRoot = ctx.projectRoot;
7818
8019
  if (!projectRoot) return { error: "kanban_queue requires ctx.projectRoot." };
7819
8020
  const maxTasks = Math.max(1, Math.min(20, Math.floor(i.maxTasks ?? 1)));
7820
- const leaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
8021
+ const baseLeaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
8022
+ const leaseTtlMs = i.awaitCompletion === true && i.timeoutMs !== void 0 ? Math.max(baseLeaseTtlMs, i.timeoutMs + 6e4) : baseLeaseTtlMs;
8023
+ const heartbeatIntervalMs = Math.max(
8024
+ 5e3,
8025
+ i.heartbeatIntervalMs ?? Math.floor(leaseTtlMs / 2)
8026
+ );
8027
+ const effectiveLeaseTtlMs = Math.max(leaseTtlMs, heartbeatIntervalMs * 2);
7821
8028
  const claimedAt = nowIso();
7822
8029
  const leaseSeeding = {
7823
8030
  leaseId: randomUUID4(),
7824
8031
  claimedAt,
7825
8032
  heartbeatAt: claimedAt,
7826
- leaseExpiresAt: new Date(Date.now() + leaseTtlMs).toISOString()
8033
+ leaseExpiresAt: new Date(Date.now() + effectiveLeaseTtlMs).toISOString()
7827
8034
  };
7828
8035
  const candidateTaskIds = i.taskId !== void 0 ? [i.taskId] : i.query ? (await listReadyTasks(projectRoot, {
7829
8036
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {}
@@ -7831,9 +8038,11 @@ function makeKanbanQueueTool(director, roster) {
7831
8038
  const dispatches = [];
7832
8039
  const errors = [];
7833
8040
  const resultFailures = [];
8041
+ const budgetRejectedTaskIds = /* @__PURE__ */ new Set();
7834
8042
  for (let index = 0; index < maxTasks; index++) {
7835
8043
  const candidateTaskId = candidateTaskIds?.[index];
7836
8044
  if (candidateTaskIds && !candidateTaskId) break;
8045
+ if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
7837
8046
  const claim = await claimReadyTask(projectRoot, {
7838
8047
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {},
7839
8048
  ...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
@@ -7858,14 +8067,22 @@ function makeKanbanQueueTool(director, roster) {
7858
8067
  if (costCeiling !== void 0) {
7859
8068
  const remaining = director.getRemainingBudgetUsd();
7860
8069
  if (remaining !== void 0 && remaining < costCeiling) {
7861
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7862
- status: "failed",
7863
- error: `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`,
7864
- lastResult: "Skipped by kanban_queue cost gate (Sprint 3)"
7865
- });
8070
+ budgetRejectedTaskIds.add(claim.task.id);
8071
+ const budgetError = `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`;
8072
+ await updateTaskAssignment(
8073
+ projectRoot,
8074
+ claim.board.id,
8075
+ claim.task.id,
8076
+ {
8077
+ ...claim.task.assignment ?? {},
8078
+ status: "failed",
8079
+ error: budgetError
8080
+ },
8081
+ { expectedLeaseId: leaseSeeding.leaseId }
8082
+ );
7866
8083
  errors.push({
7867
8084
  taskId: claim.task.id,
7868
- error: `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`
8085
+ error: budgetError
7869
8086
  });
7870
8087
  continue;
7871
8088
  }
@@ -7873,12 +8090,20 @@ function makeKanbanQueueTool(director, roster) {
7873
8090
  try {
7874
8091
  const config = buildKanbanSubagentConfig(claim.task, i, roster);
7875
8092
  subagentId = await director.spawn(config);
7876
- runTaskId = await director.assign({
7877
- id: randomUUID4(),
8093
+ const dispatchTaskId = randomUUID4();
8094
+ const taskSpec = {
8095
+ id: dispatchTaskId,
7878
8096
  subagentId,
7879
8097
  description: buildKanbanFleetTaskPrompt(claim.board, claim.task, {
7880
- heartbeatIntervalMs: i.heartbeatIntervalMs ?? 6e4,
7881
- leaseTtlMs,
8098
+ // The worker must heartbeat against the lease it actually holds.
8099
+ // effectiveLeaseTtlMs is the real lease window (raised from the
8100
+ // requested TTL when needed so the first heartbeat always lands
8101
+ // before expiry), and the computed heartbeatIntervalMs is always
8102
+ // strictly shorter than that window. Passing the raw requested
8103
+ // values here would give the worker a 60s cadence against a
8104
+ // 10s lease, letting the lease expire before its first renewal.
8105
+ heartbeatIntervalMs,
8106
+ leaseTtlMs: effectiveLeaseTtlMs,
7882
8107
  leaseId: leaseSeeding.leaseId,
7883
8108
  leaseExpiresAt: leaseSeeding.leaseExpiresAt
7884
8109
  }),
@@ -7889,16 +8114,51 @@ function makeKanbanQueueTool(director, roster) {
7889
8114
  kanban: {
7890
8115
  boardId: claim.board.id,
7891
8116
  taskId: claim.task.id,
7892
- origin: claim.task.origin
8117
+ origin: claim.task.origin,
8118
+ projectRoot
7893
8119
  }
7894
8120
  }
7895
- });
7896
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7897
- ...claim.task.assignment ?? {},
7898
- status: "running",
7899
- subagentId,
7900
- runTaskId
7901
- });
8121
+ };
8122
+ const dispatched = await updateTaskAssignment(
8123
+ projectRoot,
8124
+ claim.board.id,
8125
+ claim.task.id,
8126
+ {
8127
+ ...claim.task.assignment ?? {},
8128
+ status: "running",
8129
+ subagentId,
8130
+ runTaskId: dispatchTaskId,
8131
+ // Renew the lease from the actual dispatch moment, not from claim
8132
+ // time. The claim-time lease starts before spawn + queueing, so
8133
+ // `timeoutMs + 60_000` can expire while the worker is still
8134
+ // queued or running, letting recover_stale reclaim and duplicate
8135
+ // the assignment (see auto-extend.ts: timeout extensions can push
8136
+ // execution well past the original timeoutMs). Both heartbeatAt
8137
+ // and leaseExpiresAt are stamped from now so the worker has a
8138
+ // full lease window from the moment it was actually dispatched.
8139
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
8140
+ leaseExpiresAt: new Date(Date.now() + effectiveLeaseTtlMs).toISOString()
8141
+ },
8142
+ { expectedLeaseId: leaseSeeding.leaseId }
8143
+ );
8144
+ if (dispatched === null) {
8145
+ try {
8146
+ await director.terminate(subagentId);
8147
+ } catch (cleanupErr) {
8148
+ errors.push({
8149
+ taskId: claim.task.id,
8150
+ error: `Lease lost during dispatch; orphan subagent ${subagentId} termination failed: ${toErrorMessage(cleanupErr)}`
8151
+ });
8152
+ continue;
8153
+ }
8154
+ errors.push({
8155
+ taskId: claim.task.id,
8156
+ error: `Lease lost during dispatch (reassigned by recovery); subagent ${subagentId} terminated.`
8157
+ });
8158
+ continue;
8159
+ }
8160
+ await director.assign(taskSpec);
8161
+ runTaskId = dispatchTaskId;
7902
8162
  dispatches.push({
7903
8163
  boardId: claim.board.id,
7904
8164
  taskId: claim.task.id,
@@ -7917,28 +8177,97 @@ function makeKanbanQueueTool(director, roster) {
7917
8177
  message = `${message}; cleanup failed for spawned subagent ${subagentId}: ${toErrorMessage(cleanupErr)}`;
7918
8178
  }
7919
8179
  }
7920
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7921
- ...claim.task.assignment ?? {},
7922
- status: "failed",
7923
- ...subagentId !== void 0 ? { subagentId } : {},
7924
- ...runTaskId !== void 0 ? { runTaskId } : {},
7925
- error: message
7926
- });
8180
+ await updateTaskAssignment(
8181
+ projectRoot,
8182
+ claim.board.id,
8183
+ claim.task.id,
8184
+ {
8185
+ ...claim.task.assignment ?? {},
8186
+ status: "failed",
8187
+ ...subagentId !== void 0 ? { subagentId } : {},
8188
+ ...runTaskId !== void 0 ? { runTaskId } : {},
8189
+ error: message
8190
+ },
8191
+ { expectedLeaseId: leaseSeeding.leaseId }
8192
+ );
7927
8193
  errors.push({ taskId: claim.task.id, error: message });
7928
8194
  }
7929
8195
  }
8196
+ if (!i.awaitCompletion && dispatches.length > 0 && typeof director.fleet?.subscribe === "function") {
8197
+ const ourLeaseId = leaseSeeding.leaseId;
8198
+ const disposers = /* @__PURE__ */ new Map();
8199
+ const renewalStartedAt = Date.now();
8200
+ const maxRenewalWindowMs = 24 * 60 * 6e4;
8201
+ const stopAll = () => {
8202
+ clearInterval(renewalTimer);
8203
+ for (const dispose of disposers.values()) dispose();
8204
+ disposers.clear();
8205
+ };
8206
+ const renewalTimer = setInterval(() => {
8207
+ if (disposers.size === 0 || Date.now() - renewalStartedAt > maxRenewalWindowMs) {
8208
+ stopAll();
8209
+ return;
8210
+ }
8211
+ const refreshedExpiry = new Date(Date.now() + effectiveLeaseTtlMs).toISOString();
8212
+ for (const dispatch of dispatches) {
8213
+ if (!disposers.has(dispatch.runTaskId)) continue;
8214
+ void heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
8215
+ leaseExpiresAt: refreshedExpiry,
8216
+ expectedLeaseId: ourLeaseId
8217
+ }).catch(() => {
8218
+ });
8219
+ }
8220
+ }, heartbeatIntervalMs);
8221
+ renewalTimer.unref?.();
8222
+ for (const dispatch of dispatches) {
8223
+ const dispose = director.fleet.subscribe(dispatch.subagentId, (event) => {
8224
+ if (event.type !== "subagent.completed" && event.type !== "subagent.stopped") return;
8225
+ if (event.type === "subagent.completed" && event.taskId !== dispatch.runTaskId) return;
8226
+ disposers.get(dispatch.runTaskId)?.();
8227
+ disposers.delete(dispatch.runTaskId);
8228
+ if (disposers.size === 0) clearInterval(renewalTimer);
8229
+ });
8230
+ disposers.set(dispatch.runTaskId, dispose);
8231
+ }
8232
+ }
7930
8233
  let results;
7931
8234
  if (i.awaitCompletion && dispatches.length > 0) {
7932
- results = await director.awaitTasks(dispatches.map((dispatch) => dispatch.runTaskId));
8235
+ const ourLeaseId = leaseSeeding.leaseId;
8236
+ const runTaskIds = new Set(dispatches.map((dispatch) => dispatch.runTaskId));
8237
+ const heartbeatTimer = setInterval(async () => {
8238
+ const refreshedExpiry = new Date(Date.now() + effectiveLeaseTtlMs).toISOString();
8239
+ for (const dispatch of dispatches) {
8240
+ if (!runTaskIds.has(dispatch.runTaskId)) continue;
8241
+ try {
8242
+ await heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
8243
+ leaseExpiresAt: refreshedExpiry,
8244
+ expectedLeaseId: ourLeaseId
8245
+ });
8246
+ } catch {
8247
+ }
8248
+ }
8249
+ }, heartbeatIntervalMs);
8250
+ try {
8251
+ results = await director.awaitTasks(dispatches.map((dispatch) => dispatch.runTaskId));
8252
+ } finally {
8253
+ clearInterval(heartbeatTimer);
8254
+ }
7933
8255
  for (const result of results) {
7934
8256
  const dispatch = dispatches.find((candidate) => candidate.runTaskId === result.taskId);
7935
8257
  if (!dispatch) continue;
7936
- await updateTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
7937
- status: result.status === "success" ? "completed" : "failed",
7938
- subagentId: result.subagentId,
7939
- runTaskId: result.taskId,
7940
- ...result.status === "success" ? { lastResult: resultToText(result) } : { error: resultErrorText(result) }
7941
- });
8258
+ runTaskIds.delete(dispatch.runTaskId);
8259
+ await updateTaskAssignment(
8260
+ projectRoot,
8261
+ dispatch.boardId,
8262
+ dispatch.taskId,
8263
+ {
8264
+ status: result.status === "success" ? "completed" : "failed",
8265
+ subagentId: result.subagentId,
8266
+ runTaskId: result.taskId,
8267
+ ...result.status === "success" ? { lastResult: resultToText(result) } : { error: resultErrorText(result) }
8268
+ },
8269
+ { expectedLeaseId: ourLeaseId }
8270
+ );
7942
8271
  if (result.status !== "success") {
7943
8272
  resultFailures.push({
7944
8273
  taskId: dispatch.taskId,
@@ -8046,6 +8375,16 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
8046
8375
  task.origin.phaseId ? `phaseId: ${task.origin.phaseId}` : "",
8047
8376
  task.origin.taskId ? `sourceTaskId: ${task.origin.taskId}` : ""
8048
8377
  ].filter(Boolean).join("\n") : "";
8378
+ const boundaries = [
8379
+ board.boundary?.enabled ? `board: ${describeKanbanBoundary(board.boundary)}` : "",
8380
+ task.boundary?.enabled ? `task: ${describeKanbanBoundary(task.boundary)}` : "",
8381
+ ...(board.boundary?.allow ?? []).map(
8382
+ (selector) => `board allow ${selector.access} ${selector.kind}:${selector.path}`
8383
+ ),
8384
+ ...(task.boundary?.allow ?? []).map(
8385
+ (selector) => `task allow ${selector.access} ${selector.kind}:${selector.path}`
8386
+ )
8387
+ ].filter(Boolean);
8049
8388
  return [
8050
8389
  "You are processing a claimed WrongStack Kanban task.",
8051
8390
  "",
@@ -8068,6 +8407,8 @@ ${checks}` : "",
8068
8407
  metrics ? `Goal metrics:
8069
8408
  ${metrics}` : "",
8070
8409
  task.labels?.length ? `Labels: ${task.labels.join(", ")}` : "",
8410
+ boundaries.length ? `BOUNDARY CONTRACT (enforced by the tool runtime; do not attempt to bypass):
8411
+ ${boundaries.map((line) => `- ${line}`).join("\n")}` : "",
8071
8412
  "Lease contract (Sprint 1):",
8072
8413
  `- leaseId: ${lease.leaseId}`,
8073
8414
  `- claimedAt: ${task.assignment?.claimedAt ?? "<unknown>"}`,
@@ -8083,10 +8424,21 @@ ${metrics}` : "",
8083
8424
  "Work this task end-to-end. Treat the board as a live plan, not a frozen assignment snapshot.",
8084
8425
  `Before material work and whenever evidence changes the plan, call kanban with action "get_board" and boardId "${board.id}" to reassess current tasks, dependencies, priorities, and peer changes.`,
8085
8426
  "You may use kanban add_task, update_task, split_task, merge_tasks, move_task, delete_task, or dependency actions when reassessment shows the plan should change. Preserve traceability and do not keep obsolete work merely because it existed at dispatch time.",
8427
+ "",
8428
+ "FIELD MAINTENANCE \u2014 keep every task detail current:",
8429
+ '- Call "update_task" to refresh description, priority, type, labels, or estimatedHours as the work evolves.',
8430
+ '- Call "add_note" with author and content to record progress, decisions, or roadblock context.',
8431
+ '- Call "add_check" / "update_check" with description and status (pending/passed/failed) to track acceptance criteria.',
8432
+ '- Call "add_goal_metric" / "update_goal_metric" with name and current/target to report measurable progress.',
8433
+ '- Call "add_link" with url and type (issue/pr/doc/commit/design/file/url/other) to attach evidence.',
8434
+ '- Call "update_task" to set actualHours when you have a final time estimate.',
8435
+ "Do not leave stale or placeholder field values \u2014 the kanban board is the shared source of truth for every stakeholder.",
8436
+ "",
8437
+ "LEASE & LIFECYCLE:",
8086
8438
  "Every successful board mutation updates shared pending work and notifies the owning session. Re-read the board after mutations and adapt to changes made by other agents; never assume your initial todo list remains authoritative.",
8087
- `When you start, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", and assignmentStatus "running". Include subagentId and runTaskId when you have them.`,
8088
- `To stay alive in the queue, call kanban with action "heartbeat_assignment", boardId "${board.id}", taskId "${task.id}", and heartbeatAt set to the current time. Cadence must be <= heartbeatIntervalMs and well before leaseExpiresAt.`,
8089
- `When you finish, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", and assignmentStatus "completed" or "failed". Include lastResult or error.`,
8439
+ `When you start, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", assignmentStatus "running", and expectedLeaseId "${lease.leaseId}". Include subagentId and runTaskId when you have them. The expectedLeaseId fence makes the write a safe no-op if recover_stale already reassigned the task because your lease expired.`,
8440
+ `To stay alive in the queue, call kanban with action "heartbeat_assignment", boardId "${board.id}", taskId "${task.id}", heartbeatAt set to the current time, and expectedLeaseId "${lease.leaseId}". Cadence must be <= heartbeatIntervalMs and well before leaseExpiresAt. The expectedLeaseId fence makes the heartbeat a safe no-op if the lease was reassigned, so a stale worker cannot renew a successor's lease.`,
8441
+ `When you finish, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", assignmentStatus "completed" or "failed", and expectedLeaseId "${lease.leaseId}". Include lastResult or error. Populate every relevant detail field before the terminal mark_assignment so the card's final state is complete.`,
8090
8442
  `If you cannot finish in time, call kanban with action "heartbeat_assignment" to extend the lease, or with action "release_task" to release so another worker can claim. Do NOT silently abandon the assignment.`,
8091
8443
  'On failure the host may call kanban with action "recover_stale" (mode: retry/release/fail); respect its decisions and do not duplicate work in parallel.',
8092
8444
  "When finished, report what changed, what you verified, and any remaining blockers."
@@ -8785,18 +9137,20 @@ function isTextBlock(b) {
8785
9137
  // src/types/provider.ts
8786
9138
  var CONTEXT_OVERFLOW_RE = /context.length|context.window|max.*tokens?.*exceeded|prompt is too long|too long|exceeds the context|\btokens\b.*exceed|context_length_exceeded/i;
8787
9139
  var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
9140
+ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit/i;
8788
9141
  function classifyProviderError(status, body, message) {
8789
9142
  const type = body?.type;
9143
+ const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
8790
9144
  if (status === 0) return "network";
8791
9145
  if (status === 408) return "timeout";
8792
9146
  if (status === 599) return "stream_hang";
9147
+ if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
8793
9148
  if (type === "rate_limit_error" || status === 429) return "rate_limit";
8794
9149
  if (type === "overloaded_error" || status === 529) return "overloaded";
8795
9150
  if (status >= 500) return "server";
8796
9151
  if (type === "authentication_error" || type === "permission_error" || status === 401 || status === 403) {
8797
9152
  return "auth";
8798
9153
  }
8799
- const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
8800
9154
  if (type === "content_filter" || CONTENT_FILTER_RE.test(text)) return "content_filter";
8801
9155
  if (status === 413 || status >= 400 && CONTEXT_OVERFLOW_RE.test(text)) {
8802
9156
  return "context_overflow";
@@ -8809,6 +9163,7 @@ function isRetryableKind(kind) {
8809
9163
  }
8810
9164
  var RETRYABLE_BY_KIND = {
8811
9165
  rate_limit: true,
9166
+ quota_exhausted: false,
8812
9167
  overloaded: true,
8813
9168
  server: true,
8814
9169
  timeout: true,
@@ -8887,6 +9242,7 @@ var KIND_TO_CODE = {
8887
9242
  network: ERROR_CODES.PROVIDER_NETWORK_ERROR,
8888
9243
  timeout: ERROR_CODES.PROVIDER_NETWORK_ERROR,
8889
9244
  rate_limit: ERROR_CODES.PROVIDER_RATE_LIMITED,
9245
+ quota_exhausted: ERROR_CODES.PROVIDER_RATE_LIMITED,
8890
9246
  auth: ERROR_CODES.PROVIDER_AUTH_FAILED,
8891
9247
  overloaded: ERROR_CODES.PROVIDER_OVERLOADED,
8892
9248
  context_overflow: ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW,
@@ -8986,6 +9342,7 @@ function classifySubagentError(err, hints = {}) {
8986
9342
  function providerErrorToSubagentError(err, message, cause) {
8987
9343
  switch (err.kind) {
8988
9344
  case "rate_limit":
9345
+ case "quota_exhausted":
8989
9346
  return {
8990
9347
  kind: "provider_rate_limit",
8991
9348
  message,
@@ -9393,7 +9750,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9393
9750
  taskIds.map((id) => {
9394
9751
  const cached = this.completedResults.find((r) => r.taskId === id);
9395
9752
  if (cached) return cached;
9396
- return new Promise((resolve12, reject) => {
9753
+ return new Promise((resolve14, reject) => {
9397
9754
  const timeout = setTimeout(() => {
9398
9755
  this.off("task.completed", handler);
9399
9756
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -9402,7 +9759,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9402
9759
  if (result.taskId === id) {
9403
9760
  clearTimeout(timeout);
9404
9761
  this.off("task.completed", handler);
9405
- resolve12(result);
9762
+ resolve14(result);
9406
9763
  }
9407
9764
  };
9408
9765
  this.on("task.completed", handler);
@@ -9427,13 +9784,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9427
9784
  const done = new Set(completed.map((r) => r.taskId));
9428
9785
  return { completed, pending: taskIds.filter((id) => !done.has(id)) };
9429
9786
  }
9430
- return new Promise((resolve12) => {
9787
+ return new Promise((resolve14) => {
9431
9788
  let timer;
9432
9789
  const handler = ({ result }) => {
9433
9790
  if (!ids.has(result.taskId)) return;
9434
9791
  if (timer) clearTimeout(timer);
9435
9792
  this.off("task.completed", handler);
9436
- resolve12({
9793
+ resolve14({
9437
9794
  completed: [result],
9438
9795
  pending: taskIds.filter((id) => id !== result.taskId)
9439
9796
  });
@@ -9441,7 +9798,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9441
9798
  if (opts?.timeoutMs !== void 0) {
9442
9799
  timer = setTimeout(() => {
9443
9800
  this.off("task.completed", handler);
9444
- resolve12({ completed: [], pending: [...taskIds], timedOut: true });
9801
+ resolve14({ completed: [], pending: [...taskIds], timedOut: true });
9445
9802
  }, opts.timeoutMs);
9446
9803
  }
9447
9804
  this.on("task.completed", handler);
@@ -9723,12 +10080,12 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9723
10080
  }
9724
10081
  return new Promise((resolveDecision) => {
9725
10082
  let settled = false;
9726
- const resolve12 = (d) => {
10083
+ const resolve14 = (d) => {
9727
10084
  if (settled) return;
9728
10085
  settled = true;
9729
10086
  resolveDecision(d);
9730
10087
  };
9731
- const fallback = setTimeout(() => resolve12("stop"), DECISION_TIMEOUT_MS);
10088
+ const fallback = setTimeout(() => resolve14("stop"), DECISION_TIMEOUT_MS);
9732
10089
  const sessionId = this.currentSessionId();
9733
10090
  budget._events?.emit("budget.threshold_reached", {
9734
10091
  ...sessionId ? { sessionId } : {},
@@ -9746,11 +10103,11 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9746
10103
  // disagreeing, resolves as a stop). Async grants still resolve.
9747
10104
  extend: (extra) => {
9748
10105
  clearTimeout(fallback);
9749
- queueMicrotask(() => resolve12({ extend: extra }));
10106
+ queueMicrotask(() => resolve14({ extend: extra }));
9750
10107
  },
9751
10108
  deny: () => {
9752
10109
  clearTimeout(fallback);
9753
- resolve12("stop");
10110
+ resolve14("stop");
9754
10111
  }
9755
10112
  });
9756
10113
  });
@@ -10375,6 +10732,10 @@ var Director = class _Director {
10375
10732
  largeAnswerStore;
10376
10733
  /** Shared provider/model status tracker, or undefined. */
10377
10734
  statusTracker;
10735
+ /** Session/leader's provider id — absolute last-resort fallback for every spawn. */
10736
+ sessionProvider;
10737
+ /** Session/leader's model id — paired with sessionProvider above. */
10738
+ sessionModel;
10378
10739
  constructor(opts) {
10379
10740
  this.id = opts.config.coordinatorId || randomUUID6();
10380
10741
  this.brain = opts.brain;
@@ -10415,6 +10776,8 @@ var Director = class _Director {
10415
10776
  this.fleetManager = opts.fleetManager;
10416
10777
  this.statusTracker = opts.statusTracker;
10417
10778
  this.logger = opts.logger;
10779
+ this.sessionProvider = opts.sessionProvider;
10780
+ this.sessionModel = opts.sessionModel;
10418
10781
  if (this.sharedScratchpadPath) {
10419
10782
  void fsp5.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
10420
10783
  }
@@ -10923,20 +11286,31 @@ var Director = class _Director {
10923
11286
  return result.subagentId;
10924
11287
  }
10925
11288
  resolveSpawnModel(config) {
11289
+ if (config.provider?.trim() === "") config.provider = void 0;
11290
+ if (config.model?.trim() === "") config.model = void 0;
10926
11291
  if (!config.model && this.modelMatrix) {
10927
11292
  const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
10928
11293
  const resolution = resolveModelMatrixResolution(matrix, config.role);
10929
11294
  const entry = resolution?.source === "default" && roleNeedsIndependentReviewModel(config.role) ? void 0 : resolution?.entry;
10930
- if (entry?.model) {
10931
- config.model = entry.model;
11295
+ if (entry) {
11296
+ if (entry.model) config.model = entry.model;
10932
11297
  if (entry.provider) config.provider = entry.provider;
10933
11298
  if (entry.fallbackProfile) config.fallbackProfile = entry.fallbackProfile;
10934
11299
  if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
10935
- } else if (entry?.fallbackProfile) {
10936
- config.fallbackProfile = entry.fallbackProfile;
10937
- if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
10938
11300
  }
10939
11301
  }
11302
+ if (!config.provider && this.sessionProvider) {
11303
+ config.provider = this.sessionProvider;
11304
+ this.logger?.info(
11305
+ `spawn: provider="${config.provider}" for role "${config.role ?? "?"}" fell back to session provider (matrix resolution left it undefined)`
11306
+ );
11307
+ }
11308
+ if (!config.model && this.sessionModel) {
11309
+ config.model = this.sessionModel;
11310
+ this.logger?.info(
11311
+ `spawn: model="${config.model}" for role "${config.role ?? "?"}" fell back to session model (matrix resolution left it undefined)`
11312
+ );
11313
+ }
10940
11314
  if (this.statusTracker && config.provider && config.model) {
10941
11315
  if (!this.statusTracker.isAvailable(config.provider, config.model)) {
10942
11316
  this.logger?.warn(
@@ -11151,7 +11525,7 @@ var Director = class _Director {
11151
11525
  })),
11152
11526
  usage: this.usage.snapshot()
11153
11527
  };
11154
- await fsp5.mkdir(path7.dirname(this.manifestPath), { recursive: true });
11528
+ await fsp5.mkdir(path8.dirname(this.manifestPath), { recursive: true });
11155
11529
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
11156
11530
  return this.manifestPath;
11157
11531
  }
@@ -11346,11 +11720,11 @@ var Director = class _Director {
11346
11720
  if (cached) return cached;
11347
11721
  const existing = this.taskWaiters.get(id);
11348
11722
  if (existing) return existing.promise;
11349
- let resolve12;
11723
+ let resolve14;
11350
11724
  const promise = new Promise((res) => {
11351
- resolve12 = res;
11725
+ resolve14 = res;
11352
11726
  });
11353
- this.taskWaiters.set(id, { promise, resolve: resolve12 });
11727
+ this.taskWaiters.set(id, { promise, resolve: resolve14 });
11354
11728
  return promise;
11355
11729
  })
11356
11730
  );
@@ -11379,10 +11753,10 @@ var Director = class _Director {
11379
11753
  pending: taskIds.filter((id) => !done.has(id))
11380
11754
  });
11381
11755
  }
11382
- return new Promise((resolve12) => {
11756
+ return new Promise((resolve14) => {
11383
11757
  const entry = {
11384
11758
  ids: new Set(taskIds),
11385
- resolve: (r) => resolve12({
11759
+ resolve: (r) => resolve14({
11386
11760
  completed: [r],
11387
11761
  pending: taskIds.filter((id) => id !== r.taskId)
11388
11762
  })
@@ -11390,7 +11764,7 @@ var Director = class _Director {
11390
11764
  if (opts?.timeoutMs !== void 0) {
11391
11765
  entry.timer = setTimeout(() => {
11392
11766
  this.anyWaiters.delete(entry);
11393
- resolve12({ completed: [], pending: [...taskIds], timedOut: true });
11767
+ resolve14({ completed: [], pending: [...taskIds], timedOut: true });
11394
11768
  }, opts.timeoutMs);
11395
11769
  }
11396
11770
  this.anyWaiters.add(entry);
@@ -11555,7 +11929,7 @@ var Director = class _Director {
11555
11929
  */
11556
11930
  async readSession(subagentId, tail) {
11557
11931
  if (!this.sessionsRoot) return null;
11558
- const filePath = path7.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
11932
+ const filePath = path8.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
11559
11933
  let raw;
11560
11934
  try {
11561
11935
  raw = await fsp5.readFile(filePath, "utf8");
@@ -11709,19 +12083,19 @@ var Director = class _Director {
11709
12083
  };
11710
12084
 
11711
12085
  // src/coordination/director-session.ts
11712
- import * as path12 from "node:path";
12086
+ import * as path13 from "node:path";
11713
12087
 
11714
12088
  // src/storage/session-store.ts
11715
12089
  import { createHash as createHash4 } from "node:crypto";
11716
12090
  import { createReadStream } from "node:fs";
11717
12091
  import * as fsp9 from "node:fs/promises";
11718
- import * as path11 from "node:path";
12092
+ import * as path12 from "node:path";
11719
12093
  import { createInterface } from "node:readline";
11720
12094
 
11721
12095
  // src/storage/file-session-writer.ts
11722
12096
  import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
11723
12097
  import * as fsp6 from "node:fs/promises";
11724
- import * as path8 from "node:path";
12098
+ import * as path9 from "node:path";
11725
12099
 
11726
12100
  // src/storage/session-helpers.ts
11727
12101
  function userInputTitle(content) {
@@ -11738,7 +12112,7 @@ var FileSessionWriter = class _FileSessionWriter {
11738
12112
  this.meta = meta;
11739
12113
  this.events = events;
11740
12114
  this.resumed = opts.resumed ?? false;
11741
- this.manifestFile = opts.dir ? path8.join(opts.dir, `${path8.basename(id)}.summary.json`) : "";
12115
+ this.manifestFile = opts.dir ? path9.join(opts.dir, `${path9.basename(id)}.summary.json`) : "";
11742
12116
  this.filePath = opts.filePath ?? "";
11743
12117
  this.secretScrubber = opts.secretScrubber;
11744
12118
  this.checkpointCas = opts.checkpointCas;
@@ -12520,7 +12894,7 @@ var FileSessionWriter = class _FileSessionWriter {
12520
12894
  import { spawn } from "node:child_process";
12521
12895
  import { createHash as createHash2, randomUUID as randomUUID7 } from "node:crypto";
12522
12896
  import * as fsp7 from "node:fs/promises";
12523
- import * as path9 from "node:path";
12897
+ import * as path10 from "node:path";
12524
12898
 
12525
12899
  // src/storage/storage-concurrency.ts
12526
12900
  async function mapWithConcurrency(items, concurrency, mapper) {
@@ -12551,29 +12925,29 @@ function sha256(content) {
12551
12925
  return createHash2("sha256").update(content).digest("hex");
12552
12926
  }
12553
12927
  function isInside(root, target) {
12554
- const relative4 = path9.relative(root, target);
12555
- return relative4 === "" || !relative4.startsWith("..") && !path9.isAbsolute(relative4);
12928
+ const relative6 = path10.relative(root, target);
12929
+ return relative6 === "" || !relative6.startsWith("..") && !path10.isAbsolute(relative6);
12556
12930
  }
12557
12931
  function normalizeRelative(input) {
12558
- if (!input || path9.isAbsolute(input)) return null;
12932
+ if (!input || path10.isAbsolute(input)) return null;
12559
12933
  const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
12560
- const resolved = path9.posix.normalize(normalized);
12934
+ const resolved = path10.posix.normalize(normalized);
12561
12935
  if (!resolved || resolved === "." || resolved === ".." || resolved.startsWith("../")) return null;
12562
12936
  return resolved;
12563
12937
  }
12564
12938
  function parseNulPaths(output) {
12565
12939
  return output.split("\0").map(normalizeRelative).filter((value) => value !== null);
12566
12940
  }
12567
- function isWrongStackWorktreePath(relative4) {
12568
- return relative4 === ".wrongstack/worktrees" || relative4.startsWith(".wrongstack/worktrees/");
12941
+ function isWrongStackWorktreePath(relative6) {
12942
+ return relative6 === ".wrongstack/worktrees" || relative6.startsWith(".wrongstack/worktrees/");
12569
12943
  }
12570
12944
  var SessionCheckpointCas = class {
12571
12945
  rootDir;
12572
12946
  projectRoot;
12573
12947
  runGit;
12574
12948
  constructor(opts) {
12575
- this.rootDir = path9.resolve(opts.rootDir);
12576
- this.projectRoot = path9.resolve(opts.projectRoot);
12949
+ this.rootDir = path10.resolve(opts.rootDir);
12950
+ this.projectRoot = path10.resolve(opts.projectRoot);
12577
12951
  this.runGit = opts.runGit ?? defaultRunGit;
12578
12952
  }
12579
12953
  async capture(_sessionId, _promptIndex) {
@@ -12591,60 +12965,60 @@ var SessionCheckpointCas = class {
12591
12965
  }
12592
12966
  const relativePaths = [
12593
12967
  .../* @__PURE__ */ new Set([...parseNulPaths(tracked.stdout), ...parseNulPaths(untracked.stdout)])
12594
- ].filter((relative4) => !isWrongStackWorktreePath(relative4)).sort();
12968
+ ].filter((relative6) => !isWrongStackWorktreePath(relative6)).sort();
12595
12969
  const unresolved = [];
12596
12970
  let scheduledBytes = 0;
12597
12971
  const captured = await mapWithConcurrency(
12598
12972
  relativePaths,
12599
12973
  CAPTURE_CONCURRENCY,
12600
- async (relative4) => {
12601
- const absolute = path9.resolve(this.projectRoot, ...relative4.split("/"));
12974
+ async (relative6) => {
12975
+ const absolute = path10.resolve(this.projectRoot, ...relative6.split("/"));
12602
12976
  if (!isInside(this.projectRoot, absolute)) {
12603
- unresolved.push({ path: relative4, reason: "path escapes project root" });
12977
+ unresolved.push({ path: relative6, reason: "path escapes project root" });
12604
12978
  return null;
12605
12979
  }
12606
12980
  try {
12607
- const stat12 = await fsp7.lstat(absolute);
12608
- if (stat12.isSymbolicLink()) {
12981
+ const stat13 = await fsp7.lstat(absolute);
12982
+ if (stat13.isSymbolicLink()) {
12609
12983
  const linkTarget = await fsp7.readlink(absolute);
12610
- const resolvedLink = path9.resolve(path9.dirname(absolute), linkTarget);
12611
- if (path9.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
12984
+ const resolvedLink = path10.resolve(path10.dirname(absolute), linkTarget);
12985
+ if (path10.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
12612
12986
  unresolved.push({
12613
- path: relative4,
12987
+ path: relative6,
12614
12988
  reason: "symlink target escapes project root"
12615
12989
  });
12616
12990
  return null;
12617
12991
  }
12618
- return { path: relative4, state: "symlink", linkTarget };
12992
+ return { path: relative6, state: "symlink", linkTarget };
12619
12993
  }
12620
- if (!stat12.isFile()) {
12621
- unresolved.push({ path: relative4, reason: "changed path is not a regular file" });
12994
+ if (!stat13.isFile()) {
12995
+ unresolved.push({ path: relative6, reason: "changed path is not a regular file" });
12622
12996
  return null;
12623
12997
  }
12624
- if (stat12.size > MAX_BLOB_BYTES) {
12998
+ if (stat13.size > MAX_BLOB_BYTES) {
12625
12999
  unresolved.push({
12626
- path: relative4,
13000
+ path: relative6,
12627
13001
  reason: `file exceeds ${MAX_BLOB_BYTES}-byte checkpoint blob limit`
12628
13002
  });
12629
13003
  return null;
12630
13004
  }
12631
- if (scheduledBytes + stat12.size > MAX_CHECKPOINT_BYTES) {
13005
+ if (scheduledBytes + stat13.size > MAX_CHECKPOINT_BYTES) {
12632
13006
  unresolved.push({
12633
- path: relative4,
13007
+ path: relative6,
12634
13008
  reason: `checkpoint exceeds ${MAX_CHECKPOINT_BYTES}-byte aggregate blob limit`
12635
13009
  });
12636
13010
  return null;
12637
13011
  }
12638
- scheduledBytes += stat12.size;
13012
+ scheduledBytes += stat13.size;
12639
13013
  const content = await fsp7.readFile(absolute);
12640
13014
  const blobHash = sha256(content);
12641
13015
  await this.putBlob(blobHash, content);
12642
- return { path: relative4, state: "file", blobHash, mode: stat12.mode & 511 };
13016
+ return { path: relative6, state: "file", blobHash, mode: stat13.mode & 511 };
12643
13017
  } catch (err) {
12644
13018
  if (err.code === "ENOENT") {
12645
- return { path: relative4, state: "absent" };
13019
+ return { path: relative6, state: "absent" };
12646
13020
  }
12647
- unresolved.push({ path: relative4, reason: toErrorMessage(err) });
13021
+ unresolved.push({ path: relative6, reason: toErrorMessage(err) });
12648
13022
  return null;
12649
13023
  }
12650
13024
  }
@@ -12670,7 +13044,7 @@ var SessionCheckpointCas = class {
12670
13044
  };
12671
13045
  }
12672
13046
  async materialize(checkpoint, targetRoot) {
12673
- const target = path9.resolve(targetRoot);
13047
+ const target = path10.resolve(targetRoot);
12674
13048
  if (target === this.projectRoot) {
12675
13049
  throw new Error("Refusing to materialize a workspace checkpoint over the parent project root");
12676
13050
  }
@@ -12710,10 +13084,10 @@ var SessionCheckpointCas = class {
12710
13084
  try {
12711
13085
  const output = await this.safeOutputPath(target, realTarget, entry.path);
12712
13086
  if (entry.state === "symlink") {
12713
- if (path9.isAbsolute(entry.linkTarget)) {
13087
+ if (path10.isAbsolute(entry.linkTarget)) {
12714
13088
  throw new Error("absolute symlink target refused");
12715
13089
  }
12716
- const resolvedLink = path9.resolve(path9.dirname(output), entry.linkTarget);
13090
+ const resolvedLink = path10.resolve(path10.dirname(output), entry.linkTarget);
12717
13091
  if (!isInside(target, resolvedLink)) throw new Error("symlink target escapes checkpoint root");
12718
13092
  }
12719
13093
  prepared.push({
@@ -12741,7 +13115,7 @@ var SessionCheckpointCas = class {
12741
13115
  await fsp7.unlink(output).catch((err) => {
12742
13116
  if (err.code !== "ENOENT") throw err;
12743
13117
  });
12744
- await fsp7.mkdir(path9.dirname(output), { recursive: true });
13118
+ await fsp7.mkdir(path10.dirname(output), { recursive: true });
12745
13119
  await fsp7.symlink(entry.linkTarget, output);
12746
13120
  writtenFiles.push(entry.path);
12747
13121
  continue;
@@ -12758,15 +13132,15 @@ var SessionCheckpointCas = class {
12758
13132
  }
12759
13133
  objectPath(hash2) {
12760
13134
  if (!HASH_RE.test(hash2)) throw new Error(`Invalid CAS object hash: ${hash2}`);
12761
- return path9.join(this.rootDir, "objects", hash2.slice(0, 2), hash2.slice(2));
13135
+ return path10.join(this.rootDir, "objects", hash2.slice(0, 2), hash2.slice(2));
12762
13136
  }
12763
13137
  manifestPath(hash2) {
12764
13138
  if (!HASH_RE.test(hash2)) throw new Error(`Invalid checkpoint manifest hash: ${hash2}`);
12765
- return path9.join(this.rootDir, "manifests", `${hash2}.json`);
13139
+ return path10.join(this.rootDir, "manifests", `${hash2}.json`);
12766
13140
  }
12767
13141
  async putBlob(hash2, content) {
12768
13142
  const target = this.objectPath(hash2);
12769
- await fsp7.mkdir(path9.dirname(target), { recursive: true });
13143
+ await fsp7.mkdir(path10.dirname(target), { recursive: true });
12770
13144
  try {
12771
13145
  const existing = await fsp7.readFile(target);
12772
13146
  if (sha256(existing) !== hash2) throw new Error(`Corrupt CAS object collision: ${hash2}`);
@@ -12774,9 +13148,9 @@ var SessionCheckpointCas = class {
12774
13148
  } catch (err) {
12775
13149
  if (err.code !== "ENOENT") throw err;
12776
13150
  }
12777
- const temp = path9.join(
12778
- path9.dirname(target),
12779
- `.${path9.basename(target)}.${process.pid}.${randomUUID7()}.tmp`
13151
+ const temp = path10.join(
13152
+ path10.dirname(target),
13153
+ `.${path10.basename(target)}.${process.pid}.${randomUUID7()}.tmp`
12780
13154
  );
12781
13155
  let handle;
12782
13156
  try {
@@ -12834,10 +13208,10 @@ var SessionCheckpointCas = class {
12834
13208
  }
12835
13209
  return parsed;
12836
13210
  }
12837
- async safeOutputPath(target, realTarget, relative4) {
12838
- const normalized = normalizeRelative(relative4);
13211
+ async safeOutputPath(target, realTarget, relative6) {
13212
+ const normalized = normalizeRelative(relative6);
12839
13213
  if (!normalized) throw new Error("invalid relative path");
12840
- const output = path9.resolve(target, ...normalized.split("/"));
13214
+ const output = path10.resolve(target, ...normalized.split("/"));
12841
13215
  if (!isInside(target, output)) throw new Error("path escapes checkpoint target");
12842
13216
  let probe = output;
12843
13217
  for (; ; ) {
@@ -12847,7 +13221,7 @@ var SessionCheckpointCas = class {
12847
13221
  return output;
12848
13222
  } catch (err) {
12849
13223
  if (err.code !== "ENOENT") throw err;
12850
- const parent = path9.dirname(probe);
13224
+ const parent = path10.dirname(probe);
12851
13225
  if (parent === probe) throw err;
12852
13226
  probe = parent;
12853
13227
  }
@@ -12855,7 +13229,7 @@ var SessionCheckpointCas = class {
12855
13229
  }
12856
13230
  };
12857
13231
  function defaultRunGit(args, cwd) {
12858
- return new Promise((resolve12) => {
13232
+ return new Promise((resolve14) => {
12859
13233
  const stdoutChunks = [];
12860
13234
  const stderrChunks = [];
12861
13235
  let stdoutBytes = 0;
@@ -12898,8 +13272,8 @@ function defaultRunGit(args, cwd) {
12898
13272
  stdoutTruncated,
12899
13273
  stderrTruncated
12900
13274
  });
12901
- child.on("error", (err) => resolve12(result(1, err.message)));
12902
- child.on("close", (code) => resolve12(result(code ?? 1)));
13275
+ child.on("error", (err) => resolve14(result(1, err.message)));
13276
+ child.on("close", (code) => resolve14(result(code ?? 1)));
12903
13277
  });
12904
13278
  }
12905
13279
 
@@ -12913,13 +13287,13 @@ function generateSessionId(startedAt, _model) {
12913
13287
  // src/storage/session-resume-validation.ts
12914
13288
  import { createHash as createHash3 } from "node:crypto";
12915
13289
  import * as fsp8 from "node:fs/promises";
12916
- import * as path10 from "node:path";
13290
+ import * as path11 from "node:path";
12917
13291
  var MAX_REVALIDATE_BYTES = 5 * 1024 * 1024;
12918
13292
  var VALIDATION_CONCURRENCY = 8;
12919
13293
  var NOTICE_PATH_LIMIT = 20;
12920
13294
  function isInside2(root, target) {
12921
- const relative4 = path10.relative(root, target);
12922
- return relative4 === "" || !relative4.startsWith("..") && !path10.isAbsolute(relative4);
13295
+ const relative6 = path11.relative(root, target);
13296
+ return relative6 === "" || !relative6.startsWith("..") && !path11.isAbsolute(relative6);
12923
13297
  }
12924
13298
  function errno(err) {
12925
13299
  return err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
@@ -12930,7 +13304,7 @@ function latestObservations(events, projectRoot) {
12930
13304
  if (event.type !== "file_observation" || typeof event.path !== "string" || event.path.length === 0 || typeof event.hash !== "string" || !/^[a-f\d]{64}$/i.test(event.hash)) {
12931
13305
  continue;
12932
13306
  }
12933
- const normalized = path10.resolve(projectRoot, event.path);
13307
+ const normalized = path11.resolve(projectRoot, event.path);
12934
13308
  latest.set(normalized, {
12935
13309
  path: normalized,
12936
13310
  hash: event.hash.toLowerCase(),
@@ -12967,11 +13341,11 @@ async function validateOne(observation, lexicalRoot, realRoot) {
12967
13341
  };
12968
13342
  }
12969
13343
  try {
12970
- const stat12 = await fsp8.stat(realFile);
12971
- if (!stat12.isFile()) {
13344
+ const stat13 = await fsp8.stat(realFile);
13345
+ if (!stat13.isFile()) {
12972
13346
  return { ...base, status: "unreadable", detail: "Path is no longer a regular file." };
12973
13347
  }
12974
- if (stat12.size > MAX_REVALIDATE_BYTES) {
13348
+ if (stat13.size > MAX_REVALIDATE_BYTES) {
12975
13349
  return {
12976
13350
  ...base,
12977
13351
  status: "unreadable",
@@ -12988,7 +13362,7 @@ async function validateOne(observation, lexicalRoot, realRoot) {
12988
13362
  }
12989
13363
  }
12990
13364
  async function validateResumeFileObservations(events, projectRoot) {
12991
- const lexicalRoot = path10.resolve(projectRoot);
13365
+ const lexicalRoot = path11.resolve(projectRoot);
12992
13366
  const realRoot = await fsp8.realpath(lexicalRoot).catch(() => lexicalRoot);
12993
13367
  const observations = latestObservations(events, lexicalRoot);
12994
13368
  const results = await mapWithConcurrency(
@@ -13004,10 +13378,10 @@ async function validateResumeFileObservations(events, projectRoot) {
13004
13378
  }
13005
13379
  function formatResumeValidationNotice(validation, projectRoot) {
13006
13380
  if (validation.staleFiles.length === 0) return null;
13007
- const root = path10.resolve(projectRoot);
13381
+ const root = path11.resolve(projectRoot);
13008
13382
  const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
13009
- const relative4 = path10.relative(root, entry.path);
13010
- const display = isInside2(root, entry.path) ? relative4 || "." : entry.path;
13383
+ const relative6 = path11.relative(root, entry.path);
13384
+ const display = isInside2(root, entry.path) ? relative6 || "." : entry.path;
13011
13385
  return `- ${JSON.stringify(display)} [${entry.status}]`;
13012
13386
  });
13013
13387
  const omitted = validation.staleFiles.length - shown.length;
@@ -13140,9 +13514,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
13140
13514
  static LIST_SCAN_CONCURRENCY = 32;
13141
13515
  constructor(opts) {
13142
13516
  this.dir = opts.dir;
13143
- this.projectRoot = opts.projectRoot ? path11.resolve(opts.projectRoot) : void 0;
13517
+ this.projectRoot = opts.projectRoot ? path12.resolve(opts.projectRoot) : void 0;
13144
13518
  this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
13145
- rootDir: path11.join(this.dir, "_cas"),
13519
+ rootDir: path12.join(this.dir, "_cas"),
13146
13520
  projectRoot: this.projectRoot
13147
13521
  }) : void 0;
13148
13522
  this.events = opts.events;
@@ -13208,17 +13582,17 @@ var DefaultSessionStore = class _DefaultSessionStore {
13208
13582
  }
13209
13583
  /** Absolute path to the session index file. */
13210
13584
  get indexFile() {
13211
- return path11.join(this.dir, "_index.jsonl");
13585
+ return path12.join(this.dir, "_index.jsonl");
13212
13586
  }
13213
13587
  /** Join session ID to its absolute path within the store directory. */
13214
13588
  sessionPath(id, ext) {
13215
13589
  return sessionScopedPath(this.dir, id, ext);
13216
13590
  }
13217
13591
  shardManifestPath(shardKey) {
13218
- return shardKey ? path11.join(this.dir, shardKey, "_manifest.json") : path11.join(this.dir, "_manifest.json");
13592
+ return shardKey ? path12.join(this.dir, shardKey, "_manifest.json") : path12.join(this.dir, "_manifest.json");
13219
13593
  }
13220
13594
  shardKeyForSessionId(id) {
13221
- const dirName = path11.dirname(id);
13595
+ const dirName = path12.dirname(id);
13222
13596
  return dirName === "." ? "" : dirName;
13223
13597
  }
13224
13598
  invalidateShardManifestBySessionId(id) {
@@ -13230,7 +13604,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13230
13604
  * subdirectory so sessions group naturally by day.
13231
13605
  */
13232
13606
  async ensureShardDir(id) {
13233
- const dirPath = path11.dirname(sessionScopedPath(this.dir, id, ""));
13607
+ const dirPath = path12.dirname(sessionScopedPath(this.dir, id, ""));
13234
13608
  await ensureDir(dirPath);
13235
13609
  return dirPath;
13236
13610
  }
@@ -13394,7 +13768,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13394
13768
  // Shard directory (sessions/<date>/) — must match create() so the
13395
13769
  // .summary.json sidecar lands next to the JSONL instead of the
13396
13770
  // sessions root (where summaryFor() would never find it).
13397
- dir: path11.dirname(file),
13771
+ dir: path12.dirname(file),
13398
13772
  filePath: file,
13399
13773
  secretScrubber: this.secretScrubber,
13400
13774
  checkpointCas: this.checkpointCas,
@@ -13442,9 +13816,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
13442
13816
  let cacheHit = false;
13443
13817
  try {
13444
13818
  const s = await fsp9.stat(file);
13445
- const stat12 = { mtimeMs: s.mtimeMs, size: s.size };
13819
+ const stat13 = { mtimeMs: s.mtimeMs, size: s.size };
13446
13820
  const cached = this._loadCache.get(id);
13447
- if (cached && cached.mtimeMs === stat12.mtimeMs && cached.size === stat12.size) {
13821
+ if (cached && cached.mtimeMs === stat13.mtimeMs && cached.size === stat13.size) {
13448
13822
  cacheHit = true;
13449
13823
  this._loadCache.delete(id);
13450
13824
  this._loadCache.set(id, cached);
@@ -13633,7 +14007,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13633
14007
  this._loadCache.delete(oldest);
13634
14008
  }
13635
14009
  }
13636
- this._loadCache.set(id, { mtimeMs: stat12.mtimeMs, size: stat12.size, data });
14010
+ this._loadCache.set(id, { mtimeMs: stat13.mtimeMs, size: stat13.size, data });
13637
14011
  }
13638
14012
  return data;
13639
14013
  } catch (err) {
@@ -13682,14 +14056,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
13682
14056
  const limit = opts?.limit;
13683
14057
  const signal = opts?.signal;
13684
14058
  const out = [];
13685
- let stat12;
14059
+ let stat13;
13686
14060
  try {
13687
- stat12 = await fsp9.stat(file);
14061
+ stat13 = await fsp9.stat(file);
13688
14062
  } catch (err) {
13689
14063
  if (err.code === "ENOENT") return [];
13690
14064
  throw err;
13691
14065
  }
13692
- if (stat12.size === 0) return [];
14066
+ if (stat13.size === 0) return [];
13693
14067
  let fh;
13694
14068
  try {
13695
14069
  fh = await fsp9.open(file, "r");
@@ -13880,15 +14254,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
13880
14254
  * Returns empty array when the index doesn't exist or is corrupt.
13881
14255
  */
13882
14256
  async readIndex() {
13883
- let stat12;
14257
+ let stat13;
13884
14258
  try {
13885
14259
  const s = await fsp9.stat(this.indexFile);
13886
- stat12 = { mtimeMs: s.mtimeMs, size: s.size };
14260
+ stat13 = { mtimeMs: s.mtimeMs, size: s.size };
13887
14261
  } catch {
13888
14262
  this._indexCache = null;
13889
14263
  return [];
13890
14264
  }
13891
- if (this._indexCache !== null && this._indexCache.mtimeMs === stat12.mtimeMs && this._indexCache.size === stat12.size) {
14265
+ if (this._indexCache !== null && this._indexCache.mtimeMs === stat13.mtimeMs && this._indexCache.size === stat13.size) {
13892
14266
  return [...this._indexCache.summaries];
13893
14267
  }
13894
14268
  let raw;
@@ -13921,7 +14295,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13921
14295
  if (a.startedAt > b.startedAt) return -1;
13922
14296
  return a.id.localeCompare(b.id);
13923
14297
  });
13924
- this._indexCache = { ...stat12, summaries };
14298
+ this._indexCache = { ...stat13, summaries };
13925
14299
  return [...summaries];
13926
14300
  }
13927
14301
  /**
@@ -14015,7 +14389,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14015
14389
  return entry;
14016
14390
  }
14017
14391
  async collectSessionFilesInShard(shardKey) {
14018
- const dir = shardKey ? path11.join(this.dir, shardKey) : this.dir;
14392
+ const dir = shardKey ? path12.join(this.dir, shardKey) : this.dir;
14019
14393
  const entries = await this.collectSessionFiles(dir, shardKey);
14020
14394
  return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
14021
14395
  }
@@ -14038,13 +14412,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
14038
14412
  if (entry.name === "_index.jsonl") continue;
14039
14413
  const base = entry.name.replace(/\.jsonl$/, "");
14040
14414
  const id = prefix ? `${prefix}/${base}` : base;
14041
- files.push({ id, filePath: path11.join(dir, entry.name) });
14415
+ files.push({ id, filePath: path12.join(dir, entry.name) });
14042
14416
  }
14043
14417
  }
14044
14418
  const childFileArrays = await Promise.all(
14045
14419
  dirEntries.map((entry) => {
14046
14420
  const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
14047
- return this.collectSessionFiles(path11.join(dir, entry.name), childPrefix, depth + 1);
14421
+ return this.collectSessionFiles(path12.join(dir, entry.name), childPrefix, depth + 1);
14048
14422
  })
14049
14423
  );
14050
14424
  return [...childFileArrays.flat(), ...files];
@@ -14077,7 +14451,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14077
14451
  const childIdArrays = await Promise.all(
14078
14452
  dirEntries.map((entry) => {
14079
14453
  const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
14080
- return this.collectSessionIds(path11.join(dir, entry.name), childPrefix, depth + 1);
14454
+ return this.collectSessionIds(path12.join(dir, entry.name), childPrefix, depth + 1);
14081
14455
  })
14082
14456
  );
14083
14457
  return [...childIdArrays.flat(), ...fileIds];
@@ -14091,8 +14465,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
14091
14465
  if (fromManifest) return fromManifest;
14092
14466
  try {
14093
14467
  const full = this.sessionPath(id, ".jsonl");
14094
- const stat12 = await fsp9.stat(full);
14095
- const summary = await this.summarize(id, stat12.mtime.toISOString());
14468
+ const stat13 = await fsp9.stat(full);
14469
+ const summary = await this.summarize(id, stat13.mtime.toISOString());
14096
14470
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
14097
14471
  const msg = toErrorMessage(err);
14098
14472
  this.emitError(id, manifest, "summary_fallback", msg, true);
@@ -14133,18 +14507,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
14133
14507
  async summaryHeaderFor(ref) {
14134
14508
  let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
14135
14509
  try {
14136
- const stat12 = await fsp9.stat(ref.filePath);
14137
- if (!stat12.isFile()) {
14510
+ const stat13 = await fsp9.stat(ref.filePath);
14511
+ if (!stat13.isFile()) {
14138
14512
  return {
14139
14513
  id: ref.id,
14140
14514
  title: "(damaged)",
14141
- startedAt: stat12.mtime.toISOString(),
14515
+ startedAt: stat13.mtime.toISOString(),
14142
14516
  model: "unknown",
14143
14517
  provider: "unknown",
14144
14518
  tokenTotal: 0
14145
14519
  };
14146
14520
  }
14147
- mtime = stat12.mtime.toISOString();
14521
+ mtime = stat13.mtime.toISOString();
14148
14522
  } catch {
14149
14523
  return null;
14150
14524
  }
@@ -14192,9 +14566,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
14192
14566
  async deleteSession(id) {
14193
14567
  const jsonlPath = this.sessionPath(id, ".jsonl");
14194
14568
  const summaryPath = this.sessionPath(id, ".summary.json");
14195
- const shardDir = path11.dirname(jsonlPath);
14196
- const base = path11.basename(id);
14197
- const sessDir = path11.join(shardDir, base);
14569
+ const shardDir = path12.dirname(jsonlPath);
14570
+ const base = path12.basename(id);
14571
+ const sessDir = path12.join(shardDir, base);
14198
14572
  const deletions = [
14199
14573
  fsp9.unlink(jsonlPath),
14200
14574
  fsp9.unlink(summaryPath),
@@ -14239,7 +14613,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14239
14613
  */
14240
14614
  async readActiveSessionId() {
14241
14615
  try {
14242
- const raw = await fsp9.readFile(path11.join(this.dir, "active.json"), "utf8");
14616
+ const raw = await fsp9.readFile(path12.join(this.dir, "active.json"), "utf8");
14243
14617
  const active = JSON.parse(raw);
14244
14618
  return active.sessionId ?? null;
14245
14619
  } catch {
@@ -14302,10 +14676,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
14302
14676
  const activeSessionId = await this.readActiveSessionId();
14303
14677
  const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
14304
14678
  const pruneFile = async (dir, name, prefix) => {
14305
- const jsonlPath = path11.join(dir, name);
14679
+ const jsonlPath = path12.join(dir, name);
14306
14680
  try {
14307
- const stat12 = await fsp9.stat(jsonlPath);
14308
- if (stat12.mtimeMs >= cutoff) return;
14681
+ const stat13 = await fsp9.stat(jsonlPath);
14682
+ if (stat13.mtimeMs >= cutoff) return;
14309
14683
  } catch {
14310
14684
  return;
14311
14685
  }
@@ -14322,7 +14696,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14322
14696
  continue;
14323
14697
  }
14324
14698
  if (!entry.isDirectory()) continue;
14325
- const dateDir = path11.join(this.dir, entry.name);
14699
+ const dateDir = path12.join(this.dir, entry.name);
14326
14700
  const files = await fsp9.readdir(dateDir, { withFileTypes: true }).catch(() => []);
14327
14701
  for (const file of files) {
14328
14702
  if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
@@ -14334,7 +14708,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14334
14708
  }
14335
14709
  for (const entry of entries) {
14336
14710
  if (!entry.isDirectory()) continue;
14337
- const dateDir = path11.join(this.dir, entry.name);
14711
+ const dateDir = path12.join(this.dir, entry.name);
14338
14712
  try {
14339
14713
  const remaining = await fsp9.readdir(dateDir);
14340
14714
  if (remaining.length === 0) {
@@ -14465,9 +14839,9 @@ function makeDirectorSessionFactory(opts) {
14465
14839
  let dir;
14466
14840
  if (opts.store) {
14467
14841
  store = opts.store;
14468
- dir = opts.sessionsRoot ? path12.join(opts.sessionsRoot, runId) : "(caller-managed)";
14842
+ dir = opts.sessionsRoot ? path13.join(opts.sessionsRoot, runId) : "(caller-managed)";
14469
14843
  } else if (opts.sessionsRoot) {
14470
- dir = path12.join(opts.sessionsRoot, runId);
14844
+ dir = path13.join(opts.sessionsRoot, runId);
14471
14845
  store = new DefaultSessionStore({ dir });
14472
14846
  } else {
14473
14847
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -15813,8 +16187,8 @@ function isDesignStack(v) {
15813
16187
 
15814
16188
  // src/execution/design-kit-loader.ts
15815
16189
  import { existsSync } from "node:fs";
15816
- import * as fs3 from "node:fs/promises";
15817
- import * as path13 from "node:path";
16190
+ import * as fs4 from "node:fs/promises";
16191
+ import * as path14 from "node:path";
15818
16192
  import { fileURLToPath as fileURLToPath3 } from "node:url";
15819
16193
  var KIT_FILE = "KIT.md";
15820
16194
  var TOKENS_FILE = "tokens.json";
@@ -15921,15 +16295,15 @@ var DefaultDesignKitLoader = class {
15921
16295
  for (const { dir, source } of this.dirs) {
15922
16296
  let entries;
15923
16297
  try {
15924
- entries = await fs3.readdir(dir, { withFileTypes: true });
16298
+ entries = await fs4.readdir(dir, { withFileTypes: true });
15925
16299
  } catch {
15926
16300
  continue;
15927
16301
  }
15928
16302
  for (const e of entries) {
15929
16303
  if (!e.isDirectory()) continue;
15930
- const kitFile = path13.join(dir, e.name, KIT_FILE);
16304
+ const kitFile = path14.join(dir, e.name, KIT_FILE);
15931
16305
  try {
15932
- const raw = await fs3.readFile(kitFile, "utf8");
16306
+ const raw = await fs4.readFile(kitFile, "utf8");
15933
16307
  const fm = parseKitFrontmatter(raw);
15934
16308
  const id = fm.id ?? e.name;
15935
16309
  if (!fm.name) continue;
@@ -15990,7 +16364,7 @@ var DefaultDesignKitLoader = class {
15990
16364
  if (cached !== void 0) return cached;
15991
16365
  const m = await this.find(id);
15992
16366
  if (!m) throw new Error(`Design kit "${id}" not found`);
15993
- const raw = await fs3.readFile(m.path, "utf8");
16367
+ const raw = await fs4.readFile(m.path, "utf8");
15994
16368
  const body = narrowStackSections(stripFrontmatter(raw), stack);
15995
16369
  this.bodyCache.set(key, body);
15996
16370
  return body;
@@ -16001,9 +16375,9 @@ var DefaultDesignKitLoader = class {
16001
16375
  const m = await this.find(id);
16002
16376
  let tokens;
16003
16377
  if (m) {
16004
- const tokensPath = path13.join(path13.dirname(m.path), TOKENS_FILE);
16378
+ const tokensPath = path14.join(path14.dirname(m.path), TOKENS_FILE);
16005
16379
  try {
16006
- const raw = await fs3.readFile(tokensPath, "utf8");
16380
+ const raw = await fs4.readFile(tokensPath, "utf8");
16007
16381
  const parsed = JSON.parse(raw);
16008
16382
  tokens = parsed;
16009
16383
  } catch {
@@ -16028,12 +16402,12 @@ var DefaultDesignKitLoader = class {
16028
16402
  };
16029
16403
  function resolveBundledDesignKitsDir() {
16030
16404
  try {
16031
- const here = path13.dirname(fileURLToPath3(import.meta.url));
16405
+ const here = path14.dirname(fileURLToPath3(import.meta.url));
16032
16406
  const candidates = [
16033
- path13.join(here, "design-kits"),
16034
- path13.join(here, "..", "design-kits"),
16035
- path13.join(here, "..", "..", "design-kits"),
16036
- path13.join(here, "..", "..", "..", "design-kits")
16407
+ path14.join(here, "design-kits"),
16408
+ path14.join(here, "..", "design-kits"),
16409
+ path14.join(here, "..", "..", "design-kits"),
16410
+ path14.join(here, "..", "..", "..", "design-kits")
16037
16411
  ];
16038
16412
  for (const c of candidates) {
16039
16413
  if (existsSync(c)) return c;
@@ -16061,11 +16435,11 @@ function _resetDesignKitLoaderMemo() {
16061
16435
 
16062
16436
  // src/execution/design-project-store.ts
16063
16437
  import { existsSync as existsSync2 } from "node:fs";
16064
- import * as fs4 from "node:fs/promises";
16065
- import * as path14 from "node:path";
16438
+ import * as fs5 from "node:fs/promises";
16439
+ import * as path15 from "node:path";
16066
16440
  var DESIGN_DIR = ".design";
16067
16441
  function designProjectDir(projectRoot) {
16068
- return path14.join(projectRoot, DESIGN_DIR);
16442
+ return path15.join(projectRoot, DESIGN_DIR);
16069
16443
  }
16070
16444
  var RULE_FILES = ["rules.md", "RULES.md", "design.md"];
16071
16445
  var rulesCache = /* @__PURE__ */ new Map();
@@ -16074,7 +16448,7 @@ async function loadProjectDesignRules(projectRoot) {
16074
16448
  let rules;
16075
16449
  for (const name of RULE_FILES) {
16076
16450
  try {
16077
- const txt = await fs4.readFile(path14.join(designProjectDir(projectRoot), name), "utf8");
16451
+ const txt = await fs5.readFile(path15.join(designProjectDir(projectRoot), name), "utf8");
16078
16452
  if (txt.trim()) {
16079
16453
  rules = txt.trim();
16080
16454
  break;
@@ -16095,7 +16469,7 @@ function parseOverrides(value) {
16095
16469
  }
16096
16470
  async function loadActiveKit(projectRoot) {
16097
16471
  try {
16098
- const raw = await fs4.readFile(path14.join(designProjectDir(projectRoot), "active.json"), "utf8");
16472
+ const raw = await fs5.readFile(path15.join(designProjectDir(projectRoot), "active.json"), "utf8");
16099
16473
  const parsed = JSON.parse(raw);
16100
16474
  if (parsed && typeof parsed.kit === "string") {
16101
16475
  return {
@@ -16128,11 +16502,11 @@ function applyTokenOverrides(tokens, overrides) {
16128
16502
  }
16129
16503
  async function ensureDesignDir(projectRoot) {
16130
16504
  const dir = designProjectDir(projectRoot);
16131
- await fs4.mkdir(dir, { recursive: true });
16132
- const gi = path14.join(dir, ".gitignore");
16505
+ await fs5.mkdir(dir, { recursive: true });
16506
+ const gi = path15.join(dir, ".gitignore");
16133
16507
  if (!existsSync2(gi)) {
16134
16508
  try {
16135
- await fs4.writeFile(gi, "*\n");
16509
+ await fs5.writeFile(gi, "*\n");
16136
16510
  } catch {
16137
16511
  }
16138
16512
  }
@@ -16143,11 +16517,11 @@ async function recordKitChoice(projectRoot, kit, stack, source, isoTime, overrid
16143
16517
  const dir = await ensureDesignDir(projectRoot);
16144
16518
  const record = { kit, stack: stack ?? null };
16145
16519
  if (overrides && Object.keys(overrides).length > 0) record.overrides = overrides;
16146
- await fs4.writeFile(path14.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
16520
+ await fs5.writeFile(path15.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
16147
16521
  `);
16148
16522
  const line = `- ${isoTime} \xB7 kit=${kit}${stack ? ` stack=${stack}` : ""} \xB7 via=${source}
16149
16523
  `;
16150
- await fs4.appendFile(path14.join(dir, "decisions.md"), line);
16524
+ await fs5.appendFile(path15.join(dir, "decisions.md"), line);
16151
16525
  } catch {
16152
16526
  }
16153
16527
  }
@@ -16163,11 +16537,11 @@ async function recordOverrides(projectRoot, patch, isoTime) {
16163
16537
  const dir = await ensureDesignDir(projectRoot);
16164
16538
  const record = { kit: active.kit, stack: active.stack ?? null };
16165
16539
  if (Object.keys(merged).length > 0) record.overrides = merged;
16166
- await fs4.writeFile(path14.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
16540
+ await fs5.writeFile(path15.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
16167
16541
  `);
16168
16542
  const keys = Object.keys(patch).join(",");
16169
- await fs4.appendFile(
16170
- path14.join(dir, "decisions.md"),
16543
+ await fs5.appendFile(
16544
+ path15.join(dir, "decisions.md"),
16171
16545
  `- ${isoTime} \xB7 kit=${active.kit} \xB7 override=${keys} \xB7 via=set
16172
16546
  `
16173
16547
  );
@@ -16177,7 +16551,7 @@ async function recordOverrides(projectRoot, patch, isoTime) {
16177
16551
  }
16178
16552
  async function clearPersistedActiveKit(projectRoot) {
16179
16553
  try {
16180
- await fs4.rm(path14.join(designProjectDir(projectRoot), "active.json"), { force: true });
16554
+ await fs5.rm(path15.join(designProjectDir(projectRoot), "active.json"), { force: true });
16181
16555
  } catch {
16182
16556
  }
16183
16557
  }
@@ -16293,12 +16667,12 @@ function verifyFiles(tokens, files) {
16293
16667
  const violations = [];
16294
16668
  let onPalette = 0;
16295
16669
  let offPalette = 0;
16296
- for (const { path: path28, text } of files) {
16670
+ for (const { path: path30, text } of files) {
16297
16671
  const lines = text.split("\n");
16298
16672
  lines.forEach((lineText, i) => {
16299
16673
  const lineNo = i + 1;
16300
16674
  const flag = (snippet, reason) => {
16301
- violations.push({ file: path28, line: lineNo, snippet: snippet.slice(0, 80), reason });
16675
+ violations.push({ file: path30, line: lineNo, snippet: snippet.slice(0, 80), reason });
16302
16676
  };
16303
16677
  for (const re of [HEX_RE, FUNC_COLOR_RE]) {
16304
16678
  re.lastIndex = 0;
@@ -16350,14 +16724,14 @@ var SKIP_DIR = /* @__PURE__ */ new Set([
16350
16724
  ".design"
16351
16725
  ]);
16352
16726
  async function walkUiFiles(root, max = 200) {
16353
- const { default: fs15 } = await import("node:fs/promises");
16727
+ const { default: fs16 } = await import("node:fs/promises");
16354
16728
  const { default: nodePath } = await import("node:path");
16355
16729
  const found = [];
16356
16730
  async function rec(dir, depth) {
16357
16731
  if (found.length >= max || depth > 8) return;
16358
16732
  let entries;
16359
16733
  try {
16360
- entries = await fs15.readdir(dir, { withFileTypes: true });
16734
+ entries = await fs16.readdir(dir, { withFileTypes: true });
16361
16735
  } catch {
16362
16736
  return;
16363
16737
  }
@@ -16375,13 +16749,13 @@ async function walkUiFiles(root, max = 200) {
16375
16749
  return found;
16376
16750
  }
16377
16751
  async function runDesignVerify(projectRoot, tokens, explicitFiles) {
16378
- const { default: fs15 } = await import("node:fs/promises");
16752
+ const { default: fs16 } = await import("node:fs/promises");
16379
16753
  const { default: nodePath } = await import("node:path");
16380
16754
  const abs = explicitFiles && explicitFiles.length > 0 ? explicitFiles.map((f) => nodePath.isAbsolute(f) ? f : nodePath.join(projectRoot, f)) : await walkUiFiles(projectRoot);
16381
16755
  const files = [];
16382
16756
  for (const a of abs) {
16383
16757
  try {
16384
- files.push({ path: nodePath.relative(projectRoot, a), text: await fs15.readFile(a, "utf8") });
16758
+ files.push({ path: nodePath.relative(projectRoot, a), text: await fs16.readFile(a, "utf8") });
16385
16759
  } catch {
16386
16760
  }
16387
16761
  }
@@ -16512,10 +16886,10 @@ function makeDesignVerifyToolCallMiddleware() {
16512
16886
  const p = typeof input?.path === "string" ? input.path : "";
16513
16887
  if (!p || !detectFrontendFile(p)) return out;
16514
16888
  const ctx = out.ctx;
16515
- const { default: fs15 } = await import("node:fs/promises");
16889
+ const { default: fs16 } = await import("node:fs/promises");
16516
16890
  const { default: nodePath } = await import("node:path");
16517
16891
  const abs = nodePath.isAbsolute(p) ? p : nodePath.join(ctx.projectRoot, p);
16518
- const text = await fs15.readFile(abs, "utf8").catch(() => "");
16892
+ const text = await fs16.readFile(abs, "utf8").catch(() => "");
16519
16893
  if (!text) return out;
16520
16894
  const loader = getDesignKitLoader(ctx.projectRoot);
16521
16895
  const rawTokens = await loader.readTokens(state.activeKit);
@@ -16807,16 +17181,20 @@ function buildRecoveryStrategies(opts) {
16807
17181
  const delayMs = err.body?.retryAfterMs ?? 5e3;
16808
17182
  const delay = Math.min(6e4, Math.max(1e3, delayMs));
16809
17183
  if (ctx?.signal) {
16810
- await new Promise((resolve12) => {
17184
+ await new Promise((resolve14) => {
16811
17185
  if (ctx.signal.aborted) {
16812
- resolve12();
17186
+ resolve14();
16813
17187
  return;
16814
17188
  }
16815
- const timer = setTimeout(resolve12, delay);
16816
- ctx.signal.addEventListener("abort", () => {
16817
- clearTimeout(timer);
16818
- resolve12();
16819
- }, { once: true });
17189
+ const timer = setTimeout(resolve14, delay);
17190
+ ctx.signal.addEventListener(
17191
+ "abort",
17192
+ () => {
17193
+ clearTimeout(timer);
17194
+ resolve14();
17195
+ },
17196
+ { once: true }
17197
+ );
16820
17198
  });
16821
17199
  } else {
16822
17200
  await new Promise((r) => setTimeout(r, delay));
@@ -16907,6 +17285,7 @@ function buildRecoveryStrategies(opts) {
16907
17285
  var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
16908
17286
  var HANDLER_KIND_BY_PROVIDER_KIND = {
16909
17287
  rate_limit: "rate_limit",
17288
+ quota_exhausted: "rate_limit",
16910
17289
  overloaded: "overloaded",
16911
17290
  server: "server",
16912
17291
  stream_hang: "server",
@@ -18801,8 +19180,8 @@ async function streamProviderToResponse(provider, req, signal, ctx, events, logg
18801
19180
  });
18802
19181
  await Promise.race([
18803
19182
  drainPromise,
18804
- new Promise((resolve12) => {
18805
- drainTimer = setTimeout(resolve12, STREAM_DRAIN_TIMEOUT_MS);
19183
+ new Promise((resolve14) => {
19184
+ drainTimer = setTimeout(resolve14, STREAM_DRAIN_TIMEOUT_MS);
18806
19185
  })
18807
19186
  ]);
18808
19187
  } finally {
@@ -19097,7 +19476,7 @@ async function runProviderWithRetry(opts) {
19097
19476
  status: isProviderErr ? err.status : 0,
19098
19477
  description
19099
19478
  });
19100
- await new Promise((resolve12, reject) => {
19479
+ await new Promise((resolve14, reject) => {
19101
19480
  let settled = false;
19102
19481
  const cleanup = () => {
19103
19482
  clearTimeout(t);
@@ -19113,7 +19492,7 @@ async function runProviderWithRetry(opts) {
19113
19492
  if (settled) return;
19114
19493
  settled = true;
19115
19494
  cleanup();
19116
- resolve12();
19495
+ resolve14();
19117
19496
  }, delay);
19118
19497
  if (signal.aborted) {
19119
19498
  onAbort();
@@ -19141,6 +19520,7 @@ import { randomInt } from "node:crypto";
19141
19520
  var MAX_RETRY_AFTER_MS = 6e4;
19142
19521
  var MAX_ATTEMPTS_BY_KIND = {
19143
19522
  rate_limit: 5,
19523
+ quota_exhausted: 0,
19144
19524
  stream_hang: 5,
19145
19525
  // transient, worth retrying aggressively
19146
19526
  overloaded: 3,
@@ -19605,8 +19985,8 @@ Summarize the following message range:`;
19605
19985
  };
19606
19986
 
19607
19987
  // src/execution/skill-loader.ts
19608
- import * as fs5 from "node:fs/promises";
19609
- import * as path15 from "node:path";
19988
+ import * as fs6 from "node:fs/promises";
19989
+ import * as path16 from "node:path";
19610
19990
 
19611
19991
  // src/skills/foreign-sources.ts
19612
19992
  var FOREIGN_SKILL_TOOLS = [
@@ -19836,7 +20216,7 @@ async function entryIsDirectory(dir, entry) {
19836
20216
  if (entry.isDirectory()) return true;
19837
20217
  if (entry.isSymbolicLink()) {
19838
20218
  try {
19839
- return (await fs5.stat(path15.join(dir, entry.name))).isDirectory();
20219
+ return (await fs6.stat(path16.join(dir, entry.name))).isDirectory();
19840
20220
  } catch {
19841
20221
  return false;
19842
20222
  }
@@ -19857,7 +20237,7 @@ var DefaultSkillLoader = class {
19857
20237
  for (const tool of FOREIGN_SKILL_TOOLS) {
19858
20238
  if (!foreignIds.includes(tool.id)) continue;
19859
20239
  dirs.push({
19860
- dir: path15.join(root, "." + tool.id, tool.subdir),
20240
+ dir: path16.join(root, "." + tool.id, tool.subdir),
19861
20241
  source: "foreign",
19862
20242
  originTool: tool.id
19863
20243
  });
@@ -19879,12 +20259,14 @@ var DefaultSkillLoader = class {
19879
20259
  const seen = /* @__PURE__ */ new Set();
19880
20260
  for (const { dir, source, originTool } of this.dirs) {
19881
20261
  try {
19882
- const entries = await fs5.readdir(dir, { withFileTypes: true });
20262
+ const entries = (await fs6.readdir(dir, { withFileTypes: true })).sort(
20263
+ (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
20264
+ );
19883
20265
  for (const e of entries) {
19884
20266
  if (!await entryIsDirectory(dir, e)) continue;
19885
- const skillFile = path15.join(dir, e.name, "SKILL.md");
20267
+ const skillFile = path16.join(dir, e.name, "SKILL.md");
19886
20268
  try {
19887
- const raw = await fs5.readFile(skillFile, "utf8");
20269
+ const raw = await fs6.readFile(skillFile, "utf8");
19888
20270
  const fm = parseSkillFrontmatter(raw);
19889
20271
  if (!fm.name || !fm.description) continue;
19890
20272
  if (!isValidSkillNameFormat(fm.name)) continue;
@@ -19956,7 +20338,7 @@ var DefaultSkillLoader = class {
19956
20338
  if (cached !== void 0) return cached;
19957
20339
  const m = await this.find(name);
19958
20340
  if (!m) throw new Error(`Skill "${name}" not found`);
19959
- const body = await fs5.readFile(m.path, "utf8");
20341
+ const body = await fs6.readFile(m.path, "utf8");
19960
20342
  this.bodyCache.set(key, body);
19961
20343
  return body;
19962
20344
  }
@@ -19966,12 +20348,12 @@ var DefaultSkillLoader = class {
19966
20348
  if (cached !== void 0) return cached;
19967
20349
  const m = await this.find(name);
19968
20350
  if (!m) throw new Error(`Skill "${name}" not found`);
19969
- const savePath = path15.join(path15.dirname(m.path), "SKILL.save.md");
20351
+ const savePath = path16.join(path16.dirname(m.path), "SKILL.save.md");
19970
20352
  let result;
19971
20353
  try {
19972
- result = await fs5.readFile(savePath, "utf8");
20354
+ result = await fs6.readFile(savePath, "utf8");
19973
20355
  } catch {
19974
- const full = await fs5.readFile(m.path, "utf8");
20356
+ const full = await fs6.readFile(m.path, "utf8");
19975
20357
  const body = stripFrontmatter2(full);
19976
20358
  const compact = compactSkillBody(body);
19977
20359
  if (compact) {
@@ -19999,12 +20381,12 @@ function parseDescriptionFromText(desc) {
19999
20381
  }
20000
20382
 
20001
20383
  // src/execution/prompt-loader.ts
20002
- import * as fs7 from "node:fs/promises";
20003
- import * as path17 from "node:path";
20384
+ import * as fs8 from "node:fs/promises";
20385
+ import * as path18 from "node:path";
20004
20386
 
20005
20387
  // src/storage/prompt-store.ts
20006
- import * as fs6 from "node:fs/promises";
20007
- import * as path16 from "node:path";
20388
+ import * as fs7 from "node:fs/promises";
20389
+ import * as path17 from "node:path";
20008
20390
  var SCHEMA_VERSION = 2;
20009
20391
  function migratePromptEntry(raw) {
20010
20392
  if (!raw || typeof raw !== "object") return null;
@@ -20065,12 +20447,12 @@ var DefaultPromptStore = class {
20065
20447
  await ensureDir(this.dir);
20066
20448
  const entries = [];
20067
20449
  try {
20068
- const files = await fs6.readdir(this.dir);
20450
+ const files = await fs7.readdir(this.dir);
20069
20451
  for (const file of files) {
20070
20452
  if (!file.endsWith(".json")) continue;
20071
20453
  try {
20072
20454
  const raw = JSON.parse(
20073
- await fs6.readFile(path16.join(this.dir, file), "utf8")
20455
+ await fs7.readFile(path17.join(this.dir, file), "utf8")
20074
20456
  );
20075
20457
  const migrated = migratePromptEntry(raw.entry);
20076
20458
  if (migrated) entries.push(migrated);
@@ -20084,9 +20466,9 @@ var DefaultPromptStore = class {
20084
20466
  );
20085
20467
  }
20086
20468
  async get(id) {
20087
- const file = path16.join(this.dir, `${id}.json`);
20469
+ const file = path17.join(this.dir, `${id}.json`);
20088
20470
  try {
20089
- const raw = JSON.parse(await fs6.readFile(file, "utf8"));
20471
+ const raw = JSON.parse(await fs7.readFile(file, "utf8"));
20090
20472
  return migratePromptEntry(raw.entry);
20091
20473
  } catch {
20092
20474
  return null;
@@ -20094,14 +20476,14 @@ var DefaultPromptStore = class {
20094
20476
  }
20095
20477
  async save(entry) {
20096
20478
  await ensureDir(this.dir);
20097
- const file = path16.join(this.dir, `${entry.id}.json`);
20479
+ const file = path17.join(this.dir, `${entry.id}.json`);
20098
20480
  const raw = { version: SCHEMA_VERSION, entry };
20099
20481
  await atomicWrite(file, JSON.stringify(raw, null, 2));
20100
20482
  }
20101
20483
  async delete(id) {
20102
- const file = path16.join(this.dir, `${id}.json`);
20484
+ const file = path17.join(this.dir, `${id}.json`);
20103
20485
  try {
20104
- await fs6.unlink(file);
20486
+ await fs7.unlink(file);
20105
20487
  return true;
20106
20488
  } catch {
20107
20489
  return false;
@@ -20188,7 +20570,7 @@ var DefaultPromptLoader = class {
20188
20570
  constructor(opts) {
20189
20571
  this.projectStore = typeof opts.paths.inProjectPrompts === "string" ? new DefaultPromptStore(opts.paths.inProjectPrompts) : void 0;
20190
20572
  this.userStore = typeof opts.paths.globalPrompts === "string" ? new DefaultPromptStore(opts.paths.globalPrompts) : void 0;
20191
- this.builtinDir = opts.bundledDir ? path17.join(opts.bundledDir, "prompts") : void 0;
20573
+ this.builtinDir = opts.bundledDir ? path18.join(opts.bundledDir, "prompts") : void 0;
20192
20574
  }
20193
20575
  async list() {
20194
20576
  if (this.cache) return this.cache;
@@ -20301,7 +20683,7 @@ var DefaultPromptLoader = class {
20301
20683
  const files = await walkJson(dir);
20302
20684
  for (const file of files) {
20303
20685
  try {
20304
- const parsed = JSON.parse(await fs7.readFile(file, "utf8"));
20686
+ const parsed = JSON.parse(await fs8.readFile(file, "utf8"));
20305
20687
  const migrated = migratePromptEntry(parsed);
20306
20688
  if (migrated) out.push({ ...migrated, source: "builtin" });
20307
20689
  } catch {
@@ -20315,12 +20697,12 @@ async function walkJson(dir) {
20315
20697
  const out = [];
20316
20698
  let entries;
20317
20699
  try {
20318
- entries = await fs7.readdir(dir, { withFileTypes: true });
20700
+ entries = await fs8.readdir(dir, { withFileTypes: true });
20319
20701
  } catch {
20320
20702
  return out;
20321
20703
  }
20322
20704
  for (const e of entries) {
20323
- const full = path17.join(dir, e.name);
20705
+ const full = path18.join(dir, e.name);
20324
20706
  if (e.isDirectory()) {
20325
20707
  out.push(...await walkJson(full));
20326
20708
  } else if (e.name.endsWith(".json") && e.name !== "index.json" && e.name !== "schema.json") {
@@ -20506,7 +20888,7 @@ function readPolicy(ctx) {
20506
20888
  }
20507
20889
 
20508
20890
  // src/execution/tool-executor.ts
20509
- import { randomUUID as randomUUID11 } from "node:crypto";
20891
+ import { createHash as createHash6, randomUUID as randomUUID11 } from "node:crypto";
20510
20892
 
20511
20893
  // src/observability/process-telemetry.ts
20512
20894
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
@@ -20517,8 +20899,167 @@ function runWithProcessTelemetry(context, run) {
20517
20899
 
20518
20900
  // src/execution/tool-executor.ts
20519
20901
  import { isDeepStrictEqual } from "node:util";
20520
- import * as fs8 from "node:fs/promises";
20521
- import * as path18 from "node:path";
20902
+ import * as fs9 from "node:fs/promises";
20903
+ import * as path20 from "node:path";
20904
+
20905
+ // src/security/kanban-boundary.ts
20906
+ import { realpath as realpath3 } from "node:fs/promises";
20907
+ import * as path19 from "node:path";
20908
+ import {
20909
+ evaluateKanbanBoundaryOpaque,
20910
+ evaluateKanbanBoundaryPath,
20911
+ readBoard,
20912
+ resolveKanbanBoundaryLayers
20913
+ } from "@wrongstack/kanban";
20914
+ var PATH_KEYS = /* @__PURE__ */ new Set([
20915
+ "path",
20916
+ "paths",
20917
+ "file",
20918
+ "files",
20919
+ "directory",
20920
+ "dir",
20921
+ "cwd",
20922
+ "root",
20923
+ "baseDir",
20924
+ "out",
20925
+ "outputPath",
20926
+ "sourcePath",
20927
+ "targetPath",
20928
+ "destinationPath",
20929
+ "fromPath",
20930
+ "toPath",
20931
+ "worktreePath"
20932
+ ]);
20933
+ var TOOL_PATH_KEYS = {
20934
+ // `target` is an identifier in tools such as plan/task/document, but a
20935
+ // filesystem target in language_info. Keep ambiguous names tool-specific.
20936
+ language_info: ["target"]
20937
+ };
20938
+ async function evaluateToolKanbanBoundary(tool, input, ctx) {
20939
+ const identity = resolveKanbanIdentity(ctx);
20940
+ if (!identity.boardId) return { decision: "allow" };
20941
+ const board = await readBoard(identity.policyRoot ?? ctx.projectRoot, identity.boardId);
20942
+ if (!board) return { decision: "allow" };
20943
+ const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
20944
+ const layers = resolveKanbanBoundaryLayers(board, task);
20945
+ if (layers.length === 0) return { decision: "allow" };
20946
+ const caps = tool.capabilities ?? [];
20947
+ const access2 = caps.includes("fs.write") || caps.includes("fs.write.outside-project") ? "write" : caps.includes("fs.read") ? "read" : void 0;
20948
+ const shellLike = caps.some((capability) => capability.startsWith("shell."));
20949
+ const candidates = access2 ? await extractCandidatePaths(tool.name, input, ctx) : [];
20950
+ let evaluation;
20951
+ if (shellLike || access2 && candidates.length === 0) {
20952
+ evaluation = evaluateKanbanBoundaryOpaque(layers, tool.name);
20953
+ } else if (access2) {
20954
+ const results = candidates.map(
20955
+ (candidate) => evaluateKanbanBoundaryPath(layers, candidate, access2)
20956
+ );
20957
+ evaluation = results.find((result) => result.decision === "block") ?? results.find((result) => result.decision === "confirm") ?? { decision: "allow" };
20958
+ } else {
20959
+ evaluation = { decision: "allow" };
20960
+ }
20961
+ return {
20962
+ ...evaluation,
20963
+ boardId: board.id,
20964
+ ...task ? { taskId: task.id } : {}
20965
+ };
20966
+ }
20967
+ function resolveKanbanIdentity(ctx) {
20968
+ const metaKanban = ctx.meta["kanban"];
20969
+ const record = metaKanban && typeof metaKanban === "object" ? metaKanban : void 0;
20970
+ const boardId = ctx.currentKanbanBoardId ?? (typeof record?.["boardId"] === "string" ? record["boardId"] : void 0);
20971
+ const taskId = ctx.currentKanbanTaskId ?? (typeof record?.["taskId"] === "string" ? record["taskId"] : void 0);
20972
+ const policyRoot = typeof record?.["projectRoot"] === "string" ? record["projectRoot"] : void 0;
20973
+ return {
20974
+ ...boardId ? { boardId } : {},
20975
+ ...taskId ? { taskId } : {},
20976
+ ...policyRoot ? { policyRoot } : {}
20977
+ };
20978
+ }
20979
+ async function extractCandidatePaths(toolName, input, ctx) {
20980
+ if (toolName === "patch" && typeof input["patch"] === "string") {
20981
+ const directoryInput = stringValue(input["directory"]) ?? ctx.workingDir;
20982
+ const directory = path19.isAbsolute(directoryInput) ? directoryInput : path19.resolve(ctx.workingDir, directoryInput);
20983
+ const strip = Math.max(1, numericValue(input["strip"]) ?? 1);
20984
+ const targets = extractPatchTargets(input["patch"], strip).map(
20985
+ (target) => relativeToProject(path19.resolve(directory, target), ctx.projectRoot)
20986
+ );
20987
+ return Promise.all(targets.map((target) => canonicalizeCandidatePath(target, ctx)));
20988
+ }
20989
+ const values = [];
20990
+ const pathKeys = new Set(PATH_KEYS);
20991
+ for (const key of TOOL_PATH_KEYS[toolName] ?? []) pathKeys.add(key);
20992
+ collectPathValues(input, values, pathKeys);
20993
+ if (toolName === "scaffold" && typeof input["name"] === "string") {
20994
+ const cwd = stringValue(input["cwd"]) ?? ctx.workingDir;
20995
+ values.push(path19.join(cwd, input["name"]));
20996
+ }
20997
+ const candidates = [
20998
+ ...new Set(values.flatMap(splitPathList).map((value) => resolveInputPath(value, ctx)))
20999
+ ];
21000
+ return Promise.all(candidates.map((candidate) => canonicalizeCandidatePath(candidate, ctx)));
21001
+ }
21002
+ async function canonicalizeCandidatePath(candidate, ctx) {
21003
+ if (path19.isAbsolute(candidate)) return candidate;
21004
+ const absolute = path19.resolve(ctx.projectRoot, candidate);
21005
+ const canonicalRoot = await realpath3(ctx.projectRoot).catch(() => ctx.projectRoot);
21006
+ let probe = absolute;
21007
+ const missingSegments = [];
21008
+ while (true) {
21009
+ try {
21010
+ const canonical = path19.join(await realpath3(probe), ...missingSegments);
21011
+ return relativeToProject(canonical, canonicalRoot);
21012
+ } catch (cause) {
21013
+ const code = cause.code;
21014
+ if (code !== "ENOENT" && code !== "ENOTDIR") return absolute;
21015
+ const parent = path19.dirname(probe);
21016
+ if (parent === probe) return absolute;
21017
+ missingSegments.unshift(path19.basename(probe));
21018
+ probe = parent;
21019
+ }
21020
+ }
21021
+ }
21022
+ function collectPathValues(value, output, pathKeys, key) {
21023
+ if (typeof value === "string") {
21024
+ if (key && pathKeys.has(key)) output.push(value);
21025
+ return;
21026
+ }
21027
+ if (Array.isArray(value)) {
21028
+ for (const item of value) collectPathValues(item, output, pathKeys, key);
21029
+ return;
21030
+ }
21031
+ if (!value || typeof value !== "object") return;
21032
+ for (const [childKey, child] of Object.entries(value)) {
21033
+ collectPathValues(child, output, pathKeys, childKey);
21034
+ }
21035
+ }
21036
+ function splitPathList(value) {
21037
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
21038
+ }
21039
+ function resolveInputPath(value, ctx) {
21040
+ const absolute = path19.isAbsolute(value) ? value : path19.resolve(ctx.workingDir, value);
21041
+ return relativeToProject(absolute, ctx.projectRoot);
21042
+ }
21043
+ function relativeToProject(absolute, projectRoot) {
21044
+ const relative6 = path19.relative(projectRoot, absolute).replace(/\\/g, "/");
21045
+ return relative6.startsWith("../") || path19.isAbsolute(relative6) ? absolute : relative6 || ".";
21046
+ }
21047
+ function extractPatchTargets(patchText, strip) {
21048
+ const targets = [];
21049
+ for (const match of patchText.matchAll(/^\+\+\+\s+([^\t\r\n]+)/gm)) {
21050
+ const raw = match[1]?.trim();
21051
+ if (!raw || raw === "/dev/null") continue;
21052
+ const parts = raw.replace(/\\/g, "/").split("/").filter((part) => part && part !== ".");
21053
+ if (parts.length > strip) targets.push(parts.slice(strip).join("/"));
21054
+ }
21055
+ return targets;
21056
+ }
21057
+ function stringValue(value) {
21058
+ return typeof value === "string" && value.trim() ? value : void 0;
21059
+ }
21060
+ function numericValue(value) {
21061
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
21062
+ }
20522
21063
 
20523
21064
  // src/types/tool-executor.ts
20524
21065
  var GOVERNED_TOOL_EXECUTOR_META_KEY = "toolExecutor.executeGoverned";
@@ -20739,35 +21280,68 @@ ${errorDetails}`,
20739
21280
  return { result, tool, durationMs: Date.now() - start };
20740
21281
  }
20741
21282
  }
21283
+ const boundary = await evaluateToolKanbanBoundary(tool, use.input, ctx);
21284
+ if (boundary.decision === "block") {
21285
+ const result = {
21286
+ type: "tool_result",
21287
+ tool_use_id: use.id,
21288
+ content: `Tool "${tool.name}" blocked by Kanban boundary. ${boundary.reason ?? ""}`.trim(),
21289
+ is_error: true
21290
+ };
21291
+ budget = this.budgetForString(result.content, budget);
21292
+ return { result, tool, durationMs: Date.now() - start };
21293
+ }
20742
21294
  const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
20743
21295
  let effectivePermission = decision.permission;
20744
21296
  const policy = this.opts.permissionPolicy;
20745
21297
  const yolo = policy.getYolo?.() === true;
20746
21298
  const authoritativeAuto = decision.source === "yolo";
20747
- if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto) {
21299
+ const capabilityDowngraded = toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto;
21300
+ if (capabilityDowngraded) {
21301
+ effectivePermission = "confirm";
21302
+ }
21303
+ if (boundary.decision === "confirm" && effectivePermission !== "deny") {
20748
21304
  effectivePermission = "confirm";
20749
21305
  }
21306
+ this.opts.events?.emit("permission.evaluated", {
21307
+ sessionId: ctx.session.id,
21308
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
21309
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
21310
+ name: tool.name,
21311
+ id: use.id,
21312
+ inputHash: hashPermissionInput(use.input, this.opts.secretScrubber),
21313
+ policyDecision: decision.permission,
21314
+ effectiveDecision: effectivePermission,
21315
+ decisionSource: decision.source,
21316
+ ...decision.reason ? { reason: decision.reason } : {},
21317
+ ...decision.riskTier ?? tool.riskTier ? { riskTier: decision.riskTier ?? tool.riskTier } : {},
21318
+ yoloEnabled: yolo,
21319
+ boundaryDecision: boundary.decision,
21320
+ ...boundary.reason ? { boundaryReason: boundary.reason } : {},
21321
+ capabilityDowngraded
21322
+ });
20750
21323
  if (effectivePermission === "deny") {
20751
21324
  const result = this.deniedResult(use, decision.reason);
20752
21325
  budget = this.budgetForString(result.content, budget);
20753
21326
  return { result, tool, durationMs: Date.now() - start };
20754
21327
  }
20755
21328
  if (effectivePermission === "confirm") {
21329
+ const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
20756
21330
  if (this.opts.confirmAwaiter) {
20757
21331
  const awaiter = this.opts.confirmAwaiter;
20758
21332
  const choice = await new Promise(
20759
- (resolve12, reject) => {
21333
+ (resolve14, reject) => {
20760
21334
  const signal = ctx.signal;
20761
- const onAbort = () => resolve12("abort");
21335
+ const onAbort = () => resolve14("abort");
20762
21336
  if (signal.aborted) {
20763
- resolve12("abort");
21337
+ resolve14("abort");
20764
21338
  return;
20765
21339
  }
20766
21340
  signal.addEventListener("abort", onAbort, { once: true });
20767
- awaiter(tool, use.input, use.id, tool.name).then(
21341
+ awaiter(tool, use.input, use.id, suggestedPattern).then(
20768
21342
  (c) => {
20769
21343
  signal.removeEventListener("abort", onAbort);
20770
- resolve12(c);
21344
+ resolve14(c);
20771
21345
  },
20772
21346
  (e) => {
20773
21347
  signal.removeEventListener("abort", onAbort);
@@ -20787,7 +21361,6 @@ ${errorDetails}`,
20787
21361
  return { result, tool, durationMs: Date.now() - start };
20788
21362
  }
20789
21363
  } else {
20790
- const suggestedPattern = subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
20791
21364
  const pending = {
20792
21365
  type: "tool_confirm_pending",
20793
21366
  toolUseId: use.id,
@@ -20795,7 +21368,8 @@ ${errorDetails}`,
20795
21368
  input: use.input,
20796
21369
  suggestedPattern,
20797
21370
  decisionSource: decision.source,
20798
- riskTier: decision.riskTier ?? tool.riskTier
21371
+ riskTier: decision.riskTier ?? tool.riskTier,
21372
+ ...boundary.decision === "confirm" && boundary.reason ? { boundaryReason: boundary.reason } : {}
20799
21373
  };
20800
21374
  return { result: pending, tool, durationMs: Date.now() - start };
20801
21375
  }
@@ -20809,6 +21383,17 @@ ${errorDetails}`,
20809
21383
  "tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
20810
21384
  });
20811
21385
  try {
21386
+ const inputPath = use.input && typeof use.input === "object" ? use.input.path : void 0;
21387
+ const caps = tool.capabilities ?? [];
21388
+ const hasFileCapability = caps.includes("fs.read") || caps.includes("fs.write");
21389
+ const absPath = hasFileCapability && typeof inputPath === "string" ? path20.isAbsolute(inputPath) ? inputPath : path20.resolve(ctx.projectRoot, inputPath) : void 0;
21390
+ let writeTargetExisted;
21391
+ if (tool.name === "write" && caps.includes("fs.write") && absPath) {
21392
+ writeTargetExisted = await fs9.stat(absPath).then(
21393
+ (stat13) => stat13.isFile(),
21394
+ (error2) => error2.code === "ENOENT" ? false : void 0
21395
+ );
21396
+ }
20812
21397
  const producedText = await this.produceToolOutput(tool, use, ctx, budget);
20813
21398
  let { block: result, bytes } = this.settleToolOutput(tool, use, producedText, budget);
20814
21399
  budget -= bytes;
@@ -20837,6 +21422,35 @@ ${post.additionalContext}`;
20837
21422
  span?.setAttribute("tool.is_error", !!result.is_error);
20838
21423
  span?.setAttribute("tool.output_bytes", outputChars);
20839
21424
  this.logToolSuccess(ctx, use, tool.name, Date.now() - start, outputChars);
21425
+ if (!result.is_error && typeof inputPath === "string" && absPath) {
21426
+ const operation = tool.name === "read" ? "read" : caps.includes("fs.write") && tool.name === "write" ? writeTargetExisted === false ? "create" : "update" : caps.includes("fs.write") ? "update" : "read";
21427
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
21428
+ ctx.recordFileEvent?.({
21429
+ operation,
21430
+ filePath: inputPath,
21431
+ absPath,
21432
+ toolName: tool.name,
21433
+ toolUseId: use.id,
21434
+ durationMs: Date.now() - start
21435
+ });
21436
+ this.opts.events?.emit("file.event", {
21437
+ operation,
21438
+ filePath: inputPath,
21439
+ absPath,
21440
+ sessionId: ctx.session.id,
21441
+ agentId: ctx.agentId,
21442
+ agentName: ctx.agentName,
21443
+ provider: typeof ctx.provider === "object" ? ctx.provider.id : String(ctx.provider),
21444
+ model: ctx.model,
21445
+ toolName: tool.name,
21446
+ toolUseId: use.id,
21447
+ scope: ctx.currentKanbanTaskId ? "task" : "session",
21448
+ taskId: ctx.currentKanbanTaskId,
21449
+ boardId: ctx.currentKanbanBoardId,
21450
+ timestamp: ts,
21451
+ durationMs: Date.now() - start
21452
+ });
21453
+ }
20840
21454
  return { result, tool, durationMs: Date.now() - start };
20841
21455
  } catch (err) {
20842
21456
  if (isWrongStackError(err)) {
@@ -21065,7 +21679,7 @@ ${post.additionalContext}`;
21065
21679
  toolPromise.catch(() => {
21066
21680
  });
21067
21681
  try {
21068
- output = await new Promise((resolve12, reject) => {
21682
+ output = await new Promise((resolve14, reject) => {
21069
21683
  const onAbort = () => {
21070
21684
  setTimeout(() => reject(abortReasonToError(combined.reason)), 0);
21071
21685
  };
@@ -21073,7 +21687,7 @@ ${post.additionalContext}`;
21073
21687
  toolPromise.then(
21074
21688
  (v) => {
21075
21689
  combined.removeEventListener("abort", onAbort);
21076
- resolve12(v);
21690
+ resolve14(v);
21077
21691
  },
21078
21692
  (e) => {
21079
21693
  combined.removeEventListener("abort", onAbort);
@@ -21364,12 +21978,12 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
21364
21978
  return content;
21365
21979
  }
21366
21980
  try {
21367
- const dir = path18.join(wstackGlobalRoot(), "tool-output");
21368
- await fs8.mkdir(dir, { recursive: true });
21981
+ const dir = path20.join(wstackGlobalRoot(), "tool-output");
21982
+ await fs9.mkdir(dir, { recursive: true });
21369
21983
  const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
21370
21984
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
21371
- const filePath = path18.join(dir, `${stamp}-${safeTool}-${randomUUID11()}.log`);
21372
- await fs8.writeFile(filePath, content, "utf8");
21985
+ const filePath = path20.join(dir, `${stamp}-${safeTool}-${randomUUID11()}.log`);
21986
+ await fs9.writeFile(filePath, content, "utf8");
21373
21987
  const marker = `[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
21374
21988
  const fixedBytes = Buffer.byteLength(marker + TOOL_OUTPUT_ARTIFACT_OMISSION, "utf8");
21375
21989
  const previewBytes = Math.min(
@@ -21387,6 +22001,15 @@ ${head}${TOOL_OUTPUT_ARTIFACT_OMISSION}${tail}`;
21387
22001
  return content;
21388
22002
  }
21389
22003
  }
22004
+ function hashPermissionInput(input, scrubber) {
22005
+ let serialized;
22006
+ try {
22007
+ serialized = JSON.stringify(input) ?? "";
22008
+ } catch {
22009
+ serialized = String(input);
22010
+ }
22011
+ return createHash6("sha256").update(scrubber.scrub(serialized), "utf8").digest("hex");
22012
+ }
21390
22013
  function sliceUtf8Prefix(text, maxBytes) {
21391
22014
  if (maxBytes <= 0) return "";
21392
22015
  if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
@@ -21680,7 +22303,7 @@ var contextManagerTool = createContextManagerTool();
21680
22303
 
21681
22304
  // src/infrastructure/logger.ts
21682
22305
  import * as fsp11 from "node:fs/promises";
21683
- import * as path19 from "node:path";
22306
+ import * as path21 from "node:path";
21684
22307
  var LEVEL_RANK2 = {
21685
22308
  error: 0,
21686
22309
  warn: 1,
@@ -21744,7 +22367,7 @@ var DefaultLogger = class _DefaultLogger {
21744
22367
  this.stderr = opts.stderr !== false;
21745
22368
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
21746
22369
  if (this.file) {
21747
- const dir = path19.dirname(this.file);
22370
+ const dir = path21.dirname(this.file);
21748
22371
  this._tail = this._tail.then(async () => {
21749
22372
  await fsp11.mkdir(dir, { recursive: true });
21750
22373
  }).catch(() => void 0);
@@ -22065,28 +22688,28 @@ function codexModelMeta(id) {
22065
22688
  }
22066
22689
 
22067
22690
  // src/models/mode-store.ts
22068
- import * as fs9 from "node:fs/promises";
22069
- import * as path21 from "node:path";
22691
+ import * as fs10 from "node:fs/promises";
22692
+ import * as path23 from "node:path";
22070
22693
 
22071
22694
  // src/types/mode-prompts.ts
22072
22695
  import { readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
22073
- import * as path20 from "node:path";
22696
+ import * as path22 from "node:path";
22074
22697
  import { fileURLToPath as fileURLToPath4 } from "node:url";
22075
22698
  function modePrompt(id) {
22076
22699
  for (const dir of modePromptDirCandidates()) {
22077
22700
  try {
22078
- return readFileSync4(path20.join(dir, `${id}.md`), "utf8").trimEnd();
22701
+ return readFileSync4(path22.join(dir, `${id}.md`), "utf8").trimEnd();
22079
22702
  } catch {
22080
22703
  }
22081
22704
  }
22082
22705
  return "";
22083
22706
  }
22084
22707
  function modePromptDirCandidates() {
22085
- const here = path20.dirname(fileURLToPath4(import.meta.url));
22708
+ const here = path22.dirname(fileURLToPath4(import.meta.url));
22086
22709
  const candidates = [
22087
- path20.resolve(here, "../../instructions/modes"),
22088
- path20.resolve(here, "../instructions/modes"),
22089
- path20.resolve(here, "instructions/modes")
22710
+ path22.resolve(here, "../../instructions/modes"),
22711
+ path22.resolve(here, "../instructions/modes"),
22712
+ path22.resolve(here, "instructions/modes")
22090
22713
  ];
22091
22714
  return candidates.sort((a, b) => Number(!isDirectory3(a)) - Number(!isDirectory3(b)));
22092
22715
  }
@@ -22318,8 +22941,8 @@ var DefaultModeStore = class {
22318
22941
  }
22319
22942
  async loadActiveMode() {
22320
22943
  try {
22321
- const configPath = path21.join(this.configDir, "mode.json");
22322
- const content = await fs9.readFile(configPath, "utf8");
22944
+ const configPath = path23.join(this.configDir, "mode.json");
22945
+ const content = await fs10.readFile(configPath, "utf8");
22323
22946
  const data = JSON.parse(content);
22324
22947
  this.activeModeId = data.activeMode ?? null;
22325
22948
  } catch {
@@ -22328,8 +22951,8 @@ var DefaultModeStore = class {
22328
22951
  }
22329
22952
  async saveActiveMode() {
22330
22953
  try {
22331
- await fs9.mkdir(this.configDir, { recursive: true });
22332
- const configPath = path21.join(this.configDir, "mode.json");
22954
+ await fs10.mkdir(this.configDir, { recursive: true });
22955
+ const configPath = path23.join(this.configDir, "mode.json");
22333
22956
  await atomicWrite(
22334
22957
  configPath,
22335
22958
  JSON.stringify({ activeMode: this.activeModeId }, null, 2)
@@ -22341,14 +22964,14 @@ var DefaultModeStore = class {
22341
22964
  async function loadProjectModes(modesDir) {
22342
22965
  const modes = [];
22343
22966
  try {
22344
- const entries = await fs9.readdir(modesDir);
22967
+ const entries = await fs10.readdir(modesDir);
22345
22968
  for (const entry of entries) {
22346
22969
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
22347
- const filePath = path21.join(modesDir, entry);
22348
- const stat12 = await fs9.stat(filePath);
22349
- if (!stat12.isFile()) continue;
22350
- const content = await fs9.readFile(filePath, "utf8");
22351
- const id = path21.basename(entry, path21.extname(entry));
22970
+ const filePath = path23.join(modesDir, entry);
22971
+ const stat13 = await fs10.stat(filePath);
22972
+ if (!stat13.isFile()) continue;
22973
+ const content = await fs10.readFile(filePath, "utf8");
22974
+ const id = path23.basename(entry, path23.extname(entry));
22352
22975
  modes.push({
22353
22976
  id,
22354
22977
  name: id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
@@ -22364,8 +22987,8 @@ async function loadProjectModes(modesDir) {
22364
22987
  async function loadUserModes(modesDir) {
22365
22988
  const modes = [];
22366
22989
  try {
22367
- const manifestPath = path21.join(modesDir, "modes.json");
22368
- const content = await fs9.readFile(manifestPath, "utf8");
22990
+ const manifestPath = path23.join(modesDir, "modes.json");
22991
+ const content = await fs10.readFile(manifestPath, "utf8");
22369
22992
  const manifest = JSON.parse(content);
22370
22993
  for (const mode of manifest.modes) {
22371
22994
  modes.push(mode);
@@ -22376,8 +22999,8 @@ async function loadUserModes(modesDir) {
22376
22999
  }
22377
23000
 
22378
23001
  // src/models/models-registry.ts
22379
- import * as fs10 from "node:fs/promises";
22380
- import * as path22 from "node:path";
23002
+ import * as fs11 from "node:fs/promises";
23003
+ import * as path24 from "node:path";
22381
23004
  var DEFAULT_URL = "https://models.dev/api.json";
22382
23005
  var ENV_URL_KEY = "WRONGSTACK_MODELS_DEV_URL";
22383
23006
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -22453,7 +23076,7 @@ var DefaultModelsRegistry = class {
22453
23076
  this.overlay = opts.overlay;
22454
23077
  this.overlayUrl = opts.overlayUrl;
22455
23078
  this.overlayFile = opts.overlayFile;
22456
- this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path22.join(path22.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
23079
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path24.join(path24.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
22457
23080
  }
22458
23081
  async load(opts = {}) {
22459
23082
  if (this.payload && !opts.force) return this.payload;
@@ -22618,7 +23241,7 @@ var DefaultModelsRegistry = class {
22618
23241
  async readOverlayFile() {
22619
23242
  if (!this.overlayFile) return void 0;
22620
23243
  try {
22621
- const raw = await fs10.readFile(this.overlayFile, "utf8");
23244
+ const raw = await fs11.readFile(this.overlayFile, "utf8");
22622
23245
  return JSON.parse(raw);
22623
23246
  } catch {
22624
23247
  return void 0;
@@ -22697,7 +23320,7 @@ var DefaultModelsRegistry = class {
22697
23320
  }
22698
23321
  async readCacheAt(file) {
22699
23322
  try {
22700
- const raw = await fs10.readFile(file, "utf8");
23323
+ const raw = await fs11.readFile(file, "utf8");
22701
23324
  return JSON.parse(raw);
22702
23325
  } catch {
22703
23326
  return void 0;
@@ -22705,7 +23328,7 @@ var DefaultModelsRegistry = class {
22705
23328
  }
22706
23329
  /** Used by `wstack models refresh` to expose where the cache lives. */
22707
23330
  cacheLocation() {
22708
- return path22.resolve(this.cacheFile);
23331
+ return path24.resolve(this.cacheFile);
22709
23332
  }
22710
23333
  };
22711
23334
  function formatAge(seconds) {
@@ -22943,9 +23566,9 @@ var DefaultHealthRegistry = class {
22943
23566
  }
22944
23567
  async runOne(check) {
22945
23568
  let timer = null;
22946
- const timeout = new Promise((resolve12) => {
23569
+ const timeout = new Promise((resolve14) => {
22947
23570
  timer = setTimeout(
22948
- () => resolve12({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
23571
+ () => resolve14({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
22949
23572
  this.timeoutMs
22950
23573
  );
22951
23574
  });
@@ -23129,7 +23752,7 @@ async function startMetricsServer(opts) {
23129
23752
  const tls = opts.tls;
23130
23753
  const useHttps = !!(tls?.cert && tls?.key);
23131
23754
  const host = opts.host ?? "127.0.0.1";
23132
- const path28 = opts.path ?? "/metrics";
23755
+ const path30 = opts.path ?? "/metrics";
23133
23756
  const healthPath = opts.healthPath ?? "/healthz";
23134
23757
  const healthRegistry = opts.healthRegistry;
23135
23758
  const listener = (req, res) => {
@@ -23139,7 +23762,7 @@ async function startMetricsServer(opts) {
23139
23762
  return;
23140
23763
  }
23141
23764
  const url = req.url.split("?")[0];
23142
- if (url === path28) {
23765
+ if (url === path30) {
23143
23766
  let body;
23144
23767
  try {
23145
23768
  body = renderPrometheus(opts.sink.snapshot());
@@ -23185,14 +23808,14 @@ async function startMetricsServer(opts) {
23185
23808
  const { createServer } = await import("node:http");
23186
23809
  server = createServer(listener);
23187
23810
  }
23188
- await new Promise((resolve12, reject) => {
23811
+ await new Promise((resolve14, reject) => {
23189
23812
  const onError = (err) => {
23190
23813
  server.off("listening", onListening);
23191
23814
  reject(err);
23192
23815
  };
23193
23816
  const onListening = () => {
23194
23817
  server.off("error", onError);
23195
- resolve12();
23818
+ resolve14();
23196
23819
  };
23197
23820
  server.once("error", onError);
23198
23821
  server.once("listening", onListening);
@@ -23203,9 +23826,9 @@ async function startMetricsServer(opts) {
23203
23826
  const protocol = useHttps ? "https" : "http";
23204
23827
  return {
23205
23828
  port: boundPort,
23206
- url: `${protocol}://${host}:${boundPort}${path28}`,
23207
- close: () => new Promise((resolve12, reject) => {
23208
- server.close((err) => err ? reject(err) : resolve12());
23829
+ url: `${protocol}://${host}:${boundPort}${path30}`,
23830
+ close: () => new Promise((resolve14, reject) => {
23831
+ server.close((err) => err ? reject(err) : resolve14());
23209
23832
  })
23210
23833
  };
23211
23834
  }
@@ -23475,7 +24098,7 @@ function startOtlpTraceExporter(opts) {
23475
24098
  }
23476
24099
 
23477
24100
  // src/security/permission-policy.ts
23478
- import * as fs11 from "node:fs/promises";
24101
+ import * as fs12 from "node:fs/promises";
23479
24102
 
23480
24103
  // src/security/permission-policy-schema.ts
23481
24104
  var TRUST_POLICY_LIMITS = Object.freeze({
@@ -23520,19 +24143,19 @@ var UNSAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "prototype", "
23520
24143
  function isRecord5(value) {
23521
24144
  return typeof value === "object" && value !== null && !Array.isArray(value);
23522
24145
  }
23523
- function error(diagnostics, code, path28, message) {
23524
- diagnostics.push({ severity: "error", code, path: path28, message });
24146
+ function error(diagnostics, code, path30, message) {
24147
+ diagnostics.push({ severity: "error", code, path: path30, message });
23525
24148
  }
23526
- function validatePatterns(value, path28, diagnostics) {
24149
+ function validatePatterns(value, path30, diagnostics) {
23527
24150
  if (!Array.isArray(value)) {
23528
- error(diagnostics, "invalid_pattern_list", path28, "must be an array of strings");
24151
+ error(diagnostics, "invalid_pattern_list", path30, "must be an array of strings");
23529
24152
  return void 0;
23530
24153
  }
23531
24154
  if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
23532
24155
  error(
23533
24156
  diagnostics,
23534
24157
  "too_many_patterns",
23535
- path28,
24158
+ path30,
23536
24159
  `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
23537
24160
  );
23538
24161
  return void 0;
@@ -23540,7 +24163,7 @@ function validatePatterns(value, path28, diagnostics) {
23540
24163
  const patterns = [];
23541
24164
  const seen = /* @__PURE__ */ new Set();
23542
24165
  for (const [index, pattern] of value.entries()) {
23543
- const itemPath = `${path28}[${index}]`;
24166
+ const itemPath = `${path30}[${index}]`;
23544
24167
  if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
23545
24168
  error(
23546
24169
  diagnostics,
@@ -23652,8 +24275,8 @@ var LruCache = class {
23652
24275
  this.capacity = capacity;
23653
24276
  }
23654
24277
  get(key) {
24278
+ if (!this.store.has(key)) return void 0;
23655
24279
  const value = this.store.get(key);
23656
- if (value === void 0) return void 0;
23657
24280
  this.store.delete(key);
23658
24281
  this.store.set(key, value);
23659
24282
  return value;
@@ -23864,7 +24487,7 @@ var DefaultPermissionPolicy = class {
23864
24487
  this.policyDiagnostics = [];
23865
24488
  this.policyInvalid = false;
23866
24489
  try {
23867
- const raw = await fs11.readFile(this.trustFile, "utf8");
24490
+ const raw = await fs12.readFile(this.trustFile, "utf8");
23868
24491
  const parsed = safeParse(raw);
23869
24492
  if (!parsed.ok) {
23870
24493
  this.policy = {};
@@ -24114,6 +24737,136 @@ var DefaultPermissionPolicy = class {
24114
24737
  this.sessionAllowed.set(`${rule.tool}::${rule.pattern}`, true);
24115
24738
  this._evalCache.clear();
24116
24739
  }
24740
+ /**
24741
+ * Side-effect-free permission evaluation trace. Mirrors `evaluate()`
24742
+ * step-by-step without prompting the user, writing to trust files, or
24743
+ * mutating session state. Returns the full ordered trace and winner.
24744
+ */
24745
+ async explain(tool, input, ctx) {
24746
+ if (!this.loaded) await this.reload();
24747
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
24748
+ const steps = [];
24749
+ let winnerIndex = -1;
24750
+ const add = (rule, matched, decision, source, detail) => {
24751
+ steps.push({ rule, matched, decision, source, detail });
24752
+ };
24753
+ if (this.policyInvalid) {
24754
+ add("policy invalid", true, "deny", "deny", "trust policy is invalid; all tools are denied");
24755
+ winnerIndex = 0;
24756
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "deny", source: "deny", reason: "trust policy is invalid" } };
24757
+ }
24758
+ add("policy valid", false, "auto", "default", "trust policy loaded successfully");
24759
+ const namespaceEntry = this.findNamespaceEntry(tool.name);
24760
+ const entry = this.policy[tool.name] ?? namespaceEntry;
24761
+ const namespaceSource = namespaceEntry ? `wildcard entry matched (${Object.keys(this.policy).find((k) => k.includes("*") && matchGlob(k, tool.name))})` : "no namespace match";
24762
+ const cacheKey = `${tool.name}::${subject ?? tool.name}`;
24763
+ if (this.sessionDenied.has(cacheKey)) {
24764
+ add("session soft deny", true, "deny", "deny", 'user pressed "no" earlier in this session \u2014 blocked until reload');
24765
+ winnerIndex = steps.length - 1;
24766
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "deny", source: "deny", reason: "session soft deny (user pressed no)" } };
24767
+ }
24768
+ add("session soft deny", false, "deny", "deny", "no session-level soft deny for this tool+subject");
24769
+ if (this.sessionAllowed.has(cacheKey)) {
24770
+ add("session soft allow", true, "auto", "trust", 'user pressed "yes" in current session \u2014 one-shot auto-approve (consumed)');
24771
+ winnerIndex = steps.length - 1;
24772
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "auto", source: "trust", reason: "session one-shot allow (user pressed yes)" } };
24773
+ }
24774
+ add("session soft allow", false, "auto", "trust", "no session-level one-shot allow for this tool+subject");
24775
+ if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
24776
+ add("trust deny", true, "deny", "deny", `subject "${subject}" matched a deny pattern in trust file`);
24777
+ winnerIndex = steps.length - 1;
24778
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "deny", source: "deny", reason: "matched deny pattern" } };
24779
+ }
24780
+ add("trust deny", false, "deny", "deny", `no deny pattern matched (namespace: ${namespaceSource})`);
24781
+ if (tool.permission === "deny") {
24782
+ add("tool default deny", true, "deny", "default", `tool "${tool.name}" has permission: deny by default`);
24783
+ winnerIndex = steps.length - 1;
24784
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "deny", source: "default", reason: "tool default deny" } };
24785
+ }
24786
+ add("tool default deny", false, "deny", "default", `tool "${tool.name}" has permission: "${tool.permission}"`);
24787
+ if (entry?.allow && subject && matchesTrust(entry.allow, subject)) {
24788
+ add("trust allow", true, "auto", "trust", `subject "${subject}" matched an allow pattern in trust file`);
24789
+ winnerIndex = steps.length - 1;
24790
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "auto", source: "trust", reason: "matched allow pattern" } };
24791
+ }
24792
+ add("trust allow", false, "auto", "trust", `no allow pattern matched (namespace: ${namespaceSource})`);
24793
+ if (entry?.auto) {
24794
+ add("trust auto", true, "auto", "trust", `trust file has auto: true for "${tool.name}"`);
24795
+ winnerIndex = steps.length - 1;
24796
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "auto", source: "trust", reason: "trust auto" } };
24797
+ }
24798
+ add("trust auto", false, "auto", "trust", `no auto flag for "${tool.name}" in trust file`);
24799
+ if (!this.yolo && this.isSensitiveReadCall(tool, input)) {
24800
+ const hasDelegate2 = this.promptDelegate !== void 0;
24801
+ add(
24802
+ "sensitive read",
24803
+ true,
24804
+ hasDelegate2 ? "confirm" : "confirm",
24805
+ "default",
24806
+ hasDelegate2 ? "sensitive file read detected \u2014 would prompt user for approval" : "sensitive file read detected \u2014 returns confirm (no prompt delegate)"
24807
+ );
24808
+ winnerIndex = steps.length - 1;
24809
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "confirm", source: "default", riskTier: "standard", reason: "sensitive file read needs explicit approval" } };
24810
+ }
24811
+ add("sensitive read", false, "confirm", "default", "not a sensitive read call (or YOLO bypasses this check)");
24812
+ if (this.yolo) {
24813
+ add("yolo", true, "auto", "yolo", "YOLO mode is active \u2014 auto-approving every non-denied call");
24814
+ winnerIndex = steps.length - 1;
24815
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "auto", source: "yolo" } };
24816
+ }
24817
+ add("yolo", false, "auto", "yolo", "YOLO mode is not active");
24818
+ if (tool.name === "write" && subject) {
24819
+ const hasRead = ctx.hasRead(subject);
24820
+ add(
24821
+ "write smart bypass",
24822
+ hasRead,
24823
+ "auto",
24824
+ "context",
24825
+ hasRead ? `file "${subject}" was already read in this session \u2014 auto-approving write` : `file "${subject}" was not read in this session \u2014 bypass does not apply`
24826
+ );
24827
+ if (hasRead) {
24828
+ winnerIndex = steps.length - 1;
24829
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "auto", source: "context", reason: "file already read in this session" } };
24830
+ }
24831
+ } else {
24832
+ add("write smart bypass", false, "auto", "context", 'tool is not "write" or has no subject \u2014 bypass does not apply');
24833
+ }
24834
+ const hasWriteCap = hasCapability(tool, ToolCapabilities.FS_WRITE);
24835
+ const hasShellCap = hasCapability(tool, [
24836
+ ToolCapabilities.SHELL_ARBITRARY,
24837
+ ToolCapabilities.SHELL_RESTRICTED,
24838
+ ToolCapabilities.SHELL_EXEC
24839
+ ]);
24840
+ const hasInstallCap = hasCapability(tool, ToolCapabilities.PACKAGE_INSTALL);
24841
+ const hasConfigCap = hasCapability(tool, ToolCapabilities.CONFIG_MUTATE);
24842
+ const hasSubagentCap = hasCapability(tool, ToolCapabilities.SUBAGENT_SPAWN);
24843
+ const isMutating = tool.mutating || hasWriteCap || hasShellCap || hasInstallCap || hasConfigCap || hasSubagentCap;
24844
+ if (tool.permission === "auto" && !isMutating) {
24845
+ add("safe default auto", true, "auto", "default", `tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`);
24846
+ winnerIndex = steps.length - 1;
24847
+ return { toolName: tool.name, subject, steps, winnerIndex, decision: { permission: "auto", source: "default" } };
24848
+ }
24849
+ add(
24850
+ "mutating default confirm",
24851
+ true,
24852
+ "confirm",
24853
+ "default",
24854
+ isMutating ? `tool "${tool.name}" is mutating (default auto not enough) \u2014 needs confirmation` : `tool "${tool.name}" has "${tool.permission}" permission \u2014 needs confirmation`
24855
+ );
24856
+ winnerIndex = steps.length - 1;
24857
+ const hasDelegate = this.promptDelegate !== void 0;
24858
+ return {
24859
+ toolName: tool.name,
24860
+ subject,
24861
+ steps,
24862
+ winnerIndex,
24863
+ decision: {
24864
+ permission: "confirm",
24865
+ source: "default",
24866
+ ...hasDelegate ? { reason: "would prompt user via delegate" } : {}
24867
+ }
24868
+ };
24869
+ }
24117
24870
  findNamespaceEntry(toolName) {
24118
24871
  for (const { pattern, value } of this.wildcardEntries) {
24119
24872
  if (matchGlob(pattern, toolName)) return value;
@@ -24159,6 +24912,24 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
24159
24912
  }
24160
24913
  allowOnce() {
24161
24914
  }
24915
+ async explain(tool) {
24916
+ const decision = await this.evaluate(tool);
24917
+ return {
24918
+ toolName: tool.name,
24919
+ subject: null,
24920
+ steps: [
24921
+ {
24922
+ rule: "subagent auto",
24923
+ matched: decision.permission === "auto",
24924
+ decision: decision.permission,
24925
+ source: decision.source,
24926
+ detail: decision.reason ?? `subagent policy: ${decision.permission}`
24927
+ }
24928
+ ],
24929
+ winnerIndex: 0,
24930
+ decision
24931
+ };
24932
+ }
24162
24933
  async reload() {
24163
24934
  }
24164
24935
  };
@@ -24359,9 +25130,9 @@ var DefaultSecretScrubber = class {
24359
25130
 
24360
25131
  // src/security/secret-vault.ts
24361
25132
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, scryptSync } from "node:crypto";
24362
- import * as fs12 from "node:fs";
25133
+ import * as fs13 from "node:fs";
24363
25134
  import * as fsp12 from "node:fs/promises";
24364
- import * as path23 from "node:path";
25135
+ import * as path25 from "node:path";
24365
25136
 
24366
25137
  // src/types/secret-vault.ts
24367
25138
  var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
@@ -24501,8 +25272,8 @@ function checkKeyFilePermissions(keyFile, opts) {
24501
25272
  if (process.platform === "win32") return;
24502
25273
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
24503
25274
  try {
24504
- const stat12 = fs12.statSync(keyFile);
24505
- const actualMode = stat12.mode & 511;
25275
+ const stat13 = fs13.statSync(keyFile);
25276
+ const actualMode = stat13.mode & 511;
24506
25277
  if (actualMode !== KEY_FILE_MODE) {
24507
25278
  warn(
24508
25279
  `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Run: chmod ${KEY_FILE_MODE.toString(8)} ${keyFile}`
@@ -24598,16 +25369,16 @@ var DefaultSecretVault = class {
24598
25369
  const oldVersion = this._keyVersion;
24599
25370
  const newKey = randomBytes4(KEY_BYTES);
24600
25371
  const newVersion = oldVersion + 1;
24601
- fs12.mkdirSync(path23.dirname(this.keyFile), { recursive: true });
25372
+ fs13.mkdirSync(path25.dirname(this.keyFile), { recursive: true });
24602
25373
  const passphrase = getVaultPassphrase();
24603
25374
  if (passphrase) {
24604
- fs12.writeFileSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase), { mode: 384 });
25375
+ fs13.writeFileSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase), { mode: 384 });
24605
25376
  } else {
24606
25377
  const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
24607
25378
  KEY_FILE_MAGIC.copy(keyFileBuf, 0);
24608
25379
  keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
24609
25380
  newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
24610
- fs12.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
25381
+ fs13.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
24611
25382
  }
24612
25383
  checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
24613
25384
  this.key = newKey;
@@ -24625,7 +25396,7 @@ var DefaultSecretVault = class {
24625
25396
  const passphrase = getVaultPassphrase();
24626
25397
  if (!passphrase || !this.key) return;
24627
25398
  try {
24628
- fs12.writeFileSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase), {
25399
+ fs13.writeFileSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase), {
24629
25400
  mode: 384
24630
25401
  });
24631
25402
  checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
@@ -24635,7 +25406,7 @@ var DefaultSecretVault = class {
24635
25406
  loadOrCreateKey() {
24636
25407
  if (this.key) return this.key;
24637
25408
  try {
24638
- const buf = fs12.readFileSync(this.keyFile);
25409
+ const buf = fs13.readFileSync(this.keyFile);
24639
25410
  if (isWrappedKeyFile(buf)) {
24640
25411
  const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
24641
25412
  this.key = key2;
@@ -24682,15 +25453,15 @@ var DefaultSecretVault = class {
24682
25453
  } catch (err) {
24683
25454
  if (err.code !== "ENOENT") throw err;
24684
25455
  }
24685
- fs12.mkdirSync(path23.dirname(this.keyFile), { recursive: true });
25456
+ fs13.mkdirSync(path25.dirname(this.keyFile), { recursive: true });
24686
25457
  const key = randomBytes4(KEY_BYTES);
24687
25458
  const passphrase = getVaultPassphrase();
24688
25459
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
24689
25460
  try {
24690
- fs12.writeFileSync(this.keyFile, initialBytes, { mode: 384, flag: "wx" });
25461
+ fs13.writeFileSync(this.keyFile, initialBytes, { mode: 384, flag: "wx" });
24691
25462
  } catch (err) {
24692
25463
  if (err.code !== "EEXIST") throw err;
24693
- const buf = fs12.readFileSync(this.keyFile);
25464
+ const buf = fs13.readFileSync(this.keyFile);
24694
25465
  if (isWrappedKeyFile(buf)) {
24695
25466
  const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
24696
25467
  this.key = winnerKey;
@@ -24740,7 +25511,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
24740
25511
  }
24741
25512
  const merged = deepMerge(current, patch ?? {});
24742
25513
  const encrypted = encryptConfigSecrets(merged, vault);
24743
- await fsp12.mkdir(path23.dirname(configPath), { recursive: true });
25514
+ await fsp12.mkdir(path25.dirname(configPath), { recursive: true });
24744
25515
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
24745
25516
  await restrictFilePermissions(configPath);
24746
25517
  }
@@ -24824,7 +25595,7 @@ function walkCount(node, vault, counter) {
24824
25595
  // src/storage/attachment-store.ts
24825
25596
  import { randomBytes as randomBytes5 } from "node:crypto";
24826
25597
  import * as fsp13 from "node:fs/promises";
24827
- import * as path24 from "node:path";
25598
+ import * as path26 from "node:path";
24828
25599
  var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
24829
25600
  var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
24830
25601
  var DefaultAttachmentStore = class {
@@ -24845,7 +25616,7 @@ var DefaultAttachmentStore = class {
24845
25616
  let data = input.data;
24846
25617
  if (this.spoolDir && bytes >= this.spoolThreshold) {
24847
25618
  await fsp13.mkdir(this.spoolDir, { recursive: true });
24848
- spooledPath = path24.join(this.spoolDir, `${id}.bin`);
25619
+ spooledPath = path26.join(this.spoolDir, `${id}.bin`);
24849
25620
  await atomicWrite(spooledPath, input.data, {
24850
25621
  encoding: input.kind === "image" ? "base64" : "utf8"
24851
25622
  });
@@ -24964,8 +25735,8 @@ function mergeAdjacentText(blocks) {
24964
25735
  }
24965
25736
 
24966
25737
  // src/storage/config-loader.ts
24967
- import * as fs13 from "node:fs/promises";
24968
- import * as path25 from "node:path";
25738
+ import * as fs14 from "node:fs/promises";
25739
+ import * as path27 from "node:path";
24969
25740
 
24970
25741
  // src/types/config.ts
24971
25742
  var DEFAULT_TUI_THINKING_WORD = "thinking";
@@ -25225,6 +25996,7 @@ var defaultIndexing = {
25225
25996
  };
25226
25997
  var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
25227
25998
  "version",
25999
+ "activeProfile",
25228
26000
  "model",
25229
26001
  "cwd",
25230
26002
  "context",
@@ -25247,6 +26019,7 @@ var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
25247
26019
  "fallbackProfiles",
25248
26020
  "favoriteModels",
25249
26021
  "favoriteModelsOnly",
26022
+ "modelAvailabilitySchedule",
25250
26023
  "fallbackAuto",
25251
26024
  "models",
25252
26025
  "modelMatrix",
@@ -25282,6 +26055,7 @@ var KNOWN_DENIED_IN_PROJECT = [
25282
26055
  ];
25283
26056
  var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
25284
26057
  "version",
26058
+ "activeProfile",
25285
26059
  "provider",
25286
26060
  "model",
25287
26061
  "apiKey",
@@ -25298,6 +26072,7 @@ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
25298
26072
  "fallbackProfiles",
25299
26073
  "favoriteModels",
25300
26074
  "favoriteModelsOnly",
26075
+ "modelAvailabilitySchedule",
25301
26076
  "fallbackAuto",
25302
26077
  "hooks",
25303
26078
  "plugins",
@@ -25445,8 +26220,8 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
25445
26220
  return out;
25446
26221
  }
25447
26222
  function samePath(a, b) {
25448
- let ra = path25.resolve(a);
25449
- let rb = path25.resolve(b);
26223
+ let ra = path27.resolve(a);
26224
+ let rb = path27.resolve(b);
25450
26225
  if (process.platform === "win32" || process.platform === "darwin") {
25451
26226
  ra = ra.toLowerCase();
25452
26227
  rb = rb.toLowerCase();
@@ -25468,7 +26243,7 @@ function deepMerge2(base, patch) {
25468
26243
  opts
25469
26244
  );
25470
26245
  }
25471
- var DefaultConfigLoader = class {
26246
+ var DefaultConfigLoader = class _DefaultConfigLoader {
25472
26247
  paths;
25473
26248
  strict;
25474
26249
  vault;
@@ -25502,12 +26277,17 @@ var DefaultConfigLoader = class {
25502
26277
  let cfg = { ...BEHAVIOR_DEFAULTS };
25503
26278
  await this.ensureGlobalDefaults();
25504
26279
  const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
25505
- const [global, local, inProject] = await Promise.all([
26280
+ const [bootstrap, local, inProject] = await Promise.all([
25506
26281
  this.readJson(this.paths.globalConfig),
25507
26282
  this.readJson(this.paths.projectLocalConfig),
25508
26283
  inProjectCollides ? Promise.resolve({}) : this.readJson(this.paths.inProjectConfig)
25509
26284
  ]);
25510
- cfg = deepMerge2(cfg, global);
26285
+ cfg = deepMerge2(cfg, bootstrap);
26286
+ const profileName = bootstrap.activeProfile ?? "default";
26287
+ const profileCfg = await this.readJson(this.paths.profileConfig(profileName));
26288
+ if (Object.keys(profileCfg).length > 0) {
26289
+ cfg = deepMerge2(cfg, profileCfg);
26290
+ }
25511
26291
  cfg = deepMerge2(cfg, local);
25512
26292
  cfg = deepMerge2(
25513
26293
  cfg,
@@ -25533,10 +26313,11 @@ var DefaultConfigLoader = class {
25533
26313
  cfg = deepMerge2(cfg, patch);
25534
26314
  }
25535
26315
  } catch (err) {
25536
- this.logWarn(
25537
- "Config source load failed",
25538
- { event: "config.source_load_failed", source: src.name, message: toErrorMessage(err) }
25539
- );
26316
+ this.logWarn("Config source load failed", {
26317
+ event: "config.source_load_failed",
26318
+ source: src.name,
26319
+ message: toErrorMessage(err)
26320
+ });
25540
26321
  }
25541
26322
  }
25542
26323
  if (opts.cliFlags) {
@@ -25569,14 +26350,20 @@ var DefaultConfigLoader = class {
25569
26350
  }
25570
26351
  return Object.freeze(cfg);
25571
26352
  }
26353
+ /** Check whether a config object contains only the two bootstrap keys. */
26354
+ static isBootstrapOnly(config) {
26355
+ const BOOTSTRAP_KEYS = /* @__PURE__ */ new Set(["version", "activeProfile"]);
26356
+ return Object.keys(config).every((k) => BOOTSTRAP_KEYS.has(k));
26357
+ }
25572
26358
  async ensureGlobalDefaults() {
25573
26359
  const fp = this.paths.globalConfig;
25574
26360
  const t0 = Date.now();
25575
26361
  try {
25576
26362
  await withFileLock(fp, async () => {
25577
26363
  let parsed;
26364
+ let fileExisted = true;
25578
26365
  try {
25579
- const raw = await fs13.readFile(fp, "utf8");
26366
+ const raw = await fs14.readFile(fp, "utf8");
25580
26367
  const result = safeParse(raw);
25581
26368
  if (!result.ok || !isPlainRecord(result.value)) {
25582
26369
  return;
@@ -25595,29 +26382,46 @@ var DefaultConfigLoader = class {
25595
26382
  durationMs: Date.now() - t0,
25596
26383
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25597
26384
  });
25598
- this.logWarn(
25599
- "Config defaults read failed",
25600
- { event: "config.defaults_read_failed", path: fp, message: toErrorMessage(err) }
25601
- );
26385
+ this.logWarn("Config defaults read failed", {
26386
+ event: "config.defaults_read_failed",
26387
+ path: fp,
26388
+ message: toErrorMessage(err)
26389
+ });
25602
26390
  return;
25603
26391
  }
26392
+ fileExisted = false;
25604
26393
  parsed = {};
25605
26394
  }
25606
- const { value, changed } = fillMissingDefaults(
25607
- parsed,
25608
- BEHAVIOR_DEFAULTS
25609
- );
25610
- if (!changed) return;
25611
- await atomicWrite(fp, JSON.stringify(value, null, 2), { mode: 384 });
25612
- this.events?.emit("storage.write", {
25613
- sessionId: "~config~",
25614
- store: "config",
25615
- filePath: fp,
25616
- operation: "ensure_defaults",
25617
- outcome: "success",
25618
- durationMs: Date.now() - t0,
25619
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25620
- });
26395
+ const profileName = parsed.activeProfile ?? "default";
26396
+ const profileFp = this.paths.profileConfig(profileName);
26397
+ await this.ensureProfileConfig(profileFp, parsed, fileExisted);
26398
+ const bootstrap = {
26399
+ version: 1,
26400
+ activeProfile: profileName
26401
+ };
26402
+ let needsBootstrapWrite = false;
26403
+ if (parsed.version !== 1) {
26404
+ needsBootstrapWrite = true;
26405
+ }
26406
+ if (parsed.activeProfile === void 0) {
26407
+ needsBootstrapWrite = true;
26408
+ }
26409
+ if (!_DefaultConfigLoader.isBootstrapOnly(parsed)) {
26410
+ needsBootstrapWrite = true;
26411
+ }
26412
+ if (needsBootstrapWrite) {
26413
+ await backupConfigFile(fp, this.paths);
26414
+ await atomicWrite(fp, JSON.stringify(bootstrap, null, 2), { mode: 384 });
26415
+ this.events?.emit("storage.write", {
26416
+ sessionId: "~config~",
26417
+ store: "config",
26418
+ filePath: fp,
26419
+ operation: "ensure_defaults",
26420
+ outcome: "success",
26421
+ durationMs: Date.now() - t0,
26422
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
26423
+ });
26424
+ }
25621
26425
  });
25622
26426
  } catch (err) {
25623
26427
  this.events?.emit("storage.error", {
@@ -25631,11 +26435,104 @@ var DefaultConfigLoader = class {
25631
26435
  durationMs: Date.now() - t0,
25632
26436
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25633
26437
  });
25634
- this.logWarn(
25635
- "Config defaults write failed",
25636
- { event: "config.defaults_write_failed", path: fp, message: toErrorMessage(err) }
25637
- );
26438
+ this.logWarn("Config defaults write failed", {
26439
+ event: "config.defaults_write_failed",
26440
+ path: fp,
26441
+ message: toErrorMessage(err)
26442
+ });
26443
+ }
26444
+ }
26445
+ /**
26446
+ * Ensure a profile config file exists at the given path.
26447
+ * On first boot: migrate content from the old flat global config, or seed
26448
+ * with behavior defaults. Subsequent boots: fill any missing keys from
26449
+ * BEHAVIOR_DEFAULTS (keeping user settings intact).
26450
+ *
26451
+ * CRITICAL SAFETY NET: when the profile already exists but the old global
26452
+ * config has extra non-bootstrap keys (because WebUI/CLI persistence functions
26453
+ * mistakenly wrote settings to the root config), those keys are merged into
26454
+ * the profile so they are NOT silently destroyed by the subsequent bootstrap
26455
+ * trim. This prevents the "config keeps getting emptied" bug.
26456
+ */
26457
+ async ensureProfileConfig(profileFp, oldGlobalParsed, oldFileExisted) {
26458
+ const t0 = Date.now();
26459
+ let parsed;
26460
+ let existed = true;
26461
+ try {
26462
+ const raw = await fs14.readFile(profileFp, "utf8");
26463
+ const result = safeParse(raw);
26464
+ if (!result.ok || !isPlainRecord(result.value)) {
26465
+ this.logWarn("Profile config parse failed \u2014 falling back to defaults", {
26466
+ event: "config.profile_parse_failed",
26467
+ path: profileFp
26468
+ });
26469
+ parsed = {};
26470
+ } else {
26471
+ parsed = result.value;
26472
+ }
26473
+ } catch (err) {
26474
+ if (err.code !== "ENOENT") {
26475
+ this.logWarn("Profile config read failed", {
26476
+ event: "config.profile_read_failed",
26477
+ path: profileFp,
26478
+ message: toErrorMessage(err)
26479
+ });
26480
+ return;
26481
+ }
26482
+ existed = false;
26483
+ parsed = {};
26484
+ }
26485
+ const profileHasContent = existed && Object.keys(parsed).some((k) => k !== "version" && k !== "activeProfile");
26486
+ let seed;
26487
+ if (!profileHasContent && oldFileExisted && Object.keys(oldGlobalParsed).length >= 1) {
26488
+ seed = { ...oldGlobalParsed };
26489
+ delete seed["activeProfile"];
26490
+ const filled = fillMissingDefaults(seed, BEHAVIOR_DEFAULTS);
26491
+ seed = filled.value;
26492
+ } else {
26493
+ if (existed && !_DefaultConfigLoader.isBootstrapOnly(oldGlobalParsed)) {
26494
+ const BOOTSTRAP_KEYS = /* @__PURE__ */ new Set(["version", "activeProfile"]);
26495
+ let merged = false;
26496
+ for (const [k, v] of Object.entries(oldGlobalParsed)) {
26497
+ if (!BOOTSTRAP_KEYS.has(k) && !(k in parsed)) {
26498
+ parsed[k] = v;
26499
+ merged = true;
26500
+ }
26501
+ }
26502
+ if (merged) {
26503
+ const filled2 = fillMissingDefaults(parsed, BEHAVIOR_DEFAULTS);
26504
+ if (filled2.changed) {
26505
+ seed = filled2.value;
26506
+ await backupConfigFile(profileFp, this.paths);
26507
+ await atomicWrite(profileFp, JSON.stringify(seed, null, 2), { mode: 384 });
26508
+ this.events?.emit("storage.write", {
26509
+ sessionId: "~config~",
26510
+ store: "config",
26511
+ filePath: profileFp,
26512
+ operation: "ensure_profile_defaults",
26513
+ outcome: "success",
26514
+ durationMs: Date.now() - t0,
26515
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
26516
+ });
26517
+ return;
26518
+ }
26519
+ }
26520
+ }
26521
+ const filled = fillMissingDefaults(parsed, BEHAVIOR_DEFAULTS);
26522
+ if (!filled.changed) return;
26523
+ seed = filled.value;
25638
26524
  }
26525
+ await backupConfigFile(profileFp, this.paths);
26526
+ await atomicWrite(profileFp, JSON.stringify(seed, null, 2), { mode: 384 });
26527
+ this.events?.emit("storage.write", {
26528
+ sessionId: "~config~",
26529
+ store: "config",
26530
+ filePath: profileFp,
26531
+ operation: "ensure_profile_defaults",
26532
+ outcome: "success",
26533
+ durationMs: Date.now() - t0,
26534
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
26535
+ });
25639
26536
  }
25640
26537
  /**
25641
26538
  * Persist a sync config to ~/.wrongstack/sync.json, with the token encrypted
@@ -25685,7 +26582,7 @@ var DefaultConfigLoader = class {
25685
26582
  const fp = this.paths.syncConfig;
25686
26583
  const t0 = Date.now();
25687
26584
  try {
25688
- const raw = await fs13.readFile(fp, "utf8");
26585
+ const raw = await fs14.readFile(fp, "utf8");
25689
26586
  const parsed = safeParse(raw);
25690
26587
  if (!parsed.ok || !parsed.value) {
25691
26588
  this.events?.emit("storage.read", {
@@ -25736,10 +26633,10 @@ var DefaultConfigLoader = class {
25736
26633
  error: storageErrorString(err),
25737
26634
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25738
26635
  });
25739
- this.logWarn(
25740
- "Config sync load failed",
25741
- { event: "config.sync_load_failed", message: toErrorMessage(err) }
25742
- );
26636
+ this.logWarn("Config sync load failed", {
26637
+ event: "config.sync_load_failed",
26638
+ message: toErrorMessage(err)
26639
+ });
25743
26640
  return null;
25744
26641
  }
25745
26642
  }
@@ -25747,8 +26644,8 @@ var DefaultConfigLoader = class {
25747
26644
  const t0 = Date.now();
25748
26645
  let mtimeMs = null;
25749
26646
  try {
25750
- const stat12 = await fs13.stat(file);
25751
- mtimeMs = stat12.mtimeMs;
26647
+ const stat13 = await fs14.stat(file);
26648
+ mtimeMs = stat13.mtimeMs;
25752
26649
  const cached = this.jsonCache.get(file);
25753
26650
  if (cached && cached.mtimeMs === mtimeMs) {
25754
26651
  return structuredClone(cached.value);
@@ -25768,15 +26665,16 @@ var DefaultConfigLoader = class {
25768
26665
  error: storageErrorString(err),
25769
26666
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25770
26667
  });
25771
- this.logWarn(
25772
- "Config read failed (stat)",
25773
- { event: "config.read_failed", path: file, message: toErrorMessage(err) }
25774
- );
26668
+ this.logWarn("Config read failed (stat)", {
26669
+ event: "config.read_failed",
26670
+ path: file,
26671
+ message: toErrorMessage(err)
26672
+ });
25775
26673
  return {};
25776
26674
  }
25777
26675
  let raw;
25778
26676
  try {
25779
- raw = await fs13.readFile(file, "utf8");
26677
+ raw = await fs14.readFile(file, "utf8");
25780
26678
  } catch (err) {
25781
26679
  if (err.code !== "ENOENT") {
25782
26680
  this.events?.emit("storage.read", {
@@ -25789,10 +26687,11 @@ var DefaultConfigLoader = class {
25789
26687
  error: storageErrorString(err),
25790
26688
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25791
26689
  });
25792
- this.logWarn(
25793
- "Config read failed (read)",
25794
- { event: "config.read_failed", path: file, message: toErrorMessage(err) }
25795
- );
26690
+ this.logWarn("Config read failed (read)", {
26691
+ event: "config.read_failed",
26692
+ path: file,
26693
+ message: toErrorMessage(err)
26694
+ });
25796
26695
  }
25797
26696
  this.jsonCache.set(file, { mtimeMs: null, value: {} });
25798
26697
  return {};
@@ -25809,10 +26708,10 @@ var DefaultConfigLoader = class {
25809
26708
  error: "parse error or empty file",
25810
26709
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25811
26710
  });
25812
- this.logWarn(
25813
- "Config parse failed \u2014 falling back to defaults",
25814
- { event: "config.parse_failed", path: file }
25815
- );
26711
+ this.logWarn("Config parse failed \u2014 falling back to defaults", {
26712
+ event: "config.parse_failed",
26713
+ path: file
26714
+ });
25816
26715
  return {};
25817
26716
  }
25818
26717
  this.jsonCache.set(file, { mtimeMs, value: structuredClone(parsed.value) });
@@ -26306,7 +27205,7 @@ ${cat}:`);
26306
27205
 
26307
27206
  // src/storage/queue-store.ts
26308
27207
  import * as fsp15 from "node:fs/promises";
26309
- import * as path26 from "node:path";
27208
+ import * as path28 from "node:path";
26310
27209
  var QueueStore = class {
26311
27210
  file;
26312
27211
  // Use `| undefined` (not `?`) so exactOptionalPropertyTypes doesn't
@@ -26315,7 +27214,7 @@ var QueueStore = class {
26315
27214
  traceId;
26316
27215
  logger;
26317
27216
  constructor(opts) {
26318
- this.file = path26.join(opts.dir, "queue.json");
27217
+ this.file = path28.join(opts.dir, "queue.json");
26319
27218
  this.events = opts.events;
26320
27219
  this.traceId = opts.traceId;
26321
27220
  this.logger = opts.logger;
@@ -26473,7 +27372,7 @@ function isPersistedQueueItem(v) {
26473
27372
  // src/storage/recovery-lock.ts
26474
27373
  import * as fsp16 from "node:fs/promises";
26475
27374
  import * as os3 from "node:os";
26476
- import * as path27 from "node:path";
27375
+ import * as path29 from "node:path";
26477
27376
  var LOCK_FILE = "active.json";
26478
27377
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
26479
27378
  var RecoveryLock = class {
@@ -26484,7 +27383,7 @@ var RecoveryLock = class {
26484
27383
  sessionStore;
26485
27384
  probe;
26486
27385
  constructor(opts) {
26487
- this.file = path27.join(opts.dir, LOCK_FILE);
27386
+ this.file = path29.join(opts.dir, LOCK_FILE);
26488
27387
  this.pid = opts.pid ?? process.pid;
26489
27388
  this.hostname = opts.hostname ?? os3.hostname();
26490
27389
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -26545,7 +27444,7 @@ var RecoveryLock = class {
26545
27444
  * null return before calling this.
26546
27445
  */
26547
27446
  async write(sessionId) {
26548
- await ensureDir(path27.dirname(this.file));
27447
+ await ensureDir(path29.dirname(this.file));
26549
27448
  const lock = {
26550
27449
  v: 1,
26551
27450
  sessionId,
@@ -26844,7 +27743,7 @@ function resolveSessionLoggingConfig(cfg) {
26844
27743
  }
26845
27744
 
26846
27745
  // src/storage/session-reader.ts
26847
- import * as fs14 from "node:fs/promises";
27746
+ import * as fs15 from "node:fs/promises";
26848
27747
  var DefaultSessionReader = class _DefaultSessionReader {
26849
27748
  store;
26850
27749
  eventCache = /* @__PURE__ */ new Map();
@@ -26862,8 +27761,8 @@ var DefaultSessionReader = class _DefaultSessionReader {
26862
27761
  const sessionPath = sessionScopedPath(rootDir, sessionId, ".jsonl");
26863
27762
  let mtimeMs = null;
26864
27763
  try {
26865
- const stat12 = await fs14.stat(sessionPath);
26866
- mtimeMs = stat12.mtimeMs;
27764
+ const stat13 = await fs15.stat(sessionPath);
27765
+ mtimeMs = stat13.mtimeMs;
26867
27766
  } catch {
26868
27767
  this.eventCache.delete(sessionId);
26869
27768
  this.eventCacheMtimes.delete(sessionId);