@askexenow/exe-os 0.8.83 → 0.8.85

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 (95) 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 +97 -2
  5. package/dist/bin/cli.js +14350 -12518
  6. package/dist/bin/exe-agent.js +97 -88
  7. package/dist/bin/exe-assign.js +1003 -854
  8. package/dist/bin/exe-boot.js +1257 -320
  9. package/dist/bin/exe-call.js +10 -0
  10. package/dist/bin/exe-cloud.js +29 -6
  11. package/dist/bin/exe-dispatch.js +210 -34
  12. package/dist/bin/exe-doctor.js +403 -6
  13. package/dist/bin/exe-export-behaviors.js +175 -72
  14. package/dist/bin/exe-forget.js +97 -2
  15. package/dist/bin/exe-gateway.js +550 -171
  16. package/dist/bin/exe-healthcheck.js +1 -0
  17. package/dist/bin/exe-heartbeat.js +100 -5
  18. package/dist/bin/exe-kill.js +175 -72
  19. package/dist/bin/exe-launch-agent.js +189 -76
  20. package/dist/bin/exe-link.js +902 -80
  21. package/dist/bin/exe-new-employee.js +38 -8
  22. package/dist/bin/exe-pending-messages.js +96 -2
  23. package/dist/bin/exe-pending-notifications.js +97 -2
  24. package/dist/bin/exe-pending-reviews.js +98 -3
  25. package/dist/bin/exe-rename.js +564 -23
  26. package/dist/bin/exe-review.js +231 -73
  27. package/dist/bin/exe-search.js +989 -226
  28. package/dist/bin/exe-session-cleanup.js +4806 -1665
  29. package/dist/bin/exe-settings.js +20 -5
  30. package/dist/bin/exe-status.js +97 -2
  31. package/dist/bin/exe-team.js +97 -2
  32. package/dist/bin/git-sweep.js +899 -207
  33. package/dist/bin/graph-backfill.js +175 -72
  34. package/dist/bin/graph-export.js +175 -72
  35. package/dist/bin/install.js +38 -7
  36. package/dist/bin/list-providers.js +1 -0
  37. package/dist/bin/scan-tasks.js +904 -211
  38. package/dist/bin/setup.js +867 -268
  39. package/dist/bin/shard-migrate.js +175 -72
  40. package/dist/bin/update.js +1 -0
  41. package/dist/bin/wiki-sync.js +175 -72
  42. package/dist/gateway/index.js +548 -166
  43. package/dist/hooks/bug-report-worker.js +208 -23
  44. package/dist/hooks/commit-complete.js +897 -205
  45. package/dist/hooks/error-recall.js +988 -226
  46. package/dist/hooks/ingest-worker.js +1638 -1194
  47. package/dist/hooks/ingest.js +3 -0
  48. package/dist/hooks/instructions-loaded.js +707 -97
  49. package/dist/hooks/notification.js +699 -89
  50. package/dist/hooks/post-compact.js +714 -104
  51. package/dist/hooks/pre-compact.js +897 -205
  52. package/dist/hooks/pre-tool-use.js +742 -123
  53. package/dist/hooks/prompt-ingest-worker.js +242 -101
  54. package/dist/hooks/prompt-submit.js +995 -233
  55. package/dist/hooks/response-ingest-worker.js +242 -101
  56. package/dist/hooks/session-end.js +3941 -400
  57. package/dist/hooks/session-start.js +1001 -226
  58. package/dist/hooks/stop.js +725 -115
  59. package/dist/hooks/subagent-stop.js +714 -104
  60. package/dist/hooks/summary-worker.js +1964 -1330
  61. package/dist/index.js +1651 -1053
  62. package/dist/lib/cloud-sync.js +907 -86
  63. package/dist/lib/consolidation.js +2 -1
  64. package/dist/lib/database.js +642 -87
  65. package/dist/lib/db-daemon-client.js +503 -0
  66. package/dist/lib/device-registry.js +547 -7
  67. package/dist/lib/embedder.js +14 -28
  68. package/dist/lib/employee-templates.js +84 -74
  69. package/dist/lib/employees.js +9 -0
  70. package/dist/lib/exe-daemon-client.js +16 -29
  71. package/dist/lib/exe-daemon.js +1955 -922
  72. package/dist/lib/hybrid-search.js +988 -226
  73. package/dist/lib/identity.js +87 -67
  74. package/dist/lib/keychain.js +9 -1
  75. package/dist/lib/messaging.js +8 -1
  76. package/dist/lib/reminders.js +91 -74
  77. package/dist/lib/schedules.js +96 -2
  78. package/dist/lib/skill-learning.js +103 -85
  79. package/dist/lib/store.js +234 -73
  80. package/dist/lib/tasks.js +111 -22
  81. package/dist/lib/tmux-routing.js +120 -31
  82. package/dist/lib/token-spend.js +273 -0
  83. package/dist/lib/ws-client.js +11 -0
  84. package/dist/mcp/server.js +5222 -475
  85. package/dist/mcp/tools/complete-reminder.js +94 -77
  86. package/dist/mcp/tools/create-reminder.js +94 -77
  87. package/dist/mcp/tools/create-task.js +120 -22
  88. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  89. package/dist/mcp/tools/list-reminders.js +94 -77
  90. package/dist/mcp/tools/list-tasks.js +31 -1
  91. package/dist/mcp/tools/send-message.js +8 -1
  92. package/dist/mcp/tools/update-task.js +39 -10
  93. package/dist/runtime/index.js +911 -219
  94. package/dist/tui/App.js +997 -295
  95. package/package.json +6 -1
@@ -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,16 +482,68 @@ 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
508
  // src/lib/intercom-queue.ts
435
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
509
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
436
510
  import path5 from "path";
437
511
  import os4 from "os";
512
+ function ensureDir() {
513
+ const dir = path5.dirname(QUEUE_PATH);
514
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
515
+ }
516
+ function readQueue() {
517
+ try {
518
+ if (!existsSync4(QUEUE_PATH)) return [];
519
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
520
+ } catch {
521
+ return [];
522
+ }
523
+ }
524
+ function writeQueue(queue) {
525
+ ensureDir();
526
+ const tmp = `${QUEUE_PATH}.tmp`;
527
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
528
+ renameSync3(tmp, QUEUE_PATH);
529
+ }
530
+ function queueIntercom(targetSession, reason) {
531
+ const queue = readQueue();
532
+ const existing = queue.find((q) => q.targetSession === targetSession);
533
+ if (existing) {
534
+ existing.attempts++;
535
+ existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
536
+ existing.reason = reason;
537
+ } else {
538
+ queue.push({
539
+ targetSession,
540
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
541
+ attempts: 0,
542
+ reason
543
+ });
544
+ }
545
+ writeQueue(queue);
546
+ }
438
547
  var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
439
548
  var init_intercom_queue = __esm({
440
549
  "src/lib/intercom-queue.ts"() {
@@ -500,6 +609,443 @@ var init_db_retry = __esm({
500
609
  }
501
610
  });
502
611
 
612
+ // src/lib/exe-daemon-client.ts
613
+ import net from "net";
614
+ import { spawn } from "child_process";
615
+ import { randomUUID } from "crypto";
616
+ import { existsSync as existsSync5, unlinkSync as unlinkSync3, readFileSync as readFileSync6, openSync, closeSync, statSync } from "fs";
617
+ import path6 from "path";
618
+ import { fileURLToPath } from "url";
619
+ function handleData(chunk) {
620
+ _buffer += chunk.toString();
621
+ if (_buffer.length > MAX_BUFFER) {
622
+ _buffer = "";
623
+ return;
624
+ }
625
+ let newlineIdx;
626
+ while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
627
+ const line = _buffer.slice(0, newlineIdx).trim();
628
+ _buffer = _buffer.slice(newlineIdx + 1);
629
+ if (!line) continue;
630
+ try {
631
+ const response = JSON.parse(line);
632
+ const id = response.id;
633
+ if (!id) continue;
634
+ const entry = _pending.get(id);
635
+ if (entry) {
636
+ clearTimeout(entry.timer);
637
+ _pending.delete(id);
638
+ entry.resolve(response);
639
+ }
640
+ } catch {
641
+ }
642
+ }
643
+ }
644
+ function cleanupStaleFiles() {
645
+ if (existsSync5(PID_PATH)) {
646
+ try {
647
+ const pid = parseInt(readFileSync6(PID_PATH, "utf8").trim(), 10);
648
+ if (pid > 0) {
649
+ try {
650
+ process.kill(pid, 0);
651
+ return;
652
+ } catch {
653
+ }
654
+ }
655
+ } catch {
656
+ }
657
+ try {
658
+ unlinkSync3(PID_PATH);
659
+ } catch {
660
+ }
661
+ try {
662
+ unlinkSync3(SOCKET_PATH);
663
+ } catch {
664
+ }
665
+ }
666
+ }
667
+ function findPackageRoot() {
668
+ let dir = path6.dirname(fileURLToPath(import.meta.url));
669
+ const { root } = path6.parse(dir);
670
+ while (dir !== root) {
671
+ if (existsSync5(path6.join(dir, "package.json"))) return dir;
672
+ dir = path6.dirname(dir);
673
+ }
674
+ return null;
675
+ }
676
+ function spawnDaemon() {
677
+ const pkgRoot = findPackageRoot();
678
+ if (!pkgRoot) {
679
+ process.stderr.write("[exed-client] WARN: cannot find package root\n");
680
+ return;
681
+ }
682
+ const daemonPath = path6.join(pkgRoot, "dist", "lib", "exe-daemon.js");
683
+ if (!existsSync5(daemonPath)) {
684
+ process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
685
+ `);
686
+ return;
687
+ }
688
+ const resolvedPath = daemonPath;
689
+ process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
690
+ `);
691
+ const logPath = path6.join(path6.dirname(SOCKET_PATH), "exed.log");
692
+ let stderrFd = "ignore";
693
+ try {
694
+ stderrFd = openSync(logPath, "a");
695
+ } catch {
696
+ }
697
+ const child = spawn(process.execPath, [resolvedPath], {
698
+ detached: true,
699
+ stdio: ["ignore", "ignore", stderrFd],
700
+ env: {
701
+ ...process.env,
702
+ TMUX: void 0,
703
+ // Daemon is global — must not inherit session scope
704
+ TMUX_PANE: void 0,
705
+ // Prevents resolveExeSession() from scoping to one session
706
+ EXE_DAEMON_SOCK: SOCKET_PATH,
707
+ EXE_DAEMON_PID: PID_PATH
708
+ }
709
+ });
710
+ child.unref();
711
+ if (typeof stderrFd === "number") {
712
+ try {
713
+ closeSync(stderrFd);
714
+ } catch {
715
+ }
716
+ }
717
+ }
718
+ function acquireSpawnLock() {
719
+ try {
720
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
721
+ closeSync(fd);
722
+ return true;
723
+ } catch {
724
+ try {
725
+ const stat = statSync(SPAWN_LOCK_PATH);
726
+ if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
727
+ try {
728
+ unlinkSync3(SPAWN_LOCK_PATH);
729
+ } catch {
730
+ }
731
+ try {
732
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
733
+ closeSync(fd);
734
+ return true;
735
+ } catch {
736
+ }
737
+ }
738
+ } catch {
739
+ }
740
+ return false;
741
+ }
742
+ }
743
+ function releaseSpawnLock() {
744
+ try {
745
+ unlinkSync3(SPAWN_LOCK_PATH);
746
+ } catch {
747
+ }
748
+ }
749
+ function connectToSocket() {
750
+ return new Promise((resolve) => {
751
+ if (_socket && _connected) {
752
+ resolve(true);
753
+ return;
754
+ }
755
+ const socket = net.createConnection({ path: SOCKET_PATH });
756
+ const connectTimeout = setTimeout(() => {
757
+ socket.destroy();
758
+ resolve(false);
759
+ }, 2e3);
760
+ socket.on("connect", () => {
761
+ clearTimeout(connectTimeout);
762
+ _socket = socket;
763
+ _connected = true;
764
+ _buffer = "";
765
+ socket.on("data", handleData);
766
+ socket.on("close", () => {
767
+ _connected = false;
768
+ _socket = null;
769
+ for (const [id, entry] of _pending) {
770
+ clearTimeout(entry.timer);
771
+ _pending.delete(id);
772
+ entry.resolve({ error: "Connection closed" });
773
+ }
774
+ });
775
+ socket.on("error", () => {
776
+ _connected = false;
777
+ _socket = null;
778
+ });
779
+ resolve(true);
780
+ });
781
+ socket.on("error", () => {
782
+ clearTimeout(connectTimeout);
783
+ resolve(false);
784
+ });
785
+ });
786
+ }
787
+ async function connectEmbedDaemon() {
788
+ if (_socket && _connected) return true;
789
+ if (await connectToSocket()) return true;
790
+ if (acquireSpawnLock()) {
791
+ try {
792
+ cleanupStaleFiles();
793
+ spawnDaemon();
794
+ } finally {
795
+ releaseSpawnLock();
796
+ }
797
+ }
798
+ const start = Date.now();
799
+ let delay2 = 100;
800
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
801
+ await new Promise((r) => setTimeout(r, delay2));
802
+ if (await connectToSocket()) return true;
803
+ delay2 = Math.min(delay2 * 2, 3e3);
804
+ }
805
+ return false;
806
+ }
807
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
808
+ return new Promise((resolve) => {
809
+ if (!_socket || !_connected) {
810
+ resolve({ error: "Not connected" });
811
+ return;
812
+ }
813
+ const id = randomUUID();
814
+ const timer = setTimeout(() => {
815
+ _pending.delete(id);
816
+ resolve({ error: "Request timeout" });
817
+ }, timeoutMs);
818
+ _pending.set(id, { resolve, timer });
819
+ try {
820
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
821
+ } catch {
822
+ clearTimeout(timer);
823
+ _pending.delete(id);
824
+ resolve({ error: "Write failed" });
825
+ }
826
+ });
827
+ }
828
+ function isClientConnected() {
829
+ return _connected;
830
+ }
831
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
832
+ var init_exe_daemon_client = __esm({
833
+ "src/lib/exe-daemon-client.ts"() {
834
+ "use strict";
835
+ init_config();
836
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path6.join(EXE_AI_DIR, "exed.sock");
837
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path6.join(EXE_AI_DIR, "exed.pid");
838
+ SPAWN_LOCK_PATH = path6.join(EXE_AI_DIR, "exed-spawn.lock");
839
+ SPAWN_LOCK_STALE_MS = 3e4;
840
+ CONNECT_TIMEOUT_MS = 15e3;
841
+ REQUEST_TIMEOUT_MS = 3e4;
842
+ _socket = null;
843
+ _connected = false;
844
+ _buffer = "";
845
+ _pending = /* @__PURE__ */ new Map();
846
+ MAX_BUFFER = 1e7;
847
+ }
848
+ });
849
+
850
+ // src/lib/daemon-protocol.ts
851
+ function serializeValue(v) {
852
+ if (v === null || v === void 0) return null;
853
+ if (typeof v === "bigint") return Number(v);
854
+ if (typeof v === "boolean") return v ? 1 : 0;
855
+ if (v instanceof Uint8Array) {
856
+ return { __blob: Buffer.from(v).toString("base64") };
857
+ }
858
+ if (ArrayBuffer.isView(v)) {
859
+ return { __blob: Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64") };
860
+ }
861
+ if (v instanceof ArrayBuffer) {
862
+ return { __blob: Buffer.from(v).toString("base64") };
863
+ }
864
+ if (typeof v === "string" || typeof v === "number") return v;
865
+ return String(v);
866
+ }
867
+ function deserializeValue(v) {
868
+ if (v === null) return null;
869
+ if (typeof v === "object" && v !== null && "__blob" in v) {
870
+ const buf = Buffer.from(v.__blob, "base64");
871
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
872
+ }
873
+ return v;
874
+ }
875
+ function deserializeResultSet(srs) {
876
+ const rows = srs.rows.map((obj) => {
877
+ const values = srs.columns.map(
878
+ (col) => deserializeValue(obj[col] ?? null)
879
+ );
880
+ const row = values;
881
+ for (let i = 0; i < srs.columns.length; i++) {
882
+ const col = srs.columns[i];
883
+ if (col !== void 0) {
884
+ row[col] = values[i] ?? null;
885
+ }
886
+ }
887
+ Object.defineProperty(row, "length", {
888
+ value: values.length,
889
+ enumerable: false
890
+ });
891
+ return row;
892
+ });
893
+ return {
894
+ columns: srs.columns,
895
+ columnTypes: srs.columnTypes ?? [],
896
+ rows,
897
+ rowsAffected: srs.rowsAffected,
898
+ lastInsertRowid: srs.lastInsertRowid != null ? BigInt(srs.lastInsertRowid) : void 0,
899
+ toJSON: () => ({
900
+ columns: srs.columns,
901
+ columnTypes: srs.columnTypes ?? [],
902
+ rows: srs.rows,
903
+ rowsAffected: srs.rowsAffected,
904
+ lastInsertRowid: srs.lastInsertRowid
905
+ })
906
+ };
907
+ }
908
+ var init_daemon_protocol = __esm({
909
+ "src/lib/daemon-protocol.ts"() {
910
+ "use strict";
911
+ }
912
+ });
913
+
914
+ // src/lib/db-daemon-client.ts
915
+ var db_daemon_client_exports = {};
916
+ __export(db_daemon_client_exports, {
917
+ createDaemonDbClient: () => createDaemonDbClient,
918
+ initDaemonDbClient: () => initDaemonDbClient
919
+ });
920
+ function normalizeStatement(stmt) {
921
+ if (typeof stmt === "string") {
922
+ return { sql: stmt, args: [] };
923
+ }
924
+ const sql = stmt.sql;
925
+ let args = [];
926
+ if (Array.isArray(stmt.args)) {
927
+ args = stmt.args.map((v) => serializeValue(v));
928
+ } else if (stmt.args && typeof stmt.args === "object") {
929
+ const named = {};
930
+ for (const [key, val] of Object.entries(stmt.args)) {
931
+ named[key] = serializeValue(val);
932
+ }
933
+ return { sql, args: named };
934
+ }
935
+ return { sql, args };
936
+ }
937
+ function createDaemonDbClient(fallbackClient) {
938
+ let _useDaemon = false;
939
+ const client = {
940
+ async execute(stmt) {
941
+ if (!_useDaemon || !isClientConnected()) {
942
+ return fallbackClient.execute(stmt);
943
+ }
944
+ const { sql, args } = normalizeStatement(stmt);
945
+ const response = await sendDaemonRequest({
946
+ type: "db-execute",
947
+ sql,
948
+ args
949
+ });
950
+ if (response.error) {
951
+ const errMsg = String(response.error);
952
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
953
+ process.stderr.write(`[db-daemon] Transport error (${errMsg}), falling back to direct
954
+ `);
955
+ return fallbackClient.execute(stmt);
956
+ }
957
+ throw new Error(errMsg);
958
+ }
959
+ if (response.db) {
960
+ return deserializeResultSet(response.db);
961
+ }
962
+ process.stderr.write("[db-daemon] Unexpected response shape, falling back to direct\n");
963
+ return fallbackClient.execute(stmt);
964
+ },
965
+ async batch(stmts, mode) {
966
+ if (!_useDaemon || !isClientConnected()) {
967
+ return fallbackClient.batch(stmts, mode);
968
+ }
969
+ const statements = stmts.map(normalizeStatement);
970
+ const response = await sendDaemonRequest({
971
+ type: "db-batch",
972
+ statements,
973
+ mode: mode ?? "deferred"
974
+ });
975
+ if (response.error) {
976
+ const errMsg = String(response.error);
977
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
978
+ process.stderr.write(`[db-daemon] Batch transport error (${errMsg}), falling back to direct
979
+ `);
980
+ return fallbackClient.batch(stmts, mode);
981
+ }
982
+ throw new Error(errMsg);
983
+ }
984
+ const batchResults = response["db-batch"];
985
+ if (batchResults) {
986
+ return batchResults.map(deserializeResultSet);
987
+ }
988
+ process.stderr.write("[db-daemon] Unexpected batch response shape, falling back to direct\n");
989
+ return fallbackClient.batch(stmts, mode);
990
+ },
991
+ // Transaction support — delegate to fallback (transactions need direct connection)
992
+ async transaction(mode) {
993
+ return fallbackClient.transaction(mode);
994
+ },
995
+ // executeMultiple — delegate to fallback (used only for schema migrations)
996
+ async executeMultiple(sql) {
997
+ return fallbackClient.executeMultiple(sql);
998
+ },
999
+ // migrate — delegate to fallback
1000
+ async migrate(stmts) {
1001
+ return fallbackClient.migrate(stmts);
1002
+ },
1003
+ // Sync mode — delegate to fallback
1004
+ sync() {
1005
+ return fallbackClient.sync();
1006
+ },
1007
+ close() {
1008
+ _useDaemon = false;
1009
+ },
1010
+ get closed() {
1011
+ return fallbackClient.closed;
1012
+ },
1013
+ get protocol() {
1014
+ return fallbackClient.protocol;
1015
+ }
1016
+ };
1017
+ return {
1018
+ ...client,
1019
+ /** Enable daemon routing (call after confirming daemon is connected) */
1020
+ _enableDaemon() {
1021
+ _useDaemon = true;
1022
+ },
1023
+ /** Check if daemon routing is active */
1024
+ _isDaemonActive() {
1025
+ return _useDaemon && isClientConnected();
1026
+ }
1027
+ };
1028
+ }
1029
+ async function initDaemonDbClient(fallbackClient) {
1030
+ if (process.env.EXE_IS_DAEMON === "1") return null;
1031
+ const connected = await connectEmbedDaemon();
1032
+ if (!connected) {
1033
+ process.stderr.write("[db-daemon] Daemon unavailable \u2014 using direct SQLite\n");
1034
+ return null;
1035
+ }
1036
+ const client = createDaemonDbClient(fallbackClient);
1037
+ client._enableDaemon();
1038
+ process.stderr.write("[db-daemon] DB routing through daemon (single-writer)\n");
1039
+ return client;
1040
+ }
1041
+ var init_db_daemon_client = __esm({
1042
+ "src/lib/db-daemon-client.ts"() {
1043
+ "use strict";
1044
+ init_exe_daemon_client();
1045
+ init_daemon_protocol();
1046
+ }
1047
+ });
1048
+
503
1049
  // src/lib/database.ts
