@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
package/dist/bin/cli.js CHANGED
@@ -155,6 +155,17 @@ function normalizeOrchestration(raw) {
155
155
  const userOrg = raw.orchestration ?? {};
156
156
  raw.orchestration = { ...defaultOrg, ...userOrg };
157
157
  }
158
+ function normalizeCloudEndpoint(raw) {
159
+ const cloud = raw.cloud;
160
+ if (!cloud?.endpoint) return;
161
+ const ep = String(cloud.endpoint);
162
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
163
+ cloud.endpoint = "https://cloud.askexe.com";
164
+ process.stderr.write(
165
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
166
+ );
167
+ }
168
+ }
158
169
  async function loadConfig() {
159
170
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
160
171
  await ensurePrivateDir(dir);
@@ -180,6 +191,7 @@ async function loadConfig() {
180
191
  normalizeSessionLifecycle(migratedCfg);
181
192
  normalizeAutoUpdate(migratedCfg);
182
193
  normalizeOrchestration(migratedCfg);
194
+ normalizeCloudEndpoint(migratedCfg);
183
195
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
184
196
  if (config.dbPath.startsWith("~")) {
185
197
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -208,6 +220,7 @@ function loadConfigSync() {
208
220
  normalizeSessionLifecycle(migratedCfg);
209
221
  normalizeAutoUpdate(migratedCfg);
210
222
  normalizeOrchestration(migratedCfg);
223
+ normalizeCloudEndpoint(migratedCfg);
211
224
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
212
225
  if (config.dbPath.startsWith("~")) {
213
226
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -368,6 +381,7 @@ __export(agent_config_exports, {
368
381
  clearAgentRuntime: () => clearAgentRuntime,
369
382
  getAgentRuntime: () => getAgentRuntime,
370
383
  loadAgentConfig: () => loadAgentConfig,
384
+ normalizeCcModelName: () => normalizeCcModelName,
371
385
  saveAgentConfig: () => saveAgentConfig,
372
386
  setAgentMcps: () => setAgentMcps,
373
387
  setAgentRuntime: () => setAgentRuntime
@@ -396,6 +410,13 @@ function getAgentRuntime(agentId) {
396
410
  if (orgDefault) return orgDefault;
397
411
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
398
412
  }
413
+ function normalizeCcModelName(model) {
414
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
415
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
416
+ ccModel += "[1m]";
417
+ }
418
+ return ccModel;
419
+ }
399
420
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
400
421
  const knownModels = KNOWN_RUNTIMES[runtime];
401
422
  if (!knownModels) {
@@ -896,7 +917,7 @@ function readOrCreateDaemonToken(homeDir = os5.homedir()) {
896
917
  function buildMcpHttpHeaders(homeDir = os5.homedir(), opts = {}) {
897
918
  const agentId = opts.useShellPlaceholders ? "${AGENT_ID:-exe}" : opts.agentId ?? DEFAULT_MCP_HTTP_AGENT_ID;
898
919
  const agentRole = opts.useShellPlaceholders ? "${AGENT_ROLE:-COO}" : opts.agentRole ?? DEFAULT_MCP_HTTP_AGENT_ROLE;
899
- const sessionName = opts.useShellPlaceholders ? "$(tmux display-message -p '#{session_name}' 2>/dev/null || echo '')" : process.env.EXE_SESSION_NAME ?? "";
920
+ const sessionName = opts.useShellPlaceholders ? "" : process.env.EXE_SESSION_NAME ?? "";
900
921
  const headers = {
901
922
  Authorization: `Bearer ${readOrCreateDaemonToken(homeDir)}`,
902
923
  "X-Agent-Id": agentId,
@@ -2555,7 +2576,7 @@ __export(keychain_exports, {
2555
2576
  importMnemonic: () => importMnemonic,
2556
2577
  setMasterKey: () => setMasterKey
2557
2578
  });
2558
- import { readFile as readFile4, writeFile as writeFile4, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
2579
+ import { readFile as readFile4, writeFile as writeFile4, unlink, mkdir as mkdir4, chmod as chmod2, rename, copyFile } from "fs/promises";
2559
2580
  import { existsSync as existsSync10, statSync } from "fs";
2560
2581
  import { execSync as execSync3 } from "child_process";
2561
2582
  import path9 from "path";
@@ -2590,12 +2611,14 @@ function linuxSecretAvailable() {
2590
2611
  function isRootOnlyTrustedServerKeyFile(keyPath) {
2591
2612
  if (process.platform !== "linux") return false;
2592
2613
  try {
2593
- const uid = typeof os8.userInfo().uid === "number" ? os8.userInfo().uid : -1;
2594
2614
  const st = statSync(keyPath);
2595
2615
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
2616
+ const uid = typeof os8.userInfo().uid === "number" ? os8.userInfo().uid : -1;
2596
2617
  if (uid === 0) return true;
2597
2618
  const exeOsDir = process.env.EXE_OS_DIR;
2598
- return Boolean(exeOsDir && path9.resolve(keyPath).startsWith(path9.resolve(exeOsDir) + path9.sep));
2619
+ if (exeOsDir && path9.resolve(keyPath).startsWith(path9.resolve(exeOsDir) + path9.sep)) return true;
2620
+ if (!linuxSecretAvailable()) return true;
2621
+ return false;
2599
2622
  } catch {
2600
2623
  return false;
2601
2624
  }
@@ -2745,15 +2768,25 @@ async function writeMachineBoundFileFallback(b64) {
2745
2768
  await mkdir4(dir, { recursive: true });
2746
2769
  const keyPath = getKeyPath();
2747
2770
  const machineKey = deriveMachineKey();
2748
- if (machineKey) {
2749
- const encrypted = encryptWithMachineKey(b64, machineKey);
2750
- await writeFile4(keyPath, encrypted + "\n", "utf-8");
2751
- await chmod2(keyPath, 384);
2752
- return "encrypted";
2771
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
2772
+ const result = machineKey ? "encrypted" : "plaintext";
2773
+ const tmpPath = keyPath + ".tmp";
2774
+ try {
2775
+ if (existsSync10(keyPath)) {
2776
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
2777
+ });
2778
+ }
2779
+ await writeFile4(tmpPath, content, "utf-8");
2780
+ await chmod2(tmpPath, 384);
2781
+ await rename(tmpPath, keyPath);
2782
+ } catch (err) {
2783
+ try {
2784
+ await unlink(tmpPath);
2785
+ } catch {
2786
+ }
2787
+ throw err;
2753
2788
  }
2754
- await writeFile4(keyPath, b64 + "\n", "utf-8");
2755
- await chmod2(keyPath, 384);
2756
- return "plaintext";
2789
+ return result;
2757
2790
  }
2758
2791
  async function getMasterKey() {
2759
2792
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -2820,7 +2853,7 @@ async function getMasterKey() {
2820
2853
  b64Value = content;
2821
2854
  }
2822
2855
  const key = Buffer.from(b64Value, "base64");
2823
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
2856
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
2824
2857
  return key;
2825
2858
  }
2826
2859
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
@@ -6999,6 +7032,7 @@ __export(cloud_sync_exports, {
6999
7032
  markCloudReuploadRequired: () => markCloudReuploadRequired,
7000
7033
  mergeConfig: () => mergeConfig,
7001
7034
  mergeRosterFromRemote: () => mergeRosterFromRemote,
7035
+ migrateEndpoint: () => migrateEndpoint,
7002
7036
  pushToPostgres: () => pushToPostgres,
7003
7037
  recordRosterDeletion: () => recordRosterDeletion
7004
7038
  });
@@ -7169,6 +7203,15 @@ async function fetchWithRetry(url, init) {
7169
7203
  }
7170
7204
  throw lastError;
7171
7205
  }
7206
+ function migrateEndpoint(endpoint) {
7207
+ if (endpoint === "https://askexe.com/cloud" || endpoint === "https://askexe.com/cloud/") {
7208
+ process.stderr.write(
7209
+ "[cloud-sync] Auto-migrating endpoint from askexe.com/cloud to cloud.askexe.com (bypasses Cloudflare WAF for datacenter IPs)\n"
7210
+ );
7211
+ return "https://cloud.askexe.com";
7212
+ }
7213
+ return endpoint;
7214
+ }
7172
7215
  function assertSecureEndpoint(endpoint) {
7173
7216
  if (endpoint.startsWith("https://")) return;
7174
7217
  if (endpoint.startsWith("http://")) {
@@ -7306,6 +7349,7 @@ async function markCloudReuploadRequired(client = getClient()) {
7306
7349
  await client.execute("INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_reupload_required', '1')");
7307
7350
  }
7308
7351
  async function cloudSync(config) {
7352
+ config = { ...config, endpoint: migrateEndpoint(config.endpoint) };
7309
7353
  if (!isSyncCryptoInitialized()) {
7310
7354
  try {
7311
7355
  const { getMasterKey: getMasterKey2 } = await Promise.resolve().then(() => (init_keychain(), keychain_exports));
@@ -13929,6 +13973,7 @@ var init_provider_table = __esm({
13929
13973
  // src/lib/intercom-queue.ts
13930
13974
  var intercom_queue_exports = {};
13931
13975
  __export(intercom_queue_exports, {
13976
+ _resetDrainGuard: () => _resetDrainGuard,
13932
13977
  clearQueueForAgent: () => clearQueueForAgent,
13933
13978
  drainForSession: () => drainForSession,
13934
13979
  drainQueue: () => drainQueue,
@@ -13974,38 +14019,47 @@ function queueIntercom(targetSession, reason) {
13974
14019
  writeQueue(queue);
13975
14020
  }
13976
14021
  function drainQueue(isSessionBusy2, sendKeys) {
14022
+ if (_draining) {
14023
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
14024
+ return { drained: 0, failed: 0 };
14025
+ }
13977
14026
  const queue = readQueue();
13978
14027
  if (queue.length === 0) return { drained: 0, failed: 0 };
14028
+ _draining = true;
13979
14029
  const remaining = [];
13980
14030
  let drained = 0;
13981
14031
  let failed = 0;
13982
- for (const item of queue) {
13983
- const age = Date.now() - new Date(item.queuedAt).getTime();
13984
- if (age > TTL_MS) {
13985
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
13986
- failed++;
13987
- continue;
13988
- }
13989
- try {
13990
- if (!isSessionBusy2(item.targetSession)) {
13991
- const success = sendKeys(item.targetSession);
13992
- if (success) {
13993
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
13994
- drained++;
13995
- continue;
14032
+ try {
14033
+ for (const item of queue) {
14034
+ const age = Date.now() - new Date(item.queuedAt).getTime();
14035
+ if (age > TTL_MS) {
14036
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
14037
+ failed++;
14038
+ continue;
14039
+ }
14040
+ try {
14041
+ if (!isSessionBusy2(item.targetSession)) {
14042
+ const success = sendKeys(item.targetSession);
14043
+ if (success) {
14044
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
14045
+ drained++;
14046
+ continue;
14047
+ }
13996
14048
  }
14049
+ } catch {
13997
14050
  }
13998
- } catch {
13999
- }
14000
- item.attempts++;
14001
- if (item.attempts >= MAX_RETRIES2) {
14002
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
14003
- failed++;
14004
- continue;
14051
+ item.attempts++;
14052
+ if (item.attempts >= MAX_RETRIES2) {
14053
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
14054
+ failed++;
14055
+ continue;
14056
+ }
14057
+ remaining.push(item);
14005
14058
  }
14006
- remaining.push(item);
14059
+ writeQueue(remaining);
14060
+ } finally {
14061
+ _draining = false;
14007
14062
  }
14008
- writeQueue(remaining);
14009
14063
  return { drained, failed };
14010
14064
  }
14011
14065
  function drainForSession(targetSession, sendKeys) {
@@ -14030,6 +14084,9 @@ function clearQueueForAgent(agentName) {
14030
14084
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
14031
14085
  }
14032
14086
  }
14087
+ function _resetDrainGuard() {
14088
+ _draining = false;
14089
+ }
14033
14090
  function logQueue(msg) {
14034
14091
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
14035
14092
  `;
@@ -14041,13 +14098,14 @@ function logQueue(msg) {
14041
14098
  } catch {
14042
14099
  }
14043
14100
  }
14044
- var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
14101
+ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, _draining, INTERCOM_LOG;
14045
14102
  var init_intercom_queue = __esm({
14046
14103
  "src/lib/intercom-queue.ts"() {
14047
14104
  "use strict";
14048
14105
  QUEUE_PATH = path26.join(os15.homedir(), ".exe-os", "intercom-queue.json");
14049
14106
  MAX_RETRIES2 = 5;
14050
14107
  TTL_MS = 60 * 60 * 1e3;
14108
+ _draining = false;
14051
14109
  INTERCOM_LOG = path26.join(os15.homedir(), ".exe-os", "intercom.log");
14052
14110
  }
14053
14111
  });
@@ -14170,6 +14228,17 @@ var init_task_scope = __esm({
14170
14228
  });
14171
14229
 
14172
14230
  // src/lib/notifications.ts
14231
+ var notifications_exports = {};
14232
+ __export(notifications_exports, {
14233
+ cleanupOldNotifications: () => cleanupOldNotifications,
14234
+ formatNotifications: () => formatNotifications,
14235
+ markAsRead: () => markAsRead,
14236
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
14237
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
14238
+ migrateJsonNotifications: () => migrateJsonNotifications,
14239
+ readUnreadNotifications: () => readUnreadNotifications,
14240
+ writeNotification: () => writeNotification
14241
+ });
14173
14242
  import crypto7 from "crypto";
14174
14243
  import path28 from "path";
14175
14244
  import os16 from "os";
@@ -14206,6 +14275,52 @@ async function writeNotification(notification) {
14206
14275
  `);
14207
14276
  }
14208
14277
  }
14278
+ async function readUnreadNotifications(agentFilter, sessionScope) {
14279
+ try {
14280
+ const client = getClient();
14281
+ const conditions = ["read = 0"];
14282
+ const args2 = [];
14283
+ const scope = strictSessionScopeFilter(sessionScope);
14284
+ if (agentFilter) {
14285
+ conditions.push("agent_id = ?");
14286
+ args2.push(agentFilter);
14287
+ }
14288
+ const result = await client.execute({
14289
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
14290
+ FROM notifications
14291
+ WHERE ${conditions.join(" AND ")}${scope.sql}
14292
+ ORDER BY created_at ASC`,
14293
+ args: [...args2, ...scope.args]
14294
+ });
14295
+ return result.rows.map((r) => ({
14296
+ id: String(r.id),
14297
+ agentId: String(r.agent_id),
14298
+ agentRole: String(r.agent_role),
14299
+ event: String(r.event),
14300
+ project: String(r.project),
14301
+ summary: String(r.summary),
14302
+ taskFile: r.task_file ? String(r.task_file) : void 0,
14303
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
14304
+ timestamp: String(r.created_at),
14305
+ read: false
14306
+ }));
14307
+ } catch {
14308
+ return [];
14309
+ }
14310
+ }
14311
+ async function markAsRead(ids, sessionScope) {
14312
+ if (ids.length === 0) return;
14313
+ try {
14314
+ const client = getClient();
14315
+ const placeholders = ids.map(() => "?").join(", ");
14316
+ const scope = strictSessionScopeFilter(sessionScope);
14317
+ await client.execute({
14318
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
14319
+ args: [...ids, ...scope.args]
14320
+ });
14321
+ } catch {
14322
+ }
14323
+ }
14209
14324
  async function markAsReadByTaskFile(taskFile, sessionScope) {
14210
14325
  try {
14211
14326
  const client = getClient();
@@ -14218,11 +14333,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
14218
14333
  } catch {
14219
14334
  }
14220
14335
  }
14336
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
14337
+ try {
14338
+ const client = getClient();
14339
+ const cutoff = new Date(
14340
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
14341
+ ).toISOString();
14342
+ const scope = strictSessionScopeFilter(sessionScope);
14343
+ const result = await client.execute({
14344
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
14345
+ args: [cutoff, ...scope.args]
14346
+ });
14347
+ return result.rowsAffected;
14348
+ } catch {
14349
+ return 0;
14350
+ }
14351
+ }
14352
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
14353
+ try {
14354
+ const client = getClient();
14355
+ const scope = strictSessionScopeFilter(sessionScope);
14356
+ const result = await client.execute({
14357
+ sql: `UPDATE notifications SET read = 1
14358
+ WHERE read = 0
14359
+ AND task_file IS NOT NULL
14360
+ ${scope.sql}
14361
+ AND task_file IN (
14362
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
14363
+ )`,
14364
+ args: [...scope.args, ...scope.args]
14365
+ });
14366
+ return result.rowsAffected;
14367
+ } catch {
14368
+ return 0;
14369
+ }
14370
+ }
14371
+ function formatNotifications(notifications) {
14372
+ if (notifications.length === 0) return "";
14373
+ const grouped = /* @__PURE__ */ new Map();
14374
+ for (const n of notifications) {
14375
+ const key = `${n.agentId}|${n.agentRole}`;
14376
+ if (!grouped.has(key)) grouped.set(key, []);
14377
+ grouped.get(key).push(n);
14378
+ }
14379
+ const lines = [];
14380
+ lines.push(`## Notifications (${notifications.length} unread)
14381
+ `);
14382
+ for (const [key, items] of grouped) {
14383
+ const [agentId, agentRole] = key.split("|");
14384
+ lines.push(`**${agentId}** (${agentRole}):`);
14385
+ for (const item of items) {
14386
+ const ago = formatTimeAgo(item.timestamp);
14387
+ const icon = eventIcon(item.event);
14388
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
14389
+ }
14390
+ lines.push("");
14391
+ }
14392
+ return lines.join("\n");
14393
+ }
14394
+ async function migrateJsonNotifications() {
14395
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path28.join(os16.homedir(), ".exe-os");
14396
+ const notifDir = path28.join(base, "notifications");
14397
+ if (!existsSync26(notifDir)) return 0;
14398
+ let migrated = 0;
14399
+ try {
14400
+ const files = readdirSync5(notifDir).filter((f) => f.endsWith(".json"));
14401
+ if (files.length === 0) return 0;
14402
+ const client = getClient();
14403
+ for (const file of files) {
14404
+ try {
14405
+ const filePath = path28.join(notifDir, file);
14406
+ const data = JSON.parse(readFileSync21(filePath, "utf8"));
14407
+ await client.execute({
14408
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
14409
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
14410
+ args: [
14411
+ crypto7.randomUUID(),
14412
+ data.agentId ?? "unknown",
14413
+ data.agentRole ?? "unknown",
14414
+ data.event ?? "session_summary",
14415
+ data.project ?? "unknown",
14416
+ data.summary ?? "",
14417
+ data.taskFile ?? null,
14418
+ null,
14419
+ data.read ? 1 : 0,
14420
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
14421
+ ]
14422
+ });
14423
+ unlinkSync9(filePath);
14424
+ migrated++;
14425
+ } catch {
14426
+ }
14427
+ }
14428
+ try {
14429
+ const remaining = readdirSync5(notifDir);
14430
+ if (remaining.length === 0) {
14431
+ rmdirSync(notifDir);
14432
+ }
14433
+ } catch {
14434
+ }
14435
+ } catch {
14436
+ }
14437
+ return migrated;
14438
+ }
14439
+ function eventIcon(event) {
14440
+ switch (event) {
14441
+ case "task_complete":
14442
+ return "Completed:";
14443
+ case "task_needs_fix":
14444
+ return "Needs fix:";
14445
+ case "session_summary":
14446
+ return "Session:";
14447
+ case "error_spike":
14448
+ return "Errors:";
14449
+ case "orphan_task":
14450
+ return "Orphan:";
14451
+ case "subtasks_complete":
14452
+ return "Subtasks done:";
14453
+ case "capacity_relaunch":
14454
+ return "Relaunched:";
14455
+ }
14456
+ }
14457
+ function formatTimeAgo(timestamp) {
14458
+ const diffMs = Date.now() - new Date(timestamp).getTime();
14459
+ const mins = Math.floor(diffMs / 6e4);
14460
+ if (mins < 1) return "just now";
14461
+ if (mins < 60) return `${mins}m ago`;
14462
+ const hours = Math.floor(mins / 60);
14463
+ if (hours < 24) return `${hours}h ago`;
14464
+ const days = Math.floor(hours / 24);
14465
+ return `${days}d ago`;
14466
+ }
14467
+ var CLEANUP_DAYS;
14221
14468
  var init_notifications = __esm({
14222
14469
  "src/lib/notifications.ts"() {
14223
14470
  "use strict";
14224
14471
  init_database();
14225
14472
  init_task_scope();
14473
+ CLEANUP_DAYS = 7;
14226
14474
  }
14227
14475
  });
14228
14476
 
@@ -15229,7 +15477,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now2) {
15229
15477
  taskFile
15230
15478
  });
15231
15479
  const originalPriority = String(row.priority).toLowerCase();
15232
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
15480
+ const resultLower = result?.toLowerCase() ?? "";
15481
+ const hasTestEvidence = (
15482
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
15483
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
15484
+ );
15485
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
15486
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
15233
15487
  if (!autoApprove) {
15234
15488
  try {
15235
15489
  const key = getSessionKey();
@@ -15237,6 +15491,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now2) {
15237
15491
  if (exeSession) {
15238
15492
  sendIntercom(exeSession);
15239
15493
  }
15494
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
15495
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
15496
+ if (exeSession) {
15497
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
15498
+ sendIntercom(reviewerSession);
15499
+ }
15500
+ }
15240
15501
  } catch {
15241
15502
  }
15242
15503
  }
@@ -16012,6 +16273,20 @@ async function updateTask(input) {
16012
16273
  notifyTaskDone();
16013
16274
  }
16014
16275
  await markTaskNotificationsRead(taskFile);
16276
+ if (input.status === "needs_review" && !isCoordinator) {
16277
+ try {
16278
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
16279
+ await writeNotification2({
16280
+ agentId: String(row.assigned_to),
16281
+ agentRole: String(row.assigned_to),
16282
+ event: "task_complete",
16283
+ project: String(row.project_name),
16284
+ summary: `"${String(row.title)}" is ready for review`,
16285
+ taskFile
16286
+ });
16287
+ } catch {
16288
+ }
16289
+ }
16015
16290
  if (input.status === "done" || input.status === "closed") {
16016
16291
  try {
16017
16292
  await cascadeUnblock(taskId, input.baseDir, now2);
@@ -16444,18 +16719,31 @@ function acquireSpawnLock2(sessionName) {
16444
16719
  mkdirSync21(SPAWN_LOCK_DIR, { recursive: true });
16445
16720
  }
16446
16721
  const lockFile = spawnLockPath(sessionName);
16447
- if (existsSync29(lockFile)) {
16448
- try {
16449
- const lock = JSON.parse(readFileSync23(lockFile, "utf8"));
16450
- const age = Date.now() - lock.timestamp;
16451
- if (isProcessAlive(lock.pid) && age < 6e4) {
16452
- return false;
16453
- }
16454
- } catch {
16722
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
16723
+ const { openSync: openSync5, closeSync: closeSync5, writeSync } = __require("fs");
16724
+ const { constants: constants3 } = __require("fs");
16725
+ try {
16726
+ const fd = openSync5(lockFile, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL, 420);
16727
+ writeSync(fd, lockData);
16728
+ closeSync5(fd);
16729
+ return true;
16730
+ } catch (err) {
16731
+ if (err?.code !== "EEXIST") {
16732
+ return true;
16455
16733
  }
16456
16734
  }
16457
- writeFileSync20(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
16458
- return true;
16735
+ try {
16736
+ const lock = JSON.parse(readFileSync23(lockFile, "utf8"));
16737
+ const age = Date.now() - lock.timestamp;
16738
+ if (isProcessAlive(lock.pid) && age < 6e4) {
16739
+ return false;
16740
+ }
16741
+ writeFileSync20(lockFile, lockData);
16742
+ return true;
16743
+ } catch {
16744
+ writeFileSync20(lockFile, lockData);
16745
+ return true;
16746
+ }
16459
16747
  }
16460
16748
  function releaseSpawnLock2(sessionName) {
16461
16749
  try {
@@ -16534,6 +16822,21 @@ function parseParentExe(sessionName, agentId) {
16534
16822
  function extractRootExe(name) {
16535
16823
  if (!name) return null;
16536
16824
  if (!name.includes("-")) return name;
16825
+ try {
16826
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
16827
+ if (roster.length > 0) {
16828
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
16829
+ for (const agentName of sortedNames) {
16830
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16831
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
16832
+ const match = name.match(regex);
16833
+ if (match) {
16834
+ return extractRootExe(match[1]);
16835
+ }
16836
+ }
16837
+ }
16838
+ } catch {
16839
+ }
16537
16840
  const parts = name.split("-").filter(Boolean);
16538
16841
  return parts.length > 0 ? parts[parts.length - 1] : null;
16539
16842
  }
@@ -16552,6 +16855,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
16552
16855
  function getParentExe(sessionKey) {
16553
16856
  try {
16554
16857
  const data = JSON.parse(readFileSync23(path34.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
16858
+ if (data.registeredAt) {
16859
+ const age = Date.now() - new Date(data.registeredAt).getTime();
16860
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
16861
+ }
16555
16862
  return data.parentExe || null;
16556
16863
  } catch {
16557
16864
  return null;
@@ -17100,7 +17407,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
17100
17407
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
17101
17408
  } catch {
17102
17409
  }
17103
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
17410
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
17104
17411
  if (ccProvider !== DEFAULT_PROVIDER) {
17105
17412
  const cfg = PROVIDER_TABLE[ccProvider];
17106
17413
  if (cfg?.apiKeyEnv) {
@@ -17135,10 +17442,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
17135
17442
  }
17136
17443
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
17137
17444
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
17138
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
17139
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
17140
- ccModel += "[1m]";
17141
- }
17445
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
17446
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
17142
17447
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
17143
17448
  }
17144
17449
  }
@@ -17239,7 +17544,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
17239
17544
  releaseSpawnLock2(sessionName);
17240
17545
  return { sessionName };
17241
17546
  }
17242
- 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;
17547
+ 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;
17243
17548
  var init_tmux_routing = __esm({
17244
17549
  "src/lib/tmux-routing.ts"() {
17245
17550
  "use strict";
@@ -17259,6 +17564,7 @@ var init_tmux_routing = __esm({
17259
17564
  SESSION_CACHE = path34.join(os18.homedir(), ".exe-os", "session-cache");
17260
17565
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
17261
17566
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
17567
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
17262
17568
  VERIFY_PANE_LINES = 200;
17263
17569
  INTERCOM_DEBOUNCE_MS = 3e4;
17264
17570
  CODEX_DEBOUNCE_MS = 12e4;
@@ -20483,6 +20789,24 @@ async function runSetupWizard(opts = {}) {
20483
20789
  log("");
20484
20790
  log("=== exe-os Setup ===");
20485
20791
  log("");
20792
+ if (process.platform === "darwin") {
20793
+ const currentShell = process.env.SHELL ?? "";
20794
+ if (currentShell.includes("bash")) {
20795
+ log(" \u26A0 Your default shell is bash.");
20796
+ log(" macOS uses zsh by default since Catalina (2019).");
20797
+ log(" exe-os works with bash, but switching to zsh is recommended:");
20798
+ log(" chsh -s /bin/zsh");
20799
+ log(" Then restart your terminal.");
20800
+ log("");
20801
+ const cont = await ask3(rl, " Continue with bash? (y/n, default: y): ");
20802
+ if (cont.toLowerCase() === "n") {
20803
+ log(" Run `chsh -s /bin/zsh`, restart your terminal, then run setup again.");
20804
+ rl.close();
20805
+ return;
20806
+ }
20807
+ log("");
20808
+ }
20809
+ }
20486
20810
  log("What are you setting up?");
20487
20811
  log("");
20488
20812
  log(" [1] First device / brand new Exe OS memory");
@@ -21155,7 +21479,7 @@ var init_setup_wizard = __esm({
21155
21479
  });
21156
21480
 
21157
21481
  // src/lib/update-backup.ts
21158
- import { copyFile, readFile as readFile6, readdir as readdir3, writeFile as writeFile7, rm as rm2, mkdir as mkdir7, cp } from "fs/promises";
21482
+ import { copyFile as copyFile2, readFile as readFile6, readdir as readdir3, writeFile as writeFile7, rm as rm2, mkdir as mkdir7, cp } from "fs/promises";
21159
21483
  import { existsSync as existsSync36 } from "fs";
21160
21484
  import path42 from "path";
21161
21485
  import os20 from "os";
@@ -21191,7 +21515,7 @@ async function createUpdateBackup(currentVersion, dataDir2, homeDir = os20.homed
21191
21515
  if (!existsSync36(src)) continue;
21192
21516
  const dest = path42.join(backupDir, target.name);
21193
21517
  if (target.type === "file") {
21194
- await copyFile(src, dest);
21518
+ await copyFile2(src, dest);
21195
21519
  } else {
21196
21520
  await cp(src, dest, { recursive: true });
21197
21521
  }
@@ -21202,7 +21526,7 @@ async function createUpdateBackup(currentVersion, dataDir2, homeDir = os20.homed
21202
21526
  if (entry.isFile() && entry.name.endsWith(".db") && entry.name !== BACKUP_DIR_NAME) {
21203
21527
  const src = path42.join(dir, entry.name);
21204
21528
  const dest = path42.join(backupDir, entry.name);
21205
- await copyFile(src, dest);
21529
+ await copyFile2(src, dest);
21206
21530
  backedUpFiles.push(entry.name);
21207
21531
  }
21208
21532
  }
@@ -21211,7 +21535,7 @@ async function createUpdateBackup(currentVersion, dataDir2, homeDir = os20.homed
21211
21535
  for (const target of externalBackupTargets(homeDir)) {
21212
21536
  if (!existsSync36(target.path)) continue;
21213
21537
  await mkdir7(externalDir, { recursive: true });
21214
- await copyFile(target.path, path42.join(externalDir, target.name));
21538
+ await copyFile2(target.path, path42.join(externalDir, target.name));
21215
21539
  externalFiles.push(target);
21216
21540
  }
21217
21541
  const manifest = {
@@ -21246,14 +21570,14 @@ async function restoreFromBackup(dataDir2) {
21246
21570
  if (stat2.isDirectory()) {
21247
21571
  await cp(src, dest, { recursive: true, force: true });
21248
21572
  } else {
21249
- await copyFile(src, dest);
21573
+ await copyFile2(src, dest);
21250
21574
  }
21251
21575
  }
21252
21576
  for (const external of manifest.externalFiles ?? []) {
21253
21577
  const src = path42.join(backupDir, "external", external.name);
21254
21578
  if (!existsSync36(src)) continue;
21255
21579
  await mkdir7(path42.dirname(external.path), { recursive: true });
21256
- await copyFile(src, external.path);
21580
+ await copyFile2(src, external.path);
21257
21581
  }
21258
21582
  return manifest;
21259
21583
  }
@@ -32121,6 +32445,33 @@ var init_dangerous_patterns = __esm({
32121
32445
  regex: /\bkill\s+-9\b/,
32122
32446
  severity: "warning",
32123
32447
  reason: "Force kill signal"
32448
+ },
32449
+ // MCP bypass — agents must use MCP tools, never access the DB directly.
32450
+ // These patterns catch attempts to work around a disconnected MCP server.
32451
+ {
32452
+ regex: /\bsqlite3\b.*\bmemories\.db\b/,
32453
+ severity: "critical",
32454
+ reason: "Direct SQLite access bypasses MCP contract boundary \u2014 use MCP tools"
32455
+ },
32456
+ {
32457
+ regex: /\bsqlite3\b.*\.exe-os\b/,
32458
+ severity: "critical",
32459
+ reason: "Direct SQLite access to exe-os database \u2014 use MCP tools"
32460
+ },
32461
+ {
32462
+ regex: /\bnode\s+-e\b.*\b(better-sqlite3|libsql|sqlite3)\b/,
32463
+ severity: "critical",
32464
+ reason: "Inline Node.js script accessing SQLite directly \u2014 use MCP tools"
32465
+ },
32466
+ {
32467
+ regex: /\brequire\s*\(\s*['"].*memories\.db['"]\s*\)/,
32468
+ severity: "critical",
32469
+ reason: "Direct require of memories database \u2014 use MCP tools"
32470
+ },
32471
+ {
32472
+ regex: /\bcat\b.*\bmemories\.db\b/,
32473
+ severity: "warning",
32474
+ reason: "Reading raw database file \u2014 encrypted data, use MCP tools instead"
32124
32475
  }
32125
32476
  ];
32126
32477
  }
@@ -35693,7 +36044,7 @@ function agentColor(agentId) {
35693
36044
  return "#F0EDE8";
35694
36045
  }
35695
36046
  }
35696
- function formatTimeAgo(iso) {
36047
+ function formatTimeAgo2(iso) {
35697
36048
  const diff2 = Date.now() - new Date(iso).getTime();
35698
36049
  const hours = Math.floor(diff2 / 36e5);
35699
36050
  if (hours < 1) return "just now";
@@ -36019,7 +36370,7 @@ function WikiView({ onBack }) {
36019
36370
  result.rawText.length > 80 ? "..." : ""
36020
36371
  ] }),
36021
36372
  " ",
36022
- /* @__PURE__ */ jsx13(Text, { color: isSelected ? "#F5D76E" : "#3D3660", dimColor: !isSelected, children: formatTimeAgo(result.timestamp) })
36373
+ /* @__PURE__ */ jsx13(Text, { color: isSelected ? "#F5D76E" : "#3D3660", dimColor: !isSelected, children: formatTimeAgo2(result.timestamp) })
36023
36374
  ]
36024
36375
  }
36025
36376
  ),