@askexenow/exe-os 0.8.83 → 0.8.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/bin/backfill-conversations.js +746 -595
  2. package/dist/bin/backfill-responses.js +745 -594
  3. package/dist/bin/backfill-vectors.js +312 -226
  4. package/dist/bin/cleanup-stale-review-tasks.js +154 -21
  5. package/dist/bin/cli.js +14678 -12676
  6. package/dist/bin/exe-agent-config.js +242 -0
  7. package/dist/bin/exe-agent.js +100 -91
  8. package/dist/bin/exe-assign.js +1003 -854
  9. package/dist/bin/exe-boot.js +1420 -485
  10. package/dist/bin/exe-call.js +10 -0
  11. package/dist/bin/exe-cloud.js +29 -6
  12. package/dist/bin/exe-dispatch.js +572 -271
  13. package/dist/bin/exe-doctor.js +403 -6
  14. package/dist/bin/exe-export-behaviors.js +175 -72
  15. package/dist/bin/exe-forget.js +102 -3
  16. package/dist/bin/exe-gateway.js +796 -292
  17. package/dist/bin/exe-healthcheck.js +134 -1
  18. package/dist/bin/exe-heartbeat.js +172 -36
  19. package/dist/bin/exe-kill.js +175 -72
  20. package/dist/bin/exe-launch-agent.js +189 -76
  21. package/dist/bin/exe-link.js +927 -82
  22. package/dist/bin/exe-new-employee.js +60 -8
  23. package/dist/bin/exe-pending-messages.js +151 -19
  24. package/dist/bin/exe-pending-notifications.js +97 -2
  25. package/dist/bin/exe-pending-reviews.js +155 -22
  26. package/dist/bin/exe-rename.js +564 -23
  27. package/dist/bin/exe-review.js +231 -73
  28. package/dist/bin/exe-search.js +995 -228
  29. package/dist/bin/exe-session-cleanup.js +4930 -1664
  30. package/dist/bin/exe-settings.js +20 -5
  31. package/dist/bin/exe-start-codex.js +2598 -0
  32. package/dist/bin/exe-start.sh +15 -3
  33. package/dist/bin/exe-status.js +154 -21
  34. package/dist/bin/exe-team.js +97 -2
  35. package/dist/bin/git-sweep.js +1180 -363
  36. package/dist/bin/graph-backfill.js +175 -72
  37. package/dist/bin/graph-export.js +175 -72
  38. package/dist/bin/install.js +60 -7
  39. package/dist/bin/list-providers.js +1 -0
  40. package/dist/bin/scan-tasks.js +1185 -367
  41. package/dist/bin/setup.js +914 -270
  42. package/dist/bin/shard-migrate.js +175 -72
  43. package/dist/bin/update.js +1 -0
  44. package/dist/bin/wiki-sync.js +175 -72
  45. package/dist/gateway/index.js +792 -285
  46. package/dist/hooks/bug-report-worker.js +445 -135
  47. package/dist/hooks/commit-complete.js +1178 -361
  48. package/dist/hooks/error-recall.js +994 -228
  49. package/dist/hooks/ingest-worker.js +1799 -1234
  50. package/dist/hooks/ingest.js +3 -0
  51. package/dist/hooks/instructions-loaded.js +707 -97
  52. package/dist/hooks/notification.js +699 -89
  53. package/dist/hooks/post-compact.js +757 -109
  54. package/dist/hooks/pre-compact.js +1061 -244
  55. package/dist/hooks/pre-tool-use.js +787 -130
  56. package/dist/hooks/prompt-ingest-worker.js +242 -101
  57. package/dist/hooks/prompt-submit.js +1121 -299
  58. package/dist/hooks/response-ingest-worker.js +242 -101
  59. package/dist/hooks/session-end.js +4063 -397
  60. package/dist/hooks/session-start.js +1071 -254
  61. package/dist/hooks/stop.js +768 -120
  62. package/dist/hooks/subagent-stop.js +757 -109
  63. package/dist/hooks/summary-worker.js +1706 -1011
  64. package/dist/index.js +1821 -1098
  65. package/dist/lib/agent-config.js +167 -0
  66. package/dist/lib/cloud-sync.js +932 -88
  67. package/dist/lib/consolidation.js +2 -1
  68. package/dist/lib/database.js +642 -87
  69. package/dist/lib/db-daemon-client.js +503 -0
  70. package/dist/lib/device-registry.js +547 -7
  71. package/dist/lib/embedder.js +14 -28
  72. package/dist/lib/employee-templates.js +84 -74
  73. package/dist/lib/employees.js +9 -0
  74. package/dist/lib/exe-daemon-client.js +16 -29
  75. package/dist/lib/exe-daemon.js +2733 -1575
  76. package/dist/lib/hybrid-search.js +995 -228
  77. package/dist/lib/identity.js +87 -67
  78. package/dist/lib/keychain.js +9 -1
  79. package/dist/lib/messaging.js +103 -40
  80. package/dist/lib/reminders.js +91 -74
  81. package/dist/lib/runtime-table.js +16 -0
  82. package/dist/lib/schedules.js +96 -2
  83. package/dist/lib/session-wrappers.js +22 -0
  84. package/dist/lib/skill-learning.js +103 -85
  85. package/dist/lib/store.js +234 -73
  86. package/dist/lib/tasks.js +348 -134
  87. package/dist/lib/tmux-routing.js +422 -208
  88. package/dist/lib/token-spend.js +273 -0
  89. package/dist/lib/ws-client.js +11 -0
  90. package/dist/mcp/server.js +5742 -696
  91. package/dist/mcp/tools/complete-reminder.js +94 -77
  92. package/dist/mcp/tools/create-reminder.js +94 -77
  93. package/dist/mcp/tools/create-task.js +375 -152
  94. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  95. package/dist/mcp/tools/list-reminders.js +94 -77
  96. package/dist/mcp/tools/list-tasks.js +99 -31
  97. package/dist/mcp/tools/send-message.js +108 -45
  98. package/dist/mcp/tools/update-task.js +162 -77
  99. package/dist/runtime/index.js +1075 -258
  100. package/dist/tui/App.js +1333 -506
  101. package/package.json +6 -1
  102. package/src/commands/exe/agent-config.md +27 -0
  103. package/src/commands/exe/cc-doctor.md +10 -0
@@ -269,123 +269,19 @@ var init_provider_table = __esm({
269
269
  }
270
270
  });
271
271
 
272
- // src/lib/intercom-queue.ts
273
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
274
- import path2 from "path";
275
- import os2 from "os";
276
- function ensureDir() {
277
- const dir = path2.dirname(QUEUE_PATH);
278
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
279
- }
280
- function readQueue() {
281
- try {
282
- if (!existsSync2(QUEUE_PATH)) return [];
283
- return JSON.parse(readFileSync2(QUEUE_PATH, "utf8"));
284
- } catch {
285
- return [];
286
- }
287
- }
288
- function writeQueue(queue) {
289
- ensureDir();
290
- const tmp = `${QUEUE_PATH}.tmp`;
291
- writeFileSync2(tmp, JSON.stringify(queue, null, 2));
292
- renameSync(tmp, QUEUE_PATH);
293
- }
294
- function queueIntercom(targetSession, reason) {
295
- const queue = readQueue();
296
- const existing = queue.find((q) => q.targetSession === targetSession);
297
- if (existing) {
298
- existing.attempts++;
299
- existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
300
- existing.reason = reason;
301
- } else {
302
- queue.push({
303
- targetSession,
304
- queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
305
- attempts: 0,
306
- reason
307
- });
308
- }
309
- writeQueue(queue);
310
- }
311
- var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
312
- var init_intercom_queue = __esm({
313
- "src/lib/intercom-queue.ts"() {
314
- "use strict";
315
- QUEUE_PATH = path2.join(os2.homedir(), ".exe-os", "intercom-queue.json");
316
- TTL_MS = 60 * 60 * 1e3;
317
- INTERCOM_LOG = path2.join(os2.homedir(), ".exe-os", "intercom.log");
318
- }
319
- });
320
-
321
- // src/lib/db-retry.ts
322
- function isBusyError(err) {
323
- if (err instanceof Error) {
324
- const msg = err.message.toLowerCase();
325
- return msg.includes("sqlite_busy") || msg.includes("database is locked");
326
- }
327
- return false;
328
- }
329
- function delay(ms) {
330
- return new Promise((resolve) => setTimeout(resolve, ms));
331
- }
332
- async function retryOnBusy(fn, label) {
333
- let lastError;
334
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
335
- try {
336
- return await fn();
337
- } catch (err) {
338
- lastError = err;
339
- if (!isBusyError(err) || attempt === MAX_RETRIES) {
340
- throw err;
341
- }
342
- const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
343
- const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
344
- process.stderr.write(
345
- `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
346
- `
347
- );
348
- await delay(backoff + jitter);
349
- }
350
- }
351
- throw lastError;
352
- }
353
- function wrapWithRetry(client) {
354
- return new Proxy(client, {
355
- get(target, prop, receiver) {
356
- if (prop === "execute") {
357
- return (sql) => retryOnBusy(() => target.execute(sql), "execute");
358
- }
359
- if (prop === "batch") {
360
- return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
361
- }
362
- return Reflect.get(target, prop, receiver);
363
- }
364
- });
365
- }
366
- var MAX_RETRIES, BASE_DELAY_MS, MAX_JITTER_MS;
367
- var init_db_retry = __esm({
368
- "src/lib/db-retry.ts"() {
369
- "use strict";
370
- MAX_RETRIES = 3;
371
- BASE_DELAY_MS = 200;
372
- MAX_JITTER_MS = 300;
373
- }
374
- });
375
-
376
272
  // src/lib/config.ts
