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