@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
|
@@ -2569,15 +2569,17 @@ function sendIntercom(targetSession) {
|
|
|
2569
2569
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
2570
2570
|
return "queued";
|
|
2571
2571
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2572
|
+
if (sessionState !== "idle") {
|
|
2573
|
+
try {
|
|
2574
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
2575
|
+
const agent = baseAgentName(rawAgent);
|
|
2576
|
+
const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
2577
|
+
if (existsSync8(markerPath)) {
|
|
2578
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
2579
|
+
return "debounced";
|
|
2580
|
+
}
|
|
2581
|
+
} catch {
|
|
2579
2582
|
}
|
|
2580
|
-
} catch {
|
|
2581
2583
|
}
|
|
2582
2584
|
try {
|
|
2583
2585
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -795,6 +795,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
|
795
795
|
if (!agentName) return false;
|
|
796
796
|
return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
|
|
797
797
|
}
|
|
798
|
+
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
799
|
+
if (!existsSync6(employeesPath)) {
|
|
800
|
+
return [];
|
|
801
|
+
}
|
|
802
|
+
const raw = await readFile2(employeesPath, "utf-8");
|
|
803
|
+
try {
|
|
804
|
+
return JSON.parse(raw);
|
|
805
|
+
} catch {
|
|
806
|
+
return [];
|
|
807
|
+
}
|
|
808
|
+
}
|
|
798
809
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
799
810
|
if (!existsSync6(employeesPath)) return [];
|
|
800
811
|
try {
|
|
@@ -3978,8 +3989,35 @@ var init_tasks_crud = __esm({
|
|
|
3978
3989
|
});
|
|
3979
3990
|
|
|
3980
3991
|
// src/lib/tasks-review.ts
|
|
3992
|
+
var tasks_review_exports = {};
|
|
3993
|
+
__export(tasks_review_exports, {
|
|
3994
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
3995
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
3996
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
3997
|
+
countPendingReviews: () => countPendingReviews,
|
|
3998
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
3999
|
+
formatAge: () => formatAge,
|
|
4000
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
4001
|
+
isStale: () => isStale,
|
|
4002
|
+
listPendingReviews: () => listPendingReviews
|
|
4003
|
+
});
|
|
3981
4004
|
import path14 from "path";
|
|
3982
4005
|
import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
|
|
4006
|
+
function formatAge(isoTimestamp) {
|
|
4007
|
+
if (!isoTimestamp) return "";
|
|
4008
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
4009
|
+
if (ms < 0) return "just now";
|
|
4010
|
+
const minutes = Math.floor(ms / 6e4);
|
|
4011
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
4012
|
+
const hours = Math.floor(minutes / 60);
|
|
4013
|
+
if (hours < 24) return `${hours}h ago`;
|
|
4014
|
+
const days = Math.floor(hours / 24);
|
|
4015
|
+
return `${days}d ago`;
|
|
4016
|
+
}
|
|
4017
|
+
function isStale(isoTimestamp) {
|
|
4018
|
+
if (!isoTimestamp) return false;
|
|
4019
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
4020
|
+
}
|
|
3983
4021
|
async function countPendingReviews(sessionScope) {
|
|
3984
4022
|
const client = getClient();
|
|
3985
4023
|
const scope = strictSessionScopeFilter(
|
|
@@ -4100,6 +4138,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
4100
4138
|
]
|
|
4101
4139
|
};
|
|
4102
4140
|
}
|
|
4141
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
4142
|
+
const taskFile = String(row.task_file);
|
|
4143
|
+
const employees = await loadEmployees();
|
|
4144
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
4145
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
4146
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
4147
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
4148
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
4149
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
4150
|
+
const client = getClient();
|
|
4151
|
+
const agent = String(row.assigned_to);
|
|
4152
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
4153
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
4154
|
+
const currentStatus = String(row.status ?? "");
|
|
4155
|
+
if (currentStatus === "done") return;
|
|
4156
|
+
const existingResult = String(row.result ?? "");
|
|
4157
|
+
if (existingResult.includes("## Review notes")) return;
|
|
4158
|
+
let reviewerRole = "unknown";
|
|
4159
|
+
try {
|
|
4160
|
+
const emp = getEmployee(employees, reviewer);
|
|
4161
|
+
if (emp) reviewerRole = emp.role;
|
|
4162
|
+
} catch {
|
|
4163
|
+
}
|
|
4164
|
+
const taskTitle = String(row.title);
|
|
4165
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
4166
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
4167
|
+
process.stderr.write(
|
|
4168
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
4169
|
+
`
|
|
4170
|
+
);
|
|
4171
|
+
const reviewNotes = [
|
|
4172
|
+
`
|
|
4173
|
+
---
|
|
4174
|
+
## Review notes`,
|
|
4175
|
+
`Review lens: ${lens}`,
|
|
4176
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
4177
|
+
"",
|
|
4178
|
+
"### Checklist",
|
|
4179
|
+
...checklist,
|
|
4180
|
+
"",
|
|
4181
|
+
"### Verdict",
|
|
4182
|
+
"- **Approved:** mark this task as done",
|
|
4183
|
+
"- **Needs work:** re-open with notes"
|
|
4184
|
+
].join("\n");
|
|
4185
|
+
const originalTaskId = String(row.id);
|
|
4186
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
4187
|
+
await client.execute({
|
|
4188
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
4189
|
+
WHERE id = ?`,
|
|
4190
|
+
args: [updatedResult, now, originalTaskId]
|
|
4191
|
+
});
|
|
4192
|
+
orgBus.emit({
|
|
4193
|
+
type: "review_created",
|
|
4194
|
+
reviewId: originalTaskId,
|
|
4195
|
+
employee: agent,
|
|
4196
|
+
reviewer,
|
|
4197
|
+
timestamp: now
|
|
4198
|
+
});
|
|
4199
|
+
await writeNotification({
|
|
4200
|
+
agentId: agent,
|
|
4201
|
+
agentRole: String(row.assigned_to),
|
|
4202
|
+
event: "task_complete",
|
|
4203
|
+
project: String(row.project_name),
|
|
4204
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
4205
|
+
taskFile
|
|
4206
|
+
});
|
|
4207
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
4208
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
4209
|
+
if (!autoApprove) {
|
|
4210
|
+
try {
|
|
4211
|
+
const key = getSessionKey();
|
|
4212
|
+
const exeSession = getParentExe(key);
|
|
4213
|
+
if (exeSession) {
|
|
4214
|
+
sendIntercom(exeSession);
|
|
4215
|
+
}
|
|
4216
|
+
} catch {
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
if (autoApprove) {
|
|
4220
|
+
process.stderr.write(
|
|
4221
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
4222
|
+
`
|
|
4223
|
+
);
|
|
4224
|
+
await client.execute({
|
|
4225
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
4226
|
+
args: [now, originalTaskId]
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4103
4230
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
4104
4231
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
4105
4232
|
try {
|
|
@@ -5463,15 +5590,17 @@ function sendIntercom(targetSession) {
|
|
|
5463
5590
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
5464
5591
|
return "queued";
|
|
5465
5592
|
}
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5593
|
+
if (sessionState !== "idle") {
|
|
5594
|
+
try {
|
|
5595
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
5596
|
+
const agent = baseAgentName(rawAgent);
|
|
5597
|
+
const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
5598
|
+
if (existsSync14(markerPath)) {
|
|
5599
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
5600
|
+
return "debounced";
|
|
5601
|
+
}
|
|
5602
|
+
} catch {
|
|
5473
5603
|
}
|
|
5474
|
-
} catch {
|
|
5475
5604
|
}
|
|
5476
5605
|
try {
|
|
5477
5606
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -5542,6 +5671,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
5542
5671
|
try {
|
|
5543
5672
|
const sessions = transport.listSessions();
|
|
5544
5673
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
5674
|
+
try {
|
|
5675
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
5676
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
5677
|
+
if (pending instanceof Promise) {
|
|
5678
|
+
pending.then((count) => {
|
|
5679
|
+
if (count > 0) {
|
|
5680
|
+
execSync6(
|
|
5681
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5682
|
+
{ timeout: 3e3 }
|
|
5683
|
+
);
|
|
5684
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
5685
|
+
}
|
|
5686
|
+
}).catch(() => {
|
|
5687
|
+
});
|
|
5688
|
+
return true;
|
|
5689
|
+
}
|
|
5690
|
+
} catch {
|
|
5691
|
+
}
|
|
5545
5692
|
execSync6(
|
|
5546
5693
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5547
5694
|
{ timeout: 3e3 }
|
|
@@ -467,6 +467,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
|
467
467
|
function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
|
|
468
468
|
return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
|
|
469
469
|
}
|
|
470
|
+
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
471
|
+
if (!existsSync3(employeesPath)) {
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
const raw = await readFile2(employeesPath, "utf-8");
|
|
475
|
+
try {
|
|
476
|
+
return JSON.parse(raw);
|
|
477
|
+
} catch {
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
}
|
|
470
481
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
471
482
|
if (!existsSync3(employeesPath)) return [];
|
|
472
483
|
try {
|
|
@@ -4944,8 +4955,35 @@ var init_tasks_crud = __esm({
|
|
|
4944
4955
|
});
|
|
4945
4956
|
|
|
4946
4957
|
// src/lib/tasks-review.ts
|
|
4958
|
+
var tasks_review_exports = {};
|
|
4959
|
+
__export(tasks_review_exports, {
|
|
4960
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
4961
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
4962
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
4963
|
+
countPendingReviews: () => countPendingReviews,
|
|
4964
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
4965
|
+
formatAge: () => formatAge,
|
|
4966
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
4967
|
+
isStale: () => isStale,
|
|
4968
|
+
listPendingReviews: () => listPendingReviews
|
|
4969
|
+
});
|
|
4947
4970
|
import path15 from "path";
|
|
4948
4971
|
import { existsSync as existsSync14, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
4972
|
+
function formatAge(isoTimestamp) {
|
|
4973
|
+
if (!isoTimestamp) return "";
|
|
4974
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
4975
|
+
if (ms < 0) return "just now";
|
|
4976
|
+
const minutes = Math.floor(ms / 6e4);
|
|
4977
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
4978
|
+
const hours = Math.floor(minutes / 60);
|
|
4979
|
+
if (hours < 24) return `${hours}h ago`;
|
|
4980
|
+
const days = Math.floor(hours / 24);
|
|
4981
|
+
return `${days}d ago`;
|
|
4982
|
+
}
|
|
4983
|
+
function isStale(isoTimestamp) {
|
|
4984
|
+
if (!isoTimestamp) return false;
|
|
4985
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
4986
|
+
}
|
|
4949
4987
|
async function countPendingReviews(sessionScope) {
|
|
4950
4988
|
const client = getClient();
|
|
4951
4989
|
const scope = strictSessionScopeFilter(
|
|
@@ -5066,6 +5104,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
5066
5104
|
]
|
|
5067
5105
|
};
|
|
5068
5106
|
}
|
|
5107
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
5108
|
+
const taskFile = String(row.task_file);
|
|
5109
|
+
const employees = await loadEmployees();
|
|
5110
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
5111
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
5112
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
5113
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
5114
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
5115
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
5116
|
+
const client = getClient();
|
|
5117
|
+
const agent = String(row.assigned_to);
|
|
5118
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
5119
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
5120
|
+
const currentStatus = String(row.status ?? "");
|
|
5121
|
+
if (currentStatus === "done") return;
|
|
5122
|
+
const existingResult = String(row.result ?? "");
|
|
5123
|
+
if (existingResult.includes("## Review notes")) return;
|
|
5124
|
+
let reviewerRole = "unknown";
|
|
5125
|
+
try {
|
|
5126
|
+
const emp = getEmployee(employees, reviewer);
|
|
5127
|
+
if (emp) reviewerRole = emp.role;
|
|
5128
|
+
} catch {
|
|
5129
|
+
}
|
|
5130
|
+
const taskTitle = String(row.title);
|
|
5131
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
5132
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
5133
|
+
process.stderr.write(
|
|
5134
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
5135
|
+
`
|
|
5136
|
+
);
|
|
5137
|
+
const reviewNotes = [
|
|
5138
|
+
`
|
|
5139
|
+
---
|
|
5140
|
+
## Review notes`,
|
|
5141
|
+
`Review lens: ${lens}`,
|
|
5142
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
5143
|
+
"",
|
|
5144
|
+
"### Checklist",
|
|
5145
|
+
...checklist,
|
|
5146
|
+
"",
|
|
5147
|
+
"### Verdict",
|
|
5148
|
+
"- **Approved:** mark this task as done",
|
|
5149
|
+
"- **Needs work:** re-open with notes"
|
|
5150
|
+
].join("\n");
|
|
5151
|
+
const originalTaskId = String(row.id);
|
|
5152
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
5153
|
+
await client.execute({
|
|
5154
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
5155
|
+
WHERE id = ?`,
|
|
5156
|
+
args: [updatedResult, now, originalTaskId]
|
|
5157
|
+
});
|
|
5158
|
+
orgBus.emit({
|
|
5159
|
+
type: "review_created",
|
|
5160
|
+
reviewId: originalTaskId,
|
|
5161
|
+
employee: agent,
|
|
5162
|
+
reviewer,
|
|
5163
|
+
timestamp: now
|
|
5164
|
+
});
|
|
5165
|
+
await writeNotification({
|
|
5166
|
+
agentId: agent,
|
|
5167
|
+
agentRole: String(row.assigned_to),
|
|
5168
|
+
event: "task_complete",
|
|
5169
|
+
project: String(row.project_name),
|
|
5170
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
5171
|
+
taskFile
|
|
5172
|
+
});
|
|
5173
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
5174
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
5175
|
+
if (!autoApprove) {
|
|
5176
|
+
try {
|
|
5177
|
+
const key = getSessionKey();
|
|
5178
|
+
const exeSession = getParentExe(key);
|
|
5179
|
+
if (exeSession) {
|
|
5180
|
+
sendIntercom(exeSession);
|
|
5181
|
+
}
|
|
5182
|
+
} catch {
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
if (autoApprove) {
|
|
5186
|
+
process.stderr.write(
|
|
5187
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
5188
|
+
`
|
|
5189
|
+
);
|
|
5190
|
+
await client.execute({
|
|
5191
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
5192
|
+
args: [now, originalTaskId]
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5069
5196
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
5070
5197
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
5071
5198
|
try {
|
|
@@ -6429,15 +6556,17 @@ function sendIntercom(targetSession) {
|
|
|
6429
6556
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
6430
6557
|
return "queued";
|
|
6431
6558
|
}
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6559
|
+
if (sessionState !== "idle") {
|
|
6560
|
+
try {
|
|
6561
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
6562
|
+
const agent = baseAgentName(rawAgent);
|
|
6563
|
+
const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
6564
|
+
if (existsSync15(markerPath)) {
|
|
6565
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
6566
|
+
return "debounced";
|
|
6567
|
+
}
|
|
6568
|
+
} catch {
|
|
6439
6569
|
}
|
|
6440
|
-
} catch {
|
|
6441
6570
|
}
|
|
6442
6571
|
try {
|
|
6443
6572
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -6508,6 +6637,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
6508
6637
|
try {
|
|
6509
6638
|
const sessions = transport.listSessions();
|
|
6510
6639
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
6640
|
+
try {
|
|
6641
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
6642
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
6643
|
+
if (pending instanceof Promise) {
|
|
6644
|
+
pending.then((count) => {
|
|
6645
|
+
if (count > 0) {
|
|
6646
|
+
execSync6(
|
|
6647
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
6648
|
+
{ timeout: 3e3 }
|
|
6649
|
+
);
|
|
6650
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
6651
|
+
}
|
|
6652
|
+
}).catch(() => {
|
|
6653
|
+
});
|
|
6654
|
+
return true;
|
|
6655
|
+
}
|
|
6656
|
+
} catch {
|
|
6657
|
+
}
|
|
6511
6658
|
execSync6(
|
|
6512
6659
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
6513
6660
|
{ timeout: 3e3 }
|
|
@@ -338,6 +338,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
|
338
338
|
if (!agentName) return false;
|
|
339
339
|
return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
|
|
340
340
|
}
|
|
341
|
+
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
342
|
+
if (!existsSync3(employeesPath)) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
const raw = await readFile2(employeesPath, "utf-8");
|
|
346
|
+
try {
|
|
347
|
+
return JSON.parse(raw);
|
|
348
|
+
} catch {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
341
352
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
342
353
|
if (!existsSync3(employeesPath)) return [];
|
|
343
354
|
try {
|
|
@@ -3962,8 +3973,35 @@ var init_tasks_crud = __esm({
|
|
|
3962
3973
|
});
|
|
3963
3974
|
|
|
3964
3975
|
// src/lib/tasks-review.ts
|
|
3976
|
+
var tasks_review_exports = {};
|
|
3977
|
+
__export(tasks_review_exports, {
|
|
3978
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
3979
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
3980
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
3981
|
+
countPendingReviews: () => countPendingReviews,
|
|
3982
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
3983
|
+
formatAge: () => formatAge,
|
|
3984
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
3985
|
+
isStale: () => isStale,
|
|
3986
|
+
listPendingReviews: () => listPendingReviews
|
|
3987
|
+
});
|
|
3965
3988
|
import path15 from "path";
|
|
3966
3989
|
import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
|
|
3990
|
+
function formatAge(isoTimestamp) {
|
|
3991
|
+
if (!isoTimestamp) return "";
|
|
3992
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
3993
|
+
if (ms < 0) return "just now";
|
|
3994
|
+
const minutes = Math.floor(ms / 6e4);
|
|
3995
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
3996
|
+
const hours = Math.floor(minutes / 60);
|
|
3997
|
+
if (hours < 24) return `${hours}h ago`;
|
|
3998
|
+
const days = Math.floor(hours / 24);
|
|
3999
|
+
return `${days}d ago`;
|
|
4000
|
+
}
|
|
4001
|
+
function isStale(isoTimestamp) {
|
|
4002
|
+
if (!isoTimestamp) return false;
|
|
4003
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
4004
|
+
}
|
|
3967
4005
|
async function countPendingReviews(sessionScope) {
|
|
3968
4006
|
const client = getClient();
|
|
3969
4007
|
const scope = strictSessionScopeFilter(
|
|
@@ -4084,6 +4122,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
4084
4122
|
]
|
|
4085
4123
|
};
|
|
4086
4124
|
}
|
|
4125
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
4126
|
+
const taskFile = String(row.task_file);
|
|
4127
|
+
const employees = await loadEmployees();
|
|
4128
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
4129
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
4130
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
4131
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
4132
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
4133
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
4134
|
+
const client = getClient();
|
|
4135
|
+
const agent = String(row.assigned_to);
|
|
4136
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
4137
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
4138
|
+
const currentStatus = String(row.status ?? "");
|
|
4139
|
+
if (currentStatus === "done") return;
|
|
4140
|
+
const existingResult = String(row.result ?? "");
|
|
4141
|
+
if (existingResult.includes("## Review notes")) return;
|
|
4142
|
+
let reviewerRole = "unknown";
|
|
4143
|
+
try {
|
|
4144
|
+
const emp = getEmployee(employees, reviewer);
|
|
4145
|
+
if (emp) reviewerRole = emp.role;
|
|
4146
|
+
} catch {
|
|
4147
|
+
}
|
|
4148
|
+
const taskTitle = String(row.title);
|
|
4149
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
4150
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
4151
|
+
process.stderr.write(
|
|
4152
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
4153
|
+
`
|
|
4154
|
+
);
|
|
4155
|
+
const reviewNotes = [
|
|
4156
|
+
`
|
|
4157
|
+
---
|
|
4158
|
+
## Review notes`,
|
|
4159
|
+
`Review lens: ${lens}`,
|
|
4160
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
4161
|
+
"",
|
|
4162
|
+
"### Checklist",
|
|
4163
|
+
...checklist,
|
|
4164
|
+
"",
|
|
4165
|
+
"### Verdict",
|
|
4166
|
+
"- **Approved:** mark this task as done",
|
|
4167
|
+
"- **Needs work:** re-open with notes"
|
|
4168
|
+
].join("\n");
|
|
4169
|
+
const originalTaskId = String(row.id);
|
|
4170
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
4171
|
+
await client.execute({
|
|
4172
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
4173
|
+
WHERE id = ?`,
|
|
4174
|
+
args: [updatedResult, now, originalTaskId]
|
|
4175
|
+
});
|
|
4176
|
+
orgBus.emit({
|
|
4177
|
+
type: "review_created",
|
|
4178
|
+
reviewId: originalTaskId,
|
|
4179
|
+
employee: agent,
|
|
4180
|
+
reviewer,
|
|
4181
|
+
timestamp: now
|
|
4182
|
+
});
|
|
4183
|
+
await writeNotification({
|
|
4184
|
+
agentId: agent,
|
|
4185
|
+
agentRole: String(row.assigned_to),
|
|
4186
|
+
event: "task_complete",
|
|
4187
|
+
project: String(row.project_name),
|
|
4188
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
4189
|
+
taskFile
|
|
4190
|
+
});
|
|
4191
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
4192
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
4193
|
+
if (!autoApprove) {
|
|
4194
|
+
try {
|
|
4195
|
+
const key = getSessionKey();
|
|
4196
|
+
const exeSession = getParentExe(key);
|
|
4197
|
+
if (exeSession) {
|
|
4198
|
+
sendIntercom(exeSession);
|
|
4199
|
+
}
|
|
4200
|
+
} catch {
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
if (autoApprove) {
|
|
4204
|
+
process.stderr.write(
|
|
4205
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
4206
|
+
`
|
|
4207
|
+
);
|
|
4208
|
+
await client.execute({
|
|
4209
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
4210
|
+
args: [now, originalTaskId]
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4087
4214
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
4088
4215
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
4089
4216
|
try {
|
|
@@ -5447,15 +5574,17 @@ function sendIntercom(targetSession) {
|
|
|
5447
5574
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
5448
5575
|
return "queued";
|
|
5449
5576
|
}
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5577
|
+
if (sessionState !== "idle") {
|
|
5578
|
+
try {
|
|
5579
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
5580
|
+
const agent = baseAgentName(rawAgent);
|
|
5581
|
+
const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
5582
|
+
if (existsSync14(markerPath)) {
|
|
5583
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
5584
|
+
return "debounced";
|
|
5585
|
+
}
|
|
5586
|
+
} catch {
|
|
5457
5587
|
}
|
|
5458
|
-
} catch {
|
|
5459
5588
|
}
|
|
5460
5589
|
try {
|
|
5461
5590
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -5526,6 +5655,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
5526
5655
|
try {
|
|
5527
5656
|
const sessions = transport.listSessions();
|
|
5528
5657
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
5658
|
+
try {
|
|
5659
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
5660
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
5661
|
+
if (pending instanceof Promise) {
|
|
5662
|
+
pending.then((count) => {
|
|
5663
|
+
if (count > 0) {
|
|
5664
|
+
execSync7(
|
|
5665
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5666
|
+
{ timeout: 3e3 }
|
|
5667
|
+
);
|
|
5668
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
5669
|
+
}
|
|
5670
|
+
}).catch(() => {
|
|
5671
|
+
});
|
|
5672
|
+
return true;
|
|
5673
|
+
}
|
|
5674
|
+
} catch {
|
|
5675
|
+
}
|
|
5529
5676
|
execSync7(
|
|
5530
5677
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5531
5678
|
{ timeout: 3e3 }
|
|
@@ -8059,15 +8059,17 @@ function sendIntercom(targetSession) {
|
|
|
8059
8059
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
8060
8060
|
return "queued";
|
|
8061
8061
|
}
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8062
|
+
if (sessionState !== "idle") {
|
|
8063
|
+
try {
|
|
8064
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
8065
|
+
const agent = baseAgentName(rawAgent);
|
|
8066
|
+
const markerPath = path22.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
8067
|
+
if (existsSync18(markerPath)) {
|
|
8068
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
8069
|
+
return "debounced";
|
|
8070
|
+
}
|
|
8071
|
+
} catch {
|
|
8069
8072
|
}
|
|
8070
|
-
} catch {
|
|
8071
8073
|
}
|
|
8072
8074
|
try {
|
|
8073
8075
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -8138,6 +8140,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
8138
8140
|
try {
|
|
8139
8141
|
const sessions = transport.listSessions();
|
|
8140
8142
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
8143
|
+
try {
|
|
8144
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
8145
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
8146
|
+
if (pending instanceof Promise) {
|
|
8147
|
+
pending.then((count) => {
|
|
8148
|
+
if (count > 0) {
|
|
8149
|
+
execSync8(
|
|
8150
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
8151
|
+
{ timeout: 3e3 }
|
|
8152
|
+
);
|
|
8153
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
8154
|
+
}
|
|
8155
|
+
}).catch(() => {
|
|
8156
|
+
});
|
|
8157
|
+
return true;
|
|
8158
|
+
}
|
|
8159
|
+
} catch {
|
|
8160
|
+
}
|
|
8141
8161
|
execSync8(
|
|
8142
8162
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
8143
8163
|
{ timeout: 3e3 }
|