@askexenow/exe-os 0.9.8 → 0.9.9

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 (101) hide show
  1. package/dist/bin/backfill-conversations.js +222 -49
  2. package/dist/bin/backfill-responses.js +221 -48
  3. package/dist/bin/backfill-vectors.js +225 -52
  4. package/dist/bin/cleanup-stale-review-tasks.js +150 -28
  5. package/dist/bin/cli.js +1295 -856
  6. package/dist/bin/exe-agent-config.js +36 -8
  7. package/dist/bin/exe-agent.js +14 -4
  8. package/dist/bin/exe-assign.js +221 -48
  9. package/dist/bin/exe-boot.js +778 -427
  10. package/dist/bin/exe-call.js +41 -13
  11. package/dist/bin/exe-cloud.js +163 -58
  12. package/dist/bin/exe-dispatch.js +276 -139
  13. package/dist/bin/exe-doctor.js +145 -27
  14. package/dist/bin/exe-export-behaviors.js +141 -23
  15. package/dist/bin/exe-forget.js +137 -19
  16. package/dist/bin/exe-gateway.js +677 -388
  17. package/dist/bin/exe-heartbeat.js +227 -108
  18. package/dist/bin/exe-kill.js +138 -20
  19. package/dist/bin/exe-launch-agent.js +172 -39
  20. package/dist/bin/exe-link.js +291 -100
  21. package/dist/bin/exe-new-employee.js +214 -106
  22. package/dist/bin/exe-pending-messages.js +395 -33
  23. package/dist/bin/exe-pending-notifications.js +684 -99
  24. package/dist/bin/exe-pending-reviews.js +420 -74
  25. package/dist/bin/exe-rename.js +147 -49
  26. package/dist/bin/exe-review.js +138 -20
  27. package/dist/bin/exe-search.js +240 -69
  28. package/dist/bin/exe-session-cleanup.js +440 -250
  29. package/dist/bin/exe-settings.js +61 -17
  30. package/dist/bin/exe-start-codex.js +158 -39
  31. package/dist/bin/exe-start-opencode.js +157 -38
  32. package/dist/bin/exe-status.js +151 -29
  33. package/dist/bin/exe-team.js +138 -20
  34. package/dist/bin/git-sweep.js +404 -212
  35. package/dist/bin/graph-backfill.js +137 -19
  36. package/dist/bin/graph-export.js +140 -22
  37. package/dist/bin/install.js +90 -61
  38. package/dist/bin/scan-tasks.js +412 -220
  39. package/dist/bin/setup.js +564 -293
  40. package/dist/bin/shard-migrate.js +139 -21
  41. package/dist/bin/update.js +138 -49
  42. package/dist/bin/wiki-sync.js +137 -19
  43. package/dist/gateway/index.js +533 -320
  44. package/dist/hooks/bug-report-worker.js +344 -193
  45. package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
  46. package/dist/hooks/commit-complete.js +402 -210
  47. package/dist/hooks/error-recall.js +245 -74
  48. package/dist/hooks/exe-heartbeat-hook.js +16 -6
  49. package/dist/hooks/ingest-worker.js +3423 -3157
  50. package/dist/hooks/ingest.js +832 -97
  51. package/dist/hooks/instructions-loaded.js +227 -54
  52. package/dist/hooks/notification.js +216 -43
  53. package/dist/hooks/post-compact.js +239 -62
  54. package/dist/hooks/pre-compact.js +408 -216
  55. package/dist/hooks/pre-tool-use.js +268 -90
  56. package/dist/hooks/prompt-ingest-worker.js +352 -102
  57. package/dist/hooks/prompt-submit.js +541 -328
  58. package/dist/hooks/response-ingest-worker.js +372 -122
  59. package/dist/hooks/session-end.js +443 -240
  60. package/dist/hooks/session-start.js +313 -127
  61. package/dist/hooks/stop.js +293 -98
  62. package/dist/hooks/subagent-stop.js +239 -62
  63. package/dist/hooks/summary-worker.js +568 -236
  64. package/dist/index.js +538 -324
  65. package/dist/lib/agent-config.js +28 -6
  66. package/dist/lib/cloud-sync.js +284 -105
  67. package/dist/lib/config.js +30 -10
  68. package/dist/lib/consolidation.js +16 -6
  69. package/dist/lib/database.js +123 -25
  70. package/dist/lib/db-daemon-client.js +73 -19
  71. package/dist/lib/db.js +123 -25
  72. package/dist/lib/device-registry.js +133 -35
  73. package/dist/lib/embedder.js +107 -32
  74. package/dist/lib/employee-templates.js +14 -4
  75. package/dist/lib/employees.js +41 -13
  76. package/dist/lib/exe-daemon-client.js +88 -22
  77. package/dist/lib/exe-daemon.js +935 -587
  78. package/dist/lib/hybrid-search.js +240 -69
  79. package/dist/lib/identity.js +18 -8
  80. package/dist/lib/license.js +133 -48
  81. package/dist/lib/messaging.js +116 -56
  82. package/dist/lib/reminders.js +14 -4
  83. package/dist/lib/schedules.js +137 -19
  84. package/dist/lib/skill-learning.js +33 -6
  85. package/dist/lib/store.js +137 -19
  86. package/dist/lib/task-router.js +14 -4
  87. package/dist/lib/tasks.js +280 -234
  88. package/dist/lib/tmux-routing.js +172 -125
  89. package/dist/lib/token-spend.js +26 -8
  90. package/dist/mcp/server.js +1326 -609
  91. package/dist/mcp/tools/complete-reminder.js +14 -4
  92. package/dist/mcp/tools/create-reminder.js +14 -4
  93. package/dist/mcp/tools/create-task.js +306 -248
  94. package/dist/mcp/tools/deactivate-behavior.js +16 -6
  95. package/dist/mcp/tools/list-reminders.js +14 -4
  96. package/dist/mcp/tools/list-tasks.js +123 -107
  97. package/dist/mcp/tools/send-message.js +75 -29
  98. package/dist/mcp/tools/update-task.js +1848 -199
  99. package/dist/runtime/index.js +441 -248
  100. package/dist/tui/App.js +761 -424
  101. package/package.json +1 -1
@@ -32,9 +32,34 @@ var init_db_retry = __esm({
32
32
  }
33
33
  });
34
34
 
35
+ // src/lib/secure-files.ts
36
+ import { chmodSync, existsSync, mkdirSync } from "fs";
37
+ import { chmod, mkdir } from "fs/promises";
38
+ async function ensurePrivateDir(dirPath) {
39
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
40
+ try {
41
+ await chmod(dirPath, PRIVATE_DIR_MODE);
42
+ } catch {
43
+ }
44
+ }
45
+ async function enforcePrivateFile(filePath) {
46
+ try {
47
+ await chmod(filePath, PRIVATE_FILE_MODE);
48
+ } catch {
49
+ }
50
+ }
51
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
52
+ var init_secure_files = __esm({
53
+ "src/lib/secure-files.ts"() {
54
+ "use strict";
55
+ PRIVATE_DIR_MODE = 448;
56
+ PRIVATE_FILE_MODE = 384;
57
+ }
58
+ });
59
+
35
60
  // src/lib/config.ts
36
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
37
- import { readFileSync, existsSync, renameSync } from "fs";
61
+ import { readFile, writeFile } from "fs/promises";
62
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
38
63
  import path from "path";
39
64
  import os from "os";
