@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
@@ -219,6 +219,17 @@ function normalizeOrchestration(raw) {
219
219
  const userOrg = raw.orchestration ?? {};
220
220
  raw.orchestration = { ...defaultOrg, ...userOrg };
221
221
  }
222
+ function normalizeCloudEndpoint(raw) {
223
+ const cloud = raw.cloud;
224
+ if (!cloud?.endpoint) return;
225
+ const ep = String(cloud.endpoint);
226
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
227
+ cloud.endpoint = "https://cloud.askexe.com";
228
+ process.stderr.write(
229
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
230
+ );
231
+ }
232
+ }
222
233
  async function loadConfig() {
223
234
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
224
235
  await ensurePrivateDir(dir);
@@ -244,6 +255,7 @@ async function loadConfig() {
244
255
  normalizeSessionLifecycle(migratedCfg);
245
256
  normalizeAutoUpdate(migratedCfg);
246
257
  normalizeOrchestration(migratedCfg);
258
+ normalizeCloudEndpoint(migratedCfg);
247
259
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
248
260
  if (config.dbPath.startsWith("~")) {
249
261
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -272,6 +284,7 @@ function loadConfigSync() {
272
284
  normalizeSessionLifecycle(migratedCfg);
273
285
  normalizeAutoUpdate(migratedCfg);
274
286
  normalizeOrchestration(migratedCfg);
287
+ normalizeCloudEndpoint(migratedCfg);
275
288
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
276
289
  if (config.dbPath.startsWith("~")) {
277
290
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -432,6 +445,7 @@ __export(agent_config_exports, {
432
445
  clearAgentRuntime: () => clearAgentRuntime,
433
446
  getAgentRuntime: () => getAgentRuntime,
434
447
  loadAgentConfig: () => loadAgentConfig,
448
+ normalizeCcModelName: () => normalizeCcModelName,
435
449
  saveAgentConfig: () => saveAgentConfig,
436
450
  setAgentMcps: () => setAgentMcps,
437
451
  setAgentRuntime: () => setAgentRuntime
@@ -460,6 +474,13 @@ function getAgentRuntime(agentId) {
460
474
  if (orgDefault) return orgDefault;
461
475
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
462
476
  }
477
+ function normalizeCcModelName(model) {
478
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
479
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
480
+ ccModel += "[1m]";
481
+ }
482
+ return ccModel;
483
+ }
463
484
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
464
485
  const knownModels = KNOWN_RUNTIMES[runtime];
465
486
  if (!knownModels) {
@@ -3676,7 +3697,7 @@ var init_database = __esm({
3676
3697
  });
3677
3698
 
3678
3699
  // src/lib/keychain.ts
3679
- import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
3700
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2, rename, copyFile } from "fs/promises";
3680
3701
  import { existsSync as existsSync8, statSync as statSync3 } from "fs";
3681
3702
  import { execSync as execSync3 } from "child_process";
3682
3703
  import path7 from "path";
@@ -3711,12 +3732,14 @@ function linuxSecretAvailable() {
3711
3732
  function isRootOnlyTrustedServerKeyFile(keyPath) {
3712
3733
  if (process.platform !== "linux") return false;
3713
3734
  try {
3714
- const uid = typeof os5.userInfo().uid === "number" ? os5.userInfo().uid : -1;
3715
3735
  const st = statSync3(keyPath);
3716
3736
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
3737
+ const uid = typeof os5.userInfo().uid === "number" ? os5.userInfo().uid : -1;
3717
3738
  if (uid === 0) return true;
3718
3739
  const exeOsDir = process.env.EXE_OS_DIR;
3719
- return Boolean(exeOsDir && path7.resolve(keyPath).startsWith(path7.resolve(exeOsDir) + path7.sep));
3740
+ if (exeOsDir && path7.resolve(keyPath).startsWith(path7.resolve(exeOsDir) + path7.sep)) return true;
3741
+ if (!linuxSecretAvailable()) return true;
3742
+ return false;
3720
3743
  } catch {
3721
3744
  return false;
3722
3745
  }
@@ -3866,15 +3889,25 @@ async function writeMachineBoundFileFallback(b64) {
3866
3889
  await mkdir3(dir, { recursive: true });
3867
3890
  const keyPath = getKeyPath();
3868
3891
  const machineKey = deriveMachineKey();
3869
- if (machineKey) {
3870
- const encrypted = encryptWithMachineKey(b64, machineKey);
3871
- await writeFile3(keyPath, encrypted + "\n", "utf-8");
3872
- await chmod2(keyPath, 384);
3873
- return "encrypted";
3892
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
3893
+ const result = machineKey ? "encrypted" : "plaintext";
3894
+ const tmpPath = keyPath + ".tmp";
3895
+ try {
3896
+ if (existsSync8(keyPath)) {
3897
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
3898
+ });
3899
+ }
3900
+ await writeFile3(tmpPath, content, "utf-8");
3901
+ await chmod2(tmpPath, 384);
3902
+ await rename(tmpPath, keyPath);
3903
+ } catch (err) {
3904
+ try {
3905
+ await unlink(tmpPath);
3906
+ } catch {
3907
+ }
3908
+ throw err;
3874
3909
  }
3875
- await writeFile3(keyPath, b64 + "\n", "utf-8");
3876
- await chmod2(keyPath, 384);
3877
- return "plaintext";
3910
+ return result;
3878
3911
  }
3879
3912
  async function getMasterKey() {
3880
3913
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -3941,7 +3974,7 @@ async function getMasterKey() {
3941
3974
  b64Value = content;
3942
3975
  }
3943
3976
  const key = Buffer.from(b64Value, "base64");
3944
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
3977
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
3945
3978
  return key;
3946
3979
  }
3947
3980
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
@@ -6470,6 +6503,7 @@ var init_provider_table = __esm({
6470
6503
  // src/lib/intercom-queue.ts
6471
6504
  var intercom_queue_exports = {};
6472
6505
  __export(intercom_queue_exports, {
6506
+ _resetDrainGuard: () => _resetDrainGuard,
6473
6507
  clearQueueForAgent: () => clearQueueForAgent,
6474
6508
  drainForSession: () => drainForSession,
6475
6509
  drainQueue: () => drainQueue,
@@ -6515,38 +6549,47 @@ function queueIntercom(targetSession, reason) {
6515
6549
  writeQueue(queue);
6516
6550
  }
6517
6551
  function drainQueue(isSessionBusy2, sendKeys) {
6552
+ if (_draining) {
6553
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
6554
+ return { drained: 0, failed: 0 };
6555
+ }
6518
6556
  const queue = readQueue();
6519
6557
  if (queue.length === 0) return { drained: 0, failed: 0 };
6558
+ _draining = true;
6520
6559
  const remaining = [];
6521
6560
  let drained = 0;
6522
6561
  let failed = 0;
6523
- for (const item of queue) {
6524
- const age = Date.now() - new Date(item.queuedAt).getTime();
6525
- if (age > TTL_MS) {
6526
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6527
- failed++;
6528
- continue;
6529
- }
6530
- try {
6531
- if (!isSessionBusy2(item.targetSession)) {
6532
- const success = sendKeys(item.targetSession);
6533
- if (success) {
6534
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6535
- drained++;
6536
- continue;
6562
+ try {
6563
+ for (const item of queue) {
6564
+ const age = Date.now() - new Date(item.queuedAt).getTime();
6565
+ if (age > TTL_MS) {
6566
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6567
+ failed++;
6568
+ continue;
6569
+ }
6570
+ try {
6571
+ if (!isSessionBusy2(item.targetSession)) {
6572
+ const success = sendKeys(item.targetSession);
6573
+ if (success) {
6574
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6575
+ drained++;
6576
+ continue;
6577
+ }
6537
6578
  }
6579
+ } catch {
6538
6580
  }
6539
- } catch {
6540
- }
6541
- item.attempts++;
6542
- if (item.attempts >= MAX_RETRIES2) {
6543
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6544
- failed++;
6545
- continue;
6581
+ item.attempts++;
6582
+ if (item.attempts >= MAX_RETRIES2) {
6583
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6584
+ failed++;
6585
+ continue;
6586
+ }
6587
+ remaining.push(item);
6546
6588
  }
6547
- remaining.push(item);
6589
+ writeQueue(remaining);
6590
+ } finally {
6591
+ _draining = false;
6548
6592
  }
6549
- writeQueue(remaining);
6550
6593
  return { drained, failed };
6551
6594
  }
6552
6595
  function drainForSession(targetSession, sendKeys) {
@@ -6571,6 +6614,9 @@ function clearQueueForAgent(agentName2) {
6571
6614
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName2}`);
6572
6615
  }
6573
6616
  }
6617
+ function _resetDrainGuard() {
6618
+ _draining = false;
6619
+ }
6574
6620
  function logQueue(msg) {
6575
6621
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
6576
6622
  `;
@@ -6582,13 +6628,14 @@ function logQueue(msg) {
6582
6628
  } catch {
6583
6629
  }
6584
6630
  }
6585
- var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
6631
+ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, _draining, INTERCOM_LOG;
6586
6632
  var init_intercom_queue = __esm({
6587
6633
  "src/lib/intercom-queue.ts"() {
6588
6634
  "use strict";
6589
6635
  QUEUE_PATH = path10.join(os7.homedir(), ".exe-os", "intercom-queue.json");
6590
6636
  MAX_RETRIES2 = 5;
6591
6637
  TTL_MS = 60 * 60 * 1e3;
6638
+ _draining = false;
6592
6639
  INTERCOM_LOG = path10.join(os7.homedir(), ".exe-os", "intercom.log");
6593
6640
  }
6594
6641
  });
@@ -7203,6 +7250,17 @@ var init_agent_symlinks = __esm({
7203
7250
  });
7204
7251
 
7205
7252
  // src/lib/notifications.ts
7253
+ var notifications_exports = {};
7254
+ __export(notifications_exports, {
7255
+ cleanupOldNotifications: () => cleanupOldNotifications,
7256
+ formatNotifications: () => formatNotifications,
7257
+ markAsRead: () => markAsRead,
7258
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
7259
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
7260
+ migrateJsonNotifications: () => migrateJsonNotifications,
7261
+ readUnreadNotifications: () => readUnreadNotifications,
7262
+ writeNotification: () => writeNotification
7263
+ });
7206
7264
  import crypto2 from "crypto";
7207
7265
  import path14 from "path";
7208
7266
  import os10 from "os";
@@ -7239,6 +7297,52 @@ async function writeNotification(notification) {
7239
7297
  `);
7240
7298
  }
7241
7299
  }
7300
+ async function readUnreadNotifications(agentFilter, sessionScope) {
7301
+ try {
7302
+ const client = getClient();
7303
+ const conditions = ["read = 0"];
7304
+ const args = [];
7305
+ const scope = strictSessionScopeFilter(sessionScope);
7306
+ if (agentFilter) {
7307
+ conditions.push("agent_id = ?");
7308
+ args.push(agentFilter);
7309
+ }
7310
+ const result = await client.execute({
7311
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
7312
+ FROM notifications
7313
+ WHERE ${conditions.join(" AND ")}${scope.sql}
7314
+ ORDER BY created_at ASC`,
7315
+ args: [...args, ...scope.args]
7316
+ });
7317
+ return result.rows.map((r) => ({
7318
+ id: String(r.id),
7319
+ agentId: String(r.agent_id),
7320
+ agentRole: String(r.agent_role),
7321
+ event: String(r.event),
7322
+ project: String(r.project),
7323
+ summary: String(r.summary),
7324
+ taskFile: r.task_file ? String(r.task_file) : void 0,
7325
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
7326
+ timestamp: String(r.created_at),
7327
+ read: false
7328
+ }));
7329
+ } catch {
7330
+ return [];
7331
+ }
7332
+ }
7333
+ async function markAsRead(ids, sessionScope) {
7334
+ if (ids.length === 0) return;
7335
+ try {
7336
+ const client = getClient();
7337
+ const placeholders = ids.map(() => "?").join(", ");
7338
+ const scope = strictSessionScopeFilter(sessionScope);
7339
+ await client.execute({
7340
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
7341
+ args: [...ids, ...scope.args]
7342
+ });
7343
+ } catch {
7344
+ }
7345
+ }
7242
7346
  async function markAsReadByTaskFile(taskFile, sessionScope) {
7243
7347
  try {
7244
7348
  const client = getClient();
@@ -7251,11 +7355,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
7251
7355
  } catch {
7252
7356
  }
7253
7357
  }
7358
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
7359
+ try {
7360
+ const client = getClient();
7361
+ const cutoff = new Date(
7362
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
7363
+ ).toISOString();
7364
+ const scope = strictSessionScopeFilter(sessionScope);
7365
+ const result = await client.execute({
7366
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
7367
+ args: [cutoff, ...scope.args]
7368
+ });
7369
+ return result.rowsAffected;
7370
+ } catch {
7371
+ return 0;
7372
+ }
7373
+ }
7374
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
7375
+ try {
7376
+ const client = getClient();
7377
+ const scope = strictSessionScopeFilter(sessionScope);
7378
+ const result = await client.execute({
7379
+ sql: `UPDATE notifications SET read = 1
7380
+ WHERE read = 0
7381
+ AND task_file IS NOT NULL
7382
+ ${scope.sql}
7383
+ AND task_file IN (
7384
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
7385
+ )`,
7386
+ args: [...scope.args, ...scope.args]
7387
+ });
7388
+ return result.rowsAffected;
7389
+ } catch {
7390
+ return 0;
7391
+ }
7392
+ }
7393
+ function formatNotifications(notifications) {
7394
+ if (notifications.length === 0) return "";
7395
+ const grouped = /* @__PURE__ */ new Map();
7396
+ for (const n of notifications) {
7397
+ const key = `${n.agentId}|${n.agentRole}`;
7398
+ if (!grouped.has(key)) grouped.set(key, []);
7399
+ grouped.get(key).push(n);
7400
+ }
7401
+ const lines = [];
7402
+ lines.push(`## Notifications (${notifications.length} unread)
7403
+ `);
7404
+ for (const [key, items] of grouped) {
7405
+ const [agentId, agentRole] = key.split("|");
7406
+ lines.push(`**${agentId}** (${agentRole}):`);
7407
+ for (const item of items) {
7408
+ const ago = formatTimeAgo(item.timestamp);
7409
+ const icon = eventIcon(item.event);
7410
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
7411
+ }
7412
+ lines.push("");
7413
+ }
7414
+ return lines.join("\n");
7415
+ }
7416
+ async function migrateJsonNotifications() {
7417
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path14.join(os10.homedir(), ".exe-os");
7418
+ const notifDir = path14.join(base, "notifications");
7419
+ if (!existsSync15(notifDir)) return 0;
7420
+ let migrated = 0;
7421
+ try {
7422
+ const files = readdirSync2(notifDir).filter((f) => f.endsWith(".json"));
7423
+ if (files.length === 0) return 0;
7424
+ const client = getClient();
7425
+ for (const file of files) {
7426
+ try {
7427
+ const filePath = path14.join(notifDir, file);
7428
+ const data = JSON.parse(readFileSync10(filePath, "utf8"));
7429
+ await client.execute({
7430
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
7431
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
7432
+ args: [
7433
+ crypto2.randomUUID(),
7434
+ data.agentId ?? "unknown",
7435
+ data.agentRole ?? "unknown",
7436
+ data.event ?? "session_summary",
7437
+ data.project ?? "unknown",
7438
+ data.summary ?? "",
7439
+ data.taskFile ?? null,
7440
+ null,
7441
+ data.read ? 1 : 0,
7442
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
7443
+ ]
7444
+ });
7445
+ unlinkSync4(filePath);
7446
+ migrated++;
7447
+ } catch {
7448
+ }
7449
+ }
7450
+ try {
7451
+ const remaining = readdirSync2(notifDir);
7452
+ if (remaining.length === 0) {
7453
+ rmdirSync(notifDir);
7454
+ }
7455
+ } catch {
7456
+ }
7457
+ } catch {
7458
+ }
7459
+ return migrated;
7460
+ }
7461
+ function eventIcon(event) {
7462
+ switch (event) {
7463
+ case "task_complete":
7464
+ return "Completed:";
7465
+ case "task_needs_fix":
7466
+ return "Needs fix:";
7467
+ case "session_summary":
7468
+ return "Session:";
7469
+ case "error_spike":
7470
+ return "Errors:";
7471
+ case "orphan_task":
7472
+ return "Orphan:";
7473
+ case "subtasks_complete":
7474
+ return "Subtasks done:";
7475
+ case "capacity_relaunch":
7476
+ return "Relaunched:";
7477
+ }
7478
+ }
7479
+ function formatTimeAgo(timestamp) {
7480
+ const diffMs = Date.now() - new Date(timestamp).getTime();
7481
+ const mins = Math.floor(diffMs / 6e4);
7482
+ if (mins < 1) return "just now";
7483
+ if (mins < 60) return `${mins}m ago`;
7484
+ const hours = Math.floor(mins / 60);
7485
+ if (hours < 24) return `${hours}h ago`;
7486
+ const days = Math.floor(hours / 24);
7487
+ return `${days}d ago`;
7488
+ }
7489
+ var CLEANUP_DAYS;
7254
7490
  var init_notifications = __esm({
7255
7491
  "src/lib/notifications.ts"() {
7256
7492
  "use strict";
7257
7493
  init_database();
7258
7494
  init_task_scope();
7495
+ CLEANUP_DAYS = 7;
7259
7496
  }
7260
7497
  });
7261
7498
 
@@ -8262,7 +8499,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
8262
8499
  taskFile
8263
8500
  });
8264
8501
  const originalPriority = String(row.priority).toLowerCase();
8265
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
8502
+ const resultLower = result?.toLowerCase() ?? "";
8503
+ const hasTestEvidence = (
8504
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
8505
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
8506
+ );
8507
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
8508
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
8266
8509
  if (!autoApprove) {
8267
8510
  try {
8268
8511
  const key = getSessionKey();
@@ -8270,6 +8513,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
8270
8513
  if (exeSession2) {
8271
8514
  sendIntercom(exeSession2);
8272
8515
  }
8516
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession2) {
8517
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
8518
+ if (exeSession2) {
8519
+ const reviewerSession = employeeSessionName2(reviewer, exeSession2);
8520
+ sendIntercom(reviewerSession);
8521
+ }
8522
+ }
8273
8523
  } catch {
8274
8524
  }
8275
8525
  }
@@ -9045,6 +9295,20 @@ async function updateTask(input) {
9045
9295
  notifyTaskDone();
9046
9296
  }
9047
9297
  await markTaskNotificationsRead(taskFile);
9298
+ if (input.status === "needs_review" && !isCoordinator) {
9299
+ try {
9300
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
9301
+ await writeNotification2({
9302
+ agentId: String(row.assigned_to),
9303
+ agentRole: String(row.assigned_to),
9304
+ event: "task_complete",
9305
+ project: String(row.project_name),
9306
+ summary: `"${String(row.title)}" is ready for review`,
9307
+ taskFile
9308
+ });
9309
+ } catch {
9310
+ }
9311
+ }
9048
9312
  if (input.status === "done" || input.status === "closed") {
9049
9313
  try {
9050
9314
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -9477,18 +9741,31 @@ function acquireSpawnLock2(sessionName) {
9477
9741
  mkdirSync9(SPAWN_LOCK_DIR, { recursive: true });
9478
9742
  }
9479
9743
  const lockFile = spawnLockPath(sessionName);
9480
- if (existsSync18(lockFile)) {
9481
- try {
9482
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
9483
- const age = Date.now() - lock.timestamp;
9484
- if (isProcessAlive(lock.pid) && age < 6e4) {
9485
- return false;
9486
- }
9487
- } catch {
9744
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
9745
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
9746
+ const { constants } = __require("fs");
9747
+ try {
9748
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
9749
+ writeSync(fd, lockData);
9750
+ closeSync3(fd);
9751
+ return true;
9752
+ } catch (err) {
9753
+ if (err?.code !== "EEXIST") {
9754
+ return true;
9488
9755
  }
9489
9756
  }
9490
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
9491
- return true;
9757
+ try {
9758
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
9759
+ const age = Date.now() - lock.timestamp;
9760
+ if (isProcessAlive(lock.pid) && age < 6e4) {
9761
+ return false;
9762
+ }
9763
+ writeFileSync8(lockFile, lockData);
9764
+ return true;
9765
+ } catch {
9766
+ writeFileSync8(lockFile, lockData);
9767
+ return true;
9768
+ }
9492
9769
  }
9493
9770
  function releaseSpawnLock2(sessionName) {
9494
9771
  try {
@@ -9567,6 +9844,21 @@ function parseParentExe(sessionName, agentId) {
9567
9844
  function extractRootExe(name) {
9568
9845
  if (!name) return null;
9569
9846
  if (!name.includes("-")) return name;
9847
+ try {
9848
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
9849
+ if (roster.length > 0) {
9850
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
9851
+ for (const agentName2 of sortedNames) {
9852
+ const escaped = agentName2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9853
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
9854
+ const match = name.match(regex);
9855
+ if (match) {
9856
+ return extractRootExe(match[1]);
9857
+ }
9858
+ }
9859
+ }
9860
+ } catch {
9861
+ }
9570
9862
  const parts = name.split("-").filter(Boolean);
9571
9863
  return parts.length > 0 ? parts[parts.length - 1] : null;
9572
9864
  }
@@ -9585,6 +9877,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
9585
9877
  function getParentExe(sessionKey) {
9586
9878
  try {
9587
9879
  const data = JSON.parse(readFileSync12(path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
9880
+ if (data.registeredAt) {
9881
+ const age = Date.now() - new Date(data.registeredAt).getTime();
9882
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
9883
+ }
9588
9884
  return data.parentExe || null;
9589
9885
  } catch {
9590
9886
  return null;
@@ -10133,7 +10429,7 @@ function spawnEmployee(employeeName, exeSession2, projectDir, opts) {
10133
10429
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
10134
10430
  } catch {
10135
10431
  }
10136
- let envPrefix = `EXE_SESSION=${exeSession2} EXE_SESSION_NAME=${sessionName}`;
10432
+ let envPrefix = `EXE_SESSION=${exeSession2} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
10137
10433
  if (ccProvider !== DEFAULT_PROVIDER) {
10138
10434
  const cfg = PROVIDER_TABLE[ccProvider];
10139
10435
  if (cfg?.apiKeyEnv) {
@@ -10168,10 +10464,8 @@ function spawnEmployee(employeeName, exeSession2, projectDir, opts) {
10168
10464
  }
10169
10465
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
10170
10466
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
10171
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
10172
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
10173
- ccModel += "[1m]";
10174
- }
10467
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
10468
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
10175
10469
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
10176
10470
  }
10177
10471
  }
@@ -10272,7 +10566,7 @@ function spawnEmployee(employeeName, exeSession2, projectDir, opts) {
10272
10566
  releaseSpawnLock2(sessionName);
10273
10567
  return { sessionName };
10274
10568
  }
10275
- 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;
10569
+ 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;
10276
10570
  var init_tmux_routing = __esm({
10277
10571
  "src/lib/tmux-routing.ts"() {
10278
10572
  "use strict";
@@ -10292,6 +10586,7 @@ var init_tmux_routing = __esm({
10292
10586
  SESSION_CACHE = path20.join(os12.homedir(), ".exe-os", "session-cache");
10293
10587
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
10294
10588
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
10589
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
10295
10590
  VERIFY_PANE_LINES = 200;
10296
10591
  INTERCOM_DEBOUNCE_MS = 3e4;
10297
10592
  CODEX_DEBOUNCE_MS = 12e4;
@@ -117,6 +117,17 @@ function normalizeOrchestration(raw) {
117
117
  const userOrg = raw.orchestration ?? {};
118
118
  raw.orchestration = { ...defaultOrg, ...userOrg };
119
119
  }
120
+ function normalizeCloudEndpoint(raw) {
121
+ const cloud = raw.cloud;
122
+ if (!cloud?.endpoint) return;
123
+ const ep = String(cloud.endpoint);
124
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
125
+ cloud.endpoint = "https://cloud.askexe.com";
126
+ process.stderr.write(
127
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
128
+ );
129
+ }
130
+ }
120
131
  async function loadConfig() {
121
132
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
122
133
  await ensurePrivateDir(dir);
@@ -142,6 +153,7 @@ async function loadConfig() {
142
153
  normalizeSessionLifecycle(migratedCfg);
143
154
  normalizeAutoUpdate(migratedCfg);
144
155
  normalizeOrchestration(migratedCfg);
156
+ normalizeCloudEndpoint(migratedCfg);
145
157
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
146
158
  if (config.dbPath.startsWith("~")) {
147
159
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());