504
1050
  var database_exports = {};
505
1051
  __export(database_exports, {
@@ -508,6 +1054,7 @@ __export(database_exports, {
508
1054
  ensureSchema: () => ensureSchema,
509
1055
  getClient: () => getClient,
510
1056
  getRawClient: () => getRawClient,
1057
+ initDaemonClient: () => initDaemonClient,
511
1058
  initDatabase: () => initDatabase,
512
1059
  initTurso: () => initTurso,
513
1060
  isInitialized: () => isInitialized
@@ -535,8 +1082,27 @@ function getClient() {
535
1082
  if (!_resilientClient) {
536
1083
  throw new Error("Database client not initialized. Call initDatabase() first.");
537
1084
  }
1085
+ if (process.env.EXE_IS_DAEMON === "1") {
1086
+ return _resilientClient;
1087
+ }
1088
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
1089
+ return _daemonClient;
1090
+ }
538
1091
  return _resilientClient;
539
1092
  }
1093
+ async function initDaemonClient() {
1094
+ if (process.env.EXE_IS_DAEMON === "1") return;
1095
+ if (!_resilientClient) return;
1096
+ try {
1097
+ const { initDaemonDbClient: initDaemonDbClient2 } = await Promise.resolve().then(() => (init_db_daemon_client(), db_daemon_client_exports));
1098
+ _daemonClient = await initDaemonDbClient2(_resilientClient);
1099
+ } catch (err) {
1100
+ process.stderr.write(
1101
+ `[database] Daemon client init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}
1102
+ `
1103
+ );
1104
+ }
1105
+ }
540
1106
  function getRawClient() {
541
1107
  if (!_client) {
542
1108
  throw new Error("Database client not initialized. Call initDatabase() first.");
@@ -1023,6 +1589,12 @@ async function ensureSchema() {
1023
1589
  } catch {
1024
1590
  }
1025
1591
  }
1592
+ try {
1593
+ await client.execute(
1594
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1595
+ );
1596
+ } catch {
1597
+ }
1026
1598
  await client.executeMultiple(`
1027
1599
  CREATE TABLE IF NOT EXISTS entities (
1028
1600
  id TEXT PRIMARY KEY,
@@ -1075,7 +1647,30 @@ async function ensureSchema() {
1075
1647
  entity_id TEXT NOT NULL,
1076
1648
  PRIMARY KEY (hyperedge_id, entity_id)
1077
1649
  );
1650
+
1651
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1652
+ name,
1653
+ content=entities,
1654
+ content_rowid=rowid
1655
+ );
1656
+
1657
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1658
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1659
+ END;
1660
+
1661
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1662
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1663
+ END;
1664
+
1665
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1666
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1667
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1668
+ END;
1078
1669
  `);
1670
+ try {
1671
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1672
+ } catch {
1673
+ }
1079
1674
  await client.executeMultiple(`
1080
1675
  CREATE TABLE IF NOT EXISTS entity_aliases (
1081
1676
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1256,6 +1851,33 @@ async function ensureSchema() {
1256
1851
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1257
1852
  ON conversations(channel_id);
1258
1853
  `);
1854
+ await client.executeMultiple(`
1855
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1856
+ session_uuid TEXT PRIMARY KEY,
1857
+ agent_id TEXT NOT NULL,
1858
+ session_name TEXT,
1859
+ task_id TEXT,
1860
+ project_name TEXT,
1861
+ started_at TEXT NOT NULL
1862
+ );
1863
+
1864
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1865
+ ON session_agent_map(agent_id);
1866
+ `);
1867
+ try {
1868
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1869
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1870
+ await client.execute({
1871
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1872
+ SELECT session_id, agent_id, '', MIN(timestamp)
1873
+ FROM memories
1874
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1875
+ GROUP BY session_id, agent_id`,
1876
+ args: []
1877
+ });
1878
+ }
1879
+ } catch {
1880
+ }
1259
1881
  try {
1260
1882
  await client.execute({
1261
1883
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1389,15 +2011,41 @@ async function ensureSchema() {
1389
2011
  });
1390
2012
  } catch {
1391
2013
  }
2014
+ for (const col of [
2015
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
2016
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
2017
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
2018
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
2019
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
2020
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
2021
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
2022
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
2023
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
2024
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
2025
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
2026
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
2027
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
2028
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
2029
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
2030
+ ]) {
2031
+ try {
2032
+ await client.execute(col);
2033
+ } catch {
2034
+ }
2035
+ }
1392
2036
  }
1393
2037
  async function disposeDatabase() {
2038
+ if (_daemonClient) {
2039
+ _daemonClient.close();
2040
+ _daemonClient = null;
2041
+ }
1394
2042
  if (_client) {
1395
2043
  _client.close();
1396
2044
  _client = null;
1397
2045
  _resilientClient = null;
1398
2046
  }
1399
2047
  }
1400
- var _client, _resilientClient, initTurso, disposeTurso;
2048
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1401
2049
  var init_database = __esm({
1402
2050
  "src/lib/database.ts"() {
1403
2051
  "use strict";
@@ -1405,31 +2053,96 @@ var init_database = __esm({
1405
2053
  init_employees();
1406
2054
  _client = null;
1407
2055
  _resilientClient = null;
2056
+ _daemonClient = null;
1408
2057
  initTurso = initDatabase;
1409
2058
  disposeTurso = disposeDatabase;
1410
2059
  }
1411
2060
  });
1412
2061
 
1413
2062
  // 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";
2063
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
2064
+ import { randomUUID as randomUUID2 } from "crypto";
2065
+ import path7 from "path";
1417
2066
  import { jwtVerify, importSPKI } from "jose";
1418
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
2067
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1419
2068
  var init_license = __esm({
1420
2069
  "src/lib/license.ts"() {
1421
2070
  "use strict";
1422
2071
  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");
2072
+ LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
2073
+ CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
2074
+ DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
2075
+ PLAN_LIMITS = {
2076
+ free: { devices: 1, employees: 1, memories: 5e3 },
2077
+ pro: { devices: 3, employees: 5, memories: 1e5 },
2078
+ team: { devices: 10, employees: 20, memories: 1e6 },
2079
+ agency: { devices: 50, employees: 100, memories: 1e7 },
2080
+ enterprise: { devices: -1, employees: -1, memories: -1 }
2081
+ };
1426
2082
  }
1427
2083
  });
1428
2084
 
1429
2085
  // src/lib/plan-limits.ts
1430
- import { readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
1431
- import path7 from "path";
1432
- var CACHE_PATH2;
2086
+ import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
2087
+ import path8 from "path";
2088
+ function getLicenseSync() {
2089
+ try {
2090
+ if (!existsSync7(CACHE_PATH2)) return freeLicense();
2091
+ const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
2092
+ if (!raw.token || typeof raw.token !== "string") return freeLicense();
2093
+ const parts = raw.token.split(".");
2094
+ if (parts.length !== 3) return freeLicense();
2095
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
2096
+ const plan = payload.plan ?? "free";
2097
+ const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
2098
+ return {
2099
+ valid: true,
2100
+ plan,
2101
+ email: payload.sub ?? "",
2102
+ expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
2103
+ deviceLimit: limits.devices,
2104
+ employeeLimit: limits.employees,
2105
+ memoryLimit: limits.memories
2106
+ };
2107
+ } catch {
2108
+ return freeLicense();
2109
+ }
2110
+ }
2111
+ function freeLicense() {
2112
+ const limits = PLAN_LIMITS.free;
2113
+ return {
2114
+ valid: true,
2115
+ plan: "free",
2116
+ email: "",
2117
+ expiresAt: null,
2118
+ deviceLimit: limits.devices,
2119
+ employeeLimit: limits.employees,
2120
+ memoryLimit: limits.memories
2121
+ };
2122
+ }
2123
+ function assertEmployeeLimitSync(rosterPath) {
2124
+ const license = getLicenseSync();
2125
+ if (license.employeeLimit < 0) return;
2126
+ const filePath = rosterPath ?? EMPLOYEES_PATH;
2127
+ let count = 0;
2128
+ try {
2129
+ if (existsSync7(filePath)) {
2130
+ const raw = readFileSync8(filePath, "utf8");
2131
+ const employees = JSON.parse(raw);
2132
+ count = Array.isArray(employees) ? employees.length : 0;
2133
+ }
2134
+ } catch {
2135
+ throw new PlanLimitError(
2136
+ `Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
2137
+ );
2138
+ }
2139
+ if (count >= license.employeeLimit) {
2140
+ throw new PlanLimitError(
2141
+ `Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
2142
+ );
2143
+ }
2144
+ }
2145
+ var PlanLimitError, CACHE_PATH2;
1433
2146
  var init_plan_limits = __esm({
1434
2147
  "src/lib/plan-limits.ts"() {
1435
2148
  "use strict";
@@ -1437,32 +2150,2359 @@ var init_plan_limits = __esm({
1437
2150
  init_employees();
1438
2151
  init_license();
1439
2152
  init_config();
1440
- CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
2153
+ PlanLimitError = class extends Error {
2154
+ constructor(message) {
2155
+ super(message);
2156
+ this.name = "PlanLimitError";
2157
+ }
2158
+ };
2159
+ CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
1441
2160
  }
1442
2161
  });
1443
2162
 
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";
2163
+ // src/lib/notifications.ts
2164
+ var notifications_exports = {};
2165
+ __export(notifications_exports, {
2166
+ cleanupOldNotifications: () => cleanupOldNotifications,
2167
+ formatNotifications: () => formatNotifications,
2168
+ markAsRead: () => markAsRead,
2169
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
2170
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
2171
+ migrateJsonNotifications: () => migrateJsonNotifications,
2172
+ readUnreadNotifications: () => readUnreadNotifications,
2173
+ writeNotification: () => writeNotification
2174
+ });
2175
+ import crypto from "crypto";
2176
+ import path9 from "path";
1447
2177
  import os5 from "os";
1448
- import { fileURLToPath } from "url";
1449
- function getMySession() {
1450
- return getTransport().getMySession();
1451
- }
1452
- function extractRootExe(name) {
1453
- if (!name) return null;
1454
- if (!name.includes("-")) return name;
2178
+ import {
2179
+ readFileSync as readFileSync9,
2180
+ readdirSync as readdirSync2,
2181
+ unlinkSync as unlinkSync4,
2182
+ existsSync as existsSync8,
2183
+ rmdirSync
2184
+ } from "fs";
2185
+ async function writeNotification(notification) {
2186
+ try {
2187
+ const client = getClient();
2188
+ const id = crypto.randomUUID();
2189
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2190
+ await client.execute({
2191
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
2192
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2193
+ args: [
2194
+ id,
2195
+ notification.agentId,
2196
+ notification.agentRole,
2197
+ notification.event,
2198
+ notification.project,
2199
+ notification.summary,
2200
+ notification.taskFile ?? null,
2201
+ now
2202
+ ]
2203
+ });
2204
+ } catch (err) {
2205
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2206
+ `);
2207
+ }
2208
+ }
2209
+ async function readUnreadNotifications(agentFilter) {
2210
+ try {
2211
+ const client = getClient();
2212
+ const conditions = ["read = 0"];
2213
+ const args = [];
2214
+ if (agentFilter) {
2215
+ conditions.push("agent_id = ?");
2216
+ args.push(agentFilter);
2217
+ }
2218
+ const result = await client.execute({
2219
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, created_at
2220
+ FROM notifications
2221
+ WHERE ${conditions.join(" AND ")}
2222
+ ORDER BY created_at ASC`,
2223
+ args
2224
+ });
2225
+ return result.rows.map((r) => ({
2226
+ id: String(r.id),
2227
+ agentId: String(r.agent_id),
2228
+ agentRole: String(r.agent_role),
2229
+ event: String(r.event),
2230
+ project: String(r.project),
2231
+ summary: String(r.summary),
2232
+ taskFile: r.task_file ? String(r.task_file) : void 0,
2233
+ timestamp: String(r.created_at),
2234
+ read: false
2235
+ }));
2236
+ } catch {
2237
+ return [];
2238
+ }
2239
+ }
2240
+ async function markAsRead(ids) {
2241
+ if (ids.length === 0) return;
2242
+ try {
2243
+ const client = getClient();
2244
+ const placeholders = ids.map(() => "?").join(", ");
2245
+ await client.execute({
2246
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})`,
2247
+ args: ids
2248
+ });
2249
+ } catch {
2250
+ }
2251
+ }
2252
+ async function markAsReadByTaskFile(taskFile) {
2253
+ try {
2254
+ const client = getClient();
2255
+ await client.execute({
2256
+ sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
2257
+ args: [taskFile]
2258
+ });
2259
+ } catch {
2260
+ }
2261
+ }
2262
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS) {
2263
+ try {
2264
+ const client = getClient();
2265
+ const cutoff = new Date(
2266
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
2267
+ ).toISOString();
2268
+ const result = await client.execute({
2269
+ sql: "DELETE FROM notifications WHERE created_at < ?",
2270
+ args: [cutoff]
2271
+ });
2272
+ return result.rowsAffected;
2273
+ } catch {
2274
+ return 0;
2275
+ }
2276
+ }
2277
+ async function markDoneTaskNotificationsAsRead() {
2278
+ try {
2279
+ const client = getClient();
2280
+ const result = await client.execute({
2281
+ sql: `UPDATE notifications SET read = 1
2282
+ WHERE read = 0
2283
+ AND task_file IS NOT NULL
2284
+ AND task_file IN (
2285
+ SELECT task_file FROM tasks WHERE status = 'done'
2286
+ )`,
2287
+ args: []
2288
+ });
2289
+ return result.rowsAffected;
2290
+ } catch {
2291
+ return 0;
2292
+ }
2293
+ }
2294
+ function formatNotifications(notifications) {
2295
+ if (notifications.length === 0) return "";
2296
+ const grouped = /* @__PURE__ */ new Map();
2297
+ for (const n of notifications) {
2298
+ const key = `${n.agentId}|${n.agentRole}`;
2299
+ if (!grouped.has(key)) grouped.set(key, []);
2300
+ grouped.get(key).push(n);
2301
+ }
2302
+ const lines = [];
2303
+ lines.push(`## Notifications (${notifications.length} unread)
2304
+ `);
2305
+ for (const [key, items] of grouped) {
2306
+ const [agentId, agentRole] = key.split("|");
2307
+ lines.push(`**${agentId}** (${agentRole}):`);
2308
+ for (const item of items) {
2309
+ const ago = formatTimeAgo(item.timestamp);
2310
+ const icon = eventIcon(item.event);
2311
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
2312
+ }
2313
+ lines.push("");
2314
+ }
2315
+ return lines.join("\n");
2316
+ }
2317
+ async function migrateJsonNotifications() {
2318
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path9.join(os5.homedir(), ".exe-os");
2319
+ const notifDir = path9.join(base, "notifications");
2320
+ if (!existsSync8(notifDir)) return 0;
2321
+ let migrated = 0;
2322
+ try {
2323
+ const files = readdirSync2(notifDir).filter((f) => f.endsWith(".json"));
2324
+ if (files.length === 0) return 0;
2325
+ const client = getClient();
2326
+ for (const file of files) {
2327
+ try {
2328
+ const filePath = path9.join(notifDir, file);
2329
+ const data = JSON.parse(readFileSync9(filePath, "utf8"));
2330
+ await client.execute({
2331
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
2332
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2333
+ args: [
2334
+ crypto.randomUUID(),
2335
+ data.agentId ?? "unknown",
2336
+ data.agentRole ?? "unknown",
2337
+ data.event ?? "session_summary",
2338
+ data.project ?? "unknown",
2339
+ data.summary ?? "",
2340
+ data.taskFile ?? null,
2341
+ data.read ? 1 : 0,
2342
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2343
+ ]
2344
+ });
2345
+ unlinkSync4(filePath);
2346
+ migrated++;
2347
+ } catch {
2348
+ }
2349
+ }
2350
+ try {
2351
+ const remaining = readdirSync2(notifDir);
2352
+ if (remaining.length === 0) {
2353
+ rmdirSync(notifDir);
2354
+ }
2355
+ } catch {
2356
+ }
2357
+ } catch {
2358
+ }
2359
+ return migrated;
2360
+ }
2361
+ function eventIcon(event) {
2362
+ switch (event) {
2363
+ case "task_complete":
2364
+ return "Completed:";
2365
+ case "task_needs_fix":
2366
+ return "Needs fix:";
2367
+ case "session_summary":
2368
+ return "Session:";
2369
+ case "error_spike":
2370
+ return "Errors:";
2371
+ case "orphan_task":
2372
+ return "Orphan:";
2373
+ case "subtasks_complete":
2374
+ return "Subtasks done:";
2375
+ case "capacity_relaunch":
2376
+ return "Relaunched:";
2377
+ }
2378
+ }
2379
+ function formatTimeAgo(timestamp) {
2380
+ const diffMs = Date.now() - new Date(timestamp).getTime();
2381
+ const mins = Math.floor(diffMs / 6e4);
2382
+ if (mins < 1) return "just now";
2383
+ if (mins < 60) return `${mins}m ago`;
2384
+ const hours = Math.floor(mins / 60);
2385
+ if (hours < 24) return `${hours}h ago`;
2386
+ const days = Math.floor(hours / 24);
2387
+ return `${days}d ago`;
2388
+ }
2389
+ var CLEANUP_DAYS;
2390
+ var init_notifications = __esm({
2391
+ "src/lib/notifications.ts"() {
2392
+ "use strict";
2393
+ init_database();
2394
+ CLEANUP_DAYS = 7;
2395
+ }
2396
+ });
2397
+
2398
+ // src/lib/session-kill-telemetry.ts
2399
+ import crypto2 from "crypto";
2400
+ async function recordSessionKill(input2) {
2401
+ try {
2402
+ const client = getClient();
2403
+ await client.execute({
2404
+ sql: `INSERT INTO session_kills
2405
+ (id, session_name, agent_id, killed_at, reason,
2406
+ ticks_idle, estimated_tokens_saved)
2407
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
2408
+ args: [
2409
+ crypto2.randomUUID(),
2410
+ input2.sessionName,
2411
+ input2.agentId,
2412
+ (/* @__PURE__ */ new Date()).toISOString(),
2413
+ input2.reason,
2414
+ input2.ticksIdle ?? null,
2415
+ input2.estimatedTokensSaved ?? null
2416
+ ]
2417
+ });
2418
+ } catch (err) {
2419
+ process.stderr.write(
2420
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
2421
+ `
2422
+ );
2423
+ }
2424
+ }
2425
+ var init_session_kill_telemetry = __esm({
2426
+ "src/lib/session-kill-telemetry.ts"() {
2427
+ "use strict";
2428
+ init_database();
2429
+ }
2430
+ });
2431
+
2432
+ // src/lib/state-bus.ts
2433
+ var StateBus, orgBus;
2434
+ var init_state_bus = __esm({
2435
+ "src/lib/state-bus.ts"() {
2436
+ "use strict";
2437
+ StateBus = class {
2438
+ handlers = /* @__PURE__ */ new Map();
2439
+ globalHandlers = /* @__PURE__ */ new Set();
2440
+ /** Emit an event to all subscribers */
2441
+ emit(event) {
2442
+ const typeHandlers = this.handlers.get(event.type);
2443
+ if (typeHandlers) {
2444
+ for (const handler of typeHandlers) {
2445
+ try {
2446
+ handler(event);
2447
+ } catch {
2448
+ }
2449
+ }
2450
+ }
2451
+ for (const handler of this.globalHandlers) {
2452
+ try {
2453
+ handler(event);
2454
+ } catch {
2455
+ }
2456
+ }
2457
+ }
2458
+ /** Subscribe to a specific event type */
2459
+ on(type, handler) {
2460
+ if (!this.handlers.has(type)) {
2461
+ this.handlers.set(type, /* @__PURE__ */ new Set());
2462
+ }
2463
+ this.handlers.get(type).add(handler);
2464
+ }
2465
+ /** Subscribe to ALL events */
2466
+ onAny(handler) {
2467
+ this.globalHandlers.add(handler);
2468
+ }
2469
+ /** Unsubscribe from a specific event type */
2470
+ off(type, handler) {
2471
+ this.handlers.get(type)?.delete(handler);
2472
+ }
2473
+ /** Unsubscribe from ALL events */
2474
+ offAny(handler) {
2475
+ this.globalHandlers.delete(handler);
2476
+ }
2477
+ /** Remove all listeners */
2478
+ clear() {
2479
+ this.handlers.clear();
2480
+ this.globalHandlers.clear();
2481
+ }
2482
+ };
2483
+ orgBus = new StateBus();
2484
+ }
2485
+ });
2486
+
2487
+ // src/lib/tasks-crud.ts
2488
+ var tasks_crud_exports = {};
2489
+ __export(tasks_crud_exports, {
2490
+ TASK_ALREADY_CLAIMED_PREFIX: () => TASK_ALREADY_CLAIMED_PREFIX,
2491
+ checkStaleCompletion: () => checkStaleCompletion,
2492
+ createTaskCore: () => createTaskCore,
2493
+ deleteTaskCore: () => deleteTaskCore,
2494
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
2495
+ ensureGitignoreExe: () => ensureGitignoreExe,
2496
+ extractParentFromContext: () => extractParentFromContext,
2497
+ isTmuxSessionAlive: () => isTmuxSessionAlive,
2498
+ listTasks: () => listTasks,
2499
+ resolveTask: () => resolveTask,
2500
+ slugify: () => slugify,
2501
+ updateTaskStatus: () => updateTaskStatus,
2502
+ writeCheckpoint: () => writeCheckpoint
2503
+ });
2504
+ import crypto3 from "crypto";
2505
+ import path10 from "path";
2506
+ import os6 from "os";
2507
+ import { execSync as execSync5 } from "child_process";
2508
+ import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2509
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
2510
+ async function writeCheckpoint(input2) {
2511
+ const client = getClient();
2512
+ const row = await resolveTask(client, input2.taskId);
2513
+ const taskId = String(row.id);
2514
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2515
+ const blockedByIds = [];
2516
+ if (row.blocked_by) {
2517
+ blockedByIds.push(String(row.blocked_by));
2518
+ }
2519
+ const checkpoint = {
2520
+ step: input2.step,
2521
+ context_summary: input2.contextSummary,
2522
+ files_touched: input2.filesTouched ?? [],
2523
+ blocked_by_ids: blockedByIds,
2524
+ last_checkpoint_at: now
2525
+ };
2526
+ const result = await client.execute({
2527
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
2528
+ args: [JSON.stringify(checkpoint), now, taskId]
2529
+ });
2530
+ if (result.rowsAffected === 0) {
2531
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
2532
+ }
2533
+ const countResult = await client.execute({
2534
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
2535
+ args: [taskId]
2536
+ });
2537
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
2538
+ return { checkpointCount };
2539
+ }
2540
+ function extractParentFromContext(contextBody) {
2541
+ if (!contextBody) return null;
2542
+ const match = contextBody.match(
2543
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
2544
+ );
2545
+ return match ? match[1].toLowerCase() : null;
2546
+ }
2547
+ function slugify(title) {
2548
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2549
+ }
2550
+ function buildKeywordIndex() {
2551
+ const idx = /* @__PURE__ */ new Map();
2552
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
2553
+ for (const kw of keywords) {
2554
+ const existing = idx.get(kw) ?? [];
2555
+ existing.push(role);
2556
+ idx.set(kw, existing);
2557
+ }
2558
+ }
2559
+ return idx;
2560
+ }
2561
+ function checkLaneAffinity(title, context, assigneeName) {
2562
+ const employees = loadEmployeesSync();
2563
+ const employee = employees.find((e) => e.name === assigneeName);
2564
+ if (!employee) return void 0;
2565
+ const assigneeRole = employee.role;
2566
+ const text = `${title} ${context}`.toLowerCase();
2567
+ const matchedRoles = /* @__PURE__ */ new Set();
2568
+ for (const [keyword, roles] of KEYWORD_INDEX) {
2569
+ if (text.includes(keyword)) {
2570
+ for (const role of roles) matchedRoles.add(role);
2571
+ }
2572
+ }
2573
+ if (matchedRoles.size === 0) return void 0;
2574
+ if (matchedRoles.has(assigneeRole)) return void 0;
2575
+ if (assigneeRole === "COO") return void 0;
2576
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
2577
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
2578
+ }
2579
+ async function resolveTask(client, identifier, scopeSession) {
2580
+ const scope = sessionScopeFilter(scopeSession);
2581
+ let result = await client.execute({
2582
+ sql: `SELECT * FROM tasks WHERE id = ?${scope.sql}`,
2583
+ args: [identifier, ...scope.args]
2584
+ });
2585
+ if (result.rows.length === 1) return result.rows[0];
2586
+ result = await client.execute({
2587
+ sql: `SELECT * FROM tasks WHERE task_file LIKE ?${scope.sql}`,
2588
+ args: [`%${identifier}%`, ...scope.args]
2589
+ });
2590
+ if (result.rows.length === 1) return result.rows[0];
2591
+ if (result.rows.length > 1) {
2592
+ const exact = result.rows.filter(
2593
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
2594
+ );
2595
+ if (exact.length === 1) return exact[0];
2596
+ const candidates = exact.length > 1 ? exact : result.rows;
2597
+ const active = candidates.filter(
2598
+ (r) => !["done", "cancelled"].includes(String(r.status))
2599
+ );
2600
+ if (active.length === 1) return active[0];
2601
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
2602
+ throw new Error(
2603
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
2604
+ );
2605
+ }
2606
+ result = await client.execute({
2607
+ sql: `SELECT * FROM tasks WHERE title LIKE ?${scope.sql}`,
2608
+ args: [`%${identifier}%`, ...scope.args]
2609
+ });
2610
+ if (result.rows.length === 1) return result.rows[0];
2611
+ if (result.rows.length > 1) {
2612
+ const active = result.rows.filter(
2613
+ (r) => !["done", "cancelled"].includes(String(r.status))
2614
+ );
2615
+ if (active.length === 1) return active[0];
2616
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
2617
+ throw new Error(
2618
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
2619
+ );
2620
+ }
2621
+ throw new Error(`Task not found: ${identifier}`);
2622
+ }
2623
+ async function createTaskCore(input2) {
2624
+ const client = getClient();
2625
+ const id = crypto3.randomUUID();
2626
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2627
+ const slug = slugify(input2.title);
2628
+ let earlySessionScope = null;
2629
+ try {
2630
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2631
+ earlySessionScope = resolveExeSession2();
2632
+ } catch {
2633
+ }
2634
+ const scope = earlySessionScope ?? "default";
2635
+ const taskFile = input2.taskFile ?? `tasks/${scope}/${input2.assignedTo}/${slug}.md`;
2636
+ let blockedById = null;
2637
+ const initialStatus = input2.blockedBy ? "blocked" : "open";
2638
+ if (input2.blockedBy) {
2639
+ const blocker = await resolveTask(client, input2.blockedBy);
2640
+ blockedById = String(blocker.id);
2641
+ }
2642
+ let parentTaskId = null;
2643
+ let parentRef = input2.parentTaskId;
2644
+ if (!parentRef) {
2645
+ const extracted = extractParentFromContext(input2.context);
2646
+ if (extracted) {
2647
+ parentRef = extracted;
2648
+ process.stderr.write(
2649
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
2650
+ );
2651
+ }
2652
+ }
2653
+ if (parentRef) {
2654
+ try {
2655
+ const parent = await resolveTask(client, parentRef);
2656
+ parentTaskId = String(parent.id);
2657
+ } catch (err) {
2658
+ if (!input2.parentTaskId) {
2659
+ throw new Error(
2660
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
2661
+ );
2662
+ }
2663
+ throw err;
2664
+ }
2665
+ }
2666
+ let warning;
2667
+ const dupScope = sessionScopeFilter();
2668
+ const dupCheck = await client.execute({
2669
+ sql: `SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')${dupScope.sql}`,
2670
+ args: [input2.title, input2.assignedTo, ...dupScope.args]
2671
+ });
2672
+ if (dupCheck.rows.length > 0) {
2673
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
2674
+ }
2675
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2676
+ const laneWarning = checkLaneAffinity(input2.title, input2.context, input2.assignedTo);
2677
+ if (laneWarning) {
2678
+ warning = warning ? `${warning}
2679
+ ${laneWarning}` : laneWarning;
2680
+ }
2681
+ }
2682
+ if (input2.baseDir) {
2683
+ try {
2684
+ await mkdir3(path10.join(input2.baseDir, "exe", "output"), { recursive: true });
2685
+ await mkdir3(path10.join(input2.baseDir, "exe", "research"), { recursive: true });
2686
+ await ensureArchitectureDoc(input2.baseDir, input2.projectName);
2687
+ await ensureGitignoreExe(input2.baseDir);
2688
+ } catch {
2689
+ }
2690
+ }
2691
+ const complexity = input2.complexity ?? "standard";
2692
+ const sessionScope = earlySessionScope;
2693
+ await client.execute({
2694
+ 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)
2695
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2696
+ args: [
2697
+ id,
2698
+ input2.title,
2699
+ input2.assignedTo,
2700
+ input2.assignedBy,
2701
+ input2.projectName,
2702
+ input2.priority,
2703
+ initialStatus,
2704
+ taskFile,
2705
+ blockedById,
2706
+ parentTaskId,
2707
+ input2.reviewer ?? null,
2708
+ input2.context,
2709
+ complexity,
2710
+ input2.budgetTokens ?? null,
2711
+ input2.budgetFallbackModel ?? null,
2712
+ 0,
2713
+ null,
2714
+ sessionScope,
2715
+ now,
2716
+ now
2717
+ ]
2718
+ });
2719
+ if (input2.baseDir) {
2720
+ try {
2721
+ const EXE_OS_DIR = path10.join(os6.homedir(), ".exe-os");
2722
+ const mdPath = path10.join(EXE_OS_DIR, taskFile);
2723
+ const mdDir = path10.dirname(mdPath);
2724
+ if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
2725
+ const reviewer = input2.reviewer ?? input2.assignedBy;
2726
+ const mdContent = `# ${input2.title}
2727
+
2728
+ **ID:** ${id}
2729
+ **Status:** ${initialStatus}
2730
+ **Priority:** ${input2.priority}
2731
+ **Assigned by:** ${input2.assignedBy}
2732
+ **Assigned to:** ${input2.assignedTo}
2733
+ **Project:** ${input2.projectName}
2734
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2735
+ **Parent task:** ${parentTaskId}` : ""}
2736
+ **Reviewer:** ${reviewer}
2737
+
2738
+ ## Context
2739
+
2740
+ ${input2.context}
2741
+
2742
+ ## MANDATORY: When done
2743
+
2744
+ You MUST call update_task with status "done" and a result summary when finished.
2745
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2746
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2747
+ `;
2748
+ await writeFile3(mdPath, mdContent, "utf-8");
2749
+ } catch {
2750
+ }
2751
+ }
2752
+ return {
2753
+ id,
2754
+ title: input2.title,
2755
+ assignedTo: input2.assignedTo,
2756
+ assignedBy: input2.assignedBy,
2757
+ projectName: input2.projectName,
2758
+ priority: input2.priority,
2759
+ status: initialStatus,
2760
+ taskFile,
2761
+ createdAt: now,
2762
+ updatedAt: now,
2763
+ warning,
2764
+ budgetTokens: input2.budgetTokens ?? null,
2765
+ budgetFallbackModel: input2.budgetFallbackModel ?? null,
2766
+ tokensUsed: 0,
2767
+ tokensWarnedAt: null
2768
+ };
2769
+ }
2770
+ async function listTasks(input2) {
2771
+ const client = getClient();
2772
+ const conditions = [];
2773
+ const args = [];
2774
+ if (input2.assignedTo) {
2775
+ conditions.push("assigned_to = ?");
2776
+ args.push(input2.assignedTo);
2777
+ }
2778
+ if (input2.status) {
2779
+ conditions.push("status = ?");
2780
+ args.push(input2.status);
2781
+ } else {
2782
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
2783
+ }
2784
+ if (input2.projectName) {
2785
+ conditions.push("project_name = ?");
2786
+ args.push(input2.projectName);
2787
+ }
2788
+ if (input2.priority) {
2789
+ conditions.push("priority = ?");
2790
+ args.push(input2.priority);
2791
+ }
2792
+ const scope = sessionScopeFilter();
2793
+ if (scope.sql) {
2794
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
2795
+ args.push(...scope.args);
2796
+ }
2797
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2798
+ const result = await client.execute({
2799
+ 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`,
2800
+ args
2801
+ });
2802
+ return result.rows.map((r) => ({
2803
+ id: String(r.id),
2804
+ title: String(r.title),
2805
+ assignedTo: String(r.assigned_to),
2806
+ assignedBy: String(r.assigned_by),
2807
+ projectName: String(r.project_name),
2808
+ priority: String(r.priority),
2809
+ status: String(r.status),
2810
+ taskFile: String(r.task_file),
2811
+ createdAt: String(r.created_at),
2812
+ updatedAt: String(r.updated_at),
2813
+ checkpointCount: Number(r.checkpoint_count ?? 0),
2814
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
2815
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
2816
+ tokensUsed: Number(r.tokens_used ?? 0),
2817
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2818
+ }));
2819
+ }
2820
+ function isTmuxSessionAlive(identifier) {
2821
+ if (!identifier || identifier === "unknown") return true;
2822
+ try {
2823
+ if (identifier.startsWith("%")) {
2824
+ const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
2825
+ timeout: 2e3,
2826
+ encoding: "utf8",
2827
+ stdio: ["pipe", "pipe", "pipe"]
2828
+ });
2829
+ return output.split("\n").some((l) => l.trim() === identifier);
2830
+ } else {
2831
+ execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2832
+ timeout: 2e3,
2833
+ stdio: ["pipe", "pipe", "pipe"]
2834
+ });
2835
+ return true;
2836
+ }
2837
+ } catch {
2838
+ if (identifier.startsWith("%")) return true;
2839
+ try {
2840
+ execSync5("tmux list-sessions", {
2841
+ timeout: 2e3,
2842
+ stdio: ["pipe", "pipe", "pipe"]
2843
+ });
2844
+ return false;
2845
+ } catch {
2846
+ return true;
2847
+ }
2848
+ }
2849
+ }
2850
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
2851
+ if (!taskContext) return null;
2852
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
2853
+ try {
2854
+ const since = new Date(taskCreatedAt).toISOString();
2855
+ const branch = execSync5(
2856
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
2857
+ { encoding: "utf8", timeout: 3e3 }
2858
+ ).trim();
2859
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
2860
+ const commitCount = execSync5(
2861
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
2862
+ { encoding: "utf8", timeout: 5e3 }
2863
+ ).trim();
2864
+ const count = parseInt(commitCount, 10);
2865
+ if (count === 0) {
2866
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
2867
+ }
2868
+ return null;
2869
+ } catch {
2870
+ return null;
2871
+ }
2872
+ }
2873
+ async function updateTaskStatus(input2) {
2874
+ const client = getClient();
2875
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2876
+ const row = await resolveTask(client, input2.taskId);
2877
+ const taskId = String(row.id);
2878
+ const taskFile = String(row.task_file);
2879
+ if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
2880
+ process.stderr.write(
2881
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
2882
+ `
2883
+ );
2884
+ }
2885
+ if (input2.status === "done") {
2886
+ const existingRow = await client.execute({
2887
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
2888
+ args: [taskId]
2889
+ });
2890
+ if (existingRow.rows.length > 0) {
2891
+ const ctx = existingRow.rows[0];
2892
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
2893
+ if (warning) {
2894
+ input2.result = input2.result ? `\u26A0\uFE0F ${warning}
2895
+
2896
+ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
2897
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
2898
+ `);
2899
+ }
2900
+ }
2901
+ }
2902
+ if (input2.status === "in_progress") {
2903
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
2904
+ const claim = await client.execute({
2905
+ sql: `UPDATE tasks
2906
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
2907
+ WHERE id = ? AND status = 'open'`,
2908
+ args: [tmuxSession, now, taskId]
2909
+ });
2910
+ if (claim.rowsAffected === 0) {
2911
+ const current = await client.execute({
2912
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
2913
+ args: [taskId]
2914
+ });
2915
+ const cur = current.rows[0];
2916
+ const curStatus = cur?.status ?? "unknown";
2917
+ const claimedBySession = cur?.assigned_tmux ?? "";
2918
+ const assignedBy = cur?.assigned_by ?? "";
2919
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
2920
+ process.stderr.write(
2921
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
2922
+ `
2923
+ );
2924
+ await client.execute({
2925
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
2926
+ args: [now, taskId]
2927
+ });
2928
+ const retried = await client.execute({
2929
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
2930
+ args: [tmuxSession, now, taskId]
2931
+ });
2932
+ if (retried.rowsAffected > 0) {
2933
+ try {
2934
+ await writeCheckpoint({
2935
+ taskId,
2936
+ step: "reclaimed_dead_session",
2937
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
2938
+ });
2939
+ } catch {
2940
+ }
2941
+ return { row, taskFile, now, taskId };
2942
+ }
2943
+ }
2944
+ if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || isCoordinatorName(input2.callerAgentId))) {
2945
+ process.stderr.write(
2946
+ `[tasks] Assigner override: ${input2.callerAgentId} reclaiming ${taskId}
2947
+ `
2948
+ );
2949
+ await client.execute({
2950
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
2951
+ args: [tmuxSession, now, taskId]
2952
+ });
2953
+ try {
2954
+ await writeCheckpoint({
2955
+ taskId,
2956
+ step: "assigner_override",
2957
+ contextSummary: `Task force-reclaimed by assigner ${input2.callerAgentId}.`
2958
+ });
2959
+ } catch {
2960
+ }
2961
+ return { row, taskFile, now, taskId };
2962
+ }
2963
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
2964
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
2965
+ }
2966
+ try {
2967
+ await writeCheckpoint({
2968
+ taskId,
2969
+ step: "claimed",
2970
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
2971
+ });
2972
+ } catch {
2973
+ }
2974
+ return { row, taskFile, now, taskId };
2975
+ }
2976
+ if (input2.result) {
2977
+ await client.execute({
2978
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
2979
+ args: [input2.status, input2.result, now, taskId]
2980
+ });
2981
+ } else {
2982
+ await client.execute({
2983
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
2984
+ args: [input2.status, now, taskId]
2985
+ });
2986
+ }
2987
+ try {
2988
+ await writeCheckpoint({
2989
+ taskId,
2990
+ step: `status_transition:${input2.status}`,
2991
+ contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
2992
+ });
2993
+ } catch {
2994
+ }
2995
+ return { row, taskFile, now, taskId };
2996
+ }
2997
+ async function deleteTaskCore(taskId, _baseDir) {
2998
+ const client = getClient();
2999
+ const row = await resolveTask(client, taskId);
3000
+ const id = String(row.id);
3001
+ const taskFile = String(row.task_file);
3002
+ const assignedTo = String(row.assigned_to);
3003
+ const assignedBy = String(row.assigned_by);
3004
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
3005
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
3006
+ return { taskFile, assignedTo, assignedBy, taskSlug };
3007
+ }
3008
+ async function ensureArchitectureDoc(baseDir, projectName) {
3009
+ const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
3010
+ try {
3011
+ if (existsSync9(archPath)) return;
3012
+ const template = [
3013
+ `# ${projectName} \u2014 System Architecture`,
3014
+ "",
3015
+ "> Employees: read this before every task. Update it when you change system structure.",
3016
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
3017
+ "",
3018
+ "## Overview",
3019
+ "",
3020
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
3021
+ "",
3022
+ "## Key Components",
3023
+ "",
3024
+ "<!-- List the major modules, services, or subsystems. -->",
3025
+ "",
3026
+ "## Data Flow",
3027
+ "",
3028
+ "<!-- How does data move through the system? What writes where? -->",
3029
+ "",
3030
+ "## Invariants",
3031
+ "",
3032
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
3033
+ "",
3034
+ "## Dependencies",
3035
+ "",
3036
+ "<!-- What depends on what? If I change X, what else is affected? -->",
3037
+ ""
3038
+ ].join("\n");
3039
+ await writeFile3(archPath, template, "utf-8");
3040
+ } catch {
3041
+ }
3042
+ }
3043
+ async function ensureGitignoreExe(baseDir) {
3044
+ const gitignorePath = path10.join(baseDir, ".gitignore");
3045
+ try {
3046
+ if (existsSync9(gitignorePath)) {
3047
+ const content = readFileSync10(gitignorePath, "utf-8");
3048
+ if (/^\/?exe\/?$/m.test(content)) return;
3049
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3050
+ } else {
3051
+ await writeFile3(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
3052
+ }
3053
+ } catch {
3054
+ }
3055
+ }
3056
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3057
+ var init_tasks_crud = __esm({
3058
+ "src/lib/tasks-crud.ts"() {
3059
+ "use strict";
3060
+ init_database();
3061
+ init_task_scope();
3062
+ init_employees();
3063
+ LANE_KEYWORDS = {
3064
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
3065
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
3066
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
3067
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
3068
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
3069
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
3070
+ };
3071
+ KEYWORD_INDEX = buildKeywordIndex();
3072
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
3073
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3074
+ }
3075
+ });
3076
+
3077
+ // src/lib/tasks-review.ts
3078
+ import path11 from "path";
3079
+ import { existsSync as existsSync10, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
3080
+ async function countPendingReviews(sessionScope) {
3081
+ const client = getClient();
3082
+ if (sessionScope) {
3083
+ const result2 = await client.execute({
3084
+ sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND (session_scope = ? OR session_scope IS NULL)",
3085
+ args: [sessionScope]
3086
+ });
3087
+ return Number(result2.rows[0]?.cnt) || 0;
3088
+ }
3089
+ const result = await client.execute({
3090
+ sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3091
+ args: []
3092
+ });
3093
+ return Number(result.rows[0]?.cnt) || 0;
3094
+ }
3095
+ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3096
+ const client = getClient();
3097
+ if (sessionScope) {
3098
+ const result2 = await client.execute({
3099
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3100
+ WHERE status = 'needs_review' AND updated_at > ?
3101
+ AND session_scope = ?`,
3102
+ args: [sinceIso, sessionScope]
3103
+ });
3104
+ return Number(result2.rows[0]?.cnt) || 0;
3105
+ }
3106
+ const result = await client.execute({
3107
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3108
+ WHERE status = 'needs_review' AND updated_at > ?`,
3109
+ args: [sinceIso]
3110
+ });
3111
+ return Number(result.rows[0]?.cnt) || 0;
3112
+ }
3113
+ async function listPendingReviews(limit, sessionScope) {
3114
+ const client = getClient();
3115
+ if (sessionScope) {
3116
+ const result2 = await client.execute({
3117
+ sql: `SELECT title, assigned_to, project_name FROM tasks
3118
+ WHERE status = 'needs_review'
3119
+ AND session_scope = ?
3120
+ ORDER BY priority ASC, created_at DESC LIMIT ?`,
3121
+ args: [sessionScope, limit]
3122
+ });
3123
+ return result2.rows;
3124
+ }
3125
+ const result = await client.execute({
3126
+ sql: `SELECT title, assigned_to, project_name FROM tasks
3127
+ WHERE status = 'needs_review'
3128
+ ORDER BY priority ASC, created_at DESC LIMIT ?`,
3129
+ args: [limit]
3130
+ });
3131
+ return result.rows;
3132
+ }
3133
+ async function cleanupOrphanedReviews() {
3134
+ const client = getClient();
3135
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3136
+ const r1 = await client.execute({
3137
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3138
+ WHERE status IN ('open', 'needs_review', 'in_progress')
3139
+ AND assigned_by = 'system'
3140
+ AND title LIKE 'Review:%'
3141
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3142
+ args: [now]
3143
+ });
3144
+ const r1b = await client.execute({
3145
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3146
+ WHERE status IN ('open', 'needs_review')
3147
+ AND title LIKE 'Review:%completed%'
3148
+ 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')))`,
3149
+ args: [now]
3150
+ });
3151
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
3152
+ const r2 = await client.execute({
3153
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3154
+ WHERE status = 'needs_review'
3155
+ AND result IS NOT NULL
3156
+ AND updated_at < ?`,
3157
+ args: [now, staleThreshold]
3158
+ });
3159
+ const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
3160
+ if (total > 0) {
3161
+ process.stderr.write(
3162
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
3163
+ `
3164
+ );
3165
+ }
3166
+ return total;
3167
+ }
3168
+ function getReviewChecklist(role, agent, taskSlug) {
3169
+ const roleLower = role.toLowerCase();
3170
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
3171
+ return {
3172
+ lens: "Code Quality (Engineer)",
3173
+ checklist: [
3174
+ "1. Do all tests pass? Any new tests needed?",
3175
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
3176
+ "3. Does it follow existing patterns and conventions in the codebase?",
3177
+ "4. Any regressions in the test suite?"
3178
+ ]
3179
+ };
3180
+ }
3181
+ if (roleLower === "cto" || roleLower.includes("architect")) {
3182
+ return {
3183
+ lens: "Architecture (CTO)",
3184
+ checklist: [
3185
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
3186
+ "2. Is it backward compatible? Any breaking changes?",
3187
+ "3. Does it introduce technical debt? Is that debt justified?",
3188
+ "4. Security implications? Any new attack surface?",
3189
+ "5. Does it scale? Performance considerations?",
3190
+ "6. Coordination: does this affect other employees' work or other projects?"
3191
+ ]
3192
+ };
3193
+ }
3194
+ if (roleLower === "coo" || roleLower.includes("operations")) {
3195
+ return {
3196
+ lens: "Strategic (COO)",
3197
+ checklist: [
3198
+ "1. Does this serve the project mission?",
3199
+ "2. Is this the right work at the right time?",
3200
+ "3. Does the architectural assessment make sense for the business?",
3201
+ "4. Any cross-project implications?"
3202
+ ]
3203
+ };
3204
+ }
3205
+ return {
3206
+ lens: "General",
3207
+ checklist: [
3208
+ "1. Read the original task's acceptance criteria",
3209
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
3210
+ "3. Verify code changes match requirements",
3211
+ "4. Check if tests were added/updated",
3212
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
3213
+ ]
3214
+ };
3215
+ }
3216
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
3217
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3218
+ try {
3219
+ const client = getClient();
3220
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3221
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3222
+ if (parentId) {
3223
+ const result = await client.execute({
3224
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3225
+ args: [now, parentId]
3226
+ });
3227
+ if (result.rowsAffected > 0) {
3228
+ process.stderr.write(
3229
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3230
+ `
3231
+ );
3232
+ }
3233
+ } else {
3234
+ const fileName = taskFile.split("/").pop() ?? "";
3235
+ const reviewPrefix = fileName.replace(".md", "");
3236
+ const parts = reviewPrefix.split("-");
3237
+ if (parts.length >= 3 && parts[0] === "review") {
3238
+ const agent = parts[1];
3239
+ const slug = parts.slice(2).join("-");
3240
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
3241
+ const result = await client.execute({
3242
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
3243
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
3244
+ });
3245
+ if (result.rowsAffected > 0) {
3246
+ process.stderr.write(
3247
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
3248
+ `
3249
+ );
3250
+ }
3251
+ }
3252
+ }
3253
+ } catch (err) {
3254
+ process.stderr.write(
3255
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
3256
+ `
3257
+ );
3258
+ }
3259
+ try {
3260
+ const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
3261
+ if (existsSync10(cacheDir)) {
3262
+ for (const f of readdirSync3(cacheDir)) {
3263
+ if (f.startsWith("review-notified-")) {
3264
+ unlinkSync5(path11.join(cacheDir, f));
3265
+ }
3266
+ }
3267
+ }
3268
+ } catch {
3269
+ }
3270
+ }
3271
+ var init_tasks_review = __esm({
3272
+ "src/lib/tasks-review.ts"() {
3273
+ "use strict";
3274
+ init_database();
3275
+ init_config();
3276
+ init_employees();
3277
+ init_notifications();
3278
+ init_tmux_routing();
3279
+ init_session_key();
3280
+ init_state_bus();
3281
+ }
3282
+ });
3283
+
3284
+ // src/lib/tasks-chain.ts
3285
+ import path12 from "path";
3286
+ import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
3287
+ async function cascadeUnblock(taskId, baseDir, now) {
3288
+ const client = getClient();
3289
+ const unblocked = await client.execute({
3290
+ sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
3291
+ WHERE blocked_by = ? AND status = 'blocked'`,
3292
+ args: [now, taskId]
3293
+ });
3294
+ if (baseDir && unblocked.rowsAffected > 0) {
3295
+ const ubScope = sessionScopeFilter();
3296
+ const unblockedRows = await client.execute({
3297
+ sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?${ubScope.sql}`,
3298
+ args: [now, ...ubScope.args]
3299
+ });
3300
+ for (const ur of unblockedRows.rows) {
3301
+ try {
3302
+ const ubFile = path12.join(baseDir, String(ur.task_file));
3303
+ let ubContent = await readFile3(ubFile, "utf-8");
3304
+ ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3305
+ ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
3306
+ await writeFile4(ubFile, ubContent, "utf-8");
3307
+ } catch {
3308
+ }
3309
+ }
3310
+ }
3311
+ }
3312
+ async function findNextTask(assignedTo) {
3313
+ const client = getClient();
3314
+ const ntScope = sessionScopeFilter();
3315
+ const nextResult = await client.execute({
3316
+ sql: `SELECT title, task_file, priority FROM tasks
3317
+ WHERE assigned_to = ? AND status = 'open'${ntScope.sql}
3318
+ ORDER BY priority ASC, created_at ASC
3319
+ LIMIT 1`,
3320
+ args: [assignedTo, ...ntScope.args]
3321
+ });
3322
+ if (nextResult.rows.length === 1) {
3323
+ const nr = nextResult.rows[0];
3324
+ return {
3325
+ title: String(nr.title),
3326
+ priority: String(nr.priority),
3327
+ taskFile: String(nr.task_file)
3328
+ };
3329
+ }
3330
+ return void 0;
3331
+ }
3332
+ async function checkSubtaskCompletion(parentTaskId, projectName) {
3333
+ const client = getClient();
3334
+ const scScope = sessionScopeFilter();
3335
+ const remaining = await client.execute({
3336
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3337
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3338
+ args: [parentTaskId, ...scScope.args]
3339
+ });
3340
+ const cnt = Number(remaining.rows[0]?.cnt ?? 1);
3341
+ if (cnt === 0) {
3342
+ const parentRow = await client.execute({
3343
+ sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
3344
+ args: [parentTaskId]
3345
+ });
3346
+ if (parentRow.rows.length === 1) {
3347
+ const pr = parentRow.rows[0];
3348
+ const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
3349
+ await writeNotification({
3350
+ agentId: String(pr.assigned_to),
3351
+ agentRole: "system",
3352
+ event: "subtasks_complete",
3353
+ project: parentProject,
3354
+ summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
3355
+ taskFile: String(pr.task_file)
3356
+ });
3357
+ }
3358
+ }
3359
+ }
3360
+ var init_tasks_chain = __esm({
3361
+ "src/lib/tasks-chain.ts"() {
3362
+ "use strict";
3363
+ init_database();
3364
+ init_notifications();
3365
+ init_task_scope();
3366
+ }
3367
+ });
3368
+
3369
+ // src/lib/project-name.ts
3370
+ import { execSync as execSync6 } from "child_process";
3371
+ import path13 from "path";
3372
+ function getProjectName(cwd) {
3373
+ const dir = cwd ?? process.cwd();
3374
+ if (_cached2 && _cachedCwd === dir) return _cached2;
3375
+ try {
3376
+ let repoRoot;
3377
+ try {
3378
+ const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
3379
+ cwd: dir,
3380
+ encoding: "utf8",
3381
+ timeout: 2e3,
3382
+ stdio: ["pipe", "pipe", "pipe"]
3383
+ }).trim();
3384
+ repoRoot = path13.dirname(gitCommonDir);
3385
+ } catch {
3386
+ repoRoot = execSync6("git rev-parse --show-toplevel", {
3387
+ cwd: dir,
3388
+ encoding: "utf8",
3389
+ timeout: 2e3,
3390
+ stdio: ["pipe", "pipe", "pipe"]
3391
+ }).trim();
3392
+ }
3393
+ _cached2 = path13.basename(repoRoot);
3394
+ _cachedCwd = dir;
3395
+ return _cached2;
3396
+ } catch {
3397
+ _cached2 = path13.basename(dir);
3398
+ _cachedCwd = dir;
3399
+ return _cached2;
3400
+ }
3401
+ }
3402
+ var _cached2, _cachedCwd;
3403
+ var init_project_name = __esm({
3404
+ "src/lib/project-name.ts"() {
3405
+ "use strict";
3406
+ _cached2 = null;
3407
+ _cachedCwd = null;
3408
+ }
3409
+ });
3410
+
3411
+ // src/lib/session-scope.ts
3412
+ var session_scope_exports = {};
3413
+ __export(session_scope_exports, {
3414
+ assertSessionScope: () => assertSessionScope,
3415
+ findSessionForProject: () => findSessionForProject,
3416
+ getSessionProject: () => getSessionProject
3417
+ });
3418
+ function getSessionProject(sessionName) {
3419
+ const sessions = listSessions();
3420
+ const entry = sessions.find((s) => s.windowName === sessionName);
3421
+ if (!entry) return null;
3422
+ const parts = entry.projectDir.split("/").filter(Boolean);
3423
+ return parts[parts.length - 1] ?? null;
3424
+ }
3425
+ function findSessionForProject(projectName) {
3426
+ const sessions = listSessions();
3427
+ for (const s of sessions) {
3428
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
3429
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
3430
+ }
3431
+ return null;
3432
+ }
3433
+ function assertSessionScope(actionType, targetProject) {
3434
+ try {
3435
+ const currentProject = getProjectName();
3436
+ const exeSession = resolveExeSession();
3437
+ if (!exeSession) {
3438
+ return { allowed: true, reason: "no_session" };
3439
+ }
3440
+ if (currentProject === targetProject) {
3441
+ return {
3442
+ allowed: true,
3443
+ reason: "same_session",
3444
+ currentProject,
3445
+ targetProject
3446
+ };
3447
+ }
3448
+ process.stderr.write(
3449
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3450
+ `
3451
+ );
3452
+ return {
3453
+ allowed: false,
3454
+ reason: "cross_session_denied",
3455
+ currentProject,
3456
+ targetProject,
3457
+ targetSession: findSessionForProject(targetProject)?.windowName
3458
+ };
3459
+ } catch {
3460
+ return { allowed: true, reason: "no_session" };
3461
+ }
3462
+ }
3463
+ var init_session_scope = __esm({
3464
+ "src/lib/session-scope.ts"() {
3465
+ "use strict";
3466
+ init_session_registry();
3467
+ init_project_name();
3468
+ init_tmux_routing();
3469
+ init_employees();
3470
+ }
3471
+ });
3472
+
3473
+ // src/lib/tasks-notify.ts
3474
+ async function dispatchTaskToEmployee(input2) {
3475
+ if (isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
3476
+ let crossProject = false;
3477
+ if (input2.projectName) {
3478
+ try {
3479
+ const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
3480
+ const check = assertSessionScope2("dispatch_task", input2.projectName);
3481
+ if (check.reason === "cross_session_denied") {
3482
+ crossProject = true;
3483
+ return { dispatched: "skipped", crossProject: true };
3484
+ }
3485
+ } catch {
3486
+ }
3487
+ }
3488
+ try {
3489
+ const transport = getTransport();
3490
+ const exeSession = resolveExeSession();
3491
+ if (!exeSession) return { dispatched: "session_missing" };
3492
+ const sessionName = employeeSessionName(input2.assignedTo, exeSession);
3493
+ if (transport.isAlive(sessionName)) {
3494
+ const result = sendIntercom(sessionName);
3495
+ const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
3496
+ return { dispatched, session: sessionName, crossProject };
3497
+ } else {
3498
+ const projectDir = input2.projectDir ?? process.cwd();
3499
+ const result = ensureEmployee(input2.assignedTo, exeSession, projectDir, {
3500
+ autoInstance: isMultiInstance(input2.assignedTo)
3501
+ });
3502
+ if (result.status === "failed") {
3503
+ process.stderr.write(
3504
+ `[dispatch] Failed to spawn ${input2.assignedTo}: ${result.error}
3505
+ `
3506
+ );
3507
+ return { dispatched: "session_missing" };
3508
+ }
3509
+ return { dispatched: "spawned", session: result.sessionName, crossProject };
3510
+ }
3511
+ } catch {
3512
+ return { dispatched: "session_missing" };
3513
+ }
3514
+ }
3515
+ function notifyTaskDone() {
3516
+ try {
3517
+ const key = getSessionKey();
3518
+ if (key && !process.env.VITEST) notifyParentExe(key);
3519
+ } catch {
3520
+ }
3521
+ }
3522
+ async function markTaskNotificationsRead(taskFile) {
3523
+ try {
3524
+ await markAsReadByTaskFile(taskFile);
3525
+ } catch {
3526
+ }
3527
+ }
3528
+ var init_tasks_notify = __esm({
3529
+ "src/lib/tasks-notify.ts"() {
3530
+ "use strict";
3531
+ init_tmux_routing();
3532
+ init_session_key();
3533
+ init_notifications();
3534
+ init_transport();
3535
+ init_employees();
3536
+ }
3537
+ });
3538
+
3539
+ // src/lib/behaviors.ts
3540
+ import crypto4 from "crypto";
3541
+ async function storeBehavior(opts) {
3542
+ const client = getClient();
3543
+ const id = crypto4.randomUUID();
3544
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3545
+ await client.execute({
3546
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
3547
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
3548
+ args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
3549
+ });
3550
+ return id;
3551
+ }
3552
+ var init_behaviors = __esm({
3553
+ "src/lib/behaviors.ts"() {
3554
+ "use strict";
3555
+ init_database();
3556
+ }
3557
+ });
3558
+
3559
+ // src/lib/skill-learning.ts
3560
+ var skill_learning_exports = {};
3561
+ __export(skill_learning_exports, {
3562
+ captureAndLearn: () => captureAndLearn,
3563
+ captureTrajectory: () => captureTrajectory,
3564
+ editDistance: () => editDistance,
3565
+ extractSkill: () => extractSkill,
3566
+ extractTrajectory: () => extractTrajectory,
3567
+ findSimilarTrajectories: () => findSimilarTrajectories,
3568
+ hashSignature: () => hashSignature,
3569
+ storeTrajectory: () => storeTrajectory,
3570
+ sweepTrajectories: () => sweepTrajectories
3571
+ });
3572
+ import crypto5 from "crypto";
3573
+ async function extractTrajectory(taskId, agentId) {
3574
+ const client = getClient();
3575
+ const result = await client.execute({
3576
+ sql: `SELECT tool_name, raw_text
3577
+ FROM memories
3578
+ WHERE task_id = ? AND agent_id = ?
3579
+ ORDER BY timestamp ASC`,
3580
+ args: [taskId, agentId]
3581
+ });
3582
+ if (result.rows.length === 0) return [];
3583
+ const rawTools = result.rows.map((r) => {
3584
+ const toolName = String(r.tool_name);
3585
+ if (toolName === "Bash") {
3586
+ const text = String(r.raw_text);
3587
+ const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
3588
+ return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
3589
+ }
3590
+ return toolName;
3591
+ });
3592
+ const signature = [];
3593
+ for (const tool of rawTools) {
3594
+ if (signature.length === 0 || signature[signature.length - 1] !== tool) {
3595
+ signature.push(tool);
3596
+ }
3597
+ }
3598
+ return signature;
3599
+ }
3600
+ function hashSignature(signature) {
3601
+ return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
3602
+ }
3603
+ async function storeTrajectory(opts) {
3604
+ const client = getClient();
3605
+ const id = crypto5.randomUUID();
3606
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3607
+ const signatureHash = hashSignature(opts.signature);
3608
+ await client.execute({
3609
+ sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
3610
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3611
+ args: [
3612
+ id,
3613
+ opts.taskId,
3614
+ opts.agentId,
3615
+ opts.projectName,
3616
+ opts.taskTitle,
3617
+ JSON.stringify(opts.signature),
3618
+ signatureHash,
3619
+ opts.signature.length,
3620
+ now
3621
+ ]
3622
+ });
3623
+ return id;
3624
+ }
3625
+ async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
3626
+ const client = getClient();
3627
+ const hash = hashSignature(signature);
3628
+ const result = await client.execute({
3629
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
3630
+ FROM trajectories
3631
+ WHERE signature_hash = ?
3632
+ ORDER BY created_at DESC
3633
+ LIMIT 20`,
3634
+ args: [hash]
3635
+ });
3636
+ const mapRow = (r) => ({
3637
+ id: String(r.id),
3638
+ taskId: String(r.task_id),
3639
+ agentId: String(r.agent_id),
3640
+ projectName: String(r.project_name),
3641
+ taskTitle: String(r.task_title),
3642
+ signature: JSON.parse(String(r.signature)),
3643
+ signatureHash: String(r.signature_hash),
3644
+ toolCount: Number(r.tool_count),
3645
+ skillId: r.skill_id ? String(r.skill_id) : null,
3646
+ createdAt: String(r.created_at)
3647
+ });
3648
+ const matches = result.rows.map(mapRow);
3649
+ if (matches.length >= threshold) return matches;
3650
+ const nearResult = await client.execute({
3651
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
3652
+ FROM trajectories
3653
+ WHERE tool_count BETWEEN ? AND ?
3654
+ AND signature_hash != ?
3655
+ ORDER BY created_at DESC
3656
+ LIMIT 50`,
3657
+ args: [
3658
+ Math.max(1, signature.length - 3),
3659
+ signature.length + 3,
3660
+ hash
3661
+ ]
3662
+ });
3663
+ for (const r of nearResult.rows) {
3664
+ const candidateSig = JSON.parse(String(r.signature));
3665
+ if (editDistance(signature, candidateSig) <= 2) {
3666
+ matches.push(mapRow(r));
3667
+ }
3668
+ }
3669
+ return matches;
3670
+ }
3671
+ async function captureTrajectory(opts) {
3672
+ const signature = await extractTrajectory(opts.taskId, opts.agentId);
3673
+ if (signature.length < 3) {
3674
+ return { trajectoryId: "", similarCount: 0, similar: [] };
3675
+ }
3676
+ const trajectoryId = await storeTrajectory({
3677
+ taskId: opts.taskId,
3678
+ agentId: opts.agentId,
3679
+ projectName: opts.projectName,
3680
+ taskTitle: opts.taskTitle,
3681
+ signature
3682
+ });
3683
+ const similar = await findSimilarTrajectories(
3684
+ signature,
3685
+ opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
3686
+ );
3687
+ return { trajectoryId, similarCount: similar.length, similar };
3688
+ }
3689
+ function buildExtractionPrompt(trajectories) {
3690
+ const items = trajectories.map((t, i) => {
3691
+ const sig = t.signature.join(" \u2192 ");
3692
+ return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
3693
+ Signature: ${sig}`;
3694
+ }).join("\n\n");
3695
+ return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
3696
+
3697
+ ${items}
3698
+
3699
+ Extract the reusable procedure. Format your response EXACTLY like this:
3700
+
3701
+ SKILL: {name \u2014 short, descriptive}
3702
+ TRIGGER: {when to use this \u2014 one sentence}
3703
+ STEPS:
3704
+ 1. ...
3705
+ 2. ...
3706
+ PITFALLS: {common mistakes to avoid}
3707
+
3708
+ Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
3709
+ }
3710
+ async function extractSkill(trajectories, model) {
3711
+ if (trajectories.length === 0) return null;
3712
+ const config = await loadConfig();
3713
+ const skillModel = model ?? config.skillModel;
3714
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
3715
+ const client = new Anthropic();
3716
+ const prompt = buildExtractionPrompt(trajectories);
3717
+ const response = await client.messages.create({
3718
+ model: skillModel,
3719
+ max_tokens: 500,
3720
+ messages: [{ role: "user", content: prompt }]
3721
+ });
3722
+ const textBlock = response.content.find((b) => b.type === "text");
3723
+ const skillText = textBlock?.text;
3724
+ if (!skillText) return null;
3725
+ const agentId = trajectories[0].agentId;
3726
+ const projectName = trajectories[0].projectName;
3727
+ const skillId = await storeBehavior({
3728
+ agentId,
3729
+ content: skillText,
3730
+ domain: "skill",
3731
+ projectName
3732
+ });
3733
+ const dbClient = getClient();
3734
+ for (const t of trajectories) {
3735
+ await dbClient.execute({
3736
+ sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
3737
+ args: [skillId, t.id]
3738
+ });
3739
+ }
3740
+ process.stderr.write(
3741
+ `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
3742
+ `
3743
+ );
3744
+ return skillId;
3745
+ }
3746
+ async function captureAndLearn(opts) {
3747
+ try {
3748
+ const config = await loadConfig();
3749
+ if (!config.skillLearning) return;
3750
+ const { trajectoryId, similarCount, similar } = await captureTrajectory({
3751
+ ...opts,
3752
+ skillThreshold: config.skillThreshold
3753
+ });
3754
+ if (!trajectoryId) return;
3755
+ if (similarCount >= config.skillThreshold) {
3756
+ const unprocessed = similar.filter((t) => !t.skillId);
3757
+ if (unprocessed.length >= config.skillThreshold) {
3758
+ extractSkill(unprocessed, config.skillModel).catch((err) => {
3759
+ process.stderr.write(
3760
+ `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
3761
+ `
3762
+ );
3763
+ });
3764
+ }
3765
+ }
3766
+ } catch (err) {
3767
+ process.stderr.write(
3768
+ `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
3769
+ `
3770
+ );
3771
+ }
3772
+ }
3773
+ async function sweepTrajectories(threshold, model) {
3774
+ const config = await loadConfig();
3775
+ if (!config.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
3776
+ const t = threshold ?? config.skillThreshold;
3777
+ const client = getClient();
3778
+ const result = await client.execute({
3779
+ sql: `SELECT signature_hash, COUNT(*) as cnt
3780
+ FROM trajectories
3781
+ WHERE skill_id IS NULL
3782
+ GROUP BY signature_hash
3783
+ HAVING cnt >= ?
3784
+ ORDER BY cnt DESC
3785
+ LIMIT 10`,
3786
+ args: [t]
3787
+ });
3788
+ let clustersProcessed = 0;
3789
+ let skillsExtracted = 0;
3790
+ for (const row of result.rows) {
3791
+ const hash = String(row.signature_hash);
3792
+ const trajResult = await client.execute({
3793
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
3794
+ FROM trajectories
3795
+ WHERE signature_hash = ? AND skill_id IS NULL
3796
+ ORDER BY created_at DESC
3797
+ LIMIT 10`,
3798
+ args: [hash]
3799
+ });
3800
+ const trajectories = trajResult.rows.map((r) => ({
3801
+ id: String(r.id),
3802
+ taskId: String(r.task_id),
3803
+ agentId: String(r.agent_id),
3804
+ projectName: String(r.project_name),
3805
+ taskTitle: String(r.task_title),
3806
+ signature: JSON.parse(String(r.signature)),
3807
+ signatureHash: String(r.signature_hash),
3808
+ toolCount: Number(r.tool_count),
3809
+ skillId: null,
3810
+ createdAt: String(r.created_at)
3811
+ }));
3812
+ if (trajectories.length >= t) {
3813
+ clustersProcessed++;
3814
+ const skillId = await extractSkill(trajectories, model ?? config.skillModel);
3815
+ if (skillId) skillsExtracted++;
3816
+ }
3817
+ }
3818
+ return { clustersProcessed, skillsExtracted };
3819
+ }
3820
+ function editDistance(a, b) {
3821
+ const m = a.length;
3822
+ const n = b.length;
3823
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
3824
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
3825
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
3826
+ for (let i = 1; i <= m; i++) {
3827
+ for (let j = 1; j <= n; j++) {
3828
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3829
+ dp[i][j] = Math.min(
3830
+ dp[i - 1][j] + 1,
3831
+ dp[i][j - 1] + 1,
3832
+ dp[i - 1][j - 1] + cost
3833
+ );
3834
+ }
3835
+ }
3836
+ return dp[m][n];
3837
+ }
3838
+ var DEFAULT_SKILL_THRESHOLD;
3839
+ var init_skill_learning = __esm({
3840
+ "src/lib/skill-learning.ts"() {
3841
+ "use strict";
3842
+ init_database();
3843
+ init_behaviors();
3844
+ init_config();
3845
+ DEFAULT_SKILL_THRESHOLD = 3;
3846
+ }
3847
+ });
3848
+
3849
+ // src/lib/tasks.ts
3850
+ var tasks_exports = {};
3851
+ __export(tasks_exports, {
3852
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
3853
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
3854
+ countPendingReviews: () => countPendingReviews,
3855
+ createTask: () => createTask,
3856
+ createTaskCore: () => createTaskCore,
3857
+ deleteTask: () => deleteTask,
3858
+ deleteTaskCore: () => deleteTaskCore,
3859
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
3860
+ ensureGitignoreExe: () => ensureGitignoreExe,
3861
+ getReviewChecklist: () => getReviewChecklist,
3862
+ listPendingReviews: () => listPendingReviews,
3863
+ listTasks: () => listTasks,
3864
+ resolveTask: () => resolveTask,
3865
+ slugify: () => slugify,
3866
+ updateTask: () => updateTask,
3867
+ updateTaskStatus: () => updateTaskStatus,
3868
+ writeCheckpoint: () => writeCheckpoint
3869
+ });
3870
+ import path14 from "path";
3871
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync6 } from "fs";
3872
+ async function createTask(input2) {
3873
+ const result = await createTaskCore(input2);
3874
+ if (!input2.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
3875
+ dispatchTaskToEmployee({
3876
+ assignedTo: input2.assignedTo,
3877
+ title: input2.title,
3878
+ priority: input2.priority,
3879
+ taskFile: result.taskFile,
3880
+ initialStatus: result.status,
3881
+ projectName: input2.projectName
3882
+ });
3883
+ }
3884
+ return result;
3885
+ }
3886
+ async function updateTask(input2) {
3887
+ const { row, taskFile, now, taskId } = await updateTaskStatus(input2);
3888
+ try {
3889
+ const agent = String(row.assigned_to);
3890
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
3891
+ const cachePath = path14.join(cacheDir, `current-task-${agent}.json`);
3892
+ if (input2.status === "in_progress") {
3893
+ mkdirSync5(cacheDir, { recursive: true });
3894
+ writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3895
+ } else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled") {
3896
+ try {
3897
+ unlinkSync6(cachePath);
3898
+ } catch {
3899
+ }
3900
+ }
3901
+ } catch {
3902
+ }
3903
+ if (input2.status === "done") {
3904
+ await cleanupReviewFile(row, taskFile, input2.baseDir);
3905
+ }
3906
+ if (input2.status === "done" || input2.status === "cancelled") {
3907
+ try {
3908
+ const client = getClient();
3909
+ const taskTitle = String(row.title);
3910
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
3911
+ await client.execute({
3912
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3913
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
3914
+ args: [now, `%left '${escaped}' as in\\_progress%`]
3915
+ });
3916
+ } catch {
3917
+ }
3918
+ const assignedAgent = String(row.assigned_to);
3919
+ if (!isCoordinatorName(assignedAgent)) {
3920
+ try {
3921
+ const draftClient = getClient();
3922
+ if (input2.status === "done") {
3923
+ await draftClient.execute({
3924
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3925
+ args: [assignedAgent]
3926
+ });
3927
+ } else if (input2.status === "cancelled") {
3928
+ await draftClient.execute({
3929
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
3930
+ args: [assignedAgent]
3931
+ });
3932
+ }
3933
+ } catch {
3934
+ }
3935
+ }
3936
+ try {
3937
+ const client = getClient();
3938
+ const cascaded = await client.execute({
3939
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3940
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
3941
+ args: [now, taskId]
3942
+ });
3943
+ if (cascaded.rowsAffected > 0) {
3944
+ process.stderr.write(
3945
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
3946
+ `
3947
+ );
3948
+ }
3949
+ } catch {
3950
+ }
3951
+ }
3952
+ const isTerminal = input2.status === "done" || input2.status === "needs_review";
3953
+ if (isTerminal) {
3954
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3955
+ if (!isCoordinator) {
3956
+ notifyTaskDone();
3957
+ }
3958
+ await markTaskNotificationsRead(taskFile);
3959
+ if (input2.status === "done") {
3960
+ try {
3961
+ await cascadeUnblock(taskId, input2.baseDir, now);
3962
+ } catch {
3963
+ }
3964
+ orgBus.emit({
3965
+ type: "task_completed",
3966
+ taskId,
3967
+ employee: String(row.assigned_to),
3968
+ result: input2.result ?? "",
3969
+ timestamp: now
3970
+ });
3971
+ if (row.parent_task_id) {
3972
+ try {
3973
+ await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
3974
+ } catch {
3975
+ }
3976
+ }
3977
+ }
3978
+ }
3979
+ if (input2.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3980
+ Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3981
+ ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3982
+ taskId,
3983
+ agentId: String(row.assigned_to),
3984
+ projectName: String(row.project_name),
3985
+ taskTitle: String(row.title)
3986
+ })
3987
+ ).catch((err) => {
3988
+ process.stderr.write(
3989
+ `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
3990
+ `
3991
+ );
3992
+ });
3993
+ }
3994
+ let nextTask;
3995
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3996
+ try {
3997
+ nextTask = await findNextTask(String(row.assigned_to));
3998
+ } catch {
3999
+ }
4000
+ }
4001
+ return {
4002
+ id: String(row.id),
4003
+ title: String(row.title),
4004
+ assignedTo: String(row.assigned_to),
4005
+ assignedBy: String(row.assigned_by),
4006
+ projectName: String(row.project_name),
4007
+ priority: String(row.priority),
4008
+ status: input2.status,
4009
+ taskFile,
4010
+ createdAt: String(row.created_at),
4011
+ updatedAt: now,
4012
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
4013
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
4014
+ tokensUsed: Number(row.tokens_used ?? 0),
4015
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
4016
+ nextTask
4017
+ };
4018
+ }
4019
+ async function deleteTask(taskId, baseDir) {
4020
+ const client = getClient();
4021
+ const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
4022
+ const coordinatorName = getCoordinatorName();
4023
+ const reviewer = assignedBy || coordinatorName;
4024
+ const reviewSlug = `review-${assignedTo}-${taskSlug}`;
4025
+ const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
4026
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
4027
+ await client.execute({
4028
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
4029
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
4030
+ });
4031
+ await markAsReadByTaskFile(taskFile);
4032
+ await markAsReadByTaskFile(reviewFile);
4033
+ }
4034
+ var init_tasks = __esm({
4035
+ "src/lib/tasks.ts"() {
4036
+ "use strict";
4037
+ init_database();
4038
+ init_config();
4039
+ init_notifications();
4040
+ init_state_bus();
4041
+ init_employees();
4042
+ init_tasks_crud();
4043
+ init_tasks_review();
4044
+ init_tasks_crud();
4045
+ init_tasks_chain();
4046
+ init_tasks_review();
4047
+ init_tasks_notify();
4048
+ }
4049
+ });
4050
+
4051
+ // src/lib/capacity-monitor.ts
4052
+ var capacity_monitor_exports = {};
4053
+ __export(capacity_monitor_exports, {
4054
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
4055
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
4056
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
4057
+ confirmCapacityKill: () => confirmCapacityKill,
4058
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
4059
+ extractContextPercent: () => extractContextPercent,
4060
+ isAtCapacity: () => isAtCapacity,
4061
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
4062
+ pollCapacityDead: () => pollCapacityDead
4063
+ });
4064
+ function resumeTaskTitle(agentId) {
4065
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
4066
+ }
4067
+ function buildResumeContext(agentId, openTasks) {
4068
+ const taskList = openTasks.map(
4069
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
4070
+ ).join("\n");
4071
+ return [
4072
+ "## Context",
4073
+ "",
4074
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
4075
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
4076
+ "",
4077
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
4078
+ "",
4079
+ taskList,
4080
+ "",
4081
+ "Read each task file and chain through them. Build and commit after each one."
4082
+ ].join("\n");
4083
+ }
4084
+ function filterPaneContent(paneOutput) {
4085
+ return paneOutput.split("\n").filter((line) => {
4086
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
4087
+ for (const marker of CONTENT_LINE_MARKERS) {
4088
+ if (line.includes(marker)) return false;
4089
+ }
4090
+ for (const re of SOURCE_CODE_MARKERS) {
4091
+ if (re.test(line)) return false;
4092
+ }
4093
+ return true;
4094
+ }).join("\n");
4095
+ }
4096
+ function extractContextPercent(paneOutput) {
4097
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
4098
+ if (!match) return null;
4099
+ const parsed = Number.parseInt(match[2], 10);
4100
+ return Number.isFinite(parsed) ? parsed : null;
4101
+ }
4102
+ function isAtCapacity(paneOutput) {
4103
+ const filtered = filterPaneContent(paneOutput);
4104
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
4105
+ }
4106
+ function confirmCapacityKill(agentId, now = Date.now()) {
4107
+ const pendingSince = _pendingCapacityKill.get(agentId);
4108
+ if (pendingSince === void 0) {
4109
+ _pendingCapacityKill.set(agentId, now);
4110
+ return false;
4111
+ }
4112
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
4113
+ _pendingCapacityKill.set(agentId, now);
4114
+ return false;
4115
+ }
4116
+ _pendingCapacityKill.delete(agentId);
4117
+ return true;
4118
+ }
4119
+ function _resetPendingCapacityKills() {
4120
+ _pendingCapacityKill.clear();
4121
+ }
4122
+ function _resetLastRelaunchCache() {
4123
+ _lastRelaunch.clear();
4124
+ }
4125
+ async function lastResumeCreatedAtMs(agentId) {
4126
+ const client = getClient();
4127
+ const cmScope = sessionScopeFilter(null);
4128
+ const result = await client.execute({
4129
+ sql: `SELECT MAX(created_at) AS last_created_at
4130
+ FROM tasks
4131
+ WHERE assigned_to = ? AND title LIKE ?${cmScope.sql}`,
4132
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`, ...cmScope.args]
4133
+ });
4134
+ const raw = result.rows[0]?.last_created_at;
4135
+ if (raw === null || raw === void 0) return null;
4136
+ const parsed = Date.parse(String(raw));
4137
+ return Number.isNaN(parsed) ? null : parsed;
4138
+ }
4139
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
4140
+ const cached = _lastRelaunch.get(agentId);
4141
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
4142
+ const persisted = await lastResumeCreatedAtMs(agentId);
4143
+ if (persisted === null) return false;
4144
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
4145
+ _lastRelaunch.set(agentId, persisted);
4146
+ return true;
4147
+ }
4148
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
4149
+ const client = getClient();
4150
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4151
+ const context = buildResumeContext(agentId, openTasks);
4152
+ const rdScope = sessionScopeFilter(null);
4153
+ const existing = await client.execute({
4154
+ sql: `SELECT id FROM tasks
4155
+ WHERE assigned_to = ?
4156
+ AND title LIKE ?
4157
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})${rdScope.sql}
4158
+ ORDER BY created_at DESC
4159
+ LIMIT 1`,
4160
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES, ...rdScope.args]
4161
+ });
4162
+ if (existing.rows.length > 0) {
4163
+ const taskId = String(existing.rows[0].id);
4164
+ await client.execute({
4165
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
4166
+ args: [context, now, taskId]
4167
+ });
4168
+ return { created: false, taskId };
4169
+ }
4170
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
4171
+ const task = await createTask2({
4172
+ title: resumeTaskTitle(agentId),
4173
+ assignedTo: agentId,
4174
+ assignedBy: "system",
4175
+ projectName: projectDir.split("/").pop() ?? "unknown",
4176
+ priority: "p0",
4177
+ context,
4178
+ baseDir: projectDir
4179
+ });
4180
+ return { created: true, taskId: task.id };
4181
+ }
4182
+ async function pollCapacityDead() {
4183
+ const transport = getTransport();
4184
+ const relaunched = [];
4185
+ const registered = listSessions().filter(
4186
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
4187
+ );
4188
+ if (registered.length === 0) return [];
4189
+ let liveSessions;
4190
+ try {
4191
+ liveSessions = transport.listSessions();
4192
+ } catch {
4193
+ return [];
4194
+ }
4195
+ for (const entry of registered) {
4196
+ const { windowName, agentId, projectDir } = entry;
4197
+ if (!liveSessions.includes(windowName)) continue;
4198
+ if (await isWithinRelaunchCooldown(agentId)) continue;
4199
+ let pane;
4200
+ try {
4201
+ pane = transport.capturePane(windowName, 15);
4202
+ } catch {
4203
+ continue;
4204
+ }
4205
+ if (!isAtCapacity(pane)) continue;
4206
+ const ctxPct = extractContextPercent(pane);
4207
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
4208
+ process.stderr.write(
4209
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
4210
+ `
4211
+ );
4212
+ continue;
4213
+ }
4214
+ if (!confirmCapacityKill(agentId)) {
4215
+ process.stderr.write(
4216
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
4217
+ `
4218
+ );
4219
+ continue;
4220
+ }
4221
+ const verify = await verifyPaneAtCapacity(windowName);
4222
+ if (!verify.atCapacity) {
4223
+ process.stderr.write(
4224
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
4225
+ `
4226
+ );
4227
+ void recordSessionKill({
4228
+ sessionName: windowName,
4229
+ agentId,
4230
+ reason: "capacity_false_positive_blocked"
4231
+ });
4232
+ continue;
4233
+ }
4234
+ process.stderr.write(
4235
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
4236
+ `
4237
+ );
4238
+ try {
4239
+ transport.kill(windowName);
4240
+ void recordSessionKill({
4241
+ sessionName: windowName,
4242
+ agentId,
4243
+ reason: "capacity"
4244
+ });
4245
+ const client = getClient();
4246
+ const rlScope = sessionScopeFilter(null);
4247
+ const openTasks = await client.execute({
4248
+ sql: `SELECT id, title, priority, task_file, status
4249
+ FROM tasks
4250
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')${rlScope.sql}
4251
+ ORDER BY
4252
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
4253
+ created_at ASC
4254
+ LIMIT 10`,
4255
+ args: [agentId, ...rlScope.args]
4256
+ });
4257
+ if (openTasks.rows.length === 0) {
4258
+ process.stderr.write(
4259
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
4260
+ `
4261
+ );
4262
+ continue;
4263
+ }
4264
+ const { created } = await createOrRefreshResumeTask(
4265
+ agentId,
4266
+ projectDir,
4267
+ openTasks.rows
4268
+ );
4269
+ if (created) {
4270
+ await writeNotification({
4271
+ agentId: "system",
4272
+ agentRole: "daemon",
4273
+ event: "capacity_relaunch",
4274
+ project: projectDir.split("/").pop() ?? "unknown",
4275
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
4276
+ });
4277
+ }
4278
+ _lastRelaunch.set(agentId, Date.now());
4279
+ if (created) relaunched.push(agentId);
4280
+ } catch (err) {
4281
+ process.stderr.write(
4282
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
4283
+ `
4284
+ );
4285
+ }
4286
+ }
4287
+ return relaunched;
4288
+ }
4289
+ 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;
4290
+ var init_capacity_monitor = __esm({
4291
+ "src/lib/capacity-monitor.ts"() {
4292
+ "use strict";
4293
+ init_session_registry();
4294
+ init_transport();
4295
+ init_notifications();
4296
+ init_database();
4297
+ init_session_kill_telemetry();
4298
+ init_tmux_routing();
4299
+ init_task_scope();
4300
+ init_employees();
4301
+ CAPACITY_PATTERNS = [
4302
+ /conversation is too long/i,
4303
+ /maximum context length/i,
4304
+ /context window.*(?:limit|exceed|full)/i,
4305
+ /reached.*(?:token|context).*limit/i
4306
+ ];
4307
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
4308
+ CONTENT_LINE_MARKERS = [
4309
+ "RESUME:",
4310
+ "intercom",
4311
+ "capacity-monitor",
4312
+ "CAPACITY_PATTERNS",
4313
+ "isAtCapacity",
4314
+ "CONTENT_LINE_MARKERS",
4315
+ "pollCapacityDead",
4316
+ "confirmCapacityKill",
4317
+ "session_kills",
4318
+ "capacity-monitor.test"
4319
+ ];
4320
+ SOURCE_CODE_MARKERS = [
4321
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
4322
+ /(?:maximum context length|conversation is too long).*["'`/]/i
4323
+ ];
4324
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
4325
+ _lastRelaunch = /* @__PURE__ */ new Map();
4326
+ RESUME_TITLE_PREFIX = "RESUME:";
4327
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
4328
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
4329
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
4330
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
4331
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
4332
+ CTX_FLOOR_PERCENT = 50;
4333
+ }
4334
+ });
4335
+
4336
+ // src/lib/tmux-routing.ts
4337
+ var tmux_routing_exports = {};
4338
+ __export(tmux_routing_exports, {
4339
+ acquireSpawnLock: () => acquireSpawnLock2,
4340
+ employeeSessionName: () => employeeSessionName,
4341
+ ensureEmployee: () => ensureEmployee,
4342
+ extractRootExe: () => extractRootExe,
4343
+ findFreeInstance: () => findFreeInstance,
4344
+ getDispatchedBy: () => getDispatchedBy,
4345
+ getMySession: () => getMySession,
4346
+ getParentExe: () => getParentExe,
4347
+ getSessionState: () => getSessionState,
4348
+ isEmployeeAlive: () => isEmployeeAlive,
4349
+ isExeSession: () => isExeSession,
4350
+ isSessionBusy: () => isSessionBusy,
4351
+ notifyParentExe: () => notifyParentExe,
4352
+ parseParentExe: () => parseParentExe,
4353
+ registerParentExe: () => registerParentExe,
4354
+ releaseSpawnLock: () => releaseSpawnLock2,
4355
+ resolveExeSession: () => resolveExeSession,
4356
+ sendIntercom: () => sendIntercom,
4357
+ spawnEmployee: () => spawnEmployee,
4358
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
4359
+ });
4360
+ import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
4361
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync } from "fs";
4362
+ import path15 from "path";
4363
+ import os7 from "os";
4364
+ import { fileURLToPath as fileURLToPath2 } from "url";
4365
+ import { unlinkSync as unlinkSync7 } from "fs";
4366
+ function spawnLockPath(sessionName) {
4367
+ return path15.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4368
+ }
4369
+ function isProcessAlive(pid) {
4370
+ try {
4371
+ process.kill(pid, 0);
4372
+ return true;
4373
+ } catch {
4374
+ return false;
4375
+ }
4376
+ }
4377
+ function acquireSpawnLock2(sessionName) {
4378
+ if (!existsSync11(SPAWN_LOCK_DIR)) {
4379
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4380
+ }
4381
+ const lockFile = spawnLockPath(sessionName);
4382
+ if (existsSync11(lockFile)) {
4383
+ try {
4384
+ const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
4385
+ const age = Date.now() - lock.timestamp;
4386
+ if (isProcessAlive(lock.pid) && age < 6e4) {
4387
+ return false;
4388
+ }
4389
+ } catch {
4390
+ }
4391
+ }
4392
+ writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4393
+ return true;
4394
+ }
4395
+ function releaseSpawnLock2(sessionName) {
4396
+ try {
4397
+ unlinkSync7(spawnLockPath(sessionName));
4398
+ } catch {
4399
+ }
4400
+ }
4401
+ function resolveBehaviorsExporterScript() {
4402
+ try {
4403
+ const thisFile = fileURLToPath2(import.meta.url);
4404
+ const scriptPath = path15.join(
4405
+ path15.dirname(thisFile),
4406
+ "..",
4407
+ "bin",
4408
+ "exe-export-behaviors.js"
4409
+ );
4410
+ return existsSync11(scriptPath) ? scriptPath : null;
4411
+ } catch {
4412
+ return null;
4413
+ }
4414
+ }
4415
+ function exportBehaviorsSync(agentId, projectName, sessionKey) {
4416
+ const script = resolveBehaviorsExporterScript();
4417
+ if (!script) return null;
4418
+ try {
4419
+ const output = execFileSync2(
4420
+ process.execPath,
4421
+ [script, agentId, projectName, sessionKey],
4422
+ { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
4423
+ ).trim();
4424
+ return output.length > 0 ? output : null;
4425
+ } catch (err) {
4426
+ process.stderr.write(
4427
+ `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
4428
+ `
4429
+ );
4430
+ return null;
4431
+ }
4432
+ }
4433
+ function getMySession() {
4434
+ return getTransport().getMySession();
4435
+ }
4436
+ function isRootSession(name) {
4437
+ return name.length > 0 && !name.includes("-");
4438
+ }
4439
+ function employeeSessionName(employee, exeSession, instance) {
4440
+ if (!isRootSession(exeSession)) {
4441
+ const root = extractRootExe(exeSession);
4442
+ if (root) {
4443
+ process.stderr.write(
4444
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root session, using "${root}" instead
4445
+ `
4446
+ );
4447
+ exeSession = root;
4448
+ } else {
4449
+ throw new Error(
4450
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
4451
+ );
4452
+ }
4453
+ }
4454
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
4455
+ const name = `${employee}${suffix}-${exeSession}`;
4456
+ if (!VALID_SESSION_NAME.test(name)) {
4457
+ throw new Error(
4458
+ `Invalid session name "${name}" \u2014 must match {agent}-{rootSession} or {agent}{instance}-{rootSession}`
4459
+ );
4460
+ }
4461
+ return name;
4462
+ }
4463
+ function parseParentExe(sessionName, agentId) {
4464
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4465
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
4466
+ const match = sessionName.match(regex);
4467
+ return match?.[1] ?? null;
4468
+ }
4469
+ function extractRootExe(name) {
4470
+ if (!name) return null;
4471
+ if (!name.includes("-")) return name;
1455
4472
  const parts = name.split("-").filter(Boolean);
1456
4473
  return parts.length > 0 ? parts[parts.length - 1] : null;
1457
4474
  }
4475
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
4476
+ if (!existsSync11(SESSION_CACHE)) {
4477
+ mkdirSync6(SESSION_CACHE, { recursive: true });
4478
+ }
4479
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
4480
+ const filePath = path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4481
+ writeFileSync7(filePath, JSON.stringify({
4482
+ parentExe: rootExe,
4483
+ dispatchedBy: dispatchedBy || rootExe,
4484
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4485
+ }));
4486
+ }
1458
4487
  function getParentExe(sessionKey) {
1459
4488
  try {
1460
- const data = JSON.parse(readFileSync7(path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4489
+ const data = JSON.parse(readFileSync11(path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1461
4490
  return data.parentExe || null;
1462
4491
  } catch {
1463
4492
  return null;
1464
4493
  }
1465
4494
  }
4495
+ function getDispatchedBy(sessionKey) {
4496
+ try {
4497
+ const data = JSON.parse(readFileSync11(
4498
+ path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4499
+ "utf8"
4500
+ ));
4501
+ return data.dispatchedBy ?? data.parentExe ?? null;
4502
+ } catch {
4503
+ return null;
4504
+ }
4505
+ }
1466
4506
  function resolveExeSession() {
1467
4507
  const mySession = getMySession();
1468
4508
  if (!mySession) return null;
@@ -1476,7 +4516,455 @@ function resolveExeSession() {
1476
4516
  }
1477
4517
  return extractRootExe(mySession) ?? mySession;
1478
4518
  }
1479
- var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
4519
+ function isEmployeeAlive(sessionName) {
4520
+ return getTransport().isAlive(sessionName);
4521
+ }
4522
+ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4523
+ const base = employeeSessionName(employeeName, exeSession);
4524
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
4525
+ for (let i = 2; i <= maxInstances; i++) {
4526
+ const candidate = employeeSessionName(employeeName, exeSession, i);
4527
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
4528
+ }
4529
+ return null;
4530
+ }
4531
+ async function verifyPaneAtCapacity(sessionName) {
4532
+ const transport = getTransport();
4533
+ if (!transport.isAlive(sessionName)) {
4534
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
4535
+ }
4536
+ let pane;
4537
+ try {
4538
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
4539
+ } catch (err) {
4540
+ return {
4541
+ atCapacity: false,
4542
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
4543
+ };
4544
+ }
4545
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
4546
+ if (!isAtCapacity2(pane)) {
4547
+ return {
4548
+ atCapacity: false,
4549
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
4550
+ };
4551
+ }
4552
+ return {
4553
+ atCapacity: true,
4554
+ reason: "capacity banner matched in recent pane output"
4555
+ };
4556
+ }
4557
+ function readDebounceState() {
4558
+ try {
4559
+ if (!existsSync11(DEBOUNCE_FILE)) return {};
4560
+ return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
4561
+ } catch {
4562
+ return {};
4563
+ }
4564
+ }
4565
+ function writeDebounceState(state) {
4566
+ try {
4567
+ if (!existsSync11(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4568
+ writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
4569
+ } catch {
4570
+ }
4571
+ }
4572
+ function isDebounced(targetSession) {
4573
+ const state = readDebounceState();
4574
+ const lastSent = state[targetSession] ?? 0;
4575
+ return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4576
+ }
4577
+ function recordDebounce(targetSession) {
4578
+ const state = readDebounceState();
4579
+ state[targetSession] = Date.now();
4580
+ const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4581
+ for (const key of Object.keys(state)) {
4582
+ if ((state[key] ?? 0) < cutoff) delete state[key];
4583
+ }
4584
+ writeDebounceState(state);
4585
+ }
4586
+ function logIntercom(msg) {
4587
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
4588
+ `;
4589
+ process.stderr.write(`[intercom] ${msg}
4590
+ `);
4591
+ try {
4592
+ appendFileSync(INTERCOM_LOG2, line);
4593
+ } catch {
4594
+ }
4595
+ }
4596
+ function getSessionState(sessionName) {
4597
+ const transport = getTransport();
4598
+ if (!transport.isAlive(sessionName)) return "offline";
4599
+ try {
4600
+ const pane = transport.capturePane(sessionName, 5);
4601
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4602
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4603
+ return "no_claude";
4604
+ }
4605
+ }
4606
+ if (/Running…/.test(pane)) return "tool";
4607
+ if (BUSY_PATTERN.test(pane)) return "thinking";
4608
+ return "idle";
4609
+ } catch {
4610
+ return "offline";
4611
+ }
4612
+ }
4613
+ function isSessionBusy(sessionName) {
4614
+ const state = getSessionState(sessionName);
4615
+ return state === "thinking" || state === "tool";
4616
+ }
4617
+ function isExeSession(sessionName) {
4618
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
4619
+ const coordinatorName = getCoordinatorName();
4620
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
4621
+ }
4622
+ function sendIntercom(targetSession) {
4623
+ const transport = getTransport();
4624
+ if (isExeSession(targetSession)) {
4625
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
4626
+ return "skipped_exe";
4627
+ }
4628
+ if (isDebounced(targetSession)) {
4629
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4630
+ return "debounced";
4631
+ }
4632
+ try {
4633
+ const sessions = transport.listSessions();
4634
+ if (!sessions.includes(targetSession)) {
4635
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4636
+ return "failed";
4637
+ }
4638
+ const sessionState = getSessionState(targetSession);
4639
+ if (sessionState === "no_claude") {
4640
+ queueIntercom(targetSession, "claude not running in session");
4641
+ recordDebounce(targetSession);
4642
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4643
+ return "queued";
4644
+ }
4645
+ if (sessionState === "thinking" || sessionState === "tool") {
4646
+ queueIntercom(targetSession, "session busy at send time");
4647
+ recordDebounce(targetSession);
4648
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4649
+ return "queued";
4650
+ }
4651
+ if (transport.isPaneInCopyMode(targetSession)) {
4652
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4653
+ transport.sendKeys(targetSession, "q");
4654
+ }
4655
+ transport.sendKeys(targetSession, "/exe-intercom");
4656
+ recordDebounce(targetSession);
4657
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4658
+ return "delivered";
4659
+ } catch {
4660
+ logIntercom(`FAIL \u2192 ${targetSession}`);
4661
+ return "failed";
4662
+ }
4663
+ }
4664
+ function notifyParentExe(sessionKey) {
4665
+ const target = getDispatchedBy(sessionKey);
4666
+ if (!target) {
4667
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
4668
+ `);
4669
+ return false;
4670
+ }
4671
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4672
+ `);
4673
+ const result = sendIntercom(target);
4674
+ if (result === "failed") {
4675
+ const rootExe = resolveExeSession();
4676
+ if (rootExe && rootExe !== target) {
4677
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
4678
+ `);
4679
+ const fallback = sendIntercom(rootExe);
4680
+ return fallback !== "failed";
4681
+ }
4682
+ return false;
4683
+ }
4684
+ return true;
4685
+ }
4686
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4687
+ if (isCoordinatorName(employeeName)) {
4688
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
4689
+ }
4690
+ try {
4691
+ assertEmployeeLimitSync();
4692
+ } catch (err) {
4693
+ if (err instanceof PlanLimitError) {
4694
+ return { status: "failed", sessionName: "", error: err.message };
4695
+ }
4696
+ }
4697
+ if (employeeName.includes("-")) {
4698
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
4699
+ return {
4700
+ status: "failed",
4701
+ sessionName: "",
4702
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
4703
+ };
4704
+ }
4705
+ if (!isRootSession(exeSession)) {
4706
+ const root = extractRootExe(exeSession);
4707
+ if (root) {
4708
+ process.stderr.write(
4709
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root session). Auto-correcting to "${root}".
4710
+ `
4711
+ );
4712
+ exeSession = root;
4713
+ } else {
4714
+ return {
4715
+ status: "failed",
4716
+ sessionName: "",
4717
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
4718
+ };
4719
+ }
4720
+ }
4721
+ let effectiveInstance = opts?.instance;
4722
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
4723
+ const free = findFreeInstance(
4724
+ employeeName,
4725
+ exeSession,
4726
+ opts.maxAutoInstances ?? 10
4727
+ );
4728
+ if (free === null) {
4729
+ return {
4730
+ status: "failed",
4731
+ sessionName: employeeSessionName(employeeName, exeSession),
4732
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4733
+ };
4734
+ }
4735
+ effectiveInstance = free === 0 ? void 0 : free;
4736
+ }
4737
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
4738
+ if (isEmployeeAlive(sessionName)) {
4739
+ const result2 = sendIntercom(sessionName);
4740
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4741
+ return { status: "intercom_sent", sessionName };
4742
+ }
4743
+ if (result2 === "delivered") {
4744
+ return { status: "intercom_unprocessed", sessionName };
4745
+ }
4746
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
4747
+ }
4748
+ const spawnOpts = { ...opts, instance: effectiveInstance };
4749
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
4750
+ if (result.error) {
4751
+ return { status: "failed", sessionName, error: result.error };
4752
+ }
4753
+ return { status: "spawned", sessionName };
4754
+ }
4755
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4756
+ const transport = getTransport();
4757
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4758
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4759
+ const logDir = path15.join(os7.homedir(), ".exe-os", "session-logs");
4760
+ const logFile = path15.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4761
+ if (!existsSync11(logDir)) {
4762
+ mkdirSync6(logDir, { recursive: true });
4763
+ }
4764
+ transport.kill(sessionName);
4765
+ let cleanupSuffix = "";
4766
+ try {
4767
+ const thisFile = fileURLToPath2(import.meta.url);
4768
+ const cleanupScript = path15.join(path15.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4769
+ if (existsSync11(cleanupScript)) {
4770
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4771
+ }
4772
+ } catch {
4773
+ }
4774
+ try {
4775
+ const claudeJsonPath = path15.join(os7.homedir(), ".claude.json");
4776
+ let claudeJson = {};
4777
+ try {
4778
+ claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
4779
+ } catch {
4780
+ }
4781
+ if (!claudeJson.projects) claudeJson.projects = {};
4782
+ const projects = claudeJson.projects;
4783
+ const trustDir = opts?.cwd ?? projectDir;
4784
+ if (!projects[trustDir]) projects[trustDir] = {};
4785
+ projects[trustDir].hasTrustDialogAccepted = true;
4786
+ writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4787
+ } catch {
4788
+ }
4789
+ try {
4790
+ const settingsDir = path15.join(os7.homedir(), ".claude", "projects");
4791
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4792
+ const projSettingsDir = path15.join(settingsDir, normalizedKey);
4793
+ const settingsPath = path15.join(projSettingsDir, "settings.json");
4794
+ let settings = {};
4795
+ try {
4796
+ settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
4797
+ } catch {
4798
+ }
4799
+ const perms = settings.permissions ?? {};
4800
+ const allow = perms.allow ?? [];
4801
+ const toolNames = [
4802
+ "recall_my_memory",
4803
+ "store_memory",
4804
+ "create_task",
4805
+ "update_task",
4806
+ "list_tasks",
4807
+ "get_task",
4808
+ "ask_team_memory",
4809
+ "store_behavior",
4810
+ "get_identity",
4811
+ "send_message"
4812
+ ];
4813
+ const requiredTools = expandDualPrefixTools(toolNames);
4814
+ let changed = false;
4815
+ for (const tool of requiredTools) {
4816
+ if (!allow.includes(tool)) {
4817
+ allow.push(tool);
4818
+ changed = true;
4819
+ }
4820
+ }
4821
+ if (changed) {
4822
+ perms.allow = allow;
4823
+ settings.permissions = perms;
4824
+ mkdirSync6(projSettingsDir, { recursive: true });
4825
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4826
+ }
4827
+ } catch {
4828
+ }
4829
+ const spawnCwd = opts?.cwd ?? projectDir;
4830
+ const useExeAgent = !!(opts?.model && opts?.provider);
4831
+ const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
4832
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4833
+ let identityFlag = "";
4834
+ let behaviorsFlag = "";
4835
+ let legacyFallbackWarned = false;
4836
+ if (!useExeAgent && !useBinSymlink) {
4837
+ const identityPath = path15.join(
4838
+ os7.homedir(),
4839
+ ".exe-os",
4840
+ "identity",
4841
+ `${employeeName}.md`
4842
+ );
4843
+ _resetCcAgentSupportCache();
4844
+ const hasAgentFlag = claudeSupportsAgentFlag();
4845
+ if (hasAgentFlag) {
4846
+ identityFlag = ` --agent ${employeeName}`;
4847
+ } else if (existsSync11(identityPath)) {
4848
+ identityFlag = ` --append-system-prompt-file ${identityPath}`;
4849
+ legacyFallbackWarned = true;
4850
+ }
4851
+ const behaviorsFile = exportBehaviorsSync(
4852
+ employeeName,
4853
+ path15.basename(spawnCwd),
4854
+ sessionName
4855
+ );
4856
+ if (behaviorsFile) {
4857
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
4858
+ }
4859
+ }
4860
+ if (legacyFallbackWarned) {
4861
+ process.stderr.write(
4862
+ `[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.
4863
+ `
4864
+ );
4865
+ }
4866
+ let sessionContextFlag = "";
4867
+ try {
4868
+ const ctxDir = path15.join(os7.homedir(), ".exe-os", "session-cache");
4869
+ mkdirSync6(ctxDir, { recursive: true });
4870
+ const ctxFile = path15.join(ctxDir, `session-context-${sessionName}.md`);
4871
+ const ctxContent = [
4872
+ `## Session Context`,
4873
+ `You are running in tmux session: ${sessionName}.`,
4874
+ `Your parent coordinator session is ${exeSession}.`,
4875
+ `Your employees (if any) use the -${exeSession} suffix.`
4876
+ ].join("\n");
4877
+ writeFileSync7(ctxFile, ctxContent);
4878
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
4879
+ } catch {
4880
+ }
4881
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
4882
+ if (ccProvider !== DEFAULT_PROVIDER) {
4883
+ const cfg = PROVIDER_TABLE[ccProvider];
4884
+ if (cfg?.apiKeyEnv) {
4885
+ const keyVal = process.env[cfg.apiKeyEnv];
4886
+ if (keyVal) {
4887
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
4888
+ }
4889
+ }
4890
+ }
4891
+ let spawnCommand;
4892
+ if (useExeAgent) {
4893
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4894
+ } else if (useBinSymlink) {
4895
+ const binName = `${employeeName}-${ccProvider}`;
4896
+ process.stderr.write(
4897
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
4898
+ `
4899
+ );
4900
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
4901
+ } else {
4902
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
4903
+ }
4904
+ const spawnResult = transport.spawn(sessionName, {
4905
+ cwd: spawnCwd,
4906
+ command: spawnCommand
4907
+ });
4908
+ if (spawnResult.error) {
4909
+ releaseSpawnLock2(sessionName);
4910
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4911
+ }
4912
+ transport.pipeLog(sessionName, logFile);
4913
+ try {
4914
+ const mySession = getMySession();
4915
+ const dispatchInfo = path15.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4916
+ writeFileSync7(dispatchInfo, JSON.stringify({
4917
+ dispatchedBy: mySession,
4918
+ rootExe: exeSession,
4919
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
4920
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
4921
+ }));
4922
+ } catch {
4923
+ }
4924
+ let booted = false;
4925
+ for (let i = 0; i < 30; i++) {
4926
+ try {
4927
+ execSync7("sleep 0.5");
4928
+ } catch {
4929
+ }
4930
+ try {
4931
+ const pane = transport.capturePane(sessionName);
4932
+ if (useExeAgent) {
4933
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
4934
+ booted = true;
4935
+ break;
4936
+ }
4937
+ } else {
4938
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
4939
+ booted = true;
4940
+ break;
4941
+ }
4942
+ }
4943
+ } catch {
4944
+ }
4945
+ }
4946
+ if (!booted) {
4947
+ releaseSpawnLock2(sessionName);
4948
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4949
+ }
4950
+ if (!useExeAgent) {
4951
+ try {
4952
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
4953
+ } catch {
4954
+ }
4955
+ }
4956
+ registerSession({
4957
+ windowName: sessionName,
4958
+ agentId: employeeName,
4959
+ projectDir: spawnCwd,
4960
+ parentExe: exeSession,
4961
+ pid: 0,
4962
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4963
+ });
4964
+ releaseSpawnLock2(sessionName);
4965
+ return { sessionName };
4966
+ }
4967
+ 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
4968
  var init_tmux_routing = __esm({
1481
4969
  "src/lib/tmux-routing.ts"() {
1482
4970
  "use strict";
@@ -1489,11 +4977,16 @@ var init_tmux_routing = __esm({
1489
4977
  init_intercom_queue();
1490
4978
  init_plan_limits();
1491
4979
  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");
4980
+ SPAWN_LOCK_DIR = path15.join(os7.homedir(), ".exe-os", "spawn-locks");
4981
+ SESSION_CACHE = path15.join(os7.homedir(), ".exe-os", "session-cache");
4982
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4983
+ VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4984
+ VERIFY_PANE_LINES = 200;
4985
+ INTERCOM_DEBOUNCE_MS = 3e4;
4986
+ INTERCOM_LOG2 = path15.join(os7.homedir(), ".exe-os", "intercom.log");
4987
+ DEBOUNCE_FILE = path15.join(SESSION_CACHE, "intercom-debounce.json");
1496
4988
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4989
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
1497
4990
  }
1498
4991
  });
1499
4992
 
@@ -1531,15 +5024,15 @@ var init_memory = __esm({
1531
5024
  });
1532
5025
 
1533
5026
  // 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";
5027
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
5028
+ import { existsSync as existsSync12 } from "fs";
5029
+ import path16 from "path";
5030
+ import os8 from "os";
1538
5031
  function getKeyDir() {
1539
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path9.join(os6.homedir(), ".exe-os");
5032
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path16.join(os8.homedir(), ".exe-os");
1540
5033
  }
1541
5034
  function getKeyPath() {
1542
- return path9.join(getKeyDir(), "master.key");
5035
+ return path16.join(getKeyDir(), "master.key");
1543
5036
  }
1544
5037
  async function tryKeytar() {
1545
5038
  try {
@@ -1560,77 +5053,30 @@ async function getMasterKey() {
1560
5053
  }
1561
5054
  }
1562
5055
  const keyPath = getKeyPath();
1563
- if (!existsSync7(keyPath)) {
5056
+ if (!existsSync12(keyPath)) {
5057
+ process.stderr.write(
5058
+ `[keychain] Key not found at ${keyPath} (HOME=${os8.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5059
+ `
5060
+ );
1564
5061
  return null;
1565
5062
  }
1566
5063
  try {
1567
- const content = await readFile3(keyPath, "utf-8");
5064
+ const content = await readFile4(keyPath, "utf-8");
1568
5065
  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();
5066
+ } catch (err) {
5067
+ process.stderr.write(
5068
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
5069
+ `
5070
+ );
5071
+ return null;
5072
+ }
5073
+ }
5074
+ var SERVICE, ACCOUNT;
5075
+ var init_keychain = __esm({
5076
+ "src/lib/keychain.ts"() {
5077
+ "use strict";
5078
+ SERVICE = "exe-mem";
5079
+ ACCOUNT = "master-key";
1634
5080
  }
1635
5081
  });
1636
5082
 
@@ -1647,13 +5093,13 @@ __export(shard_manager_exports, {
1647
5093
  listShards: () => listShards,
1648
5094
  shardExists: () => shardExists
1649
5095
  });
1650
- import path10 from "path";
1651
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2 } from "fs";
5096
+ import path17 from "path";
5097
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
1652
5098
  import { createClient as createClient2 } from "@libsql/client";
1653
5099
  function initShardManager(encryptionKey) {
1654
5100
  _encryptionKey = encryptionKey;
1655
- if (!existsSync8(SHARDS_DIR)) {
1656
- mkdirSync5(SHARDS_DIR, { recursive: true });
5101
+ if (!existsSync13(SHARDS_DIR)) {
5102
+ mkdirSync7(SHARDS_DIR, { recursive: true });
1657
5103
  }
1658
5104
  _shardingEnabled = true;
1659
5105
  }
@@ -1673,7 +5119,7 @@ function getShardClient(projectName) {
1673
5119
  }
1674
5120
  const cached = _shards.get(safeName);
1675
5121
  if (cached) return cached;
1676
- const dbPath = path10.join(SHARDS_DIR, `${safeName}.db`);
5122
+ const dbPath = path17.join(SHARDS_DIR, `${safeName}.db`);
1677
5123
  const client = createClient2({
1678
5124
  url: `file:${dbPath}`,
1679
5125
  encryptionKey: _encryptionKey
@@ -1683,11 +5129,11 @@ function getShardClient(projectName) {
1683
5129
  }
1684
5130
  function shardExists(projectName) {
1685
5131
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
1686
- return existsSync8(path10.join(SHARDS_DIR, `${safeName}.db`));
5132
+ return existsSync13(path17.join(SHARDS_DIR, `${safeName}.db`));
1687
5133
  }
1688
5134
  function listShards() {
1689
- if (!existsSync8(SHARDS_DIR)) return [];
1690
- return readdirSync2(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
5135
+ if (!existsSync13(SHARDS_DIR)) return [];
5136
+ return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
1691
5137
  }
1692
5138
  async function ensureShardSchema(client) {
1693
5139
  await client.execute("PRAGMA journal_mode = WAL");
@@ -1872,7 +5318,7 @@ var init_shard_manager = __esm({
1872
5318
  "src/lib/shard-manager.ts"() {
1873
5319
  "use strict";
1874
5320
  init_config();
1875
- SHARDS_DIR = path10.join(EXE_AI_DIR, "shards");
5321
+ SHARDS_DIR = path17.join(EXE_AI_DIR, "shards");
1876
5322
  _shards = /* @__PURE__ */ new Map();
1877
5323
  _encryptionKey = null;
1878
5324
  _shardingEnabled = false;
@@ -1997,7 +5443,7 @@ __export(global_procedures_exports, {
1997
5443
  loadGlobalProcedures: () => loadGlobalProcedures,
1998
5444
  storeGlobalProcedure: () => storeGlobalProcedure
1999
5445
  });
2000
- import { randomUUID as randomUUID2 } from "crypto";
5446
+ import { randomUUID as randomUUID3 } from "crypto";
2001
5447
  async function loadGlobalProcedures() {
2002
5448
  const client = getClient();
2003
5449
  const result = await client.execute({
@@ -2026,7 +5472,7 @@ ${sections.join("\n\n")}
2026
5472
  `;
2027
5473
  }
2028
5474
  async function storeGlobalProcedure(input2) {
2029
- const id = randomUUID2();
5475
+ const id = randomUUID3();
2030
5476
  const now = (/* @__PURE__ */ new Date()).toISOString();
2031
5477
  const client = getClient();
2032
5478
  await client.execute({
@@ -2077,6 +5523,7 @@ __export(store_exports, {
2077
5523
  vectorToBlob: () => vectorToBlob,
2078
5524
  writeMemory: () => writeMemory
2079
5525
  });
5526
+ import { createHash } from "crypto";
2080
5527
  function isBusyError2(err) {
2081
5528
  if (err instanceof Error) {
2082
5529
  const msg = err.message.toLowerCase();
@@ -2150,12 +5597,52 @@ function classifyTier(record) {
2150
5597
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
2151
5598
  return 3;
2152
5599
  }
5600
+ function inferFilePaths(record) {
5601
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
5602
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
5603
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
5604
+ return match ? JSON.stringify([match[1]]) : null;
5605
+ }
5606
+ function inferCommitHash(record) {
5607
+ if (record.tool_name !== "Bash") return null;
5608
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
5609
+ return match ? match[1] : null;
5610
+ }
5611
+ function inferLanguageType(record) {
5612
+ const text = record.raw_text;
5613
+ if (!text || text.length < 10) return null;
5614
+ const trimmed = text.trimStart();
5615
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
5616
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
5617
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
5618
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
5619
+ return "mixed";
5620
+ }
5621
+ function inferDomain(record) {
5622
+ const proj = (record.project_name ?? "").toLowerCase();
5623
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
5624
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
5625
+ return null;
5626
+ }
2153
5627
  async function writeMemory(record) {
2154
5628
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
2155
5629
  throw new Error(
2156
5630
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
2157
5631
  );
2158
5632
  }
5633
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
5634
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
5635
+ return;
5636
+ }
5637
+ try {
5638
+ const client = getClient();
5639
+ const existing = await client.execute({
5640
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
5641
+ args: [contentHash, record.agent_id]
5642
+ });
5643
+ if (existing.rows.length > 0) return;
5644
+ } catch {
5645
+ }
2159
5646
  const dbRow = {
2160
5647
  id: record.id,
2161
5648
  agent_id: record.agent_id,
@@ -2185,7 +5672,23 @@ async function writeMemory(record) {
2185
5672
  supersedes_id: record.supersedes_id ?? null,
2186
5673
  draft: record.draft ? 1 : 0,
2187
5674
  memory_type: record.memory_type ?? "raw",
2188
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
5675
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
5676
+ content_hash: contentHash,
5677
+ intent: record.intent ?? null,
5678
+ outcome: record.outcome ?? null,
5679
+ domain: record.domain ?? inferDomain(record),
5680
+ referenced_entities: record.referenced_entities ?? null,
5681
+ retrieval_count: record.retrieval_count ?? 0,
5682
+ chain_position: record.chain_position ?? null,
5683
+ review_status: record.review_status ?? null,
5684
+ context_window_pct: record.context_window_pct ?? null,
5685
+ file_paths: record.file_paths ?? inferFilePaths(record),
5686
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
5687
+ duration_ms: record.duration_ms ?? null,
5688
+ token_cost: record.token_cost ?? null,
5689
+ audience: record.audience ?? null,
5690
+ language_type: record.language_type ?? inferLanguageType(record),
5691
+ parent_memory_id: record.parent_memory_id ?? null
2189
5692
  };
2190
5693
  _pendingRecords.push(dbRow);
2191
5694
  orgBus.emit({
@@ -2243,80 +5746,85 @@ async function flushBatch() {
2243
5746
  const draft = row.draft ? 1 : 0;
2244
5747
  const memoryType = row.memory_type ?? "raw";
2245
5748
  const trajectory = row.trajectory ?? null;
2246
- return {
2247
- sql: hasVector ? `INSERT OR IGNORE INTO memories
2248
- (id, agent_id, agent_role, session_id, timestamp,
2249
- tool_name, project_name,
2250
- has_error, raw_text, vector, version, task_id, importance, status,
2251
- confidence, last_accessed,
2252
- 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,
5749
+ const contentHash = row.content_hash ?? null;
5750
+ const intent = row.intent ?? null;
5751
+ const outcome = row.outcome ?? null;
5752
+ const domain = row.domain ?? null;
5753
+ const referencedEntities = row.referenced_entities ?? null;
5754
+ const retrievalCount = row.retrieval_count ?? 0;
5755
+ const chainPosition = row.chain_position ?? null;
5756
+ const reviewStatus = row.review_status ?? null;
5757
+ const contextWindowPct = row.context_window_pct ?? null;
5758
+ const filePaths = row.file_paths ?? null;
5759
+ const commitHash = row.commit_hash ?? null;
5760
+ const durationMs = row.duration_ms ?? null;
5761
+ const tokenCost = row.token_cost ?? null;
5762
+ const audience = row.audience ?? null;
5763
+ const languageType = row.language_type ?? null;
5764
+ const parentMemoryId = row.parent_memory_id ?? null;
5765
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
2256
5766
  tool_name, project_name,
2257
5767
  has_error, raw_text, vector, version, task_id, importance, status,
2258
5768
  confidence, last_accessed,
2259
5769
  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
- ]
5770
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
5771
+ intent, outcome, domain, referenced_entities, retrieval_count,
5772
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
5773
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
5774
+ const metaArgs = [
5775
+ intent,
5776
+ outcome,
5777
+ domain,
5778
+ referencedEntities,
5779
+ retrievalCount,
5780
+ chainPosition,
5781
+ reviewStatus,
5782
+ contextWindowPct,
5783
+ filePaths,
5784
+ commitHash,
5785
+ durationMs,
5786
+ tokenCost,
5787
+ audience,
5788
+ languageType,
5789
+ parentMemoryId
5790
+ ];
5791
+ const baseArgs = [
5792
+ row.id,
5793
+ row.agent_id,
5794
+ row.agent_role,
5795
+ row.session_id,
5796
+ row.timestamp,
5797
+ row.tool_name,
5798
+ row.project_name,
5799
+ row.has_error,
5800
+ row.raw_text
5801
+ ];
5802
+ const sharedArgs = [
5803
+ row.version,
5804
+ taskId,
5805
+ importance,
5806
+ status,
5807
+ confidence,
5808
+ lastAccessed,
5809
+ workspaceId,
5810
+ documentId,
5811
+ userId,
5812
+ charOffset,
5813
+ pageNumber,
5814
+ sourcePath,
5815
+ sourceType,
5816
+ tier,
5817
+ supersedesId,
5818
+ draft,
5819
+ memoryType,
5820
+ trajectory,
5821
+ contentHash
5822
+ ];
5823
+ return {
5824
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
5825
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
5826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5827
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
2320
5828
  };
2321
5829
  };
2322
5830
  const globalClient = getClient();
@@ -2566,238 +6074,221 @@ var init_store = __esm({
2566
6074
  }
2567
6075
  });
2568
6076
 
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
6077
+ // src/lib/git-task-sweep.ts
6078
+ var git_task_sweep_exports = {};
6079
+ __export(git_task_sweep_exports, {
6080
+ extractKeywords: () => extractKeywords,
6081
+ findBestMatch: () => findBestMatch,
6082
+ getRecentCommits: () => getRecentCommits,
6083
+ matchScore: () => matchScore,
6084
+ sweepTasks: () => sweepTasks
2580
6085
  });
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
- }
6086
+ import { execSync as execSync8 } from "child_process";
6087
+ function extractKeywords(text) {
6088
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
2614
6089
  }
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);
6090
+ function matchScore(task, commitMessage, changedFiles) {
6091
+ if (task.id.length >= 8 && commitMessage.includes(task.id)) {
6092
+ return EXACT_UUID_SCORE;
6093
+ }
6094
+ let score = 0;
6095
+ const titleWords = extractKeywords(task.title);
6096
+ const commitWords = new Set(extractKeywords(commitMessage));
6097
+ const overlap = titleWords.filter((w) => commitWords.has(w));
6098
+ if (overlap.length >= MIN_KEYWORD_OVERLAP) {
6099
+ score += TITLE_KEYWORD_SCORE;
6100
+ }
6101
+ if (task.context && changedFiles.length > 0) {
6102
+ const contextLower = task.context.toLowerCase();
6103
+ const hasFileMatch = changedFiles.some(
6104
+ (f) => contextLower.includes(f.toLowerCase())
6105
+ );
6106
+ if (hasFileMatch) {
6107
+ score += FILE_PATH_SCORE;
2623
6108
  }
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
6109
  }
6110
+ return score;
2645
6111
  }
2646
- async function markAsRead(ids) {
2647
- if (ids.length === 0) return;
6112
+ function getRecentCommits(limit = DEFAULT_COMMIT_LIMIT) {
2648
6113
  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
- });
6114
+ const SEPARATOR = "<<SEP>>";
6115
+ const output = execSync8(
6116
+ `git log --format="%h${SEPARATOR}%s${SEPARATOR}%aI" --name-only -n ${limit} -z`,
6117
+ { encoding: "utf8", timeout: 1e4 }
6118
+ );
6119
+ const entries = output.split("\0").filter(Boolean);
6120
+ const commits = [];
6121
+ let current = null;
6122
+ for (const entry of entries) {
6123
+ if (entry.includes(SEPARATOR)) {
6124
+ const lines = entry.split("\n");
6125
+ const headerLine = lines[0];
6126
+ const parts = headerLine.split(SEPARATOR);
6127
+ if (parts.length >= 3) {
6128
+ if (current) commits.push(current);
6129
+ current = {
6130
+ hash: parts[0],
6131
+ message: parts[1],
6132
+ files: lines.slice(1).filter(Boolean),
6133
+ date: new Date(parts[2])
6134
+ };
6135
+ }
6136
+ } else if (current) {
6137
+ const files = entry.split("\n").filter(Boolean);
6138
+ current.files.push(...files);
6139
+ }
6140
+ }
6141
+ if (current) commits.push(current);
6142
+ return commits;
2655
6143
  } catch {
6144
+ return [];
2656
6145
  }
2657
6146
  }
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
- }
6147
+ function isStale(updatedAt, staleMinutes) {
6148
+ const updated = new Date(updatedAt).getTime();
6149
+ const threshold = Date.now() - staleMinutes * 6e4;
6150
+ return updated < threshold;
2667
6151
  }
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;
6152
+ function findBestMatch(task, commits) {
6153
+ let best = null;
6154
+ for (const commit of commits) {
6155
+ const score = matchScore(task, commit.message, commit.files);
6156
+ if (score >= AUTO_ESCALATE_THRESHOLD && (!best || score > best.score)) {
6157
+ best = { commit, score };
6158
+ }
2681
6159
  }
6160
+ return best;
2682
6161
  }
2683
- async function markDoneTaskNotificationsAsRead() {
6162
+ async function sweepTasks(projectName, options = {}) {
6163
+ const commitLimit = options.commitLimit ?? DEFAULT_COMMIT_LIMIT;
6164
+ const staleMinutes = options.staleMinutes ?? DEFAULT_STALE_MINUTES;
6165
+ const dryRun = options.dryRun ?? false;
6166
+ const result = { escalated: [], unchanged: 0, errors: [] };
6167
+ const commits = getRecentCommits(commitLimit);
6168
+ if (commits.length === 0) {
6169
+ result.errors.push("No git commits found (not a git repo or empty history)");
6170
+ return result;
6171
+ }
6172
+ let tasks;
2684
6173
  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: []
6174
+ const { initStore: initStore2 } = await Promise.resolve().then(() => (init_store(), store_exports));
6175
+ await initStore2();
6176
+ const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
6177
+ const client = getClient2();
6178
+ const conditions = ["status = 'in_progress'"];
6179
+ const args = [];
6180
+ if (projectName) {
6181
+ conditions.push("project_name = ?");
6182
+ args.push(projectName);
6183
+ }
6184
+ const swScope = sessionScopeFilter();
6185
+ if (swScope.sql) {
6186
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
6187
+ args.push(...swScope.args);
6188
+ }
6189
+ const queryResult = await client.execute({
6190
+ sql: `SELECT id, title, assigned_to, project_name, status, updated_at, context
6191
+ FROM tasks WHERE ${conditions.join(" AND ")}
6192
+ ORDER BY updated_at ASC`,
6193
+ args
2694
6194
  });
2695
- return result.rowsAffected;
2696
- } catch {
2697
- return 0;
6195
+ tasks = queryResult.rows.map((r) => ({
6196
+ id: String(r.id),
6197
+ title: String(r.title),
6198
+ assignedTo: String(r.assigned_to),
6199
+ projectName: String(r.project_name),
6200
+ status: String(r.status),
6201
+ updatedAt: String(r.updated_at),
6202
+ context: r.context ? String(r.context) : void 0
6203
+ }));
6204
+ } catch (err) {
6205
+ result.errors.push(`DB query failed: ${err instanceof Error ? err.message : String(err)}`);
6206
+ return result;
2698
6207
  }
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);
6208
+ if (tasks.length === 0) {
6209
+ return result;
2707
6210
  }
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}`);
6211
+ for (const task of tasks) {
6212
+ if (!isStale(task.updatedAt, staleMinutes)) {
6213
+ result.unchanged++;
6214
+ continue;
2718
6215
  }
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) {
6216
+ const match = findBestMatch(task, commits);
6217
+ if (!match) {
6218
+ result.unchanged++;
6219
+ continue;
6220
+ }
6221
+ if (!dryRun) {
2733
6222
  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
- ]
6223
+ const { updateTaskStatus: updateTaskStatus2 } = await Promise.resolve().then(() => (init_tasks_crud(), tasks_crud_exports));
6224
+ await updateTaskStatus2({
6225
+ taskId: task.id,
6226
+ status: "needs_review",
6227
+ result: `Auto-escalated by git-sweep: matching commit ${match.commit.hash} found (score: ${match.score.toFixed(2)})`
2750
6228
  });
2751
- unlinkSync3(filePath);
2752
- migrated++;
2753
- } catch {
2754
- }
2755
- }
2756
- try {
2757
- const remaining = readdirSync3(notifDir);
2758
- if (remaining.length === 0) {
2759
- rmdirSync(notifDir);
6229
+ } catch (err) {
6230
+ result.errors.push(
6231
+ `Failed to escalate task ${task.id}: ${err instanceof Error ? err.message : String(err)}`
6232
+ );
6233
+ continue;
2760
6234
  }
2761
- } catch {
2762
6235
  }
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:";
6236
+ result.escalated.push({
6237
+ taskId: task.id,
6238
+ title: task.title,
6239
+ matchedCommit: match.commit.hash,
6240
+ score: match.score
6241
+ });
6242
+ process.stderr.write(
6243
+ `[git-sweep] ${dryRun ? "WOULD escalate" : "Escalated"} task ${task.id} \u2192 commit ${match.commit.hash} (score: ${match.score.toFixed(2)})
6244
+ `
6245
+ );
2783
6246
  }
6247
+ return result;
2784
6248
  }
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"() {
6249
+ var DEFAULT_COMMIT_LIMIT, DEFAULT_STALE_MINUTES, AUTO_ESCALATE_THRESHOLD, EXACT_UUID_SCORE, TITLE_KEYWORD_SCORE, FILE_PATH_SCORE, MIN_KEYWORD_OVERLAP, STOP_WORDS;
6250
+ var init_git_task_sweep = __esm({
6251
+ "src/lib/git-task-sweep.ts"() {
2798
6252
  "use strict";
2799
- init_database();
2800
- CLEANUP_DAYS = 7;
6253
+ init_task_scope();
6254
+ DEFAULT_COMMIT_LIMIT = 50;
6255
+ DEFAULT_STALE_MINUTES = 10;
6256
+ AUTO_ESCALATE_THRESHOLD = 0.6;
6257
+ EXACT_UUID_SCORE = 1;
6258
+ TITLE_KEYWORD_SCORE = 0.5;
6259
+ FILE_PATH_SCORE = 0.3;
6260
+ MIN_KEYWORD_OVERLAP = 3;
6261
+ STOP_WORDS = /* @__PURE__ */ new Set([
6262
+ "a",
6263
+ "an",
6264
+ "the",
6265
+ "and",
6266
+ "or",
6267
+ "but",
6268
+ "in",
6269
+ "on",
6270
+ "at",
6271
+ "to",
6272
+ "for",
6273
+ "of",
6274
+ "with",
6275
+ "by",
6276
+ "from",
6277
+ "is",
6278
+ "it",
6279
+ "as",
6280
+ "be",
6281
+ "was",
6282
+ "are",
6283
+ "this",
6284
+ "that",
6285
+ "not",
6286
+ "no",
6287
+ "if",
6288
+ "so",
6289
+ "do",
6290
+ "up"
6291
+ ]);
2801
6292
  }
2802
6293
  });
2803
6294
 
@@ -2938,7 +6429,7 @@ process.stdin.on("end", async () => {
2938
6429
  await initStore2();
2939
6430
  const { writeMemory: writeMemory2, flushBatch: flushBatch2 } = await Promise.resolve().then(() => (init_store(), store_exports));
2940
6431
  const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
2941
- const { randomUUID: randomUUID3 } = await import("crypto");
6432
+ const { randomUUID: randomUUID4 } = await import("crypto");
2942
6433
  const client = getClient2();
2943
6434
  const seScope = sessionScopeFilter();
2944
6435
  const orphanResult = await client.execute({
@@ -2948,7 +6439,7 @@ process.stdin.on("end", async () => {
2948
6439
  const orphanInfo = orphanResult.rows.length > 0 ? `
2949
6440
  Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title)}" (${String(r.status)})`).join(", ")}` : "";
2950
6441
  await writeMemory2({
2951
- id: randomUUID3(),
6442
+ id: randomUUID4(),
2952
6443
  agent_id: agent.agentId,
2953
6444
  agent_role: agent.agentRole,
2954
6445
  session_id: data.session_id,
@@ -2968,15 +6459,57 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
2968
6459
  `[session-end] WARNING: ${agent.agentId} ended with ${inProgress.length} in_progress task(s): ${titles}
2969
6460
  `
2970
6461
  );
6462
+ let commits = [];
6463
+ try {
6464
+ const { getRecentCommits: getRecentCommits2 } = await Promise.resolve().then(() => (init_git_task_sweep(), git_task_sweep_exports));
6465
+ commits = getRecentCommits2(30);
6466
+ } catch {
6467
+ }
6468
+ const autoClosed = [];
6469
+ const leftInProgress = [];
2971
6470
  for (const row of inProgress) {
6471
+ const title = String(row.title);
2972
6472
  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
- });
6473
+ if (commits.length > 0) {
6474
+ const { findBestMatch: findBestMatch2 } = await Promise.resolve().then(() => (init_git_task_sweep(), git_task_sweep_exports));
6475
+ let context;
6476
+ try {
6477
+ const ctxResult = await client.execute({
6478
+ sql: "SELECT id, context FROM tasks WHERE title = ? AND assigned_to = ? AND status = 'in_progress' LIMIT 1",
6479
+ args: [title, agent.agentId]
6480
+ });
6481
+ if (ctxResult.rows.length > 0) {
6482
+ context = ctxResult.rows[0].context ? String(ctxResult.rows[0].context) : void 0;
6483
+ }
6484
+ } catch {
6485
+ }
6486
+ const taskForMatch = { id: "", title, context };
6487
+ const match = findBestMatch2(taskForMatch, commits);
6488
+ if (match) {
6489
+ await client.execute({
6490
+ sql: "UPDATE tasks SET status = 'done', result = ?, updated_at = ? WHERE title = ? AND assigned_to = ? AND status = 'in_progress'",
6491
+ args: [
6492
+ `Auto-closed: session ended but matching commit ${match.commit.hash} found (score: ${match.score.toFixed(2)}). Message: "${match.commit.message}"`,
6493
+ (/* @__PURE__ */ new Date()).toISOString(),
6494
+ title,
6495
+ agent.agentId
6496
+ ]
6497
+ });
6498
+ autoClosed.push(`"${title}" \u2192 commit ${match.commit.hash}`);
6499
+ continue;
6500
+ }
6501
+ }
6502
+ leftInProgress.push(`"${title}"`);
2977
6503
  } catch {
2978
6504
  }
2979
6505
  }
