@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/index.js CHANGED
@@ -154,6 +154,17 @@ function normalizeOrchestration(raw) {
154
154
  const userOrg = raw.orchestration ?? {};
155
155
  raw.orchestration = { ...defaultOrg, ...userOrg };
156
156
  }
157
+ function normalizeCloudEndpoint(raw) {
158
+ const cloud = raw.cloud;
159
+ if (!cloud?.endpoint) return;
160
+ const ep = String(cloud.endpoint);
161
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
162
+ cloud.endpoint = "https://cloud.askexe.com";
163
+ process.stderr.write(
164
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
165
+ );
166
+ }
167
+ }
157
168
  async function loadConfig() {
158
169
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
159
170
  await ensurePrivateDir(dir);
@@ -179,6 +190,7 @@ async function loadConfig() {
179
190
  normalizeSessionLifecycle(migratedCfg);
180
191
  normalizeAutoUpdate(migratedCfg);
181
192
  normalizeOrchestration(migratedCfg);
193
+ normalizeCloudEndpoint(migratedCfg);
182
194
  const config2 = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
183
195
  if (config2.dbPath.startsWith("~")) {
184
196
  config2.dbPath = config2.dbPath.replace(/^~/, os2.homedir());
@@ -207,6 +219,7 @@ function loadConfigSync() {
207
219
  normalizeSessionLifecycle(migratedCfg);
208
220
  normalizeAutoUpdate(migratedCfg);
209
221
  normalizeOrchestration(migratedCfg);
222
+ normalizeCloudEndpoint(migratedCfg);
210
223
  const config2 = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
211
224
  if (config2.dbPath.startsWith("~")) {
212
225
  config2.dbPath = config2.dbPath.replace(/^~/, os2.homedir());
@@ -367,6 +380,7 @@ __export(agent_config_exports, {
367
380
  clearAgentRuntime: () => clearAgentRuntime,
368
381
  getAgentRuntime: () => getAgentRuntime,
369
382
  loadAgentConfig: () => loadAgentConfig,
383
+ normalizeCcModelName: () => normalizeCcModelName,
370
384
  saveAgentConfig: () => saveAgentConfig,
371
385
  setAgentMcps: () => setAgentMcps,
372
386
  setAgentRuntime: () => setAgentRuntime
@@ -395,6 +409,13 @@ function getAgentRuntime(agentId) {
395
409
  if (orgDefault) return orgDefault;
396
410
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
397
411
  }
412
+ function normalizeCcModelName(model) {
413
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
414
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
415
+ ccModel += "[1m]";
416
+ }
417
+ return ccModel;
418
+ }
398
419
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
399
420
  const knownModels = KNOWN_RUNTIMES[runtime];
400
421
  if (!knownModels) {
@@ -1060,6 +1081,7 @@ var init_provider_table = __esm({
1060
1081
  // src/lib/intercom-queue.ts
1061
1082
  var intercom_queue_exports = {};
1062
1083
  __export(intercom_queue_exports, {
1084
+ _resetDrainGuard: () => _resetDrainGuard,
1063
1085
  clearQueueForAgent: () => clearQueueForAgent,
1064
1086
  drainForSession: () => drainForSession,
1065
1087
  drainQueue: () => drainQueue,
@@ -1105,38 +1127,47 @@ function queueIntercom(targetSession, reason) {
1105
1127
  writeQueue(queue);
1106
1128
  }
1107
1129
  function drainQueue(isSessionBusy2, sendKeys) {
1130
+ if (_draining) {
1131
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
1132
+ return { drained: 0, failed: 0 };
1133
+ }
1108
1134
  const queue = readQueue();
1109
1135
  if (queue.length === 0) return { drained: 0, failed: 0 };
1136
+ _draining = true;
1110
1137
  const remaining = [];
1111
1138
  let drained = 0;
1112
1139
  let failed = 0;
1113
- for (const item of queue) {
1114
- const age = Date.now() - new Date(item.queuedAt).getTime();
1115
- if (age > TTL_MS) {
1116
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
1117
- failed++;
1118
- continue;
1119
- }
1120
- try {
1121
- if (!isSessionBusy2(item.targetSession)) {
1122
- const success = sendKeys(item.targetSession);
1123
- if (success) {
1124
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
1125
- drained++;
1126
- continue;
1140
+ try {
1141
+ for (const item of queue) {
1142
+ const age = Date.now() - new Date(item.queuedAt).getTime();
1143
+ if (age > TTL_MS) {
1144
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
1145
+ failed++;
1146
+ continue;
1147
+ }
1148
+ try {
1149
+ if (!isSessionBusy2(item.targetSession)) {
1150
+ const success = sendKeys(item.targetSession);
1151
+ if (success) {
1152
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
1153
+ drained++;
1154
+ continue;
1155
+ }
1127
1156
  }
1157
+ } catch {
1128
1158
  }
1129
- } catch {
1130
- }
1131
- item.attempts++;
1132
- if (item.attempts >= MAX_RETRIES) {
1133
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
1134
- failed++;
1135
- continue;
1159
+ item.attempts++;
1160
+ if (item.attempts >= MAX_RETRIES) {
1161
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
1162
+ failed++;
1163
+ continue;
1164
+ }
1165
+ remaining.push(item);
1136
1166
  }
1137
- remaining.push(item);
1167
+ writeQueue(remaining);
1168
+ } finally {
1169
+ _draining = false;
1138
1170
  }
1139
- writeQueue(remaining);
1140
1171
  return { drained, failed };
1141
1172
  }
1142
1173
  function drainForSession(targetSession, sendKeys) {
@@ -1161,6 +1192,9 @@ function clearQueueForAgent(agentName) {
1161
1192
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
1162
1193
  }
1163
1194
  }
1195
+ function _resetDrainGuard() {
1196
+ _draining = false;
1197
+ }
1164
1198
  function logQueue(msg) {
1165
1199
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
1166
1200
  `;
@@ -1172,13 +1206,14 @@ function logQueue(msg) {
1172
1206
  } catch {
1173
1207
  }
1174
1208
  }
1175
- var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
1209
+ var QUEUE_PATH, MAX_RETRIES, TTL_MS, _draining, INTERCOM_LOG;
1176
1210
  var init_intercom_queue = __esm({
1177
1211
  "src/lib/intercom-queue.ts"() {
1178
1212
  "use strict";
1179
1213
  QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
1180
1214
  MAX_RETRIES = 5;
1181
1215
  TTL_MS = 60 * 60 * 1e3;
1216
+ _draining = false;
1182
1217
  INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
1183
1218
  }
1184
1219
  });
@@ -4788,6 +4823,17 @@ var init_task_scope = __esm({
4788
4823
  });
4789
4824
 
4790
4825
  // src/lib/notifications.ts
4826
+ var notifications_exports = {};
4827
+ __export(notifications_exports, {
4828
+ cleanupOldNotifications: () => cleanupOldNotifications,
4829
+ formatNotifications: () => formatNotifications,
4830
+ markAsRead: () => markAsRead,
4831
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
4832
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
4833
+ migrateJsonNotifications: () => migrateJsonNotifications,
4834
+ readUnreadNotifications: () => readUnreadNotifications,
4835
+ writeNotification: () => writeNotification
4836
+ });
4791
4837
  import crypto2 from "crypto";
4792
4838
  import path13 from "path";
4793
4839
  import os10 from "os";
@@ -4824,6 +4870,52 @@ async function writeNotification(notification) {
4824
4870
  `);
4825
4871
  }
4826
4872
  }
4873
+ async function readUnreadNotifications(agentFilter, sessionScope) {
4874
+ try {
4875
+ const client = getClient();
4876
+ const conditions = ["read = 0"];
4877
+ const args = [];
4878
+ const scope = strictSessionScopeFilter(sessionScope);
4879
+ if (agentFilter) {
4880
+ conditions.push("agent_id = ?");
4881
+ args.push(agentFilter);
4882
+ }
4883
+ const result = await client.execute({
4884
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
4885
+ FROM notifications
4886
+ WHERE ${conditions.join(" AND ")}${scope.sql}
4887
+ ORDER BY created_at ASC`,
4888
+ args: [...args, ...scope.args]
4889
+ });
4890
+ return result.rows.map((r) => ({
4891
+ id: String(r.id),
4892
+ agentId: String(r.agent_id),
4893
+ agentRole: String(r.agent_role),
4894
+ event: String(r.event),
4895
+ project: String(r.project),
4896
+ summary: String(r.summary),
4897
+ taskFile: r.task_file ? String(r.task_file) : void 0,
4898
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
4899
+ timestamp: String(r.created_at),
4900
+ read: false
4901
+ }));
4902
+ } catch {
4903
+ return [];
4904
+ }
4905
+ }
4906
+ async function markAsRead(ids, sessionScope) {
4907
+ if (ids.length === 0) return;
4908
+ try {
4909
+ const client = getClient();
4910
+ const placeholders = ids.map(() => "?").join(", ");
4911
+ const scope = strictSessionScopeFilter(sessionScope);
4912
+ await client.execute({
4913
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
4914
+ args: [...ids, ...scope.args]
4915
+ });
4916
+ } catch {
4917
+ }
4918
+ }
4827
4919
  async function markAsReadByTaskFile(taskFile, sessionScope) {
4828
4920
  try {
4829
4921
  const client = getClient();
@@ -4836,11 +4928,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
4836
4928
  } catch {
4837
4929
  }
4838
4930
  }
4931
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
4932
+ try {
4933
+ const client = getClient();
4934
+ const cutoff = new Date(
4935
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
4936
+ ).toISOString();
4937
+ const scope = strictSessionScopeFilter(sessionScope);
4938
+ const result = await client.execute({
4939
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
4940
+ args: [cutoff, ...scope.args]
4941
+ });
4942
+ return result.rowsAffected;
4943
+ } catch {
4944
+ return 0;
4945
+ }
4946
+ }
4947
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
4948
+ try {
4949
+ const client = getClient();
4950
+ const scope = strictSessionScopeFilter(sessionScope);
4951
+ const result = await client.execute({
4952
+ sql: `UPDATE notifications SET read = 1
4953
+ WHERE read = 0
4954
+ AND task_file IS NOT NULL
4955
+ ${scope.sql}
4956
+ AND task_file IN (
4957
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
4958
+ )`,
4959
+ args: [...scope.args, ...scope.args]
4960
+ });
4961
+ return result.rowsAffected;
4962
+ } catch {
4963
+ return 0;
4964
+ }
4965
+ }
4966
+ function formatNotifications(notifications) {
4967
+ if (notifications.length === 0) return "";
4968
+ const grouped = /* @__PURE__ */ new Map();
4969
+ for (const n of notifications) {
4970
+ const key = `${n.agentId}|${n.agentRole}`;
4971
+ if (!grouped.has(key)) grouped.set(key, []);
4972
+ grouped.get(key).push(n);
4973
+ }
4974
+ const lines = [];
4975
+ lines.push(`## Notifications (${notifications.length} unread)
4976
+ `);
4977
+ for (const [key, items] of grouped) {
4978
+ const [agentId, agentRole] = key.split("|");
4979
+ lines.push(`**${agentId}** (${agentRole}):`);
4980
+ for (const item of items) {
4981
+ const ago = formatTimeAgo(item.timestamp);
4982
+ const icon = eventIcon(item.event);
4983
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
4984
+ }
4985
+ lines.push("");
4986
+ }
4987
+ return lines.join("\n");
4988
+ }
4989
+ async function migrateJsonNotifications() {
4990
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path13.join(os10.homedir(), ".exe-os");
4991
+ const notifDir = path13.join(base, "notifications");
4992
+ if (!existsSync13(notifDir)) return 0;
4993
+ let migrated = 0;
4994
+ try {
4995
+ const files = readdirSync(notifDir).filter((f) => f.endsWith(".json"));
4996
+ if (files.length === 0) return 0;
4997
+ const client = getClient();
4998
+ for (const file of files) {
4999
+ try {
5000
+ const filePath = path13.join(notifDir, file);
5001
+ const data = JSON.parse(readFileSync10(filePath, "utf8"));
5002
+ await client.execute({
5003
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
5004
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5005
+ args: [
5006
+ crypto2.randomUUID(),
5007
+ data.agentId ?? "unknown",
5008
+ data.agentRole ?? "unknown",
5009
+ data.event ?? "session_summary",
5010
+ data.project ?? "unknown",
5011
+ data.summary ?? "",
5012
+ data.taskFile ?? null,
5013
+ null,
5014
+ data.read ? 1 : 0,
5015
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
5016
+ ]
5017
+ });
5018
+ unlinkSync4(filePath);
5019
+ migrated++;
5020
+ } catch {
5021
+ }
5022
+ }
5023
+ try {
5024
+ const remaining = readdirSync(notifDir);
5025
+ if (remaining.length === 0) {
5026
+ rmdirSync(notifDir);
5027
+ }
5028
+ } catch {
5029
+ }
5030
+ } catch {
5031
+ }
5032
+ return migrated;
5033
+ }
5034
+ function eventIcon(event) {
5035
+ switch (event) {
5036
+ case "task_complete":
5037
+ return "Completed:";
5038
+ case "task_needs_fix":
5039
+ return "Needs fix:";
5040
+ case "session_summary":
5041
+ return "Session:";
5042
+ case "error_spike":
5043
+ return "Errors:";
5044
+ case "orphan_task":
5045
+ return "Orphan:";
5046
+ case "subtasks_complete":
5047
+ return "Subtasks done:";
5048
+ case "capacity_relaunch":
5049
+ return "Relaunched:";
5050
+ }
5051
+ }
5052
+ function formatTimeAgo(timestamp) {
5053
+ const diffMs = Date.now() - new Date(timestamp).getTime();
5054
+ const mins = Math.floor(diffMs / 6e4);
5055
+ if (mins < 1) return "just now";
5056
+ if (mins < 60) return `${mins}m ago`;
5057
+ const hours = Math.floor(mins / 60);
5058
+ if (hours < 24) return `${hours}h ago`;
5059
+ const days = Math.floor(hours / 24);
5060
+ return `${days}d ago`;
5061
+ }
5062
+ var CLEANUP_DAYS;
4839
5063
  var init_notifications = __esm({
4840
5064
  "src/lib/notifications.ts"() {
4841
5065
  "use strict";
4842
5066
  init_database();
4843
5067
  init_task_scope();
5068
+ CLEANUP_DAYS = 7;
4844
5069
  }
4845
5070
  });
4846
5071
 
@@ -5886,7 +6111,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5886
6111
  taskFile
5887
6112
  });
5888
6113
  const originalPriority = String(row.priority).toLowerCase();
5889
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
6114
+ const resultLower = result?.toLowerCase() ?? "";
6115
+ const hasTestEvidence = (
6116
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
6117
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
6118
+ );
6119
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
6120
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
5890
6121
  if (!autoApprove) {
5891
6122
  try {
5892
6123
  const key = getSessionKey();
@@ -5894,6 +6125,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5894
6125
  if (exeSession) {
5895
6126
  sendIntercom(exeSession);
5896
6127
  }
6128
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
6129
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6130
+ if (exeSession) {
6131
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
6132
+ sendIntercom(reviewerSession);
6133
+ }
6134
+ }
5897
6135
  } catch {
5898
6136
  }
5899
6137
  }
@@ -6746,6 +6984,20 @@ async function updateTask(input) {
6746
6984
  notifyTaskDone();
6747
6985
  }
6748
6986
  await markTaskNotificationsRead(taskFile);
6987
+ if (input.status === "needs_review" && !isCoordinator) {
6988
+ try {
6989
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
6990
+ await writeNotification2({
6991
+ agentId: String(row.assigned_to),
6992
+ agentRole: String(row.assigned_to),
6993
+ event: "task_complete",
6994
+ project: String(row.project_name),
6995
+ summary: `"${String(row.title)}" is ready for review`,
6996
+ taskFile
6997
+ });
6998
+ } catch {
6999
+ }
7000
+ }
6749
7001
  if (input.status === "done" || input.status === "closed") {
6750
7002
  try {
6751
7003
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -7178,18 +7430,31 @@ function acquireSpawnLock2(sessionName) {
7178
7430
  mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
7179
7431
  }
7180
7432
  const lockFile = spawnLockPath(sessionName);
7181
- if (existsSync16(lockFile)) {
7182
- try {
7183
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7184
- const age = Date.now() - lock.timestamp;
7185
- if (isProcessAlive(lock.pid) && age < 6e4) {
7186
- return false;
7187
- }
7188
- } catch {
7433
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
7434
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
7435
+ const { constants } = __require("fs");
7436
+ try {
7437
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
7438
+ writeSync(fd, lockData);
7439
+ closeSync3(fd);
7440
+ return true;
7441
+ } catch (err) {
7442
+ if (err?.code !== "EEXIST") {
7443
+ return true;
7189
7444
  }
7190
7445
  }
7191
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
7192
- return true;
7446
+ try {
7447
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7448
+ const age = Date.now() - lock.timestamp;
7449
+ if (isProcessAlive(lock.pid) && age < 6e4) {
7450
+ return false;
7451
+ }
7452
+ writeFileSync8(lockFile, lockData);
7453
+ return true;
7454
+ } catch {
7455
+ writeFileSync8(lockFile, lockData);
7456
+ return true;
7457
+ }
7193
7458
  }
7194
7459
  function releaseSpawnLock2(sessionName) {
7195
7460
  try {
@@ -7268,6 +7533,21 @@ function parseParentExe(sessionName, agentId) {
7268
7533
  function extractRootExe(name) {
7269
7534
  if (!name) return null;
7270
7535
  if (!name.includes("-")) return name;
7536
+ try {
7537
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
7538
+ if (roster.length > 0) {
7539
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
7540
+ for (const agentName of sortedNames) {
7541
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7542
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
7543
+ const match = name.match(regex);
7544
+ if (match) {
7545
+ return extractRootExe(match[1]);
7546
+ }
7547
+ }
7548
+ }
7549
+ } catch {
7550
+ }
7271
7551
  const parts = name.split("-").filter(Boolean);
7272
7552
  return parts.length > 0 ? parts[parts.length - 1] : null;
7273
7553
  }
@@ -7286,6 +7566,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7286
7566
  function getParentExe(sessionKey) {
7287
7567
  try {
7288
7568
  const data = JSON.parse(readFileSync12(path19.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7569
+ if (data.registeredAt) {
7570
+ const age = Date.now() - new Date(data.registeredAt).getTime();
7571
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
7572
+ }
7289
7573
  return data.parentExe || null;
7290
7574
  } catch {
7291
7575
  return null;
@@ -7834,7 +8118,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7834
8118
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7835
8119
  } catch {
7836
8120
  }
7837
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
8121
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
7838
8122
  if (ccProvider !== DEFAULT_PROVIDER) {
7839
8123
  const cfg = PROVIDER_TABLE[ccProvider];
7840
8124
  if (cfg?.apiKeyEnv) {
@@ -7869,10 +8153,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7869
8153
  }
7870
8154
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
7871
8155
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
7872
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
7873
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
7874
- ccModel += "[1m]";
7875
- }
8156
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
8157
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
7876
8158
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
7877
8159
  }
7878
8160
  }
@@ -7973,7 +8255,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7973
8255
  releaseSpawnLock2(sessionName);
7974
8256
  return { sessionName };
7975
8257
  }
7976
- 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;
8258
+ 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;
7977
8259
  var init_tmux_routing = __esm({
7978
8260
  "src/lib/tmux-routing.ts"() {
7979
8261
  "use strict";
@@ -7993,6 +8275,7 @@ var init_tmux_routing = __esm({
7993
8275
  SESSION_CACHE = path19.join(os12.homedir(), ".exe-os", "session-cache");
7994
8276
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7995
8277
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8278
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
7996
8279
  VERIFY_PANE_LINES = 200;
7997
8280
  INTERCOM_DEBOUNCE_MS = 3e4;
7998
8281
  CODEX_DEBOUNCE_MS = 12e4;
@@ -8004,7 +8287,7 @@ var init_tmux_routing = __esm({
8004
8287
  });
8005
8288
 
8006
8289
  // src/lib/keychain.ts
8007
- import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
8290
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2, rename, copyFile } from "fs/promises";
8008
8291
  import { existsSync as existsSync17, statSync as statSync3 } from "fs";
8009
8292
  import { execSync as execSync9 } from "child_process";
8010
8293
  import path20 from "path";
@@ -8039,12 +8322,14 @@ function linuxSecretAvailable() {
8039
8322
  function isRootOnlyTrustedServerKeyFile(keyPath) {
8040
8323
  if (process.platform !== "linux") return false;
8041
8324
  try {
8042
- const uid = typeof os13.userInfo().uid === "number" ? os13.userInfo().uid : -1;
8043
8325
  const st = statSync3(keyPath);
8044
8326
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
8327
+ const uid = typeof os13.userInfo().uid === "number" ? os13.userInfo().uid : -1;
8045
8328
  if (uid === 0) return true;
8046
8329
  const exeOsDir = process.env.EXE_OS_DIR;
8047
- return Boolean(exeOsDir && path20.resolve(keyPath).startsWith(path20.resolve(exeOsDir) + path20.sep));
8330
+ if (exeOsDir && path20.resolve(keyPath).startsWith(path20.resolve(exeOsDir) + path20.sep)) return true;
8331
+ if (!linuxSecretAvailable()) return true;
8332
+ return false;
8048
8333
  } catch {
8049
8334
  return false;
8050
8335
  }
@@ -8194,15 +8479,25 @@ async function writeMachineBoundFileFallback(b64) {
8194
8479
  await mkdir4(dir, { recursive: true });
8195
8480
  const keyPath = getKeyPath();
8196
8481
  const machineKey = deriveMachineKey();
8197
- if (machineKey) {
8198
- const encrypted = encryptWithMachineKey(b64, machineKey);
8199
- await writeFile5(keyPath, encrypted + "\n", "utf-8");
8200
- await chmod2(keyPath, 384);
8201
- return "encrypted";
8482
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
8483
+ const result = machineKey ? "encrypted" : "plaintext";
8484
+ const tmpPath = keyPath + ".tmp";
8485
+ try {
8486
+ if (existsSync17(keyPath)) {
8487
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
8488
+ });
8489
+ }
8490
+ await writeFile5(tmpPath, content, "utf-8");
8491
+ await chmod2(tmpPath, 384);
8492
+ await rename(tmpPath, keyPath);
8493
+ } catch (err) {
8494
+ try {
8495
+ await unlink(tmpPath);
8496
+ } catch {
8497
+ }
8498
+ throw err;
8202
8499
  }
8203
- await writeFile5(keyPath, b64 + "\n", "utf-8");
8204
- await chmod2(keyPath, 384);
8205
- return "plaintext";
8500
+ return result;
8206
8501
  }
8207
8502
  async function getMasterKey() {
8208
8503
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -8269,7 +8564,7 @@ async function getMasterKey() {
8269
8564
  b64Value = content;
8270
8565
  }
8271
8566
  const key = Buffer.from(b64Value, "base64");
8272
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
8567
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
8273
8568
  return key;
8274
8569
  }
8275
8570
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
@@ -13555,6 +13850,33 @@ var DANGEROUS_PATTERNS = [
13555
13850
  regex: /\bkill\s+-9\b/,
13556
13851
  severity: "warning",
13557
13852
  reason: "Force kill signal"
13853
+ },
13854
+ // MCP bypass — agents must use MCP tools, never access the DB directly.
13855
+ // These patterns catch attempts to work around a disconnected MCP server.
13856
+ {
13857
+ regex: /\bsqlite3\b.*\bmemories\.db\b/,
13858
+ severity: "critical",
13859
+ reason: "Direct SQLite access bypasses MCP contract boundary \u2014 use MCP tools"
13860
+ },
13861
+ {
13862
+ regex: /\bsqlite3\b.*\.exe-os\b/,
13863
+ severity: "critical",
13864
+ reason: "Direct SQLite access to exe-os database \u2014 use MCP tools"
13865
+ },
13866
+ {
13867
+ regex: /\bnode\s+-e\b.*\b(better-sqlite3|libsql|sqlite3)\b/,
13868
+ severity: "critical",
13869
+ reason: "Inline Node.js script accessing SQLite directly \u2014 use MCP tools"
13870
+ },
13871
+ {
13872
+ regex: /\brequire\s*\(\s*['"].*memories\.db['"]\s*\)/,
13873
+ severity: "critical",
13874
+ reason: "Direct require of memories database \u2014 use MCP tools"
13875
+ },
13876
+ {
13877
+ regex: /\bcat\b.*\bmemories\.db\b/,
13878
+ severity: "warning",
13879
+ reason: "Reading raw database file \u2014 encrypted data, use MCP tools instead"
13558
13880
  }
13559
13881
  ];
13560
13882
  function checkDangerousPatterns(command) {
@@ -175,6 +175,13 @@ function getAgentRuntime(agentId) {
175
175
  if (orgDefault) return orgDefault;
176
176
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
177
177
  }
178
+ function normalizeCcModelName(model) {
179
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
180
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
181
+ ccModel += "[1m]";
182
+ }
183
+ return ccModel;
184
+ }
178
185
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
179
186
  const knownModels = KNOWN_RUNTIMES[runtime];
180
187
  if (!knownModels) {
@@ -223,6 +230,7 @@ export {
223
230
  clearAgentRuntime,
224
231
  getAgentRuntime,
225
232
  loadAgentConfig,
233
+ normalizeCcModelName,
226
234
  saveAgentConfig,
227
235
  setAgentMcps,
228
236
  setAgentRuntime