@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
@@ -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 config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
183
195
  if (config.dbPath.startsWith("~")) {
184
196
  config.dbPath = config.dbPath.replace(/^~/, os.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 config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
211
224
  if (config.dbPath.startsWith("~")) {
212
225
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -435,6 +448,7 @@ __export(agent_config_exports, {
435
448
  clearAgentRuntime: () => clearAgentRuntime,
436
449
  getAgentRuntime: () => getAgentRuntime,
437
450
  loadAgentConfig: () => loadAgentConfig,
451
+ normalizeCcModelName: () => normalizeCcModelName,
438
452
  saveAgentConfig: () => saveAgentConfig,
439
453
  setAgentMcps: () => setAgentMcps,
440
454
  setAgentRuntime: () => setAgentRuntime
@@ -463,6 +477,13 @@ function getAgentRuntime(agentId) {
463
477
  if (orgDefault) return orgDefault;
464
478
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
465
479
  }
480
+ function normalizeCcModelName(model) {
481
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
482
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
483
+ ccModel += "[1m]";
484
+ }
485
+ return ccModel;
486
+ }
466
487
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
467
488
  const knownModels = KNOWN_RUNTIMES[runtime];
468
489
  if (!knownModels) {
@@ -1013,6 +1034,7 @@ var init_provider_table = __esm({
1013
1034
  // src/lib/intercom-queue.ts
1014
1035
  var intercom_queue_exports = {};
1015
1036
  __export(intercom_queue_exports, {
1037
+ _resetDrainGuard: () => _resetDrainGuard,
1016
1038
  clearQueueForAgent: () => clearQueueForAgent,
1017
1039
  drainForSession: () => drainForSession,
1018
1040
  drainQueue: () => drainQueue,
@@ -1058,38 +1080,47 @@ function queueIntercom(targetSession, reason) {
1058
1080
  writeQueue(queue);
1059
1081
  }
1060
1082
  function drainQueue(isSessionBusy2, sendKeys) {
1083
+ if (_draining) {
1084
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
1085
+ return { drained: 0, failed: 0 };
1086
+ }
1061
1087
  const queue = readQueue();
1062
1088
  if (queue.length === 0) return { drained: 0, failed: 0 };
1089
+ _draining = true;
1063
1090
  const remaining = [];
1064
1091
  let drained = 0;
1065
1092
  let failed = 0;
1066
- for (const item of queue) {
1067
- const age = Date.now() - new Date(item.queuedAt).getTime();
1068
- if (age > TTL_MS) {
1069
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
1070
- failed++;
1071
- continue;
1072
- }
1073
- try {
1074
- if (!isSessionBusy2(item.targetSession)) {
1075
- const success = sendKeys(item.targetSession);
1076
- if (success) {
1077
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
1078
- drained++;
1079
- continue;
1093
+ try {
1094
+ for (const item of queue) {
1095
+ const age = Date.now() - new Date(item.queuedAt).getTime();
1096
+ if (age > TTL_MS) {
1097
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
1098
+ failed++;
1099
+ continue;
1100
+ }
1101
+ try {
1102
+ if (!isSessionBusy2(item.targetSession)) {
1103
+ const success = sendKeys(item.targetSession);
1104
+ if (success) {
1105
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
1106
+ drained++;
1107
+ continue;
1108
+ }
1080
1109
  }
1110
+ } catch {
1081
1111
  }
1082
- } catch {
1083
- }
1084
- item.attempts++;
1085
- if (item.attempts >= MAX_RETRIES) {
1086
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
1087
- failed++;
1088
- continue;
1112
+ item.attempts++;
1113
+ if (item.attempts >= MAX_RETRIES) {
1114
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
1115
+ failed++;
1116
+ continue;
1117
+ }
1118
+ remaining.push(item);
1089
1119
  }
1090
- remaining.push(item);
1120
+ writeQueue(remaining);
1121
+ } finally {
1122
+ _draining = false;
1091
1123
  }
1092
- writeQueue(remaining);
1093
1124
  return { drained, failed };
1094
1125
  }
1095
1126
  function drainForSession(targetSession, sendKeys) {
@@ -1114,6 +1145,9 @@ function clearQueueForAgent(agentName) {
1114
1145
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
1115
1146
  }
1116
1147
  }
1148
+ function _resetDrainGuard() {
1149
+ _draining = false;
1150
+ }
1117
1151
  function logQueue(msg) {
1118
1152
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
1119
1153
  `;
@@ -1125,13 +1159,14 @@ function logQueue(msg) {
1125
1159
  } catch {
1126
1160
  }
1127
1161
  }
1128
- var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
1162
+ var QUEUE_PATH, MAX_RETRIES, TTL_MS, _draining, INTERCOM_LOG;
1129
1163
  var init_intercom_queue = __esm({
1130
1164
  "src/lib/intercom-queue.ts"() {
1131
1165
  "use strict";
1132
1166
  QUEUE_PATH = path6.join(os4.homedir(), ".exe-os", "intercom-queue.json");
1133
1167
  MAX_RETRIES = 5;
1134
1168
  TTL_MS = 60 * 60 * 1e3;
1169
+ _draining = false;
1135
1170
  INTERCOM_LOG = path6.join(os4.homedir(), ".exe-os", "intercom.log");
1136
1171
  }
1137
1172
  });
@@ -4768,6 +4803,17 @@ var init_agent_symlinks = __esm({
4768
4803
  });
4769
4804
 
4770
4805
  // src/lib/notifications.ts
4806
+ var notifications_exports = {};
4807
+ __export(notifications_exports, {
4808
+ cleanupOldNotifications: () => cleanupOldNotifications,
4809
+ formatNotifications: () => formatNotifications,
4810
+ markAsRead: () => markAsRead,
4811
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
4812
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
4813
+ migrateJsonNotifications: () => migrateJsonNotifications,
4814
+ readUnreadNotifications: () => readUnreadNotifications,
4815
+ writeNotification: () => writeNotification
4816
+ });
4771
4817
  import crypto2 from "crypto";
4772
4818
  import path13 from "path";
4773
4819
  import os9 from "os";
@@ -4804,6 +4850,52 @@ async function writeNotification(notification) {
4804
4850
  `);
4805
4851
  }
4806
4852
  }
4853
+ async function readUnreadNotifications(agentFilter, sessionScope) {
4854
+ try {
4855
+ const client = getClient();
4856
+ const conditions = ["read = 0"];
4857
+ const args = [];
4858
+ const scope = strictSessionScopeFilter(sessionScope);
4859
+ if (agentFilter) {
4860
+ conditions.push("agent_id = ?");
4861
+ args.push(agentFilter);
4862
+ }
4863
+ const result = await client.execute({
4864
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
4865
+ FROM notifications
4866
+ WHERE ${conditions.join(" AND ")}${scope.sql}
4867
+ ORDER BY created_at ASC`,
4868
+ args: [...args, ...scope.args]
4869
+ });
4870
+ return result.rows.map((r) => ({
4871
+ id: String(r.id),
4872
+ agentId: String(r.agent_id),
4873
+ agentRole: String(r.agent_role),
4874
+ event: String(r.event),
4875
+ project: String(r.project),
4876
+ summary: String(r.summary),
4877
+ taskFile: r.task_file ? String(r.task_file) : void 0,
4878
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
4879
+ timestamp: String(r.created_at),
4880
+ read: false
4881
+ }));
4882
+ } catch {
4883
+ return [];
4884
+ }
4885
+ }
4886
+ async function markAsRead(ids, sessionScope) {
4887
+ if (ids.length === 0) return;
4888
+ try {
4889
+ const client = getClient();
4890
+ const placeholders = ids.map(() => "?").join(", ");
4891
+ const scope = strictSessionScopeFilter(sessionScope);
4892
+ await client.execute({
4893
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
4894
+ args: [...ids, ...scope.args]
4895
+ });
4896
+ } catch {
4897
+ }
4898
+ }
4807
4899
  async function markAsReadByTaskFile(taskFile, sessionScope) {
4808
4900
  try {
4809
4901
  const client = getClient();
@@ -4816,11 +4908,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
4816
4908
  } catch {
4817
4909
  }
4818
4910
  }
4911
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
4912
+ try {
4913
+ const client = getClient();
4914
+ const cutoff = new Date(
4915
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
4916
+ ).toISOString();
4917
+ const scope = strictSessionScopeFilter(sessionScope);
4918
+ const result = await client.execute({
4919
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
4920
+ args: [cutoff, ...scope.args]
4921
+ });
4922
+ return result.rowsAffected;
4923
+ } catch {
4924
+ return 0;
4925
+ }
4926
+ }
4927
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
4928
+ try {
4929
+ const client = getClient();
4930
+ const scope = strictSessionScopeFilter(sessionScope);
4931
+ const result = await client.execute({
4932
+ sql: `UPDATE notifications SET read = 1
4933
+ WHERE read = 0
4934
+ AND task_file IS NOT NULL
4935
+ ${scope.sql}
4936
+ AND task_file IN (
4937
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
4938
+ )`,
4939
+ args: [...scope.args, ...scope.args]
4940
+ });
4941
+ return result.rowsAffected;
4942
+ } catch {
4943
+ return 0;
4944
+ }
4945
+ }
4946
+ function formatNotifications(notifications) {
4947
+ if (notifications.length === 0) return "";
4948
+ const grouped = /* @__PURE__ */ new Map();
4949
+ for (const n of notifications) {
4950
+ const key = `${n.agentId}|${n.agentRole}`;
4951
+ if (!grouped.has(key)) grouped.set(key, []);
4952
+ grouped.get(key).push(n);
4953
+ }
4954
+ const lines = [];
4955
+ lines.push(`## Notifications (${notifications.length} unread)
4956
+ `);
4957
+ for (const [key, items] of grouped) {
4958
+ const [agentId, agentRole] = key.split("|");
4959
+ lines.push(`**${agentId}** (${agentRole}):`);
4960
+ for (const item of items) {
4961
+ const ago = formatTimeAgo(item.timestamp);
4962
+ const icon = eventIcon(item.event);
4963
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
4964
+ }
4965
+ lines.push("");
4966
+ }
4967
+ return lines.join("\n");
4968
+ }
4969
+ async function migrateJsonNotifications() {
4970
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path13.join(os9.homedir(), ".exe-os");
4971
+ const notifDir = path13.join(base, "notifications");
4972
+ if (!existsSync13(notifDir)) return 0;
4973
+ let migrated = 0;
4974
+ try {
4975
+ const files = readdirSync2(notifDir).filter((f) => f.endsWith(".json"));
4976
+ if (files.length === 0) return 0;
4977
+ const client = getClient();
4978
+ for (const file of files) {
4979
+ try {
4980
+ const filePath = path13.join(notifDir, file);
4981
+ const data = JSON.parse(readFileSync11(filePath, "utf8"));
4982
+ await client.execute({
4983
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
4984
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4985
+ args: [
4986
+ crypto2.randomUUID(),
4987
+ data.agentId ?? "unknown",
4988
+ data.agentRole ?? "unknown",
4989
+ data.event ?? "session_summary",
4990
+ data.project ?? "unknown",
4991
+ data.summary ?? "",
4992
+ data.taskFile ?? null,
4993
+ null,
4994
+ data.read ? 1 : 0,
4995
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
4996
+ ]
4997
+ });
4998
+ unlinkSync5(filePath);
4999
+ migrated++;
5000
+ } catch {
5001
+ }
5002
+ }
5003
+ try {
5004
+ const remaining = readdirSync2(notifDir);
5005
+ if (remaining.length === 0) {
5006
+ rmdirSync(notifDir);
5007
+ }
5008
+ } catch {
5009
+ }
5010
+ } catch {
5011
+ }
5012
+ return migrated;
5013
+ }
5014
+ function eventIcon(event) {
5015
+ switch (event) {
5016
+ case "task_complete":
5017
+ return "Completed:";
5018
+ case "task_needs_fix":
5019
+ return "Needs fix:";
5020
+ case "session_summary":
5021
+ return "Session:";
5022
+ case "error_spike":
5023
+ return "Errors:";
5024
+ case "orphan_task":
5025
+ return "Orphan:";
5026
+ case "subtasks_complete":
5027
+ return "Subtasks done:";
5028
+ case "capacity_relaunch":
5029
+ return "Relaunched:";
5030
+ }
5031
+ }
5032
+ function formatTimeAgo(timestamp) {
5033
+ const diffMs = Date.now() - new Date(timestamp).getTime();
5034
+ const mins = Math.floor(diffMs / 6e4);
5035
+ if (mins < 1) return "just now";
5036
+ if (mins < 60) return `${mins}m ago`;
5037
+ const hours = Math.floor(mins / 60);
5038
+ if (hours < 24) return `${hours}h ago`;
5039
+ const days = Math.floor(hours / 24);
5040
+ return `${days}d ago`;
5041
+ }
5042
+ var CLEANUP_DAYS;
4819
5043
  var init_notifications = __esm({
4820
5044
  "src/lib/notifications.ts"() {
4821
5045
  "use strict";
4822
5046
  init_database();
4823
5047
  init_task_scope();
5048
+ CLEANUP_DAYS = 7;
4824
5049
  }
4825
5050
  });
4826
5051
 
@@ -5866,7 +6091,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5866
6091
  taskFile
5867
6092
  });
5868
6093
  const originalPriority = String(row.priority).toLowerCase();
5869
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
6094
+ const resultLower = result?.toLowerCase() ?? "";
6095
+ const hasTestEvidence = (
6096
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
6097
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
6098
+ );
6099
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
6100
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
5870
6101
  if (!autoApprove) {
5871
6102
  try {
5872
6103
  const key = getSessionKey();
@@ -5874,6 +6105,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5874
6105
  if (exeSession) {
5875
6106
  sendIntercom(exeSession);
5876
6107
  }
6108
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
6109
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6110
+ if (exeSession) {
6111
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
6112
+ sendIntercom(reviewerSession);
6113
+ }
6114
+ }
5877
6115
  } catch {
5878
6116
  }
5879
6117
  }
@@ -6649,6 +6887,20 @@ async function updateTask(input2) {
6649
6887
  notifyTaskDone();
6650
6888
  }
6651
6889
  await markTaskNotificationsRead(taskFile);
6890
+ if (input2.status === "needs_review" && !isCoordinator) {
6891
+ try {
6892
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
6893
+ await writeNotification2({
6894
+ agentId: String(row.assigned_to),
6895
+ agentRole: String(row.assigned_to),
6896
+ event: "task_complete",
6897
+ project: String(row.project_name),
6898
+ summary: `"${String(row.title)}" is ready for review`,
6899
+ taskFile
6900
+ });
6901
+ } catch {
6902
+ }
6903
+ }
6652
6904
  if (input2.status === "done" || input2.status === "closed") {
6653
6905
  try {
6654
6906
  await cascadeUnblock(taskId, input2.baseDir, now);
@@ -7081,18 +7333,31 @@ function acquireSpawnLock2(sessionName) {
7081
7333
  mkdirSync9(SPAWN_LOCK_DIR, { recursive: true });
7082
7334
  }
7083
7335
  const lockFile = spawnLockPath(sessionName);
7084
- if (existsSync16(lockFile)) {
7085
- try {
7086
- const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
7087
- const age = Date.now() - lock.timestamp;
7088
- if (isProcessAlive(lock.pid) && age < 6e4) {
7089
- return false;
7090
- }
7091
- } catch {
7336
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
7337
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
7338
+ const { constants } = __require("fs");
7339
+ try {
7340
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
7341
+ writeSync(fd, lockData);
7342
+ closeSync3(fd);
7343
+ return true;
7344
+ } catch (err) {
7345
+ if (err?.code !== "EEXIST") {
7346
+ return true;
7092
7347
  }
7093
7348
  }
7094
- writeFileSync9(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
7095
- return true;
7349
+ try {
7350
+ const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
7351
+ const age = Date.now() - lock.timestamp;
7352
+ if (isProcessAlive(lock.pid) && age < 6e4) {
7353
+ return false;
7354
+ }
7355
+ writeFileSync9(lockFile, lockData);
7356
+ return true;
7357
+ } catch {
7358
+ writeFileSync9(lockFile, lockData);
7359
+ return true;
7360
+ }
7096
7361
  }
7097
7362
  function releaseSpawnLock2(sessionName) {
7098
7363
  try {
@@ -7171,6 +7436,21 @@ function parseParentExe(sessionName, agentId) {
7171
7436
  function extractRootExe(name) {
7172
7437
  if (!name) return null;
7173
7438
  if (!name.includes("-")) return name;
7439
+ try {
7440
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
7441
+ if (roster.length > 0) {
7442
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
7443
+ for (const agentName of sortedNames) {
7444
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7445
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
7446
+ const match = name.match(regex);
7447
+ if (match) {
7448
+ return extractRootExe(match[1]);
7449
+ }
7450
+ }
7451
+ }
7452
+ } catch {
7453
+ }
7174
7454
  const parts = name.split("-").filter(Boolean);
7175
7455
  return parts.length > 0 ? parts[parts.length - 1] : null;
7176
7456
  }
@@ -7189,6 +7469,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7189
7469
  function getParentExe(sessionKey) {
7190
7470
  try {
7191
7471
  const data = JSON.parse(readFileSync13(path19.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7472
+ if (data.registeredAt) {
7473
+ const age = Date.now() - new Date(data.registeredAt).getTime();
7474
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
7475
+ }
7192
7476
  return data.parentExe || null;
7193
7477
  } catch {
7194
7478
  return null;
@@ -7737,7 +8021,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7737
8021
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7738
8022
  } catch {
7739
8023
  }
7740
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
8024
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
7741
8025
  if (ccProvider !== DEFAULT_PROVIDER) {
7742
8026
  const cfg = PROVIDER_TABLE[ccProvider];
7743
8027
  if (cfg?.apiKeyEnv) {
@@ -7772,10 +8056,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7772
8056
  }
7773
8057
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
7774
8058
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
7775
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
7776
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
7777
- ccModel += "[1m]";
7778
- }
8059
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
8060
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
7779
8061
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
7780
8062
  }
7781
8063
  }
@@ -7876,7 +8158,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7876
8158
  releaseSpawnLock2(sessionName);
7877
8159
  return { sessionName };
7878
8160
  }
7879
- 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;
8161
+ 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;
7880
8162
  var init_tmux_routing = __esm({
7881
8163
  "src/lib/tmux-routing.ts"() {
7882
8164
  "use strict";
@@ -7896,6 +8178,7 @@ var init_tmux_routing = __esm({
7896
8178
  SESSION_CACHE = path19.join(os11.homedir(), ".exe-os", "session-cache");
7897
8179
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7898
8180
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8181
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
7899
8182
  VERIFY_PANE_LINES = 200;
7900
8183
  INTERCOM_DEBOUNCE_MS = 3e4;
7901
8184
  CODEX_DEBOUNCE_MS = 12e4;
@@ -7940,7 +8223,7 @@ var init_task_scope = __esm({
7940
8223
  });
7941
8224
 
7942
8225
  // src/lib/keychain.ts
7943
- import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
8226
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2, rename, copyFile } from "fs/promises";
7944
8227
  import { existsSync as existsSync18, statSync as statSync4 } from "fs";
7945
8228
  import { execSync as execSync9 } from "child_process";
7946
8229
  import path21 from "path";
@@ -7975,12 +8258,14 @@ function linuxSecretAvailable() {
7975
8258
  function isRootOnlyTrustedServerKeyFile(keyPath) {
7976
8259
  if (process.platform !== "linux") return false;
7977
8260
  try {
7978
- const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
7979
8261
  const st = statSync4(keyPath);
7980
8262
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
8263
+ const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
7981
8264
  if (uid === 0) return true;
7982
8265
  const exeOsDir = process.env.EXE_OS_DIR;
7983
- return Boolean(exeOsDir && path21.resolve(keyPath).startsWith(path21.resolve(exeOsDir) + path21.sep));
8266
+ if (exeOsDir && path21.resolve(keyPath).startsWith(path21.resolve(exeOsDir) + path21.sep)) return true;
8267
+ if (!linuxSecretAvailable()) return true;
8268
+ return false;
7984
8269
  } catch {
7985
8270
  return false;
7986
8271
  }
@@ -8130,15 +8415,25 @@ async function writeMachineBoundFileFallback(b64) {
8130
8415
  await mkdir4(dir, { recursive: true });
8131
8416
  const keyPath = getKeyPath();
8132
8417
  const machineKey = deriveMachineKey();
8133
- if (machineKey) {
8134
- const encrypted = encryptWithMachineKey(b64, machineKey);
8135
- await writeFile5(keyPath, encrypted + "\n", "utf-8");
8136
- await chmod2(keyPath, 384);
8137
- return "encrypted";
8418
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
8419
+ const result = machineKey ? "encrypted" : "plaintext";
8420
+ const tmpPath = keyPath + ".tmp";
8421
+ try {
8422
+ if (existsSync18(keyPath)) {
8423
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
8424
+ });
8425
+ }
8426
+ await writeFile5(tmpPath, content, "utf-8");
8427
+ await chmod2(tmpPath, 384);
8428
+ await rename(tmpPath, keyPath);
8429
+ } catch (err) {
8430
+ try {
8431
+ await unlink(tmpPath);
8432
+ } catch {
8433
+ }
8434
+ throw err;
8138
8435
  }
8139
- await writeFile5(keyPath, b64 + "\n", "utf-8");
8140
- await chmod2(keyPath, 384);
8141
- return "plaintext";
8436
+ return result;
8142
8437
  }
8143
8438
  async function getMasterKey() {
8144
8439
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -8205,7 +8500,7 @@ async function getMasterKey() {
8205
8500
  b64Value = content;
8206
8501
  }
8207
8502
  const key = Buffer.from(b64Value, "base64");
8208
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
8503
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
8209
8504
  return key;
8210
8505
  }
8211
8506
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);