@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/tui/App.js CHANGED
@@ -505,6 +505,17 @@ function normalizeOrchestration(raw) {
505
505
  const userOrg = raw.orchestration ?? {};
506
506
  raw.orchestration = { ...defaultOrg, ...userOrg };
507
507
  }
508
+ function normalizeCloudEndpoint(raw) {
509
+ const cloud = raw.cloud;
510
+ if (!cloud?.endpoint) return;
511
+ const ep = String(cloud.endpoint);
512
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
513
+ cloud.endpoint = "https://cloud.askexe.com";
514
+ process.stderr.write(
515
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
516
+ );
517
+ }
518
+ }
508
519
  async function loadConfig() {
509
520
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
510
521
  await ensurePrivateDir(dir);
@@ -530,6 +541,7 @@ async function loadConfig() {
530
541
  normalizeSessionLifecycle(migratedCfg);
531
542
  normalizeAutoUpdate(migratedCfg);
532
543
  normalizeOrchestration(migratedCfg);
544
+ normalizeCloudEndpoint(migratedCfg);
533
545
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
534
546
  if (config.dbPath.startsWith("~")) {
535
547
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -558,6 +570,7 @@ function loadConfigSync() {
558
570
  normalizeSessionLifecycle(migratedCfg);
559
571
  normalizeAutoUpdate(migratedCfg);
560
572
  normalizeOrchestration(migratedCfg);
573
+ normalizeCloudEndpoint(migratedCfg);
561
574
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
562
575
  if (config.dbPath.startsWith("~")) {
563
576
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -718,6 +731,7 @@ __export(agent_config_exports, {
718
731
  clearAgentRuntime: () => clearAgentRuntime,
719
732
  getAgentRuntime: () => getAgentRuntime,
720
733
  loadAgentConfig: () => loadAgentConfig,
734
+ normalizeCcModelName: () => normalizeCcModelName,
721
735
  saveAgentConfig: () => saveAgentConfig,
722
736
  setAgentMcps: () => setAgentMcps,
723
737
  setAgentRuntime: () => setAgentRuntime
@@ -746,6 +760,13 @@ function getAgentRuntime(agentId) {
746
760
  if (orgDefault) return orgDefault;
747
761
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
748
762
  }
763
+ function normalizeCcModelName(model) {
764
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
765
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
766
+ ccModel += "[1m]";
767
+ }
768
+ return ccModel;
769
+ }
749
770
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
750
771
  const knownModels = KNOWN_RUNTIMES[runtime];
751
772
  if (!knownModels) {
@@ -815,6 +836,7 @@ var init_agent_config = __esm({
815
836
  // src/lib/intercom-queue.ts
816
837
  var intercom_queue_exports = {};
817
838
  __export(intercom_queue_exports, {
839
+ _resetDrainGuard: () => _resetDrainGuard,
818
840
  clearQueueForAgent: () => clearQueueForAgent,
819
841
  drainForSession: () => drainForSession,
820
842
  drainQueue: () => drainQueue,
@@ -860,38 +882,47 @@ function queueIntercom(targetSession, reason) {
860
882
  writeQueue(queue);
861
883
  }
862
884
  function drainQueue(isSessionBusy2, sendKeys) {
885
+ if (_draining) {
886
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
887
+ return { drained: 0, failed: 0 };
888
+ }
863
889
  const queue = readQueue();
864
890
  if (queue.length === 0) return { drained: 0, failed: 0 };
891
+ _draining = true;
865
892
  const remaining = [];
866
893
  let drained = 0;
867
894
  let failed = 0;
868
- for (const item of queue) {
869
- const age = Date.now() - new Date(item.queuedAt).getTime();
870
- if (age > TTL_MS) {
871
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
872
- failed++;
873
- continue;
874
- }
875
- try {
876
- if (!isSessionBusy2(item.targetSession)) {
877
- const success = sendKeys(item.targetSession);
878
- if (success) {
879
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
880
- drained++;
881
- continue;
895
+ try {
896
+ for (const item of queue) {
897
+ const age = Date.now() - new Date(item.queuedAt).getTime();
898
+ if (age > TTL_MS) {
899
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
900
+ failed++;
901
+ continue;
902
+ }
903
+ try {
904
+ if (!isSessionBusy2(item.targetSession)) {
905
+ const success = sendKeys(item.targetSession);
906
+ if (success) {
907
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
908
+ drained++;
909
+ continue;
910
+ }
882
911
  }
912
+ } catch {
883
913
  }
884
- } catch {
885
- }
886
- item.attempts++;
887
- if (item.attempts >= MAX_RETRIES) {
888
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
889
- failed++;
890
- continue;
914
+ item.attempts++;
915
+ if (item.attempts >= MAX_RETRIES) {
916
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
917
+ failed++;
918
+ continue;
919
+ }
920
+ remaining.push(item);
891
921
  }
892
- remaining.push(item);
922
+ writeQueue(remaining);
923
+ } finally {
924
+ _draining = false;
893
925
  }
894
- writeQueue(remaining);
895
926
  return { drained, failed };
896
927
  }
897
928
  function drainForSession(targetSession, sendKeys) {
@@ -916,6 +947,9 @@ function clearQueueForAgent(agentName) {
916
947
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
917
948
  }
918
949
  }
950
+ function _resetDrainGuard() {
951
+ _draining = false;
952
+ }
919
953
  function logQueue(msg) {
920
954
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
921
955
  `;
@@ -927,13 +961,14 @@ function logQueue(msg) {
927
961
  } catch {
928
962
  }
929
963
  }
930
- var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
964
+ var QUEUE_PATH, MAX_RETRIES, TTL_MS, _draining, INTERCOM_LOG;
931
965
  var init_intercom_queue = __esm({
932
966
  "src/lib/intercom-queue.ts"() {
933
967
  "use strict";
934
968
  QUEUE_PATH = path4.join(os3.homedir(), ".exe-os", "intercom-queue.json");
935
969
  MAX_RETRIES = 5;
936
970
  TTL_MS = 60 * 60 * 1e3;
971
+ _draining = false;
937
972
  INTERCOM_LOG = path4.join(os3.homedir(), ".exe-os", "intercom.log");
938
973
  }
939
974
  });
@@ -4761,6 +4796,17 @@ var init_agent_symlinks = __esm({
4761
4796
  });
4762
4797
 
4763
4798
  // src/lib/notifications.ts
4799
+ var notifications_exports = {};
4800
+ __export(notifications_exports, {
4801
+ cleanupOldNotifications: () => cleanupOldNotifications,
4802
+ formatNotifications: () => formatNotifications,
4803
+ markAsRead: () => markAsRead,
4804
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
4805
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
4806
+ migrateJsonNotifications: () => migrateJsonNotifications,
4807
+ readUnreadNotifications: () => readUnreadNotifications,
4808
+ writeNotification: () => writeNotification
4809
+ });
4764
4810
  import crypto2 from "crypto";
4765
4811
  import path12 from "path";
4766
4812
  import os9 from "os";
@@ -4797,6 +4843,52 @@ async function writeNotification(notification) {
4797
4843
  `);
4798
4844
  }
4799
4845
  }
4846
+ async function readUnreadNotifications(agentFilter, sessionScope) {
4847
+ try {
4848
+ const client = getClient();
4849
+ const conditions = ["read = 0"];
4850
+ const args = [];
4851
+ const scope = strictSessionScopeFilter(sessionScope);
4852
+ if (agentFilter) {
4853
+ conditions.push("agent_id = ?");
4854
+ args.push(agentFilter);
4855
+ }
4856
+ const result = await client.execute({
4857
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
4858
+ FROM notifications
4859
+ WHERE ${conditions.join(" AND ")}${scope.sql}
4860
+ ORDER BY created_at ASC`,
4861
+ args: [...args, ...scope.args]
4862
+ });
4863
+ return result.rows.map((r) => ({
4864
+ id: String(r.id),
4865
+ agentId: String(r.agent_id),
4866
+ agentRole: String(r.agent_role),
4867
+ event: String(r.event),
4868
+ project: String(r.project),
4869
+ summary: String(r.summary),
4870
+ taskFile: r.task_file ? String(r.task_file) : void 0,
4871
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
4872
+ timestamp: String(r.created_at),
4873
+ read: false
4874
+ }));
4875
+ } catch {
4876
+ return [];
4877
+ }
4878
+ }
4879
+ async function markAsRead(ids, sessionScope) {
4880
+ if (ids.length === 0) return;
4881
+ try {
4882
+ const client = getClient();
4883
+ const placeholders = ids.map(() => "?").join(", ");
4884
+ const scope = strictSessionScopeFilter(sessionScope);
4885
+ await client.execute({
4886
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
4887
+ args: [...ids, ...scope.args]
4888
+ });
4889
+ } catch {
4890
+ }
4891
+ }
4800
4892
  async function markAsReadByTaskFile(taskFile, sessionScope) {
4801
4893
  try {
4802
4894
  const client = getClient();
@@ -4809,11 +4901,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
4809
4901
  } catch {
4810
4902
  }
4811
4903
  }
4904
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
4905
+ try {
4906
+ const client = getClient();
4907
+ const cutoff = new Date(
4908
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
4909
+ ).toISOString();
4910
+ const scope = strictSessionScopeFilter(sessionScope);
4911
+ const result = await client.execute({
4912
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
4913
+ args: [cutoff, ...scope.args]
4914
+ });
4915
+ return result.rowsAffected;
4916
+ } catch {
4917
+ return 0;
4918
+ }
4919
+ }
4920
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
4921
+ try {
4922
+ const client = getClient();
4923
+ const scope = strictSessionScopeFilter(sessionScope);
4924
+ const result = await client.execute({
4925
+ sql: `UPDATE notifications SET read = 1
4926
+ WHERE read = 0
4927
+ AND task_file IS NOT NULL
4928
+ ${scope.sql}
4929
+ AND task_file IN (
4930
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
4931
+ )`,
4932
+ args: [...scope.args, ...scope.args]
4933
+ });
4934
+ return result.rowsAffected;
4935
+ } catch {
4936
+ return 0;
4937
+ }
4938
+ }
4939
+ function formatNotifications(notifications) {
4940
+ if (notifications.length === 0) return "";
4941
+ const grouped = /* @__PURE__ */ new Map();
4942
+ for (const n of notifications) {
4943
+ const key = `${n.agentId}|${n.agentRole}`;
4944
+ if (!grouped.has(key)) grouped.set(key, []);
4945
+ grouped.get(key).push(n);
4946
+ }
4947
+ const lines = [];
4948
+ lines.push(`## Notifications (${notifications.length} unread)
4949
+ `);
4950
+ for (const [key, items] of grouped) {
4951
+ const [agentId, agentRole] = key.split("|");
4952
+ lines.push(`**${agentId}** (${agentRole}):`);
4953
+ for (const item of items) {
4954
+ const ago = formatTimeAgo(item.timestamp);
4955
+ const icon = eventIcon(item.event);
4956
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
4957
+ }
4958
+ lines.push("");
4959
+ }
4960
+ return lines.join("\n");
4961
+ }
4962
+ async function migrateJsonNotifications() {
4963
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path12.join(os9.homedir(), ".exe-os");
4964
+ const notifDir = path12.join(base, "notifications");
4965
+ if (!existsSync14(notifDir)) return 0;
4966
+ let migrated = 0;
4967
+ try {
4968
+ const files = readdirSync(notifDir).filter((f) => f.endsWith(".json"));
4969
+ if (files.length === 0) return 0;
4970
+ const client = getClient();
4971
+ for (const file of files) {
4972
+ try {
4973
+ const filePath = path12.join(notifDir, file);
4974
+ const data = JSON.parse(readFileSync11(filePath, "utf8"));
4975
+ await client.execute({
4976
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
4977
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4978
+ args: [
4979
+ crypto2.randomUUID(),
4980
+ data.agentId ?? "unknown",
4981
+ data.agentRole ?? "unknown",
4982
+ data.event ?? "session_summary",
4983
+ data.project ?? "unknown",
4984
+ data.summary ?? "",
4985
+ data.taskFile ?? null,
4986
+ null,
4987
+ data.read ? 1 : 0,
4988
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
4989
+ ]
4990
+ });
4991
+ unlinkSync4(filePath);
4992
+ migrated++;
4993
+ } catch {
4994
+ }
4995
+ }
4996
+ try {
4997
+ const remaining = readdirSync(notifDir);
4998
+ if (remaining.length === 0) {
4999
+ rmdirSync(notifDir);
5000
+ }
5001
+ } catch {
5002
+ }
5003
+ } catch {
5004
+ }
5005
+ return migrated;
5006
+ }
5007
+ function eventIcon(event) {
5008
+ switch (event) {
5009
+ case "task_complete":
5010
+ return "Completed:";
5011
+ case "task_needs_fix":
5012
+ return "Needs fix:";
5013
+ case "session_summary":
5014
+ return "Session:";
5015
+ case "error_spike":
5016
+ return "Errors:";
5017
+ case "orphan_task":
5018
+ return "Orphan:";
5019
+ case "subtasks_complete":
5020
+ return "Subtasks done:";
5021
+ case "capacity_relaunch":
5022
+ return "Relaunched:";
5023
+ }
5024
+ }
5025
+ function formatTimeAgo(timestamp) {
5026
+ const diffMs = Date.now() - new Date(timestamp).getTime();
5027
+ const mins = Math.floor(diffMs / 6e4);
5028
+ if (mins < 1) return "just now";
5029
+ if (mins < 60) return `${mins}m ago`;
5030
+ const hours = Math.floor(mins / 60);
5031
+ if (hours < 24) return `${hours}h ago`;
5032
+ const days = Math.floor(hours / 24);
5033
+ return `${days}d ago`;
5034
+ }
5035
+ var CLEANUP_DAYS;
4812
5036
  var init_notifications = __esm({
4813
5037
  "src/lib/notifications.ts"() {
4814
5038
  "use strict";
4815
5039
  init_database();
4816
5040
  init_task_scope();
5041
+ CLEANUP_DAYS = 7;
4817
5042
  }
4818
5043
  });
4819
5044
 
@@ -5875,7 +6100,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5875
6100
  taskFile
5876
6101
  });
5877
6102
  const originalPriority = String(row.priority).toLowerCase();
5878
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
6103
+ const resultLower = result?.toLowerCase() ?? "";
6104
+ const hasTestEvidence = (
6105
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
6106
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
6107
+ );
6108
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
6109
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
5879
6110
  if (!autoApprove) {
5880
6111
  try {
5881
6112
  const key = getSessionKey();
@@ -5883,6 +6114,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5883
6114
  if (exeSession) {
5884
6115
  sendIntercom(exeSession);
5885
6116
  }
6117
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
6118
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6119
+ if (exeSession) {
6120
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
6121
+ sendIntercom(reviewerSession);
6122
+ }
6123
+ }
5886
6124
  } catch {
5887
6125
  }
5888
6126
  }
@@ -6658,6 +6896,20 @@ async function updateTask(input) {
6658
6896
  notifyTaskDone();
6659
6897
  }
6660
6898
  await markTaskNotificationsRead(taskFile);
6899
+ if (input.status === "needs_review" && !isCoordinator) {
6900
+ try {
6901
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
6902
+ await writeNotification2({
6903
+ agentId: String(row.assigned_to),
6904
+ agentRole: String(row.assigned_to),
6905
+ event: "task_complete",
6906
+ project: String(row.project_name),
6907
+ summary: `"${String(row.title)}" is ready for review`,
6908
+ taskFile
6909
+ });
6910
+ } catch {
6911
+ }
6912
+ }
6661
6913
  if (input.status === "done" || input.status === "closed") {
6662
6914
  try {
6663
6915
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -7090,18 +7342,31 @@ function acquireSpawnLock2(sessionName) {
7090
7342
  mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
7091
7343
  }
7092
7344
  const lockFile = spawnLockPath(sessionName);
7093
- if (existsSync17(lockFile)) {
7094
- try {
7095
- const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
7096
- const age = Date.now() - lock.timestamp;
7097
- if (isProcessAlive(lock.pid) && age < 6e4) {
7098
- return false;
7099
- }
7100
- } catch {
7345
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
7346
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
7347
+ const { constants: constants2 } = __require("fs");
7348
+ try {
7349
+ const fd = openSync3(lockFile, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL, 420);
7350
+ writeSync(fd, lockData);
7351
+ closeSync3(fd);
7352
+ return true;
7353
+ } catch (err) {
7354
+ if (err?.code !== "EEXIST") {
7355
+ return true;
7101
7356
  }
7102
7357
  }
7103
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
7104
- return true;
7358
+ try {
7359
+ const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
7360
+ const age = Date.now() - lock.timestamp;
7361
+ if (isProcessAlive(lock.pid) && age < 6e4) {
7362
+ return false;
7363
+ }
7364
+ writeFileSync8(lockFile, lockData);
7365
+ return true;
7366
+ } catch {
7367
+ writeFileSync8(lockFile, lockData);
7368
+ return true;
7369
+ }
7105
7370
  }
7106
7371
  function releaseSpawnLock2(sessionName) {
7107
7372
  try {
@@ -7180,6 +7445,21 @@ function parseParentExe(sessionName, agentId) {
7180
7445
  function extractRootExe(name) {
7181
7446
  if (!name) return null;
7182
7447
  if (!name.includes("-")) return name;
7448
+ try {
7449
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
7450
+ if (roster.length > 0) {
7451
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
7452
+ for (const agentName of sortedNames) {
7453
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7454
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
7455
+ const match = name.match(regex);
7456
+ if (match) {
7457
+ return extractRootExe(match[1]);
7458
+ }
7459
+ }
7460
+ }
7461
+ } catch {
7462
+ }
7183
7463
  const parts = name.split("-").filter(Boolean);
7184
7464
  return parts.length > 0 ? parts[parts.length - 1] : null;
7185
7465
  }
@@ -7198,6 +7478,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7198
7478
  function getParentExe(sessionKey) {
7199
7479
  try {
7200
7480
  const data = JSON.parse(readFileSync13(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7481
+ if (data.registeredAt) {
7482
+ const age = Date.now() - new Date(data.registeredAt).getTime();
7483
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
7484
+ }
7201
7485
  return data.parentExe || null;
7202
7486
  } catch {
7203
7487
  return null;
@@ -7746,7 +8030,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7746
8030
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7747
8031
  } catch {
7748
8032
  }
7749
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
8033
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
7750
8034
  if (ccProvider !== DEFAULT_PROVIDER) {
7751
8035
  const cfg = PROVIDER_TABLE[ccProvider];
7752
8036
  if (cfg?.apiKeyEnv) {
@@ -7781,10 +8065,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7781
8065
  }
7782
8066
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
7783
8067
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
7784
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
7785
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
7786
- ccModel += "[1m]";
7787
- }
8068
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
8069
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
7788
8070
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
7789
8071
  }
7790
8072
  }
@@ -7885,7 +8167,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7885
8167
  releaseSpawnLock2(sessionName);
7886
8168
  return { sessionName };
7887
8169
  }
7888
- 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;
8170
+ 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;
7889
8171
  var init_tmux_routing = __esm({
7890
8172
  "src/lib/tmux-routing.ts"() {
7891
8173
  "use strict";
@@ -7905,6 +8187,7 @@ var init_tmux_routing = __esm({
7905
8187
  SESSION_CACHE = path18.join(os11.homedir(), ".exe-os", "session-cache");
7906
8188
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7907
8189
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8190
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
7908
8191
  VERIFY_PANE_LINES = 200;
7909
8192
  INTERCOM_DEBOUNCE_MS = 3e4;
7910
8193
  CODEX_DEBOUNCE_MS = 12e4;
@@ -10605,6 +10888,33 @@ var init_dangerous_patterns = __esm({
10605
10888
  regex: /\bkill\s+-9\b/,
10606
10889
  severity: "warning",
10607
10890
  reason: "Force kill signal"
10891
+ },
10892
+ // MCP bypass — agents must use MCP tools, never access the DB directly.
10893
+ // These patterns catch attempts to work around a disconnected MCP server.
10894
+ {
10895
+ regex: /\bsqlite3\b.*\bmemories\.db\b/,
10896
+ severity: "critical",
10897
+ reason: "Direct SQLite access bypasses MCP contract boundary \u2014 use MCP tools"
10898
+ },
10899
+ {
10900
+ regex: /\bsqlite3\b.*\.exe-os\b/,
10901
+ severity: "critical",
10902
+ reason: "Direct SQLite access to exe-os database \u2014 use MCP tools"
10903
+ },
10904
+ {
10905
+ regex: /\bnode\s+-e\b.*\b(better-sqlite3|libsql|sqlite3)\b/,
10906
+ severity: "critical",
10907
+ reason: "Inline Node.js script accessing SQLite directly \u2014 use MCP tools"
10908
+ },
10909
+ {
10910
+ regex: /\brequire\s*\(\s*['"].*memories\.db['"]\s*\)/,
10911
+ severity: "critical",
10912
+ reason: "Direct require of memories database \u2014 use MCP tools"
10913
+ },
10914
+ {
10915
+ regex: /\bcat\b.*\bmemories\.db\b/,
10916
+ severity: "warning",
10917
+ reason: "Reading raw database file \u2014 encrypted data, use MCP tools instead"
10608
10918
  }
10609
10919
  ];
10610
10920
  }
@@ -11263,7 +11573,7 @@ __export(keychain_exports, {
11263
11573
  importMnemonic: () => importMnemonic,
11264
11574
  setMasterKey: () => setMasterKey
11265
11575
  });
11266
- import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
11576
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2, rename, copyFile } from "fs/promises";
11267
11577
  import { existsSync as existsSync18, statSync as statSync3 } from "fs";
11268
11578
  import { execSync as execSync10 } from "child_process";
11269
11579
  import path27 from "path";
@@ -11298,12 +11608,14 @@ function linuxSecretAvailable() {
11298
11608
  function isRootOnlyTrustedServerKeyFile(keyPath) {
11299
11609
  if (process.platform !== "linux") return false;
11300
11610
  try {
11301
- const uid = typeof os13.userInfo().uid === "number" ? os13.userInfo().uid : -1;
11302
11611
  const st = statSync3(keyPath);
11303
11612
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
11613
+ const uid = typeof os13.userInfo().uid === "number" ? os13.userInfo().uid : -1;
11304
11614
  if (uid === 0) return true;
11305
11615
  const exeOsDir = process.env.EXE_OS_DIR;
11306
- return Boolean(exeOsDir && path27.resolve(keyPath).startsWith(path27.resolve(exeOsDir) + path27.sep));
11616
+ if (exeOsDir && path27.resolve(keyPath).startsWith(path27.resolve(exeOsDir) + path27.sep)) return true;
11617
+ if (!linuxSecretAvailable()) return true;
11618
+ return false;
11307
11619
  } catch {
11308
11620
  return false;
11309
11621
  }
@@ -11453,15 +11765,25 @@ async function writeMachineBoundFileFallback(b64) {
11453
11765
  await mkdir4(dir, { recursive: true });
11454
11766
  const keyPath = getKeyPath();
11455
11767
  const machineKey = deriveMachineKey();
11456
- if (machineKey) {
11457
- const encrypted = encryptWithMachineKey(b64, machineKey);
11458
- await writeFile5(keyPath, encrypted + "\n", "utf-8");
11459
- await chmod2(keyPath, 384);
11460
- return "encrypted";
11768
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
11769
+ const result = machineKey ? "encrypted" : "plaintext";
11770
+ const tmpPath = keyPath + ".tmp";
11771
+ try {
11772
+ if (existsSync18(keyPath)) {
11773
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
11774
+ });
11775
+ }
11776
+ await writeFile5(tmpPath, content, "utf-8");
11777
+ await chmod2(tmpPath, 384);
11778
+ await rename(tmpPath, keyPath);
11779
+ } catch (err) {
11780
+ try {
11781
+ await unlink(tmpPath);
11782
+ } catch {
11783
+ }
11784
+ throw err;
11461
11785
  }
11462
- await writeFile5(keyPath, b64 + "\n", "utf-8");
11463
- await chmod2(keyPath, 384);
11464
- return "plaintext";
11786
+ return result;
11465
11787
  }
11466
11788
  async function getMasterKey() {
11467
11789
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -11528,7 +11850,7 @@ async function getMasterKey() {
11528
11850
  b64Value = content;
11529
11851
  }
11530
11852
  const key = Buffer.from(b64Value, "base64");
11531
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
11853
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
11532
11854
  return key;
11533
11855
  }
11534
11856
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
@@ -23181,7 +23503,7 @@ function agentColor(agentId) {
23181
23503
  return "#F0EDE8";
23182
23504
  }
23183
23505
  }
23184
- function formatTimeAgo(iso) {
23506
+ function formatTimeAgo2(iso) {
23185
23507
  const diff2 = Date.now() - new Date(iso).getTime();
23186
23508
  const hours = Math.floor(diff2 / 36e5);
23187
23509
  if (hours < 1) return "just now";
@@ -23507,7 +23829,7 @@ function WikiView({ onBack }) {
23507
23829
  result.rawText.length > 80 ? "..." : ""
23508
23830
  ] }),
23509
23831
  " ",
23510
- /* @__PURE__ */ jsx13(Text, { color: isSelected ? "#F5D76E" : "#3D3660", dimColor: !isSelected, children: formatTimeAgo(result.timestamp) })
23832
+ /* @__PURE__ */ jsx13(Text, { color: isSelected ? "#F5D76E" : "#3D3660", dimColor: !isSelected, children: formatTimeAgo2(result.timestamp) })
23511
23833
  ]
23512
23834
  }
23513
23835
  ),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askexenow/exe-os",
3
- "version": "0.9.113",
3
+ "version": "0.9.115",
4
4
  "description": "AI employee operating system — persistent memory, task management, and multi-agent coordination for Claude Code.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",