@wrongstack/core 0.303.0 → 0.305.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 (76) hide show
  1. package/dist/chronicle/project-server.js +18 -48
  2. package/dist/coordination/agents/index.js +530 -130
  3. package/dist/coordination/agents/project-agent-consolidation.d.ts +5 -0
  4. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  5. package/dist/coordination/agents/project-agent-identity.d.ts +26 -12
  6. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  7. package/dist/coordination/agents/project-agent-learning-structured.d.ts +46 -1
  8. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  9. package/dist/coordination/agents/project-agent-skill-layer.d.ts +55 -10
  10. package/dist/coordination/agents/types.d.ts +10 -2
  11. package/dist/coordination/director-prompts.d.ts +19 -6
  12. package/dist/coordination/director-tools.d.ts +2 -2
  13. package/dist/coordination/fleet.d.ts +0 -6
  14. package/dist/coordination/index.d.ts +2 -1
  15. package/dist/coordination/index.js +1581 -955
  16. package/dist/coordination/mailbox-project-server.js +28 -57
  17. package/dist/core/agent-types.d.ts +4 -2
  18. package/dist/core/agent.d.ts +1 -0
  19. package/dist/core/context.d.ts +15 -0
  20. package/dist/core/conversation-state.d.ts +14 -0
  21. package/dist/core/fallback-profile-manager.d.ts +70 -2
  22. package/dist/core/index.js +308 -108
  23. package/dist/core/system-prompt-blocks.d.ts +1 -1
  24. package/dist/core/system-prompt-builder.d.ts +13 -1
  25. package/dist/core/system-prompt-glossary.d.ts +73 -0
  26. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  27. package/dist/defaults/index.js +910 -693
  28. package/dist/execution/council-orchestrator.d.ts +3 -13
  29. package/dist/execution/index.js +211 -75
  30. package/dist/execution/one-shot-llm.d.ts +5 -0
  31. package/dist/hq/index.js +17 -7
  32. package/dist/hq/protocol/kanban.d.ts +21 -0
  33. package/dist/hq/protocol.js +5 -1
  34. package/dist/hq/redaction.d.ts +14 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +3505 -2539
  37. package/dist/infrastructure/index.js +247 -122
  38. package/dist/plugin/index.js +101 -3
  39. package/dist/registry/index.js +11 -0
  40. package/dist/registry/tool-registry.d.ts +8 -0
  41. package/dist/replay/hash.d.ts +9 -0
  42. package/dist/replay/index.js +14 -4
  43. package/dist/replay/replay-provider-runner.d.ts +31 -1
  44. package/dist/security/index.js +25 -20
  45. package/dist/security/secret-vault.d.ts +2 -0
  46. package/dist/session-catalog/index.js +62 -8
  47. package/dist/session-catalog/project-server.js +109 -78
  48. package/dist/session-catalog/protocol.d.ts +11 -4
  49. package/dist/session-catalog/store.d.ts +2 -2
  50. package/dist/storage/index.js +224 -67
  51. package/dist/storage/memory-consolidator.d.ts +4 -2
  52. package/dist/storage/session-resume-validation.d.ts +24 -0
  53. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  54. package/dist/storage/session-store/fork-session.d.ts +13 -1
  55. package/dist/storage/session-store/load-cache.d.ts +11 -0
  56. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  57. package/dist/storage/session-store.d.ts +18 -0
  58. package/dist/tools/index.js +174 -74
  59. package/dist/types/config/mcp-features.d.ts +31 -1
  60. package/dist/types/config/root.d.ts +12 -0
  61. package/dist/types/config/tools.d.ts +22 -0
  62. package/dist/types/config/ui.d.ts +7 -4
  63. package/dist/types/default-config.d.ts +1 -0
  64. package/dist/types/index.js +24 -1
  65. package/dist/types/session.d.ts +9 -1
  66. package/dist/utils/index.d.ts +1 -0
  67. package/dist/utils/index.js +214 -76
  68. package/dist/utils/project-state-guard.d.ts +21 -0
  69. package/dist/utils/session-scoped-path.d.ts +17 -0
  70. package/dist/utils/todos-format.d.ts +20 -0
  71. package/instructions/leader-after-task.md +3 -4
  72. package/instructions/system-lite.md +10 -13
  73. package/instructions/system-pro.md +18 -25
  74. package/instructions/system.md +18 -23
  75. package/package.json +3 -3
  76. package/skills/wrongstack-kanban/SKILL.md +95 -124
@@ -1264,8 +1264,8 @@ function defaultFormatTaskInput(task) {
1264
1264
  }
1265
1265
 
1266
1266
  // src/coordination/agents/agent-prompts.ts
