@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
@@ -459,6 +459,17 @@ function normalizeOrchestration(raw) {
459
459
  const userOrg = raw.orchestration ?? {};
460
460
  raw.orchestration = { ...defaultOrg, ...userOrg };
461
461
  }
462
+ function normalizeCloudEndpoint(raw) {
463
+ const cloud = raw.cloud;
464
+ if (!cloud?.endpoint) return;
465
+ const ep = String(cloud.endpoint);
466
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
467
+ cloud.endpoint = "https://cloud.askexe.com";
468
+ process.stderr.write(
469
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
470
+ );
471
+ }
472
+ }
462
473
  async function loadConfig() {
463
474
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
464
475
  await ensurePrivateDir(dir);
@@ -484,6 +495,7 @@ async function loadConfig() {
484
495
  normalizeSessionLifecycle(migratedCfg);
485
496
  normalizeAutoUpdate(migratedCfg);
486
497
  normalizeOrchestration(migratedCfg);
498
+ normalizeCloudEndpoint(migratedCfg);
487
499
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
488
500
  if (config.dbPath.startsWith("~")) {
489
501
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -512,6 +524,7 @@ function loadConfigSync() {
512
524
  normalizeSessionLifecycle(migratedCfg);
513
525
  normalizeAutoUpdate(migratedCfg);
514
526
  normalizeOrchestration(migratedCfg);
527
+ normalizeCloudEndpoint(migratedCfg);
515
528
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
516
529
  if (config.dbPath.startsWith("~")) {
517
530
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -672,6 +685,7 @@ __export(agent_config_exports, {
672
685
  clearAgentRuntime: () => clearAgentRuntime,
673
686
  getAgentRuntime: () => getAgentRuntime,
674
687
  loadAgentConfig: () => loadAgentConfig,
688
+ normalizeCcModelName: () => normalizeCcModelName,
675
689
  saveAgentConfig: () => saveAgentConfig,
676
690
  setAgentMcps: () => setAgentMcps,
677
691
  setAgentRuntime: () => setAgentRuntime
@@ -700,6 +714,13 @@ function getAgentRuntime(agentId) {
700
714
  if (orgDefault) return orgDefault;
701
715
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
702
716
  }
717
+ function normalizeCcModelName(model) {
718
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
719
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
720
+ ccModel += "[1m]";
721
+ }
722
+ return ccModel;
723
+ }
703
724
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
704
725
  const knownModels = KNOWN_RUNTIMES[runtime];
705
726
  if (!knownModels) {
@@ -769,6 +790,7 @@ var init_agent_config = __esm({
769
790
  // src/lib/intercom-queue.ts
770
791
  var intercom_queue_exports = {};
771
792
  __export(intercom_queue_exports, {
793
+ _resetDrainGuard: () => _resetDrainGuard,
772
794
  clearQueueForAgent: () => clearQueueForAgent,
773
795
  drainForSession: () => drainForSession,
774
796
  drainQueue: () => drainQueue,
@@ -814,38 +836,47 @@ function queueIntercom(targetSession, reason) {
814
836
  writeQueue(queue);
815
837
  }
816
838
  function drainQueue(isSessionBusy2, sendKeys) {
839
+ if (_draining) {
840
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
841
+ return { drained: 0, failed: 0 };
842
+ }
817
843
  const queue = readQueue();
818
844
  if (queue.length === 0) return { drained: 0, failed: 0 };
845
+ _draining = true;
819
846
  const remaining = [];
820
847
  let drained = 0;
821
848
  let failed = 0;
822
- for (const item of queue) {
823
- const age = Date.now() - new Date(item.queuedAt).getTime();
824
- if (age > TTL_MS) {
825
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
826
- failed++;
827
- continue;
828
- }
829
- try {
830
- if (!isSessionBusy2(item.targetSession)) {
831
- const success = sendKeys(item.targetSession);
832
- if (success) {
833
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
834
- drained++;
835
- continue;
849
+ try {
850
+ for (const item of queue) {
851
+ const age = Date.now() - new Date(item.queuedAt).getTime();
852
+ if (age > TTL_MS) {
853
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
854
+ failed++;
855
+ continue;
856
+ }
857
+ try {
858
+ if (!isSessionBusy2(item.targetSession)) {
859
+ const success = sendKeys(item.targetSession);
860
+ if (success) {
861
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
862
+ drained++;
863
+ continue;
864
+ }
836
865
  }
866
+ } catch {
837
867
  }
838
- } catch {
839
- }
840
- item.attempts++;
841
- if (item.attempts >= MAX_RETRIES) {
842
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
843
- failed++;
844
- continue;
868
+ item.attempts++;
869
+ if (item.attempts >= MAX_RETRIES) {
870
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
871
+ failed++;
872
+ continue;
873
+ }
874
+ remaining.push(item);
845
875
  }
846
- remaining.push(item);
876
+ writeQueue(remaining);
877
+ } finally {
878
+ _draining = false;
847
879
  }
848
- writeQueue(remaining);
849
880
  return { drained, failed };
850
881
  }
851
882
  function drainForSession(targetSession, sendKeys) {
@@ -870,6 +901,9 @@ function clearQueueForAgent(agentName) {
870
901
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
871
902
  }
872
903
  }
904
+ function _resetDrainGuard() {
905
+ _draining = false;
906
+ }
873
907
  function logQueue(msg) {
874
908
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
875
909
  `;
@@ -881,13 +915,14 @@ function logQueue(msg) {
881
915
  } catch {
882
916
  }
883
917
  }
884
- var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
918
+ var QUEUE_PATH, MAX_RETRIES, TTL_MS, _draining, INTERCOM_LOG;
885
919
  var init_intercom_queue = __esm({
886
920
  "src/lib/intercom-queue.ts"() {
887
921
  "use strict";
888
922
  QUEUE_PATH = path4.join(os3.homedir(), ".exe-os", "intercom-queue.json");
889
923
  MAX_RETRIES = 5;
890
924
  TTL_MS = 60 * 60 * 1e3;
925
+ _draining = false;
891
926
  INTERCOM_LOG = path4.join(os3.homedir(), ".exe-os", "intercom.log");
892
927
  }
893
928
  });
@@ -4781,6 +4816,17 @@ var init_agent_symlinks = __esm({
4781
4816
  });
4782
4817
 
4783
4818
  // src/lib/notifications.ts
4819
+ var notifications_exports = {};
4820
+ __export(notifications_exports, {
4821
+ cleanupOldNotifications: () => cleanupOldNotifications,
4822
+ formatNotifications: () => formatNotifications,
4823
+ markAsRead: () => markAsRead,
4824
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
4825
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
4826
+ migrateJsonNotifications: () => migrateJsonNotifications,
4827
+ readUnreadNotifications: () => readUnreadNotifications,
4828
+ writeNotification: () => writeNotification
4829
+ });
4784
4830
  import crypto2 from "crypto";
4785
4831
  import path12 from "path";
4786
4832
  import os9 from "os";
@@ -4817,6 +4863,52 @@ async function writeNotification(notification) {
4817
4863
  `);
4818
4864
  }
4819
4865
  }
4866
+ async function readUnreadNotifications(agentFilter, sessionScope) {
4867
+ try {
4868
+ const client = getClient();
4869
+ const conditions = ["read = 0"];
4870
+ const args = [];
4871
+ const scope = strictSessionScopeFilter(sessionScope);
4872
+ if (agentFilter) {
4873
+ conditions.push("agent_id = ?");
4874
+ args.push(agentFilter);
4875
+ }
4876
+ const result = await client.execute({
4877
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
4878
+ FROM notifications
4879
+ WHERE ${conditions.join(" AND ")}${scope.sql}
4880
+ ORDER BY created_at ASC`,
4881
+ args: [...args, ...scope.args]
4882
+ });
4883
+ return result.rows.map((r) => ({
4884
+ id: String(r.id),
4885
+ agentId: String(r.agent_id),
4886
+ agentRole: String(r.agent_role),
4887
+ event: String(r.event),
4888
+ project: String(r.project),
4889
+ summary: String(r.summary),
4890
+ taskFile: r.task_file ? String(r.task_file) : void 0,
4891
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
4892
+ timestamp: String(r.created_at),
4893
+ read: false
4894
+ }));
4895
+ } catch {
4896
+ return [];
4897
+ }
4898
+ }
4899
+ async function markAsRead(ids, sessionScope) {
4900
+ if (ids.length === 0) return;
4901
+ try {
4902
+ const client = getClient();
4903
+ const placeholders = ids.map(() => "?").join(", ");
4904
+ const scope = strictSessionScopeFilter(sessionScope);
4905
+ await client.execute({
4906
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
4907
+ args: [...ids, ...scope.args]
4908
+ });
4909
+ } catch {
4910
+ }
4911
+ }
4820
4912
  async function markAsReadByTaskFile(taskFile, sessionScope) {
4821
4913
  try {
4822
4914
  const client = getClient();
@@ -4829,11 +4921,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
4829
4921
  } catch {
4830
4922
  }
4831
4923
  }
4924
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
4925
+ try {
4926
+ const client = getClient();
4927
+ const cutoff = new Date(
4928
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
4929
+ ).toISOString();
4930
+ const scope = strictSessionScopeFilter(sessionScope);
4931
+ const result = await client.execute({
4932
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
4933
+ args: [cutoff, ...scope.args]
4934
+ });
4935
+ return result.rowsAffected;
4936
+ } catch {
4937
+ return 0;
4938
+ }
4939
+ }
4940
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
4941
+ try {
4942
+ const client = getClient();
4943
+ const scope = strictSessionScopeFilter(sessionScope);
4944
+ const result = await client.execute({
4945
+ sql: `UPDATE notifications SET read = 1
4946
+ WHERE read = 0
4947
+ AND task_file IS NOT NULL
4948
+ ${scope.sql}
4949
+ AND task_file IN (
4950
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
4951
+ )`,
4952
+ args: [...scope.args, ...scope.args]
4953
+ });
4954
+ return result.rowsAffected;
4955
+ } catch {
4956
+ return 0;
4957
+ }
4958
+ }
4959
+ function formatNotifications(notifications) {
4960
+ if (notifications.length === 0) return "";
4961
+ const grouped = /* @__PURE__ */ new Map();
4962
+ for (const n of notifications) {
4963
+ const key = `${n.agentId}|${n.agentRole}`;
4964
+ if (!grouped.has(key)) grouped.set(key, []);
4965
+ grouped.get(key).push(n);
4966
+ }
4967
+ const lines = [];
4968
+ lines.push(`## Notifications (${notifications.length} unread)
4969
+ `);
4970
+ for (const [key, items] of grouped) {
4971
+ const [agentId, agentRole] = key.split("|");
4972
+ lines.push(`**${agentId}** (${agentRole}):`);
4973
+ for (const item of items) {
4974
+ const ago = formatTimeAgo(item.timestamp);
4975
+ const icon = eventIcon(item.event);
4976
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
4977
+ }
4978
+ lines.push("");
4979
+ }
4980
+ return lines.join("\n");
4981
+ }
4982
+ async function migrateJsonNotifications() {
4983
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path12.join(os9.homedir(), ".exe-os");
4984
+ const notifDir = path12.join(base, "notifications");
4985
+ if (!existsSync13(notifDir)) return 0;
4986
+ let migrated = 0;
4987
+ try {
4988
+ const files = readdirSync(notifDir).filter((f) => f.endsWith(".json"));
4989
+ if (files.length === 0) return 0;
4990
+ const client = getClient();
4991
+ for (const file of files) {
4992
+ try {
4993
+ const filePath = path12.join(notifDir, file);
4994
+ const data = JSON.parse(readFileSync10(filePath, "utf8"));
4995
+ await client.execute({
4996
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
4997
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4998
+ args: [
4999
+ crypto2.randomUUID(),
5000
+ data.agentId ?? "unknown",
5001
+ data.agentRole ?? "unknown",
5002
+ data.event ?? "session_summary",
5003
+ data.project ?? "unknown",
5004
+ data.summary ?? "",
5005
+ data.taskFile ?? null,
5006
+ null,
5007
+ data.read ? 1 : 0,
5008
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
5009
+ ]
5010
+ });
5011
+ unlinkSync4(filePath);
5012
+ migrated++;
5013
+ } catch {
5014
+ }
5015
+ }
5016
+ try {
5017
+ const remaining = readdirSync(notifDir);
5018
+ if (remaining.length === 0) {
5019
+ rmdirSync(notifDir);
5020
+ }
5021
+ } catch {
5022
+ }
5023
+ } catch {
5024
+ }
5025
+ return migrated;
5026
+ }
5027
+ function eventIcon(event) {
5028
+ switch (event) {
5029
+ case "task_complete":
5030
+ return "Completed:";
5031
+ case "task_needs_fix":
5032
+ return "Needs fix:";
5033
+ case "session_summary":
5034
+ return "Session:";
5035
+ case "error_spike":
5036
+ return "Errors:";
5037
+ case "orphan_task":
5038
+ return "Orphan:";
5039
+ case "subtasks_complete":
5040
+ return "Subtasks done:";
5041
+ case "capacity_relaunch":
5042
+ return "Relaunched:";
5043
+ }
5044
+ }
5045
+ function formatTimeAgo(timestamp) {
5046
+ const diffMs = Date.now() - new Date(timestamp).getTime();
5047
+ const mins = Math.floor(diffMs / 6e4);
5048
+ if (mins < 1) return "just now";
5049
+ if (mins < 60) return `${mins}m ago`;
5050
+ const hours = Math.floor(mins / 60);
5051
+ if (hours < 24) return `${hours}h ago`;
5052
+ const days = Math.floor(hours / 24);
5053
+ return `${days}d ago`;
5054
+ }
5055
+ var CLEANUP_DAYS;
4832
5056
  var init_notifications = __esm({
4833
5057
  "src/lib/notifications.ts"() {
4834
5058
  "use strict";
4835
5059
  init_database();
4836
5060
  init_task_scope();
5061
+ CLEANUP_DAYS = 7;
4837
5062
  }
4838
5063
  });
4839
5064
 
@@ -5888,7 +6113,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5888
6113
  taskFile
5889
6114
  });
5890
6115
  const originalPriority = String(row.priority).toLowerCase();
5891
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
6116
+ const resultLower = result?.toLowerCase() ?? "";
6117
+ const hasTestEvidence = (
6118
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
6119
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
6120
+ );
6121
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
6122
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
5892
6123
  if (!autoApprove) {
5893
6124
  try {
5894
6125
  const key = getSessionKey();
@@ -5896,6 +6127,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5896
6127
  if (exeSession) {
5897
6128
  sendIntercom(exeSession);
5898
6129
  }
6130
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
6131
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6132
+ if (exeSession) {
6133
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
6134
+ sendIntercom(reviewerSession);
6135
+ }
6136
+ }
5899
6137
  } catch {
5900
6138
  }
5901
6139
  }
@@ -6671,6 +6909,20 @@ async function updateTask(input) {
6671
6909
  notifyTaskDone();
6672
6910
  }
6673
6911
  await markTaskNotificationsRead(taskFile);
6912
+ if (input.status === "needs_review" && !isCoordinator) {
6913
+ try {
6914
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
6915
+ await writeNotification2({
6916
+ agentId: String(row.assigned_to),
6917
+ agentRole: String(row.assigned_to),
6918
+ event: "task_complete",
6919
+ project: String(row.project_name),
6920
+ summary: `"${String(row.title)}" is ready for review`,
6921
+ taskFile
6922
+ });
6923
+ } catch {
6924
+ }
6925
+ }
6674
6926
  if (input.status === "done" || input.status === "closed") {
6675
6927
  try {
6676
6928
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -7103,18 +7355,31 @@ function acquireSpawnLock2(sessionName) {
7103
7355
  mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
7104
7356
  }
7105
7357
  const lockFile = spawnLockPath(sessionName);
7106
- if (existsSync16(lockFile)) {
7107
- try {
7108
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7109
- const age = Date.now() - lock.timestamp;
7110
- if (isProcessAlive(lock.pid) && age < 6e4) {
7111
- return false;
7112
- }
7113
- } catch {
7358
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
7359
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
7360
+ const { constants } = __require("fs");
7361
+ try {
7362
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
7363
+ writeSync(fd, lockData);
7364
+ closeSync3(fd);
7365
+ return true;
7366
+ } catch (err) {
7367
+ if (err?.code !== "EEXIST") {
7368
+ return true;
7114
7369
  }
7115
7370
  }
7116
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
7117
- return true;
7371
+ try {
7372
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7373
+ const age = Date.now() - lock.timestamp;
7374
+ if (isProcessAlive(lock.pid) && age < 6e4) {
7375
+ return false;
7376
+ }
7377
+ writeFileSync8(lockFile, lockData);
7378
+ return true;
7379
+ } catch {
7380
+ writeFileSync8(lockFile, lockData);
7381
+ return true;
7382
+ }
7118
7383
  }
7119
7384
  function releaseSpawnLock2(sessionName) {
7120
7385
  try {
@@ -7193,6 +7458,21 @@ function parseParentExe(sessionName, agentId) {
7193
7458
  function extractRootExe(name) {
7194
7459
  if (!name) return null;
7195
7460
  if (!name.includes("-")) return name;
7461
+ try {
7462
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
7463
+ if (roster.length > 0) {
7464
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
7465
+ for (const agentName of sortedNames) {
7466
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7467
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
7468
+ const match = name.match(regex);
7469
+ if (match) {
7470
+ return extractRootExe(match[1]);
7471
+ }
7472
+ }
7473
+ }
7474
+ } catch {
7475
+ }
7196
7476
  const parts = name.split("-").filter(Boolean);
7197
7477
  return parts.length > 0 ? parts[parts.length - 1] : null;
7198
7478
  }
@@ -7211,6 +7491,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7211
7491
  function getParentExe(sessionKey) {
7212
7492
  try {
7213
7493
  const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7494
+ if (data.registeredAt) {
7495
+ const age = Date.now() - new Date(data.registeredAt).getTime();
7496
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
7497
+ }
7214
7498
  return data.parentExe || null;
7215
7499
  } catch {
7216
7500
  return null;
@@ -7759,7 +8043,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7759
8043
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7760
8044
  } catch {
7761
8045
  }
7762
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
8046
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
7763
8047
  if (ccProvider !== DEFAULT_PROVIDER) {
7764
8048
  const cfg = PROVIDER_TABLE[ccProvider];
7765
8049
  if (cfg?.apiKeyEnv) {
@@ -7794,10 +8078,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7794
8078
  }
7795
8079
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
7796
8080
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
7797
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
7798
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
7799
- ccModel += "[1m]";
7800
- }
8081
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
8082
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
7801
8083
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
7802
8084
  }
7803
8085
  }
@@ -7898,7 +8180,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7898
8180
  releaseSpawnLock2(sessionName);
7899
8181
  return { sessionName };
7900
8182
  }
7901
- 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;
8183
+ 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;
7902
8184
  var init_tmux_routing = __esm({
7903
8185
  "src/lib/tmux-routing.ts"() {
7904
8186
  "use strict";
@@ -7918,6 +8200,7 @@ var init_tmux_routing = __esm({
7918
8200
  SESSION_CACHE = path18.join(os11.homedir(), ".exe-os", "session-cache");
7919
8201
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7920
8202
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8203
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
7921
8204
  VERIFY_PANE_LINES = 200;
7922
8205
  INTERCOM_DEBOUNCE_MS = 3e4;
7923
8206
  CODEX_DEBOUNCE_MS = 12e4;
@@ -7962,7 +8245,7 @@ var init_task_scope = __esm({
7962
8245
  });
7963
8246
 
7964
8247
  // src/lib/keychain.ts
7965
- import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
8248
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2, rename, copyFile } from "fs/promises";
7966
8249
  import { existsSync as existsSync17, statSync as statSync3 } from "fs";
7967
8250
  import { execSync as execSync8 } from "child_process";
7968
8251
  import path19 from "path";
@@ -7997,12 +8280,14 @@ function linuxSecretAvailable() {
7997
8280
  function isRootOnlyTrustedServerKeyFile(keyPath) {
7998
8281
  if (process.platform !== "linux") return false;
7999
8282
  try {
8000
- const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
8001
8283
  const st = statSync3(keyPath);
8002
8284
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
8285
+ const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
8003
8286
  if (uid === 0) return true;
8004
8287
  const exeOsDir = process.env.EXE_OS_DIR;
8005
- return Boolean(exeOsDir && path19.resolve(keyPath).startsWith(path19.resolve(exeOsDir) + path19.sep));
8288
+ if (exeOsDir && path19.resolve(keyPath).startsWith(path19.resolve(exeOsDir) + path19.sep)) return true;
8289
+ if (!linuxSecretAvailable()) return true;
8290
+ return false;
8006
8291
  } catch {
8007
8292
  return false;
8008
8293
  }
@@ -8152,15 +8437,25 @@ async function writeMachineBoundFileFallback(b64) {
8152
8437
  await mkdir4(dir, { recursive: true });
8153
8438
  const keyPath = getKeyPath();
8154
8439
  const machineKey = deriveMachineKey();
8155
- if (machineKey) {
8156
- const encrypted = encryptWithMachineKey(b64, machineKey);
8157
- await writeFile5(keyPath, encrypted + "\n", "utf-8");
8158
- await chmod2(keyPath, 384);
8159
- return "encrypted";
8440
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
8441
+ const result = machineKey ? "encrypted" : "plaintext";
8442
+ const tmpPath = keyPath + ".tmp";
8443
+ try {
8444
+ if (existsSync17(keyPath)) {
8445
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
8446
+ });
8447
+ }
8448
+ await writeFile5(tmpPath, content, "utf-8");
8449
+ await chmod2(tmpPath, 384);
8450
+ await rename(tmpPath, keyPath);
8451
+ } catch (err) {
8452
+ try {
8453
+ await unlink(tmpPath);
8454
+ } catch {
8455
+ }
8456
+ throw err;
8160
8457
  }
8161
- await writeFile5(keyPath, b64 + "\n", "utf-8");
8162
- await chmod2(keyPath, 384);
8163
- return "plaintext";
8458
+ return result;
8164
8459
  }
8165
8460
  async function getMasterKey() {
8166
8461
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -8227,7 +8522,7 @@ async function getMasterKey() {
8227
8522
  b64Value = content;
8228
8523
  }
8229
8524
  const key = Buffer.from(b64Value, "base64");
8230
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
8525
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
8231
8526
  return key;
8232
8527
  }
8233
8528
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);