@askexenow/exe-os 0.8.80 → 0.8.82

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 (110) hide show
  1. package/dist/bin/backfill-conversations.js +359 -267
  2. package/dist/bin/backfill-responses.js +357 -265
  3. package/dist/bin/backfill-vectors.js +339 -264
  4. package/dist/bin/cleanup-stale-review-tasks.js +315 -256
  5. package/dist/bin/cli.js +494 -240
  6. package/dist/bin/exe-agent.js +141 -46
  7. package/dist/bin/exe-assign.js +151 -63
  8. package/dist/bin/exe-boot.js +294 -115
  9. package/dist/bin/exe-call.js +76 -51
  10. package/dist/bin/exe-cloud.js +58 -45
  11. package/dist/bin/exe-dispatch.js +434 -277
  12. package/dist/bin/exe-doctor.js +317 -246
  13. package/dist/bin/exe-export-behaviors.js +328 -248
  14. package/dist/bin/exe-forget.js +314 -231
  15. package/dist/bin/exe-gateway.js +2676 -1402
  16. package/dist/bin/exe-heartbeat.js +329 -264
  17. package/dist/bin/exe-kill.js +324 -244
  18. package/dist/bin/exe-launch-agent.js +574 -463
  19. package/dist/bin/exe-link.js +1055 -95
  20. package/dist/bin/exe-new-employee.js +49 -54
  21. package/dist/bin/exe-pending-messages.js +310 -253
  22. package/dist/bin/exe-pending-notifications.js +299 -228
  23. package/dist/bin/exe-pending-reviews.js +314 -245
  24. package/dist/bin/exe-rename.js +259 -195
  25. package/dist/bin/exe-review.js +140 -64
  26. package/dist/bin/exe-search.js +543 -356
  27. package/dist/bin/exe-session-cleanup.js +463 -382
  28. package/dist/bin/exe-settings.js +129 -99
  29. package/dist/bin/exe-start.sh +6 -6
  30. package/dist/bin/exe-status.js +95 -36
  31. package/dist/bin/exe-team.js +116 -51
  32. package/dist/bin/git-sweep.js +482 -307
  33. package/dist/bin/graph-backfill.js +357 -245
  34. package/dist/bin/graph-export.js +324 -244
  35. package/dist/bin/install.js +33 -10
  36. package/dist/bin/scan-tasks.js +481 -307
  37. package/dist/bin/setup.js +1147 -140
  38. package/dist/bin/shard-migrate.js +321 -241
  39. package/dist/bin/update.js +1 -7
  40. package/dist/bin/wiki-sync.js +318 -238
  41. package/dist/gateway/index.js +2656 -1383
  42. package/dist/hooks/bug-report-worker.js +641 -472
  43. package/dist/hooks/commit-complete.js +482 -307
  44. package/dist/hooks/error-recall.js +363 -135
  45. package/dist/hooks/exe-heartbeat-hook.js +97 -27
  46. package/dist/hooks/ingest-worker.js +584 -397
  47. package/dist/hooks/ingest.js +123 -58
  48. package/dist/hooks/instructions-loaded.js +212 -82
  49. package/dist/hooks/notification.js +200 -70
  50. package/dist/hooks/post-compact.js +199 -81
  51. package/dist/hooks/pre-compact.js +352 -140
  52. package/dist/hooks/pre-tool-use.js +416 -278
  53. package/dist/hooks/prompt-ingest-worker.js +376 -299
  54. package/dist/hooks/prompt-submit.js +414 -188
  55. package/dist/hooks/response-ingest-worker.js +408 -338
  56. package/dist/hooks/session-end.js +209 -83
  57. package/dist/hooks/session-start.js +382 -158
  58. package/dist/hooks/stop.js +209 -83
  59. package/dist/hooks/subagent-stop.js +209 -85
  60. package/dist/hooks/summary-worker.js +606 -510
  61. package/dist/index.js +2133 -855
  62. package/dist/lib/cloud-sync.js +1175 -184
  63. package/dist/lib/config.js +1 -9
  64. package/dist/lib/consolidation.js +71 -34
  65. package/dist/lib/database.js +166 -14
  66. package/dist/lib/device-registry.js +189 -117
  67. package/dist/lib/embedder.js +6 -10
  68. package/dist/lib/employee-templates.js +134 -39
  69. package/dist/lib/employees.js +30 -7
  70. package/dist/lib/exe-daemon-client.js +5 -7
  71. package/dist/lib/exe-daemon.js +514 -152
  72. package/dist/lib/hybrid-search.js +543 -356
  73. package/dist/lib/identity-templates.js +15 -15
  74. package/dist/lib/identity.js +19 -15
  75. package/dist/lib/license.js +1 -7
  76. package/dist/lib/messaging.js +157 -135
  77. package/dist/lib/reminders.js +97 -0
  78. package/dist/lib/schedules.js +302 -231
  79. package/dist/lib/skill-learning.js +33 -27
  80. package/dist/lib/status-brief.js +11 -14
  81. package/dist/lib/store.js +326 -237
  82. package/dist/lib/task-router.js +105 -1
  83. package/dist/lib/tasks.js +233 -116
  84. package/dist/lib/tmux-routing.js +173 -56
  85. package/dist/lib/ws-client.js +13 -3
  86. package/dist/mcp/server.js +2009 -1015
  87. package/dist/mcp/tools/complete-reminder.js +97 -0
  88. package/dist/mcp/tools/create-reminder.js +97 -0
  89. package/dist/mcp/tools/create-task.js +426 -262
  90. package/dist/mcp/tools/deactivate-behavior.js +119 -44
  91. package/dist/mcp/tools/list-reminders.js +97 -0
  92. package/dist/mcp/tools/list-tasks.js +56 -57
  93. package/dist/mcp/tools/send-message.js +206 -143
  94. package/dist/mcp/tools/update-task.js +259 -85
  95. package/dist/runtime/index.js +495 -316
  96. package/dist/tui/App.js +1128 -919
  97. package/package.json +2 -10
  98. package/src/commands/exe/afk.md +8 -8
  99. package/src/commands/exe/assign.md +1 -1
  100. package/src/commands/exe/build-adv.md +1 -1
  101. package/src/commands/exe/call.md +10 -10
  102. package/src/commands/exe/employee-heartbeat.md +9 -6
  103. package/src/commands/exe/heartbeat.md +5 -5
  104. package/src/commands/exe/intercom.md +26 -15
  105. package/src/commands/exe/launch.md +2 -2
  106. package/src/commands/exe/new-employee.md +1 -1
  107. package/src/commands/exe/review.md +2 -2
  108. package/src/commands/exe/schedule.md +1 -1
  109. package/src/commands/exe/sessions.md +2 -2
  110. package/src/commands/exe.md +22 -20
