@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
@@ -446,6 +446,17 @@ function normalizeOrchestration(raw) {
446
446
  const userOrg = raw.orchestration ?? {};
447
447
  raw.orchestration = { ...defaultOrg, ...userOrg };
448
448
  }
449
+ function normalizeCloudEndpoint(raw) {
450
+ const cloud = raw.cloud;
451
+ if (!cloud?.endpoint) return;
452
+ const ep = String(cloud.endpoint);
453
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
454
+ cloud.endpoint = "https://cloud.askexe.com";
455
+ process.stderr.write(
456
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
457
+ );
458
+ }
459
+ }
449
460
  async function loadConfig() {
450
461
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
451
462
  await ensurePrivateDir(dir);
@@ -471,6 +482,7 @@ async function loadConfig() {
471
482
  normalizeSessionLifecycle(migratedCfg);
472
483
  normalizeAutoUpdate(migratedCfg);
473
484
  normalizeOrchestration(migratedCfg);
485
+ normalizeCloudEndpoint(migratedCfg);
474
486
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
475
487
  if (config.dbPath.startsWith("~")) {
476
488
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -499,6 +511,7 @@ function loadConfigSync() {
499
511
  normalizeSessionLifecycle(migratedCfg);
500
512
  normalizeAutoUpdate(migratedCfg);
501
513
  normalizeOrchestration(migratedCfg);
514
+ normalizeCloudEndpoint(migratedCfg);
502
515
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
503
516
  if (config.dbPath.startsWith("~")) {
504
517
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -659,6 +672,7 @@ __export(agent_config_exports, {
659
672
  clearAgentRuntime: () => clearAgentRuntime,
660
673
  getAgentRuntime: () => getAgentRuntime,
661
674
  loadAgentConfig: () => loadAgentConfig,
675
+ normalizeCcModelName: () => normalizeCcModelName,
662
676
  saveAgentConfig: () => saveAgentConfig,
663
677
  setAgentMcps: () => setAgentMcps,
664
678
  setAgentRuntime: () => setAgentRuntime
@@ -687,6 +701,13 @@ function getAgentRuntime(agentId) {
687
701
  if (orgDefault) return orgDefault;
688
702
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
689
703
  }
704
+ function normalizeCcModelName(model) {
705
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
706
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
707
+ ccModel += "[1m]";
708
+ }
709
+ return ccModel;
710
+ }
690
711
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
691
712
  const knownModels = KNOWN_RUNTIMES[runtime];
692
713
  if (!knownModels) {
@@ -756,6 +777,7 @@ var init_agent_config = __esm({
756
777
  // src/lib/intercom-queue.ts
757
778
  var intercom_queue_exports = {};
758
779
  __export(intercom_queue_exports, {
780
+ _resetDrainGuard: () => _resetDrainGuard,
759
781
  clearQueueForAgent: () => clearQueueForAgent,
760
782
  drainForSession: () => drainForSession,
761
783
  drainQueue: () => drainQueue,
@@ -801,38 +823,47 @@ function queueIntercom(targetSession, reason) {
801
823
  writeQueue(queue);
802
824
  }
803
825
  function drainQueue(isSessionBusy2, sendKeys) {
826
+ if (_draining) {
827
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
828
+ return { drained: 0, failed: 0 };
829
+ }
804
830
  const queue = readQueue();
805
831
  if (queue.length === 0) return { drained: 0, failed: 0 };
832
+ _draining = true;
806
833
  const remaining = [];
807
834
  let drained = 0;
808
835
  let failed = 0;
809
- for (const item of queue) {
810
- const age = Date.now() - new Date(item.queuedAt).getTime();
811
- if (age > TTL_MS) {
812
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
813
- failed++;
814
- continue;
815
- }
816
- try {
817
- if (!isSessionBusy2(item.targetSession)) {
818
- const success = sendKeys(item.targetSession);
819
- if (success) {
820
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
821
- drained++;
822
- continue;
836
+ try {
837
+ for (const item of queue) {
838
+ const age = Date.now() - new Date(item.queuedAt).getTime();
839
+ if (age > TTL_MS) {
840
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
841
+ failed++;
842
+ continue;
843
+ }
844
+ try {
845
+ if (!isSessionBusy2(item.targetSession)) {
846
+ const success = sendKeys(item.targetSession);
847
+ if (success) {
848
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
849
+ drained++;
850
+ continue;
851
+ }
823
852
  }
853
+ } catch {
824
854
  }
825
- } catch {
826
- }
827
- item.attempts++;
828
- if (item.attempts >= MAX_RETRIES) {
829
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
830
- failed++;
831
- continue;
855
+ item.attempts++;
856
+ if (item.attempts >= MAX_RETRIES) {
857
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
858
+ failed++;
859
+ continue;
860
+ }
861
+ remaining.push(item);
832
862
  }
833
- remaining.push(item);
863
+ writeQueue(remaining);
864
+ } finally {
865
+ _draining = false;
834
866
  }
835
- writeQueue(remaining);
836
867
  return { drained, failed };
837
868
  }
838
869
  function drainForSession(targetSession, sendKeys) {
@@ -857,6 +888,9 @@ function clearQueueForAgent(agentName) {
857
888
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
858
889
  }
859
890
  }
891
+ function _resetDrainGuard() {
892
+ _draining = false;
893
+ }
860
894
  function logQueue(msg) {
861
895
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
862
896
  `;
@@ -868,13 +902,14 @@ function logQueue(msg) {
868
902
  } catch {
869
903
  }
870
904
  }
871
- var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
905
+ var QUEUE_PATH, MAX_RETRIES, TTL_MS, _draining, INTERCOM_LOG;
872
906
  var init_intercom_queue = __esm({
873
907
  "src/lib/intercom-queue.ts"() {
874
908
  "use strict";
875
909
  QUEUE_PATH = path4.join(os3.homedir(), ".exe-os", "intercom-queue.json");
876
910
  MAX_RETRIES = 5;
877
911
  TTL_MS = 60 * 60 * 1e3;
912
+ _draining = false;
878
913
  INTERCOM_LOG = path4.join(os3.homedir(), ".exe-os", "intercom.log");
879
914
  }
880
915
  });
@@ -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 path12 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 || path12.join(os9.homedir(), ".exe-os");
4971
+ const notifDir = path12.join(base, "notifications");
4972
+ if (!existsSync13(notifDir)) return 0;
4973
+ let migrated = 0;
4974
+ try {
4975
+ const files = readdirSync(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 = path12.join(notifDir, file);
4981
+ const data = JSON.parse(readFileSync10(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
+ unlinkSync4(filePath);
4999
+ migrated++;
5000
+ } catch {
5001
+ }
5002
+ }
5003
+ try {
5004
+ const remaining = readdirSync(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
 
@@ -5882,7 +6107,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5882
6107
  taskFile
5883
6108
  });
5884
6109
  const originalPriority = String(row.priority).toLowerCase();
5885
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
6110
+ const resultLower = result?.toLowerCase() ?? "";
6111
+ const hasTestEvidence = (
6112
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
6113
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
6114
+ );
6115
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
6116
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
5886
6117
  if (!autoApprove) {
5887
6118
  try {
5888
6119
  const key = getSessionKey();
@@ -5890,6 +6121,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5890
6121
  if (exeSession) {
5891
6122
  sendIntercom(exeSession);
5892
6123
  }
6124
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
6125
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6126
+ if (exeSession) {
6127
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
6128
+ sendIntercom(reviewerSession);
6129
+ }
6130
+ }
5893
6131
  } catch {
5894
6132
  }
5895
6133
  }
@@ -6665,6 +6903,20 @@ async function updateTask(input) {
6665
6903
  notifyTaskDone();
6666
6904
  }
6667
6905
  await markTaskNotificationsRead(taskFile);
6906
+ if (input.status === "needs_review" && !isCoordinator) {
6907
+ try {
6908
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
6909
+ await writeNotification2({
6910
+ agentId: String(row.assigned_to),
6911
+ agentRole: String(row.assigned_to),
6912
+ event: "task_complete",
6913
+ project: String(row.project_name),
6914
+ summary: `"${String(row.title)}" is ready for review`,
6915
+ taskFile
6916
+ });
6917
+ } catch {
6918
+ }
6919
+ }
6668
6920
  if (input.status === "done" || input.status === "closed") {
6669
6921
  try {
6670
6922
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -7097,18 +7349,31 @@ function acquireSpawnLock2(sessionName) {
7097
7349
  mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
7098
7350
  }
7099
7351
  const lockFile = spawnLockPath(sessionName);
7100
- if (existsSync16(lockFile)) {
7101
- try {
7102
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7103
- const age = Date.now() - lock.timestamp;
7104
- if (isProcessAlive(lock.pid) && age < 6e4) {
7105
- return false;
7106
- }
7107
- } catch {
7352
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
7353
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
7354
+ const { constants } = __require("fs");
7355
+ try {
7356
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
7357
+ writeSync(fd, lockData);
7358
+ closeSync3(fd);
7359
+ return true;
7360
+ } catch (err) {
7361
+ if (err?.code !== "EEXIST") {
7362
+ return true;
7108
7363
  }
7109
7364
  }
7110
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
7111
- return true;
7365
+ try {
7366
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7367
+ const age = Date.now() - lock.timestamp;
7368
+ if (isProcessAlive(lock.pid) && age < 6e4) {
7369
+ return false;
7370
+ }
7371
+ writeFileSync8(lockFile, lockData);
7372
+ return true;
7373
+ } catch {
7374
+ writeFileSync8(lockFile, lockData);
7375
+ return true;
7376
+ }
7112
7377
  }
7113
7378
  function releaseSpawnLock2(sessionName) {
7114
7379
  try {
@@ -7187,6 +7452,21 @@ function parseParentExe(sessionName, agentId) {
7187
7452
  function extractRootExe(name) {
7188
7453
  if (!name) return null;
7189
7454
  if (!name.includes("-")) return name;
7455
+ try {
7456
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
7457
+ if (roster.length > 0) {
7458
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
7459
+ for (const agentName of sortedNames) {
7460
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7461
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
7462
+ const match = name.match(regex);
7463
+ if (match) {
7464
+ return extractRootExe(match[1]);
7465
+ }
7466
+ }
7467
+ }
7468
+ } catch {
7469
+ }
7190
7470
  const parts = name.split("-").filter(Boolean);
7191
7471
  return parts.length > 0 ? parts[parts.length - 1] : null;
7192
7472
  }
@@ -7205,6 +7485,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7205
7485
  function getParentExe(sessionKey) {
7206
7486
  try {
7207
7487
  const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7488
+ if (data.registeredAt) {
7489
+ const age = Date.now() - new Date(data.registeredAt).getTime();
7490
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
7491
+ }
7208
7492
  return data.parentExe || null;
7209
7493
  } catch {
7210
7494
  return null;
@@ -7753,7 +8037,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7753
8037
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7754
8038
  } catch {
7755
8039
  }
7756
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
8040
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
7757
8041
  if (ccProvider !== DEFAULT_PROVIDER) {
7758
8042
  const cfg = PROVIDER_TABLE[ccProvider];
7759
8043
  if (cfg?.apiKeyEnv) {
@@ -7788,10 +8072,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7788
8072
  }
7789
8073
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
7790
8074
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
7791
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
7792
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
7793
- ccModel += "[1m]";
7794
- }
8075
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
8076
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
7795
8077
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
7796
8078
  }
7797
8079
  }
@@ -7892,7 +8174,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7892
8174
  releaseSpawnLock2(sessionName);
7893
8175
  return { sessionName };
7894
8176
  }
7895
- 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;
8177
+ 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;
7896
8178
  var init_tmux_routing = __esm({
7897
8179
  "src/lib/tmux-routing.ts"() {
7898
8180
  "use strict";
@@ -7912,6 +8194,7 @@ var init_tmux_routing = __esm({
7912
8194
  SESSION_CACHE = path18.join(os11.homedir(), ".exe-os", "session-cache");
7913
8195
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7914
8196
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8197
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
7915
8198
  VERIFY_PANE_LINES = 200;
7916
8199
  INTERCOM_DEBOUNCE_MS = 3e4;
7917
8200
  CODEX_DEBOUNCE_MS = 12e4;
@@ -7956,7 +8239,7 @@ var init_task_scope = __esm({
7956
8239
  });
7957
8240
 
7958
8241
  // src/lib/keychain.ts
7959
- import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
8242
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2, rename, copyFile } from "fs/promises";
7960
8243
  import { existsSync as existsSync17, statSync as statSync3 } from "fs";
7961
8244
  import { execSync as execSync9 } from "child_process";
7962
8245
  import path19 from "path";
@@ -7991,12 +8274,14 @@ function linuxSecretAvailable() {
7991
8274
  function isRootOnlyTrustedServerKeyFile(keyPath) {
7992
8275
  if (process.platform !== "linux") return false;
7993
8276
  try {
7994
- const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
7995
8277
  const st = statSync3(keyPath);
7996
8278
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
8279
+ const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
7997
8280
  if (uid === 0) return true;
7998
8281
  const exeOsDir = process.env.EXE_OS_DIR;
7999
- return Boolean(exeOsDir && path19.resolve(keyPath).startsWith(path19.resolve(exeOsDir) + path19.sep));
8282
+ if (exeOsDir && path19.resolve(keyPath).startsWith(path19.resolve(exeOsDir) + path19.sep)) return true;
8283
+ if (!linuxSecretAvailable()) return true;
8284
+ return false;
8000
8285
  } catch {
8001
8286
  return false;
8002
8287
  }
@@ -8146,15 +8431,25 @@ async function writeMachineBoundFileFallback(b64) {
8146
8431
  await mkdir4(dir, { recursive: true });
8147
8432
  const keyPath = getKeyPath();
8148
8433
  const machineKey = deriveMachineKey();
8149
- if (machineKey) {
8150
- const encrypted = encryptWithMachineKey(b64, machineKey);
8151
- await writeFile5(keyPath, encrypted + "\n", "utf-8");
8152
- await chmod2(keyPath, 384);
8153
- return "encrypted";
8434
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
8435
+ const result = machineKey ? "encrypted" : "plaintext";
8436
+ const tmpPath = keyPath + ".tmp";
8437
+ try {
8438
+ if (existsSync17(keyPath)) {
8439
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
8440
+ });
8441
+ }
8442
+ await writeFile5(tmpPath, content, "utf-8");
8443
+ await chmod2(tmpPath, 384);
8444
+ await rename(tmpPath, keyPath);
8445
+ } catch (err) {
8446
+ try {
8447
+ await unlink(tmpPath);
8448
+ } catch {
8449
+ }
8450
+ throw err;
8154
8451
  }
8155
- await writeFile5(keyPath, b64 + "\n", "utf-8");
8156
- await chmod2(keyPath, 384);
8157
- return "plaintext";
8452
+ return result;
8158
8453
  }
8159
8454
  async function getMasterKey() {
8160
8455
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -8221,7 +8516,7 @@ async function getMasterKey() {
8221
8516
  b64Value = content;
8222
8517
  }
8223
8518
  const key = Buffer.from(b64Value, "base64");
8224
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
8519
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
8225
8520
  return key;
8226
8521
  }
8227
8522
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);