@askexenow/exe-os 0.9.7 → 0.9.9

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 (101) hide show
  1. package/dist/bin/backfill-conversations.js +953 -105
  2. package/dist/bin/backfill-responses.js +952 -104
  3. package/dist/bin/backfill-vectors.js +956 -108
  4. package/dist/bin/cleanup-stale-review-tasks.js +802 -58
  5. package/dist/bin/cli.js +2292 -1070
  6. package/dist/bin/exe-agent-config.js +157 -101
  7. package/dist/bin/exe-agent.js +55 -29
  8. package/dist/bin/exe-assign.js +940 -92
  9. package/dist/bin/exe-boot.js +1424 -442
  10. package/dist/bin/exe-call.js +240 -141
  11. package/dist/bin/exe-cloud.js +198 -70
  12. package/dist/bin/exe-dispatch.js +951 -192
  13. package/dist/bin/exe-doctor.js +791 -51
  14. package/dist/bin/exe-export-behaviors.js +790 -42
  15. package/dist/bin/exe-forget.js +771 -31
  16. package/dist/bin/exe-gateway.js +1592 -521
  17. package/dist/bin/exe-heartbeat.js +850 -109
  18. package/dist/bin/exe-kill.js +783 -35
  19. package/dist/bin/exe-launch-agent.js +1030 -107
  20. package/dist/bin/exe-link.js +916 -110
  21. package/dist/bin/exe-new-employee.js +526 -217
  22. package/dist/bin/exe-pending-messages.js +1046 -62
  23. package/dist/bin/exe-pending-notifications.js +1318 -111
  24. package/dist/bin/exe-pending-reviews.js +1040 -72
  25. package/dist/bin/exe-rename.js +772 -59
  26. package/dist/bin/exe-review.js +772 -32
  27. package/dist/bin/exe-search.js +982 -128
  28. package/dist/bin/exe-session-cleanup.js +1180 -306
  29. package/dist/bin/exe-settings.js +185 -105
  30. package/dist/bin/exe-start-codex.js +886 -132
  31. package/dist/bin/exe-start-opencode.js +873 -119
  32. package/dist/bin/exe-status.js +803 -59
  33. package/dist/bin/exe-team.js +772 -32
  34. package/dist/bin/git-sweep.js +1046 -223
  35. package/dist/bin/graph-backfill.js +779 -31
  36. package/dist/bin/graph-export.js +785 -37
  37. package/dist/bin/install.js +632 -200
  38. package/dist/bin/scan-tasks.js +1055 -232
  39. package/dist/bin/setup.js +1419 -320
  40. package/dist/bin/shard-migrate.js +783 -35
  41. package/dist/bin/update.js +138 -49
  42. package/dist/bin/wiki-sync.js +782 -34
  43. package/dist/gateway/index.js +1444 -449
  44. package/dist/hooks/bug-report-worker.js +1141 -269
  45. package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
  46. package/dist/hooks/commit-complete.js +1044 -221
  47. package/dist/hooks/error-recall.js +989 -135
  48. package/dist/hooks/exe-heartbeat-hook.js +99 -75
  49. package/dist/hooks/ingest-worker.js +4176 -3226
  50. package/dist/hooks/ingest.js +920 -168
  51. package/dist/hooks/instructions-loaded.js +874 -70
  52. package/dist/hooks/notification.js +860 -56
  53. package/dist/hooks/post-compact.js +881 -73
  54. package/dist/hooks/pre-compact.js +1050 -227
  55. package/dist/hooks/pre-tool-use.js +1084 -159
  56. package/dist/hooks/prompt-ingest-worker.js +1089 -164
  57. package/dist/hooks/prompt-submit.js +1469 -515
  58. package/dist/hooks/response-ingest-worker.js +1104 -179
  59. package/dist/hooks/session-end.js +1085 -251
  60. package/dist/hooks/session-start.js +1241 -231
  61. package/dist/hooks/stop.js +935 -109
  62. package/dist/hooks/subagent-stop.js +881 -73
  63. package/dist/hooks/summary-worker.js +1323 -307
  64. package/dist/index.js +1449 -452
  65. package/dist/lib/agent-config.js +28 -6
  66. package/dist/lib/cloud-sync.js +909 -115
  67. package/dist/lib/config.js +30 -10
  68. package/dist/lib/consolidation.js +42 -9
  69. package/dist/lib/database.js +739 -33
  70. package/dist/lib/db-daemon-client.js +73 -19
  71. package/dist/lib/db.js +2359 -0
  72. package/dist/lib/device-registry.js +760 -47
  73. package/dist/lib/embedder.js +201 -73
  74. package/dist/lib/employee-templates.js +30 -4
  75. package/dist/lib/employees.js +290 -86
  76. package/dist/lib/exe-daemon-client.js +187 -83
  77. package/dist/lib/exe-daemon.js +1696 -616
  78. package/dist/lib/hybrid-search.js +982 -128
  79. package/dist/lib/identity.js +43 -13
  80. package/dist/lib/license.js +133 -48
  81. package/dist/lib/messaging.js +167 -80
  82. package/dist/lib/reminders.js +35 -5
  83. package/dist/lib/schedules.js +772 -32
  84. package/dist/lib/skill-learning.js +54 -7
  85. package/dist/lib/store.js +779 -31
  86. package/dist/lib/task-router.js +94 -73
  87. package/dist/lib/tasks.js +298 -225
  88. package/dist/lib/tmux-routing.js +246 -172
  89. package/dist/lib/token-spend.js +52 -14
  90. package/dist/mcp/server.js +2893 -850
  91. package/dist/mcp/tools/complete-reminder.js +35 -5
  92. package/dist/mcp/tools/create-reminder.js +35 -5
  93. package/dist/mcp/tools/create-task.js +507 -323
  94. package/dist/mcp/tools/deactivate-behavior.js +40 -10
  95. package/dist/mcp/tools/list-reminders.js +35 -5
  96. package/dist/mcp/tools/list-tasks.js +277 -104
  97. package/dist/mcp/tools/send-message.js +129 -56
  98. package/dist/mcp/tools/update-task.js +1864 -188
  99. package/dist/runtime/index.js +1083 -259
  100. package/dist/tui/App.js +1501 -434
  101. package/package.json +3 -2
@@ -80,9 +80,47 @@ var init_db_retry = __esm({
80
80
  }
81
81
  });
82
82
 
