@askexenow/exe-os 0.8.83 → 0.8.86

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 (103) hide show
  1. package/dist/bin/backfill-conversations.js +746 -595
  2. package/dist/bin/backfill-responses.js +745 -594
  3. package/dist/bin/backfill-vectors.js +312 -226
  4. package/dist/bin/cleanup-stale-review-tasks.js +154 -21
  5. package/dist/bin/cli.js +14678 -12676
  6. package/dist/bin/exe-agent-config.js +242 -0
  7. package/dist/bin/exe-agent.js +100 -91
  8. package/dist/bin/exe-assign.js +1003 -854
  9. package/dist/bin/exe-boot.js +1420 -485
  10. package/dist/bin/exe-call.js +10 -0
  11. package/dist/bin/exe-cloud.js +29 -6
  12. package/dist/bin/exe-dispatch.js +572 -271
  13. package/dist/bin/exe-doctor.js +403 -6
  14. package/dist/bin/exe-export-behaviors.js +175 -72
  15. package/dist/bin/exe-forget.js +102 -3
  16. package/dist/bin/exe-gateway.js +796 -292
  17. package/dist/bin/exe-healthcheck.js +134 -1
  18. package/dist/bin/exe-heartbeat.js +172 -36
  19. package/dist/bin/exe-kill.js +175 -72
  20. package/dist/bin/exe-launch-agent.js +189 -76
  21. package/dist/bin/exe-link.js +927 -82
  22. package/dist/bin/exe-new-employee.js +60 -8
  23. package/dist/bin/exe-pending-messages.js +151 -19
  24. package/dist/bin/exe-pending-notifications.js +97 -2
  25. package/dist/bin/exe-pending-reviews.js +155 -22
  26. package/dist/bin/exe-rename.js +564 -23
  27. package/dist/bin/exe-review.js +231 -73
  28. package/dist/bin/exe-search.js +995 -228
  29. package/dist/bin/exe-session-cleanup.js +4930 -1664
  30. package/dist/bin/exe-settings.js +20 -5
  31. package/dist/bin/exe-start-codex.js +2598 -0
  32. package/dist/bin/exe-start.sh +15 -3
  33. package/dist/bin/exe-status.js +154 -21
  34. package/dist/bin/exe-team.js +97 -2
  35. package/dist/bin/git-sweep.js +1180 -363
  36. package/dist/bin/graph-backfill.js +175 -72
  37. package/dist/bin/graph-export.js +175 -72
  38. package/dist/bin/install.js +60 -7
  39. package/dist/bin/list-providers.js +1 -0
  40. package/dist/bin/scan-tasks.js +1185 -367
  41. package/dist/bin/setup.js +914 -270
  42. package/dist/bin/shard-migrate.js +175 -72
  43. package/dist/bin/update.js +1 -0
  44. package/dist/bin/wiki-sync.js +175 -72
  45. package/dist/gateway/index.js +792 -285
  46. package/dist/hooks/bug-report-worker.js +445 -135
  47. package/dist/hooks/commit-complete.js +1178 -361
  48. package/dist/hooks/error-recall.js +994 -228
  49. package/dist/hooks/ingest-worker.js +1799 -1234
  50. package/dist/hooks/ingest.js +3 -0
  51. package/dist/hooks/instructions-loaded.js +707 -97
  52. package/dist/hooks/notification.js +699 -89
  53. package/dist/hooks/post-compact.js +757 -109
  54. package/dist/hooks/pre-compact.js +1061 -244
  55. package/dist/hooks/pre-tool-use.js +787 -130
  56. package/dist/hooks/prompt-ingest-worker.js +242 -101
  57. package/dist/hooks/prompt-submit.js +1121 -299
  58. package/dist/hooks/response-ingest-worker.js +242 -101
  59. package/dist/hooks/session-end.js +4063 -397
  60. package/dist/hooks/session-start.js +1071 -254
  61. package/dist/hooks/stop.js +768 -120
  62. package/dist/hooks/subagent-stop.js +757 -109
  63. package/dist/hooks/summary-worker.js +1706 -1011
  64. package/dist/index.js +1821 -1098
  65. package/dist/lib/agent-config.js +167 -0
  66. package/dist/lib/cloud-sync.js +932 -88
  67. package/dist/lib/consolidation.js +2 -1
  68. package/dist/lib/database.js +642 -87
  69. package/dist/lib/db-daemon-client.js +503 -0
  70. package/dist/lib/device-registry.js +547 -7
  71. package/dist/lib/embedder.js +14 -28
  72. package/dist/lib/employee-templates.js +84 -74
  73. package/dist/lib/employees.js +9 -0
  74. package/dist/lib/exe-daemon-client.js +16 -29
  75. package/dist/lib/exe-daemon.js +2733 -1575
  76. package/dist/lib/hybrid-search.js +995 -228
  77. package/dist/lib/identity.js +87 -67
  78. package/dist/lib/keychain.js +9 -1
  79. package/dist/lib/messaging.js +103 -40
  80. package/dist/lib/reminders.js +91 -74
  81. package/dist/lib/runtime-table.js +16 -0
  82. package/dist/lib/schedules.js +96 -2
  83. package/dist/lib/session-wrappers.js +22 -0
  84. package/dist/lib/skill-learning.js +103 -85
  85. package/dist/lib/store.js +234 -73
  86. package/dist/lib/tasks.js +348 -134
  87. package/dist/lib/tmux-routing.js +422 -208
  88. package/dist/lib/token-spend.js +273 -0
  89. package/dist/lib/ws-client.js +11 -0
  90. package/dist/mcp/server.js +5742 -696
  91. package/dist/mcp/tools/complete-reminder.js +94 -77
  92. package/dist/mcp/tools/create-reminder.js +94 -77
  93. package/dist/mcp/tools/create-task.js +375 -152
  94. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  95. package/dist/mcp/tools/list-reminders.js +94 -77
  96. package/dist/mcp/tools/list-tasks.js +99 -31
  97. package/dist/mcp/tools/send-message.js +108 -45
  98. package/dist/mcp/tools/update-task.js +162 -77
  99. package/dist/runtime/index.js +1075 -258
  100. package/dist/tui/App.js +1333 -506
  101. package/package.json +6 -1
  102. package/src/commands/exe/agent-config.md +27 -0
  103. package/src/commands/exe/cc-doctor.md +10 -0
@@ -273,7 +273,13 @@ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
273
273
  function getEmployee(employees, name) {
274
274
  return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
275
275
  }
