@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
package/dist/lib/tasks.js CHANGED
@@ -26,23 +26,6 @@ var init_db_retry = __esm({
26
26
  }
27
27
  });
28
28
 
29
- // src/lib/database.ts
30
- import { createClient } from "@libsql/client";
31
- function getClient() {
32
- if (!_resilientClient) {
33
- throw new Error("Database client not initialized. Call initDatabase() first.");
34
- }
35
- return _resilientClient;
36
- }
37
- var _resilientClient;
38
- var init_database = __esm({
39
- "src/lib/database.ts"() {
40
- "use strict";
41
- init_db_retry();
42
- _resilientClient = null;
43
- }
44
- });
45
-
46
29
  // src/lib/config.ts
47
30
  import { readFile, writeFile, mkdir, chmod } from "fs/promises";
48
31
  import { readFileSync, existsSync, renameSync } from "fs";
@@ -189,13 +172,7 @@ var init_config = __esm({
189
172
  wikiUrl: "",
190
173
  wikiApiKey: "",
191
174
  wikiSyncIntervalMs: 30 * 60 * 1e3,
192
- wikiWorkspaceMapping: {
193
- exe: "Executive",
194
- yoshi: "Engineering",
195
- mari: "Marketing",
196
- tom: "Engineering",
197
- sasha: "Production"
198
- },
175
+ wikiWorkspaceMapping: {},
199
176
  wikiAutoUpdate: true,
200
177
  wikiAutoUpdateThreshold: 0.5,
201
178
  wikiAutoUpdateCreateNew: true,
@@ -232,15 +209,84 @@ var init_config = __esm({
232
209
  }
233
210
  });
234
211
 
235
- // src/lib/notifications.ts
236
- import crypto from "crypto";
212
+ // src/lib/employees.ts
213
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
214
+ import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
215
+ import { execSync } from "child_process";
237
216
  import path2 from "path";
238
217
  import os2 from "os";
218
+ function normalizeRole(role) {
219
+ return (role ?? "").trim().toLowerCase();
220
+ }
221
+ function isCoordinatorRole(role) {
222
+ return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
223
+ }
224
+ function getCoordinatorEmployee(employees) {
225
+ return employees.find((e) => isCoordinatorRole(e.role));
226
+ }
227
+ function getCoordinatorName(employees = loadEmployeesSync()) {
228
+ return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
229
+ }
230
+ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
231
+ if (!agentName) return false;
232
+ return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
233
+ }
234
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
235
+ if (!existsSync2(employeesPath)) return [];
236
+ try {
237
+ return JSON.parse(readFileSync2(employeesPath, "utf-8"));
238
+ } catch {
239
+ return [];
240
+ }
241
+ }
242
+ function getEmployee(employees, name) {
243
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
244
+ }
245
+ function isMultiInstance(agentName, employees) {
246
+ const roster = employees ?? loadEmployeesSync();
247
+ const emp = getEmployee(roster, agentName);
248
+ if (!emp) return false;
249
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
250
+ }
251
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
252
+ var init_employees = __esm({
253
+ "src/lib/employees.ts"() {
254
+ "use strict";
255
+ init_config();
256
+ EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
257
+ DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
258
+ COORDINATOR_ROLE = "COO";
259
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
260
+ }
261
+ });
262
+
263
+ // src/lib/database.ts
264
+ import { createClient } from "@libsql/client";
265
+ function getClient() {
266
+ if (!_resilientClient) {
267
+ throw new Error("Database client not initialized. Call initDatabase() first.");
268
+ }
269
+ return _resilientClient;
270
+ }
271
+ var _resilientClient;
272
+ var init_database = __esm({
273
+ "src/lib/database.ts"() {
274
+ "use strict";
275
+ init_db_retry();
276
+ init_employees();
277
+ _resilientClient = null;
278
+ }
279
+ });
280
+
281
+ // src/lib/notifications.ts
282
+ import crypto from "crypto";
283
+ import path3 from "path";
284
+ import os3 from "os";
239
285
  import {
240
- readFileSync as readFileSync2,
286
+ readFileSync as readFileSync3,
241
287
  readdirSync,
242
- unlinkSync,
243
- existsSync as existsSync2,
288
+ unlinkSync as unlinkSync2,
289
+ existsSync as existsSync3,
244
290
  rmdirSync
245
291
  } from "fs";