40
65
  function resolveDataDir() {
@@ -42,7 +67,7 @@ function resolveDataDir() {
42
67
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
43
68
  const newDir = path.join(os.homedir(), ".exe-os");
44
69
  const legacyDir = path.join(os.homedir(), ".exe-mem");
45
- if (!existsSync(newDir) && existsSync(legacyDir)) {
70
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
46
71
  try {
47
72
  renameSync(legacyDir, newDir);
48
73
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -105,9 +130,9 @@ function normalizeAutoUpdate(raw) {
105
130
  }
106
131
  async function loadConfig() {
107
132
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
108
- await mkdir(dir, { recursive: true });
133
+ await ensurePrivateDir(dir);
109
134
  const configPath = path.join(dir, "config.json");
110
- if (!existsSync(configPath)) {
135
+ if (!existsSync2(configPath)) {
111
136
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
112
137
  }
113
138
  const raw = await readFile(configPath, "utf-8");
@@ -120,6 +145,7 @@ async function loadConfig() {
120
145
  `);
121
146
  try {
122
147
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
148
+ await enforcePrivateFile(configPath);
123
149
  } catch {
124
150
  }
125
151
  }
@@ -139,6 +165,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
139
165
  var init_config = __esm({
140
166
  "src/lib/config.ts"() {
141
167
  "use strict";
168
+ init_secure_files();
142
169
  EXE_AI_DIR = resolveDataDir();
143
170
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
144
171
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
@@ -217,7 +244,7 @@ var init_config = __esm({
217
244
 
218
245
  // src/lib/employees.ts
219
246
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
220
- import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
247
+ import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
221
248
  import { execSync } from "child_process";
222
249
  import path2 from "path";
223
250
  import os2 from "os";
@@ -241,7 +268,7 @@ function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
241
268
  return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
242
269
  }
243
270
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
244
- if (!existsSync2(employeesPath)) return [];
271
+ if (!existsSync3(employeesPath)) return [];
245
272
  try {
246
273
  return JSON.parse(readFileSync2(employeesPath, "utf-8"));
247
274
  } catch {
@@ -259,7 +286,13 @@ function baseAgentName(name, employees) {
259
286
  if (getEmployee(roster, base)) return base;
260
287
  return name;
261
288
  }
262
- var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, IDENTITY_DIR;
289
+ function isMultiInstance(agentName, employees) {
290
+ const roster = employees ?? loadEmployeesSync();
291
+ const emp = getEmployee(roster, agentName);
292
+ if (!emp) return false;
293
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
294
+ }
295
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES, IDENTITY_DIR;
263
296
  var init_employees = __esm({
264
297
  "src/lib/employees.ts"() {
265
298
  "use strict";
@@ -267,6 +300,7 @@ var init_employees = __esm({
267
300
  EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
268
301
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
269
302
  COORDINATOR_ROLE = "COO";
303
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
270
304
  IDENTITY_DIR = path2.join(EXE_AI_DIR, "identity");
271
305
  }
272
306
  });
@@ -322,121 +356,37 @@ var init_database = __esm({
322
356
  }
323
357
  });
324
358
 
325
- // src/lib/notifications.ts
326
- import crypto from "crypto";
359
+ // src/lib/session-registry.ts
360
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
327
361
  import path4 from "path";
328
362
  import os4 from "os";
329
- import {
330
- readFileSync as readFileSync3,
331
- readdirSync,
332
- unlinkSync as unlinkSync2,
333
- existsSync as existsSync3,
334
- rmdirSync
335
- } from "fs";
336
- async function writeNotification(notification) {
337
- try {
338
- const client = getClient();
339
- const id = crypto.randomUUID();
340
- const now = (/* @__PURE__ */ new Date()).toISOString();
341
- await client.execute({
342
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
343
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
344
- args: [
345
- id,
346
- notification.agentId,
347
- notification.agentRole,
348
- notification.event,
349
- notification.project,
350
- notification.summary,
351
- notification.taskFile ?? null,
352
- now
353
- ]
354
- });
355
- } catch (err) {
356
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
357
- `);
363
+ function registerSession(entry) {
364
+ const dir = path4.dirname(REGISTRY_PATH);
365
+ if (!existsSync4(dir)) {
366
+ mkdirSync2(dir, { recursive: true });
367
+ }
368
+ const sessions = listSessions();
369
+ const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
370
+ if (idx >= 0) {
371
+ sessions[idx] = entry;
372
+ } else {
373
+ sessions.push(entry);
358
374
  }
375
+ writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
359
376
  }
360
- async function markAsReadByTaskFile(taskFile) {
377
+ function listSessions() {
361
378
  try {
362
- const client = getClient();
363
- await client.execute({
364
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
365
- args: [taskFile]
366
- });
379
+ const raw = readFileSync3(REGISTRY_PATH, "utf8");
380
+ return JSON.parse(raw);
367
381
  } catch {
382
+ return [];
368
383
  }
369
384
  }
370
- var init_notifications = __esm({
371
- "src/lib/notifications.ts"() {
372
- "use strict";
373
- init_database();
374
- }
375
- });
376
-
377
- // src/lib/state-bus.ts
378
- var StateBus, orgBus;
379
- var init_state_bus = __esm({
380
- "src/lib/state-bus.ts"() {
381
- "use strict";
382
- StateBus = class {
383
- handlers = /* @__PURE__ */ new Map();
384
- globalHandlers = /* @__PURE__ */ new Set();
385
- /** Emit an event to all subscribers */
386
- emit(event) {
387
- const typeHandlers = this.handlers.get(event.type);
388
- if (typeHandlers) {
389
- for (const handler of typeHandlers) {
390
- try {
391
- handler(event);
392
- } catch {
393
- }
394
- }
395
- }
396
- for (const handler of this.globalHandlers) {
397
- try {
398
- handler(event);
399
- } catch {
400
- }
401
- }
402
- }
403
- /** Subscribe to a specific event type */
404
- on(type, handler) {
405
- if (!this.handlers.has(type)) {
406
- this.handlers.set(type, /* @__PURE__ */ new Set());
407
- }
408
- this.handlers.get(type).add(handler);
409
- }
410
- /** Subscribe to ALL events */
411
- onAny(handler) {
412
- this.globalHandlers.add(handler);
413
- }
414
- /** Unsubscribe from a specific event type */
415
- off(type, handler) {
416
- this.handlers.get(type)?.delete(handler);
417
- }
418
- /** Unsubscribe from ALL events */
419
- offAny(handler) {
420
- this.globalHandlers.delete(handler);
421
- }
422
- /** Remove all listeners */
423
- clear() {
424
- this.handlers.clear();
425
- this.globalHandlers.clear();
426
- }
427
- };
428
- orgBus = new StateBus();
429
- }
430
- });
431
-
432
- // src/lib/session-registry.ts
433
- import path5 from "path";
434
- import os5 from "os";
435
385
  var REGISTRY_PATH;
436
386
  var init_session_registry = __esm({
437
387
  "src/lib/session-registry.ts"() {
438
388
  "use strict";
439
- REGISTRY_PATH = path5.join(os5.homedir(), ".exe-os", "session-registry.json");
389
+ REGISTRY_PATH = path4.join(os4.homedir(), ".exe-os", "session-registry.json");
440
390
  }
441
391
  });
442
392
 
@@ -617,13 +567,40 @@ var init_transport = __esm({
617
567
 
618
568
  // src/lib/cc-agent-support.ts
619
569
  import { execSync as execSync3 } from "child_process";
570
+ function _resetCcAgentSupportCache() {
571
+ _cachedSupport = null;
572
+ }
573
+ function claudeSupportsAgentFlag() {
574
+ if (_cachedSupport !== null) return _cachedSupport;
575
+ try {
576
+ const helpOutput = execSync3("claude --help 2>&1", {
577
+ encoding: "utf-8",
578
+ timeout: 5e3
579
+ });
580
+ _cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
581
+ } catch {
582
+ _cachedSupport = false;
583
+ }
584
+ return _cachedSupport;
585
+ }
586
+ var _cachedSupport;
620
587
  var init_cc_agent_support = __esm({
621
588
  "src/lib/cc-agent-support.ts"() {
622
589
  "use strict";
590
+ _cachedSupport = null;
623
591
  }
624
592
  });
625
593
 
626
594
  // src/lib/mcp-prefix.ts
595
+ function expandDualPrefixTools(shortNames) {
596
+ const out = [];
597
+ for (const name of shortNames) {
598
+ for (const prefix of MCP_TOOL_PREFIXES) {
599
+ out.push(prefix + name);
600
+ }
601
+ }
602
+ return out;
603
+ }
627
604
  var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
628
605
  var init_mcp_prefix = __esm({
629
606
  "src/lib/mcp-prefix.ts"() {
@@ -638,9 +615,26 @@ var init_mcp_prefix = __esm({
638
615
  });
639
616
 
640
617
  // src/lib/provider-table.ts
618
+ function detectActiveProvider(env = process.env) {
619
+ const baseUrl = env.ANTHROPIC_BASE_URL;
620
+ if (!baseUrl) return DEFAULT_PROVIDER;
621
+ for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
622
+ if (cfg.baseUrl === baseUrl) return name;
623
+ }
624
+ return DEFAULT_PROVIDER;
625
+ }
626
+ var PROVIDER_TABLE, DEFAULT_PROVIDER;
641
627
  var init_provider_table = __esm({
642
628
  "src/lib/provider-table.ts"() {
643
629
  "use strict";
630
+ PROVIDER_TABLE = {
631
+ opencode: {
632
+ baseUrl: "https://opencode.ai/zen/go",
633
+ apiKeyEnv: "OPENCODE_API_KEY",
634
+ defaultModel: "minimax-m2.7"
635
+ }
636
+ };
637
+ DEFAULT_PROVIDER = "default";
644
638
  }
645
639
  });
646
640
 
@@ -672,10 +666,10 @@ var init_runtime_table = __esm({
672
666
  });
673
667
 
674
668
  // src/lib/agent-config.ts
675
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync } from "fs";
676
- import path6 from "path";
669
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
670
+ import path5 from "path";
677
671
  function loadAgentConfig() {
678
- if (!existsSync4(AGENT_CONFIG_PATH)) return {};
672
+ if (!existsSync5(AGENT_CONFIG_PATH)) return {};
679
673
  try {
680
674
  return JSON.parse(readFileSync4(AGENT_CONFIG_PATH, "utf-8"));
681
675
  } catch {
@@ -696,7 +690,8 @@ var init_agent_config = __esm({
696
690
  "use strict";
697
691
  init_config();
698
692
  init_runtime_table();
699
- AGENT_CONFIG_PATH = path6.join(EXE_AI_DIR, "agent-config.json");
693
+ init_secure_files();
694
+ AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
700
695
  DEFAULT_MODELS = {
701
696
  claude: "claude-opus-4",
702
697
  codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
@@ -714,16 +709,16 @@ __export(intercom_queue_exports, {
714
709
  queueIntercom: () => queueIntercom,
715
710
  readQueue: () => readQueue
716
711
  });
717
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
718
- import path7 from "path";
719
- import os6 from "os";
712
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
713
+ import path6 from "path";
714
+ import os5 from "os";
720
715
  function ensureDir() {
721
- const dir = path7.dirname(QUEUE_PATH);
722
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
716
+ const dir = path6.dirname(QUEUE_PATH);
717
+ if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
723
718
  }
724
719
  function readQueue() {
725
720
  try {
726
- if (!existsSync5(QUEUE_PATH)) return [];
721
+ if (!existsSync6(QUEUE_PATH)) return [];
727
722
  return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
728
723
  } catch {
729
724
  return [];
@@ -732,7 +727,7 @@ function readQueue() {
732
727
  function writeQueue(queue) {
733
728
  ensureDir();
734
729
  const tmp = `${QUEUE_PATH}.tmp`;
735
- writeFileSync3(tmp, JSON.stringify(queue, null, 2));
730
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
736
731
  renameSync3(tmp, QUEUE_PATH);
737
732
  }
738
733
  function queueIntercom(targetSession, reason) {
@@ -752,7 +747,7 @@ function queueIntercom(targetSession, reason) {
752
747
  }
753
748
  writeQueue(queue);
754
749
  }
755
- function drainQueue(isSessionBusy, sendKeys) {
750
+ function drainQueue(isSessionBusy2, sendKeys) {
756
751
  const queue = readQueue();
757
752
  if (queue.length === 0) return { drained: 0, failed: 0 };
758
753
  const remaining = [];
@@ -766,7 +761,7 @@ function drainQueue(isSessionBusy, sendKeys) {
766
761
  continue;
767
762
  }
768
763
  try {
769
- if (!isSessionBusy(item.targetSession)) {
764
+ if (!isSessionBusy2(item.targetSession)) {
770
765
  const success = sendKeys(item.targetSession);
771
766
  if (success) {
772
767
  logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
@@ -824,33 +819,100 @@ var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
824
819
  var init_intercom_queue = __esm({
825
820
  "src/lib/intercom-queue.ts"() {
826
821
  "use strict";
827
- QUEUE_PATH = path7.join(os6.homedir(), ".exe-os", "intercom-queue.json");
822
+ QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
828
823
  MAX_RETRIES = 5;
829
824
  TTL_MS = 60 * 60 * 1e3;
830
- INTERCOM_LOG = path7.join(os6.homedir(), ".exe-os", "intercom.log");
825
+ INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
831
826
  }
832
827
  });
833
828
 
834
829
  // src/lib/license.ts
835
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
830
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
836
831
  import { randomUUID } from "crypto";
837
- import path8 from "path";
832
+ import { createRequire as createRequire2 } from "module";
833
+ import { pathToFileURL as pathToFileURL2 } from "url";
834
+ import os6 from "os";
835
+ import path7 from "path";
838
836
  import { jwtVerify, importSPKI } from "jose";
839
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
837
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
840
838
  var init_license = __esm({
841
839
  "src/lib/license.ts"() {
842
840
  "use strict";
843
841
  init_config();
844
- LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
845
- CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
846
- DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
842
+ LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
843
+ CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
844
+ DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
845
+ PLAN_LIMITS = {
846
+ free: { devices: 1, employees: 1, memories: 5e3 },
847
+ pro: { devices: 3, employees: 5, memories: 1e5 },
848
+ team: { devices: 10, employees: 20, memories: 1e6 },
849
+ agency: { devices: 50, employees: 100, memories: 1e7 },
850
+ enterprise: { devices: -1, employees: -1, memories: -1 }
851
+ };
847
852
  }
848
853
  });
849
854
 
850
855
  // src/lib/plan-limits.ts
851
- import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
852
- import path9 from "path";
853
- var CACHE_PATH2;
856
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
857
+ import path8 from "path";
858
+ function getLicenseSync() {
859
+ try {
860
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
861
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
862
+ if (!raw.token || typeof raw.token !== "string") return freeLicense();
863
+ const parts = raw.token.split(".");
864
+ if (parts.length !== 3) return freeLicense();
865
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
866
+ const plan = payload.plan ?? "free";
867
+ const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
868
+ return {
869
+ valid: true,
870
+ plan,
871
+ email: payload.sub ?? "",
872
+ expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
873
+ deviceLimit: limits.devices,
874
+ employeeLimit: limits.employees,
875
+ memoryLimit: limits.memories
876
+ };
877
+ } catch {
878
+ return freeLicense();
879
+ }
880
+ }
881
+ function freeLicense() {
882
+ const limits = PLAN_LIMITS.free;
883
+ return {
884
+ valid: true,
885
+ plan: "free",
886
+ email: "",
887
+ expiresAt: null,
888
+ deviceLimit: limits.devices,
889
+ employeeLimit: limits.employees,
890
+ memoryLimit: limits.memories
891
+ };
892
+ }
893
+ function assertEmployeeLimitSync(rosterPath) {
894
+ const license = getLicenseSync();
895
+ if (license.employeeLimit < 0) return;
896
+ const filePath = rosterPath ?? EMPLOYEES_PATH;
897
+ let count = 0;
898
+ try {
899
+ if (existsSync8(filePath)) {
900
+ const raw = readFileSync7(filePath, "utf8");
901
+ const employees = JSON.parse(raw);
902
+ count = Array.isArray(employees) ? employees.length : 0;
903
+ }
904
+ } catch {
905
+ throw new PlanLimitError(
906
+ `Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
907
+ );
908
+ }
909
+ if (count >= license.employeeLimit) {
910
+ throw new PlanLimitError(
911
+ `Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
912
+ );
913
+ }
914
+ }
915
+ var PlanLimitError, CACHE_PATH2;
854
916
  var init_plan_limits = __esm({
855
917
  "src/lib/plan-limits.ts"() {
856
918
  "use strict";
@@ -858,28 +920,490 @@ var init_plan_limits = __esm({
858
920
  init_employees();
859
921
  init_license();
860
922
  init_config();
861
- CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
923
+ PlanLimitError = class extends Error {
924
+ constructor(message) {
925
+ super(message);
926
+ this.name = "PlanLimitError";
927
+ }
928
+ };
929
+ CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
930
+ }
931
+ });
932
+
933
+ // src/lib/session-kill-telemetry.ts
934
+ import crypto from "crypto";
935
+ async function recordSessionKill(input) {
936
+ try {
937
+ const client = getClient();
938
+ await client.execute({
939
+ sql: `INSERT INTO session_kills
940
+ (id, session_name, agent_id, killed_at, reason,
941
+ ticks_idle, estimated_tokens_saved)
942
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
943
+ args: [
944
+ crypto.randomUUID(),
945
+ input.sessionName,
946
+ input.agentId,
947
+ (/* @__PURE__ */ new Date()).toISOString(),
948
+ input.reason,
949
+ input.ticksIdle ?? null,
950
+ input.estimatedTokensSaved ?? null
951
+ ]
952
+ });
953
+ } catch (err) {
954
+ process.stderr.write(
955
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
956
+ `
957
+ );
958
+ }
959
+ }
960
+ var init_session_kill_telemetry = __esm({
961
+ "src/lib/session-kill-telemetry.ts"() {
962
+ "use strict";
963
+ init_database();
964
+ }
965
+ });
966
+
967
+ // src/lib/capacity-monitor.ts
968
+ var capacity_monitor_exports = {};
969
+ __export(capacity_monitor_exports, {
970
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
971
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
972
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
973
+ confirmCapacityKill: () => confirmCapacityKill,
974
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
975
+ extractContextPercent: () => extractContextPercent,
976
+ isAtCapacity: () => isAtCapacity,
977
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
978
+ pollCapacityDead: () => pollCapacityDead
979
+ });
980
+ function resumeTaskTitle(agentId) {
981
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
982
+ }
983
+ function buildResumeContext(agentId, openTasks) {
984
+ const taskList = openTasks.map(
985
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
986
+ ).join("\n");
987
+ return [
988
+ "## Context",
989
+ "",
990
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
991
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
992
+ "",
993
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
994
+ "",
995
+ taskList,
996
+ "",
997
+ "Read each task file and chain through them. Build and commit after each one."
998
+ ].join("\n");
999
+ }
1000
+ function filterPaneContent(paneOutput) {
1001
+ return paneOutput.split("\n").filter((line) => {
1002
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
1003
+ for (const marker of CONTENT_LINE_MARKERS) {
1004
+ if (line.includes(marker)) return false;
1005
+ }
1006
+ for (const re of SOURCE_CODE_MARKERS) {
1007
+ if (re.test(line)) return false;
1008
+ }
1009
+ return true;
1010
+ }).join("\n");
1011
+ }
1012
+ function extractContextPercent(paneOutput) {
1013
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
1014
+ if (!match) return null;
1015
+ const parsed = Number.parseInt(match[2], 10);
1016
+ return Number.isFinite(parsed) ? parsed : null;
1017
+ }
1018
+ function isAtCapacity(paneOutput) {
1019
+ const filtered = filterPaneContent(paneOutput);
1020
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
1021
+ }
1022
+ function confirmCapacityKill(agentId, now = Date.now()) {
1023
+ const pendingSince = _pendingCapacityKill.get(agentId);
1024
+ if (pendingSince === void 0) {
1025
+ _pendingCapacityKill.set(agentId, now);
1026
+ return false;
1027
+ }
1028
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
1029
+ _pendingCapacityKill.set(agentId, now);
1030
+ return false;
1031
+ }
1032
+ _pendingCapacityKill.delete(agentId);
1033
+ return true;
1034
+ }
1035
+ function _resetPendingCapacityKills() {
1036
+ _pendingCapacityKill.clear();
1037
+ }
1038
+ function _resetLastRelaunchCache() {
1039
+ _lastRelaunch.clear();
1040
+ }
1041
+ async function lastResumeCreatedAtMs(agentId) {
1042
+ const client = getClient();
1043
+ const cmScope = sessionScopeFilter(null);
1044
+ const result = await client.execute({
1045
+ sql: `SELECT MAX(created_at) AS last_created_at
1046
+ FROM tasks
1047
+ WHERE assigned_to = ? AND title LIKE ?${cmScope.sql}`,
1048
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`, ...cmScope.args]
1049
+ });
1050
+ const raw = result.rows[0]?.last_created_at;
1051
+ if (raw === null || raw === void 0) return null;
1052
+ const parsed = Date.parse(String(raw));
1053
+ return Number.isNaN(parsed) ? null : parsed;
1054
+ }
1055
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
1056
+ const cached = _lastRelaunch.get(agentId);
1057
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
1058
+ const persisted = await lastResumeCreatedAtMs(agentId);
1059
+ if (persisted === null) return false;
1060
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
1061
+ _lastRelaunch.set(agentId, persisted);
1062
+ return true;
1063
+ }
1064
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
1065
+ const client = getClient();
1066
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1067
+ const context = buildResumeContext(agentId, openTasks);
1068
+ const rdScope = sessionScopeFilter(null);
1069
+ const existing = await client.execute({
1070
+ sql: `SELECT id FROM tasks
1071
+ WHERE assigned_to = ?
1072
+ AND title LIKE ?
1073
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})${rdScope.sql}
1074
+ ORDER BY created_at DESC
1075
+ LIMIT 1`,
1076
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES, ...rdScope.args]
1077
+ });
1078
+ if (existing.rows.length > 0) {
1079
+ const taskId = String(existing.rows[0].id);
1080
+ await client.execute({
1081
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
1082
+ args: [context, now, taskId]
1083
+ });
1084
+ return { created: false, taskId };
1085
+ }
1086
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
1087
+ const task = await createTask2({
1088
+ title: resumeTaskTitle(agentId),
1089
+ assignedTo: agentId,
1090
+ assignedBy: "system",
1091
+ projectName: projectDir.split("/").pop() ?? "unknown",
1092
+ priority: "p0",
1093
+ context,
1094
+ baseDir: projectDir
1095
+ });
1096
+ return { created: true, taskId: task.id };
1097
+ }
1098
+ async function pollCapacityDead() {
1099
+ const transport = getTransport();
1100
+ const relaunched = [];
1101
+ const registered = listSessions().filter(
1102
+ (s) => !isCoordinatorName(s.agentId)
1103
+ );
1104
+ if (registered.length === 0) return [];
1105
+ let liveSessions;
1106
+ try {
1107
+ liveSessions = transport.listSessions();
1108
+ } catch {
1109
+ return [];
1110
+ }
1111
+ for (const entry of registered) {
1112
+ const { windowName, agentId, projectDir } = entry;
1113
+ if (!liveSessions.includes(windowName)) continue;
1114
+ if (await isWithinRelaunchCooldown(agentId)) continue;
1115
+ let pane;
1116
+ try {
1117
+ pane = transport.capturePane(windowName, 15);
1118
+ } catch {
1119
+ continue;
1120
+ }
1121
+ if (!isAtCapacity(pane)) continue;
1122
+ const ctxPct = extractContextPercent(pane);
1123
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
1124
+ process.stderr.write(
1125
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
1126
+ `
1127
+ );
1128
+ continue;
1129
+ }
1130
+ if (!confirmCapacityKill(agentId)) {
1131
+ process.stderr.write(
1132
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
1133
+ `
1134
+ );
1135
+ continue;
1136
+ }
1137
+ const verify = await verifyPaneAtCapacity(windowName);
1138
+ if (!verify.atCapacity) {
1139
+ process.stderr.write(
1140
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
1141
+ `
1142
+ );
1143
+ void recordSessionKill({
1144
+ sessionName: windowName,
1145
+ agentId,
1146
+ reason: "capacity_false_positive_blocked"
1147
+ });
1148
+ continue;
1149
+ }
1150
+ process.stderr.write(
1151
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
1152
+ `
1153
+ );
1154
+ try {
1155
+ transport.kill(windowName);
1156
+ void recordSessionKill({
1157
+ sessionName: windowName,
1158
+ agentId,
1159
+ reason: "capacity"
1160
+ });
1161
+ const client = getClient();
1162
+ const rlScope = sessionScopeFilter(null);
1163
+ const openTasks = await client.execute({
1164
+ sql: `SELECT id, title, priority, task_file, status
1165
+ FROM tasks
1166
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')${rlScope.sql}
1167
+ ORDER BY
1168
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
1169
+ created_at ASC
1170
+ LIMIT 10`,
1171
+ args: [agentId, ...rlScope.args]
1172
+ });
1173
+ if (openTasks.rows.length === 0) {
1174
+ process.stderr.write(
1175
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
1176
+ `
1177
+ );
1178
+ continue;
1179
+ }
1180
+ const { created } = await createOrRefreshResumeTask(
1181
+ agentId,
1182
+ projectDir,
1183
+ openTasks.rows
1184
+ );
1185
+ if (created) {
1186
+ await writeNotification({
1187
+ agentId: "system",
1188
+ agentRole: "daemon",
1189
+ event: "capacity_relaunch",
1190
+ project: projectDir.split("/").pop() ?? "unknown",
1191
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
1192
+ });
1193
+ }
1194
+ _lastRelaunch.set(agentId, Date.now());
1195
+ if (created) relaunched.push(agentId);
1196
+ } catch (err) {
1197
+ process.stderr.write(
1198
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
1199
+ `
1200
+ );
1201
+ }
1202
+ }
1203
+ return relaunched;
1204
+ }
1205
+ 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;
1206
+ var init_capacity_monitor = __esm({
1207
+ "src/lib/capacity-monitor.ts"() {
1208
+ "use strict";
1209
+ init_session_registry();
1210
+ init_transport();
1211
+ init_notifications();
1212
+ init_database();
1213
+ init_session_kill_telemetry();
1214
+ init_tmux_routing();
1215
+ init_task_scope();
1216
+ init_employees();
1217
+ CAPACITY_PATTERNS = [
1218
+ /conversation is too long/i,
1219
+ /maximum context length/i,
1220
+ /context window.*(?:limit|exceed|full)/i,
1221
+ /reached.*(?:token|context).*limit/i
1222
+ ];
1223
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
1224
+ CONTENT_LINE_MARKERS = [
1225
+ "RESUME:",
1226
+ "intercom",
1227
+ "capacity-monitor",
1228
+ "CAPACITY_PATTERNS",
1229
+ "isAtCapacity",
1230
+ "CONTENT_LINE_MARKERS",
1231
+ "pollCapacityDead",
1232
+ "confirmCapacityKill",
1233
+ "session_kills",
1234
+ "capacity-monitor.test"
1235
+ ];
1236
+ SOURCE_CODE_MARKERS = [
1237
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
1238
+ /(?:maximum context length|conversation is too long).*["'`/]/i
1239
+ ];
1240
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
1241
+ _lastRelaunch = /* @__PURE__ */ new Map();
1242
+ RESUME_TITLE_PREFIX = "RESUME:";
1243
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
1244
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
1245
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
1246
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
1247
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
1248
+ CTX_FLOOR_PERCENT = 50;
862
1249
  }
863
1250
  });
864
1251
 
865
1252
  // src/lib/tmux-routing.ts
1253
+ var tmux_routing_exports = {};
1254
+ __export(tmux_routing_exports, {
1255
+ acquireSpawnLock: () => acquireSpawnLock,
1256
+ employeeSessionName: () => employeeSessionName,
1257
+ ensureEmployee: () => ensureEmployee,
1258
+ extractRootExe: () => extractRootExe,
1259
+ findFreeInstance: () => findFreeInstance,
1260
+ getDispatchedBy: () => getDispatchedBy,
1261
+ getMySession: () => getMySession,
1262
+ getParentExe: () => getParentExe,
1263
+ getSessionState: () => getSessionState,
1264
+ isEmployeeAlive: () => isEmployeeAlive,
1265
+ isExeSession: () => isExeSession,
1266
+ isSessionBusy: () => isSessionBusy,
1267
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
1268
+ notifyParentExe: () => notifyParentExe,
1269
+ parseParentExe: () => parseParentExe,
1270
+ registerParentExe: () => registerParentExe,
1271
+ releaseSpawnLock: () => releaseSpawnLock,
1272
+ resolveExeSession: () => resolveExeSession,
1273
+ sendIntercom: () => sendIntercom,
1274
+ spawnEmployee: () => spawnEmployee,
1275
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
1276
+ });
866
1277
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
867
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync8, appendFileSync, readdirSync as readdirSync2 } from "fs";
868
- import path10 from "path";
1278
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync } from "fs";
1279
+ import path9 from "path";
869
1280
  import os7 from "os";
870
1281
  import { fileURLToPath } from "url";
1282
+ import { unlinkSync as unlinkSync2 } from "fs";
1283
+ function spawnLockPath(sessionName) {
1284
+ return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1285
+ }
1286
+ function isProcessAlive(pid) {
1287
+ try {
1288
+ process.kill(pid, 0);
1289
+ return true;
1290
+ } catch {
1291
+ return false;
1292
+ }
1293
+ }
1294
+ function acquireSpawnLock(sessionName) {
1295
+ if (!existsSync9(SPAWN_LOCK_DIR)) {
1296
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
1297
+ }
1298
+ const lockFile = spawnLockPath(sessionName);
1299
+ if (existsSync9(lockFile)) {
1300
+ try {
1301
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1302
+ const age = Date.now() - lock.timestamp;
1303
+ if (isProcessAlive(lock.pid) && age < 6e4) {
1304
+ return false;
1305
+ }
1306
+ } catch {
1307
+ }
1308
+ }
1309
+ writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1310
+ return true;
1311
+ }
1312
+ function releaseSpawnLock(sessionName) {
1313
+ try {
1314
+ unlinkSync2(spawnLockPath(sessionName));
1315
+ } catch {
1316
+ }
1317
+ }
1318
+ function resolveBehaviorsExporterScript() {
1319
+ try {
1320
+ const thisFile = fileURLToPath(import.meta.url);
1321
+ const scriptPath = path9.join(
1322
+ path9.dirname(thisFile),
1323
+ "..",
1324
+ "bin",
1325
+ "exe-export-behaviors.js"
1326
+ );
1327
+ return existsSync9(scriptPath) ? scriptPath : null;
1328
+ } catch {
1329
+ return null;
1330
+ }
1331
+ }
1332
+ function exportBehaviorsSync(agentId, projectName, sessionKey) {
1333
+ const script = resolveBehaviorsExporterScript();
1334
+ if (!script) return null;
1335
+ try {
1336
+ const output = execFileSync2(
1337
+ process.execPath,
1338
+ [script, agentId, projectName, sessionKey],
1339
+ { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
1340
+ ).trim();
1341
+ return output.length > 0 ? output : null;
1342
+ } catch (err) {
1343
+ process.stderr.write(
1344
+ `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
1345
+ `
1346
+ );
1347
+ return null;
1348
+ }
1349
+ }
871
1350
  function getMySession() {
872
1351
  return getTransport().getMySession();
873
1352
  }
1353
+ function isRootSession(name) {
1354
+ return name.length > 0 && !name.includes("-");
1355
+ }
1356
+ function employeeSessionName(employee, exeSession, instance) {
1357
+ if (!isRootSession(exeSession)) {
1358
+ const root = extractRootExe(exeSession);
1359
+ if (root) {
1360
+ process.stderr.write(
1361
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root session, using "${root}" instead
1362
+ `
1363
+ );
1364
+ exeSession = root;
1365
+ } else {
1366
+ throw new Error(
1367
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
1368
+ );
1369
+ }
1370
+ }
1371
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
1372
+ const name = `${employee}${suffix}-${exeSession}`;
1373
+ if (!VALID_SESSION_NAME.test(name)) {
1374
+ throw new Error(
1375
+ `Invalid session name "${name}" \u2014 must match {agent}-{rootSession} or {agent}{instance}-{rootSession}`
1376
+ );
1377
+ }
1378
+ return name;
1379
+ }
1380
+ function parseParentExe(sessionName, agentId) {
1381
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1382
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
1383
+ const match = sessionName.match(regex);
1384
+ return match?.[1] ?? null;
1385
+ }
874
1386
  function extractRootExe(name) {
875
1387
  if (!name) return null;
876
1388
  if (!name.includes("-")) return name;
877
1389
  const parts = name.split("-").filter(Boolean);
878
1390
  return parts.length > 0 ? parts[parts.length - 1] : null;
879
1391
  }
1392
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1393
+ if (!existsSync9(SESSION_CACHE)) {
1394
+ mkdirSync5(SESSION_CACHE, { recursive: true });
1395
+ }
1396
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
1397
+ const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1398
+ writeFileSync6(filePath, JSON.stringify({
1399
+ parentExe: rootExe,
1400
+ dispatchedBy: dispatchedBy || rootExe,
1401
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1402
+ }));
1403
+ }
880
1404
  function getParentExe(sessionKey) {
881
1405
  try {
882
- const data = JSON.parse(readFileSync8(path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1406
+ const data = JSON.parse(readFileSync8(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
883
1407
  return data.parentExe || null;
884
1408
  } catch {
885
1409
  return null;
@@ -888,7 +1412,7 @@ function getParentExe(sessionKey) {
888
1412
  function getDispatchedBy(sessionKey) {
889
1413
  try {
890
1414
  const data = JSON.parse(readFileSync8(
891
- path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1415
+ path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
892
1416
  "utf8"
893
1417
  ));
894
1418
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -918,9 +1442,47 @@ function resolveExeSession() {
918
1442
  }
919
1443
  return fromSessionName ?? mySession;
920
1444
  }
1445
+ function isEmployeeAlive(sessionName) {
1446
+ return getTransport().isAlive(sessionName);
1447
+ }
1448
+ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
1449
+ const base = employeeSessionName(employeeName, exeSession);
1450
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
1451
+ for (let i = 2; i <= maxInstances; i++) {
1452
+ const candidate = employeeSessionName(employeeName, exeSession, i);
1453
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
1454
+ }
1455
+ return null;
1456
+ }
1457
+ async function verifyPaneAtCapacity(sessionName) {
1458
+ const transport = getTransport();
1459
+ if (!transport.isAlive(sessionName)) {
1460
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
1461
+ }
1462
+ let pane;
1463
+ try {
1464
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
1465
+ } catch (err) {
1466
+ return {
1467
+ atCapacity: false,
1468
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
1469
+ };
1470
+ }
1471
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
1472
+ if (!isAtCapacity2(pane)) {
1473
+ return {
1474
+ atCapacity: false,
1475
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
1476
+ };
1477
+ }
1478
+ return {
1479
+ atCapacity: true,
1480
+ reason: "capacity banner matched in recent pane output"
1481
+ };
1482
+ }
921
1483
  function readDebounceState() {
922
1484
  try {
923
- if (!existsSync8(DEBOUNCE_FILE)) return {};
1485
+ if (!existsSync9(DEBOUNCE_FILE)) return {};
924
1486
  const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
925
1487
  const state = {};
926
1488
  for (const [key, val] of Object.entries(raw)) {
@@ -937,8 +1499,8 @@ function readDebounceState() {
937
1499
  }
938
1500
  function writeDebounceState(state) {
939
1501
  try {
940
- if (!existsSync8(SESSION_CACHE)) mkdirSync4(SESSION_CACHE, { recursive: true });
941
- writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
1502
+ if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
1503
+ writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
942
1504
  } catch {
943
1505
  }
944
1506
  }
@@ -995,6 +1557,10 @@ function getSessionState(sessionName) {
995
1557
  return "offline";
996
1558
  }
997
1559
  }
1560
+ function isSessionBusy(sessionName) {
1561
+ const state = getSessionState(sessionName);
1562
+ return state === "thinking" || state === "tool";
1563
+ }
998
1564
  function isExeSession(sessionName) {
999
1565
  const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
1000
1566
  const coordinatorName = getCoordinatorName();
@@ -1032,8 +1598,8 @@ function sendIntercom(targetSession) {
1032
1598
  try {
1033
1599
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
1034
1600
  const agent = baseAgentName(rawAgent);
1035
- const markerPath = path10.join(SESSION_CACHE, `current-task-${agent}.json`);
1036
- if (existsSync8(markerPath)) {
1601
+ const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
1602
+ if (existsSync9(markerPath)) {
1037
1603
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
1038
1604
  return "debounced";
1039
1605
  }
@@ -1042,9 +1608,9 @@ function sendIntercom(targetSession) {
1042
1608
  try {
1043
1609
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
1044
1610
  const agent = baseAgentName(rawAgent);
1045
- const taskDir = path10.join(process.cwd(), "exe", agent);
1046
- if (existsSync8(taskDir)) {
1047
- const files = readdirSync2(taskDir).filter(
1611
+ const taskDir = path9.join(process.cwd(), "exe", agent);
1612
+ if (existsSync9(taskDir)) {
1613
+ const files = readdirSync(taskDir).filter(
1048
1614
  (f) => f.endsWith(".md") && f !== "DONE.txt"
1049
1615
  );
1050
1616
  if (files.length === 0) {
@@ -1103,7 +1669,353 @@ function notifyParentExe(sessionKey) {
1103
1669
  }
1104
1670
  return true;
1105
1671
  }
1106
- var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1672
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
1673
+ const transport = getTransport();
1674
+ try {
1675
+ const sessions = transport.listSessions();
1676
+ if (!sessions.includes(coordinatorSession)) return false;
1677
+ execSync4(
1678
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
1679
+ { timeout: 3e3 }
1680
+ );
1681
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
1682
+ return true;
1683
+ } catch {
1684
+ return false;
1685
+ }
1686
+ }
1687
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1688
+ if (isCoordinatorName(employeeName)) {
1689
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
1690
+ }
1691
+ try {
1692
+ assertEmployeeLimitSync();
1693
+ } catch (err) {
1694
+ if (err instanceof PlanLimitError) {
1695
+ return { status: "failed", sessionName: "", error: err.message };
1696
+ }
1697
+ }
1698
+ if (employeeName.includes("-")) {
1699
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
1700
+ return {
1701
+ status: "failed",
1702
+ sessionName: "",
1703
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
1704
+ };
1705
+ }
1706
+ if (!isRootSession(exeSession)) {
1707
+ const root = extractRootExe(exeSession);
1708
+ if (root) {
1709
+ process.stderr.write(
1710
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root session). Auto-correcting to "${root}".
1711
+ `
1712
+ );
1713
+ exeSession = root;
1714
+ } else {
1715
+ return {
1716
+ status: "failed",
1717
+ sessionName: "",
1718
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
1719
+ };
1720
+ }
1721
+ }
1722
+ let effectiveInstance = opts?.instance;
1723
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
1724
+ const free = findFreeInstance(
1725
+ employeeName,
1726
+ exeSession,
1727
+ opts.maxAutoInstances ?? 10
1728
+ );
1729
+ if (free === null) {
1730
+ return {
1731
+ status: "failed",
1732
+ sessionName: employeeSessionName(employeeName, exeSession),
1733
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
1734
+ };
1735
+ }
1736
+ effectiveInstance = free === 0 ? void 0 : free;
1737
+ }
1738
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
1739
+ if (isEmployeeAlive(sessionName)) {
1740
+ const result2 = sendIntercom(sessionName);
1741
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
1742
+ return { status: "intercom_sent", sessionName };
1743
+ }
1744
+ if (result2 === "delivered") {
1745
+ return { status: "intercom_unprocessed", sessionName };
1746
+ }
1747
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
1748
+ }
1749
+ const spawnOpts = { ...opts, instance: effectiveInstance };
1750
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
1751
+ if (result.error) {
1752
+ return { status: "failed", sessionName, error: result.error };
1753
+ }
1754
+ return { status: "spawned", sessionName };
1755
+ }
1756
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1757
+ const transport = getTransport();
1758
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1759
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1760
+ const logDir = path9.join(os7.homedir(), ".exe-os", "session-logs");
1761
+ const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1762
+ if (!existsSync9(logDir)) {
1763
+ mkdirSync5(logDir, { recursive: true });
1764
+ }
1765
+ transport.kill(sessionName);
1766
+ let cleanupSuffix = "";
1767
+ try {
1768
+ const thisFile = fileURLToPath(import.meta.url);
1769
+ const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1770
+ if (existsSync9(cleanupScript)) {
1771
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
1772
+ }
1773
+ } catch {
1774
+ }
1775
+ try {
1776
+ const claudeJsonPath = path9.join(os7.homedir(), ".claude.json");
1777
+ let claudeJson = {};
1778
+ try {
1779
+ claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
1780
+ } catch {
1781
+ }
1782
+ if (!claudeJson.projects) claudeJson.projects = {};
1783
+ const projects = claudeJson.projects;
1784
+ const trustDir = opts?.cwd ?? projectDir;
1785
+ if (!projects[trustDir]) projects[trustDir] = {};
1786
+ projects[trustDir].hasTrustDialogAccepted = true;
1787
+ writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1788
+ } catch {
1789
+ }
1790
+ try {
1791
+ const settingsDir = path9.join(os7.homedir(), ".claude", "projects");
1792
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1793
+ const projSettingsDir = path9.join(settingsDir, normalizedKey);
1794
+ const settingsPath = path9.join(projSettingsDir, "settings.json");
1795
+ let settings = {};
1796
+ try {
1797
+ settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
1798
+ } catch {
1799
+ }
1800
+ const perms = settings.permissions ?? {};
1801
+ const allow = perms.allow ?? [];
1802
+ const toolNames = [
1803
+ "recall_my_memory",
1804
+ "store_memory",
1805
+ "create_task",
1806
+ "update_task",
1807
+ "list_tasks",
1808
+ "get_task",
1809
+ "ask_team_memory",
1810
+ "store_behavior",
1811
+ "get_identity",
1812
+ "send_message"
1813
+ ];
1814
+ const requiredTools = expandDualPrefixTools(toolNames);
1815
+ let changed = false;
1816
+ for (const tool of requiredTools) {
1817
+ if (!allow.includes(tool)) {
1818
+ allow.push(tool);
1819
+ changed = true;
1820
+ }
1821
+ }
1822
+ if (changed) {
1823
+ perms.allow = allow;
1824
+ settings.permissions = perms;
1825
+ mkdirSync5(projSettingsDir, { recursive: true });
1826
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1827
+ }
1828
+ } catch {
1829
+ }
1830
+ const spawnCwd = opts?.cwd ?? projectDir;
1831
+ const useExeAgent = !!(opts?.model && opts?.provider);
1832
+ const agentRtConfig = getAgentRuntime(employeeName);
1833
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
1834
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
1835
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
1836
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
1837
+ let identityFlag = "";
1838
+ let behaviorsFlag = "";
1839
+ let legacyFallbackWarned = false;
1840
+ if (!useExeAgent && !useBinSymlink) {
1841
+ const identityPath = path9.join(
1842
+ os7.homedir(),
1843
+ ".exe-os",
1844
+ "identity",
1845
+ `${employeeName}.md`
1846
+ );
1847
+ _resetCcAgentSupportCache();
1848
+ const hasAgentFlag = claudeSupportsAgentFlag();
1849
+ if (hasAgentFlag) {
1850
+ identityFlag = ` --agent ${employeeName}`;
1851
+ } else if (existsSync9(identityPath)) {
1852
+ identityFlag = ` --append-system-prompt-file ${identityPath}`;
1853
+ legacyFallbackWarned = true;
1854
+ }
1855
+ const behaviorsFile = exportBehaviorsSync(
1856
+ employeeName,
1857
+ path9.basename(spawnCwd),
1858
+ sessionName
1859
+ );
1860
+ if (behaviorsFile) {
1861
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
1862
+ }
1863
+ }
1864
+ if (legacyFallbackWarned) {
1865
+ process.stderr.write(
1866
+ `[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.
1867
+ `
1868
+ );
1869
+ }
1870
+ let sessionContextFlag = "";
1871
+ try {
1872
+ const ctxDir = path9.join(os7.homedir(), ".exe-os", "session-cache");
1873
+ mkdirSync5(ctxDir, { recursive: true });
1874
+ const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
1875
+ const ctxContent = [
1876
+ `## Session Context`,
1877
+ `You are running in tmux session: ${sessionName}.`,
1878
+ `Your parent coordinator session is ${exeSession}.`,
1879
+ `Your employees (if any) use the -${exeSession} suffix.`
1880
+ ].join("\n");
1881
+ writeFileSync6(ctxFile, ctxContent);
1882
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
1883
+ } catch {
1884
+ }
1885
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
1886
+ if (ccProvider !== DEFAULT_PROVIDER) {
1887
+ const cfg = PROVIDER_TABLE[ccProvider];
1888
+ if (cfg?.apiKeyEnv) {
1889
+ const keyVal = process.env[cfg.apiKeyEnv];
1890
+ if (keyVal) {
1891
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
1892
+ }
1893
+ }
1894
+ }
1895
+ if (useCodex) {
1896
+ const codexCfg = RUNTIME_TABLE.codex;
1897
+ if (codexCfg?.apiKeyEnv) {
1898
+ const keyVal = process.env[codexCfg.apiKeyEnv];
1899
+ if (keyVal) {
1900
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
1901
+ }
1902
+ }
1903
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
1904
+ }
1905
+ if (useOpencode) {
1906
+ const ocCfg = PROVIDER_TABLE.opencode;
1907
+ if (ocCfg?.apiKeyEnv) {
1908
+ const keyVal = process.env[ocCfg.apiKeyEnv];
1909
+ if (keyVal) {
1910
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
1911
+ }
1912
+ }
1913
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
1914
+ }
1915
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
1916
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
1917
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
1918
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
1919
+ }
1920
+ }
1921
+ let spawnCommand;
1922
+ if (useExeAgent) {
1923
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
1924
+ } else if (useCodex) {
1925
+ process.stderr.write(
1926
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
1927
+ `
1928
+ );
1929
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName} --session ${sessionName}${cleanupSuffix}`;
1930
+ } else if (useOpencode) {
1931
+ const binName = `${employeeName}-opencode`;
1932
+ process.stderr.write(
1933
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
1934
+ `
1935
+ );
1936
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
1937
+ } else if (useBinSymlink) {
1938
+ const binName = `${employeeName}-${ccProvider}`;
1939
+ process.stderr.write(
1940
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
1941
+ `
1942
+ );
1943
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
1944
+ } else {
1945
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
1946
+ }
1947
+ const spawnResult = transport.spawn(sessionName, {
1948
+ cwd: spawnCwd,
1949
+ command: spawnCommand
1950
+ });
1951
+ if (spawnResult.error) {
1952
+ releaseSpawnLock(sessionName);
1953
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
1954
+ }
1955
+ transport.pipeLog(sessionName, logFile);
1956
+ try {
1957
+ const mySession = getMySession();
1958
+ const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1959
+ writeFileSync6(dispatchInfo, JSON.stringify({
1960
+ dispatchedBy: mySession,
1961
+ rootExe: exeSession,
1962
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
1963
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
1964
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
1965
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1966
+ }));
1967
+ } catch {
1968
+ }
1969
+ let booted = false;
1970
+ for (let i = 0; i < 30; i++) {
1971
+ try {
1972
+ execSync4("sleep 0.5");
1973
+ } catch {
1974
+ }
1975
+ try {
1976
+ const pane = transport.capturePane(sessionName);
1977
+ if (useExeAgent) {
1978
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
1979
+ booted = true;
1980
+ break;
1981
+ }
1982
+ } else if (useCodex) {
1983
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
1984
+ booted = true;
1985
+ break;
1986
+ }
1987
+ } else {
1988
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
1989
+ booted = true;
1990
+ break;
1991
+ }
1992
+ }
1993
+ } catch {
1994
+ }
1995
+ }
1996
+ if (!booted) {
1997
+ releaseSpawnLock(sessionName);
1998
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
1999
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
2000
+ }
2001
+ if (!useExeAgent && !useCodex) {
2002
+ try {
2003
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
2004
+ } catch {
2005
+ }
2006
+ }
2007
+ registerSession({
2008
+ windowName: sessionName,
2009
+ agentId: employeeName,
2010
+ projectDir: spawnCwd,
2011
+ parentExe: exeSession,
2012
+ pid: 0,
2013
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
2014
+ });
2015
+ releaseSpawnLock(sessionName);
2016
+ return { sessionName };
2017
+ }
2018
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1107
2019
  var init_tmux_routing = __esm({
1108
2020
  "src/lib/tmux-routing.ts"() {
1109
2021
  "use strict";
@@ -1118,12 +2030,15 @@ var init_tmux_routing = __esm({
1118
2030
  init_intercom_queue();
1119
2031
  init_plan_limits();
1120
2032
  init_employees();
1121
- SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
1122
- SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
2033
+ SPAWN_LOCK_DIR = path9.join(os7.homedir(), ".exe-os", "spawn-locks");
2034
+ SESSION_CACHE = path9.join(os7.homedir(), ".exe-os", "session-cache");
2035
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2036
+ VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
2037
+ VERIFY_PANE_LINES = 200;
1123
2038
  INTERCOM_DEBOUNCE_MS = 3e4;
1124
2039
  CODEX_DEBOUNCE_MS = 12e4;
1125
- INTERCOM_LOG2 = path10.join(os7.homedir(), ".exe-os", "intercom.log");
1126
- DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
2040
+ INTERCOM_LOG2 = path9.join(os7.homedir(), ".exe-os", "intercom.log");
2041
+ DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
1127
2042
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
1128
2043
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
1129
2044
  }
@@ -1146,20 +2061,141 @@ function sessionScopeFilter(sessionScope, tableAlias) {
1146
2061
  args: [scope]
1147
2062
  };
1148
2063
  }
2064
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
2065
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
2066
+ if (!scope) return { sql: "", args: [] };
2067
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
2068
+ return {
2069
+ sql: ` AND ${col} = ?`,
2070
+ args: [scope]
2071
+ };
2072
+ }
1149
2073
  var init_task_scope = __esm({
1150
2074
  "src/lib/task-scope.ts"() {
1151
2075
  "use strict";
1152
- init_tmux_routing();
2076
+ init_tmux_routing();
2077
+ }
2078
+ });
2079
+
2080
+ // src/lib/notifications.ts
2081
+ import crypto2 from "crypto";
2082
+ import path10 from "path";
2083
+ import os8 from "os";
2084
+ import {
2085
+ readFileSync as readFileSync9,
2086
+ readdirSync as readdirSync2,
2087
+ unlinkSync as unlinkSync3,
2088
+ existsSync as existsSync10,
2089
+ rmdirSync
2090
+ } from "fs";
2091
+ async function writeNotification(notification) {
2092
+ try {
2093
+ const client = getClient();
2094
+ const id = crypto2.randomUUID();
2095
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2096
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
2097
+ await client.execute({
2098
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
2099
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2100
+ args: [
2101
+ id,
2102
+ notification.agentId,
2103
+ notification.agentRole,
2104
+ notification.event,
2105
+ notification.project,
2106
+ notification.summary,
2107
+ notification.taskFile ?? null,
2108
+ sessionScope,
2109
+ now
2110
+ ]
2111
+ });
2112
+ } catch (err) {
2113
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2114
+ `);
2115
+ }
2116
+ }
2117
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
2118
+ try {
2119
+ const client = getClient();
2120
+ const scope = strictSessionScopeFilter(sessionScope);
2121
+ await client.execute({
2122
+ sql: `UPDATE notifications SET read = 1
2123
+ WHERE task_file = ? AND read = 0${scope.sql}`,
2124
+ args: [taskFile, ...scope.args]
2125
+ });
2126
+ } catch {
2127
+ }
2128
+ }
2129
+ var init_notifications = __esm({
2130
+ "src/lib/notifications.ts"() {
2131
+ "use strict";
2132
+ init_database();
2133
+ init_task_scope();
2134
+ }
2135
+ });
2136
+
2137
+ // src/lib/state-bus.ts
2138
+ var StateBus, orgBus;
2139
+ var init_state_bus = __esm({
2140
+ "src/lib/state-bus.ts"() {
2141
+ "use strict";
2142
+ StateBus = class {
2143
+ handlers = /* @__PURE__ */ new Map();
2144
+ globalHandlers = /* @__PURE__ */ new Set();
2145
+ /** Emit an event to all subscribers */
2146
+ emit(event) {
2147
+ const typeHandlers = this.handlers.get(event.type);
2148
+ if (typeHandlers) {
2149
+ for (const handler of typeHandlers) {
2150
+ try {
2151
+ handler(event);
2152
+ } catch {
2153
+ }
2154
+ }
2155
+ }
2156
+ for (const handler of this.globalHandlers) {
2157
+ try {
2158
+ handler(event);
2159
+ } catch {
2160
+ }
2161
+ }
2162
+ }
2163
+ /** Subscribe to a specific event type */
2164
+ on(type, handler) {
2165
+ if (!this.handlers.has(type)) {
2166
+ this.handlers.set(type, /* @__PURE__ */ new Set());
2167
+ }
2168
+ this.handlers.get(type).add(handler);
2169
+ }
2170
+ /** Subscribe to ALL events */
2171
+ onAny(handler) {
2172
+ this.globalHandlers.add(handler);
2173
+ }
2174
+ /** Unsubscribe from a specific event type */
2175
+ off(type, handler) {
2176
+ this.handlers.get(type)?.delete(handler);
2177
+ }
2178
+ /** Unsubscribe from ALL events */
2179
+ offAny(handler) {
2180
+ this.globalHandlers.delete(handler);
2181
+ }
2182
+ /** Remove all listeners */
2183
+ clear() {
2184
+ this.handlers.clear();
2185
+ this.globalHandlers.clear();
2186
+ }
2187
+ };
2188
+ orgBus = new StateBus();
1153
2189
  }
1154
2190
  });
1155
2191
 
1156
2192
  // src/lib/tasks-crud.ts
1157
- import crypto2 from "crypto";
2193
+ import crypto3 from "crypto";
1158
2194
  import path11 from "path";
1159
- import os8 from "os";
2195
+ import os9 from "os";
1160
2196
  import { execSync as execSync5 } from "child_process";
1161
2197
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1162
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2198
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1163
2199
  async function writeCheckpoint(input) {
1164
2200
  const client = getClient();
1165
2201
  const row = await resolveTask(client, input.taskId);
@@ -1190,6 +2226,16 @@ async function writeCheckpoint(input) {
1190
2226
  const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
1191
2227
  return { checkpointCount };
1192
2228
  }
2229
+ function extractParentFromContext(contextBody) {
2230
+ if (!contextBody) return null;
2231
+ const match = contextBody.match(
2232
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
2233
+ );
2234
+ return match ? match[1].toLowerCase() : null;
2235
+ }
2236
+ function slugify(title) {
2237
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2238
+ }
1193
2239
  function buildKeywordIndex() {
1194
2240
  const idx = /* @__PURE__ */ new Map();
1195
2241
  for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
@@ -1201,6 +2247,24 @@ function buildKeywordIndex() {
1201
2247
  }
1202
2248
  return idx;
1203
2249
  }
2250
+ function checkLaneAffinity(title, context, assigneeName) {
2251
+ const employees = loadEmployeesSync();
2252
+ const employee = employees.find((e) => e.name === assigneeName);
2253
+ if (!employee) return void 0;
2254
+ const assigneeRole = employee.role;
2255
+ const text = `${title} ${context}`.toLowerCase();
2256
+ const matchedRoles = /* @__PURE__ */ new Set();
2257
+ for (const [keyword, roles] of KEYWORD_INDEX) {
2258
+ if (text.includes(keyword)) {
2259
+ for (const role of roles) matchedRoles.add(role);
2260
+ }
2261
+ }
2262
+ if (matchedRoles.size === 0) return void 0;
2263
+ if (matchedRoles.has(assigneeRole)) return void 0;
2264
+ if (assigneeRole === "COO") return void 0;
2265
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
2266
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
2267
+ }
1204
2268
  async function resolveTask(client, identifier, scopeSession) {
1205
2269
  const scope = sessionScopeFilter(scopeSession);
1206
2270
  let result = await client.execute({
@@ -1245,6 +2309,207 @@ async function resolveTask(client, identifier, scopeSession) {
1245
2309
  }
1246
2310
  throw new Error(`Task not found: ${identifier}`);
1247
2311
  }
2312
+ async function createTaskCore(input) {
2313
+ const client = getClient();
2314
+ const id = crypto3.randomUUID();
2315
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2316
+ const slug = slugify(input.title);
2317
+ let earlySessionScope = null;
2318
+ try {
2319
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2320
+ earlySessionScope = resolveExeSession2();
2321
+ } catch {
2322
+ }
2323
+ const scope = earlySessionScope ?? "default";
2324
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
2325
+ let blockedById = null;
2326
+ const initialStatus = input.blockedBy ? "blocked" : "open";
2327
+ if (input.blockedBy) {
2328
+ const blocker = await resolveTask(client, input.blockedBy);
2329
+ blockedById = String(blocker.id);
2330
+ }
2331
+ let parentTaskId = null;
2332
+ let parentRef = input.parentTaskId;
2333
+ if (!parentRef) {
2334
+ const extracted = extractParentFromContext(input.context);
2335
+ if (extracted) {
2336
+ parentRef = extracted;
2337
+ process.stderr.write(
2338
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
2339
+ );
2340
+ }
2341
+ }
2342
+ if (parentRef) {
2343
+ try {
2344
+ const parent = await resolveTask(client, parentRef);
2345
+ parentTaskId = String(parent.id);
2346
+ } catch (err) {
2347
+ if (!input.parentTaskId) {
2348
+ throw new Error(
2349
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
2350
+ );
2351
+ }
2352
+ throw err;
2353
+ }
2354
+ }
2355
+ let warning;
2356
+ const dupScope = sessionScopeFilter();
2357
+ const dupCheck = await client.execute({
2358
+ sql: `SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')${dupScope.sql}`,
2359
+ args: [input.title, input.assignedTo, ...dupScope.args]
2360
+ });
2361
+ if (dupCheck.rows.length > 0) {
2362
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
2363
+ }
2364
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2365
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
2366
+ if (laneWarning) {
2367
+ warning = warning ? `${warning}
2368
+ ${laneWarning}` : laneWarning;
2369
+ }
2370
+ }
2371
+ if (input.baseDir) {
2372
+ try {
2373
+ await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
2374
+ await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
2375
+ await ensureArchitectureDoc(input.baseDir, input.projectName);
2376
+ await ensureGitignoreExe(input.baseDir);
2377
+ } catch {
2378
+ }
2379
+ }
2380
+ const complexity = input.complexity ?? "standard";
2381
+ const sessionScope = earlySessionScope;
2382
+ await client.execute({
2383
+ 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)
2384
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2385
+ args: [
2386
+ id,
2387
+ input.title,
2388
+ input.assignedTo,
2389
+ input.assignedBy,
2390
+ input.projectName,
2391
+ input.priority,
2392
+ initialStatus,
2393
+ taskFile,
2394
+ blockedById,
2395
+ parentTaskId,
2396
+ input.reviewer ?? null,
2397
+ input.context,
2398
+ complexity,
2399
+ input.budgetTokens ?? null,
2400
+ input.budgetFallbackModel ?? null,
2401
+ 0,
2402
+ null,
2403
+ sessionScope,
2404
+ now,
2405
+ now
2406
+ ]
2407
+ });
2408
+ if (input.baseDir) {
2409
+ try {
2410
+ const EXE_OS_DIR = path11.join(os9.homedir(), ".exe-os");
2411
+ const mdPath = path11.join(EXE_OS_DIR, taskFile);
2412
+ const mdDir = path11.dirname(mdPath);
2413
+ if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
2414
+ const reviewer = input.reviewer ?? input.assignedBy;
2415
+ const mdContent = `# ${input.title}
2416
+
2417
+ ## MANDATORY: When done
2418
+
2419
+ You MUST call update_task with status "done" and a result summary when finished.
2420
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2421
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2422
+
2423
+ **ID:** ${id}
2424
+ **Status:** ${initialStatus}
2425
+ **Priority:** ${input.priority}
2426
+ **Assigned by:** ${input.assignedBy}
2427
+ **Assigned to:** ${input.assignedTo}
2428
+ **Project:** ${input.projectName}
2429
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2430
+ **Parent task:** ${parentTaskId}` : ""}
2431
+ **Reviewer:** ${reviewer}
2432
+
2433
+ ## Context
2434
+
2435
+ ${input.context}
2436
+ `;
2437
+ await writeFile3(mdPath, mdContent, "utf-8");
2438
+ } catch (err) {
2439
+ process.stderr.write(
2440
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
2441
+ `
2442
+ );
2443
+ }
2444
+ }
2445
+ return {
2446
+ id,
2447
+ title: input.title,
2448
+ assignedTo: input.assignedTo,
2449
+ assignedBy: input.assignedBy,
2450
+ projectName: input.projectName,
2451
+ priority: input.priority,
2452
+ status: initialStatus,
2453
+ taskFile,
2454
+ createdAt: now,
2455
+ updatedAt: now,
2456
+ warning,
2457
+ budgetTokens: input.budgetTokens ?? null,
2458
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
2459
+ tokensUsed: 0,
2460
+ tokensWarnedAt: null
2461
+ };
2462
+ }
2463
+ async function listTasks(input) {
2464
+ const client = getClient();
2465
+ const conditions = [];
2466
+ const args = [];
2467
+ if (input.assignedTo) {
2468
+ conditions.push("assigned_to = ?");
2469
+ args.push(input.assignedTo);
2470
+ }
2471
+ if (input.status) {
2472
+ conditions.push("status = ?");
2473
+ args.push(input.status);
2474
+ } else {
2475
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
2476
+ }
2477
+ if (input.projectName) {
2478
+ conditions.push("project_name = ?");
2479
+ args.push(input.projectName);
2480
+ }
2481
+ if (input.priority) {
2482
+ conditions.push("priority = ?");
2483
+ args.push(input.priority);
2484
+ }
2485
+ const scope = sessionScopeFilter();
2486
+ if (scope.sql) {
2487
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
2488
+ args.push(...scope.args);
2489
+ }
2490
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2491
+ const result = await client.execute({
2492
+ 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`,
2493
+ args
2494
+ });
2495
+ return result.rows.map((r) => ({
2496
+ id: String(r.id),
2497
+ title: String(r.title),
2498
+ assignedTo: String(r.assigned_to),
2499
+ assignedBy: String(r.assigned_by),
2500
+ projectName: String(r.project_name),
2501
+ priority: String(r.priority),
2502
+ status: String(r.status),
2503
+ taskFile: String(r.task_file),
2504
+ createdAt: String(r.created_at),
2505
+ updatedAt: String(r.updated_at),
2506
+ checkpointCount: Number(r.checkpoint_count ?? 0),
2507
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
2508
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
2509
+ tokensUsed: Number(r.tokens_used ?? 0),
2510
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2511
+ }));
2512
+ }
1248
2513
  function isTmuxSessionAlive(identifier) {
1249
2514
  if (!identifier || identifier === "unknown") return true;
1250
2515
  try {
@@ -1416,7 +2681,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1416
2681
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
1417
2682
  } catch {
1418
2683
  }
1419
- if (input.status === "done" || input.status === "cancelled") {
2684
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
1420
2685
  try {
1421
2686
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
1422
2687
  clearQueueForAgent2(String(row.assigned_to));
@@ -1433,6 +2698,65 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1433
2698
  }
1434
2699
  return { row, taskFile, now, taskId };
1435
2700
  }
2701
+ async function deleteTaskCore(taskId, _baseDir) {
2702
+ const client = getClient();
2703
+ const row = await resolveTask(client, taskId);
2704
+ const id = String(row.id);
2705
+ const taskFile = String(row.task_file);
2706
+ const assignedTo = String(row.assigned_to);
2707
+ const assignedBy = String(row.assigned_by);
2708
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
2709
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
2710
+ return { taskFile, assignedTo, assignedBy, taskSlug };
2711
+ }
2712
+ async function ensureArchitectureDoc(baseDir, projectName) {
2713
+ const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
2714
+ try {
2715
+ if (existsSync11(archPath)) return;
2716
+ const template = [
2717
+ `# ${projectName} \u2014 System Architecture`,
2718
+ "",
2719
+ "> Employees: read this before every task. Update it when you change system structure.",
2720
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
2721
+ "",
2722
+ "## Overview",
2723
+ "",
2724
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
2725
+ "",
2726
+ "## Key Components",
2727
+ "",
2728
+ "<!-- List the major modules, services, or subsystems. -->",
2729
+ "",
2730
+ "## Data Flow",
2731
+ "",
2732
+ "<!-- How does data move through the system? What writes where? -->",
2733
+ "",
2734
+ "## Invariants",
2735
+ "",
2736
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
2737
+ "",
2738
+ "## Dependencies",
2739
+ "",
2740
+ "<!-- What depends on what? If I change X, what else is affected? -->",
2741
+ ""
2742
+ ].join("\n");
2743
+ await writeFile3(archPath, template, "utf-8");
2744
+ } catch {
2745
+ }
2746
+ }
2747
+ async function ensureGitignoreExe(baseDir) {
2748
+ const gitignorePath = path11.join(baseDir, ".gitignore");
2749
+ try {
2750
+ if (existsSync11(gitignorePath)) {
2751
+ const content = readFileSync10(gitignorePath, "utf-8");
2752
+ if (/^\/?exe\/?$/m.test(content)) return;
2753
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2754
+ } else {
2755
+ await writeFile3(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
2756
+ }
2757
+ } catch {
2758
+ }
2759
+ }
1436
2760
  var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
1437
2761
  var init_tasks_crud = __esm({
1438
2762
  "src/lib/tasks-crud.ts"() {
@@ -1456,7 +2780,127 @@ var init_tasks_crud = __esm({
1456
2780
 
1457
2781
  // src/lib/tasks-review.ts
1458
2782
  import path12 from "path";
1459
- import { existsSync as existsSync10, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
2783
+ import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2784
+ async function countPendingReviews(sessionScope) {
2785
+ const client = getClient();
2786
+ const scope = strictSessionScopeFilter(
2787
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2788
+ );
2789
+ const result = await client.execute({
2790
+ sql: `SELECT COUNT(*) as cnt FROM tasks
2791
+ WHERE status = 'needs_review'${scope.sql}`,
2792
+ args: [...scope.args]
2793
+ });
2794
+ return Number(result.rows[0]?.cnt) || 0;
2795
+ }
2796
+ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2797
+ const client = getClient();
2798
+ const scope = strictSessionScopeFilter(
2799
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2800
+ );
2801
+ const result = await client.execute({
2802
+ sql: `SELECT COUNT(*) as cnt FROM tasks
2803
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
2804
+ args: [sinceIso, ...scope.args]
2805
+ });
2806
+ return Number(result.rows[0]?.cnt) || 0;
2807
+ }
2808
+ async function listPendingReviews(limit, sessionScope) {
2809
+ const client = getClient();
2810
+ const scope = strictSessionScopeFilter(
2811
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2812
+ );
2813
+ const result = await client.execute({
2814
+ sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2815
+ WHERE status = 'needs_review'${scope.sql}
2816
+ ORDER BY updated_at ASC LIMIT ?`,
2817
+ args: [...scope.args, limit]
2818
+ });
2819
+ return result.rows;
2820
+ }
2821
+ async function cleanupOrphanedReviews() {
2822
+ const client = getClient();
2823
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2824
+ const r1 = await client.execute({
2825
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
2826
+ WHERE status IN ('open', 'needs_review', 'in_progress')
2827
+ AND assigned_by = 'system'
2828
+ AND title LIKE 'Review:%'
2829
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
2830
+ args: [now]
2831
+ });
2832
+ const r1b = await client.execute({
2833
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
2834
+ WHERE status IN ('open', 'needs_review')
2835
+ AND title LIKE 'Review:%completed%'
2836
+ 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')))`,
2837
+ args: [now]
2838
+ });
2839
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
2840
+ const r2 = await client.execute({
2841
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
2842
+ WHERE status = 'needs_review'
2843
+ AND result IS NOT NULL
2844
+ AND updated_at < ?`,
2845
+ args: [now, staleThreshold]
2846
+ });
2847
+ const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
2848
+ if (total > 0) {
2849
+ process.stderr.write(
2850
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
2851
+ `
2852
+ );
2853
+ }
2854
+ return total;
2855
+ }
2856
+ function getReviewChecklist(role, agent, taskSlug) {
2857
+ const roleLower = role.toLowerCase();
2858
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
2859
+ return {
2860
+ lens: "Code Quality (Engineer)",
2861
+ checklist: [
2862
+ "1. Do all tests pass? Any new tests needed?",
2863
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
2864
+ "3. Does it follow existing patterns and conventions in the codebase?",
2865
+ "4. Any regressions in the test suite?"
2866
+ ]
2867
+ };
2868
+ }
2869
+ if (roleLower === "cto" || roleLower.includes("architect")) {
2870
+ return {
2871
+ lens: "Architecture (CTO)",
2872
+ checklist: [
2873
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
2874
+ "2. Is it backward compatible? Any breaking changes?",
2875
+ "3. Does it introduce technical debt? Is that debt justified?",
2876
+ "4. Security implications? Any new attack surface?",
2877
+ "5. Does it scale? Performance considerations?",
2878
+ "6. Coordination: does this affect other employees' work or other projects?"
2879
+ ]
2880
+ };
2881
+ }
2882
+ if (roleLower === "coo" || roleLower.includes("operations")) {
2883
+ return {
2884
+ lens: "Strategic (COO)",
2885
+ checklist: [
2886
+ "1. Does this serve the project mission?",
2887
+ "2. Is this the right work at the right time?",
2888
+ "3. Does the architectural assessment make sense for the business?",
2889
+ "4. Any cross-project implications?"
2890
+ ]
2891
+ };
2892
+ }
2893
+ return {
2894
+ lens: "General",
2895
+ checklist: [
2896
+ "1. Read the original task's acceptance criteria",
2897
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
2898
+ "3. Verify code changes match requirements",
2899
+ "4. Check if tests were added/updated",
2900
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
2901
+ ]
2902
+ };
2903
+ }
1460
2904
  async function cleanupReviewFile(row, taskFile, _baseDir) {
1461
2905
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
1462
2906
  try {
@@ -1502,10 +2946,10 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
1502
2946
  }
1503
2947
  try {
1504
2948
  const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
1505
- if (existsSync10(cacheDir)) {
2949
+ if (existsSync12(cacheDir)) {
1506
2950
  for (const f of readdirSync3(cacheDir)) {
1507
2951
  if (f.startsWith("review-notified-")) {
1508
- unlinkSync3(path12.join(cacheDir, f));
2952
+ unlinkSync4(path12.join(cacheDir, f));
1509
2953
  }
1510
2954
  }
1511
2955
  }
@@ -1522,6 +2966,7 @@ var init_tasks_review = __esm({
1522
2966
  init_tmux_routing();
1523
2967
  init_session_key();
1524
2968
  init_state_bus();
2969
+ init_task_scope();
1525
2970
  }
1526
2971
  });
1527
2972
 
@@ -1578,7 +3023,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
1578
3023
  const scScope = sessionScopeFilter();
1579
3024
  const remaining = await client.execute({
1580
3025
  sql: `SELECT COUNT(*) as cnt FROM tasks
1581
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3026
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
1582
3027
  args: [parentTaskId, ...scScope.args]
1583
3028
  });
1584
3029
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -1610,7 +3055,152 @@ var init_tasks_chain = __esm({
1610
3055
  }
1611
3056
  });
1612
3057
 
3058
+ // src/lib/project-name.ts
3059
+ import { execSync as execSync6 } from "child_process";
3060
+ import path14 from "path";
3061
+ function getProjectName(cwd) {
3062
+ const dir = cwd ?? process.cwd();
3063
+ if (_cached2 && _cachedCwd === dir) return _cached2;
3064
+ try {
3065
+ let repoRoot;
3066
+ try {
3067
+ const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
3068
+ cwd: dir,
3069
+ encoding: "utf8",
3070
+ timeout: 2e3,
3071
+ stdio: ["pipe", "pipe", "pipe"]
3072
+ }).trim();
3073
+ repoRoot = path14.dirname(gitCommonDir);
3074
+ } catch {
3075
+ repoRoot = execSync6("git rev-parse --show-toplevel", {
3076
+ cwd: dir,
3077
+ encoding: "utf8",
3078
+ timeout: 2e3,
3079
+ stdio: ["pipe", "pipe", "pipe"]
3080
+ }).trim();
3081
+ }
3082
+ _cached2 = path14.basename(repoRoot);
3083
+ _cachedCwd = dir;
3084
+ return _cached2;
3085
+ } catch {
3086
+ _cached2 = path14.basename(dir);
3087
+ _cachedCwd = dir;
3088
+ return _cached2;
3089
+ }
3090
+ }
3091
+ var _cached2, _cachedCwd;
3092
+ var init_project_name = __esm({
3093
+ "src/lib/project-name.ts"() {
3094
+ "use strict";
3095
+ _cached2 = null;
3096
+ _cachedCwd = null;
3097
+ }
3098
+ });
3099
+
3100
+ // src/lib/session-scope.ts
3101
+ var session_scope_exports = {};
3102
+ __export(session_scope_exports, {
3103
+ assertSessionScope: () => assertSessionScope,
3104
+ findSessionForProject: () => findSessionForProject,
3105
+ getSessionProject: () => getSessionProject
3106
+ });
3107
+ function getSessionProject(sessionName) {
3108
+ const sessions = listSessions();
3109
+ const entry = sessions.find((s) => s.windowName === sessionName);
3110
+ if (!entry) return null;
3111
+ const parts = entry.projectDir.split("/").filter(Boolean);
3112
+ return parts[parts.length - 1] ?? null;
3113
+ }
3114
+ function findSessionForProject(projectName) {
3115
+ const sessions = listSessions();
3116
+ for (const s of sessions) {
3117
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
3118
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
3119
+ }
3120
+ return null;
3121
+ }
3122
+ function assertSessionScope(actionType, targetProject) {
3123
+ try {
3124
+ const currentProject = getProjectName();
3125
+ const exeSession = resolveExeSession();
3126
+ if (!exeSession) {
3127
+ return { allowed: true, reason: "no_session" };
3128
+ }
3129
+ if (currentProject === targetProject) {
3130
+ return {
3131
+ allowed: true,
3132
+ reason: "same_session",
3133
+ currentProject,
3134
+ targetProject
3135
+ };
3136
+ }
3137
+ process.stderr.write(
3138
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3139
+ `
3140
+ );
3141
+ return {
3142
+ allowed: false,
3143
+ reason: "cross_session_denied",
3144
+ currentProject,
3145
+ targetProject,
3146
+ targetSession: findSessionForProject(targetProject)?.windowName
3147
+ };
3148
+ } catch {
3149
+ return { allowed: true, reason: "no_session" };
3150
+ }
3151
+ }
3152
+ var init_session_scope = __esm({
3153
+ "src/lib/session-scope.ts"() {
3154
+ "use strict";
3155
+ init_session_registry();
3156
+ init_project_name();
3157
+ init_tmux_routing();
3158
+ init_employees();
3159
+ }
3160
+ });
3161
+
1613
3162
  // src/lib/tasks-notify.ts
3163
+ async function dispatchTaskToEmployee(input) {
3164
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
3165
+ let crossProject = false;
3166
+ if (input.projectName) {
3167
+ try {
3168
+ const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
3169
+ const check = assertSessionScope2("dispatch_task", input.projectName);
3170
+ if (check.reason === "cross_session_denied") {
3171
+ crossProject = true;
3172
+ return { dispatched: "skipped", crossProject: true };
3173
+ }
3174
+ } catch {
3175
+ }
3176
+ }
3177
+ try {
3178
+ const transport = getTransport();
3179
+ const exeSession = resolveExeSession();
3180
+ if (!exeSession) return { dispatched: "session_missing" };
3181
+ const sessionName = employeeSessionName(input.assignedTo, exeSession);
3182
+ if (transport.isAlive(sessionName)) {
3183
+ const result = sendIntercom(sessionName);
3184
+ const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
3185
+ return { dispatched, session: sessionName, crossProject };
3186
+ } else {
3187
+ const projectDir = input.projectDir ?? process.cwd();
3188
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
3189
+ autoInstance: isMultiInstance(input.assignedTo)
3190
+ });
3191
+ if (result.status === "failed") {
3192
+ process.stderr.write(
3193
+ `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
3194
+ `
3195
+ );
3196
+ return { dispatched: "session_missing" };
3197
+ }
3198
+ return { dispatched: "spawned", session: result.sessionName, crossProject };
3199
+ }
3200
+ } catch {
3201
+ return { dispatched: "session_missing" };
3202
+ }
3203
+ }
1614
3204
  function notifyTaskDone() {
1615
3205
  try {
1616
3206
  const key = getSessionKey();
@@ -1636,10 +3226,10 @@ var init_tasks_notify = __esm({
1636
3226
  });
1637
3227
 
1638
3228
  // src/lib/behaviors.ts
1639
- import crypto3 from "crypto";
3229
+ import crypto4 from "crypto";
1640
3230
  async function storeBehavior(opts) {
1641
3231
  const client = getClient();
1642
- const id = crypto3.randomUUID();
3232
+ const id = crypto4.randomUUID();
1643
3233
  const now = (/* @__PURE__ */ new Date()).toISOString();
1644
3234
  await client.execute({
1645
3235
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -1668,7 +3258,7 @@ __export(skill_learning_exports, {
1668
3258
  storeTrajectory: () => storeTrajectory,
1669
3259
  sweepTrajectories: () => sweepTrajectories
1670
3260
  });
1671
- import crypto4 from "crypto";
3261
+ import crypto5 from "crypto";
1672
3262
  async function extractTrajectory(taskId, agentId) {
1673
3263
  const client = getClient();
1674
3264
  const result = await client.execute({
@@ -1697,11 +3287,11 @@ async function extractTrajectory(taskId, agentId) {
1697
3287
  return signature;
1698
3288
  }
1699
3289
  function hashSignature(signature) {
1700
- return crypto4.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
3290
+ return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
1701
3291
  }
1702
3292
  async function storeTrajectory(opts) {
1703
3293
  const client = getClient();
1704
- const id = crypto4.randomUUID();
3294
+ const id = crypto5.randomUUID();
1705
3295
  const now = (/* @__PURE__ */ new Date()).toISOString();
1706
3296
  const signatureHash = hashSignature(opts.signature);
1707
3297
  await client.execute({
@@ -1946,29 +3536,63 @@ var init_skill_learning = __esm({
1946
3536
  });
1947
3537
 
1948
3538
  // src/lib/tasks.ts
1949
- import path14 from "path";
1950
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
3539
+ var tasks_exports = {};
3540
+ __export(tasks_exports, {
3541
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
3542
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
3543
+ countPendingReviews: () => countPendingReviews,
3544
+ createTask: () => createTask,
3545
+ createTaskCore: () => createTaskCore,
3546
+ deleteTask: () => deleteTask,
3547
+ deleteTaskCore: () => deleteTaskCore,
3548
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
3549
+ ensureGitignoreExe: () => ensureGitignoreExe,
3550
+ getReviewChecklist: () => getReviewChecklist,
3551
+ listPendingReviews: () => listPendingReviews,
3552
+ listTasks: () => listTasks,
3553
+ resolveTask: () => resolveTask,
3554
+ slugify: () => slugify,
3555
+ updateTask: () => updateTask,
3556
+ updateTaskStatus: () => updateTaskStatus,
3557
+ writeCheckpoint: () => writeCheckpoint
3558
+ });
3559
+ import path15 from "path";
3560
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
3561
+ async function createTask(input) {
3562
+ const result = await createTaskCore(input);
3563
+ if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
3564
+ dispatchTaskToEmployee({
3565
+ assignedTo: input.assignedTo,
3566
+ title: input.title,
3567
+ priority: input.priority,
3568
+ taskFile: result.taskFile,
3569
+ initialStatus: result.status,
3570
+ projectName: input.projectName
3571
+ });
3572
+ }
3573
+ return result;
3574
+ }
1951
3575
  async function updateTask(input) {
1952
3576
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
1953
3577
  try {
1954
3578
  const agent = String(row.assigned_to);
1955
- const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
1956
- const cachePath = path14.join(cacheDir, `current-task-${agent}.json`);
3579
+ const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
3580
+ const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
1957
3581
  if (input.status === "in_progress") {
1958
- mkdirSync5(cacheDir, { recursive: true });
1959
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
1960
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3582
+ mkdirSync6(cacheDir, { recursive: true });
3583
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3584
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
1961
3585
  try {
1962
- unlinkSync4(cachePath);
3586
+ unlinkSync5(cachePath);
1963
3587
  } catch {
1964
3588
  }
1965
3589
  }
1966
3590
  } catch {
1967
3591
  }
1968
- if (input.status === "done") {
3592
+ if (input.status === "done" || input.status === "closed") {
1969
3593
  await cleanupReviewFile(row, taskFile, input.baseDir);
1970
3594
  }
1971
- if (input.status === "done" || input.status === "cancelled") {
3595
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
1972
3596
  try {
1973
3597
  const client = getClient();
1974
3598
  const taskTitle = String(row.title);
@@ -1984,7 +3608,7 @@ async function updateTask(input) {
1984
3608
  if (!isCoordinatorName(assignedAgent)) {
1985
3609
  try {
1986
3610
  const draftClient = getClient();
1987
- if (input.status === "done") {
3611
+ if (input.status === "done" || input.status === "closed") {
1988
3612
  await draftClient.execute({
1989
3613
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
1990
3614
  args: [assignedAgent]
@@ -2001,7 +3625,7 @@ async function updateTask(input) {
2001
3625
  try {
2002
3626
  const client = getClient();
2003
3627
  const cascaded = await client.execute({
2004
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
3628
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
2005
3629
  WHERE parent_task_id = ? AND status = 'needs_review'`,
2006
3630
  args: [now, taskId]
2007
3631
  });
@@ -2014,14 +3638,14 @@ async function updateTask(input) {
2014
3638
  } catch {
2015
3639
  }
2016
3640
  }
2017
- const isTerminal = input.status === "done" || input.status === "needs_review";
3641
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
2018
3642
  if (isTerminal) {
2019
3643
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
2020
3644
  if (!isCoordinator) {
2021
3645
  notifyTaskDone();
2022
3646
  }
2023
3647
  await markTaskNotificationsRead(taskFile);
2024
- if (input.status === "done") {
3648
+ if (input.status === "done" || input.status === "closed") {
2025
3649
  try {
2026
3650
  await cascadeUnblock(taskId, input.baseDir, now);
2027
3651
  } catch {
@@ -2041,7 +3665,7 @@ async function updateTask(input) {
2041
3665
  }
2042
3666
  }
2043
3667
  }
2044
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3668
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
2045
3669
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
2046
3670
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
2047
3671
  taskId,
@@ -2081,6 +3705,21 @@ async function updateTask(input) {
2081
3705
  nextTask
2082
3706
  };
2083
3707
  }
3708
+ async function deleteTask(taskId, baseDir) {
3709
+ const client = getClient();
3710
+ const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
3711
+ const coordinatorName = getCoordinatorName();
3712
+ const reviewer = assignedBy || coordinatorName;
3713
+ const reviewSlug = `review-${assignedTo}-${taskSlug}`;
3714
+ const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
3715
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
3716
+ await client.execute({
3717
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
3718
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
3719
+ });
3720
+ await markAsReadByTaskFile(taskFile);
3721
+ await markAsReadByTaskFile(reviewFile);
3722
+ }
2084
3723
  var init_tasks = __esm({
2085
3724
  "src/lib/tasks.ts"() {
2086
3725
  "use strict";
@@ -2108,9 +3747,9 @@ __export(active_agent_exports, {
2108
3747
  resolveActiveAgentFromTmuxSession: () => resolveActiveAgentFromTmuxSession,
2109
3748
  writeActiveAgent: () => writeActiveAgent
2110
3749
  });
2111
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5, readdirSync as readdirSync4 } from "fs";
2112
- import { execSync as execSync6 } from "child_process";
2113
- import path15 from "path";
3750
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync6, readdirSync as readdirSync4 } from "fs";
3751
+ import { execSync as execSync7 } from "child_process";
3752
+ import path16 from "path";
2114
3753
  function isNameWithOptionalInstance(candidate, baseName) {
2115
3754
  if (candidate === baseName) return true;
2116
3755
  if (!candidate.startsWith(baseName)) return false;
@@ -2154,12 +3793,12 @@ function resolveActiveAgentFromTmuxSession(sessionName) {
2154
3793
  return null;
2155
3794
  }
2156
3795
  function getMarkerPath() {
2157
- return path15.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
3796
+ return path16.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
2158
3797
  }
2159
3798
  function writeActiveAgent(agentId, agentRole) {
2160
3799
  try {
2161
- mkdirSync6(CACHE_DIR, { recursive: true });
2162
- writeFileSync7(
3800
+ mkdirSync7(CACHE_DIR, { recursive: true });
3801
+ writeFileSync8(
2163
3802
  getMarkerPath(),
2164
3803
  JSON.stringify({ agentId, agentRole, startedAt: (/* @__PURE__ */ new Date()).toISOString() })
2165
3804
  );
@@ -2168,21 +3807,21 @@ function writeActiveAgent(agentId, agentRole) {
2168
3807
  }
2169
3808
  function clearActiveAgent() {
2170
3809
  try {
2171
- unlinkSync5(getMarkerPath());
3810
+ unlinkSync6(getMarkerPath());
2172
3811
  } catch {
2173
3812
  }
2174
3813
  }
2175
3814
  function getActiveAgent() {
2176
3815
  try {
2177
3816
  const markerPath = getMarkerPath();
2178
- const raw = readFileSync10(markerPath, "utf8");
3817
+ const raw = readFileSync11(markerPath, "utf8");
2179
3818
  const data = JSON.parse(raw);
2180
3819
  if (data.agentId) {
2181
3820
  if (data.startedAt) {
2182
3821
  const age = Date.now() - new Date(data.startedAt).getTime();
2183
3822
  if (age > STALE_MS) {
2184
3823
  try {
2185
- unlinkSync5(markerPath);
3824
+ unlinkSync6(markerPath);
2186
3825
  } catch {
2187
3826
  }
2188
3827
  } else {
@@ -2201,7 +3840,7 @@ function getActiveAgent() {
2201
3840
  } catch {
2202
3841
  }
2203
3842
  try {
2204
- const sessionName = execSync6(
3843
+ const sessionName = execSync7(
2205
3844
  "tmux display-message -p '#{session_name}' 2>/dev/null",
2206
3845
  { encoding: "utf8", timeout: 2e3 }
2207
3846
  ).trim();
@@ -2223,14 +3862,14 @@ function getAllActiveAgents() {
2223
3862
  const key = file.slice("active-agent-".length, -".json".length);
2224
3863
  if (key === "undefined") continue;
2225
3864
  try {
2226
- const raw = readFileSync10(path15.join(CACHE_DIR, file), "utf8");
3865
+ const raw = readFileSync11(path16.join(CACHE_DIR, file), "utf8");
2227
3866
  const data = JSON.parse(raw);
2228
3867
  if (!data.agentId) continue;
2229
3868
  if (data.startedAt) {
2230
3869
  const age = Date.now() - new Date(data.startedAt).getTime();
2231
3870
  if (age > STALE_MS) {
2232
3871
  try {
2233
- unlinkSync5(path15.join(CACHE_DIR, file));
3872
+ unlinkSync6(path16.join(CACHE_DIR, file));
2234
3873
  } catch {
2235
3874
  }
2236
3875
  continue;
@@ -2253,11 +3892,11 @@ function getAllActiveAgents() {
2253
3892
  function cleanupSessionMarkers() {
2254
3893
  const key = getSessionKey();
2255
3894
  try {
2256
- unlinkSync5(path15.join(CACHE_DIR, `active-agent-${key}.json`));
3895
+ unlinkSync6(path16.join(CACHE_DIR, `active-agent-${key}.json`));
2257
3896
  } catch {
2258
3897
  }
2259
3898
  try {
2260
- unlinkSync5(path15.join(CACHE_DIR, "active-agent-undefined.json"));
3899
+ unlinkSync6(path16.join(CACHE_DIR, "active-agent-undefined.json"));
2261
3900
  } catch {
2262
3901
  }
2263
3902
  }
@@ -2268,7 +3907,7 @@ var init_active_agent = __esm({
2268
3907
  init_config();
2269
3908
  init_session_key();
2270
3909
  init_employees();
2271
- CACHE_DIR = path15.join(EXE_AI_DIR, "session-cache");
3910
+ CACHE_DIR = path16.join(EXE_AI_DIR, "session-cache");
2272
3911
  STALE_MS = 24 * 60 * 60 * 1e3;
2273
3912
  }
2274
3913
  });
@@ -2287,7 +3926,7 @@ function registerUpdateTask(server) {
2287
3926
  description: "Update task status. Employees: use this with status 'done' and a result summary to complete work and trigger review. Accepts UUID, slug (filename), or title substring.",
2288
3927
  inputSchema: {
2289
3928
  task_id: z.string().describe("Task identifier \u2014 UUID, slug (e.g. 'fix-auth-bug'), or title substring"),
2290
- status: z.enum(["open", "in_progress", "done", "needs_review", "blocked", "cancelled"]).describe("New status"),
3929
+ status: z.enum(["open", "in_progress", "done", "needs_review", "blocked", "cancelled", "closed"]).describe("New status"),
2291
3930
  result: z.string().optional().describe("Result summary (include when status=done)")
2292
3931
  }
2293
3932
  },
@@ -2377,7 +4016,17 @@ function registerUpdateTask(server) {
2377
4016
  }
2378
4017
  let text = `Task "${task.title}" marked ${task.status}.
2379
4018
  File: ${task.taskFile}`;
2380
- const isTerminal = status === "done" || status === "needs_review";
4019
+ const isTerminal = status === "done" || status === "needs_review" || status === "closed";
4020
+ if (isTerminal && task.assignedBy) {
4021
+ try {
4022
+ const { notifyCoordinatorTaskCompletion: notifyCoordinatorTaskCompletion2, resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
4023
+ const coordinatorSession = resolveExeSession2();
4024
+ if (coordinatorSession) {
4025
+ notifyCoordinatorTaskCompletion2(coordinatorSession, callerAgentId ?? "agent", task.title);
4026
+ }
4027
+ } catch {
4028
+ }
4029
+ }
2381
4030
  if (isTerminal && task.nextTask) {
2382
4031
  text += `
2383
4032