@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
@@ -58,7 +58,7 @@ function wrapWithRetry(client) {
58
58
  return (sql) => retryOnBusy(() => target.execute(sql), "execute");
59
59
  }
60
60
  if (prop === "batch") {
61
- return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
61
+ return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
62
62
  }
63
63
  return Reflect.get(target, prop, receiver);
64
64
  }
@@ -74,6 +74,387 @@ var init_db_retry = __esm({
74
74
  }
75
75
  });
76
76
 
77
+ // src/lib/config.ts
78
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
79
+ import { readFileSync, existsSync, renameSync } from "fs";
80
+ import path from "path";
81
+ import os from "os";
82
+ function resolveDataDir() {
83
+ if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
84
+ if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
85
+ const newDir = path.join(os.homedir(), ".exe-os");
86
+ const legacyDir = path.join(os.homedir(), ".exe-mem");
87
+ if (!existsSync(newDir) && existsSync(legacyDir)) {
88
+ try {
89
+ renameSync(legacyDir, newDir);
90
+ process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
91
+ `);
92
+ } catch {
93
+ return legacyDir;
94
+ }
95
+ }
96
+ return newDir;
97
+ }
98
+ function migrateLegacyConfig(raw) {
99
+ if ("r2" in raw) {
100
+ process.stderr.write(
101
+ "[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"
102
+ );
103
+ delete raw.r2;
104
+ }
105
+ if ("syncIntervalMs" in raw) {
106
+ delete raw.syncIntervalMs;
107
+ }
108
+ return raw;
109
+ }
110
+ function migrateConfig(raw) {
111
+ const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
112
+ let currentVersion = fromVersion;
113
+ let migrated = false;
114
+ if (currentVersion > CURRENT_CONFIG_VERSION) {
115
+ return { config: raw, migrated: false, fromVersion };
116
+ }
117
+ for (const migration of CONFIG_MIGRATIONS) {
118
+ if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
119
+ raw = migration.migrate(raw);
120
+ currentVersion = migration.to;
121
+ migrated = true;
122
+ }
123
+ }
124
+ return { config: raw, migrated, fromVersion };
125
+ }
126
+ function normalizeScalingRoadmap(raw) {
127
+ const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
128
+ const userRoadmap = raw.scalingRoadmap ?? {};
129
+ const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
130
+ if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
131
+ userAuto.enabled = raw.rerankerEnabled;
132
+ }
133
+ raw.scalingRoadmap = {
134
+ ...userRoadmap,
135
+ rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
136
+ };
137
+ }
138
+ function normalizeSessionLifecycle(raw) {
139
+ const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
140
+ const userSL = raw.sessionLifecycle ?? {};
141
+ raw.sessionLifecycle = { ...defaultSL, ...userSL };
142
+ }
143
+ function normalizeAutoUpdate(raw) {
144
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
145
+ const userAU = raw.autoUpdate ?? {};
146
+ raw.autoUpdate = { ...defaultAU, ...userAU };
147
+ }
148
+ async function loadConfig() {
149
+ const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
150
+ await mkdir(dir, { recursive: true });
151
+ const configPath = path.join(dir, "config.json");
152
+ if (!existsSync(configPath)) {
153
+ return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
154
+ }
155
+ const raw = await readFile(configPath, "utf-8");
156
+ try {
157
+ let parsed = JSON.parse(raw);
158
+ parsed = migrateLegacyConfig(parsed);
159
+ const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
160
+ if (migrated) {
161
+ process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
162
+ `);
163
+ try {
164
+ await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
165
+ } catch {
166
+ }
167
+ }
168
+ normalizeScalingRoadmap(migratedCfg);
169
+ normalizeSessionLifecycle(migratedCfg);
170
+ normalizeAutoUpdate(migratedCfg);
171
+ const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
172
+ if (config.dbPath.startsWith("~")) {
173
+ config.dbPath = config.dbPath.replace(/^~/, os.homedir());
174
+ }
175
+ return config;
176
+ } catch {
177
+ return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
178
+ }
179
+ }
180
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
181
+ var init_config = __esm({
182
+ "src/lib/config.ts"() {
183
+ "use strict";
184
+ EXE_AI_DIR = resolveDataDir();
185
+ DB_PATH = path.join(EXE_AI_DIR, "memories.db");
186
+ MODELS_DIR = path.join(EXE_AI_DIR, "models");
187
+ CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
188
+ LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
189
+ CURRENT_CONFIG_VERSION = 1;
190
+ DEFAULT_CONFIG = {
191
+ config_version: CURRENT_CONFIG_VERSION,
192
+ dbPath: DB_PATH,
193
+ modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
194
+ embeddingDim: 1024,
195
+ batchSize: 20,
196
+ flushIntervalMs: 1e4,
197
+ autoIngestion: true,
198
+ autoRetrieval: true,
199
+ searchMode: "hybrid",
200
+ hookSearchMode: "hybrid",
201
+ fileGrepEnabled: true,
202
+ splashEffect: true,
203
+ consolidationEnabled: true,
204
+ consolidationIntervalMs: 6 * 60 * 60 * 1e3,
205
+ consolidationModel: "claude-haiku-4-5-20251001",
206
+ consolidationMaxCallsPerRun: 20,
207
+ selfQueryRouter: true,
208
+ selfQueryModel: "claude-haiku-4-5-20251001",
209
+ rerankerEnabled: true,
210
+ scalingRoadmap: {
211
+ rerankerAutoTrigger: {
212
+ enabled: true,
213
+ broadQueryMinCardinality: 5e4,
214
+ fetchTopK: 150,
215
+ returnTopK: 5
216
+ }
217
+ },
218
+ graphRagEnabled: true,
219
+ wikiEnabled: false,
220
+ wikiUrl: "",
221
+ wikiApiKey: "",
222
+ wikiSyncIntervalMs: 30 * 60 * 1e3,
223
+ wikiWorkspaceMapping: {},
224
+ wikiAutoUpdate: true,
225
+ wikiAutoUpdateThreshold: 0.5,
226
+ wikiAutoUpdateCreateNew: true,
227
+ skillLearning: true,
228
+ skillThreshold: 3,
229
+ skillModel: "claude-haiku-4-5-20251001",
230
+ exeHeartbeat: {
231
+ enabled: true,
232
+ intervalSeconds: 60,
233
+ staleInProgressThresholdHours: 2
234
+ },
235
+ sessionLifecycle: {
236
+ idleKillEnabled: true,
237
+ idleKillTicksRequired: 3,
238
+ idleKillIntercomAckWindowMs: 1e4,
239
+ maxAutoInstances: 10
240
+ },
241
+ autoUpdate: {
242
+ checkOnBoot: true,
243
+ autoInstall: false,
244
+ checkIntervalMs: 24 * 60 * 60 * 1e3
245
+ }
246
+ };
247
+ CONFIG_MIGRATIONS = [
248
+ {
249
+ from: 0,
250
+ to: 1,
251
+ migrate: (cfg) => {
252
+ cfg.config_version = 1;
253
+ return cfg;
254
+ }
255
+ }
256
+ ];
257
+ }
258
+ });
259
+
260
+ // src/lib/employees.ts
261
+ var employees_exports = {};
262
+ __export(employees_exports, {
263
+ COORDINATOR_ROLE: () => COORDINATOR_ROLE,
264
+ DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
265
+ EMPLOYEES_PATH: () => EMPLOYEES_PATH,
266
+ addEmployee: () => addEmployee,
267
+ canCoordinate: () => canCoordinate,
268
+ getCoordinatorEmployee: () => getCoordinatorEmployee,
269
+ getCoordinatorName: () => getCoordinatorName,
270
+ getEmployee: () => getEmployee,
271
+ getEmployeeByRole: () => getEmployeeByRole,
272
+ getEmployeeNamesByRole: () => getEmployeeNamesByRole,
273
+ hasRole: () => hasRole,
274
+ isCoordinatorName: () => isCoordinatorName,
275
+ isCoordinatorRole: () => isCoordinatorRole,
276
+ isMultiInstance: () => isMultiInstance,
277
+ loadEmployees: () => loadEmployees,
278
+ loadEmployeesSync: () => loadEmployeesSync,
279
+ normalizeRole: () => normalizeRole,
280
+ normalizeRosterCase: () => normalizeRosterCase,
281
+ registerBinSymlinks: () => registerBinSymlinks,
282
+ saveEmployees: () => saveEmployees,
283
+ validateEmployeeName: () => validateEmployeeName
284
+ });
285
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
286
+ import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
287
+ import { execSync } from "child_process";
288
+ import path2 from "path";
289
+ import os2 from "os";
290
+ function normalizeRole(role) {
291
+ return (role ?? "").trim().toLowerCase();
292
+ }
293
+ function isCoordinatorRole(role) {
294
+ return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
295
+ }
296
+ function getCoordinatorEmployee(employees) {
297
+ return employees.find((e) => isCoordinatorRole(e.role));
298
+ }
299
+ function getCoordinatorName(employees = loadEmployeesSync()) {
300
+ return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
301
+ }
302
+ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
303
+ if (!agentName) return false;
304
+ return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
305
+ }
306
+ function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
307
+ return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
308
+ }
309
+ function validateEmployeeName(name) {
310
+ if (!name) {
311
+ return { valid: false, error: "Name is required" };
312
+ }
313
+ if (name.length > 32) {
314
+ return { valid: false, error: "Name must be 32 characters or fewer" };
315
+ }
316
+ if (!/^[a-z][a-z0-9]*$/.test(name)) {
317
+ return {
318
+ valid: false,
319
+ error: "Name must start with a letter and contain only lowercase alphanumeric characters"
320
+ };
321
+ }
322
+ return { valid: true };
323
+ }
324
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
325
+ if (!existsSync2(employeesPath)) {
326
+ return [];
327
+ }
328
+ const raw = await readFile2(employeesPath, "utf-8");
329
+ try {
330
+ return JSON.parse(raw);
331
+ } catch {
332
+ return [];
333
+ }
334
+ }
335
+ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
336
+ await mkdir2(path2.dirname(employeesPath), { recursive: true });
337
+ await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
338
+ }
339
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
340
+ if (!existsSync2(employeesPath)) return [];
341
+ try {
342
+ return JSON.parse(readFileSync2(employeesPath, "utf-8"));
343
+ } catch {
344
+ return [];
345
+ }
346
+ }
347
+ function getEmployee(employees, name) {
348
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
349
+ }
350
+ function getEmployeeByRole(employees, role) {
351
+ const lower = role.toLowerCase();
352
+ return employees.find((e) => e.role.toLowerCase() === lower);
353
+ }
354
+ function getEmployeeNamesByRole(employees, role) {
355
+ const lower = role.toLowerCase();
356
+ return employees.filter((e) => e.role.toLowerCase() === lower).map((e) => e.name);
357
+ }
358
+ function hasRole(agentName, role) {
359
+ const employees = loadEmployeesSync();
360
+ const emp = getEmployee(employees, agentName);
361
+ return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
362
+ }
363
+ function isMultiInstance(agentName, employees) {
364
+ const roster = employees ?? loadEmployeesSync();
365
+ const emp = getEmployee(roster, agentName);
366
+ if (!emp) return false;
367
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
368
+ }
369
+ function addEmployee(employees, employee) {
370
+ const normalized = { ...employee, name: employee.name.toLowerCase() };
371
+ if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
372
+ throw new Error(`Employee '${normalized.name}' already exists`);
373
+ }
374
+ return [...employees, normalized];
375
+ }
376
+ async function normalizeRosterCase(rosterPath) {
377
+ const employees = await loadEmployees(rosterPath);
378
+ let changed = false;
379
+ for (const emp of employees) {
380
+ if (emp.name !== emp.name.toLowerCase()) {
381
+ const oldName = emp.name;
382
+ emp.name = emp.name.toLowerCase();
383
+ changed = true;
384
+ try {
385
+ const identityDir = path2.join(os2.homedir(), ".exe-os", "identity");
386
+ const oldPath = path2.join(identityDir, `${oldName}.md`);
387
+ const newPath = path2.join(identityDir, `${emp.name}.md`);
388
+ if (existsSync2(oldPath) && !existsSync2(newPath)) {
389
+ renameSync2(oldPath, newPath);
390
+ } else if (existsSync2(oldPath) && oldPath !== newPath) {
391
+ const content = readFileSync2(oldPath, "utf-8");
392
+ writeFileSync(newPath, content, "utf-8");
393
+ if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
394
+ unlinkSync(oldPath);
395
+ }
396
+ }
397
+ } catch {
398
+ }
399
+ }
400
+ }
401
+ if (changed) {
402
+ await saveEmployees(employees, rosterPath);
403
+ }
404
+ return changed;
405
+ }
406
+ function findExeBin() {
407
+ try {
408
+ return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
409
+ } catch {
410
+ return null;
411
+ }
412
+ }
413
+ function registerBinSymlinks(name) {
414
+ const created = [];
415
+ const skipped = [];
416
+ const errors = [];
417
+ const exeBinPath = findExeBin();
418
+ if (!exeBinPath) {
419
+ errors.push("Could not find 'exe-os' in PATH");
420
+ return { created, skipped, errors };
421
+ }
422
+ const binDir = path2.dirname(exeBinPath);
423
+ let target;
424
+ try {
425
+ target = readlinkSync(exeBinPath);
426
+ } catch {
427
+ errors.push("Could not read 'exe' symlink");
428
+ return { created, skipped, errors };
429
+ }
430
+ for (const suffix of ["", "-opencode"]) {
431
+ const linkName = `${name}${suffix}`;
432
+ const linkPath = path2.join(binDir, linkName);
433
+ if (existsSync2(linkPath)) {
434
+ skipped.push(linkName);
435
+ continue;
436
+ }
437
+ try {
438
+ symlinkSync(target, linkPath);
439
+ created.push(linkName);
440
+ } catch (err) {
441
+ errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
442
+ }
443
+ }
444
+ return { created, skipped, errors };
445
+ }
446
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
447
+ var init_employees = __esm({
448
+ "src/lib/employees.ts"() {
449
+ "use strict";
450
+ init_config();
451
+ EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
452
+ DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
453
+ COORDINATOR_ROLE = "COO";
454
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
455
+ }
456
+ });
457
+
77
458
  // src/lib/database.ts