83
+ // src/lib/secure-files.ts
84
+ import { chmodSync, existsSync, mkdirSync } from "fs";
85
+ import { chmod, mkdir } from "fs/promises";
86
+ async function ensurePrivateDir(dirPath) {
87
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
88
+ try {
89
+ await chmod(dirPath, PRIVATE_DIR_MODE);
90
+ } catch {
91
+ }
92
+ }
93
+ function ensurePrivateDirSync(dirPath) {
94
+ mkdirSync(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
95
+ try {
96
+ chmodSync(dirPath, PRIVATE_DIR_MODE);
97
+ } catch {
98
+ }
99
+ }
100
+ async function enforcePrivateFile(filePath) {
101
+ try {
102
+ await chmod(filePath, PRIVATE_FILE_MODE);
103
+ } catch {
104
+ }
105
+ }
106
+ function enforcePrivateFileSync(filePath) {
107
+ try {
108
+ if (existsSync(filePath)) chmodSync(filePath, PRIVATE_FILE_MODE);
109
+ } catch {
110
+ }
111
+ }
112
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
113
+ var init_secure_files = __esm({
114
+ "src/lib/secure-files.ts"() {
115
+ "use strict";
116
+ PRIVATE_DIR_MODE = 448;
117
+ PRIVATE_FILE_MODE = 384;
118
+ }
119
+ });
120
+
83
121
  // src/lib/config.ts
84
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
85
- import { readFileSync, existsSync, renameSync } from "fs";
122
+ import { readFile, writeFile } from "fs/promises";
123
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
86
124
  import path from "path";
87
125
  import os from "os";
88
126
  function resolveDataDir() {
@@ -90,7 +128,7 @@ function resolveDataDir() {
90
128
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
91
129
  const newDir = path.join(os.homedir(), ".exe-os");
92
130
  const legacyDir = path.join(os.homedir(), ".exe-mem");
93
- if (!existsSync(newDir) && existsSync(legacyDir)) {
131
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
94
132
  try {
95
133
  renameSync(legacyDir, newDir);
96
134
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -153,9 +191,9 @@ function normalizeAutoUpdate(raw) {
153
191
  }
154
192
  async function loadConfig() {
155
193
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
156
- await mkdir(dir, { recursive: true });
194
+ await ensurePrivateDir(dir);
157
195
  const configPath = path.join(dir, "config.json");
158
- if (!existsSync(configPath)) {
196
+ if (!existsSync2(configPath)) {
159
197
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
160
198
  }
161
199
  const raw = await readFile(configPath, "utf-8");
@@ -168,6 +206,7 @@ async function loadConfig() {
168
206
  `);
169
207
  try {
170
208
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
209
+ await enforcePrivateFile(configPath);
171
210
  } catch {
172
211
  }
173
212
  }
@@ -187,6 +226,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
187
226
  var init_config = __esm({
188
227
  "src/lib/config.ts"() {
189
228
  "use strict";
229
+ init_secure_files();
190
230
  EXE_AI_DIR = resolveDataDir();
191
231
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
192
232
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
@@ -263,6 +303,120 @@ var init_config = __esm({
263
303
  }
264
304
  });
265
305
 
306
+ // src/lib/runtime-table.ts
307
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
308
+ var init_runtime_table = __esm({
309
+ "src/lib/runtime-table.ts"() {
310
+ "use strict";
311
+ RUNTIME_TABLE = {
312
+ codex: {
313
+ binary: "codex",
314
+ launchMode: "interactive",
315
+ autoApproveFlag: "--dangerously-bypass-approvals-and-sandbox",
316
+ inlineFlag: "--no-alt-screen",
317
+ apiKeyEnv: "OPENAI_API_KEY",
318
+ defaultModel: "gpt-5.4"
319
+ },
320
+ opencode: {
321
+ binary: "opencode",
322
+ launchMode: "exec",
323
+ autoApproveFlag: "--dangerously-skip-permissions",
324
+ inlineFlag: "",
325
+ apiKeyEnv: "ANTHROPIC_API_KEY",
326
+ defaultModel: "anthropic/claude-sonnet-4-6"
327
+ }
328
+ };
329
+ DEFAULT_RUNTIME = "claude";
330
+ }
331
+ });
332
+
333
+ // src/lib/agent-config.ts
334
+ var agent_config_exports = {};
335
+ __export(agent_config_exports, {
336
+ AGENT_CONFIG_PATH: () => AGENT_CONFIG_PATH,
337
+ DEFAULT_MODELS: () => DEFAULT_MODELS,
338
+ KNOWN_RUNTIMES: () => KNOWN_RUNTIMES,
339
+ RUNTIME_LABELS: () => RUNTIME_LABELS,
340
+ clearAgentRuntime: () => clearAgentRuntime,
341
+ getAgentRuntime: () => getAgentRuntime,
342
+ loadAgentConfig: () => loadAgentConfig,
343
+ saveAgentConfig: () => saveAgentConfig,
344
+ setAgentRuntime: () => setAgentRuntime
345
+ });
346
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
347
+ import path2 from "path";
348
+ function loadAgentConfig() {
349
+ if (!existsSync3(AGENT_CONFIG_PATH)) return {};
350
+ try {
351
+ return JSON.parse(readFileSync2(AGENT_CONFIG_PATH, "utf-8"));
352
+ } catch {
353
+ return {};
354
+ }
355
+ }
356
+ function saveAgentConfig(config) {
357
+ const dir = path2.dirname(AGENT_CONFIG_PATH);
358
+ ensurePrivateDirSync(dir);
359
+ writeFileSync(AGENT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
360
+ enforcePrivateFileSync(AGENT_CONFIG_PATH);
361
+ }
362
+ function getAgentRuntime(agentId) {
363
+ const config = loadAgentConfig();
364
+ const entry = config[agentId];
365
+ if (entry) return entry;
366
+ const orgDefault = config["default"];
367
+ if (orgDefault) return orgDefault;
368
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
369
+ }
370
+ function setAgentRuntime(agentId, runtime, model) {
371
+ const knownModels = KNOWN_RUNTIMES[runtime];
372
+ if (!knownModels) {
373
+ return {
374
+ ok: false,
375
+ error: `Unknown runtime "${runtime}". Valid: ${Object.keys(KNOWN_RUNTIMES).join(", ")}`
376
+ };
377
+ }
378
+ if (!knownModels.includes(model)) {
379
+ return {
380
+ ok: false,
381
+ error: `Unknown model "${model}" for runtime "${runtime}". Valid: ${knownModels.join(", ")}`
382
+ };
383
+ }
384
+ const config = loadAgentConfig();
385
+ config[agentId] = { runtime, model };
386
+ saveAgentConfig(config);
387
+ return { ok: true };
388
+ }
389
+ function clearAgentRuntime(agentId) {
390
+ const config = loadAgentConfig();
391
+ delete config[agentId];
392
+ saveAgentConfig(config);
393
+ }
394
+ var AGENT_CONFIG_PATH, KNOWN_RUNTIMES, RUNTIME_LABELS, DEFAULT_MODELS;
395
+ var init_agent_config = __esm({
396
+ "src/lib/agent-config.ts"() {
397
+ "use strict";
398
+ init_config();
399
+ init_runtime_table();
400
+ init_secure_files();
401
+ AGENT_CONFIG_PATH = path2.join(EXE_AI_DIR, "agent-config.json");
402
+ KNOWN_RUNTIMES = {
403
+ claude: ["claude-opus-4", "claude-sonnet-4", "claude-haiku-4.5"],
404
+ codex: ["gpt-5.4", "gpt-5.5", "gpt-5.3-codex-spark", "o3", "o4-mini"],
405
+ opencode: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4", "google/gemini-2.5-pro", "deepseek/deepseek-r3", "minimax/minimax-m2.5"]
406
+ };
407
+ RUNTIME_LABELS = {
408
+ claude: "Claude Code (Anthropic)",
409
+ codex: "Codex (OpenAI)",
410
+ opencode: "OpenCode (open source)"
411
+ };
412
+ DEFAULT_MODELS = {
413
+ claude: "claude-opus-4",
414
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
415
+ opencode: RUNTIME_TABLE.opencode?.defaultModel ?? "anthropic/claude-sonnet-4-6"
416
+ };
417
+ }
418
+ });
419
+
266
420
  // src/lib/employees.ts
267
421
  var employees_exports = {};
268
422
  __export(employees_exports, {
@@ -278,6 +432,7 @@ __export(employees_exports, {
278
432
  getEmployeeByRole: () => getEmployeeByRole,
279
433
  getEmployeeNamesByRole: () => getEmployeeNamesByRole,
280
434
  hasRole: () => hasRole,
435
+ hireEmployee: () => hireEmployee,
281
436
  isCoordinatorName: () => isCoordinatorName,
282
437
  isCoordinatorRole: () => isCoordinatorRole,
283
438
  isMultiInstance: () => isMultiInstance,
@@ -290,9 +445,9 @@ __export(employees_exports, {
290
445
  validateEmployeeName: () => validateEmployeeName
291
446
  });
292
447
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
293
- import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
448
+ import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
294
449
  import { execSync } from "child_process";
295
- import path2 from "path";
450
+ import path3 from "path";
296
451
  import os2 from "os";
297
452
  function normalizeRole(role) {
298
453
  return (role ?? "").trim().toLowerCase();
@@ -329,7 +484,7 @@ function validateEmployeeName(name) {
329
484
  return { valid: true };
330
485
  }
331
486
  async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
332
- if (!existsSync2(employeesPath)) {
487
+ if (!existsSync4(employeesPath)) {
333
488
  return [];
334
489
  }
335
490
  const raw = await readFile2(employeesPath, "utf-8");
@@ -340,13 +495,13 @@ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
340
495
  }
341
496
  }
342
497
  async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
343
- await mkdir2(path2.dirname(employeesPath), { recursive: true });
498
+ await mkdir2(path3.dirname(employeesPath), { recursive: true });
344
499
  await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
345
500
  }
346
501
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
347
- if (!existsSync2(employeesPath)) return [];
502
+ if (!existsSync4(employeesPath)) return [];
348
503
  try {
349
- return JSON.parse(readFileSync2(employeesPath, "utf-8"));
504
+ return JSON.parse(readFileSync3(employeesPath, "utf-8"));
350
505
  } catch {
351
506
  return [];
352
507
  }
@@ -388,6 +543,52 @@ function addEmployee(employees, employee) {
388
543
  }
389
544
  return [...employees, normalized];
390
545
  }
546
+ function appendToCoordinatorTeam(employee) {
547
+ const coordinator = getCoordinatorEmployee(loadEmployeesSync());
548
+ if (!coordinator) return;
549
+ const idPath = path3.join(IDENTITY_DIR, `${coordinator.name}.md`);
550
+ if (!existsSync4(idPath)) return;
551
+ const content = readFileSync3(idPath, "utf-8");
552
+ if (content.includes(`**${capitalize(employee.name)}`)) return;
553
+ const teamMatch = content.match(TEAM_SECTION_RE);
554
+ if (!teamMatch || teamMatch.index === void 0) return;
555
+ const afterTeam = content.slice(teamMatch.index + teamMatch[0].length);
556
+ const nextHeading = afterTeam.match(/\n## /);
557
+ const entry = `
558
+ **${capitalize(employee.name)} (${employee.role}):** Newly hired. Update this description as the role develops.
559
+ `;
560
+ let updated;
561
+ if (nextHeading && nextHeading.index !== void 0) {
562
+ const insertAt = teamMatch.index + teamMatch[0].length + nextHeading.index;
563
+ updated = content.slice(0, insertAt) + entry + content.slice(insertAt);
564
+ } else {
565
+ updated = content.trimEnd() + "\n" + entry;
566
+ }
567
+ writeFileSync2(idPath, updated, "utf-8");
568
+ }
569
+ function capitalize(s) {
570
+ return s.charAt(0).toUpperCase() + s.slice(1);
571
+ }
572
+ async function hireEmployee(employee) {
573
+ const employees = await loadEmployees();
574
+ const updated = addEmployee(employees, employee);
575
+ await saveEmployees(updated);
576
+ try {
577
+ appendToCoordinatorTeam(employee);
578
+ } catch {
579
+ }
580
+ try {
581
+ const { loadAgentConfig: loadAgentConfig2, saveAgentConfig: saveAgentConfig2 } = await Promise.resolve().then(() => (init_agent_config(), agent_config_exports));
582
+ const config = loadAgentConfig2();
583
+ const name = employee.name.toLowerCase();
584
+ if (!config[name] && config["default"]) {
585
+ config[name] = { ...config["default"] };
586
+ saveAgentConfig2(config);
587
+ }
588
+ } catch {
589
+ }
590
+ return updated;
591
+ }
391
592
  async function normalizeRosterCase(rosterPath) {
392
593
  const employees = await loadEmployees(rosterPath);
393
594
  let changed = false;
@@ -397,14 +598,14 @@ async function normalizeRosterCase(rosterPath) {
397
598
  emp.name = emp.name.toLowerCase();
398
599
  changed = true;
399
600
  try {
400
- const identityDir = path2.join(os2.homedir(), ".exe-os", "identity");
401
- const oldPath = path2.join(identityDir, `${oldName}.md`);
402
- const newPath = path2.join(identityDir, `${emp.name}.md`);
403
- if (existsSync2(oldPath) && !existsSync2(newPath)) {
601
+ const identityDir = path3.join(os2.homedir(), ".exe-os", "identity");
602
+ const oldPath = path3.join(identityDir, `${oldName}.md`);
603
+ const newPath = path3.join(identityDir, `${emp.name}.md`);
604
+ if (existsSync4(oldPath) && !existsSync4(newPath)) {
404
605
  renameSync2(oldPath, newPath);
405
- } else if (existsSync2(oldPath) && oldPath !== newPath) {
406
- const content = readFileSync2(oldPath, "utf-8");
407
- writeFileSync(newPath, content, "utf-8");
606
+ } else if (existsSync4(oldPath) && oldPath !== newPath) {
607
+ const content = readFileSync3(oldPath, "utf-8");
608
+ writeFileSync2(newPath, content, "utf-8");
408
609
  if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
409
610
  unlinkSync(oldPath);
410
611
  }
@@ -434,7 +635,7 @@ function registerBinSymlinks(name) {
434
635
  errors.push("Could not find 'exe-os' in PATH");
435
636
  return { created, skipped, errors };
436
637
  }
437
- const binDir = path2.dirname(exeBinPath);
638
+ const binDir = path3.dirname(exeBinPath);
438
639
  let target;
439
640
  try {
440
641
  target = readlinkSync(exeBinPath);
@@ -444,8 +645,8 @@ function registerBinSymlinks(name) {
444
645
  }
445
646
  for (const suffix of ["", "-opencode"]) {
446
647
  const linkName = `${name}${suffix}`;
447
- const linkPath = path2.join(binDir, linkName);
448
- if (existsSync2(linkPath)) {
648
+ const linkPath = path3.join(binDir, linkName);
649
+ if (existsSync4(linkPath)) {
449
650
  skipped.push(linkName);
450
651
  continue;
451
652
  }
@@ -458,21 +659,619 @@ function registerBinSymlinks(name) {
458
659
  }
459
660
  return { created, skipped, errors };
460
661
  }
461
- var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
662
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES, IDENTITY_DIR, TEAM_SECTION_RE;
462
663
  var init_employees = __esm({
463
664
  "src/lib/employees.ts"() {
464
665
  "use strict";
465
666
  init_config();
466
- EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
667
+ EMPLOYEES_PATH = path3.join(EXE_AI_DIR, "exe-employees.json");
467
668
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
468
669
  COORDINATOR_ROLE = "COO";
469
670
  MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
671
+ IDENTITY_DIR = path3.join(EXE_AI_DIR, "identity");
672
+ TEAM_SECTION_RE = /^## Team\b.*$/m;
673
+ }
674
+ });
675
+
676
+ // src/lib/database-adapter.ts
677
+ import os3 from "os";
678
+ import path4 from "path";
679
+ import { createRequire } from "module";
680
+ import { pathToFileURL } from "url";
681
+ function quotedIdentifier(identifier) {
682
+ return `"${identifier.replace(/"/g, '""')}"`;
683
+ }
684
+ function unqualifiedTableName(name) {
685
+ const raw = name.trim().replace(/^"|"$/g, "");
686
+ const parts = raw.split(".");
687
+ return parts[parts.length - 1].replace(/^"|"$/g, "").toLowerCase();
688
+ }
689
+ function stripTrailingSemicolon(sql) {
690
+ return sql.trim().replace(/;+\s*$/u, "");
691
+ }
692
+ function appendClause(sql, clause) {
693
+ const trimmed = stripTrailingSemicolon(sql);
694
+ const returningMatch = /\sRETURNING\b[\s\S]*$/iu.exec(trimmed);
695
+ if (!returningMatch) {
696
+ return `${trimmed}${clause}`;
697
+ }
698
+ const idx = returningMatch.index;
699
+ return `${trimmed.slice(0, idx)}${clause}${trimmed.slice(idx)}`;
700
+ }
701
+ function normalizeStatement(stmt) {
702
+ if (typeof stmt === "string") {
703
+ return { kind: "positional", sql: stmt, args: [] };
704
+ }
705
+ const sql = stmt.sql;
706
+ if (Array.isArray(stmt.args) || stmt.args === void 0) {
707
+ return { kind: "positional", sql, args: stmt.args ?? [] };
708
+ }
709
+ return { kind: "named", sql, args: stmt.args };
710
+ }
711
+ function rewriteBooleanLiterals(sql) {
712
+ let out = sql;
713
+ for (const column of BOOLEAN_COLUMN_NAMES) {
714
+ const scoped = `((?:\\b[a-z_][a-z0-9_]*\\.)?${column})`;
715
+ out = out.replace(new RegExp(`${scoped}\\s*=\\s*0\\b`, "giu"), "$1 = FALSE");
716
+ out = out.replace(new RegExp(`${scoped}\\s*=\\s*1\\b`, "giu"), "$1 = TRUE");
717
+ out = out.replace(new RegExp(`${scoped}\\s*!=\\s*0\\b`, "giu"), "$1 != FALSE");
718
+ out = out.replace(new RegExp(`${scoped}\\s*!=\\s*1\\b`, "giu"), "$1 != TRUE");
719
+ out = out.replace(new RegExp(`${scoped}\\s*<>\\s*0\\b`, "giu"), "$1 <> FALSE");
720
+ out = out.replace(new RegExp(`${scoped}\\s*<>\\s*1\\b`, "giu"), "$1 <> TRUE");
721
+ }
722
+ return out;
723
+ }
724
+ function rewriteInsertOrIgnore(sql) {
725
+ if (!/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu.test(sql)) {
726
+ return sql;
727
+ }
728
+ const replaced = sql.replace(/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu, "INSERT INTO");
729
+ return /\bON\s+CONFLICT\b/iu.test(replaced) ? replaced : appendClause(replaced, " ON CONFLICT DO NOTHING");
730
+ }
731
+ function rewriteInsertOrReplace(sql) {
732
+ const match = /^\s*INSERT\s+OR\s+REPLACE\s+INTO\s+([A-Za-z0-9_."]+)\s*\(([^)]+)\)([\s\S]*)$/iu.exec(sql);
733
+ if (!match) {
734
+ return sql;
735
+ }
736
+ const rawTable = match[1];
737
+ const rawColumns = match[2];
738
+ const remainder = match[3];
739
+ const tableName = unqualifiedTableName(rawTable);
740
+ const conflictKeys = UPSERT_KEYS[tableName];
741
+ if (!conflictKeys?.length) {
742
+ return sql;
743
+ }
744
+ const columns = rawColumns.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
745
+ const updateColumns = columns.filter((col) => !conflictKeys.includes(col));
746
+ const conflictTarget = conflictKeys.map(quotedIdentifier).join(", ");
747
+ const updateClause = updateColumns.length === 0 ? " DO NOTHING" : ` DO UPDATE SET ${updateColumns.map((col) => `${quotedIdentifier(col)} = EXCLUDED.${quotedIdentifier(col)}`).join(", ")}`;
748
+ return `INSERT INTO ${rawTable} (${rawColumns})${appendClause(remainder, ` ON CONFLICT (${conflictTarget})${updateClause}`)}`;
749
+ }
750
+ function rewriteSql(sql) {
751
+ let out = sql;
752
+ out = out.replace(/\bdatetime\(\s*['"]now['"]\s*\)/giu, "CURRENT_TIMESTAMP");
753
+ out = out.replace(/\bvector32\s*\(\s*\?\s*\)/giu, "?");
754
+ out = rewriteBooleanLiterals(out);
755
+ out = rewriteInsertOrReplace(out);
756
+ out = rewriteInsertOrIgnore(out);
757
+ return stripTrailingSemicolon(out);
758
+ }
759
+ function toBoolean(value) {
760
+ if (value === null || value === void 0) return value;
761
+ if (typeof value === "boolean") return value;
762
+ if (typeof value === "number") return value !== 0;
763
+ if (typeof value === "bigint") return value !== 0n;
764
+ if (typeof value === "string") {
765
+ const normalized = value.trim().toLowerCase();
766
+ if (normalized === "0" || normalized === "false") return false;
767
+ if (normalized === "1" || normalized === "true") return true;
768
+ }
769
+ return Boolean(value);
770
+ }
771
+ function countQuestionMarks(sql, end) {
772
+ let count = 0;
773
+ let inSingle = false;
774
+ let inDouble = false;
775
+ let inLineComment = false;
776
+ let inBlockComment = false;
777
+ for (let i = 0; i < end; i++) {
778
+ const ch = sql[i];
779
+ const next = sql[i + 1];
780
+ if (inLineComment) {
781
+ if (ch === "\n") inLineComment = false;
782
+ continue;
783
+ }
784
+ if (inBlockComment) {
785
+ if (ch === "*" && next === "/") {
786
+ inBlockComment = false;
787
+ i += 1;
788
+ }
789
+ continue;
790
+ }
791
+ if (!inSingle && !inDouble && ch === "-" && next === "-") {
792
+ inLineComment = true;
793
+ i += 1;
794
+ continue;
795
+ }
796
+ if (!inSingle && !inDouble && ch === "/" && next === "*") {
797
+ inBlockComment = true;
798
+ i += 1;
799
+ continue;
800
+ }
801
+ if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
802
+ inSingle = !inSingle;
803
+ continue;
804
+ }
805
+ if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
806
+ inDouble = !inDouble;
807
+ continue;
808
+ }
809
+ if (!inSingle && !inDouble && ch === "?") {
810
+ count += 1;
811
+ }
812
+ }
813
+ return count;
814
+ }
815
+ function findBooleanPlaceholderIndexes(sql) {
816
+ const indexes = /* @__PURE__ */ new Set();
817
+ for (const column of BOOLEAN_COLUMN_NAMES) {
818
+ const pattern = new RegExp(`(?:\\b[a-z_][a-z0-9_]*\\.)?${column}\\s*=\\s*\\?`, "giu");
819
+ for (const match of sql.matchAll(pattern)) {
820
+ const matchText = match[0];
821
+ const qIndex = match.index + matchText.lastIndexOf("?");
822
+ indexes.add(countQuestionMarks(sql, qIndex + 1));
823
+ }
824
+ }
825
+ return indexes;
826
+ }
827
+ function coerceInsertBooleanArgs(sql, args) {
828
+ const match = /^\s*INSERT(?:\s+OR\s+(?:IGNORE|REPLACE))?\s+INTO\s+([A-Za-z0-9_."]+)\s*\(([^)]+)\)/iu.exec(sql);
829
+ if (!match) return;
830
+ const rawTable = match[1];
831
+ const rawColumns = match[2];
832
+ const boolColumns = BOOLEAN_COLUMNS_BY_TABLE[unqualifiedTableName(rawTable)];
833
+ if (!boolColumns?.size) return;
834
+ const columns = rawColumns.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
835
+ for (const [index, column] of columns.entries()) {
836
+ if (boolColumns.has(column) && index < args.length) {
837
+ args[index] = toBoolean(args[index]);
838
+ }
839
+ }
840
+ }
841
+ function coerceUpdateBooleanArgs(sql, args) {
842
+ const match = /^\s*UPDATE\s+([A-Za-z0-9_."]+)\s+SET\s+([\s\S]+?)(?:\s+WHERE\b|$)/iu.exec(sql);
843
+ if (!match) return;
844
+ const rawTable = match[1];
845
+ const setClause = match[2];
846
+ const boolColumns = BOOLEAN_COLUMNS_BY_TABLE[unqualifiedTableName(rawTable)];
847
+ if (!boolColumns?.size) return;
848
+ const assignments = setClause.split(",");
849
+ let placeholderIndex = 0;
850
+ for (const assignment of assignments) {
851
+ if (!assignment.includes("?")) continue;
852
+ placeholderIndex += 1;
853
+ const colMatch = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*\.)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\?/iu.exec(assignment);
854
+ if (colMatch && boolColumns.has(colMatch[1])) {
855
+ args[placeholderIndex - 1] = toBoolean(args[placeholderIndex - 1]);
856
+ }
857
+ }
858
+ }
859
+ function coerceBooleanArgs(sql, args) {
860
+ const nextArgs = [...args];
861
+ coerceInsertBooleanArgs(sql, nextArgs);
862
+ coerceUpdateBooleanArgs(sql, nextArgs);
863
+ const placeholderIndexes = findBooleanPlaceholderIndexes(sql);
864
+ for (const index of placeholderIndexes) {
865
+ if (index > 0 && index <= nextArgs.length) {
866
+ nextArgs[index - 1] = toBoolean(nextArgs[index - 1]);
867
+ }
868
+ }
869
+ return nextArgs;
870
+ }
871
+ function convertQuestionMarksToDollarParams(sql) {
872
+ let out = "";
873
+ let placeholder = 0;
874
+ let inSingle = false;
875
+ let inDouble = false;
876
+ let inLineComment = false;
877
+ let inBlockComment = false;
878
+ for (let i = 0; i < sql.length; i++) {
879
+ const ch = sql[i];
880
+ const next = sql[i + 1];
881
+ if (inLineComment) {
882
+ out += ch;
883
+ if (ch === "\n") inLineComment = false;
884
+ continue;
885
+ }
886
+ if (inBlockComment) {
887
+ out += ch;
888
+ if (ch === "*" && next === "/") {
889
+ out += next;
890
+ inBlockComment = false;
891
+ i += 1;
892
+ }
893
+ continue;
894
+ }
895
+ if (!inSingle && !inDouble && ch === "-" && next === "-") {
896
+ out += ch + next;
897
+ inLineComment = true;
898
+ i += 1;
899
+ continue;
900
+ }
901
+ if (!inSingle && !inDouble && ch === "/" && next === "*") {
902
+ out += ch + next;
903
+ inBlockComment = true;
904
+ i += 1;
905
+ continue;
906
+ }
907
+ if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
908
+ inSingle = !inSingle;
909
+ out += ch;
910
+ continue;
911
+ }
912
+ if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
913
+ inDouble = !inDouble;
914
+ out += ch;
915
+ continue;
916
+ }
917
+ if (!inSingle && !inDouble && ch === "?") {
918
+ placeholder += 1;
919
+ out += `$${placeholder}`;
920
+ continue;
921
+ }
922
+ out += ch;
923
+ }
924
+ return out;
925
+ }
926
+ function translateStatementForPostgres(stmt) {
927
+ const normalized = normalizeStatement(stmt);
928
+ if (normalized.kind === "named") {
929
+ throw new Error("Named SQL parameters are not supported by the Prisma adapter.");
930
+ }
931
+ const rewrittenSql = rewriteSql(normalized.sql);
932
+ const coercedArgs = coerceBooleanArgs(rewrittenSql, normalized.args);
933
+ return {
934
+ sql: convertQuestionMarksToDollarParams(rewrittenSql),
935
+ args: coercedArgs
936
+ };
937
+ }
938
+ function shouldBypassPostgres(stmt) {
939
+ const normalized = normalizeStatement(stmt);
940
+ if (normalized.kind === "named") {
941
+ return true;
942
+ }
943
+ return IMMEDIATE_FALLBACK_PATTERNS.some((pattern) => pattern.test(normalized.sql));
944
+ }
945
+ function shouldFallbackOnError(error) {
946
+ const message = error instanceof Error ? error.message : String(error);
947
+ return /42P01|42883|42601|does not exist|syntax error|not supported|Named SQL parameters are not supported/iu.test(message);
948
+ }
949
+ function isReadQuery(sql) {
950
+ const trimmed = sql.trimStart();
951
+ return /^(SELECT|WITH|SHOW|EXPLAIN|VALUES)\b/iu.test(trimmed) || /\bRETURNING\b/iu.test(trimmed);
952
+ }
953
+ function buildRow(row, columns) {
954
+ const values = columns.map((column) => row[column]);
955
+ return Object.assign(values, row);
956
+ }
957
+ function buildResultSet(rows, rowsAffected = 0) {
958
+ const columns = rows[0] ? Object.keys(rows[0]) : [];
959
+ const resultRows = rows.map((row) => buildRow(row, columns));
960
+ return {
961
+ columns,
962
+ columnTypes: columns.map(() => ""),
963
+ rows: resultRows,
964
+ rowsAffected,
965
+ lastInsertRowid: void 0,
966
+ toJSON() {
967
+ return {
968
+ columns,
969
+ columnTypes: columns.map(() => ""),
970
+ rows,
971
+ rowsAffected,
972
+ lastInsertRowid: void 0
973
+ };
974
+ }
975
+ };
976
+ }
977
+ async function loadPrismaClient() {
978
+ if (!prismaClientPromise) {
979
+ prismaClientPromise = (async () => {
980
+ const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
981
+ if (explicitPath) {
982
+ const module2 = await import(pathToFileURL(explicitPath).href);
983
+ const PrismaClient2 = module2.PrismaClient ?? module2.default?.PrismaClient;
984
+ if (!PrismaClient2) {
985
+ throw new Error(`No PrismaClient export found at ${explicitPath}`);
986
+ }
987
+ return new PrismaClient2();
988
+ }
989
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path4.join(os3.homedir(), "exe-db");
990
+ const requireFromExeDb = createRequire(path4.join(exeDbRoot, "package.json"));
991
+ const prismaEntry = requireFromExeDb.resolve("@prisma/client");
992
+ const module = await import(pathToFileURL(prismaEntry).href);
993
+ const PrismaClient = module.PrismaClient ?? module.default?.PrismaClient;
994
+ if (!PrismaClient) {
995
+ throw new Error(`No PrismaClient export found in ${prismaEntry}`);
996
+ }
997
+ return new PrismaClient();
998
+ })();
999
+ }
1000
+ return prismaClientPromise;
1001
+ }
1002
+ async function ensureCompatibilityViews(prisma) {
1003
+ if (!compatibilityBootstrapPromise) {
1004
+ compatibilityBootstrapPromise = (async () => {
1005
+ for (const mapping of VIEW_MAPPINGS) {
1006
+ const relation = mapping.source.replace(/"/g, "");
1007
+ const rows = await prisma.$queryRawUnsafe(
1008
+ "SELECT to_regclass($1) AS regclass",
1009
+ relation
1010
+ );
1011
+ if (!rows[0]?.regclass) {
1012
+ continue;
1013
+ }
1014
+ await prisma.$executeRawUnsafe(
1015
+ `CREATE OR REPLACE VIEW public.${quotedIdentifier(mapping.view)} AS SELECT * FROM ${mapping.source}`
1016
+ );
1017
+ }
1018
+ })();
1019
+ }
1020
+ return compatibilityBootstrapPromise;
1021
+ }
1022
+ async function executeOnPrisma(executor, stmt) {
1023
+ const translated = translateStatementForPostgres(stmt);
1024
+ if (isReadQuery(translated.sql)) {
1025
+ const rows = await executor.$queryRawUnsafe(
1026
+ translated.sql,
1027
+ ...translated.args
1028
+ );
1029
+ return buildResultSet(rows, /\bRETURNING\b/iu.test(translated.sql) ? rows.length : 0);
1030
+ }
1031
+ const rowsAffected = await executor.$executeRawUnsafe(translated.sql, ...translated.args);
1032
+ return buildResultSet([], rowsAffected);
1033
+ }
1034
+ function splitSqlStatements(sql) {
1035
+ const parts = [];
1036
+ let current = "";
1037
+ let inSingle = false;
1038
+ let inDouble = false;
1039
+ let inLineComment = false;
1040
+ let inBlockComment = false;
1041
+ for (let i = 0; i < sql.length; i++) {
1042
+ const ch = sql[i];
1043
+ const next = sql[i + 1];
1044
+ if (inLineComment) {
1045
+ current += ch;
1046
+ if (ch === "\n") inLineComment = false;
1047
+ continue;
1048
+ }
1049
+ if (inBlockComment) {
1050
+ current += ch;
1051
+ if (ch === "*" && next === "/") {
1052
+ current += next;
1053
+ inBlockComment = false;
1054
+ i += 1;
1055
+ }
1056
+ continue;
1057
+ }
1058
+ if (!inSingle && !inDouble && ch === "-" && next === "-") {
1059
+ current += ch + next;
1060
+ inLineComment = true;
1061
+ i += 1;
1062
+ continue;
1063
+ }
1064
+ if (!inSingle && !inDouble && ch === "/" && next === "*") {
1065
+ current += ch + next;
1066
+ inBlockComment = true;
1067
+ i += 1;
1068
+ continue;
1069
+ }
1070
+ if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
1071
+ inSingle = !inSingle;
1072
+ current += ch;
1073
+ continue;
1074
+ }
1075
+ if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
1076
+ inDouble = !inDouble;
1077
+ current += ch;
1078
+ continue;
1079
+ }
1080
+ if (!inSingle && !inDouble && ch === ";") {
1081
+ if (current.trim()) {
1082
+ parts.push(current.trim());
1083
+ }
1084
+ current = "";
1085
+ continue;
1086
+ }
1087
+ current += ch;
1088
+ }
1089
+ if (current.trim()) {
1090
+ parts.push(current.trim());
1091
+ }
1092
+ return parts;
1093
+ }
1094
+ async function createPrismaDbAdapter(fallbackClient) {
1095
+ const prisma = await loadPrismaClient();
1096
+ await ensureCompatibilityViews(prisma);
1097
+ let closed = false;
1098
+ let adapter;
1099
+ const fallbackExecute = async (stmt, error) => {
1100
+ if (!fallbackClient) {
1101
+ if (error) throw error;
1102
+ throw new Error("No fallback SQLite client is available for this Prisma-routed query.");
1103
+ }
1104
+ if (error) {
1105
+ process.stderr.write(
1106
+ `[database-adapter] Falling back to SQLite: ${error instanceof Error ? error.message : String(error)}
1107
+ `
1108
+ );
1109
+ }
1110
+ return fallbackClient.execute(stmt);
1111
+ };
1112
+ adapter = {
1113
+ async execute(stmt) {
1114
+ if (shouldBypassPostgres(stmt)) {
1115
+ return fallbackExecute(stmt);
1116
+ }
1117
+ try {
1118
+ return await executeOnPrisma(prisma, stmt);
1119
+ } catch (error) {
1120
+ if (shouldFallbackOnError(error)) {
1121
+ return fallbackExecute(stmt, error);
1122
+ }
1123
+ throw error;
1124
+ }
1125
+ },
1126
+ async batch(stmts, mode) {
1127
+ if (stmts.some((stmt) => shouldBypassPostgres(stmt))) {
1128
+ if (!fallbackClient) {
1129
+ throw new Error("Cannot batch unsupported SQLite-only statements without a fallback client.");
1130
+ }
1131
+ return fallbackClient.batch(stmts, mode);
1132
+ }
1133
+ try {
1134
+ if (prisma.$transaction) {
1135
+ return await prisma.$transaction(async (tx) => {
1136
+ const results2 = [];
1137
+ for (const stmt of stmts) {
1138
+ results2.push(await executeOnPrisma(tx, stmt));
1139
+ }
1140
+ return results2;
1141
+ });
1142
+ }
1143
+ const results = [];
1144
+ for (const stmt of stmts) {
1145
+ results.push(await executeOnPrisma(prisma, stmt));
1146
+ }
1147
+ return results;
1148
+ } catch (error) {
1149
+ if (fallbackClient && shouldFallbackOnError(error)) {
1150
+ process.stderr.write(
1151
+ `[database-adapter] Falling back batch to SQLite: ${error instanceof Error ? error.message : String(error)}
1152
+ `
1153
+ );
1154
+ return fallbackClient.batch(stmts, mode);
1155
+ }
1156
+ throw error;
1157
+ }
1158
+ },
1159
+ async migrate(stmts) {
1160
+ if (fallbackClient) {
1161
+ return fallbackClient.migrate(stmts);
1162
+ }
1163
+ return adapter.batch(stmts, "deferred");
1164
+ },
1165
+ async transaction(mode) {
1166
+ if (!fallbackClient) {
1167
+ throw new Error("Interactive transactions are only supported on the SQLite fallback client.");
1168
+ }
1169
+ return fallbackClient.transaction(mode);
1170
+ },
1171
+ async executeMultiple(sql) {
1172
+ if (fallbackClient && shouldBypassPostgres(sql)) {
1173
+ return fallbackClient.executeMultiple(sql);
1174
+ }
1175
+ for (const statement of splitSqlStatements(sql)) {
1176
+ await adapter.execute(statement);
1177
+ }
1178
+ },
1179
+ async sync() {
1180
+ if (fallbackClient) {
1181
+ return fallbackClient.sync();
1182
+ }
1183
+ return { frame_no: 0, frames_synced: 0 };
1184
+ },
1185
+ close() {
1186
+ closed = true;
1187
+ prismaClientPromise = null;
1188
+ compatibilityBootstrapPromise = null;
1189
+ void prisma.$disconnect?.();
1190
+ },
1191
+ get closed() {
1192
+ return closed;
1193
+ },
1194
+ get protocol() {
1195
+ return "prisma-postgres";
1196
+ }
1197
+ };
1198
+ return adapter;
1199
+ }
1200
+ var VIEW_MAPPINGS, UPSERT_KEYS, BOOLEAN_COLUMNS_BY_TABLE, BOOLEAN_COLUMN_NAMES, IMMEDIATE_FALLBACK_PATTERNS, prismaClientPromise, compatibilityBootstrapPromise;
1201
+ var init_database_adapter = __esm({
1202
+ "src/lib/database-adapter.ts"() {
1203
+ "use strict";
1204
+ VIEW_MAPPINGS = [
1205
+ { view: "memories", source: "memory.memory_records" },
1206
+ { view: "tasks", source: "memory.tasks" },
1207
+ { view: "behaviors", source: "memory.behaviors" },
1208
+ { view: "entities", source: "memory.entities" },
1209
+ { view: "relationships", source: "memory.relationships" },
1210
+ { view: "entity_memories", source: "memory.entity_memories" },
1211
+ { view: "entity_aliases", source: "memory.entity_aliases" },
1212
+ { view: "notifications", source: "memory.notifications" },
1213
+ { view: "messages", source: "memory.messages" },
1214
+ { view: "users", source: "wiki.users" },
1215
+ { view: "workspaces", source: "wiki.workspaces" },
1216
+ { view: "workspace_users", source: "wiki.workspace_users" },
1217
+ { view: "documents", source: "wiki.workspace_documents" },
1218
+ { view: "chats", source: "wiki.workspace_chats" }
1219
+ ];
1220
+ UPSERT_KEYS = {
1221
+ memories: ["id"],
1222
+ tasks: ["id"],
1223
+ behaviors: ["id"],
1224
+ entities: ["id"],
1225
+ relationships: ["id"],
1226
+ entity_aliases: ["alias"],
1227
+ notifications: ["id"],
1228
+ messages: ["id"],
1229
+ users: ["id"],
1230
+ workspaces: ["id"],
1231
+ workspace_users: ["id"],
1232
+ documents: ["id"],
1233
+ chats: ["id"]
1234
+ };
1235
+ BOOLEAN_COLUMNS_BY_TABLE = {
1236
+ memories: /* @__PURE__ */ new Set(["has_error", "draft"]),
1237
+ behaviors: /* @__PURE__ */ new Set(["active"]),
1238
+ notifications: /* @__PURE__ */ new Set(["read"]),
1239
+ users: /* @__PURE__ */ new Set(["has_personal_memory"])
1240
+ };
1241
+ BOOLEAN_COLUMN_NAMES = new Set(
1242
+ Object.values(BOOLEAN_COLUMNS_BY_TABLE).flatMap((cols) => [...cols])
1243
+ );
1244
+ IMMEDIATE_FALLBACK_PATTERNS = [
1245
+ /\bPRAGMA\b/i,
1246
+ /\bsqlite_master\b/i,
1247
+ /(?:^|[.\s])(?:memories|conversations|entities)_fts\b/i,
1248
+ /\bMATCH\b/i,
1249
+ /\bvector_distance_cos\s*\(/i,
1250
+ /\bjson_extract\s*\(/i,
1251
+ /\bjulianday\s*\(/i,
1252
+ /\bstrftime\s*\(/i,
1253
+ /\blast_insert_rowid\s*\(/i
1254
+ ];
1255
+ prismaClientPromise = null;
1256
+ compatibilityBootstrapPromise = null;
470
1257
  }
471
1258
  });
472
1259
 
473
1260
  // src/lib/database.ts
474
1261
  import { createClient } from "@libsql/client";
475
1262
  async function initDatabase(config) {
1263
+ if (_walCheckpointTimer) {
1264
+ clearInterval(_walCheckpointTimer);
1265
+ _walCheckpointTimer = null;
1266
+ }
1267
+ if (_daemonClient) {
1268
+ _daemonClient.close();
1269
+ _daemonClient = null;
1270
+ }
1271
+ if (_adapterClient && _adapterClient !== _resilientClient) {
1272
+ _adapterClient.close();
1273
+ }
1274
+ _adapterClient = null;
476
1275
  if (_client) {
477
1276
  _client.close();
478
1277
  _client = null;
@@ -486,6 +1285,7 @@ async function initDatabase(config) {
486
1285
  }
487
1286
  _client = createClient(opts);
488
1287
  _resilientClient = wrapWithRetry(_client);
1288
+ _adapterClient = _resilientClient;
489
1289
  _client.execute("PRAGMA busy_timeout = 30000").catch(() => {
490
1290
  });
491
1291
  _client.execute("PRAGMA journal_mode = WAL").catch(() => {
@@ -496,11 +1296,17 @@ async function initDatabase(config) {
496
1296
  });
497
1297
  }, 3e4);
498
1298
  _walCheckpointTimer.unref();
1299
+ if (process.env.DATABASE_URL) {
1300
+ _adapterClient = await createPrismaDbAdapter(_resilientClient);
1301
+ }
499
1302
  }
500
1303
  function getClient() {
501
- if (!_resilientClient) {
1304
+ if (!_adapterClient) {
502
1305
  throw new Error("Database client not initialized. Call initDatabase() first.");
503
1306
  }
1307
+ if (process.env.DATABASE_URL) {
1308
+ return _adapterClient;
1309
+ }
504
1310
  if (process.env.EXE_IS_DAEMON === "1") {
505
1311
  return _resilientClient;
506
1312
  }
@@ -793,6 +1599,7 @@ async function ensureSchema() {
793
1599
  project TEXT NOT NULL,
794
1600
  summary TEXT NOT NULL,
795
1601
  task_file TEXT,
1602
+ session_scope TEXT,
796
1603
  read INTEGER NOT NULL DEFAULT 0,
797
1604
  created_at TEXT NOT NULL
798
1605
  );
@@ -801,7 +1608,7 @@ async function ensureSchema() {
801
1608
  ON notifications(read);
802
1609
 
803
1610
  CREATE INDEX IF NOT EXISTS idx_notifications_agent
804
- ON notifications(agent_id);
1611
+ ON notifications(agent_id, session_scope);
805
1612
 
806
1613
  CREATE INDEX IF NOT EXISTS idx_notifications_task_file
807
1614
  ON notifications(task_file);
@@ -839,6 +1646,7 @@ async function ensureSchema() {
839
1646
  target_agent TEXT NOT NULL,
840
1647
  target_project TEXT,
841
1648
  target_device TEXT NOT NULL DEFAULT 'local',
1649
+ session_scope TEXT,
842
1650
  content TEXT NOT NULL,
843
1651
  priority TEXT DEFAULT 'normal',
844
1652
  status TEXT DEFAULT 'pending',
@@ -852,10 +1660,31 @@ async function ensureSchema() {
852
1660
  );
853
1661
 
854
1662
  CREATE INDEX IF NOT EXISTS idx_messages_target
855
- ON messages(target_agent, status);
1663
+ ON messages(target_agent, session_scope, status);
856
1664
 
857
1665
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
858
- ON messages(target_agent, from_agent, server_seq);
1666
+ ON messages(target_agent, session_scope, from_agent, server_seq);
1667
+ `);
1668
+ try {
1669
+ await client.execute({
1670
+ sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
1671
+ args: []
1672
+ });
1673
+ } catch {
1674
+ }
1675
+ try {
1676
+ await client.execute({
1677
+ sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
1678
+ args: []
1679
+ });
1680
+ } catch {
1681
+ }
1682
+ await client.executeMultiple(`
1683
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
1684
+ ON notifications(agent_id, session_scope, read, created_at);
1685
+
1686
+ CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
1687
+ ON messages(target_agent, session_scope, status, created_at);
859
1688
  `);
860
1689
  try {
861
1690
  await client.execute({
@@ -1439,17 +2268,26 @@ async function ensureSchema() {
1439
2268
  } catch {
1440
2269
  }
1441
2270
  }
2271
+ try {
2272
+ await client.execute({
2273
+ sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
2274
+ args: []
2275
+ });
2276
+ } catch {
2277
+ }
1442
2278
  }
1443
- var _client, _resilientClient, _walCheckpointTimer, _daemonClient, initTurso;
2279
+ var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso;
1444
2280
  var init_database = __esm({
1445
2281
  "src/lib/database.ts"() {
1446
2282
  "use strict";
1447
2283
  init_db_retry();
1448
2284
  init_employees();
2285
+ init_database_adapter();
1449
2286
  _client = null;
1450
2287
  _resilientClient = null;
1451
2288
  _walCheckpointTimer = null;
1452
2289
  _daemonClient = null;
2290
+ _adapterClient = null;
1453
2291
  initTurso = initDatabase;
1454
2292
  }
1455
2293
  });
@@ -1514,6 +2352,7 @@ var shard_manager_exports = {};
1514
2352
  __export(shard_manager_exports, {
1515
2353
  disposeShards: () => disposeShards,
1516
2354
  ensureShardSchema: () => ensureShardSchema,
2355
+ getOpenShardCount: () => getOpenShardCount,
1517
2356
  getReadyShardClient: () => getReadyShardClient,
1518
2357
  getShardClient: () => getShardClient,
1519
2358
  getShardsDir: () => getShardsDir,
@@ -1522,15 +2361,18 @@ __export(shard_manager_exports, {
1522
2361
  listShards: () => listShards,
1523
2362
  shardExists: () => shardExists
1524
2363
  });
1525
- import path4 from "path";
1526
- import { existsSync as existsSync4, mkdirSync, readdirSync } from "fs";
2364
+ import path6 from "path";
2365
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync } from "fs";
1527
2366
  import { createClient as createClient2 } from "@libsql/client";
1528
2367
  function initShardManager(encryptionKey) {
1529
2368
  _encryptionKey = encryptionKey;
1530
- if (!existsSync4(SHARDS_DIR)) {
1531
- mkdirSync(SHARDS_DIR, { recursive: true });
2369
+ if (!existsSync6(SHARDS_DIR)) {
2370
+ mkdirSync2(SHARDS_DIR, { recursive: true });
1532
2371
  }
1533
2372
  _shardingEnabled = true;
2373
+ if (_evictionTimer) clearInterval(_evictionTimer);
2374
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
2375
+ _evictionTimer.unref();
1534
2376
  }
1535
2377
  function isShardingEnabled() {
1536
2378
  return _shardingEnabled;
@@ -1547,21 +2389,28 @@ function getShardClient(projectName) {
1547
2389
  throw new Error(`Invalid project name for shard: "${projectName}"`);
1548
2390
  }
1549
2391
  const cached = _shards.get(safeName);
1550
- if (cached) return cached;
1551
- const dbPath = path4.join(SHARDS_DIR, `${safeName}.db`);
2392
+ if (cached) {
2393
+ _shardLastAccess.set(safeName, Date.now());
2394
+ return cached;
2395
+ }
2396
+ while (_shards.size >= MAX_OPEN_SHARDS) {
2397
+ evictLRU();
2398
+ }
2399
+ const dbPath = path6.join(SHARDS_DIR, `${safeName}.db`);
1552
2400
  const client = createClient2({
1553
2401
  url: `file:${dbPath}`,
1554
2402
  encryptionKey: _encryptionKey
1555
2403
  });
1556
2404
  _shards.set(safeName, client);
2405
+ _shardLastAccess.set(safeName, Date.now());
1557
2406
  return client;
1558
2407
  }
1559
2408
  function shardExists(projectName) {
1560
2409
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
1561
- return existsSync4(path4.join(SHARDS_DIR, `${safeName}.db`));
2410
+ return existsSync6(path6.join(SHARDS_DIR, `${safeName}.db`));
1562
2411
  }
1563
2412
  function listShards() {
1564
- if (!existsSync4(SHARDS_DIR)) return [];
2413
+ if (!existsSync6(SHARDS_DIR)) return [];
1565
2414
  return readdirSync(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
1566
2415
  }
1567
2416
  async function ensureShardSchema(client) {
@@ -1613,6 +2462,8 @@ async function ensureShardSchema(client) {
1613
2462
  for (const col of [
1614
2463
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
1615
2464
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
2465
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
2466
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
1616
2467
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
1617
2468
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
1618
2469
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -1635,7 +2486,23 @@ async function ensureShardSchema(client) {
1635
2486
  // MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
1636
2487
  "ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
1637
2488
  "ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
1638
- "ALTER TABLE memories ADD COLUMN trajectory TEXT"
2489
+ "ALTER TABLE memories ADD COLUMN trajectory TEXT",
2490
+ // Metadata enrichment columns (must match database.ts)
2491
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
2492
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
2493
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
2494
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
2495
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
2496
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
2497
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
2498
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
2499
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
2500
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
2501
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
2502
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
2503
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
2504
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
2505
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1639
2506
  ]) {
1640
2507
  try {
1641
2508
  await client.execute(col);
@@ -1734,21 +2601,69 @@ async function getReadyShardClient(projectName) {
1734
2601
  await ensureShardSchema(client);
1735
2602
  return client;
1736
2603
  }
2604
+ function evictLRU() {
2605
+ let oldest = null;
2606
+ let oldestTime = Infinity;
2607
+ for (const [name, time] of _shardLastAccess) {
2608
+ if (time < oldestTime) {
2609
+ oldestTime = time;
2610
+ oldest = name;
2611
+ }
2612
+ }
2613
+ if (oldest) {
2614
+ const client = _shards.get(oldest);
2615
+ if (client) {
2616
+ client.close();
2617
+ }
2618
+ _shards.delete(oldest);
2619
+ _shardLastAccess.delete(oldest);
2620
+ }
2621
+ }
2622
+ function evictIdleShards() {
2623
+ const now = Date.now();
2624
+ const toEvict = [];
2625
+ for (const [name, lastAccess] of _shardLastAccess) {
2626
+ if (now - lastAccess > SHARD_IDLE_MS) {
2627
+ toEvict.push(name);
2628
+ }
2629
+ }
2630
+ for (const name of toEvict) {
2631
+ const client = _shards.get(name);
2632
+ if (client) {
2633
+ client.close();
2634
+ }
2635
+ _shards.delete(name);
2636
+ _shardLastAccess.delete(name);
2637
+ }
2638
+ }
2639
+ function getOpenShardCount() {
2640
+ return _shards.size;
2641
+ }
1737
2642
  function disposeShards() {
2643
+ if (_evictionTimer) {
2644
+ clearInterval(_evictionTimer);
2645
+ _evictionTimer = null;
2646
+ }
1738
2647
  for (const [, client] of _shards) {
1739
2648
  client.close();
1740
2649
  }
1741
2650
  _shards.clear();
2651
+ _shardLastAccess.clear();
1742
2652
  _shardingEnabled = false;
1743
2653
  _encryptionKey = null;
1744
2654
  }
1745
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
2655
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
1746
2656
  var init_shard_manager = __esm({
1747
2657
  "src/lib/shard-manager.ts"() {
1748
2658
  "use strict";
1749
2659
  init_config();
1750
- SHARDS_DIR = path4.join(EXE_AI_DIR, "shards");
2660
+ SHARDS_DIR = path6.join(EXE_AI_DIR, "shards");
2661
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
2662
+ MAX_OPEN_SHARDS = 10;
2663
+ EVICTION_INTERVAL_MS = 60 * 1e3;
1751
2664
  _shards = /* @__PURE__ */ new Map();
2665
+ _shardLastAccess = /* @__PURE__ */ new Map();
2666
+ _evictionTimer = null;
1752
2667
  _encryptionKey = null;
1753
2668
  _shardingEnabled = false;
1754
2669
  }
@@ -1941,66 +2856,14 @@ ${p.content}`).join("\n\n");
1941
2856
  }
1942
2857
  });
1943
2858
 
1944
- // src/lib/notifications.ts
1945
- import crypto from "crypto";
1946
- import path5 from "path";
1947
- import os4 from "os";
1948
- import {
1949
- readFileSync as readFileSync3,
1950
- readdirSync as readdirSync2,
1951
- unlinkSync as unlinkSync2,
1952
- existsSync as existsSync5,
1953
- rmdirSync
1954
- } from "fs";
1955
- async function writeNotification(notification) {
1956
- try {
1957
- const client = getClient();
1958
- const id = crypto.randomUUID();
1959
- const now = (/* @__PURE__ */ new Date()).toISOString();
1960
- await client.execute({
1961
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
1962
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
1963
- args: [
1964
- id,
1965
- notification.agentId,
1966
- notification.agentRole,
1967
- notification.event,
1968
- notification.project,
1969
- notification.summary,
1970
- notification.taskFile ?? null,
1971
- now
1972
- ]
1973
- });
1974
- } catch (err) {
1975
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
1976
- `);
1977
- }
1978
- }
1979
- async function markAsReadByTaskFile(taskFile) {
1980
- try {
1981
- const client = getClient();
1982
- await client.execute({
1983
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
1984
- args: [taskFile]
1985
- });
1986
- } catch {
1987
- }
1988
- }
1989
- var init_notifications = __esm({
1990
- "src/lib/notifications.ts"() {
1991
- "use strict";
1992
- init_database();
1993
- }
1994
- });
1995
-
1996
2859
  // src/lib/session-registry.ts
1997
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync6 } from "fs";
1998
- import path6 from "path";
2860
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "fs";
2861
+ import path7 from "path";
1999
2862
  import os5 from "os";
2000
2863
  function registerSession(entry) {
2001
- const dir = path6.dirname(REGISTRY_PATH);
2002
- if (!existsSync6(dir)) {
2003
- mkdirSync2(dir, { recursive: true });
2864
+ const dir = path7.dirname(REGISTRY_PATH);
2865
+ if (!existsSync7(dir)) {
2866
+ mkdirSync3(dir, { recursive: true });
2004
2867
  }
2005
2868
  const sessions = listSessions();
2006
2869
  const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
@@ -2009,7 +2872,7 @@ function registerSession(entry) {
2009
2872
  } else {
2010
2873
  sessions.push(entry);
2011
2874
  }
2012
- writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
2875
+ writeFileSync3(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
2013
2876
  }
2014
2877
  function listSessions() {
2015
2878
  try {
@@ -2023,7 +2886,7 @@ var REGISTRY_PATH;
2023
2886
  var init_session_registry = __esm({
2024
2887
  "src/lib/session-registry.ts"() {
2025
2888
  "use strict";
2026
- REGISTRY_PATH = path6.join(os5.homedir(), ".exe-os", "session-registry.json");
2889
+ REGISTRY_PATH = path7.join(os5.homedir(), ".exe-os", "session-registry.json");
2027
2890
  }
2028
2891
  });
2029
2892
 
@@ -2275,67 +3138,6 @@ var init_provider_table = __esm({
2275
3138
  }
2276
3139
  });
2277
3140
 
2278
- // src/lib/runtime-table.ts
2279
- var RUNTIME_TABLE, DEFAULT_RUNTIME;
2280
- var init_runtime_table = __esm({
2281
- "src/lib/runtime-table.ts"() {
2282
- "use strict";
2283
- RUNTIME_TABLE = {
2284
- codex: {
2285
- binary: "codex",
2286
- launchMode: "interactive",
2287
- autoApproveFlag: "--dangerously-bypass-approvals-and-sandbox",
2288
- inlineFlag: "--no-alt-screen",
2289
- apiKeyEnv: "OPENAI_API_KEY",
2290
- defaultModel: "gpt-5.4"
2291
- },
2292
- opencode: {
2293
- binary: "opencode",
2294
- launchMode: "exec",
2295
- autoApproveFlag: "--dangerously-skip-permissions",
2296
- inlineFlag: "",
2297
- apiKeyEnv: "ANTHROPIC_API_KEY",
2298
- defaultModel: "anthropic/claude-sonnet-4-6"
2299
- }
2300
- };
2301
- DEFAULT_RUNTIME = "claude";
2302
- }
2303
- });
2304
-
2305
- // src/lib/agent-config.ts
2306
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
2307
- import path7 from "path";
2308
- function loadAgentConfig() {
2309
- if (!existsSync7(AGENT_CONFIG_PATH)) return {};
2310
- try {
2311
- return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
2312
- } catch {
2313
- return {};
2314
- }
2315
- }
2316
- function getAgentRuntime(agentId) {
2317
- const config = loadAgentConfig();
2318
- const entry = config[agentId];
2319
- if (entry) return entry;
2320
- const orgDefault = config["default"];
2321
- if (orgDefault) return orgDefault;
2322
- return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
2323
- }
2324
- var AGENT_CONFIG_PATH, DEFAULT_MODELS;
2325
- var init_agent_config = __esm({
2326
- "src/lib/agent-config.ts"() {
2327
- "use strict";
2328
- init_config();
2329
- init_runtime_table();
2330
- AGENT_CONFIG_PATH = path7.join(EXE_AI_DIR, "agent-config.json");
2331
- DEFAULT_MODELS = {
2332
- claude: "claude-opus-4",
2333
- codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
2334
- opencode: RUNTIME_TABLE.opencode?.defaultModel ?? "anthropic/claude-sonnet-4-6"
2335
- };
2336
- }
2337
- });
2338
-
2339
3141
  // src/lib/intercom-queue.ts
2340
3142
  var intercom_queue_exports = {};
2341
3143
  __export(intercom_queue_exports, {
@@ -2345,7 +3147,7 @@ __export(intercom_queue_exports, {
2345
3147
  queueIntercom: () => queueIntercom,
2346
3148
  readQueue: () => readQueue
2347
3149
  });
2348
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
3150
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
2349
3151
  import path8 from "path";
2350
3152
  import os6 from "os";
2351
3153
  function ensureDir() {
@@ -2355,7 +3157,7 @@ function ensureDir() {
2355
3157
  function readQueue() {
2356
3158
  try {
2357
3159
  if (!existsSync8(QUEUE_PATH)) return [];
2358
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
3160
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
2359
3161
  } catch {
2360
3162
  return [];
2361
3163
  }
@@ -2463,8 +3265,11 @@ var init_intercom_queue = __esm({
2463
3265
  });
2464
3266
 
2465
3267
  // src/lib/license.ts
2466
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
3268
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
2467
3269
  import { randomUUID as randomUUID2 } from "crypto";
3270
+ import { createRequire as createRequire2 } from "module";
3271
+ import { pathToFileURL as pathToFileURL2 } from "url";
3272
+ import os7 from "os";
2468
3273
  import path9 from "path";
2469
3274
  import { jwtVerify, importSPKI } from "jose";
2470
3275
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
@@ -2486,12 +3291,12 @@ var init_license = __esm({
2486
3291
  });
2487
3292
 
2488
3293
  // src/lib/plan-limits.ts
2489
- import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
3294
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
2490
3295
  import path10 from "path";
2491
3296
  function getLicenseSync() {
2492
3297
  try {
2493
3298
  if (!existsSync10(CACHE_PATH2)) return freeLicense();
2494
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
3299
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
2495
3300
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
2496
3301
  const parts = raw.token.split(".");
2497
3302
  if (parts.length !== 3) return freeLicense();
@@ -2530,7 +3335,7 @@ function assertEmployeeLimitSync(rosterPath) {
2530
3335
  let count = 0;
2531
3336
  try {
2532
3337
  if (existsSync10(filePath)) {
2533
- const raw = readFileSync8(filePath, "utf8");
3338
+ const raw = readFileSync7(filePath, "utf8");
2534
3339
  const employees = JSON.parse(raw);
2535
3340
  count = Array.isArray(employees) ? employees.length : 0;
2536
3341
  }
@@ -2564,7 +3369,7 @@ var init_plan_limits = __esm({
2564
3369
  });
2565
3370
 
2566
3371
  // src/lib/session-kill-telemetry.ts
2567
- import crypto2 from "crypto";
3372
+ import crypto from "crypto";
2568
3373
  async function recordSessionKill(input) {
2569
3374
  try {
2570
3375
  const client = getClient();
@@ -2574,7 +3379,7 @@ async function recordSessionKill(input) {
2574
3379
  ticks_idle, estimated_tokens_saved)
2575
3380
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
2576
3381
  args: [
2577
- crypto2.randomUUID(),
3382
+ crypto.randomUUID(),
2578
3383
  input.sessionName,
2579
3384
  input.agentId,
2580
3385
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -2897,6 +3702,7 @@ __export(tmux_routing_exports, {
2897
3702
  isEmployeeAlive: () => isEmployeeAlive,
2898
3703
  isExeSession: () => isExeSession,
2899
3704
  isSessionBusy: () => isSessionBusy,
3705
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
2900
3706
  notifyParentExe: () => notifyParentExe,
2901
3707
  parseParentExe: () => parseParentExe,
2902
3708
  registerParentExe: () => registerParentExe,
@@ -2907,11 +3713,11 @@ __export(tmux_routing_exports, {
2907
3713
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
2908
3714
  });
2909
3715
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
2910
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync, readdirSync as readdirSync3 } from "fs";
3716
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync, readdirSync as readdirSync2 } from "fs";
2911
3717
  import path11 from "path";
2912
- import os7 from "os";
3718
+ import os8 from "os";
2913
3719
  import { fileURLToPath } from "url";
2914
- import { unlinkSync as unlinkSync3 } from "fs";
3720
+ import { unlinkSync as unlinkSync2 } from "fs";
2915
3721
  function spawnLockPath(sessionName) {
2916
3722
  return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2917
3723
  }
@@ -2930,7 +3736,7 @@ function acquireSpawnLock(sessionName) {
2930
3736
  const lockFile = spawnLockPath(sessionName);
2931
3737
  if (existsSync11(lockFile)) {
2932
3738
  try {
2933
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
3739
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
2934
3740
  const age = Date.now() - lock.timestamp;
2935
3741
  if (isProcessAlive(lock.pid) && age < 6e4) {
2936
3742
  return false;
@@ -2943,7 +3749,7 @@ function acquireSpawnLock(sessionName) {
2943
3749
  }
2944
3750
  function releaseSpawnLock(sessionName) {
2945
3751
  try {
2946
- unlinkSync3(spawnLockPath(sessionName));
3752
+ unlinkSync2(spawnLockPath(sessionName));
2947
3753
  } catch {
2948
3754
  }
2949
3755
  }
@@ -3035,7 +3841,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3035
3841
  }
3036
3842
  function getParentExe(sessionKey) {
3037
3843
  try {
3038
- const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3844
+ const data = JSON.parse(readFileSync8(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3039
3845
  return data.parentExe || null;
3040
3846
  } catch {
3041
3847
  return null;
@@ -3043,7 +3849,7 @@ function getParentExe(sessionKey) {
3043
3849
  }
3044
3850
  function getDispatchedBy(sessionKey) {
3045
3851
  try {
3046
- const data = JSON.parse(readFileSync9(
3852
+ const data = JSON.parse(readFileSync8(
3047
3853
  path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3048
3854
  "utf8"
3049
3855
  ));
@@ -3115,7 +3921,7 @@ async function verifyPaneAtCapacity(sessionName) {
3115
3921
  function readDebounceState() {
3116
3922
  try {
3117
3923
  if (!existsSync11(DEBOUNCE_FILE)) return {};
3118
- const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
3924
+ const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
3119
3925
  const state = {};
3120
3926
  for (const [key, val] of Object.entries(raw)) {
3121
3927
  if (typeof val === "number") {
@@ -3242,7 +4048,7 @@ function sendIntercom(targetSession) {
3242
4048
  const agent = baseAgentName(rawAgent);
3243
4049
  const taskDir = path11.join(process.cwd(), "exe", agent);
3244
4050
  if (existsSync11(taskDir)) {
3245
- const files = readdirSync3(taskDir).filter(
4051
+ const files = readdirSync2(taskDir).filter(
3246
4052
  (f) => f.endsWith(".md") && f !== "DONE.txt"
3247
4053
  );
3248
4054
  if (files.length === 0) {
@@ -3301,6 +4107,21 @@ function notifyParentExe(sessionKey) {
3301
4107
  }
3302
4108
  return true;
3303
4109
  }
4110
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
4111
+ const transport = getTransport();
4112
+ try {
4113
+ const sessions = transport.listSessions();
4114
+ if (!sessions.includes(coordinatorSession)) return false;
4115
+ execSync4(
4116
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
4117
+ { timeout: 3e3 }
4118
+ );
4119
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
4120
+ return true;
4121
+ } catch {
4122
+ return false;
4123
+ }
4124
+ }
3304
4125
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3305
4126
  if (isCoordinatorName(employeeName)) {
3306
4127
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -3374,7 +4195,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3374
4195
  const transport = getTransport();
3375
4196
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3376
4197
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3377
- const logDir = path11.join(os7.homedir(), ".exe-os", "session-logs");
4198
+ const logDir = path11.join(os8.homedir(), ".exe-os", "session-logs");
3378
4199
  const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3379
4200
  if (!existsSync11(logDir)) {
3380
4201
  mkdirSync6(logDir, { recursive: true });
@@ -3390,10 +4211,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3390
4211
  } catch {
3391
4212
  }
3392
4213
  try {
3393
- const claudeJsonPath = path11.join(os7.homedir(), ".claude.json");
4214
+ const claudeJsonPath = path11.join(os8.homedir(), ".claude.json");
3394
4215
  let claudeJson = {};
3395
4216
  try {
3396
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
4217
+ claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
3397
4218
  } catch {
3398
4219
  }
3399
4220
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -3405,13 +4226,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3405
4226
  } catch {
3406
4227
  }
3407
4228
  try {
3408
- const settingsDir = path11.join(os7.homedir(), ".claude", "projects");
4229
+ const settingsDir = path11.join(os8.homedir(), ".claude", "projects");
3409
4230
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3410
4231
  const projSettingsDir = path11.join(settingsDir, normalizedKey);
3411
4232
  const settingsPath = path11.join(projSettingsDir, "settings.json");
3412
4233
  let settings = {};
3413
4234
  try {
3414
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
4235
+ settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
3415
4236
  } catch {
3416
4237
  }
3417
4238
  const perms = settings.permissions ?? {};
@@ -3456,7 +4277,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3456
4277
  let legacyFallbackWarned = false;
3457
4278
  if (!useExeAgent && !useBinSymlink) {
3458
4279
  const identityPath = path11.join(
3459
- os7.homedir(),
4280
+ os8.homedir(),
3460
4281
  ".exe-os",
3461
4282
  "identity",
3462
4283
  `${employeeName}.md`
@@ -3486,7 +4307,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3486
4307
  }
3487
4308
  let sessionContextFlag = "";
3488
4309
  try {
3489
- const ctxDir = path11.join(os7.homedir(), ".exe-os", "session-cache");
4310
+ const ctxDir = path11.join(os8.homedir(), ".exe-os", "session-cache");
3490
4311
  mkdirSync6(ctxDir, { recursive: true });
3491
4312
  const ctxFile = path11.join(ctxDir, `session-context-${sessionName}.md`);
3492
4313
  const ctxContent = [
@@ -3647,14 +4468,14 @@ var init_tmux_routing = __esm({
3647
4468
  init_intercom_queue();
3648
4469
  init_plan_limits();
3649
4470
  init_employees();
3650
- SPAWN_LOCK_DIR = path11.join(os7.homedir(), ".exe-os", "spawn-locks");
3651
- SESSION_CACHE = path11.join(os7.homedir(), ".exe-os", "session-cache");
4471
+ SPAWN_LOCK_DIR = path11.join(os8.homedir(), ".exe-os", "spawn-locks");
4472
+ SESSION_CACHE = path11.join(os8.homedir(), ".exe-os", "session-cache");
3652
4473
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3653
4474
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
3654
4475
  VERIFY_PANE_LINES = 200;
3655
4476
  INTERCOM_DEBOUNCE_MS = 3e4;
3656
4477
  CODEX_DEBOUNCE_MS = 12e4;
3657
- INTERCOM_LOG2 = path11.join(os7.homedir(), ".exe-os", "intercom.log");
4478
+ INTERCOM_LOG2 = path11.join(os8.homedir(), ".exe-os", "intercom.log");
3658
4479
  DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
3659
4480
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3660
4481
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
@@ -3678,6 +4499,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
3678
4499
  args: [scope]
3679
4500
  };
3680
4501
  }
4502
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
4503
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
4504
+ if (!scope) return { sql: "", args: [] };
4505
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
4506
+ return {
4507
+ sql: ` AND ${col} = ?`,
4508
+ args: [scope]
4509
+ };
4510
+ }
3681
4511
  var init_task_scope = __esm({
3682
4512
  "src/lib/task-scope.ts"() {
3683
4513
  "use strict";
@@ -3685,13 +4515,70 @@ var init_task_scope = __esm({
3685
4515
  }
3686
4516
  });
3687
4517
 
4518
+ // src/lib/notifications.ts
4519
+ import crypto2 from "crypto";
4520
+ import path12 from "path";
4521
+ import os9 from "os";
4522
+ import {
4523
+ readFileSync as readFileSync9,
4524
+ readdirSync as readdirSync3,
4525
+ unlinkSync as unlinkSync3,
4526
+ existsSync as existsSync12,
4527
+ rmdirSync
4528
+ } from "fs";
4529
+ async function writeNotification(notification) {
4530
+ try {
4531
+ const client = getClient();
4532
+ const id = crypto2.randomUUID();
4533
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4534
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
4535
+ await client.execute({
4536
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
4537
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
4538
+ args: [
4539
+ id,
4540
+ notification.agentId,
4541
+ notification.agentRole,
4542
+ notification.event,
4543
+ notification.project,
4544
+ notification.summary,
4545
+ notification.taskFile ?? null,
4546
+ sessionScope,
4547
+ now
4548
+ ]
4549
+ });
4550
+ } catch (err) {
4551
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
4552
+ `);
4553
+ }
4554
+ }
4555
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
4556
+ try {
4557
+ const client = getClient();
4558
+ const scope = strictSessionScopeFilter(sessionScope);
4559
+ await client.execute({
4560
+ sql: `UPDATE notifications SET read = 1
4561
+ WHERE task_file = ? AND read = 0${scope.sql}`,
4562
+ args: [taskFile, ...scope.args]
4563
+ });
4564
+ } catch {
4565
+ }
4566
+ }
4567
+ var init_notifications = __esm({
4568
+ "src/lib/notifications.ts"() {
4569
+ "use strict";
4570
+ init_database();
4571
+ init_task_scope();
4572
+ }
4573
+ });
4574
+
3688
4575
  // src/lib/tasks-crud.ts
3689
4576
  import crypto3 from "crypto";
3690
- import path12 from "path";
3691
- import os8 from "os";
4577
+ import path13 from "path";
4578
+ import os10 from "os";
3692
4579
  import { execSync as execSync5 } from "child_process";
3693
4580
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3694
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
4581
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
3695
4582
  async function writeCheckpoint(input) {
3696
4583
  const client = getClient();
3697
4584
  const row = await resolveTask(client, input.taskId);
@@ -3866,8 +4753,8 @@ ${laneWarning}` : laneWarning;
3866
4753
  }
3867
4754
  if (input.baseDir) {
3868
4755
  try {
3869
- await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
3870
- await mkdir4(path12.join(input.baseDir, "exe", "research"), { recursive: true });
4756
+ await mkdir4(path13.join(input.baseDir, "exe", "output"), { recursive: true });
4757
+ await mkdir4(path13.join(input.baseDir, "exe", "research"), { recursive: true });
3871
4758
  await ensureArchitectureDoc(input.baseDir, input.projectName);
3872
4759
  await ensureGitignoreExe(input.baseDir);
3873
4760
  } catch {
@@ -3903,13 +4790,19 @@ ${laneWarning}` : laneWarning;
3903
4790
  });
3904
4791
  if (input.baseDir) {
3905
4792
  try {
3906
- const EXE_OS_DIR = path12.join(os8.homedir(), ".exe-os");
3907
- const mdPath = path12.join(EXE_OS_DIR, taskFile);
3908
- const mdDir = path12.dirname(mdPath);
3909
- if (!existsSync12(mdDir)) await mkdir4(mdDir, { recursive: true });
4793
+ const EXE_OS_DIR = path13.join(os10.homedir(), ".exe-os");
4794
+ const mdPath = path13.join(EXE_OS_DIR, taskFile);
4795
+ const mdDir = path13.dirname(mdPath);
4796
+ if (!existsSync13(mdDir)) await mkdir4(mdDir, { recursive: true });
3910
4797
  const reviewer = input.reviewer ?? input.assignedBy;
3911
4798
  const mdContent = `# ${input.title}
3912
4799
 
4800
+ ## MANDATORY: When done
4801
+
4802
+ You MUST call update_task with status "done" and a result summary when finished.
4803
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
4804
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
4805
+
3913
4806
  **ID:** ${id}
3914
4807
  **Status:** ${initialStatus}
3915
4808
  **Priority:** ${input.priority}
@@ -3923,12 +4816,6 @@ ${laneWarning}` : laneWarning;
3923
4816
  ## Context
3924
4817
 
3925
4818
  ${input.context}
3926
-
3927
- ## MANDATORY: When done
3928
-
3929
- You MUST call update_task with status "done" and a result summary when finished.
3930
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3931
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
3932
4819
  `;
3933
4820
  await writeFile4(mdPath, mdContent, "utf-8");
3934
4821
  } catch (err) {
@@ -4177,7 +5064,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
4177
5064
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
4178
5065
  } catch {
4179
5066
  }
4180
- if (input.status === "done" || input.status === "cancelled") {
5067
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
4181
5068
  try {
4182
5069
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
4183
5070
  clearQueueForAgent2(String(row.assigned_to));
@@ -4206,9 +5093,9 @@ async function deleteTaskCore(taskId, _baseDir) {
4206
5093
  return { taskFile, assignedTo, assignedBy, taskSlug };
4207
5094
  }
4208
5095
  async function ensureArchitectureDoc(baseDir, projectName) {
4209
- const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
5096
+ const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
4210
5097
  try {
4211
- if (existsSync12(archPath)) return;
5098
+ if (existsSync13(archPath)) return;
4212
5099
  const template = [
4213
5100
  `# ${projectName} \u2014 System Architecture`,
4214
5101
  "",
@@ -4241,9 +5128,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
4241
5128
  }
4242
5129
  }
4243
5130
  async function ensureGitignoreExe(baseDir) {
4244
- const gitignorePath = path12.join(baseDir, ".gitignore");
5131
+ const gitignorePath = path13.join(baseDir, ".gitignore");
4245
5132
  try {
4246
- if (existsSync12(gitignorePath)) {
5133
+ if (existsSync13(gitignorePath)) {
4247
5134
  const content = readFileSync10(gitignorePath, "utf-8");
4248
5135
  if (/^\/?exe\/?$/m.test(content)) return;
4249
5136
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -4275,58 +5162,42 @@ var init_tasks_crud = __esm({
4275
5162
  });
4276
5163
 
4277
5164
  // src/lib/tasks-review.ts
4278
- import path13 from "path";
4279
- import { existsSync as existsSync13, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
5165
+ import path14 from "path";
5166
+ import { existsSync as existsSync14, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
4280
5167
  async function countPendingReviews(sessionScope) {
4281
5168
  const client = getClient();
4282
- if (sessionScope) {
4283
- const result2 = await client.execute({
4284
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND (session_scope = ? OR session_scope IS NULL)",
4285
- args: [sessionScope]
4286
- });
4287
- return Number(result2.rows[0]?.cnt) || 0;
4288
- }
5169
+ const scope = strictSessionScopeFilter(
5170
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5171
+ );
4289
5172
  const result = await client.execute({
4290
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
4291
- args: []
5173
+ sql: `SELECT COUNT(*) as cnt FROM tasks
5174
+ WHERE status = 'needs_review'${scope.sql}`,
5175
+ args: [...scope.args]
4292
5176
  });
4293
5177
  return Number(result.rows[0]?.cnt) || 0;
4294
5178
  }
4295
5179
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
4296
5180
  const client = getClient();
4297
- if (sessionScope) {
4298
- const result2 = await client.execute({
4299
- sql: `SELECT COUNT(*) as cnt FROM tasks
4300
- WHERE status = 'needs_review' AND updated_at > ?
4301
- AND session_scope = ?`,
4302
- args: [sinceIso, sessionScope]
4303
- });
4304
- return Number(result2.rows[0]?.cnt) || 0;
4305
- }
5181
+ const scope = strictSessionScopeFilter(
5182
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5183
+ );
4306
5184
  const result = await client.execute({
4307
5185
  sql: `SELECT COUNT(*) as cnt FROM tasks
4308
- WHERE status = 'needs_review' AND updated_at > ?`,
4309
- args: [sinceIso]
5186
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
5187
+ args: [sinceIso, ...scope.args]
4310
5188
  });
4311
5189
  return Number(result.rows[0]?.cnt) || 0;
4312
5190
  }
4313
5191
  async function listPendingReviews(limit, sessionScope) {
4314
5192
  const client = getClient();
4315
- if (sessionScope) {
4316
- const result2 = await client.execute({
4317
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
4318
- WHERE status = 'needs_review'
4319
- AND session_scope = ?
4320
- ORDER BY updated_at ASC LIMIT ?`,
4321
- args: [sessionScope, limit]
4322
- });
4323
- return result2.rows;
4324
- }
5193
+ const scope = strictSessionScopeFilter(
5194
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5195
+ );
4325
5196
  const result = await client.execute({
4326
5197
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
4327
- WHERE status = 'needs_review'
5198
+ WHERE status = 'needs_review'${scope.sql}
4328
5199
  ORDER BY updated_at ASC LIMIT ?`,
4329
- args: [limit]
5200
+ args: [...scope.args, limit]
4330
5201
  });
4331
5202
  return result.rows;
4332
5203
  }
@@ -4338,7 +5209,7 @@ async function cleanupOrphanedReviews() {
4338
5209
  WHERE status IN ('open', 'needs_review', 'in_progress')
4339
5210
  AND assigned_by = 'system'
4340
5211
  AND title LIKE 'Review:%'
4341
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
5212
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
4342
5213
  args: [now]
4343
5214
  });
4344
5215
  const r1b = await client.execute({
@@ -4457,11 +5328,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4457
5328
  );
4458
5329
  }
4459
5330
  try {
4460
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
4461
- if (existsSync13(cacheDir)) {
5331
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
5332
+ if (existsSync14(cacheDir)) {
4462
5333
  for (const f of readdirSync4(cacheDir)) {
4463
5334
  if (f.startsWith("review-notified-")) {
4464
- unlinkSync4(path13.join(cacheDir, f));
5335
+ unlinkSync4(path14.join(cacheDir, f));
4465
5336
  }
4466
5337
  }
4467
5338
  }
@@ -4478,11 +5349,12 @@ var init_tasks_review = __esm({
4478
5349
  init_tmux_routing();
4479
5350
  init_session_key();
4480
5351
  init_state_bus();
5352
+ init_task_scope();
4481
5353
  }
4482
5354
  });
4483
5355
 
4484
5356
  // src/lib/tasks-chain.ts
4485
- import path14 from "path";
5357
+ import path15 from "path";
4486
5358
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
4487
5359
  async function cascadeUnblock(taskId, baseDir, now) {
4488
5360
  const client = getClient();
@@ -4499,7 +5371,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
4499
5371
  });
4500
5372
  for (const ur of unblockedRows.rows) {
4501
5373
  try {
4502
- const ubFile = path14.join(baseDir, String(ur.task_file));
5374
+ const ubFile = path15.join(baseDir, String(ur.task_file));
4503
5375
  let ubContent = await readFile4(ubFile, "utf-8");
4504
5376
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
4505
5377
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -4534,7 +5406,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
4534
5406
  const scScope = sessionScopeFilter();
4535
5407
  const remaining = await client.execute({
4536
5408
  sql: `SELECT COUNT(*) as cnt FROM tasks
4537
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
5409
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
4538
5410
  args: [parentTaskId, ...scScope.args]
4539
5411
  });
4540
5412
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -4568,7 +5440,7 @@ var init_tasks_chain = __esm({
4568
5440
 
4569
5441
  // src/lib/project-name.ts
4570
5442
  import { execSync as execSync6 } from "child_process";
4571
- import path15 from "path";
5443
+ import path16 from "path";
4572
5444
  function getProjectName(cwd) {
4573
5445
  const dir = cwd ?? process.cwd();
4574
5446
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -4581,7 +5453,7 @@ function getProjectName(cwd) {
4581
5453
  timeout: 2e3,
4582
5454
  stdio: ["pipe", "pipe", "pipe"]
4583
5455
  }).trim();
4584
- repoRoot = path15.dirname(gitCommonDir);
5456
+ repoRoot = path16.dirname(gitCommonDir);
4585
5457
  } catch {
4586
5458
  repoRoot = execSync6("git rev-parse --show-toplevel", {
4587
5459
  cwd: dir,
@@ -4590,11 +5462,11 @@ function getProjectName(cwd) {
4590
5462
  stdio: ["pipe", "pipe", "pipe"]
4591
5463
  }).trim();
4592
5464
  }
4593
- _cached2 = path15.basename(repoRoot);
5465
+ _cached2 = path16.basename(repoRoot);
4594
5466
  _cachedCwd = dir;
4595
5467
  return _cached2;
4596
5468
  } catch {
4597
- _cached2 = path15.basename(dir);
5469
+ _cached2 = path16.basename(dir);
4598
5470
  _cachedCwd = dir;
4599
5471
  return _cached2;
4600
5472
  }
@@ -5067,7 +5939,7 @@ __export(tasks_exports, {
5067
5939
  updateTaskStatus: () => updateTaskStatus,
5068
5940
  writeCheckpoint: () => writeCheckpoint
5069
5941
  });
5070
- import path16 from "path";
5942
+ import path17 from "path";
5071
5943
  import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
5072
5944
  async function createTask(input) {
5073
5945
  const result = await createTaskCore(input);
@@ -5087,12 +5959,12 @@ async function updateTask(input) {
5087
5959
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
5088
5960
  try {
5089
5961
  const agent = String(row.assigned_to);
5090
- const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
5091
- const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
5962
+ const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
5963
+ const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
5092
5964
  if (input.status === "in_progress") {
5093
5965
  mkdirSync7(cacheDir, { recursive: true });
5094
5966
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5095
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
5967
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
5096
5968
  try {
5097
5969
  unlinkSync5(cachePath);
5098
5970
  } catch {
@@ -5100,10 +5972,10 @@ async function updateTask(input) {
5100
5972
  }
5101
5973
  } catch {
5102
5974
  }
5103
- if (input.status === "done") {
5975
+ if (input.status === "done" || input.status === "closed") {
5104
5976
  await cleanupReviewFile(row, taskFile, input.baseDir);
5105
5977
  }
5106
- if (input.status === "done" || input.status === "cancelled") {
5978
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
5107
5979
  try {
5108
5980
  const client = getClient();
5109
5981
  const taskTitle = String(row.title);
@@ -5119,7 +5991,7 @@ async function updateTask(input) {
5119
5991
  if (!isCoordinatorName(assignedAgent)) {
5120
5992
  try {
5121
5993
  const draftClient = getClient();
5122
- if (input.status === "done") {
5994
+ if (input.status === "done" || input.status === "closed") {
5123
5995
  await draftClient.execute({
5124
5996
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
5125
5997
  args: [assignedAgent]
@@ -5136,7 +6008,7 @@ async function updateTask(input) {
5136
6008
  try {
5137
6009
  const client = getClient();
5138
6010
  const cascaded = await client.execute({
5139
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
6011
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
5140
6012
  WHERE parent_task_id = ? AND status = 'needs_review'`,
5141
6013
  args: [now, taskId]
5142
6014
  });
@@ -5149,14 +6021,14 @@ async function updateTask(input) {
5149
6021
  } catch {
5150
6022
  }
5151
6023
  }
5152
- const isTerminal = input.status === "done" || input.status === "needs_review";
6024
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
5153
6025
  if (isTerminal) {
5154
6026
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
5155
6027
  if (!isCoordinator) {
5156
6028
  notifyTaskDone();
5157
6029
  }
5158
6030
  await markTaskNotificationsRead(taskFile);
5159
- if (input.status === "done") {
6031
+ if (input.status === "done" || input.status === "closed") {
5160
6032
  try {
5161
6033
  await cascadeUnblock(taskId, input.baseDir, now);
5162
6034
  } catch {
@@ -5176,7 +6048,7 @@ async function updateTask(input) {
5176
6048
  }
5177
6049
  }
5178
6050
  }
5179
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
6051
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
5180
6052
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
5181
6053
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
5182
6054
  taskId,
@@ -5254,16 +6126,16 @@ init_database();
5254
6126
 
5255
6127
  // src/lib/keychain.ts
5256
6128
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
5257
- import { existsSync as existsSync3 } from "fs";
5258
- import path3 from "path";
5259
- import os3 from "os";
6129
+ import { existsSync as existsSync5 } from "fs";
6130
+ import path5 from "path";
6131
+ import os4 from "os";
5260
6132
  var SERVICE = "exe-mem";
5261
6133
  var ACCOUNT = "master-key";
5262
6134
  function getKeyDir() {
5263
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path3.join(os3.homedir(), ".exe-os");
6135
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path5.join(os4.homedir(), ".exe-os");
5264
6136
  }
5265
6137
  function getKeyPath() {
5266
- return path3.join(getKeyDir(), "master.key");
6138
+ return path5.join(getKeyDir(), "master.key");
5267
6139
  }
5268
6140
  async function tryKeytar() {
5269
6141
  try {
@@ -5284,9 +6156,9 @@ async function getMasterKey() {
5284
6156
  }
5285
6157
  }
5286
6158
  const keyPath = getKeyPath();
5287
- if (!existsSync3(keyPath)) {
6159
+ if (!existsSync5(keyPath)) {
5288
6160
  process.stderr.write(
5289
- `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6161
+ `[keychain] Key not found at ${keyPath} (HOME=${os4.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5290
6162
  `
5291
6163
  );
5292
6164
  return null;