@@ -357,7 +357,7 @@ function wrapWithRetry(client) {
357
357
  return (sql) => retryOnBusy(() => target.execute(sql), "execute");
358
358
  }
359
359
  if (prop === "batch") {
360
- return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
360
+ return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
361
361
  }
362
362
  return Reflect.get(target, prop, receiver);
363
363
  }
@@ -373,6 +373,240 @@ var init_db_retry = __esm({
373
373
  }
374
374
  });
375
375
 
376
+ // src/lib/config.ts
377
+ 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";
381
+ function resolveDataDir() {
382
+ if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
383
+ 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)) {
387
+ try {
388
+ renameSync2(legacyDir, newDir);
389
+ process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
390
+ `);
391
+ } catch {
392
+ return legacyDir;
393
+ }
394
+ }
395
+ return newDir;
396
+ }
397
+ function migrateLegacyConfig(raw) {
398
+ if ("r2" in raw) {
399
+ process.stderr.write(
400
+ "[exe-os] Warning: config.json contains deprecated 'r2' field from v1.0. R2 sync has been replaced in v1.1. The 'r2' field will be ignored.\n"
401
+ );
402
+ delete raw.r2;
403
+ }
404
+ if ("syncIntervalMs" in raw) {
405
+ delete raw.syncIntervalMs;
406
+ }
407
+ return raw;
408
+ }
409
+ function migrateConfig(raw) {
410
+ const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
411
+ let currentVersion = fromVersion;
412
+ let migrated = false;
413
+ if (currentVersion > CURRENT_CONFIG_VERSION) {
414
+ return { config: raw, migrated: false, fromVersion };
415
+ }
416
+ for (const migration of CONFIG_MIGRATIONS) {
417
+ if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
418
+ raw = migration.migrate(raw);
419
+ currentVersion = migration.to;
420
+ migrated = true;
421
+ }
422
+ }
423
+ return { config: raw, migrated, fromVersion };
424
+ }
425
+ function normalizeScalingRoadmap(raw) {
426
+ const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
427
+ const userRoadmap = raw.scalingRoadmap ?? {};
428
+ const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
429
+ if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
430
+ userAuto.enabled = raw.rerankerEnabled;
431
+ }
432
+ raw.scalingRoadmap = {
433
+ ...userRoadmap,
434
+ rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
435
+ };
436
+ }
437
+ function normalizeSessionLifecycle(raw) {
438
+ const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
439
+ const userSL = raw.sessionLifecycle ?? {};
440
+ raw.sessionLifecycle = { ...defaultSL, ...userSL };
441
+ }
442
+ function normalizeAutoUpdate(raw) {
443
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
444
+ const userAU = raw.autoUpdate ?? {};
445
+ raw.autoUpdate = { ...defaultAU, ...userAU };
446
+ }
447
+ async function loadConfig() {
448
+ const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
449
+ 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") };
453
+ }
454
+ const raw = await readFile(configPath, "utf-8");
455
+ try {
456
+ let parsed = JSON.parse(raw);
457
+ parsed = migrateLegacyConfig(parsed);
458
+ const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
459
+ if (migrated) {
460
+ process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
461
+ `);
462
+ try {
463
+ await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
464
+ } catch {
465
+ }
466
+ }
467
+ normalizeScalingRoadmap(migratedCfg);
468
+ normalizeSessionLifecycle(migratedCfg);
469
+ normalizeAutoUpdate(migratedCfg);
470
+ const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
471
+ if (config.dbPath.startsWith("~")) {
472
+ config.dbPath = config.dbPath.replace(/^~/, os3.homedir());
473
+ }
474
+ return config;
475
+ } catch {
476
+ return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
477
+ }
478
+ }
479
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
480
+ var init_config = __esm({
481
+ "src/lib/config.ts"() {
482
+ "use strict";
483
+ 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");
488
+ CURRENT_CONFIG_VERSION = 1;
489
+ DEFAULT_CONFIG = {
490
+ config_version: CURRENT_CONFIG_VERSION,
491
+ dbPath: DB_PATH,
492
+ modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
493
+ embeddingDim: 1024,
494
+ batchSize: 20,
495
+ flushIntervalMs: 1e4,
496
+ autoIngestion: true,
497
+ autoRetrieval: true,
498
+ searchMode: "hybrid",
499
+ hookSearchMode: "hybrid",
500
+ fileGrepEnabled: true,
501
+ splashEffect: true,
502
+ consolidationEnabled: true,
503
+ consolidationIntervalMs: 6 * 60 * 60 * 1e3,
504
+ consolidationModel: "claude-haiku-4-5-20251001",
505
+ consolidationMaxCallsPerRun: 20,
506
+ selfQueryRouter: true,
507
+ selfQueryModel: "claude-haiku-4-5-20251001",
508
+ rerankerEnabled: true,
509
+ scalingRoadmap: {
510
+ rerankerAutoTrigger: {
511
+ enabled: true,
512
+ broadQueryMinCardinality: 5e4,
513
+ fetchTopK: 150,
514
+ returnTopK: 5
515
+ }
516
+ },
517
+ graphRagEnabled: true,
518
+ wikiEnabled: false,
519
+ wikiUrl: "",
520
+ wikiApiKey: "",
521
+ wikiSyncIntervalMs: 30 * 60 * 1e3,
522
+ wikiWorkspaceMapping: {},
523
+ wikiAutoUpdate: true,
524
+ wikiAutoUpdateThreshold: 0.5,
525
+ wikiAutoUpdateCreateNew: true,
526
+ skillLearning: true,
527
+ skillThreshold: 3,
528
+ skillModel: "claude-haiku-4-5-20251001",
529
+ exeHeartbeat: {
530
+ enabled: true,
531
+ intervalSeconds: 60,
532
+ staleInProgressThresholdHours: 2
533
+ },
534
+ sessionLifecycle: {
535
+ idleKillEnabled: true,
536
+ idleKillTicksRequired: 3,
537
+ idleKillIntercomAckWindowMs: 1e4,
538
+ maxAutoInstances: 10
539
+ },
540
+ autoUpdate: {
541
+ checkOnBoot: true,
542
+ autoInstall: false,
543
+ checkIntervalMs: 24 * 60 * 60 * 1e3
544
+ }
545
+ };
546
+ CONFIG_MIGRATIONS = [
547
+ {
548
+ from: 0,
549
+ to: 1,
550
+ migrate: (cfg) => {
551
+ cfg.config_version = 1;
552
+ return cfg;
553
+ }
554
+ }
555
+ ];
556
+ }
557
+ });
558
+
559
+ // src/lib/employees.ts
560
+ 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";
562
+ import { execSync as execSync3 } from "child_process";
563
+ import path4 from "path";
564
+ import os4 from "os";
565
+ function normalizeRole(role) {
566
+ return (role ?? "").trim().toLowerCase();
567
+ }
568
+ function isCoordinatorRole(role) {
569
+ return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
570
+ }
571
+ function getCoordinatorEmployee(employees) {
572
+ return employees.find((e) => isCoordinatorRole(e.role));
573
+ }
574
+ function getCoordinatorName(employees = loadEmployeesSync()) {
575
+ return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
576
+ }
577
+ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
578
+ if (!agentName) return false;
579
+ return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
580
+ }
581
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
582
+ if (!existsSync4(employeesPath)) return [];
583
+ try {
584
+ return JSON.parse(readFileSync4(employeesPath, "utf-8"));
585
+ } catch {
586
+ return [];
587
+ }
588
+ }
589
+ function getEmployee(employees, name) {
590
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
591
+ }
592
+ function isMultiInstance(agentName, employees) {
593
+ const roster = employees ?? loadEmployeesSync();
594
+ const emp = getEmployee(roster, agentName);
595
+ if (!emp) return false;
596
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
597
+ }
598
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
599
+ var init_employees = __esm({
600
+ "src/lib/employees.ts"() {
601
+ "use strict";
602
+ init_config();
603
+ EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
604
+ DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
605
+ COORDINATOR_ROLE = "COO";
606
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
607
+ }
608
+ });
609
+
376
610
  // src/lib/database.ts
