@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
@@ -520,6 +520,17 @@ function normalizeOrchestration(raw) {
520
520
  const userOrg = raw.orchestration ?? {};
521
521
  raw.orchestration = { ...defaultOrg, ...userOrg };
522
522
  }
523
+ function normalizeCloudEndpoint(raw) {
524
+ const cloud = raw.cloud;
525
+ if (!cloud?.endpoint) return;
526
+ const ep = String(cloud.endpoint);
527
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
528
+ cloud.endpoint = "https://cloud.askexe.com";
529
+ process.stderr.write(
530
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
531
+ );
532
+ }
533
+ }
523
534
  async function loadConfig() {
524
535
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
525
536
  await ensurePrivateDir(dir);
@@ -545,6 +556,7 @@ async function loadConfig() {
545
556
  normalizeSessionLifecycle(migratedCfg);
546
557
  normalizeAutoUpdate(migratedCfg);
547
558
  normalizeOrchestration(migratedCfg);
559
+ normalizeCloudEndpoint(migratedCfg);
548
560
  const config2 = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
549
561
  if (config2.dbPath.startsWith("~")) {
550
562
  config2.dbPath = config2.dbPath.replace(/^~/, os.homedir());
@@ -573,6 +585,7 @@ function loadConfigSync() {
573
585
  normalizeSessionLifecycle(migratedCfg);
574
586
  normalizeAutoUpdate(migratedCfg);
575
587
  normalizeOrchestration(migratedCfg);
588
+ normalizeCloudEndpoint(migratedCfg);
576
589
  const config2 = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
577
590
  if (config2.dbPath.startsWith("~")) {
578
591
  config2.dbPath = config2.dbPath.replace(/^~/, os.homedir());
@@ -733,6 +746,7 @@ __export(agent_config_exports, {
733
746
  clearAgentRuntime: () => clearAgentRuntime,
734
747
  getAgentRuntime: () => getAgentRuntime,
735
748
  loadAgentConfig: () => loadAgentConfig,
749
+ normalizeCcModelName: () => normalizeCcModelName,
736
750
  saveAgentConfig: () => saveAgentConfig,
737
751
  setAgentMcps: () => setAgentMcps,
738
752
  setAgentRuntime: () => setAgentRuntime
@@ -761,6 +775,13 @@ function getAgentRuntime(agentId) {
761
775
  if (orgDefault) return orgDefault;
762
776
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
763
777
  }
778
+ function normalizeCcModelName(model) {
779
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
780
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
781
+ ccModel += "[1m]";
782
+ }
783
+ return ccModel;
784
+ }
764
785
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
765
786
  const knownModels = KNOWN_RUNTIMES[runtime];
766
787
  if (!knownModels) {
@@ -4054,7 +4075,7 @@ var init_embedder = __esm({
4054
4075
  });
4055
4076
 
4056
4077
  // src/lib/keychain.ts
4057
- import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
4078
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2, rename, copyFile } from "fs/promises";
4058
4079
  import { existsSync as existsSync8, statSync as statSync3 } from "fs";
4059
4080
  import { execSync as execSync3 } from "child_process";
4060
4081
  import path7 from "path";
@@ -4089,12 +4110,14 @@ function linuxSecretAvailable() {
4089
4110
  function isRootOnlyTrustedServerKeyFile(keyPath) {
4090
4111
  if (process.platform !== "linux") return false;
4091
4112
  try {
4092
- const uid = typeof os5.userInfo().uid === "number" ? os5.userInfo().uid : -1;
4093
4113
  const st = statSync3(keyPath);
4094
4114
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
4115
+ const uid = typeof os5.userInfo().uid === "number" ? os5.userInfo().uid : -1;
4095
4116
  if (uid === 0) return true;
4096
4117
  const exeOsDir = process.env.EXE_OS_DIR;
4097
- return Boolean(exeOsDir && path7.resolve(keyPath).startsWith(path7.resolve(exeOsDir) + path7.sep));
4118
+ if (exeOsDir && path7.resolve(keyPath).startsWith(path7.resolve(exeOsDir) + path7.sep)) return true;
4119
+ if (!linuxSecretAvailable()) return true;
4120
+ return false;
4098
4121
  } catch {
4099
4122
  return false;
4100
4123
  }
@@ -4244,15 +4267,25 @@ async function writeMachineBoundFileFallback(b64) {
4244
4267
  await mkdir3(dir, { recursive: true });
4245
4268
  const keyPath = getKeyPath();
4246
4269
  const machineKey = deriveMachineKey();
4247
- if (machineKey) {
4248
- const encrypted = encryptWithMachineKey(b64, machineKey);
4249
- await writeFile3(keyPath, encrypted + "\n", "utf-8");
4250
- await chmod2(keyPath, 384);
4251
- return "encrypted";
4270
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
4271
+ const result = machineKey ? "encrypted" : "plaintext";
4272
+ const tmpPath = keyPath + ".tmp";
4273
+ try {
4274
+ if (existsSync8(keyPath)) {
4275
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
4276
+ });
4277
+ }
4278
+ await writeFile3(tmpPath, content, "utf-8");
4279
+ await chmod2(tmpPath, 384);
4280
+ await rename(tmpPath, keyPath);
4281
+ } catch (err) {
4282
+ try {
4283
+ await unlink(tmpPath);
4284
+ } catch {
4285
+ }
4286
+ throw err;
4252
4287
  }
4253
- await writeFile3(keyPath, b64 + "\n", "utf-8");
4254
- await chmod2(keyPath, 384);
4255
- return "plaintext";
4288
+ return result;
4256
4289
  }
4257
4290
  async function getMasterKey() {
4258
4291
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -4319,7 +4352,7 @@ async function getMasterKey() {
4319
4352
  b64Value = content;
4320
4353
  }
4321
4354
  const key = Buffer.from(b64Value, "base64");
4322
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
4355
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
4323
4356
  return key;
4324
4357
  }
4325
4358
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
@@ -7989,6 +8022,7 @@ var init_provider_table = __esm({
7989
8022
  // src/lib/intercom-queue.ts
7990
8023
  var intercom_queue_exports = {};
7991
8024
  __export(intercom_queue_exports, {
8025
+ _resetDrainGuard: () => _resetDrainGuard,
7992
8026
  clearQueueForAgent: () => clearQueueForAgent,
7993
8027
  drainForSession: () => drainForSession,
7994
8028
  drainQueue: () => drainQueue,
@@ -8034,38 +8068,47 @@ function queueIntercom(targetSession, reason) {
8034
8068
  writeQueue(queue);
8035
8069
  }
8036
8070
  function drainQueue(isSessionBusy2, sendKeys) {
8071
+ if (_draining) {
8072
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
8073
+ return { drained: 0, failed: 0 };
8074
+ }
8037
8075
  const queue = readQueue();
8038
8076
  if (queue.length === 0) return { drained: 0, failed: 0 };
8077
+ _draining = true;
8039
8078
  const remaining = [];
8040
8079
  let drained = 0;
8041
8080
  let failed = 0;
8042
- for (const item of queue) {
8043
- const age = Date.now() - new Date(item.queuedAt).getTime();
8044
- if (age > TTL_MS) {
8045
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
8046
- failed++;
8047
- continue;
8048
- }
8049
- try {
8050
- if (!isSessionBusy2(item.targetSession)) {
8051
- const success = sendKeys(item.targetSession);
8052
- if (success) {
8053
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
8054
- drained++;
8055
- continue;
8081
+ try {
8082
+ for (const item of queue) {
8083
+ const age = Date.now() - new Date(item.queuedAt).getTime();
8084
+ if (age > TTL_MS) {
8085
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
8086
+ failed++;
8087
+ continue;
8088
+ }
8089
+ try {
8090
+ if (!isSessionBusy2(item.targetSession)) {
8091
+ const success = sendKeys(item.targetSession);
8092
+ if (success) {
8093
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
8094
+ drained++;
8095
+ continue;
8096
+ }
8056
8097
  }
8098
+ } catch {
8057
8099
  }
8058
- } catch {
8059
- }
8060
- item.attempts++;
8061
- if (item.attempts >= MAX_RETRIES2) {
8062
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
8063
- failed++;
8064
- continue;
8100
+ item.attempts++;
8101
+ if (item.attempts >= MAX_RETRIES2) {
8102
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
8103
+ failed++;
8104
+ continue;
8105
+ }
8106
+ remaining.push(item);
8065
8107
  }
8066
- remaining.push(item);
8108
+ writeQueue(remaining);
8109
+ } finally {
8110
+ _draining = false;
8067
8111
  }
8068
- writeQueue(remaining);
8069
8112
  return { drained, failed };
8070
8113
  }
8071
8114
  function drainForSession(targetSession, sendKeys) {
@@ -8090,6 +8133,9 @@ function clearQueueForAgent(agentName) {
8090
8133
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
8091
8134
  }
8092
8135
  }
8136
+ function _resetDrainGuard() {
8137
+ _draining = false;
8138
+ }
8093
8139
  function logQueue(msg) {
8094
8140
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
8095
8141
  `;
@@ -8101,13 +8147,14 @@ function logQueue(msg) {
8101
8147
  } catch {
8102
8148
  }
8103
8149
  }
8104
- var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
8150
+ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, _draining, INTERCOM_LOG;
8105
8151
  var init_intercom_queue = __esm({
8106
8152
  "src/lib/intercom-queue.ts"() {
8107
8153
  "use strict";
8108
8154
  QUEUE_PATH = path11.join(os8.homedir(), ".exe-os", "intercom-queue.json");
8109
8155
  MAX_RETRIES2 = 5;
8110
8156
  TTL_MS = 60 * 60 * 1e3;
8157
+ _draining = false;
8111
8158
  INTERCOM_LOG = path11.join(os8.homedir(), ".exe-os", "intercom.log");
8112
8159
  }
8113
8160
  });
@@ -8755,6 +8802,17 @@ var init_task_scope = __esm({
8755
8802
  });
8756
8803
 
8757
8804
  // src/lib/notifications.ts
8805
+ var notifications_exports = {};
8806
+ __export(notifications_exports, {
8807
+ cleanupOldNotifications: () => cleanupOldNotifications,
8808
+ formatNotifications: () => formatNotifications,
8809
+ markAsRead: () => markAsRead,
8810
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
8811
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
8812
+ migrateJsonNotifications: () => migrateJsonNotifications,
8813
+ readUnreadNotifications: () => readUnreadNotifications,
8814
+ writeNotification: () => writeNotification
8815
+ });
8758
8816
  import crypto4 from "crypto";
8759
8817
  import path15 from "path";
8760
8818
  import os11 from "os";
@@ -8791,6 +8849,52 @@ async function writeNotification(notification) {
8791
8849
  `);
8792
8850
  }
8793
8851
  }
8852
+ async function readUnreadNotifications(agentFilter, sessionScope) {
8853
+ try {
8854
+ const client = getClient();
8855
+ const conditions = ["read = 0"];
8856
+ const args = [];
8857
+ const scope = strictSessionScopeFilter(sessionScope);
8858
+ if (agentFilter) {
8859
+ conditions.push("agent_id = ?");
8860
+ args.push(agentFilter);
8861
+ }
8862
+ const result = await client.execute({
8863
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
8864
+ FROM notifications
8865
+ WHERE ${conditions.join(" AND ")}${scope.sql}
8866
+ ORDER BY created_at ASC`,
8867
+ args: [...args, ...scope.args]
8868
+ });
8869
+ return result.rows.map((r) => ({
8870
+ id: String(r.id),
8871
+ agentId: String(r.agent_id),
8872
+ agentRole: String(r.agent_role),
8873
+ event: String(r.event),
8874
+ project: String(r.project),
8875
+ summary: String(r.summary),
8876
+ taskFile: r.task_file ? String(r.task_file) : void 0,
8877
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
8878
+ timestamp: String(r.created_at),
8879
+ read: false
8880
+ }));
8881
+ } catch {
8882
+ return [];
8883
+ }
8884
+ }
8885
+ async function markAsRead(ids, sessionScope) {
8886
+ if (ids.length === 0) return;
8887
+ try {
8888
+ const client = getClient();
8889
+ const placeholders = ids.map(() => "?").join(", ");
8890
+ const scope = strictSessionScopeFilter(sessionScope);
8891
+ await client.execute({
8892
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
8893
+ args: [...ids, ...scope.args]
8894
+ });
8895
+ } catch {
8896
+ }
8897
+ }
8794
8898
  async function markAsReadByTaskFile(taskFile, sessionScope) {
8795
8899
  try {
8796
8900
  const client = getClient();
@@ -8803,11 +8907,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
8803
8907
  } catch {
8804
8908
  }
8805
8909
  }
8910
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
8911
+ try {
8912
+ const client = getClient();
8913
+ const cutoff = new Date(
8914
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
8915
+ ).toISOString();
8916
+ const scope = strictSessionScopeFilter(sessionScope);
8917
+ const result = await client.execute({
8918
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
8919
+ args: [cutoff, ...scope.args]
8920
+ });
8921
+ return result.rowsAffected;
8922
+ } catch {
8923
+ return 0;
8924
+ }
8925
+ }
8926
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
8927
+ try {
8928
+ const client = getClient();
8929
+ const scope = strictSessionScopeFilter(sessionScope);
8930
+ const result = await client.execute({
8931
+ sql: `UPDATE notifications SET read = 1
8932
+ WHERE read = 0
8933
+ AND task_file IS NOT NULL
8934
+ ${scope.sql}
8935
+ AND task_file IN (
8936
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
8937
+ )`,
8938
+ args: [...scope.args, ...scope.args]
8939
+ });
8940
+ return result.rowsAffected;
8941
+ } catch {
8942
+ return 0;
8943
+ }
8944
+ }
8945
+ function formatNotifications(notifications) {
8946
+ if (notifications.length === 0) return "";
8947
+ const grouped = /* @__PURE__ */ new Map();
8948
+ for (const n of notifications) {
8949
+ const key = `${n.agentId}|${n.agentRole}`;
8950
+ if (!grouped.has(key)) grouped.set(key, []);
8951
+ grouped.get(key).push(n);
8952
+ }
8953
+ const lines = [];
8954
+ lines.push(`## Notifications (${notifications.length} unread)
8955
+ `);
8956
+ for (const [key, items] of grouped) {
8957
+ const [agentId, agentRole] = key.split("|");
8958
+ lines.push(`**${agentId}** (${agentRole}):`);
8959
+ for (const item of items) {
8960
+ const ago = formatTimeAgo(item.timestamp);
8961
+ const icon = eventIcon(item.event);
8962
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
8963
+ }
8964
+ lines.push("");
8965
+ }
8966
+ return lines.join("\n");
8967
+ }
8968
+ async function migrateJsonNotifications() {
8969
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path15.join(os11.homedir(), ".exe-os");
8970
+ const notifDir = path15.join(base, "notifications");
8971
+ if (!existsSync15(notifDir)) return 0;
8972
+ let migrated = 0;
8973
+ try {
8974
+ const files = readdirSync2(notifDir).filter((f) => f.endsWith(".json"));
8975
+ if (files.length === 0) return 0;
8976
+ const client = getClient();
8977
+ for (const file of files) {
8978
+ try {
8979
+ const filePath = path15.join(notifDir, file);
8980
+ const data = JSON.parse(readFileSync11(filePath, "utf8"));
8981
+ await client.execute({
8982
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
8983
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
8984
+ args: [
8985
+ crypto4.randomUUID(),
8986
+ data.agentId ?? "unknown",
8987
+ data.agentRole ?? "unknown",
8988
+ data.event ?? "session_summary",
8989
+ data.project ?? "unknown",
8990
+ data.summary ?? "",
8991
+ data.taskFile ?? null,
8992
+ null,
8993
+ data.read ? 1 : 0,
8994
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
8995
+ ]
8996
+ });
8997
+ unlinkSync4(filePath);
8998
+ migrated++;
8999
+ } catch {
9000
+ }
9001
+ }
9002
+ try {
9003
+ const remaining = readdirSync2(notifDir);
9004
+ if (remaining.length === 0) {
9005
+ rmdirSync(notifDir);
9006
+ }
9007
+ } catch {
9008
+ }
9009
+ } catch {
9010
+ }
9011
+ return migrated;
9012
+ }
9013
+ function eventIcon(event) {
9014
+ switch (event) {
9015
+ case "task_complete":
9016
+ return "Completed:";
9017
+ case "task_needs_fix":
9018
+ return "Needs fix:";
9019
+ case "session_summary":
9020
+ return "Session:";
9021
+ case "error_spike":
9022
+ return "Errors:";
9023
+ case "orphan_task":
9024
+ return "Orphan:";
9025
+ case "subtasks_complete":
9026
+ return "Subtasks done:";
9027
+ case "capacity_relaunch":
9028
+ return "Relaunched:";
9029
+ }
9030
+ }
9031
+ function formatTimeAgo(timestamp) {
9032
+ const diffMs = Date.now() - new Date(timestamp).getTime();
9033
+ const mins = Math.floor(diffMs / 6e4);
9034
+ if (mins < 1) return "just now";
9035
+ if (mins < 60) return `${mins}m ago`;
9036
+ const hours = Math.floor(mins / 60);
9037
+ if (hours < 24) return `${hours}h ago`;
9038
+ const days = Math.floor(hours / 24);
9039
+ return `${days}d ago`;
9040
+ }
9041
+ var CLEANUP_DAYS;
8806
9042
  var init_notifications = __esm({
8807
9043
  "src/lib/notifications.ts"() {
8808
9044
  "use strict";
8809
9045
  init_database();
8810
9046
  init_task_scope();
9047
+ CLEANUP_DAYS = 7;
8811
9048
  }
8812
9049
  });
8813
9050
 
@@ -9798,7 +10035,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
9798
10035
  taskFile
9799
10036
  });
9800
10037
  const originalPriority = String(row.priority).toLowerCase();
9801
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
10038
+ const resultLower = result?.toLowerCase() ?? "";
10039
+ const hasTestEvidence = (
10040
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
10041
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
10042
+ );
10043
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
10044
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
9802
10045
  if (!autoApprove) {
9803
10046
  try {
9804
10047
  const key = getSessionKey();
@@ -9806,6 +10049,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
9806
10049
  if (exeSession) {
9807
10050
  sendIntercom(exeSession);
9808
10051
  }
10052
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
10053
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
10054
+ if (exeSession) {
10055
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
10056
+ sendIntercom(reviewerSession);
10057
+ }
10058
+ }
9809
10059
  } catch {
9810
10060
  }
9811
10061
  }
@@ -10513,6 +10763,20 @@ async function updateTask(input) {
10513
10763
  notifyTaskDone();
10514
10764
  }
10515
10765
  await markTaskNotificationsRead(taskFile);
10766
+ if (input.status === "needs_review" && !isCoordinator) {
10767
+ try {
10768
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
10769
+ await writeNotification2({
10770
+ agentId: String(row.assigned_to),
10771
+ agentRole: String(row.assigned_to),
10772
+ event: "task_complete",
10773
+ project: String(row.project_name),
10774
+ summary: `"${String(row.title)}" is ready for review`,
10775
+ taskFile
10776
+ });
10777
+ } catch {
10778
+ }
10779
+ }
10516
10780
  if (input.status === "done" || input.status === "closed") {
10517
10781
  try {
10518
10782
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -10945,18 +11209,31 @@ function acquireSpawnLock2(sessionName) {
10945
11209
  mkdirSync10(SPAWN_LOCK_DIR, { recursive: true });
10946
11210
  }
10947
11211
  const lockFile = spawnLockPath(sessionName);
10948
- if (existsSync18(lockFile)) {
10949
- try {
10950
- const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
10951
- const age = Date.now() - lock.timestamp;
10952
- if (isProcessAlive(lock.pid) && age < 6e4) {
10953
- return false;
10954
- }
10955
- } catch {
11212
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
11213
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
11214
+ const { constants } = __require("fs");
11215
+ try {
11216
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
11217
+ writeSync(fd, lockData);
11218
+ closeSync3(fd);
11219
+ return true;
11220
+ } catch (err) {
11221
+ if (err?.code !== "EEXIST") {
11222
+ return true;
10956
11223
  }
10957
11224
  }
10958
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
10959
- return true;
11225
+ try {
11226
+ const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
11227
+ const age = Date.now() - lock.timestamp;
11228
+ if (isProcessAlive(lock.pid) && age < 6e4) {
11229
+ return false;
11230
+ }
11231
+ writeFileSync8(lockFile, lockData);
11232
+ return true;
11233
+ } catch {
11234
+ writeFileSync8(lockFile, lockData);
11235
+ return true;
11236
+ }
10960
11237
  }
10961
11238
  function releaseSpawnLock2(sessionName) {
10962
11239
  try {
@@ -11035,6 +11312,21 @@ function parseParentExe(sessionName, agentId) {
11035
11312
  function extractRootExe(name) {
11036
11313
  if (!name) return null;
11037
11314
  if (!name.includes("-")) return name;
11315
+ try {
11316
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
11317
+ if (roster.length > 0) {
11318
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
11319
+ for (const agentName of sortedNames) {
11320
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11321
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
11322
+ const match = name.match(regex);
11323
+ if (match) {
11324
+ return extractRootExe(match[1]);
11325
+ }
11326
+ }
11327
+ }
11328
+ } catch {
11329
+ }
11038
11330
  const parts = name.split("-").filter(Boolean);
11039
11331
  return parts.length > 0 ? parts[parts.length - 1] : null;
11040
11332
  }
@@ -11053,6 +11345,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
11053
11345
  function getParentExe(sessionKey) {
11054
11346
  try {
11055
11347
  const data = JSON.parse(readFileSync13(path21.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
11348
+ if (data.registeredAt) {
11349
+ const age = Date.now() - new Date(data.registeredAt).getTime();
11350
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
11351
+ }
11056
11352
  return data.parentExe || null;
11057
11353
  } catch {
11058
11354
  return null;
@@ -11601,7 +11897,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
11601
11897
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
11602
11898
  } catch {
11603
11899
  }
11604
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
11900
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
11605
11901
  if (ccProvider !== DEFAULT_PROVIDER) {
11606
11902
  const cfg = PROVIDER_TABLE[ccProvider];
11607
11903
  if (cfg?.apiKeyEnv) {
@@ -11636,10 +11932,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
11636
11932
  }
11637
11933
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
11638
11934
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
11639
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
11640
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
11641
- ccModel += "[1m]";
11642
- }
11935
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
11936
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
11643
11937
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
11644
11938
  }
11645
11939
  }
@@ -11740,7 +12034,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
11740
12034
  releaseSpawnLock2(sessionName);
11741
12035
  return { sessionName };
11742
12036
  }
11743
- 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;
12037
+ 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;
11744
12038
  var init_tmux_routing = __esm({
11745
12039
  "src/lib/tmux-routing.ts"() {
11746
12040
  "use strict";
@@ -11760,6 +12054,7 @@ var init_tmux_routing = __esm({
11760
12054
  SESSION_CACHE = path21.join(os13.homedir(), ".exe-os", "session-cache");
11761
12055
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
11762
12056
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
12057
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
11763
12058
  VERIFY_PANE_LINES = 200;
11764
12059
  INTERCOM_DEBOUNCE_MS = 3e4;
11765
12060
  CODEX_DEBOUNCE_MS = 12e4;