@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
package/dist/lib/tasks.js CHANGED
@@ -32,9 +32,34 @@ var init_db_retry = __esm({
32
32
  }
33
33
  });
34
34
 
35
+ // src/lib/secure-files.ts
36
+ import { chmodSync, existsSync, mkdirSync } from "fs";
37
+ import { chmod, mkdir } from "fs/promises";
38
+ async function ensurePrivateDir(dirPath) {
39
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
40
+ try {
41
+ await chmod(dirPath, PRIVATE_DIR_MODE);
42
+ } catch {
43
+ }
44
+ }
45
+ async function enforcePrivateFile(filePath) {
46
+ try {
47
+ await chmod(filePath, PRIVATE_FILE_MODE);
48
+ } catch {
49
+ }
50
+ }
51
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
52
+ var init_secure_files = __esm({
53
+ "src/lib/secure-files.ts"() {
54
+ "use strict";
55
+ PRIVATE_DIR_MODE = 448;
56
+ PRIVATE_FILE_MODE = 384;
57
+ }
58
+ });
59
+
35
60
  // src/lib/config.ts
36
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
37
- import { readFileSync, existsSync, renameSync } from "fs";
61
+ import { readFile, writeFile } from "fs/promises";
62
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
38
63
  import path from "path";
39
64
  import os from "os";
