@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
@@ -324,23 +324,6 @@ var init_db_retry = __esm({
324
324
  }
325
325
  });
326
326
 
327
- // src/lib/database.ts
328
- import { createClient } from "@libsql/client";
329
- function getClient() {
330
- if (!_resilientClient) {
331
- throw new Error("Database client not initialized. Call initDatabase() first.");
332
- }
333
- return _resilientClient;
334
- }
335
- var _resilientClient;
336
- var init_database = __esm({
337
- "src/lib/database.ts"() {
338
- "use strict";
339
- init_db_retry();
340
- _resilientClient = null;
341
- }
342
- });
343
-
344
327
  // src/lib/config.ts
345
328
  import { readFile, writeFile, mkdir, chmod } from "fs/promises";
346
329
  import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
@@ -487,13 +470,7 @@ var init_config = __esm({
487
470
  wikiUrl: "",
488
471
  wikiApiKey: "",
489
472
  wikiSyncIntervalMs: 30 * 60 * 1e3,
490
- wikiWorkspaceMapping: {
491
- exe: "Executive",
492
- yoshi: "Engineering",
493
- mari: "Marketing",
494
- tom: "Engineering",
495
- sasha: "Production"
496
- },
473
+ wikiWorkspaceMapping: {},
497
474
  wikiAutoUpdate: true,
498
475
  wikiAutoUpdateThreshold: 0.5,
499
476
  wikiAutoUpdateCreateNew: true,
@@ -536,6 +513,22 @@ import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as r
536
513
  import { execSync as execSync3 } from "child_process";
537
514
  import path4 from "path";
538
515
  import os4 from "os";
516
+ function normalizeRole(role) {
517
+ return (role ?? "").trim().toLowerCase();
518
+ }
519
+ function isCoordinatorRole(role) {
520
+ return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
521
+ }
522
+ function getCoordinatorEmployee(employees) {
523
+ return employees.find((e) => isCoordinatorRole(e.role));
524
+ }
525
+ function getCoordinatorName(employees = loadEmployeesSync()) {
526
+ return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
527
+ }
528
+ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
529
+ if (!agentName) return false;
530
+ return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
531
+ }
539
532
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
540
533
  if (!existsSync4(employeesPath)) return [];
541
534
  try {
@@ -553,16 +546,36 @@ function isMultiInstance(agentName, employees) {
553
546
  if (!emp) return false;
554
547
  return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
555
548
  }
556
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
549
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
557
550
  var init_employees = __esm({
558
551
  "src/lib/employees.ts"() {
559
552
  "use strict";
560
553
  init_config();
561
554
  EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
555
+ DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
556
+ COORDINATOR_ROLE = "COO";
562
557
  MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
563
558
  }
564
559
  });
565
560
 
561
+ // src/lib/database.ts
562
+ import { createClient } from "@libsql/client";
563
+ function getClient() {
564
+ if (!_resilientClient) {
565
+ throw new Error("Database client not initialized. Call initDatabase() first.");
566
+ }
567
+ return _resilientClient;
568
+ }
569
+ var _resilientClient;
570
+ var init_database = __esm({
571
+ "src/lib/database.ts"() {
572
+ "use strict";
573
+ init_db_retry();
574
+ init_employees();
575
+ _resilientClient = null;
576
+ }
577
+ });
578
+
566
579
  // src/lib/license.ts
567
580
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
568
581
  import { randomUUID } from "crypto";
@@ -1074,6 +1087,36 @@ async function listTasks(input) {
1074
1087
  tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
1075
1088
  }));
1076
1089
  }
1090
+ function isTmuxSessionAlive(identifier) {
1091
+ if (!identifier || identifier === "unknown") return true;
1092
+ try {
1093
+ if (identifier.startsWith("%")) {
1094
+ const output = execSync4("tmux list-panes -a -F '#{pane_id}'", {
1095
+ timeout: 2e3,
1096
+ encoding: "utf8",
1097
+ stdio: ["pipe", "pipe", "pipe"]
1098
+ });
1099
+ return output.split("\n").some((l) => l.trim() === identifier);
1100
+ } else {
1101
+ execSync4(`tmux has-session -t ${JSON.stringify(identifier)}`, {
1102
+ timeout: 2e3,
1103
+ stdio: ["pipe", "pipe", "pipe"]
1104
+ });
1105
+ return true;
1106
+ }
1107
+ } catch {
1108
+ if (identifier.startsWith("%")) return true;
1109
+ try {
1110
+ execSync4("tmux list-sessions", {
1111
+ timeout: 2e3,
1112
+ stdio: ["pipe", "pipe", "pipe"]
1113
+ });
1114
+ return false;
1115
+ } catch {
1116
+ return true;
1117
+ }
1118
+ }
1119
+ }
1077
1120
  function checkStaleCompletion(taskContext, taskCreatedAt) {
1078
1121
  if (!taskContext) return null;
1079
1122
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -1136,13 +1179,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1136
1179
  });
