@wrongstack/core 0.148.0 → 0.236.0

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 (82) hide show
  1. package/dist/{agent-bridge-r9y6gdn4.d.ts → agent-bridge-Cimv7bK7.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-1GeQE_L0.d.ts → agent-subagent-runner-C658wj_c.d.ts} +9 -8
  3. package/dist/{brain-Cp_3GIS2.d.ts → brain-sCZ3lCjq.d.ts} +28 -2
  4. package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
  5. package/dist/{config-BaVThgnT.d.ts → config-Koq6f3fs.d.ts} +2 -2
  6. package/dist/{context-C7G_MtLV.d.ts → context-CLz3z_E8.d.ts} +126 -2
  7. package/dist/coordination/index.d.ts +70 -13
  8. package/dist/coordination/index.js +2126 -151
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +27 -27
  11. package/dist/defaults/index.js +1328 -354
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +45 -16
  14. package/dist/execution/index.js +367 -59
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +86 -0
  17. package/dist/execution/prompt-enhancer.js +125 -0
  18. package/dist/execution/prompt-enhancer.js.map +1 -0
  19. package/dist/extension/index.d.ts +6 -6
  20. package/dist/extension/index.js +3 -1
  21. package/dist/extension/index.js.map +1 -1
  22. package/dist/{goal-preamble-CYJLg0wk.d.ts → goal-preamble-CnbzyVvl.d.ts} +19 -10
  23. package/dist/{index-BZdezm3g.d.ts → index-BlMqh5GO.d.ts} +8 -8
  24. package/dist/{index-CPweVoFM.d.ts → index-C2eSNPsB.d.ts} +7 -5
  25. package/dist/index.d.ts +439 -129
  26. package/dist/index.js +5206 -905
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +7 -7
  29. package/dist/infrastructure/index.js +72 -15
  30. package/dist/infrastructure/index.js.map +1 -1
  31. package/dist/kernel/index.d.ts +9 -9
  32. package/dist/kernel/index.js +7 -1
  33. package/dist/kernel/index.js.map +1 -1
  34. package/dist/{llm-selector-CP72f1lC.d.ts → llm-selector-D22R4AFz.d.ts} +2 -2
  35. package/dist/logger-DmmQhf4P.d.ts +65 -0
  36. package/dist/{mcp-servers-Bl5LTvQg.d.ts → mcp-servers-DFbirBv6.d.ts} +11 -4
  37. package/dist/models/index.d.ts +5 -5
  38. package/dist/models/index.js +89 -9
  39. package/dist/models/index.js.map +1 -1
  40. package/dist/{models-registry-D90K9UnM.d.ts → models-registry-CnJRjTXc.d.ts} +1 -1
  41. package/dist/{multi-agent-coordinator-QWEzJDlm.d.ts → multi-agent-coordinator-60weDZoA.d.ts} +8 -8
  42. package/dist/{null-fleet-bus-BUyfqh23.d.ts → null-fleet-bus-1068dEnr.d.ts} +7 -7
  43. package/dist/observability/index.d.ts +2 -2
  44. package/dist/package-outdated-watcher-pzJ5w7y8.d.ts +560 -0
  45. package/dist/{parallel-eternal-engine-C75QuhAI.d.ts → parallel-eternal-engine-DtG1fjc9.d.ts} +13 -9
  46. package/dist/{path-resolver-DRjQBkoO.d.ts → path-resolver-CA1ULU0J.d.ts} +3 -3
  47. package/dist/{permission-B7nKnEvQ.d.ts → permission-DbWPbuoA.d.ts} +1 -1
  48. package/dist/{permission-policy-8-6zBmfA.d.ts → permission-policy-AOk0LVsV.d.ts} +2 -2
  49. package/dist/pipeline-DsmlwTXu.d.ts +493 -0
  50. package/dist/{plan-templates-CkKNPU3I.d.ts → plan-templates-DPABrDvy.d.ts} +19 -8
  51. package/dist/{provider-runner-BNpuIyOL.d.ts → provider-runner-D0HgUqwV.d.ts} +3 -3
  52. package/dist/{retry-policy-rutAfVeR.d.ts → retry-policy-BVnkbMET.d.ts} +1 -1
  53. package/dist/sdd/index.d.ts +8 -8
  54. package/dist/sdd/index.js +358 -85
  55. package/dist/sdd/index.js.map +1 -1
  56. package/dist/{secret-vault-DoISxaKO.d.ts → secret-vault-BJDY28ev.d.ts} +7 -1
  57. package/dist/{secret-vault-BTcC_T5v.d.ts → secret-vault-CeVNiy_f.d.ts} +4 -3
  58. package/dist/security/index.d.ts +6 -5
  59. package/dist/security/index.js +214 -35
  60. package/dist/security/index.js.map +1 -1
  61. package/dist/{selector-4vDFZKt3.d.ts → selector-Cb4_9-hf.d.ts} +1 -1
  62. package/dist/{session-event-bridge-DWlvglC2.d.ts → session-event-bridge-BhtkkFFy.d.ts} +4 -2
  63. package/dist/{session-reader-BAtCxdaw.d.ts → session-reader-CCOssnBS.d.ts} +1 -1
  64. package/dist/skills/index.js +171 -21
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/storage/index.d.ts +151 -13
  67. package/dist/storage/index.js +1117 -256
  68. package/dist/storage/index.js.map +1 -1
  69. package/dist/types/index.d.ts +68 -21
  70. package/dist/types/index.js +616 -74
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/utils/expect-defined.js +3 -1
  73. package/dist/utils/expect-defined.js.map +1 -1
  74. package/dist/utils/index.d.ts +80 -4
  75. package/dist/utils/index.js +100 -15
  76. package/dist/utils/index.js.map +1 -1
  77. package/dist/{wstack-paths-DD50Omgn.d.ts → wstack-paths-CJjEwPXn.d.ts} +14 -1
  78. package/package.json +7 -3
  79. package/skills/chimera/SKILL.md +105 -0
  80. package/skills/research-web/SKILL.md +342 -0
  81. package/dist/logger-B9J5puGM.d.ts +0 -32
  82. package/dist/pipeline-BG7UgbDc.d.ts +0 -239
@@ -1,7 +1,8 @@
1
- import { randomUUID, randomBytes } from 'crypto';
1
+ import { randomUUID, createHash, randomBytes } from 'crypto';
2
2
  import * as fsp6 from 'fs/promises';
3
3
  import * as path4 from 'path';
4
4
  import { isAbsolute, resolve } from 'path';
5
+ import * as os from 'os';
5
6
  import { hostname } from 'os';
6
7
  import { EventEmitter } from 'events';
7
8
 
@@ -54,12 +55,12 @@ var BrainDecisionQueue = class {
54
55
  options: request.options,
55
56
  rationale: "Decision escalated to human authority."
56
57
  };
