@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
@@ -447,6 +447,17 @@ function normalizeOrchestration(raw) {
447
447
  const userOrg = raw.orchestration ?? {};
448
448
  raw.orchestration = { ...defaultOrg, ...userOrg };
449
449
  }
450
+ function normalizeCloudEndpoint(raw) {
451
+ const cloud = raw.cloud;
452
+ if (!cloud?.endpoint) return;
453
+ const ep = String(cloud.endpoint);
454
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
455
+ cloud.endpoint = "https://cloud.askexe.com";
456
+ process.stderr.write(
457
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
458
+ );
459
+ }
460
+ }
450
461
  async function loadConfig() {
451
462
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
452
463
  await ensurePrivateDir(dir);
@@ -472,6 +483,7 @@ async function loadConfig() {
472
483
  normalizeSessionLifecycle(migratedCfg);
473
484
  normalizeAutoUpdate(migratedCfg);
474
485
  normalizeOrchestration(migratedCfg);
486
+ normalizeCloudEndpoint(migratedCfg);
475
487
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
476
488
  if (config.dbPath.startsWith("~")) {
477
489
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -500,6 +512,7 @@ function loadConfigSync() {
500
512
  normalizeSessionLifecycle(migratedCfg);
501
513
  normalizeAutoUpdate(migratedCfg);
502
514
  normalizeOrchestration(migratedCfg);
515
+ normalizeCloudEndpoint(migratedCfg);
503
516
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
504
517
  if (config.dbPath.startsWith("~")) {
505
518
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -660,6 +673,7 @@ __export(agent_config_exports, {
660
673
  clearAgentRuntime: () => clearAgentRuntime,
661
674
  getAgentRuntime: () => getAgentRuntime,
662
675
  loadAgentConfig: () => loadAgentConfig,
676
+ normalizeCcModelName: () => normalizeCcModelName,
663
677
  saveAgentConfig: () => saveAgentConfig,
664
678
  setAgentMcps: () => setAgentMcps,
665
679
  setAgentRuntime: () => setAgentRuntime
@@ -688,6 +702,13 @@ function getAgentRuntime(agentId) {
688
702
  if (orgDefault) return orgDefault;
689
703
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
690
704
  }
705
+ function normalizeCcModelName(model) {
706
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
707
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
708
+ ccModel += "[1m]";
709
+ }
710
+ return ccModel;
711
+ }
691
712
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
692
713
  const knownModels = KNOWN_RUNTIMES[runtime];
693
714
  if (!knownModels) {
@@ -757,6 +778,7 @@ var init_agent_config = __esm({
757
778
  // src/lib/intercom-queue.ts
758
779
  var intercom_queue_exports = {};
759
780
  __export(intercom_queue_exports, {
781
+ _resetDrainGuard: () => _resetDrainGuard,
760
782
  clearQueueForAgent: () => clearQueueForAgent,
761
783
  drainForSession: () => drainForSession,
762
784
  drainQueue: () => drainQueue,
@@ -802,38 +824,47 @@ function queueIntercom(targetSession, reason) {
802
824
  writeQueue(queue);
803
825
  }
804
826
  function drainQueue(isSessionBusy2, sendKeys) {
827
+ if (_draining) {
828
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
829
+ return { drained: 0, failed: 0 };
830
+ }
805
831
  const queue = readQueue();
806
832
  if (queue.length === 0) return { drained: 0, failed: 0 };
833
+ _draining = true;
807
834
  const remaining = [];
808
835
  let drained = 0;
809
836
  let failed = 0;
810
- for (const item of queue) {
811
- const age = Date.now() - new Date(item.queuedAt).getTime();
812
- if (age > TTL_MS) {
813
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
814
- failed++;
815
- continue;
816
- }
817
- try {
818
- if (!isSessionBusy2(item.targetSession)) {
819
- const success = sendKeys(item.targetSession);
820
- if (success) {
821
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
822
- drained++;
823
- continue;
837
+ try {
838
+ for (const item of queue) {
839
+ const age = Date.now() - new Date(item.queuedAt).getTime();
840
+ if (age > TTL_MS) {
841
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
842
+ failed++;
843
+ continue;
844
+ }
845
+ try {
846
+ if (!isSessionBusy2(item.targetSession)) {
847
+ const success = sendKeys(item.targetSession);
848
+ if (success) {
849
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
850
+ drained++;
851
+ continue;
852
+ }
824
853
  }
854
+ } catch {
825
855
  }
826
- } catch {
827
- }
828
- item.attempts++;
829
- if (item.attempts >= MAX_RETRIES) {
830
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
831
- failed++;
832
- continue;
856
+ item.attempts++;
857
+ if (item.attempts >= MAX_RETRIES) {
858
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
859
+ failed++;
860
+ continue;
861
+ }
862
+ remaining.push(item);
833
863
  }
834
- remaining.push(item);
864
+ writeQueue(remaining);
865
+ } finally {
866
+ _draining = false;
835
867
  }
836
- writeQueue(remaining);
837
868
  return { drained, failed };
838
869
  }
839
870
  function drainForSession(targetSession, sendKeys) {
@@ -858,6 +889,9 @@ function clearQueueForAgent(agentName) {
858
889
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
859
890
  }
860
891
  }
892
+ function _resetDrainGuard() {
893
+ _draining = false;
894
+ }
861
895
  function logQueue(msg) {
862
896
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
863
897
  `;
@@ -869,13 +903,14 @@ function logQueue(msg) {
869
903
  } catch {
870
904
  }
871
905
  }
872
- var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
906
+ var QUEUE_PATH, MAX_RETRIES, TTL_MS, _draining, INTERCOM_LOG;
873
907
  var init_intercom_queue = __esm({
874
908
  "src/lib/intercom-queue.ts"() {
875
909
  "use strict";
876
910
  QUEUE_PATH = path4.join(os3.homedir(), ".exe-os", "intercom-queue.json");
877
911
  MAX_RETRIES = 5;
878
912
  TTL_MS = 60 * 60 * 1e3;
913
+ _draining = false;
879
914
  INTERCOM_LOG = path4.join(os3.homedir(), ".exe-os", "intercom.log");
880
915
  }
881
916
  });
@@ -4802,6 +4837,17 @@ var init_task_scope = __esm({
4802
4837
  });
4803
4838
 
4804
4839
  // src/lib/notifications.ts
4840
+ var notifications_exports = {};
4841
+ __export(notifications_exports, {
4842
+ cleanupOldNotifications: () => cleanupOldNotifications,
4843
+ formatNotifications: () => formatNotifications,
4844
+ markAsRead: () => markAsRead,
4845
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
4846
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
4847
+ migrateJsonNotifications: () => migrateJsonNotifications,
4848
+ readUnreadNotifications: () => readUnreadNotifications,
4849
+ writeNotification: () => writeNotification
4850
+ });
4805
4851
  import crypto2 from "crypto";
4806
4852
  import path12 from "path";
4807
4853
  import os9 from "os";
@@ -4838,6 +4884,52 @@ async function writeNotification(notification) {
4838
4884
  `);
4839
4885
  }
4840
4886
  }
4887
+ async function readUnreadNotifications(agentFilter, sessionScope) {
4888
+ try {
4889
+ const client = getClient();
4890
+ const conditions = ["read = 0"];
4891
+ const args = [];
4892
+ const scope = strictSessionScopeFilter(sessionScope);
4893
+ if (agentFilter) {
4894
+ conditions.push("agent_id = ?");
4895
+ args.push(agentFilter);
4896
+ }
4897
+ const result2 = await client.execute({
4898
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
4899
+ FROM notifications
4900
+ WHERE ${conditions.join(" AND ")}${scope.sql}
4901
+ ORDER BY created_at ASC`,
4902
+ args: [...args, ...scope.args]
4903
+ });
4904
+ return result2.rows.map((r) => ({
4905
+ id: String(r.id),
4906
+ agentId: String(r.agent_id),
4907
+ agentRole: String(r.agent_role),
4908
+ event: String(r.event),
4909
+ project: String(r.project),
4910
+ summary: String(r.summary),
4911
+ taskFile: r.task_file ? String(r.task_file) : void 0,
4912
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
4913
+ timestamp: String(r.created_at),
4914
+ read: false
4915
+ }));
4916
+ } catch {
4917
+ return [];
4918
+ }
4919
+ }
4920
+ async function markAsRead(ids, sessionScope) {
4921
+ if (ids.length === 0) return;
4922
+ try {
4923
+ const client = getClient();
4924
+ const placeholders = ids.map(() => "?").join(", ");
4925
+ const scope = strictSessionScopeFilter(sessionScope);
4926
+ await client.execute({
4927
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
4928
+ args: [...ids, ...scope.args]
4929
+ });
4930
+ } catch {
4931
+ }
4932
+ }
4841
4933
  async function markAsReadByTaskFile(taskFile, sessionScope) {
4842
4934
  try {
4843
4935
  const client = getClient();
@@ -4850,11 +4942,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
4850
4942
  } catch {
4851
4943
  }
4852
4944
  }
4945
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
4946
+ try {
4947
+ const client = getClient();
4948
+ const cutoff = new Date(
4949
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
4950
+ ).toISOString();
4951
+ const scope = strictSessionScopeFilter(sessionScope);
4952
+ const result2 = await client.execute({
4953
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
4954
+ args: [cutoff, ...scope.args]
4955
+ });
4956
+ return result2.rowsAffected;
4957
+ } catch {
4958
+ return 0;
4959
+ }
4960
+ }
4961
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
4962
+ try {
4963
+ const client = getClient();
4964
+ const scope = strictSessionScopeFilter(sessionScope);
4965
+ const result2 = await client.execute({
4966
+ sql: `UPDATE notifications SET read = 1
4967
+ WHERE read = 0
4968
+ AND task_file IS NOT NULL
4969
+ ${scope.sql}
4970
+ AND task_file IN (
4971
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
4972
+ )`,
4973
+ args: [...scope.args, ...scope.args]
4974
+ });
4975
+ return result2.rowsAffected;
4976
+ } catch {
4977
+ return 0;
4978
+ }
4979
+ }
4980
+ function formatNotifications(notifications) {
4981
+ if (notifications.length === 0) return "";
4982
+ const grouped = /* @__PURE__ */ new Map();
4983
+ for (const n of notifications) {
4984
+ const key = `${n.agentId}|${n.agentRole}`;
4985
+ if (!grouped.has(key)) grouped.set(key, []);
4986
+ grouped.get(key).push(n);
4987
+ }
4988
+ const lines = [];
4989
+ lines.push(`## Notifications (${notifications.length} unread)
4990
+ `);
4991
+ for (const [key, items] of grouped) {
4992
+ const [agentId, agentRole] = key.split("|");
4993
+ lines.push(`**${agentId}** (${agentRole}):`);
4994
+ for (const item of items) {
4995
+ const ago = formatTimeAgo(item.timestamp);
4996
+ const icon = eventIcon(item.event);
4997
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
4998
+ }
4999
+ lines.push("");
5000
+ }
5001
+ return lines.join("\n");
5002
+ }
5003
+ async function migrateJsonNotifications() {
5004
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path12.join(os9.homedir(), ".exe-os");
5005
+ const notifDir = path12.join(base, "notifications");
5006
+ if (!existsSync13(notifDir)) return 0;
5007
+ let migrated = 0;
5008
+ try {
5009
+ const files = readdirSync(notifDir).filter((f) => f.endsWith(".json"));
5010
+ if (files.length === 0) return 0;
5011
+ const client = getClient();
5012
+ for (const file of files) {
5013
+ try {
5014
+ const filePath = path12.join(notifDir, file);
5015
+ const data = JSON.parse(readFileSync10(filePath, "utf8"));
5016
+ await client.execute({
5017
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
5018
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5019
+ args: [
5020
+ crypto2.randomUUID(),
5021
+ data.agentId ?? "unknown",
5022
+ data.agentRole ?? "unknown",
5023
+ data.event ?? "session_summary",
5024
+ data.project ?? "unknown",
5025
+ data.summary ?? "",
5026
+ data.taskFile ?? null,
5027
+ null,
5028
+ data.read ? 1 : 0,
5029
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
5030
+ ]
5031
+ });
5032
+ unlinkSync4(filePath);
5033
+ migrated++;
5034
+ } catch {
5035
+ }
5036
+ }
5037
+ try {
5038
+ const remaining = readdirSync(notifDir);
5039
+ if (remaining.length === 0) {
5040
+ rmdirSync(notifDir);
5041
+ }
5042
+ } catch {
5043
+ }
5044
+ } catch {
5045
+ }
5046
+ return migrated;
5047
+ }
5048
+ function eventIcon(event) {
5049
+ switch (event) {
5050
+ case "task_complete":
5051
+ return "Completed:";
5052
+ case "task_needs_fix":
5053
+ return "Needs fix:";
5054
+ case "session_summary":
5055
+ return "Session:";
5056
+ case "error_spike":
5057
+ return "Errors:";
5058
+ case "orphan_task":
5059
+ return "Orphan:";
5060
+ case "subtasks_complete":
5061
+ return "Subtasks done:";
5062
+ case "capacity_relaunch":
5063
+ return "Relaunched:";
5064
+ }
5065
+ }
5066
+ function formatTimeAgo(timestamp) {
5067
+ const diffMs = Date.now() - new Date(timestamp).getTime();
5068
+ const mins = Math.floor(diffMs / 6e4);
5069
+ if (mins < 1) return "just now";
5070
+ if (mins < 60) return `${mins}m ago`;
5071
+ const hours = Math.floor(mins / 60);
5072
+ if (hours < 24) return `${hours}h ago`;
5073
+ const days = Math.floor(hours / 24);
5074
+ return `${days}d ago`;
5075
+ }
5076
+ var CLEANUP_DAYS;
4853
5077
  var init_notifications = __esm({
4854
5078
  "src/lib/notifications.ts"() {
4855
5079
  "use strict";
4856
5080
  init_database();
4857
5081
  init_task_scope();
5082
+ CLEANUP_DAYS = 7;
4858
5083
  }
4859
5084
  });
4860
5085
 
@@ -5900,7 +6125,13 @@ async function createReviewForCompletedTask(row, result2, _baseDir, now) {
5900
6125
  taskFile
5901
6126
  });
5902
6127
  const originalPriority = String(row.priority).toLowerCase();
5903
- const autoApprove = originalPriority === "p2" && result2?.toLowerCase().includes("tests pass");
6128
+ const resultLower = result2?.toLowerCase() ?? "";
6129
+ const hasTestEvidence = (
6130
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
6131
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
6132
+ );
6133
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
6134
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
5904
6135
  if (!autoApprove) {
5905
6136
  try {
5906
6137
  const key = getSessionKey();
@@ -5908,6 +6139,13 @@ async function createReviewForCompletedTask(row, result2, _baseDir, now) {
5908
6139
  if (exeSession2) {
5909
6140
  sendIntercom(exeSession2);
5910
6141
  }
6142
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession2) {
6143
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6144
+ if (exeSession2) {
6145
+ const reviewerSession = employeeSessionName2(reviewer, exeSession2);
6146
+ sendIntercom(reviewerSession);
6147
+ }
6148
+ }
5911
6149
  } catch {
5912
6150
  }
5913
6151
  }
@@ -6683,6 +6921,20 @@ async function updateTask(input) {
6683
6921
  notifyTaskDone();
6684
6922
  }
6685
6923
  await markTaskNotificationsRead(taskFile);
6924
+ if (input.status === "needs_review" && !isCoordinator) {
6925
+ try {
6926
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
6927
+ await writeNotification2({
6928
+ agentId: String(row.assigned_to),
6929
+ agentRole: String(row.assigned_to),
6930
+ event: "task_complete",
6931
+ project: String(row.project_name),
6932
+ summary: `"${String(row.title)}" is ready for review`,
6933
+ taskFile
6934
+ });
6935
+ } catch {
6936
+ }
6937
+ }
6686
6938
  if (input.status === "done" || input.status === "closed") {
6687
6939
  try {
6688
6940
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -7115,18 +7367,31 @@ function acquireSpawnLock2(sessionName) {
7115
7367
  mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
7116
7368
  }
7117
7369
  const lockFile = spawnLockPath(sessionName);
7118
- if (existsSync16(lockFile)) {
7119
- try {
7120
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7121
- const age = Date.now() - lock.timestamp;
7122
- if (isProcessAlive(lock.pid) && age < 6e4) {
7123
- return false;
7124
- }
7125
- } catch {
7370
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
7371
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
7372
+ const { constants } = __require("fs");
7373
+ try {
7374
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
7375
+ writeSync(fd, lockData);
7376
+ closeSync3(fd);
7377
+ return true;
7378
+ } catch (err) {
7379
+ if (err?.code !== "EEXIST") {
7380
+ return true;
7126
7381
  }
7127
7382
  }
7128
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
7129
- return true;
7383
+ try {
7384
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7385
+ const age = Date.now() - lock.timestamp;
7386
+ if (isProcessAlive(lock.pid) && age < 6e4) {
7387
+ return false;
7388
+ }
7389
+ writeFileSync8(lockFile, lockData);
7390
+ return true;
7391
+ } catch {
7392
+ writeFileSync8(lockFile, lockData);
7393
+ return true;
7394
+ }
7130
7395
  }
7131
7396
  function releaseSpawnLock2(sessionName) {
7132
7397
  try {
@@ -7205,6 +7470,21 @@ function parseParentExe(sessionName, agentId) {
7205
7470
  function extractRootExe(name) {
7206
7471
  if (!name) return null;
7207
7472
  if (!name.includes("-")) return name;
7473
+ try {
7474
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
7475
+ if (roster.length > 0) {
7476
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
7477
+ for (const agentName of sortedNames) {
7478
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7479
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
7480
+ const match = name.match(regex);
7481
+ if (match) {
7482
+ return extractRootExe(match[1]);
7483
+ }
7484
+ }
7485
+ }
7486
+ } catch {
7487
+ }
7208
7488
  const parts = name.split("-").filter(Boolean);
7209
7489
  return parts.length > 0 ? parts[parts.length - 1] : null;
7210
7490
  }
@@ -7223,6 +7503,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7223
7503
  function getParentExe(sessionKey) {
7224
7504
  try {
7225
7505
  const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7506
+ if (data.registeredAt) {
7507
+ const age = Date.now() - new Date(data.registeredAt).getTime();
7508
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
7509
+ }
7226
7510
  return data.parentExe || null;
7227
7511
  } catch {
7228
7512
  return null;
@@ -7771,7 +8055,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
7771
8055
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7772
8056
  } catch {
7773
8057
  }
7774
- let envPrefix = `EXE_SESSION=${exeSession2} EXE_SESSION_NAME=${sessionName}`;
8058
+ let envPrefix = `EXE_SESSION=${exeSession2} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
7775
8059
  if (ccProvider !== DEFAULT_PROVIDER) {
7776
8060
  const cfg = PROVIDER_TABLE[ccProvider];
7777
8061
  if (cfg?.apiKeyEnv) {
@@ -7806,10 +8090,8 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
7806
8090
  }
7807
8091
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
7808
8092
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
7809
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
7810
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
7811
- ccModel += "[1m]";
7812
- }
8093
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
8094
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
7813
8095
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
7814
8096
  }
7815
8097
  }
@@ -7910,7 +8192,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
7910
8192
  releaseSpawnLock2(sessionName);
7911
8193
  return { sessionName };
7912
8194
  }
7913
- 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;
8195
+ 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;
7914
8196
  var init_tmux_routing = __esm({
7915
8197
  "src/lib/tmux-routing.ts"() {
7916
8198
  "use strict";
@@ -7930,6 +8212,7 @@ var init_tmux_routing = __esm({
7930
8212
  SESSION_CACHE = path18.join(os11.homedir(), ".exe-os", "session-cache");
7931
8213
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7932
8214
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8215
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
7933
8216
  VERIFY_PANE_LINES = 200;
7934
8217
  INTERCOM_DEBOUNCE_MS = 3e4;
7935
8218
  CODEX_DEBOUNCE_MS = 12e4;
@@ -7941,7 +8224,7 @@ var init_tmux_routing = __esm({
7941
8224
  });
7942
8225
 
7943
8226
  // src/lib/keychain.ts
7944
- import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
8227
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2, rename, copyFile } from "fs/promises";
7945
8228
  import { existsSync as existsSync17, statSync as statSync3 } from "fs";
7946
8229
  import { execSync as execSync8 } from "child_process";
7947
8230
  import path19 from "path";
@@ -7976,12 +8259,14 @@ function linuxSecretAvailable() {
7976
8259
  function isRootOnlyTrustedServerKeyFile(keyPath) {
7977
8260
  if (process.platform !== "linux") return false;
7978
8261
  try {
7979
- const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
7980
8262
  const st = statSync3(keyPath);
7981
8263
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
8264
+ const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
7982
8265
  if (uid === 0) return true;
7983
8266
  const exeOsDir = process.env.EXE_OS_DIR;
7984
- return Boolean(exeOsDir && path19.resolve(keyPath).startsWith(path19.resolve(exeOsDir) + path19.sep));
8267
+ if (exeOsDir && path19.resolve(keyPath).startsWith(path19.resolve(exeOsDir) + path19.sep)) return true;
8268
+ if (!linuxSecretAvailable()) return true;
8269
+ return false;
7985
8270
  } catch {
7986
8271
  return false;
7987
8272
  }
@@ -8131,15 +8416,25 @@ async function writeMachineBoundFileFallback(b64) {
8131
8416
  await mkdir4(dir, { recursive: true });
8132
8417
  const keyPath = getKeyPath();
8133
8418
  const machineKey = deriveMachineKey();
8134
- if (machineKey) {
8135
- const encrypted = encryptWithMachineKey(b64, machineKey);
8136
- await writeFile5(keyPath, encrypted + "\n", "utf-8");
8137
- await chmod2(keyPath, 384);
8138
- return "encrypted";
8419
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
8420
+ const result2 = machineKey ? "encrypted" : "plaintext";
8421
+ const tmpPath = keyPath + ".tmp";
8422
+ try {
8423
+ if (existsSync17(keyPath)) {
8424
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
8425
+ });
8426
+ }
8427
+ await writeFile5(tmpPath, content, "utf-8");
8428
+ await chmod2(tmpPath, 384);
8429
+ await rename(tmpPath, keyPath);
8430
+ } catch (err) {
8431
+ try {
8432
+ await unlink(tmpPath);
8433
+ } catch {
8434
+ }
8435
+ throw err;
8139
8436
  }
8140
- await writeFile5(keyPath, b64 + "\n", "utf-8");
8141
- await chmod2(keyPath, 384);
8142
- return "plaintext";
8437
+ return result2;
8143
8438
  }
8144
8439
  async function getMasterKey() {
8145
8440
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -8206,7 +8501,7 @@ async function getMasterKey() {
8206
8501
  b64Value = content;
8207
8502
  }
8208
8503
  const key = Buffer.from(b64Value, "base64");
8209
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
8504
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
8210
8505
  return key;
8211
8506
  }
8212
8507
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);