1137
1180
  if (claim.rowsAffected === 0) {
1138
1181
  const current = await client.execute({
1139
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
1182
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
1140
1183
  args: [taskId]
1141
1184
  });
1142
1185
  const cur = current.rows[0];
1143
- const status = cur?.status ?? "unknown";
1144
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
1145
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
1186
+ const curStatus = cur?.status ?? "unknown";
1187
+ const claimedBySession = cur?.assigned_tmux ?? "";
1188
+ const assignedBy = cur?.assigned_by ?? "";
1189
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
1190
+ process.stderr.write(
1191
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
1192
+ `
1193
+ );
1194
+ await client.execute({
1195
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
1196
+ args: [now, taskId]
1197
+ });
1198
+ const retried = await client.execute({
1199
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
1200
+ args: [tmuxSession, now, taskId]
1201
+ });
1202
+ if (retried.rowsAffected > 0) {
1203
+ try {
1204
+ await writeCheckpoint({
1205
+ taskId,
1206
+ step: "reclaimed_dead_session",
1207
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
1208
+ });
1209
+ } catch {
1210
+ }
1211
+ return { row, taskFile, now, taskId };
1212
+ }
1213
+ }
1214
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
1215
+ process.stderr.write(
1216
+ `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
1217
+ `
1218
+ );
1219
+ await client.execute({
1220
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
1221
+ args: [tmuxSession, now, taskId]
1222
+ });
1223
+ try {
1224
+ await writeCheckpoint({
1225
+ taskId,
1226
+ step: "assigner_override",
1227
+ contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
1228
+ });
1229
+ } catch {
1230
+ }
1231
+ return { row, taskFile, now, taskId };
1232
+ }
1233
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
1234
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
1146
1235
  }
1147
1236
  try {
1148
1237
  await writeCheckpoint({
@@ -1240,7 +1329,7 @@ var init_tasks_crud = __esm({
1240
1329
  "use strict";
1241
1330
  init_database();
1242
1331
  init_task_scope();
1243
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
1332
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
1244
1333
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
1245
1334
  }
1246
1335
  });
@@ -1597,7 +1686,7 @@ function findSessionForProject(projectName) {
1597
1686
  const sessions = listSessions();
1598
1687
  for (const s of sessions) {
1599
1688
  const proj = s.projectDir.split("/").filter(Boolean).pop();
1600
- if (proj === projectName && s.agentId === "exe") return s;
1689
+ if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
1601
1690
  }
1602
1691
  return null;
1603
1692
  }
@@ -1637,12 +1726,13 @@ var init_session_scope = __esm({
1637
1726
  init_session_registry();
1638
1727
  init_project_name();
1639
1728
  init_tmux_routing();
1729
+ init_employees();
1640
1730
  }
1641
1731
  });
1642
1732
 
1643
1733
  // src/lib/tasks-notify.ts
1644
1734
  async function dispatchTaskToEmployee(input) {
1645
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
1735
+ if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
1646
1736
  let crossProject = false;
1647
1737
  if (input.projectName) {
1648
1738
  try {
@@ -2085,6 +2175,24 @@ async function updateTask(input) {
2085
2175
  });
2086
2176
  } catch {
2087
2177
  }
2178
+ const assignedAgent = String(row.assigned_to);
2179
+ if (!isCoordinatorName(assignedAgent)) {
2180
+ try {
2181
+ const draftClient = getClient();
2182
+ if (input.status === "done") {
2183
+ await draftClient.execute({
2184
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
2185
+ args: [assignedAgent]
2186
+ });
2187
+ } else if (input.status === "cancelled") {
2188
+ await draftClient.execute({
2189
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
2190
+ args: [assignedAgent]
2191
+ });
2192
+ }
2193
+ } catch {
2194
+ }
2195
+ }
2088
2196
  try {
2089
2197
  const client = getClient();
2090
2198
  const cascaded = await client.execute({
@@ -2103,8 +2211,8 @@ async function updateTask(input) {
2103
2211
  }
2104
2212
  const isTerminal = input.status === "done" || input.status === "needs_review";
2105
2213
  if (isTerminal) {
2106
- const isExe = String(row.assigned_to) === "exe";
2107
- if (!isExe) {
2214
+ const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
2215
+ if (!isCoordinator) {
2108
2216
  notifyTaskDone();
2109
2217
  }
2110
2218
  await markTaskNotificationsRead(taskFile);
@@ -2128,7 +2236,7 @@ async function updateTask(input) {
2128
2236
  }
2129
2237
  }
2130
2238
  }
2131
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
2239
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
2132
2240
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
2133
2241
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
2134
2242
  taskId,
@@ -2144,7 +2252,7 @@ async function updateTask(input) {
2144
2252
  });
2145
2253
  }
2146
2254
  let nextTask;
2147
- if (isTerminal && String(row.assigned_to) !== "exe") {
2255
+ if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
2148
2256
  try {
2149
2257
  nextTask = await findNextTask(String(row.assigned_to));
2150
2258
  } catch {
@@ -2171,12 +2279,14 @@ async function updateTask(input) {
2171
2279
  async function deleteTask(taskId, baseDir) {
2172
2280
  const client = getClient();
2173
2281
  const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
2174
- const reviewer = assignedBy || "exe";
2282
+ const coordinatorName = getCoordinatorName();
2283
+ const reviewer = assignedBy || coordinatorName;
2175
2284
  const reviewSlug = `review-${assignedTo}-${taskSlug}`;
2176
2285
  const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
2286
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
2177
2287
  await client.execute({
2178
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
2179
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
2288
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
2289
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
2180
2290
  });
2181
2291
  await markAsReadByTaskFile(taskFile);
2182
2292
  await markAsReadByTaskFile(reviewFile);
@@ -2188,6 +2298,7 @@ var init_tasks = __esm({
2188
2298
  init_config();
2189
2299
  init_notifications();
2190
2300
  init_state_bus();
2301
+ init_employees();
2191
2302
  init_tasks_crud();
2192
2303
  init_tasks_review();
2193
2304
  init_tasks_crud();
@@ -2273,7 +2384,7 @@ function _resetLastRelaunchCache() {
2273
2384
  }
2274
2385
  async function lastResumeCreatedAtMs(agentId) {
2275
2386
  const client = getClient();
2276
- const cmScope = sessionScopeFilter();
2387
+ const cmScope = sessionScopeFilter(null);
2277
2388
  const result = await client.execute({
2278
2389
  sql: `SELECT MAX(created_at) AS last_created_at
2279
2390
  FROM tasks
@@ -2298,7 +2409,7 @@ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
2298
2409
  const client = getClient();
2299
2410
  const now = (/* @__PURE__ */ new Date()).toISOString();
2300
2411
  const context = buildResumeContext(agentId, openTasks);
2301
- const rdScope = sessionScopeFilter();
2412
+ const rdScope = sessionScopeFilter(null);
2302
2413
  const existing = await client.execute({
2303
2414
  sql: `SELECT id FROM tasks
2304
2415
  WHERE assigned_to = ?
@@ -2332,7 +2443,7 @@ async function pollCapacityDead() {
2332
2443
  const transport = getTransport();
2333
2444
  const relaunched = [];
2334
2445
  const registered = listSessions().filter(
2335
- (s) => s.agentId !== "exe"
2446
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
2336
2447
  );
2337
2448
  if (registered.length === 0) return [];
2338
2449
  let liveSessions;
@@ -2392,7 +2503,7 @@ async function pollCapacityDead() {
2392
2503
  reason: "capacity"
2393
2504
  });
2394
2505
  const client = getClient();
2395
- const rlScope = sessionScopeFilter();
2506
+ const rlScope = sessionScopeFilter(null);
2396
2507
  const openTasks = await client.execute({
2397
2508
  sql: `SELECT id, title, priority, task_file, status
2398
2509
  FROM tasks
@@ -2446,6 +2557,7 @@ var init_capacity_monitor = __esm({
2446
2557
  init_session_kill_telemetry();
2447
2558
  init_tmux_routing();
2448
2559
  init_task_scope();
2560
+ init_employees();
2449
2561
  CAPACITY_PATTERNS = [
2450
2562
  /conversation is too long/i,
2451
2563
  /maximum context length/i,
@@ -2595,7 +2707,7 @@ function employeeSessionName(employee, exeSession, instance) {
2595
2707
  exeSession = root;
2596
2708
  } else {
2597
2709
  throw new Error(
2598
- `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
2710
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
2599
2711
  );
2600
2712
  }
2601
2713
  }
@@ -2615,8 +2727,10 @@ function parseParentExe(sessionName, agentId) {
2615
2727
  return match?.[1] ?? null;
2616
2728
  }
2617
2729
  function extractRootExe(name) {
2618
- const match = name.match(/(exe\d+)$/);
2619
- return match?.[1] ?? null;
2730
+ if (!name) return null;
2731
+ if (!name.includes("-")) return name;
2732
+ const parts = name.split("-").filter(Boolean);
2733
+ return parts.length > 0 ? parts[parts.length - 1] : null;
2620
2734
  }
2621
2735
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
2622
2736
  if (!existsSync10(SESSION_CACHE)) {
@@ -2761,12 +2875,14 @@ function isSessionBusy(sessionName) {
2761
2875
  return state === "thinking" || state === "tool";
2762
2876
  }
2763
2877
  function isExeSession(sessionName) {
2764
- return /^exe\d*$/.test(sessionName);
2878
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
2879
+ const coordinatorName = getCoordinatorName();
2880
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
2765
2881
  }
2766
2882
  function sendIntercom(targetSession) {
2767
2883
  const transport = getTransport();
2768
2884
  if (isExeSession(targetSession)) {
2769
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
2885
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
2770
2886
  return "skipped_exe";
2771
2887
  }
2772
2888
  if (isDebounced(targetSession)) {
@@ -2818,7 +2934,7 @@ function notifyParentExe(sessionKey) {
2818
2934
  if (result === "failed") {
2819
2935
  const rootExe = resolveExeSession();
2820
2936
  if (rootExe && rootExe !== target) {
2821
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
2937
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
2822
2938
  `);
2823
2939
  const fallback = sendIntercom(rootExe);
2824
2940
  return fallback !== "failed";
@@ -2828,8 +2944,8 @@ function notifyParentExe(sessionKey) {
2828
2944
  return true;
2829
2945
  }
2830
2946
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2831
- if (employeeName === "exe") {
2832
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
2947
+ if (employeeName === "exe" || isCoordinatorName(employeeName)) {
2948
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
2833
2949
  }
2834
2950
  try {
2835
2951
  assertEmployeeLimitSync();
@@ -2838,8 +2954,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2838
2954
  return { status: "failed", sessionName: "", error: err.message };
2839
2955
  }
2840
2956
  }
2841
- if (/-exe\d*$/.test(employeeName)) {
2842
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
2957
+ if (employeeName.includes("-")) {
2958
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
2843
2959
  return {
2844
2960
  status: "failed",
2845
2961
  sessionName: "",
@@ -2858,7 +2974,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2858
2974
  return {
2859
2975
  status: "failed",
2860
2976
  sessionName: "",
2861
- error: `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
2977
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
2862
2978
  };
2863
2979
  }
2864
2980
  }
@@ -3015,8 +3131,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3015
3131
  const ctxContent = [
3016
3132
  `## Session Context`,
3017
3133
  `You are running in tmux session: ${sessionName}.`,
3018
- `Your parent exe session is ${exeSession}.`,
3019
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
3134
+ `Your parent coordinator session is ${exeSession}.`,
3135
+ `Your employees (if any) use the -${exeSession} suffix.`
3020
3136
  ].join("\n");
3021
3137
  writeFileSync6(ctxFile, ctxContent);
3022
3138
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
@@ -3119,6 +3235,7 @@ var init_tmux_routing = __esm({
3119
3235
  init_provider_table();
3120
3236
  init_intercom_queue();
3121
3237
  init_plan_limits();
3238
+ init_employees();
3122
3239
  SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
3123
3240
  SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
3124
3241
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
@@ -78,16 +78,25 @@ function createWsClient(config, handlers) {
78
78
  } catch {
79
79
  }
80
80
  };
81
- ws.onclose = () => {
81
+ ws.onclose = (event) => {
82
82
  connected = false;
83
83
  if (heartbeatTimer) {
84
84
  clearInterval(heartbeatTimer);
85
85
  heartbeatTimer = null;
86
86
  }
87
+ const code = event.code ?? "unknown";
88
+ const reason = event.reason || "(none)";
89
+ process.stderr.write(
90
+ `[ws-client] Disconnected: code=${code} reason=${reason}
91
+ `
92
+ );
87
93
  handlers.onDisconnect();
88
94
  scheduleReconnect();
89
95
  };
90
- ws.onerror = () => {
96
+ ws.onerror = (event) => {
97
+ const errMsg = event.message ?? "unknown error";
98
+ process.stderr.write(`[ws-client] Error: ${errMsg}
99
+ `);
91
100
  };
92
101
  } catch {
93
102
  connected = false;
@@ -104,11 +113,12 @@ function createWsClient(config, handlers) {
104
113
  }
105
114
  function scheduleReconnect() {
106
115
  if (closed || reconnectTimer) return;
116
+ const jitter = reconnectDelay * (0.75 + Math.random() * 0.5);
107
117
  reconnectTimer = setTimeout(() => {
108
118
  reconnectTimer = null;
109
119
  reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
110
120
  connect();
111
- }, reconnectDelay);
121
+ }, jitter);
112
122
  }
113
123
  connect();
114
124
  return {