@askexenow/exe-os 0.8.83 → 0.8.85

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 (95) hide show
  1. package/dist/bin/backfill-conversations.js +746 -595
  2. package/dist/bin/backfill-responses.js +745 -594
  3. package/dist/bin/backfill-vectors.js +312 -226
  4. package/dist/bin/cleanup-stale-review-tasks.js +97 -2
  5. package/dist/bin/cli.js +14350 -12518
  6. package/dist/bin/exe-agent.js +97 -88
  7. package/dist/bin/exe-assign.js +1003 -854
  8. package/dist/bin/exe-boot.js +1257 -320
  9. package/dist/bin/exe-call.js +10 -0
  10. package/dist/bin/exe-cloud.js +29 -6
  11. package/dist/bin/exe-dispatch.js +210 -34
  12. package/dist/bin/exe-doctor.js +403 -6
  13. package/dist/bin/exe-export-behaviors.js +175 -72
  14. package/dist/bin/exe-forget.js +97 -2
  15. package/dist/bin/exe-gateway.js +550 -171
  16. package/dist/bin/exe-healthcheck.js +1 -0
  17. package/dist/bin/exe-heartbeat.js +100 -5
  18. package/dist/bin/exe-kill.js +175 -72
  19. package/dist/bin/exe-launch-agent.js +189 -76
  20. package/dist/bin/exe-link.js +902 -80
  21. package/dist/bin/exe-new-employee.js +38 -8
  22. package/dist/bin/exe-pending-messages.js +96 -2
  23. package/dist/bin/exe-pending-notifications.js +97 -2
  24. package/dist/bin/exe-pending-reviews.js +98 -3
  25. package/dist/bin/exe-rename.js +564 -23
  26. package/dist/bin/exe-review.js +231 -73
  27. package/dist/bin/exe-search.js +989 -226
  28. package/dist/bin/exe-session-cleanup.js +4806 -1665
  29. package/dist/bin/exe-settings.js +20 -5
  30. package/dist/bin/exe-status.js +97 -2
  31. package/dist/bin/exe-team.js +97 -2
  32. package/dist/bin/git-sweep.js +899 -207
  33. package/dist/bin/graph-backfill.js +175 -72
  34. package/dist/bin/graph-export.js +175 -72
  35. package/dist/bin/install.js +38 -7
  36. package/dist/bin/list-providers.js +1 -0
  37. package/dist/bin/scan-tasks.js +904 -211
  38. package/dist/bin/setup.js +867 -268
  39. package/dist/bin/shard-migrate.js +175 -72
  40. package/dist/bin/update.js +1 -0
  41. package/dist/bin/wiki-sync.js +175 -72
  42. package/dist/gateway/index.js +548 -166
  43. package/dist/hooks/bug-report-worker.js +208 -23
  44. package/dist/hooks/commit-complete.js +897 -205
  45. package/dist/hooks/error-recall.js +988 -226
  46. package/dist/hooks/ingest-worker.js +1638 -1194
  47. package/dist/hooks/ingest.js +3 -0
  48. package/dist/hooks/instructions-loaded.js +707 -97
  49. package/dist/hooks/notification.js +699 -89
  50. package/dist/hooks/post-compact.js +714 -104
  51. package/dist/hooks/pre-compact.js +897 -205
  52. package/dist/hooks/pre-tool-use.js +742 -123
  53. package/dist/hooks/prompt-ingest-worker.js +242 -101
  54. package/dist/hooks/prompt-submit.js +995 -233
  55. package/dist/hooks/response-ingest-worker.js +242 -101
  56. package/dist/hooks/session-end.js +3941 -400
  57. package/dist/hooks/session-start.js +1001 -226
  58. package/dist/hooks/stop.js +725 -115
  59. package/dist/hooks/subagent-stop.js +714 -104
  60. package/dist/hooks/summary-worker.js +1964 -1330
  61. package/dist/index.js +1651 -1053
  62. package/dist/lib/cloud-sync.js +907 -86
  63. package/dist/lib/consolidation.js +2 -1
  64. package/dist/lib/database.js +642 -87
  65. package/dist/lib/db-daemon-client.js +503 -0
  66. package/dist/lib/device-registry.js +547 -7
  67. package/dist/lib/embedder.js +14 -28
  68. package/dist/lib/employee-templates.js +84 -74
  69. package/dist/lib/employees.js +9 -0
  70. package/dist/lib/exe-daemon-client.js +16 -29
  71. package/dist/lib/exe-daemon.js +1955 -922
  72. package/dist/lib/hybrid-search.js +988 -226
  73. package/dist/lib/identity.js +87 -67
  74. package/dist/lib/keychain.js +9 -1
  75. package/dist/lib/messaging.js +8 -1
  76. package/dist/lib/reminders.js +91 -74
  77. package/dist/lib/schedules.js +96 -2
  78. package/dist/lib/skill-learning.js +103 -85
  79. package/dist/lib/store.js +234 -73
  80. package/dist/lib/tasks.js +111 -22
  81. package/dist/lib/tmux-routing.js +120 -31
  82. package/dist/lib/token-spend.js +273 -0
  83. package/dist/lib/ws-client.js +11 -0
  84. package/dist/mcp/server.js +5222 -475
  85. package/dist/mcp/tools/complete-reminder.js +94 -77
  86. package/dist/mcp/tools/create-reminder.js +94 -77
  87. package/dist/mcp/tools/create-task.js +120 -22
  88. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  89. package/dist/mcp/tools/list-reminders.js +94 -77
  90. package/dist/mcp/tools/list-tasks.js +31 -1
  91. package/dist/mcp/tools/send-message.js +8 -1
  92. package/dist/mcp/tools/update-task.js +39 -10
  93. package/dist/runtime/index.js +911 -219
  94. package/dist/tui/App.js +997 -295
  95. package/package.json +6 -1