78
459
  import { createClient } from "@libsql/client";
79
460
  async function initDatabase(config) {
@@ -207,22 +588,24 @@ async function ensureSchema() {
207
588
  ON behaviors(agent_id, active);
208
589
  `);
209
590
  try {
591
+ const coordinatorName = getCoordinatorName();
210
592
  const existing = await client.execute({
211
- sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = 'exe'",
212
- args: []
593
+ sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
594
+ args: [coordinatorName]
213
595
  });
214
596
  if (Number(existing.rows[0]?.cnt) === 0) {
215
- await client.executeMultiple(`
216
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
217
- VALUES
218
- (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');
219
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
220
- VALUES
221
- (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');
222
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
223
- VALUES
224
- (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');
225
- `);
597
+ const seededAt = "2026-03-25T00:00:00Z";
598
+ for (const [domain, content] of [
599
+ ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
600
+ ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
601
+ ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
602
+ ]) {
603
+ await client.execute({
604
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
605
+ VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
606
+ args: [coordinatorName, domain, content, seededAt, seededAt]
607
+ });
608
+ }
226
609
  }
227
610
  } catch {
228
611
  }
@@ -911,207 +1294,52 @@ async function ensureSchema() {
911
1294
  ]) {
912
1295
  try {
913
1296
  await client.execute(col);
914
- } catch {
915
- }
916
- }
917
- }
918
- var _client, _resilientClient, initTurso;
919
- var init_database = __esm({
920
- "src/lib/database.ts"() {
921
- "use strict";
922
- init_db_retry();
923
- _client = null;
924
- _resilientClient = null;
925
- initTurso = initDatabase;
926
- }
927
- });
928
-
929
- // src/lib/config.ts
930
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
931
- import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
932
- import path2 from "path";
933
- import os2 from "os";
934
- function resolveDataDir() {
935
- if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
936
- if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
937
- const newDir = path2.join(os2.homedir(), ".exe-os");
938
- const legacyDir = path2.join(os2.homedir(), ".exe-mem");
939
- if (!existsSync2(newDir) && existsSync2(legacyDir)) {
940
- try {
941
- renameSync(legacyDir, newDir);
942
- process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
943
- `);
944
- } catch {
945
- return legacyDir;
946
- }
947
- }
948
- return newDir;
949
- }
950
- function migrateLegacyConfig(raw) {
951
- if ("r2" in raw) {
952
- process.stderr.write(
953
- "[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"
954
- );
955
- delete raw.r2;
956
- }
957
- if ("syncIntervalMs" in raw) {
958
- delete raw.syncIntervalMs;
959
- }
960
- return raw;
961
- }
962
- function migrateConfig(raw) {
963
- const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
964
- let currentVersion = fromVersion;
965
- let migrated = false;
966
- if (currentVersion > CURRENT_CONFIG_VERSION) {
967
- return { config: raw, migrated: false, fromVersion };
968
- }
969
- for (const migration of CONFIG_MIGRATIONS) {
970
- if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
971
- raw = migration.migrate(raw);
972
- currentVersion = migration.to;
973
- migrated = true;
974
- }
975
- }
976
- return { config: raw, migrated, fromVersion };
977
- }
978
- function normalizeScalingRoadmap(raw) {
979
- const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
980
- const userRoadmap = raw.scalingRoadmap ?? {};
981
- const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
982
- if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
983
- userAuto.enabled = raw.rerankerEnabled;
984
- }
985
- raw.scalingRoadmap = {
986
- ...userRoadmap,
987
- rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
988
- };
989
- }
990
- function normalizeSessionLifecycle(raw) {
991
- const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
992
- const userSL = raw.sessionLifecycle ?? {};
993
- raw.sessionLifecycle = { ...defaultSL, ...userSL };
994
- }
995
- function normalizeAutoUpdate(raw) {
996
- const defaultAU = DEFAULT_CONFIG.autoUpdate;
997
- const userAU = raw.autoUpdate ?? {};
998
- raw.autoUpdate = { ...defaultAU, ...userAU };
999
- }
1000
- async function loadConfig() {
1001
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1002
- await mkdir2(dir, { recursive: true });
1003
- const configPath = path2.join(dir, "config.json");
1004
- if (!existsSync2(configPath)) {
1005
- return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
1006
- }
1007
- const raw = await readFile2(configPath, "utf-8");
1008
- try {
1009
- let parsed = JSON.parse(raw);
1010
- parsed = migrateLegacyConfig(parsed);
1011
- const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
1012
- if (migrated) {
1013
- process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
1014
- `);
1015
- try {
1016
- await writeFile2(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
1017
- } catch {
1018
- }
1019
- }
1020
- normalizeScalingRoadmap(migratedCfg);
1021
- normalizeSessionLifecycle(migratedCfg);
1022
- normalizeAutoUpdate(migratedCfg);
1023
- const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
1024
- if (config.dbPath.startsWith("~")) {
1025
- config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
1026
- }
1027
- return config;
1028
- } catch {
1029
- return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
1030
- }
1031
- }
1032
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1033
- var init_config = __esm({
1034
- "src/lib/config.ts"() {
1035
- "use strict";
1036
- EXE_AI_DIR = resolveDataDir();
1037
- DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
1038
- MODELS_DIR = path2.join(EXE_AI_DIR, "models");
1039
- CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
1040
- LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
1041
- CURRENT_CONFIG_VERSION = 1;
1042
- DEFAULT_CONFIG = {
1043
- config_version: CURRENT_CONFIG_VERSION,
1044
- dbPath: DB_PATH,
1045
- modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
1046
- embeddingDim: 1024,
1047
- batchSize: 20,
1048
- flushIntervalMs: 1e4,
1049
- autoIngestion: true,
1050
- autoRetrieval: true,
1051
- searchMode: "hybrid",
1052
- hookSearchMode: "hybrid",
1053
- fileGrepEnabled: true,
1054
- splashEffect: true,
1055
- consolidationEnabled: true,
1056
- consolidationIntervalMs: 6 * 60 * 60 * 1e3,
1057
- consolidationModel: "claude-haiku-4-5-20251001",
1058
- consolidationMaxCallsPerRun: 20,
1059
- selfQueryRouter: true,
1060
- selfQueryModel: "claude-haiku-4-5-20251001",
1061
- rerankerEnabled: true,
1062
- scalingRoadmap: {
1063
- rerankerAutoTrigger: {
1064
- enabled: true,
1065
- broadQueryMinCardinality: 5e4,
1066
- fetchTopK: 150,
1067
- returnTopK: 5
1068
- }
1069
- },
1070
- graphRagEnabled: true,
1071
- wikiEnabled: false,
1072
- wikiUrl: "",
1073
- wikiApiKey: "",
1074
- wikiSyncIntervalMs: 30 * 60 * 1e3,
1075
- wikiWorkspaceMapping: {
1076
- exe: "Executive",
1077
- yoshi: "Engineering",
1078
- mari: "Marketing",
1079
- tom: "Engineering",
1080
- sasha: "Production"
1081
- },
1082
- wikiAutoUpdate: true,
1083
- wikiAutoUpdateThreshold: 0.5,
1084
- wikiAutoUpdateCreateNew: true,
1085
- skillLearning: true,
1086
- skillThreshold: 3,
1087
- skillModel: "claude-haiku-4-5-20251001",
1088
- exeHeartbeat: {
1089
- enabled: true,
1090
- intervalSeconds: 60,
1091
- staleInProgressThresholdHours: 2
1092
- },
1093
- sessionLifecycle: {
1094
- idleKillEnabled: true,
1095
- idleKillTicksRequired: 3,
1096
- idleKillIntercomAckWindowMs: 1e4,
1097
- maxAutoInstances: 10
1098
- },
1099
- autoUpdate: {
1100
- checkOnBoot: true,
1101
- autoInstall: false,
1102
- checkIntervalMs: 24 * 60 * 60 * 1e3
1103
- }
1104
- };
1105
- CONFIG_MIGRATIONS = [
1106
- {
1107
- from: 0,
1108
- to: 1,
1109
- migrate: (cfg) => {
1110
- cfg.config_version = 1;
1111
- return cfg;
1112
- }
1113
- }
1114
- ];
1297
+ } catch {
1298
+ }
1299
+ }
1300
+ try {
1301
+ await client.execute({
1302
+ sql: `ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0`,
1303
+ args: []
1304
+ });
1305
+ } catch {
1306
+ }
1307
+ try {
1308
+ await client.execute(
1309
+ `CREATE INDEX IF NOT EXISTS idx_memories_draft ON memories(draft) WHERE draft = 1`
1310
+ );
1311
+ } catch {
1312
+ }
1313
+ try {
1314
+ await client.execute({
1315
+ sql: `ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'`,
1316
+ args: []
1317
+ });
1318
+ } catch {
1319
+ }
1320
+ try {
1321
+ await client.execute(
1322
+ `CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
1323
+ );
1324
+ } catch {
1325
+ }
1326
+ try {
1327
+ await client.execute({
1328
+ sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
1329
+ args: []
1330
+ });
1331
+ } catch {
1332
+ }
1333
+ }
1334
+ var _client, _resilientClient, initTurso;
1335
+ var init_database = __esm({
1336
+ "src/lib/database.ts"() {
1337
+ "use strict";
1338
+ init_db_retry();
1339
+ init_employees();
1340
+ _client = null;
1341
+ _resilientClient = null;
1342
+ initTurso = initDatabase;
1115
1343
  }
1116
1344
  });
1117
1345
 
@@ -1183,12 +1411,12 @@ __export(shard_manager_exports, {
1183
1411
  listShards: () => listShards,
1184
1412
  shardExists: () => shardExists
1185
1413
  });
1186
- import path3 from "path";
1187
- import { existsSync as existsSync3, mkdirSync, readdirSync } from "fs";
1414
+ import path4 from "path";
1415
+ import { existsSync as existsSync4, mkdirSync, readdirSync } from "fs";
1188
1416
  import { createClient as createClient2 } from "@libsql/client";
1189
1417
  function initShardManager(encryptionKey) {
1190
1418
  _encryptionKey = encryptionKey;
1191
- if (!existsSync3(SHARDS_DIR)) {
1419
+ if (!existsSync4(SHARDS_DIR)) {
1192
1420
  mkdirSync(SHARDS_DIR, { recursive: true });
1193
1421
  }
1194
1422
  _shardingEnabled = true;
@@ -1209,7 +1437,7 @@ function getShardClient(projectName) {
1209
1437
  }
1210
1438
  const cached = _shards.get(safeName);
1211
1439
  if (cached) return cached;
1212
- const dbPath = path3.join(SHARDS_DIR, `${safeName}.db`);
1440
+ const dbPath = path4.join(SHARDS_DIR, `${safeName}.db`);
1213
1441
  const client = createClient2({
1214
1442
  url: `file:${dbPath}`,
1215
1443
  encryptionKey: _encryptionKey
@@ -1219,10 +1447,10 @@ function getShardClient(projectName) {
1219
1447
  }
1220
1448
  function shardExists(projectName) {
1221
1449
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
1222
- return existsSync3(path3.join(SHARDS_DIR, `${safeName}.db`));
1450
+ return existsSync4(path4.join(SHARDS_DIR, `${safeName}.db`));
1223
1451
  }
1224
1452
  function listShards() {
1225
- if (!existsSync3(SHARDS_DIR)) return [];
1453
+ if (!existsSync4(SHARDS_DIR)) return [];
1226
1454
  return readdirSync(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
1227
1455
  }
1228
1456
  async function ensureShardSchema(client) {
@@ -1292,7 +1520,11 @@ async function ensureShardSchema(client) {
1292
1520
  "ALTER TABLE memories ADD COLUMN source_path TEXT",
1293
1521
  "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
1294
1522
  "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
1295
- "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
1523
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
1524
+ // MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
1525
+ "ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
1526
+ "ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
1527
+ "ALTER TABLE memories ADD COLUMN trajectory TEXT"
1296
1528
  ]) {
1297
1529
  try {
1298
1530
  await client.execute(col);
@@ -1404,7 +1636,7 @@ var init_shard_manager = __esm({
1404
1636
  "src/lib/shard-manager.ts"() {
1405
1637
  "use strict";
1406
1638
  init_config();
1407
- SHARDS_DIR = path3.join(EXE_AI_DIR, "shards");
1639
+ SHARDS_DIR = path4.join(EXE_AI_DIR, "shards");
1408
1640
  _shards = /* @__PURE__ */ new Map();
1409
1641
  _encryptionKey = null;
1410
1642
  _shardingEnabled = false;
@@ -1422,26 +1654,26 @@ var init_platform_procedures = __esm({
1422
1654
  title: "What is exe-os \u2014 the operating model every agent must understand",
1423
1655
  domain: "architecture",
1424
1656
  priority: "p0",
1425
- 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."
1657
+ 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."
1426
1658
  },
1427
1659
  {
1428
1660
  title: "Mode 1 \u2014 how exe-os runs inside Claude Code",
1429
1661
  domain: "architecture",
1430
1662
  priority: "p0",
1431
- 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."
1663
+ 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."
1432
1664
  },
1433
1665
  {
1434
- title: "Sessions explained \u2014 what exeN means and how projects work",
1666
+ title: "Sessions explained \u2014 coordinator session names and projects",
1435
1667
  domain: "architecture",
1436
1668
  priority: "p0",
1437
- 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."
1669
+ 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."
1438
1670
  },
1439
1671
  // --- Hierarchy and dispatch ---
1440
1672
  {
1441
1673
  title: "Chain of command \u2014 who talks to whom",
1442
1674
  domain: "workflow",
1443
1675
  priority: "p0",
1444
- 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."
1676
+ 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."
1445
1677
  },
1446
1678
  {
1447
1679
  title: "Single dispatch path \u2014 create_task only",
@@ -1451,30 +1683,30 @@ var init_platform_procedures = __esm({
1451
1683
  },
1452
1684
  // --- Session isolation ---
1453
1685
  {
1454
- title: "Session scoping \u2014 stay in your exe boundary",
1686
+ title: "Session scoping \u2014 stay in your coordinator boundary",
1455
1687
  domain: "security",
1456
1688
  priority: "p0",
1457
- 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."
1689
+ 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."
1458
1690
  },
1459
1691
  {
1460
1692
  title: "Session isolation \u2014 never touch another session's work",
1461
1693
  domain: "workflow",
1462
1694
  priority: "p0",
1463
- 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.`
1695
+ 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."
1464
1696
  },
1465
1697
  // --- Engineering: session scoping in code ---
1466
1698
  {
1467
1699
  title: "Three-dimensional scoping \u2014 session, project, role \u2014 enforced in every query",
1468
1700
  domain: "architecture",
1469
1701
  priority: "p0",
1470
- 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."
1702
+ 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."
1471
1703
  },
1472
1704
  // --- Hard constraints ---
1473
1705
  {
1474
1706
  title: "What you CANNOT do in exe-os \u2014 hard constraints",
1475
1707
  domain: "security",
1476
1708
  priority: "p0",
1477
- 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."
1709
+ 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."
1478
1710
  },
1479
1711
  // --- Operations ---
1480
1712
  {
@@ -1594,13 +1826,13 @@ ${p.content}`).join("\n\n");
1594
1826
 
1595
1827
  // src/lib/notifications.ts
1596
1828
  import crypto from "crypto";
1597
- import path4 from "path";
1598
- import os3 from "os";
1829
+ import path5 from "path";
1830
+ import os4 from "os";
1599
1831
  import {
1600
- readFileSync as readFileSync2,
1832
+ readFileSync as readFileSync3,
1601
1833
  readdirSync as readdirSync2,
1602
- unlinkSync,
1603
- existsSync as existsSync4,
1834
+ unlinkSync as unlinkSync2,
1835
+ existsSync as existsSync5,
1604
1836
  rmdirSync
1605
1837
  } from "fs";
1606
1838
  async function writeNotification(notification) {
@@ -1645,12 +1877,12 @@ var init_notifications = __esm({
1645
1877
  });
1646
1878
 
1647
1879
  // src/lib/session-registry.ts
1648
- import { readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
1649
- import path5 from "path";
1650
- import os4 from "os";
1880
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync6 } from "fs";
1881
+ import path6 from "path";
1882
+ import os5 from "os";
1651
1883
  function registerSession(entry) {
1652
- const dir = path5.dirname(REGISTRY_PATH);
1653
- if (!existsSync5(dir)) {
1884
+ const dir = path6.dirname(REGISTRY_PATH);
1885
+ if (!existsSync6(dir)) {
1654
1886
  mkdirSync2(dir, { recursive: true });
1655
1887
  }
1656
1888
  const sessions = listSessions();
@@ -1660,11 +1892,11 @@ function registerSession(entry) {
1660
1892
  } else {
1661
1893
  sessions.push(entry);
1662
1894
  }
1663
- writeFileSync(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
1895
+ writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
1664
1896
  }
1665
1897
  function listSessions() {
1666
1898
  try {
1667
- const raw = readFileSync3(REGISTRY_PATH, "utf8");
1899
+ const raw = readFileSync4(REGISTRY_PATH, "utf8");
1668
1900
  return JSON.parse(raw);
1669
1901
  } catch {
1670
1902
  return [];
@@ -1674,18 +1906,18 @@ var REGISTRY_PATH;
1674
1906
  var init_session_registry = __esm({
1675
1907
  "src/lib/session-registry.ts"() {
1676
1908
  "use strict";
1677
- REGISTRY_PATH = path5.join(os4.homedir(), ".exe-os", "session-registry.json");
1909
+ REGISTRY_PATH = path6.join(os5.homedir(), ".exe-os", "session-registry.json");
1678
1910
  }
1679
1911
  });
1680
1912
 
1681
1913
  // src/lib/session-key.ts
1682
- import { execSync } from "child_process";
1914
+ import { execSync as execSync2 } from "child_process";
1683
1915
  function getSessionKey() {
1684
1916
  if (_cached) return _cached;
1685
1917
  let pid = process.ppid;
1686
1918
  for (let i = 0; i < 10; i++) {
1687
1919
  try {
1688
- const info = execSync(`ps -p ${pid} -o ppid=,comm=`, {
1920
+ const info = execSync2(`ps -p ${pid} -o ppid=,comm=`, {
1689
1921
  encoding: "utf8",
1690
1922
  timeout: 2e3
1691
1923
  }).trim();
@@ -1821,14 +2053,14 @@ var init_transport = __esm({
1821
2053
  });
1822
2054
 
1823
2055
  // src/lib/cc-agent-support.ts
1824
- import { execSync as execSync2 } from "child_process";
2056
+ import { execSync as execSync3 } from "child_process";
1825
2057
  function _resetCcAgentSupportCache() {
1826
2058
  _cachedSupport = null;
1827
2059
  }
1828
2060
  function claudeSupportsAgentFlag() {
1829
2061
  if (_cachedSupport !== null) return _cachedSupport;
1830
2062
  try {
1831
- const helpOutput = execSync2("claude --help 2>&1", {
2063
+ const helpOutput = execSync3("claude --help 2>&1", {
1832
2064
  encoding: "utf-8",
1833
2065
  timeout: 5e3
1834
2066
  });
@@ -1894,17 +2126,17 @@ var init_provider_table = __esm({
1894
2126
  });
1895
2127
 
1896
2128
  // src/lib/intercom-queue.ts
1897
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
1898
- import path6 from "path";
1899
- import os5 from "os";
2129
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
2130
+ import path7 from "path";
2131
+ import os6 from "os";
1900
2132
  function ensureDir() {
1901
- const dir = path6.dirname(QUEUE_PATH);
1902
- if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
2133
+ const dir = path7.dirname(QUEUE_PATH);
2134
+ if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
1903
2135
  }
1904
2136
  function readQueue() {
1905
2137
  try {
1906
- if (!existsSync6(QUEUE_PATH)) return [];
1907
- return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
2138
+ if (!existsSync7(QUEUE_PATH)) return [];
2139
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
1908
2140
  } catch {
1909
2141
  return [];
1910
2142
  }
@@ -1912,8 +2144,8 @@ function readQueue() {
1912
2144
  function writeQueue(queue) {
1913
2145
  ensureDir();
1914
2146
  const tmp = `${QUEUE_PATH}.tmp`;
1915
- writeFileSync2(tmp, JSON.stringify(queue, null, 2));
1916
- renameSync2(tmp, QUEUE_PATH);
2147
+ writeFileSync3(tmp, JSON.stringify(queue, null, 2));
2148
+ renameSync3(tmp, QUEUE_PATH);
1917
2149
  }
1918
2150
  function queueIntercom(targetSession, reason) {
1919
2151
  const queue = readQueue();
@@ -1936,178 +2168,9 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
1936
2168
  var init_intercom_queue = __esm({
1937
2169
  "src/lib/intercom-queue.ts"() {
1938
2170
  "use strict";
1939
- QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
2171
+ QUEUE_PATH = path7.join(os6.homedir(), ".exe-os", "intercom-queue.json");
1940
2172
  TTL_MS = 60 * 60 * 1e3;
1941
- INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
1942
- }
1943
- });
1944
-
1945
- // src/lib/employees.ts
1946
- var employees_exports = {};
1947
- __export(employees_exports, {
1948
- EMPLOYEES_PATH: () => EMPLOYEES_PATH,
1949
- addEmployee: () => addEmployee,
1950
- getEmployee: () => getEmployee,
1951
- getEmployeeByRole: () => getEmployeeByRole,
1952
- getEmployeeNamesByRole: () => getEmployeeNamesByRole,
1953
- hasRole: () => hasRole,
1954
- isMultiInstance: () => isMultiInstance,
1955
- loadEmployees: () => loadEmployees,
1956
- loadEmployeesSync: () => loadEmployeesSync,
1957
- normalizeRosterCase: () => normalizeRosterCase,
1958
- registerBinSymlinks: () => registerBinSymlinks,
1959
- saveEmployees: () => saveEmployees,
1960
- validateEmployeeName: () => validateEmployeeName
1961
- });
1962
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1963
- import { existsSync as existsSync7, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1964
- import { execSync as execSync3 } from "child_process";
1965
- import path7 from "path";
1966
- import os6 from "os";
1967
- function validateEmployeeName(name) {
1968
- if (!name) {
1969
- return { valid: false, error: "Name is required" };
1970
- }
1971
- if (name.length > 32) {
1972
- return { valid: false, error: "Name must be 32 characters or fewer" };
1973
- }
1974
- if (!/^[a-z][a-z0-9]*$/.test(name)) {
1975
- return {
1976
- valid: false,
1977
- error: "Name must start with a letter and contain only lowercase alphanumeric characters"
1978
- };
1979
- }
1980
- return { valid: true };
1981
- }
1982
- async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
1983
- if (!existsSync7(employeesPath)) {
1984
- return [];
1985
- }
1986
- const raw = await readFile3(employeesPath, "utf-8");
1987
- try {
1988
- return JSON.parse(raw);
1989
- } catch {
1990
- return [];
1991
- }
1992
- }
1993
- async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
1994
- await mkdir3(path7.dirname(employeesPath), { recursive: true });
1995
- await writeFile3(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
1996
- }
1997
- function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
1998
- if (!existsSync7(employeesPath)) return [];
1999
- try {
2000
- return JSON.parse(readFileSync5(employeesPath, "utf-8"));
2001
- } catch {
2002
- return [];
2003
- }
2004
- }
2005
- function getEmployee(employees, name) {
2006
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
2007
- }
2008
- function getEmployeeByRole(employees, role) {
2009
- const lower = role.toLowerCase();
2010
- return employees.find((e) => e.role.toLowerCase() === lower);
2011
- }
2012
- function getEmployeeNamesByRole(employees, role) {
2013
- const lower = role.toLowerCase();
2014
- return employees.filter((e) => e.role.toLowerCase() === lower).map((e) => e.name);
2015
- }
2016
- function hasRole(agentName, role) {
2017
- const employees = loadEmployeesSync();
2018
- const emp = getEmployee(employees, agentName);
2019
- return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
2020
- }
2021
- function isMultiInstance(agentName, employees) {
2022
- const roster = employees ?? loadEmployeesSync();
2023
- const emp = getEmployee(roster, agentName);
2024
- if (!emp) return false;
2025
- return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
2026
- }
2027
- function addEmployee(employees, employee) {
2028
- const normalized = { ...employee, name: employee.name.toLowerCase() };
2029
- if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
2030
- throw new Error(`Employee '${normalized.name}' already exists`);
2031
- }
2032
- return [...employees, normalized];
2033
- }
2034
- async function normalizeRosterCase(rosterPath) {
2035
- const employees = await loadEmployees(rosterPath);
2036
- let changed = false;
2037
- for (const emp of employees) {
2038
- if (emp.name !== emp.name.toLowerCase()) {
2039
- const oldName = emp.name;
2040
- emp.name = emp.name.toLowerCase();
2041
- changed = true;
2042
- try {
2043
- const identityDir = path7.join(os6.homedir(), ".exe-os", "identity");
2044
- const oldPath = path7.join(identityDir, `${oldName}.md`);
2045
- const newPath = path7.join(identityDir, `${emp.name}.md`);
2046
- if (existsSync7(oldPath) && !existsSync7(newPath)) {
2047
- renameSync3(oldPath, newPath);
2048
- } else if (existsSync7(oldPath) && oldPath !== newPath) {
2049
- const content = readFileSync5(oldPath, "utf-8");
2050
- writeFileSync3(newPath, content, "utf-8");
2051
- if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
2052
- unlinkSync2(oldPath);
2053
- }
2054
- }
2055
- } catch {
2056
- }
2057
- }
2058
- }
2059
- if (changed) {
2060
- await saveEmployees(employees, rosterPath);
2061
- }
2062
- return changed;
2063
- }
2064
- function findExeBin() {
2065
- try {
2066
- return execSync3(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
2067
- } catch {
2068
- return null;
2069
- }
2070
- }
2071
- function registerBinSymlinks(name) {
2072
- const created = [];
2073
- const skipped = [];
2074
- const errors = [];
2075
- const exeBinPath = findExeBin();
2076
- if (!exeBinPath) {
2077
- errors.push("Could not find 'exe-os' in PATH");
2078
- return { created, skipped, errors };
2079
- }
2080
- const binDir = path7.dirname(exeBinPath);
2081
- let target;
2082
- try {
2083
- target = readlinkSync(exeBinPath);
2084
- } catch {
2085
- errors.push("Could not read 'exe' symlink");
2086
- return { created, skipped, errors };
2087
- }
2088
- for (const suffix of ["", "-opencode"]) {
2089
- const linkName = `${name}${suffix}`;
2090
- const linkPath = path7.join(binDir, linkName);
2091
- if (existsSync7(linkPath)) {
2092
- skipped.push(linkName);
2093
- continue;
2094
- }
2095
- try {
2096
- symlinkSync(target, linkPath);
2097
- created.push(linkName);
2098
- } catch (err) {
2099
- errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
2100
- }
2101
- }
2102
- return { created, skipped, errors };
2103
- }
2104
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
2105
- var init_employees = __esm({
2106
- "src/lib/employees.ts"() {
2107
- "use strict";
2108
- init_config();
2109
- EMPLOYEES_PATH = path7.join(EXE_AI_DIR, "exe-employees.json");
2110
- MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
2173
+ INTERCOM_LOG = path7.join(os6.homedir(), ".exe-os", "intercom.log");
2111
2174
  }
2112
2175
  });
2113
2176
 
@@ -2322,7 +2385,7 @@ function _resetLastRelaunchCache() {
2322
2385
  }
2323
2386
  async function lastResumeCreatedAtMs(agentId) {
2324
2387
  const client = getClient();
2325
- const cmScope = sessionScopeFilter();
2388
+ const cmScope = sessionScopeFilter(null);
2326
2389
  const result = await client.execute({
2327
2390
  sql: `SELECT MAX(created_at) AS last_created_at
2328
2391
  FROM tasks
@@ -2347,7 +2410,7 @@ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
2347
2410
  const client = getClient();
2348
2411
  const now = (/* @__PURE__ */ new Date()).toISOString();
2349
2412
  const context = buildResumeContext(agentId, openTasks);
2350
- const rdScope = sessionScopeFilter();
2413
+ const rdScope = sessionScopeFilter(null);
2351
2414
  const existing = await client.execute({
2352
2415
  sql: `SELECT id FROM tasks
2353
2416
  WHERE assigned_to = ?
@@ -2381,7 +2444,7 @@ async function pollCapacityDead() {
2381
2444
  const transport = getTransport();
2382
2445
  const relaunched = [];
2383
2446
  const registered = listSessions().filter(
2384
- (s) => s.agentId !== "exe"
2447
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
2385
2448
  );
2386
2449
  if (registered.length === 0) return [];
2387
2450
  let liveSessions;
@@ -2441,7 +2504,7 @@ async function pollCapacityDead() {
2441
2504
  reason: "capacity"
2442
2505
  });
2443
2506
  const client = getClient();
2444
- const rlScope = sessionScopeFilter();
2507
+ const rlScope = sessionScopeFilter(null);
2445
2508
  const openTasks = await client.execute({
2446
2509
  sql: `SELECT id, title, priority, task_file, status
2447
2510
  FROM tasks
@@ -2495,6 +2558,7 @@ var init_capacity_monitor = __esm({
2495
2558
  init_session_kill_telemetry();
2496
2559
  init_tmux_routing();
2497
2560
  init_task_scope();
2561
+ init_employees();
2498
2562
  CAPACITY_PATTERNS = [
2499
2563
  /conversation is too long/i,
2500
2564
  /maximum context length/i,
@@ -2644,7 +2708,7 @@ function employeeSessionName(employee, exeSession, instance) {
2644
2708
  exeSession = root;
2645
2709
  } else {
2646
2710
  throw new Error(
2647
- `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
2711
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
2648
2712
  );
2649
2713
  }
2650
2714
  }
@@ -2664,8 +2728,10 @@ function parseParentExe(sessionName, agentId) {
2664
2728
  return match?.[1] ?? null;
2665
2729
  }
2666
2730
  function extractRootExe(name) {
2667
- const match = name.match(/(exe\d+)$/);
2668
- return match?.[1] ?? null;
2731
+ if (!name) return null;
2732
+ if (!name.includes("-")) return name;
2733
+ const parts = name.split("-").filter(Boolean);
2734
+ return parts.length > 0 ? parts[parts.length - 1] : null;
2669
2735
  }
2670
2736
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
2671
2737
  if (!existsSync10(SESSION_CACHE)) {
@@ -2810,12 +2876,14 @@ function isSessionBusy(sessionName) {
2810
2876
  return state === "thinking" || state === "tool";
2811
2877
  }
2812
2878
  function isExeSession(sessionName) {
2813
- return /^exe\d*$/.test(sessionName);
2879
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
2880
+ const coordinatorName = getCoordinatorName();
2881
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
2814
2882
  }
2815
2883
  function sendIntercom(targetSession) {
2816
2884
  const transport = getTransport();
2817
2885
  if (isExeSession(targetSession)) {
2818
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
2886
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
2819
2887
  return "skipped_exe";
2820
2888
  }
2821
2889
  if (isDebounced(targetSession)) {
@@ -2867,7 +2935,7 @@ function notifyParentExe(sessionKey) {
2867
2935
  if (result === "failed") {
2868
2936
  const rootExe = resolveExeSession();
2869
2937
  if (rootExe && rootExe !== target) {
2870
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
2938
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
2871
2939
  `);
2872
2940
  const fallback = sendIntercom(rootExe);
2873
2941
  return fallback !== "failed";
@@ -2877,8 +2945,8 @@ function notifyParentExe(sessionKey) {
2877
2945
  return true;
2878
2946
  }
2879
2947
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2880
- if (employeeName === "exe") {
2881
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
2948
+ if (employeeName === "exe" || isCoordinatorName(employeeName)) {
2949
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
2882
2950
  }
2883
2951
  try {
2884
2952
  assertEmployeeLimitSync();
@@ -2887,8 +2955,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2887
2955
  return { status: "failed", sessionName: "", error: err.message };
2888
2956
  }
2889
2957
  }
2890
- if (/-exe\d*$/.test(employeeName)) {
2891
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
2958
+ if (employeeName.includes("-")) {
2959
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
2892
2960
  return {
2893
2961
  status: "failed",
2894
2962
  sessionName: "",
@@ -2907,7 +2975,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2907
2975
  return {
2908
2976
  status: "failed",
2909
2977
  sessionName: "",
2910
- error: `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
2978
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
2911
2979
  };
2912
2980
  }
2913
2981
  }
@@ -3064,8 +3132,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3064
3132
  const ctxContent = [
3065
3133
  `## Session Context`,
3066
3134
  `You are running in tmux session: ${sessionName}.`,
3067
- `Your parent exe session is ${exeSession}.`,
3068
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
3135
+ `Your parent coordinator session is ${exeSession}.`,
3136
+ `Your employees (if any) use the -${exeSession} suffix.`
3069
3137
  ].join("\n");
3070
3138
  writeFileSync5(ctxFile, ctxContent);
3071
3139
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
@@ -3169,6 +3237,7 @@ var init_tmux_routing = __esm({
3169
3237
  init_provider_table();
3170
3238
  init_intercom_queue();
3171
3239
  init_plan_limits();
3240
+ init_employees();
3172
3241
  SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
3173
3242
  SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
3174
3243
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
@@ -3451,6 +3520,36 @@ async function listTasks(input) {
3451
3520
  tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
3452
3521
  }));
3453
3522
  }
3523
+ function isTmuxSessionAlive(identifier) {
3524
+ if (!identifier || identifier === "unknown") return true;
3525
+ try {
3526
+ if (identifier.startsWith("%")) {
3527
+ const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
3528
+ timeout: 2e3,
3529
+ encoding: "utf8",
3530
+ stdio: ["pipe", "pipe", "pipe"]
3531
+ });
3532
+ return output.split("\n").some((l) => l.trim() === identifier);
3533
+ } else {
3534
+ execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
3535
+ timeout: 2e3,
3536
+ stdio: ["pipe", "pipe", "pipe"]
3537
+ });
3538
+ return true;
3539
+ }
3540
+ } catch {
3541
+ if (identifier.startsWith("%")) return true;
3542
+ try {
3543
+ execSync5("tmux list-sessions", {
3544
+ timeout: 2e3,
3545
+ stdio: ["pipe", "pipe", "pipe"]
3546
+ });
3547
+ return false;
3548
+ } catch {
3549
+ return true;
3550
+ }
3551
+ }
3552
+ }
3454
3553
  function checkStaleCompletion(taskContext, taskCreatedAt) {
3455
3554
  if (!taskContext) return null;
3456
3555
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -3513,13 +3612,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3513
3612
  });
3514
3613
  if (claim.rowsAffected === 0) {
3515
3614
  const current = await client.execute({
3516
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
3615
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
3517
3616
  args: [taskId]
3518
3617
  });
3519
3618
  const cur = current.rows[0];
3520
- const status = cur?.status ?? "unknown";
3521
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
3522
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
3619
+ const curStatus = cur?.status ?? "unknown";
3620
+ const claimedBySession = cur?.assigned_tmux ?? "";
3621
+ const assignedBy = cur?.assigned_by ?? "";
3622
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
3623
+ process.stderr.write(
3624
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
3625
+ `
3626
+ );
3627
+ await client.execute({
3628
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
3629
+ args: [now, taskId]
3630
+ });
3631
+ const retried = await client.execute({
3632
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
3633
+ args: [tmuxSession, now, taskId]
3634
+ });
3635
+ if (retried.rowsAffected > 0) {
3636
+ try {
3637
+ await writeCheckpoint({
3638
+ taskId,
3639
+ step: "reclaimed_dead_session",
3640
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
3641
+ });
3642
+ } catch {
3643
+ }
3644
+ return { row, taskFile, now, taskId };
3645
+ }
3646
+ }
3647
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
3648
+ process.stderr.write(
3649
+ `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
3650
+ `
3651
+ );
3652
+ await client.execute({
3653
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
3654
+ args: [tmuxSession, now, taskId]
3655
+ });
3656
+ try {
3657
+ await writeCheckpoint({
3658
+ taskId,
3659
+ step: "assigner_override",
3660
+ contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
3661
+ });
3662
+ } catch {
3663
+ }
3664
+ return { row, taskFile, now, taskId };
3665
+ }
3666
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
3667
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
3523
3668
  }
3524
3669
  try {
3525
3670
  await writeCheckpoint({
@@ -3617,7 +3762,7 @@ var init_tasks_crud = __esm({
3617
3762
  "use strict";
3618
3763
  init_database();
3619
3764
  init_task_scope();
3620
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
3765
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
3621
3766
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3622
3767
  }
3623
3768
  });
@@ -3974,7 +4119,7 @@ function findSessionForProject(projectName) {
3974
4119
  const sessions = listSessions();
3975
4120
  for (const s of sessions) {
3976
4121
  const proj = s.projectDir.split("/").filter(Boolean).pop();
3977
- if (proj === projectName && s.agentId === "exe") return s;
4122
+ if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
3978
4123
  }
3979
4124
  return null;
3980
4125
  }
@@ -4014,12 +4159,13 @@ var init_session_scope = __esm({
4014
4159
  init_session_registry();
4015
4160
  init_project_name();
4016
4161
  init_tmux_routing();
4162
+ init_employees();
4017
4163
  }
4018
4164
  });
4019
4165
 
4020
4166
  // src/lib/tasks-notify.ts
4021
4167
  async function dispatchTaskToEmployee(input) {
4022
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
4168
+ if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
4023
4169
  let crossProject = false;
4024
4170
  if (input.projectName) {
4025
4171
  try {
@@ -4462,6 +4608,24 @@ async function updateTask(input) {
4462
4608
  });
4463
4609
  } catch {
4464
4610
  }
4611
+ const assignedAgent = String(row.assigned_to);
4612
+ if (!isCoordinatorName(assignedAgent)) {
4613
+ try {
4614
+ const draftClient = getClient();
4615
+ if (input.status === "done") {
4616
+ await draftClient.execute({
4617
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
4618
+ args: [assignedAgent]
4619
+ });
4620
+ } else if (input.status === "cancelled") {
4621
+ await draftClient.execute({
4622
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
4623
+ args: [assignedAgent]
4624
+ });
4625
+ }
4626
+ } catch {
4627
+ }
4628
+ }
4465
4629
  try {
4466
4630
  const client = getClient();
4467
4631
  const cascaded = await client.execute({
@@ -4480,8 +4644,8 @@ async function updateTask(input) {
4480
4644
  }
4481
4645
  const isTerminal = input.status === "done" || input.status === "needs_review";
4482
4646
  if (isTerminal) {
4483
- const isExe = String(row.assigned_to) === "exe";
4484
- if (!isExe) {
4647
+ const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
4648
+ if (!isCoordinator) {
4485
4649
  notifyTaskDone();
4486
4650
  }
4487
4651
  await markTaskNotificationsRead(taskFile);
@@ -4505,7 +4669,7 @@ async function updateTask(input) {
4505
4669
  }
4506
4670
  }
4507
4671
  }
4508
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
4672
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4509
4673
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4510
4674
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4511
4675
  taskId,
@@ -4521,7 +4685,7 @@ async function updateTask(input) {
4521
4685
  });
4522
4686
  }
4523
4687
  let nextTask;
4524
- if (isTerminal && String(row.assigned_to) !== "exe") {
4688
+ if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
4525
4689
  try {
4526
4690
  nextTask = await findNextTask(String(row.assigned_to));
4527
4691
  } catch {
@@ -4548,12 +4712,14 @@ async function updateTask(input) {
4548
4712
  async function deleteTask(taskId, baseDir) {
4549
4713
  const client = getClient();
4550
4714
  const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
4551
- const reviewer = assignedBy || "exe";
4715
+ const coordinatorName = getCoordinatorName();
4716
+ const reviewer = assignedBy || coordinatorName;
4552
4717
  const reviewSlug = `review-${assignedTo}-${taskSlug}`;
4553
4718
  const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
4719
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
4554
4720
  await client.execute({
4555
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
4556
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
4721
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
4722
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
4557
4723
  });
4558
4724
  await markAsReadByTaskFile(taskFile);
4559
4725
  await markAsReadByTaskFile(reviewFile);
@@ -4565,6 +4731,7 @@ var init_tasks = __esm({
4565
4731
  init_config();
4566
4732
  init_notifications();
4567
4733
  init_state_bus();
4734
+ init_employees();
4568
4735
  init_tasks_crud();
4569
4736
  init_tasks_review();
4570
4737
  init_tasks_crud();
@@ -4578,17 +4745,17 @@ var init_tasks = __esm({
4578
4745
  init_database();
4579
4746
 
4580
4747
  // src/lib/keychain.ts
4581
- import { readFile, writeFile, unlink, mkdir, chmod } from "fs/promises";
4582
- import { existsSync } from "fs";
4583
- import path from "path";
4584
- import os from "os";
4748
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
4749
+ import { existsSync as existsSync3 } from "fs";
4750
+ import path3 from "path";
4751
+ import os3 from "os";
4585
4752
  var SERVICE = "exe-mem";
4586
4753
  var ACCOUNT = "master-key";
4587
4754
  function getKeyDir() {
4588
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path.join(os.homedir(), ".exe-os");
4755
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path3.join(os3.homedir(), ".exe-os");
4589
4756
  }
4590
4757
  function getKeyPath() {
4591
- return path.join(getKeyDir(), "master.key");
4758
+ return path3.join(getKeyDir(), "master.key");
4592
4759
  }
4593
4760
  async function tryKeytar() {
4594
4761
  try {
@@ -4609,11 +4776,11 @@ async function getMasterKey() {
4609
4776
  }
4610
4777
  }
4611
4778
  const keyPath = getKeyPath();
4612
- if (!existsSync(keyPath)) {
4779
+ if (!existsSync3(keyPath)) {
4613
4780
  return null;
4614
4781
  }
4615
4782
  try {
4616
- const content = await readFile(keyPath, "utf-8");
4783
+ const content = await readFile3(keyPath, "utf-8");
4617
4784
  return Buffer.from(content.trim(), "base64");
4618
4785
  } catch {
4619
4786
  return null;
@@ -4714,8 +4881,10 @@ async function main() {
4714
4881
  await initStore();
4715
4882
  const fpPrefix = fingerprint.slice(0, 8);
4716
4883
  const client = getClient();
4717
- const { loadEmployeesSync: loadEmployeesSync2, getEmployeeByRole: getEmployeeByRole2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
4718
- const ctoName = getEmployeeByRole2(loadEmployeesSync2(), "CTO")?.name ?? "yoshi";
4884
+ const { loadEmployeesSync: loadEmployeesSync2, getEmployeeByRole: getEmployeeByRole2, getCoordinatorName: getCoordinatorName2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
4885
+ const employees = loadEmployeesSync2();
4886
+ const coordinatorName = getCoordinatorName2(employees);
4887
+ const ctoName = getEmployeeByRole2(employees, "CTO")?.name ?? coordinatorName;
4719
4888
  const existing = await client.execute({
4720
4889
  sql: `SELECT id FROM tasks
4721
4890
  WHERE assigned_to = ?
@@ -4766,7 +4935,7 @@ async function main() {
4766
4935
  context,
4767
4936
  baseDir: process.cwd(),
4768
4937
  skipDispatch: true,
4769
- reviewer: "exe"
4938
+ reviewer: coordinatorName
4770
4939
  });
4771
4940
  process.stderr.write(`[bug-report-worker] Created auto-bug task for ${toolName}: ${errorSummary}
4772
4941
  `);