@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
  var database_exports = {};
378
612
  __export(database_exports, {
@@ -520,22 +754,24 @@ async function ensureSchema() {
520
754
  ON behaviors(agent_id, active);
521
755
  `);
522
756
  try {
757
+ const coordinatorName = getCoordinatorName();
523
758
  const existing = await client.execute({
524
- sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = 'exe'",
525
- args: []
759
+ sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
760
+ args: [coordinatorName]
526
761
  });
527
762
  if (Number(existing.rows[0]?.cnt) === 0) {
528
- await client.executeMultiple(`
529
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
530
- VALUES
531
- (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');
532
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
533
- VALUES
534
- (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');
535
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
536
- VALUES
537
- (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');
538
- `);
763
+ const seededAt = "2026-03-25T00:00:00Z";
764
+ for (const [domain, content] of [
765
+ ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
766
+ ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
767
+ ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
768
+ ]) {
769
+ await client.execute({
770
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
771
+ VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
772
+ args: [coordinatorName, domain, content, seededAt, seededAt]
773
+ });
774
+ }
539
775
  }
540
776
  } catch {
541
777
  }
@@ -1208,264 +1444,76 @@ async function ensureSchema() {
1208
1444
  try {
1209
1445
  await client.execute({
1210
1446
  sql: `ALTER TABLE memories ADD COLUMN supersedes_id TEXT`,
1211
- args: []
1212
- });
1213
- } catch {
1214
- }
1215
- try {
1216
- await client.execute(
1217
- `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
1218
- );
1219
- } catch {
1220
- }
1221
- for (const col of [
1222
- "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
1223
- "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
1224
- ]) {
1225
- try {
1226
- await client.execute(col);
1227
- } catch {
1228
- }
1229
- }
1230
- }
1231
- async function disposeDatabase() {
1232
- if (_client) {
1233
- _client.close();
1234
- _client = null;
1235
- _resilientClient = null;
1236
- }
1237
- }
1238
- var _client, _resilientClient, initTurso, disposeTurso;
1239
- var init_database = __esm({
1240
- "src/lib/database.ts"() {
1241
- "use strict";
1242
- init_db_retry();
1243
- _client = null;
1244
- _resilientClient = null;
1245
- initTurso = initDatabase;
1246
- disposeTurso = disposeDatabase;
1247
- }
1248
- });
1249
-
1250
- // src/lib/config.ts
1251
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
1252
- import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
1253
- import path3 from "path";
1254
- import os3 from "os";
1255
- function resolveDataDir() {
1256
- if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
1257
- if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
1258
- const newDir = path3.join(os3.homedir(), ".exe-os");
1259
- const legacyDir = path3.join(os3.homedir(), ".exe-mem");
1260
- if (!existsSync3(newDir) && existsSync3(legacyDir)) {
1261
- try {
1262
- renameSync2(legacyDir, newDir);
1263
- process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
1264
- `);
1265
- } catch {
1266
- return legacyDir;
1267
- }
1268
- }
1269
- return newDir;
1270
- }
1271
- function migrateLegacyConfig(raw) {
1272
- if ("r2" in raw) {
1273
- process.stderr.write(
1274
- "[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"
1275
- );
1276
- delete raw.r2;
1277
- }
1278
- if ("syncIntervalMs" in raw) {
1279
- delete raw.syncIntervalMs;
1280
- }
1281
- return raw;
1282
- }
1283
- function migrateConfig(raw) {
1284
- const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
1285
- let currentVersion = fromVersion;
1286
- let migrated = false;
1287
- if (currentVersion > CURRENT_CONFIG_VERSION) {
1288
- return { config: raw, migrated: false, fromVersion };
1447
+ args: []
1448
+ });
1449
+ } catch {
1289
1450
  }
1290
- for (const migration of CONFIG_MIGRATIONS) {
1291
- if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
1292
- raw = migration.migrate(raw);
1293
- currentVersion = migration.to;
1294
- migrated = true;
1451
+ try {
1452
+ await client.execute(
1453
+ `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
1454
+ );
1455
+ } catch {
1456
+ }
1457
+ for (const col of [
1458
+ "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
1459
+ "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
1460
+ ]) {
1461
+ try {
1462
+ await client.execute(col);
1463
+ } catch {
1295
1464
  }
1296
1465
  }
1297
- return { config: raw, migrated, fromVersion };
1298
- }
1299
- function normalizeScalingRoadmap(raw) {
1300
- const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
1301
- const userRoadmap = raw.scalingRoadmap ?? {};
1302
- const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
1303
- if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
1304
- userAuto.enabled = raw.rerankerEnabled;
1466
+ try {
1467
+ await client.execute({
1468
+ sql: `ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0`,
1469
+ args: []
1470
+ });
1471
+ } catch {
1305
1472
  }
1306
- raw.scalingRoadmap = {
1307
- ...userRoadmap,
1308
- rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
1309
- };
1310
- }
1311
- function normalizeSessionLifecycle(raw) {
1312
- const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
1313
- const userSL = raw.sessionLifecycle ?? {};
1314
- raw.sessionLifecycle = { ...defaultSL, ...userSL };
1315
- }
1316
- function normalizeAutoUpdate(raw) {
1317
- const defaultAU = DEFAULT_CONFIG.autoUpdate;
1318
- const userAU = raw.autoUpdate ?? {};
1319
- raw.autoUpdate = { ...defaultAU, ...userAU };
1320
- }
1321
- async function loadConfig() {
1322
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1323
- await mkdir(dir, { recursive: true });
1324
- const configPath = path3.join(dir, "config.json");
1325
- if (!existsSync3(configPath)) {
1326
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
1473
+ try {
1474
+ await client.execute(
1475
+ `CREATE INDEX IF NOT EXISTS idx_memories_draft ON memories(draft) WHERE draft = 1`
1476
+ );
1477
+ } catch {
1327
1478
  }
1328
- const raw = await readFile(configPath, "utf-8");
1329
1479
  try {
1330
- let parsed = JSON.parse(raw);
1331
- parsed = migrateLegacyConfig(parsed);
1332
- const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
1333
- if (migrated) {
1334
- process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
1335
- `);
1336
- try {
1337
- await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
1338
- } catch {
1339
- }
1340
- }
1341
- normalizeScalingRoadmap(migratedCfg);
1342
- normalizeSessionLifecycle(migratedCfg);
1343
- normalizeAutoUpdate(migratedCfg);
1344
- const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
1345
- if (config.dbPath.startsWith("~")) {
1346
- config.dbPath = config.dbPath.replace(/^~/, os3.homedir());
1347
- }
1348
- return config;
1480
+ await client.execute({
1481
+ sql: `ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'`,
1482
+ args: []
1483
+ });
1349
1484
  } catch {
1350
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
1351
1485
  }
1352
- }
1353
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1354
- var init_config = __esm({
1355
- "src/lib/config.ts"() {
1356
- "use strict";
1357
- EXE_AI_DIR = resolveDataDir();
1358
- DB_PATH = path3.join(EXE_AI_DIR, "memories.db");
1359
- MODELS_DIR = path3.join(EXE_AI_DIR, "models");
1360
- CONFIG_PATH = path3.join(EXE_AI_DIR, "config.json");
1361
- LEGACY_LANCE_PATH = path3.join(EXE_AI_DIR, "local.lance");
1362
- CURRENT_CONFIG_VERSION = 1;
1363
- DEFAULT_CONFIG = {
1364
- config_version: CURRENT_CONFIG_VERSION,
1365
- dbPath: DB_PATH,
1366
- modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
1367
- embeddingDim: 1024,
1368
- batchSize: 20,
1369
- flushIntervalMs: 1e4,
1370
- autoIngestion: true,
1371
- autoRetrieval: true,
1372
- searchMode: "hybrid",
1373
- hookSearchMode: "hybrid",
1374
- fileGrepEnabled: true,
1375
- splashEffect: true,
1376
- consolidationEnabled: true,
1377
- consolidationIntervalMs: 6 * 60 * 60 * 1e3,
1378
- consolidationModel: "claude-haiku-4-5-20251001",
1379
- consolidationMaxCallsPerRun: 20,
1380
- selfQueryRouter: true,
1381
- selfQueryModel: "claude-haiku-4-5-20251001",
1382
- rerankerEnabled: true,
1383
- scalingRoadmap: {
1384
- rerankerAutoTrigger: {
1385
- enabled: true,
1386
- broadQueryMinCardinality: 5e4,
1387
- fetchTopK: 150,
1388
- returnTopK: 5
1389
- }
1390
- },
1391
- graphRagEnabled: true,
1392
- wikiEnabled: false,
1393
- wikiUrl: "",
1394
- wikiApiKey: "",
1395
- wikiSyncIntervalMs: 30 * 60 * 1e3,
1396
- wikiWorkspaceMapping: {
1397
- exe: "Executive",
1398
- yoshi: "Engineering",
1399
- mari: "Marketing",
1400
- tom: "Engineering",
1401
- sasha: "Production"
1402
- },
1403
- wikiAutoUpdate: true,
1404
- wikiAutoUpdateThreshold: 0.5,
1405
- wikiAutoUpdateCreateNew: true,
1406
- skillLearning: true,
1407
- skillThreshold: 3,
1408
- skillModel: "claude-haiku-4-5-20251001",
1409
- exeHeartbeat: {
1410
- enabled: true,
1411
- intervalSeconds: 60,
1412
- staleInProgressThresholdHours: 2
1413
- },
1414
- sessionLifecycle: {
1415
- idleKillEnabled: true,
1416
- idleKillTicksRequired: 3,
1417
- idleKillIntercomAckWindowMs: 1e4,
1418
- maxAutoInstances: 10
1419
- },
1420
- autoUpdate: {
1421
- checkOnBoot: true,
1422
- autoInstall: false,
1423
- checkIntervalMs: 24 * 60 * 60 * 1e3
1424
- }
1425
- };
1426
- CONFIG_MIGRATIONS = [
1427
- {
1428
- from: 0,
1429
- to: 1,
1430
- migrate: (cfg) => {
1431
- cfg.config_version = 1;
1432
- return cfg;
1433
- }
1434
- }
1435
- ];
1486
+ try {
1487
+ await client.execute(
1488
+ `CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
1489
+ );
1490
+ } catch {
1436
1491
  }
1437
- });
1438
-
1439
- // src/lib/employees.ts
1440
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1441
- import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
1442
- import { execSync as execSync3 } from "child_process";
1443
- import path4 from "path";
1444
- import os4 from "os";
1445
- function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
1446
- if (!existsSync4(employeesPath)) return [];
1447
1492
  try {
1448
- return JSON.parse(readFileSync4(employeesPath, "utf-8"));
1493
+ await client.execute({
1494
+ sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
1495
+ args: []
1496
+ });
1449
1497
  } catch {
1450
- return [];
1451
1498
  }
1452
1499
  }
1453
- function getEmployee(employees, name) {
1454
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
1455
- }
1456
- function isMultiInstance(agentName, employees) {
1457
- const roster = employees ?? loadEmployeesSync();
1458
- const emp = getEmployee(roster, agentName);
1459
- if (!emp) return false;
1460
- return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
1500
+ async function disposeDatabase() {
1501
+ if (_client) {
1502
+ _client.close();
1503
+ _client = null;
1504
+ _resilientClient = null;
1505
+ }
1461
1506
  }
1462
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
1463
- var init_employees = __esm({
1464
- "src/lib/employees.ts"() {
1507
+ var _client, _resilientClient, initTurso, disposeTurso;
1508
+ var init_database = __esm({
1509
+ "src/lib/database.ts"() {
1465
1510
  "use strict";
1466
- init_config();
1467
- EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
1468
- MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
1511
+ init_db_retry();
1512
+ init_employees();
1513
+ _client = null;
1514
+ _resilientClient = null;
1515
+ initTurso = initDatabase;
1516
+ disposeTurso = disposeDatabase;
1469
1517
  }
1470
1518
  });
1471
1519
 
@@ -1956,6 +2004,36 @@ async function listTasks(input) {
1956
2004
  tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
1957
2005
  }));
1958
2006
  }
2007
+ function isTmuxSessionAlive(identifier) {
2008
+ if (!identifier || identifier === "unknown") return true;
2009
+ try {
2010
+ if (identifier.startsWith("%")) {
2011
+ const output = execSync4("tmux list-panes -a -F '#{pane_id}'", {
2012
+ timeout: 2e3,
2013
+ encoding: "utf8",
2014
+ stdio: ["pipe", "pipe", "pipe"]
2015
+ });
2016
+ return output.split("\n").some((l) => l.trim() === identifier);
2017
+ } else {
2018
+ execSync4(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2019
+ timeout: 2e3,
2020
+ stdio: ["pipe", "pipe", "pipe"]
2021
+ });
2022
+ return true;
2023
+ }
2024
+ } catch {
2025
+ if (identifier.startsWith("%")) return true;
2026
+ try {
2027
+ execSync4("tmux list-sessions", {
2028
+ timeout: 2e3,
2029
+ stdio: ["pipe", "pipe", "pipe"]
2030
+ });
2031
+ return false;
2032
+ } catch {
2033
+ return true;
2034
+ }
2035
+ }
2036
+ }
1959
2037
  function checkStaleCompletion(taskContext, taskCreatedAt) {
1960
2038
  if (!taskContext) return null;
1961
2039
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -2018,13 +2096,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2018
2096
  });
2019
2097
  if (claim.rowsAffected === 0) {
2020
2098
  const current = await client.execute({
2021
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
2099
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
2022
2100
  args: [taskId]
2023
2101
  });
2024
2102
  const cur = current.rows[0];
2025
- const status = cur?.status ?? "unknown";
2026
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2027
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
2103
+ const curStatus = cur?.status ?? "unknown";
2104
+ const claimedBySession = cur?.assigned_tmux ?? "";
2105
+ const assignedBy = cur?.assigned_by ?? "";
2106
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
2107
+ process.stderr.write(
2108
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
2109
+ `
2110
+ );
2111
+ await client.execute({
2112
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
2113
+ args: [now, taskId]
2114
+ });
2115
+ const retried = await client.execute({
2116
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
2117
+ args: [tmuxSession, now, taskId]
2118
+ });
2119
+ if (retried.rowsAffected > 0) {
2120
+ try {
2121
+ await writeCheckpoint({
2122
+ taskId,
2123
+ step: "reclaimed_dead_session",
2124
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
2125
+ });
2126
+ } catch {
2127
+ }
2128
+ return { row, taskFile, now, taskId };
2129
+ }
2130
+ }
2131
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2132
+ process.stderr.write(
2133
+ `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2134
+ `
2135
+ );
2136
+ await client.execute({
2137
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
2138
+ args: [tmuxSession, now, taskId]
2139
+ });
2140
+ try {
2141
+ await writeCheckpoint({
2142
+ taskId,
2143
+ step: "assigner_override",
2144
+ contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
2145
+ });
2146
+ } catch {
2147
+ }
2148
+ return { row, taskFile, now, taskId };
2149
+ }
2150
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
2151
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
2028
2152
  }
2029
2153
  try {
2030
2154
  await writeCheckpoint({
@@ -2122,7 +2246,7 @@ var init_tasks_crud = __esm({
2122
2246
  "use strict";
2123
2247
  init_database();
2124
2248
  init_task_scope();
2125
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
2249
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2126
2250
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2127
2251
  }
2128
2252
  });
@@ -2488,7 +2612,7 @@ function findSessionForProject(projectName) {
2488
2612
  const sessions = listSessions();
2489
2613
  for (const s of sessions) {
2490
2614
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2491
- if (proj === projectName && s.agentId === "exe") return s;
2615
+ if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2492
2616
  }
2493
2617
  return null;
2494
2618
  }
@@ -2528,12 +2652,13 @@ var init_session_scope = __esm({
2528
2652
  init_session_registry();
2529
2653
  init_project_name();
2530
2654
  init_tmux_routing();
2655
+ init_employees();
2531
2656
  }
2532
2657
  });
2533
2658
 
2534
2659
  // src/lib/tasks-notify.ts
2535
2660
  async function dispatchTaskToEmployee(input) {
2536
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
2661
+ if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2537
2662
  let crossProject = false;
2538
2663
  if (input.projectName) {
2539
2664
  try {
@@ -2976,6 +3101,24 @@ async function updateTask(input) {
2976
3101
  });
2977
3102
  } catch {
2978
3103
  }
3104
+ const assignedAgent = String(row.assigned_to);
3105
+ if (!isCoordinatorName(assignedAgent)) {
3106
+ try {
3107
+ const draftClient = getClient();
3108
+ if (input.status === "done") {
3109
+ await draftClient.execute({
3110
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3111
+ args: [assignedAgent]
3112
+ });
3113
+ } else if (input.status === "cancelled") {
3114
+ await draftClient.execute({
3115
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
3116
+ args: [assignedAgent]
3117
+ });
3118
+ }
3119
+ } catch {
3120
+ }
3121
+ }
2979
3122
  try {
2980
3123
  const client = getClient();
2981
3124
  const cascaded = await client.execute({
@@ -2994,8 +3137,8 @@ async function updateTask(input) {
2994
3137
  }
2995
3138
  const isTerminal = input.status === "done" || input.status === "needs_review";
2996
3139
  if (isTerminal) {
2997
- const isExe = String(row.assigned_to) === "exe";
2998
- if (!isExe) {
3140
+ const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3141
+ if (!isCoordinator) {
2999
3142
  notifyTaskDone();
3000
3143
  }
3001
3144
  await markTaskNotificationsRead(taskFile);
@@ -3019,7 +3162,7 @@ async function updateTask(input) {
3019
3162
  }
3020
3163
  }
3021
3164
  }
3022
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
3165
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3023
3166
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3024
3167
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3025
3168
  taskId,
@@ -3035,7 +3178,7 @@ async function updateTask(input) {
3035
3178
  });
3036
3179
  }
3037
3180
  let nextTask;
3038
- if (isTerminal && String(row.assigned_to) !== "exe") {
3181
+ if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3039
3182
  try {
3040
3183
  nextTask = await findNextTask(String(row.assigned_to));
3041
3184
  } catch {
@@ -3062,12 +3205,14 @@ async function updateTask(input) {
3062
3205
  async function deleteTask(taskId, baseDir) {
3063
3206
  const client = getClient();
3064
3207
  const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
3065
- const reviewer = assignedBy || "exe";
3208
+ const coordinatorName = getCoordinatorName();
3209
+ const reviewer = assignedBy || coordinatorName;
3066
3210
  const reviewSlug = `review-${assignedTo}-${taskSlug}`;
3067
3211
  const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
3212
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
3068
3213
  await client.execute({
3069
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
3070
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
3214
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
3215
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
3071
3216
  });
3072
3217
  await markAsReadByTaskFile(taskFile);
3073
3218
  await markAsReadByTaskFile(reviewFile);
@@ -3079,6 +3224,7 @@ var init_tasks = __esm({
3079
3224
  init_config();
3080
3225
  init_notifications();
3081
3226
  init_state_bus();
3227
+ init_employees();
3082
3228
  init_tasks_crud();
3083
3229
  init_tasks_review();
3084
3230
  init_tasks_crud();
@@ -3164,7 +3310,7 @@ function _resetLastRelaunchCache() {
3164
3310
  }
3165
3311
  async function lastResumeCreatedAtMs(agentId) {
3166
3312
  const client = getClient();
3167
- const cmScope = sessionScopeFilter();
3313
+ const cmScope = sessionScopeFilter(null);
3168
3314
  const result = await client.execute({
3169
3315
  sql: `SELECT MAX(created_at) AS last_created_at
3170
3316
  FROM tasks
@@ -3189,7 +3335,7 @@ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
3189
3335
  const client = getClient();
3190
3336
  const now = (/* @__PURE__ */ new Date()).toISOString();
3191
3337
  const context = buildResumeContext(agentId, openTasks);
3192
- const rdScope = sessionScopeFilter();
3338
+ const rdScope = sessionScopeFilter(null);
3193
3339
  const existing = await client.execute({
3194
3340
  sql: `SELECT id FROM tasks
3195
3341
  WHERE assigned_to = ?
@@ -3223,7 +3369,7 @@ async function pollCapacityDead() {
3223
3369
  const transport = getTransport();
3224
3370
  const relaunched = [];
3225
3371
  const registered = listSessions().filter(
3226
- (s) => s.agentId !== "exe"
3372
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
3227
3373
  );
3228
3374
  if (registered.length === 0) return [];
3229
3375
  let liveSessions;
@@ -3283,7 +3429,7 @@ async function pollCapacityDead() {
3283
3429
  reason: "capacity"
3284
3430
  });
3285
3431
  const client = getClient();
3286
- const rlScope = sessionScopeFilter();
3432
+ const rlScope = sessionScopeFilter(null);
3287
3433
  const openTasks = await client.execute({
3288
3434
  sql: `SELECT id, title, priority, task_file, status
3289
3435
  FROM tasks
@@ -3337,6 +3483,7 @@ var init_capacity_monitor = __esm({
3337
3483
  init_session_kill_telemetry();
3338
3484
  init_tmux_routing();
3339
3485
  init_task_scope();
3486
+ init_employees();
3340
3487
  CAPACITY_PATTERNS = [
3341
3488
  /conversation is too long/i,
3342
3489
  /maximum context length/i,
@@ -3486,7 +3633,7 @@ function employeeSessionName(employee, exeSession, instance) {
3486
3633
  exeSession = root;
3487
3634
  } else {
3488
3635
  throw new Error(
3489
- `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
3636
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3490
3637
  );
3491
3638
  }
3492
3639
  }
@@ -3506,8 +3653,10 @@ function parseParentExe(sessionName, agentId) {
3506
3653
  return match?.[1] ?? null;
3507
3654
  }
3508
3655
  function extractRootExe(name) {
3509
- const match = name.match(/(exe\d+)$/);
3510
- return match?.[1] ?? null;
3656
+ if (!name) return null;
3657
+ if (!name.includes("-")) return name;
3658
+ const parts = name.split("-").filter(Boolean);
3659
+ return parts.length > 0 ? parts[parts.length - 1] : null;
3511
3660
  }
3512
3661
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3513
3662
  if (!existsSync10(SESSION_CACHE)) {
@@ -3652,12 +3801,14 @@ function isSessionBusy(sessionName) {
3652
3801
  return state === "thinking" || state === "tool";
3653
3802
  }
3654
3803
  function isExeSession(sessionName) {
3655
- return /^exe\d*$/.test(sessionName);
3804
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
3805
+ const coordinatorName = getCoordinatorName();
3806
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
3656
3807
  }
3657
3808
  function sendIntercom(targetSession) {
3658
3809
  const transport = getTransport();
3659
3810
  if (isExeSession(targetSession)) {
3660
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
3811
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
3661
3812
  return "skipped_exe";
3662
3813
  }
3663
3814
  if (isDebounced(targetSession)) {
@@ -3709,7 +3860,7 @@ function notifyParentExe(sessionKey) {
3709
3860
  if (result === "failed") {
3710
3861
  const rootExe = resolveExeSession();
3711
3862
  if (rootExe && rootExe !== target) {
3712
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3863
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
3713
3864
  `);
3714
3865
  const fallback = sendIntercom(rootExe);
3715
3866
  return fallback !== "failed";
@@ -3719,8 +3870,8 @@ function notifyParentExe(sessionKey) {
3719
3870
  return true;
3720
3871
  }
3721
3872
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3722
- if (employeeName === "exe") {
3723
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
3873
+ if (employeeName === "exe" || isCoordinatorName(employeeName)) {
3874
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3724
3875
  }
3725
3876
  try {
3726
3877
  assertEmployeeLimitSync();
@@ -3729,8 +3880,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3729
3880
  return { status: "failed", sessionName: "", error: err.message };
3730
3881
  }
3731
3882
  }
3732
- if (/-exe\d*$/.test(employeeName)) {
3733
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
3883
+ if (employeeName.includes("-")) {
3884
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
3734
3885
  return {
3735
3886
  status: "failed",
3736
3887
  sessionName: "",
@@ -3749,7 +3900,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3749
3900
  return {
3750
3901
  status: "failed",
3751
3902
  sessionName: "",
3752
- error: `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
3903
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3753
3904
  };
3754
3905
  }
3755
3906
  }
@@ -3906,8 +4057,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3906
4057
  const ctxContent = [
3907
4058
  `## Session Context`,
3908
4059
  `You are running in tmux session: ${sessionName}.`,
3909
- `Your parent exe session is ${exeSession}.`,
3910
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
4060
+ `Your parent coordinator session is ${exeSession}.`,
4061
+ `Your employees (if any) use the -${exeSession} suffix.`
3911
4062
  ].join("\n");
3912
4063
  writeFileSync6(ctxFile, ctxContent);
3913
4064
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
@@ -4011,6 +4162,7 @@ var init_tmux_routing = __esm({
4011
4162
  init_provider_table();
4012
4163
  init_intercom_queue();
4013
4164
  init_plan_limits();
4165
+ init_employees();
4014
4166
  SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
4015
4167
  SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
4016
4168
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
@@ -4228,7 +4380,11 @@ async function ensureShardSchema(client) {
4228
4380
  "ALTER TABLE memories ADD COLUMN source_path TEXT",
4229
4381
  "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
4230
4382
  "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
4231
- "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
4383
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
4384
+ // MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
4385
+ "ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
4386
+ "ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
4387
+ "ALTER TABLE memories ADD COLUMN trajectory TEXT"
4232
4388
  ]) {
4233
4389
  try {
4234
4390
  await client.execute(col);
@@ -4358,26 +4514,26 @@ var init_platform_procedures = __esm({
4358
4514
  title: "What is exe-os \u2014 the operating model every agent must understand",
4359
4515
  domain: "architecture",
4360
4516
  priority: "p0",
4361
- 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."
4517
+ 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."
4362
4518
  },
4363
4519
  {
4364
4520
  title: "Mode 1 \u2014 how exe-os runs inside Claude Code",
4365
4521
  domain: "architecture",
4366
4522
  priority: "p0",
4367
- 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."
4523
+ 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."
4368
4524
  },
4369
4525
  {
4370
- title: "Sessions explained \u2014 what exeN means and how projects work",
4526
+ title: "Sessions explained \u2014 coordinator session names and projects",
4371
4527
  domain: "architecture",
4372
4528
  priority: "p0",
4373
- 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."
4529
+ 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."
4374
4530
  },
4375
4531
  // --- Hierarchy and dispatch ---
4376
4532
  {
4377
4533
  title: "Chain of command \u2014 who talks to whom",
4378
4534
  domain: "workflow",
4379
4535
  priority: "p0",
4380
- 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."
4536
+ 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."
4381
4537
  },
4382
4538
  {
4383
4539
  title: "Single dispatch path \u2014 create_task only",
@@ -4387,30 +4543,30 @@ var init_platform_procedures = __esm({
4387
4543
  },
4388
4544
  // --- Session isolation ---
4389
4545
  {
4390
- title: "Session scoping \u2014 stay in your exe boundary",
4546
+ title: "Session scoping \u2014 stay in your coordinator boundary",
4391
4547
  domain: "security",
4392
4548
  priority: "p0",
4393
- 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."
4549
+ 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."
4394
4550
  },
4395
4551
  {
4396
4552
  title: "Session isolation \u2014 never touch another session's work",
4397
4553
  domain: "workflow",
4398
4554
  priority: "p0",
4399
- 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.`
4555
+ 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."
4400
4556
  },
4401
4557
  // --- Engineering: session scoping in code ---
4402
4558
  {
4403
4559
  title: "Three-dimensional scoping \u2014 session, project, role \u2014 enforced in every query",
4404
4560
  domain: "architecture",
4405
4561
  priority: "p0",
4406
- 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."
4562
+ 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."
4407
4563
  },
4408
4564
  // --- Hard constraints ---
4409
4565
  {
4410
4566
  title: "What you CANNOT do in exe-os \u2014 hard constraints",
4411
4567
  domain: "security",
4412
4568
  priority: "p0",
4413
- 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."
4569
+ 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."
4414
4570
  },
4415
4571
  // --- Operations ---
4416
4572
  {
@@ -4650,7 +4806,10 @@ async function writeMemory(record) {
4650
4806
  source_path: record.source_path ?? null,
4651
4807
  source_type: record.source_type ?? null,
4652
4808
  tier: record.tier ?? classifyTier(record),
4653
- supersedes_id: record.supersedes_id ?? null
4809
+ supersedes_id: record.supersedes_id ?? null,
4810
+ draft: record.draft ? 1 : 0,
4811
+ memory_type: record.memory_type ?? "raw",
4812
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
4654
4813
  };
4655
4814
  _pendingRecords.push(dbRow);
4656
4815
  orgBus.emit({
@@ -4705,6 +4864,9 @@ async function flushBatch() {
4705
4864
  const sourceType = row.source_type ?? null;
4706
4865
  const tier = row.tier ?? 3;
4707
4866
  const supersedesId = row.supersedes_id ?? null;
4867
+ const draft = row.draft ? 1 : 0;
4868
+ const memoryType = row.memory_type ?? "raw";
4869
+ const trajectory = row.trajectory ?? null;
4708
4870
  return {
4709
4871
  sql: hasVector ? `INSERT OR IGNORE INTO memories
4710
4872
  (id, agent_id, agent_role, session_id, timestamp,
@@ -4712,15 +4874,15 @@ async function flushBatch() {
4712
4874
  has_error, raw_text, vector, version, task_id, importance, status,
4713
4875
  confidence, last_accessed,
4714
4876
  workspace_id, document_id, user_id, char_offset, page_number,
4715
- source_path, source_type, tier, supersedes_id)
4716
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4877
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
4878
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4717
4879
  (id, agent_id, agent_role, session_id, timestamp,
4718
4880
  tool_name, project_name,
4719
4881
  has_error, raw_text, vector, version, task_id, importance, status,
4720
4882
  confidence, last_accessed,
4721
4883
  workspace_id, document_id, user_id, char_offset, page_number,
4722
- source_path, source_type, tier, supersedes_id)
4723
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4884
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
4885
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4724
4886
  args: hasVector ? [
4725
4887
  row.id,
4726
4888
  row.agent_id,
@@ -4746,7 +4908,10 @@ async function flushBatch() {
4746
4908
  sourcePath,
4747
4909
  sourceType,
4748
4910
  tier,
4749
- supersedesId
4911
+ supersedesId,
4912
+ draft,
4913
+ memoryType,
4914
+ trajectory
4750
4915
  ] : [
4751
4916
  row.id,
4752
4917
  row.agent_id,
@@ -4771,7 +4936,10 @@ async function flushBatch() {
4771
4936
  sourcePath,
4772
4937
  sourceType,
4773
4938
  tier,
4774
- supersedesId
4939
+ supersedesId,
4940
+ draft,
4941
+ memoryType,
4942
+ trajectory
4775
4943
  ]
4776
4944
  };
4777
4945
  };
@@ -4840,6 +5008,8 @@ async function searchMemories(queryVector, agentId, options) {
4840
5008
  const limit = options?.limit ?? 10;
4841
5009
  const statusFilter = options?.includeArchived ? "" : `
4842
5010
  AND COALESCE(status, 'active') = 'active'`;
5011
+ const draftFilter = options?.includeDrafts ? "" : `
5012
+ AND (draft = 0 OR draft IS NULL)`;
4843
5013
  let sql = `SELECT id, agent_id, agent_role, session_id, timestamp,
4844
5014
  tool_name, project_name,
4845
5015
  has_error, raw_text, vector, importance, status,
@@ -4849,7 +5019,7 @@ async function searchMemories(queryVector, agentId, options) {
4849
5019
  source_path, source_type
4850
5020
  FROM memories
4851
5021
  WHERE agent_id = ?
4852
- AND vector IS NOT NULL${statusFilter}
5022
+ AND vector IS NOT NULL${statusFilter}${draftFilter}
4853
5023
  AND COALESCE(confidence, 0.7) >= 0.3`;
4854
5024
  const args = [agentId];
4855
5025
  const scope = buildWikiScopeFilter(options, "");
@@ -4871,6 +5041,10 @@ async function searchMemories(queryVector, agentId, options) {
4871
5041
  sql += ` AND timestamp >= ?`;
4872
5042
  args.push(options.since);
4873
5043
  }
5044
+ if (options?.memoryType) {
5045
+ sql += ` AND memory_type = ?`;
5046
+ args.push(options.memoryType);
5047
+ }
4874
5048
  sql += ` ORDER BY vector_distance_cos(vector, vector32(?))`;
4875
5049
  args.push(vectorToBlob(queryVector));
4876
5050
  sql += ` LIMIT ?`;