@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/bin/cli.js +150 -8
- package/dist/bin/exe-boot.js +150 -8
- package/dist/bin/exe-dispatch.js +155 -8
- package/dist/bin/exe-gateway.js +531 -166
- package/dist/bin/exe-link.js +6 -0
- package/dist/bin/exe-session-cleanup.js +28 -8
- package/dist/bin/git-sweep.js +157 -10
- package/dist/bin/scan-tasks.js +155 -8
- package/dist/bin/setup.js +6 -0
- package/dist/gateway/index.js +144 -8
- package/dist/hooks/bug-report-worker.js +394 -258
- package/dist/hooks/codex-stop-task-finalizer.js +10 -8
- package/dist/hooks/commit-complete.js +155 -8
- package/dist/hooks/ingest-worker.js +155 -8
- package/dist/hooks/pre-compact.js +155 -8
- package/dist/hooks/prompt-submit.js +28 -8
- package/dist/hooks/session-end.js +157 -10
- package/dist/hooks/summary-worker.js +6 -0
- package/dist/index.js +144 -8
- package/dist/lib/cloud-sync.js +6 -0
- package/dist/lib/exe-daemon.js +28 -8
- package/dist/lib/messaging.js +10 -8
- package/dist/lib/tasks.js +460 -313
- package/dist/lib/tmux-routing.js +155 -8
- package/dist/mcp/server.js +396 -254
- package/dist/mcp/tools/create-task.js +449 -313
- package/dist/mcp/tools/send-message.js +10 -8
- package/dist/mcp/tools/update-task.js +460 -313
- package/dist/runtime/index.js +155 -8
- package/dist/tui/App.js +144 -8
- package/package.json +1 -1
|
@@ -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
|
|
1592
|
-
import
|
|
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
|
|
1958
|
+
import { unlinkSync as unlinkSync3 } from "fs";
|
|
1596
1959
|
function spawnLockPath(sessionName) {
|
|
1597
|
-
return
|
|
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 (!
|
|
1971
|
+
if (!existsSync10(SPAWN_LOCK_DIR)) {
|
|
1609
1972
|
mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
|
|
1610
1973
|
}
|
|
1611
1974
|
const lockFile = spawnLockPath(sessionName);
|
|
1612
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
1635
|
-
|
|
1997
|
+
const scriptPath = path10.join(
|
|
1998
|
+
path10.dirname(thisFile),
|
|
1636
1999
|
"..",
|
|
1637
2000
|
"bin",
|
|
1638
2001
|
"exe-export-behaviors.js"
|
|
1639
2002
|
);
|
|
1640
|
-
return
|
|
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 (!
|
|
2069
|
+
if (!existsSync10(SESSION_CACHE)) {
|
|
1707
2070
|
mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
1708
2071
|
}
|
|
1709
2072
|
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
1710
|
-
const filePath =
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
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 =
|
|
1925
|
-
if (
|
|
1926
|
-
const files =
|
|
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 =
|
|
2074
|
-
const logFile =
|
|
2075
|
-
if (!
|
|
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 =
|
|
2083
|
-
if (
|
|
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 =
|
|
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 =
|
|
2487
|
+
const settingsDir = path10.join(os7.homedir(), ".claude", "projects");
|
|
2105
2488
|
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
2106
|
-
const projSettingsDir =
|
|
2107
|
-
const settingsPath =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
2568
|
+
const ctxDir = path10.join(os7.homedir(), ".exe-os", "session-cache");
|
|
2186
2569
|
mkdirSync5(ctxDir, { recursive: true });
|
|
2187
|
-
const ctxFile =
|
|
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 =
|
|
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 =
|
|
2347
|
-
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 =
|
|
2354
|
-
DEBOUNCE_FILE =
|
|
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
|
|
2778
|
+
import path11 from "path";
|
|
2396
2779
|
import os8 from "os";
|
|
2397
2780
|
import {
|
|
2398
2781
|
readFileSync as readFileSync9,
|
|
2399
|
-
readdirSync as
|
|
2400
|
-
unlinkSync as
|
|
2401
|
-
existsSync as
|
|
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
|
|
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 =
|
|
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 =
|
|
2857
|
+
_cached2 = path12.basename(repoRoot);
|
|
2530
2858
|
_cachedCwd = dir;
|
|
2531
2859
|
return _cached2;
|
|
2532
2860
|
} catch {
|
|
2533
|
-
_cached2 =
|
|
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
|
|
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
|
|
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(
|
|
2810
|
-
await mkdir3(
|
|
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 =
|
|
2847
|
-
const mdPath =
|
|
2848
|
-
const mdDir =
|
|
2849
|
-
if (!
|
|
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 =
|
|
3477
|
+
const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
3150
3478
|
try {
|
|
3151
|
-
if (
|
|
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 =
|
|
3512
|
+
const gitignorePath = path13.join(baseDir, ".gitignore");
|
|
3185
3513
|
try {
|
|
3186
|
-
if (
|
|
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";
|