@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
  });
@@ -4703,6 +4738,17 @@ var init_agent_symlinks = __esm({
4703
4738
  });
4704
4739
 
4705
4740
  // src/lib/notifications.ts
4741
+ var notifications_exports = {};
4742
+ __export(notifications_exports, {
4743
+ cleanupOldNotifications: () => cleanupOldNotifications,
4744
+ formatNotifications: () => formatNotifications,
4745
+ markAsRead: () => markAsRead,
4746
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
4747
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
4748
+ migrateJsonNotifications: () => migrateJsonNotifications,
4749
+ readUnreadNotifications: () => readUnreadNotifications,
4750
+ writeNotification: () => writeNotification
4751
+ });
4706
4752
  import crypto2 from "crypto";
4707
4753
  import path12 from "path";
4708
4754
  import os9 from "os";
@@ -4739,6 +4785,52 @@ async function writeNotification(notification) {
4739
4785
  `);
4740
4786
  }
4741
4787
  }
4788
+ async function readUnreadNotifications(agentFilter, sessionScope) {
4789
+ try {
4790
+ const client = getClient();
4791
+ const conditions = ["read = 0"];
4792
+ const args2 = [];
4793
+ const scope = strictSessionScopeFilter(sessionScope);
4794
+ if (agentFilter) {
4795
+ conditions.push("agent_id = ?");
4796
+ args2.push(agentFilter);
4797
+ }
4798
+ const result = await client.execute({
4799
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
4800
+ FROM notifications
4801
+ WHERE ${conditions.join(" AND ")}${scope.sql}
4802
+ ORDER BY created_at ASC`,
4803
+ args: [...args2, ...scope.args]
4804
+ });
4805
+ return result.rows.map((r) => ({
4806
+ id: String(r.id),
4807
+ agentId: String(r.agent_id),
4808
+ agentRole: String(r.agent_role),
4809
+ event: String(r.event),
4810
+ project: String(r.project),
4811
+ summary: String(r.summary),
4812
+ taskFile: r.task_file ? String(r.task_file) : void 0,
4813
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
4814
+ timestamp: String(r.created_at),
4815
+ read: false
4816
+ }));
4817
+ } catch {
4818
+ return [];
4819
+ }
4820
+ }
4821
+ async function markAsRead(ids, sessionScope) {
4822
+ if (ids.length === 0) return;
4823
+ try {
4824
+ const client = getClient();
4825
+ const placeholders = ids.map(() => "?").join(", ");
4826
+ const scope = strictSessionScopeFilter(sessionScope);
4827
+ await client.execute({
4828
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
4829
+ args: [...ids, ...scope.args]
4830
+ });
4831
+ } catch {
4832
+ }
4833
+ }
4742
4834
  async function markAsReadByTaskFile(taskFile, sessionScope) {
4743
4835
  try {
4744
4836
  const client = getClient();
@@ -4751,11 +4843,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
4751
4843
  } catch {
4752
4844
  }
4753
4845
  }