57
- const pending = new Promise((resolve2) => {
58
- const entry = { request, resolve: resolve2 };
58
+ const pending = new Promise((resolve3) => {
59
+ const entry = { request, resolve: resolve3 };
59
60
  if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
60
61
  entry.timer = setTimeout(() => {
61
62
  this.pending.delete(request.id);
62
- resolve2({ type: "deny", reason: "Brain human decision timed out." });
63
+ resolve3({ type: "deny", reason: "Brain human decision timed out." });
63
64
  }, this.opts.timeoutMs);
64
65
  }
65
66
  this.pending.set(request.id, entry);
@@ -191,6 +192,49 @@ async function atomicWrite(targetPath, content, opts = {}) {
191
192
  async function ensureDir(dir) {
192
193
  await fsp6.mkdir(dir, { recursive: true });
193
194
  }
195
+ async function withFileLock(targetPath, fn, opts = {}) {
196
+ const dir = path4.dirname(targetPath);
197
+ await fsp6.mkdir(dir, { recursive: true });
198
+ const lockPath = path4.join(dir, `.${path4.basename(targetPath)}.lock`);
199
+ const timeoutMs = opts.timeoutMs ?? 5e3;
200
+ const staleMs = opts.staleMs ?? 3e4;
201
+ const started = Date.now();
202
+ let handle;
203
+ for (; ; ) {
204
+ try {
205
+ handle = await fsp6.open(lockPath, "wx");
206
+ await handle.writeFile(`${process.pid}:${Date.now()}`);
207
+ break;
208
+ } catch (err) {
209
+ if (err.code !== "EEXIST") throw err;
210
+ try {
211
+ const stat5 = await fsp6.stat(lockPath);
212
+ if (Date.now() - stat5.mtimeMs > staleMs) {
213
+ await fsp6.unlink(lockPath);
214
+ continue;
215
+ }
216
+ } catch {
217
+ continue;
218
+ }
219
+ if (Date.now() - started >= timeoutMs) {
220
+ throw new Error(`Timed out waiting for file lock: ${targetPath}`);
221
+ }
222
+ await new Promise((resolve3) => setTimeout(resolve3, 25));
223
+ }
224
+ }
225
+ try {
226
+ return await fn();
227
+ } finally {
228
+ try {
229
+ await handle?.close();
230
+ } catch {
231
+ }
232
+ try {
233
+ await fsp6.unlink(lockPath);
234
+ } catch {
235
+ }
236
+ }
237
+ }
194
238
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
195
239
  async function renameWithRetry(from, to) {
196
240
  if (process.platform !== "win32") {
@@ -209,7 +253,7 @@ async function renameWithRetry(from, to) {
209
253
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
210
254
  throw err;
211
255
  }
212
- await new Promise((resolve2) => setTimeout(resolve2, delays[i]));
256
+ await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
213
257
  }
214
258
  }
215
259
  throw lastErr;
@@ -395,6 +439,62 @@ function safeParse(input, maxBytes = 5e6) {
395
439
  }
396
440
  }
397
441
 
442
+ // src/types/errors.ts
443
+ var ERROR_CODES = {
444
+ // Provider
445
+ PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
446
+ PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
447
+ PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
448
+ PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
449
+ PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
450
+ PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
451
+ // Agent
452
+ AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
453
+ AGENT_ABORTED: "AGENT_ABORTED",
454
+ AGENT_RUN_FAILED: "AGENT_RUN_FAILED"};
455
+ var WrongStackError = class extends Error {
456
+ code;
457
+ subsystem;
458
+ severity;
459
+ recoverable;
460
+ context;
461
+ constructor(opts) {
462
+ super(opts.message, { cause: opts.cause });
463
+ this.name = "WrongStackError";
464
+ this.code = opts.code;
465
+ this.subsystem = opts.subsystem;
466
+ this.severity = opts.severity ?? "error";
467
+ this.recoverable = opts.recoverable ?? false;
468
+ this.context = opts.context;
469
+ }
470
+ /**
471
+ * Render a one-line user-facing description.
472
+ * Subclasses should override for domain-specific formatting.
473
+ */
474
+ describe() {
475
+ const ctx = this.context ? ` ${formatContext(this.context)}` : "";
476
+ return `${this.code}: ${this.message}${ctx}`;
477
+ }
478
+ };
479
+ function formatContext(ctx) {
480
+ const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
481
+ return parts.length > 0 ? `[${parts.join(" ")}]` : "";
482
+ }
483
+ var AgentError = class extends WrongStackError {
484
+ constructor(opts) {
485
+ super({
486
+ message: opts.message,
487
+ code: opts.code,
488
+ subsystem: "agent",
489
+ severity: opts.code === ERROR_CODES.AGENT_ABORTED ? "warning" : "error",
490
+ recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
491
+ context: opts.context,
492
+ cause: opts.cause
493
+ });
494
+ this.name = "AgentError";
495
+ }
496
+ };
497
+
398
498
  // src/coordination/in-memory-transport.ts
399
499
  var InMemoryBridgeTransport = class {
400
500
  subs = /* @__PURE__ */ new Map();
@@ -483,28 +583,40 @@ var InMemoryAgentBridge = class {
483
583
  return () => this.subscriptions.delete(handler);
484
584
  }
485
585
  async request(msg, timeoutMs) {
486
- if (this.stopped) throw new Error("Bridge is stopped");
586
+ if (this.stopped) throw new AgentError({
587
+ message: "Bridge is stopped",
588
+ code: ERROR_CODES.AGENT_ABORTED
589
+ });
487
590
  const timeout = timeoutMs ?? this.timeoutMs;
488
591
  const correlationId = msg.id;
489
592
  if (this.inflightGuards.has(correlationId)) {
490
- throw new Error(
491
- `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
492
- );
593
+ throw new AgentError({
594
+ message: `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`,
595
+ code: ERROR_CODES.AGENT_RUN_FAILED,
596
+ context: { correlationId }
597
+ });
493
598
  }
494
599
  this.inflightGuards.add(correlationId);
495
- return new Promise((resolve2, reject) => {
600
+ return new Promise((resolve3, reject) => {
496
601
  const timer = setTimeout(() => {
497
602
  this.inflightGuards.delete(correlationId);
498
603
  this.pendingRequests.delete(correlationId);
499
- reject(new Error(`Request ${correlationId} timed out after ${timeout}ms`));
604
+ reject(new AgentError({
605
+ message: `Request ${correlationId} timed out after ${timeout}ms`,
606
+ code: ERROR_CODES.AGENT_RUN_FAILED,
607
+ context: { correlationId, timeoutMs: timeout }
608
+ }));
500
609
  }, timeout);
501
610
  if (!this.inflightGuards.has(correlationId)) {
502
611
  clearTimeout(timer);
503
- reject(new Error("Bridge stopped"));
612
+ reject(new AgentError({
613
+ message: "Bridge stopped",
614
+ code: ERROR_CODES.AGENT_ABORTED
615
+ }));
504
616
  return;
505
617
  }
506
618
  this.pendingRequests.set(correlationId, {
507
- resolve: resolve2,
619
+ resolve: resolve3,
508
620
  reject,
509
621
  timer
510
622
  });
@@ -521,7 +633,10 @@ var InMemoryAgentBridge = class {
521
633
  this.stopped = true;
522
634
  for (const [, p] of this.pendingRequests) {
523
635
  clearTimeout(p.timer);
524
- p.reject(new Error("Bridge stopped"));
636
+ p.reject(new AgentError({
637
+ message: "Bridge stopped",
638
+ code: ERROR_CODES.AGENT_ABORTED
639
+ }));
525
640
  }
526
641
  this.pendingRequests.clear();
527
642
  this.inflightGuards.clear();
@@ -546,7 +661,9 @@ function createMessage(type, from, payload, to) {
546
661
  // src/utils/expect-defined.ts
547
662
  function expectDefined(value, label) {
548
663
  if (value === null || value === void 0) {
549
- throw new Error("Expected value to be defined");
664
+ const err = new Error("Expected value to be defined");
665
+ err.name = "ExpectDefinedError";
666
+ throw err;
550
667
  }
551
668
  return value;
552
669
  }
@@ -1306,7 +1423,23 @@ Bridge contract:
1306
1423
  subagents' context. Those are not yours to read.
1307
1424
  - Your final task output is what the Director sees. Be concise,
1308
1425
  structured, and self-contained \u2014 assume the Director will paste your
1309
- output into its own context.`;
1426
+ output into its own context.
1427
+
1428
+ Inter-agent mailbox (if you have the \`mail_send\`/\`mail_inbox\`/\`mailbox\` tools):
1429
+ - You are part of a project-wide fleet that may span other terminals and
1430
+ WebUIs. Your mailbox identity is \`<your-name>@<session-tag>\` (unique
1431
+ per session); mail addressed to you, to your bare name, or broadcast
1432
+ to \`*\` is injected into your conversation automatically before each
1433
+ step \u2014 read it once, it is marked read.
1434
+ - Broadcast milestones: when you complete a significant piece of work,
1435
+ \`mail_send to="*"\` a one-line summary so parallel agents don't collide
1436
+ with or duplicate it.
1437
+ - Hand off matching work: if another online agent's role fits a follow-up
1438
+ better (e.g. a reviewer while you just wrote code), \`mail_send\` it to
1439
+ their exact id instead of doing everything yourself. Discover ids with
1440
+ \`mailbox action=online\`.
1441
+ - Answer your mail: reply to the sender's exact \`from\` id. When done with
1442
+ an assigned task, post a \`result\` back to whoever assigned it.`;
1310
1443
  function composeDirectorPrompt(parts = {}) {
1311
1444
  const sections = [];
1312
1445
  const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
@@ -1380,11 +1513,11 @@ var HEAVY_BUDGET = {
1380
1513
  };
1381
1514
  var TOOLS = {
1382
1515
  /** Pure read/inspect — safe for analysis and review agents. */
1383
- read: ["read", "grep", "glob", "search", "tree"],
1516
+ read: ["read", "grep", "glob", "search", "tree", "mailbox"],
1384
1517
  /** Read + structured inspection (logs, diffs, json, dependency audit). */
1385
- inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit"],
1518
+ inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit", "mailbox"],
1386
1519
  /** Read + edit (no shell). For agents that write code/docs but don't run it. */
1387
- write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch"],
1520
+ write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch", "mailbox"],
1388
1521
  /** Full build loop: edit + run (lint/format/typecheck/test/bash). */
1389
1522
  build: [
1390
1523
  "read",
@@ -1401,16 +1534,17 @@ var TOOLS = {
1401
1534
  "lint",
1402
1535
  "format",
1403
1536
  "typecheck",
1404
- "test"
1537
+ "test",
1538
+ "mailbox"
1405
1539
  ],
1406
1540
  /** Version control. */
1407
1541
  vcs: ["read", "grep", "glob", "git", "diff"],
1408
1542
  /** Dependency management + CVE audit. */
1409
- deps: ["read", "grep", "glob", "install", "outdated", "audit", "json"],
1543
+ deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
1410
1544
  /** Documentation authoring. */
1411
- docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document"],
1545
+ docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document", "mailbox"],
1412
1546
  /** Web research. */
1413
- research: ["read", "grep", "glob", "search", "fetch"]
1547
+ research: ["read", "grep", "glob", "search", "fetch", "mailbox"]
1414
1548
  };
1415
1549
 
1416
1550
  // src/coordination/agents/phase1-discovery.ts
@@ -2208,15 +2342,44 @@ Working rules:
2208
2342
  id: "e2e",
2209
2343
  name: "E2E",
2210
2344
  role: "e2e",
2211
- tools: [...TOOLS.build, "fetch"],
2345
+ tools: [
2346
+ ...TOOLS.build,
2347
+ "fetch",
2348
+ "playwright_navigate",
2349
+ "playwright_screenshot",
2350
+ "playwright_click",
2351
+ "playwright_type",
2352
+ "playwright_evaluate",
2353
+ "playwright_select_option",
2354
+ "playwright_hover",
2355
+ "playwright_fill_form",
2356
+ "playwright_wait_for",
2357
+ "playwright_press_key",
2358
+ "playwright_drag"
2359
+ ],
2212
2360
  prompt: `You are the E2E agent. Your job is end-to-end testing: drive the whole
2213
2361
  system the way a user would and verify the full flow works across boundaries.
2214
2362
 
2215
2363
  Scope:
2216
2364
  - Author end-to-end scenarios that exercise real user journeys
2217
2365
  - Drive UI/CLI/API across process and network boundaries
2366
+ - Use Playwright browser tools (navigate, click, type, screenshot, evaluate)
2367
+ to automate web UI flows \u2014 open pages, interact with forms, capture evidence
2218
2368
  - Set up and tear down realistic test state
2219
- - Capture failures with enough detail to reproduce (screenshots, logs)
2369
+ - Capture failures with enough detail to reproduce (screenshots, logs, page HTML)
2370
+
2371
+ Playwright tools available (require the "playwright" MCP server to be enabled):
2372
+ playwright_navigate(url) \u2014 open a page at the given URL
2373
+ playwright_screenshot() \u2014 capture a full-page or viewport screenshot
2374
+ playwright_click(selector) \u2014 click on an element matching a CSS selector
2375
+ playwright_type(selector, text) \u2014 type text into a focused input element
2376
+ playwright_evaluate(script) \u2014 run arbitrary JavaScript in the page context
2377
+ playwright_select_option(selector, value) \u2014 pick a <select> dropdown option
2378
+ playwright_hover(selector) \u2014 hover the mouse over an element
2379
+ playwright_fill_form(fields) \u2014 fill multiple form fields in one call
2380
+ playwright_wait_for(selector) \u2014 block until an element appears on the page
2381
+ playwright_press_key(key) \u2014 press a keyboard key (Enter, Tab, Escape, \u2026)
2382
+ playwright_drag(from, to) \u2014 drag an element from one selector to another
2220
2383
 
2221
2384
  Input format you accept:
2222
2385
  { "task": "scenario | smoke | journey", "flow": "<user journey>", "surface": "ui | cli | api" }
@@ -2230,8 +2393,10 @@ Output: Markdown e2e report:
2230
2393
  Working rules:
2231
2394
  - Test the real flow end to end; don't stub the thing under test
2232
2395
  - Make scenarios deterministic \u2014 control time, randomness, and external state
2233
- - On failure, capture artifacts (logs/screenshots) for reproduction
2234
- - Keep scenarios independent so one failure doesn't cascade`
2396
+ - On failure, capture artifacts (screenshots, page HTML, logs) for reproduction
2397
+ - Keep scenarios independent so one failure doesn't cascade
2398
+ - For browser tests: playwright_navigate first, then interact, then playwright_screenshot as evidence
2399
+ - If playwright tools are unavailable, report it and fall back to API/CLI testing`
2235
2400
  },
2236
2401
  budget: HEAVY_BUDGET,
2237
2402
  capability: {
@@ -2244,10 +2409,106 @@ Working rules:
2244
2409
  "user journey",
2245
2410
  "smoke test",
2246
2411
  "playwright",
2412
+ "browser",
2413
+ "screenshot",
2414
+ "web ui",
2415
+ "headless",
2247
2416
  "cypress",
2248
2417
  "full flow",
2249
2418
  "browser test",
2250
- "acceptance test"
2419
+ "acceptance test",
2420
+ "navigate",
2421
+ "click",
2422
+ "form fill",
2423
+ "dom",
2424
+ "page load"
2425
+ ]
2426
+ }
2427
+ },
2428
+ {
2429
+ config: {
2430
+ id: "browser",
2431
+ name: "Browser",
2432
+ role: "browser",
2433
+ tools: [
2434
+ ...TOOLS.read,
2435
+ "fetch",
2436
+ "playwright_navigate",
2437
+ "playwright_screenshot",
2438
+ "playwright_click",
2439
+ "playwright_type",
2440
+ "playwright_evaluate",
2441
+ "playwright_select_option",
2442
+ "playwright_hover",
2443
+ "playwright_fill_form",
2444
+ "playwright_wait_for",
2445
+ "playwright_press_key",
2446
+ "playwright_drag"
2447
+ ],
2448
+ prompt: `You are the Browser agent. Your job is browser automation: open web pages,
2449
+ interact with them, extract data, capture screenshots, and return structured
2450
+ results. You are a read-focused agent \u2014 you drive the browser, not the filesystem.
2451
+
2452
+ Scope:
2453
+ - Navigate to URLs and wait for pages to load
2454
+ - Take full-page or element screenshots as evidence
2455
+ - Click buttons, fill forms, select options, type text \u2014 full user simulation
2456
+ - Extract page content: text, HTML, element attributes, data tables
2457
+ - Evaluate JavaScript in the page context to extract structured data
2458
+ - Verify visual state (element visibility, text content, attribute values)
2459
+
2460
+ Playwright tools available (require the "playwright" MCP server to be enabled):
2461
+ playwright_navigate(url) \u2014 open a page at the given URL
2462
+ playwright_screenshot() \u2014 capture a full-page or viewport screenshot
2463
+ playwright_click(selector) \u2014 click on an element matching a CSS selector
2464
+ playwright_type(selector, text) \u2014 type text into a focused input element
2465
+ playwright_evaluate(script) \u2014 run arbitrary JavaScript in the page context
2466
+ playwright_select_option(selector, value) \u2014 pick a <select> dropdown option
2467
+ playwright_hover(selector) \u2014 hover the mouse over an element
2468
+ playwright_fill_form(fields) \u2014 fill multiple form fields in one call
2469
+ playwright_wait_for(selector) \u2014 block until an element appears on the page
2470
+ playwright_press_key(key) \u2014 press a keyboard key (Enter, Tab, Escape, \u2026)
2471
+ playwright_drag(from, to) \u2014 drag an element from one selector to another
2472
+
2473
+ Input format you accept:
2474
+ { "task": "navigate | screenshot | extract | interact | verify", "url": "<url>", "steps": ["step1", "step2"] }
2475
+
2476
+ Output: Structured markdown report:
2477
+ - ## Page (URL, title, load status)
2478
+ - ## Actions Taken (step-by-step with timestamps)
2479
+ - ## Results (extracted data, element states, verification results)
2480
+ - ## Screenshots (list attached screenshot references)
2481
+ - ## Errors (any failures with stack traces)
2482
+
2483
+ Working rules:
2484
+ - Always playwright_navigate first before any interaction
2485
+ - Always playwright_wait_for after navigation to ensure the page is ready
2486
+ - playwright_screenshot is your primary evidence \u2014 use it before and after interactions
2487
+ - Use playwright_evaluate for structured data extraction (JSON, text content)
2488
+ - If a selector fails, try alternative selectors before giving up
2489
+ - Report exact CSS selectors used \u2014 they're part of the evidence
2490
+ - If playwright tools are unavailable, report the error immediately \u2014 do not guess`
2491
+ },
2492
+ budget: MEDIUM_BUDGET,
2493
+ capability: {
2494
+ phase: "verify",
2495
+ summary: "Browser automation: opens pages, clicks, types, screenshots, extracts data via Playwright headless Chromium.",
2496
+ keywords: [
2497
+ "browser",
2498
+ "screenshot",
2499
+ "navigate",
2500
+ "web page",
2501
+ "scrape",
2502
+ "crawl",
2503
+ "headless",
2504
+ "chrome",
2505
+ "open url",
2506
+ "capture",
2507
+ "page title",
2508
+ "extract data",
2509
+ "fill form",
2510
+ "click button",
2511
+ "take screenshot"
2251
2512
  ]
2252
2513
  }
2253
2514
  },
@@ -3696,7 +3957,7 @@ Working rules:
3696
3957
  id: "tech-stack",
3697
3958
  name: "Tech Stack Validator",
3698
3959
  role: "tech-stack",
3699
- tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json"],
3960
+ tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json", "mailbox"],
3700
3961
  prompt: `You are the Tech Stack Validator \u2014 a single-shot validation agent that fires
3701
3962
  before any package, library, or framework choice is committed.
3702
3963
 
@@ -3704,6 +3965,16 @@ Your ONLY job: verify that a technology choice is current, real, and not obsolet
3704
3965
  You are the "this isn't code, this is 10-year-old technology" agent. Intervene
3705
3966
  hard when the LLM hallucinates a version number or suggests dead tech.
3706
3967
 
3968
+ ## Before you begin
3969
+
3970
+ Check the inter-agent mailbox for pending tasks. Other agents or the file-watcher
3971
+ may have left assign messages with dependency files to audit:
3972
+ - mailbox action=check
3973
+
3974
+ If you find an assign message, use the specified file path and packages.
3975
+ When done, post results back:
3976
+ - mailbox action=send to=<sender> type=result subject="Tech stack audit results" body="..."
3977
+
3707
3978
  ## Critical rules
3708
3979
 
3709
3980
  1. **Verify existence.** Search npm registry (fetch https://registry.npmjs.org/<pkg>/latest)
@@ -3762,11 +4033,11 @@ When APPROVED:
3762
4033
  **Install**: pnpm add <name>@^<major>.<minor>.0`
3763
4034
  },
3764
4035
  budget: {
3765
- timeoutMs: 6e4,
3766
- maxIterations: 5,
3767
- maxToolCalls: 20,
3768
- maxTokens: 4e4,
3769
- maxCostUsd: 0.1
4036
+ timeoutMs: 12e4,
4037
+ maxIterations: 10,
4038
+ maxToolCalls: 40,
4039
+ maxTokens: 6e4,
4040
+ maxCostUsd: 0.25
3770
4041
  },
3771
4042
  capability: {
3772
4043
  phase: "meta",
@@ -4898,12 +5169,12 @@ var SubagentBudget = class _SubagentBudget {
4898
5169
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
4899
5170
  return Promise.resolve("stop");
4900
5171
  }
4901
- return new Promise((resolve2) => {
5172
+ return new Promise((resolve3) => {
4902
5173
  let resolved = false;
4903
5174
  const respond = (d) => {
4904
5175
  if (resolved) return;
4905
5176
  resolved = true;
4906
- resolve2(d);
5177
+ resolve3(d);
4907
5178
  };
4908
5179
  const fallback = setTimeout(
4909
5180
  () => respond("stop"),
@@ -5024,44 +5295,6 @@ var SubagentBudget = class _SubagentBudget {
5024
5295
  }
5025
5296
  };
5026
5297
 
5027
- // src/types/errors.ts
5028
- var ERROR_CODES = {
5029
- // Provider
5030
- PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
5031
- PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
5032
- PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
5033
- PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
5034
- PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
5035
- PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR"};
5036
- var WrongStackError = class extends Error {
5037
- code;
5038
- subsystem;
5039
- severity;
5040
- recoverable;
5041
- context;
5042
- constructor(opts) {
5043
- super(opts.message, { cause: opts.cause });
5044
- this.name = "WrongStackError";
5045
- this.code = opts.code;
5046
- this.subsystem = opts.subsystem;
5047
- this.severity = opts.severity ?? "error";
5048
- this.recoverable = opts.recoverable ?? false;
5049
- this.context = opts.context;
5050
- }
5051
- /**
5052
- * Render a one-line user-facing description.
5053
- * Subclasses should override for domain-specific formatting.
5054
- */
5055
- describe() {
5056
- const ctx = this.context ? ` ${formatContext(this.context)}` : "";
5057
- return `${this.code}: ${this.message}${ctx}`;
5058
- }
5059
- };
5060
- function formatContext(ctx) {
5061
- const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
5062
- return parts.length > 0 ? `[${parts.join(" ")}]` : "";
5063
- }
5064
-
5065
5298
  // src/types/provider.ts
5066
5299
  var ProviderError = class extends WrongStackError {
5067
5300
  status;
@@ -5136,6 +5369,9 @@ function providerStatusToCode(status, type) {
5136
5369
 
5137
5370
  // src/coordination/coordinator/error-classifier.ts
5138
5371
  function classifySubagentError(err, hints = {}) {
5372
+ if (err instanceof AgentError && err.cause) {
5373
+ return classifySubagentError(err.cause, hints);
5374
+ }
5139
5375
  const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
5140
5376
  if (err instanceof ProviderError) {
5141
5377
  const baseMessage2 = err.describe();
@@ -5168,7 +5404,7 @@ function classifySubagentError(err, hints = {}) {
5168
5404
  if (/agent exhausted iteration limit$/i.test(baseMessage)) {
5169
5405
  return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
5170
5406
  }
5171
- if (/empty response$/i.test(baseMessage)) {
5407
+ if (/empty response/i.test(baseMessage)) {
5172
5408
  return { kind: "empty_response", message: baseMessage, retryable: false, cause };
5173
5409
  }
5174
5410
  if (/^tool failed: /i.test(baseMessage)) {
@@ -5853,7 +6089,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
5853
6089
  taskIds.map((id) => {
5854
6090
  const cached = this.completedResults.find((r) => r.taskId === id);
5855
6091
  if (cached) return cached;
5856
- return new Promise((resolve2, reject) => {
6092
+ return new Promise((resolve3, reject) => {
5857
6093
  const timeout = setTimeout(() => {
5858
6094
  this.off("task.completed", handler);
5859
6095
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -5862,7 +6098,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
5862
6098
  if (result.taskId === id) {
5863
6099
  clearTimeout(timeout);
5864
6100
  this.off("task.completed", handler);
5865
- resolve2(result);
6101
+ resolve3(result);
5866
6102
  }
5867
6103
  };
5868
6104
  this.on("task.completed", handler);
@@ -7235,11 +7471,11 @@ var Director = class _Director {
7235
7471
  if (cached) return cached;
7236
7472
  const existing = this.taskWaiters.get(id);
7237
7473
  if (existing) return existing.promise;
7238
- let resolve2;
7474
+ let resolve3;
7239
7475
  const promise = new Promise((res) => {
7240
- resolve2 = res;
7476
+ resolve3 = res;
7241
7477
  });
7242
- this.taskWaiters.set(id, { promise, resolve: resolve2 });
7478
+ this.taskWaiters.set(id, { promise, resolve: resolve3 });
7243
7479
  return promise;
7244
7480
  })
7245
7481
  );
@@ -7635,7 +7871,7 @@ function createDelegateTool(opts) {
7635
7871
  subagentId
7636
7872
  });
7637
7873
  const dir = director;
7638
- const result = await new Promise((resolve2) => {
7874
+ const result = await new Promise((resolve3) => {
7639
7875
  let settled = false;
7640
7876
  let timer;
7641
7877
  const finish = (value) => {
@@ -7645,7 +7881,7 @@ function createDelegateTool(opts) {
7645
7881
  offTool();
7646
7882
  offIter();
7647
7883
  offProgress();
7648
- resolve2(value);
7884
+ resolve3(value);
7649
7885
  };
7650
7886
  const arm = () => {
7651
7887
  if (timer) clearTimeout(timer);
@@ -7906,6 +8142,7 @@ async function readSubagentPartial(opts, subagentId) {
7906
8142
  function makeAgentSubagentRunner(opts) {
7907
8143
  const format = opts.formatTaskInput ?? defaultFormatTaskInput;
7908
8144
  return async (task, ctx) => {
8145
+ const taskStartedAt = Date.now();
7909
8146
  const factoryResult = await opts.factory(ctx.config);
7910
8147
  const { agent, events } = factoryResult;
7911
8148
  const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
@@ -8002,7 +8239,7 @@ function makeAgentSubagentRunner(opts) {
8002
8239
  }),
8003
8240
  events.on("provider.text_delta", (e) => {
8004
8241
  ctx.budget.markActivity();
8005
- streamingTextAcc = (streamingTextAcc + e.text).slice(-200);
8242
+ streamingTextAcc = (streamingTextAcc + e.text).slice(-2e3);
8006
8243
  })
8007
8244
  );
8008
8245
  const onParentAbort = () => aborter.abort();
@@ -8010,6 +8247,15 @@ function makeAgentSubagentRunner(opts) {
8010
8247
  let result;
8011
8248
  try {
8012
8249
  result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
8250
+ events.emit("subagent.task_completed", {
8251
+ subagentId: ctx.subagentId,
8252
+ taskId: task.id,
8253
+ status: result.status === "done" ? "success" : "failed",
8254
+ iterations: result.iterations,
8255
+ toolCalls: ctx.budget.usage().toolCalls,
8256
+ durationMs: Date.now() - taskStartedAt,
8257
+ finalText: result.finalText?.trim() || void 0
8258
+ });
8013
8259
  } finally {
8014
8260
  detachFleet?.();
8015
8261
  ctx.signal.removeEventListener("abort", onParentAbort);
@@ -8045,21 +8291,40 @@ function makeAgentSubagentRunner(opts) {
8045
8291
  if (budgetError) throw budgetError;
8046
8292
  }
8047
8293
  if (result.status === "failed") {
8048
- throw result.error instanceof Error ? result.error : new Error(String(result.error ?? "agent failed"));
8294
+ throw result.error instanceof AgentError ? result.error : new AgentError({
8295
+ message: result.error instanceof Error ? result.error.message : String(result.error ?? "agent failed"),
8296
+ code: ERROR_CODES.AGENT_RUN_FAILED,
8297
+ cause: result.error
8298
+ });
8049
8299
  }
8050
8300
  if (result.status === "aborted") {
8051
- throw new Error("agent aborted");
8301
+ throw new AgentError({
8302
+ message: "agent aborted",
8303
+ code: ERROR_CODES.AGENT_ABORTED
8304
+ });
8052
8305
  }
8053
8306
  if (result.status === "max_iterations") {
8054
- throw new Error("agent exhausted iteration limit");
8307
+ throw new AgentError({
8308
+ message: "agent exhausted iteration limit",
8309
+ code: ERROR_CODES.AGENT_ITERATION_LIMIT,
8310
+ recoverable: true
8311
+ });
8055
8312
  }
8056
8313
  const usage = ctx.budget.usage();
8057
8314
  const finalText = (result.finalText ?? "").trim();
8058
8315
  if (finalText.length === 0 && usage.toolCalls === 0) {
8059
- throw new Error("empty response");
8316
+ throw new AgentError({
8317
+ message: "empty response \u2014 agent produced no text and no tool calls",
8318
+ code: ERROR_CODES.AGENT_RUN_FAILED,
8319
+ context: { iterations: result.iterations }
8320
+ });
8060
8321
  }
8061
8322
  if (finalText.length === 0 && lastToolFailed !== null) {
8062
- throw new Error(`tool failed: ${lastToolFailed}`);
8323
+ throw new AgentError({
8324
+ message: `unrecovered tool failure: ${lastToolFailed} \u2014 agent ended turn without acknowledging the error`,
8325
+ code: ERROR_CODES.AGENT_RUN_FAILED,
8326
+ context: { tool: lastToolFailed, iterations: result.iterations }
8327
+ });
8063
8328
  }
8064
8329
  return {
8065
8330
  result: result.finalText,
@@ -8224,7 +8489,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
8224
8489
  onClose: (s) => this.appendToIndex(s)
8225
8490
  });
8226
8491
  } catch (err) {
8227
- await handle.close().catch((e) => console.warn(`[session-store] handle.close() failed: ${e}`));
8492
+ await handle.close().catch((e) => console.warn(JSON.stringify({
8493
+ level: "warn",
8494
+ event: "session_store.handle_close_failed",
8495
+ message: e instanceof Error ? e.message : String(e),
8496
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8497
+ })));
8228
8498
  throw err;
8229
8499
  }
8230
8500
  }
@@ -8251,11 +8521,25 @@ var DefaultSessionStore = class _DefaultSessionStore {
8251
8521
  provider: data.metadata.provider
8252
8522
  },
8253
8523
  this.events,
8254
- { resumed: true, dir: this.dir, filePath: file, secretScrubber: this.secretScrubber, onClose: (s) => this.appendToIndex(s) }
8524
+ {
8525
+ resumed: true,
8526
+ // Shard directory (sessions/<date>/) — must match create() so the
8527
+ // .summary.json sidecar lands next to the JSONL instead of the
8528
+ // sessions root (where summaryFor() would never find it).
8529
+ dir: path4.dirname(file),
8530
+ filePath: file,
8531
+ secretScrubber: this.secretScrubber,
8532
+ onClose: (s) => this.appendToIndex(s)
8533
+ }
8255
8534
  );
8256
8535
  return { writer, data };
8257
8536
  } catch (err) {
8258
- await handle.close().catch((e) => console.warn(`[session-store] handle.close() failed: ${e}`));
8537
+ await handle.close().catch((e) => console.warn(JSON.stringify({
8538
+ level: "warn",
8539
+ event: "session_store.handle_close_failed",
8540
+ message: e instanceof Error ? e.message : String(e),
8541
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8542
+ })));
8259
8543
  throw err;
8260
8544
  }
8261
8545
  }
@@ -8275,7 +8559,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
8275
8559
  }
8276
8560
  const meta = this.metaFromEvents(id, events);
8277
8561
  const { messages, usage } = this.replay(events, id);
8278
- return { metadata: meta, events, messages, usage };
8562
+ const toolCallEnds = extractToolCallEnds(events);
8563
+ return { metadata: meta, events, messages, usage, toolCallEnds };
8279
8564
  }
8280
8565
  async list(limit = 20) {
8281
8566
  try {
@@ -8431,10 +8716,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
8431
8716
  const stat5 = await fsp6.stat(full);
8432
8717
  const summary = await this.summarize(id, stat5.mtime.toISOString());
8433
8718
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
8434
- console.warn(
8435
- `[session-store] Failed to write manifest for "${id}":`,
8436
- err instanceof Error ? err.message : String(err)
8437
- );
8719
+ console.warn(JSON.stringify({
8720
+ level: "warn",
8721
+ event: "session_store.manifest_write_failed",
8722
+ sessionId: id,
8723
+ message: err instanceof Error ? err.message : String(err),
8724
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8725
+ }));
8438
8726
  });
8439
8727
  return summary;
8440
8728
  }
@@ -8442,17 +8730,48 @@ var DefaultSessionStore = class _DefaultSessionStore {
8442
8730
  /**
8443
8731
  * Delete a session and all associated files: JSONL, summary, plan/todos
8444
8732
  * sidecars, and the session directory (fleet.json, shared/, subagents/).
8733
+ *
8734
+ * Individual file deletions are best-effort (logged as structured warnings),
8735
+ * but a tombstone is always written so readIndex() filters this session out.
8736
+ * If the session directory itself can't be removed, the error is surfaced
8737
+ * to the caller so prune() can report it.
8445
8738
  */
8446
8739
  async deleteSession(id) {
8447
- await fsp6.unlink(this.sessionPath(id, ".jsonl")).catch((err) => console.warn(`[session-store] delete .jsonl failed: ${err}`));
8448
- await fsp6.unlink(this.sessionPath(id, ".summary.json")).catch((err) => console.warn(`[session-store] delete .summary.json failed: ${err}`));
8740
+ const jsonlPath = this.sessionPath(id, ".jsonl");
8741
+ const summaryPath = this.sessionPath(id, ".summary.json");
8449
8742
  const shardDir = path4.dirname(path4.join(this.dir, id));
8450
8743
  const base = path4.basename(id);
8451
- for (const ext of [".plan.json", ".todos.json"]) {
8452
- await fsp6.unlink(path4.join(shardDir, `${base}${ext}`)).catch((err) => console.warn(`[session-store] delete ${ext} failed: ${err}`));
8453
- }
8454
8744
  const sessDir = path4.join(shardDir, base);
8455
- await fsp6.rm(sessDir, { recursive: true, force: true }).catch((err) => console.warn(`[session-store] delete session dir failed: ${err}`));
8745
+ const deletions = [
8746
+ fsp6.unlink(jsonlPath),
8747
+ fsp6.unlink(summaryPath),
8748
+ fsp6.unlink(path4.join(shardDir, `${base}.plan.json`)),
8749
+ fsp6.unlink(path4.join(shardDir, `${base}.todos.json`))
8750
+ ];
8751
+ const results = await Promise.allSettled(deletions);
8752
+ for (const r of results) {
8753
+ if (r.status === "rejected") {
8754
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
8755
+ if (r.reason?.code !== "ENOENT") {
8756
+ console.warn(JSON.stringify({
8757
+ level: "warn",
8758
+ event: "session_store.delete_failed",
8759
+ sessionId: id,
8760
+ message: msg,
8761
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8762
+ }));
8763
+ }
8764
+ }
8765
+ }
8766
+ await fsp6.rm(sessDir, { recursive: true, force: true }).catch((err) => {
8767
+ console.warn(JSON.stringify({
8768
+ level: "warn",
8769
+ event: "session_store.rmdir_failed",
8770
+ sessionId: id,
8771
+ message: err instanceof Error ? err.message : String(err),
8772
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8773
+ }));
8774
+ });
8456
8775
  await this.writeTombstone(id);
8457
8776
  }
8458
8777
  async delete(id) {
@@ -8468,24 +8787,33 @@ var DefaultSessionStore = class _DefaultSessionStore {
8468
8787
  activeSessionId = active.sessionId ?? null;
8469
8788
  } catch {
8470
8789
  }
8790
+ const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
8791
+ const pruneFile = async (dir, name, prefix) => {
8792
+ const jsonlPath = path4.join(dir, name);
8793
+ try {
8794
+ const stat5 = await fsp6.stat(jsonlPath);
8795
+ if (stat5.mtimeMs >= cutoff) return;
8796
+ } catch {
8797
+ return;
8798
+ }
8799
+ const base = name.replace(/\.jsonl$/, "");
8800
+ const id = prefix ? `${prefix}/${base}` : base;
8801
+ if (activeSessionId && id === activeSessionId) return;
8802
+ await this.deleteSession(id);
8803
+ deleted++;
8804
+ };
8471
8805
  const entries = await fsp6.readdir(this.dir, { withFileTypes: true }).catch(() => []);
8472
8806
  for (const entry of entries) {
8807
+ if (entry.isFile()) {
8808
+ if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
8809
+ continue;
8810
+ }
8473
8811
  if (!entry.isDirectory()) continue;
8474
8812
  const dateDir = path4.join(this.dir, entry.name);
8475
8813
  const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
8476
8814
  for (const file of files) {
8477
- if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
8478
- const jsonlPath = path4.join(dateDir, file.name);
8479
- try {
8480
- const stat5 = await fsp6.stat(jsonlPath);
8481
- if (stat5.mtimeMs >= cutoff) continue;
8482
- } catch {
8483
- continue;
8484
- }
8485
- const id = `${entry.name}/${file.name.replace(/\.jsonl$/, "")}`;
8486
- if (activeSessionId && id === activeSessionId) continue;
8487
- await this.deleteSession(id);
8488
- deleted++;
8815
+ if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
8816
+ await pruneFile(dateDir, file.name, entry.name);
8489
8817
  }
8490
8818
  }
8491
8819
  if (deleted > 0) {
@@ -8574,7 +8902,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8574
8902
  }
8575
8903
  metaFromEvents(id, events) {
8576
8904
  const start = events.find((e) => e.type === "session_start");
8577
- const end = events.find((e) => e.type === "session_end");
8905
+ const end = events.findLast((e) => e.type === "session_end");
8578
8906
  return {
8579
8907
  id,
8580
8908
  startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
@@ -8591,9 +8919,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
8591
8919
  for (const e of events) {
8592
8920
  if (e.type === "user_input") {
8593
8921
  openToolUses.clear();
8594
- messages.push({ role: "user", content: e.content });
8922
+ messages.push({ role: "user", content: e.content, ts: e.ts });
8595
8923
  } else if (e.type === "llm_response") {
8596
- messages.push({ role: "assistant", content: e.content });
8924
+ messages.push({ role: "assistant", content: e.content, ts: e.ts });
8597
8925
  for (const b of e.content) {
8598
8926
  if (b.type === "tool_use") openToolUses.add(b.id);
8599
8927
  }
@@ -8612,25 +8940,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
8612
8940
  continue;
8613
8941
  }
8614
8942
  openToolUses.delete(e.id);
8615
- const content = [
8616
- {
8617
- type: "tool_result",
8618
- tool_use_id: e.id,
8619
- content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
8620
- is_error: e.isError
8621
- }
8622
- ];
8943
+ const resultBlock = {
8944
+ type: "tool_result",
8945
+ tool_use_id: e.id,
8946
+ content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
8947
+ is_error: e.isError
8948
+ };
8623
8949
  const last = messages[messages.length - 1];
8624
- if (last && last.role === "user") {
8625
- if (Array.isArray(last.content)) {
8626
- last.content.push(...content);
8627
- } else if (typeof last.content === "string") {
8628
- last.content = [{ type: "text", text: last.content }, ...content];
8629
- } else {
8630
- messages.push({ role: "user", content });
8631
- }
8950
+ const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
8951
+ if (lastIsToolResultUser && Array.isArray(last.content)) {
8952
+ last.content.push(resultBlock);
8632
8953
  } else {
8633
- messages.push({ role: "user", content });
8954
+ messages.push({ role: "user", content: [resultBlock], ts: e.ts });
8634
8955
  }
8635
8956
  }
8636
8957
  }
@@ -8650,7 +8971,24 @@ var DefaultSessionStore = class _DefaultSessionStore {
8650
8971
  return { messages: repaired.messages, usage };
8651
8972
  }
8652
8973
  };
8653
- var FileSessionWriter = class {
8974
+ function extractToolCallEnds(events) {
8975
+ const result = [];
8976
+ for (const e of events) {
8977
+ if (e.type === "tool_call_end") {
8978
+ result.push({
8979
+ name: e.name,
8980
+ id: e.id,
8981
+ durationMs: e.durationMs,
8982
+ ok: e.ok ?? false,
8983
+ outputBytes: e.outputBytes,
8984
+ outputTokens: e.outputTokens,
8985
+ outputLines: e.outputLines
8986
+ });
8987
+ }
8988
+ }
8989
+ return result;
8990
+ }
8991
+ var FileSessionWriter = class _FileSessionWriter {
8654
8992
  constructor(id, handle, startedAt, meta, events, opts = {}) {
8655
8993
  this.id = id;
8656
8994
  this.handle = handle;
@@ -8677,7 +9015,7 @@ var FileSessionWriter = class {
8677
9015
  meta;
8678
9016
  events;
8679
9017
  closed = false;
8680
- closing = false;
9018
+ closePromise = null;
8681
9019
  manifestFile;
8682
9020
  summary;
8683
9021
  tokenIn = 0;
@@ -8686,12 +9024,51 @@ var FileSessionWriter = class {
8686
9024
  get transcriptPath() {
8687
9025
  return this.filePath || void 0;
8688
9026
  }
8689
- initDone = false;
9027
+ /**
9028
+ * Lazy session_start/session_resumed init, shared by all appenders.
9029
+ * A single promise (not a boolean) so a second append racing the first
9030
+ * can't push its event into the buffer BEFORE the first append's event —
9031
+ * every appender awaits the same init and resumes in FIFO call order.
9032
+ */
9033
+ initPromise = null;
9034
+ ensureInit() {
9035
+ if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
9036
+ return this.initPromise;
9037
+ }
8690
9038
  resumed;
8691
9039
  appendFailCount = 0;
8692
9040
  lastAppendWarnAt = 0;
8693
9041
  secretScrubber;
8694
9042
  onCloseCb;
9043
+ // ── Write buffer — batches events to reduce per-event disk I/O ─────────
9044
+ //
9045
+ // Every append() pushes the scrubbed event into an in-memory buffer instead
9046
+ // of calling handle.appendFile() synchronously. The buffer flushes to disk
9047
+ // when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
9048
+ // This cuts the number of disk writes by ~95% without changing the on-disk
9049
+ // format — the JSONL is still one JSON object per line.
9050
+ writeBuffer = [];
9051
+ flushTimer = null;
9052
+ static FLUSH_INTERVAL_MS = 500;
9053
+ static FLUSH_SIZE = 50;
9054
+ // ── Write serialization ─────────────────────────────────────────────────
9055
+ //
9056
+ // All disk writes are funneled through a FIFO promise chain. Without it,
9057
+ // a timer-driven flush racing an explicit flush()/close() issues two
9058
+ // concurrent appendFile() calls on the shared O_APPEND handle — the kernel
9059
+ // may complete them out of order (chronology breaks) or, for large
9060
+ // batches, interleave partial writes (torn JSONL lines). The chain keeps
9061
+ // exactly one write in flight; failures don't break the chain.
9062
+ writeChain = Promise.resolve();
9063
+ /** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
9064
+ enqueueWrite(data) {
9065
+ const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
9066
+ this.writeChain = write.then(
9067
+ () => void 0,
9068
+ () => void 0
9069
+ );
9070
+ return write;
9071
+ }
8695
9072
  // ── Enriched summary tracking ──────────────────────────────────────────
8696
9073
  iterationCount = 0;
8697
9074
  toolCallCount = 0;
@@ -8741,31 +9118,91 @@ var FileSessionWriter = class {
8741
9118
  })}
8742
9119
  `;
8743
9120
  try {
8744
- if (this.filePath) {
8745
- await fsp6.writeFile(this.filePath, record, { flag: "a", mode: 384 });
8746
- }
9121
+ await this.enqueueWrite(record);
8747
9122
  } catch {
8748
9123
  }
8749
9124
  }
8750
9125
  async append(event) {
8751
9126
  if (this.closed) return;
8752
- if (!this.initDone) {
8753
- this.initDone = true;
8754
- await this.writeSessionStartLazy();
8755
- }
9127
+ await this.ensureInit();
8756
9128
  const scrubbed = this.scrubEvent(event);
8757
9129
  this.observeForSummary(scrubbed);
9130
+ this.writeBuffer.push(scrubbed);
9131
+ if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
9132
+ if (this.flushTimer) {
9133
+ clearTimeout(this.flushTimer);
9134
+ this.flushTimer = null;
9135
+ }
9136
+ await this.flushBuffer();
9137
+ } else {
9138
+ this.scheduleFlush();
9139
+ }
9140
+ }
9141
+ async appendBatch(events) {
9142
+ if (this.closed || events.length === 0) return;
9143
+ await this.ensureInit();
9144
+ for (const event of events) {
9145
+ const scrubbed = this.scrubEvent(event);
9146
+ this.observeForSummary(scrubbed);
9147
+ this.writeBuffer.push(scrubbed);
9148
+ }
9149
+ if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
9150
+ if (this.flushTimer) {
9151
+ clearTimeout(this.flushTimer);
9152
+ this.flushTimer = null;
9153
+ }
9154
+ await this.flushBuffer();
9155
+ } else {
9156
+ this.scheduleFlush();
9157
+ }
9158
+ }
9159
+ /**
9160
+ * Flush buffered events to disk immediately. Critical events
9161
+ * (user_input, llm_response) call this so they survive SIGKILL/crash
9162
+ * instead of sitting in the in-memory buffer for up to 500ms.
9163
+ *
9164
+ * Idempotent — cancels any pending timer and writes whatever has
9165
+ * accumulated in the buffer. Safe to call even when the buffer
9166
+ * is empty (no-op).
9167
+ */
9168
+ async flush() {
9169
+ if (this.flushTimer) {
9170
+ clearTimeout(this.flushTimer);
9171
+ this.flushTimer = null;
9172
+ }
9173
+ await this.flushBuffer();
9174
+ }
9175
+ /** Schedule a deferred flush. No-op if a timer is already pending. */
9176
+ scheduleFlush() {
9177
+ if (this.flushTimer) return;
9178
+ this.flushTimer = setTimeout(() => {
9179
+ this.flushTimer = null;
9180
+ this.flushBuffer().catch(() => {
9181
+ });
9182
+ }, _FileSessionWriter.FLUSH_INTERVAL_MS);
9183
+ }
9184
+ /**
9185
+ * Flush all buffered events to disk as a single appendFile call.
9186
+ * Errors use the same throttled-warning pattern the old per-event
9187
+ * append path used — one warning every 5s with a suppressed count.
9188
+ * On failure the buffer is cleared (events are best-effort, same as
9189
+ * the old per-event path where a failed write was silently dropped).
9190
+ */
9191
+ async flushBuffer() {
9192
+ if (this.writeBuffer.length === 0) return;
9193
+ const eventCount = this.writeBuffer.length;
9194
+ const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
9195
+ this.writeBuffer = [];
8758
9196
  try {
8759
- await this.handle.appendFile(`${JSON.stringify(scrubbed)}
8760
- `, "utf8");
9197
+ await this.enqueueWrite(batch);
8761
9198
  } catch (err) {
8762
- this.appendFailCount++;
9199
+ this.appendFailCount += eventCount;
8763
9200
  const now = Date.now();
8764
9201
  if (now - this.lastAppendWarnAt > 5e3) {
8765
9202
  const suppressed = this.appendFailCount - 1;
8766
9203
  const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
8767
9204
  console.warn(
8768
- "[session] append failed:",
9205
+ "[session] flush failed:",
8769
9206
  err instanceof Error ? err.message : String(err),
8770
9207
  tail
8771
9208
  );
@@ -8775,6 +9212,11 @@ var FileSessionWriter = class {
8775
9212
  }
8776
9213
  }
8777
9214
  observeForSummary(event) {
9215
+ if (event.type === "llm_response") {
9216
+ for (const block of event.content) {
9217
+ if (block.type === "tool_use") this.openToolUses.add(block.id);
9218
+ }
9219
+ }
8778
9220
  if (event.type === "tool_use") {
8779
9221
  this.openToolUses.add(event.id);
8780
9222
  } else if (event.type === "tool_call_start") {
@@ -8808,9 +9250,18 @@ var FileSessionWriter = class {
8808
9250
  }
8809
9251
  }
8810
9252
  async close() {
8811
- if (this.closing) return;
8812
- this.closing = true;
9253
+ if (this.closePromise) return this.closePromise;
9254
+ this.closePromise = this.doClose();
9255
+ return this.closePromise;
9256
+ }
9257
+ async doClose() {
8813
9258
  this.closed = true;
9259
+ if (this.flushTimer) {
9260
+ clearTimeout(this.flushTimer);
9261
+ this.flushTimer = null;
9262
+ }
9263
+ await this.flushBuffer();
9264
+ await this.writeChain;
8814
9265
  this.summary = {
8815
9266
  ...this.summary,
8816
9267
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8866,6 +9317,12 @@ var FileSessionWriter = class {
8866
9317
  }
8867
9318
  async truncateToCheckpoint(targetPromptIndex) {
8868
9319
  if (!this.filePath) return 0;
9320
+ if (this.flushTimer) {
9321
+ clearTimeout(this.flushTimer);
9322
+ this.flushTimer = null;
9323
+ }
9324
+ await this.flushBuffer();
9325
+ await this.writeChain;
8869
9326
  const raw = await fsp6.readFile(this.filePath, "utf8");
8870
9327
  const lines = raw.split("\n");
8871
9328
  const kept = [];
@@ -8928,6 +9385,12 @@ var FileSessionWriter = class {
8928
9385
  }
8929
9386
  async clearSession() {
8930
9387
  if (!this.filePath) return;
9388
+ if (this.flushTimer) {
9389
+ clearTimeout(this.flushTimer);
9390
+ this.flushTimer = null;
9391
+ }
9392
+ this.writeBuffer = [];
9393
+ await this.writeChain;
8931
9394
  const record = `${JSON.stringify({
8932
9395
  type: "session_start",
8933
9396
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -9426,6 +9889,1518 @@ var FleetManager = class {
9426
9889
  }
9427
9890
  };
9428
9891
 
9429
- export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BudgetExceededError, BudgetThresholdSignal, CollabSession, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMessage, dispatchAgent, formatHumanPrompt, getAgentDefinition, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, rosterSummaryFromConfigs, scoreAgents };
9892
+ // src/coordination/mailbox-types.ts
9893
+ function normalizeRecipient(to) {
9894
+ return to.trim().toLowerCase() === "all" ? "*" : to.trim();
9895
+ }
9896
+
9897
+ // src/coordination/mailbox.ts
9898
+ var MAILBOX_FILE = "_mailbox.jsonl";
9899
+ var LINE_SEPARATOR = "\n";
9900
+ var DefaultMailbox = class {
9901
+ filePath;
9902
+ constructor(sessionDir) {
9903
+ this.filePath = path4.join(sessionDir, MAILBOX_FILE);
9904
+ }
9905
+ get mailboxPath() {
9906
+ return this.filePath;
9907
+ }
9908
+ // ── Send ──────────────────────────────────────────────────────────────
9909
+ async send(input) {
9910
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9911
+ const msg = {
9912
+ id: randomUUID(),
9913
+ from: input.from,
9914
+ // "all" is an accepted spelling of the broadcast address — canonical
9915
+ // form on disk is '*' so every query/checker matches it.
9916
+ to: normalizeRecipient(input.to),
9917
+ type: input.type,
9918
+ subject: input.subject,
9919
+ body: input.body,
9920
+ priority: input.priority ?? "normal",
9921
+ readBy: {},
9922
+ completed: false,
9923
+ timestamp: now,
9924
+ replyTo: input.replyTo,
9925
+ taskContext: input.taskContext
9926
+ };
9927
+ const line = JSON.stringify(msg) + LINE_SEPARATOR;
9928
+ await fsp6.mkdir(path4.dirname(this.filePath), { recursive: true });
9929
+ await withFileLock(this.filePath, async () => {
9930
+ await fsp6.appendFile(this.filePath, line, "utf8");
9931
+ });
9932
+ return msg;
9933
+ }
9934
+ // ── Query ─────────────────────────────────────────────────────────────
9935
+ async query(q) {
9936
+ const all = await this._readAll();
9937
+ const limit = q.limit ?? 50;
9938
+ let filtered = all;
9939
+ if (q.to !== void 0) {
9940
+ filtered = filtered.filter((m) => m.to === q.to || m.to === "*");
9941
+ }
9942
+ if (q.from !== void 0) {
9943
+ filtered = filtered.filter((m) => m.from === q.from);
9944
+ }
9945
+ if (q.unreadBy !== void 0) {
9946
+ filtered = filtered.filter((m) => !(q.unreadBy in m.readBy));
9947
+ }
9948
+ if (q.incompleteOnly) {
9949
+ filtered = filtered.filter((m) => !m.completed);
9950
+ }
9951
+ if (q.type !== void 0) {
9952
+ filtered = filtered.filter((m) => m.type === q.type);
9953
+ }
9954
+ if (q.minPriority !== void 0) {
9955
+ const order = { low: 0, normal: 1, high: 2 };
9956
+ const min = order[q.minPriority];
9957
+ filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
9958
+ }
9959
+ if (q.since !== void 0) {
9960
+ const since = q.since;
9961
+ filtered = filtered.filter((m) => m.timestamp > since);
9962
+ }
9963
+ filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
9964
+ return filtered.slice(0, limit);
9965
+ }
9966
+ // ── Ack ───────────────────────────────────────────────────────────────
9967
+ async ack(input) {
9968
+ let result = null;
9969
+ await withFileLock(this.filePath, async () => {
9970
+ const all = await this._readAll();
9971
+ const idx = all.findIndex((m) => m.id === input.messageId);
9972
+ if (idx === -1) return;
9973
+ const msg = all[idx];
9974
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9975
+ if (input.read !== false) {
9976
+ msg.readBy[input.readerId] = now;
9977
+ }
9978
+ if (input.completed) {
9979
+ msg.completed = true;
9980
+ msg.completedBy = input.readerId;
9981
+ msg.completedAt = now;
9982
+ }
9983
+ if (input.outcome !== void 0) {
9984
+ msg.outcome = input.outcome;
9985
+ }
9986
+ const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
9987
+ await fsp6.writeFile(this.filePath, serialized, "utf8");
9988
+ result = msg;
9989
+ });
9990
+ return result;
9991
+ }
9992
+ // ── Agent statuses ────────────────────────────────────────────────────
9993
+ async getAgentStatuses() {
9994
+ const all = await this._readAll();
9995
+ const latest = /* @__PURE__ */ new Map();
9996
+ for (const m of all) {
9997
+ if (m.type !== "status") continue;
9998
+ const existing = latest.get(m.from);
9999
+ if (existing && m.timestamp <= existing.lastActivityAt) continue;
10000
+ latest.set(m.from, {
10001
+ agentId: m.from,
10002
+ name: m.taskContext?.agentName ?? m.from,
10003
+ role: m.taskContext?.agentRole,
10004
+ sessionId: m.senderSessionId ?? "?",
10005
+ status: m.taskContext?.status ?? "idle",
10006
+ currentTool: void 0,
10007
+ currentTask: m.subject,
10008
+ iterations: 0,
10009
+ toolCalls: 0,
10010
+ lastActivityAt: m.timestamp,
10011
+ lastSeenAt: m.timestamp,
10012
+ online: true,
10013
+ pid: 0,
10014
+ source: void 0
10015
+ });
10016
+ }
10017
+ return Array.from(latest.values()).sort(
10018
+ (a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt)
10019
+ );
10020
+ }
10021
+ // ── Stubs for cross-session features (not applicable per-session) ─────
10022
+ async getOnlineAgents() {
10023
+ return this.getAgentStatuses();
10024
+ }
10025
+ async registerAgent(_input) {
10026
+ }
10027
+ async heartbeat(_input) {
10028
+ }
10029
+ async unreadCount(forAgentId) {
10030
+ const all = await this._readAll();
10031
+ return all.filter(
10032
+ (m) => (m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed
10033
+ ).length;
10034
+ }
10035
+ async close() {
10036
+ }
10037
+ async clearAll() {
10038
+ await withFileLock(this.filePath, async () => {
10039
+ await fsp6.writeFile(this.filePath, "", "utf8");
10040
+ });
10041
+ }
10042
+ // ── Internal ──────────────────────────────────────────────────────────
10043
+ async _readAll() {
10044
+ try {
10045
+ const raw = await fsp6.readFile(this.filePath, "utf8");
10046
+ const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
10047
+ const messages = [];
10048
+ for (const line of lines) {
10049
+ try {
10050
+ const parsed = JSON.parse(line);
10051
+ if (!parsed["readBy"]) {
10052
+ const readBy = {};
10053
+ if (parsed["read"] && parsed["readAt"]) {
10054
+ readBy[parsed["to"] ?? "unknown"] = parsed["readAt"];
10055
+ }
10056
+ parsed["readBy"] = readBy;
10057
+ delete parsed["read"];
10058
+ delete parsed["readAt"];
10059
+ }
10060
+ messages.push(parsed);
10061
+ } catch {
10062
+ }
10063
+ }
10064
+ return messages;
10065
+ } catch (err) {
10066
+ if (err.code === "ENOENT") return [];
10067
+ throw err;
10068
+ }
10069
+ }
10070
+ };
10071
+ var BrainMonitor = class {
10072
+ constructor(opts) {
10073
+ this.opts = opts;
10074
+ this.toolFailureStreak = opts.toolFailureStreak ?? 3;
10075
+ this.errorStormCount = opts.errorStormCount ?? 4;
10076
+ this.errorStormWindowMs = opts.errorStormWindowMs ?? 6e4;
10077
+ this.cooldownMs = opts.cooldownMs ?? 12e4;
10078
+ }
10079
+ opts;
10080
+ failStreaks = /* @__PURE__ */ new Map();
10081
+ errorTimestamps = [];
10082
+ lastEngagedAt = /* @__PURE__ */ new Map();
10083
+ unsubscribers = [];
10084
+ engaging = false;
10085
+ toolFailureStreak;
10086
+ errorStormCount;
10087
+ errorStormWindowMs;
10088
+ cooldownMs;
10089
+ start() {
10090
+ this.unsubscribers.push(
10091
+ this.opts.events.on("tool.executed", (e) => {
10092
+ if (e.ok) {
10093
+ this.failStreaks.delete(e.name);
10094
+ return;
10095
+ }
10096
+ const streak = (this.failStreaks.get(e.name) ?? 0) + 1;
10097
+ this.failStreaks.set(e.name, streak);
10098
+ if (streak >= this.toolFailureStreak) {
10099
+ this.failStreaks.delete(e.name);
10100
+ void this.engage("tool_failure_streak", {
10101
+ question: `The tool "${e.name}" has failed ${streak} times in a row. Should the agent be steered to a different approach?`,
10102
+ context: [
10103
+ `Tool: ${e.name}`,
10104
+ `Consecutive failures: ${streak}`,
10105
+ e.output ? `Last output (truncated): ${String(e.output).slice(0, 400)}` : ""
10106
+ ].filter(Boolean).join("\n")
10107
+ });
10108
+ }
10109
+ })
10110
+ );
10111
+ this.unsubscribers.push(
10112
+ this.opts.events.on("error", (e) => {
10113
+ const now = Date.now();
10114
+ this.errorTimestamps.push(now);
10115
+ this.errorTimestamps = this.errorTimestamps.filter(
10116
+ (t) => now - t <= this.errorStormWindowMs
10117
+ );
10118
+ if (this.errorTimestamps.length >= this.errorStormCount) {
10119
+ const count = this.errorTimestamps.length;
10120
+ this.errorTimestamps = [];
10121
+ const message = e.err instanceof Error ? e.err.message : String(e.err);
10122
+ void this.engage("error_storm", {
10123
+ question: `${count} errors occurred within ${Math.round(this.errorStormWindowMs / 1e3)}s (phase: ${e.phase}). Should the agent be steered before more work is wasted?`,
10124
+ context: `Latest error: ${message.slice(0, 400)}`
10125
+ });
10126
+ }
10127
+ })
10128
+ );
10129
+ }
10130
+ stop() {
10131
+ for (const off of this.unsubscribers) off();
10132
+ this.unsubscribers.length = 0;
10133
+ this.failStreaks.clear();
10134
+ this.errorTimestamps = [];
10135
+ }
10136
+ async engage(kind, input) {
10137
+ const last = this.lastEngagedAt.get(kind) ?? 0;
10138
+ if (this.engaging || Date.now() - last < this.cooldownMs) return;
10139
+ this.engaging = true;
10140
+ this.lastEngagedAt.set(kind, Date.now());
10141
+ try {
10142
+ const request = {
10143
+ id: `brainmon-${randomUUID()}`,
10144
+ source: "system",
10145
+ question: input.question,
10146
+ context: input.context,
10147
+ options: [
10148
+ {
10149
+ id: "steer",
10150
+ label: "Steer the agent with corrective guidance",
10151
+ consequence: "A steer message is injected before its next step.",
10152
+ risk: "low"
10153
+ },
10154
+ {
10155
+ id: "continue",
10156
+ label: "Let the agent continue unaided",
10157
+ risk: "low"
10158
+ }
10159
+ ],
10160
+ risk: "medium",
10161
+ // Without an LLM layer the policy brain resolves this fallback to
10162
+ // "continue" — the monitor observes but never interferes.
10163
+ fallback: "continue"
10164
+ };
10165
+ const decision = await this.opts.brain.decide(request);
10166
+ const intervened = await this.maybeIntervene(kind, request, decision);
10167
+ this.opts.events.emit("brain.intervention", {
10168
+ kind,
10169
+ request,
10170
+ decision,
10171
+ intervened,
10172
+ at: Date.now()
10173
+ });
10174
+ } catch {
10175
+ } finally {
10176
+ this.engaging = false;
10177
+ }
10178
+ }
10179
+ async maybeIntervene(kind, request, decision) {
10180
+ if (decision.type !== "answer") return false;
10181
+ const choseSteer = decision.optionId === "steer";
10182
+ const freeTextGuidance = !decision.optionId && !/^continue\b/i.test(decision.text.trim()) && decision.text.trim().length > 0;
10183
+ if (!choseSteer && !freeTextGuidance) return false;
10184
+ const guidance = decision.rationale?.trim() || decision.text.trim();
10185
+ try {
10186
+ await this.opts.intervene({
10187
+ subject: `Brain intervention: ${kind.replace(/_/g, " ")}`,
10188
+ body: [
10189
+ `The Brain engaged after detecting: ${request.question}`,
10190
+ "",
10191
+ `Guidance: ${guidance}`,
10192
+ "",
10193
+ "Adjust your approach accordingly \u2014 do not simply retry the same action."
10194
+ ].join("\n")
10195
+ });
10196
+ return true;
10197
+ } catch {
10198
+ return false;
10199
+ }
10200
+ }
10201
+ };
10202
+ function projectSlug(absRoot) {
10203
+ const base = slugify(path4.basename(absRoot));
10204
+ const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
10205
+ return `${base}-${hash}`;
10206
+ }
10207
+ function slugify(name) {
10208
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
10209
+ }
10210
+ function wstackGlobalRoot() {
10211
+ const fromEnv = process.env["WRONGSTACK_HOME"];
10212
+ if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
10213
+ return path4.join(os.homedir(), ".wrongstack");
10214
+ }
10215
+
10216
+ // src/coordination/global-mailbox.ts
10217
+ var MAILBOX_FILE2 = "_mailbox.jsonl";
10218
+ var AGENT_STALE_MS = 6e4;
10219
+ var HEARTBEAT_THROTTLE_MS = 5e3;
10220
+ var REGISTRY_CACHE_TTL_MS = 2e3;
10221
+ var LINE_SEPARATOR2 = "\n";
10222
+ function resolveProjectDir(projectRoot, globalRoot) {
10223
+ return path4.join(globalRoot, "projects", projectSlug(projectRoot));
10224
+ }
10225
+ var GlobalMailbox = class {
10226
+ /** Path to the JSONL message file. */
10227
+ messagePath;
10228
+ /** Path to the JSON agent registry file. */
10229
+ registryPath;
10230
+ /** Optional event bus for emitting agent registration/heartbeat events. */
10231
+ _events;
10232
+ /**
10233
+ * Local cache of the agent registry to avoid re-reading on every call.
10234
+ * Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
10235
+ * whole point of GlobalMailbox), so a cache served forever would never see
10236
+ * agents registered by other sessions. Writers always bypass it.
10237
+ */
10238
+ _registryCache = null;
10239
+ /** When the registry cache was last refreshed from disk (epoch ms). */
10240
+ _registryCacheAt = 0;
10241
+ /** Last time each local agent sent a heartbeat (throttle). */
10242
+ _lastHeartbeat = /* @__PURE__ */ new Map();
10243
+ /**
10244
+ * @param projectDir — `~/.wrongstack/projects/<slug>/`
10245
+ * @param events — optional EventBus for real-time TUI/WebUI notifications
10246
+ */
10247
+ constructor(projectDir, events) {
10248
+ this.messagePath = path4.join(projectDir, MAILBOX_FILE2);
10249
+ this.registryPath = path4.join(projectDir, "_mailbox.registry.json");
10250
+ this._events = events;
10251
+ }
10252
+ // ── Messages ────────────────────────────────────────────────────────────
10253
+ async send(input) {
10254
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10255
+ const msg = {
10256
+ id: randomUUID(),
10257
+ from: input.from,
10258
+ // "all" is an accepted spelling of the broadcast address — canonical
10259
+ // form on disk is '*' so every query/checker matches it.
10260
+ to: normalizeRecipient(input.to),
10261
+ type: input.type,
10262
+ subject: input.subject,
10263
+ body: input.body,
10264
+ priority: input.priority ?? "normal",
10265
+ readBy: {},
10266
+ completed: false,
10267
+ timestamp: now,
10268
+ replyTo: input.replyTo,
10269
+ taskContext: input.taskContext
10270
+ };
10271
+ const line = JSON.stringify(msg) + LINE_SEPARATOR2;
10272
+ await fsp6.mkdir(path4.dirname(this.messagePath), { recursive: true });
10273
+ await withFileLock(this.messagePath, async () => {
10274
+ await fsp6.appendFile(this.messagePath, line, "utf8");
10275
+ });
10276
+ return msg;
10277
+ }
10278
+ async query(q) {
10279
+ const all = await this._readMessages();
10280
+ const limit = q.limit ?? 50;
10281
+ let filtered = all;
10282
+ if (q.to !== void 0) {
10283
+ filtered = filtered.filter((m) => m.to === q.to || m.to === "*");
10284
+ }
10285
+ if (q.from !== void 0) {
10286
+ filtered = filtered.filter((m) => m.from === q.from);
10287
+ }
10288
+ if (q.unreadBy !== void 0) {
10289
+ filtered = filtered.filter((m) => !(q.unreadBy in m.readBy));
10290
+ }
10291
+ if (q.incompleteOnly) {
10292
+ filtered = filtered.filter((m) => !m.completed);
10293
+ }
10294
+ if (q.type !== void 0) {
10295
+ filtered = filtered.filter((m) => m.type === q.type);
10296
+ }
10297
+ if (q.minPriority !== void 0) {
10298
+ const order = { low: 0, normal: 1, high: 2 };
10299
+ const min = order[q.minPriority];
10300
+ filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
10301
+ }
10302
+ if (q.since !== void 0) {
10303
+ const since = q.since;
10304
+ filtered = filtered.filter((m) => m.timestamp > since);
10305
+ }
10306
+ filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
10307
+ return filtered.slice(0, limit);
10308
+ }
10309
+ async ack(input) {
10310
+ let result = null;
10311
+ await withFileLock(this.messagePath, async () => {
10312
+ const all = await this._readMessages();
10313
+ const idx = all.findIndex((m) => m.id === input.messageId);
10314
+ if (idx === -1) return;
10315
+ const msg = all[idx];
10316
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10317
+ if (input.read !== false) {
10318
+ msg.readBy[input.readerId] = now;
10319
+ }
10320
+ if (input.completed) {
10321
+ msg.completed = true;
10322
+ msg.completedBy = input.readerId;
10323
+ msg.completedAt = now;
10324
+ }
10325
+ if (input.outcome !== void 0) {
10326
+ msg.outcome = input.outcome;
10327
+ }
10328
+ const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
10329
+ await fsp6.writeFile(this.messagePath, serialized, "utf8");
10330
+ result = msg;
10331
+ });
10332
+ return result;
10333
+ }
10334
+ async unreadCount(forAgentId) {
10335
+ const all = await this._readMessages();
10336
+ return all.filter(
10337
+ (m) => (m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed
10338
+ ).length;
10339
+ }
10340
+ // ── Agent registry ──────────────────────────────────────────────────────
10341
+ async registerAgent(input) {
10342
+ await this._ensureRegistry();
10343
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10344
+ const agent = {
10345
+ agentId: input.agentId,
10346
+ sessionId: input.sessionId,
10347
+ name: input.name,
10348
+ role: input.role,
10349
+ status: "idle",
10350
+ currentTool: void 0,
10351
+ currentTask: void 0,
10352
+ iterations: 0,
10353
+ toolCalls: 0,
10354
+ registeredAt: now,
10355
+ lastSeenAt: now,
10356
+ pid: input.pid,
10357
+ source: input.source
10358
+ };
10359
+ await withFileLock(this.registryPath, async () => {
10360
+ const registry = await this._readRegistry({ fresh: true });
10361
+ this._pruneStaleInPlace(registry);
10362
+ registry.set(input.agentId, agent);
10363
+ this._registryCache = registry;
10364
+ this._registryCacheAt = Date.now();
10365
+ await this._writeRegistry(registry);
10366
+ });
10367
+ this._events?.emitCustom("mailbox.agent_registered", {
10368
+ agentId: input.agentId,
10369
+ sessionId: input.sessionId,
10370
+ name: input.name,
10371
+ role: input.role,
10372
+ source: input.source
10373
+ });
10374
+ }
10375
+ async heartbeat(input) {
10376
+ const last = this._lastHeartbeat.get(input.agentId) ?? 0;
10377
+ const now = Date.now();
10378
+ if (now - last < HEARTBEAT_THROTTLE_MS) return;
10379
+ this._lastHeartbeat.set(input.agentId, now);
10380
+ await this._ensureRegistry();
10381
+ await withFileLock(this.registryPath, async () => {
10382
+ const registry = await this._readRegistry({ fresh: true });
10383
+ this._pruneStaleInPlace(registry);
10384
+ const agent = registry.get(input.agentId);
10385
+ if (agent) {
10386
+ const iso = (/* @__PURE__ */ new Date()).toISOString();
10387
+ agent.lastSeenAt = iso;
10388
+ if (input.status !== void 0) agent.status = input.status;
10389
+ if (input.currentTool !== void 0) agent.currentTool = input.currentTool;
10390
+ if (input.currentTask !== void 0) agent.currentTask = input.currentTask;
10391
+ if (input.iterations !== void 0) agent.iterations = input.iterations;
10392
+ if (input.toolCalls !== void 0) agent.toolCalls = input.toolCalls;
10393
+ }
10394
+ this._registryCache = registry;
10395
+ this._registryCacheAt = Date.now();
10396
+ await this._writeRegistry(registry);
10397
+ });
10398
+ this._events?.emitCustom("mailbox.agent_heartbeat", {
10399
+ agentId: input.agentId,
10400
+ status: input.status,
10401
+ currentTool: input.currentTool,
10402
+ currentTask: input.currentTask
10403
+ });
10404
+ }
10405
+ async getAgentStatuses() {
10406
+ await this._ensureRegistry();
10407
+ const registry = await this._readRegistry();
10408
+ this._pruneStaleInPlace(registry);
10409
+ const now = Date.now();
10410
+ return Array.from(registry.values()).map((a) => ({
10411
+ agentId: a.agentId,
10412
+ name: a.name,
10413
+ role: a.role,
10414
+ sessionId: a.sessionId,
10415
+ status: a.status,
10416
+ currentTool: a.currentTool,
10417
+ currentTask: a.currentTask,
10418
+ iterations: a.iterations,
10419
+ toolCalls: a.toolCalls,
10420
+ lastActivityAt: a.lastSeenAt,
10421
+ lastSeenAt: a.lastSeenAt,
10422
+ online: now - new Date(a.lastSeenAt).getTime() < AGENT_STALE_MS,
10423
+ pid: a.pid,
10424
+ source: a.source
10425
+ })).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
10426
+ }
10427
+ async getOnlineAgents() {
10428
+ const all = await this.getAgentStatuses();
10429
+ return all.filter((a) => a.online);
10430
+ }
10431
+ // ── Lifecycle ───────────────────────────────────────────────────────────
10432
+ async close() {
10433
+ this._registryCache = null;
10434
+ }
10435
+ async clearAll() {
10436
+ await withFileLock(this.messagePath, async () => {
10437
+ await fsp6.writeFile(this.messagePath, "", "utf8");
10438
+ });
10439
+ }
10440
+ // ── Internal ────────────────────────────────────────────────────────────
10441
+ async _readMessages() {
10442
+ try {
10443
+ const raw = await fsp6.readFile(this.messagePath, "utf8");
10444
+ const lines = raw.split(LINE_SEPARATOR2).filter((l) => l.trim().length > 0);
10445
+ const messages = [];
10446
+ for (const line of lines) {
10447
+ try {
10448
+ const parsed = JSON.parse(line);
10449
+ if (!parsed["readBy"]) {
10450
+ const readBy = {};
10451
+ if (parsed["read"] && parsed["readAt"]) {
10452
+ readBy[parsed["to"]] = parsed["readAt"];
10453
+ }
10454
+ parsed["readBy"] = readBy;
10455
+ delete parsed["read"];
10456
+ delete parsed["readAt"];
10457
+ }
10458
+ messages.push(parsed);
10459
+ } catch {
10460
+ }
10461
+ }
10462
+ return messages;
10463
+ } catch (err) {
10464
+ if (err.code === "ENOENT") return [];
10465
+ throw err;
10466
+ }
10467
+ }
10468
+ async _ensureRegistry() {
10469
+ await fsp6.mkdir(path4.dirname(this.registryPath), { recursive: true });
10470
+ }
10471
+ async _readRegistry(opts) {
10472
+ if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
10473
+ return new Map(this._registryCache);
10474
+ }
10475
+ try {
10476
+ const raw = await fsp6.readFile(this.registryPath, "utf8");
10477
+ const data = JSON.parse(raw);
10478
+ const map = /* @__PURE__ */ new Map();
10479
+ for (const [id, agent] of Object.entries(data)) {
10480
+ map.set(id, agent);
10481
+ }
10482
+ this._registryCache = map;
10483
+ this._registryCacheAt = Date.now();
10484
+ return new Map(map);
10485
+ } catch (err) {
10486
+ if (err.code === "ENOENT") {
10487
+ const empty = /* @__PURE__ */ new Map();
10488
+ this._registryCache = empty;
10489
+ this._registryCacheAt = Date.now();
10490
+ return empty;
10491
+ }
10492
+ throw err;
10493
+ }
10494
+ }
10495
+ _pruneStaleInPlace(registry) {
10496
+ const cutoff = Date.now() - AGENT_STALE_MS;
10497
+ for (const agent of registry.values()) {
10498
+ if (new Date(agent.lastSeenAt).getTime() < cutoff) {
10499
+ agent.status = "idle";
10500
+ }
10501
+ }
10502
+ }
10503
+ async _writeRegistry(registry) {
10504
+ const obj = {};
10505
+ for (const [id, agent] of registry) {
10506
+ obj[id] = agent;
10507
+ }
10508
+ const tmp = `${this.registryPath}.${randomUUID().slice(0, 8)}.tmp`;
10509
+ await fsp6.writeFile(tmp, JSON.stringify(obj, null, 2), "utf8");
10510
+ await fsp6.rename(tmp, this.registryPath);
10511
+ }
10512
+ };
10513
+ function defaultResolveProjectDir(ctx) {
10514
+ return resolveProjectDir(ctx.projectRoot, wstackGlobalRoot());
10515
+ }
10516
+ function mailboxSessionTag(sessionId) {
10517
+ return createHash("sha256").update(sessionId).digest("hex").slice(0, 8);
10518
+ }
10519
+ function resolveMailboxIdentity(ctx, fallbackBase = "leader") {
10520
+ const fieldId = ctx.agentId && ctx.agentId !== "unknown" ? ctx.agentId : void 0;
10521
+ const baseId = ctx.meta["agentId"] ?? fieldId ?? fallbackBase;
10522
+ const sessionId = ctx.meta["sessionId"] ?? ctx.session?.id ?? "default";
10523
+ const callerId = ctx.meta["globalAgentId"] ?? `${baseId}@${mailboxSessionTag(sessionId)}`;
10524
+ const fieldName = ctx.agentName && ctx.agentName !== "Unknown Agent" ? ctx.agentName : void 0;
10525
+ const name = ctx.meta["agentName"] ?? fieldName ?? baseId;
10526
+ const role = ctx.meta["agentRole"];
10527
+ return { baseId, callerId, name, role, sessionId };
10528
+ }
10529
+ function makeMailboxTool(opts = {}) {
10530
+ const resolveMailbox = opts.resolveMailbox ?? ((ctx) => {
10531
+ const dir = opts.projectDir ?? defaultResolveProjectDir(ctx);
10532
+ return new GlobalMailbox(dir, opts.events);
10533
+ });
10534
+ const agentId = opts.agentId ?? "leader";
10535
+ const sessionId = opts.sessionId ?? "default";
10536
+ const shortHint = "Sub-commands: check (unread), send (to/broadcast), ack (read/complete), query (filter), status (all agents), online (active only), unread (count).";
10537
+ return {
10538
+ name: "mailbox",
10539
+ description: "Inter-agent mailbox with cross-session support. Send messages, check for incoming messages, acknowledge with read receipts, query by criteria, see online agents.",
10540
+ usageHint: shortHint,
10541
+ category: "coordination",
10542
+ permission: "auto",
10543
+ mutating: true,
10544
+ inputSchema: {
10545
+ type: "object",
10546
+ properties: {
10547
+ action: {
10548
+ type: "string",
10549
+ enum: ["check", "send", "ack", "query", "status", "online", "unread"],
10550
+ description: "Which mailbox operation to perform."
10551
+ },
10552
+ to: { type: "string", description: "Recipient agent id, or '*' / 'all' for broadcast." },
10553
+ type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result"], description: "Message type." },
10554
+ subject: { type: "string", description: "Short subject line." },
10555
+ body: { type: "string", description: "Full message content." },
10556
+ priority: { type: "string", enum: ["low", "normal", "high"] },
10557
+ replyTo: { type: "string", description: "Reply to a specific message id." },
10558
+ messageId: { type: "string", description: "Message id to acknowledge. Required for 'ack'." },
10559
+ read: { type: "boolean", description: "Mark as read (adds read receipt)." },
10560
+ completed: { type: "boolean", description: "Mark as completed." },
10561
+ outcome: { type: "string", description: "Outcome summary when marking complete." },
10562
+ unreadBy: { type: "string", description: "Filter messages unread by this agent. Used by 'check'." },
10563
+ incompleteOnly: { type: "boolean", description: "Only incomplete messages." },
10564
+ from: { type: "string", description: "Filter by sender." },
10565
+ minPriority: { type: "string", enum: ["low", "normal", "high"] },
10566
+ since: { type: "string", description: "ISO8601 timestamp \u2014 only messages after this." },
10567
+ limit: { type: "number", description: "Max messages to return." }
10568
+ },
10569
+ required: ["action"]
10570
+ },
10571
+ async execute(input, ctx) {
10572
+ const mb = resolveMailbox(ctx);
10573
+ const i = input ?? {};
10574
+ const action = i.action;
10575
+ const identity = resolveMailboxIdentity(ctx, agentId);
10576
+ const baseCallerId = identity.baseId;
10577
+ const callerId = identity.callerId;
10578
+ const callerSessionId = ctx.meta["sessionId"] ?? (ctx.session?.id ?? sessionId);
10579
+ try {
10580
+ await mb.registerAgent({
10581
+ agentId: callerId,
10582
+ sessionId: callerSessionId,
10583
+ name: identity.name,
10584
+ role: identity.role,
10585
+ pid: process.pid,
10586
+ source: ctx.meta["source"] ?? "cli"
10587
+ });
10588
+ } catch {
10589
+ }
10590
+ try {
10591
+ await mb.heartbeat({ agentId: callerId });
10592
+ } catch {
10593
+ }
10594
+ switch (action) {
10595
+ case "check":
10596
+ return executeCheck(mb, callerId, [baseCallerId], i);
10597
+ case "send":
10598
+ return executeSend(mb, callerId, callerSessionId, i);
10599
+ case "ack":
10600
+ return executeAck(mb, callerId, i);
10601
+ case "query":
10602
+ return executeQuery(mb, i);
10603
+ case "status":
10604
+ return executeStatus(mb);
10605
+ case "online":
10606
+ return executeOnline(mb);
10607
+ case "unread":
10608
+ return executeUnread(mb, callerId, [baseCallerId]);
10609
+ default:
10610
+ return { ok: false, error: `Unknown action: "${action}". Use check, send, ack, query, status, online, or unread.` };
10611
+ }
10612
+ }
10613
+ };
10614
+ }
10615
+ async function executeCheck(mb, agentId, aliases, i) {
10616
+ const limit = i.limit ?? 20;
10617
+ const targets = [agentId, ...aliases.filter((al) => al && al !== agentId)];
10618
+ const batches = await Promise.all(
10619
+ targets.map(
10620
+ (to) => mb.query({ to, unreadBy: agentId, limit, minPriority: "low" }).catch(() => [])
10621
+ )
10622
+ );
10623
+ const seen = /* @__PURE__ */ new Set();
10624
+ const messages = batches.flat().filter((m) => {
10625
+ if (seen.has(m.id)) return false;
10626
+ seen.add(m.id);
10627
+ return true;
10628
+ });
10629
+ const acked = await Promise.all(
10630
+ messages.map(async (m) => {
10631
+ const updated = await mb.ack({ messageId: m.id, readerId: agentId, read: true }).catch(() => null);
10632
+ return updated ?? m;
10633
+ })
10634
+ );
10635
+ return {
10636
+ ok: true,
10637
+ count: acked.length,
10638
+ messages: acked.map((m) => formatMessage(m, agentId)),
10639
+ summary: acked.length === 0 ? "No unread messages." : `${acked.length} unread message(s).`
10640
+ };
10641
+ }
10642
+ async function executeSend(mb, agentId, _sessionId, i) {
10643
+ const to = i.to;
10644
+ const tp = i.type;
10645
+ const subject = i.subject;
10646
+ const body = i.body;
10647
+ if (!to) return { ok: false, error: '"to" is required.' };
10648
+ if (!tp) return { ok: false, error: '"type" is required.' };
10649
+ if (!subject) return { ok: false, error: '"subject" is required.' };
10650
+ if (body === void 0 || body === null) return { ok: false, error: '"body" is required.' };
10651
+ const msg = await mb.send({
10652
+ from: agentId,
10653
+ to,
10654
+ type: tp,
10655
+ subject,
10656
+ body,
10657
+ priority: i.priority ?? "normal",
10658
+ replyTo: i.replyTo
10659
+ });
10660
+ return {
10661
+ ok: true,
10662
+ messageId: msg.id,
10663
+ to: msg.to,
10664
+ type: msg.type,
10665
+ timestamp: msg.timestamp,
10666
+ summary: `Message sent to ${msg.to === "*" ? "all agents" : msg.to}. Id: ${msg.id}`
10667
+ };
10668
+ }
10669
+ async function executeAck(mb, agentId, i) {
10670
+ const messageId = i.messageId;
10671
+ if (!messageId) return { ok: false, error: '"messageId" is required.' };
10672
+ const updated = await mb.ack({
10673
+ messageId,
10674
+ readerId: agentId,
10675
+ read: i.read,
10676
+ completed: i.completed,
10677
+ outcome: i.outcome
10678
+ });
10679
+ if (!updated) return { ok: false, error: `Message "${messageId}" not found.` };
10680
+ return {
10681
+ ok: true,
10682
+ messageId: updated.id,
10683
+ readBy: Object.keys(updated.readBy),
10684
+ readByCount: Object.keys(updated.readBy).length,
10685
+ completed: updated.completed,
10686
+ completedBy: updated.completedBy,
10687
+ outcome: updated.outcome,
10688
+ summary: `Message ${messageId} acknowledged. Read by ${Object.keys(updated.readBy).length} agent(s), Completed: ${updated.completed}.`
10689
+ };
10690
+ }
10691
+ async function executeQuery(mb, i) {
10692
+ const limit = i.limit ?? 50;
10693
+ const messages = await mb.query({
10694
+ to: i.to,
10695
+ from: i.from,
10696
+ unreadBy: i.unreadBy,
10697
+ incompleteOnly: i.incompleteOnly,
10698
+ type: i.type,
10699
+ minPriority: i.minPriority,
10700
+ since: i.since,
10701
+ limit
10702
+ });
10703
+ return { ok: true, count: messages.length, messages, summary: `${messages.length} message(s).` };
10704
+ }
10705
+ async function executeStatus(mb) {
10706
+ const agents = await mb.getAgentStatuses();
10707
+ return {
10708
+ ok: true,
10709
+ count: agents.length,
10710
+ agents: agents.map((a) => ({
10711
+ agentId: a.agentId,
10712
+ name: a.name,
10713
+ role: a.role,
10714
+ sessionId: a.sessionId,
10715
+ status: a.status,
10716
+ currentTool: a.currentTool,
10717
+ currentTask: a.currentTask,
10718
+ iterations: a.iterations,
10719
+ toolCalls: a.toolCalls,
10720
+ lastSeenAt: a.lastSeenAt,
10721
+ online: a.online,
10722
+ pid: a.pid,
10723
+ source: a.source
10724
+ })),
10725
+ summary: `${agents.filter((a) => a.online).length} online, ${agents.length} total.`
10726
+ };
10727
+ }
10728
+ async function executeOnline(mb) {
10729
+ const agents = await mb.getOnlineAgents();
10730
+ return {
10731
+ ok: true,
10732
+ count: agents.length,
10733
+ agents: agents.map((a) => ({
10734
+ agentId: a.agentId,
10735
+ name: a.name,
10736
+ role: a.role,
10737
+ sessionId: a.sessionId,
10738
+ status: a.status,
10739
+ currentTool: a.currentTool,
10740
+ currentTask: a.currentTask,
10741
+ lastSeenAt: a.lastSeenAt,
10742
+ source: a.source
10743
+ })),
10744
+ summary: `${agents.length} online agent(s).`
10745
+ };
10746
+ }
10747
+ async function executeUnread(mb, agentId, aliases = []) {
10748
+ const targets = [agentId, ...aliases.filter((al) => al && al !== agentId)];
10749
+ const batches = await Promise.all(
10750
+ targets.map((to) => mb.query({ to, unreadBy: agentId, limit: 200 }).catch(() => []))
10751
+ );
10752
+ const ids = new Set(batches.flat().map((m) => m.id));
10753
+ return { ok: true, count: ids.size, summary: `${ids.size} unread message(s) for you.` };
10754
+ }
10755
+ function formatMessage(m, readerId) {
10756
+ const maxBody = 2e3;
10757
+ const truncated = m.body.length > maxBody ? `${m.body.slice(0, maxBody)}\u2026 [truncated]` : m.body;
10758
+ return {
10759
+ id: m.id,
10760
+ from: m.from,
10761
+ to: m.to,
10762
+ type: m.type,
10763
+ subject: m.subject,
10764
+ body: truncated,
10765
+ priority: m.priority,
10766
+ readByMe: readerId in m.readBy,
10767
+ readByCount: Object.keys(m.readBy).length,
10768
+ readBy: m.readBy,
10769
+ completed: m.completed,
10770
+ completedBy: m.completedBy,
10771
+ outcome: m.outcome,
10772
+ timestamp: m.timestamp,
10773
+ replyTo: m.replyTo,
10774
+ senderSessionId: m.senderSessionId
10775
+ };
10776
+ }
10777
+
10778
+ // src/coordination/mail-tools.ts
10779
+ function makeResolver(opts) {
10780
+ return opts.resolveMailbox ?? ((ctx) => new GlobalMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
10781
+ }
10782
+ async function register(mb, ctx) {
10783
+ const identity = resolveMailboxIdentity(ctx);
10784
+ try {
10785
+ await mb.registerAgent({
10786
+ agentId: identity.callerId,
10787
+ sessionId: identity.sessionId,
10788
+ name: identity.name,
10789
+ role: identity.role,
10790
+ pid: process.pid,
10791
+ source: ctx.meta["source"] ?? "cli"
10792
+ });
10793
+ await mb.heartbeat({ agentId: identity.callerId });
10794
+ } catch {
10795
+ }
10796
+ return identity;
10797
+ }
10798
+ function makeMailSendTool(opts = {}) {
10799
+ const resolveMailbox = makeResolver(opts);
10800
+ return {
10801
+ name: "mail_send",
10802
+ description: 'Send a mail to other agents working on this project (other terminals, TUIs, WebUIs). Use it to hand off work ("can you review src/auth.ts?"), ask questions, or announce what you just did. to="*" broadcasts to everyone; to="leader" reaches every leader process; an exact id like "leader@a1b2c3d4" reaches one agent. Recipients see your mail automatically before their next step.',
10803
+ usageHint: 'mail_send to="*" subject="auth refactor done" body="touched src/auth/*, please review"',
10804
+ category: "coordination",
10805
+ permission: "auto",
10806
+ mutating: true,
10807
+ inputSchema: {
10808
+ type: "object",
10809
+ properties: {
10810
+ to: {
10811
+ type: "string",
10812
+ description: 'Recipient: exact agent id ("leader@a1b2c3d4"), base alias ("leader"), or "*" / "all" for everyone.'
10813
+ },
10814
+ subject: { type: "string", description: "Short subject line." },
10815
+ body: { type: "string", description: "The message." },
10816
+ type: {
10817
+ type: "string",
10818
+ enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result"],
10819
+ description: 'Message intent. Default: "broadcast" when to="*", otherwise "note".'
10820
+ },
10821
+ priority: { type: "string", enum: ["low", "normal", "high"] },
10822
+ replyTo: { type: "string", description: "Message id this replies to." }
10823
+ },
10824
+ required: ["to", "subject", "body"]
10825
+ },
10826
+ async execute(input, ctx) {
10827
+ const i = input ?? {};
10828
+ const rawTo = i.to;
10829
+ const subject = i.subject;
10830
+ const body = i.body;
10831
+ if (!rawTo || !subject || body === void 0 || body === null) {
10832
+ return { ok: false, error: '"to", "subject" and "body" are required.' };
10833
+ }
10834
+ const to = normalizeRecipient(rawTo);
10835
+ const mb = resolveMailbox(ctx);
10836
+ const identity = await register(mb, ctx);
10837
+ const type = i.type ?? (to === "*" ? "broadcast" : "note");
10838
+ const msg = await mb.send({
10839
+ from: identity.callerId,
10840
+ to,
10841
+ type,
10842
+ subject,
10843
+ body,
10844
+ priority: i.priority ?? "normal",
10845
+ replyTo: i.replyTo
10846
+ });
10847
+ return {
10848
+ ok: true,
10849
+ messageId: msg.id,
10850
+ from: identity.callerId,
10851
+ to: msg.to,
10852
+ summary: `Mail sent to ${msg.to === "*" ? "all agents" : msg.to} as ${identity.callerId}.`
10853
+ };
10854
+ }
10855
+ };
10856
+ }
10857
+ function makeMailInboxTool(opts = {}) {
10858
+ const resolveMailbox = makeResolver(opts);
10859
+ return {
10860
+ name: "mail_inbox",
10861
+ description: 'Read your unread mail from other agents on this project and mark it read. Covers mail addressed to you directly, to your base name (e.g. "leader"), and broadcasts ("*"). Urgent steer/btw mail is already injected automatically \u2014 use this to catch up on notes, questions, handoffs and results, or after a long stretch of tool work.',
10862
+ usageHint: "mail_inbox (optionally: limit=10, markRead=false to peek)",
10863
+ category: "coordination",
10864
+ permission: "auto",
10865
+ mutating: false,
10866
+ inputSchema: {
10867
+ type: "object",
10868
+ properties: {
10869
+ limit: { type: "number", description: "Max messages to return (default 20)." },
10870
+ markRead: {
10871
+ type: "boolean",
10872
+ description: "Add a read receipt for each returned message (default true)."
10873
+ }
10874
+ }
10875
+ },
10876
+ async execute(input, ctx) {
10877
+ const i = input ?? {};
10878
+ const limit = i.limit ?? 20;
10879
+ const markRead = i.markRead ?? true;
10880
+ const mb = resolveMailbox(ctx);
10881
+ const identity = await register(mb, ctx);
10882
+ const targets = [identity.callerId];
10883
+ if (identity.baseId !== identity.callerId) targets.push(identity.baseId);
10884
+ const batches = await Promise.all(
10885
+ targets.map(
10886
+ (to) => mb.query({ to, unreadBy: identity.callerId, limit }).catch(() => [])
10887
+ )
10888
+ );
10889
+ const seen = /* @__PURE__ */ new Set();
10890
+ const messages = batches.flat().filter((m) => {
10891
+ if (seen.has(m.id) || m.from === identity.callerId) return false;
10892
+ seen.add(m.id);
10893
+ return true;
10894
+ }).slice(0, limit);
10895
+ if (markRead) {
10896
+ await Promise.all(
10897
+ messages.map(
10898
+ (m) => mb.ack({ messageId: m.id, readerId: identity.callerId, read: true }).catch(() => null)
10899
+ )
10900
+ );
10901
+ }
10902
+ return {
10903
+ ok: true,
10904
+ you: identity.callerId,
10905
+ count: messages.length,
10906
+ messages: messages.map((m) => ({
10907
+ id: m.id,
10908
+ from: m.from,
10909
+ to: m.to,
10910
+ type: m.type,
10911
+ subject: m.subject,
10912
+ body: m.body.length > 2e3 ? `${m.body.slice(0, 2e3)}\u2026 [truncated]` : m.body,
10913
+ timestamp: m.timestamp,
10914
+ replyTo: m.replyTo
10915
+ })),
10916
+ summary: messages.length === 0 ? "Inbox empty." : `${messages.length} unread message(s)${markRead ? " (marked read)" : ""}. Reply with mail_send using the sender id.`
10917
+ };
10918
+ }
10919
+ };
10920
+ }
10921
+
10922
+ // src/coordination/dep-watcher.ts
10923
+ var DEPENDENCY_FILE_PATTERNS = [
10924
+ "package.json",
10925
+ "tsconfig.json",
10926
+ "pnpm-lock.yaml",
10927
+ "yarn.lock",
10928
+ "package-lock.json",
10929
+ "go.mod",
10930
+ "go.sum",
10931
+ "Cargo.toml",
10932
+ "Cargo.lock",
10933
+ "pyproject.toml",
10934
+ "setup.py",
10935
+ "setup.cfg",
10936
+ "requirements.txt",
10937
+ "Pipfile",
10938
+ "Pipfile.lock",
10939
+ "Gemfile",
10940
+ "Gemfile.lock",
10941
+ "composer.json",
10942
+ "composer.lock",
10943
+ "mix.exs",
10944
+ "mix.lock",
10945
+ "pom.xml",
10946
+ "build.gradle",
10947
+ "build.gradle.kts",
10948
+ "settings.gradle",
10949
+ "settings.gradle.kts",
10950
+ "*.csproj",
10951
+ "packages.config",
10952
+ "pubspec.yaml",
10953
+ "pubspec.lock",
10954
+ "CMakeLists.txt",
10955
+ "conanfile.txt",
10956
+ "conanfile.py",
10957
+ "vcpkg.json"
10958
+ ];
10959
+ function makeDependencyWatcherConfig(opts) {
10960
+ const {
10961
+ projectRoot,
10962
+ mailbox,
10963
+ targetAgent = "*",
10964
+ watcherAgentId = "dep-watcher",
10965
+ debounceMs = 3e3,
10966
+ patterns = DEPENDENCY_FILE_PATTERNS
10967
+ } = opts;
10968
+ const watchPaths = [];
10969
+ for (const p of patterns) {
10970
+ if (p.includes("*")) {
10971
+ continue;
10972
+ }
10973
+ watchPaths.push(`${projectRoot}/${p}`);
10974
+ }
10975
+ watchPaths.push(projectRoot);
10976
+ const unique = [...new Set(watchPaths)];
10977
+ const globPatterns = patterns.filter((p) => p.includes("*"));
10978
+ const plainPatterns = patterns.filter((p) => !p.includes("*"));
10979
+ function matchesPattern(filePath) {
10980
+ const basename5 = filePath.split("/").pop()?.split("\\").pop() ?? "";
10981
+ if (plainPatterns.includes(basename5)) return true;
10982
+ for (const gp of globPatterns) {
10983
+ const regex = new RegExp(
10984
+ "^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
10985
+ );
10986
+ if (regex.test(basename5)) return true;
10987
+ }
10988
+ return false;
10989
+ }
10990
+ const pending = /* @__PURE__ */ new Map();
10991
+ return {
10992
+ watchPaths: unique,
10993
+ debounceMs,
10994
+ async onChange(entry) {
10995
+ if (entry.event === "delete") return;
10996
+ if (!matchesPattern(entry.path)) return;
10997
+ const key = entry.path;
10998
+ const existing = pending.get(key);
10999
+ if (existing) clearTimeout(existing);
11000
+ pending.set(
11001
+ key,
11002
+ setTimeout(async () => {
11003
+ pending.delete(key);
11004
+ try {
11005
+ const fileName = entry.path.split("/").pop()?.split("\\").pop() ?? entry.path;
11006
+ await mailbox.send({
11007
+ from: watcherAgentId,
11008
+ to: targetAgent,
11009
+ type: "assign",
11010
+ subject: `Dependency file changed: ${fileName}`,
11011
+ body: [
11012
+ `File: ${entry.path}`,
11013
+ `Event: ${entry.event}`,
11014
+ `Timestamp: ${entry.timestamp}`,
11015
+ "",
11016
+ `Action: Run a tech-stack audit on the changed dependency file.`,
11017
+ `Validate any new packages, check versions, flag deprecated or prehistoric packages.`,
11018
+ `Report findings back via mailbox (type: result).`
11019
+ ].join("\n"),
11020
+ priority: "high",
11021
+ taskContext: {
11022
+ agentRole: "tech-stack",
11023
+ status: "pending"
11024
+ }
11025
+ });
11026
+ } catch {
11027
+ }
11028
+ }, debounceMs)
11029
+ );
11030
+ }
11031
+ };
11032
+ }
11033
+
11034
+ // src/coordination/dep-watcher-bridge.ts
11035
+ function attachDepWatcherBridge(opts) {
11036
+ const {
11037
+ events,
11038
+ mailbox,
11039
+ projectRoot,
11040
+ targetAgent = "tech-stack",
11041
+ watcherAgentId = "dep-watcher",
11042
+ debounceMs = 3e3
11043
+ } = opts;
11044
+ const cfg = makeDependencyWatcherConfig({
11045
+ projectRoot,
11046
+ mailbox,
11047
+ targetAgent,
11048
+ watcherAgentId,
11049
+ debounceMs
11050
+ });
11051
+ const unsub = events.onPattern("file-watcher:changed", (_eventName, rawPayload) => {
11052
+ const payload = rawPayload;
11053
+ if (!payload?.path) return;
11054
+ void cfg.onChange({
11055
+ path: payload.path,
11056
+ event: payload.event ?? "change",
11057
+ timestamp: payload.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
11058
+ }).catch(() => {
11059
+ });
11060
+ });
11061
+ return () => {
11062
+ unsub();
11063
+ };
11064
+ }
11065
+
11066
+ // src/coordination/mailbox-hooks.ts
11067
+ function createMailboxHooks(opts) {
11068
+ const { mailbox, agentId, notifyNewMail = true, heartbeat = true } = opts;
11069
+ let lastUnreadCount = -1;
11070
+ return {
11071
+ /**
11072
+ * Call before each tool execution. Checks mailbox and emits events.
11073
+ * @param events — EventBus-like object with emit method.
11074
+ */
11075
+ async beforeTool(events) {
11076
+ try {
11077
+ const count = await mailbox.unreadCount(agentId);
11078
+ if (notifyNewMail && count !== lastUnreadCount) {
11079
+ lastUnreadCount = count;
11080
+ events.emit("mailbox.unread_count", { agentId, count });
11081
+ }
11082
+ } catch {
11083
+ }
11084
+ },
11085
+ /**
11086
+ * Call after each tool execution. Updates heartbeat and optionally
11087
+ * current tool status.
11088
+ */
11089
+ async afterTool(toolName) {
11090
+ if (!heartbeat) return;
11091
+ try {
11092
+ await mailbox.heartbeat({
11093
+ agentId,
11094
+ status: "running",
11095
+ currentTool: toolName
11096
+ });
11097
+ } catch {
11098
+ }
11099
+ },
11100
+ /** Reset the cached unread count (e.g., after the agent checks manually). */
11101
+ reset() {
11102
+ lastUnreadCount = -1;
11103
+ }
11104
+ };
11105
+ }
11106
+ var DEFAULT_MAX_ENTRIES = 1e4;
11107
+ var LOG_FILENAME = "package-authors.json";
11108
+ function logPath(storageDir) {
11109
+ return path4.join(storageDir, LOG_FILENAME);
11110
+ }
11111
+ async function loadLog(storageDir, projectRoot) {
11112
+ try {
11113
+ const raw = await fsp6.readFile(logPath(storageDir), "utf-8");
11114
+ const parsed = JSON.parse(raw);
11115
+ if (!parsed.entries || !Array.isArray(parsed.entries)) {
11116
+ return { projectRoot, entries: [] };
11117
+ }
11118
+ return parsed;
11119
+ } catch (err) {
11120
+ if (err.code === "ENOENT") {
11121
+ return { projectRoot, entries: [] };
11122
+ }
11123
+ throw err;
11124
+ }
11125
+ }
11126
+ async function saveLog(storageDir, log) {
11127
+ await fsp6.mkdir(storageDir, { recursive: true });
11128
+ const tmp = `${logPath(storageDir)}.tmp.${Date.now()}`;
11129
+ await fsp6.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
11130
+ await fsp6.rename(tmp, logPath(storageDir));
11131
+ }
11132
+ function detectEcosystem(manifestPath) {
11133
+ const name = path4.basename(manifestPath).toLowerCase();
11134
+ if (name === "package.json") return "npm";
11135
+ if (name === "go.mod") return "go";
11136
+ if (name === "cargo.toml") return "cargo";
11137
+ if (name === "pyproject.toml" || name === "requirements.txt" || name === "pipfile" || name === "pipfile.lock") return "pip";
11138
+ if (name === "gemfile" || name === "gemfile.lock") return "gem";
11139
+ if (name === "composer.json" || name === "composer.lock") return "composer";
11140
+ if (name.endsWith(".csproj") || name === "packages.config") return "nuget";
11141
+ if (name === "mix.exs" || name === "mix.lock") return "elixir";
11142
+ if (name === "pom.xml" || name.startsWith("build.gradle")) return "maven";
11143
+ if (name === "pubspec.yaml" || name === "pubspec.lock") return "dart";
11144
+ if (name === "vcpkg.json") return "vcpkg";
11145
+ if (name === "conanfile.txt" || name === "conanfile.py") return "conan";
11146
+ if (name === "cmakeLists.txt") return "cmake";
11147
+ return "unknown";
11148
+ }
11149
+ async function recordPackageAction(opts, entry) {
11150
+ const { storageDir, projectRoot, maxEntries = DEFAULT_MAX_ENTRIES } = opts;
11151
+ const log = await loadLog(storageDir, projectRoot);
11152
+ log.entries.push({
11153
+ ...entry,
11154
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11155
+ });
11156
+ if (log.entries.length > maxEntries) {
11157
+ const keep = Math.floor(maxEntries * 0.8);
11158
+ log.entries = log.entries.slice(-keep);
11159
+ log.lastCompactedAt = (/* @__PURE__ */ new Date()).toISOString();
11160
+ }
11161
+ await saveLog(storageDir, log);
11162
+ }
11163
+ async function getPackageAuthor(opts, manifestPath, packageName) {
11164
+ const log = await loadLog(opts.storageDir, opts.projectRoot);
11165
+ const normalizedManifest = manifestPath.replace(/\\/g, "/");
11166
+ for (let i = log.entries.length - 1; i >= 0; i--) {
11167
+ const e = log.entries[i];
11168
+ if (e && e.manifestPath.replace(/\\/g, "/") === normalizedManifest && e.packageName === packageName) {
11169
+ return e;
11170
+ }
11171
+ }
11172
+ return void 0;
11173
+ }
11174
+ async function getManifestPackages(opts, manifestPath) {
11175
+ const log = await loadLog(opts.storageDir, opts.projectRoot);
11176
+ const normalizedManifest = manifestPath.replace(/\\/g, "/");
11177
+ return log.entries.filter(
11178
+ (e) => e.manifestPath.replace(/\\/g, "/") === normalizedManifest
11179
+ );
11180
+ }
11181
+ async function getPackagesByAgent(opts, agentId) {
11182
+ const log = await loadLog(opts.storageDir, opts.projectRoot);
11183
+ const map = /* @__PURE__ */ new Map();
11184
+ for (const e of log.entries) {
11185
+ if (e.agentId === agentId) {
11186
+ const key = `${e.manifestPath}|${e.packageName}`;
11187
+ map.set(key, e);
11188
+ }
11189
+ }
11190
+ return map;
11191
+ }
11192
+ async function updatePackageOutdatedStatus(opts, manifestPath, packageName, outdated, latestVersion) {
11193
+ const { storageDir, projectRoot } = opts;
11194
+ const log = await loadLog(storageDir, projectRoot);
11195
+ log.entries.push({
11196
+ manifestPath,
11197
+ packageName,
11198
+ versionSpec: "",
11199
+ ecosystem: detectEcosystem(manifestPath),
11200
+ agentId: "outdated-checker",
11201
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11202
+ outdated,
11203
+ latestVersion
11204
+ });
11205
+ await saveLog(storageDir, log);
11206
+ }
11207
+ async function getFullPackageLog(opts) {
11208
+ return loadLog(opts.storageDir, opts.projectRoot);
11209
+ }
11210
+
11211
+ // src/coordination/package-outdated-watcher.ts
11212
+ function parseOutdatedPackages(body) {
11213
+ const results = [];
11214
+ const tableRows = body.matchAll(
11215
+ /^\|\s*([^-][^|]*?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/gm
11216
+ );
11217
+ for (const rowMatch of tableRows) {
11218
+ const cols = rowMatch[0].split("|").map((c) => c.trim()).filter(Boolean);
11219
+ if (cols.length >= 5 && cols[0] && cols[0] !== "Package") {
11220
+ results.push({
11221
+ name: cols[0] ?? "",
11222
+ currentVersion: cols[1] ?? "",
11223
+ latestVersion: cols[2] ?? "",
11224
+ wantedVersion: cols[3] ?? "",
11225
+ manifestPath: cols[4] ?? "",
11226
+ ecosystem: detectEcosystem2(cols[4] ?? "")
11227
+ });
11228
+ }
11229
+ }
11230
+ if (results.length === 0) {
11231
+ const kvMatches = body.matchAll(
11232
+ /(?:package|name)[\s:=]+([\w@/-]+).*?(?:current|version)[\s:=]+([\d.]+).*?latest[\s:=]+([\d.]+)/gi
11233
+ );
11234
+ for (const m of kvMatches) {
11235
+ results.push({
11236
+ name: m[1] ?? "",
11237
+ currentVersion: m[2] ?? "",
11238
+ latestVersion: m[3] ?? "",
11239
+ wantedVersion: m[2] ?? "",
11240
+ manifestPath: "",
11241
+ ecosystem: "unknown"
11242
+ });
11243
+ }
11244
+ }
11245
+ return results;
11246
+ }
11247
+ function detectEcosystem2(manifestPath) {
11248
+ const name = manifestPath.split("/").pop()?.split("\\").pop() ?? manifestPath;
11249
+ if (name === "package.json") return "npm";
11250
+ if (name === "go.mod") return "go";
11251
+ if (name === "cargo.toml") return "cargo";
11252
+ if (name === "pyproject.toml" || name === "requirements.txt") return "pip";
11253
+ if (name === "gemfile" || name === "gemfile.lock") return "gem";
11254
+ if (name === "composer.json" || name === "composer.lock") return "composer";
11255
+ if (name.endsWith(".csproj") || name === "packages.config") return "nuget";
11256
+ if (name === "mix.exs" || name === "mix.lock") return "elixir";
11257
+ if (name === "pom.xml" || name.startsWith("build.gradle")) return "maven";
11258
+ if (name === "pubspec.yaml" || name === "pubspec.lock") return "dart";
11259
+ return "unknown";
11260
+ }
11261
+ function startPackageOutdatedWatcher(opts) {
11262
+ const {
11263
+ mailbox,
11264
+ packageTrackerOpts,
11265
+ pollIntervalMs = 60 * 60 * 1e3,
11266
+ watcherAgentId = "pkg-outdated-watcher",
11267
+ onNotify,
11268
+ onLog,
11269
+ onError
11270
+ } = opts;
11271
+ const log = (msg) => onLog?.(msg);
11272
+ const handleError = (err) => onError?.(err);
11273
+ const state = {
11274
+ running: true,
11275
+ timer: null,
11276
+ processedIds: /* @__PURE__ */ new Set()
11277
+ };
11278
+ async function pollOnce() {
11279
+ if (!state.running) return;
11280
+ try {
11281
+ const messages = await mailbox.query({
11282
+ to: watcherAgentId,
11283
+ type: "result",
11284
+ unreadBy: watcherAgentId,
11285
+ limit: 10
11286
+ });
11287
+ for (const msg of messages) {
11288
+ if (state.processedIds.has(msg.id)) continue;
11289
+ state.processedIds.add(msg.id);
11290
+ await mailbox.ack({
11291
+ messageId: msg.id,
11292
+ readerId: watcherAgentId,
11293
+ read: true
11294
+ });
11295
+ await processResultMessage(msg);
11296
+ }
11297
+ } catch (err) {
11298
+ handleError(err);
11299
+ }
11300
+ }
11301
+ async function processResultMessage(msg) {
11302
+ const entries = parseOutdatedPackages(msg.body ?? "");
11303
+ if (entries.length === 0) {
11304
+ log(`[pkg-outdated-watcher] No outdated packages found in message ${msg.id}`);
11305
+ return;
11306
+ }
11307
+ log(`[pkg-outdated-watcher] Processing ${entries.length} outdated package(s) from ${msg.from}`);
11308
+ for (const entry of entries) {
11309
+ try {
11310
+ const author = await getPackageAuthor(
11311
+ packageTrackerOpts,
11312
+ entry.manifestPath,
11313
+ entry.name
11314
+ );
11315
+ const notifyTarget = author?.agentId ?? "*";
11316
+ const notifyBody = buildNotifyBody(entry, author?.agentName);
11317
+ const notifyMsg = {
11318
+ from: watcherAgentId,
11319
+ to: notifyTarget,
11320
+ subject: `Outdated package: ${entry.name}@${entry.currentVersion} \u2192 ${entry.latestVersion}`,
11321
+ body: notifyBody,
11322
+ priority: "high"
11323
+ };
11324
+ await onNotify(notifyMsg);
11325
+ log(
11326
+ `[pkg-outdated-watcher] Notified ${notifyTarget} about outdated ${entry.name} (${entry.currentVersion} \u2192 ${entry.latestVersion}) in ${entry.manifestPath}`
11327
+ );
11328
+ } catch (err) {
11329
+ handleError(err);
11330
+ log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${err instanceof Error ? err.message : String(err)}`);
11331
+ }
11332
+ }
11333
+ }
11334
+ state.timer = setInterval(() => {
11335
+ void pollOnce();
11336
+ }, pollIntervalMs);
11337
+ void pollOnce();
11338
+ return () => {
11339
+ state.running = false;
11340
+ if (state.timer) {
11341
+ clearInterval(state.timer);
11342
+ state.timer = null;
11343
+ }
11344
+ };
11345
+ }
11346
+ function buildNotifyBody(entry, authorName) {
11347
+ const lines = [
11348
+ `The package **${entry.name}** is outdated.`,
11349
+ "",
11350
+ `| Field | Value |`,
11351
+ `|-------|-------|`,
11352
+ `| Package | ${entry.name} |`,
11353
+ `| Installed | ${entry.currentVersion} |`,
11354
+ `| Latest | ${entry.latestVersion} |`,
11355
+ `| Wanted | ${entry.wantedVersion} |`,
11356
+ `| Manifest | ${entry.manifestPath} |`,
11357
+ `| Ecosystem | ${entry.ecosystem} |`,
11358
+ ""
11359
+ ];
11360
+ if (authorName) {
11361
+ lines.push(
11362
+ `You added this package${authorName !== "unknown" ? ` (as ${authorName})` : ""}. Consider updating it with the install tool.`
11363
+ );
11364
+ } else {
11365
+ lines.push(
11366
+ `This package appears to have been added by an agent no longer on record. Consider reviewing and updating it.`
11367
+ );
11368
+ }
11369
+ lines.push(
11370
+ "",
11371
+ `Update with:`,
11372
+ `\`\`\``,
11373
+ `${getUpdateCommand(entry)}`,
11374
+ `\`\`\``
11375
+ );
11376
+ return lines.join("\n");
11377
+ }
11378
+ function getUpdateCommand(entry) {
11379
+ switch (entry.ecosystem) {
11380
+ case "npm":
11381
+ return `pnpm add ${entry.name}@latest # or: pnpm update ${entry.name}`;
11382
+ case "cargo":
11383
+ return `cargo update ${entry.name}`;
11384
+ case "go":
11385
+ return `go get ${entry.name}@latest`;
11386
+ case "pip":
11387
+ return `pip install --upgrade ${entry.name}`;
11388
+ case "gem":
11389
+ return `gem install ${entry.name}`;
11390
+ case "composer":
11391
+ return `composer require ${entry.name}:^${entry.latestVersion} --update-with-dependencies`;
11392
+ case "nuget":
11393
+ return `dotnet add package ${entry.name}`;
11394
+ case "maven":
11395
+ return `# Update the <version> in pom.xml or run:
11396
+ mvn versions:use-latest-versions`;
11397
+ case "dart":
11398
+ return `dart pub upgrade ${entry.name}`;
11399
+ default:
11400
+ return `# Update ${entry.name} to ${entry.latestVersion} using your package manager`;
11401
+ }
11402
+ }
11403
+
11404
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CollabSession, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus };
9430
11405
  //# sourceMappingURL=index.js.map
9431
11406
  //# sourceMappingURL=index.js.map