@askexenow/exe-os 0.9.113 → 0.9.115

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 (86) hide show
  1. package/dist/bin/agentic-ontology-backfill.js +36 -12
  2. package/dist/bin/agentic-reflection-backfill.js +36 -12
  3. package/dist/bin/agentic-semantic-label.js +36 -12
  4. package/dist/bin/backfill-conversations.js +36 -12
  5. package/dist/bin/backfill-responses.js +36 -12
  6. package/dist/bin/backfill-vectors.js +36 -12
  7. package/dist/bin/bulk-sync-postgres.js +36 -12
  8. package/dist/bin/cleanup-stale-review-tasks.js +470 -113
  9. package/dist/bin/cli.js +413 -62
  10. package/dist/bin/exe-agent.js +27 -0
  11. package/dist/bin/exe-assign.js +36 -12
  12. package/dist/bin/exe-boot.js +246 -54
  13. package/dist/bin/exe-call.js +8 -0
  14. package/dist/bin/exe-cloud.js +47 -12
  15. package/dist/bin/exe-dispatch.js +348 -53
  16. package/dist/bin/exe-doctor.js +51 -13
  17. package/dist/bin/exe-export-behaviors.js +37 -12
  18. package/dist/bin/exe-forget.js +36 -12
  19. package/dist/bin/exe-gateway.js +348 -53
  20. package/dist/bin/exe-heartbeat.js +471 -113
  21. package/dist/bin/exe-kill.js +36 -12
  22. package/dist/bin/exe-launch-agent.js +117 -18
  23. package/dist/bin/exe-new-employee.js +9 -1
  24. package/dist/bin/exe-pending-messages.js +452 -95
  25. package/dist/bin/exe-pending-notifications.js +452 -95
  26. package/dist/bin/exe-pending-reviews.js +452 -95
  27. package/dist/bin/exe-rename.js +36 -12
  28. package/dist/bin/exe-review.js +36 -12
  29. package/dist/bin/exe-search.js +37 -12
  30. package/dist/bin/exe-session-cleanup.js +348 -53
  31. package/dist/bin/exe-settings.js +12 -0
  32. package/dist/bin/exe-start-codex.js +46 -13
  33. package/dist/bin/exe-start-opencode.js +46 -13
  34. package/dist/bin/exe-status.js +460 -114
  35. package/dist/bin/exe-support.js +12 -0
  36. package/dist/bin/exe-team.js +36 -12
  37. package/dist/bin/git-sweep.js +348 -53
  38. package/dist/bin/graph-backfill.js +36 -12
  39. package/dist/bin/graph-export.js +36 -12
  40. package/dist/bin/install.js +9 -1
  41. package/dist/bin/intercom-check.js +255 -53
  42. package/dist/bin/scan-tasks.js +348 -53
  43. package/dist/bin/setup.js +74 -12
  44. package/dist/bin/shard-migrate.js +36 -12
  45. package/dist/gateway/index.js +348 -53
  46. package/dist/hooks/bug-report-worker.js +348 -53
  47. package/dist/hooks/codex-stop-task-finalizer.js +308 -37
  48. package/dist/hooks/commit-complete.js +348 -53
  49. package/dist/hooks/error-recall.js +37 -12
  50. package/dist/hooks/ingest.js +363 -54
  51. package/dist/hooks/instructions-loaded.js +36 -12
  52. package/dist/hooks/notification.js +36 -12
  53. package/dist/hooks/post-compact.js +426 -72
  54. package/dist/hooks/post-tool-combined.js +501 -146
  55. package/dist/hooks/pre-compact.js +348 -53
  56. package/dist/hooks/pre-tool-use.js +92 -13
  57. package/dist/hooks/prompt-submit.js +348 -53
  58. package/dist/hooks/session-end.js +158 -53
  59. package/dist/hooks/session-start.js +66 -13
  60. package/dist/hooks/stop.js +420 -72
  61. package/dist/hooks/subagent-stop.js +419 -72
  62. package/dist/hooks/summary-worker.js +442 -121
  63. package/dist/index.js +375 -53
  64. package/dist/lib/agent-config.js +8 -0
  65. package/dist/lib/cloud-sync.js +35 -12
  66. package/dist/lib/config.js +13 -0
  67. package/dist/lib/consolidation.js +9 -1
  68. package/dist/lib/embedder.js +13 -0
  69. package/dist/lib/employees.js +8 -0
  70. package/dist/lib/exe-daemon.js +524 -60
  71. package/dist/lib/hybrid-search.js +37 -12
  72. package/dist/lib/keychain.js +25 -13
  73. package/dist/lib/messaging.js +395 -74
  74. package/dist/lib/schedules.js +36 -12
  75. package/dist/lib/skill-learning.js +21 -0
  76. package/dist/lib/store.js +36 -12
  77. package/dist/lib/tasks.js +324 -41
  78. package/dist/lib/tmux-routing.js +324 -41
  79. package/dist/mcp/server.js +374 -54
  80. package/dist/mcp/tools/create-task.js +324 -41
  81. package/dist/mcp/tools/list-tasks.js +406 -57
  82. package/dist/mcp/tools/send-message.js +395 -74
  83. package/dist/mcp/tools/update-task.js +324 -41
  84. package/dist/runtime/index.js +375 -53
  85. package/dist/tui/App.js +377 -55
  86. package/package.json +1 -1
