@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
@@ -264,6 +264,7 @@ __export(employees_exports, {
264
264
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
265
265
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
266
266
  addEmployee: () => addEmployee,
267
+ baseAgentName: () => baseAgentName,
267
268
  canCoordinate: () => canCoordinate,
268
269
  getCoordinatorEmployee: () => getCoordinatorEmployee,
269
270
  getCoordinatorName: () => getCoordinatorName,
@@ -360,6 +361,14 @@ function hasRole(agentName, role) {
360
361
  const emp = getEmployee(employees, agentName);
361
362
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
362
363
  }
364
+ function baseAgentName(name, employees) {
365
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
366
+ if (!match) return name;
367
+ const base = match[1];
368
+ const roster = employees ?? loadEmployeesSync();
369
+ if (getEmployee(roster, base)) return base;
370
+ return name;
371
+ }
363
372
  function isMultiInstance(agentName, employees) {
364
373
  const roster = employees ?? loadEmployeesSync();
365
374
  const emp = getEmployee(roster, agentName);
@@ -476,6 +485,12 @@ function getClient() {
476
485
  if (!_resilientClient) {
477
486
  throw new Error("Database client not initialized. Call initDatabase() first.");
478
487
  }
488
+ if (process.env.EXE_IS_DAEMON === "1") {
489
+ return _resilientClient;
490
+ }
491
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
492
+ return _daemonClient;
493
+ }
479
494
  return _resilientClient;
480
495
  }
481
496
  function getRawClient() {
@@ -964,6 +979,12 @@ async function ensureSchema() {
964
979
  } catch {
965
980
  }
966
981
  }
982
+ try {
983
+ await client.execute(
984
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
985
+ );
986
+ } catch {
987
+ }
967
988
  await client.executeMultiple(`
968
989
  CREATE TABLE IF NOT EXISTS entities (
969
990
  id TEXT PRIMARY KEY,
@@ -1016,7 +1037,30 @@ async function ensureSchema() {
1016
1037
  entity_id TEXT NOT NULL,
1017
1038
  PRIMARY KEY (hyperedge_id, entity_id)
1018
1039
  );
1040
+
1041
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1042
+ name,
1043
+ content=entities,
1044
+ content_rowid=rowid
1045
+ );
1046
+
1047
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1048
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1049
+ END;
1050
+
1051
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1052
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1053
+ END;
1054
+
1055
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1056
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1057
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1058
+ END;
1019
1059
  `);
1060
+ try {
1061
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1062
+ } catch {
1063
+ }
1020
1064
  await client.executeMultiple(`
1021
1065
  CREATE TABLE IF NOT EXISTS entity_aliases (
1022
1066
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1197,6 +1241,33 @@ async function ensureSchema() {
1197
1241
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1198
1242
  ON conversations(channel_id);
1199
1243
  `);
1244
+ await client.executeMultiple(`
1245
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1246
+ session_uuid TEXT PRIMARY KEY,
1247
+ agent_id TEXT NOT NULL,
1248
+ session_name TEXT,
1249
+ task_id TEXT,
1250
+ project_name TEXT,
1251
+ started_at TEXT NOT NULL
1252
+ );
1253
+
1254
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1255
+ ON session_agent_map(agent_id);
1256
+ `);
1257
+ try {
1258
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1259
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1260
+ await client.execute({
1261
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1262
+ SELECT session_id, agent_id, '', MIN(timestamp)
1263
+ FROM memories
1264
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1265
+ GROUP BY session_id, agent_id`,
1266
+ args: []
1267
+ });
1268
+ }
1269
+ } catch {
1270
+ }
1200
1271
  try {
1201
1272
  await client.execute({
1202
1273
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1330,8 +1401,30 @@ async function ensureSchema() {
1330
1401
  });
1331
1402
  } catch {
1332
1403
  }
1404
+ for (const col of [
1405
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1406
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1407
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1408
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1409
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1410
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1411
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1412
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1413
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1414
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1415
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1416
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1417
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1418
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1419
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1420
+ ]) {
1421
+ try {
1422
+ await client.execute(col);
1423
+ } catch {
1424
+ }
1425
+ }
1333
1426
  }
1334
- var _client, _resilientClient, initTurso;
1427
+ var _client, _resilientClient, _daemonClient, initTurso;
1335
1428
  var init_database = __esm({
1336
1429
  "src/lib/database.ts"() {
1337
1430
  "use strict";
@@ -1339,6 +1432,7 @@ var init_database = __esm({
1339
1432
  init_employees();
1340
1433
  _client = null;
1341
1434
  _resilientClient = null;
1435
+ _daemonClient = null;
1342
1436
  initTurso = initDatabase;
1343
1437
  }
1344
1438
  });
@@ -2945,7 +3039,7 @@ function notifyParentExe(sessionKey) {
2945
3039
  return true;
2946
3040
  }
2947
3041
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2948
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
3042
+ if (isCoordinatorName(employeeName)) {
2949
3043
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
2950
3044
  }
2951
3045
  try {
@@ -3278,6 +3372,7 @@ var init_task_scope = __esm({
3278
3372
  // src/lib/tasks-crud.ts
3279
3373
  import crypto3 from "crypto";
3280
3374
  import path11 from "path";
3375
+ import os8 from "os";
3281
3376
  import { execSync as execSync5 } from "child_process";
3282
3377
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3283
3378
  import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
@@ -3321,6 +3416,35 @@ function extractParentFromContext(contextBody) {
3321
3416
  function slugify(title) {
3322
3417
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3323
3418
  }
3419
+ function buildKeywordIndex() {
3420
+ const idx = /* @__PURE__ */ new Map();
3421
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
3422
+ for (const kw of keywords) {
3423
+ const existing = idx.get(kw) ?? [];
3424
+ existing.push(role);
3425
+ idx.set(kw, existing);
3426
+ }
3427
+ }
3428
+ return idx;
3429
+ }
3430
+ function checkLaneAffinity(title, context, assigneeName) {
3431
+ const employees = loadEmployeesSync();
3432
+ const employee = employees.find((e) => e.name === assigneeName);
3433
+ if (!employee) return void 0;
3434
+ const assigneeRole = employee.role;
3435
+ const text = `${title} ${context}`.toLowerCase();
3436
+ const matchedRoles = /* @__PURE__ */ new Set();
3437
+ for (const [keyword, roles] of KEYWORD_INDEX) {
3438
+ if (text.includes(keyword)) {
3439
+ for (const role of roles) matchedRoles.add(role);
3440
+ }
3441
+ }
3442
+ if (matchedRoles.size === 0) return void 0;
3443
+ if (matchedRoles.has(assigneeRole)) return void 0;
3444
+ if (assigneeRole === "COO") return void 0;
3445
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
3446
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
3447
+ }
3324
3448
  async function resolveTask(client, identifier, scopeSession) {
3325
3449
  const scope = sessionScopeFilter(scopeSession);
3326
3450
  let result = await client.execute({
@@ -3370,7 +3494,14 @@ async function createTaskCore(input) {
3370
3494
  const id = crypto3.randomUUID();
3371
3495
  const now = (/* @__PURE__ */ new Date()).toISOString();
3372
3496
  const slug = slugify(input.title);
3373
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
3497
+ let earlySessionScope = null;
3498
+ try {
3499
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3500
+ earlySessionScope = resolveExeSession2();
3501
+ } catch {
3502
+ }
3503
+ const scope = earlySessionScope ?? "default";
3504
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
3374
3505
  let blockedById = null;
3375
3506
  const initialStatus = input.blockedBy ? "blocked" : "open";
3376
3507
  if (input.blockedBy) {
@@ -3410,6 +3541,13 @@ async function createTaskCore(input) {
3410
3541
  if (dupCheck.rows.length > 0) {
3411
3542
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
3412
3543
  }
3544
+ if (!process.env.DISABLE_LANE_AFFINITY) {
3545
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
3546
+ if (laneWarning) {
3547
+ warning = warning ? `${warning}
3548
+ ${laneWarning}` : laneWarning;
3549
+ }
3550
+ }
3413
3551
  if (input.baseDir) {
3414
3552
  try {
3415
3553
  await mkdir4(path11.join(input.baseDir, "exe", "output"), { recursive: true });
@@ -3420,12 +3558,7 @@ async function createTaskCore(input) {
3420
3558
  }
3421
3559
  }
3422
3560
  const complexity = input.complexity ?? "standard";
3423
- let sessionScope = null;
3424
- try {
3425
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3426
- sessionScope = resolveExeSession2();
3427
- } catch {
3428
- }
3561
+ const sessionScope = earlySessionScope;
3429
3562
  await client.execute({
3430
3563
  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)
3431
3564
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -3452,6 +3585,39 @@ async function createTaskCore(input) {
3452
3585
  now
3453
3586
  ]
3454
3587
  });
3588
+ if (input.baseDir) {
3589
+ try {
3590
+ const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
3591
+ const mdPath = path11.join(EXE_OS_DIR, taskFile);
3592
+ const mdDir = path11.dirname(mdPath);
3593
+ if (!existsSync11(mdDir)) await mkdir4(mdDir, { recursive: true });
3594
+ const reviewer = input.reviewer ?? input.assignedBy;
3595
+ const mdContent = `# ${input.title}
3596
+
3597
+ **ID:** ${id}
3598
+ **Status:** ${initialStatus}
3599
+ **Priority:** ${input.priority}
3600
+ **Assigned by:** ${input.assignedBy}
3601
+ **Assigned to:** ${input.assignedTo}
3602
+ **Project:** ${input.projectName}
3603
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
3604
+ **Parent task:** ${parentTaskId}` : ""}
3605
+ **Reviewer:** ${reviewer}
3606
+
3607
+ ## Context
3608
+
3609
+ ${input.context}
3610
+
3611
+ ## MANDATORY: When done
3612
+
3613
+ You MUST call update_task with status "done" and a result summary when finished.
3614
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3615
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
3616
+ `;
3617
+ await writeFile4(mdPath, mdContent, "utf-8");
3618
+ } catch {
3619
+ }
3620
+ }
3455
3621
  return {
3456
3622
  id,
3457
3623
  title: input.title,
@@ -3644,7 +3810,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3644
3810
  return { row, taskFile, now, taskId };
3645
3811
  }
3646
3812
  }
3647
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
3813
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
3648
3814
  process.stderr.write(
3649
3815
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
3650
3816
  `
@@ -3756,12 +3922,22 @@ async function ensureGitignoreExe(baseDir) {
3756
3922
  } catch {
3757
3923
  }
3758
3924
  }
3759
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3925
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3760
3926
  var init_tasks_crud = __esm({
3761
3927
  "src/lib/tasks-crud.ts"() {
3762
3928
  "use strict";
3763
3929
  init_database();
3764
3930
  init_task_scope();
3931
+ init_employees();
3932
+ LANE_KEYWORDS = {
3933
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
3934
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
3935
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
3936
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
3937
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
3938
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
3939
+ };
3940
+ KEYWORD_INDEX = buildKeywordIndex();
3765
3941
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
3766
3942
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3767
3943
  }
@@ -3791,7 +3967,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3791
3967
  const result2 = await client.execute({
3792
3968
  sql: `SELECT COUNT(*) as cnt FROM tasks
3793
3969
  WHERE status = 'needs_review' AND updated_at > ?
3794
- AND (session_scope = ? OR session_scope IS NULL)`,
3970
+ AND session_scope = ?`,
3795
3971
  args: [sinceIso, sessionScope]
3796
3972
  });
3797
3973
  return Number(result2.rows[0]?.cnt) || 0;
@@ -3809,7 +3985,7 @@ async function listPendingReviews(limit, sessionScope) {
3809
3985
  const result2 = await client.execute({
3810
3986
  sql: `SELECT title, assigned_to, project_name FROM tasks
3811
3987
  WHERE status = 'needs_review'
3812
- AND (session_scope = ? OR session_scope IS NULL)
3988
+ AND session_scope = ?
3813
3989
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
3814
3990
  args: [sessionScope, limit]
3815
3991
  });
@@ -3930,14 +4106,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3930
4106
  if (parts.length >= 3 && parts[0] === "review") {
3931
4107
  const agent = parts[1];
3932
4108
  const slug = parts.slice(2).join("-");
3933
- const originalTaskFile = `exe/${agent}/${slug}.md`;
4109
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
3934
4110
  const result = await client.execute({
3935
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
3936
- args: [now, originalTaskFile]
4111
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
4112
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
3937
4113
  });
3938
4114
  if (result.rowsAffected > 0) {
3939
4115
  process.stderr.write(
3940
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
4116
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
3941
4117
  `
3942
4118
  );
3943
4119
  }
@@ -4119,7 +4295,7 @@ function findSessionForProject(projectName) {
4119
4295
  const sessions = listSessions();
4120
4296
  for (const s of sessions) {
4121
4297
  const proj = s.projectDir.split("/").filter(Boolean).pop();
4122
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
4298
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
4123
4299
  }
4124
4300
  return null;
4125
4301
  }
@@ -4165,7 +4341,7 @@ var init_session_scope = __esm({
4165
4341
 
4166
4342
  // src/lib/tasks-notify.ts
4167
4343
  async function dispatchTaskToEmployee(input) {
4168
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
4344
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
4169
4345
  let crossProject = false;
4170
4346
  if (input.projectName) {
4171
4347
  try {
@@ -4644,7 +4820,7 @@ async function updateTask(input) {
4644
4820
  }
4645
4821
  const isTerminal = input.status === "done" || input.status === "needs_review";
4646
4822
  if (isTerminal) {
4647
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
4823
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
4648
4824
  if (!isCoordinator) {
4649
4825
  notifyTaskDone();
4650
4826
  }
@@ -4669,7 +4845,7 @@ async function updateTask(input) {
4669
4845
  }
4670
4846
  }
4671
4847
  }
4672
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4848
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4673
4849
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4674
4850
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4675
4851
  taskId,
@@ -4685,7 +4861,7 @@ async function updateTask(input) {
4685
4861
  });
4686
4862
  }
4687
4863
  let nextTask;
4688
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
4864
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
4689
4865
  try {
4690
4866
  nextTask = await findNextTask(String(row.assigned_to));
4691
4867
  } catch {
@@ -4742,6 +4918,7 @@ var init_tasks = __esm({
4742
4918
  });
4743
4919
 
4744
4920
  // src/lib/store.ts
4921
+ import { createHash } from "crypto";
4745
4922
  init_database();
4746
4923
 
4747
4924
  // src/lib/keychain.ts
@@ -4777,12 +4954,20 @@ async function getMasterKey() {
4777
4954
  }
4778
4955
  const keyPath = getKeyPath();
4779
4956
  if (!existsSync3(keyPath)) {
4957
+ process.stderr.write(
4958
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
4959
+ `
4960
+ );
4780
4961
  return null;
4781
4962
  }
4782
4963
  try {
4783
4964
  const content = await readFile3(keyPath, "utf-8");
4784
4965
  return Buffer.from(content.trim(), "base64");
4785
- } catch {
4966
+ } catch (err) {
4967
+ process.stderr.write(
4968
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
4969
+ `
4970
+ );
4786
4971
  return null;
4787
4972
  }
4788
4973
  }