246
292
  async function writeNotification(notification) {
@@ -340,12 +386,12 @@ var init_state_bus = __esm({
340
386
  });
341
387
 
342
388
  // src/lib/session-registry.ts
343
- import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync as existsSync3 } from "fs";
344
- import path3 from "path";
345
- import os3 from "os";
389
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync4 } from "fs";
390
+ import path4 from "path";
391
+ import os4 from "os";
346
392
  function registerSession(entry) {
347
- const dir = path3.dirname(REGISTRY_PATH);
348
- if (!existsSync3(dir)) {
393
+ const dir = path4.dirname(REGISTRY_PATH);
394
+ if (!existsSync4(dir)) {
349
395
  mkdirSync(dir, { recursive: true });
350
396
  }
351
397
  const sessions = listSessions();
@@ -355,11 +401,11 @@ function registerSession(entry) {
355
401
  } else {
356
402
  sessions.push(entry);
357
403
  }
358
- writeFileSync(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
404
+ writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
359
405
  }
360
406
  function listSessions() {
361
407
  try {
362
- const raw = readFileSync3(REGISTRY_PATH, "utf8");
408
+ const raw = readFileSync4(REGISTRY_PATH, "utf8");
363
409
  return JSON.parse(raw);
364
410
  } catch {
365
411
  return [];
@@ -369,18 +415,18 @@ var REGISTRY_PATH;
369
415
  var init_session_registry = __esm({
370
416
  "src/lib/session-registry.ts"() {
371
417
  "use strict";
372
- REGISTRY_PATH = path3.join(os3.homedir(), ".exe-os", "session-registry.json");
418
+ REGISTRY_PATH = path4.join(os4.homedir(), ".exe-os", "session-registry.json");
373
419
  }
374
420
  });
375
421
 
376
422
  // src/lib/session-key.ts
377
- import { execSync } from "child_process";
423
+ import { execSync as execSync2 } from "child_process";
378
424
  function getSessionKey() {
379
425
  if (_cached) return _cached;
380
426
  let pid = process.ppid;
381
427
  for (let i = 0; i < 10; i++) {
382
428
  try {
383
- const info = execSync(`ps -p ${pid} -o ppid=,comm=`, {
429
+ const info = execSync2(`ps -p ${pid} -o ppid=,comm=`, {
384
430
  encoding: "utf8",
385
431
  timeout: 2e3
386
432
  }).trim();
@@ -516,14 +562,14 @@ var init_transport = __esm({
516
562
  });
517
563
 
518
564
  // src/lib/cc-agent-support.ts
519
- import { execSync as execSync2 } from "child_process";
565
+ import { execSync as execSync3 } from "child_process";
520
566
  function _resetCcAgentSupportCache() {
521
567
  _cachedSupport = null;
522
568
  }
523
569
  function claudeSupportsAgentFlag() {
524
570
  if (_cachedSupport !== null) return _cachedSupport;
525
571
  try {
526
- const helpOutput = execSync2("claude --help 2>&1", {
572
+ const helpOutput = execSync3("claude --help 2>&1", {
527
573
  encoding: "utf-8",
528
574
  timeout: 5e3
529
575
  });
@@ -589,17 +635,17 @@ var init_provider_table = __esm({
589
635
  });
590
636
 
591
637
  // src/lib/intercom-queue.ts
592
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
593
- import path4 from "path";
594
- import os4 from "os";
638
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
639
+ import path5 from "path";
640
+ import os5 from "os";
595
641
  function ensureDir() {
596
- const dir = path4.dirname(QUEUE_PATH);
597
- if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
642
+ const dir = path5.dirname(QUEUE_PATH);
643
+ if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
598
644
  }
599
645
  function readQueue() {
600
646
  try {
601
- if (!existsSync4(QUEUE_PATH)) return [];
602
- return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
647
+ if (!existsSync5(QUEUE_PATH)) return [];
648
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
603
649
  } catch {
604
650
  return [];
605
651
  }
@@ -607,8 +653,8 @@ function readQueue() {
607
653
  function writeQueue(queue) {
608
654
  ensureDir();
609
655
  const tmp = `${QUEUE_PATH}.tmp`;
610
- writeFileSync2(tmp, JSON.stringify(queue, null, 2));
611
- renameSync2(tmp, QUEUE_PATH);
656
+ writeFileSync3(tmp, JSON.stringify(queue, null, 2));
657
+ renameSync3(tmp, QUEUE_PATH);
612
658
  }
613
659
  function queueIntercom(targetSession, reason) {
614
660
  const queue = readQueue();
@@ -631,42 +677,9 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
631
677
  var init_intercom_queue = __esm({
632
678
  "src/lib/intercom-queue.ts"() {
633
679
  "use strict";
634
- QUEUE_PATH = path4.join(os4.homedir(), ".exe-os", "intercom-queue.json");
680
+ QUEUE_PATH = path5.join(os5.homedir(), ".exe-os", "intercom-queue.json");
635
681
  TTL_MS = 60 * 60 * 1e3;
636
- INTERCOM_LOG = path4.join(os4.homedir(), ".exe-os", "intercom.log");
637
- }
638
- });
639
-
640
- // src/lib/employees.ts
641
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
642
- import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
643
- import { execSync as execSync3 } from "child_process";
644
- import path5 from "path";
645
- import os5 from "os";
646
- function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
647
- if (!existsSync5(employeesPath)) return [];
648
- try {
649
- return JSON.parse(readFileSync5(employeesPath, "utf-8"));
650
- } catch {
651
- return [];
652
- }
653
- }
654
- function getEmployee(employees, name) {
655
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
656
- }
657
- function isMultiInstance(agentName, employees) {
658
- const roster = employees ?? loadEmployeesSync();
659
- const emp = getEmployee(roster, agentName);
660
- if (!emp) return false;
661
- return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
662
- }
663
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
664
- var init_employees = __esm({
665
- "src/lib/employees.ts"() {
666
- "use strict";
667
- init_config();
668
- EMPLOYEES_PATH = path5.join(EXE_AI_DIR, "exe-employees.json");
669
- MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
682
+ INTERCOM_LOG = path5.join(os5.homedir(), ".exe-os", "intercom.log");
670
683
  }
671
684
  });
672
685
 
@@ -881,7 +894,7 @@ function _resetLastRelaunchCache() {
881
894
  }
882
895
  async function lastResumeCreatedAtMs(agentId) {
883
896
  const client = getClient();
884
- const cmScope = sessionScopeFilter();
897
+ const cmScope = sessionScopeFilter(null);
885
898
  const result = await client.execute({
886
899
  sql: `SELECT MAX(created_at) AS last_created_at
887
900
  FROM tasks
@@ -906,7 +919,7 @@ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
906
919
  const client = getClient();
907
920
  const now = (/* @__PURE__ */ new Date()).toISOString();
908
921
  const context = buildResumeContext(agentId, openTasks);
909
- const rdScope = sessionScopeFilter();
922
+ const rdScope = sessionScopeFilter(null);
910
923
  const existing = await client.execute({
911
924
  sql: `SELECT id FROM tasks
912
925
  WHERE assigned_to = ?
@@ -940,7 +953,7 @@ async function pollCapacityDead() {
940
953
  const transport = getTransport();
941
954
  const relaunched = [];
942
955
  const registered = listSessions().filter(
943
- (s) => s.agentId !== "exe"
956
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
944
957
  );
945
958
  if (registered.length === 0) return [];
946
959
  let liveSessions;
@@ -1000,7 +1013,7 @@ async function pollCapacityDead() {
1000
1013
  reason: "capacity"
1001
1014
  });
1002
1015
  const client = getClient();
1003
- const rlScope = sessionScopeFilter();
1016
+ const rlScope = sessionScopeFilter(null);
1004
1017
  const openTasks = await client.execute({
1005
1018
  sql: `SELECT id, title, priority, task_file, status
1006
1019
  FROM tasks
@@ -1054,6 +1067,7 @@ var init_capacity_monitor = __esm({
1054
1067
  init_session_kill_telemetry();
1055
1068
  init_tmux_routing();
1056
1069
  init_task_scope();
1070
+ init_employees();
1057
1071
  CAPACITY_PATTERNS = [
1058
1072
  /conversation is too long/i,
1059
1073
  /maximum context length/i,
@@ -1203,7 +1217,7 @@ function employeeSessionName(employee, exeSession, instance) {
1203
1217
  exeSession = root;
1204
1218
  } else {
1205
1219
  throw new Error(
1206
- `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
1220
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
1207
1221
  );
1208
1222
  }
1209
1223
  }
@@ -1223,8 +1237,10 @@ function parseParentExe(sessionName, agentId) {
1223
1237
  return match?.[1] ?? null;
1224
1238
  }
1225
1239
  function extractRootExe(name) {
1226
- const match = name.match(/(exe\d+)$/);
1227
- return match?.[1] ?? null;
1240
+ if (!name) return null;
1241
+ if (!name.includes("-")) return name;
1242
+ const parts = name.split("-").filter(Boolean);
1243
+ return parts.length > 0 ? parts[parts.length - 1] : null;
1228
1244
  }
1229
1245
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1230
1246
  if (!existsSync8(SESSION_CACHE)) {
@@ -1369,12 +1385,14 @@ function isSessionBusy(sessionName) {
1369
1385
  return state === "thinking" || state === "tool";
1370
1386
  }
1371
1387
  function isExeSession(sessionName) {
1372
- return /^exe\d*$/.test(sessionName);
1388
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
1389
+ const coordinatorName = getCoordinatorName();
1390
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
1373
1391
  }
1374
1392
  function sendIntercom(targetSession) {
1375
1393
  const transport = getTransport();
1376
1394
  if (isExeSession(targetSession)) {
1377
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
1395
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
1378
1396
  return "skipped_exe";
1379
1397
  }
1380
1398
  if (isDebounced(targetSession)) {
@@ -1426,7 +1444,7 @@ function notifyParentExe(sessionKey) {
1426
1444
  if (result === "failed") {
1427
1445
  const rootExe = resolveExeSession();
1428
1446
  if (rootExe && rootExe !== target) {
1429
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
1447
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
1430
1448
  `);
1431
1449
  const fallback = sendIntercom(rootExe);
1432
1450
  return fallback !== "failed";
@@ -1436,8 +1454,8 @@ function notifyParentExe(sessionKey) {
1436
1454
  return true;
1437
1455
  }
1438
1456
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1439
- if (employeeName === "exe") {
1440
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
1457
+ if (employeeName === "exe" || isCoordinatorName(employeeName)) {
1458
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
1441
1459
  }
1442
1460
  try {
1443
1461
  assertEmployeeLimitSync();
@@ -1446,8 +1464,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1446
1464
  return { status: "failed", sessionName: "", error: err.message };
1447
1465
  }
1448
1466
  }
1449
- if (/-exe\d*$/.test(employeeName)) {
1450
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
1467
+ if (employeeName.includes("-")) {
1468
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
1451
1469
  return {
1452
1470
  status: "failed",
1453
1471
  sessionName: "",
@@ -1466,7 +1484,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1466
1484
  return {
1467
1485
  status: "failed",
1468
1486
  sessionName: "",
1469
- error: `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
1487
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
1470
1488
  };
1471
1489
  }
1472
1490
  }
@@ -1623,8 +1641,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1623
1641
  const ctxContent = [
1624
1642
  `## Session Context`,
1625
1643
  `You are running in tmux session: ${sessionName}.`,
1626
- `Your parent exe session is ${exeSession}.`,
1627
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
1644
+ `Your parent coordinator session is ${exeSession}.`,
1645
+ `Your employees (if any) use the -${exeSession} suffix.`
1628
1646
  ].join("\n");
1629
1647
  writeFileSync5(ctxFile, ctxContent);
1630
1648
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
@@ -1728,6 +1746,7 @@ var init_tmux_routing = __esm({
1728
1746
  init_provider_table();
1729
1747
  init_intercom_queue();
1730
1748
  init_plan_limits();
1749
+ init_employees();
1731
1750
  SPAWN_LOCK_DIR = path8.join(os6.homedir(), ".exe-os", "spawn-locks");
1732
1751
  SESSION_CACHE = path8.join(os6.homedir(), ".exe-os", "session-cache");
1733
1752
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
@@ -2010,6 +2029,36 @@ async function listTasks(input) {
2010
2029
  tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2011
2030
  }));
2012
2031
  }
2032
+ function isTmuxSessionAlive(identifier) {
2033
+ if (!identifier || identifier === "unknown") return true;
2034
+ try {
2035
+ if (identifier.startsWith("%")) {
2036
+ const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
2037
+ timeout: 2e3,
2038
+ encoding: "utf8",
2039
+ stdio: ["pipe", "pipe", "pipe"]
2040
+ });
2041
+ return output.split("\n").some((l) => l.trim() === identifier);
2042
+ } else {
2043
+ execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2044
+ timeout: 2e3,
2045
+ stdio: ["pipe", "pipe", "pipe"]
2046
+ });
2047
+ return true;
2048
+ }
2049
+ } catch {
2050
+ if (identifier.startsWith("%")) return true;
2051
+ try {
2052
+ execSync5("tmux list-sessions", {
2053
+ timeout: 2e3,
2054
+ stdio: ["pipe", "pipe", "pipe"]
2055
+ });
2056
+ return false;
2057
+ } catch {
2058
+ return true;
2059
+ }
2060
+ }
2061
+ }
2013
2062
  function checkStaleCompletion(taskContext, taskCreatedAt) {
2014
2063
  if (!taskContext) return null;
2015
2064
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -2072,13 +2121,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2072
2121
  });
2073
2122
  if (claim.rowsAffected === 0) {
2074
2123
  const current = await client.execute({
2075
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
2124
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
2076
2125
  args: [taskId]
2077
2126
  });
2078
2127
  const cur = current.rows[0];
2079
- const status = cur?.status ?? "unknown";
2080
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2081
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
2128
+ const curStatus = cur?.status ?? "unknown";
2129
+ const claimedBySession = cur?.assigned_tmux ?? "";
2130
+ const assignedBy = cur?.assigned_by ?? "";
2131
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
2132
+ process.stderr.write(
2133
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
2134
+ `
2135
+ );
2136
+ await client.execute({
2137
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
2138
+ args: [now, taskId]
2139
+ });
2140
+ const retried = await client.execute({
2141
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
2142
+ args: [tmuxSession, now, taskId]
2143
+ });
2144
+ if (retried.rowsAffected > 0) {
2145
+ try {
2146
+ await writeCheckpoint({
2147
+ taskId,
2148
+ step: "reclaimed_dead_session",
2149
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
2150
+ });
2151
+ } catch {
2152
+ }
2153
+ return { row, taskFile, now, taskId };
2154
+ }
2155
+ }
2156
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2157
+ process.stderr.write(
2158
+ `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2159
+ `
2160
+ );
2161
+ await client.execute({
2162
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
2163
+ args: [tmuxSession, now, taskId]
2164
+ });
2165
+ try {
2166
+ await writeCheckpoint({
2167
+ taskId,
2168
+ step: "assigner_override",
2169
+ contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
2170
+ });
2171
+ } catch {
2172
+ }
2173
+ return { row, taskFile, now, taskId };
2174
+ }
2175
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
2176
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
2082
2177
  }
2083
2178
  try {
2084
2179
  await writeCheckpoint({
@@ -2176,7 +2271,7 @@ var init_tasks_crud = __esm({
2176
2271
  "use strict";
2177
2272
  init_database();
2178
2273
  init_task_scope();
2179
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
2274
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2180
2275
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2181
2276
  }
2182
2277
  });
@@ -2533,7 +2628,7 @@ function findSessionForProject(projectName) {
2533
2628
  const sessions = listSessions();
2534
2629
  for (const s of sessions) {
2535
2630
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2536
- if (proj === projectName && s.agentId === "exe") return s;
2631
+ if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2537
2632
  }
2538
2633
  return null;
2539
2634
  }
@@ -2573,12 +2668,13 @@ var init_session_scope = __esm({
2573
2668
  init_session_registry();
2574
2669
  init_project_name();
2575
2670
  init_tmux_routing();
2671
+ init_employees();
2576
2672
  }
2577
2673
  });
2578
2674
 
2579
2675
  // src/lib/tasks-notify.ts
2580
2676
  async function dispatchTaskToEmployee(input) {
2581
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
2677
+ if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2582
2678
  let crossProject = false;
2583
2679
  if (input.projectName) {
2584
2680
  try {
@@ -3021,6 +3117,24 @@ async function updateTask(input) {
3021
3117
  });
3022
3118
  } catch {
3023
3119
  }
3120
+ const assignedAgent = String(row.assigned_to);
3121
+ if (!isCoordinatorName(assignedAgent)) {
3122
+ try {
3123
+ const draftClient = getClient();
3124
+ if (input.status === "done") {
3125
+ await draftClient.execute({
3126
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3127
+ args: [assignedAgent]
3128
+ });
3129
+ } else if (input.status === "cancelled") {
3130
+ await draftClient.execute({
3131
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
3132
+ args: [assignedAgent]
3133
+ });
3134
+ }
3135
+ } catch {
3136
+ }
3137
+ }
3024
3138
  try {
3025
3139
  const client = getClient();
3026
3140
  const cascaded = await client.execute({
@@ -3039,8 +3153,8 @@ async function updateTask(input) {
3039
3153
  }
3040
3154
  const isTerminal = input.status === "done" || input.status === "needs_review";
3041
3155
  if (isTerminal) {
3042
- const isExe = String(row.assigned_to) === "exe";
3043
- if (!isExe) {
3156
+ const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3157
+ if (!isCoordinator) {
3044
3158
  notifyTaskDone();
3045
3159
  }
3046
3160
  await markTaskNotificationsRead(taskFile);
@@ -3064,7 +3178,7 @@ async function updateTask(input) {
3064
3178
  }
3065
3179
  }
3066
3180
  }
3067
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
3181
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3068
3182
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3069
3183
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3070
3184
  taskId,
@@ -3080,7 +3194,7 @@ async function updateTask(input) {
3080
3194
  });
3081
3195
  }
3082
3196
  let nextTask;
3083
- if (isTerminal && String(row.assigned_to) !== "exe") {
3197
+ if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3084
3198
  try {
3085
3199
  nextTask = await findNextTask(String(row.assigned_to));
3086
3200
  } catch {
@@ -3107,12 +3221,14 @@ async function updateTask(input) {
3107
3221
  async function deleteTask(taskId, baseDir) {
3108
3222
  const client = getClient();
3109
3223
  const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
3110
- const reviewer = assignedBy || "exe";
3224
+ const coordinatorName = getCoordinatorName();
3225
+ const reviewer = assignedBy || coordinatorName;
3111
3226
  const reviewSlug = `review-${assignedTo}-${taskSlug}`;
3112
3227
  const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
3228
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
3113
3229
  await client.execute({
3114
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
3115
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
3230
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
3231
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
3116
3232
  });
3117
3233
  await markAsReadByTaskFile(taskFile);
3118
3234
  await markAsReadByTaskFile(reviewFile);
@@ -3123,6 +3239,7 @@ var init_tasks = __esm({
3123
3239
  init_config();
3124
3240
  init_notifications();
3125
3241
  init_state_bus();
3242
+ init_employees();
3126
3243
  init_tasks_crud();
3127
3244
  init_tasks_review();
3128
3245
  init_tasks_crud();