276
- var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE;
276
+ function isMultiInstance(agentName, employees) {
277
+ const roster = employees ?? loadEmployeesSync();
278
+ const emp = getEmployee(roster, agentName);
279
+ if (!emp) return false;
280
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
281
+ }
282
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
277
283
  var init_employees = __esm({
278
284
  "src/lib/employees.ts"() {
279
285
  "use strict";
@@ -281,12 +287,36 @@ var init_employees = __esm({
281
287
  EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
282
288
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
283
289
  COORDINATOR_ROLE = "COO";
290
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
284
291
  }
285
292
  });
286
293
 
287
294
  // src/lib/session-registry.ts
295
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
288
296
  import path4 from "path";
289
297
  import os3 from "os";
298
+ function registerSession(entry) {
299
+ const dir = path4.dirname(REGISTRY_PATH);
300
+ if (!existsSync3(dir)) {
301
+ mkdirSync2(dir, { recursive: true });
302
+ }
303
+ const sessions = listSessions();
304
+ const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
305
+ if (idx >= 0) {
306
+ sessions[idx] = entry;
307
+ } else {
308
+ sessions.push(entry);
309
+ }
310
+ writeFileSync3(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
311
+ }
312
+ function listSessions() {
313
+ try {
314
+ const raw = readFileSync4(REGISTRY_PATH, "utf8");
315
+ return JSON.parse(raw);
316
+ } catch {
317
+ return [];
318
+ }
319
+ }
290
320
  var REGISTRY_PATH;
291
321
  var init_session_registry = __esm({
292
322
  "src/lib/session-registry.ts"() {
@@ -404,13 +434,40 @@ var init_transport = __esm({
404
434
 
405
435
  // src/lib/cc-agent-support.ts
406
436
  import { execSync as execSync4 } from "child_process";
437
+ function _resetCcAgentSupportCache() {
438
+ _cachedSupport = null;
439
+ }
440
+ function claudeSupportsAgentFlag() {
441
+ if (_cachedSupport !== null) return _cachedSupport;
442
+ try {
443
+ const helpOutput = execSync4("claude --help 2>&1", {
444
+ encoding: "utf-8",
445
+ timeout: 5e3
446
+ });
447
+ _cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
448
+ } catch {
449
+ _cachedSupport = false;
450
+ }
451
+ return _cachedSupport;
452
+ }
453
+ var _cachedSupport;
407
454
  var init_cc_agent_support = __esm({
408
455
  "src/lib/cc-agent-support.ts"() {
409
456
  "use strict";
457
+ _cachedSupport = null;
410
458
  }
411
459
  });
412
460
 
413
461
  // src/lib/mcp-prefix.ts
462
+ function expandDualPrefixTools(shortNames) {
463
+ const out = [];
464
+ for (const name of shortNames) {
465
+ for (const prefix of MCP_TOOL_PREFIXES) {
466
+ out.push(prefix + name);
467
+ }
468
+ }
469
+ return out;
470
+ }
414
471
  var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
415
472
  var init_mcp_prefix = __esm({
416
473
  "src/lib/mcp-prefix.ts"() {
@@ -425,23 +482,126 @@ var init_mcp_prefix = __esm({
425
482
  });
426
483
 
427
484
  // src/lib/provider-table.ts
485
+ function detectActiveProvider(env = process.env) {
486
+ const baseUrl = env.ANTHROPIC_BASE_URL;
487
+ if (!baseUrl) return DEFAULT_PROVIDER;
488
+ for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
489
+ if (cfg.baseUrl === baseUrl) return name;
490
+ }
491
+ return DEFAULT_PROVIDER;
492
+ }
493
+ var PROVIDER_TABLE, DEFAULT_PROVIDER;
428
494
  var init_provider_table = __esm({
429
495
  "src/lib/provider-table.ts"() {
430
496
  "use strict";
497
+ PROVIDER_TABLE = {
498
+ opencode: {
499
+ baseUrl: "https://opencode.ai/zen/go",
500
+ apiKeyEnv: "OPENCODE_API_KEY",
501
+ defaultModel: "minimax-m2.7"
502
+ }
503
+ };
504
+ DEFAULT_PROVIDER = "default";
431
505
  }
432
506
  });
433
507
 
434
- // src/lib/intercom-queue.ts
435
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
508
+ // src/lib/runtime-table.ts
509
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
510
+ var init_runtime_table = __esm({
511
+ "src/lib/runtime-table.ts"() {
512
+ "use strict";
513
+ RUNTIME_TABLE = {
514
+ codex: {
515
+ binary: "codex",
516
+ launchMode: "exec",
517
+ autoApproveFlag: "--full-auto",
518
+ inlineFlag: "--no-alt-screen",
519
+ apiKeyEnv: "OPENAI_API_KEY",
520
+ defaultModel: "gpt-5.4"
521
+ }
522
+ };
523
+ DEFAULT_RUNTIME = "claude";
524
+ }
525
+ });
526
+
527
+ // src/lib/agent-config.ts
528
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
436
529
  import path5 from "path";
530
+ function loadAgentConfig() {
531
+ if (!existsSync4(AGENT_CONFIG_PATH)) return {};
532
+ try {
533
+ return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
534
+ } catch {
535
+ return {};
536
+ }
537
+ }
538
+ function getAgentRuntime(agentId) {
539
+ const config = loadAgentConfig();
540
+ const entry = config[agentId];
541
+ if (entry) return entry;
542
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
543
+ }
544
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
545
+ var init_agent_config = __esm({
546
+ "src/lib/agent-config.ts"() {
547
+ "use strict";
548
+ init_config();
549
+ init_runtime_table();
550
+ AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
551
+ DEFAULT_MODELS = {
552
+ claude: "claude-opus-4",
553
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
554
+ opencode: "minimax-m2.7"
555
+ };
556
+ }
557
+ });
558
+
559
+ // src/lib/intercom-queue.ts
560
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
561
+ import path6 from "path";
437
562
  import os4 from "os";
563
+ function ensureDir() {
564
+ const dir = path6.dirname(QUEUE_PATH);
565
+ if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
566
+ }
567
+ function readQueue() {
568
+ try {
569
+ if (!existsSync5(QUEUE_PATH)) return [];
570
+ return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
571
+ } catch {
572
+ return [];
573
+ }
574
+ }
575
+ function writeQueue(queue) {
576
+ ensureDir();
577
+ const tmp = `${QUEUE_PATH}.tmp`;
578
+ writeFileSync5(tmp, JSON.stringify(queue, null, 2));
579
+ renameSync3(tmp, QUEUE_PATH);
580
+ }
581
+ function queueIntercom(targetSession, reason) {
582
+ const queue = readQueue();
583
+ const existing = queue.find((q) => q.targetSession === targetSession);
584
+ if (existing) {
585
+ existing.attempts++;
586
+ existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
587
+ existing.reason = reason;
588
+ } else {
589
+ queue.push({
590
+ targetSession,
591
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
592
+ attempts: 0,
593
+ reason
594
+ });
595
+ }
596
+ writeQueue(queue);
597
+ }
438
598
  var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
439
599
  var init_intercom_queue = __esm({
440
600
  "src/lib/intercom-queue.ts"() {
441
601
  "use strict";
442
- QUEUE_PATH = path5.join(os4.homedir(), ".exe-os", "intercom-queue.json");
602
+ QUEUE_PATH = path6.join(os4.homedir(), ".exe-os", "intercom-queue.json");
443
603
  TTL_MS = 60 * 60 * 1e3;
444
- INTERCOM_LOG = path5.join(os4.homedir(), ".exe-os", "intercom.log");
604
+ INTERCOM_LOG = path6.join(os4.homedir(), ".exe-os", "intercom.log");
445
605
  }
446
606
  });
447
607
 
@@ -500,6 +660,443 @@ var init_db_retry = __esm({
500
660
  }
501
661
  });
502
662
 
663
+ // src/lib/exe-daemon-client.ts
664
+ import net from "net";
665
+ import { spawn } from "child_process";
666
+ import { randomUUID } from "crypto";
667
+ import { existsSync as existsSync6, unlinkSync as unlinkSync3, readFileSync as readFileSync7, openSync, closeSync, statSync } from "fs";
668
+ import path7 from "path";
669
+ import { fileURLToPath } from "url";
670
+ function handleData(chunk) {
671
+ _buffer += chunk.toString();
672
+ if (_buffer.length > MAX_BUFFER) {
673
+ _buffer = "";
674
+ return;
675
+ }
676
+ let newlineIdx;
677
+ while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
678
+ const line = _buffer.slice(0, newlineIdx).trim();
679
+ _buffer = _buffer.slice(newlineIdx + 1);
680
+ if (!line) continue;
681
+ try {
682
+ const response = JSON.parse(line);
683
+ const id = response.id;
684
+ if (!id) continue;
685
+ const entry = _pending.get(id);
686
+ if (entry) {
687
+ clearTimeout(entry.timer);
688
+ _pending.delete(id);
689
+ entry.resolve(response);
690
+ }
691
+ } catch {
692
+ }
693
+ }
694
+ }
695
+ function cleanupStaleFiles() {
696
+ if (existsSync6(PID_PATH)) {
697
+ try {
698
+ const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
699
+ if (pid > 0) {
700
+ try {
701
+ process.kill(pid, 0);
702
+ return;
703
+ } catch {
704
+ }
705
+ }
706
+ } catch {
707
+ }
708
+ try {
709
+ unlinkSync3(PID_PATH);
710
+ } catch {
711
+ }
712
+ try {
713
+ unlinkSync3(SOCKET_PATH);
714
+ } catch {
715
+ }
716
+ }
717
+ }
718
+ function findPackageRoot() {
719
+ let dir = path7.dirname(fileURLToPath(import.meta.url));
720
+ const { root } = path7.parse(dir);
721
+ while (dir !== root) {
722
+ if (existsSync6(path7.join(dir, "package.json"))) return dir;
723
+ dir = path7.dirname(dir);
724
+ }
725
+ return null;
726
+ }
727
+ function spawnDaemon() {
728
+ const pkgRoot = findPackageRoot();
729
+ if (!pkgRoot) {
730
+ process.stderr.write("[exed-client] WARN: cannot find package root\n");
731
+ return;
732
+ }
733
+ const daemonPath = path7.join(pkgRoot, "dist", "lib", "exe-daemon.js");
734
+ if (!existsSync6(daemonPath)) {
735
+ process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
736
+ `);
737
+ return;
738
+ }
739
+ const resolvedPath = daemonPath;
740
+ process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
741
+ `);
742
+ const logPath = path7.join(path7.dirname(SOCKET_PATH), "exed.log");
743
+ let stderrFd = "ignore";
744
+ try {
745
+ stderrFd = openSync(logPath, "a");
746
+ } catch {
747
+ }
748
+ const child = spawn(process.execPath, [resolvedPath], {
749
+ detached: true,
750
+ stdio: ["ignore", "ignore", stderrFd],
751
+ env: {
752
+ ...process.env,
753
+ TMUX: void 0,
754
+ // Daemon is global — must not inherit session scope
755
+ TMUX_PANE: void 0,
756
+ // Prevents resolveExeSession() from scoping to one session
757
+ EXE_DAEMON_SOCK: SOCKET_PATH,
758
+ EXE_DAEMON_PID: PID_PATH
759
+ }
760
+ });
761
+ child.unref();
762
+ if (typeof stderrFd === "number") {
763
+ try {
764
+ closeSync(stderrFd);
765
+ } catch {
766
+ }
767
+ }
768
+ }
769
+ function acquireSpawnLock() {
770
+ try {
771
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
772
+ closeSync(fd);
773
+ return true;
774
+ } catch {
775
+ try {
776
+ const stat = statSync(SPAWN_LOCK_PATH);
777
+ if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
778
+ try {
779
+ unlinkSync3(SPAWN_LOCK_PATH);
780
+ } catch {
781
+ }
782
+ try {
783
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
784
+ closeSync(fd);
785
+ return true;
786
+ } catch {
787
+ }
788
+ }
789
+ } catch {
790
+ }
791
+ return false;
792
+ }
793
+ }
794
+ function releaseSpawnLock() {
795
+ try {
796
+ unlinkSync3(SPAWN_LOCK_PATH);
797
+ } catch {
798
+ }
799
+ }
800
+ function connectToSocket() {
801
+ return new Promise((resolve) => {
802
+ if (_socket && _connected) {
803
+ resolve(true);
804
+ return;
805
+ }
806
+ const socket = net.createConnection({ path: SOCKET_PATH });
807
+ const connectTimeout = setTimeout(() => {
808
+ socket.destroy();
809
+ resolve(false);
810
+ }, 2e3);
811
+ socket.on("connect", () => {
812
+ clearTimeout(connectTimeout);
813
+ _socket = socket;
814
+ _connected = true;
815
+ _buffer = "";
816
+ socket.on("data", handleData);
817
+ socket.on("close", () => {
818
+ _connected = false;
819
+ _socket = null;
820
+ for (const [id, entry] of _pending) {
821
+ clearTimeout(entry.timer);
822
+ _pending.delete(id);
823
+ entry.resolve({ error: "Connection closed" });
824
+ }
825
+ });
826
+ socket.on("error", () => {
827
+ _connected = false;
828
+ _socket = null;
829
+ });
830
+ resolve(true);
831
+ });
832
+ socket.on("error", () => {
833
+ clearTimeout(connectTimeout);
834
+ resolve(false);
835
+ });
836
+ });
837
+ }
838
+ async function connectEmbedDaemon() {
839
+ if (_socket && _connected) return true;
840
+ if (await connectToSocket()) return true;
841
+ if (acquireSpawnLock()) {
842
+ try {
843
+ cleanupStaleFiles();
844
+ spawnDaemon();
845
+ } finally {
846
+ releaseSpawnLock();
847
+ }
848
+ }
849
+ const start = Date.now();
850
+ let delay2 = 100;
851
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
852
+ await new Promise((r) => setTimeout(r, delay2));
853
+ if (await connectToSocket()) return true;
854
+ delay2 = Math.min(delay2 * 2, 3e3);
855
+ }
856
+ return false;
857
+ }
858
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
859
+ return new Promise((resolve) => {
860
+ if (!_socket || !_connected) {
861
+ resolve({ error: "Not connected" });
862
+ return;
863
+ }
864
+ const id = randomUUID();
865
+ const timer = setTimeout(() => {
866
+ _pending.delete(id);
867
+ resolve({ error: "Request timeout" });
868
+ }, timeoutMs);
869
+ _pending.set(id, { resolve, timer });
870
+ try {
871
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
872
+ } catch {
873
+ clearTimeout(timer);
874
+ _pending.delete(id);
875
+ resolve({ error: "Write failed" });
876
+ }
877
+ });
878
+ }
879
+ function isClientConnected() {
880
+ return _connected;
881
+ }
882
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
883
+ var init_exe_daemon_client = __esm({
884
+ "src/lib/exe-daemon-client.ts"() {
885
+ "use strict";
886
+ init_config();
887
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path7.join(EXE_AI_DIR, "exed.sock");
888
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path7.join(EXE_AI_DIR, "exed.pid");
889
+ SPAWN_LOCK_PATH = path7.join(EXE_AI_DIR, "exed-spawn.lock");
890
+ SPAWN_LOCK_STALE_MS = 3e4;
891
+ CONNECT_TIMEOUT_MS = 15e3;
892
+ REQUEST_TIMEOUT_MS = 3e4;
893
+ _socket = null;
894
+ _connected = false;
895
+ _buffer = "";
896
+ _pending = /* @__PURE__ */ new Map();
897
+ MAX_BUFFER = 1e7;
898
+ }
899
+ });
900
+
901
+ // src/lib/daemon-protocol.ts
902
+ function serializeValue(v) {
903
+ if (v === null || v === void 0) return null;
904
+ if (typeof v === "bigint") return Number(v);
905
+ if (typeof v === "boolean") return v ? 1 : 0;
906
+ if (v instanceof Uint8Array) {
907
+ return { __blob: Buffer.from(v).toString("base64") };
908
+ }
909
+ if (ArrayBuffer.isView(v)) {
910
+ return { __blob: Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64") };
911
+ }
912
+ if (v instanceof ArrayBuffer) {
913
+ return { __blob: Buffer.from(v).toString("base64") };
914
+ }
915
+ if (typeof v === "string" || typeof v === "number") return v;
916
+ return String(v);
917
+ }
918
+ function deserializeValue(v) {
919
+ if (v === null) return null;
920
+ if (typeof v === "object" && v !== null && "__blob" in v) {
921
+ const buf = Buffer.from(v.__blob, "base64");
922
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
923
+ }
924
+ return v;
925
+ }
926
+ function deserializeResultSet(srs) {
927
+ const rows = srs.rows.map((obj) => {
928
+ const values = srs.columns.map(
929
+ (col) => deserializeValue(obj[col] ?? null)
930
+ );
931
+ const row = values;
932
+ for (let i = 0; i < srs.columns.length; i++) {
933
+ const col = srs.columns[i];
934
+ if (col !== void 0) {
935
+ row[col] = values[i] ?? null;
936
+ }
937
+ }
938
+ Object.defineProperty(row, "length", {
939
+ value: values.length,
940
+ enumerable: false
941
+ });
942
+ return row;
943
+ });
944
+ return {
945
+ columns: srs.columns,
946
+ columnTypes: srs.columnTypes ?? [],
947
+ rows,
948
+ rowsAffected: srs.rowsAffected,
949
+ lastInsertRowid: srs.lastInsertRowid != null ? BigInt(srs.lastInsertRowid) : void 0,
950
+ toJSON: () => ({
951
+ columns: srs.columns,
952
+ columnTypes: srs.columnTypes ?? [],
953
+ rows: srs.rows,
954
+ rowsAffected: srs.rowsAffected,
955
+ lastInsertRowid: srs.lastInsertRowid
956
+ })
957
+ };
958
+ }
959
+ var init_daemon_protocol = __esm({
960
+ "src/lib/daemon-protocol.ts"() {
961
+ "use strict";
962
+ }
963
+ });
964
+
965
+ // src/lib/db-daemon-client.ts
966
+ var db_daemon_client_exports = {};
967
+ __export(db_daemon_client_exports, {
968
+ createDaemonDbClient: () => createDaemonDbClient,
969
+ initDaemonDbClient: () => initDaemonDbClient
970
+ });
971
+ function normalizeStatement(stmt) {
972
+ if (typeof stmt === "string") {
973
+ return { sql: stmt, args: [] };
974
+ }
975
+ const sql = stmt.sql;
976
+ let args = [];
977
+ if (Array.isArray(stmt.args)) {
978
+ args = stmt.args.map((v) => serializeValue(v));
979
+ } else if (stmt.args && typeof stmt.args === "object") {
980
+ const named = {};
981
+ for (const [key, val] of Object.entries(stmt.args)) {
982
+ named[key] = serializeValue(val);
983
+ }
984
+ return { sql, args: named };
985
+ }
986
+ return { sql, args };
987
+ }
988
+ function createDaemonDbClient(fallbackClient) {
989
+ let _useDaemon = false;
990
+ const client = {
991
+ async execute(stmt) {
992
+ if (!_useDaemon || !isClientConnected()) {
993
+ return fallbackClient.execute(stmt);
994
+ }
995
+ const { sql, args } = normalizeStatement(stmt);
996
+ const response = await sendDaemonRequest({
997
+ type: "db-execute",
998
+ sql,
999
+ args
1000
+ });
1001
+ if (response.error) {
1002
+ const errMsg = String(response.error);
1003
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
1004
+ process.stderr.write(`[db-daemon] Transport error (${errMsg}), falling back to direct
1005
+ `);
1006
+ return fallbackClient.execute(stmt);
1007
+ }
1008
+ throw new Error(errMsg);
1009
+ }
1010
+ if (response.db) {
1011
+ return deserializeResultSet(response.db);
1012
+ }
1013
+ process.stderr.write("[db-daemon] Unexpected response shape, falling back to direct\n");
1014
+ return fallbackClient.execute(stmt);
1015
+ },
1016
+ async batch(stmts, mode) {
1017
+ if (!_useDaemon || !isClientConnected()) {
1018
+ return fallbackClient.batch(stmts, mode);
1019
+ }
1020
+ const statements = stmts.map(normalizeStatement);
1021
+ const response = await sendDaemonRequest({
1022
+ type: "db-batch",
1023
+ statements,
1024
+ mode: mode ?? "deferred"
1025
+ });
1026
+ if (response.error) {
1027
+ const errMsg = String(response.error);
1028
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
1029
+ process.stderr.write(`[db-daemon] Batch transport error (${errMsg}), falling back to direct
1030
+ `);
1031
+ return fallbackClient.batch(stmts, mode);
1032
+ }
1033
+ throw new Error(errMsg);
1034
+ }
1035
+ const batchResults = response["db-batch"];
1036
+ if (batchResults) {
1037
+ return batchResults.map(deserializeResultSet);
1038
+ }
1039
+ process.stderr.write("[db-daemon] Unexpected batch response shape, falling back to direct\n");
1040
+ return fallbackClient.batch(stmts, mode);
1041
+ },
1042
+ // Transaction support — delegate to fallback (transactions need direct connection)
1043
+ async transaction(mode) {
1044
+ return fallbackClient.transaction(mode);
1045
+ },
1046
+ // executeMultiple — delegate to fallback (used only for schema migrations)
1047
+ async executeMultiple(sql) {
1048
+ return fallbackClient.executeMultiple(sql);
1049
+ },
1050
+ // migrate — delegate to fallback
1051
+ async migrate(stmts) {
1052
+ return fallbackClient.migrate(stmts);
1053
+ },
1054
+ // Sync mode — delegate to fallback
1055
+ sync() {
1056
+ return fallbackClient.sync();
1057
+ },
1058
+ close() {
1059
+ _useDaemon = false;
1060
+ },
1061
+ get closed() {
1062
+ return fallbackClient.closed;
1063
+ },
1064
+ get protocol() {
1065
+ return fallbackClient.protocol;
1066
+ }
1067
+ };
1068
+ return {
1069
+ ...client,
1070
+ /** Enable daemon routing (call after confirming daemon is connected) */
1071
+ _enableDaemon() {
1072
+ _useDaemon = true;
1073
+ },
1074
+ /** Check if daemon routing is active */
1075
+ _isDaemonActive() {
1076
+ return _useDaemon && isClientConnected();
1077
+ }
1078
+ };
1079
+ }
1080
+ async function initDaemonDbClient(fallbackClient) {
1081
+ if (process.env.EXE_IS_DAEMON === "1") return null;
1082
+ const connected = await connectEmbedDaemon();
1083
+ if (!connected) {
1084
+ process.stderr.write("[db-daemon] Daemon unavailable \u2014 using direct SQLite\n");
1085
+ return null;
1086
+ }
1087
+ const client = createDaemonDbClient(fallbackClient);
1088
+ client._enableDaemon();
1089
+ process.stderr.write("[db-daemon] DB routing through daemon (single-writer)\n");
1090
+ return client;
1091
+ }
1092
+ var init_db_daemon_client = __esm({
1093
+ "src/lib/db-daemon-client.ts"() {
1094
+ "use strict";
1095
+ init_exe_daemon_client();
1096
+ init_daemon_protocol();
1097
+ }
1098
+ });
1099
+
503
1100
  // src/lib/database.ts
504
1101
  var database_exports = {};
505
1102
  __export(database_exports, {
@@ -508,6 +1105,7 @@ __export(database_exports, {
508
1105
  ensureSchema: () => ensureSchema,
509
1106
  getClient: () => getClient,
510
1107
  getRawClient: () => getRawClient,
1108
+ initDaemonClient: () => initDaemonClient,
511
1109
  initDatabase: () => initDatabase,
512
1110
  initTurso: () => initTurso,
513
1111
  isInitialized: () => isInitialized
@@ -535,8 +1133,27 @@ function getClient() {
535
1133
  if (!_resilientClient) {
536
1134
  throw new Error("Database client not initialized. Call initDatabase() first.");
537
1135
  }
1136
+ if (process.env.EXE_IS_DAEMON === "1") {
1137
+ return _resilientClient;
1138
+ }
1139
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
1140
+ return _daemonClient;
1141
+ }
538
1142
  return _resilientClient;
539
1143
  }
1144
+ async function initDaemonClient() {
1145
+ if (process.env.EXE_IS_DAEMON === "1") return;
1146
+ if (!_resilientClient) return;
1147
+ try {
1148
+ const { initDaemonDbClient: initDaemonDbClient2 } = await Promise.resolve().then(() => (init_db_daemon_client(), db_daemon_client_exports));
1149
+ _daemonClient = await initDaemonDbClient2(_resilientClient);
1150
+ } catch (err) {
1151
+ process.stderr.write(
1152
+ `[database] Daemon client init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}
1153
+ `
1154
+ );
1155
+ }
1156
+ }
540
1157
  function getRawClient() {
541
1158
  if (!_client) {
542
1159
  throw new Error("Database client not initialized. Call initDatabase() first.");
@@ -1023,6 +1640,12 @@ async function ensureSchema() {
1023
1640
  } catch {
1024
1641
  }
1025
1642
  }
1643
+ try {
1644
+ await client.execute(
1645
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1646
+ );
1647
+ } catch {
1648
+ }
1026
1649
  await client.executeMultiple(`
1027
1650
  CREATE TABLE IF NOT EXISTS entities (
1028
1651
  id TEXT PRIMARY KEY,
@@ -1075,7 +1698,30 @@ async function ensureSchema() {
1075
1698
  entity_id TEXT NOT NULL,
1076
1699
  PRIMARY KEY (hyperedge_id, entity_id)
1077
1700
  );
1701
+
1702
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1703
+ name,
1704
+ content=entities,
1705
+ content_rowid=rowid
1706
+ );
1707
+
1708
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1709
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1710
+ END;
1711
+
1712
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1713
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1714
+ END;
1715
+
1716
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1717
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1718
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1719
+ END;
1078
1720
  `);
1721
+ try {
1722
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1723
+ } catch {
1724
+ }
1079
1725
  await client.executeMultiple(`
1080
1726
  CREATE TABLE IF NOT EXISTS entity_aliases (
1081
1727
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1256,6 +1902,33 @@ async function ensureSchema() {
1256
1902
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1257
1903
  ON conversations(channel_id);
1258
1904
  `);
1905
+ await client.executeMultiple(`
1906
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1907
+ session_uuid TEXT PRIMARY KEY,
1908
+ agent_id TEXT NOT NULL,
1909
+ session_name TEXT,
1910
+ task_id TEXT,
1911
+ project_name TEXT,
1912
+ started_at TEXT NOT NULL
1913
+ );
1914
+
1915
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1916
+ ON session_agent_map(agent_id);
1917
+ `);
1918
+ try {
1919
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1920
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1921
+ await client.execute({
1922
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1923
+ SELECT session_id, agent_id, '', MIN(timestamp)
1924
+ FROM memories
1925
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1926
+ GROUP BY session_id, agent_id`,
1927
+ args: []
1928
+ });
1929
+ }
1930
+ } catch {
1931
+ }
1259
1932
  try {
1260
1933
  await client.execute({
1261
1934
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1389,15 +2062,41 @@ async function ensureSchema() {
1389
2062
  });
1390
2063
  } catch {
1391
2064
  }
2065
+ for (const col of [
2066
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
2067
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
2068
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
2069
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
2070
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
2071
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
2072
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
2073
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
2074
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
2075
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
2076
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
2077
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
2078
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
2079
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
2080
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
2081
+ ]) {
2082
+ try {
2083
+ await client.execute(col);
2084
+ } catch {
2085
+ }
2086
+ }
1392
2087
  }
1393
2088
  async function disposeDatabase() {
2089
+ if (_daemonClient) {
2090
+ _daemonClient.close();
2091
+ _daemonClient = null;
2092
+ }
1394
2093
  if (_client) {
1395
2094
  _client.close();
1396
2095
  _client = null;
1397
2096
  _resilientClient = null;
1398
2097
  }
1399
2098
  }
1400
- var _client, _resilientClient, initTurso, disposeTurso;
2099
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1401
2100
  var init_database = __esm({
1402
2101
  "src/lib/database.ts"() {
1403
2102
  "use strict";
@@ -1405,31 +2104,96 @@ var init_database = __esm({
1405
2104
  init_employees();
1406
2105
  _client = null;
1407
2106
  _resilientClient = null;
2107
+ _daemonClient = null;
1408
2108
  initTurso = initDatabase;
1409
2109
  disposeTurso = disposeDatabase;
1410
2110
  }
1411
2111
  });
1412
2112
 
1413
2113
  // src/lib/license.ts
1414
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
1415
- import { randomUUID } from "crypto";
1416
- import path6 from "path";
2114
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
2115
+ import { randomUUID as randomUUID2 } from "crypto";
2116
+ import path8 from "path";
1417
2117
  import { jwtVerify, importSPKI } from "jose";
1418
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
2118
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1419
2119
  var init_license = __esm({
1420
2120
  "src/lib/license.ts"() {
1421
2121
  "use strict";
1422
2122
  init_config();
1423
- LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
1424
- CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
1425
- DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
2123
+ LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
2124
+ CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2125
+ DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
2126
+ PLAN_LIMITS = {
2127
+ free: { devices: 1, employees: 1, memories: 5e3 },
2128
+ pro: { devices: 3, employees: 5, memories: 1e5 },
2129
+ team: { devices: 10, employees: 20, memories: 1e6 },
2130
+ agency: { devices: 50, employees: 100, memories: 1e7 },
2131
+ enterprise: { devices: -1, employees: -1, memories: -1 }
2132
+ };
1426
2133
  }
1427
2134
  });
1428
2135
 
1429
2136
  // src/lib/plan-limits.ts
1430
- import { readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
1431
- import path7 from "path";
1432
- var CACHE_PATH2;
2137
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
2138
+ import path9 from "path";
2139
+ function getLicenseSync() {
2140
+ try {
2141
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
2142
+ const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
2143
+ if (!raw.token || typeof raw.token !== "string") return freeLicense();
2144
+ const parts = raw.token.split(".");
2145
+ if (parts.length !== 3) return freeLicense();
2146
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
2147
+ const plan = payload.plan ?? "free";
2148
+ const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
2149
+ return {
2150
+ valid: true,
2151
+ plan,
2152
+ email: payload.sub ?? "",
2153
+ expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
2154
+ deviceLimit: limits.devices,
2155
+ employeeLimit: limits.employees,
2156
+ memoryLimit: limits.memories
2157
+ };
2158
+ } catch {
2159
+ return freeLicense();
2160
+ }
2161
+ }
2162
+ function freeLicense() {
2163
+ const limits = PLAN_LIMITS.free;
2164
+ return {
2165
+ valid: true,
2166
+ plan: "free",
2167
+ email: "",
2168
+ expiresAt: null,
2169
+ deviceLimit: limits.devices,
2170
+ employeeLimit: limits.employees,
2171
+ memoryLimit: limits.memories
2172
+ };
2173
+ }
2174
+ function assertEmployeeLimitSync(rosterPath) {
2175
+ const license = getLicenseSync();
2176
+ if (license.employeeLimit < 0) return;
2177
+ const filePath = rosterPath ?? EMPLOYEES_PATH;
2178
+ let count = 0;
2179
+ try {
2180
+ if (existsSync8(filePath)) {
2181
+ const raw = readFileSync9(filePath, "utf8");
2182
+ const employees = JSON.parse(raw);
2183
+ count = Array.isArray(employees) ? employees.length : 0;
2184
+ }
2185
+ } catch {
2186
+ throw new PlanLimitError(
2187
+ `Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
2188
+ );
2189
+ }
2190
+ if (count >= license.employeeLimit) {
2191
+ throw new PlanLimitError(
2192
+ `Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
2193
+ );
2194
+ }
2195
+ }
2196
+ var PlanLimitError, CACHE_PATH2;
1433
2197
  var init_plan_limits = __esm({
1434
2198
  "src/lib/plan-limits.ts"() {
1435
2199
  "use strict";
@@ -1437,32 +2201,2363 @@ var init_plan_limits = __esm({
1437
2201
  init_employees();
1438
2202
  init_license();
1439
2203
  init_config();
1440
- CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
2204
+ PlanLimitError = class extends Error {
2205
+ constructor(message) {
2206
+ super(message);
2207
+ this.name = "PlanLimitError";
2208
+ }
2209
+ };
2210
+ CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
1441
2211
  }
1442
2212
  });
1443
2213
 
1444
- // src/lib/tmux-routing.ts
1445
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync6, appendFileSync } from "fs";
1446
- import path8 from "path";
2214
+ // src/lib/notifications.ts
2215
+ var notifications_exports = {};
2216
+ __export(notifications_exports, {
2217
+ cleanupOldNotifications: () => cleanupOldNotifications,
2218
+ formatNotifications: () => formatNotifications,
2219
+ markAsRead: () => markAsRead,
2220
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
2221
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
2222
+ migrateJsonNotifications: () => migrateJsonNotifications,
2223
+ readUnreadNotifications: () => readUnreadNotifications,
2224
+ writeNotification: () => writeNotification
2225
+ });
2226
+ import crypto from "crypto";
2227
+ import path10 from "path";
1447
2228
  import os5 from "os";
1448
- import { fileURLToPath } from "url";
2229
+ import {
2230
+ readFileSync as readFileSync10,
2231
+ readdirSync as readdirSync2,
2232
+ unlinkSync as unlinkSync4,
2233
+ existsSync as existsSync9,
2234
+ rmdirSync
2235
+ } from "fs";
2236
+ async function writeNotification(notification) {
2237
+ try {
2238
+ const client = getClient();
2239
+ const id = crypto.randomUUID();
2240
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2241
+ await client.execute({
2242
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
2243
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2244
+ args: [
2245
+ id,
2246
+ notification.agentId,
2247
+ notification.agentRole,
2248
+ notification.event,
2249
+ notification.project,
2250
+ notification.summary,
2251
+ notification.taskFile ?? null,
2252
+ now
2253
+ ]
2254
+ });
2255
+ } catch (err) {
2256
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2257
+ `);
2258
+ }
2259
+ }
2260
+ async function readUnreadNotifications(agentFilter) {
2261
+ try {
2262
+ const client = getClient();
2263
+ const conditions = ["read = 0"];
2264
+ const args = [];
2265
+ if (agentFilter) {
2266
+ conditions.push("agent_id = ?");
2267
+ args.push(agentFilter);
2268
+ }
2269
+ const result = await client.execute({
2270
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, created_at
2271
+ FROM notifications
2272
+ WHERE ${conditions.join(" AND ")}
2273
+ ORDER BY created_at ASC`,
2274
+ args
2275
+ });
2276
+ return result.rows.map((r) => ({
2277
+ id: String(r.id),
2278
+ agentId: String(r.agent_id),
2279
+ agentRole: String(r.agent_role),
2280
+ event: String(r.event),
2281
+ project: String(r.project),
2282
+ summary: String(r.summary),
2283
+ taskFile: r.task_file ? String(r.task_file) : void 0,
2284
+ timestamp: String(r.created_at),
2285
+ read: false
2286
+ }));
2287
+ } catch {
2288
+ return [];
2289
+ }
2290
+ }
2291
+ async function markAsRead(ids) {
2292
+ if (ids.length === 0) return;
2293
+ try {
2294
+ const client = getClient();
2295
+ const placeholders = ids.map(() => "?").join(", ");
2296
+ await client.execute({
2297
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})`,
2298
+ args: ids
2299
+ });
2300
+ } catch {
2301
+ }
2302
+ }
2303
+ async function markAsReadByTaskFile(taskFile) {
2304
+ try {
2305
+ const client = getClient();
2306
+ await client.execute({
2307
+ sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
2308
+ args: [taskFile]
2309
+ });
2310
+ } catch {
2311
+ }
2312
+ }
2313
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS) {
2314
+ try {
2315
+ const client = getClient();
2316
+ const cutoff = new Date(
2317
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
2318
+ ).toISOString();
2319
+ const result = await client.execute({
2320
+ sql: "DELETE FROM notifications WHERE created_at < ?",
2321
+ args: [cutoff]
2322
+ });
2323
+ return result.rowsAffected;
2324
+ } catch {
2325
+ return 0;
2326
+ }
2327
+ }
2328
+ async function markDoneTaskNotificationsAsRead() {
2329
+ try {
2330
+ const client = getClient();
2331
+ const result = await client.execute({
2332
+ sql: `UPDATE notifications SET read = 1
2333
+ WHERE read = 0
2334
+ AND task_file IS NOT NULL
2335
+ AND task_file IN (
2336
+ SELECT task_file FROM tasks WHERE status = 'done'
2337
+ )`,
2338
+ args: []
2339
+ });
2340
+ return result.rowsAffected;
2341
+ } catch {
2342
+ return 0;
2343
+ }
2344
+ }
2345
+ function formatNotifications(notifications) {
2346
+ if (notifications.length === 0) return "";
2347
+ const grouped = /* @__PURE__ */ new Map();
2348
+ for (const n of notifications) {
2349
+ const key = `${n.agentId}|${n.agentRole}`;
2350
+ if (!grouped.has(key)) grouped.set(key, []);
2351
+ grouped.get(key).push(n);
2352
+ }
2353
+ const lines = [];
2354
+ lines.push(`## Notifications (${notifications.length} unread)
2355
+ `);
2356
+ for (const [key, items] of grouped) {
2357
+ const [agentId, agentRole] = key.split("|");
2358
+ lines.push(`**${agentId}** (${agentRole}):`);
2359
+ for (const item of items) {
2360
+ const ago = formatTimeAgo(item.timestamp);
2361
+ const icon = eventIcon(item.event);
2362
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
2363
+ }
2364
+ lines.push("");
2365
+ }
2366
+ return lines.join("\n");
2367
+ }
2368
+ async function migrateJsonNotifications() {
2369
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path10.join(os5.homedir(), ".exe-os");
2370
+ const notifDir = path10.join(base, "notifications");
2371
+ if (!existsSync9(notifDir)) return 0;
2372
+ let migrated = 0;
2373
+ try {
2374
+ const files = readdirSync2(notifDir).filter((f) => f.endsWith(".json"));
2375
+ if (files.length === 0) return 0;
2376
+ const client = getClient();
2377
+ for (const file of files) {
2378
+ try {
2379
+ const filePath = path10.join(notifDir, file);
2380
+ const data = JSON.parse(readFileSync10(filePath, "utf8"));
2381
+ await client.execute({
2382
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
2383
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2384
+ args: [
2385
+ crypto.randomUUID(),
2386
+ data.agentId ?? "unknown",
2387
+ data.agentRole ?? "unknown",
2388
+ data.event ?? "session_summary",
2389
+ data.project ?? "unknown",
2390
+ data.summary ?? "",
2391
+ data.taskFile ?? null,
2392
+ data.read ? 1 : 0,
2393
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2394
+ ]
2395
+ });
2396
+ unlinkSync4(filePath);
2397
+ migrated++;
2398
+ } catch {
2399
+ }
2400
+ }
2401
+ try {
2402
+ const remaining = readdirSync2(notifDir);
2403
+ if (remaining.length === 0) {
2404
+ rmdirSync(notifDir);
2405
+ }
2406
+ } catch {
2407
+ }
2408
+ } catch {
2409
+ }
2410
+ return migrated;
2411
+ }
2412
+ function eventIcon(event) {
2413
+ switch (event) {
2414
+ case "task_complete":
2415
+ return "Completed:";
2416
+ case "task_needs_fix":
2417
+ return "Needs fix:";
2418
+ case "session_summary":
2419
+ return "Session:";
2420
+ case "error_spike":
2421
+ return "Errors:";
2422
+ case "orphan_task":
2423
+ return "Orphan:";
2424
+ case "subtasks_complete":
2425
+ return "Subtasks done:";
2426
+ case "capacity_relaunch":
2427
+ return "Relaunched:";
2428
+ }
2429
+ }
2430
+ function formatTimeAgo(timestamp) {
2431
+ const diffMs = Date.now() - new Date(timestamp).getTime();
2432
+ const mins = Math.floor(diffMs / 6e4);
2433
+ if (mins < 1) return "just now";
2434
+ if (mins < 60) return `${mins}m ago`;
2435
+ const hours = Math.floor(mins / 60);
2436
+ if (hours < 24) return `${hours}h ago`;
2437
+ const days = Math.floor(hours / 24);
2438
+ return `${days}d ago`;
2439
+ }
2440
+ var CLEANUP_DAYS;
2441
+ var init_notifications = __esm({
2442
+ "src/lib/notifications.ts"() {
2443
+ "use strict";
2444
+ init_database();
2445
+ CLEANUP_DAYS = 7;
2446
+ }
2447
+ });
2448
+
2449
+ // src/lib/session-kill-telemetry.ts
2450
+ import crypto2 from "crypto";
2451
+ async function recordSessionKill(input2) {
2452
+ try {
2453
+ const client = getClient();
2454
+ await client.execute({
2455
+ sql: `INSERT INTO session_kills
2456
+ (id, session_name, agent_id, killed_at, reason,
2457
+ ticks_idle, estimated_tokens_saved)
2458
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
2459
+ args: [
2460
+ crypto2.randomUUID(),
2461
+ input2.sessionName,
2462
+ input2.agentId,
2463
+ (/* @__PURE__ */ new Date()).toISOString(),
2464
+ input2.reason,
2465
+ input2.ticksIdle ?? null,
2466
+ input2.estimatedTokensSaved ?? null
2467
+ ]
2468
+ });
2469
+ } catch (err) {
2470
+ process.stderr.write(
2471
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
2472
+ `
2473
+ );
2474
+ }
2475
+ }
2476
+ var init_session_kill_telemetry = __esm({
2477
+ "src/lib/session-kill-telemetry.ts"() {
2478
+ "use strict";
2479
+ init_database();
2480
+ }
2481
+ });
2482
+
2483
+ // src/lib/state-bus.ts
2484
+ var StateBus, orgBus;
2485
+ var init_state_bus = __esm({
2486
+ "src/lib/state-bus.ts"() {
2487
+ "use strict";
2488
+ StateBus = class {
2489
+ handlers = /* @__PURE__ */ new Map();
2490
+ globalHandlers = /* @__PURE__ */ new Set();
2491
+ /** Emit an event to all subscribers */
2492
+ emit(event) {
2493
+ const typeHandlers = this.handlers.get(event.type);
2494
+ if (typeHandlers) {
2495
+ for (const handler of typeHandlers) {
2496
+ try {
2497
+ handler(event);
2498
+ } catch {
2499
+ }
2500
+ }
2501
+ }
2502
+ for (const handler of this.globalHandlers) {
2503
+ try {
2504
+ handler(event);
2505
+ } catch {
2506
+ }
2507
+ }
2508
+ }
2509
+ /** Subscribe to a specific event type */
2510
+ on(type, handler) {
2511
+ if (!this.handlers.has(type)) {
2512
+ this.handlers.set(type, /* @__PURE__ */ new Set());
2513
+ }
2514
+ this.handlers.get(type).add(handler);
2515
+ }
2516
+ /** Subscribe to ALL events */
2517
+ onAny(handler) {
2518
+ this.globalHandlers.add(handler);
2519
+ }
2520
+ /** Unsubscribe from a specific event type */
2521
+ off(type, handler) {
2522
+ this.handlers.get(type)?.delete(handler);
2523
+ }
2524
+ /** Unsubscribe from ALL events */
2525
+ offAny(handler) {
2526
+ this.globalHandlers.delete(handler);
2527
+ }
2528
+ /** Remove all listeners */
2529
+ clear() {
2530
+ this.handlers.clear();
2531
+ this.globalHandlers.clear();
2532
+ }
2533
+ };
2534
+ orgBus = new StateBus();
2535
+ }
2536
+ });
2537
+
2538
+ // src/lib/tasks-crud.ts
2539
+ var tasks_crud_exports = {};
2540
+ __export(tasks_crud_exports, {
2541
+ TASK_ALREADY_CLAIMED_PREFIX: () => TASK_ALREADY_CLAIMED_PREFIX,
2542
+ checkStaleCompletion: () => checkStaleCompletion,
2543
+ createTaskCore: () => createTaskCore,
2544
+ deleteTaskCore: () => deleteTaskCore,
2545
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
2546
+ ensureGitignoreExe: () => ensureGitignoreExe,
2547
+ extractParentFromContext: () => extractParentFromContext,
2548
+ isTmuxSessionAlive: () => isTmuxSessionAlive,
2549
+ listTasks: () => listTasks,
2550
+ resolveTask: () => resolveTask,
2551
+ slugify: () => slugify,
2552
+ updateTaskStatus: () => updateTaskStatus,
2553
+ writeCheckpoint: () => writeCheckpoint
2554
+ });
2555
+ import crypto3 from "crypto";
2556
+ import path11 from "path";
2557
+ import os6 from "os";
2558
+ import { execSync as execSync5 } from "child_process";
2559
+ import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2560
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
2561
+ async function writeCheckpoint(input2) {
2562
+ const client = getClient();
2563
+ const row = await resolveTask(client, input2.taskId);
2564
+ const taskId = String(row.id);
2565
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2566
+ const blockedByIds = [];
2567
+ if (row.blocked_by) {
2568
+ blockedByIds.push(String(row.blocked_by));
2569
+ }
2570
+ const checkpoint = {
2571
+ step: input2.step,
2572
+ context_summary: input2.contextSummary,
2573
+ files_touched: input2.filesTouched ?? [],
2574
+ blocked_by_ids: blockedByIds,
2575
+ last_checkpoint_at: now
2576
+ };
2577
+ const result = await client.execute({
2578
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
2579
+ args: [JSON.stringify(checkpoint), now, taskId]
2580
+ });
2581
+ if (result.rowsAffected === 0) {
2582
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
2583
+ }
2584
+ const countResult = await client.execute({
2585
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
2586
+ args: [taskId]
2587
+ });
2588
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
2589
+ return { checkpointCount };
2590
+ }
2591
+ function extractParentFromContext(contextBody) {
2592
+ if (!contextBody) return null;
2593
+ const match = contextBody.match(
2594
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
2595
+ );
2596
+ return match ? match[1].toLowerCase() : null;
2597
+ }
2598
+ function slugify(title) {
2599
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2600
+ }
2601
+ function buildKeywordIndex() {
2602
+ const idx = /* @__PURE__ */ new Map();
2603
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
2604
+ for (const kw of keywords) {
2605
+ const existing = idx.get(kw) ?? [];
2606
+ existing.push(role);
2607
+ idx.set(kw, existing);
2608
+ }
2609
+ }
2610
+ return idx;
2611
+ }
2612
+ function checkLaneAffinity(title, context, assigneeName) {
2613
+ const employees = loadEmployeesSync();
2614
+ const employee = employees.find((e) => e.name === assigneeName);
2615
+ if (!employee) return void 0;
2616
+ const assigneeRole = employee.role;
2617
+ const text = `${title} ${context}`.toLowerCase();
2618
+ const matchedRoles = /* @__PURE__ */ new Set();
2619
+ for (const [keyword, roles] of KEYWORD_INDEX) {
2620
+ if (text.includes(keyword)) {
2621
+ for (const role of roles) matchedRoles.add(role);
2622
+ }
2623
+ }
2624
+ if (matchedRoles.size === 0) return void 0;
2625
+ if (matchedRoles.has(assigneeRole)) return void 0;
2626
+ if (assigneeRole === "COO") return void 0;
2627
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
2628
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
2629
+ }
2630
+ async function resolveTask(client, identifier, scopeSession) {
2631
+ const scope = sessionScopeFilter(scopeSession);
2632
+ let result = await client.execute({
2633
+ sql: `SELECT * FROM tasks WHERE id = ?${scope.sql}`,
2634
+ args: [identifier, ...scope.args]
2635
+ });
2636
+ if (result.rows.length === 1) return result.rows[0];
2637
+ result = await client.execute({
2638
+ sql: `SELECT * FROM tasks WHERE task_file LIKE ?${scope.sql}`,
2639
+ args: [`%${identifier}%`, ...scope.args]
2640
+ });
2641
+ if (result.rows.length === 1) return result.rows[0];
2642
+ if (result.rows.length > 1) {
2643
+ const exact = result.rows.filter(
2644
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
2645
+ );
2646
+ if (exact.length === 1) return exact[0];
2647
+ const candidates = exact.length > 1 ? exact : result.rows;
2648
+ const active = candidates.filter(
2649
+ (r) => !["done", "cancelled"].includes(String(r.status))
2650
+ );
2651
+ if (active.length === 1) return active[0];
2652
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
2653
+ throw new Error(
2654
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
2655
+ );
2656
+ }
2657
+ result = await client.execute({
2658
+ sql: `SELECT * FROM tasks WHERE title LIKE ?${scope.sql}`,
2659
+ args: [`%${identifier}%`, ...scope.args]
2660
+ });
2661
+ if (result.rows.length === 1) return result.rows[0];
2662
+ if (result.rows.length > 1) {
2663
+ const active = result.rows.filter(
2664
+ (r) => !["done", "cancelled"].includes(String(r.status))
2665
+ );
2666
+ if (active.length === 1) return active[0];
2667
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
2668
+ throw new Error(
2669
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
2670
+ );
2671
+ }
2672
+ throw new Error(`Task not found: ${identifier}`);
2673
+ }
2674
+ async function createTaskCore(input2) {
2675
+ const client = getClient();
2676
+ const id = crypto3.randomUUID();
2677
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2678
+ const slug = slugify(input2.title);
2679
+ let earlySessionScope = null;
2680
+ try {
2681
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2682
+ earlySessionScope = resolveExeSession2();
2683
+ } catch {
2684
+ }
2685
+ const scope = earlySessionScope ?? "default";
2686
+ const taskFile = input2.taskFile ?? `tasks/${scope}/${input2.assignedTo}/${slug}.md`;
2687
+ let blockedById = null;
2688
+ const initialStatus = input2.blockedBy ? "blocked" : "open";
2689
+ if (input2.blockedBy) {
2690
+ const blocker = await resolveTask(client, input2.blockedBy);
2691
+ blockedById = String(blocker.id);
2692
+ }
2693
+ let parentTaskId = null;
2694
+ let parentRef = input2.parentTaskId;
2695
+ if (!parentRef) {
2696
+ const extracted = extractParentFromContext(input2.context);
2697
+ if (extracted) {
2698
+ parentRef = extracted;
2699
+ process.stderr.write(
2700
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
2701
+ );
2702
+ }
2703
+ }
2704
+ if (parentRef) {
2705
+ try {
2706
+ const parent = await resolveTask(client, parentRef);
2707
+ parentTaskId = String(parent.id);
2708
+ } catch (err) {
2709
+ if (!input2.parentTaskId) {
2710
+ throw new Error(
2711
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
2712
+ );
2713
+ }
2714
+ throw err;
2715
+ }
2716
+ }
2717
+ let warning;
2718
+ const dupScope = sessionScopeFilter();
2719
+ const dupCheck = await client.execute({
2720
+ sql: `SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')${dupScope.sql}`,
2721
+ args: [input2.title, input2.assignedTo, ...dupScope.args]
2722
+ });
2723
+ if (dupCheck.rows.length > 0) {
2724
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
2725
+ }
2726
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2727
+ const laneWarning = checkLaneAffinity(input2.title, input2.context, input2.assignedTo);
2728
+ if (laneWarning) {
2729
+ warning = warning ? `${warning}
2730
+ ${laneWarning}` : laneWarning;
2731
+ }
2732
+ }
2733
+ if (input2.baseDir) {
2734
+ try {
2735
+ await mkdir3(path11.join(input2.baseDir, "exe", "output"), { recursive: true });
2736
+ await mkdir3(path11.join(input2.baseDir, "exe", "research"), { recursive: true });
2737
+ await ensureArchitectureDoc(input2.baseDir, input2.projectName);
2738
+ await ensureGitignoreExe(input2.baseDir);
2739
+ } catch {
2740
+ }
2741
+ }
2742
+ const complexity = input2.complexity ?? "standard";
2743
+ const sessionScope = earlySessionScope;
2744
+ await client.execute({
2745
+ sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
2746
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2747
+ args: [
2748
+ id,
2749
+ input2.title,
2750
+ input2.assignedTo,
2751
+ input2.assignedBy,
2752
+ input2.projectName,
2753
+ input2.priority,
2754
+ initialStatus,
2755
+ taskFile,
2756
+ blockedById,
2757
+ parentTaskId,
2758
+ input2.reviewer ?? null,
2759
+ input2.context,
2760
+ complexity,
2761
+ input2.budgetTokens ?? null,
2762
+ input2.budgetFallbackModel ?? null,
2763
+ 0,
2764
+ null,
2765
+ sessionScope,
2766
+ now,
2767
+ now
2768
+ ]
2769
+ });
2770
+ if (input2.baseDir) {
2771
+ try {
2772
+ const EXE_OS_DIR = path11.join(os6.homedir(), ".exe-os");
2773
+ const mdPath = path11.join(EXE_OS_DIR, taskFile);
2774
+ const mdDir = path11.dirname(mdPath);
2775
+ if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2776
+ const reviewer = input2.reviewer ?? input2.assignedBy;
2777
+ const mdContent = `# ${input2.title}
2778
+
2779
+ **ID:** ${id}
2780
+ **Status:** ${initialStatus}
2781
+ **Priority:** ${input2.priority}
2782
+ **Assigned by:** ${input2.assignedBy}
2783
+ **Assigned to:** ${input2.assignedTo}
2784
+ **Project:** ${input2.projectName}
2785
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2786
+ **Parent task:** ${parentTaskId}` : ""}
2787
+ **Reviewer:** ${reviewer}
2788
+
2789
+ ## Context
2790
+
2791
+ ${input2.context}
2792
+
2793
+ ## MANDATORY: When done
2794
+
2795
+ You MUST call update_task with status "done" and a result summary when finished.
2796
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2797
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2798
+ `;
2799
+ await writeFile3(mdPath, mdContent, "utf-8");
2800
+ } catch (err) {
2801
+ process.stderr.write(
2802
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
2803
+ `
2804
+ );
2805
+ }
2806
+ }
2807
+ return {
2808
+ id,
2809
+ title: input2.title,
2810
+ assignedTo: input2.assignedTo,
2811
+ assignedBy: input2.assignedBy,
2812
+ projectName: input2.projectName,
2813
+ priority: input2.priority,
2814
+ status: initialStatus,
2815
+ taskFile,
2816
+ createdAt: now,
2817
+ updatedAt: now,
2818
+ warning,
2819
+ budgetTokens: input2.budgetTokens ?? null,
2820
+ budgetFallbackModel: input2.budgetFallbackModel ?? null,
2821
+ tokensUsed: 0,
2822
+ tokensWarnedAt: null
2823
+ };
2824
+ }
2825
+ async function listTasks(input2) {
2826
+ const client = getClient();
2827
+ const conditions = [];
2828
+ const args = [];
2829
+ if (input2.assignedTo) {
2830
+ conditions.push("assigned_to = ?");
2831
+ args.push(input2.assignedTo);
2832
+ }
2833
+ if (input2.status) {
2834
+ conditions.push("status = ?");
2835
+ args.push(input2.status);
2836
+ } else {
2837
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
2838
+ }
2839
+ if (input2.projectName) {
2840
+ conditions.push("project_name = ?");
2841
+ args.push(input2.projectName);
2842
+ }
2843
+ if (input2.priority) {
2844
+ conditions.push("priority = ?");
2845
+ args.push(input2.priority);
2846
+ }
2847
+ const scope = sessionScopeFilter();
2848
+ if (scope.sql) {
2849
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
2850
+ args.push(...scope.args);
2851
+ }
2852
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2853
+ const result = await client.execute({
2854
+ sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC LIMIT 1000`,
2855
+ args
2856
+ });
2857
+ return result.rows.map((r) => ({
2858
+ id: String(r.id),
2859
+ title: String(r.title),
2860
+ assignedTo: String(r.assigned_to),
2861
+ assignedBy: String(r.assigned_by),
2862
+ projectName: String(r.project_name),
2863
+ priority: String(r.priority),
2864
+ status: String(r.status),
2865
+ taskFile: String(r.task_file),
2866
+ createdAt: String(r.created_at),
2867
+ updatedAt: String(r.updated_at),
2868
+ checkpointCount: Number(r.checkpoint_count ?? 0),
2869
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
2870
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
2871
+ tokensUsed: Number(r.tokens_used ?? 0),
2872
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2873
+ }));
2874
+ }
2875
+ function isTmuxSessionAlive(identifier) {
2876
+ if (!identifier || identifier === "unknown") return true;
2877
+ try {
2878
+ if (identifier.startsWith("%")) {
2879
+ const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
2880
+ timeout: 2e3,
2881
+ encoding: "utf8",
2882
+ stdio: ["pipe", "pipe", "pipe"]
2883
+ });
2884
+ return output.split("\n").some((l) => l.trim() === identifier);
2885
+ } else {
2886
+ execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2887
+ timeout: 2e3,
2888
+ stdio: ["pipe", "pipe", "pipe"]
2889
+ });
2890
+ return true;
2891
+ }
2892
+ } catch {
2893
+ if (identifier.startsWith("%")) return true;
2894
+ try {
2895
+ execSync5("tmux list-sessions", {
2896
+ timeout: 2e3,
2897
+ stdio: ["pipe", "pipe", "pipe"]
2898
+ });
2899
+ return false;
2900
+ } catch {
2901
+ return true;
2902
+ }
2903
+ }
2904
+ }
2905
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
2906
+ if (!taskContext) return null;
2907
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
2908
+ try {
2909
+ const since = new Date(taskCreatedAt).toISOString();
2910
+ const branch = execSync5(
2911
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
2912
+ { encoding: "utf8", timeout: 3e3 }
2913
+ ).trim();
2914
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
2915
+ const commitCount = execSync5(
2916
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
2917
+ { encoding: "utf8", timeout: 5e3 }
2918
+ ).trim();
2919
+ const count = parseInt(commitCount, 10);
2920
+ if (count === 0) {
2921
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
2922
+ }
2923
+ return null;
2924
+ } catch {
2925
+ return null;
2926
+ }
2927
+ }
2928
+ async function updateTaskStatus(input2) {
2929
+ const client = getClient();
2930
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2931
+ const row = await resolveTask(client, input2.taskId);
2932
+ const taskId = String(row.id);
2933
+ const taskFile = String(row.task_file);
2934
+ if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
2935
+ process.stderr.write(
2936
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
2937
+ `
2938
+ );
2939
+ }
2940
+ if (input2.status === "done") {
2941
+ const existingRow = await client.execute({
2942
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
2943
+ args: [taskId]
2944
+ });
2945
+ if (existingRow.rows.length > 0) {
2946
+ const ctx = existingRow.rows[0];
2947
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
2948
+ if (warning) {
2949
+ input2.result = input2.result ? `\u26A0\uFE0F ${warning}
2950
+
2951
+ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
2952
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
2953
+ `);
2954
+ }
2955
+ }
2956
+ }
2957
+ if (input2.status === "in_progress") {
2958
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
2959
+ const claim = await client.execute({
2960
+ sql: `UPDATE tasks
2961
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
2962
+ WHERE id = ? AND status = 'open'`,
2963
+ args: [tmuxSession, now, taskId]
2964
+ });
2965
+ if (claim.rowsAffected === 0) {
2966
+ const current = await client.execute({
2967
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
2968
+ args: [taskId]
2969
+ });
2970
+ const cur = current.rows[0];
2971
+ const curStatus = cur?.status ?? "unknown";
2972
+ const claimedBySession = cur?.assigned_tmux ?? "";
2973
+ const assignedBy = cur?.assigned_by ?? "";
2974
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
2975
+ process.stderr.write(
2976
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
2977
+ `
2978
+ );
2979
+ await client.execute({
2980
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
2981
+ args: [now, taskId]
2982
+ });
2983
+ const retried = await client.execute({
2984
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
2985
+ args: [tmuxSession, now, taskId]
2986
+ });
2987
+ if (retried.rowsAffected > 0) {
2988
+ try {
2989
+ await writeCheckpoint({
2990
+ taskId,
2991
+ step: "reclaimed_dead_session",
2992
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
2993
+ });
2994
+ } catch {
2995
+ }
2996
+ return { row, taskFile, now, taskId };
2997
+ }
2998
+ }
2999
+ if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || isCoordinatorName(input2.callerAgentId))) {
3000
+ process.stderr.write(
3001
+ `[tasks] Assigner override: ${input2.callerAgentId} reclaiming ${taskId}
3002
+ `
3003
+ );
3004
+ await client.execute({
3005
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
3006
+ args: [tmuxSession, now, taskId]
3007
+ });
3008
+ try {
3009
+ await writeCheckpoint({
3010
+ taskId,
3011
+ step: "assigner_override",
3012
+ contextSummary: `Task force-reclaimed by assigner ${input2.callerAgentId}.`
3013
+ });
3014
+ } catch {
3015
+ }
3016
+ return { row, taskFile, now, taskId };
3017
+ }
3018
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
3019
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
3020
+ }
3021
+ try {
3022
+ await writeCheckpoint({
3023
+ taskId,
3024
+ step: "claimed",
3025
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3026
+ });
3027
+ } catch {
3028
+ }
3029
+ return { row, taskFile, now, taskId };
3030
+ }
3031
+ if (input2.result) {
3032
+ await client.execute({
3033
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
3034
+ args: [input2.status, input2.result, now, taskId]
3035
+ });
3036
+ } else {
3037
+ await client.execute({
3038
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
3039
+ args: [input2.status, now, taskId]
3040
+ });
3041
+ }
3042
+ try {
3043
+ await writeCheckpoint({
3044
+ taskId,
3045
+ step: `status_transition:${input2.status}`,
3046
+ contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
3047
+ });
3048
+ } catch {
3049
+ }
3050
+ return { row, taskFile, now, taskId };
3051
+ }
3052
+ async function deleteTaskCore(taskId, _baseDir) {
3053
+ const client = getClient();
3054
+ const row = await resolveTask(client, taskId);
3055
+ const id = String(row.id);
3056
+ const taskFile = String(row.task_file);
3057
+ const assignedTo = String(row.assigned_to);
3058
+ const assignedBy = String(row.assigned_by);
3059
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
3060
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
3061
+ return { taskFile, assignedTo, assignedBy, taskSlug };
3062
+ }
3063
+ async function ensureArchitectureDoc(baseDir, projectName) {
3064
+ const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
3065
+ try {
3066
+ if (existsSync10(archPath)) return;
3067
+ const template = [
3068
+ `# ${projectName} \u2014 System Architecture`,
3069
+ "",
3070
+ "> Employees: read this before every task. Update it when you change system structure.",
3071
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
3072
+ "",
3073
+ "## Overview",
3074
+ "",
3075
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
3076
+ "",
3077
+ "## Key Components",
3078
+ "",
3079
+ "<!-- List the major modules, services, or subsystems. -->",
3080
+ "",
3081
+ "## Data Flow",
3082
+ "",
3083
+ "<!-- How does data move through the system? What writes where? -->",
3084
+ "",
3085
+ "## Invariants",
3086
+ "",
3087
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
3088
+ "",
3089
+ "## Dependencies",
3090
+ "",
3091
+ "<!-- What depends on what? If I change X, what else is affected? -->",
3092
+ ""
3093
+ ].join("\n");
3094
+ await writeFile3(archPath, template, "utf-8");
3095
+ } catch {
3096
+ }
3097
+ }
3098
+ async function ensureGitignoreExe(baseDir) {
3099
+ const gitignorePath = path11.join(baseDir, ".gitignore");
3100
+ try {
3101
+ if (existsSync10(gitignorePath)) {
3102
+ const content = readFileSync11(gitignorePath, "utf-8");
3103
+ if (/^\/?exe\/?$/m.test(content)) return;
3104
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3105
+ } else {
3106
+ await writeFile3(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
3107
+ }
3108
+ } catch {
3109
+ }
3110
+ }
3111
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3112
+ var init_tasks_crud = __esm({
3113
+ "src/lib/tasks-crud.ts"() {
3114
+ "use strict";
3115
+ init_database();
3116
+ init_task_scope();
3117
+ init_employees();
3118
+ LANE_KEYWORDS = {
3119
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
3120
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
3121
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
3122
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
3123
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
3124
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
3125
+ };
3126
+ KEYWORD_INDEX = buildKeywordIndex();
3127
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
3128
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3129
+ }
3130
+ });
3131
+
3132
+ // src/lib/tasks-review.ts
3133
+ import path12 from "path";
3134
+ import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
3135
+ async function countPendingReviews(sessionScope) {
3136
+ const client = getClient();
3137
+ if (sessionScope) {
3138
+ const result2 = await client.execute({
3139
+ sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND (session_scope = ? OR session_scope IS NULL)",
3140
+ args: [sessionScope]
3141
+ });
3142
+ return Number(result2.rows[0]?.cnt) || 0;
3143
+ }
3144
+ const result = await client.execute({
3145
+ sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3146
+ args: []
3147
+ });
3148
+ return Number(result.rows[0]?.cnt) || 0;
3149
+ }
3150
+ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3151
+ const client = getClient();
3152
+ if (sessionScope) {
3153
+ const result2 = await client.execute({
3154
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3155
+ WHERE status = 'needs_review' AND updated_at > ?
3156
+ AND session_scope = ?`,
3157
+ args: [sinceIso, sessionScope]
3158
+ });
3159
+ return Number(result2.rows[0]?.cnt) || 0;
3160
+ }
3161
+ const result = await client.execute({
3162
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3163
+ WHERE status = 'needs_review' AND updated_at > ?`,
3164
+ args: [sinceIso]
3165
+ });
3166
+ return Number(result.rows[0]?.cnt) || 0;
3167
+ }
3168
+ async function listPendingReviews(limit, sessionScope) {
3169
+ const client = getClient();
3170
+ if (sessionScope) {
3171
+ const result2 = await client.execute({
3172
+ sql: `SELECT title, assigned_to, project_name FROM tasks
3173
+ WHERE status = 'needs_review'
3174
+ AND session_scope = ?
3175
+ ORDER BY priority ASC, created_at DESC LIMIT ?`,
3176
+ args: [sessionScope, limit]
3177
+ });
3178
+ return result2.rows;
3179
+ }
3180
+ const result = await client.execute({
3181
+ sql: `SELECT title, assigned_to, project_name FROM tasks
3182
+ WHERE status = 'needs_review'
3183
+ ORDER BY priority ASC, created_at DESC LIMIT ?`,
3184
+ args: [limit]
3185
+ });
3186
+ return result.rows;
3187
+ }
3188
+ async function cleanupOrphanedReviews() {
3189
+ const client = getClient();
3190
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3191
+ const r1 = await client.execute({
3192
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3193
+ WHERE status IN ('open', 'needs_review', 'in_progress')
3194
+ AND assigned_by = 'system'
3195
+ AND title LIKE 'Review:%'
3196
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3197
+ args: [now]
3198
+ });
3199
+ const r1b = await client.execute({
3200
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3201
+ WHERE status IN ('open', 'needs_review')
3202
+ AND title LIKE 'Review:%completed%'
3203
+ AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
3204
+ args: [now]
3205
+ });
3206
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
3207
+ const r2 = await client.execute({
3208
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3209
+ WHERE status = 'needs_review'
3210
+ AND result IS NOT NULL
3211
+ AND updated_at < ?`,
3212
+ args: [now, staleThreshold]
3213
+ });
3214
+ const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
3215
+ if (total > 0) {
3216
+ process.stderr.write(
3217
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
3218
+ `
3219
+ );
3220
+ }
3221
+ return total;
3222
+ }
3223
+ function getReviewChecklist(role, agent, taskSlug) {
3224
+ const roleLower = role.toLowerCase();
3225
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
3226
+ return {
3227
+ lens: "Code Quality (Engineer)",
3228
+ checklist: [
3229
+ "1. Do all tests pass? Any new tests needed?",
3230
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
3231
+ "3. Does it follow existing patterns and conventions in the codebase?",
3232
+ "4. Any regressions in the test suite?"
3233
+ ]
3234
+ };
3235
+ }
3236
+ if (roleLower === "cto" || roleLower.includes("architect")) {
3237
+ return {
3238
+ lens: "Architecture (CTO)",
3239
+ checklist: [
3240
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
3241
+ "2. Is it backward compatible? Any breaking changes?",
3242
+ "3. Does it introduce technical debt? Is that debt justified?",
3243
+ "4. Security implications? Any new attack surface?",
3244
+ "5. Does it scale? Performance considerations?",
3245
+ "6. Coordination: does this affect other employees' work or other projects?"
3246
+ ]
3247
+ };
3248
+ }
3249
+ if (roleLower === "coo" || roleLower.includes("operations")) {
3250
+ return {
3251
+ lens: "Strategic (COO)",
3252
+ checklist: [
3253
+ "1. Does this serve the project mission?",
3254
+ "2. Is this the right work at the right time?",
3255
+ "3. Does the architectural assessment make sense for the business?",
3256
+ "4. Any cross-project implications?"
3257
+ ]
3258
+ };
3259
+ }
3260
+ return {
3261
+ lens: "General",
3262
+ checklist: [
3263
+ "1. Read the original task's acceptance criteria",
3264
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
3265
+ "3. Verify code changes match requirements",
3266
+ "4. Check if tests were added/updated",
3267
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
3268
+ ]
3269
+ };
3270
+ }
3271
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
3272
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3273
+ try {
3274
+ const client = getClient();
3275
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3276
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3277
+ if (parentId) {
3278
+ const result = await client.execute({
3279
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3280
+ args: [now, parentId]
3281
+ });
3282
+ if (result.rowsAffected > 0) {
3283
+ process.stderr.write(
3284
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3285
+ `
3286
+ );
3287
+ }
3288
+ } else {
3289
+ const fileName = taskFile.split("/").pop() ?? "";
3290
+ const reviewPrefix = fileName.replace(".md", "");
3291
+ const parts = reviewPrefix.split("-");
3292
+ if (parts.length >= 3 && parts[0] === "review") {
3293
+ const agent = parts[1];
3294
+ const slug = parts.slice(2).join("-");
3295
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
3296
+ const result = await client.execute({
3297
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
3298
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
3299
+ });
3300
+ if (result.rowsAffected > 0) {
3301
+ process.stderr.write(
3302
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
3303
+ `
3304
+ );
3305
+ }
3306
+ }
3307
+ }
3308
+ } catch (err) {
3309
+ process.stderr.write(
3310
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
3311
+ `
3312
+ );
3313
+ }
3314
+ try {
3315
+ const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3316
+ if (existsSync11(cacheDir)) {
3317
+ for (const f of readdirSync3(cacheDir)) {
3318
+ if (f.startsWith("review-notified-")) {
3319
+ unlinkSync5(path12.join(cacheDir, f));
3320
+ }
3321
+ }
3322
+ }
3323
+ } catch {
3324
+ }
3325
+ }
3326
+ var init_tasks_review = __esm({
3327
+ "src/lib/tasks-review.ts"() {
3328
+ "use strict";
3329
+ init_database();
3330
+ init_config();
3331
+ init_employees();
3332
+ init_notifications();
3333
+ init_tmux_routing();
3334
+ init_session_key();
3335
+ init_state_bus();
3336
+ }
3337
+ });
3338
+
3339
+ // src/lib/tasks-chain.ts
3340
+ import path13 from "path";
3341
+ import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
3342
+ async function cascadeUnblock(taskId, baseDir, now) {
3343
+ const client = getClient();
3344
+ const unblocked = await client.execute({
3345
+ sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
3346
+ WHERE blocked_by = ? AND status = 'blocked'`,
3347
+ args: [now, taskId]
3348
+ });
3349
+ if (baseDir && unblocked.rowsAffected > 0) {
3350
+ const ubScope = sessionScopeFilter();
3351
+ const unblockedRows = await client.execute({
3352
+ sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?${ubScope.sql}`,
3353
+ args: [now, ...ubScope.args]
3354
+ });
3355
+ for (const ur of unblockedRows.rows) {
3356
+ try {
3357
+ const ubFile = path13.join(baseDir, String(ur.task_file));
3358
+ let ubContent = await readFile3(ubFile, "utf-8");
3359
+ ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3360
+ ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
3361
+ await writeFile4(ubFile, ubContent, "utf-8");
3362
+ } catch {
3363
+ }
3364
+ }
3365
+ }
3366
+ }
3367
+ async function findNextTask(assignedTo) {
3368
+ const client = getClient();
3369
+ const ntScope = sessionScopeFilter();
3370
+ const nextResult = await client.execute({
3371
+ sql: `SELECT title, task_file, priority FROM tasks
3372
+ WHERE assigned_to = ? AND status = 'open'${ntScope.sql}
3373
+ ORDER BY priority ASC, created_at ASC
3374
+ LIMIT 1`,
3375
+ args: [assignedTo, ...ntScope.args]
3376
+ });
3377
+ if (nextResult.rows.length === 1) {
3378
+ const nr = nextResult.rows[0];
3379
+ return {
3380
+ title: String(nr.title),
3381
+ priority: String(nr.priority),
3382
+ taskFile: String(nr.task_file)
3383
+ };
3384
+ }
3385
+ return void 0;
3386
+ }
3387
+ async function checkSubtaskCompletion(parentTaskId, projectName) {
3388
+ const client = getClient();
3389
+ const scScope = sessionScopeFilter();
3390
+ const remaining = await client.execute({
3391
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3392
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3393
+ args: [parentTaskId, ...scScope.args]
3394
+ });
3395
+ const cnt = Number(remaining.rows[0]?.cnt ?? 1);
3396
+ if (cnt === 0) {
3397
+ const parentRow = await client.execute({
3398
+ sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
3399
+ args: [parentTaskId]
3400
+ });
3401
+ if (parentRow.rows.length === 1) {
3402
+ const pr = parentRow.rows[0];
3403
+ const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
3404
+ await writeNotification({
3405
+ agentId: String(pr.assigned_to),
3406
+ agentRole: "system",
3407
+ event: "subtasks_complete",
3408
+ project: parentProject,
3409
+ summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
3410
+ taskFile: String(pr.task_file)
3411
+ });
3412
+ }
3413
+ }
3414
+ }
3415
+ var init_tasks_chain = __esm({
3416
+ "src/lib/tasks-chain.ts"() {
3417
+ "use strict";
3418
+ init_database();
3419
+ init_notifications();
3420
+ init_task_scope();
3421
+ }
3422
+ });
3423
+
3424
+ // src/lib/project-name.ts
3425
+ import { execSync as execSync6 } from "child_process";
3426
+ import path14 from "path";
3427
+ function getProjectName(cwd) {
3428
+ const dir = cwd ?? process.cwd();
3429
+ if (_cached2 && _cachedCwd === dir) return _cached2;
3430
+ try {
3431
+ let repoRoot;
3432
+ try {
3433
+ const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
3434
+ cwd: dir,
3435
+ encoding: "utf8",
3436
+ timeout: 2e3,
3437
+ stdio: ["pipe", "pipe", "pipe"]
3438
+ }).trim();
3439
+ repoRoot = path14.dirname(gitCommonDir);
3440
+ } catch {
3441
+ repoRoot = execSync6("git rev-parse --show-toplevel", {
3442
+ cwd: dir,
3443
+ encoding: "utf8",
3444
+ timeout: 2e3,
3445
+ stdio: ["pipe", "pipe", "pipe"]
3446
+ }).trim();
3447
+ }
3448
+ _cached2 = path14.basename(repoRoot);
3449
+ _cachedCwd = dir;
3450
+ return _cached2;
3451
+ } catch {
3452
+ _cached2 = path14.basename(dir);
3453
+ _cachedCwd = dir;
3454
+ return _cached2;
3455
+ }
3456
+ }
3457
+ var _cached2, _cachedCwd;
3458
+ var init_project_name = __esm({
3459
+ "src/lib/project-name.ts"() {
3460
+ "use strict";
3461
+ _cached2 = null;
3462
+ _cachedCwd = null;
3463
+ }
3464
+ });
3465
+
3466
+ // src/lib/session-scope.ts
3467
+ var session_scope_exports = {};
3468
+ __export(session_scope_exports, {
3469
+ assertSessionScope: () => assertSessionScope,
3470
+ findSessionForProject: () => findSessionForProject,
3471
+ getSessionProject: () => getSessionProject
3472
+ });
3473
+ function getSessionProject(sessionName) {
3474
+ const sessions = listSessions();
3475
+ const entry = sessions.find((s) => s.windowName === sessionName);
3476
+ if (!entry) return null;
3477
+ const parts = entry.projectDir.split("/").filter(Boolean);
3478
+ return parts[parts.length - 1] ?? null;
3479
+ }
3480
+ function findSessionForProject(projectName) {
3481
+ const sessions = listSessions();
3482
+ for (const s of sessions) {
3483
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
3484
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
3485
+ }
3486
+ return null;
3487
+ }
3488
+ function assertSessionScope(actionType, targetProject) {
3489
+ try {
3490
+ const currentProject = getProjectName();
3491
+ const exeSession = resolveExeSession();
3492
+ if (!exeSession) {
3493
+ return { allowed: true, reason: "no_session" };
3494
+ }
3495
+ if (currentProject === targetProject) {
3496
+ return {
3497
+ allowed: true,
3498
+ reason: "same_session",
3499
+ currentProject,
3500
+ targetProject
3501
+ };
3502
+ }
3503
+ process.stderr.write(
3504
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3505
+ `
3506
+ );
3507
+ return {
3508
+ allowed: false,
3509
+ reason: "cross_session_denied",
3510
+ currentProject,
3511
+ targetProject,
3512
+ targetSession: findSessionForProject(targetProject)?.windowName
3513
+ };
3514
+ } catch {
3515
+ return { allowed: true, reason: "no_session" };
3516
+ }
3517
+ }
3518
+ var init_session_scope = __esm({
3519
+ "src/lib/session-scope.ts"() {
3520
+ "use strict";
3521
+ init_session_registry();
3522
+ init_project_name();
3523
+ init_tmux_routing();
3524
+ init_employees();
3525
+ }
3526
+ });
3527
+
3528
+ // src/lib/tasks-notify.ts
3529
+ async function dispatchTaskToEmployee(input2) {
3530
+ if (isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
3531
+ let crossProject = false;
3532
+ if (input2.projectName) {
3533
+ try {
3534
+ const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
3535
+ const check = assertSessionScope2("dispatch_task", input2.projectName);
3536
+ if (check.reason === "cross_session_denied") {
3537
+ crossProject = true;
3538
+ return { dispatched: "skipped", crossProject: true };
3539
+ }
3540
+ } catch {
3541
+ }
3542
+ }
3543
+ try {
3544
+ const transport = getTransport();
3545
+ const exeSession = resolveExeSession();
3546
+ if (!exeSession) return { dispatched: "session_missing" };
3547
+ const sessionName = employeeSessionName(input2.assignedTo, exeSession);
3548
+ if (transport.isAlive(sessionName)) {
3549
+ const result = sendIntercom(sessionName);
3550
+ const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
3551
+ return { dispatched, session: sessionName, crossProject };
3552
+ } else {
3553
+ const projectDir = input2.projectDir ?? process.cwd();
3554
+ const result = ensureEmployee(input2.assignedTo, exeSession, projectDir, {
3555
+ autoInstance: isMultiInstance(input2.assignedTo)
3556
+ });
3557
+ if (result.status === "failed") {
3558
+ process.stderr.write(
3559
+ `[dispatch] Failed to spawn ${input2.assignedTo}: ${result.error}
3560
+ `
3561
+ );
3562
+ return { dispatched: "session_missing" };
3563
+ }
3564
+ return { dispatched: "spawned", session: result.sessionName, crossProject };
3565
+ }
3566
+ } catch {
3567
+ return { dispatched: "session_missing" };
3568
+ }
3569
+ }
3570
+ function notifyTaskDone() {
3571
+ try {
3572
+ const key = getSessionKey();
3573
+ if (key && !process.env.VITEST) notifyParentExe(key);
3574
+ } catch {
3575
+ }
3576
+ }
3577
+ async function markTaskNotificationsRead(taskFile) {
3578
+ try {
3579
+ await markAsReadByTaskFile(taskFile);
3580
+ } catch {
3581
+ }
3582
+ }
3583
+ var init_tasks_notify = __esm({
3584
+ "src/lib/tasks-notify.ts"() {
3585
+ "use strict";
3586
+ init_tmux_routing();
3587
+ init_session_key();
3588
+ init_notifications();
3589
+ init_transport();
3590
+ init_employees();
3591
+ }
3592
+ });
3593
+
3594
+ // src/lib/behaviors.ts
3595
+ import crypto4 from "crypto";
3596
+ async function storeBehavior(opts) {
3597
+ const client = getClient();
3598
+ const id = crypto4.randomUUID();
3599
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3600
+ await client.execute({
3601
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
3602
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
3603
+ args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
3604
+ });
3605
+ return id;
3606
+ }
3607
+ var init_behaviors = __esm({
3608
+ "src/lib/behaviors.ts"() {
3609
+ "use strict";
3610
+ init_database();
3611
+ }
3612
+ });
3613
+
3614
+ // src/lib/skill-learning.ts
3615
+ var skill_learning_exports = {};
3616
+ __export(skill_learning_exports, {
3617
+ captureAndLearn: () => captureAndLearn,
3618
+ captureTrajectory: () => captureTrajectory,
3619
+ editDistance: () => editDistance,
3620
+ extractSkill: () => extractSkill,
3621
+ extractTrajectory: () => extractTrajectory,
3622
+ findSimilarTrajectories: () => findSimilarTrajectories,
3623
+ hashSignature: () => hashSignature,
3624
+ storeTrajectory: () => storeTrajectory,
3625
+ sweepTrajectories: () => sweepTrajectories
3626
+ });
3627
+ import crypto5 from "crypto";
3628
+ async function extractTrajectory(taskId, agentId) {
3629
+ const client = getClient();
3630
+ const result = await client.execute({
3631
+ sql: `SELECT tool_name, raw_text
3632
+ FROM memories
3633
+ WHERE task_id = ? AND agent_id = ?
3634
+ ORDER BY timestamp ASC`,
3635
+ args: [taskId, agentId]
3636
+ });
3637
+ if (result.rows.length === 0) return [];
3638
+ const rawTools = result.rows.map((r) => {
3639
+ const toolName = String(r.tool_name);
3640
+ if (toolName === "Bash") {
3641
+ const text = String(r.raw_text);
3642
+ const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
3643
+ return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
3644
+ }
3645
+ return toolName;
3646
+ });
3647
+ const signature = [];
3648
+ for (const tool of rawTools) {
3649
+ if (signature.length === 0 || signature[signature.length - 1] !== tool) {
3650
+ signature.push(tool);
3651
+ }
3652
+ }
3653
+ return signature;
3654
+ }
3655
+ function hashSignature(signature) {
3656
+ return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
3657
+ }
3658
+ async function storeTrajectory(opts) {
3659
+ const client = getClient();
3660
+ const id = crypto5.randomUUID();
3661
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3662
+ const signatureHash = hashSignature(opts.signature);
3663
+ await client.execute({
3664
+ sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
3665
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3666
+ args: [
3667
+ id,
3668
+ opts.taskId,
3669
+ opts.agentId,
3670
+ opts.projectName,
3671
+ opts.taskTitle,
3672
+ JSON.stringify(opts.signature),
3673
+ signatureHash,
3674
+ opts.signature.length,
3675
+ now
3676
+ ]
3677
+ });
3678
+ return id;
3679
+ }
3680
+ async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
3681
+ const client = getClient();
3682
+ const hash = hashSignature(signature);
3683
+ const result = await client.execute({
3684
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
3685
+ FROM trajectories
3686
+ WHERE signature_hash = ?
3687
+ ORDER BY created_at DESC
3688
+ LIMIT 20`,
3689
+ args: [hash]
3690
+ });
3691
+ const mapRow = (r) => ({
3692
+ id: String(r.id),
3693
+ taskId: String(r.task_id),
3694
+ agentId: String(r.agent_id),
3695
+ projectName: String(r.project_name),
3696
+ taskTitle: String(r.task_title),
3697
+ signature: JSON.parse(String(r.signature)),
3698
+ signatureHash: String(r.signature_hash),
3699
+ toolCount: Number(r.tool_count),
3700
+ skillId: r.skill_id ? String(r.skill_id) : null,
3701
+ createdAt: String(r.created_at)
3702
+ });
3703
+ const matches = result.rows.map(mapRow);
3704
+ if (matches.length >= threshold) return matches;
3705
+ const nearResult = await client.execute({
3706
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
3707
+ FROM trajectories
3708
+ WHERE tool_count BETWEEN ? AND ?
3709
+ AND signature_hash != ?
3710
+ ORDER BY created_at DESC
3711
+ LIMIT 50`,
3712
+ args: [
3713
+ Math.max(1, signature.length - 3),
3714
+ signature.length + 3,
3715
+ hash
3716
+ ]
3717
+ });
3718
+ for (const r of nearResult.rows) {
3719
+ const candidateSig = JSON.parse(String(r.signature));
3720
+ if (editDistance(signature, candidateSig) <= 2) {
3721
+ matches.push(mapRow(r));
3722
+ }
3723
+ }
3724
+ return matches;
3725
+ }
3726
+ async function captureTrajectory(opts) {
3727
+ const signature = await extractTrajectory(opts.taskId, opts.agentId);
3728
+ if (signature.length < 3) {
3729
+ return { trajectoryId: "", similarCount: 0, similar: [] };
3730
+ }
3731
+ const trajectoryId = await storeTrajectory({
3732
+ taskId: opts.taskId,
3733
+ agentId: opts.agentId,
3734
+ projectName: opts.projectName,
3735
+ taskTitle: opts.taskTitle,
3736
+ signature
3737
+ });
3738
+ const similar = await findSimilarTrajectories(
3739
+ signature,
3740
+ opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
3741
+ );
3742
+ return { trajectoryId, similarCount: similar.length, similar };
3743
+ }
3744
+ function buildExtractionPrompt(trajectories) {
3745
+ const items = trajectories.map((t, i) => {
3746
+ const sig = t.signature.join(" \u2192 ");
3747
+ return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
3748
+ Signature: ${sig}`;
3749
+ }).join("\n\n");
3750
+ return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
3751
+
3752
+ ${items}
3753
+
3754
+ Extract the reusable procedure. Format your response EXACTLY like this:
3755
+
3756
+ SKILL: {name \u2014 short, descriptive}
3757
+ TRIGGER: {when to use this \u2014 one sentence}
3758
+ STEPS:
3759
+ 1. ...
3760
+ 2. ...
3761
+ PITFALLS: {common mistakes to avoid}
3762
+
3763
+ Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
3764
+ }
3765
+ async function extractSkill(trajectories, model) {
3766
+ if (trajectories.length === 0) return null;
3767
+ const config = await loadConfig();
3768
+ const skillModel = model ?? config.skillModel;
3769
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
3770
+ const client = new Anthropic();
3771
+ const prompt = buildExtractionPrompt(trajectories);
3772
+ const response = await client.messages.create({
3773
+ model: skillModel,
3774
+ max_tokens: 500,
3775
+ messages: [{ role: "user", content: prompt }]
3776
+ });
3777
+ const textBlock = response.content.find((b) => b.type === "text");
3778
+ const skillText = textBlock?.text;
3779
+ if (!skillText) return null;
3780
+ const agentId = trajectories[0].agentId;
3781
+ const projectName = trajectories[0].projectName;
3782
+ const skillId = await storeBehavior({
3783
+ agentId,
3784
+ content: skillText,
3785
+ domain: "skill",
3786
+ projectName
3787
+ });
3788
+ const dbClient = getClient();
3789
+ for (const t of trajectories) {
3790
+ await dbClient.execute({
3791
+ sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
3792
+ args: [skillId, t.id]
3793
+ });
3794
+ }
3795
+ process.stderr.write(
3796
+ `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
3797
+ `
3798
+ );
3799
+ return skillId;
3800
+ }
3801
+ async function captureAndLearn(opts) {
3802
+ try {
3803
+ const config = await loadConfig();
3804
+ if (!config.skillLearning) return;
3805
+ const { trajectoryId, similarCount, similar } = await captureTrajectory({
3806
+ ...opts,
3807
+ skillThreshold: config.skillThreshold
3808
+ });
3809
+ if (!trajectoryId) return;
3810
+ if (similarCount >= config.skillThreshold) {
3811
+ const unprocessed = similar.filter((t) => !t.skillId);
3812
+ if (unprocessed.length >= config.skillThreshold) {
3813
+ extractSkill(unprocessed, config.skillModel).catch((err) => {
3814
+ process.stderr.write(
3815
+ `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
3816
+ `
3817
+ );
3818
+ });
3819
+ }
3820
+ }
3821
+ } catch (err) {
3822
+ process.stderr.write(
3823
+ `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
3824
+ `
3825
+ );
3826
+ }
3827
+ }
3828
+ async function sweepTrajectories(threshold, model) {
3829
+ const config = await loadConfig();
3830
+ if (!config.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
3831
+ const t = threshold ?? config.skillThreshold;
3832
+ const client = getClient();
3833
+ const result = await client.execute({
3834
+ sql: `SELECT signature_hash, COUNT(*) as cnt
3835
+ FROM trajectories
3836
+ WHERE skill_id IS NULL
3837
+ GROUP BY signature_hash
3838
+ HAVING cnt >= ?
3839
+ ORDER BY cnt DESC
3840
+ LIMIT 10`,
3841
+ args: [t]
3842
+ });
3843
+ let clustersProcessed = 0;
3844
+ let skillsExtracted = 0;
3845
+ for (const row of result.rows) {
3846
+ const hash = String(row.signature_hash);
3847
+ const trajResult = await client.execute({
3848
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
3849
+ FROM trajectories
3850
+ WHERE signature_hash = ? AND skill_id IS NULL
3851
+ ORDER BY created_at DESC
3852
+ LIMIT 10`,
3853
+ args: [hash]
3854
+ });
3855
+ const trajectories = trajResult.rows.map((r) => ({
3856
+ id: String(r.id),
3857
+ taskId: String(r.task_id),
3858
+ agentId: String(r.agent_id),
3859
+ projectName: String(r.project_name),
3860
+ taskTitle: String(r.task_title),
3861
+ signature: JSON.parse(String(r.signature)),
3862
+ signatureHash: String(r.signature_hash),
3863
+ toolCount: Number(r.tool_count),
3864
+ skillId: null,
3865
+ createdAt: String(r.created_at)
3866
+ }));
3867
+ if (trajectories.length >= t) {
3868
+ clustersProcessed++;
3869
+ const skillId = await extractSkill(trajectories, model ?? config.skillModel);
3870
+ if (skillId) skillsExtracted++;
3871
+ }
3872
+ }
3873
+ return { clustersProcessed, skillsExtracted };
3874
+ }
3875
+ function editDistance(a, b) {
3876
+ const m = a.length;
3877
+ const n = b.length;
3878
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
3879
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
3880
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
3881
+ for (let i = 1; i <= m; i++) {
3882
+ for (let j = 1; j <= n; j++) {
3883
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3884
+ dp[i][j] = Math.min(
3885
+ dp[i - 1][j] + 1,
3886
+ dp[i][j - 1] + 1,
3887
+ dp[i - 1][j - 1] + cost
3888
+ );
3889
+ }
3890
+ }
3891
+ return dp[m][n];
3892
+ }
3893
+ var DEFAULT_SKILL_THRESHOLD;
3894
+ var init_skill_learning = __esm({
3895
+ "src/lib/skill-learning.ts"() {
3896
+ "use strict";
3897
+ init_database();
3898
+ init_behaviors();
3899
+ init_config();
3900
+ DEFAULT_SKILL_THRESHOLD = 3;
3901
+ }
3902
+ });
3903
+
3904
+ // src/lib/tasks.ts
3905
+ var tasks_exports = {};
3906
+ __export(tasks_exports, {
3907
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
3908
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
3909
+ countPendingReviews: () => countPendingReviews,
3910
+ createTask: () => createTask,
3911
+ createTaskCore: () => createTaskCore,
3912
+ deleteTask: () => deleteTask,
3913
+ deleteTaskCore: () => deleteTaskCore,
3914
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
3915
+ ensureGitignoreExe: () => ensureGitignoreExe,
3916
+ getReviewChecklist: () => getReviewChecklist,
3917
+ listPendingReviews: () => listPendingReviews,
3918
+ listTasks: () => listTasks,
3919
+ resolveTask: () => resolveTask,
3920
+ slugify: () => slugify,
3921
+ updateTask: () => updateTask,
3922
+ updateTaskStatus: () => updateTaskStatus,
3923
+ writeCheckpoint: () => writeCheckpoint
3924
+ });
3925
+ import path15 from "path";
3926
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync6 } from "fs";
3927
+ async function createTask(input2) {
3928
+ const result = await createTaskCore(input2);
3929
+ if (!input2.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
3930
+ dispatchTaskToEmployee({
3931
+ assignedTo: input2.assignedTo,
3932
+ title: input2.title,
3933
+ priority: input2.priority,
3934
+ taskFile: result.taskFile,
3935
+ initialStatus: result.status,
3936
+ projectName: input2.projectName
3937
+ });
3938
+ }
3939
+ return result;
3940
+ }
3941
+ async function updateTask(input2) {
3942
+ const { row, taskFile, now, taskId } = await updateTaskStatus(input2);
3943
+ try {
3944
+ const agent = String(row.assigned_to);
3945
+ const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
3946
+ const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
3947
+ if (input2.status === "in_progress") {
3948
+ mkdirSync6(cacheDir, { recursive: true });
3949
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3950
+ } else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled") {
3951
+ try {
3952
+ unlinkSync6(cachePath);
3953
+ } catch {
3954
+ }
3955
+ }
3956
+ } catch {
3957
+ }
3958
+ if (input2.status === "done") {
3959
+ await cleanupReviewFile(row, taskFile, input2.baseDir);
3960
+ }
3961
+ if (input2.status === "done" || input2.status === "cancelled") {
3962
+ try {
3963
+ const client = getClient();
3964
+ const taskTitle = String(row.title);
3965
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
3966
+ await client.execute({
3967
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3968
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
3969
+ args: [now, `%left '${escaped}' as in\\_progress%`]
3970
+ });
3971
+ } catch {
3972
+ }
3973
+ const assignedAgent = String(row.assigned_to);
3974
+ if (!isCoordinatorName(assignedAgent)) {
3975
+ try {
3976
+ const draftClient = getClient();
3977
+ if (input2.status === "done") {
3978
+ await draftClient.execute({
3979
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3980
+ args: [assignedAgent]
3981
+ });
3982
+ } else if (input2.status === "cancelled") {
3983
+ await draftClient.execute({
3984
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
3985
+ args: [assignedAgent]
3986
+ });
3987
+ }
3988
+ } catch {
3989
+ }
3990
+ }
3991
+ try {
3992
+ const client = getClient();
3993
+ const cascaded = await client.execute({
3994
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3995
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
3996
+ args: [now, taskId]
3997
+ });
3998
+ if (cascaded.rowsAffected > 0) {
3999
+ process.stderr.write(
4000
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
4001
+ `
4002
+ );
4003
+ }
4004
+ } catch {
4005
+ }
4006
+ }
4007
+ const isTerminal = input2.status === "done" || input2.status === "needs_review";
4008
+ if (isTerminal) {
4009
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
4010
+ if (!isCoordinator) {
4011
+ notifyTaskDone();
4012
+ }
4013
+ await markTaskNotificationsRead(taskFile);
4014
+ if (input2.status === "done") {
4015
+ try {
4016
+ await cascadeUnblock(taskId, input2.baseDir, now);
4017
+ } catch {
4018
+ }
4019
+ orgBus.emit({
4020
+ type: "task_completed",
4021
+ taskId,
4022
+ employee: String(row.assigned_to),
4023
+ result: input2.result ?? "",
4024
+ timestamp: now
4025
+ });
4026
+ if (row.parent_task_id) {
4027
+ try {
4028
+ await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
4029
+ } catch {
4030
+ }
4031
+ }
4032
+ }
4033
+ }
4034
+ if (input2.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4035
+ Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4036
+ ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4037
+ taskId,
4038
+ agentId: String(row.assigned_to),
4039
+ projectName: String(row.project_name),
4040
+ taskTitle: String(row.title)
4041
+ })
4042
+ ).catch((err) => {
4043
+ process.stderr.write(
4044
+ `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
4045
+ `
4046
+ );
4047
+ });
4048
+ }
4049
+ let nextTask;
4050
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
4051
+ try {
4052
+ nextTask = await findNextTask(String(row.assigned_to));
4053
+ } catch {
4054
+ }
4055
+ }
4056
+ return {
4057
+ id: String(row.id),
4058
+ title: String(row.title),
4059
+ assignedTo: String(row.assigned_to),
4060
+ assignedBy: String(row.assigned_by),
4061
+ projectName: String(row.project_name),
4062
+ priority: String(row.priority),
4063
+ status: input2.status,
4064
+ taskFile,
4065
+ createdAt: String(row.created_at),
4066
+ updatedAt: now,
4067
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
4068
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
4069
+ tokensUsed: Number(row.tokens_used ?? 0),
4070
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
4071
+ nextTask
4072
+ };
4073
+ }
4074
+ async function deleteTask(taskId, baseDir) {
4075
+ const client = getClient();
4076
+ const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
4077
+ const coordinatorName = getCoordinatorName();
4078
+ const reviewer = assignedBy || coordinatorName;
4079
+ const reviewSlug = `review-${assignedTo}-${taskSlug}`;
4080
+ const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
4081
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
4082
+ await client.execute({
4083
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
4084
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
4085
+ });
4086
+ await markAsReadByTaskFile(taskFile);
4087
+ await markAsReadByTaskFile(reviewFile);
4088
+ }
4089
+ var init_tasks = __esm({
4090
+ "src/lib/tasks.ts"() {
4091
+ "use strict";
4092
+ init_database();
4093
+ init_config();
4094
+ init_notifications();
4095
+ init_state_bus();
4096
+ init_employees();
4097
+ init_tasks_crud();
4098
+ init_tasks_review();
4099
+ init_tasks_crud();
4100
+ init_tasks_chain();
4101
+ init_tasks_review();
4102
+ init_tasks_notify();
4103
+ }
4104
+ });
4105
+
4106
+ // src/lib/capacity-monitor.ts
4107
+ var capacity_monitor_exports = {};
4108
+ __export(capacity_monitor_exports, {
4109
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
4110
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
4111
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
4112
+ confirmCapacityKill: () => confirmCapacityKill,
4113
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
4114
+ extractContextPercent: () => extractContextPercent,
4115
+ isAtCapacity: () => isAtCapacity,
4116
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
4117
+ pollCapacityDead: () => pollCapacityDead
4118
+ });
4119
+ function resumeTaskTitle(agentId) {
4120
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
4121
+ }
4122
+ function buildResumeContext(agentId, openTasks) {
4123
+ const taskList = openTasks.map(
4124
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
4125
+ ).join("\n");
4126
+ return [
4127
+ "## Context",
4128
+ "",
4129
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
4130
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
4131
+ "",
4132
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
4133
+ "",
4134
+ taskList,
4135
+ "",
4136
+ "Read each task file and chain through them. Build and commit after each one."
4137
+ ].join("\n");
4138
+ }
4139
+ function filterPaneContent(paneOutput) {
4140
+ return paneOutput.split("\n").filter((line) => {
4141
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
4142
+ for (const marker of CONTENT_LINE_MARKERS) {
4143
+ if (line.includes(marker)) return false;
4144
+ }
4145
+ for (const re of SOURCE_CODE_MARKERS) {
4146
+ if (re.test(line)) return false;
4147
+ }
4148
+ return true;
4149
+ }).join("\n");
4150
+ }
4151
+ function extractContextPercent(paneOutput) {
4152
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
4153
+ if (!match) return null;
4154
+ const parsed = Number.parseInt(match[2], 10);
4155
+ return Number.isFinite(parsed) ? parsed : null;
4156
+ }
4157
+ function isAtCapacity(paneOutput) {
4158
+ const filtered = filterPaneContent(paneOutput);
4159
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
4160
+ }
4161
+ function confirmCapacityKill(agentId, now = Date.now()) {
4162
+ const pendingSince = _pendingCapacityKill.get(agentId);
4163
+ if (pendingSince === void 0) {
4164
+ _pendingCapacityKill.set(agentId, now);
4165
+ return false;
4166
+ }
4167
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
4168
+ _pendingCapacityKill.set(agentId, now);
4169
+ return false;
4170
+ }
4171
+ _pendingCapacityKill.delete(agentId);
4172
+ return true;
4173
+ }
4174
+ function _resetPendingCapacityKills() {
4175
+ _pendingCapacityKill.clear();
4176
+ }
4177
+ function _resetLastRelaunchCache() {
4178
+ _lastRelaunch.clear();
4179
+ }
4180
+ async function lastResumeCreatedAtMs(agentId) {
4181
+ const client = getClient();
4182
+ const cmScope = sessionScopeFilter(null);
4183
+ const result = await client.execute({
4184
+ sql: `SELECT MAX(created_at) AS last_created_at
4185
+ FROM tasks
4186
+ WHERE assigned_to = ? AND title LIKE ?${cmScope.sql}`,
4187
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`, ...cmScope.args]
4188
+ });
4189
+ const raw = result.rows[0]?.last_created_at;
4190
+ if (raw === null || raw === void 0) return null;
4191
+ const parsed = Date.parse(String(raw));
4192
+ return Number.isNaN(parsed) ? null : parsed;
4193
+ }
4194
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
4195
+ const cached = _lastRelaunch.get(agentId);
4196
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
4197
+ const persisted = await lastResumeCreatedAtMs(agentId);
4198
+ if (persisted === null) return false;
4199
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
4200
+ _lastRelaunch.set(agentId, persisted);
4201
+ return true;
4202
+ }
4203
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
4204
+ const client = getClient();
4205
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4206
+ const context = buildResumeContext(agentId, openTasks);
4207
+ const rdScope = sessionScopeFilter(null);
4208
+ const existing = await client.execute({
4209
+ sql: `SELECT id FROM tasks
4210
+ WHERE assigned_to = ?
4211
+ AND title LIKE ?
4212
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})${rdScope.sql}
4213
+ ORDER BY created_at DESC
4214
+ LIMIT 1`,
4215
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES, ...rdScope.args]
4216
+ });
4217
+ if (existing.rows.length > 0) {
4218
+ const taskId = String(existing.rows[0].id);
4219
+ await client.execute({
4220
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
4221
+ args: [context, now, taskId]
4222
+ });
4223
+ return { created: false, taskId };
4224
+ }
4225
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
4226
+ const task = await createTask2({
4227
+ title: resumeTaskTitle(agentId),
4228
+ assignedTo: agentId,
4229
+ assignedBy: "system",
4230
+ projectName: projectDir.split("/").pop() ?? "unknown",
4231
+ priority: "p0",
4232
+ context,
4233
+ baseDir: projectDir
4234
+ });
4235
+ return { created: true, taskId: task.id };
4236
+ }
4237
+ async function pollCapacityDead() {
4238
+ const transport = getTransport();
4239
+ const relaunched = [];
4240
+ const registered = listSessions().filter(
4241
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
4242
+ );
4243
+ if (registered.length === 0) return [];
4244
+ let liveSessions;
4245
+ try {
4246
+ liveSessions = transport.listSessions();
4247
+ } catch {
4248
+ return [];
4249
+ }
4250
+ for (const entry of registered) {
4251
+ const { windowName, agentId, projectDir } = entry;
4252
+ if (!liveSessions.includes(windowName)) continue;
4253
+ if (await isWithinRelaunchCooldown(agentId)) continue;
4254
+ let pane;
4255
+ try {
4256
+ pane = transport.capturePane(windowName, 15);
4257
+ } catch {
4258
+ continue;
4259
+ }
4260
+ if (!isAtCapacity(pane)) continue;
4261
+ const ctxPct = extractContextPercent(pane);
4262
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
4263
+ process.stderr.write(
4264
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
4265
+ `
4266
+ );
4267
+ continue;
4268
+ }
4269
+ if (!confirmCapacityKill(agentId)) {
4270
+ process.stderr.write(
4271
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
4272
+ `
4273
+ );
4274
+ continue;
4275
+ }
4276
+ const verify = await verifyPaneAtCapacity(windowName);
4277
+ if (!verify.atCapacity) {
4278
+ process.stderr.write(
4279
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
4280
+ `
4281
+ );
4282
+ void recordSessionKill({
4283
+ sessionName: windowName,
4284
+ agentId,
4285
+ reason: "capacity_false_positive_blocked"
4286
+ });
4287
+ continue;
4288
+ }
4289
+ process.stderr.write(
4290
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
4291
+ `
4292
+ );
4293
+ try {
4294
+ transport.kill(windowName);
4295
+ void recordSessionKill({
4296
+ sessionName: windowName,
4297
+ agentId,
4298
+ reason: "capacity"
4299
+ });
4300
+ const client = getClient();
4301
+ const rlScope = sessionScopeFilter(null);
4302
+ const openTasks = await client.execute({
4303
+ sql: `SELECT id, title, priority, task_file, status
4304
+ FROM tasks
4305
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')${rlScope.sql}
4306
+ ORDER BY
4307
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
4308
+ created_at ASC
4309
+ LIMIT 10`,
4310
+ args: [agentId, ...rlScope.args]
4311
+ });
4312
+ if (openTasks.rows.length === 0) {
4313
+ process.stderr.write(
4314
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
4315
+ `
4316
+ );
4317
+ continue;
4318
+ }
4319
+ const { created } = await createOrRefreshResumeTask(
4320
+ agentId,
4321
+ projectDir,
4322
+ openTasks.rows
4323
+ );
4324
+ if (created) {
4325
+ await writeNotification({
4326
+ agentId: "system",
4327
+ agentRole: "daemon",
4328
+ event: "capacity_relaunch",
4329
+ project: projectDir.split("/").pop() ?? "unknown",
4330
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
4331
+ });
4332
+ }
4333
+ _lastRelaunch.set(agentId, Date.now());
4334
+ if (created) relaunched.push(agentId);
4335
+ } catch (err) {
4336
+ process.stderr.write(
4337
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
4338
+ `
4339
+ );
4340
+ }
4341
+ }
4342
+ return relaunched;
4343
+ }
4344
+ var CAPACITY_PATTERNS, CONTENT_LINE_PREFIX, CONTENT_LINE_MARKERS, SOURCE_CODE_MARKERS, RELAUNCH_COOLDOWN_MS, _lastRelaunch, RESUME_TITLE_PREFIX, RESUME_TITLE_LIKE_PATTERN, RESUME_ACTIVE_STATUSES, CONFIRMATION_WINDOW_MS, _pendingCapacityKill, CC_CONTEXT_BAR_RE, CTX_FLOOR_PERCENT;
4345
+ var init_capacity_monitor = __esm({
4346
+ "src/lib/capacity-monitor.ts"() {
4347
+ "use strict";
4348
+ init_session_registry();
4349
+ init_transport();
4350
+ init_notifications();
4351
+ init_database();
4352
+ init_session_kill_telemetry();
4353
+ init_tmux_routing();
4354
+ init_task_scope();
4355
+ init_employees();
4356
+ CAPACITY_PATTERNS = [
4357
+ /conversation is too long/i,
4358
+ /maximum context length/i,
4359
+ /context window.*(?:limit|exceed|full)/i,
4360
+ /reached.*(?:token|context).*limit/i
4361
+ ];
4362
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
4363
+ CONTENT_LINE_MARKERS = [
4364
+ "RESUME:",
4365
+ "intercom",
4366
+ "capacity-monitor",
4367
+ "CAPACITY_PATTERNS",
4368
+ "isAtCapacity",
4369
+ "CONTENT_LINE_MARKERS",
4370
+ "pollCapacityDead",
4371
+ "confirmCapacityKill",
4372
+ "session_kills",
4373
+ "capacity-monitor.test"
4374
+ ];
4375
+ SOURCE_CODE_MARKERS = [
4376
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
4377
+ /(?:maximum context length|conversation is too long).*["'`/]/i
4378
+ ];
4379
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
4380
+ _lastRelaunch = /* @__PURE__ */ new Map();
4381
+ RESUME_TITLE_PREFIX = "RESUME:";
4382
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
4383
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
4384
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
4385
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
4386
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
4387
+ CTX_FLOOR_PERCENT = 50;
4388
+ }
4389
+ });
4390
+
4391
+ // src/lib/tmux-routing.ts
4392
+ var tmux_routing_exports = {};
4393
+ __export(tmux_routing_exports, {
4394
+ acquireSpawnLock: () => acquireSpawnLock2,
4395
+ employeeSessionName: () => employeeSessionName,
4396
+ ensureEmployee: () => ensureEmployee,
4397
+ extractRootExe: () => extractRootExe,
4398
+ findFreeInstance: () => findFreeInstance,
4399
+ getDispatchedBy: () => getDispatchedBy,
4400
+ getMySession: () => getMySession,
4401
+ getParentExe: () => getParentExe,
4402
+ getSessionState: () => getSessionState,
4403
+ isEmployeeAlive: () => isEmployeeAlive,
4404
+ isExeSession: () => isExeSession,
4405
+ isSessionBusy: () => isSessionBusy,
4406
+ notifyParentExe: () => notifyParentExe,
4407
+ parseParentExe: () => parseParentExe,
4408
+ registerParentExe: () => registerParentExe,
4409
+ releaseSpawnLock: () => releaseSpawnLock2,
4410
+ resolveExeSession: () => resolveExeSession,
4411
+ sendIntercom: () => sendIntercom,
4412
+ spawnEmployee: () => spawnEmployee,
4413
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
4414
+ });
4415
+ import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
4416
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, existsSync as existsSync12, appendFileSync } from "fs";
4417
+ import path16 from "path";
4418
+ import os7 from "os";
4419
+ import { fileURLToPath as fileURLToPath2 } from "url";
4420
+ import { unlinkSync as unlinkSync7 } from "fs";
4421
+ function spawnLockPath(sessionName) {
4422
+ return path16.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4423
+ }
4424
+ function isProcessAlive(pid) {
4425
+ try {
4426
+ process.kill(pid, 0);
4427
+ return true;
4428
+ } catch {
4429
+ return false;
4430
+ }
4431
+ }
4432
+ function acquireSpawnLock2(sessionName) {
4433
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
4434
+ mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
4435
+ }
4436
+ const lockFile = spawnLockPath(sessionName);
4437
+ if (existsSync12(lockFile)) {
4438
+ try {
4439
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
4440
+ const age = Date.now() - lock.timestamp;
4441
+ if (isProcessAlive(lock.pid) && age < 6e4) {
4442
+ return false;
4443
+ }
4444
+ } catch {
4445
+ }
4446
+ }
4447
+ writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4448
+ return true;
4449
+ }
4450
+ function releaseSpawnLock2(sessionName) {
4451
+ try {
4452
+ unlinkSync7(spawnLockPath(sessionName));
4453
+ } catch {
4454
+ }
4455
+ }
4456
+ function resolveBehaviorsExporterScript() {
4457
+ try {
4458
+ const thisFile = fileURLToPath2(import.meta.url);
4459
+ const scriptPath = path16.join(
4460
+ path16.dirname(thisFile),
4461
+ "..",
4462
+ "bin",
4463
+ "exe-export-behaviors.js"
4464
+ );
4465
+ return existsSync12(scriptPath) ? scriptPath : null;
4466
+ } catch {
4467
+ return null;
4468
+ }
4469
+ }
4470
+ function exportBehaviorsSync(agentId, projectName, sessionKey) {
4471
+ const script = resolveBehaviorsExporterScript();
4472
+ if (!script) return null;
4473
+ try {
4474
+ const output = execFileSync2(
4475
+ process.execPath,
4476
+ [script, agentId, projectName, sessionKey],
4477
+ { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
4478
+ ).trim();
4479
+ return output.length > 0 ? output : null;
4480
+ } catch (err) {
4481
+ process.stderr.write(
4482
+ `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
4483
+ `
4484
+ );
4485
+ return null;
4486
+ }
4487
+ }
1449
4488
  function getMySession() {
1450
4489
  return getTransport().getMySession();
1451
4490
  }
4491
+ function isRootSession(name) {
4492
+ return name.length > 0 && !name.includes("-");
4493
+ }
4494
+ function employeeSessionName(employee, exeSession, instance) {
4495
+ if (!isRootSession(exeSession)) {
4496
+ const root = extractRootExe(exeSession);
4497
+ if (root) {
4498
+ process.stderr.write(
4499
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root session, using "${root}" instead
4500
+ `
4501
+ );
4502
+ exeSession = root;
4503
+ } else {
4504
+ throw new Error(
4505
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
4506
+ );
4507
+ }
4508
+ }
4509
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
4510
+ const name = `${employee}${suffix}-${exeSession}`;
4511
+ if (!VALID_SESSION_NAME.test(name)) {
4512
+ throw new Error(
4513
+ `Invalid session name "${name}" \u2014 must match {agent}-{rootSession} or {agent}{instance}-{rootSession}`
4514
+ );
4515
+ }
4516
+ return name;
4517
+ }
4518
+ function parseParentExe(sessionName, agentId) {
4519
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4520
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
4521
+ const match = sessionName.match(regex);
4522
+ return match?.[1] ?? null;
4523
+ }
1452
4524
  function extractRootExe(name) {
1453
4525
  if (!name) return null;
1454
4526
  if (!name.includes("-")) return name;
1455
4527
  const parts = name.split("-").filter(Boolean);
1456
4528
  return parts.length > 0 ? parts[parts.length - 1] : null;
1457
4529
  }
4530
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
4531
+ if (!existsSync12(SESSION_CACHE)) {
4532
+ mkdirSync7(SESSION_CACHE, { recursive: true });
4533
+ }
4534
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
4535
+ const filePath = path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4536
+ writeFileSync8(filePath, JSON.stringify({
4537
+ parentExe: rootExe,
4538
+ dispatchedBy: dispatchedBy || rootExe,
4539
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4540
+ }));
4541
+ }
1458
4542
  function getParentExe(sessionKey) {
1459
4543
  try {
1460
- const data = JSON.parse(readFileSync7(path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4544
+ const data = JSON.parse(readFileSync12(path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1461
4545
  return data.parentExe || null;
1462
4546
  } catch {
1463
4547
  return null;
1464
4548
  }
1465
4549
  }
4550
+ function getDispatchedBy(sessionKey) {
4551
+ try {
4552
+ const data = JSON.parse(readFileSync12(
4553
+ path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4554
+ "utf8"
4555
+ ));
4556
+ return data.dispatchedBy ?? data.parentExe ?? null;
4557
+ } catch {
4558
+ return null;
4559
+ }
4560
+ }
1466
4561
  function resolveExeSession() {
1467
4562
  const mySession = getMySession();
1468
4563
  if (!mySession) return null;
@@ -1476,7 +4571,523 @@ function resolveExeSession() {
1476
4571
  }
1477
4572
  return extractRootExe(mySession) ?? mySession;
1478
4573
  }
1479
- var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
4574
+ function isEmployeeAlive(sessionName) {
4575
+ return getTransport().isAlive(sessionName);
4576
+ }
4577
+ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4578
+ const base = employeeSessionName(employeeName, exeSession);
4579
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
4580
+ for (let i = 2; i <= maxInstances; i++) {
4581
+ const candidate = employeeSessionName(employeeName, exeSession, i);
4582
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
4583
+ }
4584
+ return null;
4585
+ }
4586
+ async function verifyPaneAtCapacity(sessionName) {
4587
+ const transport = getTransport();
4588
+ if (!transport.isAlive(sessionName)) {
4589
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
4590
+ }
4591
+ let pane;
4592
+ try {
4593
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
4594
+ } catch (err) {
4595
+ return {
4596
+ atCapacity: false,
4597
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
4598
+ };
4599
+ }
4600
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
4601
+ if (!isAtCapacity2(pane)) {
4602
+ return {
4603
+ atCapacity: false,
4604
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
4605
+ };
4606
+ }
4607
+ return {
4608
+ atCapacity: true,
4609
+ reason: "capacity banner matched in recent pane output"
4610
+ };
4611
+ }
4612
+ function readDebounceState() {
4613
+ try {
4614
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
4615
+ const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
4616
+ const state = {};
4617
+ for (const [key, val] of Object.entries(raw)) {
4618
+ if (typeof val === "number") {
4619
+ state[key] = { lastSent: val, pending: 0 };
4620
+ } else if (val && typeof val === "object" && "lastSent" in val) {
4621
+ state[key] = val;
4622
+ }
4623
+ }
4624
+ return state;
4625
+ } catch {
4626
+ return {};
4627
+ }
4628
+ }
4629
+ function writeDebounceState(state) {
4630
+ try {
4631
+ if (!existsSync12(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
4632
+ writeFileSync8(DEBOUNCE_FILE, JSON.stringify(state));
4633
+ } catch {
4634
+ }
4635
+ }
4636
+ function isDebounced(targetSession) {
4637
+ const state = readDebounceState();
4638
+ const entry = state[targetSession];
4639
+ const lastSent = entry?.lastSent ?? 0;
4640
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
4641
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
4642
+ state[targetSession].pending++;
4643
+ writeDebounceState(state);
4644
+ return true;
4645
+ }
4646
+ return false;
4647
+ }
4648
+ function recordDebounce(targetSession) {
4649
+ const state = readDebounceState();
4650
+ const batched = state[targetSession]?.pending ?? 0;
4651
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
4652
+ const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4653
+ for (const key of Object.keys(state)) {
4654
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
4655
+ }
4656
+ writeDebounceState(state);
4657
+ return batched;
4658
+ }
4659
+ function logIntercom(msg) {
4660
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
4661
+ `;
4662
+ process.stderr.write(`[intercom] ${msg}
4663
+ `);
4664
+ try {
4665
+ appendFileSync(INTERCOM_LOG2, line);
4666
+ } catch {
4667
+ }
4668
+ }
4669
+ function getSessionState(sessionName) {
4670
+ const transport = getTransport();
4671
+ if (!transport.isAlive(sessionName)) return "offline";
4672
+ try {
4673
+ const pane = transport.capturePane(sessionName, 5);
4674
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4675
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4676
+ return "no_claude";
4677
+ }
4678
+ }
4679
+ if (/Running…/.test(pane)) return "tool";
4680
+ if (BUSY_PATTERN.test(pane)) return "thinking";
4681
+ return "idle";
4682
+ } catch {
4683
+ return "offline";
4684
+ }
4685
+ }
4686
+ function isSessionBusy(sessionName) {
4687
+ const state = getSessionState(sessionName);
4688
+ return state === "thinking" || state === "tool";
4689
+ }
4690
+ function isExeSession(sessionName) {
4691
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
4692
+ const coordinatorName = getCoordinatorName();
4693
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
4694
+ }
4695
+ function sendIntercom(targetSession) {
4696
+ const transport = getTransport();
4697
+ if (isExeSession(targetSession)) {
4698
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
4699
+ return "skipped_exe";
4700
+ }
4701
+ if (isDebounced(targetSession)) {
4702
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
4703
+ return "debounced";
4704
+ }
4705
+ try {
4706
+ const sessions = transport.listSessions();
4707
+ if (!sessions.includes(targetSession)) {
4708
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4709
+ return "failed";
4710
+ }
4711
+ const sessionState = getSessionState(targetSession);
4712
+ if (sessionState === "no_claude") {
4713
+ queueIntercom(targetSession, "claude not running in session");
4714
+ const batched2 = recordDebounce(targetSession);
4715
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4716
+ return "queued";
4717
+ }
4718
+ if (sessionState === "thinking" || sessionState === "tool") {
4719
+ queueIntercom(targetSession, "session busy at send time");
4720
+ const batched2 = recordDebounce(targetSession);
4721
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4722
+ return "queued";
4723
+ }
4724
+ if (transport.isPaneInCopyMode(targetSession)) {
4725
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4726
+ transport.sendKeys(targetSession, "q");
4727
+ }
4728
+ transport.sendKeys(targetSession, "/exe-intercom");
4729
+ const batched = recordDebounce(targetSession);
4730
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
4731
+ return "delivered";
4732
+ } catch {
4733
+ logIntercom(`FAIL \u2192 ${targetSession}`);
4734
+ return "failed";
4735
+ }
4736
+ }
4737
+ function notifyParentExe(sessionKey) {
4738
+ const target = getDispatchedBy(sessionKey);
4739
+ if (!target) {
4740
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
4741
+ `);
4742
+ return false;
4743
+ }
4744
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4745
+ `);
4746
+ const result = sendIntercom(target);
4747
+ if (result === "failed") {
4748
+ const rootExe = resolveExeSession();
4749
+ if (rootExe && rootExe !== target) {
4750
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
4751
+ `);
4752
+ const fallback = sendIntercom(rootExe);
4753
+ return fallback !== "failed";
4754
+ }
4755
+ return false;
4756
+ }
4757
+ return true;
4758
+ }
4759
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4760
+ if (isCoordinatorName(employeeName)) {
4761
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
4762
+ }
4763
+ try {
4764
+ assertEmployeeLimitSync();
4765
+ } catch (err) {
4766
+ if (err instanceof PlanLimitError) {
4767
+ return { status: "failed", sessionName: "", error: err.message };
4768
+ }
4769
+ }
4770
+ if (employeeName.includes("-")) {
4771
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
4772
+ return {
4773
+ status: "failed",
4774
+ sessionName: "",
4775
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
4776
+ };
4777
+ }
4778
+ if (!isRootSession(exeSession)) {
4779
+ const root = extractRootExe(exeSession);
4780
+ if (root) {
4781
+ process.stderr.write(
4782
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root session). Auto-correcting to "${root}".
4783
+ `
4784
+ );
4785
+ exeSession = root;
4786
+ } else {
4787
+ return {
4788
+ status: "failed",
4789
+ sessionName: "",
4790
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
4791
+ };
4792
+ }
4793
+ }
4794
+ let effectiveInstance = opts?.instance;
4795
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
4796
+ const free = findFreeInstance(
4797
+ employeeName,
4798
+ exeSession,
4799
+ opts.maxAutoInstances ?? 10
4800
+ );
4801
+ if (free === null) {
4802
+ return {
4803
+ status: "failed",
4804
+ sessionName: employeeSessionName(employeeName, exeSession),
4805
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4806
+ };
4807
+ }
4808
+ effectiveInstance = free === 0 ? void 0 : free;
4809
+ }
4810
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
4811
+ if (isEmployeeAlive(sessionName)) {
4812
+ const result2 = sendIntercom(sessionName);
4813
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4814
+ return { status: "intercom_sent", sessionName };
4815
+ }
4816
+ if (result2 === "delivered") {
4817
+ return { status: "intercom_unprocessed", sessionName };
4818
+ }
4819
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
4820
+ }
4821
+ const spawnOpts = { ...opts, instance: effectiveInstance };
4822
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
4823
+ if (result.error) {
4824
+ return { status: "failed", sessionName, error: result.error };
4825
+ }
4826
+ return { status: "spawned", sessionName };
4827
+ }
4828
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4829
+ const transport = getTransport();
4830
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4831
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4832
+ const logDir = path16.join(os7.homedir(), ".exe-os", "session-logs");
4833
+ const logFile = path16.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4834
+ if (!existsSync12(logDir)) {
4835
+ mkdirSync7(logDir, { recursive: true });
4836
+ }
4837
+ transport.kill(sessionName);
4838
+ let cleanupSuffix = "";
4839
+ try {
4840
+ const thisFile = fileURLToPath2(import.meta.url);
4841
+ const cleanupScript = path16.join(path16.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4842
+ if (existsSync12(cleanupScript)) {
4843
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4844
+ }
4845
+ } catch {
4846
+ }
4847
+ try {
4848
+ const claudeJsonPath = path16.join(os7.homedir(), ".claude.json");
4849
+ let claudeJson = {};
4850
+ try {
4851
+ claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
4852
+ } catch {
4853
+ }
4854
+ if (!claudeJson.projects) claudeJson.projects = {};
4855
+ const projects = claudeJson.projects;
4856
+ const trustDir = opts?.cwd ?? projectDir;
4857
+ if (!projects[trustDir]) projects[trustDir] = {};
4858
+ projects[trustDir].hasTrustDialogAccepted = true;
4859
+ writeFileSync8(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4860
+ } catch {
4861
+ }
4862
+ try {
4863
+ const settingsDir = path16.join(os7.homedir(), ".claude", "projects");
4864
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4865
+ const projSettingsDir = path16.join(settingsDir, normalizedKey);
4866
+ const settingsPath = path16.join(projSettingsDir, "settings.json");
4867
+ let settings = {};
4868
+ try {
4869
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
4870
+ } catch {
4871
+ }
4872
+ const perms = settings.permissions ?? {};
4873
+ const allow = perms.allow ?? [];
4874
+ const toolNames = [
4875
+ "recall_my_memory",
4876
+ "store_memory",
4877
+ "create_task",
4878
+ "update_task",
4879
+ "list_tasks",
4880
+ "get_task",
4881
+ "ask_team_memory",
4882
+ "store_behavior",
4883
+ "get_identity",
4884
+ "send_message"
4885
+ ];
4886
+ const requiredTools = expandDualPrefixTools(toolNames);
4887
+ let changed = false;
4888
+ for (const tool of requiredTools) {
4889
+ if (!allow.includes(tool)) {
4890
+ allow.push(tool);
4891
+ changed = true;
4892
+ }
4893
+ }
4894
+ if (changed) {
4895
+ perms.allow = allow;
4896
+ settings.permissions = perms;
4897
+ mkdirSync7(projSettingsDir, { recursive: true });
4898
+ writeFileSync8(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4899
+ }
4900
+ } catch {
4901
+ }
4902
+ const spawnCwd = opts?.cwd ?? projectDir;
4903
+ const useExeAgent = !!(opts?.model && opts?.provider);
4904
+ const agentRtConfig = getAgentRuntime(employeeName);
4905
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
4906
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
4907
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
4908
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4909
+ let identityFlag = "";
4910
+ let behaviorsFlag = "";
4911
+ let legacyFallbackWarned = false;
4912
+ if (!useExeAgent && !useBinSymlink) {
4913
+ const identityPath = path16.join(
4914
+ os7.homedir(),
4915
+ ".exe-os",
4916
+ "identity",
4917
+ `${employeeName}.md`
4918
+ );
4919
+ _resetCcAgentSupportCache();
4920
+ const hasAgentFlag = claudeSupportsAgentFlag();
4921
+ if (hasAgentFlag) {
4922
+ identityFlag = ` --agent ${employeeName}`;
4923
+ } else if (existsSync12(identityPath)) {
4924
+ identityFlag = ` --append-system-prompt-file ${identityPath}`;
4925
+ legacyFallbackWarned = true;
4926
+ }
4927
+ const behaviorsFile = exportBehaviorsSync(
4928
+ employeeName,
4929
+ path16.basename(spawnCwd),
4930
+ sessionName
4931
+ );
4932
+ if (behaviorsFile) {
4933
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
4934
+ }
4935
+ }
4936
+ if (legacyFallbackWarned) {
4937
+ process.stderr.write(
4938
+ `[tmux-routing] claude --agent not supported by installed CC. Falling back to --append-system-prompt-file for ${employeeName}. Upgrade Claude Code to enable native --agent launch.
4939
+ `
4940
+ );
4941
+ }
4942
+ let sessionContextFlag = "";
4943
+ try {
4944
+ const ctxDir = path16.join(os7.homedir(), ".exe-os", "session-cache");
4945
+ mkdirSync7(ctxDir, { recursive: true });
4946
+ const ctxFile = path16.join(ctxDir, `session-context-${sessionName}.md`);
4947
+ const ctxContent = [
4948
+ `## Session Context`,
4949
+ `You are running in tmux session: ${sessionName}.`,
4950
+ `Your parent coordinator session is ${exeSession}.`,
4951
+ `Your employees (if any) use the -${exeSession} suffix.`
4952
+ ].join("\n");
4953
+ writeFileSync8(ctxFile, ctxContent);
4954
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
4955
+ } catch {
4956
+ }
4957
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
4958
+ if (ccProvider !== DEFAULT_PROVIDER) {
4959
+ const cfg = PROVIDER_TABLE[ccProvider];
4960
+ if (cfg?.apiKeyEnv) {
4961
+ const keyVal = process.env[cfg.apiKeyEnv];
4962
+ if (keyVal) {
4963
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
4964
+ }
4965
+ }
4966
+ }
4967
+ if (useCodex) {
4968
+ const codexCfg = RUNTIME_TABLE.codex;
4969
+ if (codexCfg?.apiKeyEnv) {
4970
+ const keyVal = process.env[codexCfg.apiKeyEnv];
4971
+ if (keyVal) {
4972
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
4973
+ }
4974
+ }
4975
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
4976
+ }
4977
+ if (useOpencode) {
4978
+ const ocCfg = PROVIDER_TABLE.opencode;
4979
+ if (ocCfg?.apiKeyEnv) {
4980
+ const keyVal = process.env[ocCfg.apiKeyEnv];
4981
+ if (keyVal) {
4982
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
4983
+ }
4984
+ }
4985
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
4986
+ }
4987
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
4988
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
4989
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
4990
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
4991
+ }
4992
+ }
4993
+ let spawnCommand;
4994
+ if (useExeAgent) {
4995
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4996
+ } else if (useCodex) {
4997
+ process.stderr.write(
4998
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
4999
+ `
5000
+ );
5001
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
5002
+ } else if (useOpencode) {
5003
+ const binName = `${employeeName}-opencode`;
5004
+ process.stderr.write(
5005
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
5006
+ `
5007
+ );
5008
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
5009
+ } else if (useBinSymlink) {
5010
+ const binName = `${employeeName}-${ccProvider}`;
5011
+ process.stderr.write(
5012
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
5013
+ `
5014
+ );
5015
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
5016
+ } else {
5017
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
5018
+ }
5019
+ const spawnResult = transport.spawn(sessionName, {
5020
+ cwd: spawnCwd,
5021
+ command: spawnCommand
5022
+ });
5023
+ if (spawnResult.error) {
5024
+ releaseSpawnLock2(sessionName);
5025
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
5026
+ }
5027
+ transport.pipeLog(sessionName, logFile);
5028
+ try {
5029
+ const mySession = getMySession();
5030
+ const dispatchInfo = path16.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5031
+ writeFileSync8(dispatchInfo, JSON.stringify({
5032
+ dispatchedBy: mySession,
5033
+ rootExe: exeSession,
5034
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
5035
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
5036
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
5037
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5038
+ }));
5039
+ } catch {
5040
+ }
5041
+ let booted = false;
5042
+ for (let i = 0; i < 30; i++) {
5043
+ try {
5044
+ execSync7("sleep 0.5");
5045
+ } catch {
5046
+ }
5047
+ try {
5048
+ const pane = transport.capturePane(sessionName);
5049
+ if (useExeAgent) {
5050
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
5051
+ booted = true;
5052
+ break;
5053
+ }
5054
+ } else if (useCodex) {
5055
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
5056
+ booted = true;
5057
+ break;
5058
+ }
5059
+ } else {
5060
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
5061
+ booted = true;
5062
+ break;
5063
+ }
5064
+ }
5065
+ } catch {
5066
+ }
5067
+ }
5068
+ if (!booted) {
5069
+ releaseSpawnLock2(sessionName);
5070
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
5071
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
5072
+ }
5073
+ if (!useExeAgent && !useCodex) {
5074
+ try {
5075
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
5076
+ } catch {
5077
+ }
5078
+ }
5079
+ registerSession({
5080
+ windowName: sessionName,
5081
+ agentId: employeeName,
5082
+ projectDir: spawnCwd,
5083
+ parentExe: exeSession,
5084
+ pid: 0,
5085
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
5086
+ });
5087
+ releaseSpawnLock2(sessionName);
5088
+ return { sessionName };
5089
+ }
5090
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1480
5091
  var init_tmux_routing = __esm({
1481
5092
  "src/lib/tmux-routing.ts"() {
1482
5093
  "use strict";
@@ -1486,14 +5097,21 @@ var init_tmux_routing = __esm({
1486
5097
  init_cc_agent_support();
1487
5098
  init_mcp_prefix();
1488
5099
  init_provider_table();
5100
+ init_agent_config();
5101
+ init_runtime_table();
1489
5102
  init_intercom_queue();
1490
5103
  init_plan_limits();
1491
5104
  init_employees();
1492
- SPAWN_LOCK_DIR = path8.join(os5.homedir(), ".exe-os", "spawn-locks");
1493
- SESSION_CACHE = path8.join(os5.homedir(), ".exe-os", "session-cache");
1494
- INTERCOM_LOG2 = path8.join(os5.homedir(), ".exe-os", "intercom.log");
1495
- DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
5105
+ SPAWN_LOCK_DIR = path16.join(os7.homedir(), ".exe-os", "spawn-locks");
5106
+ SESSION_CACHE = path16.join(os7.homedir(), ".exe-os", "session-cache");
5107
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5108
+ VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
5109
+ VERIFY_PANE_LINES = 200;
5110
+ INTERCOM_DEBOUNCE_MS = 3e4;
5111
+ INTERCOM_LOG2 = path16.join(os7.homedir(), ".exe-os", "intercom.log");
5112
+ DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
1496
5113
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5114
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
1497
5115
  }
1498
5116
  });
1499
5117
 
@@ -1531,15 +5149,15 @@ var init_memory = __esm({
1531
5149
  });
1532
5150
 
1533
5151
  // src/lib/keychain.ts
1534
- import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
1535
- import { existsSync as existsSync7 } from "fs";
1536
- import path9 from "path";
1537
- import os6 from "os";
5152
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
5153
+ import { existsSync as existsSync13 } from "fs";
5154
+ import path17 from "path";
5155
+ import os8 from "os";
1538
5156
  function getKeyDir() {
1539
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path9.join(os6.homedir(), ".exe-os");
5157
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path17.join(os8.homedir(), ".exe-os");
1540
5158
  }
1541
5159
  function getKeyPath() {
1542
- return path9.join(getKeyDir(), "master.key");
5160
+ return path17.join(getKeyDir(), "master.key");
1543
5161
  }
1544
5162
  async function tryKeytar() {
1545
5163
  try {
@@ -1560,77 +5178,30 @@ async function getMasterKey() {
1560
5178
  }
1561
5179
  }
1562
5180
  const keyPath = getKeyPath();
1563
- if (!existsSync7(keyPath)) {
5181
+ if (!existsSync13(keyPath)) {
5182
+ process.stderr.write(
5183
+ `[keychain] Key not found at ${keyPath} (HOME=${os8.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5184
+ `
5185
+ );
1564
5186
  return null;
1565
5187
  }
1566
5188
  try {
1567
- const content = await readFile3(keyPath, "utf-8");
5189
+ const content = await readFile4(keyPath, "utf-8");
1568
5190
  return Buffer.from(content.trim(), "base64");
1569
- } catch {
1570
- return null;
1571
- }
1572
- }
1573
- var SERVICE, ACCOUNT;
1574
- var init_keychain = __esm({
1575
- "src/lib/keychain.ts"() {
1576
- "use strict";
1577
- SERVICE = "exe-mem";
1578
- ACCOUNT = "master-key";
1579
- }
1580
- });
1581
-
1582
- // src/lib/state-bus.ts
1583
- var StateBus, orgBus;
1584
- var init_state_bus = __esm({
1585
- "src/lib/state-bus.ts"() {
1586
- "use strict";
1587
- StateBus = class {
1588
- handlers = /* @__PURE__ */ new Map();
1589
- globalHandlers = /* @__PURE__ */ new Set();
1590
- /** Emit an event to all subscribers */
1591
- emit(event) {
1592
- const typeHandlers = this.handlers.get(event.type);
1593
- if (typeHandlers) {
1594
- for (const handler of typeHandlers) {
1595
- try {
1596
- handler(event);
1597
- } catch {
1598
- }
1599
- }
1600
- }
1601
- for (const handler of this.globalHandlers) {
1602
- try {
1603
- handler(event);
1604
- } catch {
1605
- }
1606
- }
1607
- }
1608
- /** Subscribe to a specific event type */
1609
- on(type, handler) {
1610
- if (!this.handlers.has(type)) {
1611
- this.handlers.set(type, /* @__PURE__ */ new Set());
1612
- }
1613
- this.handlers.get(type).add(handler);
1614
- }
1615
- /** Subscribe to ALL events */
1616
- onAny(handler) {
1617
- this.globalHandlers.add(handler);
1618
- }
1619
- /** Unsubscribe from a specific event type */
1620
- off(type, handler) {
1621
- this.handlers.get(type)?.delete(handler);
1622
- }
1623
- /** Unsubscribe from ALL events */
1624
- offAny(handler) {
1625
- this.globalHandlers.delete(handler);
1626
- }
1627
- /** Remove all listeners */
1628
- clear() {
1629
- this.handlers.clear();
1630
- this.globalHandlers.clear();
1631
- }
1632
- };
1633
- orgBus = new StateBus();
5191
+ } catch (err) {
5192
+ process.stderr.write(
5193
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
5194
+ `
5195
+ );
5196
+ return null;
5197
+ }
5198
+ }
5199
+ var SERVICE, ACCOUNT;
5200
+ var init_keychain = __esm({
5201
+ "src/lib/keychain.ts"() {
5202
+ "use strict";
5203
+ SERVICE = "exe-mem";
5204
+ ACCOUNT = "master-key";
1634
5205
  }
1635
5206
  });
1636
5207
 
@@ -1647,13 +5218,13 @@ __export(shard_manager_exports, {
1647
5218
  listShards: () => listShards,
1648
5219
  shardExists: () => shardExists
1649
5220
  });
1650
- import path10 from "path";
1651
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2 } from "fs";
5221
+ import path18 from "path";
5222
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync4 } from "fs";
1652
5223
  import { createClient as createClient2 } from "@libsql/client";