377
273
  import { readFile, writeFile, mkdir, chmod } from "fs/promises";
378
- import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
379
- import path3 from "path";
380
- import os3 from "os";
274
+ import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
275
+ import path2 from "path";
276
+ import os2 from "os";
381
277
  function resolveDataDir() {
382
278
  if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
383
279
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
384
- const newDir = path3.join(os3.homedir(), ".exe-os");
385
- const legacyDir = path3.join(os3.homedir(), ".exe-mem");
386
- if (!existsSync3(newDir) && existsSync3(legacyDir)) {
280
+ const newDir = path2.join(os2.homedir(), ".exe-os");
281
+ const legacyDir = path2.join(os2.homedir(), ".exe-mem");
282
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
387
283
  try {
388
- renameSync2(legacyDir, newDir);
284
+ renameSync(legacyDir, newDir);
389
285
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
390
286
  `);
391
287
  } catch {
@@ -447,9 +343,9 @@ function normalizeAutoUpdate(raw) {
447
343
  async function loadConfig() {
448
344
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
449
345
  await mkdir(dir, { recursive: true });
450
- const configPath = path3.join(dir, "config.json");
451
- if (!existsSync3(configPath)) {
452
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
346
+ const configPath = path2.join(dir, "config.json");
347
+ if (!existsSync2(configPath)) {
348
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
453
349
  }
454
350
  const raw = await readFile(configPath, "utf-8");
455
351
  try {
@@ -467,13 +363,13 @@ async function loadConfig() {
467
363
  normalizeScalingRoadmap(migratedCfg);
468
364
  normalizeSessionLifecycle(migratedCfg);
469
365
  normalizeAutoUpdate(migratedCfg);
470
- const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
366
+ const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
471
367
  if (config.dbPath.startsWith("~")) {
472
- config.dbPath = config.dbPath.replace(/^~/, os3.homedir());
368
+ config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
473
369
  }
474
370
  return config;
475
371
  } catch {
476
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
372
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
477
373
  }
478
374
  }
479
375
  var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
@@ -481,10 +377,10 @@ var init_config = __esm({
481
377
  "src/lib/config.ts"() {
482
378
  "use strict";
483
379
  EXE_AI_DIR = resolveDataDir();
484
- DB_PATH = path3.join(EXE_AI_DIR, "memories.db");
485
- MODELS_DIR = path3.join(EXE_AI_DIR, "models");
486
- CONFIG_PATH = path3.join(EXE_AI_DIR, "config.json");
487
- LEGACY_LANCE_PATH = path3.join(EXE_AI_DIR, "local.lance");
380
+ DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
381
+ MODELS_DIR = path2.join(EXE_AI_DIR, "models");
382
+ CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
383
+ LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
488
384
  CURRENT_CONFIG_VERSION = 1;
489
385
  DEFAULT_CONFIG = {
490
386
  config_version: CURRENT_CONFIG_VERSION,
@@ -556,11 +452,166 @@ var init_config = __esm({
556
452
  }
557
453
  });
558
454
 
455
+ // src/lib/runtime-table.ts
456
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
457
+ var init_runtime_table = __esm({
458
+ "src/lib/runtime-table.ts"() {
459
+ "use strict";
460
+ RUNTIME_TABLE = {
461
+ codex: {
462
+ binary: "codex",
463
+ launchMode: "exec",
464
+ autoApproveFlag: "--full-auto",
465
+ inlineFlag: "--no-alt-screen",
466
+ apiKeyEnv: "OPENAI_API_KEY",
467
+ defaultModel: "gpt-5.4"
468
+ }
469
+ };
470
+ DEFAULT_RUNTIME = "claude";
471
+ }
472
+ });
473
+
474
+ // src/lib/agent-config.ts
475
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
476
+ import path3 from "path";
477
+ function loadAgentConfig() {
478
+ if (!existsSync3(AGENT_CONFIG_PATH)) return {};
479
+ try {
480
+ return JSON.parse(readFileSync3(AGENT_CONFIG_PATH, "utf-8"));
481
+ } catch {
482
+ return {};
483
+ }
484
+ }
485
+ function getAgentRuntime(agentId) {
486
+ const config = loadAgentConfig();
487
+ const entry = config[agentId];
488
+ if (entry) return entry;
489
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
490
+ }
491
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
492
+ var init_agent_config = __esm({
493
+ "src/lib/agent-config.ts"() {
494
+ "use strict";
495
+ init_config();
496
+ init_runtime_table();
497
+ AGENT_CONFIG_PATH = path3.join(EXE_AI_DIR, "agent-config.json");
498
+ DEFAULT_MODELS = {
499
+ claude: "claude-opus-4",
500
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
501
+ opencode: "minimax-m2.7"
502
+ };
503
+ }
504
+ });
505
+
506
+ // src/lib/intercom-queue.ts
507
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
508
+ import path4 from "path";
509
+ import os3 from "os";
510
+ function ensureDir() {
511
+ const dir = path4.dirname(QUEUE_PATH);
512
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
513
+ }
514
+ function readQueue() {
515
+ try {
516
+ if (!existsSync4(QUEUE_PATH)) return [];
517
+ return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
518
+ } catch {
519
+ return [];
520
+ }
521
+ }
522
+ function writeQueue(queue) {
523
+ ensureDir();
524
+ const tmp = `${QUEUE_PATH}.tmp`;
525
+ writeFileSync3(tmp, JSON.stringify(queue, null, 2));
526
+ renameSync2(tmp, QUEUE_PATH);
527
+ }
528
+ function queueIntercom(targetSession, reason) {
529
+ const queue = readQueue();
530
+ const existing = queue.find((q) => q.targetSession === targetSession);
531
+ if (existing) {
532
+ existing.attempts++;
533
+ existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
534
+ existing.reason = reason;
535
+ } else {
536
+ queue.push({
537
+ targetSession,
538
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
539
+ attempts: 0,
540
+ reason
541
+ });
542
+ }
543
+ writeQueue(queue);
544
+ }
545
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
546
+ var init_intercom_queue = __esm({
547
+ "src/lib/intercom-queue.ts"() {
548
+ "use strict";
549
+ QUEUE_PATH = path4.join(os3.homedir(), ".exe-os", "intercom-queue.json");
550
+ TTL_MS = 60 * 60 * 1e3;
551
+ INTERCOM_LOG = path4.join(os3.homedir(), ".exe-os", "intercom.log");
552
+ }
553
+ });
554
+
555
+ // src/lib/db-retry.ts
556
+ function isBusyError(err) {
557
+ if (err instanceof Error) {
558
+ const msg = err.message.toLowerCase();
559
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
560
+ }
561
+ return false;
562
+ }
563
+ function delay(ms) {
564
+ return new Promise((resolve) => setTimeout(resolve, ms));
565
+ }
566
+ async function retryOnBusy(fn, label) {
567
+ let lastError;
568
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
569
+ try {
570
+ return await fn();
571
+ } catch (err) {
572
+ lastError = err;
573
+ if (!isBusyError(err) || attempt === MAX_RETRIES) {
574
+ throw err;
575
+ }
576
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
577
+ const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
578
+ process.stderr.write(
579
+ `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
580
+ `
581
+ );
582
+ await delay(backoff + jitter);
583
+ }
584
+ }
585
+ throw lastError;
586
+ }
587
+ function wrapWithRetry(client) {
588
+ return new Proxy(client, {
589
+ get(target, prop, receiver) {
590
+ if (prop === "execute") {
591
+ return (sql) => retryOnBusy(() => target.execute(sql), "execute");
592
+ }
593
+ if (prop === "batch") {
594
+ return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
595
+ }
596
+ return Reflect.get(target, prop, receiver);
597
+ }
598
+ });
599
+ }
600
+ var MAX_RETRIES, BASE_DELAY_MS, MAX_JITTER_MS;
601
+ var init_db_retry = __esm({
602
+ "src/lib/db-retry.ts"() {
603
+ "use strict";
604
+ MAX_RETRIES = 3;
605
+ BASE_DELAY_MS = 200;
606
+ MAX_JITTER_MS = 300;
607
+ }
608
+ });
609
+
559
610
  // src/lib/employees.ts
560
611
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
561
- import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
612
+ import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
562
613
  import { execSync as execSync3 } from "child_process";
563
- import path4 from "path";
614
+ import path5 from "path";
564
615
  import os4 from "os";
565
616
  function normalizeRole(role) {
566
617
  return (role ?? "").trim().toLowerCase();
@@ -579,9 +630,9 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
579
630
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
580
631
  }
581
632
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
582
- if (!existsSync4(employeesPath)) return [];
633
+ if (!existsSync5(employeesPath)) return [];
583
634
  try {
584
- return JSON.parse(readFileSync4(employeesPath, "utf-8"));
635
+ return JSON.parse(readFileSync5(employeesPath, "utf-8"));
585
636
  } catch {
586
637
  return [];
587
638
  }
@@ -600,7 +651,7 @@ var init_employees = __esm({
600
651
  "src/lib/employees.ts"() {
601
652
  "use strict";
602
653
  init_config();
603
- EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
654
+ EMPLOYEES_PATH = path5.join(EXE_AI_DIR, "exe-employees.json");
604
655
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
605
656
  COORDINATOR_ROLE = "COO";
606
657
  MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
@@ -628,6 +679,12 @@ function getClient() {
628
679
  if (!_resilientClient) {
629
680
  throw new Error("Database client not initialized. Call initDatabase() first.");
630
681
  }
682
+ if (process.env.EXE_IS_DAEMON === "1") {
683
+ return _resilientClient;
684
+ }
685
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
686
+ return _daemonClient;
687
+ }
631
688
  return _resilientClient;
632
689
  }
633
690
  function getRawClient() {
@@ -1116,6 +1173,12 @@ async function ensureSchema() {
1116
1173
  } catch {
1117
1174
  }
1118
1175
  }
1176
+ try {
1177
+ await client.execute(
1178
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1179
+ );
1180
+ } catch {
1181
+ }
1119
1182
  await client.executeMultiple(`
1120
1183
  CREATE TABLE IF NOT EXISTS entities (
1121
1184
  id TEXT PRIMARY KEY,
@@ -1168,7 +1231,30 @@ async function ensureSchema() {
1168
1231
  entity_id TEXT NOT NULL,
1169
1232
  PRIMARY KEY (hyperedge_id, entity_id)
1170
1233
  );
1234
+
1235
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1236
+ name,
1237
+ content=entities,
1238
+ content_rowid=rowid
1239
+ );
1240
+
1241
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1242
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1243
+ END;
1244
+
1245
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1246
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1247
+ END;
1248
+
1249
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1250
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1251
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1252
+ END;
1171
1253
  `);
1254
+ try {
1255
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1256
+ } catch {
1257
+ }
1172
1258
  await client.executeMultiple(`
1173
1259
  CREATE TABLE IF NOT EXISTS entity_aliases (
1174
1260
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1349,6 +1435,33 @@ async function ensureSchema() {
1349
1435
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1350
1436
  ON conversations(channel_id);
1351
1437
  `);
1438
+ await client.executeMultiple(`
1439
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1440
+ session_uuid TEXT PRIMARY KEY,
1441
+ agent_id TEXT NOT NULL,
1442
+ session_name TEXT,
1443
+ task_id TEXT,
1444
+ project_name TEXT,
1445
+ started_at TEXT NOT NULL
1446
+ );
1447
+
1448
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1449
+ ON session_agent_map(agent_id);
1450
+ `);
1451
+ try {
1452
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1453
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1454
+ await client.execute({
1455
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1456
+ SELECT session_id, agent_id, '', MIN(timestamp)
1457
+ FROM memories
1458
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1459
+ GROUP BY session_id, agent_id`,
1460
+ args: []
1461
+ });
1462
+ }
1463
+ } catch {
1464
+ }
1352
1465
  try {
1353
1466
  await client.execute({
1354
1467
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1482,8 +1595,30 @@ async function ensureSchema() {
1482
1595
  });
1483
1596
  } catch {
1484
1597
  }
1598
+ for (const col of [
1599
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1600
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1601
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1602
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1603
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1604
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1605
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1606
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1607
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1608
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1609
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1610
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1611
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1612
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1613
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1614
+ ]) {
1615
+ try {
1616
+ await client.execute(col);
1617
+ } catch {
1618
+ }
1619
+ }
1485
1620
  }