6506
+ const parts = [];
6507
+ if (autoClosed.length > 0) {
6508
+ parts.push(`Auto-closed (work committed): ${autoClosed.join(", ")}`);
6509
+ }
6510
+ if (leftInProgress.length > 0) {
6511
+ parts.push(`Left in_progress (no matching commits, needs triage): ${leftInProgress.join(", ")}`);
6512
+ }
2980
6513
  try {
2981
6514
  const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
2982
6515
  await writeNotification2({
@@ -2984,10 +6517,18 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
2984
6517
  agentRole: agent.agentRole,
2985
6518
  event: "orphan_task",
2986
6519
  project: process.env.EXE_PROJECT_NAME ?? "unknown",
2987
- summary: `${agent.agentId} session ended with ${inProgress.length} in_progress task(s): ${titles}. Tasks marked blocked.`
6520
+ summary: `${agent.agentId} session ended with ${inProgress.length} in_progress task(s). ${parts.join(". ")}`
2988
6521
  });
2989
6522
  } catch {
2990
6523
  }
6524
+ if (autoClosed.length > 0) {
6525
+ process.stderr.write(`[session-end] Auto-closed ${autoClosed.length} task(s) with matching commits
6526
+ `);
6527
+ }
6528
+ if (leftInProgress.length > 0) {
6529
+ process.stderr.write(`[session-end] Left ${leftInProgress.length} task(s) as in_progress for coordinator triage
6530
+ `);
6531
+ }
2991
6532
  }
2992
6533
  }
2993
6534
  } catch {