@@ -107,6 +107,7 @@ __export(employees_exports, {
107
107
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
108
108
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
109
109
  addEmployee: () => addEmployee,
110
+ baseAgentName: () => baseAgentName,
110
111
  canCoordinate: () => canCoordinate,
111
112
  getCoordinatorEmployee: () => getCoordinatorEmployee,
112
113
  getCoordinatorName: () => getCoordinatorName,
@@ -203,6 +204,14 @@ function hasRole(agentName, role) {
203
204
  const emp = getEmployee(employees, agentName);
204
205
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
205
206
  }
207
+ function baseAgentName(name, employees) {
208
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
209
+ if (!match) return name;
210
+ const base = match[1];
211
+ const roster = employees ?? loadEmployeesSync();
212
+ if (getEmployee(roster, base)) return base;
213
+ return name;
214
+ }
206
215
  function isMultiInstance(agentName, employees) {
207
216
  const roster = employees ?? loadEmployeesSync();
208
217
  const emp = getEmployee(roster, agentName);
@@ -1080,6 +1089,7 @@ import { realpathSync } from "fs";
1080
1089
  import { fileURLToPath } from "url";
1081
1090
  function isMainModule(importMetaUrl) {
1082
1091
  if (process.argv[1] == null) return false;
1092
+ if (process.argv[1].includes("mcp/server")) return false;
1083
1093
  try {
1084
1094
  const scriptPath = realpathSync(process.argv[1]);
1085
1095
  const modulePath = realpathSync(fileURLToPath(importMetaUrl));
@@ -46,12 +46,20 @@ async function getMasterKey() {
46
46
  }
47
47
  const keyPath = getKeyPath();
48
48
  if (!existsSync(keyPath)) {
49
+ process.stderr.write(
50
+ `[keychain] Key not found at ${keyPath} (HOME=${os.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
51
+ `
52
+ );
49
53
  return null;
50
54
  }
51
55
  try {
52
56
  const content = await readFile(keyPath, "utf-8");
53
57
  return Buffer.from(content.trim(), "base64");
54
- } catch {
58
+ } catch (err) {
59
+ process.stderr.write(
60
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
61
+ `
62
+ );
55
63
  return null;
56
64
  }
57
65
  }
@@ -720,6 +728,19 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
720
728
  }
721
729
  });
722
730
 
731
+ // src/lib/crdt-sync.ts
732
+ import * as Y from "yjs";
733
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
734
+ import path5 from "path";
735
+ import { homedir } from "os";
736
+ var DEFAULT_STATE_PATH;
737
+ var init_crdt_sync = __esm({
738
+ "src/lib/crdt-sync.ts"() {
739
+ "use strict";
740
+ DEFAULT_STATE_PATH = path5.join(homedir(), ".exe-os", "crdt-state.bin");
741
+ }
742
+ });
743
+
723
744
  // src/bin/exe-cloud.ts
724
745
  init_keychain();
725
746
  init_config();
@@ -745,6 +766,7 @@ import { realpathSync } from "fs";
745
766
  import { fileURLToPath } from "url";
746
767
  function isMainModule(importMetaUrl) {
747
768
  if (process.argv[1] == null) return false;
769
+ if (process.argv[1].includes("mcp/server")) return false;
748
770
  try {
749
771
  const scriptPath = realpathSync(process.argv[1]);
750
772
  const modulePath = realpathSync(fileURLToPath(importMetaUrl));
@@ -756,10 +778,10 @@ function isMainModule(importMetaUrl) {
756
778
 
757
779
  // src/lib/cloud-sync.ts
758
780
  init_database();
759
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync as unlinkSync2, openSync, closeSync } from "fs";
781
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync3, appendFileSync, unlinkSync as unlinkSync3, openSync, closeSync } from "fs";
760
782
  import crypto3 from "crypto";
761
- import path5 from "path";
762
- import { homedir } from "os";
783
+ import path6 from "path";
784
+ import { homedir as homedir2 } from "os";
763
785
 
764
786
  // src/lib/crypto.ts
765
787
  import crypto2 from "crypto";
@@ -770,9 +792,10 @@ import { brotliCompressSync, brotliDecompressSync, constants } from "zlib";
770
792
  // src/lib/cloud-sync.ts
771
793
  init_license();
772
794
  init_config();
795
+ init_crdt_sync();
773
796
  init_employees();
774
797
  var LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
775
- var ROSTER_LOCK_PATH = path5.join(EXE_AI_DIR, "roster-merge.lock");
798
+ var ROSTER_LOCK_PATH = path6.join(EXE_AI_DIR, "roster-merge.lock");
776
799
  function assertSecureEndpoint(endpoint) {
777
800
  if (endpoint.startsWith("https://")) return;
778
801
  if (endpoint.startsWith("http://")) {
@@ -787,7 +810,7 @@ function assertSecureEndpoint(endpoint) {
787
810
  );
788
811
  }
789
812
  }
790
- var ROSTER_DELETIONS_PATH = path5.join(EXE_AI_DIR, "roster-deletions.json");
813
+ var ROSTER_DELETIONS_PATH = path6.join(EXE_AI_DIR, "roster-deletions.json");
791
814
 
792
815
  // src/bin/exe-cloud.ts
793
816
  var BAR = "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550";
@@ -628,6 +628,12 @@ function getClient() {
628
628
  if (!_resilientClient) {
629
629
  throw new Error("Database client not initialized. Call initDatabase() first.");
630
630
  }
631
+ if (process.env.EXE_IS_DAEMON === "1") {
632
+ return _resilientClient;
633
+ }
634
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
635
+ return _daemonClient;
636
+ }
631
637
  return _resilientClient;
632
638
  }
633
639
  function getRawClient() {
@@ -1116,6 +1122,12 @@ async function ensureSchema() {
1116
1122
  } catch {
1117
1123
  }
1118
1124
  }
1125
+ try {
1126
+ await client.execute(
1127
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1128
+ );
1129
+ } catch {
1130
+ }
1119
1131
  await client.executeMultiple(`
1120
1132
  CREATE TABLE IF NOT EXISTS entities (
1121
1133
  id TEXT PRIMARY KEY,
@@ -1168,7 +1180,30 @@ async function ensureSchema() {
1168
1180
  entity_id TEXT NOT NULL,
1169
1181
  PRIMARY KEY (hyperedge_id, entity_id)
1170
1182
  );
1183
+
1184
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1185
+ name,
1186
+ content=entities,
1187
+ content_rowid=rowid
1188
+ );
1189
+
1190
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1191
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1192
+ END;
1193
+
1194
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1195
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1196
+ END;
1197
+
1198
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1199
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1200
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1201
+ END;
1171
1202
  `);
1203
+ try {
1204
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1205
+ } catch {
1206
+ }
1172
1207
  await client.executeMultiple(`
1173
1208
  CREATE TABLE IF NOT EXISTS entity_aliases (
1174
1209
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1349,6 +1384,33 @@ async function ensureSchema() {
1349
1384
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1350
1385
  ON conversations(channel_id);
1351
1386
  `);
1387
+ await client.executeMultiple(`
1388
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1389
+ session_uuid TEXT PRIMARY KEY,
1390
+ agent_id TEXT NOT NULL,
1391
+ session_name TEXT,
1392
+ task_id TEXT,
1393
+ project_name TEXT,
1394
+ started_at TEXT NOT NULL
1395
+ );
1396
+
1397
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1398
+ ON session_agent_map(agent_id);
1399
+ `);
1400
+ try {
1401
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1402
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1403
+ await client.execute({
1404
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1405
+ SELECT session_id, agent_id, '', MIN(timestamp)
1406
+ FROM memories
1407
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1408
+ GROUP BY session_id, agent_id`,
1409
+ args: []
1410
+ });
1411
+ }
1412
+ } catch {
1413
+ }
1352
1414
  try {
1353
1415
  await client.execute({
1354
1416
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1482,8 +1544,30 @@ async function ensureSchema() {
1482
1544
  });
1483
1545
  } catch {
1484
1546
  }
1547
+ for (const col of [
1548
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1549
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1550
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1551
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1552
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1553
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1554
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1555
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1556
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1557
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1558
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1559
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1560
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1561
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1562
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1563
+ ]) {
1564
+ try {
1565
+ await client.execute(col);
1566
+ } catch {
1567
+ }
1568
+ }
1485
1569
  }
1486
- var _client, _resilientClient, initTurso;
1570
+ var _client, _resilientClient, _daemonClient, initTurso;
1487
1571
  var init_database = __esm({
1488
1572
  "src/lib/database.ts"() {
1489
1573
  "use strict";
@@ -1491,6 +1575,7 @@ var init_database = __esm({
1491
1575
  init_employees();
1492
1576
  _client = null;
1493
1577
  _resilientClient = null;
1578
+ _daemonClient = null;
1494
1579
  initTurso = initDatabase;
1495
1580
  }
1496
1581
  });
@@ -1764,6 +1849,7 @@ var init_state_bus = __esm({
1764
1849
  // src/lib/tasks-crud.ts
1765
1850
  import crypto3 from "crypto";
1766
1851
  import path8 from "path";
1852
+ import os6 from "os";
1767
1853
  import { execSync as execSync4 } from "child_process";
1768
1854
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1769
1855
  import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
@@ -1807,6 +1893,35 @@ function extractParentFromContext(contextBody) {
1807
1893
  function slugify(title) {
1808
1894
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1809
1895
  }
1896
+ function buildKeywordIndex() {
1897
+ const idx = /* @__PURE__ */ new Map();
1898
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
1899
+ for (const kw of keywords) {
1900
+ const existing = idx.get(kw) ?? [];
1901
+ existing.push(role);
1902
+ idx.set(kw, existing);
1903
+ }
1904
+ }
1905
+ return idx;
1906
+ }
1907
+ function checkLaneAffinity(title, context, assigneeName) {
1908
+ const employees = loadEmployeesSync();
1909
+ const employee = employees.find((e) => e.name === assigneeName);
1910
+ if (!employee) return void 0;
1911
+ const assigneeRole = employee.role;
1912
+ const text = `${title} ${context}`.toLowerCase();
1913
+ const matchedRoles = /* @__PURE__ */ new Set();
1914
+ for (const [keyword, roles] of KEYWORD_INDEX) {
1915
+ if (text.includes(keyword)) {
1916
+ for (const role of roles) matchedRoles.add(role);
1917
+ }
1918
+ }
1919
+ if (matchedRoles.size === 0) return void 0;
1920
+ if (matchedRoles.has(assigneeRole)) return void 0;
1921
+ if (assigneeRole === "COO") return void 0;
1922
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
1923
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
1924
+ }
1810
1925
  async function resolveTask(client, identifier, scopeSession) {
1811
1926
  const scope = sessionScopeFilter(scopeSession);
1812
1927
  let result2 = await client.execute({
@@ -1856,7 +1971,14 @@ async function createTaskCore(input) {
1856
1971
  const id = crypto3.randomUUID();
1857
1972
  const now = (/* @__PURE__ */ new Date()).toISOString();
1858
1973
  const slug = slugify(input.title);
1859
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
1974
+ let earlySessionScope = null;
1975
+ try {
1976
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1977
+ earlySessionScope = resolveExeSession2();
1978
+ } catch {
1979
+ }
1980
+ const scope = earlySessionScope ?? "default";
1981
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
1860
1982
  let blockedById = null;
1861
1983
  const initialStatus = input.blockedBy ? "blocked" : "open";
1862
1984
  if (input.blockedBy) {
@@ -1896,6 +2018,13 @@ async function createTaskCore(input) {
1896
2018
  if (dupCheck.rows.length > 0) {
1897
2019
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
1898
2020
  }
2021
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2022
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
2023
+ if (laneWarning) {
2024
+ warning = warning ? `${warning}
2025
+ ${laneWarning}` : laneWarning;
2026
+ }
2027
+ }
1899
2028
  if (input.baseDir) {
1900
2029
  try {
1901
2030
  await mkdir3(path8.join(input.baseDir, "exe", "output"), { recursive: true });
@@ -1906,12 +2035,7 @@ async function createTaskCore(input) {
1906
2035
  }
1907
2036
  }
1908
2037
  const complexity = input.complexity ?? "standard";
1909
- let sessionScope = null;
1910
- try {
1911
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1912
- sessionScope = resolveExeSession2();
1913
- } catch {
1914
- }
2038
+ const sessionScope = earlySessionScope;
1915
2039
  await client.execute({
1916
2040
  sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
1917
2041
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -1938,6 +2062,39 @@ async function createTaskCore(input) {
1938
2062
  now
1939
2063
  ]
1940
2064
  });
2065
+ if (input.baseDir) {
2066
+ try {
2067
+ const EXE_OS_DIR = path8.join(os6.homedir(), ".exe-os");
2068
+ const mdPath = path8.join(EXE_OS_DIR, taskFile);
2069
+ const mdDir = path8.dirname(mdPath);
2070
+ if (!existsSync8(mdDir)) await mkdir3(mdDir, { recursive: true });
2071
+ const reviewer = input.reviewer ?? input.assignedBy;
2072
+ const mdContent = `# ${input.title}
2073
+
2074
+ **ID:** ${id}
2075
+ **Status:** ${initialStatus}
2076
+ **Priority:** ${input.priority}
2077
+ **Assigned by:** ${input.assignedBy}
2078
+ **Assigned to:** ${input.assignedTo}
2079
+ **Project:** ${input.projectName}
2080
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2081
+ **Parent task:** ${parentTaskId}` : ""}
2082
+ **Reviewer:** ${reviewer}
2083
+
2084
+ ## Context
2085
+
2086
+ ${input.context}
2087
+
2088
+ ## MANDATORY: When done
2089
+
2090
+ You MUST call update_task with status "done" and a result summary when finished.
2091
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2092
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2093
+ `;
2094
+ await writeFile3(mdPath, mdContent, "utf-8");
2095
+ } catch {
2096
+ }
2097
+ }
1941
2098
  return {
1942
2099
  id,
1943
2100
  title: input.title,
@@ -2130,7 +2287,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2130
2287
  return { row, taskFile, now, taskId };
2131
2288
  }
2132
2289
  }
2133
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2290
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
2134
2291
  process.stderr.write(
2135
2292
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2136
2293
  `
@@ -2242,12 +2399,22 @@ async function ensureGitignoreExe(baseDir) {
2242
2399
  } catch {
2243
2400
  }
2244
2401
  }
2245
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2402
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2246
2403
  var init_tasks_crud = __esm({
2247
2404
  "src/lib/tasks-crud.ts"() {
2248
2405
  "use strict";
2249
2406
  init_database();
2250
2407
  init_task_scope();
2408
+ init_employees();
2409
+ LANE_KEYWORDS = {
2410
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
2411
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
2412
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
2413
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
2414
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
2415
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
2416
+ };
2417
+ KEYWORD_INDEX = buildKeywordIndex();
2251
2418
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2252
2419
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2253
2420
  }
@@ -2277,7 +2444,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2277
2444
  const result3 = await client.execute({
2278
2445
  sql: `SELECT COUNT(*) as cnt FROM tasks
2279
2446
  WHERE status = 'needs_review' AND updated_at > ?
2280
- AND (session_scope = ? OR session_scope IS NULL)`,
2447
+ AND session_scope = ?`,
2281
2448
  args: [sinceIso, sessionScope]
2282
2449
  });
2283
2450
  return Number(result3.rows[0]?.cnt) || 0;
@@ -2295,7 +2462,7 @@ async function listPendingReviews(limit, sessionScope) {
2295
2462
  const result3 = await client.execute({
2296
2463
  sql: `SELECT title, assigned_to, project_name FROM tasks
2297
2464
  WHERE status = 'needs_review'
2298
- AND (session_scope = ? OR session_scope IS NULL)
2465
+ AND session_scope = ?
2299
2466
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
2300
2467
  args: [sessionScope, limit]
2301
2468
  });
@@ -2416,14 +2583,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2416
2583
  if (parts.length >= 3 && parts[0] === "review") {
2417
2584
  const agent = parts[1];
2418
2585
  const slug = parts.slice(2).join("-");
2419
- const originalTaskFile = `exe/${agent}/${slug}.md`;
2586
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
2420
2587
  const result2 = await client.execute({
2421
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2422
- args: [now, originalTaskFile]
2588
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
2589
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
2423
2590
  });
2424
2591
  if (result2.rowsAffected > 0) {
2425
2592
  process.stderr.write(
2426
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
2593
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
2427
2594
  `
2428
2595
  );
2429
2596
  }
@@ -2605,7 +2772,7 @@ function findSessionForProject(projectName) {
2605
2772
  const sessions = listSessions();
2606
2773
  for (const s of sessions) {
2607
2774
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2608
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2775
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2609
2776
  }
2610
2777
  return null;
2611
2778
  }
@@ -2651,7 +2818,7 @@ var init_session_scope = __esm({
2651
2818
 
2652
2819
  // src/lib/tasks-notify.ts
2653
2820
  async function dispatchTaskToEmployee(input) {
2654
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2821
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2655
2822
  let crossProject = false;
2656
2823
  if (input.projectName) {
2657
2824
  try {
@@ -3130,7 +3297,7 @@ async function updateTask(input) {
3130
3297
  }
3131
3298
  const isTerminal = input.status === "done" || input.status === "needs_review";
3132
3299
  if (isTerminal) {
3133
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3300
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3134
3301
  if (!isCoordinator) {
3135
3302
  notifyTaskDone();
3136
3303
  }
@@ -3155,7 +3322,7 @@ async function updateTask(input) {
3155
3322
  }
3156
3323
  }
3157
3324
  }
3158
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3325
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3159
3326
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3160
3327
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3161
3328
  taskId,
@@ -3171,7 +3338,7 @@ async function updateTask(input) {
3171
3338
  });
3172
3339
  }
3173
3340
  let nextTask;
3174
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3341
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3175
3342
  try {
3176
3343
  nextTask = await findNextTask(String(row.assigned_to));
3177
3344
  } catch {
@@ -3539,7 +3706,7 @@ __export(tmux_routing_exports, {
3539
3706
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
3540
3707
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync } from "fs";
3541
3708
  import path13 from "path";
3542
- import os6 from "os";
3709
+ import os7 from "os";
3543
3710
  import { fileURLToPath } from "url";
3544
3711
  import { unlinkSync as unlinkSync5 } from "fs";
3545
3712
  function spawnLockPath(sessionName) {
@@ -3863,7 +4030,7 @@ function notifyParentExe(sessionKey) {
3863
4030
  return true;
3864
4031
  }
3865
4032
  function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
3866
- if (employeeName2 === "exe" || isCoordinatorName(employeeName2)) {
4033
+ if (isCoordinatorName(employeeName2)) {
3867
4034
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3868
4035
  }
3869
4036
  try {
@@ -3935,7 +4102,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
3935
4102
  const transport = getTransport();
3936
4103
  const sessionName = employeeSessionName(employeeName2, exeSession2, opts?.instance);
3937
4104
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName2}${opts.instance}` : employeeName2;
3938
- const logDir = path13.join(os6.homedir(), ".exe-os", "session-logs");
4105
+ const logDir = path13.join(os7.homedir(), ".exe-os", "session-logs");
3939
4106
  const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3940
4107
  if (!existsSync10(logDir)) {
3941
4108
  mkdirSync5(logDir, { recursive: true });
@@ -3951,7 +4118,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
3951
4118
  } catch {
3952
4119
  }
3953
4120
  try {
3954
- const claudeJsonPath = path13.join(os6.homedir(), ".claude.json");
4121
+ const claudeJsonPath = path13.join(os7.homedir(), ".claude.json");
3955
4122
  let claudeJson = {};
3956
4123
  try {
3957
4124
  claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
@@ -3966,7 +4133,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
3966
4133
  } catch {
3967
4134
  }
3968
4135
  try {
3969
- const settingsDir = path13.join(os6.homedir(), ".claude", "projects");
4136
+ const settingsDir = path13.join(os7.homedir(), ".claude", "projects");
3970
4137
  const normalizedKey = (opts?.cwd ?? projectDir2).replace(/\//g, "-").replace(/^-/, "");
3971
4138
  const projSettingsDir = path13.join(settingsDir, normalizedKey);
3972
4139
  const settingsPath = path13.join(projSettingsDir, "settings.json");
@@ -4014,7 +4181,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4014
4181
  let legacyFallbackWarned = false;
4015
4182
  if (!useExeAgent && !useBinSymlink) {
4016
4183
  const identityPath = path13.join(
4017
- os6.homedir(),
4184
+ os7.homedir(),
4018
4185
  ".exe-os",
4019
4186
  "identity",
4020
4187
  `${employeeName2}.md`
@@ -4044,7 +4211,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4044
4211
  }
4045
4212
  let sessionContextFlag = "";
4046
4213
  try {
4047
- const ctxDir = path13.join(os6.homedir(), ".exe-os", "session-cache");
4214
+ const ctxDir = path13.join(os7.homedir(), ".exe-os", "session-cache");
4048
4215
  mkdirSync5(ctxDir, { recursive: true });
4049
4216
  const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
4050
4217
  const ctxContent = [
@@ -4156,13 +4323,13 @@ var init_tmux_routing = __esm({
4156
4323
  init_intercom_queue();
4157
4324
  init_plan_limits();
4158
4325
  init_employees();
4159
- SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
4160
- SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
4326
+ SPAWN_LOCK_DIR = path13.join(os7.homedir(), ".exe-os", "spawn-locks");
4327
+ SESSION_CACHE = path13.join(os7.homedir(), ".exe-os", "session-cache");
4161
4328
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4162
4329
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4163
4330
  VERIFY_PANE_LINES = 200;
4164
4331
  INTERCOM_DEBOUNCE_MS = 3e4;
4165
- INTERCOM_LOG2 = path13.join(os6.homedir(), ".exe-os", "intercom.log");
4332
+ INTERCOM_LOG2 = path13.join(os7.homedir(), ".exe-os", "intercom.log");
4166
4333
  DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
4167
4334
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4168
4335
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
@@ -4600,17 +4767,18 @@ init_tmux_routing();
4600
4767
  init_tasks_crud();
4601
4768
 
4602
4769
  // src/lib/store.ts
4770
+ import { createHash } from "crypto";
4603
4771
  init_database();
4604
4772
 
4605
4773
  // src/lib/keychain.ts
4606
4774
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
4607
4775
  import { existsSync as existsSync11 } from "fs";
4608
4776
  import path14 from "path";
4609
- import os7 from "os";
4777
+ import os8 from "os";
4610
4778
  var SERVICE = "exe-mem";
4611
4779
  var ACCOUNT = "master-key";
4612
4780
  function getKeyDir() {
4613
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path14.join(os7.homedir(), ".exe-os");
4781
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path14.join(os8.homedir(), ".exe-os");
4614
4782
  }
4615
4783
  function getKeyPath() {
4616
4784
  return path14.join(getKeyDir(), "master.key");
@@ -4635,12 +4803,20 @@ async function getMasterKey() {
4635
4803
  }
4636
4804
  const keyPath = getKeyPath();
4637
4805
  if (!existsSync11(keyPath)) {
4806
+ process.stderr.write(
4807
+ `[keychain] Key not found at ${keyPath} (HOME=${os8.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
4808
+ `
4809
+ );
4638
4810
  return null;
4639
4811
  }
4640
4812
  try {
4641
4813
  const content = await readFile4(keyPath, "utf-8");
4642
4814
  return Buffer.from(content.trim(), "base64");
4643
- } catch {
4815
+ } catch (err) {
4816
+ process.stderr.write(
4817
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
4818
+ `
4819
+ );
4644
4820
  return null;
4645
4821
  }
4646
4822
  }