@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
@@ -148,7 +148,7 @@ function wrapWithRetry(client) {
148
148
  return (sql) => retryOnBusy(() => target.execute(sql), "execute");
149
149
  }
150
150
  if (prop === "batch") {
151
- return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
151
+ return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
152
152
  }
153
153
  return Reflect.get(target, prop, receiver);
154
154
  }
@@ -164,6 +164,300 @@ var init_db_retry = __esm({
164
164
  }
165
165
  });
166
166
 
167
+ // src/lib/config.ts
168
+ var config_exports = {};
169
+ __export(config_exports, {
170
+ CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
171
+ CONFIG_PATH: () => CONFIG_PATH,
172
+ CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
173
+ DB_PATH: () => DB_PATH,
174
+ EXE_AI_DIR: () => EXE_AI_DIR,
175
+ LEGACY_LANCE_PATH: () => LEGACY_LANCE_PATH,
176
+ MODELS_DIR: () => MODELS_DIR,
177
+ loadConfig: () => loadConfig,
178
+ loadConfigFrom: () => loadConfigFrom,
179
+ loadConfigSync: () => loadConfigSync,
180
+ migrateConfig: () => migrateConfig,
181
+ saveConfig: () => saveConfig
182
+ });
183
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
184
+ import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
185
+ import path3 from "path";
186
+ import os from "os";
187
+ function resolveDataDir() {
188
+ if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
189
+ if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
190
+ const newDir = path3.join(os.homedir(), ".exe-os");
191
+ const legacyDir = path3.join(os.homedir(), ".exe-mem");
192
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
193
+ try {
194
+ renameSync(legacyDir, newDir);
195
+ process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
196
+ `);
197
+ } catch {
198
+ return legacyDir;
199
+ }
200
+ }
201
+ return newDir;
202
+ }
203
+ function migrateLegacyConfig(raw) {
204
+ if ("r2" in raw) {
205
+ process.stderr.write(
206
+ "[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"
207
+ );
208
+ delete raw.r2;
209
+ }
210
+ if ("syncIntervalMs" in raw) {
211
+ delete raw.syncIntervalMs;
212
+ }
213
+ return raw;
214
+ }
215
+ function migrateConfig(raw) {
216
+ const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
217
+ let currentVersion = fromVersion;
218
+ let migrated = false;
219
+ if (currentVersion > CURRENT_CONFIG_VERSION) {
220
+ return { config: raw, migrated: false, fromVersion };
221
+ }
222
+ for (const migration of CONFIG_MIGRATIONS) {
223
+ if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
224
+ raw = migration.migrate(raw);
225
+ currentVersion = migration.to;
226
+ migrated = true;
227
+ }
228
+ }
229
+ return { config: raw, migrated, fromVersion };
230
+ }
231
+ function normalizeScalingRoadmap(raw) {
232
+ const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
233
+ const userRoadmap = raw.scalingRoadmap ?? {};
234
+ const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
235
+ if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
236
+ userAuto.enabled = raw.rerankerEnabled;
237
+ }
238
+ raw.scalingRoadmap = {
239
+ ...userRoadmap,
240
+ rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
241
+ };
242
+ }
243
+ function normalizeSessionLifecycle(raw) {
244
+ const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
245
+ const userSL = raw.sessionLifecycle ?? {};
246
+ raw.sessionLifecycle = { ...defaultSL, ...userSL };
247
+ }
248
+ function normalizeAutoUpdate(raw) {
249
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
250
+ const userAU = raw.autoUpdate ?? {};
251
+ raw.autoUpdate = { ...defaultAU, ...userAU };
252
+ }
253
+ async function loadConfig() {
254
+ const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
255
+ await mkdir(dir, { recursive: true });
256
+ const configPath = path3.join(dir, "config.json");
257
+ if (!existsSync2(configPath)) {
258
+ return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
259
+ }
260
+ const raw = await readFile(configPath, "utf-8");
261
+ try {
262
+ let parsed = JSON.parse(raw);
263
+ parsed = migrateLegacyConfig(parsed);
264
+ const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
265
+ if (migrated) {
266
+ process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
267
+ `);
268
+ try {
269
+ await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
270
+ } catch {
271
+ }
272
+ }
273
+ normalizeScalingRoadmap(migratedCfg);
274
+ normalizeSessionLifecycle(migratedCfg);
275
+ normalizeAutoUpdate(migratedCfg);
276
+ const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
277
+ if (config.dbPath.startsWith("~")) {
278
+ config.dbPath = config.dbPath.replace(/^~/, os.homedir());
279
+ }
280
+ return config;
281
+ } catch {
282
+ return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
283
+ }
284
+ }
285
+ function loadConfigSync() {
286
+ const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
287
+ const configPath = path3.join(dir, "config.json");
288
+ if (!existsSync2(configPath)) {
289
+ return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
290
+ }
291
+ try {
292
+ const raw = readFileSync2(configPath, "utf-8");
293
+ let parsed = JSON.parse(raw);
294
+ parsed = migrateLegacyConfig(parsed);
295
+ const { config: migratedCfg } = migrateConfig(parsed);
296
+ normalizeScalingRoadmap(migratedCfg);
297
+ normalizeSessionLifecycle(migratedCfg);
298
+ normalizeAutoUpdate(migratedCfg);
299
+ return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
300
+ } catch {
301
+ return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
302
+ }
303
+ }
304
+ async function saveConfig(config) {
305
+ const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
306
+ await mkdir(dir, { recursive: true });
307
+ const configPath = path3.join(dir, "config.json");
308
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
309
+ if (config.cloud?.apiKey) {
310
+ await chmod(configPath, 384);
311
+ }
312
+ }
313
+ async function loadConfigFrom(configPath) {
314
+ const raw = await readFile(configPath, "utf-8");
315
+ try {
316
+ let parsed = JSON.parse(raw);
317
+ parsed = migrateLegacyConfig(parsed);
318
+ const { config: migratedCfg } = migrateConfig(parsed);
319
+ normalizeScalingRoadmap(migratedCfg);
320
+ normalizeSessionLifecycle(migratedCfg);
321
+ normalizeAutoUpdate(migratedCfg);
322
+ return { ...DEFAULT_CONFIG, ...migratedCfg };
323
+ } catch {
324
+ return { ...DEFAULT_CONFIG };
325
+ }
326
+ }
327
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
328
+ var init_config = __esm({
329
+ "src/lib/config.ts"() {
330
+ "use strict";
331
+ EXE_AI_DIR = resolveDataDir();
332
+ DB_PATH = path3.join(EXE_AI_DIR, "memories.db");
333
+ MODELS_DIR = path3.join(EXE_AI_DIR, "models");
334
+ CONFIG_PATH = path3.join(EXE_AI_DIR, "config.json");
335
+ LEGACY_LANCE_PATH = path3.join(EXE_AI_DIR, "local.lance");
336
+ CURRENT_CONFIG_VERSION = 1;
337
+ DEFAULT_CONFIG = {
338
+ config_version: CURRENT_CONFIG_VERSION,
339
+ dbPath: DB_PATH,
340
+ modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
341
+ embeddingDim: 1024,
342
+ batchSize: 20,
343
+ flushIntervalMs: 1e4,
344
+ autoIngestion: true,
345
+ autoRetrieval: true,
346
+ searchMode: "hybrid",
347
+ hookSearchMode: "hybrid",
348
+ fileGrepEnabled: true,
349
+ splashEffect: true,
350
+ consolidationEnabled: true,
351
+ consolidationIntervalMs: 6 * 60 * 60 * 1e3,
352
+ consolidationModel: "claude-haiku-4-5-20251001",
353
+ consolidationMaxCallsPerRun: 20,
354
+ selfQueryRouter: true,
355
+ selfQueryModel: "claude-haiku-4-5-20251001",
356
+ rerankerEnabled: true,
357
+ scalingRoadmap: {
358
+ rerankerAutoTrigger: {
359
+ enabled: true,
360
+ broadQueryMinCardinality: 5e4,
361
+ fetchTopK: 150,
362
+ returnTopK: 5
363
+ }
364
+ },
365
+ graphRagEnabled: true,
366
+ wikiEnabled: false,
367
+ wikiUrl: "",
368
+ wikiApiKey: "",
369
+ wikiSyncIntervalMs: 30 * 60 * 1e3,
370
+ wikiWorkspaceMapping: {},
371
+ wikiAutoUpdate: true,
372
+ wikiAutoUpdateThreshold: 0.5,
373
+ wikiAutoUpdateCreateNew: true,
374
+ skillLearning: true,
375
+ skillThreshold: 3,
376
+ skillModel: "claude-haiku-4-5-20251001",
377
+ exeHeartbeat: {
378
+ enabled: true,
379
+ intervalSeconds: 60,
380
+ staleInProgressThresholdHours: 2
381
+ },
382
+ sessionLifecycle: {
383
+ idleKillEnabled: true,
384
+ idleKillTicksRequired: 3,
385
+ idleKillIntercomAckWindowMs: 1e4,
386
+ maxAutoInstances: 10
387
+ },
388
+ autoUpdate: {
389
+ checkOnBoot: true,
390
+ autoInstall: false,
391
+ checkIntervalMs: 24 * 60 * 60 * 1e3
392
+ }
393
+ };
394
+ CONFIG_MIGRATIONS = [
395
+ {
396
+ from: 0,
397
+ to: 1,
398
+ migrate: (cfg) => {
399
+ cfg.config_version = 1;
400
+ return cfg;
401
+ }
402
+ }
403
+ ];
404
+ }
405
+ });
406
+
407
+ // src/lib/employees.ts
408
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
409
+ import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
410
+ import { execSync as execSync3 } from "child_process";
411
+ import path4 from "path";
412
+ import os2 from "os";
413
+ function normalizeRole(role) {
414
+ return (role ?? "").trim().toLowerCase();
415
+ }
416
+ function isCoordinatorRole(role) {
417
+ return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
418
+ }
419
+ function getCoordinatorEmployee(employees) {
420
+ return employees.find((e) => isCoordinatorRole(e.role));
421
+ }
422
+ function getCoordinatorName(employees = loadEmployeesSync()) {
423
+ return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
424
+ }
425
+ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
426
+ if (!agentName) return false;
427
+ return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
428
+ }
429
+ function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
430
+ return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
431
+ }
432
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
433
+ if (!existsSync3(employeesPath)) return [];
434
+ try {
435
+ return JSON.parse(readFileSync3(employeesPath, "utf-8"));
436
+ } catch {
437
+ return [];
438
+ }
439
+ }
440
+ function getEmployee(employees, name) {
441
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
442
+ }
443
+ function isMultiInstance(agentName, employees) {
444
+ const roster = employees ?? loadEmployeesSync();
445
+ const emp = getEmployee(roster, agentName);
446
+ if (!emp) return false;
447
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
448
+ }
449
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
450
+ var init_employees = __esm({
451
+ "src/lib/employees.ts"() {
452
+ "use strict";
453
+ init_config();
454
+ EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
455
+ DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
456
+ COORDINATOR_ROLE = "COO";
457
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
458
+ }
459
+ });
460
+
167
461
  // src/lib/database.ts