377
611
  import { createClient } from "@libsql/client";
378
612
  async function initDatabase(config) {
@@ -506,22 +740,24 @@ async function ensureSchema() {
506
740
  ON behaviors(agent_id, active);
507
741
  `);
508
742
  try {
743
+ const coordinatorName = getCoordinatorName();
509
744
  const existing = await client.execute({
510
- sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = 'exe'",
511
- args: []
745
+ sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
746
+ args: [coordinatorName]
512
747
  });
513
748
  if (Number(existing.rows[0]?.cnt) === 0) {
514
- await client.executeMultiple(`
515
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
516
- VALUES
517
- (hex(randomblob(16)), 'exe', NULL, 'workflow', 'Don''t ask "keep going?" \u2014 just keep executing phases/plans autonomously', 1, '2026-03-25T00:00:00Z', '2026-03-25T00:00:00Z');
518
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
519
- VALUES
520
- (hex(randomblob(16)), 'exe', NULL, 'tool-use', 'Always use create_task MCP tool, never write .md files directly for task creation', 1, '2026-03-25T00:00:00Z', '2026-03-25T00:00:00Z');
521
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
522
- VALUES
523
- (hex(randomblob(16)), 'exe', NULL, 'workflow', 'Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission', 1, '2026-03-25T00:00:00Z', '2026-03-25T00:00:00Z');
524
- `);
749
+ const seededAt = "2026-03-25T00:00:00Z";
750
+ for (const [domain, content] of [
751
+ ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
752
+ ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
753
+ ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
754
+ ]) {
755
+ await client.execute({
756
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
757
+ VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
758
+ args: [coordinatorName, domain, content, seededAt, seededAt]
759
+ });
760
+ }
525
761
  }
526
762
  } catch {
527
763
  }
@@ -1213,237 +1449,49 @@ async function ensureSchema() {
1213
1449
  } catch {
1214
1450
  }
1215
1451
  }
1216
- }
1217
- var _client, _resilientClient, initTurso;
1218
- var init_database = __esm({
1219
- "src/lib/database.ts"() {
1220
- "use strict";
1221
- init_db_retry();
1222
- _client = null;
1223
- _resilientClient = null;
1224
- initTurso = initDatabase;
1225
- }
1226
- });
1227
-
1228
- // src/lib/config.ts
1229
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
1230
- import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
1231
- import path3 from "path";
1232
- import os3 from "os";
1233
- function resolveDataDir() {
1234
- if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
1235
- if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
1236
- const newDir = path3.join(os3.homedir(), ".exe-os");
1237
- const legacyDir = path3.join(os3.homedir(), ".exe-mem");
1238
- if (!existsSync3(newDir) && existsSync3(legacyDir)) {
1239
- try {
1240
- renameSync2(legacyDir, newDir);
1241
- process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
1242
- `);
1243
- } catch {
1244
- return legacyDir;
1245
- }
1452
+ try {
1453
+ await client.execute({
1454
+ sql: `ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0`,
1455
+ args: []
1456
+ });
1457
+ } catch {
1246
1458
  }