4846
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
4847
+ try {
4848
+ const client = getClient();
4849
+ const cutoff = new Date(
4850
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
4851
+ ).toISOString();
4852
+ const scope = strictSessionScopeFilter(sessionScope);
4853
+ const result = await client.execute({
4854
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
4855
+ args: [cutoff, ...scope.args]
4856
+ });
4857
+ return result.rowsAffected;
4858
+ } catch {
4859
+ return 0;
4860
+ }
4861
+ }
4862
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
4863
+ try {
4864
+ const client = getClient();
4865
+ const scope = strictSessionScopeFilter(sessionScope);
4866
+ const result = await client.execute({
4867
+ sql: `UPDATE notifications SET read = 1
4868
+ WHERE read = 0
4869
+ AND task_file IS NOT NULL
4870
+ ${scope.sql}
4871
+ AND task_file IN (
4872
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
4873
+ )`,
4874
+ args: [...scope.args, ...scope.args]
4875
+ });
4876
+ return result.rowsAffected;
4877
+ } catch {
4878
+ return 0;
4879
+ }
4880
+ }
4881
+ function formatNotifications(notifications) {
4882
+ if (notifications.length === 0) return "";
4883
+ const grouped = /* @__PURE__ */ new Map();
4884
+ for (const n of notifications) {
4885
+ const key = `${n.agentId}|${n.agentRole}`;
4886
+ if (!grouped.has(key)) grouped.set(key, []);
4887
+ grouped.get(key).push(n);
4888
+ }
4889
+ const lines = [];
4890
+ lines.push(`## Notifications (${notifications.length} unread)
4891
+ `);
4892
+ for (const [key, items] of grouped) {
4893
+ const [agentId, agentRole] = key.split("|");
4894
+ lines.push(`**${agentId}** (${agentRole}):`);
4895
+ for (const item of items) {
4896
+ const ago = formatTimeAgo(item.timestamp);
4897
+ const icon = eventIcon(item.event);
4898
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
4899
+ }
4900
+ lines.push("");
4901
+ }
4902
+ return lines.join("\n");
4903
+ }
4904
+ async function migrateJsonNotifications() {
4905
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path12.join(os9.homedir(), ".exe-os");
4906
+ const notifDir = path12.join(base, "notifications");
4907
+ if (!existsSync13(notifDir)) return 0;
4908
+ let migrated = 0;
4909
+ try {
4910
+ const files = readdirSync(notifDir).filter((f) => f.endsWith(".json"));
4911
+ if (files.length === 0) return 0;
4912
+ const client = getClient();
4913
+ for (const file of files) {
4914
+ try {
4915
+ const filePath = path12.join(notifDir, file);
4916
+ const data = JSON.parse(readFileSync10(filePath, "utf8"));
4917
+ await client.execute({
4918
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
4919
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4920
+ args: [
4921
+ crypto2.randomUUID(),
4922
+ data.agentId ?? "unknown",
4923
+ data.agentRole ?? "unknown",
4924
+ data.event ?? "session_summary",
4925
+ data.project ?? "unknown",
4926
+ data.summary ?? "",
4927
+ data.taskFile ?? null,
4928
+ null,
4929
+ data.read ? 1 : 0,
4930
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
4931
+ ]
4932
+ });
4933
+ unlinkSync4(filePath);
4934
+ migrated++;
4935
+ } catch {
4936
+ }
4937
+ }
4938
+ try {
4939
+ const remaining = readdirSync(notifDir);
4940
+ if (remaining.length === 0) {
4941
+ rmdirSync(notifDir);
4942
+ }
4943
+ } catch {
4944
+ }
4945
+ } catch {
4946
+ }
4947
+ return migrated;
4948
+ }
4949
+ function eventIcon(event) {
4950
+ switch (event) {
4951
+ case "task_complete":
4952
+ return "Completed:";
4953
+ case "task_needs_fix":
4954
+ return "Needs fix:";
4955
+ case "session_summary":
4956
+ return "Session:";
4957
+ case "error_spike":
4958
+ return "Errors:";
4959
+ case "orphan_task":
4960
+ return "Orphan:";
4961
+ case "subtasks_complete":
4962
+ return "Subtasks done:";
4963
+ case "capacity_relaunch":
4964
+ return "Relaunched:";
4965
+ }
4966
+ }
4967
+ function formatTimeAgo(timestamp) {
4968
+ const diffMs = Date.now() - new Date(timestamp).getTime();
4969
+ const mins = Math.floor(diffMs / 6e4);
4970
+ if (mins < 1) return "just now";
4971
+ if (mins < 60) return `${mins}m ago`;
4972
+ const hours = Math.floor(mins / 60);
4973
+ if (hours < 24) return `${hours}h ago`;
4974
+ const days = Math.floor(hours / 24);
4975
+ return `${days}d ago`;
4976
+ }
4977
+ var CLEANUP_DAYS;
4754
4978
  var init_notifications = __esm({
4755
4979
  "src/lib/notifications.ts"() {
4756
4980
  "use strict";
4757
4981
  init_database();
4758
4982
  init_task_scope();
4983
+ CLEANUP_DAYS = 7;
4759
4984
  }
4760
4985
  });
4761
4986
 
@@ -5817,7 +6042,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5817
6042
  taskFile
5818
6043
  });
5819
6044
  const originalPriority = String(row.priority).toLowerCase();
