@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/bin/cli.js
CHANGED
|
@@ -5261,6 +5261,12 @@ async function cloudSync(config) {
|
|
|
5261
5261
|
pulled = pullResult.records.length;
|
|
5262
5262
|
}
|
|
5263
5263
|
}
|
|
5264
|
+
if (pulled > 0) {
|
|
5265
|
+
try {
|
|
5266
|
+
await pushToPostgres(pullResult.records);
|
|
5267
|
+
} catch {
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5264
5270
|
if (pullResult.maxVersion > lastPullVersion) {
|
|
5265
5271
|
await client.execute({
|
|
5266
5272
|
sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
|
|
@@ -10393,8 +10399,35 @@ var init_tasks_crud = __esm({
|
|
|
10393
10399
|
});
|
|
10394
10400
|
|
|
10395
10401
|
// src/lib/tasks-review.ts
|
|
10402
|
+
var tasks_review_exports = {};
|
|
10403
|
+
__export(tasks_review_exports, {
|
|
10404
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
10405
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
10406
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
10407
|
+
countPendingReviews: () => countPendingReviews,
|
|
10408
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
10409
|
+
formatAge: () => formatAge,
|
|
10410
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
10411
|
+
isStale: () => isStale,
|
|
10412
|
+
listPendingReviews: () => listPendingReviews
|
|
10413
|
+
});
|
|
10396
10414
|
import path25 from "path";
|
|
10397
10415
|
import { existsSync as existsSync22, readdirSync as readdirSync5, unlinkSync as unlinkSync6 } from "fs";
|
|
10416
|
+
function formatAge(isoTimestamp) {
|
|
10417
|
+
if (!isoTimestamp) return "";
|
|
10418
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
10419
|
+
if (ms < 0) return "just now";
|
|
10420
|
+
const minutes = Math.floor(ms / 6e4);
|
|
10421
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
10422
|
+
const hours = Math.floor(minutes / 60);
|
|
10423
|
+
if (hours < 24) return `${hours}h ago`;
|
|
10424
|
+
const days = Math.floor(hours / 24);
|
|
10425
|
+
return `${days}d ago`;
|
|
10426
|
+
}
|
|
10427
|
+
function isStale(isoTimestamp) {
|
|
10428
|
+
if (!isoTimestamp) return false;
|
|
10429
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
10430
|
+
}
|
|
10398
10431
|
async function countPendingReviews(sessionScope) {
|
|
10399
10432
|
const client = getClient();
|
|
10400
10433
|
const scope = strictSessionScopeFilter(
|
|
@@ -10515,6 +10548,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
10515
10548
|
]
|
|
10516
10549
|
};
|
|
10517
10550
|
}
|
|
10551
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
10552
|
+
const taskFile = String(row.task_file);
|
|
10553
|
+
const employees = await loadEmployees();
|
|
10554
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
10555
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
10556
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
10557
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
10558
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
10559
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
10560
|
+
const client = getClient();
|
|
10561
|
+
const agent = String(row.assigned_to);
|
|
10562
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
10563
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
10564
|
+
const currentStatus = String(row.status ?? "");
|
|
10565
|
+
if (currentStatus === "done") return;
|
|
10566
|
+
const existingResult = String(row.result ?? "");
|
|
10567
|
+
if (existingResult.includes("## Review notes")) return;
|
|
10568
|
+
let reviewerRole = "unknown";
|
|
10569
|
+
try {
|
|
10570
|
+
const emp = getEmployee(employees, reviewer);
|
|
10571
|
+
if (emp) reviewerRole = emp.role;
|
|
10572
|
+
} catch {
|
|
10573
|
+
}
|
|
10574
|
+
const taskTitle = String(row.title);
|
|
10575
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
10576
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
10577
|
+
process.stderr.write(
|
|
10578
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
10579
|
+
`
|
|
10580
|
+
);
|
|
10581
|
+
const reviewNotes = [
|
|
10582
|
+
`
|
|
10583
|
+
---
|
|
10584
|
+
## Review notes`,
|
|
10585
|
+
`Review lens: ${lens}`,
|
|
10586
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
10587
|
+
"",
|
|
10588
|
+
"### Checklist",
|
|
10589
|
+
...checklist,
|
|
10590
|
+
"",
|
|
10591
|
+
"### Verdict",
|
|
10592
|
+
"- **Approved:** mark this task as done",
|
|
10593
|
+
"- **Needs work:** re-open with notes"
|
|
10594
|
+
].join("\n");
|
|
10595
|
+
const originalTaskId = String(row.id);
|
|
10596
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
10597
|
+
await client.execute({
|
|
10598
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
10599
|
+
WHERE id = ?`,
|
|
10600
|
+
args: [updatedResult, now, originalTaskId]
|
|
10601
|
+
});
|
|
10602
|
+
orgBus.emit({
|
|
10603
|
+
type: "review_created",
|
|
10604
|
+
reviewId: originalTaskId,
|
|
10605
|
+
employee: agent,
|
|
10606
|
+
reviewer,
|
|
10607
|
+
timestamp: now
|
|
10608
|
+
});
|
|
10609
|
+
await writeNotification({
|
|
10610
|
+
agentId: agent,
|
|
10611
|
+
agentRole: String(row.assigned_to),
|
|
10612
|
+
event: "task_complete",
|
|
10613
|
+
project: String(row.project_name),
|
|
10614
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
10615
|
+
taskFile
|
|
10616
|
+
});
|
|
10617
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
10618
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
10619
|
+
if (!autoApprove) {
|
|
10620
|
+
try {
|
|
10621
|
+
const key = getSessionKey();
|
|
10622
|
+
const exeSession = getParentExe(key);
|
|
10623
|
+
if (exeSession) {
|
|
10624
|
+
sendIntercom(exeSession);
|
|
10625
|
+
}
|
|
10626
|
+
} catch {
|
|
10627
|
+
}
|
|
10628
|
+
}
|
|
10629
|
+
if (autoApprove) {
|
|
10630
|
+
process.stderr.write(
|
|
10631
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
10632
|
+
`
|
|
10633
|
+
);
|
|
10634
|
+
await client.execute({
|
|
10635
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
10636
|
+
args: [now, originalTaskId]
|
|
10637
|
+
});
|
|
10638
|
+
}
|
|
10639
|
+
}
|
|
10518
10640
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
10519
10641
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
10520
10642
|
try {
|
|
@@ -11878,15 +12000,17 @@ function sendIntercom(targetSession) {
|
|
|
11878
12000
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
11879
12001
|
return "queued";
|
|
11880
12002
|
}
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
12003
|
+
if (sessionState !== "idle") {
|
|
12004
|
+
try {
|
|
12005
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
12006
|
+
const agent = baseAgentName(rawAgent);
|
|
12007
|
+
const markerPath = path28.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
12008
|
+
if (existsSync23(markerPath)) {
|
|
12009
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
12010
|
+
return "debounced";
|
|
12011
|
+
}
|
|
12012
|
+
} catch {
|
|
11888
12013
|
}
|
|
11889
|
-
} catch {
|
|
11890
12014
|
}
|
|
11891
12015
|
try {
|
|
11892
12016
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -11957,6 +12081,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
11957
12081
|
try {
|
|
11958
12082
|
const sessions = transport.listSessions();
|
|
11959
12083
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
12084
|
+
try {
|
|
12085
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
12086
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
12087
|
+
if (pending instanceof Promise) {
|
|
12088
|
+
pending.then((count) => {
|
|
12089
|
+
if (count > 0) {
|
|
12090
|
+
execSync8(
|
|
12091
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
12092
|
+
{ timeout: 3e3 }
|
|
12093
|
+
);
|
|
12094
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
12095
|
+
}
|
|
12096
|
+
}).catch(() => {
|
|
12097
|
+
});
|
|
12098
|
+
return true;
|
|
12099
|
+
}
|
|
12100
|
+
} catch {
|
|
12101
|
+
}
|
|
11960
12102
|
execSync8(
|
|
11961
12103
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
11962
12104
|
{ timeout: 3e3 }
|
package/dist/bin/exe-boot.js
CHANGED
|
@@ -5477,8 +5477,35 @@ var init_tasks_crud = __esm({
|
|
|
5477
5477
|
});
|
|
5478
5478
|
|
|
5479
5479
|
// src/lib/tasks-review.ts
|
|
5480
|
+
var tasks_review_exports = {};
|
|
5481
|
+
__export(tasks_review_exports, {
|
|
5482
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
5483
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
5484
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
5485
|
+
countPendingReviews: () => countPendingReviews,
|
|
5486
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
5487
|
+
formatAge: () => formatAge,
|
|
5488
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
5489
|
+
isStale: () => isStale,
|
|
5490
|
+
listPendingReviews: () => listPendingReviews
|
|
5491
|
+
});
|
|
5480
5492
|
import path16 from "path";
|
|
5481
5493
|
import { existsSync as existsSync15, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
5494
|
+
function formatAge(isoTimestamp) {
|
|
5495
|
+
if (!isoTimestamp) return "";
|
|
5496
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
5497
|
+
if (ms < 0) return "just now";
|
|
5498
|
+
const minutes = Math.floor(ms / 6e4);
|
|
5499
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
5500
|
+
const hours = Math.floor(minutes / 60);
|
|
5501
|
+
if (hours < 24) return `${hours}h ago`;
|
|
5502
|
+
const days = Math.floor(hours / 24);
|
|
5503
|
+
return `${days}d ago`;
|
|
5504
|
+
}
|
|
5505
|
+
function isStale(isoTimestamp) {
|
|
5506
|
+
if (!isoTimestamp) return false;
|
|
5507
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
5508
|
+
}
|
|
5482
5509
|
async function countPendingReviews(sessionScope) {
|
|
5483
5510
|
const client = getClient();
|
|
5484
5511
|
const scope = strictSessionScopeFilter(
|
|
@@ -5599,6 +5626,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
5599
5626
|
]
|
|
5600
5627
|
};
|
|
5601
5628
|
}
|
|
5629
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
5630
|
+
const taskFile = String(row.task_file);
|
|
5631
|
+
const employees = await loadEmployees();
|
|
5632
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
5633
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
5634
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
5635
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
5636
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
5637
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
5638
|
+
const client = getClient();
|
|
5639
|
+
const agent = String(row.assigned_to);
|
|
5640
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
5641
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
5642
|
+
const currentStatus = String(row.status ?? "");
|
|
5643
|
+
if (currentStatus === "done") return;
|
|
5644
|
+
const existingResult = String(row.result ?? "");
|
|
5645
|
+
if (existingResult.includes("## Review notes")) return;
|
|
5646
|
+
let reviewerRole = "unknown";
|
|
5647
|
+
try {
|
|
5648
|
+
const emp = getEmployee(employees, reviewer);
|
|
5649
|
+
if (emp) reviewerRole = emp.role;
|
|
5650
|
+
} catch {
|
|
5651
|
+
}
|
|
5652
|
+
const taskTitle = String(row.title);
|
|
5653
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
5654
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
5655
|
+
process.stderr.write(
|
|
5656
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
5657
|
+
`
|
|
5658
|
+
);
|
|
5659
|
+
const reviewNotes = [
|
|
5660
|
+
`
|
|
5661
|
+
---
|
|
5662
|
+
## Review notes`,
|
|
5663
|
+
`Review lens: ${lens}`,
|
|
5664
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
5665
|
+
"",
|
|
5666
|
+
"### Checklist",
|
|
5667
|
+
...checklist,
|
|
5668
|
+
"",
|
|
5669
|
+
"### Verdict",
|
|
5670
|
+
"- **Approved:** mark this task as done",
|
|
5671
|
+
"- **Needs work:** re-open with notes"
|
|
5672
|
+
].join("\n");
|
|
5673
|
+
const originalTaskId = String(row.id);
|
|
5674
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
5675
|
+
await client.execute({
|
|
5676
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
5677
|
+
WHERE id = ?`,
|
|
5678
|
+
args: [updatedResult, now, originalTaskId]
|
|
5679
|
+
});
|
|
5680
|
+
orgBus.emit({
|
|
5681
|
+
type: "review_created",
|
|
5682
|
+
reviewId: originalTaskId,
|
|
5683
|
+
employee: agent,
|
|
5684
|
+
reviewer,
|
|
5685
|
+
timestamp: now
|
|
5686
|
+
});
|
|
5687
|
+
await writeNotification({
|
|
5688
|
+
agentId: agent,
|
|
5689
|
+
agentRole: String(row.assigned_to),
|
|
5690
|
+
event: "task_complete",
|
|
5691
|
+
project: String(row.project_name),
|
|
5692
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
5693
|
+
taskFile
|
|
5694
|
+
});
|
|
5695
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
5696
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
5697
|
+
if (!autoApprove) {
|
|
5698
|
+
try {
|
|
5699
|
+
const key = getSessionKey();
|
|
5700
|
+
const exeSession = getParentExe(key);
|
|
5701
|
+
if (exeSession) {
|
|
5702
|
+
sendIntercom(exeSession);
|
|
5703
|
+
}
|
|
5704
|
+
} catch {
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
if (autoApprove) {
|
|
5708
|
+
process.stderr.write(
|
|
5709
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
5710
|
+
`
|
|
5711
|
+
);
|
|
5712
|
+
await client.execute({
|
|
5713
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
5714
|
+
args: [now, originalTaskId]
|
|
5715
|
+
});
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5602
5718
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
5603
5719
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
5604
5720
|
try {
|
|
@@ -6962,15 +7078,17 @@ function sendIntercom(targetSession) {
|
|
|
6962
7078
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
6963
7079
|
return "queued";
|
|
6964
7080
|
}
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
7081
|
+
if (sessionState !== "idle") {
|
|
7082
|
+
try {
|
|
7083
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
7084
|
+
const agent = baseAgentName(rawAgent);
|
|
7085
|
+
const markerPath = path19.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
7086
|
+
if (existsSync16(markerPath)) {
|
|
7087
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
7088
|
+
return "debounced";
|
|
7089
|
+
}
|
|
7090
|
+
} catch {
|
|
6972
7091
|
}
|
|
6973
|
-
} catch {
|
|
6974
7092
|
}
|
|
6975
7093
|
try {
|
|
6976
7094
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -7041,6 +7159,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
7041
7159
|
try {
|
|
7042
7160
|
const sessions = transport.listSessions();
|
|
7043
7161
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
7162
|
+
try {
|
|
7163
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
7164
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
7165
|
+
if (pending instanceof Promise) {
|
|
7166
|
+
pending.then((count) => {
|
|
7167
|
+
if (count > 0) {
|
|
7168
|
+
execSync7(
|
|
7169
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
7170
|
+
{ timeout: 3e3 }
|
|
7171
|
+
);
|
|
7172
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
7173
|
+
}
|
|
7174
|
+
}).catch(() => {
|
|
7175
|
+
});
|
|
7176
|
+
return true;
|
|
7177
|
+
}
|
|
7178
|
+
} catch {
|
|
7179
|
+
}
|
|
7044
7180
|
execSync7(
|
|
7045
7181
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
7046
7182
|
{ timeout: 3e3 }
|
|
@@ -8230,6 +8366,12 @@ async function cloudSync(config) {
|
|
|
8230
8366
|
pulled = pullResult.records.length;
|
|
8231
8367
|
}
|
|
8232
8368
|
}
|
|
8369
|
+
if (pulled > 0) {
|
|
8370
|
+
try {
|
|
8371
|
+
await pushToPostgres(pullResult.records);
|
|
8372
|
+
} catch {
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8233
8375
|
if (pullResult.maxVersion > lastPullVersion) {
|
|
8234
8376
|
await client.execute({
|
|
8235
8377
|
sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
|
package/dist/bin/exe-dispatch.js
CHANGED
|
@@ -783,6 +783,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
|
783
783
|
if (!agentName) return false;
|
|
784
784
|
return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
|
|
785
785
|
}
|
|
786
|
+
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
787
|
+
if (!existsSync6(employeesPath)) {
|
|
788
|
+
return [];
|
|
789
|
+
}
|
|
790
|
+
const raw = await readFile2(employeesPath, "utf-8");
|
|
791
|
+
try {
|
|
792
|
+
return JSON.parse(raw);
|
|
793
|
+
} catch {
|
|
794
|
+
return [];
|
|
795
|
+
}
|
|
796
|
+
}
|
|
786
797
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
787
798
|
if (!existsSync6(employeesPath)) return [];
|
|
788
799
|
try {
|
|
@@ -3436,8 +3447,35 @@ var init_tasks_crud = __esm({
|
|
|
3436
3447
|
});
|
|
3437
3448
|
|
|
3438
3449
|
// src/lib/tasks-review.ts
|
|
3450
|
+
var tasks_review_exports = {};
|
|
3451
|
+
__export(tasks_review_exports, {
|
|
3452
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
3453
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
3454
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
3455
|
+
countPendingReviews: () => countPendingReviews,
|
|
3456
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
3457
|
+
formatAge: () => formatAge,
|
|
3458
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
3459
|
+
isStale: () => isStale,
|
|
3460
|
+
listPendingReviews: () => listPendingReviews
|
|
3461
|
+
});
|
|
3439
3462
|
import path12 from "path";
|
|
3440
3463
|
import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
3464
|
+
function formatAge(isoTimestamp) {
|
|
3465
|
+
if (!isoTimestamp) return "";
|
|
3466
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
3467
|
+
if (ms < 0) return "just now";
|
|
3468
|
+
const minutes = Math.floor(ms / 6e4);
|
|
3469
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
3470
|
+
const hours = Math.floor(minutes / 60);
|
|
3471
|
+
if (hours < 24) return `${hours}h ago`;
|
|
3472
|
+
const days = Math.floor(hours / 24);
|
|
3473
|
+
return `${days}d ago`;
|
|
3474
|
+
}
|
|
3475
|
+
function isStale(isoTimestamp) {
|
|
3476
|
+
if (!isoTimestamp) return false;
|
|
3477
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
3478
|
+
}
|
|
3441
3479
|
async function countPendingReviews(sessionScope) {
|
|
3442
3480
|
const client = getClient();
|
|
3443
3481
|
const scope = strictSessionScopeFilter(
|
|
@@ -3558,6 +3596,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
3558
3596
|
]
|
|
3559
3597
|
};
|
|
3560
3598
|
}
|
|
3599
|
+
async function createReviewForCompletedTask(row, result2, _baseDir, now) {
|
|
3600
|
+
const taskFile = String(row.task_file);
|
|
3601
|
+
const employees = await loadEmployees();
|
|
3602
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
3603
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
3604
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
3605
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
3606
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
3607
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
3608
|
+
const client = getClient();
|
|
3609
|
+
const agent = String(row.assigned_to);
|
|
3610
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
3611
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
3612
|
+
const currentStatus = String(row.status ?? "");
|
|
3613
|
+
if (currentStatus === "done") return;
|
|
3614
|
+
const existingResult = String(row.result ?? "");
|
|
3615
|
+
if (existingResult.includes("## Review notes")) return;
|
|
3616
|
+
let reviewerRole = "unknown";
|
|
3617
|
+
try {
|
|
3618
|
+
const emp = getEmployee(employees, reviewer);
|
|
3619
|
+
if (emp) reviewerRole = emp.role;
|
|
3620
|
+
} catch {
|
|
3621
|
+
}
|
|
3622
|
+
const taskTitle = String(row.title);
|
|
3623
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
3624
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
3625
|
+
process.stderr.write(
|
|
3626
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
3627
|
+
`
|
|
3628
|
+
);
|
|
3629
|
+
const reviewNotes = [
|
|
3630
|
+
`
|
|
3631
|
+
---
|
|
3632
|
+
## Review notes`,
|
|
3633
|
+
`Review lens: ${lens}`,
|
|
3634
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
3635
|
+
"",
|
|
3636
|
+
"### Checklist",
|
|
3637
|
+
...checklist,
|
|
3638
|
+
"",
|
|
3639
|
+
"### Verdict",
|
|
3640
|
+
"- **Approved:** mark this task as done",
|
|
3641
|
+
"- **Needs work:** re-open with notes"
|
|
3642
|
+
].join("\n");
|
|
3643
|
+
const originalTaskId = String(row.id);
|
|
3644
|
+
const updatedResult = (result2 ?? "No result summary provided") + reviewNotes;
|
|
3645
|
+
await client.execute({
|
|
3646
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
3647
|
+
WHERE id = ?`,
|
|
3648
|
+
args: [updatedResult, now, originalTaskId]
|
|
3649
|
+
});
|
|
3650
|
+
orgBus.emit({
|
|
3651
|
+
type: "review_created",
|
|
3652
|
+
reviewId: originalTaskId,
|
|
3653
|
+
employee: agent,
|
|
3654
|
+
reviewer,
|
|
3655
|
+
timestamp: now
|
|
3656
|
+
});
|
|
3657
|
+
await writeNotification({
|
|
3658
|
+
agentId: agent,
|
|
3659
|
+
agentRole: String(row.assigned_to),
|
|
3660
|
+
event: "task_complete",
|
|
3661
|
+
project: String(row.project_name),
|
|
3662
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
3663
|
+
taskFile
|
|
3664
|
+
});
|
|
3665
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
3666
|
+
const autoApprove = originalPriority === "p2" && result2?.toLowerCase().includes("tests pass");
|
|
3667
|
+
if (!autoApprove) {
|
|
3668
|
+
try {
|
|
3669
|
+
const key = getSessionKey();
|
|
3670
|
+
const exeSession2 = getParentExe(key);
|
|
3671
|
+
if (exeSession2) {
|
|
3672
|
+
sendIntercom(exeSession2);
|
|
3673
|
+
}
|
|
3674
|
+
} catch {
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
if (autoApprove) {
|
|
3678
|
+
process.stderr.write(
|
|
3679
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
3680
|
+
`
|
|
3681
|
+
);
|
|
3682
|
+
await client.execute({
|
|
3683
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
3684
|
+
args: [now, originalTaskId]
|
|
3685
|
+
});
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3561
3688
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
3562
3689
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
3563
3690
|
try {
|
|
@@ -4921,15 +5048,17 @@ function sendIntercom(targetSession) {
|
|
|
4921
5048
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
4922
5049
|
return "queued";
|
|
4923
5050
|
}
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
5051
|
+
if (sessionState !== "idle") {
|
|
5052
|
+
try {
|
|
5053
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
5054
|
+
const agent = baseAgentName(rawAgent);
|
|
5055
|
+
const markerPath = path15.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
5056
|
+
if (existsSync12(markerPath)) {
|
|
5057
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
5058
|
+
return "debounced";
|
|
5059
|
+
}
|
|
5060
|
+
} catch {
|
|
4931
5061
|
}
|
|
4932
|
-
} catch {
|
|
4933
5062
|
}
|
|
4934
5063
|
try {
|
|
4935
5064
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -5000,6 +5129,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
5000
5129
|
try {
|
|
5001
5130
|
const sessions = transport.listSessions();
|
|
5002
5131
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
5132
|
+
try {
|
|
5133
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
5134
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
5135
|
+
if (pending instanceof Promise) {
|
|
5136
|
+
pending.then((count) => {
|
|
5137
|
+
if (count > 0) {
|
|
5138
|
+
execSync6(
|
|
5139
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5140
|
+
{ timeout: 3e3 }
|
|
5141
|
+
);
|
|
5142
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
5143
|
+
}
|
|
5144
|
+
}).catch(() => {
|
|
5145
|
+
});
|
|
5146
|
+
return true;
|
|
5147
|
+
}
|
|
5148
|
+
} catch {
|
|
5149
|
+
}
|
|
5003
5150
|
execSync6(
|
|
5004
5151
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5005
5152
|
{ timeout: 3e3 }
|