1247
- return newDir;
1248
- }
1249
- function migrateLegacyConfig(raw) {
1250
- if ("r2" in raw) {
1251
- process.stderr.write(
1252
- "[exe-os] Warning: config.json contains deprecated 'r2' field from v1.0. R2 sync has been replaced in v1.1. The 'r2' field will be ignored.\n"
1459
+ try {
1460
+ await client.execute(
1461
+ `CREATE INDEX IF NOT EXISTS idx_memories_draft ON memories(draft) WHERE draft = 1`
1253
1462
  );
1254
- delete raw.r2;
1255
- }
1256
- if ("syncIntervalMs" in raw) {
1257
- delete raw.syncIntervalMs;
1258
- }
1259
- return raw;
1260
- }
1261
- function migrateConfig(raw) {
1262
- const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
1263
- let currentVersion = fromVersion;
1264
- let migrated = false;
1265
- if (currentVersion > CURRENT_CONFIG_VERSION) {
1266
- return { config: raw, migrated: false, fromVersion };
1267
- }
1268
- for (const migration of CONFIG_MIGRATIONS) {
1269
- if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
1270
- raw = migration.migrate(raw);
1271
- currentVersion = migration.to;
1272
- migrated = true;
1273
- }
1274
- }
1275
- return { config: raw, migrated, fromVersion };
1276
- }
1277
- function normalizeScalingRoadmap(raw) {
1278
- const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
1279
- const userRoadmap = raw.scalingRoadmap ?? {};
1280
- const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
1281
- if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
1282
- userAuto.enabled = raw.rerankerEnabled;
1283
- }
1284
- raw.scalingRoadmap = {
1285
- ...userRoadmap,
1286
- rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
1287
- };
1288
- }
1289
- function normalizeSessionLifecycle(raw) {
1290
- const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
1291
- const userSL = raw.sessionLifecycle ?? {};
1292
- raw.sessionLifecycle = { ...defaultSL, ...userSL };
1293
- }
1294
- function normalizeAutoUpdate(raw) {
1295
- const defaultAU = DEFAULT_CONFIG.autoUpdate;
1296
- const userAU = raw.autoUpdate ?? {};
1297
- raw.autoUpdate = { ...defaultAU, ...userAU };
1298
- }
1299
- async function loadConfig() {
1300
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1301
- await mkdir(dir, { recursive: true });
1302
- const configPath = path3.join(dir, "config.json");
1303
- if (!existsSync3(configPath)) {
1304
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
1463
+ } catch {
1305
1464
  }
1306
- const raw = await readFile(configPath, "utf-8");
1307
1465
  try {
1308
- let parsed = JSON.parse(raw);
1309
- parsed = migrateLegacyConfig(parsed);
1310
- const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
1311
- if (migrated) {
1312
- process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
1313
- `);
1314
- try {
1315
- await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
1316
- } catch {
1317
- }
1318
- }
1319
- normalizeScalingRoadmap(migratedCfg);
1320
- normalizeSessionLifecycle(migratedCfg);
1321
- normalizeAutoUpdate(migratedCfg);
1322
- const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
1323
- if (config.dbPath.startsWith("~")) {
1324
- config.dbPath = config.dbPath.replace(/^~/, os3.homedir());
1325
- }
1326
- return config;
1466
+ await client.execute({
1467
+ sql: `ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'`,
1468
+ args: []
1469
+ });
1327
1470
  } catch {
1328
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
1329
1471
  }
