@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
package/dist/mcp/server.js
CHANGED
|
@@ -7545,6 +7545,314 @@ var init_capacity_monitor = __esm({
|
|
|
7545
7545
|
}
|
|
7546
7546
|
});
|
|
7547
7547
|
|
|
7548
|
+
// src/lib/tasks-review.ts
|
|
7549
|
+
var tasks_review_exports = {};
|
|
7550
|
+
__export(tasks_review_exports, {
|
|
7551
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
7552
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
7553
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
7554
|
+
countPendingReviews: () => countPendingReviews,
|
|
7555
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
7556
|
+
formatAge: () => formatAge,
|
|
7557
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
7558
|
+
isStale: () => isStale,
|
|
7559
|
+
listPendingReviews: () => listPendingReviews
|
|
7560
|
+
});
|
|
7561
|
+
import path19 from "path";
|
|
7562
|
+
import { existsSync as existsSync15, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
7563
|
+
function formatAge(isoTimestamp) {
|
|
7564
|
+
if (!isoTimestamp) return "";
|
|
7565
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
7566
|
+
if (ms < 0) return "just now";
|
|
7567
|
+
const minutes = Math.floor(ms / 6e4);
|
|
7568
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
7569
|
+
const hours = Math.floor(minutes / 60);
|
|
7570
|
+
if (hours < 24) return `${hours}h ago`;
|
|
7571
|
+
const days = Math.floor(hours / 24);
|
|
7572
|
+
return `${days}d ago`;
|
|
7573
|
+
}
|
|
7574
|
+
function isStale(isoTimestamp) {
|
|
7575
|
+
if (!isoTimestamp) return false;
|
|
7576
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
7577
|
+
}
|
|
7578
|
+
async function countPendingReviews(sessionScope) {
|
|
7579
|
+
const client = getClient();
|
|
7580
|
+
const scope = strictSessionScopeFilter(
|
|
7581
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
7582
|
+
);
|
|
7583
|
+
const result = await client.execute({
|
|
7584
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
7585
|
+
WHERE status = 'needs_review'${scope.sql}`,
|
|
7586
|
+
args: [...scope.args]
|
|
7587
|
+
});
|
|
7588
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
7589
|
+
}
|
|
7590
|
+
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
7591
|
+
const client = getClient();
|
|
7592
|
+
const scope = strictSessionScopeFilter(
|
|
7593
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
7594
|
+
);
|
|
7595
|
+
const result = await client.execute({
|
|
7596
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
7597
|
+
WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
|
|
7598
|
+
args: [sinceIso, ...scope.args]
|
|
7599
|
+
});
|
|
7600
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
7601
|
+
}
|
|
7602
|
+
async function listPendingReviews(limit, sessionScope) {
|
|
7603
|
+
const client = getClient();
|
|
7604
|
+
const scope = strictSessionScopeFilter(
|
|
7605
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
7606
|
+
);
|
|
7607
|
+
const result = await client.execute({
|
|
7608
|
+
sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
|
|
7609
|
+
WHERE status = 'needs_review'${scope.sql}
|
|
7610
|
+
ORDER BY updated_at ASC LIMIT ?`,
|
|
7611
|
+
args: [...scope.args, limit]
|
|
7612
|
+
});
|
|
7613
|
+
return result.rows;
|
|
7614
|
+
}
|
|
7615
|
+
async function cleanupOrphanedReviews() {
|
|
7616
|
+
const client = getClient();
|
|
7617
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7618
|
+
const r1 = await client.execute({
|
|
7619
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
7620
|
+
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
7621
|
+
AND assigned_by = 'system'
|
|
7622
|
+
AND title LIKE 'Review:%'
|
|
7623
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
|
|
7624
|
+
args: [now]
|
|
7625
|
+
});
|
|
7626
|
+
const r1b = await client.execute({
|
|
7627
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
7628
|
+
WHERE status IN ('open', 'needs_review')
|
|
7629
|
+
AND title LIKE 'Review:%completed%'
|
|
7630
|
+
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')))`,
|
|
7631
|
+
args: [now]
|
|
7632
|
+
});
|
|
7633
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
7634
|
+
const r2 = await client.execute({
|
|
7635
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
7636
|
+
WHERE status = 'needs_review'
|
|
7637
|
+
AND result IS NOT NULL
|
|
7638
|
+
AND updated_at < ?`,
|
|
7639
|
+
args: [now, staleThreshold]
|
|
7640
|
+
});
|
|
7641
|
+
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
7642
|
+
if (total > 0) {
|
|
7643
|
+
process.stderr.write(
|
|
7644
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
7645
|
+
`
|
|
7646
|
+
);
|
|
7647
|
+
}
|
|
7648
|
+
return total;
|
|
7649
|
+
}
|
|
7650
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
7651
|
+
const roleLower = role.toLowerCase();
|
|
7652
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
7653
|
+
return {
|
|
7654
|
+
lens: "Code Quality (Engineer)",
|
|
7655
|
+
checklist: [
|
|
7656
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
7657
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
7658
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
7659
|
+
"4. Any regressions in the test suite?"
|
|
7660
|
+
]
|
|
7661
|
+
};
|
|
7662
|
+
}
|
|
7663
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
7664
|
+
return {
|
|
7665
|
+
lens: "Architecture (CTO)",
|
|
7666
|
+
checklist: [
|
|
7667
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
7668
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
7669
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
7670
|
+
"4. Security implications? Any new attack surface?",
|
|
7671
|
+
"5. Does it scale? Performance considerations?",
|
|
7672
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
7673
|
+
]
|
|
7674
|
+
};
|
|
7675
|
+
}
|
|
7676
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
7677
|
+
return {
|
|
7678
|
+
lens: "Strategic (COO)",
|
|
7679
|
+
checklist: [
|
|
7680
|
+
"1. Does this serve the project mission?",
|
|
7681
|
+
"2. Is this the right work at the right time?",
|
|
7682
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
7683
|
+
"4. Any cross-project implications?"
|
|
7684
|
+
]
|
|
7685
|
+
};
|
|
7686
|
+
}
|
|
7687
|
+
return {
|
|
7688
|
+
lens: "General",
|
|
7689
|
+
checklist: [
|
|
7690
|
+
"1. Read the original task's acceptance criteria",
|
|
7691
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
7692
|
+
"3. Verify code changes match requirements",
|
|
7693
|
+
"4. Check if tests were added/updated",
|
|
7694
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
7695
|
+
]
|
|
7696
|
+
};
|
|
7697
|
+
}
|
|
7698
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
7699
|
+
const taskFile = String(row.task_file);
|
|
7700
|
+
const employees = await loadEmployees();
|
|
7701
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
7702
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
7703
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
7704
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
7705
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
7706
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
7707
|
+
const client = getClient();
|
|
7708
|
+
const agent = String(row.assigned_to);
|
|
7709
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
7710
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
7711
|
+
const currentStatus = String(row.status ?? "");
|
|
7712
|
+
if (currentStatus === "done") return;
|
|
7713
|
+
const existingResult = String(row.result ?? "");
|
|
7714
|
+
if (existingResult.includes("## Review notes")) return;
|
|
7715
|
+
let reviewerRole = "unknown";
|
|
7716
|
+
try {
|
|
7717
|
+
const emp = getEmployee(employees, reviewer);
|
|
7718
|
+
if (emp) reviewerRole = emp.role;
|
|
7719
|
+
} catch {
|
|
7720
|
+
}
|
|
7721
|
+
const taskTitle = String(row.title);
|
|
7722
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
7723
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
7724
|
+
process.stderr.write(
|
|
7725
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
7726
|
+
`
|
|
7727
|
+
);
|
|
7728
|
+
const reviewNotes = [
|
|
7729
|
+
`
|
|
7730
|
+
---
|
|
7731
|
+
## Review notes`,
|
|
7732
|
+
`Review lens: ${lens}`,
|
|
7733
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
7734
|
+
"",
|
|
7735
|
+
"### Checklist",
|
|
7736
|
+
...checklist,
|
|
7737
|
+
"",
|
|
7738
|
+
"### Verdict",
|
|
7739
|
+
"- **Approved:** mark this task as done",
|
|
7740
|
+
"- **Needs work:** re-open with notes"
|
|
7741
|
+
].join("\n");
|
|
7742
|
+
const originalTaskId = String(row.id);
|
|
7743
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
7744
|
+
await client.execute({
|
|
7745
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
7746
|
+
WHERE id = ?`,
|
|
7747
|
+
args: [updatedResult, now, originalTaskId]
|
|
7748
|
+
});
|
|
7749
|
+
orgBus.emit({
|
|
7750
|
+
type: "review_created",
|
|
7751
|
+
reviewId: originalTaskId,
|
|
7752
|
+
employee: agent,
|
|
7753
|
+
reviewer,
|
|
7754
|
+
timestamp: now
|
|
7755
|
+
});
|
|
7756
|
+
await writeNotification({
|
|
7757
|
+
agentId: agent,
|
|
7758
|
+
agentRole: String(row.assigned_to),
|
|
7759
|
+
event: "task_complete",
|
|
7760
|
+
project: String(row.project_name),
|
|
7761
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
7762
|
+
taskFile
|
|
7763
|
+
});
|
|
7764
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
7765
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
7766
|
+
if (!autoApprove) {
|
|
7767
|
+
try {
|
|
7768
|
+
const key = getSessionKey();
|
|
7769
|
+
const exeSession = getParentExe(key);
|
|
7770
|
+
if (exeSession) {
|
|
7771
|
+
sendIntercom(exeSession);
|
|
7772
|
+
}
|
|
7773
|
+
} catch {
|
|
7774
|
+
}
|
|
7775
|
+
}
|
|
7776
|
+
if (autoApprove) {
|
|
7777
|
+
process.stderr.write(
|
|
7778
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
7779
|
+
`
|
|
7780
|
+
);
|
|
7781
|
+
await client.execute({
|
|
7782
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
7783
|
+
args: [now, originalTaskId]
|
|
7784
|
+
});
|
|
7785
|
+
}
|
|
7786
|
+
}
|
|
7787
|
+
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
7788
|
+
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
7789
|
+
try {
|
|
7790
|
+
const client = getClient();
|
|
7791
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7792
|
+
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
7793
|
+
if (parentId) {
|
|
7794
|
+
const result = await client.execute({
|
|
7795
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
7796
|
+
args: [now, parentId]
|
|
7797
|
+
});
|
|
7798
|
+
if (result.rowsAffected > 0) {
|
|
7799
|
+
process.stderr.write(
|
|
7800
|
+
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
7801
|
+
`
|
|
7802
|
+
);
|
|
7803
|
+
}
|
|
7804
|
+
} else {
|
|
7805
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
7806
|
+
const reviewPrefix = fileName.replace(".md", "");
|
|
7807
|
+
const parts = reviewPrefix.split("-");
|
|
7808
|
+
if (parts.length >= 3 && parts[0] === "review") {
|
|
7809
|
+
const agent = parts[1];
|
|
7810
|
+
const slug = parts.slice(2).join("-");
|
|
7811
|
+
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
7812
|
+
const result = await client.execute({
|
|
7813
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
7814
|
+
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
7815
|
+
});
|
|
7816
|
+
if (result.rowsAffected > 0) {
|
|
7817
|
+
process.stderr.write(
|
|
7818
|
+
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
7819
|
+
`
|
|
7820
|
+
);
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
}
|
|
7824
|
+
} catch (err) {
|
|
7825
|
+
process.stderr.write(
|
|
7826
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
7827
|
+
`
|
|
7828
|
+
);
|
|
7829
|
+
}
|
|
7830
|
+
try {
|
|
7831
|
+
const cacheDir = path19.join(EXE_AI_DIR, "session-cache");
|
|
7832
|
+
if (existsSync15(cacheDir)) {
|
|
7833
|
+
for (const f of readdirSync4(cacheDir)) {
|
|
7834
|
+
if (f.startsWith("review-notified-")) {
|
|
7835
|
+
unlinkSync4(path19.join(cacheDir, f));
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
}
|
|
7839
|
+
} catch {
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
7842
|
+
var init_tasks_review = __esm({
|
|
7843
|
+
"src/lib/tasks-review.ts"() {
|
|
7844
|
+
"use strict";
|
|
7845
|
+
init_database();
|
|
7846
|
+
init_config();
|
|
7847
|
+
init_employees();
|
|
7848
|
+
init_notifications();
|
|
7849
|
+
init_tmux_routing();
|
|
7850
|
+
init_session_key();
|
|
7851
|
+
init_state_bus();
|
|
7852
|
+
init_task_scope();
|
|
7853
|
+
}
|
|
7854
|
+
});
|
|
7855
|
+
|
|
7548
7856
|
// src/lib/tmux-routing.ts
|
|
7549
7857
|
var tmux_routing_exports = {};
|
|
7550
7858
|
__export(tmux_routing_exports, {
|
|
@@ -7571,13 +7879,13 @@ __export(tmux_routing_exports, {
|
|
|
7571
7879
|
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
7572
7880
|
});
|
|
7573
7881
|
import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
|
|
7574
|
-
import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7, existsSync as
|
|
7575
|
-
import
|
|
7882
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7, existsSync as existsSync16, appendFileSync, readdirSync as readdirSync5 } from "fs";
|
|
7883
|
+
import path20 from "path";
|
|
7576
7884
|
import os9 from "os";
|
|
7577
7885
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7578
|
-
import { unlinkSync as
|
|
7886
|
+
import { unlinkSync as unlinkSync5 } from "fs";
|
|
7579
7887
|
function spawnLockPath(sessionName) {
|
|
7580
|
-
return
|
|
7888
|
+
return path20.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
7581
7889
|
}
|
|
7582
7890
|
function isProcessAlive(pid) {
|
|
7583
7891
|
try {
|
|
@@ -7588,11 +7896,11 @@ function isProcessAlive(pid) {
|
|
|
7588
7896
|
}
|
|
7589
7897
|
}
|
|
7590
7898
|
function acquireSpawnLock2(sessionName) {
|
|
7591
|
-
if (!
|
|
7899
|
+
if (!existsSync16(SPAWN_LOCK_DIR)) {
|
|
7592
7900
|
mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
|
|
7593
7901
|
}
|
|
7594
7902
|
const lockFile = spawnLockPath(sessionName);
|
|
7595
|
-
if (
|
|
7903
|
+
if (existsSync16(lockFile)) {
|
|
7596
7904
|
try {
|
|
7597
7905
|
const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
|
|
7598
7906
|
const age = Date.now() - lock.timestamp;
|
|
@@ -7607,20 +7915,20 @@ function acquireSpawnLock2(sessionName) {
|
|
|
7607
7915
|
}
|
|
7608
7916
|
function releaseSpawnLock2(sessionName) {
|
|
7609
7917
|
try {
|
|
7610
|
-
|
|
7918
|
+
unlinkSync5(spawnLockPath(sessionName));
|
|
7611
7919
|
} catch {
|
|
7612
7920
|
}
|
|
7613
7921
|
}
|
|
7614
7922
|
function resolveBehaviorsExporterScript() {
|
|
7615
7923
|
try {
|
|
7616
7924
|
const thisFile = fileURLToPath2(import.meta.url);
|
|
7617
|
-
const scriptPath =
|
|
7618
|
-
|
|
7925
|
+
const scriptPath = path20.join(
|
|
7926
|
+
path20.dirname(thisFile),
|
|
7619
7927
|
"..",
|
|
7620
7928
|
"bin",
|
|
7621
7929
|
"exe-export-behaviors.js"
|
|
7622
7930
|
);
|
|
7623
|
-
return
|
|
7931
|
+
return existsSync16(scriptPath) ? scriptPath : null;
|
|
7624
7932
|
} catch {
|
|
7625
7933
|
return null;
|
|
7626
7934
|
}
|
|
@@ -7686,11 +7994,11 @@ function extractRootExe(name) {
|
|
|
7686
7994
|
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
7687
7995
|
}
|
|
7688
7996
|
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
7689
|
-
if (!
|
|
7997
|
+
if (!existsSync16(SESSION_CACHE)) {
|
|
7690
7998
|
mkdirSync7(SESSION_CACHE, { recursive: true });
|
|
7691
7999
|
}
|
|
7692
8000
|
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
7693
|
-
const filePath =
|
|
8001
|
+
const filePath = path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
7694
8002
|
writeFileSync10(filePath, JSON.stringify({
|
|
7695
8003
|
parentExe: rootExe,
|
|
7696
8004
|
dispatchedBy: dispatchedBy || rootExe,
|
|
@@ -7699,7 +8007,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
|
7699
8007
|
}
|
|
7700
8008
|
function getParentExe(sessionKey) {
|
|
7701
8009
|
try {
|
|
7702
|
-
const data = JSON.parse(readFileSync12(
|
|
8010
|
+
const data = JSON.parse(readFileSync12(path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
7703
8011
|
return data.parentExe || null;
|
|
7704
8012
|
} catch {
|
|
7705
8013
|
return null;
|
|
@@ -7708,7 +8016,7 @@ function getParentExe(sessionKey) {
|
|
|
7708
8016
|
function getDispatchedBy(sessionKey) {
|
|
7709
8017
|
try {
|
|
7710
8018
|
const data = JSON.parse(readFileSync12(
|
|
7711
|
-
|
|
8019
|
+
path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
7712
8020
|
"utf8"
|
|
7713
8021
|
));
|
|
7714
8022
|
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
@@ -7778,7 +8086,7 @@ async function verifyPaneAtCapacity(sessionName) {
|
|
|
7778
8086
|
}
|
|
7779
8087
|
function readDebounceState() {
|
|
7780
8088
|
try {
|
|
7781
|
-
if (!
|
|
8089
|
+
if (!existsSync16(DEBOUNCE_FILE)) return {};
|
|
7782
8090
|
const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
|
|
7783
8091
|
const state = {};
|
|
7784
8092
|
for (const [key, val] of Object.entries(raw)) {
|
|
@@ -7795,7 +8103,7 @@ function readDebounceState() {
|
|
|
7795
8103
|
}
|
|
7796
8104
|
function writeDebounceState(state) {
|
|
7797
8105
|
try {
|
|
7798
|
-
if (!
|
|
8106
|
+
if (!existsSync16(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
|
|
7799
8107
|
writeFileSync10(DEBOUNCE_FILE, JSON.stringify(state));
|
|
7800
8108
|
} catch {
|
|
7801
8109
|
}
|
|
@@ -7891,22 +8199,24 @@ function sendIntercom(targetSession) {
|
|
|
7891
8199
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
7892
8200
|
return "queued";
|
|
7893
8201
|
}
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
8202
|
+
if (sessionState !== "idle") {
|
|
8203
|
+
try {
|
|
8204
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
8205
|
+
const agent = baseAgentName(rawAgent);
|
|
8206
|
+
const markerPath = path20.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
8207
|
+
if (existsSync16(markerPath)) {
|
|
8208
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
8209
|
+
return "debounced";
|
|
8210
|
+
}
|
|
8211
|
+
} catch {
|
|
7901
8212
|
}
|
|
7902
|
-
} catch {
|
|
7903
8213
|
}
|
|
7904
8214
|
try {
|
|
7905
8215
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
7906
8216
|
const agent = baseAgentName(rawAgent);
|
|
7907
|
-
const taskDir =
|
|
7908
|
-
if (
|
|
7909
|
-
const files =
|
|
8217
|
+
const taskDir = path20.join(process.cwd(), "exe", agent);
|
|
8218
|
+
if (existsSync16(taskDir)) {
|
|
8219
|
+
const files = readdirSync5(taskDir).filter(
|
|
7910
8220
|
(f) => f.endsWith(".md") && f !== "DONE.txt"
|
|
7911
8221
|
);
|
|
7912
8222
|
if (files.length === 0) {
|
|
@@ -7970,6 +8280,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
7970
8280
|
try {
|
|
7971
8281
|
const sessions = transport.listSessions();
|
|
7972
8282
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
8283
|
+
try {
|
|
8284
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
8285
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
8286
|
+
if (pending instanceof Promise) {
|
|
8287
|
+
pending.then((count) => {
|
|
8288
|
+
if (count > 0) {
|
|
8289
|
+
execSync7(
|
|
8290
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
8291
|
+
{ timeout: 3e3 }
|
|
8292
|
+
);
|
|
8293
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
8294
|
+
}
|
|
8295
|
+
}).catch(() => {
|
|
8296
|
+
});
|
|
8297
|
+
return true;
|
|
8298
|
+
}
|
|
8299
|
+
} catch {
|
|
8300
|
+
}
|
|
7973
8301
|
execSync7(
|
|
7974
8302
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
7975
8303
|
{ timeout: 3e3 }
|
|
@@ -8053,23 +8381,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
8053
8381
|
const transport = getTransport();
|
|
8054
8382
|
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
8055
8383
|
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
8056
|
-
const logDir =
|
|
8057
|
-
const logFile =
|
|
8058
|
-
if (!
|
|
8384
|
+
const logDir = path20.join(os9.homedir(), ".exe-os", "session-logs");
|
|
8385
|
+
const logFile = path20.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
8386
|
+
if (!existsSync16(logDir)) {
|
|
8059
8387
|
mkdirSync7(logDir, { recursive: true });
|
|
8060
8388
|
}
|
|
8061
8389
|
transport.kill(sessionName);
|
|
8062
8390
|
let cleanupSuffix = "";
|
|
8063
8391
|
try {
|
|
8064
8392
|
const thisFile = fileURLToPath2(import.meta.url);
|
|
8065
|
-
const cleanupScript =
|
|
8066
|
-
if (
|
|
8393
|
+
const cleanupScript = path20.join(path20.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
8394
|
+
if (existsSync16(cleanupScript)) {
|
|
8067
8395
|
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
8068
8396
|
}
|
|
8069
8397
|
} catch {
|
|
8070
8398
|
}
|
|
8071
8399
|
try {
|
|
8072
|
-
const claudeJsonPath =
|
|
8400
|
+
const claudeJsonPath = path20.join(os9.homedir(), ".claude.json");
|
|
8073
8401
|
let claudeJson = {};
|
|
8074
8402
|
try {
|
|
8075
8403
|
claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
|
|
@@ -8084,10 +8412,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
8084
8412
|
} catch {
|
|
8085
8413
|
}
|
|
8086
8414
|
try {
|
|
8087
|
-
const settingsDir =
|
|
8415
|
+
const settingsDir = path20.join(os9.homedir(), ".claude", "projects");
|
|
8088
8416
|
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
8089
|
-
const projSettingsDir =
|
|
8090
|
-
const settingsPath =
|
|
8417
|
+
const projSettingsDir = path20.join(settingsDir, normalizedKey);
|
|
8418
|
+
const settingsPath = path20.join(projSettingsDir, "settings.json");
|
|
8091
8419
|
let settings = {};
|
|
8092
8420
|
try {
|
|
8093
8421
|
settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
|
|
@@ -8134,7 +8462,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
8134
8462
|
let behaviorsFlag = "";
|
|
8135
8463
|
let legacyFallbackWarned = false;
|
|
8136
8464
|
if (!useExeAgent && !useBinSymlink) {
|
|
8137
|
-
const identityPath2 =
|
|
8465
|
+
const identityPath2 = path20.join(
|
|
8138
8466
|
os9.homedir(),
|
|
8139
8467
|
".exe-os",
|
|
8140
8468
|
"identity",
|
|
@@ -8144,13 +8472,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
8144
8472
|
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
8145
8473
|
if (hasAgentFlag) {
|
|
8146
8474
|
identityFlag = ` --agent ${employeeName}`;
|
|
8147
|
-
} else if (
|
|
8475
|
+
} else if (existsSync16(identityPath2)) {
|
|
8148
8476
|
identityFlag = ` --append-system-prompt-file ${identityPath2}`;
|
|
8149
8477
|
legacyFallbackWarned = true;
|
|
8150
8478
|
}
|
|
8151
8479
|
const behaviorsFile = exportBehaviorsSync(
|
|
8152
8480
|
employeeName,
|
|
8153
|
-
|
|
8481
|
+
path20.basename(spawnCwd),
|
|
8154
8482
|
sessionName
|
|
8155
8483
|
);
|
|
8156
8484
|
if (behaviorsFile) {
|
|
@@ -8165,9 +8493,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
8165
8493
|
}
|
|
8166
8494
|
let sessionContextFlag = "";
|
|
8167
8495
|
try {
|
|
8168
|
-
const ctxDir =
|
|
8496
|
+
const ctxDir = path20.join(os9.homedir(), ".exe-os", "session-cache");
|
|
8169
8497
|
mkdirSync7(ctxDir, { recursive: true });
|
|
8170
|
-
const ctxFile =
|
|
8498
|
+
const ctxFile = path20.join(ctxDir, `session-context-${sessionName}.md`);
|
|
8171
8499
|
const ctxContent = [
|
|
8172
8500
|
`## Session Context`,
|
|
8173
8501
|
`You are running in tmux session: ${sessionName}.`,
|
|
@@ -8251,7 +8579,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
8251
8579
|
transport.pipeLog(sessionName, logFile);
|
|
8252
8580
|
try {
|
|
8253
8581
|
const mySession = getMySession();
|
|
8254
|
-
const dispatchInfo =
|
|
8582
|
+
const dispatchInfo = path20.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
8255
8583
|
writeFileSync10(dispatchInfo, JSON.stringify({
|
|
8256
8584
|
dispatchedBy: mySession,
|
|
8257
8585
|
rootExe: exeSession,
|
|
@@ -8326,15 +8654,15 @@ var init_tmux_routing = __esm({
|
|
|
8326
8654
|
init_intercom_queue();
|
|
8327
8655
|
init_plan_limits();
|
|
8328
8656
|
init_employees();
|
|
8329
|
-
SPAWN_LOCK_DIR =
|
|
8330
|
-
SESSION_CACHE =
|
|
8657
|
+
SPAWN_LOCK_DIR = path20.join(os9.homedir(), ".exe-os", "spawn-locks");
|
|
8658
|
+
SESSION_CACHE = path20.join(os9.homedir(), ".exe-os", "session-cache");
|
|
8331
8659
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
8332
8660
|
VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
8333
8661
|
VERIFY_PANE_LINES = 200;
|
|
8334
8662
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
8335
8663
|
CODEX_DEBOUNCE_MS = 12e4;
|
|
8336
|
-
INTERCOM_LOG2 =
|
|
8337
|
-
DEBOUNCE_FILE =
|
|
8664
|
+
INTERCOM_LOG2 = path20.join(os9.homedir(), ".exe-os", "intercom.log");
|
|
8665
|
+
DEBOUNCE_FILE = path20.join(SESSION_CACHE, "intercom-debounce.json");
|
|
8338
8666
|
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
8339
8667
|
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
|
|
8340
8668
|
}
|
|
@@ -8375,13 +8703,13 @@ var init_task_scope = __esm({
|
|
|
8375
8703
|
|
|
8376
8704
|
// src/lib/notifications.ts
|
|
8377
8705
|
import crypto7 from "crypto";
|
|
8378
|
-
import
|
|
8706
|
+
import path21 from "path";
|
|
8379
8707
|
import os10 from "os";
|
|
8380
8708
|
import {
|
|
8381
8709
|
readFileSync as readFileSync13,
|
|
8382
|
-
readdirSync as
|
|
8383
|
-
unlinkSync as
|
|
8384
|
-
existsSync as
|
|
8710
|
+
readdirSync as readdirSync6,
|
|
8711
|
+
unlinkSync as unlinkSync6,
|
|
8712
|
+
existsSync as existsSync17,
|
|
8385
8713
|
rmdirSync
|
|
8386
8714
|
} from "fs";
|
|
8387
8715
|
async function writeNotification(notification) {
|
|
@@ -8510,11 +8838,11 @@ __export(tasks_crud_exports, {
|
|
|
8510
8838
|
writeCheckpoint: () => writeCheckpoint
|
|
8511
8839
|
});
|
|
8512
8840
|
import crypto8 from "crypto";
|
|
8513
|
-
import
|
|
8841
|
+
import path22 from "path";
|
|
8514
8842
|
import os11 from "os";
|
|
8515
8843
|
import { execSync as execSync8 } from "child_process";
|
|
8516
8844
|
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
8517
|
-
import { existsSync as
|
|
8845
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14 } from "fs";
|
|
8518
8846
|
async function writeCheckpoint(input) {
|
|
8519
8847
|
const client = getClient();
|
|
8520
8848
|
const row = await resolveTask(client, input.taskId);
|
|
@@ -8708,8 +9036,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
|
|
|
8708
9036
|
}
|
|
8709
9037
|
if (input.baseDir) {
|
|
8710
9038
|
try {
|
|
8711
|
-
await mkdir4(
|
|
8712
|
-
await mkdir4(
|
|
9039
|
+
await mkdir4(path22.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
9040
|
+
await mkdir4(path22.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
8713
9041
|
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
8714
9042
|
await ensureGitignoreExe(input.baseDir);
|
|
8715
9043
|
} catch {
|
|
@@ -8745,10 +9073,10 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
|
|
|
8745
9073
|
});
|
|
8746
9074
|
if (input.baseDir) {
|
|
8747
9075
|
try {
|
|
8748
|
-
const EXE_OS_DIR =
|
|
8749
|
-
const mdPath =
|
|
8750
|
-
const mdDir =
|
|
8751
|
-
if (!
|
|
9076
|
+
const EXE_OS_DIR = path22.join(os11.homedir(), ".exe-os");
|
|
9077
|
+
const mdPath = path22.join(EXE_OS_DIR, taskFile);
|
|
9078
|
+
const mdDir = path22.dirname(mdPath);
|
|
9079
|
+
if (!existsSync18(mdDir)) await mkdir4(mdDir, { recursive: true });
|
|
8752
9080
|
const reviewer = input.reviewer ?? input.assignedBy;
|
|
8753
9081
|
const mdContent = `# ${input.title}
|
|
8754
9082
|
|
|
@@ -9048,9 +9376,9 @@ async function deleteTaskCore(taskId, _baseDir) {
|
|
|
9048
9376
|
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
9049
9377
|
}
|
|
9050
9378
|
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
9051
|
-
const archPath =
|
|
9379
|
+
const archPath = path22.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
9052
9380
|
try {
|
|
9053
|
-
if (
|
|
9381
|
+
if (existsSync18(archPath)) return;
|
|
9054
9382
|
const template = [
|
|
9055
9383
|
`# ${projectName} \u2014 System Architecture`,
|
|
9056
9384
|
"",
|
|
@@ -9083,9 +9411,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
|
|
|
9083
9411
|
}
|
|
9084
9412
|
}
|
|
9085
9413
|
async function ensureGitignoreExe(baseDir) {
|
|
9086
|
-
const gitignorePath =
|
|
9414
|
+
const gitignorePath = path22.join(baseDir, ".gitignore");
|
|
9087
9415
|
try {
|
|
9088
|
-
if (
|
|
9416
|
+
if (existsSync18(gitignorePath)) {
|
|
9089
9417
|
const content = readFileSync14(gitignorePath, "utf-8");
|
|
9090
9418
|
if (/^\/?exe\/?$/m.test(content)) return;
|
|
9091
9419
|
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
@@ -9116,198 +9444,6 @@ var init_tasks_crud = __esm({
|
|
|
9116
9444
|
}
|
|
9117
9445
|
});
|
|
9118
9446
|
|
|
9119
|
-
// src/lib/tasks-review.ts
|
|
9120
|
-
import path22 from "path";
|
|
9121
|
-
import { existsSync as existsSync18, readdirSync as readdirSync6, unlinkSync as unlinkSync6 } from "fs";
|
|
9122
|
-
async function countPendingReviews(sessionScope) {
|
|
9123
|
-
const client = getClient();
|
|
9124
|
-
const scope = strictSessionScopeFilter(
|
|
9125
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
9126
|
-
);
|
|
9127
|
-
const result = await client.execute({
|
|
9128
|
-
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
9129
|
-
WHERE status = 'needs_review'${scope.sql}`,
|
|
9130
|
-
args: [...scope.args]
|
|
9131
|
-
});
|
|
9132
|
-
return Number(result.rows[0]?.cnt) || 0;
|
|
9133
|
-
}
|
|
9134
|
-
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
9135
|
-
const client = getClient();
|
|
9136
|
-
const scope = strictSessionScopeFilter(
|
|
9137
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
9138
|
-
);
|
|
9139
|
-
const result = await client.execute({
|
|
9140
|
-
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
9141
|
-
WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
|
|
9142
|
-
args: [sinceIso, ...scope.args]
|
|
9143
|
-
});
|
|
9144
|
-
return Number(result.rows[0]?.cnt) || 0;
|
|
9145
|
-
}
|
|
9146
|
-
async function listPendingReviews(limit, sessionScope) {
|
|
9147
|
-
const client = getClient();
|
|
9148
|
-
const scope = strictSessionScopeFilter(
|
|
9149
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
9150
|
-
);
|
|
9151
|
-
const result = await client.execute({
|
|
9152
|
-
sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
|
|
9153
|
-
WHERE status = 'needs_review'${scope.sql}
|
|
9154
|
-
ORDER BY updated_at ASC LIMIT ?`,
|
|
9155
|
-
args: [...scope.args, limit]
|
|
9156
|
-
});
|
|
9157
|
-
return result.rows;
|
|
9158
|
-
}
|
|
9159
|
-
async function cleanupOrphanedReviews() {
|
|
9160
|
-
const client = getClient();
|
|
9161
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9162
|
-
const r1 = await client.execute({
|
|
9163
|
-
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
9164
|
-
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
9165
|
-
AND assigned_by = 'system'
|
|
9166
|
-
AND title LIKE 'Review:%'
|
|
9167
|
-
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
|
|
9168
|
-
args: [now]
|
|
9169
|
-
});
|
|
9170
|
-
const r1b = await client.execute({
|
|
9171
|
-
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
9172
|
-
WHERE status IN ('open', 'needs_review')
|
|
9173
|
-
AND title LIKE 'Review:%completed%'
|
|
9174
|
-
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')))`,
|
|
9175
|
-
args: [now]
|
|
9176
|
-
});
|
|
9177
|
-
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
9178
|
-
const r2 = await client.execute({
|
|
9179
|
-
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
9180
|
-
WHERE status = 'needs_review'
|
|
9181
|
-
AND result IS NOT NULL
|
|
9182
|
-
AND updated_at < ?`,
|
|
9183
|
-
args: [now, staleThreshold]
|
|
9184
|
-
});
|
|
9185
|
-
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
9186
|
-
if (total > 0) {
|
|
9187
|
-
process.stderr.write(
|
|
9188
|
-
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
9189
|
-
`
|
|
9190
|
-
);
|
|
9191
|
-
}
|
|
9192
|
-
return total;
|
|
9193
|
-
}
|
|
9194
|
-
function getReviewChecklist(role, agent, taskSlug) {
|
|
9195
|
-
const roleLower = role.toLowerCase();
|
|
9196
|
-
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
9197
|
-
return {
|
|
9198
|
-
lens: "Code Quality (Engineer)",
|
|
9199
|
-
checklist: [
|
|
9200
|
-
"1. Do all tests pass? Any new tests needed?",
|
|
9201
|
-
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
9202
|
-
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
9203
|
-
"4. Any regressions in the test suite?"
|
|
9204
|
-
]
|
|
9205
|
-
};
|
|
9206
|
-
}
|
|
9207
|
-
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
9208
|
-
return {
|
|
9209
|
-
lens: "Architecture (CTO)",
|
|
9210
|
-
checklist: [
|
|
9211
|
-
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
9212
|
-
"2. Is it backward compatible? Any breaking changes?",
|
|
9213
|
-
"3. Does it introduce technical debt? Is that debt justified?",
|
|
9214
|
-
"4. Security implications? Any new attack surface?",
|
|
9215
|
-
"5. Does it scale? Performance considerations?",
|
|
9216
|
-
"6. Coordination: does this affect other employees' work or other projects?"
|
|
9217
|
-
]
|
|
9218
|
-
};
|
|
9219
|
-
}
|
|
9220
|
-
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
9221
|
-
return {
|
|
9222
|
-
lens: "Strategic (COO)",
|
|
9223
|
-
checklist: [
|
|
9224
|
-
"1. Does this serve the project mission?",
|
|
9225
|
-
"2. Is this the right work at the right time?",
|
|
9226
|
-
"3. Does the architectural assessment make sense for the business?",
|
|
9227
|
-
"4. Any cross-project implications?"
|
|
9228
|
-
]
|
|
9229
|
-
};
|
|
9230
|
-
}
|
|
9231
|
-
return {
|
|
9232
|
-
lens: "General",
|
|
9233
|
-
checklist: [
|
|
9234
|
-
"1. Read the original task's acceptance criteria",
|
|
9235
|
-
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
9236
|
-
"3. Verify code changes match requirements",
|
|
9237
|
-
"4. Check if tests were added/updated",
|
|
9238
|
-
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
9239
|
-
]
|
|
9240
|
-
};
|
|
9241
|
-
}
|
|
9242
|
-
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
9243
|
-
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
9244
|
-
try {
|
|
9245
|
-
const client = getClient();
|
|
9246
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9247
|
-
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
9248
|
-
if (parentId) {
|
|
9249
|
-
const result = await client.execute({
|
|
9250
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
9251
|
-
args: [now, parentId]
|
|
9252
|
-
});
|
|
9253
|
-
if (result.rowsAffected > 0) {
|
|
9254
|
-
process.stderr.write(
|
|
9255
|
-
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
9256
|
-
`
|
|
9257
|
-
);
|
|
9258
|
-
}
|
|
9259
|
-
} else {
|
|
9260
|
-
const fileName = taskFile.split("/").pop() ?? "";
|
|
9261
|
-
const reviewPrefix = fileName.replace(".md", "");
|
|
9262
|
-
const parts = reviewPrefix.split("-");
|
|
9263
|
-
if (parts.length >= 3 && parts[0] === "review") {
|
|
9264
|
-
const agent = parts[1];
|
|
9265
|
-
const slug = parts.slice(2).join("-");
|
|
9266
|
-
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
9267
|
-
const result = await client.execute({
|
|
9268
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
9269
|
-
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
9270
|
-
});
|
|
9271
|
-
if (result.rowsAffected > 0) {
|
|
9272
|
-
process.stderr.write(
|
|
9273
|
-
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
9274
|
-
`
|
|
9275
|
-
);
|
|
9276
|
-
}
|
|
9277
|
-
}
|
|
9278
|
-
}
|
|
9279
|
-
} catch (err) {
|
|
9280
|
-
process.stderr.write(
|
|
9281
|
-
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
9282
|
-
`
|
|
9283
|
-
);
|
|
9284
|
-
}
|
|
9285
|
-
try {
|
|
9286
|
-
const cacheDir = path22.join(EXE_AI_DIR, "session-cache");
|
|
9287
|
-
if (existsSync18(cacheDir)) {
|
|
9288
|
-
for (const f of readdirSync6(cacheDir)) {
|
|
9289
|
-
if (f.startsWith("review-notified-")) {
|
|
9290
|
-
unlinkSync6(path22.join(cacheDir, f));
|
|
9291
|
-
}
|
|
9292
|
-
}
|
|
9293
|
-
}
|
|
9294
|
-
} catch {
|
|
9295
|
-
}
|
|
9296
|
-
}
|
|
9297
|
-
var init_tasks_review = __esm({
|
|
9298
|
-
"src/lib/tasks-review.ts"() {
|
|
9299
|
-
"use strict";
|
|
9300
|
-
init_database();
|
|
9301
|
-
init_config();
|
|
9302
|
-
init_employees();
|
|
9303
|
-
init_notifications();
|
|
9304
|
-
init_tmux_routing();
|
|
9305
|
-
init_session_key();
|
|
9306
|
-
init_state_bus();
|
|
9307
|
-
init_task_scope();
|
|
9308
|
-
}
|
|
9309
|
-
});
|
|
9310
|
-
|
|
9311
9447
|
// src/lib/tasks-chain.ts
|
|
9312
9448
|
import path23 from "path";
|
|
9313
9449
|
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
@@ -19823,6 +19959,12 @@ async function cloudSync(config2) {
|
|
|
19823
19959
|
pulled = pullResult.records.length;
|
|
19824
19960
|
}
|
|
19825
19961
|
}
|
|
19962
|
+
if (pulled > 0) {
|
|
19963
|
+
try {
|
|
19964
|
+
await pushToPostgres(pullResult.records);
|
|
19965
|
+
} catch {
|
|
19966
|
+
}
|
|
19967
|
+
}
|
|
19826
19968
|
if (pullResult.maxVersion > lastPullVersion) {
|
|
19827
19969
|
await client.execute({
|
|
19828
19970
|
sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
|