@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.
@@ -1562,6 +1562,369 @@ var init_capacity_monitor = __esm({
1562
1562
  }
1563
1563
  });
1564
1564
 
1565
+ // src/lib/state-bus.ts
1566
+ var StateBus, orgBus;
1567
+ var init_state_bus = __esm({
1568
+ "src/lib/state-bus.ts"() {
1569
+ "use strict";
1570
+ StateBus = class {
1571
+ handlers = /* @__PURE__ */ new Map();
1572
+ globalHandlers = /* @__PURE__ */ new Set();
1573
+ /** Emit an event to all subscribers */
1574
+ emit(event) {
1575
+ const typeHandlers = this.handlers.get(event.type);
1576
+ if (typeHandlers) {
1577
+ for (const handler of typeHandlers) {
1578
+ try {
1579
+ handler(event);
1580
+ } catch {
1581
+ }
1582
+ }
1583
+ }
1584
+ for (const handler of this.globalHandlers) {
1585
+ try {
1586
+ handler(event);
1587
+ } catch {
1588
+ }
1589
+ }
1590
+ }
1591
+ /** Subscribe to a specific event type */
1592
+ on(type, handler) {
1593
+ if (!this.handlers.has(type)) {
1594
+ this.handlers.set(type, /* @__PURE__ */ new Set());
1595
+ }
1596
+ this.handlers.get(type).add(handler);
1597
+ }
1598
+ /** Subscribe to ALL events */
1599
+ onAny(handler) {
1600
+ this.globalHandlers.add(handler);
1601
+ }
1602
+ /** Unsubscribe from a specific event type */
1603
+ off(type, handler) {
1604
+ this.handlers.get(type)?.delete(handler);
1605
+ }
1606
+ /** Unsubscribe from ALL events */
1607
+ offAny(handler) {
1608
+ this.globalHandlers.delete(handler);
1609
+ }
1610
+ /** Remove all listeners */
1611
+ clear() {
1612
+ this.handlers.clear();
1613
+ this.globalHandlers.clear();
1614
+ }
1615
+ };
1616
+ orgBus = new StateBus();
1617
+ }
1618
+ });
1619
+
1620
+ // src/lib/tasks-review.ts
1621
+ var tasks_review_exports = {};
1622
+ __export(tasks_review_exports, {
1623
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
1624
+ cleanupReviewFile: () => cleanupReviewFile,
1625
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
1626
+ countPendingReviews: () => countPendingReviews,
1627
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
1628
+ formatAge: () => formatAge,
1629
+ getReviewChecklist: () => getReviewChecklist,
1630
+ isStale: () => isStale,
1631
+ listPendingReviews: () => listPendingReviews
1632
+ });
1633
+ import path9 from "path";
1634
+ import { existsSync as existsSync9, readdirSync, unlinkSync as unlinkSync2 } from "fs";
1635
+ function formatAge(isoTimestamp) {
1636
+ if (!isoTimestamp) return "";
1637
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
1638
+ if (ms < 0) return "just now";
1639
+ const minutes = Math.floor(ms / 6e4);
1640
+ if (minutes < 60) return `${minutes}m ago`;
1641
+ const hours = Math.floor(minutes / 60);
1642
+ if (hours < 24) return `${hours}h ago`;
1643
+ const days = Math.floor(hours / 24);
1644
+ return `${days}d ago`;
1645
+ }
1646
+ function isStale(isoTimestamp) {
1647
+ if (!isoTimestamp) return false;
1648
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
1649
+ }
1650
+ async function countPendingReviews(sessionScope) {
1651
+ const client = getClient();
1652
+ const scope = strictSessionScopeFilter(
1653
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1654
+ );
1655
+ const result = await client.execute({
1656
+ sql: `SELECT COUNT(*) as cnt FROM tasks
1657
+ WHERE status = 'needs_review'${scope.sql}`,
1658
+ args: [...scope.args]
1659
+ });
1660
+ return Number(result.rows[0]?.cnt) || 0;
1661
+ }
1662
+ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
1663
+ const client = getClient();
1664
+ const scope = strictSessionScopeFilter(
1665
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1666
+ );
1667
+ const result = await client.execute({
1668
+ sql: `SELECT COUNT(*) as cnt FROM tasks
1669
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
1670
+ args: [sinceIso, ...scope.args]
1671
+ });
1672
+ return Number(result.rows[0]?.cnt) || 0;
1673
+ }
1674
+ async function listPendingReviews(limit, sessionScope) {
1675
+ const client = getClient();
1676
+ const scope = strictSessionScopeFilter(
1677
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1678
+ );
1679
+ const result = await client.execute({
1680
+ sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
1681
+ WHERE status = 'needs_review'${scope.sql}
1682
+ ORDER BY updated_at ASC LIMIT ?`,
1683
+ args: [...scope.args, limit]
1684
+ });
1685
+ return result.rows;
1686
+ }
1687
+ async function cleanupOrphanedReviews() {
1688
+ const client = getClient();
1689
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1690
+ const r1 = await client.execute({
1691
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
1692
+ WHERE status IN ('open', 'needs_review', 'in_progress')
1693
+ AND assigned_by = 'system'
1694
+ AND title LIKE 'Review:%'
1695
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
1696
+ args: [now]
1697
+ });
1698
+ const r1b = await client.execute({
1699
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
1700
+ WHERE status IN ('open', 'needs_review')
1701
+ AND title LIKE 'Review:%completed%'
1702
+ 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')))`,
1703
+ args: [now]
1704
+ });
1705
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
1706
+ const r2 = await client.execute({
1707
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
1708
+ WHERE status = 'needs_review'
1709
+ AND result IS NOT NULL
1710
+ AND updated_at < ?`,
1711
+ args: [now, staleThreshold]
1712
+ });
1713
+ const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
1714
+ if (total > 0) {
1715
+ process.stderr.write(
1716
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
1717
+ `
1718
+ );
1719
+ }
1720
+ return total;
1721
+ }
1722
+ function getReviewChecklist(role, agent, taskSlug) {
1723
+ const roleLower = role.toLowerCase();
1724
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
1725
+ return {
1726
+ lens: "Code Quality (Engineer)",
1727
+ checklist: [
1728
+ "1. Do all tests pass? Any new tests needed?",
1729
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
1730
+ "3. Does it follow existing patterns and conventions in the codebase?",
1731
+ "4. Any regressions in the test suite?"
1732
+ ]
1733
+ };
1734
+ }
1735
+ if (roleLower === "cto" || roleLower.includes("architect")) {
1736
+ return {
1737
+ lens: "Architecture (CTO)",
1738
+ checklist: [
1739
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
1740
+ "2. Is it backward compatible? Any breaking changes?",
1741
+ "3. Does it introduce technical debt? Is that debt justified?",
1742
+ "4. Security implications? Any new attack surface?",
1743
+ "5. Does it scale? Performance considerations?",
1744
+ "6. Coordination: does this affect other employees' work or other projects?"
1745
+ ]
1746
+ };
1747
+ }
1748
+ if (roleLower === "coo" || roleLower.includes("operations")) {
1749
+ return {
1750
+ lens: "Strategic (COO)",
1751
+ checklist: [
1752
+ "1. Does this serve the project mission?",
1753
+ "2. Is this the right work at the right time?",
1754
+ "3. Does the architectural assessment make sense for the business?",
1755
+ "4. Any cross-project implications?"
1756
+ ]
1757
+ };
1758
+ }
1759
+ return {
1760
+ lens: "General",
1761
+ checklist: [
1762
+ "1. Read the original task's acceptance criteria",
1763
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
1764
+ "3. Verify code changes match requirements",
1765
+ "4. Check if tests were added/updated",
1766
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
1767
+ ]
1768
+ };
1769
+ }
1770
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
1771
+ const taskFile = String(row.task_file);
1772
+ const employees = await loadEmployees();
1773
+ const coordinatorName = getCoordinatorName(employees);
1774
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
1775
+ if (String(row.title).startsWith("Review:")) return;
1776
+ const fileName = taskFile.split("/").pop() ?? "";
1777
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
1778
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
1779
+ const client = getClient();
1780
+ const agent = String(row.assigned_to);
1781
+ const rawReviewer = row.reviewer || row.assigned_by;
1782
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
1783
+ const currentStatus = String(row.status ?? "");
1784
+ if (currentStatus === "done") return;
1785
+ const existingResult = String(row.result ?? "");
1786
+ if (existingResult.includes("## Review notes")) return;
1787
+ let reviewerRole = "unknown";
1788
+ try {
1789
+ const emp = getEmployee(employees, reviewer);
1790
+ if (emp) reviewerRole = emp.role;
1791
+ } catch {
1792
+ }
1793
+ const taskTitle = String(row.title);
1794
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
1795
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
1796
+ process.stderr.write(
1797
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
1798
+ `
1799
+ );
1800
+ const reviewNotes = [
1801
+ `
1802
+ ---
1803
+ ## Review notes`,
1804
+ `Review lens: ${lens}`,
1805
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
1806
+ "",
1807
+ "### Checklist",
1808
+ ...checklist,
1809
+ "",
1810
+ "### Verdict",
1811
+ "- **Approved:** mark this task as done",
1812
+ "- **Needs work:** re-open with notes"
1813
+ ].join("\n");
1814
+ const originalTaskId = String(row.id);
1815
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
1816
+ await client.execute({
1817
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
1818
+ WHERE id = ?`,
1819
+ args: [updatedResult, now, originalTaskId]
1820
+ });
1821
+ orgBus.emit({
1822
+ type: "review_created",
1823
+ reviewId: originalTaskId,
1824
+ employee: agent,
1825
+ reviewer,
1826
+ timestamp: now
1827
+ });
1828
+ await writeNotification({
1829
+ agentId: agent,
1830
+ agentRole: String(row.assigned_to),
1831
+ event: "task_complete",
1832
+ project: String(row.project_name),
1833
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
1834
+ taskFile
1835
+ });
1836
+ const originalPriority = String(row.priority).toLowerCase();
1837
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
1838
+ if (!autoApprove) {
1839
+ try {
1840
+ const key = getSessionKey();
1841
+ const exeSession = getParentExe(key);
1842
+ if (exeSession) {
1843
+ sendIntercom(exeSession);
1844
+ }
1845
+ } catch {
1846
+ }
1847
+ }
1848
+ if (autoApprove) {
1849
+ process.stderr.write(
1850
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
1851
+ `
1852
+ );
1853
+ await client.execute({
1854
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
1855
+ args: [now, originalTaskId]
1856
+ });
1857
+ }
1858
+ }
1859
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
1860
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
1861
+ try {
1862
+ const client = getClient();
1863
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1864
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
1865
+ if (parentId) {
1866
+ const result = await client.execute({
1867
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
1868
+ args: [now, parentId]
1869
+ });
1870
+ if (result.rowsAffected > 0) {
1871
+ process.stderr.write(
1872
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
1873
+ `
1874
+ );
1875
+ }
1876
+ } else {
1877
+ const fileName = taskFile.split("/").pop() ?? "";
1878
+ const reviewPrefix = fileName.replace(".md", "");
1879
+ const parts = reviewPrefix.split("-");
1880
+ if (parts.length >= 3 && parts[0] === "review") {
1881
+ const agent = parts[1];
1882
+ const slug = parts.slice(2).join("-");
1883
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
1884
+ const result = await client.execute({
1885
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
1886
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
1887
+ });
1888
+ if (result.rowsAffected > 0) {
1889
+ process.stderr.write(
1890
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
1891
+ `
1892
+ );
1893
+ }
1894
+ }
1895
+ }
1896
+ } catch (err) {
1897
+ process.stderr.write(
1898
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
1899
+ `
1900
+ );
1901
+ }
1902
+ try {
1903
+ const cacheDir = path9.join(EXE_AI_DIR, "session-cache");
1904
+ if (existsSync9(cacheDir)) {
1905
+ for (const f of readdirSync(cacheDir)) {
1906
+ if (f.startsWith("review-notified-")) {
1907
+ unlinkSync2(path9.join(cacheDir, f));
1908
+ }
1909
+ }
1910
+ }
1911
+ } catch {
1912
+ }
1913
+ }
1914
+ var init_tasks_review = __esm({
1915
+ "src/lib/tasks-review.ts"() {
1916
+ "use strict";
1917
+ init_database();
1918
+ init_config();
1919
+ init_employees();
1920
+ init_notifications();
1921
+ init_tmux_routing();
1922
+ init_session_key();
1923
+ init_state_bus();
1924
+ init_task_scope();
1925
+ }
1926
+ });
1927
+
1565
1928
  // src/lib/tmux-routing.ts
1566
1929
  var tmux_routing_exports = {};
1567
1930
  __export(tmux_routing_exports, {
@@ -1588,13 +1951,13 @@ __export(tmux_routing_exports, {
1588
1951
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
1589
1952
  });
1590
1953
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1591
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync } from "fs";
1592
- import path9 from "path";
1954
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync, readdirSync as readdirSync2 } from "fs";
1955
+ import path10 from "path";
1593
1956
  import os7 from "os";
1594
1957
  import { fileURLToPath } from "url";
1595
- import { unlinkSync as unlinkSync2 } from "fs";
1958
+ import { unlinkSync as unlinkSync3 } from "fs";
1596
1959
  function spawnLockPath(sessionName) {
1597
- return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1960
+ return path10.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1598
1961
  }
1599
1962
  function isProcessAlive(pid) {
1600
1963
  try {
@@ -1605,11 +1968,11 @@ function isProcessAlive(pid) {
1605
1968
  }
1606
1969
  }
1607
1970
  function acquireSpawnLock(sessionName) {
1608
- if (!existsSync9(SPAWN_LOCK_DIR)) {
1971
+ if (!existsSync10(SPAWN_LOCK_DIR)) {
1609
1972
  mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
1610
1973
  }
1611
1974
  const lockFile = spawnLockPath(sessionName);
1612
- if (existsSync9(lockFile)) {
1975
+ if (existsSync10(lockFile)) {
1613
1976
  try {
1614
1977
  const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1615
1978
  const age = Date.now() - lock.timestamp;
@@ -1624,20 +1987,20 @@ function acquireSpawnLock(sessionName) {
1624
1987
  }
1625
1988
  function releaseSpawnLock(sessionName) {
1626
1989
  try {
1627
- unlinkSync2(spawnLockPath(sessionName));
1990
+ unlinkSync3(spawnLockPath(sessionName));
1628
1991
  } catch {
1629
1992
  }
1630
1993
  }
1631
1994
  function resolveBehaviorsExporterScript() {
1632
1995
  try {
1633
1996
  const thisFile = fileURLToPath(import.meta.url);
1634
- const scriptPath = path9.join(
1635
- path9.dirname(thisFile),
1997
+ const scriptPath = path10.join(
1998
+ path10.dirname(thisFile),
1636
1999
  "..",
1637
2000
  "bin",
1638
2001
  "exe-export-behaviors.js"
1639
2002
  );
1640
- return existsSync9(scriptPath) ? scriptPath : null;
2003
+ return existsSync10(scriptPath) ? scriptPath : null;
1641
2004
  } catch {
1642
2005
  return null;
1643
2006
  }
@@ -1703,11 +2066,11 @@ function extractRootExe(name) {
1703
2066
  return parts.length > 0 ? parts[parts.length - 1] : null;
1704
2067
  }
1705
2068
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1706
- if (!existsSync9(SESSION_CACHE)) {
2069
+ if (!existsSync10(SESSION_CACHE)) {
1707
2070
  mkdirSync5(SESSION_CACHE, { recursive: true });
1708
2071
  }
1709
2072
  const rootExe = extractRootExe(parentExe) ?? parentExe;
1710
- const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
2073
+ const filePath = path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1711
2074
  writeFileSync6(filePath, JSON.stringify({
1712
2075
  parentExe: rootExe,
1713
2076
  dispatchedBy: dispatchedBy || rootExe,
@@ -1716,7 +2079,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1716
2079
  }
1717
2080
  function getParentExe(sessionKey) {
1718
2081
  try {
1719
- const data = JSON.parse(readFileSync8(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
2082
+ const data = JSON.parse(readFileSync8(path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1720
2083
  return data.parentExe || null;
1721
2084
  } catch {
1722
2085
  return null;
@@ -1725,7 +2088,7 @@ function getParentExe(sessionKey) {
1725
2088
  function getDispatchedBy(sessionKey) {
1726
2089
  try {
1727
2090
  const data = JSON.parse(readFileSync8(
1728
- path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
2091
+ path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1729
2092
  "utf8"
1730
2093
  ));
1731
2094
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -1795,7 +2158,7 @@ async function verifyPaneAtCapacity(sessionName) {
1795
2158
  }
1796
2159
  function readDebounceState() {
1797
2160
  try {
1798
- if (!existsSync9(DEBOUNCE_FILE)) return {};
2161
+ if (!existsSync10(DEBOUNCE_FILE)) return {};
1799
2162
  const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1800
2163
  const state = {};
1801
2164
  for (const [key, val] of Object.entries(raw)) {
@@ -1812,7 +2175,7 @@ function readDebounceState() {
1812
2175
  }
1813
2176
  function writeDebounceState(state) {
1814
2177
  try {
1815
- if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
2178
+ if (!existsSync10(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
1816
2179
  writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
1817
2180
  } catch {
1818
2181
  }
@@ -1908,22 +2271,24 @@ function sendIntercom(targetSession) {
1908
2271
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
1909
2272
  return "queued";
1910
2273
  }
1911
- try {
1912
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
1913
- const agent = baseAgentName(rawAgent);
1914
- const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
1915
- if (existsSync9(markerPath)) {
1916
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
1917
- return "debounced";
2274
+ if (sessionState !== "idle") {
2275
+ try {
2276
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
2277
+ const agent = baseAgentName(rawAgent);
2278
+ const markerPath = path10.join(SESSION_CACHE, `current-task-${agent}.json`);
2279
+ if (existsSync10(markerPath)) {
2280
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
2281
+ return "debounced";
2282
+ }
2283
+ } catch {
1918
2284
  }
1919
- } catch {
1920
2285
  }
1921
2286
  try {
1922
2287
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
1923
2288
  const agent = baseAgentName(rawAgent);
1924
- const taskDir = path9.join(process.cwd(), "exe", agent);
1925
- if (existsSync9(taskDir)) {
1926
- const files = readdirSync(taskDir).filter(
2289
+ const taskDir = path10.join(process.cwd(), "exe", agent);
2290
+ if (existsSync10(taskDir)) {
2291
+ const files = readdirSync2(taskDir).filter(
1927
2292
  (f) => f.endsWith(".md") && f !== "DONE.txt"
1928
2293
  );
1929
2294
  if (files.length === 0) {
@@ -1987,6 +2352,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
1987
2352
  try {
1988
2353
  const sessions = transport.listSessions();
1989
2354
  if (!sessions.includes(coordinatorSession)) return false;
2355
+ try {
2356
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
2357
+ const pending = countPendingReviews2(coordinatorSession);
2358
+ if (pending instanceof Promise) {
2359
+ pending.then((count) => {
2360
+ if (count > 0) {
2361
+ execSync4(
2362
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
2363
+ { timeout: 3e3 }
2364
+ );
2365
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
2366
+ }
2367
+ }).catch(() => {
2368
+ });
2369
+ return true;
2370
+ }
2371
+ } catch {
2372
+ }
1990
2373
  execSync4(
1991
2374
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
1992
2375
  { timeout: 3e3 }
@@ -2070,23 +2453,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2070
2453
  const transport = getTransport();
2071
2454
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
2072
2455
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
2073
- const logDir = path9.join(os7.homedir(), ".exe-os", "session-logs");
2074
- const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
2075
- if (!existsSync9(logDir)) {
2456
+ const logDir = path10.join(os7.homedir(), ".exe-os", "session-logs");
2457
+ const logFile = path10.join(logDir, `${instanceLabel}-${Date.now()}.log`);
2458
+ if (!existsSync10(logDir)) {
2076
2459
  mkdirSync5(logDir, { recursive: true });
2077
2460
  }
2078
2461
  transport.kill(sessionName);
2079
2462
  let cleanupSuffix = "";
2080
2463
  try {
2081
2464
  const thisFile = fileURLToPath(import.meta.url);
2082
- const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
2083
- if (existsSync9(cleanupScript)) {
2465
+ const cleanupScript = path10.join(path10.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
2466
+ if (existsSync10(cleanupScript)) {
2084
2467
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
2085
2468
  }
2086
2469
  } catch {
2087
2470
  }
2088
2471
  try {
2089
- const claudeJsonPath = path9.join(os7.homedir(), ".claude.json");
2472
+ const claudeJsonPath = path10.join(os7.homedir(), ".claude.json");
2090
2473
  let claudeJson = {};
2091
2474
  try {
2092
2475
  claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
@@ -2101,10 +2484,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2101
2484
  } catch {
2102
2485
  }
2103
2486
  try {
2104
- const settingsDir = path9.join(os7.homedir(), ".claude", "projects");
2487
+ const settingsDir = path10.join(os7.homedir(), ".claude", "projects");
2105
2488
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
2106
- const projSettingsDir = path9.join(settingsDir, normalizedKey);
2107
- const settingsPath = path9.join(projSettingsDir, "settings.json");
2489
+ const projSettingsDir = path10.join(settingsDir, normalizedKey);
2490
+ const settingsPath = path10.join(projSettingsDir, "settings.json");
2108
2491
  let settings = {};
2109
2492
  try {
2110
2493
  settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
@@ -2151,7 +2534,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2151
2534
  let behaviorsFlag = "";
2152
2535
  let legacyFallbackWarned = false;
2153
2536
  if (!useExeAgent && !useBinSymlink) {
2154
- const identityPath2 = path9.join(
2537
+ const identityPath2 = path10.join(
2155
2538
  os7.homedir(),
2156
2539
  ".exe-os",
2157
2540
  "identity",
@@ -2161,13 +2544,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2161
2544
  const hasAgentFlag = claudeSupportsAgentFlag();
2162
2545
  if (hasAgentFlag) {
2163
2546
  identityFlag = ` --agent ${employeeName}`;
2164
- } else if (existsSync9(identityPath2)) {
2547
+ } else if (existsSync10(identityPath2)) {
2165
2548
  identityFlag = ` --append-system-prompt-file ${identityPath2}`;
2166
2549
  legacyFallbackWarned = true;
2167
2550
  }
2168
2551
  const behaviorsFile = exportBehaviorsSync(
2169
2552
  employeeName,
2170
- path9.basename(spawnCwd),
2553
+ path10.basename(spawnCwd),
2171
2554
  sessionName
2172
2555
  );
2173
2556
  if (behaviorsFile) {
@@ -2182,9 +2565,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2182
2565
  }
2183
2566
  let sessionContextFlag = "";
2184
2567
  try {
2185
- const ctxDir = path9.join(os7.homedir(), ".exe-os", "session-cache");
2568
+ const ctxDir = path10.join(os7.homedir(), ".exe-os", "session-cache");
2186
2569
  mkdirSync5(ctxDir, { recursive: true });
2187
- const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
2570
+ const ctxFile = path10.join(ctxDir, `session-context-${sessionName}.md`);
2188
2571
  const ctxContent = [
2189
2572
  `## Session Context`,
2190
2573
  `You are running in tmux session: ${sessionName}.`,
@@ -2268,7 +2651,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2268
2651
  transport.pipeLog(sessionName, logFile);
2269
2652
  try {
2270
2653
  const mySession = getMySession();
2271
- const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
2654
+ const dispatchInfo = path10.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
2272
2655
  writeFileSync6(dispatchInfo, JSON.stringify({
2273
2656
  dispatchedBy: mySession,
2274
2657
  rootExe: exeSession,
@@ -2343,15 +2726,15 @@ var init_tmux_routing = __esm({
2343
2726
  init_intercom_queue();
2344
2727
  init_plan_limits();
2345
2728
  init_employees();
2346
- SPAWN_LOCK_DIR = path9.join(os7.homedir(), ".exe-os", "spawn-locks");
2347
- SESSION_CACHE = path9.join(os7.homedir(), ".exe-os", "session-cache");
2729
+ SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
2730
+ SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
2348
2731
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2349
2732
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
2350
2733
  VERIFY_PANE_LINES = 200;
2351
2734
  INTERCOM_DEBOUNCE_MS = 3e4;
2352
2735
  CODEX_DEBOUNCE_MS = 12e4;
2353
- INTERCOM_LOG2 = path9.join(os7.homedir(), ".exe-os", "intercom.log");
2354
- DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
2736
+ INTERCOM_LOG2 = path10.join(os7.homedir(), ".exe-os", "intercom.log");
2737
+ DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
2355
2738
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
2356
2739
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
2357
2740
  }
@@ -2392,13 +2775,13 @@ var init_task_scope = __esm({
2392
2775
 
2393
2776
  // src/lib/notifications.ts
2394
2777
  import crypto2 from "crypto";
2395
- import path10 from "path";
2778
+ import path11 from "path";
2396
2779
  import os8 from "os";
2397
2780
  import {
2398
2781
  readFileSync as readFileSync9,
2399
- readdirSync as readdirSync2,
2400
- unlinkSync as unlinkSync3,
2401
- existsSync as existsSync10,
2782
+ readdirSync as readdirSync3,
2783
+ unlinkSync as unlinkSync4,
2784
+ existsSync as existsSync11,
2402
2785
  rmdirSync
2403
2786
  } from "fs";
2404
2787
  async function writeNotification(notification) {
@@ -2447,64 +2830,9 @@ var init_notifications = __esm({
2447
2830
  }
2448
2831
  });
2449
2832
 
2450
- // src/lib/state-bus.ts
2451
- var StateBus, orgBus;
2452
- var init_state_bus = __esm({
2453
- "src/lib/state-bus.ts"() {
2454
- "use strict";
2455
- StateBus = class {
2456
- handlers = /* @__PURE__ */ new Map();
2457
- globalHandlers = /* @__PURE__ */ new Set();
2458
- /** Emit an event to all subscribers */
2459
- emit(event) {
2460
- const typeHandlers = this.handlers.get(event.type);
2461
- if (typeHandlers) {
2462
- for (const handler of typeHandlers) {
2463
- try {
2464
- handler(event);
2465
- } catch {
2466
- }
2467
- }
2468
- }
2469
- for (const handler of this.globalHandlers) {
2470
- try {
2471
- handler(event);
2472
- } catch {
2473
- }
2474
- }
2475
- }
2476
- /** Subscribe to a specific event type */
2477
- on(type, handler) {
2478
- if (!this.handlers.has(type)) {
2479
- this.handlers.set(type, /* @__PURE__ */ new Set());
2480
- }
2481
- this.handlers.get(type).add(handler);
2482
- }
2483
- /** Subscribe to ALL events */
2484
- onAny(handler) {
2485
- this.globalHandlers.add(handler);
2486
- }
2487
- /** Unsubscribe from a specific event type */
2488
- off(type, handler) {
2489
- this.handlers.get(type)?.delete(handler);
2490
- }
2491
- /** Unsubscribe from ALL events */
2492
- offAny(handler) {
2493
- this.globalHandlers.delete(handler);
2494
- }
2495
- /** Remove all listeners */
2496
- clear() {
2497
- this.handlers.clear();
2498
- this.globalHandlers.clear();
2499
- }
2500
- };
2501
- orgBus = new StateBus();
2502
- }
2503
- });
2504
-
2505
2833
  // src/lib/project-name.ts
2506
2834
  import { execSync as execSync5 } from "child_process";
2507
- import path11 from "path";
2835
+ import path12 from "path";
2508
2836
  function getProjectName(cwd) {
2509
2837
  const dir = cwd ?? process.cwd();
2510
2838
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2517,7 +2845,7 @@ function getProjectName(cwd) {
2517
2845
  timeout: 2e3,
2518
2846
  stdio: ["pipe", "pipe", "pipe"]
2519
2847
  }).trim();
2520
- repoRoot = path11.dirname(gitCommonDir);
2848
+ repoRoot = path12.dirname(gitCommonDir);
2521
2849
  } catch {
2522
2850
  repoRoot = execSync5("git rev-parse --show-toplevel", {
2523
2851
  cwd: dir,
@@ -2526,11 +2854,11 @@ function getProjectName(cwd) {
2526
2854
  stdio: ["pipe", "pipe", "pipe"]
2527
2855
  }).trim();
2528
2856
  }
2529
- _cached2 = path11.basename(repoRoot);
2857
+ _cached2 = path12.basename(repoRoot);
2530
2858
  _cachedCwd = dir;
2531
2859
  return _cached2;
2532
2860
  } catch {
2533
- _cached2 = path11.basename(dir);
2861
+ _cached2 = path12.basename(dir);
2534
2862
  _cachedCwd = dir;
2535
2863
  return _cached2;
2536
2864
  }
@@ -2608,11 +2936,11 @@ var init_session_scope = __esm({
2608
2936
 
2609
2937
  // src/lib/tasks-crud.ts
2610
2938
  import crypto3 from "crypto";
2611
- import path12 from "path";
2939
+ import path13 from "path";
2612
2940
  import os9 from "os";
2613
2941
  import { execSync as execSync6 } from "child_process";
2614
2942
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2615
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2943
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
2616
2944
  async function writeCheckpoint(input) {
2617
2945
  const client = getClient();
2618
2946
  const row = await resolveTask(client, input.taskId);
@@ -2806,8 +3134,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
2806
3134
  }
2807
3135
  if (input.baseDir) {
2808
3136
  try {
2809
- await mkdir3(path12.join(input.baseDir, "exe", "output"), { recursive: true });
2810
- await mkdir3(path12.join(input.baseDir, "exe", "research"), { recursive: true });
3137
+ await mkdir3(path13.join(input.baseDir, "exe", "output"), { recursive: true });
3138
+ await mkdir3(path13.join(input.baseDir, "exe", "research"), { recursive: true });
2811
3139
  await ensureArchitectureDoc(input.baseDir, input.projectName);
2812
3140
  await ensureGitignoreExe(input.baseDir);
2813
3141
  } catch {
@@ -2843,10 +3171,10 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
2843
3171
  });
2844
3172
  if (input.baseDir) {
2845
3173
  try {
2846
- const EXE_OS_DIR = path12.join(os9.homedir(), ".exe-os");
2847
- const mdPath = path12.join(EXE_OS_DIR, taskFile);
2848
- const mdDir = path12.dirname(mdPath);
2849
- if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
3174
+ const EXE_OS_DIR = path13.join(os9.homedir(), ".exe-os");
3175
+ const mdPath = path13.join(EXE_OS_DIR, taskFile);
3176
+ const mdDir = path13.dirname(mdPath);
3177
+ if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
2850
3178
  const reviewer = input.reviewer ?? input.assignedBy;
2851
3179
  const mdContent = `# ${input.title}
2852
3180
 
@@ -3146,9 +3474,9 @@ async function deleteTaskCore(taskId, _baseDir) {
3146
3474
  return { taskFile, assignedTo, assignedBy, taskSlug };
3147
3475
  }
3148
3476
  async function ensureArchitectureDoc(baseDir, projectName) {
3149
- const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
3477
+ const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
3150
3478
  try {
3151
- if (existsSync11(archPath)) return;
3479
+ if (existsSync12(archPath)) return;
3152
3480
  const template = [
3153
3481
  `# ${projectName} \u2014 System Architecture`,
3154
3482
  "",
@@ -3181,9 +3509,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
3181
3509
  }
3182
3510
  }
3183
3511
  async function ensureGitignoreExe(baseDir) {
3184
- const gitignorePath = path12.join(baseDir, ".gitignore");
3512
+ const gitignorePath = path13.join(baseDir, ".gitignore");
3185
3513
  try {
3186
- if (existsSync11(gitignorePath)) {
3514
+ if (existsSync12(gitignorePath)) {
3187
3515
  const content = readFileSync10(gitignorePath, "utf-8");
3188
3516
  if (/^\/?exe\/?$/m.test(content)) return;
3189
3517
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -3214,198 +3542,6 @@ var init_tasks_crud = __esm({
3214
3542
  }
3215
3543
  });
3216
3544
 
3217
- // src/lib/tasks-review.ts
3218
- import path13 from "path";
3219
- import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
3220
- async function countPendingReviews(sessionScope) {
3221
- const client = getClient();
3222
- const scope = strictSessionScopeFilter(
3223
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3224
- );
3225
- const result = await client.execute({
3226
- sql: `SELECT COUNT(*) as cnt FROM tasks
3227
- WHERE status = 'needs_review'${scope.sql}`,
3228
- args: [...scope.args]
3229
- });
3230
- return Number(result.rows[0]?.cnt) || 0;
3231
- }
3232
- async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3233
- const client = getClient();
3234
- const scope = strictSessionScopeFilter(
3235
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3236
- );
3237
- const result = await client.execute({
3238
- sql: `SELECT COUNT(*) as cnt FROM tasks
3239
- WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
3240
- args: [sinceIso, ...scope.args]
3241
- });
3242
- return Number(result.rows[0]?.cnt) || 0;
3243
- }
3244
- async function listPendingReviews(limit, sessionScope) {
3245
- const client = getClient();
3246
- const scope = strictSessionScopeFilter(
3247
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3248
- );
3249
- const result = await client.execute({
3250
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3251
- WHERE status = 'needs_review'${scope.sql}
3252
- ORDER BY updated_at ASC LIMIT ?`,
3253
- args: [...scope.args, limit]
3254
- });
3255
- return result.rows;
3256
- }
3257
- async function cleanupOrphanedReviews() {
3258
- const client = getClient();
3259
- const now = (/* @__PURE__ */ new Date()).toISOString();
3260
- const r1 = await client.execute({
3261
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3262
- WHERE status IN ('open', 'needs_review', 'in_progress')
3263
- AND assigned_by = 'system'
3264
- AND title LIKE 'Review:%'
3265
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3266
- args: [now]
3267
- });
3268
- const r1b = await client.execute({
3269
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3270
- WHERE status IN ('open', 'needs_review')
3271
- AND title LIKE 'Review:%completed%'
3272
- 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')))`,
3273
- args: [now]
3274
- });
3275
- const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
3276
- const r2 = await client.execute({
3277
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
3278
- WHERE status = 'needs_review'
3279
- AND result IS NOT NULL
3280
- AND updated_at < ?`,
3281
- args: [now, staleThreshold]
3282
- });
3283
- const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
3284
- if (total > 0) {
3285
- process.stderr.write(
3286
- `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
3287
- `
3288
- );
3289
- }
3290
- return total;
3291
- }
3292
- function getReviewChecklist(role, agent, taskSlug) {
3293
- const roleLower = role.toLowerCase();
3294
- if (roleLower.includes("engineer") || roleLower === "principal engineer") {
3295
- return {
3296
- lens: "Code Quality (Engineer)",
3297
- checklist: [
3298
- "1. Do all tests pass? Any new tests needed?",
3299
- "2. Is the code clean \u2014 no dead code, no TODOs left?",
3300
- "3. Does it follow existing patterns and conventions in the codebase?",
3301
- "4. Any regressions in the test suite?"
3302
- ]
3303
- };
3304
- }
3305
- if (roleLower === "cto" || roleLower.includes("architect")) {
3306
- return {
3307
- lens: "Architecture (CTO)",
3308
- checklist: [
3309
- "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
3310
- "2. Is it backward compatible? Any breaking changes?",
3311
- "3. Does it introduce technical debt? Is that debt justified?",
3312
- "4. Security implications? Any new attack surface?",
3313
- "5. Does it scale? Performance considerations?",
3314
- "6. Coordination: does this affect other employees' work or other projects?"
3315
- ]
3316
- };
3317
- }
3318
- if (roleLower === "coo" || roleLower.includes("operations")) {
3319
- return {
3320
- lens: "Strategic (COO)",
3321
- checklist: [
3322
- "1. Does this serve the project mission?",
3323
- "2. Is this the right work at the right time?",
3324
- "3. Does the architectural assessment make sense for the business?",
3325
- "4. Any cross-project implications?"
3326
- ]
3327
- };
3328
- }
3329
- return {
3330
- lens: "General",
3331
- checklist: [
3332
- "1. Read the original task's acceptance criteria",
3333
- `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
3334
- "3. Verify code changes match requirements",
3335
- "4. Check if tests were added/updated",
3336
- `5. Look for output files in exe/output/${agent}-${taskSlug}*`
3337
- ]
3338
- };
3339
- }
3340
- async function cleanupReviewFile(row, taskFile, _baseDir) {
3341
- if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3342
- try {
3343
- const client = getClient();
3344
- const now = (/* @__PURE__ */ new Date()).toISOString();
3345
- const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3346
- if (parentId) {
3347
- const result = await client.execute({
3348
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3349
- args: [now, parentId]
3350
- });
3351
- if (result.rowsAffected > 0) {
3352
- process.stderr.write(
3353
- `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3354
- `
3355
- );
3356
- }
3357
- } else {
3358
- const fileName = taskFile.split("/").pop() ?? "";
3359
- const reviewPrefix = fileName.replace(".md", "");
3360
- const parts = reviewPrefix.split("-");
3361
- if (parts.length >= 3 && parts[0] === "review") {
3362
- const agent = parts[1];
3363
- const slug = parts.slice(2).join("-");
3364
- const legacyTaskFile = `exe/${agent}/${slug}.md`;
3365
- const result = await client.execute({
3366
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
3367
- args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
3368
- });
3369
- if (result.rowsAffected > 0) {
3370
- process.stderr.write(
3371
- `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
3372
- `
3373
- );
3374
- }
3375
- }
3376
- }
3377
- } catch (err) {
3378
- process.stderr.write(
3379
- `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
3380
- `
3381
- );
3382
- }
3383
- try {
3384
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3385
- if (existsSync12(cacheDir)) {
3386
- for (const f of readdirSync3(cacheDir)) {
3387
- if (f.startsWith("review-notified-")) {
3388
- unlinkSync4(path13.join(cacheDir, f));
3389
- }
3390
- }
3391
- }
3392
- } catch {
3393
- }
3394
- }
3395
- var init_tasks_review = __esm({
3396
- "src/lib/tasks-review.ts"() {
3397
- "use strict";
3398
- init_database();
3399
- init_config();
3400
- init_employees();
3401
- init_notifications();
3402
- init_tmux_routing();
3403
- init_session_key();
3404
- init_state_bus();
3405
- init_task_scope();
3406
- }
3407
- });
3408
-
3409
3545
  // src/lib/tasks-chain.ts
3410
3546
  import path14 from "path";
3411
3547
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";