@@ -154,6 +154,17 @@ function normalizeOrchestration(raw) {
154
154
  const userOrg = raw.orchestration ?? {};
155
155
  raw.orchestration = { ...defaultOrg, ...userOrg };
156
156
  }
157
+ function normalizeCloudEndpoint(raw) {
158
+ const cloud = raw.cloud;
159
+ if (!cloud?.endpoint) return;
160
+ const ep = String(cloud.endpoint);
161
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
162
+ cloud.endpoint = "https://cloud.askexe.com";
163
+ process.stderr.write(
164
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
165
+ );
166
+ }
167
+ }
157
168
  async function loadConfig() {
158
169
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
159
170
  await ensurePrivateDir(dir);
@@ -179,6 +190,7 @@ async function loadConfig() {
179
190
  normalizeSessionLifecycle(migratedCfg);
180
191
  normalizeAutoUpdate(migratedCfg);
181
192
  normalizeOrchestration(migratedCfg);
193
+ normalizeCloudEndpoint(migratedCfg);
182
194
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
183
195
  if (config.dbPath.startsWith("~")) {
184
196
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -207,6 +219,7 @@ function loadConfigSync() {
207
219
  normalizeSessionLifecycle(migratedCfg);
208
220
  normalizeAutoUpdate(migratedCfg);
209
221
  normalizeOrchestration(migratedCfg);
222
+ normalizeCloudEndpoint(migratedCfg);
210
223
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
211
224
  if (config.dbPath.startsWith("~")) {
212
225
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -435,6 +448,7 @@ __export(agent_config_exports, {
435
448
  clearAgentRuntime: () => clearAgentRuntime,
436
449
  getAgentRuntime: () => getAgentRuntime,
437
450
  loadAgentConfig: () => loadAgentConfig,
451
+ normalizeCcModelName: () => normalizeCcModelName,
438
452
  saveAgentConfig: () => saveAgentConfig,
439
453
  setAgentMcps: () => setAgentMcps,
440
454
  setAgentRuntime: () => setAgentRuntime
@@ -463,6 +477,13 @@ function getAgentRuntime(agentId) {
463
477
  if (orgDefault) return orgDefault;
464
478
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
465
479
  }
480
+ function normalizeCcModelName(model) {
481
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
482
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
483
+ ccModel += "[1m]";
484
+ }
485
+ return ccModel;
486
+ }
466
487
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
467
488
  const knownModels = KNOWN_RUNTIMES[runtime];
468
489
  if (!knownModels) {
@@ -3782,7 +3803,7 @@ var init_database = __esm({
3782
3803
  });
3783
3804
 
3784
3805
  // src/lib/keychain.ts
3785
- import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
3806
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2, rename, copyFile } from "fs/promises";
3786
3807
  import { existsSync as existsSync10, statSync as statSync3 } from "fs";
3787
3808
  import { execSync as execSync5 } from "child_process";
3788
3809
  import path10 from "path";
@@ -3817,12 +3838,14 @@ function linuxSecretAvailable() {
3817
3838
  function isRootOnlyTrustedServerKeyFile(keyPath) {
3818
3839
  if (process.platform !== "linux") return false;
3819
3840
  try {
3820
- const uid = typeof os6.userInfo().uid === "number" ? os6.userInfo().uid : -1;
3821
3841
  const st = statSync3(keyPath);
3822
3842
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
3843
+ const uid = typeof os6.userInfo().uid === "number" ? os6.userInfo().uid : -1;
3823
3844
  if (uid === 0) return true;
3824
3845
  const exeOsDir = process.env.EXE_OS_DIR;
3825
- return Boolean(exeOsDir && path10.resolve(keyPath).startsWith(path10.resolve(exeOsDir) + path10.sep));
3846
+ if (exeOsDir && path10.resolve(keyPath).startsWith(path10.resolve(exeOsDir) + path10.sep)) return true;
3847
+ if (!linuxSecretAvailable()) return true;
3848
+ return false;
3826
3849
  } catch {
3827
3850
  return false;
3828
3851
  }
@@ -3972,15 +3995,25 @@ async function writeMachineBoundFileFallback(b64) {
3972
3995
  await mkdir3(dir, { recursive: true });
3973
3996
  const keyPath = getKeyPath();
3974
3997
  const machineKey = deriveMachineKey();
3975
- if (machineKey) {
3976
- const encrypted = encryptWithMachineKey(b64, machineKey);
3977
- await writeFile3(keyPath, encrypted + "\n", "utf-8");
3978
- await chmod2(keyPath, 384);
3979
- return "encrypted";
3998
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
3999
+ const result = machineKey ? "encrypted" : "plaintext";
4000
+ const tmpPath = keyPath + ".tmp";
4001
+ try {
4002
+ if (existsSync10(keyPath)) {
4003
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
4004
+ });
4005
+ }
4006
+ await writeFile3(tmpPath, content, "utf-8");
4007
+ await chmod2(tmpPath, 384);
4008
+ await rename(tmpPath, keyPath);
4009
+ } catch (err) {
4010
+ try {
4011
+ await unlink(tmpPath);
4012
+ } catch {
4013
+ }
4014
+ throw err;
3980
4015
  }
3981
- await writeFile3(keyPath, b64 + "\n", "utf-8");
3982
- await chmod2(keyPath, 384);
3983
- return "plaintext";
4016
+ return result;
3984
4017
  }
3985
4018
  async function getMasterKey() {
3986
4019
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -4047,7 +4080,7 @@ async function getMasterKey() {
4047
4080
  b64Value = content;
4048
4081
  }
4049
4082
  const key = Buffer.from(b64Value, "base64");
4050
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
4083
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
4051
4084
  return key;
4052
4085
  }
4053
4086
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
@@ -6594,6 +6627,7 @@ var init_provider_table = __esm({
6594
6627
  // src/lib/intercom-queue.ts
6595
6628
  var intercom_queue_exports = {};
6596
6629
  __export(intercom_queue_exports, {
6630
+ _resetDrainGuard: () => _resetDrainGuard,
6597
6631
  clearQueueForAgent: () => clearQueueForAgent,
6598
6632
  drainForSession: () => drainForSession,
6599
6633
  drainQueue: () => drainQueue,
@@ -6639,38 +6673,47 @@ function queueIntercom(targetSession, reason) {
6639
6673
  writeQueue(queue);
6640
6674
  }
6641
6675
  function drainQueue(isSessionBusy2, sendKeys) {
6676
+ if (_draining) {
6677
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
6678
+ return { drained: 0, failed: 0 };
6679
+ }
6642
6680
  const queue = readQueue();
6643
6681
  if (queue.length === 0) return { drained: 0, failed: 0 };
6682
+ _draining = true;
6644
6683
  const remaining = [];
6645
6684
  let drained = 0;
6646
6685
  let failed = 0;
6647
- for (const item of queue) {
6648
- const age = Date.now() - new Date(item.queuedAt).getTime();
6649
- if (age > TTL_MS) {
6650
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6651
- failed++;
6652
- continue;
6653
- }
6654
- try {
6655
- if (!isSessionBusy2(item.targetSession)) {
6656
- const success = sendKeys(item.targetSession);
6657
- if (success) {
6658
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6659
- drained++;
6660
- continue;
6686
+ try {
6687
+ for (const item of queue) {
6688
+ const age = Date.now() - new Date(item.queuedAt).getTime();
6689
+ if (age > TTL_MS) {
6690
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6691
+ failed++;
6692
+ continue;
6693
+ }
6694
+ try {
6695
+ if (!isSessionBusy2(item.targetSession)) {
6696
+ const success = sendKeys(item.targetSession);
6697
+ if (success) {
6698
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6699
+ drained++;
6700
+ continue;
6701
+ }
6661
6702
  }
6703
+ } catch {
6662
6704
  }
6663
- } catch {
6664
- }
6665
- item.attempts++;
6666
- if (item.attempts >= MAX_RETRIES2) {
6667
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6668
- failed++;
6669
- continue;
6705
+ item.attempts++;
6706
+ if (item.attempts >= MAX_RETRIES2) {
6707
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6708
+ failed++;
6709
+ continue;
6710
+ }
6711
+ remaining.push(item);
6670
6712
  }
6671
- remaining.push(item);
6713
+ writeQueue(remaining);
6714
+ } finally {
6715
+ _draining = false;
6672
6716
  }
6673
- writeQueue(remaining);
6674
6717
  return { drained, failed };
6675
6718
  }
6676
6719
  function drainForSession(targetSession, sendKeys) {
@@ -6695,6 +6738,9 @@ function clearQueueForAgent(agentName) {
6695
6738
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
6696
6739
  }
6697
6740
  }
6741
+ function _resetDrainGuard() {
6742
+ _draining = false;
6743
+ }
6698
6744
  function logQueue(msg) {
6699
6745
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
6700
6746
  `;
@@ -6706,13 +6752,14 @@ function logQueue(msg) {
6706
6752
  } catch {
6707
6753
  }
6708
6754
  }
6709
- var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
6755
+ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, _draining, INTERCOM_LOG;
6710
6756
  var init_intercom_queue = __esm({
6711
6757
  "src/lib/intercom-queue.ts"() {
6712
6758
  "use strict";
6713
6759
  QUEUE_PATH = path14.join(os8.homedir(), ".exe-os", "intercom-queue.json");
6714
6760
  MAX_RETRIES2 = 5;
6715
6761
  TTL_MS = 60 * 60 * 1e3;
6762
+ _draining = false;
6716
6763
  INTERCOM_LOG = path14.join(os8.homedir(), ".exe-os", "intercom.log");
6717
6764
  }
6718
6765
  });
@@ -7327,6 +7374,17 @@ var init_agent_symlinks = __esm({
7327
7374
  });
7328
7375
 
7329
7376
  // src/lib/notifications.ts
7377
+ var notifications_exports = {};
7378
+ __export(notifications_exports, {
7379
+ cleanupOldNotifications: () => cleanupOldNotifications,
7380
+ formatNotifications: () => formatNotifications,
7381
+ markAsRead: () => markAsRead,
7382
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
7383
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
7384
+ migrateJsonNotifications: () => migrateJsonNotifications,
7385
+ readUnreadNotifications: () => readUnreadNotifications,
7386
+ writeNotification: () => writeNotification
7387
+ });
7330
7388
  import crypto3 from "crypto";
7331
7389
  import path18 from "path";
7332
7390
  import os11 from "os";
@@ -7363,6 +7421,52 @@ async function writeNotification(notification) {
7363
7421
  `);
7364
7422
  }
7365
7423
  }
7424
+ async function readUnreadNotifications(agentFilter, sessionScope) {
7425
+ try {
7426
+ const client = getClient();
7427
+ const conditions = ["read = 0"];
7428
+ const args = [];
7429
+ const scope = strictSessionScopeFilter(sessionScope);
7430
+ if (agentFilter) {
7431
+ conditions.push("agent_id = ?");
7432
+ args.push(agentFilter);
7433
+ }
7434
+ const result = await client.execute({
7435
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
7436
+ FROM notifications
7437
+ WHERE ${conditions.join(" AND ")}${scope.sql}
7438
+ ORDER BY created_at ASC`,
7439
+ args: [...args, ...scope.args]
7440
+ });
7441
+ return result.rows.map((r) => ({
7442
+ id: String(r.id),
7443
+ agentId: String(r.agent_id),
7444
+ agentRole: String(r.agent_role),
7445
+ event: String(r.event),
7446
+ project: String(r.project),
7447
+ summary: String(r.summary),
7448
+ taskFile: r.task_file ? String(r.task_file) : void 0,
7449
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
7450
+ timestamp: String(r.created_at),
7451
+ read: false
7452
+ }));
7453
+ } catch {
7454
+ return [];
7455
+ }
7456
+ }
7457
+ async function markAsRead(ids, sessionScope) {
7458
+ if (ids.length === 0) return;
7459
+ try {
7460
+ const client = getClient();
7461
+ const placeholders = ids.map(() => "?").join(", ");
7462
+ const scope = strictSessionScopeFilter(sessionScope);
7463
+ await client.execute({
7464
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
7465
+ args: [...ids, ...scope.args]
7466
+ });
7467
+ } catch {
7468
+ }
7469
+ }
7366
7470
  async function markAsReadByTaskFile(taskFile, sessionScope) {
7367
7471
  try {
7368
7472
  const client = getClient();
@@ -7375,11 +7479,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
7375
7479
  } catch {
7376
7480
  }
7377
7481
  }
7482
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
7483
+ try {
7484
+ const client = getClient();
7485
+ const cutoff = new Date(
7486
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
7487
+ ).toISOString();
7488
+ const scope = strictSessionScopeFilter(sessionScope);
7489
+ const result = await client.execute({
7490
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
7491
+ args: [cutoff, ...scope.args]
7492
+ });
7493
+ return result.rowsAffected;
7494
+ } catch {
7495
+ return 0;
7496
+ }
7497
+ }
7498
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
7499
+ try {
7500
+ const client = getClient();
7501
+ const scope = strictSessionScopeFilter(sessionScope);
7502
+ const result = await client.execute({
7503
+ sql: `UPDATE notifications SET read = 1
7504
+ WHERE read = 0
7505
+ AND task_file IS NOT NULL
7506
+ ${scope.sql}
7507
+ AND task_file IN (
7508
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
7509
+ )`,
7510
+ args: [...scope.args, ...scope.args]
7511
+ });
7512
+ return result.rowsAffected;
7513
+ } catch {
7514
+ return 0;
7515
+ }
7516
+ }
7517
+ function formatNotifications(notifications) {
7518
+ if (notifications.length === 0) return "";
7519
+ const grouped = /* @__PURE__ */ new Map();
7520
+ for (const n of notifications) {
7521
+ const key = `${n.agentId}|${n.agentRole}`;
7522
+ if (!grouped.has(key)) grouped.set(key, []);
7523
+ grouped.get(key).push(n);
7524
+ }
7525
+ const lines = [];
7526
+ lines.push(`## Notifications (${notifications.length} unread)
7527
+ `);
7528
+ for (const [key, items] of grouped) {
7529
+ const [agentId, agentRole] = key.split("|");
7530
+ lines.push(`**${agentId}** (${agentRole}):`);
7531
+ for (const item of items) {
7532
+ const ago = formatTimeAgo(item.timestamp);
7533
+ const icon = eventIcon(item.event);
7534
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
7535
+ }
7536
+ lines.push("");
7537
+ }
7538
+ return lines.join("\n");
7539
+ }
7540
+ async function migrateJsonNotifications() {
7541
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path18.join(os11.homedir(), ".exe-os");
7542
+ const notifDir = path18.join(base, "notifications");
7543
+ if (!existsSync17(notifDir)) return 0;
7544
+ let migrated = 0;
7545
+ try {
7546
+ const files = readdirSync4(notifDir).filter((f) => f.endsWith(".json"));
7547
+ if (files.length === 0) return 0;
7548
+ const client = getClient();
7549
+ for (const file of files) {
7550
+ try {
7551
+ const filePath = path18.join(notifDir, file);
7552
+ const data = JSON.parse(readFileSync12(filePath, "utf8"));
7553
+ await client.execute({
7554
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
7555
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
7556
+ args: [
7557
+ crypto3.randomUUID(),
7558
+ data.agentId ?? "unknown",
7559
+ data.agentRole ?? "unknown",
7560
+ data.event ?? "session_summary",
7561
+ data.project ?? "unknown",
7562
+ data.summary ?? "",
7563
+ data.taskFile ?? null,
7564
+ null,
7565
+ data.read ? 1 : 0,
7566
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
7567
+ ]
7568
+ });
7569
+ unlinkSync7(filePath);
7570
+ migrated++;
7571
+ } catch {
7572
+ }
7573
+ }
7574
+ try {
7575
+ const remaining = readdirSync4(notifDir);
7576
+ if (remaining.length === 0) {
7577
+ rmdirSync(notifDir);
7578
+ }
7579
+ } catch {
7580
+ }
7581
+ } catch {
7582
+ }
7583
+ return migrated;
7584
+ }
7585
+ function eventIcon(event) {
7586
+ switch (event) {
7587
+ case "task_complete":
7588
+ return "Completed:";
7589
+ case "task_needs_fix":
7590
+ return "Needs fix:";
7591
+ case "session_summary":
7592
+ return "Session:";
7593
+ case "error_spike":
7594
+ return "Errors:";
7595
+ case "orphan_task":
7596
+ return "Orphan:";
7597
+ case "subtasks_complete":
7598
+ return "Subtasks done:";
7599
+ case "capacity_relaunch":
7600
+ return "Relaunched:";
7601
+ }
7602
+ }
7603
+ function formatTimeAgo(timestamp) {
7604
+ const diffMs = Date.now() - new Date(timestamp).getTime();
7605
+ const mins = Math.floor(diffMs / 6e4);
7606
+ if (mins < 1) return "just now";
7607
+ if (mins < 60) return `${mins}m ago`;
7608
+ const hours = Math.floor(mins / 60);
7609
+ if (hours < 24) return `${hours}h ago`;
7610
+ const days = Math.floor(hours / 24);
7611
+ return `${days}d ago`;
7612
+ }
7613
+ var CLEANUP_DAYS;
7378
7614
  var init_notifications = __esm({
7379
7615
  "src/lib/notifications.ts"() {
7380
7616
  "use strict";
7381
7617
  init_database();
7382
7618
  init_task_scope();
7619
+ CLEANUP_DAYS = 7;
7383
7620
  }
7384
7621
  });
7385
7622
 
@@ -7634,7 +7871,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
7634
7871
  taskFile
7635
7872
  });
7636
7873
  const originalPriority = String(row.priority).toLowerCase();
7637
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
7874
+ const resultLower = result?.toLowerCase() ?? "";
7875
+ const hasTestEvidence = (
7876
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
7877
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
7878
+ );
7879
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
7880
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
7638
7881
  if (!autoApprove) {
7639
7882
  try {
7640
7883
  const key = getSessionKey();
@@ -7642,6 +7885,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
7642
7885
  if (exeSession) {
7643
7886
  sendIntercom(exeSession);
7644
7887
  }
7888
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
7889
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
7890
+ if (exeSession) {
7891
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
7892
+ sendIntercom(reviewerSession);
7893
+ }
7894
+ }
7645
7895
  } catch {
7646
7896
  }
7647
7897
  }
@@ -8530,6 +8780,20 @@ async function updateTask(input2) {
8530
8780
  notifyTaskDone();
8531
8781
  }
8532
8782
  await markTaskNotificationsRead(taskFile);
8783
+ if (input2.status === "needs_review" && !isCoordinator) {
8784
+ try {
8785
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
8786
+ await writeNotification2({
8787
+ agentId: String(row.assigned_to),
8788
+ agentRole: String(row.assigned_to),
8789
+ event: "task_complete",
8790
+ project: String(row.project_name),
8791
+ summary: `"${String(row.title)}" is ready for review`,
8792
+ taskFile
8793
+ });
8794
+ } catch {
8795
+ }
8796
+ }
8533
8797
  if (input2.status === "done" || input2.status === "closed") {
8534
8798
  try {
8535
8799
  await cascadeUnblock(taskId, input2.baseDir, now);
@@ -8962,18 +9226,31 @@ function acquireSpawnLock2(sessionName) {
8962
9226
  mkdirSync12(SPAWN_LOCK_DIR, { recursive: true });
8963
9227
  }
8964
9228
  const lockFile = spawnLockPath(sessionName);
8965
- if (existsSync19(lockFile)) {
8966
- try {
8967
- const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
8968
- const age = Date.now() - lock.timestamp;
8969
- if (isProcessAlive(lock.pid) && age < 6e4) {
8970
- return false;
8971
- }
8972
- } catch {
9229
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
9230
+ const { openSync: openSync4, closeSync: closeSync4, writeSync } = __require("fs");
9231
+ const { constants } = __require("fs");
9232
+ try {
9233
+ const fd = openSync4(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
9234
+ writeSync(fd, lockData);
9235
+ closeSync4(fd);
9236
+ return true;
9237
+ } catch (err) {
9238
+ if (err?.code !== "EEXIST") {
9239
+ return true;
8973
9240
  }
8974
9241
  }
8975
- writeFileSync11(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
8976
- return true;
9242
+ try {
9243
+ const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
9244
+ const age = Date.now() - lock.timestamp;
9245
+ if (isProcessAlive(lock.pid) && age < 6e4) {
9246
+ return false;
9247
+ }
9248
+ writeFileSync11(lockFile, lockData);
9249
+ return true;
9250
+ } catch {
9251
+ writeFileSync11(lockFile, lockData);
9252
+ return true;
9253
+ }
8977
9254
  }
8978
9255
  function releaseSpawnLock2(sessionName) {
8979
9256
  try {
@@ -9052,6 +9329,21 @@ function parseParentExe(sessionName, agentId) {
9052
9329
  function extractRootExe(name) {
9053
9330
  if (!name) return null;
9054
9331
  if (!name.includes("-")) return name;
9332
+ try {
9333
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
9334
+ if (roster.length > 0) {
9335
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
9336
+ for (const agentName of sortedNames) {
9337
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9338
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
9339
+ const match = name.match(regex);
9340
+ if (match) {
9341
+ return extractRootExe(match[1]);
9342
+ }
9343
+ }
9344
+ }
9345
+ } catch {
9346
+ }
9055
9347
  const parts = name.split("-").filter(Boolean);
9056
9348
  return parts.length > 0 ? parts[parts.length - 1] : null;
9057
9349
  }
@@ -9070,6 +9362,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
9070
9362
  function getParentExe(sessionKey) {
9071
9363
  try {
9072
9364
  const data = JSON.parse(readFileSync13(path23.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
9365
+ if (data.registeredAt) {
9366
+ const age = Date.now() - new Date(data.registeredAt).getTime();
9367
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
9368
+ }
9073
9369
  return data.parentExe || null;
9074
9370
  } catch {
9075
9371
  return null;
@@ -9618,7 +9914,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
9618
9914
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
9619
9915
  } catch {
9620
9916
  }
9621
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
9917
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
9622
9918
  if (ccProvider !== DEFAULT_PROVIDER) {
9623
9919
  const cfg = PROVIDER_TABLE[ccProvider];
9624
9920
  if (cfg?.apiKeyEnv) {
@@ -9653,10 +9949,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
9653
9949
  }
9654
9950
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
9655
9951
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
9656
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
9657
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
9658
- ccModel += "[1m]";
9659
- }
9952
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
9953
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
9660
9954
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
9661
9955
  }
9662
9956
  }
@@ -9757,7 +10051,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
9757
10051
  releaseSpawnLock2(sessionName);
9758
10052
  return { sessionName };
9759
10053
  }
9760
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
10054
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, PARENT_EXE_CACHE_TTL_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
9761
10055
  var init_tmux_routing = __esm({
9762
10056
  "src/lib/tmux-routing.ts"() {
9763
10057
  "use strict";
@@ -9777,6 +10071,7 @@ var init_tmux_routing = __esm({
9777
10071
  SESSION_CACHE = path23.join(os12.homedir(), ".exe-os", "session-cache");
9778
10072
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
9779
10073
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
10074
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
9780
10075
  VERIFY_PANE_LINES = 200;
9781
10076
  INTERCOM_DEBOUNCE_MS = 3e4;
9782
10077
  CODEX_DEBOUNCE_MS = 12e4;
@@ -10962,7 +11257,21 @@ function tryAcquireWorkerSlot() {
10962
11257
  for (const f of files) {
10963
11258
  if (!f.endsWith(".pid")) continue;
10964
11259
  if (f.startsWith("res-")) {
10965
- alive++;
11260
+ const resParts = f.replace(".pid", "").split("-");
11261
+ const resPid = parseInt(resParts[1] ?? "", 10);
11262
+ if (!isNaN(resPid) && resPid > 0) {
11263
+ try {
11264
+ process.kill(resPid, 0);
11265
+ alive++;
11266
+ } catch {
11267
+ try {
11268
+ unlinkSync4(path6.join(WORKER_PID_DIR, f));
11269
+ } catch {
11270
+ }
11271
+ }
11272
+ } else {
11273
+ alive++;
11274
+ }
10966
11275
  continue;
10967
11276
  }
10968
11277
  const dashIdx = f.lastIndexOf("-");