@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
|
@@ -3687,6 +3687,314 @@ var init_capacity_monitor = __esm({
|
|
|
3687
3687
|
}
|
|
3688
3688
|
});
|
|
3689
3689
|
|
|
3690
|
+
// src/lib/tasks-review.ts
|
|
3691
|
+
var tasks_review_exports = {};
|
|
3692
|
+
__export(tasks_review_exports, {
|
|
3693
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
3694
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
3695
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
3696
|
+
countPendingReviews: () => countPendingReviews,
|
|
3697
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
3698
|
+
formatAge: () => formatAge,
|
|
3699
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
3700
|
+
isStale: () => isStale,
|
|
3701
|
+
listPendingReviews: () => listPendingReviews
|
|
3702
|
+
});
|
|
3703
|
+
import path11 from "path";
|
|
3704
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
3705
|
+
function formatAge(isoTimestamp) {
|
|
3706
|
+
if (!isoTimestamp) return "";
|
|
3707
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
3708
|
+
if (ms < 0) return "just now";
|
|
3709
|
+
const minutes = Math.floor(ms / 6e4);
|
|
3710
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
3711
|
+
const hours = Math.floor(minutes / 60);
|
|
3712
|
+
if (hours < 24) return `${hours}h ago`;
|
|
3713
|
+
const days = Math.floor(hours / 24);
|
|
3714
|
+
return `${days}d ago`;
|
|
3715
|
+
}
|
|
3716
|
+
function isStale(isoTimestamp) {
|
|
3717
|
+
if (!isoTimestamp) return false;
|
|
3718
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
3719
|
+
}
|
|
3720
|
+
async function countPendingReviews(sessionScope) {
|
|
3721
|
+
const client = getClient();
|
|
3722
|
+
const scope = strictSessionScopeFilter(
|
|
3723
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
3724
|
+
);
|
|
3725
|
+
const result = await client.execute({
|
|
3726
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3727
|
+
WHERE status = 'needs_review'${scope.sql}`,
|
|
3728
|
+
args: [...scope.args]
|
|
3729
|
+
});
|
|
3730
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3731
|
+
}
|
|
3732
|
+
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
3733
|
+
const client = getClient();
|
|
3734
|
+
const scope = strictSessionScopeFilter(
|
|
3735
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
3736
|
+
);
|
|
3737
|
+
const result = await client.execute({
|
|
3738
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3739
|
+
WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
|
|
3740
|
+
args: [sinceIso, ...scope.args]
|
|
3741
|
+
});
|
|
3742
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3743
|
+
}
|
|
3744
|
+
async function listPendingReviews(limit, sessionScope) {
|
|
3745
|
+
const client = getClient();
|
|
3746
|
+
const scope = strictSessionScopeFilter(
|
|
3747
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
3748
|
+
);
|
|
3749
|
+
const result = await client.execute({
|
|
3750
|
+
sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
|
|
3751
|
+
WHERE status = 'needs_review'${scope.sql}
|
|
3752
|
+
ORDER BY updated_at ASC LIMIT ?`,
|
|
3753
|
+
args: [...scope.args, limit]
|
|
3754
|
+
});
|
|
3755
|
+
return result.rows;
|
|
3756
|
+
}
|
|
3757
|
+
async function cleanupOrphanedReviews() {
|
|
3758
|
+
const client = getClient();
|
|
3759
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3760
|
+
const r1 = await client.execute({
|
|
3761
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
3762
|
+
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
3763
|
+
AND assigned_by = 'system'
|
|
3764
|
+
AND title LIKE 'Review:%'
|
|
3765
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
|
|
3766
|
+
args: [now]
|
|
3767
|
+
});
|
|
3768
|
+
const r1b = await client.execute({
|
|
3769
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
3770
|
+
WHERE status IN ('open', 'needs_review')
|
|
3771
|
+
AND title LIKE 'Review:%completed%'
|
|
3772
|
+
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')))`,
|
|
3773
|
+
args: [now]
|
|
3774
|
+
});
|
|
3775
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
3776
|
+
const r2 = await client.execute({
|
|
3777
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3778
|
+
WHERE status = 'needs_review'
|
|
3779
|
+
AND result IS NOT NULL
|
|
3780
|
+
AND updated_at < ?`,
|
|
3781
|
+
args: [now, staleThreshold]
|
|
3782
|
+
});
|
|
3783
|
+
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
3784
|
+
if (total > 0) {
|
|
3785
|
+
process.stderr.write(
|
|
3786
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
3787
|
+
`
|
|
3788
|
+
);
|
|
3789
|
+
}
|
|
3790
|
+
return total;
|
|
3791
|
+
}
|
|
3792
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
3793
|
+
const roleLower = role.toLowerCase();
|
|
3794
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
3795
|
+
return {
|
|
3796
|
+
lens: "Code Quality (Engineer)",
|
|
3797
|
+
checklist: [
|
|
3798
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
3799
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
3800
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
3801
|
+
"4. Any regressions in the test suite?"
|
|
3802
|
+
]
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
3806
|
+
return {
|
|
3807
|
+
lens: "Architecture (CTO)",
|
|
3808
|
+
checklist: [
|
|
3809
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
3810
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
3811
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
3812
|
+
"4. Security implications? Any new attack surface?",
|
|
3813
|
+
"5. Does it scale? Performance considerations?",
|
|
3814
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
3815
|
+
]
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
3819
|
+
return {
|
|
3820
|
+
lens: "Strategic (COO)",
|
|
3821
|
+
checklist: [
|
|
3822
|
+
"1. Does this serve the project mission?",
|
|
3823
|
+
"2. Is this the right work at the right time?",
|
|
3824
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
3825
|
+
"4. Any cross-project implications?"
|
|
3826
|
+
]
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
return {
|
|
3830
|
+
lens: "General",
|
|
3831
|
+
checklist: [
|
|
3832
|
+
"1. Read the original task's acceptance criteria",
|
|
3833
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
3834
|
+
"3. Verify code changes match requirements",
|
|
3835
|
+
"4. Check if tests were added/updated",
|
|
3836
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
3837
|
+
]
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
3841
|
+
const taskFile = String(row.task_file);
|
|
3842
|
+
const employees = await loadEmployees();
|
|
3843
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
3844
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
3845
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
3846
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
3847
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
3848
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
3849
|
+
const client = getClient();
|
|
3850
|
+
const agent = String(row.assigned_to);
|
|
3851
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
3852
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
3853
|
+
const currentStatus = String(row.status ?? "");
|
|
3854
|
+
if (currentStatus === "done") return;
|
|
3855
|
+
const existingResult = String(row.result ?? "");
|
|
3856
|
+
if (existingResult.includes("## Review notes")) return;
|
|
3857
|
+
let reviewerRole = "unknown";
|
|
3858
|
+
try {
|
|
3859
|
+
const emp = getEmployee(employees, reviewer);
|
|
3860
|
+
if (emp) reviewerRole = emp.role;
|
|
3861
|
+
} catch {
|
|
3862
|
+
}
|
|
3863
|
+
const taskTitle = String(row.title);
|
|
3864
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
3865
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
3866
|
+
process.stderr.write(
|
|
3867
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
3868
|
+
`
|
|
3869
|
+
);
|
|
3870
|
+
const reviewNotes = [
|
|
3871
|
+
`
|
|
3872
|
+
---
|
|
3873
|
+
## Review notes`,
|
|
3874
|
+
`Review lens: ${lens}`,
|
|
3875
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
3876
|
+
"",
|
|
3877
|
+
"### Checklist",
|
|
3878
|
+
...checklist,
|
|
3879
|
+
"",
|
|
3880
|
+
"### Verdict",
|
|
3881
|
+
"- **Approved:** mark this task as done",
|
|
3882
|
+
"- **Needs work:** re-open with notes"
|
|
3883
|
+
].join("\n");
|
|
3884
|
+
const originalTaskId = String(row.id);
|
|
3885
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
3886
|
+
await client.execute({
|
|
3887
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
3888
|
+
WHERE id = ?`,
|
|
3889
|
+
args: [updatedResult, now, originalTaskId]
|
|
3890
|
+
});
|
|
3891
|
+
orgBus.emit({
|
|
3892
|
+
type: "review_created",
|
|
3893
|
+
reviewId: originalTaskId,
|
|
3894
|
+
employee: agent,
|
|
3895
|
+
reviewer,
|
|
3896
|
+
timestamp: now
|
|
3897
|
+
});
|
|
3898
|
+
await writeNotification({
|
|
3899
|
+
agentId: agent,
|
|
3900
|
+
agentRole: String(row.assigned_to),
|
|
3901
|
+
event: "task_complete",
|
|
3902
|
+
project: String(row.project_name),
|
|
3903
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
3904
|
+
taskFile
|
|
3905
|
+
});
|
|
3906
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
3907
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
3908
|
+
if (!autoApprove) {
|
|
3909
|
+
try {
|
|
3910
|
+
const key = getSessionKey();
|
|
3911
|
+
const exeSession = getParentExe(key);
|
|
3912
|
+
if (exeSession) {
|
|
3913
|
+
sendIntercom(exeSession);
|
|
3914
|
+
}
|
|
3915
|
+
} catch {
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
if (autoApprove) {
|
|
3919
|
+
process.stderr.write(
|
|
3920
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
3921
|
+
`
|
|
3922
|
+
);
|
|
3923
|
+
await client.execute({
|
|
3924
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
3925
|
+
args: [now, originalTaskId]
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
3930
|
+
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
3931
|
+
try {
|
|
3932
|
+
const client = getClient();
|
|
3933
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3934
|
+
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
3935
|
+
if (parentId) {
|
|
3936
|
+
const result = await client.execute({
|
|
3937
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
3938
|
+
args: [now, parentId]
|
|
3939
|
+
});
|
|
3940
|
+
if (result.rowsAffected > 0) {
|
|
3941
|
+
process.stderr.write(
|
|
3942
|
+
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
3943
|
+
`
|
|
3944
|
+
);
|
|
3945
|
+
}
|
|
3946
|
+
} else {
|
|
3947
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
3948
|
+
const reviewPrefix = fileName.replace(".md", "");
|
|
3949
|
+
const parts = reviewPrefix.split("-");
|
|
3950
|
+
if (parts.length >= 3 && parts[0] === "review") {
|
|
3951
|
+
const agent = parts[1];
|
|
3952
|
+
const slug = parts.slice(2).join("-");
|
|
3953
|
+
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
3954
|
+
const result = await client.execute({
|
|
3955
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
3956
|
+
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
3957
|
+
});
|
|
3958
|
+
if (result.rowsAffected > 0) {
|
|
3959
|
+
process.stderr.write(
|
|
3960
|
+
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
3961
|
+
`
|
|
3962
|
+
);
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
} catch (err) {
|
|
3967
|
+
process.stderr.write(
|
|
3968
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
3969
|
+
`
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
try {
|
|
3973
|
+
const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
|
|
3974
|
+
if (existsSync11(cacheDir)) {
|
|
3975
|
+
for (const f of readdirSync2(cacheDir)) {
|
|
3976
|
+
if (f.startsWith("review-notified-")) {
|
|
3977
|
+
unlinkSync2(path11.join(cacheDir, f));
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
} catch {
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
var init_tasks_review = __esm({
|
|
3985
|
+
"src/lib/tasks-review.ts"() {
|
|
3986
|
+
"use strict";
|
|
3987
|
+
init_database();
|
|
3988
|
+
init_config();
|
|
3989
|
+
init_employees();
|
|
3990
|
+
init_notifications();
|
|
3991
|
+
init_tmux_routing();
|
|
3992
|
+
init_session_key();
|
|
3993
|
+
init_state_bus();
|
|
3994
|
+
init_task_scope();
|
|
3995
|
+
}
|
|
3996
|
+
});
|
|
3997
|
+
|
|
3690
3998
|
// src/lib/tmux-routing.ts
|
|
3691
3999
|
var tmux_routing_exports = {};
|
|
3692
4000
|
__export(tmux_routing_exports, {
|
|
@@ -3713,13 +4021,13 @@ __export(tmux_routing_exports, {
|
|
|
3713
4021
|
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
3714
4022
|
});
|
|
3715
4023
|
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
3716
|
-
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as
|
|
3717
|
-
import
|
|
4024
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
|
|
4025
|
+
import path12 from "path";
|
|
3718
4026
|
import os8 from "os";
|
|
3719
4027
|
import { fileURLToPath } from "url";
|
|
3720
|
-
import { unlinkSync as
|
|
4028
|
+
import { unlinkSync as unlinkSync3 } from "fs";
|
|
3721
4029
|
function spawnLockPath(sessionName) {
|
|
3722
|
-
return
|
|
4030
|
+
return path12.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
3723
4031
|
}
|
|
3724
4032
|
function isProcessAlive(pid) {
|
|
3725
4033
|
try {
|
|
@@ -3730,11 +4038,11 @@ function isProcessAlive(pid) {
|
|
|
3730
4038
|
}
|
|
3731
4039
|
}
|
|
3732
4040
|
function acquireSpawnLock(sessionName) {
|
|
3733
|
-
if (!
|
|
4041
|
+
if (!existsSync12(SPAWN_LOCK_DIR)) {
|
|
3734
4042
|
mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
|
|
3735
4043
|
}
|
|
3736
4044
|
const lockFile = spawnLockPath(sessionName);
|
|
3737
|
-
if (
|
|
4045
|
+
if (existsSync12(lockFile)) {
|
|
3738
4046
|
try {
|
|
3739
4047
|
const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
|
|
3740
4048
|
const age = Date.now() - lock.timestamp;
|
|
@@ -3749,20 +4057,20 @@ function acquireSpawnLock(sessionName) {
|
|
|
3749
4057
|
}
|
|
3750
4058
|
function releaseSpawnLock(sessionName) {
|
|
3751
4059
|
try {
|
|
3752
|
-
|
|
4060
|
+
unlinkSync3(spawnLockPath(sessionName));
|
|
3753
4061
|
} catch {
|
|
3754
4062
|
}
|
|
3755
4063
|
}
|
|
3756
4064
|
function resolveBehaviorsExporterScript() {
|
|
3757
4065
|
try {
|
|
3758
4066
|
const thisFile = fileURLToPath(import.meta.url);
|
|
3759
|
-
const scriptPath =
|
|
3760
|
-
|
|
4067
|
+
const scriptPath = path12.join(
|
|
4068
|
+
path12.dirname(thisFile),
|
|
3761
4069
|
"..",
|
|
3762
4070
|
"bin",
|
|
3763
4071
|
"exe-export-behaviors.js"
|
|
3764
4072
|
);
|
|
3765
|
-
return
|
|
4073
|
+
return existsSync12(scriptPath) ? scriptPath : null;
|
|
3766
4074
|
} catch {
|
|
3767
4075
|
return null;
|
|
3768
4076
|
}
|
|
@@ -3828,11 +4136,11 @@ function extractRootExe(name) {
|
|
|
3828
4136
|
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
3829
4137
|
}
|
|
3830
4138
|
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
3831
|
-
if (!
|
|
4139
|
+
if (!existsSync12(SESSION_CACHE)) {
|
|
3832
4140
|
mkdirSync6(SESSION_CACHE, { recursive: true });
|
|
3833
4141
|
}
|
|
3834
4142
|
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
3835
|
-
const filePath =
|
|
4143
|
+
const filePath = path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
3836
4144
|
writeFileSync6(filePath, JSON.stringify({
|
|
3837
4145
|
parentExe: rootExe,
|
|
3838
4146
|
dispatchedBy: dispatchedBy || rootExe,
|
|
@@ -3841,7 +4149,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
|
3841
4149
|
}
|
|
3842
4150
|
function getParentExe(sessionKey) {
|
|
3843
4151
|
try {
|
|
3844
|
-
const data = JSON.parse(readFileSync8(
|
|
4152
|
+
const data = JSON.parse(readFileSync8(path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
3845
4153
|
return data.parentExe || null;
|
|
3846
4154
|
} catch {
|
|
3847
4155
|
return null;
|
|
@@ -3850,7 +4158,7 @@ function getParentExe(sessionKey) {
|
|
|
3850
4158
|
function getDispatchedBy(sessionKey) {
|
|
3851
4159
|
try {
|
|
3852
4160
|
const data = JSON.parse(readFileSync8(
|
|
3853
|
-
|
|
4161
|
+
path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
3854
4162
|
"utf8"
|
|
3855
4163
|
));
|
|
3856
4164
|
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
@@ -3920,7 +4228,7 @@ async function verifyPaneAtCapacity(sessionName) {
|
|
|
3920
4228
|
}
|
|
3921
4229
|
function readDebounceState() {
|
|
3922
4230
|
try {
|
|
3923
|
-
if (!
|
|
4231
|
+
if (!existsSync12(DEBOUNCE_FILE)) return {};
|
|
3924
4232
|
const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
|
|
3925
4233
|
const state = {};
|
|
3926
4234
|
for (const [key, val] of Object.entries(raw)) {
|
|
@@ -3937,7 +4245,7 @@ function readDebounceState() {
|
|
|
3937
4245
|
}
|
|
3938
4246
|
function writeDebounceState(state) {
|
|
3939
4247
|
try {
|
|
3940
|
-
if (!
|
|
4248
|
+
if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
|
|
3941
4249
|
writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
|
|
3942
4250
|
} catch {
|
|
3943
4251
|
}
|
|
@@ -4033,22 +4341,24 @@ function sendIntercom(targetSession) {
|
|
|
4033
4341
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
4034
4342
|
return "queued";
|
|
4035
4343
|
}
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4344
|
+
if (sessionState !== "idle") {
|
|
4345
|
+
try {
|
|
4346
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
4347
|
+
const agent = baseAgentName(rawAgent);
|
|
4348
|
+
const markerPath = path12.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
4349
|
+
if (existsSync12(markerPath)) {
|
|
4350
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
4351
|
+
return "debounced";
|
|
4352
|
+
}
|
|
4353
|
+
} catch {
|
|
4043
4354
|
}
|
|
4044
|
-
} catch {
|
|
4045
4355
|
}
|
|
4046
4356
|
try {
|
|
4047
4357
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
4048
4358
|
const agent = baseAgentName(rawAgent);
|
|
4049
|
-
const taskDir =
|
|
4050
|
-
if (
|
|
4051
|
-
const files =
|
|
4359
|
+
const taskDir = path12.join(process.cwd(), "exe", agent);
|
|
4360
|
+
if (existsSync12(taskDir)) {
|
|
4361
|
+
const files = readdirSync3(taskDir).filter(
|
|
4052
4362
|
(f) => f.endsWith(".md") && f !== "DONE.txt"
|
|
4053
4363
|
);
|
|
4054
4364
|
if (files.length === 0) {
|
|
@@ -4112,6 +4422,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
4112
4422
|
try {
|
|
4113
4423
|
const sessions = transport.listSessions();
|
|
4114
4424
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
4425
|
+
try {
|
|
4426
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
4427
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
4428
|
+
if (pending instanceof Promise) {
|
|
4429
|
+
pending.then((count) => {
|
|
4430
|
+
if (count > 0) {
|
|
4431
|
+
execSync4(
|
|
4432
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
4433
|
+
{ timeout: 3e3 }
|
|
4434
|
+
);
|
|
4435
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
4436
|
+
}
|
|
4437
|
+
}).catch(() => {
|
|
4438
|
+
});
|
|
4439
|
+
return true;
|
|
4440
|
+
}
|
|
4441
|
+
} catch {
|
|
4442
|
+
}
|
|
4115
4443
|
execSync4(
|
|
4116
4444
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
4117
4445
|
{ timeout: 3e3 }
|
|
@@ -4195,23 +4523,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4195
4523
|
const transport = getTransport();
|
|
4196
4524
|
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
4197
4525
|
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
4198
|
-
const logDir =
|
|
4199
|
-
const logFile =
|
|
4200
|
-
if (!
|
|
4526
|
+
const logDir = path12.join(os8.homedir(), ".exe-os", "session-logs");
|
|
4527
|
+
const logFile = path12.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
4528
|
+
if (!existsSync12(logDir)) {
|
|
4201
4529
|
mkdirSync6(logDir, { recursive: true });
|
|
4202
4530
|
}
|
|
4203
4531
|
transport.kill(sessionName);
|
|
4204
4532
|
let cleanupSuffix = "";
|
|
4205
4533
|
try {
|
|
4206
4534
|
const thisFile = fileURLToPath(import.meta.url);
|
|
4207
|
-
const cleanupScript =
|
|
4208
|
-
if (
|
|
4535
|
+
const cleanupScript = path12.join(path12.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
4536
|
+
if (existsSync12(cleanupScript)) {
|
|
4209
4537
|
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
4210
4538
|
}
|
|
4211
4539
|
} catch {
|
|
4212
4540
|
}
|
|
4213
4541
|
try {
|
|
4214
|
-
const claudeJsonPath =
|
|
4542
|
+
const claudeJsonPath = path12.join(os8.homedir(), ".claude.json");
|
|
4215
4543
|
let claudeJson = {};
|
|
4216
4544
|
try {
|
|
4217
4545
|
claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
|
|
@@ -4226,10 +4554,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4226
4554
|
} catch {
|
|
4227
4555
|
}
|
|
4228
4556
|
try {
|
|
4229
|
-
const settingsDir =
|
|
4557
|
+
const settingsDir = path12.join(os8.homedir(), ".claude", "projects");
|
|
4230
4558
|
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
4231
|
-
const projSettingsDir =
|
|
4232
|
-
const settingsPath =
|
|
4559
|
+
const projSettingsDir = path12.join(settingsDir, normalizedKey);
|
|
4560
|
+
const settingsPath = path12.join(projSettingsDir, "settings.json");
|
|
4233
4561
|
let settings = {};
|
|
4234
4562
|
try {
|
|
4235
4563
|
settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
|
|
@@ -4276,7 +4604,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4276
4604
|
let behaviorsFlag = "";
|
|
4277
4605
|
let legacyFallbackWarned = false;
|
|
4278
4606
|
if (!useExeAgent && !useBinSymlink) {
|
|
4279
|
-
const identityPath =
|
|
4607
|
+
const identityPath = path12.join(
|
|
4280
4608
|
os8.homedir(),
|
|
4281
4609
|
".exe-os",
|
|
4282
4610
|
"identity",
|
|
@@ -4286,13 +4614,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4286
4614
|
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
4287
4615
|
if (hasAgentFlag) {
|
|
4288
4616
|
identityFlag = ` --agent ${employeeName}`;
|
|
4289
|
-
} else if (
|
|
4617
|
+
} else if (existsSync12(identityPath)) {
|
|
4290
4618
|
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
4291
4619
|
legacyFallbackWarned = true;
|
|
4292
4620
|
}
|
|
4293
4621
|
const behaviorsFile = exportBehaviorsSync(
|
|
4294
4622
|
employeeName,
|
|
4295
|
-
|
|
4623
|
+
path12.basename(spawnCwd),
|
|
4296
4624
|
sessionName
|
|
4297
4625
|
);
|
|
4298
4626
|
if (behaviorsFile) {
|
|
@@ -4307,9 +4635,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4307
4635
|
}
|
|
4308
4636
|
let sessionContextFlag = "";
|
|
4309
4637
|
try {
|
|
4310
|
-
const ctxDir =
|
|
4638
|
+
const ctxDir = path12.join(os8.homedir(), ".exe-os", "session-cache");
|
|
4311
4639
|
mkdirSync6(ctxDir, { recursive: true });
|
|
4312
|
-
const ctxFile =
|
|
4640
|
+
const ctxFile = path12.join(ctxDir, `session-context-${sessionName}.md`);
|
|
4313
4641
|
const ctxContent = [
|
|
4314
4642
|
`## Session Context`,
|
|
4315
4643
|
`You are running in tmux session: ${sessionName}.`,
|
|
@@ -4393,7 +4721,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4393
4721
|
transport.pipeLog(sessionName, logFile);
|
|
4394
4722
|
try {
|
|
4395
4723
|
const mySession = getMySession();
|
|
4396
|
-
const dispatchInfo =
|
|
4724
|
+
const dispatchInfo = path12.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
4397
4725
|
writeFileSync6(dispatchInfo, JSON.stringify({
|
|
4398
4726
|
dispatchedBy: mySession,
|
|
4399
4727
|
rootExe: exeSession,
|
|
@@ -4468,15 +4796,15 @@ var init_tmux_routing = __esm({
|
|
|
4468
4796
|
init_intercom_queue();
|
|
4469
4797
|
init_plan_limits();
|
|
4470
4798
|
init_employees();
|
|
4471
|
-
SPAWN_LOCK_DIR =
|
|
4472
|
-
SESSION_CACHE =
|
|
4799
|
+
SPAWN_LOCK_DIR = path12.join(os8.homedir(), ".exe-os", "spawn-locks");
|
|
4800
|
+
SESSION_CACHE = path12.join(os8.homedir(), ".exe-os", "session-cache");
|
|
4473
4801
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
4474
4802
|
VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
4475
4803
|
VERIFY_PANE_LINES = 200;
|
|
4476
4804
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
4477
4805
|
CODEX_DEBOUNCE_MS = 12e4;
|
|
4478
|
-
INTERCOM_LOG2 =
|
|
4479
|
-
DEBOUNCE_FILE =
|
|
4806
|
+
INTERCOM_LOG2 = path12.join(os8.homedir(), ".exe-os", "intercom.log");
|
|
4807
|
+
DEBOUNCE_FILE = path12.join(SESSION_CACHE, "intercom-debounce.json");
|
|
4480
4808
|
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
4481
4809
|
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
|
|
4482
4810
|
}
|
|
@@ -4517,13 +4845,13 @@ var init_task_scope = __esm({
|
|
|
4517
4845
|
|
|
4518
4846
|
// src/lib/notifications.ts
|
|
4519
4847
|
import crypto2 from "crypto";
|
|
4520
|
-
import
|
|
4848
|
+
import path13 from "path";
|
|
4521
4849
|
import os9 from "os";
|
|
4522
4850
|
import {
|
|
4523
4851
|
readFileSync as readFileSync9,
|
|
4524
|
-
readdirSync as
|
|
4525
|
-
unlinkSync as
|
|
4526
|
-
existsSync as
|
|
4852
|
+
readdirSync as readdirSync4,
|
|
4853
|
+
unlinkSync as unlinkSync4,
|
|
4854
|
+
existsSync as existsSync13,
|
|
4527
4855
|
rmdirSync
|
|
4528
4856
|
} from "fs";
|
|
4529
4857
|
async function writeNotification(notification) {
|
|
@@ -4574,7 +4902,7 @@ var init_notifications = __esm({
|
|
|
4574
4902
|
|
|
4575
4903
|
// src/lib/project-name.ts
|
|
4576
4904
|
import { execSync as execSync5 } from "child_process";
|
|
4577
|
-
import
|
|
4905
|
+
import path14 from "path";
|
|
4578
4906
|
function getProjectName(cwd) {
|
|
4579
4907
|
const dir = cwd ?? process.cwd();
|
|
4580
4908
|
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
@@ -4587,7 +4915,7 @@ function getProjectName(cwd) {
|
|
|
4587
4915
|
timeout: 2e3,
|
|
4588
4916
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4589
4917
|
}).trim();
|
|
4590
|
-
repoRoot =
|
|
4918
|
+
repoRoot = path14.dirname(gitCommonDir);
|
|
4591
4919
|
} catch {
|
|
4592
4920
|
repoRoot = execSync5("git rev-parse --show-toplevel", {
|
|
4593
4921
|
cwd: dir,
|
|
@@ -4596,11 +4924,11 @@ function getProjectName(cwd) {
|
|
|
4596
4924
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4597
4925
|
}).trim();
|
|
4598
4926
|
}
|
|
4599
|
-
_cached2 =
|
|
4927
|
+
_cached2 = path14.basename(repoRoot);
|
|
4600
4928
|
_cachedCwd = dir;
|
|
4601
4929
|
return _cached2;
|
|
4602
4930
|
} catch {
|
|
4603
|
-
_cached2 =
|
|
4931
|
+
_cached2 = path14.basename(dir);
|
|
4604
4932
|
_cachedCwd = dir;
|
|
4605
4933
|
return _cached2;
|
|
4606
4934
|
}
|
|
@@ -4678,11 +5006,11 @@ var init_session_scope = __esm({
|
|
|
4678
5006
|
|
|
4679
5007
|
// src/lib/tasks-crud.ts
|
|
4680
5008
|
import crypto3 from "crypto";
|
|
4681
|
-
import
|
|
5009
|
+
import path15 from "path";
|
|
4682
5010
|
import os10 from "os";
|
|
4683
5011
|
import { execSync as execSync6 } from "child_process";
|
|
4684
5012
|
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
4685
|
-
import { existsSync as
|
|
5013
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
|
|
4686
5014
|
async function writeCheckpoint(input) {
|
|
4687
5015
|
const client = getClient();
|
|
4688
5016
|
const row = await resolveTask(client, input.taskId);
|
|
@@ -4876,8 +5204,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
|
|
|
4876
5204
|
}
|
|
4877
5205
|
if (input.baseDir) {
|
|
4878
5206
|
try {
|
|
4879
|
-
await mkdir4(
|
|
4880
|
-
await mkdir4(
|
|
5207
|
+
await mkdir4(path15.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
5208
|
+
await mkdir4(path15.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
4881
5209
|
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
4882
5210
|
await ensureGitignoreExe(input.baseDir);
|
|
4883
5211
|
} catch {
|
|
@@ -4913,10 +5241,10 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
|
|
|
4913
5241
|
});
|
|
4914
5242
|
if (input.baseDir) {
|
|
4915
5243
|
try {
|
|
4916
|
-
const EXE_OS_DIR =
|
|
4917
|
-
const mdPath =
|
|
4918
|
-
const mdDir =
|
|
4919
|
-
if (!
|
|
5244
|
+
const EXE_OS_DIR = path15.join(os10.homedir(), ".exe-os");
|
|
5245
|
+
const mdPath = path15.join(EXE_OS_DIR, taskFile);
|
|
5246
|
+
const mdDir = path15.dirname(mdPath);
|
|
5247
|
+
if (!existsSync14(mdDir)) await mkdir4(mdDir, { recursive: true });
|
|
4920
5248
|
const reviewer = input.reviewer ?? input.assignedBy;
|
|
4921
5249
|
const mdContent = `# ${input.title}
|
|
4922
5250
|
|
|
@@ -5216,9 +5544,9 @@ async function deleteTaskCore(taskId, _baseDir) {
|
|
|
5216
5544
|
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
5217
5545
|
}
|
|
5218
5546
|
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
5219
|
-
const archPath =
|
|
5547
|
+
const archPath = path15.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
5220
5548
|
try {
|
|
5221
|
-
if (
|
|
5549
|
+
if (existsSync14(archPath)) return;
|
|
5222
5550
|
const template = [
|
|
5223
5551
|
`# ${projectName} \u2014 System Architecture`,
|
|
5224
5552
|
"",
|
|
@@ -5251,9 +5579,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
|
|
|
5251
5579
|
}
|
|
5252
5580
|
}
|
|
5253
5581
|
async function ensureGitignoreExe(baseDir) {
|
|
5254
|
-
const gitignorePath =
|
|
5582
|
+
const gitignorePath = path15.join(baseDir, ".gitignore");
|
|
5255
5583
|
try {
|
|
5256
|
-
if (
|
|
5584
|
+
if (existsSync14(gitignorePath)) {
|
|
5257
5585
|
const content = readFileSync10(gitignorePath, "utf-8");
|
|
5258
5586
|
if (/^\/?exe\/?$/m.test(content)) return;
|
|
5259
5587
|
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
@@ -5284,198 +5612,6 @@ var init_tasks_crud = __esm({
|
|
|
5284
5612
|
}
|
|
5285
5613
|
});
|
|
5286
5614
|
|
|
5287
|
-
// src/lib/tasks-review.ts
|
|
5288
|
-
import path15 from "path";
|
|
5289
|
-
import { existsSync as existsSync14, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
5290
|
-
async function countPendingReviews(sessionScope) {
|
|
5291
|
-
const client = getClient();
|
|
5292
|
-
const scope = strictSessionScopeFilter(
|
|
5293
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
5294
|
-
);
|
|
5295
|
-
const result = await client.execute({
|
|
5296
|
-
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
5297
|
-
WHERE status = 'needs_review'${scope.sql}`,
|
|
5298
|
-
args: [...scope.args]
|
|
5299
|
-
});
|
|
5300
|
-
return Number(result.rows[0]?.cnt) || 0;
|
|
5301
|
-
}
|
|
5302
|
-
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
5303
|
-
const client = getClient();
|
|
5304
|
-
const scope = strictSessionScopeFilter(
|
|
5305
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
5306
|
-
);
|
|
5307
|
-
const result = await client.execute({
|
|
5308
|
-
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
5309
|
-
WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
|
|
5310
|
-
args: [sinceIso, ...scope.args]
|
|
5311
|
-
});
|
|
5312
|
-
return Number(result.rows[0]?.cnt) || 0;
|
|
5313
|
-
}
|
|
5314
|
-
async function listPendingReviews(limit, sessionScope) {
|
|
5315
|
-
const client = getClient();
|
|
5316
|
-
const scope = strictSessionScopeFilter(
|
|
5317
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
5318
|
-
);
|
|
5319
|
-
const result = await client.execute({
|
|
5320
|
-
sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
|
|
5321
|
-
WHERE status = 'needs_review'${scope.sql}
|
|
5322
|
-
ORDER BY updated_at ASC LIMIT ?`,
|
|
5323
|
-
args: [...scope.args, limit]
|
|
5324
|
-
});
|
|
5325
|
-
return result.rows;
|
|
5326
|
-
}
|
|
5327
|
-
async function cleanupOrphanedReviews() {
|
|
5328
|
-
const client = getClient();
|
|
5329
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5330
|
-
const r1 = await client.execute({
|
|
5331
|
-
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
5332
|
-
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
5333
|
-
AND assigned_by = 'system'
|
|
5334
|
-
AND title LIKE 'Review:%'
|
|
5335
|
-
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
|
|
5336
|
-
args: [now]
|
|
5337
|
-
});
|
|
5338
|
-
const r1b = await client.execute({
|
|
5339
|
-
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
5340
|
-
WHERE status IN ('open', 'needs_review')
|
|
5341
|
-
AND title LIKE 'Review:%completed%'
|
|
5342
|
-
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')))`,
|
|
5343
|
-
args: [now]
|
|
5344
|
-
});
|
|
5345
|
-
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
5346
|
-
const r2 = await client.execute({
|
|
5347
|
-
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
5348
|
-
WHERE status = 'needs_review'
|
|
5349
|
-
AND result IS NOT NULL
|
|
5350
|
-
AND updated_at < ?`,
|
|
5351
|
-
args: [now, staleThreshold]
|
|
5352
|
-
});
|
|
5353
|
-
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
5354
|
-
if (total > 0) {
|
|
5355
|
-
process.stderr.write(
|
|
5356
|
-
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
5357
|
-
`
|
|
5358
|
-
);
|
|
5359
|
-
}
|
|
5360
|
-
return total;
|
|
5361
|
-
}
|
|
5362
|
-
function getReviewChecklist(role, agent, taskSlug) {
|
|
5363
|
-
const roleLower = role.toLowerCase();
|
|
5364
|
-
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
5365
|
-
return {
|
|
5366
|
-
lens: "Code Quality (Engineer)",
|
|
5367
|
-
checklist: [
|
|
5368
|
-
"1. Do all tests pass? Any new tests needed?",
|
|
5369
|
-
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
5370
|
-
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
5371
|
-
"4. Any regressions in the test suite?"
|
|
5372
|
-
]
|
|
5373
|
-
};
|
|
5374
|
-
}
|
|
5375
|
-
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
5376
|
-
return {
|
|
5377
|
-
lens: "Architecture (CTO)",
|
|
5378
|
-
checklist: [
|
|
5379
|
-
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
5380
|
-
"2. Is it backward compatible? Any breaking changes?",
|
|
5381
|
-
"3. Does it introduce technical debt? Is that debt justified?",
|
|
5382
|
-
"4. Security implications? Any new attack surface?",
|
|
5383
|
-
"5. Does it scale? Performance considerations?",
|
|
5384
|
-
"6. Coordination: does this affect other employees' work or other projects?"
|
|
5385
|
-
]
|
|
5386
|
-
};
|
|
5387
|
-
}
|
|
5388
|
-
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
5389
|
-
return {
|
|
5390
|
-
lens: "Strategic (COO)",
|
|
5391
|
-
checklist: [
|
|
5392
|
-
"1. Does this serve the project mission?",
|
|
5393
|
-
"2. Is this the right work at the right time?",
|
|
5394
|
-
"3. Does the architectural assessment make sense for the business?",
|
|
5395
|
-
"4. Any cross-project implications?"
|
|
5396
|
-
]
|
|
5397
|
-
};
|
|
5398
|
-
}
|
|
5399
|
-
return {
|
|
5400
|
-
lens: "General",
|
|
5401
|
-
checklist: [
|
|
5402
|
-
"1. Read the original task's acceptance criteria",
|
|
5403
|
-
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
5404
|
-
"3. Verify code changes match requirements",
|
|
5405
|
-
"4. Check if tests were added/updated",
|
|
5406
|
-
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
5407
|
-
]
|
|
5408
|
-
};
|
|
5409
|
-
}
|
|
5410
|
-
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
5411
|
-
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
5412
|
-
try {
|
|
5413
|
-
const client = getClient();
|
|
5414
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5415
|
-
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
5416
|
-
if (parentId) {
|
|
5417
|
-
const result = await client.execute({
|
|
5418
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
5419
|
-
args: [now, parentId]
|
|
5420
|
-
});
|
|
5421
|
-
if (result.rowsAffected > 0) {
|
|
5422
|
-
process.stderr.write(
|
|
5423
|
-
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
5424
|
-
`
|
|
5425
|
-
);
|
|
5426
|
-
}
|
|
5427
|
-
} else {
|
|
5428
|
-
const fileName = taskFile.split("/").pop() ?? "";
|
|
5429
|
-
const reviewPrefix = fileName.replace(".md", "");
|
|
5430
|
-
const parts = reviewPrefix.split("-");
|
|
5431
|
-
if (parts.length >= 3 && parts[0] === "review") {
|
|
5432
|
-
const agent = parts[1];
|
|
5433
|
-
const slug = parts.slice(2).join("-");
|
|
5434
|
-
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
5435
|
-
const result = await client.execute({
|
|
5436
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
5437
|
-
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
5438
|
-
});
|
|
5439
|
-
if (result.rowsAffected > 0) {
|
|
5440
|
-
process.stderr.write(
|
|
5441
|
-
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
5442
|
-
`
|
|
5443
|
-
);
|
|
5444
|
-
}
|
|
5445
|
-
}
|
|
5446
|
-
}
|
|
5447
|
-
} catch (err) {
|
|
5448
|
-
process.stderr.write(
|
|
5449
|
-
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
5450
|
-
`
|
|
5451
|
-
);
|
|
5452
|
-
}
|
|
5453
|
-
try {
|
|
5454
|
-
const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
|
|
5455
|
-
if (existsSync14(cacheDir)) {
|
|
5456
|
-
for (const f of readdirSync4(cacheDir)) {
|
|
5457
|
-
if (f.startsWith("review-notified-")) {
|
|
5458
|
-
unlinkSync4(path15.join(cacheDir, f));
|
|
5459
|
-
}
|
|
5460
|
-
}
|
|
5461
|
-
}
|
|
5462
|
-
} catch {
|
|
5463
|
-
}
|
|
5464
|
-
}
|
|
5465
|
-
var init_tasks_review = __esm({
|
|
5466
|
-
"src/lib/tasks-review.ts"() {
|
|
5467
|
-
"use strict";
|
|
5468
|
-
init_database();
|
|
5469
|
-
init_config();
|
|
5470
|
-
init_employees();
|
|
5471
|
-
init_notifications();
|
|
5472
|
-
init_tmux_routing();
|
|
5473
|
-
init_session_key();
|
|
5474
|
-
init_state_bus();
|
|
5475
|
-
init_task_scope();
|
|
5476
|
-
}
|
|
5477
|
-
});
|
|
5478
|
-
|
|
5479
5615
|
// src/lib/tasks-chain.ts
|
|
5480
5616
|
import path16 from "path";
|
|
5481
5617
|
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|