1486
- var _client, _resilientClient, initTurso;
1621
+ var _client, _resilientClient, _daemonClient, initTurso;
1487
1622
  var init_database = __esm({
1488
1623
  "src/lib/database.ts"() {
1489
1624
  "use strict";
@@ -1491,23 +1626,24 @@ var init_database = __esm({
1491
1626
  init_employees();
1492
1627
  _client = null;
1493
1628
  _resilientClient = null;
1629
+ _daemonClient = null;
1494
1630
  initTurso = initDatabase;
1495
1631
  }
1496
1632
  });
1497
1633
 
1498
1634
  // src/lib/license.ts
1499
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
1635
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
1500
1636
  import { randomUUID } from "crypto";
1501
- import path5 from "path";
1637
+ import path6 from "path";
1502
1638
  import { jwtVerify, importSPKI } from "jose";
1503
1639
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1504
1640
  var init_license = __esm({
1505
1641
  "src/lib/license.ts"() {
1506
1642
  "use strict";
1507
1643
  init_config();
1508
- LICENSE_PATH = path5.join(EXE_AI_DIR, "license.key");
1509
- CACHE_PATH = path5.join(EXE_AI_DIR, "license-cache.json");
1510
- DEVICE_ID_PATH = path5.join(EXE_AI_DIR, "device-id");
1644
+ LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
1645
+ CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
1646
+ DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
1511
1647
  PLAN_LIMITS = {
1512
1648
  free: { devices: 1, employees: 1, memories: 5e3 },
1513
1649
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -1519,12 +1655,12 @@ var init_license = __esm({
1519
1655
  });
1520
1656
 
1521
1657
  // src/lib/plan-limits.ts
1522
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
1523
- import path6 from "path";
1658
+ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
1659
+ import path7 from "path";
1524
1660
  function getLicenseSync() {
1525
1661
  try {
1526
- if (!existsSync6(CACHE_PATH2)) return freeLicense();
1527
- const raw = JSON.parse(readFileSync6(CACHE_PATH2, "utf8"));
1662
+ if (!existsSync7(CACHE_PATH2)) return freeLicense();
1663
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
1528
1664
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
1529
1665
  const parts = raw.token.split(".");
1530
1666
  if (parts.length !== 3) return freeLicense();
@@ -1562,8 +1698,8 @@ function assertEmployeeLimitSync(rosterPath) {
1562
1698
  const filePath = rosterPath ?? EMPLOYEES_PATH;
1563
1699
  let count = 0;
1564
1700
  try {
1565
- if (existsSync6(filePath)) {
1566
- const raw = readFileSync6(filePath, "utf8");
1701
+ if (existsSync7(filePath)) {
1702
+ const raw = readFileSync7(filePath, "utf8");
1567
1703
  const employees = JSON.parse(raw);
1568
1704
  count = Array.isArray(employees) ? employees.length : 0;
1569
1705
  }
@@ -1592,19 +1728,19 @@ var init_plan_limits = __esm({
1592
1728
  this.name = "PlanLimitError";
1593
1729
  }
1594
1730
  };
1595
- CACHE_PATH2 = path6.join(EXE_AI_DIR, "license-cache.json");
1731
+ CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
1596
1732
  }
1597
1733
  });
1598
1734
 
1599
1735
  // src/lib/notifications.ts
1600
1736
  import crypto from "crypto";
1601
- import path7 from "path";
1737
+ import path8 from "path";
1602
1738
  import os5 from "os";
1603
1739
  import {
1604
- readFileSync as readFileSync7,
1740
+ readFileSync as readFileSync8,
1605
1741
  readdirSync,
1606
1742
  unlinkSync as unlinkSync2,
1607
- existsSync as existsSync7,
1743
+ existsSync as existsSync8,
1608
1744
  rmdirSync
1609
1745
  } from "fs";
1610
1746
  async function writeNotification(notification) {
@@ -1763,10 +1899,11 @@ var init_state_bus = __esm({
1763
1899
 
1764
1900
  // src/lib/tasks-crud.ts
1765
1901
  import crypto3 from "crypto";
1766
- import path8 from "path";
1902
+ import path9 from "path";
1903
+ import os6 from "os";
1767
1904
  import { execSync as execSync4 } from "child_process";
1768
1905
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1769
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
1906
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1770
1907
  async function writeCheckpoint(input) {
1771
1908
  const client = getClient();
1772
1909
  const row = await resolveTask(client, input.taskId);
@@ -1807,6 +1944,35 @@ function extractParentFromContext(contextBody) {
1807
1944
  function slugify(title) {
1808
1945
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1809
1946
  }
1947
+ function buildKeywordIndex() {
1948
+ const idx = /* @__PURE__ */ new Map();
1949
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
1950
+ for (const kw of keywords) {
1951
+ const existing = idx.get(kw) ?? [];
1952
+ existing.push(role);
1953
+ idx.set(kw, existing);
1954
+ }
1955
+ }
1956
+ return idx;
1957
+ }
1958
+ function checkLaneAffinity(title, context, assigneeName) {
1959
+ const employees = loadEmployeesSync();
1960
+ const employee = employees.find((e) => e.name === assigneeName);
1961
+ if (!employee) return void 0;
1962
+ const assigneeRole = employee.role;
1963
+ const text = `${title} ${context}`.toLowerCase();
1964
+ const matchedRoles = /* @__PURE__ */ new Set();
1965
+ for (const [keyword, roles] of KEYWORD_INDEX) {
1966
+ if (text.includes(keyword)) {
1967
+ for (const role of roles) matchedRoles.add(role);
1968
+ }
1969
+ }
1970
+ if (matchedRoles.size === 0) return void 0;
1971
+ if (matchedRoles.has(assigneeRole)) return void 0;
1972
+ if (assigneeRole === "COO") return void 0;
1973
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
1974
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
1975
+ }
1810
1976
  async function resolveTask(client, identifier, scopeSession) {
1811
1977
  const scope = sessionScopeFilter(scopeSession);
1812
1978
  let result2 = await client.execute({
@@ -1856,7 +2022,14 @@ async function createTaskCore(input) {
1856
2022
  const id = crypto3.randomUUID();
1857
2023
  const now = (/* @__PURE__ */ new Date()).toISOString();
1858
2024
  const slug = slugify(input.title);
1859
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
2025
+ let earlySessionScope = null;
2026
+ try {
2027
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2028
+ earlySessionScope = resolveExeSession2();
2029
+ } catch {
2030
+ }
2031
+ const scope = earlySessionScope ?? "default";
2032
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
1860
2033
  let blockedById = null;
1861
2034
  const initialStatus = input.blockedBy ? "blocked" : "open";
1862
2035
  if (input.blockedBy) {
@@ -1896,22 +2069,24 @@ async function createTaskCore(input) {
1896
2069
  if (dupCheck.rows.length > 0) {
1897
2070
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
1898
2071
  }
2072
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2073
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
2074
+ if (laneWarning) {
2075
+ warning = warning ? `${warning}
2076
+ ${laneWarning}` : laneWarning;
2077
+ }
2078
+ }
1899
2079
  if (input.baseDir) {
1900
2080
  try {
1901
- await mkdir3(path8.join(input.baseDir, "exe", "output"), { recursive: true });
1902
- await mkdir3(path8.join(input.baseDir, "exe", "research"), { recursive: true });
2081
+ await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
2082
+ await mkdir3(path9.join(input.baseDir, "exe", "research"), { recursive: true });
1903
2083
  await ensureArchitectureDoc(input.baseDir, input.projectName);
1904
2084
  await ensureGitignoreExe(input.baseDir);
1905
2085
  } catch {
1906
2086
  }
1907
2087
  }
1908
2088
  const complexity = input.complexity ?? "standard";
1909
- let sessionScope = null;
1910
- try {
1911
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1912
- sessionScope = resolveExeSession2();
1913
- } catch {
1914
- }
2089
+ const sessionScope = earlySessionScope;
1915
2090
  await client.execute({
1916
2091
  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)
1917
2092
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -1938,6 +2113,43 @@ async function createTaskCore(input) {
1938
2113
  now
1939
2114
  ]
1940
2115
  });
2116
+ if (input.baseDir) {
2117
+ try {
2118
+ const EXE_OS_DIR = path9.join(os6.homedir(), ".exe-os");
2119
+ const mdPath = path9.join(EXE_OS_DIR, taskFile);
2120
+ const mdDir = path9.dirname(mdPath);
2121
+ if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
2122
+ const reviewer = input.reviewer ?? input.assignedBy;
2123
+ const mdContent = `# ${input.title}
2124
+
2125
+ **ID:** ${id}
2126
+ **Status:** ${initialStatus}
2127
+ **Priority:** ${input.priority}
2128
+ **Assigned by:** ${input.assignedBy}
2129
+ **Assigned to:** ${input.assignedTo}
2130
+ **Project:** ${input.projectName}
2131
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2132
+ **Parent task:** ${parentTaskId}` : ""}
2133
+ **Reviewer:** ${reviewer}
2134
+
2135
+ ## Context
2136
+
2137
+ ${input.context}
2138
+
2139
+ ## MANDATORY: When done
2140
+
2141
+ You MUST call update_task with status "done" and a result summary when finished.
2142
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2143
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2144
+ `;
2145
+ await writeFile3(mdPath, mdContent, "utf-8");
2146
+ } catch (err) {
2147
+ process.stderr.write(
2148
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
2149
+ `
2150
+ );
2151
+ }
2152
+ }
1941
2153
  return {
1942
2154
  id,
1943
2155
  title: input.title,
@@ -2130,7 +2342,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2130
2342
  return { row, taskFile, now, taskId };
2131
2343
  }
2132
2344
  }
2133
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2345
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
2134
2346
  process.stderr.write(
2135
2347
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2136
2348
  `
@@ -2195,9 +2407,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2195
2407
  return { taskFile, assignedTo, assignedBy, taskSlug };
2196
2408
  }
2197
2409
  async function ensureArchitectureDoc(baseDir, projectName) {
2198
- const archPath = path8.join(baseDir, "exe", "ARCHITECTURE.md");
2410
+ const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
2199
2411
  try {
2200
- if (existsSync8(archPath)) return;
2412
+ if (existsSync9(archPath)) return;
2201
2413
  const template = [
2202
2414
  `# ${projectName} \u2014 System Architecture`,
2203
2415
  "",
@@ -2230,10 +2442,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2230
2442
  }
2231
2443
  }
2232
2444
  async function ensureGitignoreExe(baseDir) {
2233
- const gitignorePath = path8.join(baseDir, ".gitignore");
2445
+ const gitignorePath = path9.join(baseDir, ".gitignore");
2234
2446
  try {
2235
- if (existsSync8(gitignorePath)) {
2236
- const content = readFileSync8(gitignorePath, "utf-8");
2447
+ if (existsSync9(gitignorePath)) {
2448
+ const content = readFileSync9(gitignorePath, "utf-8");
2237
2449
  if (/^\/?exe\/?$/m.test(content)) return;
2238
2450
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2239
2451
  } else {
@@ -2242,20 +2454,30 @@ async function ensureGitignoreExe(baseDir) {
2242
2454
  } catch {
2243
2455
  }
2244
2456
  }
2245
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2457
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2246
2458
  var init_tasks_crud = __esm({
2247
2459
  "src/lib/tasks-crud.ts"() {
2248
2460
  "use strict";
2249
2461
  init_database();
2250
2462
  init_task_scope();
2463
+ init_employees();
2464
+ LANE_KEYWORDS = {
2465
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
2466
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
2467
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
2468
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
2469
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
2470
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
2471
+ };
2472
+ KEYWORD_INDEX = buildKeywordIndex();
2251
2473
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2252
2474
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2253
2475
  }
2254
2476
  });
2255
2477
 
2256
2478
  // src/lib/tasks-review.ts
2257
- import path9 from "path";
2258
- import { existsSync as existsSync9, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
2479
+ import path10 from "path";
2480
+ import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
2259
2481
  async function countPendingReviews(sessionScope) {
2260
2482
  const client = getClient();
2261
2483
  if (sessionScope) {
@@ -2277,7 +2499,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2277
2499
  const result3 = await client.execute({
2278
2500
  sql: `SELECT COUNT(*) as cnt FROM tasks
2279
2501
  WHERE status = 'needs_review' AND updated_at > ?
2280
- AND (session_scope = ? OR session_scope IS NULL)`,
2502
+ AND session_scope = ?`,
2281
2503
  args: [sinceIso, sessionScope]
2282
2504
  });
2283
2505
  return Number(result3.rows[0]?.cnt) || 0;
@@ -2295,7 +2517,7 @@ async function listPendingReviews(limit, sessionScope) {
2295
2517
  const result3 = await client.execute({
2296
2518
  sql: `SELECT title, assigned_to, project_name FROM tasks
2297
2519
  WHERE status = 'needs_review'
2298
- AND (session_scope = ? OR session_scope IS NULL)
2520
+ AND session_scope = ?
2299
2521
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
2300
2522
  args: [sessionScope, limit]
2301
2523
  });
@@ -2416,14 +2638,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2416
2638
  if (parts.length >= 3 && parts[0] === "review") {
2417
2639
  const agent = parts[1];
2418
2640
  const slug = parts.slice(2).join("-");
2419
- const originalTaskFile = `exe/${agent}/${slug}.md`;
2641
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
2420
2642
  const result2 = await client.execute({
2421
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2422
- args: [now, originalTaskFile]
2643
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
2644
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
2423
2645
  });
2424
2646
  if (result2.rowsAffected > 0) {
2425
2647
  process.stderr.write(
2426
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
2648
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
2427
2649
  `
2428
2650
  );
2429
2651
  }
@@ -2436,11 +2658,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2436
2658
  );
2437
2659
  }
2438
2660
  try {
2439
- const cacheDir = path9.join(EXE_AI_DIR, "session-cache");
2440
- if (existsSync9(cacheDir)) {
2661
+ const cacheDir = path10.join(EXE_AI_DIR, "session-cache");
2662
+ if (existsSync10(cacheDir)) {
2441
2663
  for (const f of readdirSync2(cacheDir)) {
2442
2664
  if (f.startsWith("review-notified-")) {
2443
- unlinkSync3(path9.join(cacheDir, f));
2665
+ unlinkSync3(path10.join(cacheDir, f));
2444
2666
  }
2445
2667
  }
2446
2668
  }
@@ -2461,7 +2683,7 @@ var init_tasks_review = __esm({
2461
2683
  });
2462
2684
 
2463
2685
  // src/lib/tasks-chain.ts
2464
- import path10 from "path";
2686
+ import path11 from "path";
2465
2687
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2466
2688
  async function cascadeUnblock(taskId, baseDir, now) {
2467
2689
  const client = getClient();
@@ -2478,7 +2700,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2478
2700
  });
2479
2701
  for (const ur of unblockedRows.rows) {
2480
2702
  try {
2481
- const ubFile = path10.join(baseDir, String(ur.task_file));
2703
+ const ubFile = path11.join(baseDir, String(ur.task_file));
2482
2704
  let ubContent = await readFile3(ubFile, "utf-8");
2483
2705
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2484
2706
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2547,7 +2769,7 @@ var init_tasks_chain = __esm({
2547
2769
 
2548
2770
  // src/lib/project-name.ts
2549
2771
  import { execSync as execSync5 } from "child_process";
2550
- import path11 from "path";
2772
+ import path12 from "path";
2551
2773
  function getProjectName(cwd) {
2552
2774
  const dir = cwd ?? process.cwd();
2553
2775
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2560,7 +2782,7 @@ function getProjectName(cwd) {
2560
2782
  timeout: 2e3,
2561
2783
  stdio: ["pipe", "pipe", "pipe"]
2562
2784
  }).trim();
2563
- repoRoot = path11.dirname(gitCommonDir);
2785
+ repoRoot = path12.dirname(gitCommonDir);
2564
2786
  } catch {
2565
2787
  repoRoot = execSync5("git rev-parse --show-toplevel", {
2566
2788
  cwd: dir,
@@ -2569,11 +2791,11 @@ function getProjectName(cwd) {
2569
2791
  stdio: ["pipe", "pipe", "pipe"]
2570
2792
  }).trim();
2571
2793
  }
2572
- _cached2 = path11.basename(repoRoot);
2794
+ _cached2 = path12.basename(repoRoot);
2573
2795
  _cachedCwd = dir;
2574
2796
  return _cached2;
2575
2797
  } catch {
2576
- _cached2 = path11.basename(dir);
2798
+ _cached2 = path12.basename(dir);
2577
2799
  _cachedCwd = dir;
2578
2800
  return _cached2;
2579
2801
  }
@@ -2605,7 +2827,7 @@ function findSessionForProject(projectName) {
2605
2827
  const sessions = listSessions();
2606
2828
  for (const s of sessions) {
2607
2829
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2608
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2830
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2609
2831
  }
2610
2832
  return null;
2611
2833
  }
@@ -2651,7 +2873,7 @@ var init_session_scope = __esm({
2651
2873
 
2652
2874
  // src/lib/tasks-notify.ts
2653
2875
  async function dispatchTaskToEmployee(input) {
2654
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2876
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2655
2877
  let crossProject = false;
2656
2878
  if (input.projectName) {
2657
2879
  try {
@@ -3046,8 +3268,8 @@ __export(tasks_exports, {
3046
3268
  updateTaskStatus: () => updateTaskStatus,
3047
3269
  writeCheckpoint: () => writeCheckpoint
3048
3270
  });
3049
- import path12 from "path";
3050
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4 } from "fs";
3271
+ import path13 from "path";
3272
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
3051
3273
  async function createTask(input) {
3052
3274
  const result2 = await createTaskCore(input);
3053
3275
  if (!input.skipDispatch && result2.status !== "blocked" && !process.env.VITEST) {
@@ -3066,11 +3288,11 @@ async function updateTask(input) {
3066
3288
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
3067
3289
  try {
3068
3290
  const agent = String(row.assigned_to);
3069
- const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3070
- const cachePath = path12.join(cacheDir, `current-task-${agent}.json`);
3291
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3292
+ const cachePath = path13.join(cacheDir, `current-task-${agent}.json`);
3071
3293
  if (input.status === "in_progress") {
3072
- mkdirSync4(cacheDir, { recursive: true });
3073
- writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3294
+ mkdirSync5(cacheDir, { recursive: true });
3295
+ writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3074
3296
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3075
3297
  try {
3076
3298
  unlinkSync4(cachePath);
@@ -3130,7 +3352,7 @@ async function updateTask(input) {
3130
3352
  }
3131
3353
  const isTerminal = input.status === "done" || input.status === "needs_review";
3132
3354
  if (isTerminal) {
3133
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3355
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3134
3356
  if (!isCoordinator) {
3135
3357
  notifyTaskDone();
3136
3358
  }
@@ -3155,7 +3377,7 @@ async function updateTask(input) {
3155
3377
  }
3156
3378
  }
3157
3379
  }
3158
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3380
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3159
3381
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3160
3382
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3161
3383
  taskId,
@@ -3171,7 +3393,7 @@ async function updateTask(input) {
3171
3393
  });
3172
3394
  }
3173
3395
  let nextTask;
3174
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3396
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3175
3397
  try {
3176
3398
  nextTask = await findNextTask(String(row.assigned_to));
3177
3399
  } catch {
@@ -3537,13 +3759,13 @@ __export(tmux_routing_exports, {
3537
3759
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
3538
3760
  });
3539
3761
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
3540
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync } from "fs";
3541
- import path13 from "path";
3542
- import os6 from "os";
3762
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync } from "fs";
3763
+ import path14 from "path";
3764
+ import os7 from "os";
3543
3765
  import { fileURLToPath } from "url";
3544
3766
  import { unlinkSync as unlinkSync5 } from "fs";
3545
3767
  function spawnLockPath(sessionName) {
3546
- return path13.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3768
+ return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3547
3769
  }
3548
3770
  function isProcessAlive(pid) {
3549
3771
  try {
@@ -3554,13 +3776,13 @@ function isProcessAlive(pid) {
3554
3776
  }
3555
3777
  }
3556
3778
  function acquireSpawnLock(sessionName) {
3557
- if (!existsSync10(SPAWN_LOCK_DIR)) {
3558
- mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
3779
+ if (!existsSync11(SPAWN_LOCK_DIR)) {
3780
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3559
3781
  }
3560
3782
  const lockFile = spawnLockPath(sessionName);
3561
- if (existsSync10(lockFile)) {
3783
+ if (existsSync11(lockFile)) {
3562
3784
  try {
3563
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
3785
+ const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
3564
3786
  const age = Date.now() - lock.timestamp;
3565
3787
  if (isProcessAlive(lock.pid) && age < 6e4) {
3566
3788
  return false;
@@ -3568,7 +3790,7 @@ function acquireSpawnLock(sessionName) {
3568
3790
  } catch {
3569
3791
  }
3570
3792
  }
3571
- writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3793
+ writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3572
3794
  return true;
3573
3795
  }
3574
3796
  function releaseSpawnLock(sessionName) {
@@ -3580,13 +3802,13 @@ function releaseSpawnLock(sessionName) {
3580
3802
  function resolveBehaviorsExporterScript() {
3581
3803
  try {
3582
3804
  const thisFile = fileURLToPath(import.meta.url);
3583
- const scriptPath = path13.join(
3584
- path13.dirname(thisFile),
3805
+ const scriptPath = path14.join(
3806
+ path14.dirname(thisFile),
3585
3807
  "..",
3586
3808
  "bin",
3587
3809
  "exe-export-behaviors.js"
3588
3810
  );
3589
- return existsSync10(scriptPath) ? scriptPath : null;
3811
+ return existsSync11(scriptPath) ? scriptPath : null;
3590
3812
  } catch {
3591
3813
  return null;
3592
3814
  }
@@ -3652,12 +3874,12 @@ function extractRootExe(name) {
3652
3874
  return parts.length > 0 ? parts[parts.length - 1] : null;
3653
3875
  }
3654
3876
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3655
- if (!existsSync10(SESSION_CACHE)) {
3656
- mkdirSync5(SESSION_CACHE, { recursive: true });
3877
+ if (!existsSync11(SESSION_CACHE)) {
3878
+ mkdirSync6(SESSION_CACHE, { recursive: true });
3657
3879
  }
3658
3880
  const rootExe = extractRootExe(parentExe) ?? parentExe;
3659
- const filePath = path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3660
- writeFileSync6(filePath, JSON.stringify({
3881
+ const filePath = path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3882
+ writeFileSync7(filePath, JSON.stringify({
3661
3883
  parentExe: rootExe,
3662
3884
  dispatchedBy: dispatchedBy || rootExe,
3663
3885
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -3665,7 +3887,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3665
3887
  }
3666
3888
  function getParentExe(sessionKey) {
3667
3889
  try {
3668
- const data = JSON.parse(readFileSync9(path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3890
+ const data = JSON.parse(readFileSync10(path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3669
3891
  return data.parentExe || null;
3670
3892
  } catch {
3671
3893
  return null;
@@ -3673,8 +3895,8 @@ function getParentExe(sessionKey) {
3673
3895
  }
3674
3896
  function getDispatchedBy(sessionKey) {
3675
3897
  try {
3676
- const data = JSON.parse(readFileSync9(
3677
- path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3898
+ const data = JSON.parse(readFileSync10(
3899
+ path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3678
3900
  "utf8"
3679
3901
  ));
3680
3902
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -3735,32 +3957,50 @@ async function verifyPaneAtCapacity(sessionName) {
3735
3957
  }
3736
3958
  function readDebounceState() {
3737
3959
  try {
3738
- if (!existsSync10(DEBOUNCE_FILE)) return {};
3739
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
3960
+ if (!existsSync11(DEBOUNCE_FILE)) return {};
3961
+ const raw = JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
3962
+ const state = {};
3963
+ for (const [key, val] of Object.entries(raw)) {
3964
+ if (typeof val === "number") {
3965
+ state[key] = { lastSent: val, pending: 0 };
3966
+ } else if (val && typeof val === "object" && "lastSent" in val) {
3967
+ state[key] = val;
3968
+ }
3969
+ }
3970
+ return state;
3740
3971
  } catch {
3741
3972
  return {};
3742
3973
  }
3743
3974
  }
3744
3975
  function writeDebounceState(state) {
3745
3976
  try {
3746
- if (!existsSync10(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
3747
- writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
3977
+ if (!existsSync11(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3978
+ writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
3748
3979
  } catch {
3749
3980
  }
3750
3981
  }
3751
3982
  function isDebounced(targetSession) {
3752
3983
  const state = readDebounceState();
3753
- const lastSent = state[targetSession] ?? 0;
3754
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
3984
+ const entry = state[targetSession];
3985
+ const lastSent = entry?.lastSent ?? 0;
3986
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
3987
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
3988
+ state[targetSession].pending++;
3989
+ writeDebounceState(state);
3990
+ return true;
3991
+ }
3992
+ return false;
3755
3993
  }
3756
3994
  function recordDebounce(targetSession) {
3757
3995
  const state = readDebounceState();
3758
- state[targetSession] = Date.now();
3996
+ const batched = state[targetSession]?.pending ?? 0;
3997
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
3759
3998
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
3760
3999
  for (const key of Object.keys(state)) {
3761
- if ((state[key] ?? 0) < cutoff) delete state[key];
4000
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
3762
4001
  }
3763
4002
  writeDebounceState(state);
4003
+ return batched;
3764
4004
  }
3765
4005
  function logIntercom(msg) {
3766
4006
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -3805,7 +4045,7 @@ function sendIntercom(targetSession) {
3805
4045
  return "skipped_exe";
3806
4046
  }
3807
4047
  if (isDebounced(targetSession)) {
3808
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4048
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
3809
4049
  return "debounced";
3810
4050
  }
3811
4051
  try {
@@ -3817,14 +4057,14 @@ function sendIntercom(targetSession) {
3817
4057
  const sessionState = getSessionState(targetSession);
3818
4058
  if (sessionState === "no_claude") {
3819
4059
  queueIntercom(targetSession, "claude not running in session");
3820
- recordDebounce(targetSession);
3821
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4060
+ const batched2 = recordDebounce(targetSession);
4061
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
3822
4062
  return "queued";
3823
4063
  }
3824
4064
  if (sessionState === "thinking" || sessionState === "tool") {
3825
4065
  queueIntercom(targetSession, "session busy at send time");
3826
- recordDebounce(targetSession);
3827
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4066
+ const batched2 = recordDebounce(targetSession);
4067
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
3828
4068
  return "queued";
3829
4069
  }
3830
4070
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -3832,8 +4072,8 @@ function sendIntercom(targetSession) {
3832
4072
  transport.sendKeys(targetSession, "q");
3833
4073
  }
3834
4074
  transport.sendKeys(targetSession, "/exe-intercom");
3835
- recordDebounce(targetSession);
3836
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4075
+ const batched = recordDebounce(targetSession);
4076
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
3837
4077
  return "delivered";
3838
4078
  } catch {
3839
4079
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -3863,7 +4103,7 @@ function notifyParentExe(sessionKey) {
3863
4103
  return true;
3864
4104
  }
3865
4105
  function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
3866
- if (employeeName2 === "exe" || isCoordinatorName(employeeName2)) {
4106
+ if (isCoordinatorName(employeeName2)) {
3867
4107
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3868
4108
  }
3869
4109
  try {
@@ -3935,26 +4175,26 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
3935
4175
  const transport = getTransport();
3936
4176
  const sessionName = employeeSessionName(employeeName2, exeSession2, opts?.instance);
3937
4177
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName2}${opts.instance}` : employeeName2;
3938
- const logDir = path13.join(os6.homedir(), ".exe-os", "session-logs");
3939
- const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3940
- if (!existsSync10(logDir)) {
3941
- mkdirSync5(logDir, { recursive: true });
4178
+ const logDir = path14.join(os7.homedir(), ".exe-os", "session-logs");
4179
+ const logFile = path14.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4180
+ if (!existsSync11(logDir)) {
4181
+ mkdirSync6(logDir, { recursive: true });
3942
4182
  }
3943
4183
  transport.kill(sessionName);
3944
4184
  let cleanupSuffix = "";
3945
4185
  try {
3946
4186
  const thisFile = fileURLToPath(import.meta.url);
3947
- const cleanupScript = path13.join(path13.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3948
- if (existsSync10(cleanupScript)) {
4187
+ const cleanupScript = path14.join(path14.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4188
+ if (existsSync11(cleanupScript)) {
3949
4189
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName2}" "${exeSession2}"`;
3950
4190
  }
3951
4191
  } catch {
3952
4192
  }
3953
4193
  try {
3954
- const claudeJsonPath = path13.join(os6.homedir(), ".claude.json");
4194
+ const claudeJsonPath = path14.join(os7.homedir(), ".claude.json");
3955
4195
  let claudeJson = {};
3956
4196
  try {
3957
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
4197
+ claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
3958
4198
  } catch {
3959
4199
  }
3960
4200
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -3962,17 +4202,17 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
3962
4202
  const trustDir = opts?.cwd ?? projectDir2;
3963
4203
  if (!projects[trustDir]) projects[trustDir] = {};
3964
4204
  projects[trustDir].hasTrustDialogAccepted = true;
3965
- writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4205
+ writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3966
4206
  } catch {
3967
4207
  }
3968
4208
  try {
3969
- const settingsDir = path13.join(os6.homedir(), ".claude", "projects");
4209
+ const settingsDir = path14.join(os7.homedir(), ".claude", "projects");
3970
4210
  const normalizedKey = (opts?.cwd ?? projectDir2).replace(/\//g, "-").replace(/^-/, "");
3971
- const projSettingsDir = path13.join(settingsDir, normalizedKey);
3972
- const settingsPath = path13.join(projSettingsDir, "settings.json");
4211
+ const projSettingsDir = path14.join(settingsDir, normalizedKey);
4212
+ const settingsPath = path14.join(projSettingsDir, "settings.json");
3973
4213
  let settings = {};
3974
4214
  try {
3975
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
4215
+ settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
3976
4216
  } catch {
3977
4217
  }
3978
4218
  const perms = settings.permissions ?? {};
@@ -4000,21 +4240,24 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4000
4240
  if (changed) {
4001
4241
  perms.allow = allow;
4002
4242
  settings.permissions = perms;
4003
- mkdirSync5(projSettingsDir, { recursive: true });
4004
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4243
+ mkdirSync6(projSettingsDir, { recursive: true });
4244
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4005
4245
  }
4006
4246
  } catch {
4007
4247
  }
4008
4248
  const spawnCwd = opts?.cwd ?? projectDir2;
4009
4249
  const useExeAgent = !!(opts?.model && opts?.provider);
4010
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
4250
+ const agentRtConfig = getAgentRuntime(employeeName2);
4251
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
4252
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
4253
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
4011
4254
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4012
4255
  let identityFlag = "";
4013
4256
  let behaviorsFlag = "";
4014
4257
  let legacyFallbackWarned = false;
4015
4258
  if (!useExeAgent && !useBinSymlink) {
4016
- const identityPath = path13.join(
4017
- os6.homedir(),
4259
+ const identityPath = path14.join(
4260
+ os7.homedir(),
4018
4261
  ".exe-os",
4019
4262
  "identity",
4020
4263
  `${employeeName2}.md`
@@ -4023,13 +4266,13 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4023
4266
  const hasAgentFlag = claudeSupportsAgentFlag();
4024
4267
  if (hasAgentFlag) {
4025
4268
  identityFlag = ` --agent ${employeeName2}`;
4026
- } else if (existsSync10(identityPath)) {
4269
+ } else if (existsSync11(identityPath)) {
4027
4270
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
4028
4271
  legacyFallbackWarned = true;
4029
4272
  }
4030
4273
  const behaviorsFile = exportBehaviorsSync(
4031
4274
  employeeName2,
4032
- path13.basename(spawnCwd),
4275
+ path14.basename(spawnCwd),
4033
4276
  sessionName
4034
4277
  );
4035
4278
  if (behaviorsFile) {
@@ -4044,16 +4287,16 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4044
4287
  }
4045
4288
  let sessionContextFlag = "";
4046
4289
  try {
4047
- const ctxDir = path13.join(os6.homedir(), ".exe-os", "session-cache");
4048
- mkdirSync5(ctxDir, { recursive: true });
4049
- const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
4290
+ const ctxDir = path14.join(os7.homedir(), ".exe-os", "session-cache");
4291
+ mkdirSync6(ctxDir, { recursive: true });
4292
+ const ctxFile = path14.join(ctxDir, `session-context-${sessionName}.md`);
4050
4293
  const ctxContent = [
4051
4294
  `## Session Context`,
4052
4295
  `You are running in tmux session: ${sessionName}.`,
4053
4296
  `Your parent coordinator session is ${exeSession2}.`,
4054
4297
  `Your employees (if any) use the -${exeSession2} suffix.`
4055
4298
  ].join("\n");
4056
- writeFileSync6(ctxFile, ctxContent);
4299
+ writeFileSync7(ctxFile, ctxContent);
4057
4300
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
4058
4301
  } catch {
4059
4302
  }
@@ -4067,9 +4310,48 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4067
4310
  }
4068
4311
  }
4069
4312
  }
4313
+ if (useCodex) {
4314
+ const codexCfg = RUNTIME_TABLE.codex;
4315
+ if (codexCfg?.apiKeyEnv) {
4316
+ const keyVal = process.env[codexCfg.apiKeyEnv];
4317
+ if (keyVal) {
4318
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
4319
+ }
4320
+ }
4321
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
4322
+ }
4323
+ if (useOpencode) {
4324
+ const ocCfg = PROVIDER_TABLE.opencode;
4325
+ if (ocCfg?.apiKeyEnv) {
4326
+ const keyVal = process.env[ocCfg.apiKeyEnv];
4327
+ if (keyVal) {
4328
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
4329
+ }
4330
+ }
4331
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
4332
+ }
4333
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
4334
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
4335
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
4336
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
4337
+ }
4338
+ }
4070
4339
  let spawnCommand;
4071
4340
  if (useExeAgent) {
4072
4341
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName2} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4342
+ } else if (useCodex) {
4343
+ process.stderr.write(
4344
+ `[tmux-routing] agent-config: ${employeeName2} \u2192 codex (${agentRtConfig.model})
4345
+ `
4346
+ );
4347
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName2}${cleanupSuffix}`;
4348
+ } else if (useOpencode) {
4349
+ const binName = `${employeeName2}-opencode`;
4350
+ process.stderr.write(
4351
+ `[tmux-routing] agent-config: ${employeeName2} \u2192 opencode (${agentRtConfig.model})
4352
+ `
4353
+ );
4354
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
4073
4355
  } else if (useBinSymlink) {
4074
4356
  const binName = `${employeeName2}-${ccProvider}`;
4075
4357
  process.stderr.write(
@@ -4091,11 +4373,13 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4091
4373
  transport.pipeLog(sessionName, logFile);
4092
4374
  try {
4093
4375
  const mySession = getMySession();
4094
- const dispatchInfo = path13.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4095
- writeFileSync6(dispatchInfo, JSON.stringify({
4376
+ const dispatchInfo = path14.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4377
+ writeFileSync7(dispatchInfo, JSON.stringify({
4096
4378
  dispatchedBy: mySession,
4097
4379
  rootExe: exeSession2,
4098
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
4380
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
4381
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
4382
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
4099
4383
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4100
4384
  }));
4101
4385
  } catch {
@@ -4113,6 +4397,11 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4113
4397
  booted = true;
4114
4398
  break;
4115
4399
  }
4400
+ } else if (useCodex) {
4401
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
4402
+ booted = true;
4403
+ break;
4404
+ }
4116
4405
  } else {
4117
4406
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
4118
4407
  booted = true;
@@ -4124,9 +4413,10 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4124
4413
  }
4125
4414
  if (!booted) {
4126
4415
  releaseSpawnLock(sessionName);
4127
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4416
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
4417
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
4128
4418
  }
4129
- if (!useExeAgent) {
4419
+ if (!useExeAgent && !useCodex) {
4130
4420
  try {
4131
4421
  transport.sendKeys(sessionName, `/exe-call ${employeeName2}`);
4132
4422
  } catch {
@@ -4153,17 +4443,19 @@ var init_tmux_routing = __esm({
4153
4443
  init_cc_agent_support();
4154
4444
  init_mcp_prefix();
4155
4445
  init_provider_table();
4446
+ init_agent_config();
4447
+ init_runtime_table();
4156
4448
  init_intercom_queue();
4157
4449
  init_plan_limits();
4158
4450
  init_employees();
4159
- SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
4160
- SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
4451
+ SPAWN_LOCK_DIR = path14.join(os7.homedir(), ".exe-os", "spawn-locks");
4452
+ SESSION_CACHE = path14.join(os7.homedir(), ".exe-os", "session-cache");
4161
4453
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4162
4454
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4163
4455
  VERIFY_PANE_LINES = 200;
4164
4456
  INTERCOM_DEBOUNCE_MS = 3e4;
4165
- INTERCOM_LOG2 = path13.join(os6.homedir(), ".exe-os", "intercom.log");
4166
- DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
4457
+ INTERCOM_LOG2 = path14.join(os7.homedir(), ".exe-os", "intercom.log");
4458
+ DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
4167
4459
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4168
4460
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4169
4461
  }
@@ -4182,13 +4474,13 @@ __export(shard_manager_exports, {
4182
4474
  listShards: () => listShards,
4183
4475
  shardExists: () => shardExists
4184
4476
  });
4185
- import path15 from "path";
4186
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync3 } from "fs";
4477
+ import path16 from "path";
4478
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readdirSync as readdirSync3 } from "fs";
4187
4479
  import { createClient as createClient2 } from "@libsql/client";
4188
4480
  function initShardManager(encryptionKey) {
4189
4481
  _encryptionKey = encryptionKey;
4190
- if (!existsSync12(SHARDS_DIR)) {
4191
- mkdirSync6(SHARDS_DIR, { recursive: true });
4482
+ if (!existsSync13(SHARDS_DIR)) {
4483
+ mkdirSync7(SHARDS_DIR, { recursive: true });
4192
4484
  }
4193
4485
  _shardingEnabled = true;
4194
4486
  }
@@ -4208,7 +4500,7 @@ function getShardClient(projectName) {
4208
4500
  }
4209
4501
  const cached = _shards.get(safeName);
4210
4502
  if (cached) return cached;
4211
- const dbPath = path15.join(SHARDS_DIR, `${safeName}.db`);
4503
+ const dbPath = path16.join(SHARDS_DIR, `${safeName}.db`);
4212
4504
  const client = createClient2({
4213
4505
  url: `file:${dbPath}`,
4214
4506
  encryptionKey: _encryptionKey
@@ -4218,10 +4510,10 @@ function getShardClient(projectName) {
4218
4510
  }
4219
4511
  function shardExists(projectName) {
4220
4512
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
4221
- return existsSync12(path15.join(SHARDS_DIR, `${safeName}.db`));
4513
+ return existsSync13(path16.join(SHARDS_DIR, `${safeName}.db`));
4222
4514
  }
4223
4515
  function listShards() {
4224
- if (!existsSync12(SHARDS_DIR)) return [];
4516
+ if (!existsSync13(SHARDS_DIR)) return [];
4225
4517
  return readdirSync3(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
4226
4518
  }
4227
4519
  async function ensureShardSchema(client) {
@@ -4407,7 +4699,7 @@ var init_shard_manager = __esm({
4407
4699
  "src/lib/shard-manager.ts"() {
4408
4700
  "use strict";
4409
4701
  init_config();
4410
- SHARDS_DIR = path15.join(EXE_AI_DIR, "shards");
4702
+ SHARDS_DIR = path16.join(EXE_AI_DIR, "shards");
4411
4703
  _shards = /* @__PURE__ */ new Map();
4412
4704
  _encryptionKey = null;
4413
4705
  _shardingEnabled = false;
@@ -4600,20 +4892,21 @@ init_tmux_routing();
4600
4892
  init_tasks_crud();
4601
4893
 
4602
4894
  // src/lib/store.ts
4895
+ import { createHash } from "crypto";
4603
4896
  init_database();
4604
4897
 
4605
4898
  // src/lib/keychain.ts
4606
4899
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
4607
- import { existsSync as existsSync11 } from "fs";
4608
- import path14 from "path";
4609
- import os7 from "os";
4900
+ import { existsSync as existsSync12 } from "fs";
4901
+ import path15 from "path";
4902
+ import os8 from "os";
4610
4903
  var SERVICE = "exe-mem";
4611
4904
  var ACCOUNT = "master-key";
4612
4905
  function getKeyDir() {
4613
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path14.join(os7.homedir(), ".exe-os");
4906
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(os8.homedir(), ".exe-os");
4614
4907
  }
4615
4908
  function getKeyPath() {
4616
- return path14.join(getKeyDir(), "master.key");
4909
+ return path15.join(getKeyDir(), "master.key");
4617
4910
  }
4618
4911
  async function tryKeytar() {
4619
4912
  try {
@@ -4634,13 +4927,21 @@ async function getMasterKey() {
4634
4927
  }
4635
4928
  }
4636
4929
  const keyPath = getKeyPath();
4637
- if (!existsSync11(keyPath)) {
4930
+ if (!existsSync12(keyPath)) {
4931
+ process.stderr.write(
4932
+ `[keychain] Key not found at ${keyPath} (HOME=${os8.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
4933
+ `
4934
+ );
4638
4935
  return null;
4639
4936
  }
4640
4937
  try {
4641
4938
  const content = await readFile4(keyPath, "utf-8");
4642
4939
  return Buffer.from(content.trim(), "base64");
4643
- } catch {
4940
+ } catch (err) {
4941
+ process.stderr.write(
4942
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
4943
+ `
4944
+ );
4644
4945
  return null;
4645
4946
  }
4646
4947
  }