40
65
  function resolveDataDir() {
@@ -42,7 +67,7 @@ function resolveDataDir() {
42
67
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
43
68
  const newDir = path.join(os.homedir(), ".exe-os");
44
69
  const legacyDir = path.join(os.homedir(), ".exe-mem");
45
- if (!existsSync(newDir) && existsSync(legacyDir)) {
70
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
46
71
  try {
47
72
  renameSync(legacyDir, newDir);
48
73
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -105,9 +130,9 @@ function normalizeAutoUpdate(raw) {
105
130
  }
106
131
  async function loadConfig() {
107
132
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
108
- await mkdir(dir, { recursive: true });
133
+ await ensurePrivateDir(dir);
109
134
  const configPath = path.join(dir, "config.json");
110
- if (!existsSync(configPath)) {
135
+ if (!existsSync2(configPath)) {
111
136
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
112
137
  }
113
138
  const raw = await readFile(configPath, "utf-8");
@@ -120,6 +145,7 @@ async function loadConfig() {
120
145
  `);
121
146
  try {
122
147
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
148
+ await enforcePrivateFile(configPath);
123
149
  } catch {
124
150
  }
125
151
  }
@@ -139,6 +165,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
139
165
  var init_config = __esm({
140
166
  "src/lib/config.ts"() {
141
167
  "use strict";
168
+ init_secure_files();
142
169
  EXE_AI_DIR = resolveDataDir();
143
170
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
144
171
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
@@ -217,7 +244,7 @@ var init_config = __esm({
217
244
 
218
245
  // src/lib/employees.ts
219
246
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
220
- import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
247
+ import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
221
248
  import { execSync } from "child_process";
222
249
  import path2 from "path";
223
250
  import os2 from "os";
@@ -238,7 +265,7 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
238
265
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
239
266
  }
240
267
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
241
- if (!existsSync2(employeesPath)) return [];
268
+ if (!existsSync3(employeesPath)) return [];
242
269
  try {
243
270
  return JSON.parse(readFileSync2(employeesPath, "utf-8"));
244
271
  } catch {
@@ -262,7 +289,7 @@ function isMultiInstance(agentName, employees) {
262
289
  if (!emp) return false;
263
290
  return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
264
291
  }
265
- var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
292
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES, IDENTITY_DIR;
266
293
  var init_employees = __esm({
267
294
  "src/lib/employees.ts"() {
268
295
  "use strict";
@@ -271,15 +298,40 @@ var init_employees = __esm({
271
298
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
272
299
  COORDINATOR_ROLE = "COO";
273
300
  MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
301
+ IDENTITY_DIR = path2.join(EXE_AI_DIR, "identity");
302
+ }
303
+ });
304
+
305
+ // src/lib/database-adapter.ts
306
+ import os3 from "os";
307
+ import path3 from "path";
308
+ import { createRequire } from "module";
309
+ import { pathToFileURL } from "url";
310
+ var BOOLEAN_COLUMNS_BY_TABLE, BOOLEAN_COLUMN_NAMES;
311
+ var init_database_adapter = __esm({
312
+ "src/lib/database-adapter.ts"() {
313
+ "use strict";
314
+ BOOLEAN_COLUMNS_BY_TABLE = {
315
+ memories: /* @__PURE__ */ new Set(["has_error", "draft"]),
316
+ behaviors: /* @__PURE__ */ new Set(["active"]),
317
+ notifications: /* @__PURE__ */ new Set(["read"]),
318
+ users: /* @__PURE__ */ new Set(["has_personal_memory"])
319
+ };
320
+ BOOLEAN_COLUMN_NAMES = new Set(
321
+ Object.values(BOOLEAN_COLUMNS_BY_TABLE).flatMap((cols) => [...cols])
322
+ );
274
323
  }
275
324
  });
276
325
 
277
326
  // src/lib/database.ts
278
327
  import { createClient } from "@libsql/client";
279
328
  function getClient() {
280
- if (!_resilientClient) {
329
+ if (!_adapterClient) {
281
330
  throw new Error("Database client not initialized. Call initDatabase() first.");
282
331
  }
332
+ if (process.env.DATABASE_URL) {
333
+ return _adapterClient;
334
+ }
283
335
  if (process.env.EXE_IS_DAEMON === "1") {
284
336
  return _resilientClient;
285
337
  }
@@ -288,132 +340,27 @@ function getClient() {
288
340
  }
289
341
  return _resilientClient;
290
342
  }
291
- var _resilientClient, _daemonClient;
343
+ var _resilientClient, _daemonClient, _adapterClient;
292
344
  var init_database = __esm({
293
345
  "src/lib/database.ts"() {
294
346
  "use strict";
295
347
  init_db_retry();
296
348
  init_employees();
349
+ init_database_adapter();
297
350
  _resilientClient = null;
298
351
  _daemonClient = null;
299
- }
300
- });
301
-
302
- // src/lib/notifications.ts
303
- import crypto from "crypto";
304
- import path3 from "path";
305
- import os3 from "os";
306
- import {
307
- readFileSync as readFileSync3,
308
- readdirSync,
309
- unlinkSync as unlinkSync2,
310
- existsSync as existsSync3,
311
- rmdirSync
312
- } from "fs";
313
- async function writeNotification(notification) {
314
- try {
315
- const client = getClient();
316
- const id = crypto.randomUUID();
317
- const now = (/* @__PURE__ */ new Date()).toISOString();
318
- await client.execute({
319
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
320
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
321
- args: [
322
- id,
323
- notification.agentId,
324
- notification.agentRole,
325
- notification.event,
326
- notification.project,
327
- notification.summary,
328
- notification.taskFile ?? null,
329
- now
330
- ]
331
- });
332
- } catch (err) {
333
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
334
- `);
335
- }
336
- }
337
- async function markAsReadByTaskFile(taskFile) {
338
- try {
339
- const client = getClient();
340
- await client.execute({
341
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
342
- args: [taskFile]
343
- });
344
- } catch {
345
- }
346
- }
347
- var init_notifications = __esm({
348
- "src/lib/notifications.ts"() {
349
- "use strict";
350
- init_database();
351
- }
352
- });
353
-
354
- // src/lib/state-bus.ts
355
- var StateBus, orgBus;
356
- var init_state_bus = __esm({
357
- "src/lib/state-bus.ts"() {
358
- "use strict";
359
- StateBus = class {
360
- handlers = /* @__PURE__ */ new Map();
361
- globalHandlers = /* @__PURE__ */ new Set();
362
- /** Emit an event to all subscribers */
363
- emit(event) {
364
- const typeHandlers = this.handlers.get(event.type);
365
- if (typeHandlers) {
366
- for (const handler of typeHandlers) {
367
- try {
368
- handler(event);
369
- } catch {
370
- }
371
- }
372
- }
373
- for (const handler of this.globalHandlers) {
374
- try {
375
- handler(event);
376
- } catch {
377
- }
378
- }
379
- }
380
- /** Subscribe to a specific event type */
381
- on(type, handler) {
382
- if (!this.handlers.has(type)) {
383
- this.handlers.set(type, /* @__PURE__ */ new Set());
384
- }
385
- this.handlers.get(type).add(handler);
386
- }
387
- /** Subscribe to ALL events */
388
- onAny(handler) {
389
- this.globalHandlers.add(handler);
390
- }
391
- /** Unsubscribe from a specific event type */
392
- off(type, handler) {
393
- this.handlers.get(type)?.delete(handler);
394
- }
395
- /** Unsubscribe from ALL events */
396
- offAny(handler) {
397
- this.globalHandlers.delete(handler);
398
- }
399
- /** Remove all listeners */
400
- clear() {
401
- this.handlers.clear();
402
- this.globalHandlers.clear();
403
- }
404
- };
405
- orgBus = new StateBus();
352
+ _adapterClient = null;
406
353
  }
407
354
  });
408
355
 
409
356
  // src/lib/session-registry.ts
410
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync4 } from "fs";
357
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
411
358
  import path4 from "path";
412
359
  import os4 from "os";
413
360
  function registerSession(entry) {
414
361
  const dir = path4.dirname(REGISTRY_PATH);
415
362
  if (!existsSync4(dir)) {
416
- mkdirSync(dir, { recursive: true });
363
+ mkdirSync2(dir, { recursive: true });
417
364
  }
418
365
  const sessions = listSessions();
419
366
  const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
@@ -426,7 +373,7 @@ function registerSession(entry) {
426
373
  }
427
374
  function listSessions() {
428
375
  try {
429
- const raw = readFileSync4(REGISTRY_PATH, "utf8");
376
+ const raw = readFileSync3(REGISTRY_PATH, "utf8");
430
377
  return JSON.parse(raw);
431
378
  } catch {
432
379
  return [];
@@ -716,12 +663,12 @@ var init_runtime_table = __esm({
716
663
  });
717
664
 
718
665
  // src/lib/agent-config.ts
719
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
666
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
720
667
  import path5 from "path";
721
668
  function loadAgentConfig() {
722
669
  if (!existsSync5(AGENT_CONFIG_PATH)) return {};
723
670
  try {
724
- return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
671
+ return JSON.parse(readFileSync4(AGENT_CONFIG_PATH, "utf-8"));
725
672
  } catch {
726
673
  return {};
727
674
  }
@@ -740,6 +687,7 @@ var init_agent_config = __esm({
740
687
  "use strict";
741
688
  init_config();
742
689
  init_runtime_table();
690
+ init_secure_files();
743
691
  AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
744
692
  DEFAULT_MODELS = {
745
693
  claude: "claude-opus-4",
@@ -758,7 +706,7 @@ __export(intercom_queue_exports, {
758
706
  queueIntercom: () => queueIntercom,
759
707
  readQueue: () => readQueue
760
708
  });
761
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
709
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
762
710
  import path6 from "path";
763
711
  import os5 from "os";
764
712
  function ensureDir() {
@@ -768,7 +716,7 @@ function ensureDir() {
768
716
  function readQueue() {
769
717
  try {
770
718
  if (!existsSync6(QUEUE_PATH)) return [];
771
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
719
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
772
720
  } catch {
773
721
  return [];
774
722
  }
@@ -876,8 +824,11 @@ var init_intercom_queue = __esm({
876
824
  });
877
825
 
878
826
  // src/lib/license.ts
879
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
827
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
880
828
  import { randomUUID } from "crypto";
829
+ import { createRequire as createRequire2 } from "module";
830
+ import { pathToFileURL as pathToFileURL2 } from "url";
831
+ import os6 from "os";
881
832
  import path7 from "path";
882
833
  import { jwtVerify, importSPKI } from "jose";
883
834
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
@@ -899,12 +850,12 @@ var init_license = __esm({
899
850
  });
900
851
 
901
852
  // src/lib/plan-limits.ts
902
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
853
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
903
854
  import path8 from "path";
904
855
  function getLicenseSync() {
905
856
  try {
906
857
  if (!existsSync8(CACHE_PATH2)) return freeLicense();
907
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
858
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
908
859
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
909
860
  const parts = raw.token.split(".");
910
861
  if (parts.length !== 3) return freeLicense();
@@ -943,7 +894,7 @@ function assertEmployeeLimitSync(rosterPath) {
943
894
  let count = 0;
944
895
  try {
945
896
  if (existsSync8(filePath)) {
946
- const raw = readFileSync8(filePath, "utf8");
897
+ const raw = readFileSync7(filePath, "utf8");
947
898
  const employees = JSON.parse(raw);
948
899
  count = Array.isArray(employees) ? employees.length : 0;
949
900
  }
@@ -977,7 +928,7 @@ var init_plan_limits = __esm({
977
928
  });
978
929
 
979
930
  // src/lib/session-kill-telemetry.ts
980
- import crypto2 from "crypto";
931
+ import crypto from "crypto";
981
932
  async function recordSessionKill(input) {
982
933
  try {
983
934
  const client = getClient();
@@ -987,7 +938,7 @@ async function recordSessionKill(input) {
987
938
  ticks_idle, estimated_tokens_saved)
988
939
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
989
940
  args: [
990
- crypto2.randomUUID(),
941
+ crypto.randomUUID(),
991
942
  input.sessionName,
992
943
  input.agentId,
993
944
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -1310,6 +1261,7 @@ __export(tmux_routing_exports, {
1310
1261
  isEmployeeAlive: () => isEmployeeAlive,
1311
1262
  isExeSession: () => isExeSession,
1312
1263
  isSessionBusy: () => isSessionBusy,
1264
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
1313
1265
  notifyParentExe: () => notifyParentExe,
1314
1266
  parseParentExe: () => parseParentExe,
1315
1267
  registerParentExe: () => registerParentExe,
@@ -1320,11 +1272,11 @@ __export(tmux_routing_exports, {
1320
1272
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
1321
1273
  });
1322
1274
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1323
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync as readdirSync2 } from "fs";
1275
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync } from "fs";
1324
1276
  import path9 from "path";
1325
- import os6 from "os";
1277
+ import os7 from "os";
1326
1278
  import { fileURLToPath } from "url";
1327
- import { unlinkSync as unlinkSync3 } from "fs";
1279
+ import { unlinkSync as unlinkSync2 } from "fs";
1328
1280
  function spawnLockPath(sessionName) {
1329
1281
  return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1330
1282
  }
@@ -1343,7 +1295,7 @@ function acquireSpawnLock(sessionName) {
1343
1295
  const lockFile = spawnLockPath(sessionName);
1344
1296
  if (existsSync9(lockFile)) {
1345
1297
  try {
1346
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1298
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1347
1299
  const age = Date.now() - lock.timestamp;
1348
1300
  if (isProcessAlive(lock.pid) && age < 6e4) {
1349
1301
  return false;
@@ -1356,7 +1308,7 @@ function acquireSpawnLock(sessionName) {
1356
1308
  }
1357
1309
  function releaseSpawnLock(sessionName) {
1358
1310
  try {
1359
- unlinkSync3(spawnLockPath(sessionName));
1311
+ unlinkSync2(spawnLockPath(sessionName));
1360
1312
  } catch {
1361
1313
  }
1362
1314
  }
@@ -1448,7 +1400,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1448
1400
  }
1449
1401
  function getParentExe(sessionKey) {
1450
1402
  try {
1451
- const data = JSON.parse(readFileSync9(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1403
+ const data = JSON.parse(readFileSync8(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1452
1404
  return data.parentExe || null;
1453
1405
  } catch {
1454
1406
  return null;
@@ -1456,7 +1408,7 @@ function getParentExe(sessionKey) {
1456
1408
  }
1457
1409
  function getDispatchedBy(sessionKey) {
1458
1410
  try {
1459
- const data = JSON.parse(readFileSync9(
1411
+ const data = JSON.parse(readFileSync8(
1460
1412
  path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1461
1413
  "utf8"
1462
1414
  ));
@@ -1528,7 +1480,7 @@ async function verifyPaneAtCapacity(sessionName) {
1528
1480
  function readDebounceState() {
1529
1481
  try {
1530
1482
  if (!existsSync9(DEBOUNCE_FILE)) return {};
1531
- const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
1483
+ const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1532
1484
  const state = {};
1533
1485
  for (const [key, val] of Object.entries(raw)) {
1534
1486
  if (typeof val === "number") {
@@ -1655,7 +1607,7 @@ function sendIntercom(targetSession) {
1655
1607
  const agent = baseAgentName(rawAgent);
1656
1608
  const taskDir = path9.join(process.cwd(), "exe", agent);
1657
1609
  if (existsSync9(taskDir)) {
1658
- const files = readdirSync2(taskDir).filter(
1610
+ const files = readdirSync(taskDir).filter(
1659
1611
  (f) => f.endsWith(".md") && f !== "DONE.txt"
1660
1612
  );
1661
1613
  if (files.length === 0) {
@@ -1714,6 +1666,21 @@ function notifyParentExe(sessionKey) {
1714
1666
  }
1715
1667
  return true;
1716
1668
  }
1669
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
1670
+ const transport = getTransport();
1671
+ try {
1672
+ const sessions = transport.listSessions();
1673
+ if (!sessions.includes(coordinatorSession)) return false;
1674
+ execSync4(
1675
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
1676
+ { timeout: 3e3 }
1677
+ );
1678
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
1679
+ return true;
1680
+ } catch {
1681
+ return false;
1682
+ }
1683
+ }
1717
1684
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1718
1685
  if (isCoordinatorName(employeeName)) {
1719
1686
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -1787,7 +1754,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1787
1754
  const transport = getTransport();
1788
1755
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1789
1756
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1790
- const logDir = path9.join(os6.homedir(), ".exe-os", "session-logs");
1757
+ const logDir = path9.join(os7.homedir(), ".exe-os", "session-logs");
1791
1758
  const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1792
1759
  if (!existsSync9(logDir)) {
1793
1760
  mkdirSync5(logDir, { recursive: true });
@@ -1803,10 +1770,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1803
1770
  } catch {
1804
1771
  }
1805
1772
  try {
1806
- const claudeJsonPath = path9.join(os6.homedir(), ".claude.json");
1773
+ const claudeJsonPath = path9.join(os7.homedir(), ".claude.json");
1807
1774
  let claudeJson = {};
1808
1775
  try {
1809
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
1776
+ claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
1810
1777
  } catch {
1811
1778
  }
1812
1779
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -1818,13 +1785,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1818
1785
  } catch {
1819
1786
  }
1820
1787
  try {
1821
- const settingsDir = path9.join(os6.homedir(), ".claude", "projects");
1788
+ const settingsDir = path9.join(os7.homedir(), ".claude", "projects");
1822
1789
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1823
1790
  const projSettingsDir = path9.join(settingsDir, normalizedKey);
1824
1791
  const settingsPath = path9.join(projSettingsDir, "settings.json");
1825
1792
  let settings = {};
1826
1793
  try {
1827
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
1794
+ settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
1828
1795
  } catch {
1829
1796
  }
1830
1797
  const perms = settings.permissions ?? {};
@@ -1869,7 +1836,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1869
1836
  let legacyFallbackWarned = false;
1870
1837
  if (!useExeAgent && !useBinSymlink) {
1871
1838
  const identityPath = path9.join(
1872
- os6.homedir(),
1839
+ os7.homedir(),
1873
1840
  ".exe-os",
1874
1841
  "identity",
1875
1842
  `${employeeName}.md`
@@ -1899,7 +1866,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1899
1866
  }
1900
1867
  let sessionContextFlag = "";
1901
1868
  try {
1902
- const ctxDir = path9.join(os6.homedir(), ".exe-os", "session-cache");
1869
+ const ctxDir = path9.join(os7.homedir(), ".exe-os", "session-cache");
1903
1870
  mkdirSync5(ctxDir, { recursive: true });
1904
1871
  const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
1905
1872
  const ctxContent = [
@@ -2060,14 +2027,14 @@ var init_tmux_routing = __esm({
2060
2027
  init_intercom_queue();
2061
2028
  init_plan_limits();
2062
2029
  init_employees();
2063
- SPAWN_LOCK_DIR = path9.join(os6.homedir(), ".exe-os", "spawn-locks");
2064
- SESSION_CACHE = path9.join(os6.homedir(), ".exe-os", "session-cache");
2030
+ SPAWN_LOCK_DIR = path9.join(os7.homedir(), ".exe-os", "spawn-locks");
2031
+ SESSION_CACHE = path9.join(os7.homedir(), ".exe-os", "session-cache");
2065
2032
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2066
2033
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
2067
2034
  VERIFY_PANE_LINES = 200;
2068
2035
  INTERCOM_DEBOUNCE_MS = 3e4;
2069
2036
  CODEX_DEBOUNCE_MS = 12e4;
2070
- INTERCOM_LOG2 = path9.join(os6.homedir(), ".exe-os", "intercom.log");
2037
+ INTERCOM_LOG2 = path9.join(os7.homedir(), ".exe-os", "intercom.log");
2071
2038
  DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
2072
2039
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
2073
2040
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
@@ -2091,6 +2058,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
2091
2058
  args: [scope]
2092
2059
  };
2093
2060
  }
2061
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
2062
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
2063
+ if (!scope) return { sql: "", args: [] };
2064
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
2065
+ return {
2066
+ sql: ` AND ${col} = ?`,
2067
+ args: [scope]
2068
+ };
2069
+ }
2094
2070
  var init_task_scope = __esm({
2095
2071
  "src/lib/task-scope.ts"() {
2096
2072
  "use strict";
@@ -2098,13 +2074,125 @@ var init_task_scope = __esm({
2098
2074
  }
2099
2075
  });
2100
2076
 
2077
+ // src/lib/notifications.ts
2078
+ import crypto2 from "crypto";
2079
+ import path10 from "path";
2080
+ import os8 from "os";
2081
+ import {
2082
+ readFileSync as readFileSync9,
2083
+ readdirSync as readdirSync2,
2084
+ unlinkSync as unlinkSync3,
2085
+ existsSync as existsSync10,
2086
+ rmdirSync
2087
+ } from "fs";
2088
+ async function writeNotification(notification) {
2089
+ try {
2090
+ const client = getClient();
2091
+ const id = crypto2.randomUUID();
2092
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2093
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
2094
+ await client.execute({
2095
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
2096
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2097
+ args: [
2098
+ id,
2099
+ notification.agentId,
2100
+ notification.agentRole,
2101
+ notification.event,
2102
+ notification.project,
2103
+ notification.summary,
2104
+ notification.taskFile ?? null,
2105
+ sessionScope,
2106
+ now
2107
+ ]
2108
+ });
2109
+ } catch (err) {
2110
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2111
+ `);
2112
+ }
2113
+ }
2114
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
2115
+ try {
2116
+ const client = getClient();
2117
+ const scope = strictSessionScopeFilter(sessionScope);
2118
+ await client.execute({
2119
+ sql: `UPDATE notifications SET read = 1
2120
+ WHERE task_file = ? AND read = 0${scope.sql}`,
2121
+ args: [taskFile, ...scope.args]
2122
+ });
2123
+ } catch {
2124
+ }
2125
+ }
2126
+ var init_notifications = __esm({
2127
+ "src/lib/notifications.ts"() {
2128
+ "use strict";
2129
+ init_database();
2130
+ init_task_scope();
2131
+ }
2132
+ });
2133
+
2134
+ // src/lib/state-bus.ts
2135
+ var StateBus, orgBus;
2136
+ var init_state_bus = __esm({
2137
+ "src/lib/state-bus.ts"() {
2138
+ "use strict";
2139
+ StateBus = class {
2140
+ handlers = /* @__PURE__ */ new Map();
2141
+ globalHandlers = /* @__PURE__ */ new Set();
2142
+ /** Emit an event to all subscribers */
2143
+ emit(event) {
2144
+ const typeHandlers = this.handlers.get(event.type);
2145
+ if (typeHandlers) {
2146
+ for (const handler of typeHandlers) {
2147
+ try {
2148
+ handler(event);
2149
+ } catch {
2150
+ }
2151
+ }
2152
+ }
2153
+ for (const handler of this.globalHandlers) {
2154
+ try {
2155
+ handler(event);
2156
+ } catch {
2157
+ }
2158
+ }
2159
+ }
2160
+ /** Subscribe to a specific event type */
2161
+ on(type, handler) {
2162
+ if (!this.handlers.has(type)) {
2163
+ this.handlers.set(type, /* @__PURE__ */ new Set());
2164
+ }
2165
+ this.handlers.get(type).add(handler);
2166
+ }
2167
+ /** Subscribe to ALL events */
2168
+ onAny(handler) {
2169
+ this.globalHandlers.add(handler);
2170
+ }
2171
+ /** Unsubscribe from a specific event type */
2172
+ off(type, handler) {
2173
+ this.handlers.get(type)?.delete(handler);
2174
+ }
2175
+ /** Unsubscribe from ALL events */
2176
+ offAny(handler) {
2177
+ this.globalHandlers.delete(handler);
2178
+ }
2179
+ /** Remove all listeners */
2180
+ clear() {
2181
+ this.handlers.clear();
2182
+ this.globalHandlers.clear();
2183
+ }
2184
+ };
2185
+ orgBus = new StateBus();
2186
+ }
2187
+ });
2188
+
2101
2189
  // src/lib/tasks-crud.ts
2102
2190
  import crypto3 from "crypto";
2103
- import path10 from "path";
2104
- import os7 from "os";
2191
+ import path11 from "path";
2192
+ import os9 from "os";
2105
2193
  import { execSync as execSync5 } from "child_process";
2106
2194
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2107
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2195
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2108
2196
  async function writeCheckpoint(input) {
2109
2197
  const client = getClient();
2110
2198
  const row = await resolveTask(client, input.taskId);
@@ -2279,8 +2367,8 @@ ${laneWarning}` : laneWarning;
2279
2367
  }
2280
2368
  if (input.baseDir) {
2281
2369
  try {
2282
- await mkdir3(path10.join(input.baseDir, "exe", "output"), { recursive: true });
2283
- await mkdir3(path10.join(input.baseDir, "exe", "research"), { recursive: true });
2370
+ await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
2371
+ await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
2284
2372
  await ensureArchitectureDoc(input.baseDir, input.projectName);
2285
2373
  await ensureGitignoreExe(input.baseDir);
2286
2374
  } catch {
@@ -2316,13 +2404,19 @@ ${laneWarning}` : laneWarning;
2316
2404
  });
2317
2405
  if (input.baseDir) {
2318
2406
  try {
2319
- const EXE_OS_DIR = path10.join(os7.homedir(), ".exe-os");
2320
- const mdPath = path10.join(EXE_OS_DIR, taskFile);
2321
- const mdDir = path10.dirname(mdPath);
2322
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2407
+ const EXE_OS_DIR = path11.join(os9.homedir(), ".exe-os");
2408
+ const mdPath = path11.join(EXE_OS_DIR, taskFile);
2409
+ const mdDir = path11.dirname(mdPath);
2410
+ if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
2323
2411
  const reviewer = input.reviewer ?? input.assignedBy;
2324
2412
  const mdContent = `# ${input.title}
2325
2413
 
2414
+ ## MANDATORY: When done
2415
+
2416
+ You MUST call update_task with status "done" and a result summary when finished.
2417
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2418
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2419
+
2326
2420
  **ID:** ${id}
2327
2421
  **Status:** ${initialStatus}
2328
2422
  **Priority:** ${input.priority}
@@ -2336,12 +2430,6 @@ ${laneWarning}` : laneWarning;
2336
2430
  ## Context
2337
2431
 
2338
2432
  ${input.context}
2339
-
2340
- ## MANDATORY: When done
2341
-
2342
- You MUST call update_task with status "done" and a result summary when finished.
2343
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2344
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
2345
2433
  `;
2346
2434
  await writeFile3(mdPath, mdContent, "utf-8");
2347
2435
  } catch (err) {
@@ -2590,7 +2678,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2590
2678
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
2591
2679
  } catch {
2592
2680
  }
2593
- if (input.status === "done" || input.status === "cancelled") {
2681
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
2594
2682
  try {
2595
2683
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
2596
2684
  clearQueueForAgent2(String(row.assigned_to));
@@ -2619,9 +2707,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2619
2707
  return { taskFile, assignedTo, assignedBy, taskSlug };
2620
2708
  }
2621
2709
  async function ensureArchitectureDoc(baseDir, projectName) {
2622
- const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
2710
+ const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
2623
2711
  try {
2624
- if (existsSync10(archPath)) return;
2712
+ if (existsSync11(archPath)) return;
2625
2713
  const template = [
2626
2714
  `# ${projectName} \u2014 System Architecture`,
2627
2715
  "",
@@ -2654,9 +2742,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2654
2742
  }
2655
2743
  }
2656
2744
  async function ensureGitignoreExe(baseDir) {
2657
- const gitignorePath = path10.join(baseDir, ".gitignore");
2745
+ const gitignorePath = path11.join(baseDir, ".gitignore");
2658
2746
  try {
2659
- if (existsSync10(gitignorePath)) {
2747
+ if (existsSync11(gitignorePath)) {
2660
2748
  const content = readFileSync10(gitignorePath, "utf-8");
2661
2749
  if (/^\/?exe\/?$/m.test(content)) return;
2662
2750
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -2688,58 +2776,42 @@ var init_tasks_crud = __esm({
2688
2776
  });
2689
2777
 
2690
2778
  // src/lib/tasks-review.ts
2691
- import path11 from "path";
2692
- import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2779
+ import path12 from "path";
2780
+ import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2693
2781
  async function countPendingReviews(sessionScope) {
2694
2782
  const client = getClient();
2695
- if (sessionScope) {
2696
- const result2 = await client.execute({
2697
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND (session_scope = ? OR session_scope IS NULL)",
2698
- args: [sessionScope]
2699
- });
2700
- return Number(result2.rows[0]?.cnt) || 0;
2701
- }
2783
+ const scope = strictSessionScopeFilter(
2784
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2785
+ );
2702
2786
  const result = await client.execute({
2703
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
2704
- args: []
2787
+ sql: `SELECT COUNT(*) as cnt FROM tasks
2788
+ WHERE status = 'needs_review'${scope.sql}`,
2789
+ args: [...scope.args]
2705
2790
  });
2706
2791
  return Number(result.rows[0]?.cnt) || 0;
2707
2792
  }
2708
2793
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2709
2794
  const client = getClient();
2710
- if (sessionScope) {
2711
- const result2 = await client.execute({
2712
- sql: `SELECT COUNT(*) as cnt FROM tasks
2713
- WHERE status = 'needs_review' AND updated_at > ?
2714
- AND session_scope = ?`,
2715
- args: [sinceIso, sessionScope]
2716
- });
2717
- return Number(result2.rows[0]?.cnt) || 0;
2718
- }
2795
+ const scope = strictSessionScopeFilter(
2796
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2797
+ );
2719
2798
  const result = await client.execute({
2720
2799
  sql: `SELECT COUNT(*) as cnt FROM tasks
2721
- WHERE status = 'needs_review' AND updated_at > ?`,
2722
- args: [sinceIso]
2800
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
2801
+ args: [sinceIso, ...scope.args]
2723
2802
  });
2724
2803
  return Number(result.rows[0]?.cnt) || 0;
2725
2804
  }
2726
2805
  async function listPendingReviews(limit, sessionScope) {
2727
2806
  const client = getClient();
2728
- if (sessionScope) {
2729
- const result2 = await client.execute({
2730
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2731
- WHERE status = 'needs_review'
2732
- AND session_scope = ?
2733
- ORDER BY updated_at ASC LIMIT ?`,
2734
- args: [sessionScope, limit]
2735
- });
2736
- return result2.rows;
2737
- }
2807
+ const scope = strictSessionScopeFilter(
2808
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2809
+ );
2738
2810
  const result = await client.execute({
2739
2811
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2740
- WHERE status = 'needs_review'
2812
+ WHERE status = 'needs_review'${scope.sql}
2741
2813
  ORDER BY updated_at ASC LIMIT ?`,
2742
- args: [limit]
2814
+ args: [...scope.args, limit]
2743
2815
  });
2744
2816
  return result.rows;
2745
2817
  }
@@ -2751,7 +2823,7 @@ async function cleanupOrphanedReviews() {
2751
2823
  WHERE status IN ('open', 'needs_review', 'in_progress')
2752
2824
  AND assigned_by = 'system'
2753
2825
  AND title LIKE 'Review:%'
2754
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
2826
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
2755
2827
  args: [now]
2756
2828
  });
2757
2829
  const r1b = await client.execute({
@@ -2870,11 +2942,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2870
2942
  );
2871
2943
  }
2872
2944
  try {
2873
- const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
2874
- if (existsSync11(cacheDir)) {
2945
+ const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
2946
+ if (existsSync12(cacheDir)) {
2875
2947
  for (const f of readdirSync3(cacheDir)) {
2876
2948
  if (f.startsWith("review-notified-")) {
2877
- unlinkSync4(path11.join(cacheDir, f));
2949
+ unlinkSync4(path12.join(cacheDir, f));
2878
2950
  }
2879
2951
  }
2880
2952
  }
@@ -2891,11 +2963,12 @@ var init_tasks_review = __esm({
2891
2963
  init_tmux_routing();
2892
2964
  init_session_key();
2893
2965
  init_state_bus();
2966
+ init_task_scope();
2894
2967
  }
2895
2968
  });
2896
2969
 
2897
2970
  // src/lib/tasks-chain.ts
2898
- import path12 from "path";
2971
+ import path13 from "path";
2899
2972
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2900
2973
  async function cascadeUnblock(taskId, baseDir, now) {
2901
2974
  const client = getClient();
@@ -2912,7 +2985,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2912
2985
  });
2913
2986
  for (const ur of unblockedRows.rows) {
2914
2987
  try {
2915
- const ubFile = path12.join(baseDir, String(ur.task_file));
2988
+ const ubFile = path13.join(baseDir, String(ur.task_file));
2916
2989
  let ubContent = await readFile3(ubFile, "utf-8");
2917
2990
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2918
2991
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2947,7 +3020,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
2947
3020
  const scScope = sessionScopeFilter();
2948
3021
  const remaining = await client.execute({
2949
3022
  sql: `SELECT COUNT(*) as cnt FROM tasks
2950
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3023
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
2951
3024
  args: [parentTaskId, ...scScope.args]
2952
3025
  });
2953
3026
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -2981,7 +3054,7 @@ var init_tasks_chain = __esm({
2981
3054
 
2982
3055
  // src/lib/project-name.ts
2983
3056
  import { execSync as execSync6 } from "child_process";
2984
- import path13 from "path";
3057
+ import path14 from "path";
2985
3058
  function getProjectName(cwd) {
2986
3059
  const dir = cwd ?? process.cwd();
2987
3060
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2994,7 +3067,7 @@ function getProjectName(cwd) {
2994
3067
  timeout: 2e3,
2995
3068
  stdio: ["pipe", "pipe", "pipe"]
2996
3069
  }).trim();
2997
- repoRoot = path13.dirname(gitCommonDir);
3070
+ repoRoot = path14.dirname(gitCommonDir);
2998
3071
  } catch {
2999
3072
  repoRoot = execSync6("git rev-parse --show-toplevel", {
3000
3073
  cwd: dir,
@@ -3003,11 +3076,11 @@ function getProjectName(cwd) {
3003
3076
  stdio: ["pipe", "pipe", "pipe"]
3004
3077
  }).trim();
3005
3078
  }
3006
- _cached2 = path13.basename(repoRoot);
3079
+ _cached2 = path14.basename(repoRoot);
3007
3080
  _cachedCwd = dir;
3008
3081
  return _cached2;
3009
3082
  } catch {
3010
- _cached2 = path13.basename(dir);
3083
+ _cached2 = path14.basename(dir);
3011
3084
  _cachedCwd = dir;
3012
3085
  return _cached2;
3013
3086
  }
@@ -3480,7 +3553,7 @@ __export(tasks_exports, {
3480
3553
  updateTaskStatus: () => updateTaskStatus,
3481
3554
  writeCheckpoint: () => writeCheckpoint
3482
3555
  });
3483
- import path14 from "path";
3556
+ import path15 from "path";
3484
3557
  import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
3485
3558
  async function createTask(input) {
3486
3559
  const result = await createTaskCore(input);
@@ -3500,12 +3573,12 @@ async function updateTask(input) {
3500
3573
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
3501
3574
  try {
3502
3575
  const agent = String(row.assigned_to);
3503
- const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
3504
- const cachePath = path14.join(cacheDir, `current-task-${agent}.json`);
3576
+ const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
3577
+ const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
3505
3578
  if (input.status === "in_progress") {
3506
3579
  mkdirSync6(cacheDir, { recursive: true });
3507
3580
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3508
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3581
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
3509
3582
  try {
3510
3583
  unlinkSync5(cachePath);
3511
3584
  } catch {
@@ -3513,10 +3586,10 @@ async function updateTask(input) {
3513
3586
  }
3514
3587
  } catch {
3515
3588
  }
3516
- if (input.status === "done") {
3589
+ if (input.status === "done" || input.status === "closed") {
3517
3590
  await cleanupReviewFile(row, taskFile, input.baseDir);
3518
3591
  }
3519
- if (input.status === "done" || input.status === "cancelled") {
3592
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3520
3593
  try {
3521
3594
  const client = getClient();
3522
3595
  const taskTitle = String(row.title);
@@ -3532,7 +3605,7 @@ async function updateTask(input) {
3532
3605
  if (!isCoordinatorName(assignedAgent)) {
3533
3606
  try {
3534
3607
  const draftClient = getClient();
3535
- if (input.status === "done") {
3608
+ if (input.status === "done" || input.status === "closed") {
3536
3609
  await draftClient.execute({
3537
3610
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3538
3611
  args: [assignedAgent]
@@ -3549,7 +3622,7 @@ async function updateTask(input) {
3549
3622
  try {
3550
3623
  const client = getClient();
3551
3624
  const cascaded = await client.execute({
3552
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
3625
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
3553
3626
  WHERE parent_task_id = ? AND status = 'needs_review'`,
3554
3627
  args: [now, taskId]
3555
3628
  });
@@ -3562,14 +3635,14 @@ async function updateTask(input) {
3562
3635
  } catch {
3563
3636
  }
3564
3637
  }
3565
- const isTerminal = input.status === "done" || input.status === "needs_review";
3638
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
3566
3639
  if (isTerminal) {
3567
3640
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
3568
3641
  if (!isCoordinator) {
3569
3642
  notifyTaskDone();
3570
3643
  }
3571
3644
  await markTaskNotificationsRead(taskFile);
3572
- if (input.status === "done") {
3645
+ if (input.status === "done" || input.status === "closed") {
3573
3646
  try {
3574
3647
  await cascadeUnblock(taskId, input.baseDir, now);
3575
3648
  } catch {
@@ -3589,7 +3662,7 @@ async function updateTask(input) {
3589
3662
  }
3590
3663
  }
3591
3664
  }
3592
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3665
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3593
3666
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3594
3667
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3595
3668
  taskId,