168
462
  var database_exports = {};
169
463
  __export(database_exports, {
@@ -311,22 +605,24 @@ async function ensureSchema() {
311
605
  ON behaviors(agent_id, active);
312
606
  `);
313
607
  try {
608
+ const coordinatorName = getCoordinatorName();
314
609
  const existing = await client.execute({
315
- sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = 'exe'",
316
- args: []
610
+ sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
611
+ args: [coordinatorName]
317
612
  });
318
613
  if (Number(existing.rows[0]?.cnt) === 0) {
319
- await client.executeMultiple(`
320
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
321
- VALUES
322
- (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');
323
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
324
- VALUES
325
- (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');
326
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
327
- VALUES
328
- (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');
329
- `);
614
+ const seededAt = "2026-03-25T00:00:00Z";
615
+ for (const [domain, content] of [
616
+ ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
617
+ ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
618
+ ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
619
+ ]) {
620
+ await client.execute({
621
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
622
+ VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
623
+ args: [coordinatorName, domain, content, seededAt, seededAt]
624
+ });
625
+ }
330
626
  }
331
627
  } catch {
332
628
  }
@@ -993,296 +1289,82 @@ async function ensureSchema() {
993
1289
  await client.execute({
994
1290
  sql: `UPDATE memories SET tier = 2 WHERE tool_name IN ('store_memory', 'manual') AND importance >= 5 AND tier = 3`,
995
1291
  args: []
996
- });
997
- } catch {
998
- }
999
- try {
1000
- await client.execute({
1001
- sql: `ALTER TABLE memories ADD COLUMN supersedes_id TEXT`,
1002
- args: []
1003
- });
1004
- } catch {
1005
- }
1006
- try {
1007
- await client.execute(
1008
- `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
1009
- );
1010
- } catch {
1011
- }
1012
- for (const col of [
1013
- "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
1014
- "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
1015
- ]) {
1016
- try {
1017
- await client.execute(col);
1018
- } catch {
1019
- }
1020
- }
1021
- }
1022
- async function disposeDatabase() {
1023
- if (_client) {
1024
- _client.close();
1025
- _client = null;
1026
- _resilientClient = null;
1027
- }
1028
- }
1029
- var _client, _resilientClient, initTurso, disposeTurso;
1030
- var init_database = __esm({
1031
- "src/lib/database.ts"() {
1032
- "use strict";
1033
- init_db_retry();
1034
- _client = null;
1035
- _resilientClient = null;
1036
- initTurso = initDatabase;
1037
- disposeTurso = disposeDatabase;
1038
- }
1039
- });
1040
-
1041
- // src/lib/config.ts
1042
- var config_exports = {};
1043
- __export(config_exports, {
1044
- CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
1045
- CONFIG_PATH: () => CONFIG_PATH,
1046
- COO_AGENT_NAME: () => COO_AGENT_NAME,
1047
- CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
1048
- DB_PATH: () => DB_PATH,
1049
- EXE_AI_DIR: () => EXE_AI_DIR,
1050
- LEGACY_LANCE_PATH: () => LEGACY_LANCE_PATH,
1051
- MODELS_DIR: () => MODELS_DIR,
1052
- loadConfig: () => loadConfig,
1053
- loadConfigFrom: () => loadConfigFrom,
1054
- loadConfigSync: () => loadConfigSync,
1055
- migrateConfig: () => migrateConfig,
1056
- saveConfig: () => saveConfig
1057
- });
1058
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
1059
- import { readFileSync as readFileSync2, existsSync as existsSync3, renameSync } from "fs";
1060
- import path4 from "path";
1061
- import os2 from "os";
1062
- function resolveDataDir() {
1063
- if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
1064
- if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
1065
- const newDir = path4.join(os2.homedir(), ".exe-os");
1066
- const legacyDir = path4.join(os2.homedir(), ".exe-mem");
1067
- if (!existsSync3(newDir) && existsSync3(legacyDir)) {
1068
- try {
1069
- renameSync(legacyDir, newDir);
1070
- process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
1071
- `);
1072
- } catch {
1073
- return legacyDir;
1074
- }
1075
- }
1076
- return newDir;
1077
- }
1078
- function migrateLegacyConfig(raw) {
1079
- if ("r2" in raw) {
1080
- process.stderr.write(
1081
- "[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"
1082
- );
1083
- delete raw.r2;
1084
- }
1085
- if ("syncIntervalMs" in raw) {
1086
- delete raw.syncIntervalMs;
1087
- }
1088
- return raw;
1089
- }
1090
- function migrateConfig(raw) {
1091
- const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
1092
- let currentVersion = fromVersion;
1093
- let migrated = false;
1094
- if (currentVersion > CURRENT_CONFIG_VERSION) {
1095
- return { config: raw, migrated: false, fromVersion };
1096
- }
1097
- for (const migration of CONFIG_MIGRATIONS) {
1098
- if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
1099
- raw = migration.migrate(raw);
1100
- currentVersion = migration.to;
1101
- migrated = true;
1102
- }
1103
- }
1104
- return { config: raw, migrated, fromVersion };
1105
- }
1106
- function normalizeScalingRoadmap(raw) {
1107
- const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
1108
- const userRoadmap = raw.scalingRoadmap ?? {};
1109
- const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
1110
- if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
1111
- userAuto.enabled = raw.rerankerEnabled;
1112
- }
1113
- raw.scalingRoadmap = {
1114
- ...userRoadmap,
1115
- rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
1116
- };
1117
- }
1118
- function normalizeSessionLifecycle(raw) {
1119
- const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
1120
- const userSL = raw.sessionLifecycle ?? {};
1121
- raw.sessionLifecycle = { ...defaultSL, ...userSL };
1122
- }
1123
- function normalizeAutoUpdate(raw) {
1124
- const defaultAU = DEFAULT_CONFIG.autoUpdate;
1125
- const userAU = raw.autoUpdate ?? {};
1126
- raw.autoUpdate = { ...defaultAU, ...userAU };
1127
- }
1128
- async function loadConfig() {
1129
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1130
- await mkdir2(dir, { recursive: true });
1131
- const configPath = path4.join(dir, "config.json");
1132
- if (!existsSync3(configPath)) {
1133
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
1292
+ });
1293
+ } catch {
1134
1294
  }
1135
- const raw = await readFile2(configPath, "utf-8");
1136
1295
  try {
1137
- let parsed = JSON.parse(raw);
1138
- parsed = migrateLegacyConfig(parsed);
1139
- const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
1140
- if (migrated) {
1141
- process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
1142
- `);
1143
- try {
1144
- await writeFile2(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
1145
- } catch {
1146
- }
1147
- }
1148
- normalizeScalingRoadmap(migratedCfg);
1149
- normalizeSessionLifecycle(migratedCfg);
1150
- normalizeAutoUpdate(migratedCfg);
1151
- const config = { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db"), ...migratedCfg };
1152
- if (config.dbPath.startsWith("~")) {
1153
- config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
1296
+ await client.execute({
1297
+ sql: `ALTER TABLE memories ADD COLUMN supersedes_id TEXT`,
1298
+ args: []
1299
+ });
1300
+ } catch {
1301
+ }
1302
+ try {
1303
+ await client.execute(
1304
+ `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
1305
+ );
1306
+ } catch {
1307
+ }
1308
+ for (const col of [
1309
+ "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
1310
+ "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
1311
+ ]) {
1312
+ try {
1313
+ await client.execute(col);
1314
+ } catch {
1154
1315
  }
1155
- return config;
1316
+ }
1317
+ try {
1318
+ await client.execute({
1319
+ sql: `ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0`,
1320
+ args: []
1321
+ });
1156
1322
  } catch {
1157
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
1158
1323
  }
1159
- }
1160
- function loadConfigSync() {
1161
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1162
- const configPath = path4.join(dir, "config.json");
1163
- if (!existsSync3(configPath)) {
1164
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
1324
+ try {
1325
+ await client.execute(
1326
+ `CREATE INDEX IF NOT EXISTS idx_memories_draft ON memories(draft) WHERE draft = 1`
1327
+ );
1328
+ } catch {
1165
1329
  }
1166
1330
  try {
1167
- const raw = readFileSync2(configPath, "utf-8");
1168
- let parsed = JSON.parse(raw);
1169
- parsed = migrateLegacyConfig(parsed);
1170
- const { config: migratedCfg } = migrateConfig(parsed);
1171
- normalizeScalingRoadmap(migratedCfg);
1172
- normalizeSessionLifecycle(migratedCfg);
1173
- normalizeAutoUpdate(migratedCfg);
1174
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db"), ...migratedCfg };
1331
+ await client.execute({
1332
+ sql: `ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'`,
1333
+ args: []
1334
+ });
1175
1335
  } catch {
1176
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
1177
1336
  }
1178
- }
1179
- async function saveConfig(config) {
1180
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1181
- await mkdir2(dir, { recursive: true });
1182
- const configPath = path4.join(dir, "config.json");
1183
- await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n");
1184
- if (config.cloud?.apiKey) {
1185
- await chmod2(configPath, 384);
1337
+ try {
1338
+ await client.execute(
1339
+ `CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
1340
+ );
1341
+ } catch {
1186
1342
  }
1187
- }
1188
- async function loadConfigFrom(configPath) {
1189
- const raw = await readFile2(configPath, "utf-8");
1190
1343
  try {
1191
- let parsed = JSON.parse(raw);
1192
- parsed = migrateLegacyConfig(parsed);
1193
- const { config: migratedCfg } = migrateConfig(parsed);
1194
- normalizeScalingRoadmap(migratedCfg);
1195
- normalizeSessionLifecycle(migratedCfg);
1196
- normalizeAutoUpdate(migratedCfg);
1197
- return { ...DEFAULT_CONFIG, ...migratedCfg };
1344
+ await client.execute({
1345
+ sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
1346
+ args: []
1347
+ });
1198
1348
  } catch {
1199
- return { ...DEFAULT_CONFIG };
1200
1349
  }
1201
1350
  }
1202
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1203
- var init_config = __esm({
1204
- "src/lib/config.ts"() {
1351
+ async function disposeDatabase() {
1352
+ if (_client) {
1353
+ _client.close();
1354
+ _client = null;
1355
+ _resilientClient = null;
1356
+ }
1357
+ }
1358
+ var _client, _resilientClient, initTurso, disposeTurso;
1359
+ var init_database = __esm({
1360
+ "src/lib/database.ts"() {
1205
1361
  "use strict";
1206
- EXE_AI_DIR = resolveDataDir();
1207
- DB_PATH = path4.join(EXE_AI_DIR, "memories.db");
1208
- MODELS_DIR = path4.join(EXE_AI_DIR, "models");
1209
- CONFIG_PATH = path4.join(EXE_AI_DIR, "config.json");
1210
- COO_AGENT_NAME = "exe";
1211
- LEGACY_LANCE_PATH = path4.join(EXE_AI_DIR, "local.lance");
1212
- CURRENT_CONFIG_VERSION = 1;
1213
- DEFAULT_CONFIG = {
1214
- config_version: CURRENT_CONFIG_VERSION,
1215
- dbPath: DB_PATH,
1216
- modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
1217
- embeddingDim: 1024,
1218
- batchSize: 20,
1219
- flushIntervalMs: 1e4,
1220
- autoIngestion: true,
1221
- autoRetrieval: true,
1222
- searchMode: "hybrid",
1223
- hookSearchMode: "hybrid",
1224
- fileGrepEnabled: true,
1225
- splashEffect: true,
1226
- consolidationEnabled: true,
1227
- consolidationIntervalMs: 6 * 60 * 60 * 1e3,
1228
- consolidationModel: "claude-haiku-4-5-20251001",
1229
- consolidationMaxCallsPerRun: 20,
1230
- selfQueryRouter: true,
1231
- selfQueryModel: "claude-haiku-4-5-20251001",
1232
- rerankerEnabled: true,
1233
- scalingRoadmap: {
1234
- rerankerAutoTrigger: {
1235
- enabled: true,
1236
- broadQueryMinCardinality: 5e4,
1237
- fetchTopK: 150,
1238
- returnTopK: 5
1239
- }
1240
- },
1241
- graphRagEnabled: true,
1242
- wikiEnabled: false,
1243
- wikiUrl: "",
1244
- wikiApiKey: "",
1245
- wikiSyncIntervalMs: 30 * 60 * 1e3,
1246
- wikiWorkspaceMapping: {
1247
- exe: "Executive",
1248
- yoshi: "Engineering",
1249
- mari: "Marketing",
1250
- tom: "Engineering",
1251
- sasha: "Production"
1252
- },
1253
- wikiAutoUpdate: true,
1254
- wikiAutoUpdateThreshold: 0.5,
1255
- wikiAutoUpdateCreateNew: true,
1256
- skillLearning: true,
1257
- skillThreshold: 3,
1258
- skillModel: "claude-haiku-4-5-20251001",
1259
- exeHeartbeat: {
1260
- enabled: true,
1261
- intervalSeconds: 60,
1262
- staleInProgressThresholdHours: 2
1263
- },
1264
- sessionLifecycle: {
1265
- idleKillEnabled: true,
1266
- idleKillTicksRequired: 3,
1267
- idleKillIntercomAckWindowMs: 1e4,
1268
- maxAutoInstances: 10
1269
- },
1270
- autoUpdate: {
1271
- checkOnBoot: true,
1272
- autoInstall: false,
1273
- checkIntervalMs: 24 * 60 * 60 * 1e3
1274
- }
1275
- };
1276
- CONFIG_MIGRATIONS = [
1277
- {
1278
- from: 0,
1279
- to: 1,
1280
- migrate: (cfg) => {
1281
- cfg.config_version = 1;
1282
- return cfg;
1283
- }
1284
- }
1285
- ];
1362
+ init_db_retry();
1363
+ init_employees();
1364
+ _client = null;
1365
+ _resilientClient = null;
1366
+ initTurso = initDatabase;
1367
+ disposeTurso = disposeDatabase;
1286
1368
  }
1287
1369
  });
1288
1370
 
@@ -1354,12 +1436,12 @@ __export(shard_manager_exports, {
1354
1436
  listShards: () => listShards,
1355
1437
  shardExists: () => shardExists
1356
1438
  });
1357
- import path5 from "path";
1358
- import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync2 } from "fs";
1439
+ import path6 from "path";
1440
+ import { existsSync as existsSync5, mkdirSync, readdirSync as readdirSync2 } from "fs";
1359
1441
  import { createClient as createClient2 } from "@libsql/client";
1360
1442
  function initShardManager(encryptionKey) {
1361
1443
  _encryptionKey = encryptionKey;
1362
- if (!existsSync4(SHARDS_DIR)) {
1444
+ if (!existsSync5(SHARDS_DIR)) {
1363
1445
  mkdirSync(SHARDS_DIR, { recursive: true });
1364
1446
  }
1365
1447
  _shardingEnabled = true;
@@ -1380,7 +1462,7 @@ function getShardClient(projectName) {
1380
1462
  }
1381
1463
  const cached = _shards.get(safeName);
1382
1464
  if (cached) return cached;
1383
- const dbPath = path5.join(SHARDS_DIR, `${safeName}.db`);
1465
+ const dbPath = path6.join(SHARDS_DIR, `${safeName}.db`);
1384
1466
  const client = createClient2({
1385
1467
  url: `file:${dbPath}`,
1386
1468
  encryptionKey: _encryptionKey
@@ -1390,10 +1472,10 @@ function getShardClient(projectName) {
1390
1472
  }
1391
1473
  function shardExists(projectName) {
1392
1474
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
1393
- return existsSync4(path5.join(SHARDS_DIR, `${safeName}.db`));
1475
+ return existsSync5(path6.join(SHARDS_DIR, `${safeName}.db`));
1394
1476
  }
1395
1477
  function listShards() {
1396
- if (!existsSync4(SHARDS_DIR)) return [];
1478
+ if (!existsSync5(SHARDS_DIR)) return [];
1397
1479
  return readdirSync2(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
1398
1480
  }
1399
1481
  async function ensureShardSchema(client) {
@@ -1463,7 +1545,11 @@ async function ensureShardSchema(client) {
1463
1545
  "ALTER TABLE memories ADD COLUMN source_path TEXT",
1464
1546
  "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
1465
1547
  "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
1466
- "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
1548
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
1549
+ // MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
1550
+ "ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
1551
+ "ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
1552
+ "ALTER TABLE memories ADD COLUMN trajectory TEXT"
1467
1553
  ]) {
1468
1554
  try {
1469
1555
  await client.execute(col);
@@ -1575,7 +1661,7 @@ var init_shard_manager = __esm({
1575
1661
  "src/lib/shard-manager.ts"() {
1576
1662
  "use strict";
1577
1663
  init_config();
1578
- SHARDS_DIR = path5.join(EXE_AI_DIR, "shards");
1664
+ SHARDS_DIR = path6.join(EXE_AI_DIR, "shards");
1579
1665
  _shards = /* @__PURE__ */ new Map();
1580
1666
  _encryptionKey = null;
1581
1667
  _shardingEnabled = false;
@@ -1593,26 +1679,26 @@ var init_platform_procedures = __esm({
1593
1679
  title: "What is exe-os \u2014 the operating model every agent must understand",
1594
1680
  domain: "architecture",
1595
1681
  priority: "p0",
1596
- 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."
1682
+ 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."
1597
1683
  },
1598
1684
  {
1599
1685
  title: "Mode 1 \u2014 how exe-os runs inside Claude Code",
1600
1686
  domain: "architecture",
1601
1687
  priority: "p0",
1602
- 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."
1688
+ 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."
1603
1689
  },
1604
1690
  {
1605
- title: "Sessions explained \u2014 what exeN means and how projects work",
1691
+ title: "Sessions explained \u2014 coordinator session names and projects",
1606
1692
  domain: "architecture",
1607
1693
  priority: "p0",
1608
- 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."
1694
+ 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."
1609
1695
  },
1610
1696
  // --- Hierarchy and dispatch ---
1611
1697
  {
1612
1698
  title: "Chain of command \u2014 who talks to whom",
1613
1699
  domain: "workflow",
1614
1700
  priority: "p0",
1615
- 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."
1701
+ 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."
1616
1702
  },
1617
1703
  {
1618
1704
  title: "Single dispatch path \u2014 create_task only",
@@ -1622,30 +1708,30 @@ var init_platform_procedures = __esm({
1622
1708
  },
1623
1709
  // --- Session isolation ---
1624
1710
  {
1625
- title: "Session scoping \u2014 stay in your exe boundary",
1711
+ title: "Session scoping \u2014 stay in your coordinator boundary",
1626
1712
  domain: "security",
1627
1713
  priority: "p0",
1628
- 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."
1714
+ 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."
1629
1715
  },
1630
1716
  {
1631
1717
  title: "Session isolation \u2014 never touch another session's work",
1632
1718
  domain: "workflow",
1633
1719
  priority: "p0",
1634
- 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.`
1720
+ 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."
1635
1721
  },
1636
1722
  // --- Engineering: session scoping in code ---
1637
1723
  {
1638
1724
  title: "Three-dimensional scoping \u2014 session, project, role \u2014 enforced in every query",
1639
1725
  domain: "architecture",
1640
1726
  priority: "p0",
1641
- 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."
1727
+ 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."
1642
1728
  },
1643
1729
  // --- Hard constraints ---
1644
1730
  {
1645
1731
  title: "What you CANNOT do in exe-os \u2014 hard constraints",
1646
1732
  domain: "security",
1647
1733
  priority: "p0",
1648
- 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."
1734
+ 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."
1649
1735
  },
1650
1736
  // --- Operations ---
1651
1737
  {
@@ -1765,13 +1851,13 @@ ${p.content}`).join("\n\n");
1765
1851
 
1766
1852
  // src/lib/notifications.ts
1767
1853
  import crypto2 from "crypto";
1768
- import path6 from "path";
1769
- import os3 from "os";
1854
+ import path7 from "path";
1855
+ import os4 from "os";
1770
1856
  import {
1771
- readFileSync as readFileSync3,
1857
+ readFileSync as readFileSync4,
1772
1858
  readdirSync as readdirSync3,
1773
- unlinkSync,
1774
- existsSync as existsSync5,
1859
+ unlinkSync as unlinkSync2,
1860
+ existsSync as existsSync6,
1775
1861
  rmdirSync
1776
1862
  } from "fs";
1777
1863
  async function writeNotification(notification) {
@@ -1815,39 +1901,6 @@ var init_notifications = __esm({
1815
1901
  }
1816
1902
  });
1817
1903
 
1818
- // src/lib/employees.ts
1819
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1820
- import { existsSync as existsSync6, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
1821
- import { execSync as execSync3 } from "child_process";
1822
- import path7 from "path";
1823
- import os4 from "os";
1824
- function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
1825
- if (!existsSync6(employeesPath)) return [];
1826
- try {
1827
- return JSON.parse(readFileSync4(employeesPath, "utf-8"));
1828
- } catch {
1829
- return [];
1830
- }
1831
- }
1832
- function getEmployee(employees, name) {
1833
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
1834
- }
1835
- function isMultiInstance(agentName, employees) {
1836
- const roster = employees ?? loadEmployeesSync();
1837
- const emp = getEmployee(roster, agentName);
1838
- if (!emp) return false;
1839
- return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
1840
- }
1841
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
1842
- var init_employees = __esm({
1843
- "src/lib/employees.ts"() {
1844
- "use strict";
1845
- init_config();
1846
- EMPLOYEES_PATH = path7.join(EXE_AI_DIR, "exe-employees.json");
1847
- MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
1848
- }
1849
- });
1850
-
1851
1904
  // src/lib/license.ts
1852
1905
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
1853
1906
  import { randomUUID as randomUUID2 } from "crypto";
@@ -2265,6 +2318,10 @@ function spawnDaemon() {
2265
2318
  stdio: ["ignore", "ignore", stderrFd],
2266
2319
  env: {
2267
2320
  ...process.env,
2321
+ TMUX: void 0,
2322
+ // Daemon is global — must not inherit session scope
2323
+ TMUX_PANE: void 0,
2324
+ // Prevents resolveExeSession() from scoping to one session
2268
2325
  EXE_DAEMON_SOCK: SOCKET_PATH,
2269
2326
  EXE_DAEMON_PID: PID_PATH
2270
2327
  }
@@ -2971,7 +3028,7 @@ function _resetLastRelaunchCache() {
2971
3028
  }
2972
3029
  async function lastResumeCreatedAtMs(agentId) {
2973
3030
  const client = getClient();
2974
- const cmScope = sessionScopeFilter();
3031
+ const cmScope = sessionScopeFilter(null);
2975
3032
  const result = await client.execute({
2976
3033
  sql: `SELECT MAX(created_at) AS last_created_at
2977
3034
  FROM tasks
@@ -2996,7 +3053,7 @@ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
2996
3053
  const client = getClient();
2997
3054
  const now = (/* @__PURE__ */ new Date()).toISOString();
2998
3055
  const context = buildResumeContext(agentId, openTasks);
2999
- const rdScope = sessionScopeFilter();
3056
+ const rdScope = sessionScopeFilter(null);
3000
3057
  const existing = await client.execute({
3001
3058
  sql: `SELECT id FROM tasks
3002
3059
  WHERE assigned_to = ?
@@ -3030,7 +3087,7 @@ async function pollCapacityDead() {
3030
3087
  const transport = getTransport();
3031
3088
  const relaunched = [];
3032
3089
  const registered = listSessions().filter(
3033
- (s) => s.agentId !== "exe"
3090
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
3034
3091
  );
3035
3092
  if (registered.length === 0) return [];
3036
3093
  let liveSessions;
@@ -3090,7 +3147,7 @@ async function pollCapacityDead() {
3090
3147
  reason: "capacity"
3091
3148
  });
3092
3149
  const client = getClient();
3093
- const rlScope = sessionScopeFilter();
3150
+ const rlScope = sessionScopeFilter(null);
3094
3151
  const openTasks = await client.execute({
3095
3152
  sql: `SELECT id, title, priority, task_file, status
3096
3153
  FROM tasks
@@ -3144,6 +3201,7 @@ var init_capacity_monitor = __esm({
3144
3201
  init_session_kill_telemetry();
3145
3202
  init_tmux_routing();
3146
3203
  init_task_scope();
3204
+ init_employees();
3147
3205
  CAPACITY_PATTERNS = [
3148
3206
  /conversation is too long/i,
3149
3207
  /maximum context length/i,
@@ -3293,7 +3351,7 @@ function employeeSessionName(employee, exeSession, instance) {
3293
3351
  exeSession = root;
3294
3352
  } else {
3295
3353
  throw new Error(
3296
- `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
3354
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3297
3355
  );
3298
3356
  }
3299
3357
  }
@@ -3313,8 +3371,10 @@ function parseParentExe(sessionName, agentId) {
3313
3371
  return match?.[1] ?? null;
3314
3372
  }
3315
3373
  function extractRootExe(name) {
3316
- const match = name.match(/(exe\d+)$/);
3317
- return match?.[1] ?? null;
3374
+ if (!name) return null;
3375
+ if (!name.includes("-")) return name;
3376
+ const parts = name.split("-").filter(Boolean);
3377
+ return parts.length > 0 ? parts[parts.length - 1] : null;
3318
3378
  }
3319
3379
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3320
3380
  if (!existsSync12(SESSION_CACHE)) {
@@ -3459,12 +3519,14 @@ function isSessionBusy(sessionName) {
3459
3519
  return state === "thinking" || state === "tool";
3460
3520
  }
3461
3521
  function isExeSession(sessionName) {
3462
- return /^exe\d*$/.test(sessionName);
3522
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
3523
+ const coordinatorName = getCoordinatorName();
3524
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
3463
3525
  }
3464
3526
  function sendIntercom(targetSession) {
3465
3527
  const transport = getTransport();
3466
3528
  if (isExeSession(targetSession)) {
3467
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
3529
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
3468
3530
  return "skipped_exe";
3469
3531
  }
3470
3532
  if (isDebounced(targetSession)) {
@@ -3516,7 +3578,7 @@ function notifyParentExe(sessionKey) {
3516
3578
  if (result === "failed") {
3517
3579
  const rootExe = resolveExeSession();
3518
3580
  if (rootExe && rootExe !== target) {
3519
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3581
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
3520
3582
  `);
3521
3583
  const fallback = sendIntercom(rootExe);
3522
3584
  return fallback !== "failed";
@@ -3526,8 +3588,8 @@ function notifyParentExe(sessionKey) {
3526
3588
  return true;
3527
3589
  }
3528
3590
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3529
- if (employeeName === "exe") {
3530
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
3591
+ if (employeeName === "exe" || isCoordinatorName(employeeName)) {
3592
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3531
3593
  }
3532
3594
  try {
3533
3595
  assertEmployeeLimitSync();
@@ -3536,8 +3598,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3536
3598
  return { status: "failed", sessionName: "", error: err.message };
3537
3599
  }
3538
3600
  }
3539
- if (/-exe\d*$/.test(employeeName)) {
3540
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
3601
+ if (employeeName.includes("-")) {
3602
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
3541
3603
  return {
3542
3604
  status: "failed",
3543
3605
  sessionName: "",
@@ -3556,7 +3618,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3556
3618
  return {
3557
3619
  status: "failed",
3558
3620
  sessionName: "",
3559
- error: `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
3621
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3560
3622
  };
3561
3623
  }
3562
3624
  }
@@ -3713,8 +3775,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3713
3775
  const ctxContent = [
3714
3776
  `## Session Context`,
3715
3777
  `You are running in tmux session: ${sessionName}.`,
3716
- `Your parent exe session is ${exeSession}.`,
3717
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
3778
+ `Your parent coordinator session is ${exeSession}.`,
3779
+ `Your employees (if any) use the -${exeSession} suffix.`
3718
3780
  ].join("\n");
3719
3781
  writeFileSync5(ctxFile, ctxContent);
3720
3782
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
@@ -3818,6 +3880,7 @@ var init_tmux_routing = __esm({
3818
3880
  init_provider_table();
3819
3881
  init_intercom_queue();
3820
3882
  init_plan_limits();
3883
+ init_employees();
3821
3884
  SPAWN_LOCK_DIR = path13.join(os7.homedir(), ".exe-os", "spawn-locks");
3822
3885
  SESSION_CACHE = path13.join(os7.homedir(), ".exe-os", "session-cache");
3823
3886
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
@@ -4100,6 +4163,36 @@ async function listTasks(input2) {
4100
4163
  tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
4101
4164
  }));
4102
4165
  }
4166
+ function isTmuxSessionAlive(identifier) {
4167
+ if (!identifier || identifier === "unknown") return true;
4168
+ try {
4169
+ if (identifier.startsWith("%")) {
4170
+ const output = execSync7("tmux list-panes -a -F '#{pane_id}'", {
4171
+ timeout: 2e3,
4172
+ encoding: "utf8",
4173
+ stdio: ["pipe", "pipe", "pipe"]
4174
+ });
4175
+ return output.split("\n").some((l) => l.trim() === identifier);
4176
+ } else {
4177
+ execSync7(`tmux has-session -t ${JSON.stringify(identifier)}`, {
4178
+ timeout: 2e3,
4179
+ stdio: ["pipe", "pipe", "pipe"]
4180
+ });
4181
+ return true;
4182
+ }
4183
+ } catch {
4184
+ if (identifier.startsWith("%")) return true;
4185
+ try {
4186
+ execSync7("tmux list-sessions", {
4187
+ timeout: 2e3,
4188
+ stdio: ["pipe", "pipe", "pipe"]
4189
+ });
4190
+ return false;
4191
+ } catch {
4192
+ return true;
4193
+ }
4194
+ }
4195
+ }
4103
4196
  function checkStaleCompletion(taskContext, taskCreatedAt) {
4104
4197
  if (!taskContext) return null;
4105
4198
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -4162,13 +4255,59 @@ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
4162
4255
  });
4163
4256
  if (claim.rowsAffected === 0) {
4164
4257
  const current = await client.execute({
4165
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
4258
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
4166
4259
  args: [taskId]
4167
4260
  });
4168
4261
  const cur = current.rows[0];
4169
- const status = cur?.status ?? "unknown";
4170
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
4171
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
4262
+ const curStatus = cur?.status ?? "unknown";
4263
+ const claimedBySession = cur?.assigned_tmux ?? "";
4264
+ const assignedBy = cur?.assigned_by ?? "";
4265
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
4266
+ process.stderr.write(
4267
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
4268
+ `
4269
+ );
4270
+ await client.execute({
4271
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
4272
+ args: [now, taskId]
4273
+ });
4274
+ const retried = await client.execute({
4275
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
4276
+ args: [tmuxSession, now, taskId]
4277
+ });
4278
+ if (retried.rowsAffected > 0) {
4279
+ try {
4280
+ await writeCheckpoint({
4281
+ taskId,
4282
+ step: "reclaimed_dead_session",
4283
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
4284
+ });
4285
+ } catch {
4286
+ }
4287
+ return { row, taskFile, now, taskId };
4288
+ }
4289
+ }
4290
+ if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || input2.callerAgentId === "exe")) {
4291
+ process.stderr.write(
4292
+ `[tasks] Assigner override: ${input2.callerAgentId} reclaiming ${taskId}
4293
+ `
4294
+ );
4295
+ await client.execute({
4296
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
4297
+ args: [tmuxSession, now, taskId]
4298
+ });
4299
+ try {
4300
+ await writeCheckpoint({
4301
+ taskId,
4302
+ step: "assigner_override",
4303
+ contextSummary: `Task force-reclaimed by assigner ${input2.callerAgentId}.`
4304
+ });
4305
+ } catch {
4306
+ }
4307
+ return { row, taskFile, now, taskId };
4308
+ }
4309
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
4310
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
4172
4311
  }
4173
4312
  try {
4174
4313
  await writeCheckpoint({
@@ -4266,7 +4405,7 @@ var init_tasks_crud = __esm({
4266
4405
  "use strict";
4267
4406
  init_database();
4268
4407
  init_task_scope();
4269
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
4408
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
4270
4409
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4271
4410
  }
4272
4411
  });
@@ -4581,7 +4720,7 @@ function findSessionForProject(projectName) {
4581
4720
  const sessions = listSessions();
4582
4721
  for (const s of sessions) {
4583
4722
  const proj = s.projectDir.split("/").filter(Boolean).pop();
4584
- if (proj === projectName && s.agentId === "exe") return s;
4723
+ if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
4585
4724
  }
4586
4725
  return null;
4587
4726
  }
@@ -4621,12 +4760,13 @@ var init_session_scope = __esm({
4621
4760
  init_session_registry();
4622
4761
  init_project_name();
4623
4762
  init_tmux_routing();
4763
+ init_employees();
4624
4764
  }
4625
4765
  });
4626
4766
 
4627
4767
  // src/lib/tasks-notify.ts
4628
4768
  async function dispatchTaskToEmployee(input2) {
4629
- if (input2.assignedTo === "exe") return { dispatched: "skipped" };
4769
+ if (input2.assignedTo === "exe" || isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
4630
4770
  let crossProject = false;
4631
4771
  if (input2.projectName) {
4632
4772
  try {
@@ -5069,6 +5209,24 @@ async function updateTask(input2) {
5069
5209
  });
5070
5210
  } catch {
5071
5211
  }
5212
+ const assignedAgent = String(row.assigned_to);
5213
+ if (!isCoordinatorName(assignedAgent)) {
5214
+ try {
5215
+ const draftClient = getClient();
5216
+ if (input2.status === "done") {
5217
+ await draftClient.execute({
5218
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
5219
+ args: [assignedAgent]
5220
+ });
5221
+ } else if (input2.status === "cancelled") {
5222
+ await draftClient.execute({
5223
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
5224
+ args: [assignedAgent]
5225
+ });
5226
+ }
5227
+ } catch {
5228
+ }
5229
+ }
5072
5230
  try {
5073
5231
  const client = getClient();
5074
5232
  const cascaded = await client.execute({
@@ -5087,8 +5245,8 @@ async function updateTask(input2) {
5087
5245
  }
5088
5246
  const isTerminal = input2.status === "done" || input2.status === "needs_review";
5089
5247
  if (isTerminal) {
5090
- const isExe = String(row.assigned_to) === "exe";
5091
- if (!isExe) {
5248
+ const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
5249
+ if (!isCoordinator) {
5092
5250
  notifyTaskDone();
5093
5251
  }
5094
5252
  await markTaskNotificationsRead(taskFile);
@@ -5112,7 +5270,7 @@ async function updateTask(input2) {
5112
5270
  }
5113
5271
  }
5114
5272
  }
5115
- if (input2.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
5273
+ if (input2.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
5116
5274
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
5117
5275
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
5118
5276
  taskId,
@@ -5128,7 +5286,7 @@ async function updateTask(input2) {
5128
5286
  });
5129
5287
  }
5130
5288
  let nextTask;
5131
- if (isTerminal && String(row.assigned_to) !== "exe") {
5289
+ if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
5132
5290
  try {
5133
5291
  nextTask = await findNextTask(String(row.assigned_to));
5134
5292
  } catch {
@@ -5155,12 +5313,14 @@ async function updateTask(input2) {
5155
5313
  async function deleteTask(taskId, baseDir) {
5156
5314
  const client = getClient();
5157
5315
  const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
5158
- const reviewer = assignedBy || "exe";
5316
+ const coordinatorName = getCoordinatorName();
5317
+ const reviewer = assignedBy || coordinatorName;
5159
5318
  const reviewSlug = `review-${assignedTo}-${taskSlug}`;
5160
5319
  const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
5320
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
5161
5321
  await client.execute({
5162
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
5163
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
5322
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
5323
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
5164
5324
  });
5165
5325
  await markAsReadByTaskFile(taskFile);
5166
5326
  await markAsReadByTaskFile(reviewFile);
@@ -5172,6 +5332,7 @@ var init_tasks = __esm({
5172
5332
  init_config();
5173
5333
  init_notifications();
5174
5334
  init_state_bus();
5335
+ init_employees();
5175
5336
  init_tasks_crud();
5176
5337
  init_tasks_review();
5177
5338
  init_tasks_crud();
@@ -5406,17 +5567,17 @@ init_memory();
5406
5567
  init_database();
5407
5568
 
5408
5569
  // src/lib/keychain.ts
5409
- import { readFile, writeFile, unlink, mkdir, chmod } from "fs/promises";
5410
- import { existsSync as existsSync2 } from "fs";
5411
- import path3 from "path";
5412
- import os from "os";
5570
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
5571
+ import { existsSync as existsSync4 } from "fs";
5572
+ import path5 from "path";
5573
+ import os3 from "os";
5413
5574
  var SERVICE = "exe-mem";
5414
5575
  var ACCOUNT = "master-key";
5415
5576
  function getKeyDir() {
5416
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path3.join(os.homedir(), ".exe-os");
5577
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path5.join(os3.homedir(), ".exe-os");
5417
5578
  }
5418
5579
  function getKeyPath() {
5419
- return path3.join(getKeyDir(), "master.key");
5580
+ return path5.join(getKeyDir(), "master.key");
5420
5581
  }
5421
5582
  async function tryKeytar() {
5422
5583
  try {
@@ -5437,11 +5598,11 @@ async function getMasterKey() {
5437
5598
  }
5438
5599
  }
5439
5600
  const keyPath = getKeyPath();
5440
- if (!existsSync2(keyPath)) {
5601
+ if (!existsSync4(keyPath)) {
5441
5602
  return null;
5442
5603
  }
5443
5604
  try {
5444
- const content = await readFile(keyPath, "utf-8");
5605
+ const content = await readFile3(keyPath, "utf-8");
5445
5606
  return Buffer.from(content.trim(), "base64");
5446
5607
  } catch {
5447
5608
  return null;
@@ -5564,7 +5725,10 @@ async function writeMemory(record) {
5564
5725
  source_path: record.source_path ?? null,
5565
5726
  source_type: record.source_type ?? null,
5566
5727
  tier: record.tier ?? classifyTier(record),
5567
- supersedes_id: record.supersedes_id ?? null
5728
+ supersedes_id: record.supersedes_id ?? null,
5729
+ draft: record.draft ? 1 : 0,
5730
+ memory_type: record.memory_type ?? "raw",
5731
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
5568
5732
  };
5569
5733
  _pendingRecords.push(dbRow);
5570
5734
  orgBus.emit({
@@ -5619,6 +5783,9 @@ async function flushBatch() {
5619
5783
  const sourceType = row.source_type ?? null;
5620
5784
  const tier = row.tier ?? 3;
5621
5785
  const supersedesId = row.supersedes_id ?? null;
5786
+ const draft = row.draft ? 1 : 0;
5787
+ const memoryType = row.memory_type ?? "raw";
5788
+ const trajectory = row.trajectory ?? null;
5622
5789
  return {
5623
5790
  sql: hasVector ? `INSERT OR IGNORE INTO memories
5624
5791
  (id, agent_id, agent_role, session_id, timestamp,
@@ -5626,15 +5793,15 @@ async function flushBatch() {
5626
5793
  has_error, raw_text, vector, version, task_id, importance, status,
5627
5794
  confidence, last_accessed,
5628
5795
  workspace_id, document_id, user_id, char_offset, page_number,
5629
- source_path, source_type, tier, supersedes_id)
5630
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
5796
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
5797
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
5631
5798
  (id, agent_id, agent_role, session_id, timestamp,
5632
5799
  tool_name, project_name,
5633
5800
  has_error, raw_text, vector, version, task_id, importance, status,
5634
5801
  confidence, last_accessed,
5635
5802
  workspace_id, document_id, user_id, char_offset, page_number,
5636
- source_path, source_type, tier, supersedes_id)
5637
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5803
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
5804
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5638
5805
  args: hasVector ? [
5639
5806
  row.id,
5640
5807
  row.agent_id,
@@ -5660,7 +5827,10 @@ async function flushBatch() {
5660
5827
  sourcePath,
5661
5828
  sourceType,
5662
5829
  tier,
5663
- supersedesId
5830
+ supersedesId,
5831
+ draft,
5832
+ memoryType,
5833
+ trajectory
5664
5834
  ] : [
5665
5835
  row.id,
5666
5836
  row.agent_id,
@@ -5685,7 +5855,10 @@ async function flushBatch() {
5685
5855
  sourcePath,
5686
5856
  sourceType,
5687
5857
  tier,
5688
- supersedesId
5858
+ supersedesId,
5859
+ draft,
5860
+ memoryType,
5861
+ trajectory
5689
5862
  ]
5690
5863
  };
5691
5864
  };
@@ -5918,6 +6091,7 @@ function extractResponseText(response) {
5918
6091
 
5919
6092
  // src/adapters/claude/hooks/ingest-worker.ts
5920
6093
  init_plan_limits();
6094
+ init_employees();
5921
6095
  process.env.EXE_EMBED_PRIORITY = "low";
5922
6096
  function assignConfidence(toolName, agentId) {
5923
6097
  if (agentId === "default") return 0.9;
@@ -5980,19 +6154,33 @@ process.stdin.on("end", async () => {
5980
6154
  } catch {
5981
6155
  }
5982
6156
  const agentId = process.env.AGENT_ID;
6157
+ const agentRole = process.env.AGENT_ROLE ?? "unknown";
6158
+ const isDraft = taskId !== null && !canCoordinate(agentId, agentRole);
6159
+ const hasError = detectError(data);
6160
+ const toolInputStr = JSON.stringify(data.tool_input ?? "").slice(0, 200);
6161
+ const toolOutputStr = JSON.stringify(data.tool_response ?? "").slice(0, 200);
6162
+ const trajectory = {
6163
+ input: toolInputStr,
6164
+ tool: data.tool_name,
6165
+ output: toolOutputStr,
6166
+ result_type: hasError ? "error" : "success"
6167
+ };
5983
6168
  await writeMemory({
5984
6169
  id: crypto7.randomUUID(),
5985
6170
  agent_id: agentId,
5986
- agent_role: process.env.AGENT_ROLE ?? "unknown",
6171
+ agent_role: agentRole,
5987
6172
  session_id: data.session_id,
5988
6173
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5989
6174
  tool_name: data.tool_name,
5990
6175
  project_name: getProjectName(data.cwd ?? process.cwd()),
5991
- has_error: detectError(data),
6176
+ has_error: hasError,
5992
6177
  raw_text: rawText,
5993
6178
  vector,
5994
6179
  task_id: taskId,
5995
- confidence: assignConfidence(data.tool_name, agentId)
6180
+ confidence: assignConfidence(data.tool_name, agentId),
6181
+ draft: isDraft,
6182
+ memory_type: "raw",
6183
+ trajectory
5996
6184
  });
5997
6185
  await flushBatch();
5998
6186
  if (needsBackfill) {
@@ -6005,8 +6193,7 @@ process.stdin.on("end", async () => {
6005
6193
  `);
6006
6194
  }
6007
6195
  }
6008
- const agentRole = process.env.AGENT_ROLE ?? "employee";
6009
- if (agentId !== "exe" && agentId !== "default" && (data.tool_name === "Edit" || data.tool_name === "Write")) {
6196
+ if (!canCoordinate(agentId, agentRole) && (data.tool_name === "Edit" || data.tool_name === "Write")) {
6010
6197
  const filePath = data.tool_input?.file_path ?? "";
6011
6198
  const taskFileMatch = filePath.match(/exe\/([^/]+)\/([^/]+\.md)$/);
6012
6199
  if (taskFileMatch) {
@@ -6074,7 +6261,7 @@ process.stdin.on("end", async () => {
6074
6261
  const priMatch = fileContent.match(/^\*\*Priority:\*\*\s*(\w+)/im);
6075
6262
  const priority = priMatch?.[1]?.toLowerCase() ?? "p1";
6076
6263
  const assignedByMatch = fileContent.match(/^\*\*Assigned by:\*\*\s*(\w+)/im);
6077
- const assignedBy = assignedByMatch?.[1] ?? "exe";
6264
+ const assignedBy = assignedByMatch?.[1] ?? getCoordinatorName();
6078
6265
  const projMatch = fileContent.match(/^\*\*Project:\*\*\s*(.+)/im);
6079
6266
  const projectName = projMatch?.[1]?.trim() ?? getProjectName(data.cwd ?? process.cwd());
6080
6267
  const now = (/* @__PURE__ */ new Date()).toISOString();