1653
5224
  function initShardManager(encryptionKey) {
1654
5225
  _encryptionKey = encryptionKey;
1655
- if (!existsSync8(SHARDS_DIR)) {
1656
- mkdirSync5(SHARDS_DIR, { recursive: true });
5226
+ if (!existsSync14(SHARDS_DIR)) {
5227
+ mkdirSync8(SHARDS_DIR, { recursive: true });
1657
5228
  }
1658
5229
  _shardingEnabled = true;
1659
5230
  }
@@ -1673,7 +5244,7 @@ function getShardClient(projectName) {
1673
5244
  }
1674
5245
  const cached = _shards.get(safeName);
1675
5246
  if (cached) return cached;
1676
- const dbPath = path10.join(SHARDS_DIR, `${safeName}.db`);
5247
+ const dbPath = path18.join(SHARDS_DIR, `${safeName}.db`);
1677
5248
  const client = createClient2({
1678
5249
  url: `file:${dbPath}`,
1679
5250
  encryptionKey: _encryptionKey
@@ -1683,11 +5254,11 @@ function getShardClient(projectName) {
1683
5254
  }
1684
5255
  function shardExists(projectName) {
1685
5256
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
1686
- return existsSync8(path10.join(SHARDS_DIR, `${safeName}.db`));
5257
+ return existsSync14(path18.join(SHARDS_DIR, `${safeName}.db`));
1687
5258
  }
1688
5259
  function listShards() {
1689
- if (!existsSync8(SHARDS_DIR)) return [];
1690
- return readdirSync2(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
5260
+ if (!existsSync14(SHARDS_DIR)) return [];
5261
+ return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
1691
5262
  }
1692
5263
  async function ensureShardSchema(client) {
1693
5264
  await client.execute("PRAGMA journal_mode = WAL");
@@ -1872,7 +5443,7 @@ var init_shard_manager = __esm({
1872
5443
  "src/lib/shard-manager.ts"() {
1873
5444
  "use strict";
1874
5445
  init_config();
1875
- SHARDS_DIR = path10.join(EXE_AI_DIR, "shards");
5446
+ SHARDS_DIR = path18.join(EXE_AI_DIR, "shards");
1876
5447
  _shards = /* @__PURE__ */ new Map();
1877
5448
  _encryptionKey = null;
1878
5449
  _shardingEnabled = false;
@@ -1997,7 +5568,7 @@ __export(global_procedures_exports, {
1997
5568
  loadGlobalProcedures: () => loadGlobalProcedures,
1998
5569
  storeGlobalProcedure: () => storeGlobalProcedure
1999
5570
  });
2000
- import { randomUUID as randomUUID2 } from "crypto";
5571
+ import { randomUUID as randomUUID3 } from "crypto";
2001
5572
  async function loadGlobalProcedures() {
2002
5573
  const client = getClient();
2003
5574
  const result = await client.execute({
@@ -2026,7 +5597,7 @@ ${sections.join("\n\n")}
2026
5597
  `;
2027
5598
  }
2028
5599
  async function storeGlobalProcedure(input2) {
2029
- const id = randomUUID2();
5600
+ const id = randomUUID3();
2030
5601
  const now = (/* @__PURE__ */ new Date()).toISOString();
2031
5602
  const client = getClient();
2032
5603
  await client.execute({
@@ -2077,6 +5648,7 @@ __export(store_exports, {
2077
5648
  vectorToBlob: () => vectorToBlob,
2078
5649
  writeMemory: () => writeMemory
2079
5650
  });
5651
+ import { createHash } from "crypto";
2080
5652
  function isBusyError2(err) {
2081
5653
  if (err instanceof Error) {
2082
5654
  const msg = err.message.toLowerCase();
@@ -2150,12 +5722,52 @@ function classifyTier(record) {
2150
5722
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
2151
5723
  return 3;
2152
5724
  }
5725
+ function inferFilePaths(record) {
5726
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
5727
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
5728
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
5729
+ return match ? JSON.stringify([match[1]]) : null;
5730
+ }
5731
+ function inferCommitHash(record) {
5732
+ if (record.tool_name !== "Bash") return null;
5733
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
5734
+ return match ? match[1] : null;
5735
+ }
5736
+ function inferLanguageType(record) {
5737
+ const text = record.raw_text;
5738
+ if (!text || text.length < 10) return null;
5739
+ const trimmed = text.trimStart();
5740
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
5741
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
5742
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
5743
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
5744
+ return "mixed";
5745
+ }
5746
+ function inferDomain(record) {
5747
+ const proj = (record.project_name ?? "").toLowerCase();
5748
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
5749
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
5750
+ return null;
5751
+ }
2153
5752
  async function writeMemory(record) {
2154
5753
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
2155
5754
  throw new Error(
2156
5755
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
2157
5756
  );
2158
5757
  }
5758
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
5759
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
5760
+ return;
5761
+ }
5762
+ try {
5763
+ const client = getClient();
5764
+ const existing = await client.execute({
5765
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
5766
+ args: [contentHash, record.agent_id]
5767
+ });
5768
+ if (existing.rows.length > 0) return;
5769
+ } catch {
5770
+ }
2159
5771
  const dbRow = {
2160
5772
  id: record.id,
2161
5773
  agent_id: record.agent_id,
@@ -2185,7 +5797,23 @@ async function writeMemory(record) {
2185
5797
  supersedes_id: record.supersedes_id ?? null,
2186
5798
  draft: record.draft ? 1 : 0,
2187
5799
  memory_type: record.memory_type ?? "raw",
2188
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
5800
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
5801
+ content_hash: contentHash,
5802
+ intent: record.intent ?? null,
5803
+ outcome: record.outcome ?? null,
5804
+ domain: record.domain ?? inferDomain(record),
5805
+ referenced_entities: record.referenced_entities ?? null,
5806
+ retrieval_count: record.retrieval_count ?? 0,
5807
+ chain_position: record.chain_position ?? null,
5808
+ review_status: record.review_status ?? null,
5809
+ context_window_pct: record.context_window_pct ?? null,
5810
+ file_paths: record.file_paths ?? inferFilePaths(record),
5811
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
5812
+ duration_ms: record.duration_ms ?? null,
5813
+ token_cost: record.token_cost ?? null,
5814
+ audience: record.audience ?? null,
5815
+ language_type: record.language_type ?? inferLanguageType(record),
5816
+ parent_memory_id: record.parent_memory_id ?? null
2189
5817
  };
2190
5818
  _pendingRecords.push(dbRow);
2191
5819
  orgBus.emit({
@@ -2243,80 +5871,85 @@ async function flushBatch() {
2243
5871
  const draft = row.draft ? 1 : 0;
2244
5872
  const memoryType = row.memory_type ?? "raw";
2245
5873
  const trajectory = row.trajectory ?? null;
2246
- return {
2247
- sql: hasVector ? `INSERT OR IGNORE INTO memories
2248
- (id, agent_id, agent_role, session_id, timestamp,
5874
+ const contentHash = row.content_hash ?? null;
5875
+ const intent = row.intent ?? null;
5876
+ const outcome = row.outcome ?? null;
5877
+ const domain = row.domain ?? null;
5878
+ const referencedEntities = row.referenced_entities ?? null;
5879
+ const retrievalCount = row.retrieval_count ?? 0;
5880
+ const chainPosition = row.chain_position ?? null;
5881
+ const reviewStatus = row.review_status ?? null;
5882
+ const contextWindowPct = row.context_window_pct ?? null;
5883
+ const filePaths = row.file_paths ?? null;
5884
+ const commitHash = row.commit_hash ?? null;
5885
+ const durationMs = row.duration_ms ?? null;
5886
+ const tokenCost = row.token_cost ?? null;
5887
+ const audience = row.audience ?? null;
5888
+ const languageType = row.language_type ?? null;
5889
+ const parentMemoryId = row.parent_memory_id ?? null;
5890
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
2249
5891
  tool_name, project_name,
2250
5892
  has_error, raw_text, vector, version, task_id, importance, status,
2251
5893
  confidence, last_accessed,
2252
5894
  workspace_id, document_id, user_id, char_offset, page_number,
2253
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2254
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2255
- (id, agent_id, agent_role, session_id, timestamp,
2256
- tool_name, project_name,
2257
- has_error, raw_text, vector, version, task_id, importance, status,
2258
- confidence, last_accessed,
2259
- workspace_id, document_id, user_id, char_offset, page_number,
2260
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2261
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2262
- args: hasVector ? [
2263
- row.id,
2264
- row.agent_id,
2265
- row.agent_role,
2266
- row.session_id,
2267
- row.timestamp,
2268
- row.tool_name,
2269
- row.project_name,
2270
- row.has_error,
2271
- row.raw_text,
2272
- vectorToBlob(row.vector),
2273
- row.version,
2274
- taskId,
2275
- importance,
2276
- status,
2277
- confidence,
2278
- lastAccessed,
2279
- workspaceId,
2280
- documentId,
2281
- userId,
2282
- charOffset,
2283
- pageNumber,
2284
- sourcePath,
2285
- sourceType,
2286
- tier,
2287
- supersedesId,
2288
- draft,
2289
- memoryType,
2290
- trajectory
2291
- ] : [
2292
- row.id,
2293
- row.agent_id,
2294
- row.agent_role,
2295
- row.session_id,
2296
- row.timestamp,
2297
- row.tool_name,
2298
- row.project_name,
2299
- row.has_error,
2300
- row.raw_text,
2301
- row.version,
2302
- taskId,
2303
- importance,
2304
- status,
2305
- confidence,
2306
- lastAccessed,
2307
- workspaceId,
2308
- documentId,
2309
- userId,
2310
- charOffset,
2311
- pageNumber,
2312
- sourcePath,
2313
- sourceType,
2314
- tier,
2315
- supersedesId,
2316
- draft,
2317
- memoryType,
2318
- trajectory
2319
- ]
5895
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
5896
+ intent, outcome, domain, referenced_entities, retrieval_count,
5897
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
5898
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
5899
+ const metaArgs = [
5900
+ intent,
5901
+ outcome,
5902
+ domain,
5903
+ referencedEntities,
5904
+ retrievalCount,
5905
+ chainPosition,
5906
+ reviewStatus,
5907
+ contextWindowPct,
5908
+ filePaths,
5909
+ commitHash,
5910
+ durationMs,
5911
+ tokenCost,
5912
+ audience,
5913
+ languageType,
5914
+ parentMemoryId
5915
+ ];
5916
+ const baseArgs = [
5917
+ row.id,
5918
+ row.agent_id,
5919
+ row.agent_role,
5920
+ row.session_id,
5921
+ row.timestamp,
5922
+ row.tool_name,
5923
+ row.project_name,
5924
+ row.has_error,
5925
+ row.raw_text
5926
+ ];
5927
+ const sharedArgs = [
5928
+ row.version,
5929
+ taskId,
5930
+ importance,
5931
+ status,
5932
+ confidence,
5933
+ lastAccessed,
5934
+ workspaceId,
5935
+ documentId,
5936
+ userId,
5937
+ charOffset,
5938
+ pageNumber,
5939
+ sourcePath,
5940
+ sourceType,
5941
+ tier,
5942
+ supersedesId,
5943
+ draft,
5944
+ memoryType,
5945
+ trajectory,
5946
+ contentHash
5947
+ ];
5948
+ return {
5949
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
5950
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
5951
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5952
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
2320
5953
  };
2321
5954
  };
2322
5955
  const globalClient = getClient();
@@ -2566,238 +6199,221 @@ var init_store = __esm({
2566
6199
  }
2567
6200
  });
2568
6201
 
2569
- // src/lib/notifications.ts
2570
- var notifications_exports = {};
2571
- __export(notifications_exports, {
2572
- cleanupOldNotifications: () => cleanupOldNotifications,
2573
- formatNotifications: () => formatNotifications,
2574
- markAsRead: () => markAsRead,
2575
- markAsReadByTaskFile: () => markAsReadByTaskFile,
2576
- markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
2577
- migrateJsonNotifications: () => migrateJsonNotifications,
2578
- readUnreadNotifications: () => readUnreadNotifications,
2579
- writeNotification: () => writeNotification
6202
+ // src/lib/git-task-sweep.ts
6203
+ var git_task_sweep_exports = {};
6204
+ __export(git_task_sweep_exports, {
6205
+ extractKeywords: () => extractKeywords,
6206
+ findBestMatch: () => findBestMatch,
6207
+ getRecentCommits: () => getRecentCommits,
6208
+ matchScore: () => matchScore,
6209
+ sweepTasks: () => sweepTasks
2580
6210
  });
2581
- import crypto from "crypto";
2582
- import path11 from "path";
2583
- import os7 from "os";
2584
- import {
2585
- readFileSync as readFileSync8,
2586
- readdirSync as readdirSync3,
2587
- unlinkSync as unlinkSync3,
2588
- existsSync as existsSync9,
2589
- rmdirSync
2590
- } from "fs";
2591
- async function writeNotification(notification) {
2592
- try {
2593
- const client = getClient();
2594
- const id = crypto.randomUUID();
2595
- const now = (/* @__PURE__ */ new Date()).toISOString();
2596
- await client.execute({
2597
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
2598
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2599
- args: [
2600
- id,
2601
- notification.agentId,
2602
- notification.agentRole,
2603
- notification.event,
2604
- notification.project,
2605
- notification.summary,
2606
- notification.taskFile ?? null,
2607
- now
2608
- ]
2609
- });
2610
- } catch (err) {
2611
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2612
- `);
2613
- }
6211
+ import { execSync as execSync8 } from "child_process";
6212
+ function extractKeywords(text) {
6213
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
2614
6214
  }
2615
- async function readUnreadNotifications(agentFilter) {
2616
- try {
2617
- const client = getClient();
2618
- const conditions = ["read = 0"];
2619
- const args = [];
2620
- if (agentFilter) {
2621
- conditions.push("agent_id = ?");
2622
- args.push(agentFilter);
6215
+ function matchScore(task, commitMessage, changedFiles) {
6216
+ if (task.id.length >= 8 && commitMessage.includes(task.id)) {
6217
+ return EXACT_UUID_SCORE;
6218
+ }
6219
+ let score = 0;
6220
+ const titleWords = extractKeywords(task.title);
6221
+ const commitWords = new Set(extractKeywords(commitMessage));
6222
+ const overlap = titleWords.filter((w) => commitWords.has(w));
6223
+ if (overlap.length >= MIN_KEYWORD_OVERLAP) {
6224
+ score += TITLE_KEYWORD_SCORE;
6225
+ }
6226
+ if (task.context && changedFiles.length > 0) {
6227
+ const contextLower = task.context.toLowerCase();
6228
+ const hasFileMatch = changedFiles.some(
6229
+ (f) => contextLower.includes(f.toLowerCase())
6230
+ );
6231
+ if (hasFileMatch) {
6232
+ score += FILE_PATH_SCORE;
2623
6233
  }
2624
- const result = await client.execute({
2625
- sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, created_at
2626
- FROM notifications
2627
- WHERE ${conditions.join(" AND ")}
2628
- ORDER BY created_at ASC`,
2629
- args
2630
- });
2631
- return result.rows.map((r) => ({
2632
- id: String(r.id),
2633
- agentId: String(r.agent_id),
2634
- agentRole: String(r.agent_role),
2635
- event: String(r.event),
2636
- project: String(r.project),
2637
- summary: String(r.summary),
2638
- taskFile: r.task_file ? String(r.task_file) : void 0,
2639
- timestamp: String(r.created_at),
2640
- read: false
2641
- }));
2642
- } catch {
2643
- return [];
2644
6234
  }
6235
+ return score;
2645
6236
  }
2646
- async function markAsRead(ids) {
2647
- if (ids.length === 0) return;
6237
+ function getRecentCommits(limit = DEFAULT_COMMIT_LIMIT) {
2648
6238
  try {
2649
- const client = getClient();
2650
- const placeholders = ids.map(() => "?").join(", ");
2651
- await client.execute({
2652
- sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})`,
2653
- args: ids
2654
- });
6239
+ const SEPARATOR = "<<SEP>>";
6240
+ const output = execSync8(
6241
+ `git log --format="%h${SEPARATOR}%s${SEPARATOR}%aI" --name-only -n ${limit} -z`,
6242
+ { encoding: "utf8", timeout: 1e4 }
6243
+ );
6244
+ const entries = output.split("\0").filter(Boolean);
6245
+ const commits = [];
6246
+ let current = null;
6247
+ for (const entry of entries) {
6248
+ if (entry.includes(SEPARATOR)) {
6249
+ const lines = entry.split("\n");
6250
+ const headerLine = lines[0];
6251
+ const parts = headerLine.split(SEPARATOR);
6252
+ if (parts.length >= 3) {
6253
+ if (current) commits.push(current);
6254
+ current = {
6255
+ hash: parts[0],
6256
+ message: parts[1],
6257
+ files: lines.slice(1).filter(Boolean),
6258
+ date: new Date(parts[2])
6259
+ };
6260
+ }
6261
+ } else if (current) {
6262
+ const files = entry.split("\n").filter(Boolean);
6263
+ current.files.push(...files);
6264
+ }
6265
+ }
6266
+ if (current) commits.push(current);
6267
+ return commits;
2655
6268
  } catch {
6269
+ return [];
2656
6270
  }
2657
6271
  }
2658
- async function markAsReadByTaskFile(taskFile) {
2659
- try {
2660
- const client = getClient();
2661
- await client.execute({
2662
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
2663
- args: [taskFile]
2664
- });
2665
- } catch {
2666
- }
6272
+ function isStale(updatedAt, staleMinutes) {
6273
+ const updated = new Date(updatedAt).getTime();
6274
+ const threshold = Date.now() - staleMinutes * 6e4;
6275
+ return updated < threshold;
2667
6276
  }
2668
- async function cleanupOldNotifications(daysOld = CLEANUP_DAYS) {
2669
- try {
2670
- const client = getClient();
2671
- const cutoff = new Date(
2672
- Date.now() - daysOld * 24 * 60 * 60 * 1e3
2673
- ).toISOString();
2674
- const result = await client.execute({
2675
- sql: "DELETE FROM notifications WHERE created_at < ?",
2676
- args: [cutoff]
2677
- });
2678
- return result.rowsAffected;
2679
- } catch {
2680
- return 0;
6277
+ function findBestMatch(task, commits) {
6278
+ let best = null;
6279
+ for (const commit of commits) {
6280
+ const score = matchScore(task, commit.message, commit.files);
6281
+ if (score >= AUTO_ESCALATE_THRESHOLD && (!best || score > best.score)) {
6282
+ best = { commit, score };
6283
+ }
2681
6284
  }
6285
+ return best;
2682
6286
  }
2683
- async function markDoneTaskNotificationsAsRead() {
6287
+ async function sweepTasks(projectName, options = {}) {
6288
+ const commitLimit = options.commitLimit ?? DEFAULT_COMMIT_LIMIT;
6289
+ const staleMinutes = options.staleMinutes ?? DEFAULT_STALE_MINUTES;
6290
+ const dryRun = options.dryRun ?? false;
6291
+ const result = { escalated: [], unchanged: 0, errors: [] };
6292
+ const commits = getRecentCommits(commitLimit);
6293
+ if (commits.length === 0) {
6294
+ result.errors.push("No git commits found (not a git repo or empty history)");
6295
+ return result;
6296
+ }
6297
+ let tasks;
2684
6298
  try {
2685
- const client = getClient();
2686
- const result = await client.execute({
2687
- sql: `UPDATE notifications SET read = 1
2688
- WHERE read = 0
2689
- AND task_file IS NOT NULL
2690
- AND task_file IN (
2691
- SELECT task_file FROM tasks WHERE status = 'done'
2692
- )`,
2693
- args: []
6299
+ const { initStore: initStore2 } = await Promise.resolve().then(() => (init_store(), store_exports));
6300
+ await initStore2();
6301
+ const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
6302
+ const client = getClient2();
6303
+ const conditions = ["status = 'in_progress'"];
6304
+ const args = [];
6305
+ if (projectName) {
6306
+ conditions.push("project_name = ?");
6307
+ args.push(projectName);
6308
+ }
6309
+ const swScope = sessionScopeFilter();
6310
+ if (swScope.sql) {
6311
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
6312
+ args.push(...swScope.args);
6313
+ }
6314
+ const queryResult = await client.execute({
6315
+ sql: `SELECT id, title, assigned_to, project_name, status, updated_at, context
6316
+ FROM tasks WHERE ${conditions.join(" AND ")}
6317
+ ORDER BY updated_at ASC`,
6318
+ args
2694
6319
  });
2695
- return result.rowsAffected;
2696
- } catch {
2697
- return 0;
6320
+ tasks = queryResult.rows.map((r) => ({
6321
+ id: String(r.id),
6322
+ title: String(r.title),
6323
+ assignedTo: String(r.assigned_to),
6324
+ projectName: String(r.project_name),
6325
+ status: String(r.status),
6326
+ updatedAt: String(r.updated_at),
6327
+ context: r.context ? String(r.context) : void 0
6328
+ }));
6329
+ } catch (err) {
6330
+ result.errors.push(`DB query failed: ${err instanceof Error ? err.message : String(err)}`);
6331
+ return result;
2698
6332
  }
2699
- }
2700
- function formatNotifications(notifications) {
2701
- if (notifications.length === 0) return "";
2702
- const grouped = /* @__PURE__ */ new Map();
2703
- for (const n of notifications) {
2704
- const key = `${n.agentId}|${n.agentRole}`;
2705
- if (!grouped.has(key)) grouped.set(key, []);
2706
- grouped.get(key).push(n);
6333
+ if (tasks.length === 0) {
6334
+ return result;
2707
6335
  }
2708
- const lines = [];
2709
- lines.push(`## Notifications (${notifications.length} unread)
2710
- `);
2711
- for (const [key, items] of grouped) {
2712
- const [agentId, agentRole] = key.split("|");
2713
- lines.push(`**${agentId}** (${agentRole}):`);
2714
- for (const item of items) {
2715
- const ago = formatTimeAgo(item.timestamp);
2716
- const icon = eventIcon(item.event);
2717
- lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
6336
+ for (const task of tasks) {
6337
+ if (!isStale(task.updatedAt, staleMinutes)) {
6338
+ result.unchanged++;
6339
+ continue;
2718
6340
  }
2719
- lines.push("");
2720
- }
2721
- return lines.join("\n");
2722
- }
2723
- async function migrateJsonNotifications() {
2724
- const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path11.join(os7.homedir(), ".exe-os");
2725
- const notifDir = path11.join(base, "notifications");
2726
- if (!existsSync9(notifDir)) return 0;
2727
- let migrated = 0;
2728
- try {
2729
- const files = readdirSync3(notifDir).filter((f) => f.endsWith(".json"));
2730
- if (files.length === 0) return 0;
2731
- const client = getClient();
2732
- for (const file of files) {
6341
+ const match = findBestMatch(task, commits);
6342
+ if (!match) {
6343
+ result.unchanged++;
6344
+ continue;
6345
+ }
6346
+ if (!dryRun) {
2733
6347
  try {
2734
- const filePath = path11.join(notifDir, file);
2735
- const data = JSON.parse(readFileSync8(filePath, "utf8"));
2736
- await client.execute({
2737
- sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
2738
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2739
- args: [
2740
- crypto.randomUUID(),
2741
- data.agentId ?? "unknown",
2742
- data.agentRole ?? "unknown",
2743
- data.event ?? "session_summary",
2744
- data.project ?? "unknown",
2745
- data.summary ?? "",
2746
- data.taskFile ?? null,
2747
- data.read ? 1 : 0,
2748
- data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2749
- ]
6348
+ const { updateTaskStatus: updateTaskStatus2 } = await Promise.resolve().then(() => (init_tasks_crud(), tasks_crud_exports));
6349
+ await updateTaskStatus2({
6350
+ taskId: task.id,
6351
+ status: "needs_review",
6352
+ result: `Auto-escalated by git-sweep: matching commit ${match.commit.hash} found (score: ${match.score.toFixed(2)})`
2750
6353
  });
2751
- unlinkSync3(filePath);
2752
- migrated++;
2753
- } catch {
2754
- }
2755
- }
2756
- try {
2757
- const remaining = readdirSync3(notifDir);
2758
- if (remaining.length === 0) {
2759
- rmdirSync(notifDir);
6354
+ } catch (err) {
6355
+ result.errors.push(
6356
+ `Failed to escalate task ${task.id}: ${err instanceof Error ? err.message : String(err)}`
6357
+ );
6358
+ continue;
2760
6359
  }
2761
- } catch {
2762
6360
  }
2763
- } catch {
2764
- }
2765
- return migrated;
2766
- }
2767
- function eventIcon(event) {
2768
- switch (event) {
2769
- case "task_complete":
2770
- return "Completed:";
2771
- case "task_needs_fix":
2772
- return "Needs fix:";
2773
- case "session_summary":
2774
- return "Session:";
2775
- case "error_spike":
2776
- return "Errors:";
2777
- case "orphan_task":
2778
- return "Orphan:";
2779
- case "subtasks_complete":
2780
- return "Subtasks done:";
2781
- case "capacity_relaunch":
2782
- return "Relaunched:";
6361
+ result.escalated.push({
6362
+ taskId: task.id,
6363
+ title: task.title,
6364
+ matchedCommit: match.commit.hash,
6365
+ score: match.score
6366
+ });
6367
+ process.stderr.write(
6368
+ `[git-sweep] ${dryRun ? "WOULD escalate" : "Escalated"} task ${task.id} \u2192 commit ${match.commit.hash} (score: ${match.score.toFixed(2)})
6369
+ `
6370
+ );
2783
6371
  }
6372
+ return result;
2784
6373
  }
2785
- function formatTimeAgo(timestamp) {
2786
- const diffMs = Date.now() - new Date(timestamp).getTime();
2787
- const mins = Math.floor(diffMs / 6e4);
2788
- if (mins < 1) return "just now";
2789
- if (mins < 60) return `${mins}m ago`;
2790
- const hours = Math.floor(mins / 60);
2791
- if (hours < 24) return `${hours}h ago`;
2792
- const days = Math.floor(hours / 24);
2793
- return `${days}d ago`;
2794
- }
2795
- var CLEANUP_DAYS;
2796
- var init_notifications = __esm({
2797
- "src/lib/notifications.ts"() {
6374
+ var DEFAULT_COMMIT_LIMIT, DEFAULT_STALE_MINUTES, AUTO_ESCALATE_THRESHOLD, EXACT_UUID_SCORE, TITLE_KEYWORD_SCORE, FILE_PATH_SCORE, MIN_KEYWORD_OVERLAP, STOP_WORDS;
6375
+ var init_git_task_sweep = __esm({
6376
+ "src/lib/git-task-sweep.ts"() {
2798
6377
  "use strict";
2799
- init_database();
2800
- CLEANUP_DAYS = 7;
6378
+ init_task_scope();
6379
+ DEFAULT_COMMIT_LIMIT = 50;
6380
+ DEFAULT_STALE_MINUTES = 10;
6381
+ AUTO_ESCALATE_THRESHOLD = 0.6;
6382
+ EXACT_UUID_SCORE = 1;
6383
+ TITLE_KEYWORD_SCORE = 0.5;
6384
+ FILE_PATH_SCORE = 0.3;
6385
+ MIN_KEYWORD_OVERLAP = 3;
6386
+ STOP_WORDS = /* @__PURE__ */ new Set([
6387
+ "a",
6388
+ "an",
6389
+ "the",
6390
+ "and",
6391
+ "or",
6392
+ "but",
6393
+ "in",
6394
+ "on",
6395
+ "at",
6396
+ "to",
6397
+ "for",
6398
+ "of",
6399
+ "with",
6400
+ "by",
6401
+ "from",
6402
+ "is",
6403
+ "it",
6404
+ "as",
6405
+ "be",
6406
+ "was",
6407
+ "are",
6408
+ "this",
6409
+ "that",
6410
+ "not",
6411
+ "no",
6412
+ "if",
6413
+ "so",
6414
+ "do",
6415
+ "up"
6416
+ ]);
2801
6417
  }
2802
6418
  });
2803
6419
 
@@ -2938,7 +6554,7 @@ process.stdin.on("end", async () => {
2938
6554
  await initStore2();
2939
6555
  const { writeMemory: writeMemory2, flushBatch: flushBatch2 } = await Promise.resolve().then(() => (init_store(), store_exports));
2940
6556
  const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
2941
- const { randomUUID: randomUUID3 } = await import("crypto");
6557
+ const { randomUUID: randomUUID4 } = await import("crypto");
2942
6558
  const client = getClient2();
2943
6559
  const seScope = sessionScopeFilter();
2944
6560
  const orphanResult = await client.execute({
@@ -2948,7 +6564,7 @@ process.stdin.on("end", async () => {
2948
6564
  const orphanInfo = orphanResult.rows.length > 0 ? `
2949
6565
  Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title)}" (${String(r.status)})`).join(", ")}` : "";
2950
6566
  await writeMemory2({
2951
- id: randomUUID3(),
6567
+ id: randomUUID4(),
2952
6568
  agent_id: agent.agentId,
2953
6569
  agent_role: agent.agentRole,
2954
6570
  session_id: data.session_id,
@@ -2968,15 +6584,57 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
2968
6584
  `[session-end] WARNING: ${agent.agentId} ended with ${inProgress.length} in_progress task(s): ${titles}
2969
6585
  `
2970
6586
  );
6587
+ let commits = [];
6588
+ try {
6589
+ const { getRecentCommits: getRecentCommits2 } = await Promise.resolve().then(() => (init_git_task_sweep(), git_task_sweep_exports));
6590
+ commits = getRecentCommits2(30);
6591
+ } catch {
6592
+ }
6593
+ const autoClosed = [];
6594
+ const leftInProgress = [];
2971
6595
  for (const row of inProgress) {
6596
+ const title = String(row.title);
2972
6597
  try {
2973
- await client.execute({
2974
- sql: "UPDATE tasks SET status = 'blocked', updated_at = ? WHERE title = ? AND assigned_to = ? AND status = 'in_progress'",
2975
- args: [(/* @__PURE__ */ new Date()).toISOString(), String(row.title), agent.agentId]
2976
- });
6598
+ if (commits.length > 0) {
6599
+ const { findBestMatch: findBestMatch2 } = await Promise.resolve().then(() => (init_git_task_sweep(), git_task_sweep_exports));
6600
+ let context;
6601
+ try {
6602
+ const ctxResult = await client.execute({
6603
+ sql: "SELECT id, context FROM tasks WHERE title = ? AND assigned_to = ? AND status = 'in_progress' LIMIT 1",
6604
+ args: [title, agent.agentId]
6605
+ });
6606
+ if (ctxResult.rows.length > 0) {
6607
+ context = ctxResult.rows[0].context ? String(ctxResult.rows[0].context) : void 0;
6608
+ }
6609
+ } catch {
6610
+ }
6611
+ const taskForMatch = { id: "", title, context };
6612
+ const match = findBestMatch2(taskForMatch, commits);
6613
+ if (match) {
6614
+ await client.execute({
6615
+ sql: "UPDATE tasks SET status = 'done', result = ?, updated_at = ? WHERE title = ? AND assigned_to = ? AND status = 'in_progress'",
6616
+ args: [
6617
+ `Auto-closed: session ended but matching commit ${match.commit.hash} found (score: ${match.score.toFixed(2)}). Message: "${match.commit.message}"`,
6618
+ (/* @__PURE__ */ new Date()).toISOString(),
6619
+ title,
6620
+ agent.agentId
6621
+ ]
6622
+ });
6623
+ autoClosed.push(`"${title}" \u2192 commit ${match.commit.hash}`);
6624
+ continue;
6625
+ }
6626
+ }
6627
+ leftInProgress.push(`"${title}"`);
2977
6628
  } catch {
2978
6629
  }
2979
6630
  }
6631
+ const parts = [];
6632
+ if (autoClosed.length > 0) {
6633
+ parts.push(`Auto-closed (work committed): ${autoClosed.join(", ")}`);
6634
+ }
6635
+ if (leftInProgress.length > 0) {
6636
+ parts.push(`Left in_progress (no matching commits, needs triage): ${leftInProgress.join(", ")}`);
6637
+ }
2980
6638
  try {
2981
6639
  const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
2982
6640
  await writeNotification2({
@@ -2984,10 +6642,18 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
2984
6642
  agentRole: agent.agentRole,
2985
6643
  event: "orphan_task",
2986
6644
  project: process.env.EXE_PROJECT_NAME ?? "unknown",
2987
- summary: `${agent.agentId} session ended with ${inProgress.length} in_progress task(s): ${titles}. Tasks marked blocked.`
6645
+ summary: `${agent.agentId} session ended with ${inProgress.length} in_progress task(s). ${parts.join(". ")}`
2988
6646
  });
2989
6647
  } catch {
2990
6648
  }
6649
+ if (autoClosed.length > 0) {
6650
+ process.stderr.write(`[session-end] Auto-closed ${autoClosed.length} task(s) with matching commits
6651
+ `);
6652
+ }
6653
+ if (leftInProgress.length > 0) {
6654
+ process.stderr.write(`[session-end] Left ${leftInProgress.length} task(s) as in_progress for coordinator triage
6655
+ `);
6656
+ }
2991
6657
  }
2992
6658
  }
2993
6659
  } catch {