1330
- }
1331
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1332
- var init_config = __esm({
1333
- "src/lib/config.ts"() {
1334
- "use strict";
1335
- EXE_AI_DIR = resolveDataDir();
1336
- DB_PATH = path3.join(EXE_AI_DIR, "memories.db");
1337
- MODELS_DIR = path3.join(EXE_AI_DIR, "models");
1338
- CONFIG_PATH = path3.join(EXE_AI_DIR, "config.json");
1339
- LEGACY_LANCE_PATH = path3.join(EXE_AI_DIR, "local.lance");
1340
- CURRENT_CONFIG_VERSION = 1;
1341
- DEFAULT_CONFIG = {
1342
- config_version: CURRENT_CONFIG_VERSION,
1343
- dbPath: DB_PATH,
1344
- modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
1345
- embeddingDim: 1024,
1346
- batchSize: 20,
1347
- flushIntervalMs: 1e4,
1348
- autoIngestion: true,
1349
- autoRetrieval: true,
1350
- searchMode: "hybrid",
1351
- hookSearchMode: "hybrid",
1352
- fileGrepEnabled: true,
1353
- splashEffect: true,
1354
- consolidationEnabled: true,
1355
- consolidationIntervalMs: 6 * 60 * 60 * 1e3,
1356
- consolidationModel: "claude-haiku-4-5-20251001",
1357
- consolidationMaxCallsPerRun: 20,
1358
- selfQueryRouter: true,
1359
- selfQueryModel: "claude-haiku-4-5-20251001",
1360
- rerankerEnabled: true,
1361
- scalingRoadmap: {
1362
- rerankerAutoTrigger: {
1363
- enabled: true,
1364
- broadQueryMinCardinality: 5e4,
1365
- fetchTopK: 150,
1366
- returnTopK: 5
1367
- }
1368
- },
1369
- graphRagEnabled: true,
1370
- wikiEnabled: false,
1371
- wikiUrl: "",
1372
- wikiApiKey: "",
1373
- wikiSyncIntervalMs: 30 * 60 * 1e3,
1374
- wikiWorkspaceMapping: {
1375
- exe: "Executive",
1376
- yoshi: "Engineering",
1377
- mari: "Marketing",
1378
- tom: "Engineering",
1379
- sasha: "Production"
1380
- },
1381
- wikiAutoUpdate: true,
1382
- wikiAutoUpdateThreshold: 0.5,
1383
- wikiAutoUpdateCreateNew: true,
1384
- skillLearning: true,
1385
- skillThreshold: 3,
1386
- skillModel: "claude-haiku-4-5-20251001",
1387
- exeHeartbeat: {
1388
- enabled: true,
1389
- intervalSeconds: 60,
1390
- staleInProgressThresholdHours: 2
1391
- },
1392
- sessionLifecycle: {
1393
- idleKillEnabled: true,
1394
- idleKillTicksRequired: 3,
1395
- idleKillIntercomAckWindowMs: 1e4,
1396
- maxAutoInstances: 10
1397
- },
1398
- autoUpdate: {
1399
- checkOnBoot: true,
1400
- autoInstall: false,
1401
- checkIntervalMs: 24 * 60 * 60 * 1e3
1402
- }
1403
- };
1404
- CONFIG_MIGRATIONS = [
1405
- {
1406
- from: 0,
1407
- to: 1,
1408
- migrate: (cfg) => {
1409
- cfg.config_version = 1;
1410
- return cfg;
1411
- }
1412
- }
1413
- ];
1472
+ try {
1473
+ await client.execute(
1474
+ `CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
1475
+ );
1476
+ } catch {
1414
1477
  }
1415
- });
1416
-
1417
- // src/lib/employees.ts
1418
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1419
- import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
1420
- import { execSync as execSync3 } from "child_process";
1421
- import path4 from "path";
1422
- import os4 from "os";
1423
- function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
1424
- if (!existsSync4(employeesPath)) return [];
1425
1478
  try {
1426
- return JSON.parse(readFileSync4(employeesPath, "utf-8"));
1479
+ await client.execute({
1480
+ sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
1481
+ args: []
1482
+ });
1427
1483
  } catch {
1428
- return [];
1429
1484
  }
1430
1485
  }
1431
- function getEmployee(employees, name) {
1432
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
1433
- }
1434
- function isMultiInstance(agentName, employees) {
1435
- const roster = employees ?? loadEmployeesSync();
1436
- const emp = getEmployee(roster, agentName);
1437
- if (!emp) return false;
1438
- return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
1439
- }
1440
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
1441
- var init_employees = __esm({
1442
- "src/lib/employees.ts"() {
1486
+ var _client, _resilientClient, initTurso;
1487
+ var init_database = __esm({
1488
+ "src/lib/database.ts"() {
1443
1489
  "use strict";
1444
- init_config();
1445
- EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
1446
- MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
1490
+ init_db_retry();
1491
+ init_employees();
1492
+ _client = null;
1493
+ _resilientClient = null;
1494
+ initTurso = initDatabase;
1447
1495
  }
1448
1496
  });
1449
1497
 
@@ -1958,6 +2006,36 @@ async function listTasks(input) {
1958
2006
  tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
1959
2007
  }));
1960
2008
  }
2009
+ function isTmuxSessionAlive(identifier) {
2010
+ if (!identifier || identifier === "unknown") return true;
2011
+ try {
2012
+ if (identifier.startsWith("%")) {
2013
+ const output = execSync4("tmux list-panes -a -F '#{pane_id}'", {
2014
+ timeout: 2e3,
2015
+ encoding: "utf8",
2016
+ stdio: ["pipe", "pipe", "pipe"]
2017
+ });
2018
+ return output.split("\n").some((l) => l.trim() === identifier);
2019
+ } else {
2020
+ execSync4(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2021
+ timeout: 2e3,
2022
+ stdio: ["pipe", "pipe", "pipe"]
2023
+ });
2024
+ return true;
2025
+ }
2026
+ } catch {
2027
+ if (identifier.startsWith("%")) return true;
2028
+ try {
2029
+ execSync4("tmux list-sessions", {
2030
+ timeout: 2e3,
2031
+ stdio: ["pipe", "pipe", "pipe"]
2032
+ });
2033
+ return false;
2034
+ } catch {
2035
+ return true;
2036
+ }
2037
+ }
2038
+ }
1961
2039
  function checkStaleCompletion(taskContext, taskCreatedAt) {
1962
2040
  if (!taskContext) return null;
1963
2041
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -2020,13 +2098,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2020
2098
  });
2021
2099
  if (claim.rowsAffected === 0) {
2022
2100
  const current = await client.execute({
2023
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
2101
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
2024
2102
  args: [taskId]
2025
2103
  });
2026
2104
  const cur = current.rows[0];
2027
- const status = cur?.status ?? "unknown";
2028
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2029
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
2105
+ const curStatus = cur?.status ?? "unknown";
2106
+ const claimedBySession = cur?.assigned_tmux ?? "";
2107
+ const assignedBy = cur?.assigned_by ?? "";
2108
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
2109
+ process.stderr.write(
2110
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
2111
+ `
2112
+ );
2113
+ await client.execute({
2114
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
2115
+ args: [now, taskId]
2116
+ });
2117
+ const retried = await client.execute({
2118
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
2119
+ args: [tmuxSession, now, taskId]
2120
+ });
2121
+ if (retried.rowsAffected > 0) {
2122
+ try {
2123
+ await writeCheckpoint({
2124
+ taskId,
2125
+ step: "reclaimed_dead_session",
2126
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
2127
+ });
2128
+ } catch {
2129
+ }
2130
+ return { row, taskFile, now, taskId };
2131
+ }
2132
+ }
2133
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2134
+ process.stderr.write(
2135
+ `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2136
+ `
2137
+ );
2138
+ await client.execute({
2139
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
2140
+ args: [tmuxSession, now, taskId]
2141
+ });
2142
+ try {
2143
+ await writeCheckpoint({
2144
+ taskId,
2145
+ step: "assigner_override",
2146
+ contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
2147
+ });
2148
+ } catch {
2149
+ }
2150
+ return { row, taskFile, now, taskId };
2151
+ }
2152
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
2153
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
2030
2154
  }
2031
2155
  try {
2032
2156
  await writeCheckpoint({
@@ -2124,7 +2248,7 @@ var init_tasks_crud = __esm({
2124
2248
  "use strict";
2125
2249
  init_database();
2126
2250
  init_task_scope();
2127
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
2251
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2128
2252
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2129
2253
  }
2130
2254
  });
@@ -2481,7 +2605,7 @@ function findSessionForProject(projectName) {
2481
2605
  const sessions = listSessions();
2482
2606
  for (const s of sessions) {
2483
2607
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2484
- if (proj === projectName && s.agentId === "exe") return s;
2608
+ if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2485
2609
  }
2486
2610
  return null;
2487
2611
  }
@@ -2521,12 +2645,13 @@ var init_session_scope = __esm({
2521
2645
  init_session_registry();
2522
2646
  init_project_name();
2523
2647
  init_tmux_routing();
2648
+ init_employees();
2524
2649
  }
2525
2650
  });
2526
2651
 
2527
2652
  // src/lib/tasks-notify.ts
2528
2653
  async function dispatchTaskToEmployee(input) {
2529
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
2654
+ if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2530
2655
  let crossProject = false;
2531
2656
  if (input.projectName) {
2532
2657
  try {
@@ -2969,6 +3094,24 @@ async function updateTask(input) {
2969
3094
  });
2970
3095
  } catch {
2971
3096
  }
3097
+ const assignedAgent = String(row.assigned_to);
3098
+ if (!isCoordinatorName(assignedAgent)) {
3099
+ try {
3100
+ const draftClient = getClient();
3101
+ if (input.status === "done") {
3102
+ await draftClient.execute({
3103
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3104
+ args: [assignedAgent]
3105
+ });
3106
+ } else if (input.status === "cancelled") {
3107
+ await draftClient.execute({
3108
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
3109
+ args: [assignedAgent]
3110
+ });
3111
+ }
3112
+ } catch {
3113
+ }
3114
+ }
2972
3115
  try {
2973
3116
  const client = getClient();
2974
3117
  const cascaded = await client.execute({
@@ -2987,8 +3130,8 @@ async function updateTask(input) {
2987
3130
  }
2988
3131
  const isTerminal = input.status === "done" || input.status === "needs_review";
2989
3132
  if (isTerminal) {
2990
- const isExe = String(row.assigned_to) === "exe";
2991
- if (!isExe) {
3133
+ const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3134
+ if (!isCoordinator) {
2992
3135
  notifyTaskDone();
2993
3136
  }
2994
3137
  await markTaskNotificationsRead(taskFile);
@@ -3012,7 +3155,7 @@ async function updateTask(input) {
3012
3155
  }
3013
3156
  }
3014
3157
  }
3015
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
3158
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3016
3159
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3017
3160
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3018
3161
  taskId,
@@ -3028,7 +3171,7 @@ async function updateTask(input) {
3028
3171
  });
3029
3172
  }
3030
3173
  let nextTask;
3031
- if (isTerminal && String(row.assigned_to) !== "exe") {
3174
+ if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3032
3175
  try {
3033
3176
  nextTask = await findNextTask(String(row.assigned_to));
3034
3177
  } catch {
@@ -3055,12 +3198,14 @@ async function updateTask(input) {
3055
3198
  async function deleteTask(taskId, baseDir) {
3056
3199
  const client = getClient();
3057
3200
  const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
3058
- const reviewer = assignedBy || "exe";
3201
+ const coordinatorName = getCoordinatorName();
3202
+ const reviewer = assignedBy || coordinatorName;
3059
3203
  const reviewSlug = `review-${assignedTo}-${taskSlug}`;
3060
3204
  const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
3205
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
3061
3206
  await client.execute({
3062
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
3063
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
3207
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
3208
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
3064
3209
  });
3065
3210
  await markAsReadByTaskFile(taskFile);
3066
3211
  await markAsReadByTaskFile(reviewFile);
@@ -3072,6 +3217,7 @@ var init_tasks = __esm({
3072
3217
  init_config();
3073
3218
  init_notifications();
3074
3219
  init_state_bus();
3220
+ init_employees();
3075
3221
  init_tasks_crud();
3076
3222
  init_tasks_review();
3077
3223
  init_tasks_crud();
@@ -3157,7 +3303,7 @@ function _resetLastRelaunchCache() {
3157
3303
  }
3158
3304
  async function lastResumeCreatedAtMs(agentId) {
3159
3305
  const client = getClient();
3160
- const cmScope = sessionScopeFilter();
3306
+ const cmScope = sessionScopeFilter(null);
3161
3307
  const result2 = await client.execute({
3162
3308
  sql: `SELECT MAX(created_at) AS last_created_at
3163
3309
  FROM tasks
@@ -3182,7 +3328,7 @@ async function createOrRefreshResumeTask(agentId, projectDir2, openTasks) {
3182
3328
  const client = getClient();
3183
3329
  const now = (/* @__PURE__ */ new Date()).toISOString();
3184
3330
  const context = buildResumeContext(agentId, openTasks);
3185
- const rdScope = sessionScopeFilter();
3331
+ const rdScope = sessionScopeFilter(null);
3186
3332
  const existing = await client.execute({
3187
3333
  sql: `SELECT id FROM tasks
3188
3334
  WHERE assigned_to = ?
@@ -3216,7 +3362,7 @@ async function pollCapacityDead() {
3216
3362
  const transport = getTransport();
3217
3363
  const relaunched = [];
3218
3364
  const registered = listSessions().filter(
3219
- (s) => s.agentId !== "exe"
3365
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
3220
3366
  );
3221
3367
  if (registered.length === 0) return [];
3222
3368
  let liveSessions;
@@ -3276,7 +3422,7 @@ async function pollCapacityDead() {
3276
3422
  reason: "capacity"
3277
3423
  });
3278
3424
  const client = getClient();
3279
- const rlScope = sessionScopeFilter();
3425
+ const rlScope = sessionScopeFilter(null);
3280
3426
  const openTasks = await client.execute({
3281
3427
  sql: `SELECT id, title, priority, task_file, status
3282
3428
  FROM tasks
@@ -3330,6 +3476,7 @@ var init_capacity_monitor = __esm({
3330
3476
  init_session_kill_telemetry();
3331
3477
  init_tmux_routing();
3332
3478
  init_task_scope();
3479
+ init_employees();
3333
3480
  CAPACITY_PATTERNS = [
3334
3481
  /conversation is too long/i,
3335
3482
  /maximum context length/i,
@@ -3479,7 +3626,7 @@ function employeeSessionName(employee, exeSession2, instance) {
3479
3626
  exeSession2 = root;
3480
3627
  } else {
3481
3628
  throw new Error(
3482
- `Invalid exeSession "${exeSession2}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
3629
+ `Invalid coordinator session "${exeSession2}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3483
3630
  );
3484
3631
  }
3485
3632
  }
@@ -3499,8 +3646,10 @@ function parseParentExe(sessionName, agentId) {
3499
3646
  return match?.[1] ?? null;
3500
3647
  }
3501
3648
  function extractRootExe(name) {
3502
- const match = name.match(/(exe\d+)$/);
3503
- return match?.[1] ?? null;
3649
+ if (!name) return null;
3650
+ if (!name.includes("-")) return name;
3651
+ const parts = name.split("-").filter(Boolean);
3652
+ return parts.length > 0 ? parts[parts.length - 1] : null;
3504
3653
  }
3505
3654
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3506
3655
  if (!existsSync10(SESSION_CACHE)) {
@@ -3645,12 +3794,14 @@ function isSessionBusy(sessionName) {
3645
3794
  return state === "thinking" || state === "tool";
3646
3795
  }
3647
3796
  function isExeSession(sessionName) {
3648
- return /^exe\d*$/.test(sessionName);
3797
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
3798
+ const coordinatorName = getCoordinatorName();
3799
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
3649
3800
  }
3650
3801
  function sendIntercom(targetSession) {
3651
3802
  const transport = getTransport();
3652
3803
  if (isExeSession(targetSession)) {
3653
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
3804
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
3654
3805
  return "skipped_exe";
3655
3806
  }
3656
3807
  if (isDebounced(targetSession)) {
@@ -3702,7 +3853,7 @@ function notifyParentExe(sessionKey) {
3702
3853
  if (result2 === "failed") {
3703
3854
  const rootExe = resolveExeSession();
3704
3855
  if (rootExe && rootExe !== target) {
3705
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3856
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
3706
3857
  `);
3707
3858
  const fallback = sendIntercom(rootExe);
3708
3859
  return fallback !== "failed";
@@ -3712,8 +3863,8 @@ function notifyParentExe(sessionKey) {
3712
3863
  return true;
3713
3864
  }
3714
3865
  function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
3715
- if (employeeName2 === "exe") {
3716
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
3866
+ if (employeeName2 === "exe" || isCoordinatorName(employeeName2)) {
3867
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3717
3868
  }
3718
3869
  try {
3719
3870
  assertEmployeeLimitSync();
@@ -3722,8 +3873,8 @@ function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
3722
3873
  return { status: "failed", sessionName: "", error: err.message };
3723
3874
  }
3724
3875
  }
3725
- if (/-exe\d*$/.test(employeeName2)) {
3726
- const bare = employeeName2.replace(/-exe\d*$/, "").replace(/\d+$/, "");
3876
+ if (employeeName2.includes("-")) {
3877
+ const bare = employeeName2.split("-")[0].replace(/\d+$/, "");
3727
3878
  return {
3728
3879
  status: "failed",
3729
3880
  sessionName: "",
@@ -3742,7 +3893,7 @@ function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
3742
3893
  return {
3743
3894
  status: "failed",
3744
3895
  sessionName: "",
3745
- error: `Invalid exeSession "${exeSession2}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
3896
+ error: `Invalid coordinator session "${exeSession2}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3746
3897
  };
3747
3898
  }
3748
3899
  }
@@ -3899,8 +4050,8 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
3899
4050
  const ctxContent = [
3900
4051
  `## Session Context`,
3901
4052
  `You are running in tmux session: ${sessionName}.`,
3902
- `Your parent exe session is ${exeSession2}.`,
3903
- `Your employees (if any) use the -${exeSession2} suffix (e.g., tom-${exeSession2}).`
4053
+ `Your parent coordinator session is ${exeSession2}.`,
4054
+ `Your employees (if any) use the -${exeSession2} suffix.`
3904
4055
  ].join("\n");
3905
4056
  writeFileSync6(ctxFile, ctxContent);
3906
4057
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
@@ -4004,6 +4155,7 @@ var init_tmux_routing = __esm({
4004
4155
  init_provider_table();
4005
4156
  init_intercom_queue();
4006
4157
  init_plan_limits();
4158
+ init_employees();
4007
4159
  SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
4008
4160
  SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
4009
4161
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
@@ -4139,7 +4291,11 @@ async function ensureShardSchema(client) {
4139
4291
  "ALTER TABLE memories ADD COLUMN source_path TEXT",
4140
4292
  "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
4141
4293
  "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
4142
- "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
4294
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
4295
+ // MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
4296
+ "ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
4297
+ "ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
4298
+ "ALTER TABLE memories ADD COLUMN trajectory TEXT"
4143
4299
  ]) {
4144
4300
  try {
4145
4301
  await client.execute(col);
@@ -4269,26 +4425,26 @@ var init_platform_procedures = __esm({
4269
4425
  title: "What is exe-os \u2014 the operating model every agent must understand",
4270
4426
  domain: "architecture",
4271
4427
  priority: "p0",
4272
- content: "Exe OS is an AI employee operating system. A founder runs 5-10 AI agents as a real org: COO (exe), CTO (yoshi), CMO (mari), engineers (tom), content (sasha). Each agent has identity, expertise, and experience layers \u2014 persistent memory that makes them better over time. All data is local-first, E2EE, owned by the user. The MCP server is the ONLY data interface \u2014 never access the DB directly."
4428
+ content: "Exe OS is an AI employee operating system. A founder runs 5-10 AI agents as a real org: COO, CTO, CMO, engineers, and content production specialists. Each agent has identity, expertise, and experience layers \u2014 persistent memory that makes them better over time. All data is local-first, E2EE, owned by the user. The MCP server is the ONLY data interface \u2014 never access the DB directly."
4273
4429
  },
4274
4430
  {
4275
4431
  title: "Mode 1 \u2014 how exe-os runs inside Claude Code",
4276
4432
  domain: "architecture",
4277
4433
  priority: "p0",
4278
- content: "Mode 1: exe-os runs AS hooks + MCP + skills inside Claude Code. The founder opens CC, runs /exe to boot the COO. exe manages employees in tmux sessions. Each exeN is a separate CC window/project. Employees (yoshi, tom, mari) run in their own tmux panes via create_task auto-spawn. The founder talks to exe; exe orchestrates the team. CC is the shell, exe-os is the brain."
4434
+ content: "Mode 1: exe-os runs AS hooks + MCP + skills inside Claude Code. The founder opens CC and boots the COO. The COO manages employees in tmux sessions. Each coordinator session is a separate CC window/project. Employees run in their own tmux panes via create_task auto-spawn. The founder talks to the COO; the COO orchestrates the team. CC is the shell, exe-os is the brain."
4279
4435
  },
4280
4436
  {
4281
- title: "Sessions explained \u2014 what exeN means and how projects work",
4437
+ title: "Sessions explained \u2014 coordinator session names and projects",
4282
4438
  domain: "architecture",
4283
4439
  priority: "p0",
4284
- content: "Each exeN (exe1, exe2, exe3) is an isolated project session. exe1 might be exe-os development, exe2 might be exe-wiki. Each session spawns its own employees: exe1\u2192yoshi-exe1\u2192tom-exe1. Sessions share the same memory DB but tasks are scoped to the session that created them. A founder can run multiple projects simultaneously. Sessions never interfere with each other."
4440
+ content: "Each coordinator session is an isolated project session. One might be exe-os development, another might be exe-wiki. Each session spawns its own employees using {employee}-{coordinatorSession}. Sessions share the same memory DB but tasks are scoped to the session that created them. A founder can run multiple projects simultaneously. Sessions never interfere with each other."
4285
4441
  },
4286
4442
  // --- Hierarchy and dispatch ---
4287
4443
  {
4288
4444
  title: "Chain of command \u2014 who talks to whom",
4289
4445
  domain: "workflow",
4290
4446
  priority: "p0",
4291
- content: "Founder \u2192 exe (COO) \u2192 yoshi (CTO) / mari (CMO). Yoshi \u2192 tom (engineer). Mari \u2192 sasha (content). Never skip levels: exe never assigns directly to tom. Tom never reports directly to exe. If you need cross-team info, use ask_team_memory \u2014 don't read other agents' task folders. Each level owns dispatch downward and review upward."
4447
+ content: "Founder -> COO -> CTO/CMO. CTO -> engineers. CMO -> content production. Never skip levels: the COO does not bypass managers for specialist work. Specialists report to their manager. If you need cross-team info, use ask_team_memory \u2014 don't read other agents' task folders. Each level owns dispatch downward and review upward."
4292
4448
  },
4293
4449
  {
4294
4450
  title: "Single dispatch path \u2014 create_task only",
@@ -4298,30 +4454,30 @@ var init_platform_procedures = __esm({
4298
4454
  },
4299
4455
  // --- Session isolation ---
4300
4456
  {
4301
- title: "Session scoping \u2014 stay in your exe boundary",
4457
+ title: "Session scoping \u2014 stay in your coordinator boundary",
4302
4458
  domain: "security",
4303
4459
  priority: "p0",
4304
- content: "Session scoping is mandatory. Managers dispatch to workers within their own exe session ONLY. exe1\u2192yoshi-exe1\u2192tom-exe1. exe2\u2192yoshi-exe2\u2192tom2-exe2. Cross-session dispatch is blocked by the system. Verify session names before dispatch. Tasks are scoped to the creating exe session."
4460
+ content: "Session scoping is mandatory. Managers dispatch to workers within their own coordinator session ONLY. Employee sessions use {employee}-{coordinatorSession}. Cross-session dispatch is blocked by the system. Verify session names before dispatch. Tasks are scoped to the creating coordinator session."
4305
4461
  },
4306
4462
  {
4307
4463
  title: "Session isolation \u2014 never touch another session's work",
4308
4464
  domain: "workflow",
4309
4465
  priority: "p0",
4310
- content: `Sessions are isolated. exeN owns ONLY tasks it dispatched. (1) Never close/update/cancel tasks from another exe session. (2) Never review work from a different session \u2014 report "belongs to exeN" and skip. (3) Ignore other sessions' items in list_tasks results. (4) Employees inherit session: yoshi-exe1 works ONLY on exe1 tasks. Cross-session work is a system violation.`
4466
+ content: "Sessions are isolated. A coordinator session owns ONLY tasks it dispatched. (1) Never close/update/cancel tasks from another coordinator session. (2) Never review work from a different session \u2014 report that it belongs to another session and skip. (3) Ignore other sessions' items in list_tasks results. (4) Employees inherit session: employee sessions work ONLY on their parent coordinator session's tasks. Cross-session work is a system violation."
4311
4467
  },
4312
4468
  // --- Engineering: session scoping in code ---
4313
4469
  {
4314
4470
  title: "Three-dimensional scoping \u2014 session, project, role \u2014 enforced in every query",
4315
4471
  domain: "architecture",
4316
4472
  priority: "p0",
4317
- content: "Every DB query, notification, review count, and task operation MUST be scoped on 3 dimensions: (1) Session \u2014 filter by session_scope matching current exeN. (2) Project \u2014 filter by project_name. (3) Role \u2014 agents only see data at their hierarchy level. When writing ANY function that touches tasks, reviews, messages, or notifications: always accept a sessionScope parameter and pass it to the SQL WHERE clause. Unscoped queries are bugs. Test by running 2+ exe sessions simultaneously."
4473
+ content: "Every DB query, notification, review count, and task operation MUST be scoped on 3 dimensions: (1) Session \u2014 filter by session_scope matching the current coordinator session. (2) Project \u2014 filter by project_name. (3) Role \u2014 agents only see data at their hierarchy level. When writing ANY function that touches tasks, reviews, messages, or notifications: always accept a sessionScope parameter and pass it to the SQL WHERE clause. Unscoped queries are bugs. Test by running 2+ coordinator sessions simultaneously."
4318
4474
  },
4319
4475
  // --- Hard constraints ---
4320
4476
  {
4321
4477
  title: "What you CANNOT do in exe-os \u2014 hard constraints",
4322
4478
  domain: "security",
4323
4479
  priority: "p0",
4324
- content: "NEVER: (1) Access the database directly \u2014 it's SQLCipher encrypted, always fails. Use MCP tools only. (2) Manually spawn tmux sessions \u2014 create_task handles it. (3) Run git checkout main \u2014 agents work in worktrees. (4) Modify another agent's in-progress task. (5) Push to remote \u2014 exe reviews and pushes. (6) Skip update_task(done) \u2014 it's the ONLY way your work gets reviewed. (7) Run git init."
4480
+ content: "NEVER: (1) Access the database directly \u2014 it's SQLCipher encrypted, always fails. Use MCP tools only. (2) Manually spawn tmux sessions \u2014 create_task handles it. (3) Run git checkout main \u2014 agents work in worktrees. (4) Modify another agent's in-progress task. (5) Push to remote \u2014 the COO reviews and pushes. (6) Skip update_task(done) \u2014 it's the ONLY way your work gets reviewed. (7) Run git init."
4325
4481
  },
4326
4482
  // --- Operations ---
4327
4483
  {
@@ -4570,6 +4726,7 @@ async function initStore(options) {
4570
4726
  }
4571
4727
 
4572
4728
  // src/bin/exe-dispatch.ts
4729
+ init_employees();
4573
4730
  var employeeName = process.argv[2];
4574
4731
  if (!employeeName) {
4575
4732
  console.error("Usage: exe-dispatch <employeeName> [projectDir]");
@@ -4581,7 +4738,7 @@ if (!exeSession) {
4581
4738
  console.log(JSON.stringify({
4582
4739
  status: "failed",
4583
4740
  sessionName: "",
4584
- error: "Not in tmux \u2014 cannot resolve exe session"
4741
+ error: "Not in tmux \u2014 cannot resolve coordinator session"
4585
4742
  }));
4586
4743
  process.exit(1);
4587
4744
  }
@@ -4590,7 +4747,7 @@ try {
4590
4747
  await createTaskCore({
4591
4748
  title: `Dispatch: ${employeeName} (CLI)`,
4592
4749
  assignedTo: employeeName,
4593
- assignedBy: "exe",
4750
+ assignedBy: getCoordinatorName(),
4594
4751
  projectName: "exe-os",
4595
4752
  priority: "p1",
4596
4753
  context: "Session dispatched via exe-dispatch CLI. Agent will pick up queued tasks via intercom.",