@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
@@ -536,6 +536,17 @@ function normalizeOrchestration(raw) {
536
536
  const userOrg = raw.orchestration ?? {};
537
537
  raw.orchestration = { ...defaultOrg, ...userOrg };
538
538
  }
539
+ function normalizeCloudEndpoint(raw) {
540
+ const cloud = raw.cloud;
541
+ if (!cloud?.endpoint) return;
542
+ const ep = String(cloud.endpoint);
543
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
544
+ cloud.endpoint = "https://cloud.askexe.com";
545
+ process.stderr.write(
546
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
547
+ );
548
+ }
549
+ }
539
550
  async function loadConfig() {
540
551
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
541
552
  await ensurePrivateDir(dir);
@@ -561,6 +572,7 @@ async function loadConfig() {
561
572
  normalizeSessionLifecycle(migratedCfg);
562
573
  normalizeAutoUpdate(migratedCfg);
563
574
  normalizeOrchestration(migratedCfg);
575
+ normalizeCloudEndpoint(migratedCfg);
564
576
  const config2 = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
565
577
  if (config2.dbPath.startsWith("~")) {
566
578
  config2.dbPath = config2.dbPath.replace(/^~/, os2.homedir());
@@ -589,6 +601,7 @@ function loadConfigSync() {
589
601
  normalizeSessionLifecycle(migratedCfg);
590
602
  normalizeAutoUpdate(migratedCfg);
591
603
  normalizeOrchestration(migratedCfg);
604
+ normalizeCloudEndpoint(migratedCfg);
592
605
  const config2 = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
593
606
  if (config2.dbPath.startsWith("~")) {
594
607
  config2.dbPath = config2.dbPath.replace(/^~/, os2.homedir());
@@ -749,6 +762,7 @@ __export(agent_config_exports, {
749
762
  clearAgentRuntime: () => clearAgentRuntime,
750
763
  getAgentRuntime: () => getAgentRuntime,
751
764
  loadAgentConfig: () => loadAgentConfig,
765
+ normalizeCcModelName: () => normalizeCcModelName,
752
766
  saveAgentConfig: () => saveAgentConfig,
753
767
  setAgentMcps: () => setAgentMcps,
754
768
  setAgentRuntime: () => setAgentRuntime
@@ -777,6 +791,13 @@ function getAgentRuntime(agentId) {
777
791
  if (orgDefault) return orgDefault;
778
792
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
779
793
  }
794
+ function normalizeCcModelName(model) {
795
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
796
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
797
+ ccModel += "[1m]";
798
+ }
799
+ return ccModel;
800
+ }
780
801
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
781
802
  const knownModels = KNOWN_RUNTIMES[runtime];
782
803
  if (!knownModels) {
@@ -4070,7 +4091,7 @@ var init_embedder = __esm({
4070
4091
  });
4071
4092
 
4072
4093
  // src/lib/keychain.ts
4073
- import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
4094
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2, rename, copyFile } from "fs/promises";
4074
4095
  import { existsSync as existsSync8, statSync as statSync3 } from "fs";
4075
4096
  import { execSync as execSync3 } from "child_process";
4076
4097
  import path8 from "path";
@@ -4105,12 +4126,14 @@ function linuxSecretAvailable() {
4105
4126
  function isRootOnlyTrustedServerKeyFile(keyPath) {
4106
4127
  if (process.platform !== "linux") return false;
4107
4128
  try {
4108
- const uid = typeof os6.userInfo().uid === "number" ? os6.userInfo().uid : -1;
4109
4129
  const st = statSync3(keyPath);
4110
4130
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
4131
+ const uid = typeof os6.userInfo().uid === "number" ? os6.userInfo().uid : -1;
4111
4132
  if (uid === 0) return true;
4112
4133
  const exeOsDir = process.env.EXE_OS_DIR;
4113
- return Boolean(exeOsDir && path8.resolve(keyPath).startsWith(path8.resolve(exeOsDir) + path8.sep));
4134
+ if (exeOsDir && path8.resolve(keyPath).startsWith(path8.resolve(exeOsDir) + path8.sep)) return true;
4135
+ if (!linuxSecretAvailable()) return true;
4136
+ return false;
4114
4137
  } catch {
4115
4138
  return false;
4116
4139
  }
@@ -4260,15 +4283,25 @@ async function writeMachineBoundFileFallback(b64) {
4260
4283
  await mkdir3(dir, { recursive: true });
4261
4284
  const keyPath = getKeyPath();
4262
4285
  const machineKey = deriveMachineKey();
4263
- if (machineKey) {
4264
- const encrypted = encryptWithMachineKey(b64, machineKey);
4265
- await writeFile3(keyPath, encrypted + "\n", "utf-8");
4266
- await chmod2(keyPath, 384);
4267
- return "encrypted";
4286
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
4287
+ const result = machineKey ? "encrypted" : "plaintext";
4288
+ const tmpPath = keyPath + ".tmp";
4289
+ try {
4290
+ if (existsSync8(keyPath)) {
4291
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
4292
+ });
4293
+ }
4294
+ await writeFile3(tmpPath, content, "utf-8");
4295
+ await chmod2(tmpPath, 384);
4296
+ await rename(tmpPath, keyPath);
4297
+ } catch (err) {
4298
+ try {
4299
+ await unlink(tmpPath);
4300
+ } catch {
4301
+ }
4302
+ throw err;
4268
4303
  }
4269
- await writeFile3(keyPath, b64 + "\n", "utf-8");
4270
- await chmod2(keyPath, 384);
4271
- return "plaintext";
4304
+ return result;
4272
4305
  }
4273
4306
  async function getMasterKey() {
4274
4307
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -4335,7 +4368,7 @@ async function getMasterKey() {
4335
4368
  b64Value = content;
4336
4369
  }
4337
4370
  const key = Buffer.from(b64Value, "base64");
4338
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
4371
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
4339
4372
  return key;
4340
4373
  }
4341
4374
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
@@ -10136,6 +10169,7 @@ var init_provider_table = __esm({
10136
10169
  // src/lib/intercom-queue.ts
10137
10170
  var intercom_queue_exports = {};
10138
10171
  __export(intercom_queue_exports, {
10172
+ _resetDrainGuard: () => _resetDrainGuard,
10139
10173
  clearQueueForAgent: () => clearQueueForAgent,
10140
10174
  drainForSession: () => drainForSession,
10141
10175
  drainQueue: () => drainQueue,
@@ -10181,38 +10215,47 @@ function queueIntercom(targetSession, reason) {
10181
10215
  writeQueue(queue);
10182
10216
  }
10183
10217
  function drainQueue(isSessionBusy2, sendKeys) {
10218
+ if (_draining) {
10219
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
10220
+ return { drained: 0, failed: 0 };
10221
+ }
10184
10222
  const queue = readQueue();
10185
10223
  if (queue.length === 0) return { drained: 0, failed: 0 };
10224
+ _draining = true;
10186
10225
  const remaining = [];
10187
10226
  let drained = 0;
10188
10227
  let failed = 0;
10189
- for (const item of queue) {
10190
- const age = Date.now() - new Date(item.queuedAt).getTime();
10191
- if (age > TTL_MS) {
10192
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
10193
- failed++;
10194
- continue;
10195
- }
10196
- try {
10197
- if (!isSessionBusy2(item.targetSession)) {
10198
- const success = sendKeys(item.targetSession);
10199
- if (success) {
10200
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
10201
- drained++;
10202
- continue;
10228
+ try {
10229
+ for (const item of queue) {
10230
+ const age = Date.now() - new Date(item.queuedAt).getTime();
10231
+ if (age > TTL_MS) {
10232
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
10233
+ failed++;
10234
+ continue;
10235
+ }
10236
+ try {
10237
+ if (!isSessionBusy2(item.targetSession)) {
10238
+ const success = sendKeys(item.targetSession);
10239
+ if (success) {
10240
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
10241
+ drained++;
10242
+ continue;
10243
+ }
10203
10244
  }
10245
+ } catch {
10204
10246
  }
10205
- } catch {
10206
- }
10207
- item.attempts++;
10208
- if (item.attempts >= MAX_RETRIES2) {
10209
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
10210
- failed++;
10211
- continue;
10247
+ item.attempts++;
10248
+ if (item.attempts >= MAX_RETRIES2) {
10249
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
10250
+ failed++;
10251
+ continue;
10252
+ }
10253
+ remaining.push(item);
10212
10254
  }
10213
- remaining.push(item);
10255
+ writeQueue(remaining);
10256
+ } finally {
10257
+ _draining = false;
10214
10258
  }
10215
- writeQueue(remaining);
10216
10259
  return { drained, failed };
10217
10260
  }
10218
10261
  function drainForSession(targetSession, sendKeys) {
@@ -10237,6 +10280,9 @@ function clearQueueForAgent(agentName) {
10237
10280
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
10238
10281
  }
10239
10282
  }
10283
+ function _resetDrainGuard() {
10284
+ _draining = false;
10285
+ }
10240
10286
  function logQueue(msg) {
10241
10287
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
10242
10288
  `;
@@ -10248,13 +10294,14 @@ function logQueue(msg) {
10248
10294
  } catch {
10249
10295
  }
10250
10296
  }
10251
- var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
10297
+ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, _draining, INTERCOM_LOG;
10252
10298
  var init_intercom_queue = __esm({
10253
10299
  "src/lib/intercom-queue.ts"() {
10254
10300
  "use strict";
10255
10301
  QUEUE_PATH = path13.join(os10.homedir(), ".exe-os", "intercom-queue.json");
10256
10302
  MAX_RETRIES2 = 5;
10257
10303
  TTL_MS = 60 * 60 * 1e3;
10304
+ _draining = false;
10258
10305
  INTERCOM_LOG = path13.join(os10.homedir(), ".exe-os", "intercom.log");
10259
10306
  }
10260
10307
  });
@@ -10435,6 +10482,17 @@ var init_task_scope = __esm({
10435
10482
  });
10436
10483
 
10437
10484
  // src/lib/notifications.ts
10485
+ var notifications_exports = {};
10486
+ __export(notifications_exports, {
10487
+ cleanupOldNotifications: () => cleanupOldNotifications,
10488
+ formatNotifications: () => formatNotifications,
10489
+ markAsRead: () => markAsRead,
10490
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
10491
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
10492
+ migrateJsonNotifications: () => migrateJsonNotifications,
10493
+ readUnreadNotifications: () => readUnreadNotifications,
10494
+ writeNotification: () => writeNotification
10495
+ });
10438
10496
  import crypto5 from "crypto";
10439
10497
  import path16 from "path";
10440
10498
  import os12 from "os";
@@ -10471,6 +10529,52 @@ async function writeNotification(notification) {
10471
10529
  `);
10472
10530
  }
10473
10531
  }
10532
+ async function readUnreadNotifications(agentFilter, sessionScope) {
10533
+ try {
10534
+ const client = getClient();
10535
+ const conditions = ["read = 0"];
10536
+ const args = [];
10537
+ const scope = strictSessionScopeFilter(sessionScope);
10538
+ if (agentFilter) {
10539
+ conditions.push("agent_id = ?");
10540
+ args.push(agentFilter);
10541
+ }
10542
+ const result = await client.execute({
10543
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
10544
+ FROM notifications
10545
+ WHERE ${conditions.join(" AND ")}${scope.sql}
10546
+ ORDER BY created_at ASC`,
10547
+ args: [...args, ...scope.args]
10548
+ });
10549
+ return result.rows.map((r) => ({
10550
+ id: String(r.id),
10551
+ agentId: String(r.agent_id),
10552
+ agentRole: String(r.agent_role),
10553
+ event: String(r.event),
10554
+ project: String(r.project),
10555
+ summary: String(r.summary),
10556
+ taskFile: r.task_file ? String(r.task_file) : void 0,
10557
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
10558
+ timestamp: String(r.created_at),
10559
+ read: false
10560
+ }));
10561
+ } catch {
10562
+ return [];
10563
+ }
10564
+ }
10565
+ async function markAsRead(ids, sessionScope) {
10566
+ if (ids.length === 0) return;
10567
+ try {
10568
+ const client = getClient();
10569
+ const placeholders = ids.map(() => "?").join(", ");
10570
+ const scope = strictSessionScopeFilter(sessionScope);
10571
+ await client.execute({
10572
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
10573
+ args: [...ids, ...scope.args]
10574
+ });
10575
+ } catch {
10576
+ }
10577
+ }
10474
10578
  async function markAsReadByTaskFile(taskFile, sessionScope) {
10475
10579
  try {
10476
10580
  const client = getClient();
@@ -10483,11 +10587,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
10483
10587
  } catch {
10484
10588
  }
10485
10589
  }
10590
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
10591
+ try {
10592
+ const client = getClient();
10593
+ const cutoff = new Date(
10594
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
10595
+ ).toISOString();
10596
+ const scope = strictSessionScopeFilter(sessionScope);
10597
+ const result = await client.execute({
10598
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
10599
+ args: [cutoff, ...scope.args]
10600
+ });
10601
+ return result.rowsAffected;
10602
+ } catch {
10603
+ return 0;
10604
+ }
10605
+ }
10606
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
10607
+ try {
10608
+ const client = getClient();
10609
+ const scope = strictSessionScopeFilter(sessionScope);
10610
+ const result = await client.execute({
10611
+ sql: `UPDATE notifications SET read = 1
10612
+ WHERE read = 0
10613
+ AND task_file IS NOT NULL
10614
+ ${scope.sql}
10615
+ AND task_file IN (
10616
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
10617
+ )`,
10618
+ args: [...scope.args, ...scope.args]
10619
+ });
10620
+ return result.rowsAffected;
10621
+ } catch {
10622
+ return 0;
10623
+ }
10624
+ }
10625
+ function formatNotifications(notifications) {
10626
+ if (notifications.length === 0) return "";
10627
+ const grouped = /* @__PURE__ */ new Map();
10628
+ for (const n of notifications) {
10629
+ const key = `${n.agentId}|${n.agentRole}`;
10630
+ if (!grouped.has(key)) grouped.set(key, []);
10631
+ grouped.get(key).push(n);
10632
+ }
10633
+ const lines = [];
10634
+ lines.push(`## Notifications (${notifications.length} unread)
10635
+ `);
10636
+ for (const [key, items] of grouped) {
10637
+ const [agentId, agentRole] = key.split("|");
10638
+ lines.push(`**${agentId}** (${agentRole}):`);
10639
+ for (const item of items) {
10640
+ const ago = formatTimeAgo(item.timestamp);
10641
+ const icon = eventIcon(item.event);
10642
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
10643
+ }
10644
+ lines.push("");
10645
+ }
10646
+ return lines.join("\n");
10647
+ }
10648
+ async function migrateJsonNotifications() {
10649
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path16.join(os12.homedir(), ".exe-os");
10650
+ const notifDir = path16.join(base, "notifications");
10651
+ if (!existsSync15(notifDir)) return 0;
10652
+ let migrated = 0;
10653
+ try {
10654
+ const files = readdirSync2(notifDir).filter((f) => f.endsWith(".json"));
10655
+ if (files.length === 0) return 0;
10656
+ const client = getClient();
10657
+ for (const file of files) {
10658
+ try {
10659
+ const filePath = path16.join(notifDir, file);
10660
+ const data = JSON.parse(readFileSync11(filePath, "utf8"));
10661
+ await client.execute({
10662
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
10663
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
10664
+ args: [
10665
+ crypto5.randomUUID(),
10666
+ data.agentId ?? "unknown",
10667
+ data.agentRole ?? "unknown",
10668
+ data.event ?? "session_summary",
10669
+ data.project ?? "unknown",
10670
+ data.summary ?? "",
10671
+ data.taskFile ?? null,
10672
+ null,
10673
+ data.read ? 1 : 0,
10674
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
10675
+ ]
10676
+ });
10677
+ unlinkSync4(filePath);
10678
+ migrated++;
10679
+ } catch {
10680
+ }
10681
+ }
10682
+ try {
10683
+ const remaining = readdirSync2(notifDir);
10684
+ if (remaining.length === 0) {
10685
+ rmdirSync(notifDir);
10686
+ }
10687
+ } catch {
10688
+ }
10689
+ } catch {
10690
+ }
10691
+ return migrated;
10692
+ }
10693
+ function eventIcon(event) {
10694
+ switch (event) {
10695
+ case "task_complete":
10696
+ return "Completed:";
10697
+ case "task_needs_fix":
10698
+ return "Needs fix:";
10699
+ case "session_summary":
10700
+ return "Session:";
10701
+ case "error_spike":
10702
+ return "Errors:";
10703
+ case "orphan_task":
10704
+ return "Orphan:";
10705
+ case "subtasks_complete":
10706
+ return "Subtasks done:";
10707
+ case "capacity_relaunch":
10708
+ return "Relaunched:";
10709
+ }
10710
+ }
10711
+ function formatTimeAgo(timestamp) {
10712
+ const diffMs = Date.now() - new Date(timestamp).getTime();
10713
+ const mins = Math.floor(diffMs / 6e4);
10714
+ if (mins < 1) return "just now";
10715
+ if (mins < 60) return `${mins}m ago`;
10716
+ const hours = Math.floor(mins / 60);
10717
+ if (hours < 24) return `${hours}h ago`;
10718
+ const days = Math.floor(hours / 24);
10719
+ return `${days}d ago`;
10720
+ }
10721
+ var CLEANUP_DAYS;
10486
10722
  var init_notifications = __esm({
10487
10723
  "src/lib/notifications.ts"() {
10488
10724
  "use strict";
10489
10725
  init_database();
10490
10726
  init_task_scope();
10727
+ CLEANUP_DAYS = 7;
10491
10728
  }
10492
10729
  });
10493
10730
 
@@ -11478,7 +11715,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
11478
11715
  taskFile
11479
11716
  });
11480
11717
  const originalPriority = String(row.priority).toLowerCase();
11481
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
11718
+ const resultLower = result?.toLowerCase() ?? "";
11719
+ const hasTestEvidence = (
11720
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
11721
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
11722
+ );
11723
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
11724
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
11482
11725
  if (!autoApprove) {
11483
11726
  try {
11484
11727
  const key = getSessionKey();
@@ -11486,6 +11729,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
11486
11729
  if (exeSession) {
11487
11730
  sendIntercom(exeSession);
11488
11731
  }
11732
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
11733
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
11734
+ if (exeSession) {
11735
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
11736
+ sendIntercom(reviewerSession);
11737
+ }
11738
+ }
11489
11739
  } catch {
11490
11740
  }
11491
11741
  }
@@ -12193,6 +12443,20 @@ async function updateTask(input) {
12193
12443
  notifyTaskDone();
12194
12444
  }
12195
12445
  await markTaskNotificationsRead(taskFile);
12446
+ if (input.status === "needs_review" && !isCoordinator) {
12447
+ try {
12448
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
12449
+ await writeNotification2({
12450
+ agentId: String(row.assigned_to),
12451
+ agentRole: String(row.assigned_to),
12452
+ event: "task_complete",
12453
+ project: String(row.project_name),
12454
+ summary: `"${String(row.title)}" is ready for review`,
12455
+ taskFile
12456
+ });
12457
+ } catch {
12458
+ }
12459
+ }
12196
12460
  if (input.status === "done" || input.status === "closed") {
12197
12461
  try {
12198
12462
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -12625,18 +12889,31 @@ function acquireSpawnLock2(sessionName) {
12625
12889
  mkdirSync10(SPAWN_LOCK_DIR, { recursive: true });
12626
12890
  }
12627
12891
  const lockFile = spawnLockPath(sessionName);
12628
- if (existsSync18(lockFile)) {
12629
- try {
12630
- const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
12631
- const age = Date.now() - lock.timestamp;
12632
- if (isProcessAlive(lock.pid) && age < 6e4) {
12633
- return false;
12634
- }
12635
- } catch {
12892
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
12893
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
12894
+ const { constants } = __require("fs");
12895
+ try {
12896
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
12897
+ writeSync(fd, lockData);
12898
+ closeSync3(fd);
12899
+ return true;
12900
+ } catch (err) {
12901
+ if (err?.code !== "EEXIST") {
12902
+ return true;
12636
12903
  }
12637
12904
  }
12638
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
12639
- return true;
12905
+ try {
12906
+ const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
12907
+ const age = Date.now() - lock.timestamp;
12908
+ if (isProcessAlive(lock.pid) && age < 6e4) {
12909
+ return false;
12910
+ }
12911
+ writeFileSync8(lockFile, lockData);
12912
+ return true;
12913
+ } catch {
12914
+ writeFileSync8(lockFile, lockData);
12915
+ return true;
12916
+ }
12640
12917
  }
12641
12918
  function releaseSpawnLock2(sessionName) {
12642
12919
  try {
@@ -12715,6 +12992,21 @@ function parseParentExe(sessionName, agentId) {
12715
12992
  function extractRootExe(name) {
12716
12993
  if (!name) return null;
12717
12994
  if (!name.includes("-")) return name;
12995
+ try {
12996
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
12997
+ if (roster.length > 0) {
12998
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
12999
+ for (const agentName of sortedNames) {
13000
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13001
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
13002
+ const match = name.match(regex);
13003
+ if (match) {
13004
+ return extractRootExe(match[1]);
13005
+ }
13006
+ }
13007
+ }
13008
+ } catch {
13009
+ }
12718
13010
  const parts = name.split("-").filter(Boolean);
12719
13011
  return parts.length > 0 ? parts[parts.length - 1] : null;
12720
13012
  }
@@ -12733,6 +13025,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
12733
13025
  function getParentExe(sessionKey) {
12734
13026
  try {
12735
13027
  const data = JSON.parse(readFileSync13(path22.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
13028
+ if (data.registeredAt) {
13029
+ const age = Date.now() - new Date(data.registeredAt).getTime();
13030
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
13031
+ }
12736
13032
  return data.parentExe || null;
12737
13033
  } catch {
12738
13034
  return null;
@@ -13281,7 +13577,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
13281
13577
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
13282
13578
  } catch {
13283
13579
  }
13284
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
13580
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
13285
13581
  if (ccProvider !== DEFAULT_PROVIDER) {
13286
13582
  const cfg = PROVIDER_TABLE[ccProvider];
13287
13583
  if (cfg?.apiKeyEnv) {
@@ -13316,10 +13612,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
13316
13612
  }
13317
13613
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
13318
13614
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
13319
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
13320
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
13321
- ccModel += "[1m]";
13322
- }
13615
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
13616
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
13323
13617
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
13324
13618
  }
13325
13619
  }
@@ -13420,7 +13714,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
13420
13714
  releaseSpawnLock2(sessionName);
13421
13715
  return { sessionName };
13422
13716
  }
13423
- 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;
13717
+ 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;
13424
13718
  var init_tmux_routing = __esm({
13425
13719
  "src/lib/tmux-routing.ts"() {
13426
13720
  "use strict";
@@ -13440,6 +13734,7 @@ var init_tmux_routing = __esm({
13440
13734
  SESSION_CACHE = path22.join(os14.homedir(), ".exe-os", "session-cache");
13441
13735
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
13442
13736
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
13737
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
13443
13738
  VERIFY_PANE_LINES = 200;
13444
13739
  INTERCOM_DEBOUNCE_MS = 3e4;
13445
13740
  CODEX_DEBOUNCE_MS = 12e4;