@askexenow/exe-os 0.9.10 → 0.9.11

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.
@@ -267,6 +267,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
267
267
  function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
268
268
  return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
269
269
  }
270
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
271
+ if (!existsSync3(employeesPath)) {
272
+ return [];
273
+ }
274
+ const raw = await readFile2(employeesPath, "utf-8");
275
+ try {
276
+ return JSON.parse(raw);
277
+ } catch {
278
+ return [];
279
+ }
280
+ }
270
281
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
271
282
  if (!existsSync3(employeesPath)) return [];
272
283
  try {
@@ -1249,6 +1260,369 @@ var init_capacity_monitor = __esm({
1249
1260
  }
1250
1261
  });
1251
1262
 
1263
+ // src/lib/state-bus.ts
1264
+ var StateBus, orgBus;
1265
+ var init_state_bus = __esm({
1266
+ "src/lib/state-bus.ts"() {
1267
+ "use strict";
1268
+ StateBus = class {
1269
+ handlers = /* @__PURE__ */ new Map();
1270
+ globalHandlers = /* @__PURE__ */ new Set();
1271
+ /** Emit an event to all subscribers */
1272
+ emit(event) {
1273
+ const typeHandlers = this.handlers.get(event.type);
1274
+ if (typeHandlers) {
1275
+ for (const handler of typeHandlers) {
1276
+ try {
1277
+ handler(event);
1278
+ } catch {
1279
+ }
1280
+ }
1281
+ }
1282
+ for (const handler of this.globalHandlers) {
1283
+ try {
1284
+ handler(event);
1285
+ } catch {
1286
+ }
1287
+ }
1288
+ }
1289
+ /** Subscribe to a specific event type */
1290
+ on(type, handler) {
1291
+ if (!this.handlers.has(type)) {
1292
+ this.handlers.set(type, /* @__PURE__ */ new Set());
1293
+ }
1294
+ this.handlers.get(type).add(handler);
1295
+ }
1296
+ /** Subscribe to ALL events */
1297
+ onAny(handler) {
1298
+ this.globalHandlers.add(handler);
1299
+ }
1300
+ /** Unsubscribe from a specific event type */
1301
+ off(type, handler) {
1302
+ this.handlers.get(type)?.delete(handler);
1303
+ }
1304
+ /** Unsubscribe from ALL events */
1305
+ offAny(handler) {
1306
+ this.globalHandlers.delete(handler);
1307
+ }
1308
+ /** Remove all listeners */
1309
+ clear() {
1310
+ this.handlers.clear();
1311
+ this.globalHandlers.clear();
1312
+ }
1313
+ };
1314
+ orgBus = new StateBus();
1315
+ }
1316
+ });
1317
+
1318
+ // src/lib/tasks-review.ts
1319
+ var tasks_review_exports = {};
1320
+ __export(tasks_review_exports, {
1321
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
1322
+ cleanupReviewFile: () => cleanupReviewFile,
1323
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
1324
+ countPendingReviews: () => countPendingReviews,
1325
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
1326
+ formatAge: () => formatAge,
1327
+ getReviewChecklist: () => getReviewChecklist,
1328
+ isStale: () => isStale,
1329
+ listPendingReviews: () => listPendingReviews
1330
+ });
1331
+ import path9 from "path";
1332
+ import { existsSync as existsSync9, readdirSync, unlinkSync as unlinkSync2 } from "fs";
1333
+ function formatAge(isoTimestamp) {
1334
+ if (!isoTimestamp) return "";
1335
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
1336
+ if (ms < 0) return "just now";
1337
+ const minutes = Math.floor(ms / 6e4);
1338
+ if (minutes < 60) return `${minutes}m ago`;
1339
+ const hours = Math.floor(minutes / 60);
1340
+ if (hours < 24) return `${hours}h ago`;
1341
+ const days = Math.floor(hours / 24);
1342
+ return `${days}d ago`;
1343
+ }
1344
+ function isStale(isoTimestamp) {
1345
+ if (!isoTimestamp) return false;
1346
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
1347
+ }
1348
+ async function countPendingReviews(sessionScope) {
1349
+ const client = getClient();
1350
+ const scope = strictSessionScopeFilter(
1351
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1352
+ );
1353
+ const result = await client.execute({
1354
+ sql: `SELECT COUNT(*) as cnt FROM tasks
1355
+ WHERE status = 'needs_review'${scope.sql}`,
1356
+ args: [...scope.args]
1357
+ });
1358
+ return Number(result.rows[0]?.cnt) || 0;
1359
+ }
1360
+ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
1361
+ const client = getClient();
1362
+ const scope = strictSessionScopeFilter(
1363
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1364
+ );
1365
+ const result = await client.execute({
1366
+ sql: `SELECT COUNT(*) as cnt FROM tasks
1367
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
1368
+ args: [sinceIso, ...scope.args]
1369
+ });
1370
+ return Number(result.rows[0]?.cnt) || 0;
1371
+ }
1372
+ async function listPendingReviews(limit, sessionScope) {
1373
+ const client = getClient();
1374
+ const scope = strictSessionScopeFilter(
1375
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1376
+ );
1377
+ const result = await client.execute({
1378
+ sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
1379
+ WHERE status = 'needs_review'${scope.sql}
1380
+ ORDER BY updated_at ASC LIMIT ?`,
1381
+ args: [...scope.args, limit]
1382
+ });
1383
+ return result.rows;
1384
+ }
1385
+ async function cleanupOrphanedReviews() {
1386
+ const client = getClient();
1387
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1388
+ const r1 = await client.execute({
1389
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
1390
+ WHERE status IN ('open', 'needs_review', 'in_progress')
1391
+ AND assigned_by = 'system'
1392
+ AND title LIKE 'Review:%'
1393
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
1394
+ args: [now]
1395
+ });
1396
+ const r1b = await client.execute({
1397
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
1398
+ WHERE status IN ('open', 'needs_review')
1399
+ AND title LIKE 'Review:%completed%'
1400
+ AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
1401
+ args: [now]
1402
+ });
1403
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
1404
+ const r2 = await client.execute({
1405
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
1406
+ WHERE status = 'needs_review'
1407
+ AND result IS NOT NULL
1408
+ AND updated_at < ?`,
1409
+ args: [now, staleThreshold]
1410
+ });
1411
+ const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
1412
+ if (total > 0) {
1413
+ process.stderr.write(
1414
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
1415
+ `
1416
+ );
1417
+ }
1418
+ return total;
1419
+ }
1420
+ function getReviewChecklist(role, agent, taskSlug) {
1421
+ const roleLower = role.toLowerCase();
1422
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
1423
+ return {
1424
+ lens: "Code Quality (Engineer)",
1425
+ checklist: [
1426
+ "1. Do all tests pass? Any new tests needed?",
1427
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
1428
+ "3. Does it follow existing patterns and conventions in the codebase?",
1429
+ "4. Any regressions in the test suite?"
1430
+ ]
1431
+ };
1432
+ }
1433
+ if (roleLower === "cto" || roleLower.includes("architect")) {
1434
+ return {
1435
+ lens: "Architecture (CTO)",
1436
+ checklist: [
1437
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
1438
+ "2. Is it backward compatible? Any breaking changes?",
1439
+ "3. Does it introduce technical debt? Is that debt justified?",
1440
+ "4. Security implications? Any new attack surface?",
1441
+ "5. Does it scale? Performance considerations?",
1442
+ "6. Coordination: does this affect other employees' work or other projects?"
1443
+ ]
1444
+ };
1445
+ }
1446
+ if (roleLower === "coo" || roleLower.includes("operations")) {
1447
+ return {
1448
+ lens: "Strategic (COO)",
1449
+ checklist: [
1450
+ "1. Does this serve the project mission?",
1451
+ "2. Is this the right work at the right time?",
1452
+ "3. Does the architectural assessment make sense for the business?",
1453
+ "4. Any cross-project implications?"
1454
+ ]
1455
+ };
1456
+ }
1457
+ return {
1458
+ lens: "General",
1459
+ checklist: [
1460
+ "1. Read the original task's acceptance criteria",
1461
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
1462
+ "3. Verify code changes match requirements",
1463
+ "4. Check if tests were added/updated",
1464
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
1465
+ ]
1466
+ };
1467
+ }
1468
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
1469
+ const taskFile = String(row.task_file);
1470
+ const employees = await loadEmployees();
1471
+ const coordinatorName = getCoordinatorName(employees);
1472
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
1473
+ if (String(row.title).startsWith("Review:")) return;
1474
+ const fileName = taskFile.split("/").pop() ?? "";
1475
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
1476
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
1477
+ const client = getClient();
1478
+ const agent = String(row.assigned_to);
1479
+ const rawReviewer = row.reviewer || row.assigned_by;
1480
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
1481
+ const currentStatus = String(row.status ?? "");
1482
+ if (currentStatus === "done") return;
1483
+ const existingResult = String(row.result ?? "");
1484
+ if (existingResult.includes("## Review notes")) return;
1485
+ let reviewerRole = "unknown";
1486
+ try {
1487
+ const emp = getEmployee(employees, reviewer);
1488
+ if (emp) reviewerRole = emp.role;
1489
+ } catch {
1490
+ }
1491
+ const taskTitle = String(row.title);
1492
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
1493
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
1494
+ process.stderr.write(
1495
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
1496
+ `
1497
+ );
1498
+ const reviewNotes = [
1499
+ `
1500
+ ---
1501
+ ## Review notes`,
1502
+ `Review lens: ${lens}`,
1503
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
1504
+ "",
1505
+ "### Checklist",
1506
+ ...checklist,
1507
+ "",
1508
+ "### Verdict",
1509
+ "- **Approved:** mark this task as done",
1510
+ "- **Needs work:** re-open with notes"
1511
+ ].join("\n");
1512
+ const originalTaskId = String(row.id);
1513
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
1514
+ await client.execute({
1515
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
1516
+ WHERE id = ?`,
1517
+ args: [updatedResult, now, originalTaskId]
1518
+ });
1519
+ orgBus.emit({
1520
+ type: "review_created",
1521
+ reviewId: originalTaskId,
1522
+ employee: agent,
1523
+ reviewer,
1524
+ timestamp: now
1525
+ });
1526
+ await writeNotification({
1527
+ agentId: agent,
1528
+ agentRole: String(row.assigned_to),
1529
+ event: "task_complete",
1530
+ project: String(row.project_name),
1531
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
1532
+ taskFile
1533
+ });
1534
+ const originalPriority = String(row.priority).toLowerCase();
1535
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
1536
+ if (!autoApprove) {
1537
+ try {
1538
+ const key = getSessionKey();
1539
+ const exeSession = getParentExe(key);
1540
+ if (exeSession) {
1541
+ sendIntercom(exeSession);
1542
+ }
1543
+ } catch {
1544
+ }
1545
+ }
1546
+ if (autoApprove) {
1547
+ process.stderr.write(
1548
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
1549
+ `
1550
+ );
1551
+ await client.execute({
1552
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
1553
+ args: [now, originalTaskId]
1554
+ });
1555
+ }
1556
+ }
1557
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
1558
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
1559
+ try {
1560
+ const client = getClient();
1561
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1562
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
1563
+ if (parentId) {
1564
+ const result = await client.execute({
1565
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
1566
+ args: [now, parentId]
1567
+ });
1568
+ if (result.rowsAffected > 0) {
1569
+ process.stderr.write(
1570
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
1571
+ `
1572
+ );
1573
+ }
1574
+ } else {
1575
+ const fileName = taskFile.split("/").pop() ?? "";
1576
+ const reviewPrefix = fileName.replace(".md", "");
1577
+ const parts = reviewPrefix.split("-");
1578
+ if (parts.length >= 3 && parts[0] === "review") {
1579
+ const agent = parts[1];
1580
+ const slug = parts.slice(2).join("-");
1581
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
1582
+ const result = await client.execute({
1583
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
1584
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
1585
+ });
1586
+ if (result.rowsAffected > 0) {
1587
+ process.stderr.write(
1588
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
1589
+ `
1590
+ );
1591
+ }
1592
+ }
1593
+ }
1594
+ } catch (err) {
1595
+ process.stderr.write(
1596
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
1597
+ `
1598
+ );
1599
+ }
1600
+ try {
1601
+ const cacheDir = path9.join(EXE_AI_DIR, "session-cache");
1602
+ if (existsSync9(cacheDir)) {
1603
+ for (const f of readdirSync(cacheDir)) {
1604
+ if (f.startsWith("review-notified-")) {
1605
+ unlinkSync2(path9.join(cacheDir, f));
1606
+ }
1607
+ }
1608
+ }
1609
+ } catch {
1610
+ }
1611
+ }
1612
+ var init_tasks_review = __esm({
1613
+ "src/lib/tasks-review.ts"() {
1614
+ "use strict";
1615
+ init_database();
1616
+ init_config();
1617
+ init_employees();
1618
+ init_notifications();
1619
+ init_tmux_routing();
1620
+ init_session_key();
1621
+ init_state_bus();
1622
+ init_task_scope();
1623
+ }
1624
+ });
1625
+
1252
1626
  // src/lib/tmux-routing.ts
1253
1627
  var tmux_routing_exports = {};
1254
1628
  __export(tmux_routing_exports, {
@@ -1275,13 +1649,13 @@ __export(tmux_routing_exports, {
1275
1649
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
1276
1650
  });
1277
1651
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1278
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync } from "fs";
1279
- import path9 from "path";
1652
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync, readdirSync as readdirSync2 } from "fs";
1653
+ import path10 from "path";
1280
1654
  import os7 from "os";
1281
1655
  import { fileURLToPath } from "url";
1282
- import { unlinkSync as unlinkSync2 } from "fs";
1656
+ import { unlinkSync as unlinkSync3 } from "fs";
1283
1657
  function spawnLockPath(sessionName) {
1284
- return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1658
+ return path10.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1285
1659
  }
1286
1660
  function isProcessAlive(pid) {
1287
1661
  try {
@@ -1292,11 +1666,11 @@ function isProcessAlive(pid) {
1292
1666
  }
1293
1667
  }
1294
1668
  function acquireSpawnLock(sessionName) {
1295
- if (!existsSync9(SPAWN_LOCK_DIR)) {
1669
+ if (!existsSync10(SPAWN_LOCK_DIR)) {
1296
1670
  mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
1297
1671
  }
1298
1672
  const lockFile = spawnLockPath(sessionName);
1299
- if (existsSync9(lockFile)) {
1673
+ if (existsSync10(lockFile)) {
1300
1674
  try {
1301
1675
  const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1302
1676
  const age = Date.now() - lock.timestamp;
@@ -1311,20 +1685,20 @@ function acquireSpawnLock(sessionName) {
1311
1685
  }
1312
1686
  function releaseSpawnLock(sessionName) {
1313
1687
  try {
1314
- unlinkSync2(spawnLockPath(sessionName));
1688
+ unlinkSync3(spawnLockPath(sessionName));
1315
1689
  } catch {
1316
1690
  }
1317
1691
  }
1318
1692
  function resolveBehaviorsExporterScript() {
1319
1693
  try {
1320
1694
  const thisFile = fileURLToPath(import.meta.url);
1321
- const scriptPath = path9.join(
1322
- path9.dirname(thisFile),
1695
+ const scriptPath = path10.join(
1696
+ path10.dirname(thisFile),
1323
1697
  "..",
1324
1698
  "bin",
1325
1699
  "exe-export-behaviors.js"
1326
1700
  );
1327
- return existsSync9(scriptPath) ? scriptPath : null;
1701
+ return existsSync10(scriptPath) ? scriptPath : null;
1328
1702
  } catch {
1329
1703
  return null;
1330
1704
  }
@@ -1390,11 +1764,11 @@ function extractRootExe(name) {
1390
1764
  return parts.length > 0 ? parts[parts.length - 1] : null;
1391
1765
  }
1392
1766
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1393
- if (!existsSync9(SESSION_CACHE)) {
1767
+ if (!existsSync10(SESSION_CACHE)) {
1394
1768
  mkdirSync5(SESSION_CACHE, { recursive: true });
1395
1769
  }
1396
1770
  const rootExe = extractRootExe(parentExe) ?? parentExe;
1397
- const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1771
+ const filePath = path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1398
1772
  writeFileSync6(filePath, JSON.stringify({
1399
1773
  parentExe: rootExe,
1400
1774
  dispatchedBy: dispatchedBy || rootExe,
@@ -1403,7 +1777,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1403
1777
  }
1404
1778
  function getParentExe(sessionKey) {
1405
1779
  try {
1406
- const data = JSON.parse(readFileSync8(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1780
+ const data = JSON.parse(readFileSync8(path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1407
1781
  return data.parentExe || null;
1408
1782
  } catch {
1409
1783
  return null;
@@ -1412,7 +1786,7 @@ function getParentExe(sessionKey) {
1412
1786
  function getDispatchedBy(sessionKey) {
1413
1787
  try {
1414
1788
  const data = JSON.parse(readFileSync8(
1415
- path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1789
+ path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1416
1790
  "utf8"
1417
1791
  ));
1418
1792
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -1482,7 +1856,7 @@ async function verifyPaneAtCapacity(sessionName) {
1482
1856
  }
1483
1857
  function readDebounceState() {
1484
1858
  try {
1485
- if (!existsSync9(DEBOUNCE_FILE)) return {};
1859
+ if (!existsSync10(DEBOUNCE_FILE)) return {};
1486
1860
  const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1487
1861
  const state = {};
1488
1862
  for (const [key, val] of Object.entries(raw)) {
@@ -1499,7 +1873,7 @@ function readDebounceState() {
1499
1873
  }
1500
1874
  function writeDebounceState(state) {
1501
1875
  try {
1502
- if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
1876
+ if (!existsSync10(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
1503
1877
  writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
1504
1878
  } catch {
1505
1879
  }
@@ -1595,22 +1969,24 @@ function sendIntercom(targetSession) {
1595
1969
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
1596
1970
  return "queued";
1597
1971
  }
1598
- try {
1599
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
1600
- const agent = baseAgentName(rawAgent);
1601
- const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
1602
- if (existsSync9(markerPath)) {
1603
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
1604
- return "debounced";
1972
+ if (sessionState !== "idle") {
1973
+ try {
1974
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
1975
+ const agent = baseAgentName(rawAgent);
1976
+ const markerPath = path10.join(SESSION_CACHE, `current-task-${agent}.json`);
1977
+ if (existsSync10(markerPath)) {
1978
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
1979
+ return "debounced";
1980
+ }
1981
+ } catch {
1605
1982
  }
1606
- } catch {
1607
1983
  }
1608
1984
  try {
1609
1985
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
1610
1986
  const agent = baseAgentName(rawAgent);
1611
- const taskDir = path9.join(process.cwd(), "exe", agent);
1612
- if (existsSync9(taskDir)) {
1613
- const files = readdirSync(taskDir).filter(
1987
+ const taskDir = path10.join(process.cwd(), "exe", agent);
1988
+ if (existsSync10(taskDir)) {
1989
+ const files = readdirSync2(taskDir).filter(
1614
1990
  (f) => f.endsWith(".md") && f !== "DONE.txt"
1615
1991
  );
1616
1992
  if (files.length === 0) {
@@ -1674,6 +2050,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
1674
2050
  try {
1675
2051
  const sessions = transport.listSessions();
1676
2052
  if (!sessions.includes(coordinatorSession)) return false;
2053
+ try {
2054
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
2055
+ const pending = countPendingReviews2(coordinatorSession);
2056
+ if (pending instanceof Promise) {
2057
+ pending.then((count) => {
2058
+ if (count > 0) {
2059
+ execSync4(
2060
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
2061
+ { timeout: 3e3 }
2062
+ );
2063
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
2064
+ }
2065
+ }).catch(() => {
2066
+ });
2067
+ return true;
2068
+ }
2069
+ } catch {
2070
+ }
1677
2071
  execSync4(
1678
2072
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
1679
2073
  { timeout: 3e3 }
@@ -1757,23 +2151,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1757
2151
  const transport = getTransport();
1758
2152
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1759
2153
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1760
- const logDir = path9.join(os7.homedir(), ".exe-os", "session-logs");
1761
- const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1762
- if (!existsSync9(logDir)) {
2154
+ const logDir = path10.join(os7.homedir(), ".exe-os", "session-logs");
2155
+ const logFile = path10.join(logDir, `${instanceLabel}-${Date.now()}.log`);
2156
+ if (!existsSync10(logDir)) {
1763
2157
  mkdirSync5(logDir, { recursive: true });
1764
2158
  }
1765
2159
  transport.kill(sessionName);
1766
2160
  let cleanupSuffix = "";
1767
2161
  try {
1768
2162
  const thisFile = fileURLToPath(import.meta.url);
1769
- const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1770
- if (existsSync9(cleanupScript)) {
2163
+ const cleanupScript = path10.join(path10.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
2164
+ if (existsSync10(cleanupScript)) {
1771
2165
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
1772
2166
  }
1773
2167
  } catch {
1774
2168
  }
1775
2169
  try {
1776
- const claudeJsonPath = path9.join(os7.homedir(), ".claude.json");
2170
+ const claudeJsonPath = path10.join(os7.homedir(), ".claude.json");
1777
2171
  let claudeJson = {};
1778
2172
  try {
1779
2173
  claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
@@ -1788,10 +2182,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1788
2182
  } catch {
1789
2183
  }
1790
2184
  try {
1791
- const settingsDir = path9.join(os7.homedir(), ".claude", "projects");
2185
+ const settingsDir = path10.join(os7.homedir(), ".claude", "projects");
1792
2186
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1793
- const projSettingsDir = path9.join(settingsDir, normalizedKey);
1794
- const settingsPath = path9.join(projSettingsDir, "settings.json");
2187
+ const projSettingsDir = path10.join(settingsDir, normalizedKey);
2188
+ const settingsPath = path10.join(projSettingsDir, "settings.json");
1795
2189
  let settings = {};
1796
2190
  try {
1797
2191
  settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
@@ -1838,7 +2232,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1838
2232
  let behaviorsFlag = "";
1839
2233
  let legacyFallbackWarned = false;
1840
2234
  if (!useExeAgent && !useBinSymlink) {
1841
- const identityPath = path9.join(
2235
+ const identityPath = path10.join(
1842
2236
  os7.homedir(),
1843
2237
  ".exe-os",
1844
2238
  "identity",
@@ -1848,13 +2242,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1848
2242
  const hasAgentFlag = claudeSupportsAgentFlag();
1849
2243
  if (hasAgentFlag) {
1850
2244
  identityFlag = ` --agent ${employeeName}`;
1851
- } else if (existsSync9(identityPath)) {
2245
+ } else if (existsSync10(identityPath)) {
1852
2246
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
1853
2247
  legacyFallbackWarned = true;
1854
2248
  }
1855
2249
  const behaviorsFile = exportBehaviorsSync(
1856
2250
  employeeName,
1857
- path9.basename(spawnCwd),
2251
+ path10.basename(spawnCwd),
1858
2252
  sessionName
1859
2253
  );
1860
2254
  if (behaviorsFile) {
@@ -1869,9 +2263,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1869
2263
  }
1870
2264
  let sessionContextFlag = "";
1871
2265
  try {
1872
- const ctxDir = path9.join(os7.homedir(), ".exe-os", "session-cache");
2266
+ const ctxDir = path10.join(os7.homedir(), ".exe-os", "session-cache");
1873
2267
  mkdirSync5(ctxDir, { recursive: true });
1874
- const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
2268
+ const ctxFile = path10.join(ctxDir, `session-context-${sessionName}.md`);
1875
2269
  const ctxContent = [
1876
2270
  `## Session Context`,
1877
2271
  `You are running in tmux session: ${sessionName}.`,
@@ -1955,7 +2349,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1955
2349
  transport.pipeLog(sessionName, logFile);
1956
2350
  try {
1957
2351
  const mySession = getMySession();
1958
- const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
2352
+ const dispatchInfo = path10.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1959
2353
  writeFileSync6(dispatchInfo, JSON.stringify({
1960
2354
  dispatchedBy: mySession,
1961
2355
  rootExe: exeSession,
@@ -2030,15 +2424,15 @@ var init_tmux_routing = __esm({
2030
2424
  init_intercom_queue();
2031
2425
  init_plan_limits();
2032
2426
  init_employees();
2033
- SPAWN_LOCK_DIR = path9.join(os7.homedir(), ".exe-os", "spawn-locks");
2034
- SESSION_CACHE = path9.join(os7.homedir(), ".exe-os", "session-cache");
2427
+ SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
2428
+ SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
2035
2429
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2036
2430
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
2037
2431
  VERIFY_PANE_LINES = 200;
2038
2432
  INTERCOM_DEBOUNCE_MS = 3e4;
2039
2433
  CODEX_DEBOUNCE_MS = 12e4;
2040
- INTERCOM_LOG2 = path9.join(os7.homedir(), ".exe-os", "intercom.log");
2041
- DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
2434
+ INTERCOM_LOG2 = path10.join(os7.homedir(), ".exe-os", "intercom.log");
2435
+ DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
2042
2436
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
2043
2437
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
2044
2438
  }
@@ -2079,13 +2473,13 @@ var init_task_scope = __esm({
2079
2473
 
2080
2474
  // src/lib/notifications.ts
2081
2475
  import crypto2 from "crypto";
2082
- import path10 from "path";
2476
+ import path11 from "path";
2083
2477
  import os8 from "os";
2084
2478
  import {
2085
2479
  readFileSync as readFileSync9,
2086
- readdirSync as readdirSync2,
2087
- unlinkSync as unlinkSync3,
2088
- existsSync as existsSync10,
2480
+ readdirSync as readdirSync3,
2481
+ unlinkSync as unlinkSync4,
2482
+ existsSync as existsSync11,
2089
2483
  rmdirSync
2090
2484
  } from "fs";
2091
2485
  async function writeNotification(notification) {
@@ -2134,64 +2528,9 @@ var init_notifications = __esm({
2134
2528
  }
2135
2529
  });
2136
2530
 
2137
- // src/lib/state-bus.ts
2138
- var StateBus, orgBus;
2139
- var init_state_bus = __esm({
2140
- "src/lib/state-bus.ts"() {
2141
- "use strict";
2142
- StateBus = class {
2143
- handlers = /* @__PURE__ */ new Map();
2144
- globalHandlers = /* @__PURE__ */ new Set();
2145
- /** Emit an event to all subscribers */
2146
- emit(event) {
2147
- const typeHandlers = this.handlers.get(event.type);
2148
- if (typeHandlers) {
2149
- for (const handler of typeHandlers) {
2150
- try {
2151
- handler(event);
2152
- } catch {
2153
- }
2154
- }
2155
- }
2156
- for (const handler of this.globalHandlers) {
2157
- try {
2158
- handler(event);
2159
- } catch {
2160
- }
2161
- }
2162
- }
2163
- /** Subscribe to a specific event type */
2164
- on(type, handler) {
2165
- if (!this.handlers.has(type)) {
2166
- this.handlers.set(type, /* @__PURE__ */ new Set());
2167
- }
2168
- this.handlers.get(type).add(handler);
2169
- }
2170
- /** Subscribe to ALL events */
2171
- onAny(handler) {
2172
- this.globalHandlers.add(handler);
2173
- }
2174
- /** Unsubscribe from a specific event type */
2175
- off(type, handler) {
2176
- this.handlers.get(type)?.delete(handler);
2177
- }
2178
- /** Unsubscribe from ALL events */
2179
- offAny(handler) {
2180
- this.globalHandlers.delete(handler);
2181
- }
2182
- /** Remove all listeners */
2183
- clear() {
2184
- this.handlers.clear();
2185
- this.globalHandlers.clear();
2186
- }
2187
- };
2188
- orgBus = new StateBus();
2189
- }
2190
- });
2191
-
2192
2531
  // src/lib/project-name.ts
2193
2532
  import { execSync as execSync5 } from "child_process";
2194
- import path11 from "path";
2533
+ import path12 from "path";
2195
2534
  function getProjectName(cwd) {
2196
2535
  const dir = cwd ?? process.cwd();
2197
2536
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2204,7 +2543,7 @@ function getProjectName(cwd) {
2204
2543
  timeout: 2e3,
2205
2544
  stdio: ["pipe", "pipe", "pipe"]
2206
2545
  }).trim();
2207
- repoRoot = path11.dirname(gitCommonDir);
2546
+ repoRoot = path12.dirname(gitCommonDir);
2208
2547
  } catch {
2209
2548
  repoRoot = execSync5("git rev-parse --show-toplevel", {
2210
2549
  cwd: dir,
@@ -2213,11 +2552,11 @@ function getProjectName(cwd) {
2213
2552
  stdio: ["pipe", "pipe", "pipe"]
2214
2553
  }).trim();
2215
2554
  }
2216
- _cached2 = path11.basename(repoRoot);
2555
+ _cached2 = path12.basename(repoRoot);
2217
2556
  _cachedCwd = dir;
2218
2557
  return _cached2;
2219
2558
  } catch {
2220
- _cached2 = path11.basename(dir);
2559
+ _cached2 = path12.basename(dir);
2221
2560
  _cachedCwd = dir;
2222
2561
  return _cached2;
2223
2562
  }
@@ -2295,11 +2634,11 @@ var init_session_scope = __esm({
2295
2634
 
2296
2635
  // src/lib/tasks-crud.ts
2297
2636
  import crypto3 from "crypto";
2298
- import path12 from "path";
2637
+ import path13 from "path";
2299
2638
  import os9 from "os";
2300
2639
  import { execSync as execSync6 } from "child_process";
2301
2640
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2302
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2641
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
2303
2642
  async function writeCheckpoint(input) {
2304
2643
  const client = getClient();
2305
2644
  const row = await resolveTask(client, input.taskId);
@@ -2493,8 +2832,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
2493
2832
  }
2494
2833
  if (input.baseDir) {
2495
2834
  try {
2496
- await mkdir3(path12.join(input.baseDir, "exe", "output"), { recursive: true });
2497
- await mkdir3(path12.join(input.baseDir, "exe", "research"), { recursive: true });
2835
+ await mkdir3(path13.join(input.baseDir, "exe", "output"), { recursive: true });
2836
+ await mkdir3(path13.join(input.baseDir, "exe", "research"), { recursive: true });
2498
2837
  await ensureArchitectureDoc(input.baseDir, input.projectName);
2499
2838
  await ensureGitignoreExe(input.baseDir);
2500
2839
  } catch {
@@ -2530,10 +2869,10 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
2530
2869
  });
2531
2870
  if (input.baseDir) {
2532
2871
  try {
2533
- const EXE_OS_DIR = path12.join(os9.homedir(), ".exe-os");
2534
- const mdPath = path12.join(EXE_OS_DIR, taskFile);
2535
- const mdDir = path12.dirname(mdPath);
2536
- if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
2872
+ const EXE_OS_DIR = path13.join(os9.homedir(), ".exe-os");
2873
+ const mdPath = path13.join(EXE_OS_DIR, taskFile);
2874
+ const mdDir = path13.dirname(mdPath);
2875
+ if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
2537
2876
  const reviewer = input.reviewer ?? input.assignedBy;
2538
2877
  const mdContent = `# ${input.title}
2539
2878
 
@@ -2833,9 +3172,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2833
3172
  return { taskFile, assignedTo, assignedBy, taskSlug };
2834
3173
  }
2835
3174
  async function ensureArchitectureDoc(baseDir, projectName) {
2836
- const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
3175
+ const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
2837
3176
  try {
2838
- if (existsSync11(archPath)) return;
3177
+ if (existsSync12(archPath)) return;
2839
3178
  const template = [
2840
3179
  `# ${projectName} \u2014 System Architecture`,
2841
3180
  "",
@@ -2868,9 +3207,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2868
3207
  }
2869
3208
  }
2870
3209
  async function ensureGitignoreExe(baseDir) {
2871
- const gitignorePath = path12.join(baseDir, ".gitignore");
3210
+ const gitignorePath = path13.join(baseDir, ".gitignore");
2872
3211
  try {
2873
- if (existsSync11(gitignorePath)) {
3212
+ if (existsSync12(gitignorePath)) {
2874
3213
  const content = readFileSync10(gitignorePath, "utf-8");
2875
3214
  if (/^\/?exe\/?$/m.test(content)) return;
2876
3215
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -2901,198 +3240,6 @@ var init_tasks_crud = __esm({
2901
3240
  }
2902
3241
  });
2903
3242
 
2904
- // src/lib/tasks-review.ts
2905
- import path13 from "path";
2906
- import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2907
- async function countPendingReviews(sessionScope) {
2908
- const client = getClient();
2909
- const scope = strictSessionScopeFilter(
2910
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2911
- );
2912
- const result = await client.execute({
2913
- sql: `SELECT COUNT(*) as cnt FROM tasks
2914
- WHERE status = 'needs_review'${scope.sql}`,
2915
- args: [...scope.args]
2916
- });
2917
- return Number(result.rows[0]?.cnt) || 0;
2918
- }
2919
- async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2920
- const client = getClient();
2921
- const scope = strictSessionScopeFilter(
2922
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2923
- );
2924
- const result = await client.execute({
2925
- sql: `SELECT COUNT(*) as cnt FROM tasks
2926
- WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
2927
- args: [sinceIso, ...scope.args]
2928
- });
2929
- return Number(result.rows[0]?.cnt) || 0;
2930
- }
2931
- async function listPendingReviews(limit, sessionScope) {
2932
- const client = getClient();
2933
- const scope = strictSessionScopeFilter(
2934
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2935
- );
2936
- const result = await client.execute({
2937
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2938
- WHERE status = 'needs_review'${scope.sql}
2939
- ORDER BY updated_at ASC LIMIT ?`,
2940
- args: [...scope.args, limit]
2941
- });
2942
- return result.rows;
2943
- }
2944
- async function cleanupOrphanedReviews() {
2945
- const client = getClient();
2946
- const now = (/* @__PURE__ */ new Date()).toISOString();
2947
- const r1 = await client.execute({
2948
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
2949
- WHERE status IN ('open', 'needs_review', 'in_progress')
2950
- AND assigned_by = 'system'
2951
- AND title LIKE 'Review:%'
2952
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
2953
- args: [now]
2954
- });
2955
- const r1b = await client.execute({
2956
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
2957
- WHERE status IN ('open', 'needs_review')
2958
- AND title LIKE 'Review:%completed%'
2959
- AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
2960
- args: [now]
2961
- });
2962
- const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
2963
- const r2 = await client.execute({
2964
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
2965
- WHERE status = 'needs_review'
2966
- AND result IS NOT NULL
2967
- AND updated_at < ?`,
2968
- args: [now, staleThreshold]
2969
- });
2970
- const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
2971
- if (total > 0) {
2972
- process.stderr.write(
2973
- `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
2974
- `
2975
- );
2976
- }
2977
- return total;
2978
- }
2979
- function getReviewChecklist(role, agent, taskSlug) {
2980
- const roleLower = role.toLowerCase();
2981
- if (roleLower.includes("engineer") || roleLower === "principal engineer") {
2982
- return {
2983
- lens: "Code Quality (Engineer)",
2984
- checklist: [
2985
- "1. Do all tests pass? Any new tests needed?",
2986
- "2. Is the code clean \u2014 no dead code, no TODOs left?",
2987
- "3. Does it follow existing patterns and conventions in the codebase?",
2988
- "4. Any regressions in the test suite?"
2989
- ]
2990
- };
2991
- }
2992
- if (roleLower === "cto" || roleLower.includes("architect")) {
2993
- return {
2994
- lens: "Architecture (CTO)",
2995
- checklist: [
2996
- "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
2997
- "2. Is it backward compatible? Any breaking changes?",
2998
- "3. Does it introduce technical debt? Is that debt justified?",
2999
- "4. Security implications? Any new attack surface?",
3000
- "5. Does it scale? Performance considerations?",
3001
- "6. Coordination: does this affect other employees' work or other projects?"
3002
- ]
3003
- };
3004
- }
3005
- if (roleLower === "coo" || roleLower.includes("operations")) {
3006
- return {
3007
- lens: "Strategic (COO)",
3008
- checklist: [
3009
- "1. Does this serve the project mission?",
3010
- "2. Is this the right work at the right time?",
3011
- "3. Does the architectural assessment make sense for the business?",
3012
- "4. Any cross-project implications?"
3013
- ]
3014
- };
3015
- }
3016
- return {
3017
- lens: "General",
3018
- checklist: [
3019
- "1. Read the original task's acceptance criteria",
3020
- `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
3021
- "3. Verify code changes match requirements",
3022
- "4. Check if tests were added/updated",
3023
- `5. Look for output files in exe/output/${agent}-${taskSlug}*`
3024
- ]
3025
- };
3026
- }
3027
- async function cleanupReviewFile(row, taskFile, _baseDir) {
3028
- if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3029
- try {
3030
- const client = getClient();
3031
- const now = (/* @__PURE__ */ new Date()).toISOString();
3032
- const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3033
- if (parentId) {
3034
- const result = await client.execute({
3035
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3036
- args: [now, parentId]
3037
- });
3038
- if (result.rowsAffected > 0) {
3039
- process.stderr.write(
3040
- `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3041
- `
3042
- );
3043
- }
3044
- } else {
3045
- const fileName = taskFile.split("/").pop() ?? "";
3046
- const reviewPrefix = fileName.replace(".md", "");
3047
- const parts = reviewPrefix.split("-");
3048
- if (parts.length >= 3 && parts[0] === "review") {
3049
- const agent = parts[1];
3050
- const slug = parts.slice(2).join("-");
3051
- const legacyTaskFile = `exe/${agent}/${slug}.md`;
3052
- const result = await client.execute({
3053
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
3054
- args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
3055
- });
3056
- if (result.rowsAffected > 0) {
3057
- process.stderr.write(
3058
- `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
3059
- `
3060
- );
3061
- }
3062
- }
3063
- }
3064
- } catch (err) {
3065
- process.stderr.write(
3066
- `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
3067
- `
3068
- );
3069
- }
3070
- try {
3071
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3072
- if (existsSync12(cacheDir)) {
3073
- for (const f of readdirSync3(cacheDir)) {
3074
- if (f.startsWith("review-notified-")) {
3075
- unlinkSync4(path13.join(cacheDir, f));
3076
- }
3077
- }
3078
- }
3079
- } catch {
3080
- }
3081
- }
3082
- var init_tasks_review = __esm({
3083
- "src/lib/tasks-review.ts"() {
3084
- "use strict";
3085
- init_database();
3086
- init_config();
3087
- init_employees();
3088
- init_notifications();
3089
- init_tmux_routing();
3090
- init_session_key();
3091
- init_state_bus();
3092
- init_task_scope();
3093
- }
3094
- });
3095
-
3096
3243
  // src/lib/tasks-chain.ts
3097
3244
  import path14 from "path";
3098
3245
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";