@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
package/dist/lib/tasks.js CHANGED
@@ -266,15 +266,22 @@ function getClient() {
266
266
  if (!_resilientClient) {
267
267
  throw new Error("Database client not initialized. Call initDatabase() first.");
268
268
  }
269
+ if (process.env.EXE_IS_DAEMON === "1") {
270
+ return _resilientClient;
271
+ }
272
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
273
+ return _daemonClient;
274
+ }
269
275
  return _resilientClient;
270
276
  }
271
- var _resilientClient;
277
+ var _resilientClient, _daemonClient;
272
278
  var init_database = __esm({
273
279
  "src/lib/database.ts"() {
274
280
  "use strict";
275
281
  init_db_retry();
276
282
  init_employees();
277
283
  _resilientClient = null;
284
+ _daemonClient = null;
278
285
  }
279
286
  });
280
287
 
@@ -1454,7 +1461,7 @@ function notifyParentExe(sessionKey) {
1454
1461
  return true;
1455
1462
  }
1456
1463
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1457
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
1464
+ if (isCoordinatorName(employeeName)) {
1458
1465
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
1459
1466
  }
1460
1467
  try {
@@ -1787,6 +1794,7 @@ var init_task_scope = __esm({
1787
1794
  // src/lib/tasks-crud.ts
1788
1795
  import crypto3 from "crypto";
1789
1796
  import path9 from "path";
1797
+ import os7 from "os";
1790
1798
  import { execSync as execSync5 } from "child_process";
1791
1799
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1792
1800
  import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
@@ -1830,6 +1838,35 @@ function extractParentFromContext(contextBody) {
1830
1838
  function slugify(title) {
1831
1839
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1832
1840
  }
1841
+ function buildKeywordIndex() {
1842
+ const idx = /* @__PURE__ */ new Map();
1843
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
1844
+ for (const kw of keywords) {
1845
+ const existing = idx.get(kw) ?? [];
1846
+ existing.push(role);
1847
+ idx.set(kw, existing);
1848
+ }
1849
+ }
1850
+ return idx;
1851
+ }
1852
+ function checkLaneAffinity(title, context, assigneeName) {
1853
+ const employees = loadEmployeesSync();
1854
+ const employee = employees.find((e) => e.name === assigneeName);
1855
+ if (!employee) return void 0;
1856
+ const assigneeRole = employee.role;
1857
+ const text = `${title} ${context}`.toLowerCase();
1858
+ const matchedRoles = /* @__PURE__ */ new Set();
1859
+ for (const [keyword, roles] of KEYWORD_INDEX) {
1860
+ if (text.includes(keyword)) {
1861
+ for (const role of roles) matchedRoles.add(role);
1862
+ }
1863
+ }
1864
+ if (matchedRoles.size === 0) return void 0;
1865
+ if (matchedRoles.has(assigneeRole)) return void 0;
1866
+ if (assigneeRole === "COO") return void 0;
1867
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
1868
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
1869
+ }
1833
1870
  async function resolveTask(client, identifier, scopeSession) {
1834
1871
  const scope = sessionScopeFilter(scopeSession);
1835
1872
  let result = await client.execute({
@@ -1879,7 +1916,14 @@ async function createTaskCore(input) {
1879
1916
  const id = crypto3.randomUUID();
1880
1917
  const now = (/* @__PURE__ */ new Date()).toISOString();
1881
1918
  const slug = slugify(input.title);
1882
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
1919
+ let earlySessionScope = null;
1920
+ try {
1921
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1922
+ earlySessionScope = resolveExeSession2();
1923
+ } catch {
1924
+ }
1925
+ const scope = earlySessionScope ?? "default";
1926
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
1883
1927
  let blockedById = null;
1884
1928
  const initialStatus = input.blockedBy ? "blocked" : "open";
1885
1929
  if (input.blockedBy) {
@@ -1919,6 +1963,13 @@ async function createTaskCore(input) {
1919
1963
  if (dupCheck.rows.length > 0) {
1920
1964
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
1921
1965
  }
1966
+ if (!process.env.DISABLE_LANE_AFFINITY) {
1967
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
1968
+ if (laneWarning) {
1969
+ warning = warning ? `${warning}
1970
+ ${laneWarning}` : laneWarning;
1971
+ }
1972
+ }
1922
1973
  if (input.baseDir) {
1923
1974
  try {
1924
1975
  await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
@@ -1929,12 +1980,7 @@ async function createTaskCore(input) {
1929
1980
  }
1930
1981
  }
1931
1982
  const complexity = input.complexity ?? "standard";
1932
- let sessionScope = null;
1933
- try {
1934
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1935
- sessionScope = resolveExeSession2();
1936
- } catch {
1937
- }
1983
+ const sessionScope = earlySessionScope;
1938
1984
  await client.execute({
1939
1985
  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)
1940
1986
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -1961,6 +2007,39 @@ async function createTaskCore(input) {
1961
2007
  now
1962
2008
  ]
1963
2009
  });
2010
+ if (input.baseDir) {
2011
+ try {
2012
+ const EXE_OS_DIR = path9.join(os7.homedir(), ".exe-os");
2013
+ const mdPath = path9.join(EXE_OS_DIR, taskFile);
2014
+ const mdDir = path9.dirname(mdPath);
2015
+ if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
2016
+ const reviewer = input.reviewer ?? input.assignedBy;
2017
+ const mdContent = `# ${input.title}
2018
+
2019
+ **ID:** ${id}
2020
+ **Status:** ${initialStatus}
2021
+ **Priority:** ${input.priority}
2022
+ **Assigned by:** ${input.assignedBy}
2023
+ **Assigned to:** ${input.assignedTo}
2024
+ **Project:** ${input.projectName}
2025
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2026
+ **Parent task:** ${parentTaskId}` : ""}
2027
+ **Reviewer:** ${reviewer}
2028
+
2029
+ ## Context
2030
+
2031
+ ${input.context}
2032
+
2033
+ ## MANDATORY: When done
2034
+
2035
+ You MUST call update_task with status "done" and a result summary when finished.
2036
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2037
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2038
+ `;
2039
+ await writeFile3(mdPath, mdContent, "utf-8");
2040
+ } catch {
2041
+ }
2042
+ }
1964
2043
  return {
1965
2044
  id,
1966
2045
  title: input.title,
@@ -2153,7 +2232,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2153
2232
  return { row, taskFile, now, taskId };
2154
2233
  }
2155
2234
  }
2156
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2235
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
2157
2236
  process.stderr.write(
2158
2237
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2159
2238
  `
@@ -2265,12 +2344,22 @@ async function ensureGitignoreExe(baseDir) {
2265
2344
  } catch {
2266
2345
  }
2267
2346
  }
2268
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2347
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2269
2348
  var init_tasks_crud = __esm({
2270
2349
  "src/lib/tasks-crud.ts"() {
2271
2350
  "use strict";
2272
2351
  init_database();
2273
2352
  init_task_scope();
2353
+ init_employees();
2354
+ LANE_KEYWORDS = {
2355
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
2356
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
2357
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
2358
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
2359
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
2360
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
2361
+ };
2362
+ KEYWORD_INDEX = buildKeywordIndex();
2274
2363
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2275
2364
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2276
2365
  }
@@ -2300,7 +2389,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2300
2389
  const result2 = await client.execute({
2301
2390
  sql: `SELECT COUNT(*) as cnt FROM tasks
2302
2391
  WHERE status = 'needs_review' AND updated_at > ?
2303
- AND (session_scope = ? OR session_scope IS NULL)`,
2392
+ AND session_scope = ?`,
2304
2393
  args: [sinceIso, sessionScope]
2305
2394
  });
2306
2395
  return Number(result2.rows[0]?.cnt) || 0;
@@ -2318,7 +2407,7 @@ async function listPendingReviews(limit, sessionScope) {
2318
2407
  const result2 = await client.execute({
2319
2408
  sql: `SELECT title, assigned_to, project_name FROM tasks
2320
2409
  WHERE status = 'needs_review'
2321
- AND (session_scope = ? OR session_scope IS NULL)
2410
+ AND session_scope = ?
2322
2411
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
2323
2412
  args: [sessionScope, limit]
2324
2413
  });
@@ -2439,14 +2528,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2439
2528
  if (parts.length >= 3 && parts[0] === "review") {
2440
2529
  const agent = parts[1];
2441
2530
  const slug = parts.slice(2).join("-");
2442
- const originalTaskFile = `exe/${agent}/${slug}.md`;
2531
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
2443
2532
  const result = await client.execute({
2444
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2445
- args: [now, originalTaskFile]
2533
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
2534
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
2446
2535
  });
2447
2536
  if (result.rowsAffected > 0) {
2448
2537
  process.stderr.write(
2449
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
2538
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
2450
2539
  `
2451
2540
  );
2452
2541
  }
@@ -2628,7 +2717,7 @@ function findSessionForProject(projectName) {
2628
2717
  const sessions = listSessions();
2629
2718
  for (const s of sessions) {
2630
2719
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2631
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2720
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2632
2721
  }
2633
2722
  return null;
2634
2723
  }
@@ -2674,7 +2763,7 @@ var init_session_scope = __esm({
2674
2763
 
2675
2764
  // src/lib/tasks-notify.ts
2676
2765
  async function dispatchTaskToEmployee(input) {
2677
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2766
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2678
2767
  let crossProject = false;
2679
2768
  if (input.projectName) {
2680
2769
  try {
@@ -3153,7 +3242,7 @@ async function updateTask(input) {
3153
3242
  }
3154
3243
  const isTerminal = input.status === "done" || input.status === "needs_review";
3155
3244
  if (isTerminal) {
3156
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3245
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3157
3246
  if (!isCoordinator) {
3158
3247
  notifyTaskDone();
3159
3248
  }
@@ -3178,7 +3267,7 @@ async function updateTask(input) {
3178
3267
  }
3179
3268
  }
3180
3269
  }
3181
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3270
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3182
3271
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3183
3272
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3184
3273
  taskId,
@@ -3194,7 +3283,7 @@ async function updateTask(input) {
3194
3283
  });
3195
3284
  }
3196
3285
  let nextTask;
3197
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3286
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3198
3287
  try {
3199
3288
  nextTask = await findNextTask(String(row.assigned_to));
3200
3289
  } catch {
@@ -564,15 +564,22 @@ function getClient() {
564
564
  if (!_resilientClient) {
565
565
  throw new Error("Database client not initialized. Call initDatabase() first.");
566
566
  }
567
+ if (process.env.EXE_IS_DAEMON === "1") {
568
+ return _resilientClient;
569
+ }
570
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
571
+ return _daemonClient;
572
+ }
567
573
  return _resilientClient;
568
574
  }
569
- var _resilientClient;
575
+ var _resilientClient, _daemonClient;
570
576
  var init_database = __esm({
571
577
  "src/lib/database.ts"() {
572
578
  "use strict";
573
579
  init_db_retry();
574
580
  init_employees();
575
581
  _resilientClient = null;
582
+ _daemonClient = null;
576
583
  }
577
584
  });
578
585
 
@@ -845,6 +852,7 @@ var init_state_bus = __esm({
845
852
  // src/lib/tasks-crud.ts
846
853
  import crypto3 from "crypto";
847
854
  import path8 from "path";
855
+ import os6 from "os";
848
856
  import { execSync as execSync4 } from "child_process";
849
857
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
850
858
  import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
@@ -888,6 +896,35 @@ function extractParentFromContext(contextBody) {
888
896
  function slugify(title) {
889
897
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
890
898
  }
899
+ function buildKeywordIndex() {
900
+ const idx = /* @__PURE__ */ new Map();
901
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
902
+ for (const kw of keywords) {
903
+ const existing = idx.get(kw) ?? [];
904
+ existing.push(role);
905
+ idx.set(kw, existing);
906
+ }
907
+ }
908
+ return idx;
909
+ }
910
+ function checkLaneAffinity(title, context, assigneeName) {
911
+ const employees = loadEmployeesSync();
912
+ const employee = employees.find((e) => e.name === assigneeName);
913
+ if (!employee) return void 0;
914
+ const assigneeRole = employee.role;
915
+ const text = `${title} ${context}`.toLowerCase();
916
+ const matchedRoles = /* @__PURE__ */ new Set();
917
+ for (const [keyword, roles] of KEYWORD_INDEX) {
918
+ if (text.includes(keyword)) {
919
+ for (const role of roles) matchedRoles.add(role);
920
+ }
921
+ }
922
+ if (matchedRoles.size === 0) return void 0;
923
+ if (matchedRoles.has(assigneeRole)) return void 0;
924
+ if (assigneeRole === "COO") return void 0;
925
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
926
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
927
+ }
891
928
  async function resolveTask(client, identifier, scopeSession) {
892
929
  const scope = sessionScopeFilter(scopeSession);
893
930
  let result = await client.execute({
@@ -937,7 +974,14 @@ async function createTaskCore(input) {
937
974
  const id = crypto3.randomUUID();
938
975
  const now = (/* @__PURE__ */ new Date()).toISOString();
939
976
  const slug = slugify(input.title);
940
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
977
+ let earlySessionScope = null;
978
+ try {
979
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
980
+ earlySessionScope = resolveExeSession2();
981
+ } catch {
982
+ }
983
+ const scope = earlySessionScope ?? "default";
984
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
941
985
  let blockedById = null;
942
986
  const initialStatus = input.blockedBy ? "blocked" : "open";
943
987
  if (input.blockedBy) {
@@ -977,6 +1021,13 @@ async function createTaskCore(input) {
977
1021
  if (dupCheck.rows.length > 0) {
978
1022
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
979
1023
  }
1024
+ if (!process.env.DISABLE_LANE_AFFINITY) {
1025
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
1026
+ if (laneWarning) {
1027
+ warning = warning ? `${warning}
1028
+ ${laneWarning}` : laneWarning;
1029
+ }
1030
+ }
980
1031
  if (input.baseDir) {
981
1032
  try {
982
1033
  await mkdir3(path8.join(input.baseDir, "exe", "output"), { recursive: true });
@@ -987,12 +1038,7 @@ async function createTaskCore(input) {
987
1038
  }
988
1039
  }
989
1040
  const complexity = input.complexity ?? "standard";
990
- let sessionScope = null;
991
- try {
992
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
993
- sessionScope = resolveExeSession2();
994
- } catch {
995
- }
1041
+ const sessionScope = earlySessionScope;
996
1042
  await client.execute({
997
1043
  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)
998
1044
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -1019,6 +1065,39 @@ async function createTaskCore(input) {
1019
1065
  now
1020
1066
  ]
1021
1067
  });
1068
+ if (input.baseDir) {
1069
+ try {
1070
+ const EXE_OS_DIR = path8.join(os6.homedir(), ".exe-os");
1071
+ const mdPath = path8.join(EXE_OS_DIR, taskFile);
1072
+ const mdDir = path8.dirname(mdPath);
1073
+ if (!existsSync8(mdDir)) await mkdir3(mdDir, { recursive: true });
1074
+ const reviewer = input.reviewer ?? input.assignedBy;
1075
+ const mdContent = `# ${input.title}
1076
+
1077
+ **ID:** ${id}
1078
+ **Status:** ${initialStatus}
1079
+ **Priority:** ${input.priority}
1080
+ **Assigned by:** ${input.assignedBy}
1081
+ **Assigned to:** ${input.assignedTo}
1082
+ **Project:** ${input.projectName}
1083
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
1084
+ **Parent task:** ${parentTaskId}` : ""}
1085
+ **Reviewer:** ${reviewer}
1086
+
1087
+ ## Context
1088
+
1089
+ ${input.context}
1090
+
1091
+ ## MANDATORY: When done
1092
+
1093
+ You MUST call update_task with status "done" and a result summary when finished.
1094
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
1095
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
1096
+ `;
1097
+ await writeFile3(mdPath, mdContent, "utf-8");
1098
+ } catch {
1099
+ }
1100
+ }
1022
1101
  return {
1023
1102
  id,
1024
1103
  title: input.title,
@@ -1211,7 +1290,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1211
1290
  return { row, taskFile, now, taskId };
1212
1291
  }
1213
1292
  }
1214
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
1293
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
1215
1294
  process.stderr.write(
1216
1295
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
1217
1296
  `
@@ -1323,12 +1402,22 @@ async function ensureGitignoreExe(baseDir) {
1323
1402
  } catch {
1324
1403
  }
1325
1404
  }
1326
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
1405
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
1327
1406
  var init_tasks_crud = __esm({
1328
1407
  "src/lib/tasks-crud.ts"() {
1329
1408
  "use strict";
1330
1409
  init_database();
1331
1410
  init_task_scope();
1411
+ init_employees();
1412
+ LANE_KEYWORDS = {
1413
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
1414
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
1415
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
1416
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
1417
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
1418
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
1419
+ };
1420
+ KEYWORD_INDEX = buildKeywordIndex();
1332
1421
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
1333
1422
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
1334
1423
  }
@@ -1358,7 +1447,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
1358
1447
  const result2 = await client.execute({
1359
1448
  sql: `SELECT COUNT(*) as cnt FROM tasks
1360
1449
  WHERE status = 'needs_review' AND updated_at > ?
1361
- AND (session_scope = ? OR session_scope IS NULL)`,
1450
+ AND session_scope = ?`,
1362
1451
  args: [sinceIso, sessionScope]
1363
1452
  });
1364
1453
  return Number(result2.rows[0]?.cnt) || 0;
@@ -1376,7 +1465,7 @@ async function listPendingReviews(limit, sessionScope) {
1376
1465
  const result2 = await client.execute({
1377
1466
  sql: `SELECT title, assigned_to, project_name FROM tasks
1378
1467
  WHERE status = 'needs_review'
1379
- AND (session_scope = ? OR session_scope IS NULL)
1468
+ AND session_scope = ?
1380
1469
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
1381
1470
  args: [sessionScope, limit]
1382
1471
  });
@@ -1497,14 +1586,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
1497
1586
  if (parts.length >= 3 && parts[0] === "review") {
1498
1587
  const agent = parts[1];
1499
1588
  const slug = parts.slice(2).join("-");
1500
- const originalTaskFile = `exe/${agent}/${slug}.md`;
1589
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
1501
1590
  const result = await client.execute({
1502
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
1503
- args: [now, originalTaskFile]
1591
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
1592
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
1504
1593
  });
1505
1594
  if (result.rowsAffected > 0) {
1506
1595
  process.stderr.write(
1507
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
1596
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
1508
1597
  `
1509
1598
  );
1510
1599
  }
@@ -1686,7 +1775,7 @@ function findSessionForProject(projectName) {
1686
1775
  const sessions = listSessions();
1687
1776
  for (const s of sessions) {
1688
1777
  const proj = s.projectDir.split("/").filter(Boolean).pop();
1689
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
1778
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
1690
1779
  }
1691
1780
  return null;
1692
1781
  }
@@ -1732,7 +1821,7 @@ var init_session_scope = __esm({
1732
1821
 
1733
1822
  // src/lib/tasks-notify.ts
1734
1823
  async function dispatchTaskToEmployee(input) {
1735
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
1824
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
1736
1825
  let crossProject = false;
1737
1826
  if (input.projectName) {
1738
1827
  try {
@@ -2211,7 +2300,7 @@ async function updateTask(input) {
2211
2300
  }
2212
2301
  const isTerminal = input.status === "done" || input.status === "needs_review";
2213
2302
  if (isTerminal) {
2214
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
2303
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
2215
2304
  if (!isCoordinator) {
2216
2305
  notifyTaskDone();
2217
2306
  }
@@ -2236,7 +2325,7 @@ async function updateTask(input) {
2236
2325
  }
2237
2326
  }
2238
2327
  }
2239
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
2328
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
2240
2329
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
2241
2330
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
2242
2331
  taskId,
@@ -2252,7 +2341,7 @@ async function updateTask(input) {
2252
2341
  });
2253
2342
  }
2254
2343
  let nextTask;
2255
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
2344
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
2256
2345
  try {
2257
2346
  nextTask = await findNextTask(String(row.assigned_to));
2258
2347
  } catch {
@@ -2620,7 +2709,7 @@ __export(tmux_routing_exports, {
2620
2709
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
2621
2710
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync } from "fs";
2622
2711
  import path13 from "path";
2623
- import os6 from "os";
2712
+ import os7 from "os";
2624
2713
  import { fileURLToPath } from "url";
2625
2714
  import { unlinkSync as unlinkSync5 } from "fs";
2626
2715
  function spawnLockPath(sessionName) {
@@ -2944,7 +3033,7 @@ function notifyParentExe(sessionKey) {
2944
3033
  return true;
2945
3034
  }
2946
3035
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2947
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
3036
+ if (isCoordinatorName(employeeName)) {
2948
3037
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
2949
3038
  }
2950
3039
  try {
@@ -3016,7 +3105,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3016
3105
  const transport = getTransport();
3017
3106
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3018
3107
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3019
- const logDir = path13.join(os6.homedir(), ".exe-os", "session-logs");
3108
+ const logDir = path13.join(os7.homedir(), ".exe-os", "session-logs");
3020
3109
  const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3021
3110
  if (!existsSync10(logDir)) {
3022
3111
  mkdirSync5(logDir, { recursive: true });
@@ -3032,7 +3121,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3032
3121
  } catch {
3033
3122
  }
3034
3123
  try {
3035
- const claudeJsonPath = path13.join(os6.homedir(), ".claude.json");
3124
+ const claudeJsonPath = path13.join(os7.homedir(), ".claude.json");
3036
3125
  let claudeJson = {};
3037
3126
  try {
3038
3127
  claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
@@ -3047,7 +3136,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3047
3136
  } catch {
3048
3137
  }
3049
3138
  try {
3050
- const settingsDir = path13.join(os6.homedir(), ".claude", "projects");
3139
+ const settingsDir = path13.join(os7.homedir(), ".claude", "projects");
3051
3140
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3052
3141
  const projSettingsDir = path13.join(settingsDir, normalizedKey);
3053
3142
  const settingsPath = path13.join(projSettingsDir, "settings.json");
@@ -3095,7 +3184,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3095
3184
  let legacyFallbackWarned = false;
3096
3185
  if (!useExeAgent && !useBinSymlink) {
3097
3186
  const identityPath = path13.join(
3098
- os6.homedir(),
3187
+ os7.homedir(),
3099
3188
  ".exe-os",
3100
3189
  "identity",
3101
3190
  `${employeeName}.md`
@@ -3125,7 +3214,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3125
3214
  }
3126
3215
  let sessionContextFlag = "";
3127
3216
  try {
3128
- const ctxDir = path13.join(os6.homedir(), ".exe-os", "session-cache");
3217
+ const ctxDir = path13.join(os7.homedir(), ".exe-os", "session-cache");
3129
3218
  mkdirSync5(ctxDir, { recursive: true });
3130
3219
  const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
3131
3220
  const ctxContent = [
@@ -3236,13 +3325,13 @@ var init_tmux_routing = __esm({
3236
3325
  init_intercom_queue();
3237
3326
  init_plan_limits();
3238
3327
  init_employees();
3239
- SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
3240
- SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
3328
+ SPAWN_LOCK_DIR = path13.join(os7.homedir(), ".exe-os", "spawn-locks");
3329
+ SESSION_CACHE = path13.join(os7.homedir(), ".exe-os", "session-cache");
3241
3330
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3242
3331
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
3243
3332
  VERIFY_PANE_LINES = 200;
3244
3333
  INTERCOM_DEBOUNCE_MS = 3e4;
3245
- INTERCOM_LOG2 = path13.join(os6.homedir(), ".exe-os", "intercom.log");
3334
+ INTERCOM_LOG2 = path13.join(os7.homedir(), ".exe-os", "intercom.log");
3246
3335
  DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
3247
3336
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3248
3337
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;