1267
- import { readFileSync as readFileSync9, statSync as statSync2 } from "node:fs";
1268
- import * as path10 from "node:path";
1267
+ import { readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
1268
+ import * as path12 from "node:path";
1269
1269
  import { fileURLToPath } from "node:url";
1270
1270
 
1271
1271
  // src/utils/wstack-paths.ts
@@ -1408,8 +1408,8 @@ function resolveWstackPaths(opts) {
1408
1408
  }
1409
1409
 
1410
1410
  // src/coordination/agents/project-agent-identity.ts
1411
- import { existsSync as existsSync4, readFileSync as readFileSync8 } from "node:fs";
1412
- import * as path9 from "node:path";
1411
+ import { existsSync as existsSync4, readFileSync as readFileSync9 } from "node:fs";
1412
+ import * as path11 from "node:path";
1413
1413
 
1414
1414
  // src/types/runtime-capability-manifest.ts
1415
1415
  var PLAYWRIGHT_ALIASES = {
@@ -1901,8 +1901,8 @@ function loadProjectAgentConfig(role, projectRoot) {
1901
1901
  }
1902
1902
 
1903
1903
  // src/coordination/agents/project-agent-consolidation.ts
1904
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
1905
- import * as path5 from "node:path";
1904
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
1905
+ import * as path6 from "node:path";
1906
1906
 
1907
1907
  // src/coordination/agents/project-agent-learning-entries.ts
1908
1908
  function tokenOverlap(a, b) {
@@ -2040,6 +2040,21 @@ function normalizeForComparison(text) {
2040
2040
  }
2041
2041
 
2042
2042
  // src/coordination/agents/project-agent-learning-structured.ts
2043
+ function directiveTrials(entry) {
2044
+ const count = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
2045
+ const applied = count(entry.applied);
2046
+ const wins = Math.min(applied, count(entry.wins));
2047
+ return { applied, wins, losses: applied - wins };
2048
+ }
2049
+ function directiveUtility(entry) {
2050
+ const { applied, wins } = directiveTrials(entry);
2051
+ return (wins + 1) / (applied + 2);
2052
+ }
2053
+ var DIRECTIVE_PROVEN_MIN_APPLIED = 5;
2054
+ var DIRECTIVE_PROVEN_MIN_UTILITY = 0.7;
2055
+ function isProvenDirective(entry) {
2056
+ return directiveTrials(entry).applied >= DIRECTIVE_PROVEN_MIN_APPLIED && directiveUtility(entry) >= DIRECTIVE_PROVEN_MIN_UTILITY;
2057
+ }
2043
2058
  function parseLearnedEntryStamp(entry) {
2044
2059
  const structuredMatch = entry.match(
2045
2060
  /<!--\s*learned-stamp:\s*category=([\w-]+);\s*capturedAt=([^;]+?)\s*-->/
@@ -2158,6 +2173,10 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
2158
2173
  const category = parseLearnedCategory(attributes["category"]) ?? "fact";
2159
2174
  const capturedAt = attributes["capturedAt"] ?? "";
2160
2175
  const skill = attributes["skill"];
2176
+ const trials = directiveTrials({
2177
+ applied: Number(attributes["applied"]),
2178
+ wins: Number(attributes["wins"])
2179
+ });
2161
2180
  const start = (stamp.index ?? 0) + stamp[0].length;
2162
2181
  const end = stamps[index + 1]?.index ?? raw.length;
2163
2182
  const parsed = parseEntryBody(raw.slice(start, end));
@@ -2169,7 +2188,8 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
2169
2188
  why: parsed.why || WHY_BY_CATEGORY[category],
2170
2189
  how: parsed.how,
2171
2190
  capturedAt,
2172
- ...skill ? { skill } : {}
2191
+ ...skill ? { skill } : {},
2192
+ ...trials.applied > 0 ? { applied: trials.applied, wins: trials.wins } : {}
2173
2193
  });
2174
2194
  }
2175
2195
  if (structured.length === 0) {
@@ -2193,17 +2213,31 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
2193
2213
  }
2194
2214
  function mergeStructuredEntries(existing, fresh) {
2195
2215
  const { what, why, how } = decomposeLearnedEntry(fresh.text, fresh.category);
2216
+ const key = directiveKey(fresh.text);
2217
+ const overlapping = existing.filter((entry) => tokenOverlap(entry.key, key) >= 0.55);
2218
+ const proven = overlapping.find(
2219
+ (entry) => entry.category === fresh.category && isProvenDirective(entry)
2220
+ );
2221
+ if (proven) return sortStructuredEntries([...existing]);
2222
+ const ancestor = overlapping.filter((entry) => entry.category === fresh.category).sort((a, b) => directiveTrials(b).applied - directiveTrials(a).applied)[0];
2223
+ const inherited = ancestor ? directiveTrials(ancestor) : { applied: 0, wins: 0 };
2196
2224
  const freshEntry = {
2197
- key: directiveKey(fresh.text),
2225
+ key,
2198
2226
  category: fresh.category,
2199
2227
  what,
2200
2228
  why,
2201
2229
  how,
2202
2230
  capturedAt: fresh.capturedAt,
2203
- ...fresh.skill ? { skill: fresh.skill } : {}
2231
+ ...fresh.skill ? { skill: fresh.skill } : {},
2232
+ ...inherited.applied > 0 ? { applied: inherited.applied, wins: inherited.wins } : {}
2204
2233
  };
2205
- const merged = existing.filter((entry) => tokenOverlap(entry.key, freshEntry.key) < 0.55);
2234
+ const merged = existing.filter(
2235
+ (entry) => entry.category !== fresh.category || tokenOverlap(entry.key, key) < 0.55
2236
+ );
2206
2237
  merged.push(freshEntry);
2238
+ return sortStructuredEntries(merged);
2239
+ }
2240
+ function sortStructuredEntries(merged) {
2207
2241
  const order = {
2208
2242
  warning: 0,
2209
2243
  convention: 1,
@@ -2253,10 +2287,15 @@ function renderLearnedInstructions(role, entries, capturedAt) {
2253
2287
  sections.push(SECTION_TITLE[category]);
2254
2288
  sections.push("");
2255
2289
  for (const entry of list) {
2290
+ const trials = directiveTrials(entry);
2256
2291
  const attributes = [
2257
2292
  `category=${entry.category}`,
2258
2293
  `capturedAt=${entry.capturedAt}`,
2259
- ...entry.skill ? [`skill=${entry.skill}`] : []
2294
+ ...entry.skill ? [`skill=${entry.skill}`] : [],
2295
+ // Zero counters are omitted so an entry with no track record renders
2296
+ // exactly as it did before this field existed — the parse→render
2297
+ // fixed point holds for every pre-existing buffer on disk.
2298
+ ...trials.applied > 0 ? [`applied=${trials.applied}`, `wins=${trials.wins}`] : []
2260
2299
  ].join("; ");
2261
2300
  sections.push(`<!-- learned-stamp: ${attributes} -->`);
2262
2301
  sections.push(`- **${entry.what}**`);
@@ -2280,26 +2319,155 @@ function enforceLearnedBudget(entries, capturedAt, maxBytes, role = "agent") {
2280
2319
  const kept = [...entries];
2281
2320
  const dropped = [];
2282
2321
  const size = (list) => Buffer.byteLength(renderLearnedInstructions(role, list, capturedAt), "utf8");
2322
+ const UTILITY_EPSILON = 1e-9;
2283
2323
  while (kept.length > 1 && size(kept) > maxBytes) {
2284
2324
  let victimIndex = 0;
2285
2325
  for (let i = 1; i < kept.length; i++) {
2286
2326
  const a = kept[i];
2287
2327
  const b = kept[victimIndex];
2288
2328
  const byPriority = DROP_PRIORITY[a.category] - DROP_PRIORITY[b.category];
2289
- if (byPriority < 0 || byPriority === 0 && a.capturedAt < b.capturedAt) victimIndex = i;
2329
+ if (byPriority !== 0) {
2330
+ if (byPriority < 0) victimIndex = i;
2331
+ continue;
2332
+ }
2333
+ const byUtility = directiveUtility(a) - directiveUtility(b);
2334
+ if (Math.abs(byUtility) > UTILITY_EPSILON) {
2335
+ if (byUtility < 0) victimIndex = i;
2336
+ continue;
2337
+ }
2338
+ if (a.capturedAt < b.capturedAt) victimIndex = i;
2290
2339
  }
2291
2340
  dropped.push(...kept.splice(victimIndex, 1));
2292
2341
  }
2293
2342
  return { kept, dropped };
2294
2343
  }
2295
2344
 
2345
+ // src/coordination/agents/project-agent-quarantine.ts
2346
+ import { readFileSync as readFileSync3 } from "node:fs";
2347
+ import * as path5 from "node:path";
2348
+ var QUARANTINE_MAX_BYTES = 64 * 1024;
2349
+ var RETIRED_MATCH_THRESHOLD = 0.55;
2350
+ function quarantinePath(role, projectRoot) {
2351
+ return path5.join(roleDir(role, projectRoot), "quarantine.md");
2352
+ }
2353
+ function readTextOrEmpty(filePath) {
2354
+ try {
2355
+ return readFileSync3(filePath, "utf8");
2356
+ } catch {
2357
+ return "";
2358
+ }
2359
+ }
2360
+ function appendQuarantine(role, retired, at, projectRoot) {
2361
+ if (retired.length === 0) return;
2362
+ const filePath = quarantinePath(role, projectRoot);
2363
+ const existing = readTextOrEmpty(filePath);
2364
+ const header = existing ? "" : `# Retired directives for \`${role}\`
2365
+
2366
+ > Directives that were exercised repeatedly and kept correlating with
2367
+ > failure. They are no longer injected anywhere. Kept for audit \u2014 a directive
2368
+ > can be right about a project that has since changed.
2369
+
2370
+ `;
2371
+ const block = retired.map((entry) => {
2372
+ const { applied, wins, losses } = directiveTrials(entry);
2373
+ const attributes = [
2374
+ `at=${at}`,
2375
+ `category=${entry.category}`,
2376
+ ...entry.skill ? [`skill=${entry.skill}`] : [],
2377
+ `applied=${applied}`,
2378
+ `wins=${wins}`
2379
+ ].join("; ");
2380
+ return [
2381
+ `<!-- quarantined: ${attributes} -->`,
2382
+ `- **${entry.what}**`,
2383
+ ` - *Record:* ${wins} succeeded / ${losses} failed of ${applied} applied`,
2384
+ ""
2385
+ ].join("\n");
2386
+ }).join("\n");
2387
+ let next = `${existing}${header}${block}`;
2388
+ if (Buffer.byteLength(next, "utf8") > QUARANTINE_MAX_BYTES) {
2389
+ const marker = "<!-- quarantined:";
2390
+ const overflow = Buffer.byteLength(next, "utf8") - QUARANTINE_MAX_BYTES;
2391
+ const cut = next.indexOf(marker, overflow);
2392
+ next = cut === -1 ? block : next.slice(cut);
2393
+ }
2394
+ writeTextAtomically(filePath, next);
2395
+ }
2396
+ function readQuarantinedDirectives(role, projectRoot) {
2397
+ const raw = readTextOrEmpty(quarantinePath(role, projectRoot));
2398
+ if (!raw) return [];
2399
+ const out = [];
2400
+ const stamps = [...raw.matchAll(/<!--\s*quarantined:\s*([^>]*?)\s*-->/g)];
2401
+ for (const [index, stamp] of stamps.entries()) {
2402
+ const attributes = {};
2403
+ for (const part of (stamp[1] ?? "").split(";")) {
2404
+ const eq = part.indexOf("=");
2405
+ if (eq === -1) continue;
2406
+ attributes[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
2407
+ }
2408
+ const start = (stamp.index ?? 0) + stamp[0].length;
2409
+ const end = stamps[index + 1]?.index ?? raw.length;
2410
+ const what = /^-\s+\*\*([\s\S]+?)\*\*\s*$/m.exec(raw.slice(start, end))?.[1]?.replace(/\s+/g, " ").trim();
2411
+ if (!what) continue;
2412
+ const skill = attributes["skill"];
2413
+ out.push({ what, ...skill ? { skill } : {} });
2414
+ }
2415
+ return out;
2416
+ }
2417
+ function bareClaim(line) {
2418
+ return line.replace(/^\s*[-*+]\s+/, "").replace(/^\s*\d+[.)]\s+/, "").replace(/\*\*/g, "").replace(/`/g, "").trim();
2419
+ }
2420
+ function indentOf(line) {
2421
+ return line.length - line.trimStart().length;
2422
+ }
2423
+ function scrubRetiredLines(text, retired) {
2424
+ if (!text.trim() || retired.length === 0) return text;
2425
+ const keys = retired.map((what) => normalizeForComparison(what));
2426
+ const lines = text.split("\n");
2427
+ const kept = [];
2428
+ for (let i = 0; i < lines.length; i++) {
2429
+ const line = lines[i];
2430
+ const claim = bareClaim(line);
2431
+ if (claim.length >= MIN_INSTRUCTIVE_LENGTH && keys.some(
2432
+ (key) => tokenOverlap(key, normalizeForComparison(claim)) >= RETIRED_MATCH_THRESHOLD
2433
+ )) {
2434
+ const base = indentOf(line);
2435
+ while (i + 1 < lines.length) {
2436
+ const next = lines[i + 1];
2437
+ if (!next.trim() || indentOf(next) <= base) break;
2438
+ i++;
2439
+ }
2440
+ continue;
2441
+ }
2442
+ kept.push(line);
2443
+ }
2444
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
2445
+ }
2446
+ function hasDirectiveContent(text) {
2447
+ return text.split("\n").some((line) => {
2448
+ const trimmed = line.trimStart();
2449
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(">")) return false;
2450
+ if (/^-{3,}\s*$/.test(trimmed)) return false;
2451
+ if (/^\*[^*].*\*$/.test(trimmed)) return false;
2452
+ return bareClaim(line).length >= MIN_INSTRUCTIVE_LENGTH;
2453
+ });
2454
+ }
2455
+ function retiredDirectivesToWarnAbout(role, currentDirectives, projectRoot, skill) {
2456
+ const currentKeys = currentDirectives.map((text) => normalizeForComparison(text));
2457
+ return readQuarantinedDirectives(role, projectRoot).filter((entry) => skill ? entry.skill === skill : true).map((entry) => entry.what).filter(
2458
+ (what) => !currentKeys.some(
2459
+ (key) => tokenOverlap(key, normalizeForComparison(what)) >= RETIRED_MATCH_THRESHOLD
2460
+ )
2461
+ );
2462
+ }
2463
+
2296
2464
  // src/coordination/agents/project-agent-consolidation.ts
2297
2465
  function learnedPath(role, projectRoot) {
2298
- return path5.join(roleDir(role, projectRoot), "learned.md");
2466
+ return path6.join(roleDir(role, projectRoot), "learned.md");
2299
2467
  }
2300
2468
  function loadProjectAgentLearnedText(role, projectRoot) {
2301
2469
  try {
2302
- return readFileSync3(learnedPath(role, projectRoot), "utf8").trim();
2470
+ return readFileSync4(learnedPath(role, projectRoot), "utf8").trim();
2303
2471
  } catch {
2304
2472
  return "";
2305
2473
  }
@@ -2309,28 +2477,27 @@ function readRawLearnedEntries(role, projectRoot) {
2309
2477
  return parseStructuredLearnedEntriesFromContent(raw, splitLearnedEntries(raw));
2310
2478
  }
2311
2479
  function consolidationPath(role, projectRoot) {
2312
- return path5.join(roleDir(role, projectRoot), "consolidated.md");
2480
+ return path6.join(roleDir(role, projectRoot), "consolidated.md");
2481
+ }
2482
+ function consolidatedDocumentPath(role, projectRoot) {
2483
+ return consolidationPath(assertProjectAgentRole(role), projectRoot);
2313
2484
  }
2314
2485
  function consolidationMetaPath(role, projectRoot) {
2315
- return path5.join(roleDir(role, projectRoot), "consolidation.json");
2486
+ return path6.join(roleDir(role, projectRoot), "consolidation.json");
2316
2487
  }
2317
2488
  function archivePath(role, at, projectRoot) {
2318
- return path5.join(
2319
- roleDir(role, projectRoot),
2320
- "archive",
2321
- `learned-${at.replace(/[:.]/g, "-")}.md`
2322
- );
2489
+ return path6.join(roleDir(role, projectRoot), "archive", `learned-${at.replace(/[:.]/g, "-")}.md`);
2323
2490
  }
2324
2491
  function loadProjectAgentConsolidated(role, projectRoot) {
2325
2492
  try {
2326
- return readFileSync3(consolidationPath(role, projectRoot), "utf8").trim();
2493
+ return readFileSync4(consolidationPath(role, projectRoot), "utf8").trim();
2327
2494
  } catch {
2328
2495
  return "";
2329
2496
  }
2330
2497
  }
2331
2498
  function loadConsolidationMetadata(role, projectRoot) {
2332
2499
  try {
2333
- const raw = readFileSync3(consolidationMetaPath(role, projectRoot), "utf8");
2500
+ const raw = readFileSync4(consolidationMetaPath(role, projectRoot), "utf8");
2334
2501
  const parsed = JSON.parse(raw);
2335
2502
  if (typeof parsed === "object" && parsed !== null && typeof parsed.consolidatedAt === "string" && typeof parsed.sourceEntryCount === "number") {
2336
2503
  return parsed;
@@ -2401,10 +2568,13 @@ function clearProjectAgentConsolidated(role, projectRoot) {
2401
2568
  function buildConsolidationInstruction(role, projectRoot) {
2402
2569
  const normalizedRole = assertProjectAgentRole(role);
2403
2570
  const entries = readRawLearnedEntries(normalizedRole, projectRoot);
2404
- const rawEntries = entries.map(
2405
- (entry) => entry.how ? `${entry.what}
2406
- Anchors: ${entry.how.split("\n").join(", ")}` : entry.what
2407
- );
2571
+ const rawEntries = entries.map((entry) => {
2572
+ const lines = [entry.what];
2573
+ if (entry.how) lines.push(` Anchors: ${entry.how.split("\n").join(", ")}`);
2574
+ const { applied, wins } = directiveTrials(entry);
2575
+ if (applied > 0) lines.push(` Track record: applied ${applied}\xD7, ${wins} succeeded`);
2576
+ return lines.join("\n");
2577
+ });
2408
2578
  const existingConsolidation = loadProjectAgentConsolidated(normalizedRole, projectRoot);
2409
2579
  const rawBody = rawEntries.map((entry, i) => `### Entry ${i + 1}
2410
2580
 
@@ -2424,6 +2594,7 @@ ${entry}`).join("\n\n");
2424
2594
  "6. **Structured format.** Organize the content under clear markdown headings (##) by topic. Use bullet points for individual facts. Group by category (Conventions, Patterns, Warnings, Project Facts) when it helps scannability.",
2425
2595
  "7. **Preserve actionable anchors.** Keep exact file paths, command names, package names, and configuration values that make a directive concrete and runnable.",
2426
2596
  "8. **No filler.** Do not include meta-commentary about the consolidation process. The document should read as if it were always a single authoritative reference.",
2597
+ "9. **Trust the track record over your own judgement of plausibility.** An entry may carry `Track record: applied N\xD7, M succeeded` \u2014 completed tasks that exercised it, and how many of those succeeded. Keep the ones that keep working, even when they read as obvious. An entry applied several times with few successes has been tested and has failed the test: drop it, or narrow it to the condition under which it actually holds. Entries with no track record are unproven rather than bad \u2014 judge them on merit, but do not let one displace a proven entry.",
2427
2598
  "",
2428
2599
  rawEntries.length > 0 ? `## Raw learned entries (${rawEntries.length} total)
2429
2600
 
@@ -2437,6 +2608,21 @@ ${rawBody}` : "## Raw learned entries\n\n(No raw entries yet \u2014 return an em
2437
2608
  ${existingConsolidation}`
2438
2609
  );
2439
2610
  }
2611
+ const retired = retiredDirectivesToWarnAbout(
2612
+ normalizedRole,
2613
+ entries.map((entry) => entry.what),
2614
+ projectRoot
2615
+ );
2616
+ if (retired.length > 0) {
2617
+ sections.push(
2618
+ "",
2619
+ `## Retired \u2014 do not reinstate (${retired.length})
2620
+
2621
+ These were exercised repeatedly and kept correlating with failed tasks. If the existing document above still states any of them, drop it.
2622
+
2623
+ ${retired.map((what) => `- ${what}`).join("\n")}`
2624
+ );
2625
+ }
2440
2626
  sections.push(
2441
2627
  "",
2442
2628
  "## Output",
@@ -2516,7 +2702,7 @@ var BUILT_IN_KNOWLEDGE_MANIFESTS = {
2516
2702
  };
2517
2703
 
2518
2704
  // src/coordination/agents/project-agent-learning-policy.ts
2519
- import { readFileSync as readFileSync4 } from "node:fs";
2705
+ import { readFileSync as readFileSync5 } from "node:fs";
2520
2706
  var DEFAULT_LEARNING_POLICY = {
2521
2707
  enabled: true,
2522
2708
  lifetimeCaptureCount: 0
@@ -2524,23 +2710,26 @@ var DEFAULT_LEARNING_POLICY = {
2524
2710
  function loadProjectAgentLearningPolicy(role, projectRoot) {
2525
2711
  try {
2526
2712
  const parsed = JSON.parse(
2527
- readFileSync4(learningPolicyPath(role, projectRoot), "utf8")
2713
+ readFileSync5(learningPolicyPath(role, projectRoot), "utf8")
2528
2714
  );
2529
2715
  return {
2530
2716
  enabled: parsed.enabled !== false,
2531
2717
  lifetimeCaptureCount: typeof parsed.lifetimeCaptureCount === "number" && Number.isInteger(parsed.lifetimeCaptureCount) && parsed.lifetimeCaptureCount >= 0 ? parsed.lifetimeCaptureCount : 0,
2532
2718
  ...typeof parsed.lastCaptureAt === "string" ? { lastCaptureAt: parsed.lastCaptureAt } : {},
2533
- ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {}
2719
+ ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {},
2720
+ ...typeof parsed.lastOptimizeAt === "string" ? { lastOptimizeAt: parsed.lastOptimizeAt } : {}
2534
2721
  };
2535
2722
  } catch {
2536
2723
  return { ...DEFAULT_LEARNING_POLICY };
2537
2724
  }
2538
2725
  }
2539
2726
  function updateProjectAgentLearningPolicy(role, patch, projectRoot) {
2540
- const current = loadProjectAgentLearningPolicy(role, projectRoot);
2727
+ const defined = Object.fromEntries(
2728
+ Object.entries(patch).filter(([, value]) => value !== void 0)
2729
+ );
2541
2730
  const updated = {
2542
- ...current,
2543
- ...patch.enabled !== void 0 ? { enabled: patch.enabled } : {}
2731
+ ...loadProjectAgentLearningPolicy(role, projectRoot),
2732
+ ...defined
2544
2733
  };
2545
2734
  writeTextAtomically(
2546
2735
  learningPolicyPath(role, projectRoot),
@@ -2549,14 +2738,20 @@ function updateProjectAgentLearningPolicy(role, patch, projectRoot) {
2549
2738
  );
2550
2739
  return updated;
2551
2740
  }
2741
+ function recordProjectAgentOptimizePass(role, projectRoot, at = (/* @__PURE__ */ new Date()).toISOString()) {
2742
+ try {
2743
+ updateProjectAgentLearningPolicy(role, { lastOptimizeAt: at }, projectRoot);
2744
+ } catch {
2745
+ }
2746
+ }
2552
2747
 
2553
2748
  // src/coordination/agents/project-agent-skill-layer.ts
2554
- import { readdirSync as readdirSync2, readFileSync as readFileSync6, rmSync as rmSync3 } from "node:fs";
2555
- import * as path7 from "node:path";
2749
+ import { readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync3 } from "node:fs";
2750
+ import * as path8 from "node:path";
2556
2751
 
2557
2752
  // src/coordination/agents/project-agent-profile.ts
2558
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5 } from "node:fs";
2559
- import * as path6 from "node:path";
2753
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6 } from "node:fs";
2754
+ import * as path7 from "node:path";
2560
2755
  function slugifyProjectAgentRole(name) {
2561
2756
  const slug = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "").slice(0, 96);
2562
2757
  return assertProjectAgentRole(slug);
@@ -2564,7 +2759,7 @@ function slugifyProjectAgentRole(name) {
2564
2759
  function loadProjectAgentProfile(role, projectRoot) {
2565
2760
  try {
2566
2761
  const parsed = JSON.parse(
2567
- readFileSync5(projectAgentProfilePath(role, projectRoot), "utf8")
2762
+ readFileSync6(projectAgentProfilePath(role, projectRoot), "utf8")
2568
2763
  );
2569
2764
  const normalizedRole = assertProjectAgentRole(parsed.role ?? role);
2570
2765
  const baseRole = assertProjectAgentRole(parsed.baseRole ?? "generic");
@@ -2635,7 +2830,7 @@ function createProjectAgent(input, projectRoot) {
2635
2830
  ""
2636
2831
  ].join("\n");
2637
2832
  mkdirSync3(dir, { recursive: true });
2638
- writeTextAtomically(path6.join(dir, "identity.md"), identity);
2833
+ writeTextAtomically(path7.join(dir, "identity.md"), identity);
2639
2834
  writeTextAtomically(
2640
2835
  projectAgentProfilePath(role, projectRoot),
2641
2836
  `${JSON.stringify(profile, null, 2)}
@@ -2924,17 +3119,17 @@ function assertProjectSkillName(name) {
2924
3119
  return normalized;
2925
3120
  }
2926
3121
  function projectSkillsDir(role, projectRoot) {
2927
- return path7.join(roleDir(role, projectRoot), "skills");
3122
+ return path8.join(roleDir(role, projectRoot), "skills");
2928
3123
  }
2929
3124
  function projectSkillAugmentationPath(role, skill, projectRoot) {
2930
- return path7.join(projectSkillsDir(role, projectRoot), `${assertProjectSkillName(skill)}.md`);
3125
+ return path8.join(projectSkillsDir(role, projectRoot), `${assertProjectSkillName(skill)}.md`);
2931
3126
  }
2932
3127
  function projectSkillAffinityPath(role, projectRoot) {
2933
- return path7.join(projectSkillsDir(role, projectRoot), "affinity.json");
3128
+ return path8.join(projectSkillsDir(role, projectRoot), "affinity.json");
2934
3129
  }
2935
3130
  function loadProjectSkillAugmentation(role, skill, projectRoot) {
2936
3131
  try {
2937
- return readFileSync6(projectSkillAugmentationPath(role, skill, projectRoot), "utf8").trim();
3132
+ return readFileSync7(projectSkillAugmentationPath(role, skill, projectRoot), "utf8").trim();
2938
3133
  } catch {
2939
3134
  return "";
2940
3135
  }
@@ -2943,13 +3138,32 @@ function saveProjectSkillAugmentation(role, skill, content, projectRoot) {
2943
3138
  const filePath = projectSkillAugmentationPath(role, skill, projectRoot);
2944
3139
  const trimmed = content.trim();
2945
3140
  if (!trimmed) return filePath;
2946
- const bounded = Buffer.byteLength(trimmed, "utf8") > SKILL_AUGMENTATION_MAX_BYTES ? `${trimmed.slice(0, SKILL_AUGMENTATION_MAX_BYTES)}
2947
-
2948
- _(truncated at ${SKILL_AUGMENTATION_MAX_BYTES} bytes)_` : trimmed;
2949
- writeTextAtomically(filePath, `${bounded}
3141
+ writeTextAtomically(filePath, `${boundAugmentation(trimmed)}
2950
3142
  `);
2951
3143
  return filePath;
2952
3144
  }
3145
+ function boundAugmentation(text) {
3146
+ if (Buffer.byteLength(text, "utf8") <= SKILL_AUGMENTATION_MAX_BYTES) return text;
3147
+ const notice = `
3148
+
3149
+ _(truncated at ${SKILL_AUGMENTATION_MAX_BYTES} bytes)_`;
3150
+ const lines = text.split("\n");
3151
+ const title = lines[0] ?? "";
3152
+ let budget = SKILL_AUGMENTATION_MAX_BYTES - Buffer.byteLength(`${title}
3153
+ ${notice}`, "utf8") - Buffer.byteLength("\u2026\n", "utf8");
3154
+ const tail = [];
3155
+ for (let i = lines.length - 1; i > 0; i--) {
3156
+ const cost = Buffer.byteLength(`${lines[i]}
3157
+ `, "utf8");
3158
+ if (cost > budget) break;
3159
+ tail.unshift(lines[i]);
3160
+ budget -= cost;
3161
+ }
3162
+ if (tail.length === 0) return `${title.slice(0, SKILL_AUGMENTATION_MAX_BYTES)}${notice}`;
3163
+ return `${title}
3164
+ \u2026
3165
+ ${tail.join("\n").trimEnd()}${notice}`;
3166
+ }
2953
3167
  function clearProjectSkillAugmentation(role, skill, projectRoot) {
2954
3168
  const target = skill ? projectSkillAugmentationPath(role, skill, projectRoot) : projectSkillsDir(role, projectRoot);
2955
3169
  try {
@@ -2964,7 +3178,25 @@ function listProjectSkillAugmentations(role, projectRoot) {
2964
3178
  return [];
2965
3179
  }
2966
3180
  }
2967
- function renderSkillAugmentation(role, skill, directives, updatedAt) {
3181
+ function renderSkillAugmentation(role, skill, directives, updatedAt, existing) {
3182
+ const priorBody = stripAugmentationFooter(existing ?? "");
3183
+ const priorLines = priorBody.split("\n").map((line) => normalizeForComparison(line.replace(/^[-*]\s+/, ""))).filter(Boolean);
3184
+ const fresh = directives.filter(
3185
+ (directive) => !priorLines.some((line) => tokenOverlap(line, normalizeForComparison(directive)) >= 0.55)
3186
+ );
3187
+ if (priorBody) {
3188
+ if (fresh.length === 0) return `${(existing ?? "").trim()}
3189
+ `;
3190
+ return [
3191
+ priorBody,
3192
+ "",
3193
+ ...fresh.map((directive) => `- ${directive}`),
3194
+ "",
3195
+ "---",
3196
+ `*Distilled ${updatedAt} \xB7 ${fresh.length} new directive${fresh.length === 1 ? "" : "s"}*`,
3197
+ ""
3198
+ ].join("\n");
3199
+ }
2968
3200
  return [
2969
3201
  `# Project practice: \`${skill}\` (role: \`${role}\`)`,
2970
3202
  "",
@@ -2978,7 +3210,10 @@ function renderSkillAugmentation(role, skill, directives, updatedAt) {
2978
3210
  ""
2979
3211
  ].join("\n");
2980
3212
  }
2981
- function buildSkillDistillInstruction(role, skill, directives, existing) {
3213
+ function stripAugmentationFooter(text) {
3214
+ return text.replace(/\n*---\s*\n\*(?:Distilled|Truncated)[^\n]*\*\s*$/i, "").replace(/\n*_\(truncated at \d+ bytes\)_\s*$/i, "").trimEnd();
3215
+ }
3216
+ function buildSkillDistillInstruction(role, skill, directives, existing, retired = []) {
2982
3217
  const sections = [
2983
3218
  `You are refining the \`${skill}\` skill for the "${role}" agent **in this project**.`,
2984
3219
  "",
@@ -2992,6 +3227,7 @@ function buildSkillDistillInstruction(role, skill, directives, existing) {
2992
3227
  "4. **Merge, do not append.** Combine overlapping directives into one sharper rule. The output must be shorter than the input.",
2993
3228
  "5. **Structure.** Short `##` sections when it helps (e.g. Commands, Conventions, Pitfalls); bullets otherwise.",
2994
3229
  "6. **Budget.** Stay under 400 words. Drop the weakest directives rather than compressing everything into noise.",
3230
+ "7. **Weigh the track record.** Some directives carry one, shown as `[applied N\xD7, M ok]` \u2014 the number of completed tasks that exercised the directive and how many of those succeeded. Lead with the ones that keep working. A directive applied several times with few successes has been tested and found wanting: drop it, or state the narrower condition under which it actually holds. A directive with no record is unproven, not bad \u2014 keep it if it is sound, but do not let it displace a proven one.",
2995
3231
  "",
2996
3232
  `## Directives captured for \`${skill}\` (${directives.length})`,
2997
3233
  "",
@@ -3000,6 +3236,16 @@ function buildSkillDistillInstruction(role, skill, directives, existing) {
3000
3236
  if (existing?.trim()) {
3001
3237
  sections.push("", "## Existing addendum (improve upon it)", "", existing.trim());
3002
3238
  }
3239
+ if (retired.length > 0) {
3240
+ sections.push(
3241
+ "",
3242
+ `## Retired \u2014 do not reinstate (${retired.length})`,
3243
+ "",
3244
+ "These were exercised repeatedly and kept correlating with failed tasks. If the existing addendum still states any of them, drop it.",
3245
+ "",
3246
+ ...retired.map((what) => `- ${what}`)
3247
+ );
3248
+ }
3003
3249
  sections.push(
3004
3250
  "",
3005
3251
  "## Output",
@@ -3025,7 +3271,7 @@ function loadSkillAffinity(role, projectRoot) {
3025
3271
  const normalizedRole = assertProjectAgentRole(role);
3026
3272
  try {
3027
3273
  const parsed = JSON.parse(
3028
- readFileSync6(projectSkillAffinityPath(normalizedRole, projectRoot), "utf8")
3274
+ readFileSync7(projectSkillAffinityPath(normalizedRole, projectRoot), "utf8")
3029
3275
  );
3030
3276
  const entries = {};
3031
3277
  for (const [skill, entry] of Object.entries(parsed.entries ?? {})) {
@@ -3074,9 +3320,11 @@ function recordSkillLoad(role, skills, projectRoot) {
3074
3320
  });
3075
3321
  }
3076
3322
  function recordSkillOutcome(role, skills, ok, projectRoot) {
3323
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3077
3324
  mutateAffinity(role, skills, projectRoot, (entry) => {
3078
3325
  if (ok) entry.succeeded += 1;
3079
3326
  else entry.failed += 1;
3327
+ entry.lastUsedAt = now;
3080
3328
  });
3081
3329
  }
3082
3330
  function recordSkillLearned(role, skill, projectRoot) {
@@ -3090,24 +3338,30 @@ function setSkillPinned(role, skill, pinned, projectRoot) {
3090
3338
  else delete entry.pinned;
3091
3339
  });
3092
3340
  }
3093
- function scoreSkillAffinity(entry) {
3094
- if (!entry) return 0;
3095
- if (entry.pinned) return Number.POSITIVE_INFINITY;
3096
- const outcomes = entry.succeeded + entry.failed;
3097
- const successRate = outcomes === 0 ? 0 : (entry.succeeded + 1) / (outcomes + 2);
3098
- return entry.learned * 2 + successRate * 3 + Math.min(entry.loaded, 10) * 0.1;
3341
+ var SKILL_EVIDENCE_HALF_LIFE_DAYS = 30;
3342
+ function scoreSkillAffinity(entry, now = Date.now()) {
3343
+ const record = entry ?? EMPTY_ENTRY;
3344
+ if (record.pinned) return Number.POSITIVE_INFINITY;
3345
+ const outcomes = record.succeeded + record.failed;
3346
+ const successRate = (record.succeeded + 1) / (outcomes + 2);
3347
+ const ageDays = record.lastUsedAt ? (now - Date.parse(record.lastUsedAt)) / 864e5 : Number.NaN;
3348
+ const recency = Number.isFinite(ageDays) ? 0.5 ** (Math.max(0, ageDays) / SKILL_EVIDENCE_HALF_LIFE_DAYS) : 1;
3349
+ const evidence = (successRate - 0.5) * 4 * recency;
3350
+ const exploration = 1 / Math.sqrt(1 + record.loaded);
3351
+ return Math.log1p(record.learned) * 2 + evidence + exploration;
3099
3352
  }
3100
3353
  function rankRoleSkills(role, candidates, projectRoot, limit = DEFAULT_EAGER_SKILL_LIMIT) {
3101
3354
  const unique = [...new Set(candidates.filter(isProjectSkillName))];
3102
3355
  if (unique.length <= limit) return unique;
3103
3356
  const affinity = loadSkillAffinity(role, projectRoot);
3104
3357
  const augmented = new Set(listProjectSkillAugmentations(role, projectRoot));
3358
+ const now = Date.now();
3105
3359
  return unique.map((skill, index) => ({
3106
3360
  skill,
3107
3361
  index,
3108
3362
  // An existing addendum is itself evidence the project developed this
3109
3363
  // skill, even when the affinity file was reset or hand-edited.
3110
- score: scoreSkillAffinity(affinity.entries[skill]) + (augmented.has(skill) ? 1 : 0)
3364
+ score: scoreSkillAffinity(affinity.entries[skill], now) + (augmented.has(skill) ? 1 : 0)
3111
3365
  })).sort((a, b) => b.score === a.score ? a.index - b.index : b.score - a.score).slice(0, limit).map((item) => item.skill);
3112
3366
  }
3113
3367
  function resolveRoleSkillCandidates(role, projectRoot) {
@@ -3177,6 +3431,13 @@ function isTextBlock(b) {
3177
3431
  // src/coordination/agents/project-agent-optimizer.ts
3178
3432
  var DEFAULT_MAX_TOKENS = 8e3;
3179
3433
  var DEFAULT_TIMEOUT_MS = 12e4;
3434
+ function describeDirective(entry) {
3435
+ const parts = [entry.what];
3436
+ if (entry.how) parts.push(`(anchors: ${entry.how.split("\n").join(", ")})`);
3437
+ const { applied, wins } = directiveTrials(entry);
3438
+ if (applied > 0) parts.push(`[applied ${applied}\xD7, ${wins} ok]`);
3439
+ return parts.join(" ");
3440
+ }
3180
3441
  function unwrapWholeDocumentFence(text) {
3181
3442
  const wholeDocFence = /^```(?:markdown|md)?[^\n]*\n([\s\S]*?)\n?```\s*$/i;
3182
3443
  const inner = wholeDocFence.exec(text.trim())?.[1];
@@ -3223,13 +3484,14 @@ async function optimizeProjectAgentLearning(role, projectRoot, options = {}) {
3223
3484
  for (const entry of entries) {
3224
3485
  if (!entry.skill) continue;
3225
3486
  const bucket = bySkill.get(entry.skill) ?? [];
3226
- bucket.push(entry.how ? `${entry.what} (anchors: ${entry.how.split("\n").join(", ")})` : entry.what);
3487
+ bucket.push(describeDirective(entry));
3227
3488
  bySkill.set(entry.skill, bucket);
3228
3489
  }
3229
3490
  const at = (/* @__PURE__ */ new Date()).toISOString();
3230
3491
  const refreshed = [];
3231
3492
  for (const [skill, directives] of bySkill) {
3232
- let body = renderSkillAugmentation(normalizedRole, skill, directives, at);
3493
+ const existing = loadProjectSkillAugmentation(normalizedRole, skill, projectRoot);
3494
+ let body = renderSkillAugmentation(normalizedRole, skill, directives, at, existing);
3233
3495
  if (options.llm) {
3234
3496
  try {
3235
3497
  const synthesized = await complete(
@@ -3239,7 +3501,8 @@ async function optimizeProjectAgentLearning(role, projectRoot, options = {}) {
3239
3501
  normalizedRole,
3240
3502
  skill,
3241
3503
  directives,
3242
- loadProjectSkillAugmentation(normalizedRole, skill, projectRoot)
3504
+ existing,
3505
+ retiredDirectivesToWarnAbout(normalizedRole, directives, projectRoot, skill)
3243
3506
  ),
3244
3507
  options
3245
3508
  );
@@ -3252,6 +3515,7 @@ async function optimizeProjectAgentLearning(role, projectRoot, options = {}) {
3252
3515
  }
3253
3516
  const { instruction } = buildConsolidationInstruction(normalizedRole, projectRoot);
3254
3517
  if (!options.llm) {
3518
+ recordProjectAgentOptimizePass(normalizedRole, projectRoot, at);
3255
3519
  return { ...base, status: "no-llm", skills: refreshed, instruction };
3256
3520
  }
3257
3521
  let content;
@@ -3266,6 +3530,7 @@ async function optimizeProjectAgentLearning(role, projectRoot, options = {}) {
3266
3530
  };
3267
3531
  }
3268
3532
  if (!content) {
3533
+ recordProjectAgentOptimizePass(normalizedRole, projectRoot);
3269
3534
  return { ...base, status: "empty-synthesis", skills: refreshed, model: options.llm.model };
3270
3535
  }
3271
3536
  const prune = options.prune ?? true;
@@ -3284,6 +3549,7 @@ async function optimizeProjectAgentLearning(role, projectRoot, options = {}) {
3284
3549
  error: error instanceof Error ? error.message : "failed to persist consolidation"
3285
3550
  };
3286
3551
  }
3552
+ recordProjectAgentOptimizePass(normalizedRole, projectRoot);
3287
3553
  return {
3288
3554
  ...base,
3289
3555
  status: "optimized",
@@ -3321,17 +3587,16 @@ function resolveAutoOptimizePolicy(overrides) {
3321
3587
  function evaluateAutoOptimize(role, projectRoot, policy, now = Date.now()) {
3322
3588
  if (!policy.enabled) return { eligible: false, reason: "disabled" };
3323
3589
  const normalizedRole = assertProjectAgentRole(role);
3324
- if (!loadProjectAgentLearningPolicy(normalizedRole, projectRoot).enabled) {
3590
+ const learningPolicy = loadProjectAgentLearningPolicy(normalizedRole, projectRoot);
3591
+ if (!learningPolicy.enabled) {
3325
3592
  return { eligible: false, reason: "learning-paused" };
3326
3593
  }
3327
3594
  const entries = readRawLearnedEntries(normalizedRole, projectRoot);
3328
3595
  if (entries.length < policy.minEntries) return { eligible: false, reason: "too-few-entries" };
3329
3596
  const meta = loadConsolidationMetadata(normalizedRole, projectRoot);
3330
- if (meta) {
3331
- const since = now - Date.parse(meta.consolidatedAt);
3332
- if (Number.isFinite(since) && since < policy.minIntervalMs) {
3333
- return { eligible: false, reason: "cooling-down" };
3334
- }
3597
+ const lastPassAt = [meta?.consolidatedAt, learningPolicy.lastOptimizeAt].map((stamp) => stamp ? Date.parse(stamp) : Number.NaN).filter((value) => Number.isFinite(value));
3598
+ if (lastPassAt.length > 0 && now - Math.max(...lastPassAt) < policy.minIntervalMs) {
3599
+ return { eligible: false, reason: "cooling-down" };
3335
3600
  }
3336
3601
  const bytes = entries.reduce(
3337
3602
  (sum, entry) => sum + Buffer.byteLength(`${entry.what}${entry.why}${entry.how}`, "utf8"),
@@ -3472,26 +3737,130 @@ var LearningOptimizationScheduler = class {
3472
3737
  }
3473
3738
  };
3474
3739
 
3740
+ // src/coordination/agents/project-agent-directive-outcome.ts
3741
+ import * as path9 from "node:path";
3742
+ var DIRECTIVE_QUARANTINE_MIN_APPLIED = 8;
3743
+ var DIRECTIVE_QUARANTINE_MAX_UTILITY = 0.3;
3744
+ var MIN_ANCHOR_LENGTH = 6;
3745
+ var DISTINCTIVE_TOKEN_LENGTH = 6;
3746
+ var MIN_DISTINCTIVE_TOKENS = 5;
3747
+ var DISTINCTIVE_COVERAGE = 0.6;
3748
+ function learnedPath2(role, projectRoot) {
3749
+ return path9.join(roleDir(role, projectRoot), "learned.md");
3750
+ }
3751
+ function directiveAnchors(entry) {
3752
+ return entry.how.split("\n").map((line) => line.replace(/`/g, "").trim().toLowerCase()).filter((anchor) => anchor.length >= MIN_ANCHOR_LENGTH);
3753
+ }
3754
+ function distinctiveTokens(text) {
3755
+ return [
3756
+ ...new Set(
3757
+ text.toLowerCase().replace(/[^\w\s./@-]/g, " ").split(/\s+/).map((token) => token.replace(/^[./@-]+/, "").replace(/[./@-]+$/, "")).filter((token) => token.length >= DISTINCTIVE_TOKEN_LENGTH)
3758
+ )
3759
+ ];
3760
+ }
3761
+ function directiveWasApplied(entry, report) {
3762
+ const haystack = report.toLowerCase();
3763
+ if (!haystack) return false;
3764
+ const anchors = directiveAnchors(entry);
3765
+ if (anchors.length > 0) {
3766
+ return anchors.some((anchor) => haystack.includes(anchor));
3767
+ }
3768
+ const tokens = distinctiveTokens(entry.what);
3769
+ if (tokens.length < MIN_DISTINCTIVE_TOKENS) return false;
3770
+ const hits = tokens.filter((token) => haystack.includes(token)).length;
3771
+ return hits >= MIN_DISTINCTIVE_TOKENS && hits / tokens.length >= DISTINCTIVE_COVERAGE;
3772
+ }
3773
+ function recordDirectiveOutcomes(role, report, succeeded, projectRoot) {
3774
+ const normalizedRole = assertProjectAgentRole(role);
3775
+ const empty = { role: normalizedRole, attributed: 0, quarantined: [] };
3776
+ if (!report.trim()) return empty;
3777
+ const entries = readRawLearnedEntries(normalizedRole, projectRoot);
3778
+ if (entries.length === 0) return empty;
3779
+ const updated = [];
3780
+ const retired = [];
3781
+ let attributed = 0;
3782
+ for (const entry of entries) {
3783
+ if (!directiveWasApplied(entry, report)) {
3784
+ updated.push(entry);
3785
+ continue;
3786
+ }
3787
+ attributed++;
3788
+ const trials = directiveTrials(entry);
3789
+ const next = {
3790
+ ...entry,
3791
+ applied: trials.applied + 1,
3792
+ wins: trials.wins + (succeeded ? 1 : 0)
3793
+ };
3794
+ if (directiveTrials(next).applied >= DIRECTIVE_QUARANTINE_MIN_APPLIED && directiveUtility(next) < DIRECTIVE_QUARANTINE_MAX_UTILITY) {
3795
+ retired.push(next);
3796
+ continue;
3797
+ }
3798
+ updated.push(next);
3799
+ }
3800
+ if (attributed === 0) return empty;
3801
+ const at = (/* @__PURE__ */ new Date()).toISOString();
3802
+ const newest = updated.map((entry) => entry.capturedAt).filter(Boolean).sort().pop() ?? at;
3803
+ writeTextAtomically(
3804
+ learnedPath2(normalizedRole, projectRoot),
3805
+ renderLearnedInstructions(normalizedRole, updated, newest)
3806
+ );
3807
+ if (retired.length > 0) {
3808
+ appendQuarantine(normalizedRole, retired, at, projectRoot);
3809
+ scrubRetiredFromInjectedDocuments(normalizedRole, retired, projectRoot);
3810
+ }
3811
+ return {
3812
+ role: normalizedRole,
3813
+ attributed,
3814
+ quarantined: retired.map((entry) => entry.what)
3815
+ };
3816
+ }
3817
+ function scrubRetiredFromInjectedDocuments(role, retired, projectRoot) {
3818
+ const texts = retired.map((entry) => entry.what);
3819
+ for (const skill of new Set(retired.map((entry) => entry.skill).filter(Boolean))) {
3820
+ try {
3821
+ const current = loadProjectSkillAugmentation(role, skill, projectRoot);
3822
+ if (!current) continue;
3823
+ const scrubbed = scrubRetiredLines(current, texts);
3824
+ if (scrubbed === current.trim()) continue;
3825
+ if (!hasDirectiveContent(scrubbed)) {
3826
+ clearProjectSkillAugmentation(role, skill, projectRoot);
3827
+ continue;
3828
+ }
3829
+ writeTextAtomically(projectSkillAugmentationPath(role, skill, projectRoot), `${scrubbed}
3830
+ `);
3831
+ } catch {
3832
+ }
3833
+ }
3834
+ try {
3835
+ const consolidated = loadProjectAgentConsolidated(role, projectRoot);
3836
+ if (!consolidated) return;
3837
+ const scrubbed = scrubRetiredLines(consolidated, texts);
3838
+ if (scrubbed === consolidated.trim() || !hasDirectiveContent(scrubbed)) return;
3839
+ writeTextAtomically(consolidatedDocumentPath(role, projectRoot), scrubbed);
3840
+ } catch {
3841
+ }
3842
+ }
3843
+
3475
3844
  // src/coordination/agents/project-agent-files.ts
3476
3845
  import {
3477
3846
  existsSync as existsSync3,
3478
3847
  mkdirSync as mkdirSync4,
3479
3848
  readdirSync as readdirSync3,
3480
- readFileSync as readFileSync7,
3849
+ readFileSync as readFileSync8,
3481
3850
  rmSync as rmSync4
3482
3851
  } from "node:fs";
3483
- import * as path8 from "node:path";
3852
+ import * as path10 from "node:path";
3484
3853
  function updateProjectAgentLearned(role, content, projectRoot, mode = "append") {
3485
3854
  const dir = roleDir(role, projectRoot);
3486
3855
  mkdirSync4(dir, { recursive: true });
3487
- const filePath = path8.join(dir, "learned.md");
3856
+ const filePath = path10.join(dir, "learned.md");
3488
3857
  if (mode === "replace") {
3489
3858
  writeTextAtomically(filePath, content);
3490
3859
  return filePath;
3491
3860
  }
3492
3861
  const existing = (() => {
3493
3862
  try {
3494
- return readFileSync7(filePath, "utf8");
3863
+ return readFileSync8(filePath, "utf8");
3495
3864
  } catch {
3496
3865
  return "";
3497
3866
  }
@@ -3519,7 +3888,7 @@ function updateProjectAgentLearned(role, content, projectRoot, mode = "append")
3519
3888
  function updateProjectAgentIdentity(role, content, projectRoot) {
3520
3889
  const dir = roleDir(role, projectRoot);
3521
3890
  mkdirSync4(dir, { recursive: true });
3522
- const filePath = path8.join(dir, "identity.md");
3891
+ const filePath = path10.join(dir, "identity.md");
3523
3892
  writeTextAtomically(filePath, content);
3524
3893
  return filePath;
3525
3894
  }
@@ -3527,7 +3896,7 @@ function updateProjectAgentConfig(role, config, projectRoot) {
3527
3896
  const validated = validateProjectAgentConfig(config);
3528
3897
  const dir = roleDir(role, projectRoot);
3529
3898
  mkdirSync4(dir, { recursive: true });
3530
- const filePath = path8.join(dir, "config.json");
3899
+ const filePath = path10.join(dir, "config.json");
3531
3900
  writeTextAtomically(filePath, `${JSON.stringify(validated, null, 2)}
3532
3901
  `);
3533
3902
  return filePath;
@@ -3535,7 +3904,7 @@ function updateProjectAgentConfig(role, config, projectRoot) {
3535
3904
  function updateProjectAgentKnowledge(role, manifest, projectRoot) {
3536
3905
  const dir = roleDir(role, projectRoot);
3537
3906
  mkdirSync4(dir, { recursive: true });
3538
- const filePath = path8.join(dir, "knowledge.json");
3907
+ const filePath = path10.join(dir, "knowledge.json");
3539
3908
  writeTextAtomically(filePath, `${JSON.stringify(manifest, null, 2)}
3540
3909
  `);
3541
3910
  return filePath;
@@ -3561,18 +3930,18 @@ function refreshProjectAgentIdentity(role, projectRoot) {
3561
3930
  const dir = roleDir(role, projectRoot);
3562
3931
  mkdirSync4(dir, { recursive: true });
3563
3932
  for (const file of ["learned.md", "identity.md", "consolidated.md", "consolidation.json"]) {
3564
- const fp = path8.join(dir, file);
3933
+ const fp = path10.join(dir, file);
3565
3934
  try {
3566
3935
  rmSync4(fp, { force: true });
3567
3936
  } catch {
3568
3937
  }
3569
3938
  }
3570
3939
  writeTextAtomically(
3571
- path8.join(dir, "learned.md"),
3940
+ path10.join(dir, "learned.md"),
3572
3941
  renderLearnedInstructions(role, [], (/* @__PURE__ */ new Date()).toISOString())
3573
3942
  );
3574
3943
  writeTextAtomically(
3575
- path8.join(dir, "identity.md"),
3944
+ path10.join(dir, "identity.md"),
3576
3945
  `# Project identity for ${role}
3577
3946
 
3578
3947
  <!-- Describe how this agent should behave in the context of this project. -->
@@ -3585,7 +3954,7 @@ function listProjectAgentRoles(projectRoot) {
3585
3954
  try {
3586
3955
  return readdirSync3(dir).filter((name) => {
3587
3956
  if (!isProjectAgentRoleName(name)) return false;
3588
- const sub = path8.join(dir, name);
3957
+ const sub = path10.join(dir, name);
3589
3958
  try {
3590
3959
  return [
3591
3960
  "learned.md",
@@ -3596,7 +3965,7 @@ function listProjectAgentRoles(projectRoot) {
3596
3965
  "profile.json",
3597
3966
  "consolidated.md",
3598
3967
  "consolidation.json"
3599
- ].some((file) => existsSync3(path8.join(sub, file)));
3968
+ ].some((file) => existsSync3(path10.join(sub, file)));
3600
3969
  } catch {
3601
3970
  return false;
3602
3971
  }
@@ -3663,25 +4032,25 @@ function createProjectAgentRoster(baseRoster, projectRoot) {
3663
4032
  });
3664
4033
  }
3665
4034
  function loadProjectAgentIdentity(role, projectRoot) {
3666
- const identityPath = path9.join(roleDir(role, projectRoot), "identity.md");
4035
+ const identityPath = path11.join(roleDir(role, projectRoot), "identity.md");
3667
4036
  try {
3668
- return readFileSync8(identityPath, "utf8").trim();
4037
+ return readFileSync9(identityPath, "utf8").trim();
3669
4038
  } catch {
3670
4039
  return "";
3671
4040
  }
3672
4041
  }
3673
4042
  function loadProjectAgentLearned(role, projectRoot) {
3674
- const learnedPath2 = path9.join(roleDir(role, projectRoot), "learned.md");
4043
+ const learnedPath3 = path11.join(roleDir(role, projectRoot), "learned.md");
3675
4044
  try {
3676
- return readFileSync8(learnedPath2, "utf8").trim();
4045
+ return readFileSync9(learnedPath3, "utf8").trim();
3677
4046
  } catch {
3678
4047
  return "";
3679
4048
  }
3680
4049
  }
3681
4050
  function loadRoleKnowledgeManifest(role, projectRoot) {
3682
- const projectPath = path9.join(roleDir(role, projectRoot), "knowledge.json");
4051
+ const projectPath = path11.join(roleDir(role, projectRoot), "knowledge.json");
3683
4052
  try {
3684
- const raw = readFileSync8(projectPath, "utf8");
4053
+ const raw = readFileSync9(projectPath, "utf8");
3685
4054
  return JSON.parse(raw);
3686
4055
  } catch {
3687
4056
  return BUILT_IN_KNOWLEDGE_MANIFESTS[role];
@@ -3822,7 +4191,7 @@ ${learnedContent}`);
3822
4191
  }
3823
4192
  }
3824
4193
  const effectiveProjectRoot = projectRoot || process.cwd();
3825
- const learnedFilePath = path9.join(
4194
+ const learnedFilePath = path11.join(
3826
4195
  effectiveProjectRoot,
3827
4196
  ".wrongstack",
3828
4197
  "agents",
@@ -3849,6 +4218,8 @@ The file below stores **learning data for this project's "${role}" agent** \u201
3849
4218
  - **Self-contained** \u2014 understandable without the surrounding session context.
3850
4219
  - **Front-load concrete anchors** \u2014 commands in backticks, package names like \`@wrongstack/core\`, file paths like \`packages/core/src/.../foo.ts\`. The structured-list renderer extracts these as the "how" for the entry.
3851
4220
 
4221
+ **Your directives are scored against real outcomes.** After every task the runtime checks which stored directives were actually exercised \u2014 it matches their anchors against the report \u2014 and folds that task's success or failure into each one's record. A directive that keeps correlating with success outlives newer arrivals and survives rewording; one that has been exercised repeatedly and kept correlating with failure is retired and stops being injected. Two consequences for how you write them: anchors are what make a directive *measurable*, not just runnable, so an anchorless directive can never earn a record; and a directive you are unsure about costs nothing to write, because the loop will find out.
4222
+
3852
4223
  **Tag the skill you are developing.** When a directive refines one of your skills, mark it: \`## LEARNED [skill: testing]\`. Tagged directives are distilled into that skill's project addendum, so the lesson arrives as part of the skill itself on every future run instead of as a loose fact. Untagged directives are routed automatically when the wording makes the target obvious, and stay role-level otherwise.
3853
4224
 
3854
4225
  **Bad** (session log \u2014 rejected at capture time):
@@ -3914,7 +4285,7 @@ ${additions}
3914
4285
  ${contextEnd}`;
3915
4286
  }
3916
4287
  function resolveCaptureKey(role, projectRoot) {
3917
- return `${path9.resolve(projectRoot ?? process.cwd())}\0${assertProjectAgentRole(role)}`;
4288
+ return `${path11.resolve(projectRoot ?? process.cwd())}\0${assertProjectAgentRole(role)}`;
3918
4289
  }
3919
4290
  function canCaptureNewLearned(role, _existingSize, isManual, projectRoot) {
3920
4291
  if (isManual) return void 0;
@@ -3932,13 +4303,13 @@ function canCaptureNewLearned(role, _existingSize, isManual, projectRoot) {
3932
4303
  }
3933
4304
  function getProjectAgentLearnStats(role, projectRoot) {
3934
4305
  const dir = roleDir(role, projectRoot);
3935
- const learnedPath_ = path9.join(dir, "learned.md");
3936
- const identityPath_ = path9.join(dir, "identity.md");
3937
- const configPath_ = path9.join(dir, "config.json");
3938
- const knowledgePath_ = path9.join(dir, "knowledge.json");
4306
+ const learnedPath_ = path11.join(dir, "learned.md");
4307
+ const identityPath_ = path11.join(dir, "identity.md");
4308
+ const configPath_ = path11.join(dir, "config.json");
4309
+ const knowledgePath_ = path11.join(dir, "knowledge.json");
3939
4310
  let learnedText = "";
3940
4311
  try {
3941
- learnedText = readFileSync8(learnedPath_, "utf8");
4312
+ learnedText = readFileSync9(learnedPath_, "utf8");
3942
4313
  } catch {
3943
4314
  }
3944
4315
  const entries = parseStructuredLearnedEntries(role, projectRoot);
@@ -3952,12 +4323,18 @@ function getProjectAgentLearnStats(role, projectRoot) {
3952
4323
  const lastTs = runtimeLastTs ?? (Number.isFinite(persistedLastTs) ? persistedLastTs : null);
3953
4324
  const cooldownRemaining = lastTs ? Math.max(0, CAPTURE_COOLDOWN_MS - (Date.now() - lastTs)) : 0;
3954
4325
  const freq = window.count;
4326
+ const trials = entries.map((entry) => directiveTrials(entry));
4327
+ const totalApplied = trials.reduce((sum, trial) => sum + trial.applied, 0);
4328
+ const totalWins = trials.reduce((sum, trial) => sum + trial.wins, 0);
3955
4329
  return {
3956
4330
  role,
3957
4331
  exists,
3958
4332
  entryCount: entries.length,
3959
4333
  skills: skillAugmentations,
3960
4334
  skilledEntryCount: entries.filter((entry) => entry.skill).length,
4335
+ appliedEntryCount: trials.filter((trial) => trial.applied > 0).length,
4336
+ deadEntryCount: trials.filter((trial) => trial.applied === 0).length,
4337
+ directiveHitRate: totalApplied > 0 ? totalWins / totalApplied : null,
3961
4338
  totalBytes: Buffer.byteLength(learnedText, "utf8"),
3962
4339
  lastCapture: lastTs ? new Date(lastTs).toISOString() : null,
3963
4340
  lastCaptureTimestamp: lastTs,
@@ -4130,7 +4507,7 @@ function captureLearnedFromAgentOutputDetailed(output, role, projectRoot, isManu
4130
4507
  normalizedRole
4131
4508
  );
4132
4509
  const newContent = renderLearnedInstructions(normalizedRole, budget.kept, nowIso);
4133
- writeTextAtomically(path9.join(roleDir(normalizedRole, projectRoot), "learned.md"), newContent);
4510
+ writeTextAtomically(path11.join(roleDir(normalizedRole, projectRoot), "learned.md"), newContent);
4134
4511
  recordCaptureAttempt(key, now.getTime());
4135
4512
  for (const skill of new Set(routedSkills)) {
4136
4513
  try {
@@ -4138,16 +4515,14 @@ function captureLearnedFromAgentOutputDetailed(output, role, projectRoot, isManu
4138
4515
  } catch {
4139
4516
  }
4140
4517
  }
4141
- const nextPolicy = {
4142
- ...policy,
4143
- lifetimeCaptureCount: policy.lifetimeCaptureCount + captured,
4144
- lastCaptureAt: now.toISOString(),
4145
- lastCaptureSource: isManual ? "manual" : "automatic"
4146
- };
4147
- writeTextAtomically(
4148
- learningPolicyPath(normalizedRole, projectRoot),
4149
- `${JSON.stringify(nextPolicy, null, 2)}
4150
- `
4518
+ updateProjectAgentLearningPolicy(
4519
+ normalizedRole,
4520
+ {
4521
+ lifetimeCaptureCount: policy.lifetimeCaptureCount + captured,
4522
+ lastCaptureAt: now.toISOString(),
4523
+ lastCaptureSource: isManual ? "manual" : "automatic"
4524
+ },
4525
+ projectRoot
4151
4526
  );
4152
4527
  return {
4153
4528
  role: normalizedRole,
@@ -4243,38 +4618,38 @@ function loadTechVersionPolicy() {
4243
4618
  return techVersionPolicyBody;
4244
4619
  }
4245
4620
  function policyCandidateDirs() {
4246
- const here = path10.dirname(fileURLToPath(import.meta.url));
4621
+ const here = path12.dirname(fileURLToPath(import.meta.url));
4247
4622
  const profileInstructions = resolveWstackPaths({ projectRoot: process.cwd() }).globalInstructions;
4248
4623
  return [
4249
- path10.resolve(here, "../../../../instructions/agents"),
4250
- path10.resolve(here, "../../../instructions/agents"),
4251
- path10.resolve(here, "../../instructions/agents"),
4252
- path10.resolve(here, "../instructions/agents"),
4253
- path10.resolve(here, "instructions/agents"),
4254
- path10.join(profileInstructions, "agents"),
4624
+ path12.resolve(here, "../../../../instructions/agents"),
4625
+ path12.resolve(here, "../../../instructions/agents"),
4626
+ path12.resolve(here, "../../instructions/agents"),
4627
+ path12.resolve(here, "../instructions/agents"),
4628
+ path12.resolve(here, "instructions/agents"),
4629
+ path12.join(profileInstructions, "agents"),
4255
4630
  profileInstructions
4256
4631
  ].filter((dir, index, all) => all.indexOf(dir) === index);
4257
4632
  }
4258
4633
  function policyBody() {
4259
- const here = path10.dirname(fileURLToPath(import.meta.url));
4634
+ const here = path12.dirname(fileURLToPath(import.meta.url));
4260
4635
  const roots = [
4261
- path10.resolve(here, "_policy/tech-version.md"),
4262
- path10.resolve(here, "../_policy/tech-version.md"),
4263
- path10.resolve(here, "../../../instructions/agents/_policy/tech-version.md"),
4264
- path10.resolve(here, "../../instructions/agents/_policy/tech-version.md"),
4265
- path10.resolve(here, "../instructions/agents/_policy/tech-version.md"),
4266
- path10.resolve(here, "../../../../instructions/agents/_policy/tech-version.md"),
4267
- path10.resolve(here, "instructions/agents/_policy/tech-version.md")
4636
+ path12.resolve(here, "_policy/tech-version.md"),
4637
+ path12.resolve(here, "../_policy/tech-version.md"),
4638
+ path12.resolve(here, "../../../instructions/agents/_policy/tech-version.md"),
4639
+ path12.resolve(here, "../../instructions/agents/_policy/tech-version.md"),
4640
+ path12.resolve(here, "../instructions/agents/_policy/tech-version.md"),
4641
+ path12.resolve(here, "../../../../instructions/agents/_policy/tech-version.md"),
4642
+ path12.resolve(here, "instructions/agents/_policy/tech-version.md")
4268
4643
  ];
4269
4644
  for (const file of roots) {
4270
4645
  try {
4271
- return readFileSync9(file, "utf8").trim();
4646
+ return readFileSync10(file, "utf8").trim();
4272
4647
  } catch {
4273
4648
  }
4274
4649
  }
4275
4650
  for (const dir of policyCandidateDirs()) {
4276
4651
  try {
4277
- return readFileSync9(path10.join(dir, "_policy", "tech-version.md"), "utf8").trim();
4652
+ return readFileSync10(path12.join(dir, "_policy", "tech-version.md"), "utf8").trim();
4278
4653
  } catch {
4279
4654
  }
4280
4655
  }
@@ -4293,7 +4668,7 @@ function agentPrompt(id) {
4293
4668
  let resolved = "";
4294
4669
  for (const dir of agentPromptDirCandidates(envDir)) {
4295
4670
  try {
4296
- resolved = readFileSync9(path10.join(dir, fileName), "utf8").trim();
4671
+ resolved = readFileSync10(path12.join(dir, fileName), "utf8").trim();
4297
4672
  break;
4298
4673
  } catch {
4299
4674
  }
@@ -4319,16 +4694,16 @@ function agentPromptDirCandidates(envDir) {
4319
4694
  const candKey = `${envDir}\0${profileInstructions}`;
4320
4695
  const cached = candidateCache.get(candKey);
4321
4696
  if (cached !== void 0) return cached;
4322
- const here = path10.dirname(fileURLToPath(import.meta.url));
4697
+ const here = path12.dirname(fileURLToPath(import.meta.url));
4323
4698
  const explicitDir = envDir || void 0;
4324
4699
  const candidates = [
4325
- ...explicitDir ? [path10.resolve(explicitDir)] : [],
4326
- path10.join(profileInstructions, "agents"),
4327
- path10.resolve(here, "../../../../instructions/agents"),
4328
- path10.resolve(here, "../../../instructions/agents"),
4329
- path10.resolve(here, "../../instructions/agents"),
4330
- path10.resolve(here, "../instructions/agents"),
4331
- path10.resolve(here, "instructions/agents")
4700
+ ...explicitDir ? [path12.resolve(explicitDir)] : [],
4701
+ path12.join(profileInstructions, "agents"),
4702
+ path12.resolve(here, "../../../../instructions/agents"),
4703
+ path12.resolve(here, "../../../instructions/agents"),
4704
+ path12.resolve(here, "../../instructions/agents"),
4705
+ path12.resolve(here, "../instructions/agents"),
4706
+ path12.resolve(here, "instructions/agents")
4332
4707
  ];
4333
4708
  const ordered = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
4334
4709
  candidateCache.set(candKey, ordered);
@@ -4385,8 +4760,16 @@ var TOOLS = {
4385
4760
  "test",
4386
4761
  "mailbox"
4387
4762
  ],
4388
- /** Version control. */
4389
- vcs: ["read", "grep", "glob", "git", "diff"],
4763
+ /**
4764
+ * Version control.
4765
+ *
4766
+ * `mailbox` is in every preset on purpose: a subagent that hits a wall must
4767
+ * be able to say so. This was the one preset without it, which left the `git`
4768
+ * and `release` roles able to fail but not to ask — the two roles whose work
4769
+ * most often needs a decision from the leader (force-push, tag collision,
4770
+ * dirty tree) and least often has a safe default.
4771
+ */
4772
+ vcs: ["read", "grep", "glob", "git", "diff", "mailbox"],
4390
4773
  /** Dependency management + CVE audit. */
4391
4774
  deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
4392
4775
  /** Documentation authoring. */
@@ -7817,26 +8200,26 @@ var BrainMonitor = class {
7817
8200
  trackFileChurn(toolName, ok, input) {
7818
8201
  if (!this.signals.fileChurn) return;
7819
8202
  if (!ok || !this.fileEditTools.has(toolName.toLowerCase())) return;
7820
- const path38 = editedPath(input);
7821
- if (!path38) return;
8203
+ const path40 = editedPath(input);
8204
+ if (!path40) return;
7822
8205
  const now = Date.now();
7823
- const stamps = (this.editTimestamps.get(path38) ?? []).filter(
8206
+ const stamps = (this.editTimestamps.get(path40) ?? []).filter(
7824
8207
  (t) => now - t <= this.fileChurnWindowMs
7825
8208
  );
7826
8209
  stamps.push(now);
7827
8210
  if (stamps.length >= this.fileChurnThreshold) {
7828
- this.editTimestamps.delete(path38);
8211
+ this.editTimestamps.delete(path40);
7829
8212
  void this.engage("file_churn", {
7830
- question: `The file "${path38}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
8213
+ question: `The file "${path40}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
7831
8214
  context: [
7832
- `File: ${path38}`,
8215
+ `File: ${path40}`,
7833
8216
  `Edits in window: ${stamps.length}`,
7834
8217
  `Window: ${Math.round(this.fileChurnWindowMs / 1e3)}s`
7835
8218
  ].join("\n")
7836
8219
  });
7837
8220
  return;
7838
8221
  }
7839
- this.editTimestamps.set(path38, stamps);
8222
+ this.editTimestamps.set(path40, stamps);
7840
8223
  }
7841
8224
  async engage(kind, input) {
7842
8225
  const last = this.lastEngagedAt.get(kind) ?? 0;
@@ -9416,7 +9799,7 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
9416
9799
 
9417
9800
  // src/coordination/commit-safety.ts
9418
9801
  import { spawn } from "node:child_process";
9419
- import * as path12 from "node:path";
9802
+ import * as path14 from "node:path";
9420
9803
 
9421
9804
  // src/utils/child-env.ts
9422
9805
  var ALLOWED_KEYS = /* @__PURE__ */ new Set([
@@ -9556,7 +9939,7 @@ function buildChildEnv(optsOrSessionId) {
9556
9939
 
9557
9940
  // src/coordination/file-author-tracker.ts
9558
9941
  import * as fs2 from "node:fs/promises";
9559
- import * as path11 from "node:path";
9942
+ import * as path13 from "node:path";
9560
9943
 
9561
9944
  // src/utils/atomic-write.ts
9562
9945
  import {
@@ -9579,7 +9962,7 @@ var withFileLock = primitives.withFileLock;
9579
9962
  var DEFAULT_MAX_ENTRIES = 5e3;
9580
9963
  var LOG_FILENAME = "file-authors.json";
9581
9964
  function logPath(storageDir) {
9582
- return path11.join(storageDir, LOG_FILENAME);
9965
+ return path13.join(storageDir, LOG_FILENAME);
9583
9966
  }
9584
9967
  async function loadLog(storageDir, projectRoot) {
9585
9968
  try {
@@ -9647,7 +10030,7 @@ async function assessCommitSafety(opts) {
9647
10030
  const foreignFiles = [];
9648
10031
  const unverifiedFiles = [];
9649
10032
  for (const rel of dirty) {
9650
- const abs = path12.resolve(topLevel, rel);
10033
+ const abs = path14.resolve(topLevel, rel);
9651
10034
  const entry = authors.get(normPath(abs)) ?? authors.get(normPath(rel));
9652
10035
  if (!entry) {
9653
10036
  unverifiedFiles.push(rel);
@@ -9720,8 +10103,8 @@ async function loadAuthorMap(projectRoot, storageDirOverride) {
9720
10103
  const map = /* @__PURE__ */ new Map();
9721
10104
  for (const e of log.entries) {
9722
10105
  map.set(normPath(e.filePath), e);
9723
- if (!path12.isAbsolute(e.filePath)) {
9724
- map.set(normPath(path12.resolve(projectRoot, e.filePath)), e);
10106
+ if (!path14.isAbsolute(e.filePath)) {
10107
+ map.set(normPath(path14.resolve(projectRoot, e.filePath)), e);
9725
10108
  }
9726
10109
  }
9727
10110
  return map;
@@ -9793,7 +10176,7 @@ function runGit(args, cwd, signal) {
9793
10176
  // src/coordination/delegate-tool.ts
9794
10177
  import { randomUUID as randomUUID5 } from "node:crypto";
9795
10178
  import * as fsp3 from "node:fs/promises";
9796
- import * as path13 from "node:path";
10179
+ import * as path15 from "node:path";
9797
10180
 
9798
10181
  // src/utils/safe-json.ts
9799
10182
  function safeParse(input, maxBytes = 5e6) {
@@ -9851,6 +10234,14 @@ var SHADOW_AGENT = {
9851
10234
  };
9852
10235
  var CRITIC_AGENT = defineAgent("critic", "Critic");
9853
10236
  var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
10237
+ function withDispatchMetadata(definition) {
10238
+ const summary = definition.capability?.summary?.trim();
10239
+ if (!summary) return definition.config;
10240
+ return {
10241
+ ...definition.config,
10242
+ dispatch: { summary, keywords: [...definition.capability.keywords ?? []] }
10243
+ };
10244
+ }
9854
10245
  var FLEET_ROSTER = {
9855
10246
  "audit-log": AUDIT_LOG_AGENT,
9856
10247
  "bug-hunter": BUG_HUNTER_AGENT,
@@ -9860,7 +10251,7 @@ var FLEET_ROSTER = {
9860
10251
  generic: GENERIC_AGENT,
9861
10252
  "shadow-agent": SHADOW_AGENT,
9862
10253
  ...Object.fromEntries(
9863
- ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
10254
+ ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
9864
10255
  )
9865
10256
  };
9866
10257
  var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -10456,13 +10847,13 @@ async function readSubagentPartial(opts, subagentId) {
10456
10847
  if (!opts.sessionsRoot) return void 0;
10457
10848
  const candidates = [];
10458
10849
  if (opts.directorRunId) {
10459
- candidates.push(path13.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
10850
+ candidates.push(path15.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
10460
10851
  } else {
10461
10852
  try {
10462
10853
  const entries = await fsp3.readdir(opts.sessionsRoot, { withFileTypes: true });
10463
10854
  for (const entry of entries) {
10464
10855
  if (entry.isDirectory()) {
10465
- candidates.push(path13.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
10856
+ candidates.push(path15.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
10466
10857
  }
10467
10858
  }
10468
10859
  } catch {
@@ -11011,7 +11402,7 @@ var DirectorStateCheckpoint = class {
11011
11402
 
11012
11403
  // src/coordination/checkpoint-wiring.ts
11013
11404
  import * as fsp5 from "node:fs/promises";
11014
- import * as path14 from "node:path";
11405
+ import * as path16 from "node:path";
11015
11406
  async function appendSessionEvent(host, event) {
11016
11407
  if (!host.sessionWriter) return;
11017
11408
  try {
@@ -11059,7 +11450,7 @@ async function writeManifest(host) {
11059
11450
  }),
11060
11451
  usage: host.usage.snapshot()
11061
11452
  };
11062
- await fsp5.mkdir(path14.dirname(host.manifestPath), { recursive: true });
11453
+ await fsp5.mkdir(path16.dirname(host.manifestPath), { recursive: true });
11063
11454
  await atomicWrite(host.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
11064
11455
  return host.manifestPath;
11065
11456
  }
@@ -11716,9 +12107,6 @@ import {
11716
12107
  updateTaskAssignment
11717
12108
  } from "@wrongstack/kanban";
11718
12109
 
11719
- // src/coordination/director-kanban-queue-helpers.ts
11720
- import { describeKanbanBoundary } from "@wrongstack/kanban";
11721
-
11722
12110
  // src/coordination/director-input-helpers.ts
11723
12111
  import { randomUUID as randomUUID7 } from "node:crypto";
11724
12112
  function stringArray2(value) {
@@ -11739,6 +12127,7 @@ function instantiateRosterConfig2(role, base) {
11739
12127
  }
11740
12128
 
11741
12129
  // src/coordination/director-kanban-queue-helpers.ts
12130
+ import { describeKanbanBoundary } from "@wrongstack/kanban";
11742
12131
  function normalizeKanbanQueueInput(input) {
11743
12132
  const raw = input ?? {};
11744
12133
  return {
@@ -11907,8 +12296,8 @@ function ensureKanbanTool(tools) {
11907
12296
  }
11908
12297
 
11909
12298
  // src/utils/instruction-file.ts
11910
- import { readFileSync as readFileSync10, statSync as statSync3 } from "node:fs";
11911
- import * as path15 from "node:path";
12299
+ import { readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
12300
+ import * as path17 from "node:path";
11912
12301
  import { fileURLToPath as fileURLToPath2 } from "node:url";
11913
12302
  var textCache = /* @__PURE__ */ new Map();
11914
12303
  var rootCandidates;
@@ -11918,7 +12307,7 @@ function readBundledInstructionText(relativePath) {
11918
12307
  let resolved = "";
11919
12308
  for (const root of instructionRootCandidates()) {
11920
12309
  try {
11921
- resolved = readFileSync10(path15.join(root, relativePath), "utf8").trimEnd();
12310
+ resolved = readFileSync11(path17.join(root, relativePath), "utf8").trimEnd();
11922
12311
  break;
11923
12312
  } catch {
11924
12313
  }
@@ -11934,11 +12323,11 @@ function renderInstructionTemplate(template, values) {
11934
12323
  }
11935
12324
  function instructionRootCandidates() {
11936
12325
  if (rootCandidates !== void 0) return rootCandidates;
11937
- const here = path15.dirname(fileURLToPath2(import.meta.url));
12326
+ const here = path17.dirname(fileURLToPath2(import.meta.url));
11938
12327
  const candidates = [
11939
- path15.resolve(here, "../../instructions"),
11940
- path15.resolve(here, "../instructions"),
11941
- path15.resolve(here, "instructions")
12328
+ path17.resolve(here, "../../instructions"),
12329
+ path17.resolve(here, "../instructions"),
12330
+ path17.resolve(here, "instructions")
11942
12331
  ];
11943
12332
  rootCandidates = candidates.sort((a, b) => Number(!isDirectory2(a)) - Number(!isDirectory2(b)));
11944
12333
  return rootCandidates;
@@ -12073,378 +12462,336 @@ function makeLLMClassifier(complete2) {
12073
12462
  };
12074
12463
  }
12075
12464
 
12076
- // src/coordination/director-quality-gate-tool.ts
12465
+ // src/coordination/director-basic-tools.ts
12077
12466
  import { randomUUID as randomUUID8 } from "node:crypto";
12078
- function makeQualityGateTool(director, roster) {
12467
+ function makeAssignTool(director) {
12468
+ const inputSchema = {
12469
+ type: "object",
12470
+ properties: {
12471
+ subagentId: { type: "string", minLength: 1, description: "Target subagent id. Required." },
12472
+ description: {
12473
+ type: "string",
12474
+ minLength: 1,
12475
+ description: "The task in natural language \u2014 what you want this subagent to do."
12476
+ },
12477
+ maxToolCalls: {
12478
+ type: "number",
12479
+ minimum: 1,
12480
+ description: "Optional per-task tool-call budget override."
12481
+ },
12482
+ timeoutMs: { type: "number", minimum: 1, description: "Optional per-task timeout in ms." }
12483
+ },
12484
+ required: ["subagentId", "description"]
12485
+ };
12079
12486
  return {
12080
- name: "quality_gate",
12081
- description: "Run a first-class implementation quality gate. It can await implementer task ids, spawn independent verifier/reviewer agents, summarize their verdicts, and optionally send must-fix feedback back to an implementer until the gate passes or the repair-attempt limit is reached.",
12082
- usageHint: "Use after code-changing work. Provide implementerTaskIds when available. Add repairSubagentId to iterate fixes automatically. Verdict only passes when every enabled reviewer/verifier explicitly passes.",
12487
+ name: "assign_task",
12488
+ description: "Queue a task on a previously spawned subagent. NON-BLOCKING: returns a `taskId` IMMEDIATELY \u2014 the subagent processes the task on its next iteration with its own LLM budget. The `taskId` is the durable handle for retrieving the result later via `await_tasks`, `roll_up`, or `ask_result`. Many `assign_task` calls can be in flight in parallel against the same or different subagents. This is the primary tool for fan-out work; do NOT use `delegate` to spawn multiple investigations sequentially.",
12083
12489
  permission: "auto",
12084
12490
  mutating: false,
12085
12491
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
12492
+ inputSchema,
12493
+ async execute(input) {
12494
+ const i = input;
12495
+ const task = {
12496
+ id: randomUUID8(),
12497
+ description: i.description,
12498
+ subagentId: i.subagentId,
12499
+ maxToolCalls: i.maxToolCalls,
12500
+ timeoutMs: i.timeoutMs
12501
+ };
12502
+ const taskId = await director.assign(task);
12503
+ return { taskId, subagentId: i.subagentId };
12504
+ }
12505
+ };
12506
+ }
12507
+ function makeAwaitTasksTool(director) {
12508
+ return {
12509
+ name: "await_tasks",
12510
+ description: 'Block until one or more `taskId`s complete, then return their results. The subagents keep running in the background \u2014 only the leader\'s iteration pauses, which is the point: this is the correct tool to retrieve a result you started earlier with `assign_task`. mode:"all" (default) blocks until EVERY named task completes. mode:"any" returns as soon as AT LEAST ONE completes \u2014 use it for independent tasks so you can handle each finisher immediately (reassign work, spawn helpers) instead of idling on the slowest; call again with the returned `pending` ids to pick up the next finisher. The pattern "fan out via assign_task, then await_tasks({mode:\'any\'})" is the async replacement for serial `delegate` calls.',
12511
+ permission: "auto",
12512
+ mutating: false,
12513
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12086
12514
  inputSchema: {
12087
12515
  type: "object",
12088
12516
  properties: {
12089
- task: {
12090
- type: "string",
12091
- description: "Original implementation task or acceptance goal being gated."
12092
- },
12093
- implementerTaskIds: {
12517
+ taskIds: {
12094
12518
  type: "array",
12095
12519
  items: { type: "string" },
12096
- description: "Optional completed or in-flight implementer task ids to await and include as implementation evidence."
12520
+ description: "One or more task ids returned by `assign_task`."
12097
12521
  },
12098
- repairSubagentId: {
12522
+ mode: {
12099
12523
  type: "string",
12100
- description: "Optional implementer subagent id. When set and the gate fails, quality_gate assigns a repair task with reviewer/verifier feedback and reruns the gate."
12524
+ enum: ["all", "any"],
12525
+ description: '"all" (default): block until every task completes. "any": return on the first completion with the rest listed as pending.'
12101
12526
  },
12102
- maxRepairAttempts: {
12527
+ timeoutMs: {
12103
12528
  type: "number",
12104
- minimum: 0,
12105
- maximum: 5,
12106
- description: "Maximum automatic repair iterations. Default: 2 when repairSubagentId is set, otherwise 0."
12107
- },
12108
- targets: {
12109
- type: "array",
12110
- items: { type: "string" },
12111
- description: "Files, packages, or paths that reviewer/verifier should focus on."
12112
- },
12113
- commands: {
12114
- type: "array",
12115
- items: { type: "string" },
12116
- description: 'Verification commands that should pass, e.g. ["pnpm --filter @wrongstack/core typecheck"].'
12117
- },
12118
- expected: {
12119
- type: "string",
12120
- description: "Expected behavior or acceptance criteria."
12121
- },
12122
- evidence: {
12123
- type: "string",
12124
- description: "Known implementation notes, diff summary, or commands already run."
12125
- },
12126
- reviewer: {
12127
- type: "boolean",
12128
- description: "Whether to run the reviewer lane. Default true."
12129
- },
12130
- verifier: {
12131
- type: "boolean",
12132
- description: "Whether to run the verifier lane. Default true."
12529
+ minimum: 1,
12530
+ description: 'mode:"any" only \u2014 return {timedOut:true, completed:[]} if nothing completes within this window instead of blocking.'
12531
+ }
12532
+ },
12533
+ required: ["taskIds"]
12534
+ },
12535
+ async execute(input) {
12536
+ const i = input;
12537
+ if (i.mode === "any") {
12538
+ const r = await director.awaitTasksAny(
12539
+ i.taskIds,
12540
+ i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : void 0
12541
+ );
12542
+ return {
12543
+ mode: "any",
12544
+ completed: r.completed,
12545
+ pending: r.pending,
12546
+ ...r.timedOut ? { timedOut: true } : {},
12547
+ ...r.pending.length > 0 ? {
12548
+ hint: 'Handle the completed results now. Re-call await_tasks with the pending ids (mode:"any") for the next finisher \u2014 or assign new work to the now-idle subagent first.'
12549
+ } : {}
12550
+ };
12551
+ }
12552
+ const results = await director.awaitTasks(i.taskIds);
12553
+ return { results };
12554
+ }
12555
+ };
12556
+ }
12557
+ function makeAskTool(director) {
12558
+ return {
12559
+ name: "ask_subagent",
12560
+ description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
12561
+ permission: "auto",
12562
+ mutating: false,
12563
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12564
+ inputSchema: {
12565
+ type: "object",
12566
+ properties: {
12567
+ subagentId: {
12568
+ type: "string",
12569
+ minLength: 1,
12570
+ description: "Subagent to ask. Must be a previously spawned id."
12133
12571
  },
12572
+ question: { type: "string", minLength: 1, description: "The question or instruction." },
12134
12573
  timeoutMs: {
12135
12574
  type: "number",
12136
12575
  minimum: 1,
12137
- description: "Optional per reviewer/verifier/repair task timeout."
12138
- },
12139
- reviewerWorktree: {
12140
- anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
12141
- description: "Reviewer worktree override. Default off because reviewer is read-only."
12142
- },
12143
- verifierWorktree: {
12144
- anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
12145
- description: "Verifier worktree override. Default auto so test artifacts stay isolated when fleet policy wants it."
12576
+ description: "Optional timeout in ms (default 30s)."
12146
12577
  }
12147
- }
12578
+ },
12579
+ required: ["subagentId", "question"]
12148
12580
  },
12149
12581
  async execute(input) {
12150
- const i = normalizeQualityGateInput(input);
12151
- const runReviewer = i.reviewer !== false;
12152
- const runVerifier = i.verifier !== false;
12153
- if (!runReviewer && !runVerifier) {
12582
+ const i = input;
12583
+ try {
12584
+ const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
12585
+ const stored = director.largeAnswerStore.storeAnswer(answer);
12586
+ if (stored.inline) {
12587
+ return { ok: true, answer: stored.summary };
12588
+ }
12154
12589
  return {
12155
- verdict: "inconclusive",
12156
- passed: false,
12157
- error: "quality_gate requires reviewer, verifier, or both."
12590
+ ok: true,
12591
+ answer: stored.summary,
12592
+ _answerKey: stored.key,
12593
+ _hint: "Response was large and stored. Use ask_result with the key to retrieve it."
12158
12594
  };
12595
+ } catch (err) {
12596
+ return { ok: false, error: toErrorMessage(err) };
12159
12597
  }
12160
- const implementerResults = i.implementerTaskIds && i.implementerTaskIds.length > 0 ? await director.awaitTasks(i.implementerTaskIds) : [];
12161
- const maxRepairAttempts = clampRepairAttempts(
12162
- i.maxRepairAttempts ?? (i.repairSubagentId ? 2 : 0)
12163
- );
12164
- const repairResults = [];
12165
- const attempts = [];
12166
- for (let attempt = 1; ; attempt++) {
12167
- const gateTaskIds = [];
12168
- const taskRoleById = /* @__PURE__ */ new Map();
12169
- if (runVerifier) {
12170
- const subagentId = await director.spawn(
12171
- makeQualityGateSubagentConfig("verifier", roster, i.verifierWorktree ?? "auto")
12172
- );
12173
- const taskId = await director.assign({
12174
- id: randomUUID8(),
12175
- subagentId,
12176
- description: buildVerifierTask(i, {
12177
- attempt,
12178
- implementerResults,
12179
- repairResults,
12180
- priorAttempts: attempts
12181
- }),
12182
- timeoutMs: i.timeoutMs
12183
- });
12184
- gateTaskIds.push(taskId);
12185
- taskRoleById.set(taskId, "verifier");
12186
- }
12187
- if (runReviewer) {
12188
- const subagentId = await director.spawn(
12189
- makeQualityGateSubagentConfig("reviewer", roster, i.reviewerWorktree ?? "off")
12190
- );
12191
- const taskId = await director.assign({
12192
- id: randomUUID8(),
12193
- subagentId,
12194
- description: buildReviewerTask(i, {
12195
- attempt,
12196
- implementerResults,
12197
- repairResults,
12198
- priorAttempts: attempts
12199
- }),
12200
- timeoutMs: i.timeoutMs
12201
- });
12202
- gateTaskIds.push(taskId);
12203
- taskRoleById.set(taskId, "reviewer");
12204
- }
12205
- const gateResults = await director.awaitTasks(gateTaskIds);
12206
- const reports = gateResults.map((r) => assessRoleResult(taskRoleById.get(r.taskId), r));
12207
- const assessment = assessQualityGate(reports);
12208
- attempts.push({ attempt, reports, ...assessment });
12209
- if (assessment.passed || !i.repairSubagentId || attempt > maxRepairAttempts) {
12210
- return {
12211
- verdict: assessment.verdict,
12212
- passed: assessment.passed,
12213
- attempts,
12214
- repairAttemptsUsed: repairResults.length,
12215
- implementerResults: implementerResults.map(summarizeTaskResult),
12216
- nextAction: assessment.passed ? "accept" : i.repairSubagentId && attempt > maxRepairAttempts ? "manual_intervention_or_raise_repair_limit" : "inspect_failures"
12217
- };
12218
- }
12219
- const repairTaskId = await director.assign({
12220
- id: randomUUID8(),
12221
- subagentId: i.repairSubagentId,
12222
- description: buildRepairTask(i, attempts[attempts.length - 1], attempt),
12223
- timeoutMs: i.timeoutMs
12224
- });
12225
- const [repairResult] = await director.awaitTasks([repairTaskId]);
12226
- if (repairResult) repairResults.push(repairResult);
12227
- if (repairResult?.status !== "success") {
12228
- return {
12229
- verdict: "fail",
12230
- passed: false,
12231
- attempts,
12232
- repairAttemptsUsed: repairResults.length,
12233
- repairResult: repairResult ? summarizeTaskResult(repairResult) : void 0,
12234
- implementerResults: implementerResults.map(summarizeTaskResult),
12235
- nextAction: "repair_failed"
12236
- };
12598
+ }
12599
+ };
12600
+ }
12601
+ function makeAskResultTool(director) {
12602
+ return {
12603
+ name: "ask_result",
12604
+ description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
12605
+ permission: "auto",
12606
+ mutating: false,
12607
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12608
+ inputSchema: {
12609
+ type: "object",
12610
+ properties: {
12611
+ key: {
12612
+ type: "string",
12613
+ minLength: 1,
12614
+ description: "The `_answerKey` returned by `ask_subagent` for a large response."
12237
12615
  }
12616
+ },
12617
+ required: ["key"]
12618
+ },
12619
+ async execute(input) {
12620
+ const i = input;
12621
+ const value = director.largeAnswerStore.retrieveAnswer(i.key);
12622
+ if (value === void 0) {
12623
+ return {
12624
+ ok: false,
12625
+ error: `No stored answer found for key "${i.key}" \u2014 it may have been cleared or the key is invalid.`
12626
+ };
12238
12627
  }
12628
+ return { ok: true, value };
12239
12629
  }
12240
12630
  };
12241
12631
  }
12242
- function normalizeQualityGateInput(input) {
12243
- const raw = input ?? {};
12632
+ function makeRollUpTool(director) {
12244
12633
  return {
12245
- task: typeof raw.task === "string" ? raw.task : void 0,
12246
- implementerTaskIds: stringArray2(raw.implementerTaskIds),
12247
- repairSubagentId: typeof raw.repairSubagentId === "string" && raw.repairSubagentId.trim() ? raw.repairSubagentId.trim() : void 0,
12248
- maxRepairAttempts: typeof raw.maxRepairAttempts === "number" ? raw.maxRepairAttempts : void 0,
12249
- targets: stringArray2(raw.targets),
12250
- commands: stringArray2(raw.commands),
12251
- expected: typeof raw.expected === "string" ? raw.expected : void 0,
12252
- evidence: typeof raw.evidence === "string" ? raw.evidence : void 0,
12253
- reviewer: typeof raw.reviewer === "boolean" ? raw.reviewer : void 0,
12254
- verifier: typeof raw.verifier === "boolean" ? raw.verifier : void 0,
12255
- timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : void 0,
12256
- reviewerWorktree: normalizeWorktreeOverride(raw.reviewerWorktree),
12257
- verifierWorktree: normalizeWorktreeOverride(raw.verifierWorktree)
12634
+ name: "roll_up",
12635
+ description: "Aggregate completed task results into a single formatted summary.",
12636
+ permission: "auto",
12637
+ mutating: false,
12638
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12639
+ inputSchema: {
12640
+ type: "object",
12641
+ properties: {
12642
+ taskIds: {
12643
+ type: "array",
12644
+ items: { type: "string" },
12645
+ description: "Completed task ids to aggregate."
12646
+ },
12647
+ style: {
12648
+ type: "string",
12649
+ enum: ["markdown", "json"],
12650
+ description: "Output flavor \u2014 markdown (default) or json."
12651
+ }
12652
+ },
12653
+ required: ["taskIds"]
12654
+ },
12655
+ async execute(input) {
12656
+ const i = input;
12657
+ const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
12658
+ return { summary, count: i.taskIds.length };
12659
+ }
12258
12660
  };
12259
12661
  }
12260
- function clampRepairAttempts(value) {
12261
- if (!Number.isFinite(value)) return 0;
12262
- return Math.max(0, Math.min(5, Math.floor(value)));
12263
- }
12264
- function makeQualityGateSubagentConfig(role, roster, worktree) {
12265
- const base = roster?.[role] ?? getAgentDefinition(role)?.config ?? { name: role, role };
12662
+ function makeTerminateTool(director) {
12266
12663
  return {
12267
- ...instantiateRosterConfig2(role, base),
12268
- worktree
12664
+ name: "terminate_subagent",
12665
+ description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
12666
+ permission: "auto",
12667
+ mutating: true,
12668
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
12669
+ inputSchema: {
12670
+ type: "object",
12671
+ properties: { subagentId: { type: "string", description: "Subagent to abort." } },
12672
+ required: ["subagentId"]
12673
+ },
12674
+ async execute(input) {
12675
+ const i = input;
12676
+ await director.terminate(i.subagentId);
12677
+ return { ok: true };
12678
+ }
12269
12679
  };
12270
12680
  }
12271
- function buildVerifierTask(input, state) {
12272
- return [
12273
- "Run the independent verification gate for this implementation.",
12274
- "Return Markdown with `## Verdict` and make the first verdict word exactly `pass`, `fail`, or `blocked`.",
12275
- "Do not edit code. Run the smallest meaningful command set and include exact failures.",
12276
- "",
12277
- `Gate attempt: ${state.attempt}`,
12278
- input.task ? `Original task:
12279
- ${input.task}` : void 0,
12280
- input.targets?.length ? `Targets:
12281
- ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
12282
- input.commands?.length ? `Required or suggested commands:
12283
- ${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
12284
- input.expected ? `Expected behavior:
12285
- ${input.expected}` : void 0,
12286
- input.evidence ? `Known evidence:
12287
- ${input.evidence}` : void 0,
12288
- taskResultsBlock("Implementer results", state.implementerResults),
12289
- taskResultsBlock("Repair results so far", state.repairResults),
12290
- priorAttemptsBlock(state.priorAttempts)
12291
- ].filter((part) => !!part).join("\n\n");
12292
- }
12293
- function buildReviewerTask(input, state) {
12294
- return [
12295
- "Run independent code review for this implementation.",
12296
- "Return Markdown with `## Verdict` and make the first verdict phrase exactly `approve`, `request changes`, or `needs verification`.",
12297
- "Do not edit code. Treat missing proof, vague tests, and uncertainty as blocking until verifier evidence exists.",
12298
- "",
12299
- `Gate attempt: ${state.attempt}`,
12300
- input.task ? `Original task:
12301
- ${input.task}` : void 0,
12302
- input.targets?.length ? `Targets:
12303
- ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
12304
- input.expected ? `Expected behavior:
12305
- ${input.expected}` : void 0,
12306
- input.evidence ? `Known evidence:
12307
- ${input.evidence}` : void 0,
12308
- taskResultsBlock("Implementer results", state.implementerResults),
12309
- taskResultsBlock("Repair results so far", state.repairResults),
12310
- priorAttemptsBlock(state.priorAttempts)
12311
- ].filter((part) => !!part).join("\n\n");
12312
- }
12313
- function buildRepairTask(input, attempt, attemptNumber) {
12314
- return [
12315
- `Repair the implementation after quality gate attempt ${attemptNumber} failed.`,
12316
- "Address every must-fix item. Run relevant checks before returning.",
12317
- "Do not claim done unless verifier/reviewer feedback is resolved.",
12318
- "",
12319
- input.task ? `Original task:
12320
- ${input.task}` : void 0,
12321
- input.targets?.length ? `Targets:
12322
- ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
12323
- input.commands?.length ? `Commands expected to pass:
12324
- ${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
12325
- input.expected ? `Expected behavior:
12326
- ${input.expected}` : void 0,
12327
- attempt.mustFix.length ? `Must fix:
12328
- ${attempt.mustFix.map((f) => `- ${f}`).join("\n")}` : void 0,
12329
- attempt.uncertaintyFlags.length ? `Uncertainty flags to resolve:
12330
- ${attempt.uncertaintyFlags.map((f) => `- ${f}`).join("\n")}` : void 0,
12331
- `Reviewer/verifier reports:
12332
- ${attempt.reports.map((r) => `### ${r.role} (${r.verdict})
12333
- ${r.summary}`).join("\n\n")}`
12334
- ].filter((part) => !!part).join("\n\n");
12335
- }
12336
- function taskResultsBlock(title, results) {
12337
- if (results.length === 0) return void 0;
12338
- return `${title}:
12339
- ${results.map((r) => `### ${r.subagentId}/${r.taskId}
12340
- ${summarizeTaskResult(r).summary}`).join("\n\n")}`;
12341
- }
12342
- function priorAttemptsBlock(attempts) {
12343
- if (attempts.length === 0) return void 0;
12344
- return `Prior quality gate attempts:
12345
- ${attempts.map(
12346
- (a) => `### Attempt ${a.attempt}
12347
- ${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : ""}`).join("\n")}`
12348
- ).join("\n\n")}`;
12349
- }
12350
- function summarizeTaskResult(result) {
12351
- const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
12352
- const error = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
12681
+ function makeTerminateAllTool(director) {
12353
12682
  return {
12354
- taskId: result.taskId,
12355
- subagentId: result.subagentId,
12356
- status: result.status,
12357
- summary: excerpt(text || error || "(no output)", 4e3),
12358
- error
12683
+ name: "terminate_all",
12684
+ description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
12685
+ permission: "auto",
12686
+ mutating: true,
12687
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
12688
+ inputSchema: { type: "object", properties: {}, required: [] },
12689
+ async execute() {
12690
+ await director.terminateAll();
12691
+ return {
12692
+ ok: true,
12693
+ message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.`
12694
+ };
12695
+ }
12359
12696
  };
12360
12697
  }
12361
- function assessRoleResult(role, result) {
12362
- const resolvedRole = role ?? (result.subagentId.includes("review") ? "reviewer" : "verifier");
12363
- const summary = summarizeTaskResult(result);
12364
- const text = summary.summary;
12365
- const uncertaintyFlags = extractSection(text, "Uncertainty Flags");
12366
- if (result.status !== "success") {
12367
- return {
12368
- role: resolvedRole,
12369
- subagentId: result.subagentId,
12370
- taskId: result.taskId,
12371
- status: result.status,
12372
- verdict: "fail",
12373
- summary: text,
12374
- uncertaintyFlags,
12375
- error: summary.error ?? result.status
12376
- };
12377
- }
12698
+ function makeFleetTool(director) {
12378
12699
  return {
12379
- role: resolvedRole,
12380
- subagentId: result.subagentId,
12381
- taskId: result.taskId,
12382
- status: result.status,
12383
- verdict: parseQualityVerdict(resolvedRole, text),
12384
- summary: text,
12385
- uncertaintyFlags
12386
- };
12387
- }
12388
- function assessQualityGate(reports) {
12389
- const mustFix = [];
12390
- const uncertaintyFlags = [];
12391
- let hasFail = false;
12392
- let hasInconclusive = false;
12393
- for (const report of reports) {
12394
- if (report.verdict === "fail") hasFail = true;
12395
- if (report.verdict === "inconclusive") hasInconclusive = true;
12396
- const blocking = extractSection(report.summary, "Must Fix") || extractSection(report.summary, "Failures") || extractSection(report.summary, "Verification Gaps");
12397
- if (blocking) mustFix.push(`${report.role}: ${excerpt(blocking, 1e3)}`);
12398
- if (report.uncertaintyFlags) {
12399
- uncertaintyFlags.push(`${report.role}: ${excerpt(report.uncertaintyFlags, 1e3)}`);
12400
- }
12401
- if (report.error) mustFix.push(`${report.role}: ${report.error}`);
12402
- }
12403
- if (hasFail) return { verdict: "fail", passed: false, mustFix, uncertaintyFlags };
12404
- if (hasInconclusive || reports.length === 0) {
12405
- return { verdict: "inconclusive", passed: false, mustFix, uncertaintyFlags };
12406
- }
12407
- return { verdict: "pass", passed: true, mustFix, uncertaintyFlags };
12408
- }
12409
- function parseQualityVerdict(role, text) {
12410
- const normalized = text.toLowerCase();
12411
- const verdictBlock = normalized.match(/(?:^|\n)\s*(?:#+\s*)?verdict\b[^\n]*(?:\n|:|-)?([\s\S]{0,500})/)?.[0] ?? normalized.slice(0, 1e3);
12412
- if (role === "reviewer") {
12413
- if (/\b(request\s+changes|needs\s+verification|reject|rejected|fail|failed|blocked)\b/.test(
12414
- verdictBlock
12415
- )) {
12416
- return "fail";
12417
- }
12418
- if (/\b(approve|approved|pass|passed)\b/.test(verdictBlock)) return "pass";
12419
- if (sectionHasBlockingContent(text, "Must Fix") || sectionHasBlockingContent(text, "Verification Gaps")) {
12420
- return "fail";
12700
+ name: "fleet",
12701
+ description: 'Fleet observation tool. Use `action` to select what you need: "status" \u2014 snapshot of all subagents + coordinator counts + pending tasks; "usage" \u2014 token + cost breakdown per subagent and totals; "health" \u2014 per-subagent budget pressure, last activity, and status; "session" \u2014 read a subagent\'s JSONL transcript (requires subagentId).',
12702
+ usageHint: 'action: "status" (default) | "usage" | "health" | "session".\nFor "session", pass subagentId (required) and optional tail (trailing JSONL lines).',
12703
+ permission: "auto",
12704
+ mutating: false,
12705
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12706
+ inputSchema: {
12707
+ type: "object",
12708
+ properties: {
12709
+ action: {
12710
+ type: "string",
12711
+ enum: ["status", "usage", "health", "session"],
12712
+ description: "Observation to retrieve (default: status)."
12713
+ },
12714
+ subagentId: {
12715
+ type: "string",
12716
+ description: 'Subagent id (required for action: "session").'
12717
+ },
12718
+ tail: {
12719
+ type: "number",
12720
+ description: 'Number of trailing JSONL lines (action: "session" only). Omit for the full transcript.'
12721
+ }
12722
+ }
12723
+ },
12724
+ async execute(input) {
12725
+ const i = input ?? {};
12726
+ const action = i.action ?? "status";
12727
+ switch (action) {
12728
+ case "status": {
12729
+ const base = director.status();
12730
+ const fm = director.fleetManager;
12731
+ const stats = fm?.getFleetStats();
12732
+ const fleetStatus = fm?.getFleetStatus();
12733
+ return {
12734
+ action: "status",
12735
+ subagents: base.subagents,
12736
+ coordinatorStats: stats ? {
12737
+ total: stats.total,
12738
+ running: stats.running,
12739
+ idle: stats.idle,
12740
+ stopped: stats.stopped
12741
+ } : void 0,
12742
+ pending: fleetStatus?.pending ?? [],
12743
+ usage: fm?.snapshot()
12744
+ };
12745
+ }
12746
+ case "usage": {
12747
+ return { action: "usage", ...director.snapshot() };
12748
+ }
12749
+ case "health": {
12750
+ const status = director.status();
12751
+ const snapshot = director.snapshot();
12752
+ const subagents = status.subagents ?? [];
12753
+ const perSubagent = snapshot.perSubagent ?? {};
12754
+ return {
12755
+ action: "health",
12756
+ subagents: subagents.map((s) => {
12757
+ const usage = perSubagent[s.id];
12758
+ return {
12759
+ id: s.id,
12760
+ status: s.status,
12761
+ lastEventAt: usage?.lastEventAt,
12762
+ budgetPressure: {
12763
+ iterations: usage?.iterations,
12764
+ toolCalls: usage?.toolCalls,
12765
+ costUsd: usage?.cost
12766
+ }
12767
+ };
12768
+ })
12769
+ };
12770
+ }
12771
+ case "session": {
12772
+ const subagentId = i.subagentId;
12773
+ if (!subagentId) {
12774
+ return {
12775
+ action: "session",
12776
+ error: 'fleet: subagentId is required for action: "session"'
12777
+ };
12778
+ }
12779
+ const result = await director.readSession(subagentId, i.tail);
12780
+ if (!result) {
12781
+ return {
12782
+ action: "session",
12783
+ error: `fleet: transcript unavailable for "${subagentId}". Is sessionsRoot configured?`
12784
+ };
12785
+ }
12786
+ return { action: "session", ...result };
12787
+ }
12788
+ default:
12789
+ return {
12790
+ error: `fleet: unknown action "${action}". Valid: status, usage, health, session.`
12791
+ };
12792
+ }
12421
12793
  }
12422
- return "inconclusive";
12423
- }
12424
- if (/\b(fail|failed|blocked|red)\b/.test(verdictBlock)) return "fail";
12425
- if (/\b(pass|passed|green|approve|approved)\b/.test(verdictBlock)) return "pass";
12426
- if (sectionHasBlockingContent(text, "Failures")) return "fail";
12427
- return "inconclusive";
12428
- }
12429
- function sectionHasBlockingContent(text, heading) {
12430
- const section = extractSection(text, heading);
12431
- if (!section) return false;
12432
- return !/^\s*(none|n\/a|no\b|no issues|empty|\(none\))\s*\.?\s*$/i.test(section.trim());
12433
- }
12434
- function extractSection(text, heading) {
12435
- const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12436
- const pattern = new RegExp(
12437
- `(?:^|\\n)\\s*#{1,6}\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=\\n\\s*#{1,6}\\s+|$)`,
12438
- "i"
12439
- );
12440
- const match = text.match(pattern);
12441
- const body = match?.[1]?.trim();
12442
- return body ? body : void 0;
12443
- }
12444
- function excerpt(text, max) {
12445
- if (text.length <= max) return text;
12446
- return `${text.slice(0, max - 20).trimEnd()}
12447
- ...(truncated)`;
12794
+ };
12448
12795
  }
12449
12796
 
12450
12797
  // src/coordination/director-collab-tools.ts
@@ -12565,336 +12912,378 @@ function makeWorkCompleteTool(director) {
12565
12912
  };
12566
12913
  }
12567
12914
 
12568
- // src/coordination/director-basic-tools.ts
12915
+ // src/coordination/director-quality-gate-tool.ts
12569
12916
  import { randomUUID as randomUUID9 } from "node:crypto";
12570
- function makeAssignTool(director) {
12571
- const inputSchema = {
12572
- type: "object",
12573
- properties: {
12574
- subagentId: { type: "string", minLength: 1, description: "Target subagent id. Required." },
12575
- description: {
12576
- type: "string",
12577
- minLength: 1,
12578
- description: "The task in natural language \u2014 what you want this subagent to do."
12579
- },
12580
- maxToolCalls: {
12581
- type: "number",
12582
- minimum: 1,
12583
- description: "Optional per-task tool-call budget override."
12584
- },
12585
- timeoutMs: { type: "number", minimum: 1, description: "Optional per-task timeout in ms." }
12586
- },
12587
- required: ["subagentId", "description"]
12588
- };
12917
+ function makeQualityGateTool(director, roster) {
12589
12918
  return {
12590
- name: "assign_task",
12591
- description: "Queue a task on a previously spawned subagent. NON-BLOCKING: returns a `taskId` IMMEDIATELY \u2014 the subagent processes the task on its next iteration with its own LLM budget. The `taskId` is the durable handle for retrieving the result later via `await_tasks`, `roll_up`, or `ask_result`. Many `assign_task` calls can be in flight in parallel against the same or different subagents. This is the primary tool for fan-out work; do NOT use `delegate` to spawn multiple investigations sequentially.",
12919
+ name: "quality_gate",
12920
+ description: "Run a first-class implementation quality gate. It can await implementer task ids, spawn independent verifier/reviewer agents, summarize their verdicts, and optionally send must-fix feedback back to an implementer until the gate passes or the repair-attempt limit is reached.",
12921
+ usageHint: "Use after code-changing work. Provide implementerTaskIds when available. Add repairSubagentId to iterate fixes automatically. Verdict only passes when every enabled reviewer/verifier explicitly passes.",
12592
12922
  permission: "auto",
12593
12923
  mutating: false,
12594
12924
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
12595
- inputSchema,
12596
- async execute(input) {
12597
- const i = input;
12598
- const task = {
12599
- id: randomUUID9(),
12600
- description: i.description,
12601
- subagentId: i.subagentId,
12602
- maxToolCalls: i.maxToolCalls,
12603
- timeoutMs: i.timeoutMs
12604
- };
12605
- const taskId = await director.assign(task);
12606
- return { taskId, subagentId: i.subagentId };
12607
- }
12608
- };
12609
- }
12610
- function makeAwaitTasksTool(director) {
12611
- return {
12612
- name: "await_tasks",
12613
- description: 'Block until one or more `taskId`s complete, then return their results. The subagents keep running in the background \u2014 only the leader\'s iteration pauses, which is the point: this is the correct tool to retrieve a result you started earlier with `assign_task`. mode:"all" (default) blocks until EVERY named task completes. mode:"any" returns as soon as AT LEAST ONE completes \u2014 use it for independent tasks so you can handle each finisher immediately (reassign work, spawn helpers) instead of idling on the slowest; call again with the returned `pending` ids to pick up the next finisher. The pattern "fan out via assign_task, then await_tasks({mode:\'any\'})" is the async replacement for serial `delegate` calls.',
12614
- permission: "auto",
12615
- mutating: false,
12616
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12617
12925
  inputSchema: {
12618
12926
  type: "object",
12619
12927
  properties: {
12620
- taskIds: {
12928
+ task: {
12929
+ type: "string",
12930
+ description: "Original implementation task or acceptance goal being gated."
12931
+ },
12932
+ implementerTaskIds: {
12621
12933
  type: "array",
12622
12934
  items: { type: "string" },
12623
- description: "One or more task ids returned by `assign_task`."
12935
+ description: "Optional completed or in-flight implementer task ids to await and include as implementation evidence."
12624
12936
  },
12625
- mode: {
12937
+ repairSubagentId: {
12626
12938
  type: "string",
12627
- enum: ["all", "any"],
12628
- description: '"all" (default): block until every task completes. "any": return on the first completion with the rest listed as pending.'
12939
+ description: "Optional implementer subagent id. When set and the gate fails, quality_gate assigns a repair task with reviewer/verifier feedback and reruns the gate."
12940
+ },
12941
+ maxRepairAttempts: {
12942
+ type: "number",
12943
+ minimum: 0,
12944
+ maximum: 5,
12945
+ description: "Maximum automatic repair iterations. Default: 2 when repairSubagentId is set, otherwise 0."
12946
+ },
12947
+ targets: {
12948
+ type: "array",
12949
+ items: { type: "string" },
12950
+ description: "Files, packages, or paths that reviewer/verifier should focus on."
12951
+ },
12952
+ commands: {
12953
+ type: "array",
12954
+ items: { type: "string" },
12955
+ description: 'Verification commands that should pass, e.g. ["pnpm --filter @wrongstack/core typecheck"].'
12956
+ },
12957
+ expected: {
12958
+ type: "string",
12959
+ description: "Expected behavior or acceptance criteria."
12960
+ },
12961
+ evidence: {
12962
+ type: "string",
12963
+ description: "Known implementation notes, diff summary, or commands already run."
12964
+ },
12965
+ reviewer: {
12966
+ type: "boolean",
12967
+ description: "Whether to run the reviewer lane. Default true."
12968
+ },
12969
+ verifier: {
12970
+ type: "boolean",
12971
+ description: "Whether to run the verifier lane. Default true."
12629
12972
  },
12630
12973
  timeoutMs: {
12631
12974
  type: "number",
12632
12975
  minimum: 1,
12633
- description: 'mode:"any" only \u2014 return {timedOut:true, completed:[]} if nothing completes within this window instead of blocking.'
12976
+ description: "Optional per reviewer/verifier/repair task timeout."
12977
+ },
12978
+ reviewerWorktree: {
12979
+ anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
12980
+ description: "Reviewer worktree override. Default off because reviewer is read-only."
12981
+ },
12982
+ verifierWorktree: {
12983
+ anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
12984
+ description: "Verifier worktree override. Default auto so test artifacts stay isolated when fleet policy wants it."
12634
12985
  }
12635
- },
12636
- required: ["taskIds"]
12986
+ }
12637
12987
  },
12638
12988
  async execute(input) {
12639
- const i = input;
12640
- if (i.mode === "any") {
12641
- const r = await director.awaitTasksAny(
12642
- i.taskIds,
12643
- i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : void 0
12644
- );
12989
+ const i = normalizeQualityGateInput(input);
12990
+ const runReviewer = i.reviewer !== false;
12991
+ const runVerifier = i.verifier !== false;
12992
+ if (!runReviewer && !runVerifier) {
12645
12993
  return {
12646
- mode: "any",
12647
- completed: r.completed,
12648
- pending: r.pending,
12649
- ...r.timedOut ? { timedOut: true } : {},
12650
- ...r.pending.length > 0 ? {
12651
- hint: 'Handle the completed results now. Re-call await_tasks with the pending ids (mode:"any") for the next finisher \u2014 or assign new work to the now-idle subagent first.'
12652
- } : {}
12994
+ verdict: "inconclusive",
12995
+ passed: false,
12996
+ error: "quality_gate requires reviewer, verifier, or both."
12653
12997
  };
12654
12998
  }
12655
- const results = await director.awaitTasks(i.taskIds);
12656
- return { results };
12657
- }
12658
- };
12659
- }
12660
- function makeAskTool(director) {
12661
- return {
12662
- name: "ask_subagent",
12663
- description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
12664
- permission: "auto",
12665
- mutating: false,
12666
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12667
- inputSchema: {
12668
- type: "object",
12669
- properties: {
12670
- subagentId: {
12671
- type: "string",
12672
- minLength: 1,
12673
- description: "Subagent to ask. Must be a previously spawned id."
12674
- },
12675
- question: { type: "string", minLength: 1, description: "The question or instruction." },
12676
- timeoutMs: {
12677
- type: "number",
12678
- minimum: 1,
12679
- description: "Optional timeout in ms (default 30s)."
12999
+ const implementerResults = i.implementerTaskIds && i.implementerTaskIds.length > 0 ? await director.awaitTasks(i.implementerTaskIds) : [];
13000
+ const maxRepairAttempts = clampRepairAttempts(
13001
+ i.maxRepairAttempts ?? (i.repairSubagentId ? 2 : 0)
13002
+ );
13003
+ const repairResults = [];
13004
+ const attempts = [];
13005
+ for (let attempt = 1; ; attempt++) {
13006
+ const gateTaskIds = [];
13007
+ const taskRoleById = /* @__PURE__ */ new Map();
13008
+ if (runVerifier) {
13009
+ const subagentId = await director.spawn(
13010
+ makeQualityGateSubagentConfig("verifier", roster, i.verifierWorktree ?? "auto")
13011
+ );
13012
+ const taskId = await director.assign({
13013
+ id: randomUUID9(),
13014
+ subagentId,
13015
+ description: buildVerifierTask(i, {
13016
+ attempt,
13017
+ implementerResults,
13018
+ repairResults,
13019
+ priorAttempts: attempts
13020
+ }),
13021
+ timeoutMs: i.timeoutMs
13022
+ });
13023
+ gateTaskIds.push(taskId);
13024
+ taskRoleById.set(taskId, "verifier");
12680
13025
  }
12681
- },
12682
- required: ["subagentId", "question"]
12683
- },
12684
- async execute(input) {
12685
- const i = input;
12686
- try {
12687
- const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
12688
- const stored = director.largeAnswerStore.storeAnswer(answer);
12689
- if (stored.inline) {
12690
- return { ok: true, answer: stored.summary };
13026
+ if (runReviewer) {
13027
+ const subagentId = await director.spawn(
13028
+ makeQualityGateSubagentConfig("reviewer", roster, i.reviewerWorktree ?? "off")
13029
+ );
13030
+ const taskId = await director.assign({
13031
+ id: randomUUID9(),
13032
+ subagentId,
13033
+ description: buildReviewerTask(i, {
13034
+ attempt,
13035
+ implementerResults,
13036
+ repairResults,
13037
+ priorAttempts: attempts
13038
+ }),
13039
+ timeoutMs: i.timeoutMs
13040
+ });
13041
+ gateTaskIds.push(taskId);
13042
+ taskRoleById.set(taskId, "reviewer");
13043
+ }
13044
+ const gateResults = await director.awaitTasks(gateTaskIds);
13045
+ const reports = gateResults.map((r) => assessRoleResult(taskRoleById.get(r.taskId), r));
13046
+ const assessment = assessQualityGate(reports);
13047
+ attempts.push({ attempt, reports, ...assessment });
13048
+ if (assessment.passed || !i.repairSubagentId || attempt > maxRepairAttempts) {
13049
+ return {
13050
+ verdict: assessment.verdict,
13051
+ passed: assessment.passed,
13052
+ attempts,
13053
+ repairAttemptsUsed: repairResults.length,
13054
+ implementerResults: implementerResults.map(summarizeTaskResult),
13055
+ nextAction: assessment.passed ? "accept" : i.repairSubagentId && attempt > maxRepairAttempts ? "manual_intervention_or_raise_repair_limit" : "inspect_failures"
13056
+ };
13057
+ }
13058
+ const repairTaskId = await director.assign({
13059
+ id: randomUUID9(),
13060
+ subagentId: i.repairSubagentId,
13061
+ description: buildRepairTask(i, attempts[attempts.length - 1], attempt),
13062
+ timeoutMs: i.timeoutMs
13063
+ });
13064
+ const [repairResult] = await director.awaitTasks([repairTaskId]);
13065
+ if (repairResult) repairResults.push(repairResult);
13066
+ if (repairResult?.status !== "success") {
13067
+ return {
13068
+ verdict: "fail",
13069
+ passed: false,
13070
+ attempts,
13071
+ repairAttemptsUsed: repairResults.length,
13072
+ repairResult: repairResult ? summarizeTaskResult(repairResult) : void 0,
13073
+ implementerResults: implementerResults.map(summarizeTaskResult),
13074
+ nextAction: "repair_failed"
13075
+ };
12691
13076
  }
12692
- return {
12693
- ok: true,
12694
- answer: stored.summary,
12695
- _answerKey: stored.key,
12696
- _hint: "Response was large and stored. Use ask_result with the key to retrieve it."
12697
- };
12698
- } catch (err) {
12699
- return { ok: false, error: toErrorMessage(err) };
12700
13077
  }
12701
13078
  }
12702
13079
  };
12703
13080
  }
12704
- function makeAskResultTool(director) {
13081
+ function normalizeQualityGateInput(input) {
13082
+ const raw = input ?? {};
12705
13083
  return {
12706
- name: "ask_result",
12707
- description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
12708
- permission: "auto",
12709
- mutating: false,
12710
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12711
- inputSchema: {
12712
- type: "object",
12713
- properties: {
12714
- key: {
12715
- type: "string",
12716
- minLength: 1,
12717
- description: "The `_answerKey` returned by `ask_subagent` for a large response."
12718
- }
12719
- },
12720
- required: ["key"]
12721
- },
12722
- async execute(input) {
12723
- const i = input;
12724
- const value = director.largeAnswerStore.retrieveAnswer(i.key);
12725
- if (value === void 0) {
12726
- return {
12727
- ok: false,
12728
- error: `No stored answer found for key "${i.key}" \u2014 it may have been cleared or the key is invalid.`
12729
- };
12730
- }
12731
- return { ok: true, value };
12732
- }
13084
+ task: typeof raw.task === "string" ? raw.task : void 0,
13085
+ implementerTaskIds: stringArray2(raw.implementerTaskIds),
13086
+ repairSubagentId: typeof raw.repairSubagentId === "string" && raw.repairSubagentId.trim() ? raw.repairSubagentId.trim() : void 0,
13087
+ maxRepairAttempts: typeof raw.maxRepairAttempts === "number" ? raw.maxRepairAttempts : void 0,
13088
+ targets: stringArray2(raw.targets),
13089
+ commands: stringArray2(raw.commands),
13090
+ expected: typeof raw.expected === "string" ? raw.expected : void 0,
13091
+ evidence: typeof raw.evidence === "string" ? raw.evidence : void 0,
13092
+ reviewer: typeof raw.reviewer === "boolean" ? raw.reviewer : void 0,
13093
+ verifier: typeof raw.verifier === "boolean" ? raw.verifier : void 0,
13094
+ timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : void 0,
13095
+ reviewerWorktree: normalizeWorktreeOverride(raw.reviewerWorktree),
13096
+ verifierWorktree: normalizeWorktreeOverride(raw.verifierWorktree)
12733
13097
  };
12734
13098
  }
12735
- function makeRollUpTool(director) {
13099
+ function clampRepairAttempts(value) {
13100
+ if (!Number.isFinite(value)) return 0;
13101
+ return Math.max(0, Math.min(5, Math.floor(value)));
13102
+ }
13103
+ function makeQualityGateSubagentConfig(role, roster, worktree) {
13104
+ const base = roster?.[role] ?? getAgentDefinition(role)?.config ?? { name: role, role };
12736
13105
  return {
12737
- name: "roll_up",
12738
- description: "Aggregate completed task results into a single formatted summary.",
12739
- permission: "auto",
12740
- mutating: false,
12741
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12742
- inputSchema: {
12743
- type: "object",
12744
- properties: {
12745
- taskIds: {
12746
- type: "array",
12747
- items: { type: "string" },
12748
- description: "Completed task ids to aggregate."
12749
- },
12750
- style: {
12751
- type: "string",
12752
- enum: ["markdown", "json"],
12753
- description: "Output flavor \u2014 markdown (default) or json."
12754
- }
12755
- },
12756
- required: ["taskIds"]
12757
- },
12758
- async execute(input) {
12759
- const i = input;
12760
- const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
12761
- return { summary, count: i.taskIds.length };
12762
- }
13106
+ ...instantiateRosterConfig2(role, base),
13107
+ worktree
12763
13108
  };
12764
13109
  }
12765
- function makeTerminateTool(director) {
13110
+ function buildVerifierTask(input, state) {
13111
+ return [
13112
+ "Run the independent verification gate for this implementation.",
13113
+ "Return Markdown with `## Verdict` and make the first verdict word exactly `pass`, `fail`, or `blocked`.",
13114
+ "Do not edit code. Run the smallest meaningful command set and include exact failures.",
13115
+ "",
13116
+ `Gate attempt: ${state.attempt}`,
13117
+ input.task ? `Original task:
13118
+ ${input.task}` : void 0,
13119
+ input.targets?.length ? `Targets:
13120
+ ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
13121
+ input.commands?.length ? `Required or suggested commands:
13122
+ ${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
13123
+ input.expected ? `Expected behavior:
13124
+ ${input.expected}` : void 0,
13125
+ input.evidence ? `Known evidence:
13126
+ ${input.evidence}` : void 0,
13127
+ taskResultsBlock("Implementer results", state.implementerResults),
13128
+ taskResultsBlock("Repair results so far", state.repairResults),
13129
+ priorAttemptsBlock(state.priorAttempts)
13130
+ ].filter((part) => !!part).join("\n\n");
13131
+ }
13132
+ function buildReviewerTask(input, state) {
13133
+ return [
13134
+ "Run independent code review for this implementation.",
13135
+ "Return Markdown with `## Verdict` and make the first verdict phrase exactly `approve`, `request changes`, or `needs verification`.",
13136
+ "Do not edit code. Treat missing proof, vague tests, and uncertainty as blocking until verifier evidence exists.",
13137
+ "",
13138
+ `Gate attempt: ${state.attempt}`,
13139
+ input.task ? `Original task:
13140
+ ${input.task}` : void 0,
13141
+ input.targets?.length ? `Targets:
13142
+ ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
13143
+ input.expected ? `Expected behavior:
13144
+ ${input.expected}` : void 0,
13145
+ input.evidence ? `Known evidence:
13146
+ ${input.evidence}` : void 0,
13147
+ taskResultsBlock("Implementer results", state.implementerResults),
13148
+ taskResultsBlock("Repair results so far", state.repairResults),
13149
+ priorAttemptsBlock(state.priorAttempts)
13150
+ ].filter((part) => !!part).join("\n\n");
13151
+ }
13152
+ function buildRepairTask(input, attempt, attemptNumber) {
13153
+ return [
13154
+ `Repair the implementation after quality gate attempt ${attemptNumber} failed.`,
13155
+ "Address every must-fix item. Run relevant checks before returning.",
13156
+ "Do not claim done unless verifier/reviewer feedback is resolved.",
13157
+ "",
13158
+ input.task ? `Original task:
13159
+ ${input.task}` : void 0,
13160
+ input.targets?.length ? `Targets:
13161
+ ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
13162
+ input.commands?.length ? `Commands expected to pass:
13163
+ ${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
13164
+ input.expected ? `Expected behavior:
13165
+ ${input.expected}` : void 0,
13166
+ attempt.mustFix.length ? `Must fix:
13167
+ ${attempt.mustFix.map((f) => `- ${f}`).join("\n")}` : void 0,
13168
+ attempt.uncertaintyFlags.length ? `Uncertainty flags to resolve:
13169
+ ${attempt.uncertaintyFlags.map((f) => `- ${f}`).join("\n")}` : void 0,
13170
+ `Reviewer/verifier reports:
13171
+ ${attempt.reports.map((r) => `### ${r.role} (${r.verdict})
13172
+ ${r.summary}`).join("\n\n")}`
13173
+ ].filter((part) => !!part).join("\n\n");
13174
+ }
13175
+ function taskResultsBlock(title, results) {
13176
+ if (results.length === 0) return void 0;
13177
+ return `${title}:
13178
+ ${results.map((r) => `### ${r.subagentId}/${r.taskId}
13179
+ ${summarizeTaskResult(r).summary}`).join("\n\n")}`;
13180
+ }
13181
+ function priorAttemptsBlock(attempts) {
13182
+ if (attempts.length === 0) return void 0;
13183
+ return `Prior quality gate attempts:
13184
+ ${attempts.map(
13185
+ (a) => `### Attempt ${a.attempt}
13186
+ ${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : ""}`).join("\n")}`
13187
+ ).join("\n\n")}`;
13188
+ }
13189
+ function summarizeTaskResult(result) {
13190
+ const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
13191
+ const error = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
12766
13192
  return {
12767
- name: "terminate_subagent",
12768
- description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
12769
- permission: "auto",
12770
- mutating: true,
12771
- capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
12772
- inputSchema: {
12773
- type: "object",
12774
- properties: { subagentId: { type: "string", description: "Subagent to abort." } },
12775
- required: ["subagentId"]
12776
- },
12777
- async execute(input) {
12778
- const i = input;
12779
- await director.terminate(i.subagentId);
12780
- return { ok: true };
12781
- }
13193
+ taskId: result.taskId,
13194
+ subagentId: result.subagentId,
13195
+ status: result.status,
13196
+ summary: excerpt(text || error || "(no output)", 4e3),
13197
+ error
12782
13198
  };
12783
13199
  }
12784
- function makeTerminateAllTool(director) {
13200
+ function assessRoleResult(role, result) {
13201
+ const resolvedRole = role ?? (result.subagentId.includes("review") ? "reviewer" : "verifier");
13202
+ const summary = summarizeTaskResult(result);
13203
+ const text = summary.summary;
13204
+ const uncertaintyFlags = extractSection(text, "Uncertainty Flags");
13205
+ if (result.status !== "success") {
13206
+ return {
13207
+ role: resolvedRole,
13208
+ subagentId: result.subagentId,
13209
+ taskId: result.taskId,
13210
+ status: result.status,
13211
+ verdict: "fail",
13212
+ summary: text,
13213
+ uncertaintyFlags,
13214
+ error: summary.error ?? result.status
13215
+ };
13216
+ }
12785
13217
  return {
12786
- name: "terminate_all",
12787
- description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
12788
- permission: "auto",
12789
- mutating: true,
12790
- capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
12791
- inputSchema: { type: "object", properties: {}, required: [] },
12792
- async execute() {
12793
- await director.terminateAll();
12794
- return {
12795
- ok: true,
12796
- message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.`
12797
- };
13218
+ role: resolvedRole,
13219
+ subagentId: result.subagentId,
13220
+ taskId: result.taskId,
13221
+ status: result.status,
13222
+ verdict: parseQualityVerdict(resolvedRole, text),
13223
+ summary: text,
13224
+ uncertaintyFlags
13225
+ };
13226
+ }
13227
+ function assessQualityGate(reports) {
13228
+ const mustFix = [];
13229
+ const uncertaintyFlags = [];
13230
+ let hasFail = false;
13231
+ let hasInconclusive = false;
13232
+ for (const report of reports) {
13233
+ if (report.verdict === "fail") hasFail = true;
13234
+ if (report.verdict === "inconclusive") hasInconclusive = true;
13235
+ const blocking = extractSection(report.summary, "Must Fix") || extractSection(report.summary, "Failures") || extractSection(report.summary, "Verification Gaps");
13236
+ if (blocking) mustFix.push(`${report.role}: ${excerpt(blocking, 1e3)}`);
13237
+ if (report.uncertaintyFlags) {
13238
+ uncertaintyFlags.push(`${report.role}: ${excerpt(report.uncertaintyFlags, 1e3)}`);
13239
+ }
13240
+ if (report.error) mustFix.push(`${report.role}: ${report.error}`);
13241
+ }
13242
+ if (hasFail) return { verdict: "fail", passed: false, mustFix, uncertaintyFlags };
13243
+ if (hasInconclusive || reports.length === 0) {
13244
+ return { verdict: "inconclusive", passed: false, mustFix, uncertaintyFlags };
13245
+ }
13246
+ return { verdict: "pass", passed: true, mustFix, uncertaintyFlags };
13247
+ }
13248
+ function parseQualityVerdict(role, text) {
13249
+ const normalized = text.toLowerCase();
13250
+ const verdictBlock = normalized.match(/(?:^|\n)\s*(?:#+\s*)?verdict\b[^\n]*(?:\n|:|-)?([\s\S]{0,500})/)?.[0] ?? normalized.slice(0, 1e3);
13251
+ if (role === "reviewer") {
13252
+ if (/\b(request\s+changes|needs\s+verification|reject|rejected|fail|failed|blocked)\b/.test(
13253
+ verdictBlock
13254
+ )) {
13255
+ return "fail";
12798
13256
  }
12799
- };
12800
- }
12801
- function makeFleetTool(director) {
12802
- return {
12803
- name: "fleet",
12804
- description: 'Fleet observation tool. Use `action` to select what you need: "status" \u2014 snapshot of all subagents + coordinator counts + pending tasks; "usage" \u2014 token + cost breakdown per subagent and totals; "health" \u2014 per-subagent budget pressure, last activity, and status; "session" \u2014 read a subagent\'s JSONL transcript (requires subagentId).',
12805
- usageHint: 'action: "status" (default) | "usage" | "health" | "session".\nFor "session", pass subagentId (required) and optional tail (trailing JSONL lines).',
12806
- permission: "auto",
12807
- mutating: false,
12808
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
12809
- inputSchema: {
12810
- type: "object",
12811
- properties: {
12812
- action: {
12813
- type: "string",
12814
- enum: ["status", "usage", "health", "session"],
12815
- description: "Observation to retrieve (default: status)."
12816
- },
12817
- subagentId: {
12818
- type: "string",
12819
- description: 'Subagent id (required for action: "session").'
12820
- },
12821
- tail: {
12822
- type: "number",
12823
- description: 'Number of trailing JSONL lines (action: "session" only). Omit for the full transcript.'
12824
- }
12825
- }
12826
- },
12827
- async execute(input) {
12828
- const i = input ?? {};
12829
- const action = i.action ?? "status";
12830
- switch (action) {
12831
- case "status": {
12832
- const base = director.status();
12833
- const fm = director.fleetManager;
12834
- const stats = fm?.getFleetStats();
12835
- const fleetStatus = fm?.getFleetStatus();
12836
- return {
12837
- action: "status",
12838
- subagents: base.subagents,
12839
- coordinatorStats: stats ? {
12840
- total: stats.total,
12841
- running: stats.running,
12842
- idle: stats.idle,
12843
- stopped: stats.stopped
12844
- } : void 0,
12845
- pending: fleetStatus?.pending ?? [],
12846
- usage: fm?.snapshot()
12847
- };
12848
- }
12849
- case "usage": {
12850
- return { action: "usage", ...director.snapshot() };
12851
- }
12852
- case "health": {
12853
- const status = director.status();
12854
- const snapshot = director.snapshot();
12855
- const subagents = status.subagents ?? [];
12856
- const perSubagent = snapshot.perSubagent ?? {};
12857
- return {
12858
- action: "health",
12859
- subagents: subagents.map((s) => {
12860
- const usage = perSubagent[s.id];
12861
- return {
12862
- id: s.id,
12863
- status: s.status,
12864
- lastEventAt: usage?.lastEventAt,
12865
- budgetPressure: {
12866
- iterations: usage?.iterations,
12867
- toolCalls: usage?.toolCalls,
12868
- costUsd: usage?.cost
12869
- }
12870
- };
12871
- })
12872
- };
12873
- }
12874
- case "session": {
12875
- const subagentId = i.subagentId;
12876
- if (!subagentId) {
12877
- return {
12878
- action: "session",
12879
- error: 'fleet: subagentId is required for action: "session"'
12880
- };
12881
- }
12882
- const result = await director.readSession(subagentId, i.tail);
12883
- if (!result) {
12884
- return {
12885
- action: "session",
12886
- error: `fleet: transcript unavailable for "${subagentId}". Is sessionsRoot configured?`
12887
- };
12888
- }
12889
- return { action: "session", ...result };
12890
- }
12891
- default:
12892
- return {
12893
- error: `fleet: unknown action "${action}". Valid: status, usage, health, session.`
12894
- };
12895
- }
13257
+ if (/\b(approve|approved|pass|passed)\b/.test(verdictBlock)) return "pass";
13258
+ if (sectionHasBlockingContent(text, "Must Fix") || sectionHasBlockingContent(text, "Verification Gaps")) {
13259
+ return "fail";
12896
13260
  }
12897
- };
13261
+ return "inconclusive";
13262
+ }
13263
+ if (/\b(fail|failed|blocked|red)\b/.test(verdictBlock)) return "fail";
13264
+ if (/\b(pass|passed|green|approve|approved)\b/.test(verdictBlock)) return "pass";
13265
+ if (sectionHasBlockingContent(text, "Failures")) return "fail";
13266
+ return "inconclusive";
13267
+ }
13268
+ function sectionHasBlockingContent(text, heading) {
13269
+ const section = extractSection(text, heading);
13270
+ if (!section) return false;
13271
+ return !/^\s*(none|n\/a|no\b|no issues|empty|\(none\))\s*\.?\s*$/i.test(section.trim());
13272
+ }
13273
+ function extractSection(text, heading) {
13274
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13275
+ const pattern = new RegExp(
13276
+ `(?:^|\\n)\\s*#{1,6}\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=\\n\\s*#{1,6}\\s+|$)`,
13277
+ "i"
13278
+ );
13279
+ const match = text.match(pattern);
13280
+ const body = match?.[1]?.trim();
13281
+ return body ? body : void 0;
13282
+ }
13283
+ function excerpt(text, max) {
13284
+ if (text.length <= max) return text;
13285
+ return `${text.slice(0, max - 20).trimEnd()}
13286
+ ...(truncated)`;
12898
13287
  }
12899
13288
 
12900
13289
  // src/coordination/director-tools.ts
@@ -12929,13 +13318,13 @@ function makeSpawnTool(director, roster) {
12929
13318
  const inputSchema = {
12930
13319
  type: "object",
12931
13320
  properties: {
12932
- role: {
13321
+ description: {
12933
13322
  type: "string",
12934
- description: "Roster role id. When set, the spawn uses the matching config from the roster and ignores other fields."
13323
+ description: "What the subagent has to do, in free form. PREFER THIS over `role`: the dispatcher scores it against the capability metadata of every agent in the roster and picks the specialist, which is more reliable than recalling a role id from a list of 77. Only reach for `role` when you are certain which one you want."
12935
13324
  },
12936
- description: {
13325
+ role: {
12937
13326
  type: "string",
12938
- description: "Free-form task description. When `role` is not set, the director uses the smart dispatcher to route this to the best-matching catalog agent. Use this when you don't know the exact role name."
13327
+ description: "Roster role id. When set, the spawn uses the matching config from the roster, ignores other fields, and SKIPS DISPATCH ENTIRELY \u2014 so a half-remembered id silently costs you the specialist. Prefer `description` unless the id is certain."
12939
13328
  },
12940
13329
  name: {
12941
13330
  type: "string",
@@ -12981,7 +13370,7 @@ function makeSpawnTool(director, roster) {
12981
13370
  return {
12982
13371
  name: "spawn_subagent",
12983
13372
  description: "Create a new subagent under this director (own LLM context, own budget). NON-BLOCKING: returns a `subagentId` immediately without triggering any model call. Pair with `assign_task` to send work and `await_tasks` to retrieve the result later \u2014 this is the async-delegation pattern that lets the leader keep working while the subagent runs in its own context.",
12984
- usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`. Use this instead of `delegate` when you want to fan out to multiple subagents or keep the leader unblocked while work runs in parallel.",
13373
+ usageHint: "Pass `description` (what the work is \u2014 dispatched to the best-matching specialist), or `role` when you are certain of the id, or `name` + `provider`/`model`. Returns `{ subagentId }`. The roster is deep and specialised: there is very likely an agent built for this exact job, so describe the work rather than defaulting to a generalist. Use this instead of `delegate` when you want to fan out to multiple subagents or keep the leader unblocked while work runs in parallel.",
12985
13374
  permission: "auto",
12986
13375
  mutating: false,
12987
13376
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
@@ -13485,8 +13874,9 @@ function composeDirectorPrompt(parts = {}) {
13485
13874
  const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
13486
13875
  if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
13487
13876
  if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
13488
- sections.push(`Available roles you can spawn:
13489
- ${parts.rosterSummary.trim()}`);
13877
+ sections.push(
13878
+ "Roles you can spawn. This roster is deep and specialised \u2014 before handing work to a generalist, look for the agent built for exactly this job, and prefer describing the work (`description`) over recalling an id (`role`) so the dispatcher can find it:\n" + parts.rosterSummary.trim()
13879
+ );
13490
13880
  }
13491
13881
  if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
13492
13882
  sections.push(parts.basePrompt.trim());
@@ -13523,37 +13913,47 @@ ${parts.skills.trim()}`);
13523
13913
  }
13524
13914
  return sections.join("\n\n");
13525
13915
  }
13916
+ var ROSTER_SUMMARY_MAX_CHARS = 150;
13917
+ function nameAddsInformation(roleId, name) {
13918
+ const flatten = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
13919
+ return flatten(name) !== flatten(roleId);
13920
+ }
13526
13921
  function rosterSummaryFromConfigs(roster) {
13527
13922
  const lines = [];
13528
13923
  for (const [roleId, cfg] of Object.entries(roster)) {
13529
13924
  const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
13530
- const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
13925
+ const label = nameAddsInformation(roleId, cfg.name) ? ` (${cfg.name})` : "";
13926
+ const capability = cfg.dispatch?.summary?.trim() || // Fallback headline: a role prompt that opens with a markdown heading
13927
+ // would otherwise render as "# Generic Project Agent", which reads as
13928
+ // formatting rather than as a capability.
13929
+ (cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").replace(/^#+\s*/, "").trim() : "");
13930
+ const headline = capability.length > ROSTER_SUMMARY_MAX_CHARS ? `${capability.slice(0, ROSTER_SUMMARY_MAX_CHARS - 1).trimEnd()}\u2026` : capability;
13531
13931
  const tail = headline ? ` \u2014 ${headline}` : "";
13532
- lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
13932
+ lines.push(`- ${roleId}${label}${tag}${tail}`);
13533
13933
  }
13534
13934
  return lines.join("\n");
13535
13935
  }
13536
13936
 
13537
13937
  // src/coordination/director-session.ts
13538
13938
  import * as fsp18 from "node:fs/promises";
13539
- import * as path28 from "node:path";
13939
+ import * as path30 from "node:path";
13540
13940
 
13541
13941
  // src/storage/session-store.ts
13542
13942
  import { randomUUID as randomUUID12 } from "node:crypto";
13543
13943
  import * as fsp17 from "node:fs/promises";
13544
- import * as path27 from "node:path";
13944
+ import * as path29 from "node:path";
13545
13945
 
13546
13946
  // src/session-catalog/client.ts
13547
13947
  import { spawn as spawn2 } from "node:child_process";
13548
13948
  import * as fs3 from "node:fs";
13549
13949
  import * as net from "node:net";
13550
- import * as path17 from "node:path";
13950
+ import * as path19 from "node:path";
13551
13951
  import { fileURLToPath as fileURLToPath3 } from "node:url";
13552
13952
 
13553
13953
  // src/session-catalog/endpoint.ts
13554
13954
  import { createHash as createHash2 } from "node:crypto";
13555
13955
  import * as os2 from "node:os";
13556
- import * as path16 from "node:path";
13956
+ import * as path18 from "node:path";
13557
13957
 
13558
13958
  // src/session-catalog/protocol.ts
13559
13959
  var SESSION_CATALOG_PROTOCOL_VERSION = 1;
@@ -13566,7 +13966,7 @@ function encodeSessionCatalogMessage(message) {
13566
13966
  // src/session-catalog/endpoint.ts
13567
13967
  var SESSION_CATALOG_METADATA_FILE = ".session-catalog-server.json";
13568
13968
  function normalizedPath(value) {
13569
- const resolved = path16.resolve(value);
13969
+ const resolved = path18.resolve(value);
13570
13970
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
13571
13971
  }
13572
13972
  function sessionCatalogProjectServerKey(projectDir) {
@@ -13577,10 +13977,10 @@ function sessionCatalogProjectServerEndpoint(projectDir) {
13577
13977
  if (process.platform === "win32") {
13578
13978
  return `\\\\.\\pipe\\wrongstack-session-catalog-v${SESSION_CATALOG_PROTOCOL_VERSION}-${key}`;
13579
13979
  }
13580
- return path16.join(os2.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
13980
+ return path18.join(os2.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
13581
13981
  }
13582
13982
  function sessionCatalogProjectServerMetadataPath(projectDir) {
13583
- return path16.join(projectDir, SESSION_CATALOG_METADATA_FILE);
13983
+ return path18.join(projectDir, SESSION_CATALOG_METADATA_FILE);
13584
13984
  }
13585
13985
 
13586
13986
  // src/session-catalog/client.ts
@@ -13615,7 +14015,7 @@ function resolveSessionCatalogProjectServerUrl(moduleUrl = import.meta.url, exis
13615
14015
  return availability.kind === "available" ? availability.url : null;
13616
14016
  }
13617
14017
  function normalize2(value) {
13618
- const resolved = path17.resolve(value);
14018
+ const resolved = path19.resolve(value);
13619
14019
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
13620
14020
  }
13621
14021
  function delay(ms) {
@@ -14060,18 +14460,32 @@ function ulid(seedTime = Date.now()) {
14060
14460
  }
14061
14461
 
14062
14462
  // src/utils/session-scoped-path.ts
14063
- import * as path18 from "node:path";
14463
+ import * as path20 from "node:path";
14064
14464
  function sessionScopedPath(dir, sessionId, suffix) {
14065
14465
  if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
14066
14466
  throw invalid(sessionId);
14067
14467
  }
14068
- const resolved = path18.resolve(dir, `${sessionId}${suffix}`);
14069
- const rel = path18.relative(path18.resolve(dir), resolved);
14070
- if (rel.startsWith("..") || path18.isAbsolute(rel)) {
14468
+ const resolved = path20.resolve(dir, `${sessionId}${suffix}`);
14469
+ const rel = path20.relative(path20.resolve(dir), resolved);
14470
+ if (rel.startsWith("..") || path20.isAbsolute(rel)) {
14071
14471
  throw invalid(sessionId);
14072
14472
  }
14073
14473
  return resolved;
14074
14474
  }
14475
+ var SESSION_SIDECAR_JSONL_SUFFIXES = [
14476
+ ".replay.jsonl",
14477
+ ".audit.jsonl",
14478
+ ".annotations.jsonl"
14479
+ ];
14480
+ var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
14481
+ "_index.jsonl",
14482
+ "_mailbox.jsonl"
14483
+ ]);
14484
+ function isSessionTranscriptFileName(name) {
14485
+ if (!name.endsWith(".jsonl")) return false;
14486
+ if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
14487
+ return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
14488
+ }
14075
14489
  function invalid(sessionId) {
14076
14490
  return new FsError({
14077
14491
  message: `Invalid sessionId: ${sessionId}`,
@@ -14089,7 +14503,7 @@ function truncate3(s, max) {
14089
14503
  // src/storage/file-session-writer.ts
14090
14504
  import { closeSync, createReadStream as createReadStream2, fsyncSync, openSync, writeSync } from "node:fs";
14091
14505
  import * as fsp7 from "node:fs/promises";
14092
- import * as path19 from "node:path";
14506
+ import * as path21 from "node:path";
14093
14507
  import { createInterface as createInterface2 } from "node:readline";
14094
14508
 
14095
14509
  // src/storage/session-workspace-checkpoints.ts
@@ -14355,7 +14769,7 @@ var FileSessionWriter = class _FileSessionWriter {
14355
14769
  this.meta = meta;
14356
14770
  this.events = events;
14357
14771
  this.resumed = opts.resumed ?? false;
14358
- this.manifestFile = opts.dir ? path19.join(opts.dir, `${path19.basename(id)}.summary.json`) : "";
14772
+ this.manifestFile = opts.dir ? path21.join(opts.dir, `${path21.basename(id)}.summary.json`) : "";
14359
14773
  this.filePath = opts.filePath ?? "";
14360
14774
  this.secretScrubber = opts.secretScrubber;
14361
14775
  this.checkpointCas = opts.checkpointCas;
@@ -15247,7 +15661,7 @@ var FileSessionWriter = class _FileSessionWriter {
15247
15661
  import { spawn as spawn3 } from "node:child_process";
15248
15662
  import { createHash as createHash3, randomUUID as randomUUID11 } from "node:crypto";
15249
15663
  import * as fsp8 from "node:fs/promises";
15250
- import * as path20 from "node:path";
15664
+ import * as path22 from "node:path";
15251
15665
 
15252
15666
  // src/storage/storage-concurrency.ts
15253
15667
  async function mapWithConcurrency(items, concurrency, mapper) {
@@ -15278,13 +15692,13 @@ function sha256(content) {
15278
15692
  return createHash3("sha256").update(content).digest("hex");
15279
15693
  }
15280
15694
  function isInside(root, target) {
15281
- const relative4 = path20.relative(root, target);
15282
- return relative4 === "" || !relative4.startsWith("..") && !path20.isAbsolute(relative4);
15695
+ const relative4 = path22.relative(root, target);
15696
+ return relative4 === "" || !relative4.startsWith("..") && !path22.isAbsolute(relative4);
15283
15697
  }
15284
15698
  function normalizeRelative(input) {
15285
- if (!input || path20.isAbsolute(input)) return null;
15699
+ if (!input || path22.isAbsolute(input)) return null;
15286
15700
  const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
15287
- const resolved = path20.posix.normalize(normalized);
15701
+ const resolved = path22.posix.normalize(normalized);
15288
15702
  if (!resolved || resolved === "." || resolved === ".." || resolved.startsWith("../")) return null;
15289
15703
  return resolved;
15290
15704
  }
@@ -15299,8 +15713,8 @@ var SessionCheckpointCas = class {
15299
15713
  projectRoot;
15300
15714
  runGit;
15301
15715
  constructor(opts) {
15302
- this.rootDir = path20.resolve(opts.rootDir);
15303
- this.projectRoot = path20.resolve(opts.projectRoot);
15716
+ this.rootDir = path22.resolve(opts.rootDir);
15717
+ this.projectRoot = path22.resolve(opts.projectRoot);
15304
15718
  this.runGit = opts.runGit ?? defaultRunGit;
15305
15719
  }
15306
15720
  async capture(_sessionId, _promptIndex) {
@@ -15325,7 +15739,7 @@ var SessionCheckpointCas = class {
15325
15739
  relativePaths,
15326
15740
  CAPTURE_CONCURRENCY,
15327
15741
  async (relative4) => {
15328
- const absolute = path20.resolve(this.projectRoot, ...relative4.split("/"));
15742
+ const absolute = path22.resolve(this.projectRoot, ...relative4.split("/"));
15329
15743
  if (!isInside(this.projectRoot, absolute)) {
15330
15744
  unresolved.push({ path: relative4, reason: "path escapes project root" });
15331
15745
  return null;
@@ -15334,8 +15748,8 @@ var SessionCheckpointCas = class {
15334
15748
  const stat10 = await fsp8.lstat(absolute);
15335
15749
  if (stat10.isSymbolicLink()) {
15336
15750
  const linkTarget = await fsp8.readlink(absolute);
15337
- const resolvedLink = path20.resolve(path20.dirname(absolute), linkTarget);
15338
- if (path20.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
15751
+ const resolvedLink = path22.resolve(path22.dirname(absolute), linkTarget);
15752
+ if (path22.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
15339
15753
  unresolved.push({
15340
15754
  path: relative4,
15341
15755
  reason: "symlink target escapes project root"
@@ -15397,7 +15811,7 @@ var SessionCheckpointCas = class {
15397
15811
  };
15398
15812
  }
15399
15813
  async materialize(checkpoint, targetRoot) {
15400
- const target = path20.resolve(targetRoot);
15814
+ const target = path22.resolve(targetRoot);
15401
15815
  if (target === this.projectRoot) {
15402
15816
  throw new Error("Refusing to materialize a workspace checkpoint over the parent project root");
15403
15817
  }
@@ -15437,10 +15851,10 @@ var SessionCheckpointCas = class {
15437
15851
  try {
15438
15852
  const output = await this.safeOutputPath(target, realTarget, entry.path);
15439
15853
  if (entry.state === "symlink") {
15440
- if (path20.isAbsolute(entry.linkTarget)) {
15854
+ if (path22.isAbsolute(entry.linkTarget)) {
15441
15855
  throw new Error("absolute symlink target refused");
15442
15856
  }
15443
- const resolvedLink = path20.resolve(path20.dirname(output), entry.linkTarget);
15857
+ const resolvedLink = path22.resolve(path22.dirname(output), entry.linkTarget);
15444
15858
  if (!isInside(target, resolvedLink)) throw new Error("symlink target escapes checkpoint root");
15445
15859
  }
15446
15860
  prepared.push({
@@ -15468,7 +15882,7 @@ var SessionCheckpointCas = class {
15468
15882
  await fsp8.unlink(output).catch((err) => {
15469
15883
  if (err.code !== "ENOENT") throw err;
15470
15884
  });
15471
- await fsp8.mkdir(path20.dirname(output), { recursive: true });
15885
+ await fsp8.mkdir(path22.dirname(output), { recursive: true });
15472
15886
  await fsp8.symlink(entry.linkTarget, output);
15473
15887
  writtenFiles.push(entry.path);
15474
15888
  continue;
@@ -15485,15 +15899,15 @@ var SessionCheckpointCas = class {
15485
15899
  }
15486
15900
  objectPath(hash) {
15487
15901
  if (!HASH_RE.test(hash)) throw new Error(`Invalid CAS object hash: ${hash}`);
15488
- return path20.join(this.rootDir, "objects", hash.slice(0, 2), hash.slice(2));
15902
+ return path22.join(this.rootDir, "objects", hash.slice(0, 2), hash.slice(2));
15489
15903
  }
15490
15904
  manifestPath(hash) {
15491
15905
  if (!HASH_RE.test(hash)) throw new Error(`Invalid checkpoint manifest hash: ${hash}`);
15492
- return path20.join(this.rootDir, "manifests", `${hash}.json`);
15906
+ return path22.join(this.rootDir, "manifests", `${hash}.json`);
15493
15907
  }
15494
15908
  async putBlob(hash, content) {
15495
15909
  const target = this.objectPath(hash);
15496
- await fsp8.mkdir(path20.dirname(target), { recursive: true });
15910
+ await fsp8.mkdir(path22.dirname(target), { recursive: true });
15497
15911
  try {
15498
15912
  const existing = await fsp8.readFile(target);
15499
15913
  if (sha256(existing) !== hash) throw new Error(`Corrupt CAS object collision: ${hash}`);
@@ -15501,9 +15915,9 @@ var SessionCheckpointCas = class {
15501
15915
  } catch (err) {
15502
15916
  if (err.code !== "ENOENT") throw err;
15503
15917
  }
15504
- const temp = path20.join(
15505
- path20.dirname(target),
15506
- `.${path20.basename(target)}.${process.pid}.${randomUUID11()}.tmp`
15918
+ const temp = path22.join(
15919
+ path22.dirname(target),
15920
+ `.${path22.basename(target)}.${process.pid}.${randomUUID11()}.tmp`
15507
15921
  );
15508
15922
  let handle;
15509
15923
  try {
@@ -15564,7 +15978,7 @@ var SessionCheckpointCas = class {
15564
15978
  async safeOutputPath(target, realTarget, relative4) {
15565
15979
  const normalized = normalizeRelative(relative4);
15566
15980
  if (!normalized) throw new Error("invalid relative path");
15567
- const output = path20.resolve(target, ...normalized.split("/"));
15981
+ const output = path22.resolve(target, ...normalized.split("/"));
15568
15982
  if (!isInside(target, output)) throw new Error("path escapes checkpoint target");
15569
15983
  let probe = output;
15570
15984
  for (; ; ) {
@@ -15574,7 +15988,7 @@ var SessionCheckpointCas = class {
15574
15988
  return output;
15575
15989
  } catch (err) {
15576
15990
  if (err.code !== "ENOENT") throw err;
15577
- const parent = path20.dirname(probe);
15991
+ const parent = path22.dirname(probe);
15578
15992
  if (parent === probe) throw err;
15579
15993
  probe = parent;
15580
15994
  }
@@ -15638,13 +16052,13 @@ function generateSessionId(startedAt, _model) {
15638
16052
  }
15639
16053
 
15640
16054
  // src/storage/session-id-resolver.ts
15641
- import * as path21 from "node:path";
16055
+ import * as path23 from "node:path";
15642
16056
  function resolveSessionId(query, candidateIds) {
15643
16057
  const normalized = query.trim();
15644
16058
  if (!normalized) return { status: "missing", query: normalized };
15645
16059
  const uniqueIds = [...new Set(candidateIds)];
15646
16060
  if (uniqueIds.includes(normalized)) return { status: "resolved", id: normalized };
15647
- const leaf = (id) => path21.posix.basename(id.replace(/\\/g, "/"));
16061
+ const leaf = (id) => path23.posix.basename(id.replace(/\\/g, "/"));
15648
16062
  const exactLeafMatches = uniqueIds.filter((id) => leaf(id) === normalized);
15649
16063
  if (exactLeafMatches.length === 1) {
15650
16064
  return { status: "resolved", id: exactLeafMatches[0] };
@@ -15695,13 +16109,13 @@ function scrubPersistedSessionSummary(summary, scrubber2) {
15695
16109
  // src/storage/session-resume-validation.ts
15696
16110
  import { createHash as createHash4 } from "node:crypto";
15697
16111
  import * as fsp9 from "node:fs/promises";
15698
- import * as path22 from "node:path";
16112
+ import * as path24 from "node:path";
15699
16113
  var MAX_REVALIDATE_BYTES = 5 * 1024 * 1024;
15700
16114
  var VALIDATION_CONCURRENCY = 8;
15701
16115
  var NOTICE_PATH_LIMIT = 20;
15702
16116
  function isInside2(root, target) {
15703
- const relative4 = path22.relative(root, target);
15704
- return relative4 === "" || !relative4.startsWith("..") && !path22.isAbsolute(relative4);
16117
+ const relative4 = path24.relative(root, target);
16118
+ return relative4 === "" || !relative4.startsWith("..") && !path24.isAbsolute(relative4);
15705
16119
  }
15706
16120
  function errno(err) {
15707
16121
  return err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
@@ -15712,7 +16126,7 @@ function latestObservations(events, projectRoot) {
15712
16126
  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)) {
15713
16127
  continue;
15714
16128
  }
15715
- const normalized = path22.resolve(projectRoot, event.path);
16129
+ const normalized = path24.resolve(projectRoot, event.path);
15716
16130
  latest.set(normalized, {
15717
16131
  path: normalized,
15718
16132
  hash: event.hash.toLowerCase(),
@@ -15770,7 +16184,7 @@ async function validateOne(observation, lexicalRoot, realRoot) {
15770
16184
  }
15771
16185
  }
15772
16186
  async function validateResumeFileObservations(events, projectRoot) {
15773
- const lexicalRoot = path22.resolve(projectRoot);
16187
+ const lexicalRoot = path24.resolve(projectRoot);
15774
16188
  const realRoot = await fsp9.realpath(lexicalRoot).catch(() => lexicalRoot);
15775
16189
  const observations = latestObservations(events, lexicalRoot);
15776
16190
  const results = await mapWithConcurrency(
@@ -15784,11 +16198,20 @@ async function validateResumeFileObservations(events, projectRoot) {
15784
16198
  staleFiles: results.filter((entry) => entry !== null)
15785
16199
  };
15786
16200
  }
16201
+ var RESUME_NOTICE_HEADERS = [
16202
+ "[SESSION RESUME FILE VALIDATION]",
16203
+ "[SESSION RESUME INTERRUPTED WORK]"
16204
+ ];
16205
+ function isResumeNoticeMessage(message) {
16206
+ if (message.role !== "system" || typeof message.content !== "string") return false;
16207
+ return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
16208
+ `));
16209
+ }
15787
16210
  function formatResumeValidationNotice(validation, projectRoot) {
15788
16211
  if (validation.staleFiles.length === 0) return null;
15789
- const root = path22.resolve(projectRoot);
16212
+ const root = path24.resolve(projectRoot);
15790
16213
  const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
15791
- const relative4 = path22.relative(root, entry.path);
16214
+ const relative4 = path24.relative(root, entry.path);
15792
16215
  const display = isInside2(root, entry.path) ? relative4 || "." : entry.path;
15793
16216
  return `- ${JSON.stringify(display)} [${entry.status}]`;
15794
16217
  });
@@ -15813,22 +16236,22 @@ function formatInterruptedToolNotice(pendingToolUseCount) {
15813
16236
 
15814
16237
  // src/storage/session-store/delete-session-artifacts.ts
15815
16238
  import * as fsp10 from "node:fs/promises";
15816
- import * as path24 from "node:path";
16239
+ import * as path26 from "node:path";
15817
16240
 
15818
16241
  // src/storage/session-store/paths.ts
15819
- import * as path23 from "node:path";
16242
+ import * as path25 from "node:path";
15820
16243
  function sessionPath(storeDir, id, ext) {
15821
16244
  return sessionScopedPath(storeDir, id, ext);
15822
16245
  }
15823
16246
  function shardManifestPath(storeDir, shardKey) {
15824
- return shardKey ? path23.join(storeDir, shardKey, "_manifest.json") : path23.join(storeDir, "_manifest.json");
16247
+ return shardKey ? path25.join(storeDir, shardKey, "_manifest.json") : path25.join(storeDir, "_manifest.json");
15825
16248
  }
15826
16249
  function shardKeyForSessionId(id) {
15827
- const dirName = path23.dirname(id);
16250
+ const dirName = path25.dirname(id);
15828
16251
  return dirName === "." ? "" : dirName;
15829
16252
  }
15830
16253
  async function ensureShardDir(storeDir, id) {
15831
- const dirPath = path23.dirname(sessionScopedPath(storeDir, id, ""));
16254
+ const dirPath = path25.dirname(sessionScopedPath(storeDir, id, ""));
15832
16255
  await ensureDir(dirPath);
15833
16256
  return dirPath;
15834
16257
  }
@@ -15839,9 +16262,9 @@ async function deleteSessionArtifacts({
15839
16262
  id,
15840
16263
  jsonlPath
15841
16264
  }) {
15842
- const shardDir = path24.dirname(jsonlPath);
15843
- const base = path24.basename(id);
15844
- const sessDir = path24.join(shardDir, base);
16265
+ const shardDir = path26.dirname(jsonlPath);
16266
+ const base = path26.basename(id);
16267
+ const sessDir = path26.join(shardDir, base);
15845
16268
  const deletions = [
15846
16269
  fsp10.unlink(jsonlPath),
15847
16270
  fsp10.unlink(sessionPath(rootDir, id, ".summary.json")),
@@ -15894,13 +16317,10 @@ async function assertSessionCanBeDeleted(sessionId, isSessionInUse) {
15894
16317
  function shouldSkipSessionDirectoryEntry(name) {
15895
16318
  return name.startsWith(".") && name !== ".wrongstack" || name === "shared" || name === "subagents" || name === "attachments";
15896
16319
  }
15897
- function isSessionJsonlFileName(name) {
15898
- return name.endsWith(".jsonl") && name !== "_index.jsonl";
15899
- }
15900
16320
 
15901
16321
  // src/storage/session-store/directory-session-files.ts
15902
16322
  import * as fsp11 from "node:fs/promises";
15903
- import * as path25 from "node:path";
16323
+ import * as path27 from "node:path";
15904
16324
  function sessionIdForFile(prefix, name) {
15905
16325
  const base = name.replace(/\.jsonl$/, "");
15906
16326
  return prefix ? `${prefix}/${base}` : base;
@@ -15921,13 +16341,13 @@ async function collectSessionFiles(dir, prefix = "", depth = 0) {
15921
16341
  if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
15922
16342
  if (entry.isDirectory()) {
15923
16343
  dirEntries.push(entry);
15924
- } else if (entry.isFile() && isSessionJsonlFileName(entry.name)) {
15925
- files.push({ id: sessionIdForFile(prefix, entry.name), filePath: path25.join(dir, entry.name) });
16344
+ } else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
16345
+ files.push({ id: sessionIdForFile(prefix, entry.name), filePath: path27.join(dir, entry.name) });
15926
16346
  }
15927
16347
  }
15928
16348
  const childFileArrays = await Promise.all(
15929
16349
  dirEntries.map(
15930
- (entry) => collectSessionFiles(path25.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
16350
+ (entry) => collectSessionFiles(path27.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
15931
16351
  )
15932
16352
  );
15933
16353
  return [...childFileArrays.flat(), ...files];
@@ -15945,13 +16365,13 @@ async function collectSessionIds(dir, prefix = "", depth = 0) {
15945
16365
  if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
15946
16366
  if (entry.isDirectory()) {
15947
16367
  dirEntries.push(entry);
15948
- } else if (entry.isFile() && isSessionJsonlFileName(entry.name)) {
16368
+ } else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
15949
16369
  fileIds.push(sessionIdForFile(prefix, entry.name));
15950
16370
  }
15951
16371
  }
15952
16372
  const childIdArrays = await Promise.all(
15953
16373
  dirEntries.map(
15954
- (entry) => collectSessionIds(path25.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
16374
+ (entry) => collectSessionIds(path27.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
15955
16375
  )
15956
16376
  );
15957
16377
  return [...childIdArrays.flat(), ...fileIds];
@@ -16047,13 +16467,13 @@ function inheritsIntoFork(event) {
16047
16467
 
16048
16468
  // src/storage/session-store/fork-session.ts
16049
16469
  async function forkSession(host, id, opts = {}) {
16050
- const parent = await host.load(id);
16051
- let boundary = parent.events.length - 1;
16470
+ const parentEvents = await host.readRawEvents(id);
16471
+ let boundary = parentEvents.length - 1;
16052
16472
  let targetCheckpoint;
16053
16473
  if (opts.checkpointPromptIndex !== void 0) {
16054
16474
  boundary = -1;
16055
- for (let i = 0; i < parent.events.length; i++) {
16056
- const event = parent.events[i];
16475
+ for (let i = 0; i < parentEvents.length; i++) {
16476
+ const event = parentEvents[i];
16057
16477
  if (event?.type === "checkpoint" && event.promptIndex === opts.checkpointPromptIndex) {
16058
16478
  boundary = i;
16059
16479
  targetCheckpoint = event;
@@ -16063,15 +16483,18 @@ async function forkSession(host, id, opts = {}) {
16063
16483
  throw new Error(`Checkpoint ${opts.checkpointPromptIndex} not found in session "${id}"`);
16064
16484
  }
16065
16485
  }
16066
- const parentPrefix = parent.events.slice(0, boundary + 1);
16486
+ const parentPrefix = parentEvents.slice(0, boundary + 1);
16067
16487
  const workspaceCheckpoint = targetCheckpoint?.workspaceCheckpoint;
16068
16488
  const checkpointHash = createHash5("sha256").update(parentPrefix.map((event) => JSON.stringify(event)).join("\n") + "\n", "utf8").digest("hex");
16069
16489
  const inherited = parentPrefix.filter(inheritsIntoFork);
16490
+ const start = parentEvents.find(
16491
+ (event) => event.type === "session_start"
16492
+ );
16070
16493
  const writer = await host.create({
16071
16494
  id: "",
16072
- title: parent.metadata.title,
16073
- model: parent.metadata.model,
16074
- provider: parent.metadata.provider
16495
+ title: "",
16496
+ model: start?.model,
16497
+ provider: start?.provider
16075
16498
  });
16076
16499
  try {
16077
16500
  await writer.append({
@@ -16177,6 +16600,17 @@ var SessionLoadCache = class {
16177
16600
  this.entries.clear();
16178
16601
  this.bytes = 0;
16179
16602
  }
16603
+ /**
16604
+ * A hit hands back fresh `messages` / `events` arrays over the cached
16605
+ * contents.
16606
+ *
16607
+ * The entry outlives every caller, and callers treat what they get as their
16608
+ * own: `resume()` passes `messages` straight into a live conversation, and
16609
+ * anything walking `events` may splice it. Returning the cached arrays
16610
+ * themselves let one caller's edit rewrite what the next one loads. The
16611
+ * elements are still shared — copying them would defeat the cache — so
16612
+ * entries remain read-only *contents* behind private containers.
16613
+ */
16180
16614
  getFresh(id, stat10, full) {
16181
16615
  const cached = this.entries.get(id);
16182
16616
  if (!cached || cached.mtimeMs !== stat10.mtimeMs || cached.size !== stat10.size) {
@@ -16184,8 +16618,11 @@ var SessionLoadCache = class {
16184
16618
  }
16185
16619
  this.entries.delete(id);
16186
16620
  this.entries.set(id, cached);
16187
- if (full) return cached.data;
16188
- return { ...cached.data, messages: [] };
16621
+ return {
16622
+ ...cached.data,
16623
+ messages: full ? [...cached.data.messages] : [],
16624
+ events: [...cached.data.events]
16625
+ };
16189
16626
  }
16190
16627
  set(id, stat10, data) {
16191
16628
  this.delete(id);
@@ -16374,6 +16811,9 @@ function stripSnapshotPayload(event) {
16374
16811
  event.messagesOmitted = event.messages.length;
16375
16812
  event.messages = [];
16376
16813
  }
16814
+ function isStrippedSnapshot(event) {
16815
+ return event.messages.length === 0 && typeof event.messagesOmitted === "number" && event.messagesOmitted > 0;
16816
+ }
16377
16817
  function isSessionEventLike(value) {
16378
16818
  return value !== null && typeof value === "object" && typeof value.type === "string" && typeof value.ts === "string";
16379
16819
  }
@@ -16403,7 +16843,12 @@ function replaySessionEvent(params) {
16403
16843
  emitDamaged(params, `Ignored malformed message_updated event at index ${ev.index}`);
16404
16844
  }
16405
16845
  } else if (ev.type === "messages_replaced" && ev.version === 1) {
16406
- if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
16846
+ if (isStrippedSnapshot(ev)) {
16847
+ emitDamaged(
16848
+ params,
16849
+ `Ignored messages_replaced event whose payload was stripped before persistence (${String(ev.messagesOmitted)} messages)`
16850
+ );
16851
+ } else if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
16407
16852
  exactJournalActive = true;
16408
16853
  } else {
16409
16854
  emitDamaged(params, "Ignored malformed messages_replaced event");
@@ -16419,9 +16864,16 @@ function replaySessionEvent(params) {
16419
16864
  emitDamaged(params, `Ignored malformed messages_dropped event (count ${String(ev.count)})`);
16420
16865
  }
16421
16866
  } else if (ev.type === "context_snapshot") {
16422
- if (!applyContextSnapshot(messages, openToolUses, ev.messages)) {
16867
+ if (isStrippedSnapshot(ev)) {
16868
+ emitDamaged(
16869
+ params,
16870
+ `Ignored context_snapshot event whose payload was stripped before persistence (${String(ev.messagesOmitted)} messages)`
16871
+ );
16872
+ } else if (!applyContextSnapshot(messages, openToolUses, ev.messages)) {
16423
16873
  emitDamaged(params, "Ignored malformed context_snapshot event");
16424
16874
  }
16875
+ } else if (ev.type === "messages_replaced" || ev.type === "message_appended" || ev.type === "message_updated" || ev.type === "messages_dropped") {
16876
+ emitDamaged(params, `Ignored ${ev.type} event with unsupported version`);
16425
16877
  } else if (!exactJournalActive && ev.type === "user_input") {
16426
16878
  openToolUses.clear();
16427
16879
  messages.push({ role: "user", content: ev.content, ts: ev.ts });
@@ -16472,15 +16924,15 @@ function emitDamaged(params, detail) {
16472
16924
 
16473
16925
  // src/storage/session-store/prune-helpers.ts
16474
16926
  import * as fsp13 from "node:fs/promises";
16475
- import * as path26 from "node:path";
16927
+ import * as path28 from "node:path";
16476
16928
  function isPrunableSessionJsonl(name) {
16477
- return name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
16929
+ return isSessionTranscriptFileName(name);
16478
16930
  }
16479
16931
  async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
16480
16932
  const cutoff = Date.now() - maxAgeDays * 864e5;
16481
16933
  let deleted = 0;
16482
16934
  const pruneFile = async (dir, name, prefix) => {
16483
- const jsonlPath = path26.join(dir, name);
16935
+ const jsonlPath = path28.join(dir, name);
16484
16936
  try {
16485
16937
  const stat10 = await fsp13.stat(jsonlPath);
16486
16938
  if (stat10.mtimeMs >= cutoff) return;
@@ -16499,7 +16951,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
16499
16951
  continue;
16500
16952
  }
16501
16953
  if (!entry.isDirectory()) continue;
16502
- const dateDir = path26.join(storeDir, entry.name);
16954
+ const dateDir = path28.join(storeDir, entry.name);
16503
16955
  const files = await fsp13.readdir(dateDir, { withFileTypes: true }).catch(() => []);
16504
16956
  for (const file of files) {
16505
16957
  if (!file.isFile() || !isPrunableSessionJsonl(file.name)) continue;
@@ -16508,7 +16960,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
16508
16960
  }
16509
16961
  for (const entry of entries) {
16510
16962
  if (!entry.isDirectory()) continue;
16511
- const dateDir = path26.join(storeDir, entry.name);
16963
+ const dateDir = path28.join(storeDir, entry.name);
16512
16964
  try {
16513
16965
  const remaining = await fsp13.readdir(dateDir);
16514
16966
  if (remaining.length === 0) {
@@ -16861,9 +17313,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
16861
17313
  static LIST_SCAN_CONCURRENCY = 32;
16862
17314
  constructor(opts) {
16863
17315
  this.dir = opts.dir;
16864
- this.projectRoot = opts.projectRoot ? path27.resolve(opts.projectRoot) : void 0;
17316
+ this.projectRoot = opts.projectRoot ? path29.resolve(opts.projectRoot) : void 0;
16865
17317
  this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
16866
- rootDir: path27.join(this.dir, "_cas"),
17318
+ rootDir: path29.join(this.dir, "_cas"),
16867
17319
  projectRoot: this.projectRoot
16868
17320
  }) : void 0;
16869
17321
  this.events = opts.events;
@@ -16874,7 +17326,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
16874
17326
  this.onAppendBatch = opts.onAppendBatch;
16875
17327
  const builtRuntime = import.meta.url.includes("/dist/");
16876
17328
  this.catalogClient = this.projectRoot && (builtRuntime || process.env["WRONGSTACK_SESSION_CATALOG_FORCE"] === "1") && resolveSessionCatalogProjectServerUrl() ? new SessionCatalogProjectClient({
16877
- projectDir: path27.dirname(this.dir),
17329
+ projectDir: path29.dirname(this.dir),
16878
17330
  projectRoot: this.projectRoot
16879
17331
  }) : void 0;
16880
17332
  }
@@ -16906,7 +17358,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
16906
17358
  // ── Storage event helpers ───────────────────────────────────────────────────
16907
17359
  /** Absolute path to the session index file. */
16908
17360
  get indexFile() {
16909
- return path27.join(this.dir, "_index.jsonl");
17361
+ return path29.join(this.dir, "_index.jsonl");
16910
17362
  }
16911
17363
  /** Join session ID to its absolute path within the store directory. */
16912
17364
  sessionPath(id, ext) {
@@ -16997,6 +17449,21 @@ var DefaultSessionStore = class _DefaultSessionStore {
16997
17449
  async fork(id, opts = {}) {
16998
17450
  return forkSession(this, id, opts);
16999
17451
  }
17452
+ /**
17453
+ * Implements {@link SessionForkHost.readRawEvents} — the parent stream a fork
17454
+ * inherits, unmodified.
17455
+ *
17456
+ * Deliberately NOT `load()`: that loader empties superseded snapshot payloads
17457
+ * in place and front-drops events past its retention budget, both of which
17458
+ * are correct for reconstructing a conversation and wrong for copying a
17459
+ * journal prefix into a child. Streaming with an accept-everything predicate
17460
+ * keeps the scrubbing contract (`searchEvents` scrubs each line the same way
17461
+ * `load()` does) without either transformation.
17462
+ */
17463
+ async readRawEvents(id) {
17464
+ const hits = await this.searchEvents(id, () => true);
17465
+ return hits.map((hit) => hit.event);
17466
+ }
17000
17467
  /**
17001
17468
  * Capture the deterministic post-tool workspace identity through the store-owned CAS.
17002
17469
  */
@@ -17085,14 +17552,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
17085
17552
  ts: (/* @__PURE__ */ new Date()).toISOString()
17086
17553
  });
17087
17554
  }
17555
+ const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
17088
17556
  const resumedData = {
17089
17557
  ...data,
17090
17558
  ...resumeValidation ? { resumeValidation } : {},
17091
- ...noticeMessages.length > 0 ? { messages: [...data.messages, ...noticeMessages] } : {}
17559
+ messages: [...carriedMessages, ...noticeMessages]
17092
17560
  };
17093
17561
  let handle;
17094
17562
  try {
17095
- handle = await fsp17.open(file, "a", 384);
17563
+ handle = await openSessionForAppend(file);
17096
17564
  } catch (err) {
17097
17565
  emitSessionStoreError(this.events, canonicalId, file, "resume", toErrorMessage(err), false);
17098
17566
  throw new Error(
@@ -17119,7 +17587,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
17119
17587
  // Shard directory (sessions/<date>/) — must match create() so the
17120
17588
  // .summary.json sidecar lands next to the JSONL instead of the
17121
17589
  // sessions root (where summaryFor() would never find it).
17122
- dir: path27.dirname(file),
17590
+ dir: path29.dirname(file),
17123
17591
  filePath: file,
17124
17592
  secretScrubber: this.secretScrubber,
17125
17593
  checkpointCas: this.checkpointCas,
@@ -17271,10 +17739,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
17271
17739
  const limit = criteria.limit ?? 100;
17272
17740
  if (this.catalogClient) {
17273
17741
  const records = await this.catalogClient.call("list_catalog", {
17274
- limit: Math.min(1e3, Math.max(limit, 100)),
17275
- ...criteria.titleContains ? { search: criteria.titleContains } : {}
17742
+ limit,
17743
+ ...criteria
17276
17744
  });
17277
- return this.scrubSummaries(records).filter((summary) => matchesSessionFilter(summary, criteria)).slice(0, limit);
17745
+ return this.scrubSummaries(records);
17278
17746
  }
17279
17747
  try {
17280
17748
  const indexed = await this.readIndex();
@@ -17499,11 +17967,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
17499
17967
  return shardKeys;
17500
17968
  }
17501
17969
  async readOrBuildShardManifest(shardKey) {
17502
- const cached = this.shardManifestCache.get(shardKey);
17503
- if (cached) return cached;
17504
17970
  const manifestPath = this.shardManifestPath(shardKey);
17971
+ const cached = await this.freshShardManifestCacheEntry(shardKey, manifestPath);
17972
+ if (cached) return cached;
17505
17973
  return withFileLock(manifestPath, async () => {
17506
- const lockedCached = this.shardManifestCache.get(shardKey);
17974
+ const lockedCached = await this.freshShardManifestCacheEntry(shardKey, manifestPath);
17507
17975
  if (lockedCached) return lockedCached;
17508
17976
  const entry = await readOrBuildShardManifestEntry({
17509
17977
  shardKey,
@@ -17514,12 +17982,40 @@ var DefaultSessionStore = class _DefaultSessionStore {
17514
17982
  summaryHeaderFor: (ref) => this.summaryHeaderFor(ref),
17515
17983
  summaryFor: (id) => this.summaryFor(id)
17516
17984
  });
17517
- this.shardManifestCache.set(shardKey, entry);
17985
+ try {
17986
+ const stat10 = await fsp17.stat(manifestPath);
17987
+ this.shardManifestCache.set(shardKey, {
17988
+ entry,
17989
+ mtimeMs: stat10.mtimeMs,
17990
+ size: stat10.size,
17991
+ ino: stat10.ino
17992
+ });
17993
+ } catch {
17994
+ this.shardManifestCache.delete(shardKey);
17995
+ }
17518
17996
  return entry;
17519
17997
  });
17520
17998
  }
17999
+ /**
18000
+ * Shard manifests are invalidated by other store processes via atomic
18001
+ * delete/rebuild. Validate the in-memory projection against the persisted
18002
+ * file so one long-lived process cannot retain another process's stale view.
18003
+ */
18004
+ async freshShardManifestCacheEntry(shardKey, manifestPath) {
18005
+ const cached = this.shardManifestCache.get(shardKey);
18006
+ if (!cached) return void 0;
18007
+ try {
18008
+ const stat10 = await fsp17.stat(manifestPath);
18009
+ if (stat10.mtimeMs === cached.mtimeMs && stat10.size === cached.size && stat10.ino === cached.ino) {
18010
+ return cached.entry;
18011
+ }
18012
+ } catch {
18013
+ }
18014
+ this.shardManifestCache.delete(shardKey);
18015
+ return void 0;
18016
+ }
17521
18017
  async collectSessionFilesInShard(shardKey) {
17522
- const dir = shardKey ? path27.join(this.dir, shardKey) : this.dir;
18018
+ const dir = shardKey ? path29.join(this.dir, shardKey) : this.dir;
17523
18019
  const entries = await this.collectSessionFiles(dir, shardKey);
17524
18020
  return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
17525
18021
  }
@@ -17797,6 +18293,23 @@ var DefaultSessionStore = class _DefaultSessionStore {
17797
18293
  });
17798
18294
  }
17799
18295
  };
18296
+ async function openSessionForAppend(file) {
18297
+ const handle = await fsp17.open(file, "a+", 384);
18298
+ try {
18299
+ const stat10 = await handle.stat();
18300
+ if (stat10.size > 0) {
18301
+ const tail = Buffer.allocUnsafe(1);
18302
+ const { bytesRead } = await handle.read(tail, 0, 1, stat10.size - 1);
18303
+ if (bytesRead === 1 && tail[0] !== 10) {
18304
+ await handle.appendFile("\n", "utf8");
18305
+ }
18306
+ }
18307
+ return handle;
18308
+ } catch (err) {
18309
+ await handle.close().catch(() => void 0);
18310
+ throw err;
18311
+ }
18312
+ }
17800
18313
 
17801
18314
  // src/coordination/director-session.ts
17802
18315
  function makeDirectorSessionFactory(opts) {
@@ -17806,9 +18319,9 @@ function makeDirectorSessionFactory(opts) {
17806
18319
  let dir;
17807
18320
  if (opts.store) {
17808
18321
  store = opts.store;
17809
- dir = opts.sessionsRoot ? path28.join(opts.sessionsRoot, runId) : "(caller-managed)";
18322
+ dir = opts.sessionsRoot ? path30.join(opts.sessionsRoot, runId) : "(caller-managed)";
17810
18323
  } else if (opts.sessionsRoot) {
17811
- dir = path28.join(opts.sessionsRoot, runId);
18324
+ dir = path30.join(opts.sessionsRoot, runId);
17812
18325
  store = new DefaultSessionStore({ dir });
17813
18326
  } else {
17814
18327
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -17832,7 +18345,7 @@ function makeDirectorSessionFactory(opts) {
17832
18345
  }
17833
18346
  async function readDirectorSubagentSession(args) {
17834
18347
  if (!args.sessionsRoot) return null;
17835
- const filePath = path28.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
18348
+ const filePath = path30.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
17836
18349
  let raw;
17837
18350
  try {
17838
18351
  raw = await fsp18.readFile(filePath, "utf8");
@@ -18286,12 +18799,97 @@ var FallbackProfileManager = class {
18286
18799
  return this.resolveRefs([ref], exclude);
18287
18800
  }
18288
18801
  /**
18289
- * Resolve every usable configured target as an uncapped last-resort chain.
18802
+ * Resolve every usable configured target as a bounded last-resort chain.
18290
18803
  * Normal smart defaults stay bounded; callers append this only after the
18291
- * preferred chain and only when automatic fallback is enabled.
18804
+ * preferred chain and only when automatic fallback is enabled. The cap
18805
+ * ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
18806
+ * from producing a degenerate chain of doomed requests during a systemic
18807
+ * outage — by this point the smart default, bridge, and default profile
18808
+ * have already failed.
18292
18809
  */
18293
18810
  resolveAllConfigured(exclude) {
18294
- return this.smartDefault(exclude, Number.POSITIVE_INFINITY);
18811
+ return this.smartDefault(exclude, this.lastResortCap());
18812
+ }
18813
+ /**
18814
+ * Effective cap for the last-resort append. Reads the user-configurable
18815
+ * {@link Config.fallbackMaxLastResortCandidates} when set and valid;
18816
+ * otherwise falls back to the compiled-in default
18817
+ * {@link MAX_LAST_RESORT_CANDIDATES}.
18818
+ */
18819
+ lastResortCap() {
18820
+ const configured = this.config.fallbackMaxLastResortCandidates;
18821
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
18822
+ return Math.floor(configured);
18823
+ }
18824
+ return MAX_LAST_RESORT_CANDIDATES;
18825
+ }
18826
+ /**
18827
+ * Build the complete fallback candidate chain shared by the agent-loop
18828
+ * extension and the one-shot orchestrator. Centralizes the bridge → primary →
18829
+ * selected → default-profile → all-configured ladder and the fromExplicitSource
18830
+ * gate so both consumers produce identical ordering and depth semantics.
18831
+ *
18832
+ * Layering (each step deduped against all prior):
18833
+ * 1. Bridge (emergency continuity route).
18834
+ * 2. Configured primary, when the live context drifted from it.
18835
+ * 3. The selected chain (explicit refs → named profile → smart default),
18836
+ * via {@link resolveEffective}.
18837
+ * 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
18838
+ * 5. Every other configured provider — last resort, ONLY when the chain was
18839
+ * auto-derived AND `effectiveFallbackAuto` is true.
18840
+ *
18841
+ * Returns the empty chain when `closedWorld` is true and no explicit
18842
+ * refs/profile resolved — a model allowlist never leaks to unlisted models.
18843
+ *
18844
+ * @internal caller-aware options (primary, closedWorld) are accepted because
18845
+ * the agent loop has context the manager does not own; the resolution
18846
+ * pipeline itself is identical for both callers.
18847
+ */
18848
+ resolveCandidates(current, opts = {}) {
18849
+ const configuredPrimary = opts.primary ?? {
18850
+ providerId: this.config.provider,
18851
+ model: this.config.model
18852
+ };
18853
+ const configFallbackAuto = this.config.fallbackAuto;
18854
+ const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
18855
+ const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
18856
+ const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
18857
+ const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
18858
+ const fromExplicitSource = explicitUsable || profileUsable;
18859
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
18860
+ fallbackModels: explicitRefs,
18861
+ fallbackProfile: opts.fallbackProfile,
18862
+ fallbackAuto: effectiveFallbackAuto,
18863
+ exclude: current
18864
+ });
18865
+ const candidates = [];
18866
+ if (opts.closedWorld) {
18867
+ candidates.push(...selectedChain);
18868
+ } else {
18869
+ candidates.push(...this.resolveBridge(current));
18870
+ if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
18871
+ candidates.push({
18872
+ providerId: configuredPrimary.providerId,
18873
+ model: configuredPrimary.model,
18874
+ providerSwitched: configuredPrimary.providerId !== current.providerId
18875
+ });
18876
+ }
18877
+ candidates.push(...selectedChain);
18878
+ if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
18879
+ candidates.push(...this.resolve("default", { exclude: current }));
18880
+ }
18881
+ if (!fromExplicitSource && effectiveFallbackAuto) {
18882
+ const cap = this.lastResortCap();
18883
+ if (cap > 0) {
18884
+ const usedKeys = new Set(
18885
+ candidates.map((c) => `${c.providerId}/${c.model}`)
18886
+ );
18887
+ const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
18888
+ candidates.push(...lastResort);
18889
+ }
18890
+ }
18891
+ }
18892
+ return dedupeChain(candidates, current);
18295
18893
  }
18296
18894
  // ── Provider availability (read-only) ──────────────────────────────────
18297
18895
  checkProvider(providerId) {
@@ -18415,7 +19013,20 @@ var FallbackProfileManager = class {
18415
19013
  );
18416
19014
  }
18417
19015
  };
19016
+ var MAX_LAST_RESORT_CANDIDATES = 12;
18418
19017
  var FREEZER_EMPTY = Object.freeze([]);
19018
+ function dedupeChain(entries, current) {
19019
+ const seen = /* @__PURE__ */ new Set();
19020
+ const currentKey = `${current.providerId}/${current.model}`;
19021
+ return Object.freeze(
19022
+ entries.filter((entry) => {
19023
+ const key = `${entry.providerId}/${entry.model}`;
19024
+ if (key === currentKey || seen.has(key)) return false;
19025
+ seen.add(key);
19026
+ return true;
19027
+ })
19028
+ );
19029
+ }
18419
19030
 
18420
19031
  // src/core/fallback-model.ts
18421
19032
  function fallbackProfileChain(config, profileName) {
@@ -21054,7 +21665,7 @@ var Director = class _Director {
21054
21665
  // src/coordination/fleet-manager.ts
21055
21666
  import { randomUUID as randomUUID15 } from "node:crypto";
21056
21667
  import * as fsp20 from "node:fs/promises";
21057
- import * as path29 from "node:path";
21668
+ import * as path31 from "node:path";
21058
21669
  var FleetManager = class {
21059
21670
  /** The fleet-wide event bus. */
21060
21671
  fleet;
@@ -21387,7 +21998,7 @@ var FleetManager = class {
21387
21998
  })),
21388
21999
  usage: this.usage.snapshot()
21389
22000
  };
21390
- await fsp20.mkdir(path29.dirname(this.manifestPath), { recursive: true });
22001
+ await fsp20.mkdir(path31.dirname(this.manifestPath), { recursive: true });
21391
22002
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
21392
22003
  return this.manifestPath;
21393
22004
  }
@@ -21550,7 +22161,7 @@ var FleetManager = class {
21550
22161
  };
21551
22162
 
21552
22163
  // src/coordination/remote-mailbox.ts
21553
- import * as path33 from "node:path";
22164
+ import * as path35 from "node:path";
21554
22165
 
21555
22166
  // src/coordination/mailbox-constants.ts
21556
22167
  var HQ_MAILBOX_SNAPSHOT_MIN_INTERVAL_MS = 1e4;
@@ -21560,13 +22171,13 @@ var UNREAD_CHECK_MIN_INTERVAL_MS = 1e3;
21560
22171
  import { spawn as spawn5 } from "node:child_process";
21561
22172
  import * as fs4 from "node:fs";
21562
22173
  import * as net2 from "node:net";
21563
- import * as path31 from "node:path";
22174
+ import * as path33 from "node:path";
21564
22175
  import { fileURLToPath as fileURLToPath4 } from "node:url";
21565
22176
 
21566
22177
  // src/coordination/mailbox-project-server-endpoint.ts
21567
22178
  import { createHash as createHash6 } from "node:crypto";
21568
22179
  import * as os3 from "node:os";
21569
- import * as path30 from "node:path";
22180
+ import * as path32 from "node:path";
21570
22181
 
21571
22182
  // src/coordination/mailbox-project-server-protocol.ts
21572
22183
  var MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION = 4;
@@ -21599,7 +22210,7 @@ function encodeMailboxProjectServerMessage(message) {
21599
22210
  // src/coordination/mailbox-project-server-endpoint.ts
21600
22211
  var MAILBOX_PROJECT_SERVER_METADATA_FILE = ".mailbox-server.json";
21601
22212
  function normalizeLocalPath(value) {
21602
- const resolved = path30.resolve(value);
22213
+ const resolved = path32.resolve(value);
21603
22214
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
21604
22215
  }
21605
22216
  function mailboxProjectServerKey(projectDir) {
@@ -21610,14 +22221,14 @@ function mailboxProjectServerEndpoint(projectDir) {
21610
22221
  if (process.platform === "win32") {
21611
22222
  return `\\\\.\\pipe\\wrongstack-mailbox-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
21612
22223
  }
21613
- return path30.join(
22224
+ return path32.join(
21614
22225
  os3.tmpdir(),
21615
22226
  `wsmb-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}`,
21616
22227
  `${key}.sock`
21617
22228
  );
21618
22229
  }
21619
22230
  function mailboxProjectServerMetadataPath(projectDir) {
21620
- return path30.join(path30.resolve(projectDir), MAILBOX_PROJECT_SERVER_METADATA_FILE);
22231
+ return path32.join(path32.resolve(projectDir), MAILBOX_PROJECT_SERVER_METADATA_FILE);
21621
22232
  }
21622
22233
 
21623
22234
  // src/coordination/mailbox-project-server-client.ts
@@ -21628,7 +22239,7 @@ var DEFAULT_HEARTBEAT_INTERVAL_MS = 1e4;
21628
22239
  var AUTH_RETRY_DELAY_MS = 150;
21629
22240
  var AUTH_RETRY_MAX_ATTEMPTS = 13;
21630
22241
  function normalizePath(value) {
21631
- const resolved = path31.resolve(value);
22242
+ const resolved = path33.resolve(value);
21632
22243
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
21633
22244
  }
21634
22245
  function resolveProjectServerUrl() {
@@ -21656,7 +22267,7 @@ function isUnauthorizedMailboxError(error) {
21656
22267
  var MailboxProjectServerConnection = class {
21657
22268
  constructor(projectDir) {
21658
22269
  this.projectDir = projectDir;
21659
- this.projectDir = path31.resolve(projectDir);
22270
+ this.projectDir = path33.resolve(projectDir);
21660
22271
  this.endpoint = mailboxProjectServerEndpoint(this.projectDir);
21661
22272
  this.state = {
21662
22273
  status: isMailboxProjectServerAvailable() ? "offline" : "unavailable",
@@ -22334,9 +22945,9 @@ var CredentialVerifyThrottle = class {
22334
22945
  var credentialVerifyThrottle = new CredentialVerifyThrottle();
22335
22946
 
22336
22947
  // src/coordination/global-mailbox-paths.ts
22337
- import * as path32 from "node:path";
22948
+ import * as path34 from "node:path";
22338
22949
  function resolveProjectDir(projectRoot, globalRoot) {
22339
- return path32.join(globalRoot, "projects", projectSlug(projectRoot));
22950
+ return path34.join(globalRoot, "projects", projectSlug(projectRoot));
22340
22951
  }
22341
22952
 
22342
22953
  // src/coordination/sqlite-mailbox.ts
@@ -22386,8 +22997,8 @@ var RemoteMailbox = class {
22386
22997
  hqPublisher,
22387
22998
  eventEmitter
22388
22999
  } : optionsOrProjectDir;
22389
- this.projectDir = path33.resolve(options.projectDir);
22390
- this.messagePath = path33.join(this.projectDir, SQLITE_MAILBOX_FILE);
23000
+ this.projectDir = path35.resolve(options.projectDir);
23001
+ this.messagePath = path35.join(this.projectDir, SQLITE_MAILBOX_FILE);
22391
23002
  this.registryPath = this.messagePath;
22392
23003
  this.clientRegistryPath = this.messagePath;
22393
23004
  this.events = options.events;
@@ -22633,7 +23244,7 @@ var RemoteMailbox = class {
22633
23244
  this.hqEventPending.clear();
22634
23245
  return;
22635
23246
  }
22636
- const mailboxId = `${path33.basename(this.projectDir)}:mailbox`;
23247
+ const mailboxId = `${path35.basename(this.projectDir)}:mailbox`;
22637
23248
  const inFlight = this.query({ includeDeleted: true, limit: 100 }).then((messages) => {
22638
23249
  const message = messages.find((candidate) => candidate.id === event.messageId);
22639
23250
  const action = event.type === "message.sent" ? "message.sent" : "message.updated";
@@ -22656,7 +23267,7 @@ var RemoteMailbox = class {
22656
23267
  if (!publisher || !event.startsWith("mailbox.agent_") && !event.startsWith("mailbox.client_")) {
22657
23268
  return;
22658
23269
  }
22659
- const mailboxId = `${path33.basename(this.projectDir)}:mailbox`;
23270
+ const mailboxId = `${path35.basename(this.projectDir)}:mailbox`;
22660
23271
  const record = typeof payload === "object" && payload !== null ? payload : {};
22661
23272
  const agentId = typeof record["agentId"] === "string" ? record["agentId"] : void 0;
22662
23273
  const action = event === "mailbox.agent_registered" ? "agent.registered" : event === "mailbox.agent_heartbeat" ? "agent.heartbeat" : event === "mailbox.agent_deregistered" ? "agent.deregistered" : void 0;
@@ -22679,7 +23290,7 @@ function createProjectMailbox(options) {
22679
23290
  return new RemoteMailbox(options);
22680
23291
  }
22681
23292
  function getSharedProjectMailbox(projectDir, events, hqPublisher) {
22682
- const key = path33.resolve(projectDir);
23293
+ const key = path35.resolve(projectDir);
22683
23294
  let projectCache = sharedRemoteMailboxes.get(key);
22684
23295
  if (!projectCache) {
22685
23296
  projectCache = {
@@ -25047,16 +25658,16 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
25047
25658
  throw validationError(`routePath must not start with '?' (got ${JSON.stringify(routePath)})`);
25048
25659
  }
25049
25660
  const queryIndex = url.indexOf("?");
25050
- const path38 = queryIndex === -1 ? url : url.slice(0, queryIndex);
25661
+ const path40 = queryIndex === -1 ? url : url.slice(0, queryIndex);
25051
25662
  if (actor !== void 0) {
25052
- const requiredCapability = requiredCredentialCapability(method, path38);
25663
+ const requiredCapability = requiredCredentialCapability(method, path40);
25053
25664
  if (requiredCapability === void 0) {
25054
25665
  writeJson(response, 403, {
25055
- error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${path38}` }
25666
+ error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${path40}` }
25056
25667
  });
25057
25668
  return;
25058
25669
  }
25059
- if (path38 !== "/mailbox/send" && !hasMailboxCapability(actor, requiredCapability)) {
25670
+ if (path40 !== "/mailbox/send" && !hasMailboxCapability(actor, requiredCapability)) {
25060
25671
  writeJson(response, 403, {
25061
25672
  error: {
25062
25673
  code: "FORBIDDEN",
@@ -25066,7 +25677,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
25066
25677
  return;
25067
25678
  }
25068
25679
  }
25069
- if (method === "POST" && path38 === "/mailbox/send") {
25680
+ if (method === "POST" && path40 === "/mailbox/send") {
25070
25681
  const input = validateSend(await readJsonBody(request, maxBodyBytes), actor?.actorId);
25071
25682
  if (actor !== void 0) {
25072
25683
  const requiredCapability = requiredSendCapability(input.type);
@@ -25084,7 +25695,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
25084
25695
  writeJson(response, 201, await mailbox.send(input));
25085
25696
  return;
25086
25697
  }
25087
- if (method === "POST" && path38 === "/mailbox/query") {
25698
+ if (method === "POST" && path40 === "/mailbox/query") {
25088
25699
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
25089
25700
  if ("error" in queryContext) {
25090
25701
  writeJson(response, 400, { error: queryContext.error });
@@ -25104,7 +25715,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
25104
25715
  writeJson(response, 200, { data: projected, count: projected.length });
25105
25716
  return;
25106
25717
  }
25107
- if (method === "POST" && path38 === "/mailbox/check") {
25718
+ if (method === "POST" && path40 === "/mailbox/check") {
25108
25719
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
25109
25720
  if ("error" in queryContext) {
25110
25721
  writeJson(response, 400, { error: queryContext.error });
@@ -25138,7 +25749,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
25138
25749
  writeJson(response, 200, { data: projected, count: projected.length });
25139
25750
  return;
25140
25751
  }
25141
- if (method === "POST" && path38 === "/mailbox/ack") {
25752
+ if (method === "POST" && path40 === "/mailbox/ack") {
25142
25753
  const input = validateAck(await readJsonBody(request, maxBodyBytes), actor?.actorId);
25143
25754
  if (actor !== void 0) {
25144
25755
  input.readerId = actor.actorId;
@@ -25152,7 +25763,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
25152
25763
  writeJson(response, 200, { updated: projectedAck });
25153
25764
  return;
25154
25765
  }
25155
- if (method === "POST" && path38 === "/mailbox/ack-many") {
25766
+ if (method === "POST" && path40 === "/mailbox/ack-many") {
25156
25767
  const input = validateAckMany(await readJsonBody(request, maxBodyBytes), actor?.actorId);
25157
25768
  if (actor !== void 0) {
25158
25769
  const requestedIds = new Set(input.acks.map((ack) => ack.messageId));
@@ -25170,54 +25781,54 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
25170
25781
  writeJson(response, 200, { updated: projectedMany, count: projectedMany.length });
25171
25782
  return;
25172
25783
  }
25173
- if (method === "POST" && path38 === "/mailbox/unread-count") {
25784
+ if (method === "POST" && path40 === "/mailbox/unread-count") {
25174
25785
  const body = await readJsonBody(request, maxBodyBytes);
25175
25786
  const count = actor === void 0 ? await mailbox.unreadCount(requireString2(body, "forAgentId")) : await unreadCountForActor(mailbox, actor);
25176
25787
  writeJson(response, 200, { count });
25177
25788
  return;
25178
25789
  }
25179
- if (method === "POST" && path38 === "/mailbox/agents/register") {
25790
+ if (method === "POST" && path40 === "/mailbox/agents/register") {
25180
25791
  const input = validateAgentRegistration(await readJsonBody(request, maxBodyBytes), actor);
25181
25792
  await mailbox.registerAgent(input);
25182
25793
  writeJson(response, 200, { ok: true });
25183
25794
  return;
25184
25795
  }
25185
- if (method === "POST" && path38 === "/mailbox/agents/heartbeat") {
25796
+ if (method === "POST" && path40 === "/mailbox/agents/heartbeat") {
25186
25797
  const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor?.actorId);
25187
25798
  if (actor !== void 0) input.agentId = actor.actorId;
25188
25799
  await mailbox.heartbeat(input);
25189
25800
  writeJson(response, 200, { ok: true });
25190
25801
  return;
25191
25802
  }
25192
- if (method === "POST" && path38 === "/mailbox/register-client") {
25803
+ if (method === "POST" && path40 === "/mailbox/register-client") {
25193
25804
  await mailbox.registerClient(
25194
25805
  validateClientRegistration(await readJsonBody(request, maxBodyBytes))
25195
25806
  );
25196
25807
  writeJson(response, 200, { ok: true });
25197
25808
  return;
25198
25809
  }
25199
- if (method === "POST" && path38 === "/mailbox/heartbeat") {
25810
+ if (method === "POST" && path40 === "/mailbox/heartbeat") {
25200
25811
  await mailbox.clientHeartbeat(
25201
25812
  validateClientHeartbeat(await readJsonBody(request, maxBodyBytes))
25202
25813
  );
25203
25814
  writeJson(response, 200, { ok: true });
25204
25815
  return;
25205
25816
  }
25206
- if (method === "POST" && path38 === "/mailbox/purge-clients") {
25817
+ if (method === "POST" && path40 === "/mailbox/purge-clients") {
25207
25818
  writeJson(response, 200, { ok: true, purged: await mailbox.purgeClients() });
25208
25819
  return;
25209
25820
  }
25210
- if (method === "GET" && path38 === "/mailbox/agents") {
25821
+ if (method === "GET" && path40 === "/mailbox/agents") {
25211
25822
  const agents = await mailbox.getAgentStatuses();
25212
25823
  writeJson(response, 200, { data: agents, count: agents.length });
25213
25824
  return;
25214
25825
  }
25215
- if (method === "GET" && path38 === "/mailbox/agents/online") {
25826
+ if (method === "GET" && path40 === "/mailbox/agents/online") {
25216
25827
  const agents = await mailbox.getOnlineAgents();
25217
25828
  writeJson(response, 200, { data: agents, count: agents.length });
25218
25829
  return;
25219
25830
  }
25220
- if (method === "GET" && path38 === "/mailbox/events" && eventEmitter) {
25831
+ if (method === "GET" && path40 === "/mailbox/events" && eventEmitter) {
25221
25832
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
25222
25833
  if ("error" in queryContext) {
25223
25834
  writeJson(response, 400, { error: queryContext.error });
@@ -25326,25 +25937,25 @@ function requiredSendCapability(type) {
25326
25937
  }
25327
25938
  return "mail.send.informational";
25328
25939
  }
25329
- function requiredCredentialCapability(method, path38) {
25330
- if (method === "POST" && path38 === "/mailbox/send") return "mail.send.informational";
25331
- if (method === "POST" && (path38 === "/mailbox/query" || path38 === "/mailbox/check")) {
25940
+ function requiredCredentialCapability(method, path40) {
25941
+ if (method === "POST" && path40 === "/mailbox/send") return "mail.send.informational";
25942
+ if (method === "POST" && (path40 === "/mailbox/query" || path40 === "/mailbox/check")) {
25332
25943
  return "mail.read.self";
25333
25944
  }
25334
- if (method === "POST" && (path38 === "/mailbox/ack" || path38 === "/mailbox/ack-many")) {
25945
+ if (method === "POST" && (path40 === "/mailbox/ack" || path40 === "/mailbox/ack-many")) {
25335
25946
  return "mail.ack.self";
25336
25947
  }
25337
- if (method === "POST" && path38 === "/mailbox/unread-count") return "mail.read.self";
25338
- if (method === "POST" && path38 === "/mailbox/agents/register") {
25948
+ if (method === "POST" && path40 === "/mailbox/unread-count") return "mail.read.self";
25949
+ if (method === "POST" && path40 === "/mailbox/agents/register") {
25339
25950
  return "mail.presence.register.self";
25340
25951
  }
25341
- if (method === "POST" && path38 === "/mailbox/agents/heartbeat") {
25952
+ if (method === "POST" && path40 === "/mailbox/agents/heartbeat") {
25342
25953
  return "mail.presence.heartbeat.self";
25343
25954
  }
25344
- if (method === "GET" && (path38 === "/mailbox/agents" || path38 === "/mailbox/agents/online")) {
25955
+ if (method === "GET" && (path40 === "/mailbox/agents" || path40 === "/mailbox/agents/online")) {
25345
25956
  return "mail.presence.read";
25346
25957
  }
25347
- if (method === "GET" && path38 === "/mailbox/events") return "mail.events.self";
25958
+ if (method === "GET" && path40 === "/mailbox/events") return "mail.events.self";
25348
25959
  return void 0;
25349
25960
  }
25350
25961
  function extractEventTimestamp(event) {
@@ -25547,11 +26158,11 @@ var NULL_FLEET_BUS = new FleetBus();
25547
26158
 
25548
26159
  // src/coordination/package-author-tracker.ts
25549
26160
  import * as fs5 from "node:fs/promises";
25550
- import * as path34 from "node:path";
26161
+ import * as path36 from "node:path";
25551
26162
  var DEFAULT_MAX_ENTRIES2 = 1e4;
25552
26163
  var LOG_FILENAME2 = "package-authors.json";
25553
26164
  function logPath2(storageDir) {
25554
- return path34.join(storageDir, LOG_FILENAME2);
26165
+ return path36.join(storageDir, LOG_FILENAME2);
25555
26166
  }
25556
26167
  async function loadLog2(storageDir, projectRoot) {
25557
26168
  try {
@@ -25573,7 +26184,7 @@ async function saveLog2(storageDir, log) {
25573
26184
  `);
25574
26185
  }
25575
26186
  function detectEcosystem(manifestPath) {
25576
- const name = path34.win32.basename(manifestPath).toLowerCase();
26187
+ const name = path36.win32.basename(manifestPath).toLowerCase();
25577
26188
  if (name === "package.json") return "npm";
25578
26189
  if (name === "go.mod") return "go";
25579
26190
  if (name === "cargo.toml") return "cargo";
@@ -26456,12 +27067,12 @@ import { randomBytes as randomBytes2 } from "node:crypto";
26456
27067
  import * as fsp21 from "node:fs/promises";
26457
27068
  import { execFile } from "node:child_process";
26458
27069
  import * as os4 from "node:os";
26459
- import * as path35 from "node:path";
27070
+ import * as path37 from "node:path";
26460
27071
  var MAILBOX_BRIDGE_LOCK_FILENAME = ".mailbox-bridge.lock";
26461
27072
  var MAILBOX_BRIDGE_TOKEN_FILENAME = ".mailbox.token";
26462
27073
  async function acquireOrJoin(opts) {
26463
- const lockPath = path35.join(opts.projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
26464
- const tokenPath = path35.join(opts.projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
27074
+ const lockPath = path37.join(opts.projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
27075
+ const tokenPath = path37.join(opts.projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
26465
27076
  const inspected = await readLockForInspection(lockPath);
26466
27077
  if (inspected.kind === "live") {
26467
27078
  const existing = inspected.lock;
@@ -26489,8 +27100,8 @@ async function acquireOrJoin(opts) {
26489
27100
  return { kind: "acquired", lock: tentative, tokenPath };
26490
27101
  }
26491
27102
  async function finalize(projectDir, tentative, boundPort) {
26492
- const lockPath = path35.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
26493
- const tokenPath = path35.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
27103
+ const lockPath = path37.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
27104
+ const tokenPath = path37.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
26494
27105
  const finalized = {
26495
27106
  ...tentative,
26496
27107
  port: boundPort,
@@ -26501,8 +27112,8 @@ async function finalize(projectDir, tentative, boundPort) {
26501
27112
  return finalized;
26502
27113
  }
26503
27114
  async function release(projectDir, generation) {
26504
- const lockPath = path35.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
26505
- const tokenPath = path35.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
27115
+ const lockPath = path37.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
27116
+ const tokenPath = path37.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
26506
27117
  try {
26507
27118
  const raw = await fsp21.readFile(lockPath, "utf-8");
26508
27119
  const parsed = JSON.parse(raw);
@@ -26536,7 +27147,7 @@ async function readLockForInspection(lockPath) {
26536
27147
  return { kind: "live", lock: parsed };
26537
27148
  }
26538
27149
  async function readLiveLock(projectDir) {
26539
- const lockPath = path35.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
27150
+ const lockPath = path37.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
26540
27151
  const result = await readLockForInspection(lockPath);
26541
27152
  if (result.kind === "live") {
26542
27153
  return { kind: "live", lock: result.lock };
@@ -26547,7 +27158,7 @@ async function readLiveLock(projectDir) {
26547
27158
  return { kind: "absent" };
26548
27159
  }
26549
27160
  async function atomicWriteJson(targetPath, value) {
26550
- const dir = path35.dirname(targetPath);
27161
+ const dir = path37.dirname(targetPath);
26551
27162
  await fsp21.mkdir(dir, { recursive: true });
26552
27163
  const tmp = `${targetPath}.tmp.${process.pid}.${randomBytes2(4).toString("hex")}`;
26553
27164
  const body = JSON.stringify(value, null, 2) + "\n";
@@ -26710,8 +27321,8 @@ function extractManifestPath(msg) {
26710
27321
  }
26711
27322
  return void 0;
26712
27323
  }
26713
- function isManifestFile(path38) {
26714
- const name = pathBasename(path38).toLowerCase();
27324
+ function isManifestFile(path40) {
27325
+ const name = pathBasename(path40).toLowerCase();
26715
27326
  const manifests = [
26716
27327
  "package.json",
26717
27328
  "package-lock.json",
@@ -27005,7 +27616,7 @@ var AdaptiveConcurrencyController = class {
27005
27616
  // src/coordination/agent-monitor.ts
27006
27617
  import { createReadStream as createReadStream5 } from "node:fs";
27007
27618
  import * as fs6 from "node:fs/promises";
27008
- import * as path36 from "node:path";
27619
+ import * as path38 from "node:path";
27009
27620
  import { createInterface as createInterface5 } from "node:readline";
27010
27621
  var AgentMonitorService = class _AgentMonitorService {
27011
27622
  _fleetBus;
@@ -27109,7 +27720,7 @@ var AgentMonitorService = class _AgentMonitorService {
27109
27720
  return this.getAllSessions();
27110
27721
  }
27111
27722
  async _readTranscriptFile(subagentId) {
27112
- const file = path36.join(this._transcriptsDir, subagentId, "transcript.jsonl");
27723
+ const file = path38.join(this._transcriptsDir, subagentId, "transcript.jsonl");
27113
27724
  const accessible = await fs6.access(file).then(() => true).catch(() => false);
27114
27725
  if (!accessible) return [];
27115
27726
  const out = [];
@@ -27450,12 +28061,12 @@ var AgentMonitorService = class _AgentMonitorService {
27450
28061
  this._writeDrain = drain;
27451
28062
  }
27452
28063
  async _appendToFile(subagentId, line) {
27453
- const dir = path36.join(this._transcriptsDir, subagentId);
28064
+ const dir = path38.join(this._transcriptsDir, subagentId);
27454
28065
  if (!this._ensuredDirs.has(dir)) {
27455
28066
  await fs6.mkdir(dir, { recursive: true });
27456
28067
  this._ensuredDirs.add(dir);
27457
28068
  }
27458
- const filePath = path36.join(dir, "transcript.jsonl");
28069
+ const filePath = path38.join(dir, "transcript.jsonl");
27459
28070
  try {
27460
28071
  await fs6.appendFile(filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
27461
28072
  } catch (error) {
@@ -27848,7 +28459,7 @@ import { randomUUID as randomUUID20 } from "node:crypto";
27848
28459
  // src/coordination/knowledge-graph.ts
27849
28460
  import { randomUUID as randomUUID18 } from "node:crypto";
27850
28461
  import * as fsp22 from "node:fs/promises";
27851
- import * as path37 from "node:path";
28462
+ import * as path39 from "node:path";
27852
28463
  var DEFAULT_MAX_NODES = 2e3;
27853
28464
  var MAX_SUBSCRIPTIONS = 1e3;
27854
28465
  var MAX_PENDING_DELIVERIES_PER_SUBSCRIPTION = 1e3;
@@ -27883,8 +28494,8 @@ var KnowledgeGraph = class _KnowledgeGraph {
27883
28494
  return this.index;
27884
28495
  }
27885
28496
  constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES, compactEveryWrites = Math.max(1e3, maxNodes * 4)) {
27886
- this.filePath = path37.join(sessionDir, "_knowledge_graph");
27887
- this.graphFilePath = path37.join(this.filePath, "graph.jsonl");
28497
+ this.filePath = path39.join(sessionDir, "_knowledge_graph");
28498
+ this.graphFilePath = path39.join(this.filePath, "graph.jsonl");
27888
28499
  this.maxNodes = maxNodes;
27889
28500
  this.compactEveryWrites = Math.max(1, Math.floor(compactEveryWrites));
27890
28501
  }
@@ -30285,6 +30896,8 @@ export {
30285
30896
  DEFAULT_SUBAGENT_BASELINE,
30286
30897
  DELIVERY_AGENTS,
30287
30898
  DEPENDENCY_FILE_PATTERNS,
30899
+ DIRECTIVE_QUARANTINE_MAX_UTILITY,
30900
+ DIRECTIVE_QUARANTINE_MIN_APPLIED,
30288
30901
  DISCOVERY_AGENTS,
30289
30902
  DOMAIN_AGENTS,
30290
30903
  DefaultBrainArbiter,
@@ -30380,6 +30993,7 @@ export {
30380
30993
  collabPauseMiddleware,
30381
30994
  composeDirectorPrompt,
30382
30995
  composeSubagentPrompt,
30996
+ consolidatedDocumentPath,
30383
30997
  createAgentMonitorService,
30384
30998
  createDelegateTool,
30385
30999
  createLedgerGuardBrainArbiter,
@@ -30392,6 +31006,9 @@ export {
30392
31006
  credentialVerifyThrottle,
30393
31007
  detectEcosystem,
30394
31008
  detectLearnedConflicts,
31009
+ directiveTrials,
31010
+ directiveUtility,
31011
+ directiveWasApplied,
30395
31012
  dispatchAgent,
30396
31013
  evaluateAutoOptimize,
30397
31014
  finalize,
@@ -30409,6 +31026,7 @@ export {
30409
31026
  isMailboxLeader,
30410
31027
  isMailboxMessageVisibleTo,
30411
31028
  isMailboxProjectServerAvailable,
31029
+ isProvenDirective,
30412
31030
  isValidMatrixKey,
30413
31031
  listProjectAgentLearnedEntries,
30414
31032
  listProjectAgentRoles,
@@ -30424,6 +31042,8 @@ export {
30424
31042
  loadRoleKnowledgeManifest,
30425
31043
  loadSkillAffinity,
30426
31044
  mailboxIdentityBase,
31045
+ mailboxProjectServerEndpoint,
31046
+ mailboxProjectServerMetadataPath,
30427
31047
  mailboxSessionTag,
30428
31048
  makeAgentSubagentRunner,
30429
31049
  makeAskResultTool,
@@ -30456,10 +31076,13 @@ export {
30456
31076
  parseMailboxQueryInput,
30457
31077
  parseMailboxSendInput,
30458
31078
  phaseForRole,
31079
+ quarantinePath,
30459
31080
  rankRoleSkills,
30460
31081
  readLiveLock,
31082
+ readQuarantinedDirectives,
30461
31083
  readRawLearnedEntries,
30462
31084
  readSubagentStructuredReport,
31085
+ recordDirectiveOutcomes,
30463
31086
  recordFileAction,
30464
31087
  recordPackageAction,
30465
31088
  recordSkillLoad,
@@ -30482,6 +31105,7 @@ export {
30482
31105
  resolveRoleSkillCandidates,
30483
31106
  resolveSubagentModelTarget,
30484
31107
  resolveSubagentWorktreeDecision,
31108
+ retiredDirectivesToWarnAbout,
30485
31109
  roleNeedsIndependentReviewModel,
30486
31110
  rosterSummaryFromConfigs,
30487
31111
  routeDirectiveToSkill,
@@ -30489,6 +31113,8 @@ export {
30489
31113
  saveProjectAgentConsolidated,
30490
31114
  saveProjectSkillAugmentation,
30491
31115
  scoreAgents,
31116
+ scoreSkillAffinity,
31117
+ scrubRetiredLines,
30492
31118
  sessionRecipient,
30493
31119
  setSkillPinned,
30494
31120
  slugifyProjectAgentRole,