5820
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
6045
+ const resultLower = result?.toLowerCase() ?? "";
6046
+ const hasTestEvidence = (
6047
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
6048
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
6049
+ );
6050
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
6051
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
5821
6052
  if (!autoApprove) {
5822
6053
  try {
5823
6054
  const key = getSessionKey();
@@ -5825,6 +6056,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5825
6056
  if (exeSession) {
5826
6057
  sendIntercom(exeSession);
5827
6058
  }
6059
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
6060
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6061
+ if (exeSession) {
6062
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
6063
+ sendIntercom(reviewerSession);
6064
+ }
6065
+ }
5828
6066
  } catch {
5829
6067
  }
5830
6068
  }
@@ -6600,6 +6838,20 @@ async function updateTask(input) {
6600
6838
  notifyTaskDone();
6601
6839
  }
6602
6840
  await markTaskNotificationsRead(taskFile);
6841
+ if (input.status === "needs_review" && !isCoordinator) {
6842
+ try {
6843
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
6844
+ await writeNotification2({
6845
+ agentId: String(row.assigned_to),
6846
+ agentRole: String(row.assigned_to),
6847
+ event: "task_complete",
6848
+ project: String(row.project_name),
6849
+ summary: `"${String(row.title)}" is ready for review`,
6850
+ taskFile
6851
+ });
6852
+ } catch {
6853
+ }
6854
+ }
6603
6855
  if (input.status === "done" || input.status === "closed") {
6604
6856
  try {
6605
6857
  await cascadeUnblock(taskId, input.baseDir, now);
@@ -7032,18 +7284,31 @@ function acquireSpawnLock2(sessionName) {
7032
7284
  mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
7033
7285
  }
7034
7286
  const lockFile = spawnLockPath(sessionName);
7035
- if (existsSync16(lockFile)) {
7036
- try {
7037
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7038
- const age = Date.now() - lock.timestamp;
7039
- if (isProcessAlive(lock.pid) && age < 6e4) {
7040
- return false;
7041
- }
7042
- } catch {
7287
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
7288
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
7289
+ const { constants } = __require("fs");
7290
+ try {
7291
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
7292
+ writeSync(fd, lockData);
7293
+ closeSync3(fd);
7294
+ return true;
7295
+ } catch (err) {
7296
+ if (err?.code !== "EEXIST") {
7297
+ return true;
7043
7298
  }
7044
7299
  }
7045
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
7046
- return true;
7300
+ try {
7301
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7302
+ const age = Date.now() - lock.timestamp;
7303
+ if (isProcessAlive(lock.pid) && age < 6e4) {
7304
+ return false;
7305
+ }
7306
+ writeFileSync8(lockFile, lockData);
7307
+ return true;
7308
+ } catch {
7309
+ writeFileSync8(lockFile, lockData);
7310
+ return true;
7311
+ }
7047
7312
  }
7048
7313
  function releaseSpawnLock2(sessionName) {
7049
7314
  try {
@@ -7122,6 +7387,21 @@ function parseParentExe(sessionName, agentId) {
7122
7387
  function extractRootExe(name) {
7123
7388
  if (!name) return null;
7124
7389
  if (!name.includes("-")) return name;
7390
+ try {
7391
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
7392
+ if (roster.length > 0) {
7393
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
7394
+ for (const agentName of sortedNames) {
7395
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7396
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
7397
+ const match = name.match(regex);
7398
+ if (match) {
7399
+ return extractRootExe(match[1]);
7400
+ }
7401
+ }
7402
+ }
7403
+ } catch {
7404
+ }
7125
7405
  const parts = name.split("-").filter(Boolean);
7126
7406
  return parts.length > 0 ? parts[parts.length - 1] : null;
7127
7407
  }
@@ -7140,6 +7420,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7140
7420
  function getParentExe(sessionKey) {
7141
7421
  try {
7142
7422
  const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7423
+ if (data.registeredAt) {
7424
+ const age = Date.now() - new Date(data.registeredAt).getTime();
7425
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
7426
+ }
7143
7427
  return data.parentExe || null;
7144
7428
  } catch {
7145
7429
  return null;
@@ -7688,7 +7972,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7688
7972
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7689
7973
  } catch {
7690
7974
  }
7691
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
7975
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
7692
7976
  if (ccProvider !== DEFAULT_PROVIDER) {
7693
7977
  const cfg = PROVIDER_TABLE[ccProvider];
7694
7978
  if (cfg?.apiKeyEnv) {
@@ -7723,10 +8007,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7723
8007
  }
7724
8008
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
7725
8009
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
7726
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
7727
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
7728
- ccModel += "[1m]";
7729
- }
8010
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
8011
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
7730
8012
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
7731
8013
  }
7732
8014
  }
@@ -7827,7 +8109,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7827
8109
  releaseSpawnLock2(sessionName);
7828
8110
  return { sessionName };
7829
8111
  }
7830
- 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;
8112
+ 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;
7831
8113
  var init_tmux_routing = __esm({
7832
8114
  "src/lib/tmux-routing.ts"() {
7833
8115
  "use strict";
@@ -7847,6 +8129,7 @@ var init_tmux_routing = __esm({
7847
8129
  SESSION_CACHE = path18.join(os11.homedir(), ".exe-os", "session-cache");
7848
8130
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7849
8131
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8132
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
7850
8133
  VERIFY_PANE_LINES = 200;
7851
8134
  INTERCOM_DEBOUNCE_MS = 3e4;
7852
8135
  CODEX_DEBOUNCE_MS = 12e4;
@@ -7891,7 +8174,7 @@ var init_task_scope = __esm({
7891
8174
  });
7892
8175
 
7893
8176
  // src/lib/keychain.ts
7894
- import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
8177
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2, rename, copyFile } from "fs/promises";
7895
8178
  import { existsSync as existsSync17, statSync as statSync3 } from "fs";
7896
8179
  import { execSync as execSync8 } from "child_process";
7897
8180
  import path19 from "path";
@@ -7926,12 +8209,14 @@ function linuxSecretAvailable() {
7926
8209
  function isRootOnlyTrustedServerKeyFile(keyPath) {
7927
8210
  if (process.platform !== "linux") return false;
7928
8211
  try {
7929
- const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
7930
8212
  const st = statSync3(keyPath);
7931
8213
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
8214
+ const uid = typeof os12.userInfo().uid === "number" ? os12.userInfo().uid : -1;
7932
8215
  if (uid === 0) return true;
7933
8216
  const exeOsDir = process.env.EXE_OS_DIR;
7934
- return Boolean(exeOsDir && path19.resolve(keyPath).startsWith(path19.resolve(exeOsDir) + path19.sep));
8217
+ if (exeOsDir && path19.resolve(keyPath).startsWith(path19.resolve(exeOsDir) + path19.sep)) return true;
8218
+ if (!linuxSecretAvailable()) return true;
8219
+ return false;
7935
8220
  } catch {
7936
8221
  return false;
7937
8222
  }
@@ -8081,15 +8366,25 @@ async function writeMachineBoundFileFallback(b64) {
8081
8366
  await mkdir4(dir, { recursive: true });
8082
8367
  const keyPath = getKeyPath();
8083
8368
  const machineKey = deriveMachineKey();
8084
- if (machineKey) {
8085
- const encrypted = encryptWithMachineKey(b64, machineKey);
8086
- await writeFile5(keyPath, encrypted + "\n", "utf-8");
8087
- await chmod2(keyPath, 384);
8088
- return "encrypted";
8369
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
8370
+ const result = machineKey ? "encrypted" : "plaintext";
8371
+ const tmpPath = keyPath + ".tmp";
8372
+ try {
8373
+ if (existsSync17(keyPath)) {
8374
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
8375
+ });
8376
+ }
8377
+ await writeFile5(tmpPath, content, "utf-8");
8378
+ await chmod2(tmpPath, 384);
8379
+ await rename(tmpPath, keyPath);
8380
+ } catch (err) {
8381
+ try {
8382
+ await unlink(tmpPath);
8383
+ } catch {
8384
+ }
8385
+ throw err;
8089
8386
  }
8090
- await writeFile5(keyPath, b64 + "\n", "utf-8");
8091
- await chmod2(keyPath, 384);
8092
- return "plaintext";
8387
+ return result;
8093
8388
  }
8094
8389
  async function getMasterKey() {
8095
8390
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -8156,7 +8451,7 @@ async function getMasterKey() {
8156
8451
  b64Value = content;
8157
8452
  }
8158
8453
  const key = Buffer.from(b64Value, "base64");
8159
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
8454
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
8160
8455
  return key;
8161
8456
  }
8162
8457
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);