@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/exe-link.js
CHANGED
|
@@ -3423,6 +3423,12 @@ async function cloudSync(config) {
|
|
|
3423
3423
|
pulled = pullResult.records.length;
|
|
3424
3424
|
}
|
|
3425
3425
|
}
|
|
3426
|
+
if (pulled > 0) {
|
|
3427
|
+
try {
|
|
3428
|
+
await pushToPostgres(pullResult.records);
|
|
3429
|
+
} catch {
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3426
3432
|
if (pullResult.maxVersion > lastPullVersion) {
|
|
3427
3433
|
await client.execute({
|
|
3428
3434
|
sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
|
|
@@ -6940,15 +6940,17 @@ function sendIntercom(targetSession) {
|
|
|
6940
6940
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
6941
6941
|
return "queued";
|
|
6942
6942
|
}
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6943
|
+
if (sessionState !== "idle") {
|
|
6944
|
+
try {
|
|
6945
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
6946
|
+
const agent = baseAgentName(rawAgent);
|
|
6947
|
+
const markerPath = path19.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
6948
|
+
if (existsSync16(markerPath)) {
|
|
6949
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
6950
|
+
return "debounced";
|
|
6951
|
+
}
|
|
6952
|
+
} catch {
|
|
6950
6953
|
}
|
|
6951
|
-
} catch {
|
|
6952
6954
|
}
|
|
6953
6955
|
try {
|
|
6954
6956
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -7019,6 +7021,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName2, taskTit
|
|
|
7019
7021
|
try {
|
|
7020
7022
|
const sessions = transport.listSessions();
|
|
7021
7023
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
7024
|
+
try {
|
|
7025
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
7026
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
7027
|
+
if (pending instanceof Promise) {
|
|
7028
|
+
pending.then((count) => {
|
|
7029
|
+
if (count > 0) {
|
|
7030
|
+
execSync6(
|
|
7031
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
7032
|
+
{ timeout: 3e3 }
|
|
7033
|
+
);
|
|
7034
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName2} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
7035
|
+
}
|
|
7036
|
+
}).catch(() => {
|
|
7037
|
+
});
|
|
7038
|
+
return true;
|
|
7039
|
+
}
|
|
7040
|
+
} catch {
|
|
7041
|
+
}
|
|
7022
7042
|
execSync6(
|
|
7023
7043
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
7024
7044
|
{ timeout: 3e3 }
|
package/dist/bin/git-sweep.js
CHANGED
|
@@ -796,6 +796,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
|
796
796
|
if (!agentName) return false;
|
|
797
797
|
return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
|
|
798
798
|
}
|
|
799
|
+
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
800
|
+
if (!existsSync6(employeesPath)) {
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
const raw = await readFile2(employeesPath, "utf-8");
|
|
804
|
+
try {
|
|
805
|
+
return JSON.parse(raw);
|
|
806
|
+
} catch {
|
|
807
|
+
return [];
|
|
808
|
+
}
|
|
809
|
+
}
|
|
799
810
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
800
811
|
if (!existsSync6(employeesPath)) return [];
|
|
801
812
|
try {
|
|
@@ -3979,8 +3990,35 @@ var init_tasks_crud = __esm({
|
|
|
3979
3990
|
});
|
|
3980
3991
|
|
|
3981
3992
|
// src/lib/tasks-review.ts
|
|
3993
|
+
var tasks_review_exports = {};
|
|
3994
|
+
__export(tasks_review_exports, {
|
|
3995
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
3996
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
3997
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
3998
|
+
countPendingReviews: () => countPendingReviews,
|
|
3999
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
4000
|
+
formatAge: () => formatAge,
|
|
4001
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
4002
|
+
isStale: () => isStale,
|
|
4003
|
+
listPendingReviews: () => listPendingReviews
|
|
4004
|
+
});
|
|
3982
4005
|
import path14 from "path";
|
|
3983
4006
|
import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
|
|
4007
|
+
function formatAge(isoTimestamp) {
|
|
4008
|
+
if (!isoTimestamp) return "";
|
|
4009
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
4010
|
+
if (ms < 0) return "just now";
|
|
4011
|
+
const minutes = Math.floor(ms / 6e4);
|
|
4012
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
4013
|
+
const hours = Math.floor(minutes / 60);
|
|
4014
|
+
if (hours < 24) return `${hours}h ago`;
|
|
4015
|
+
const days = Math.floor(hours / 24);
|
|
4016
|
+
return `${days}d ago`;
|
|
4017
|
+
}
|
|
4018
|
+
function isStale(isoTimestamp) {
|
|
4019
|
+
if (!isoTimestamp) return false;
|
|
4020
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
4021
|
+
}
|
|
3984
4022
|
async function countPendingReviews(sessionScope) {
|
|
3985
4023
|
const client = getClient();
|
|
3986
4024
|
const scope = strictSessionScopeFilter(
|
|
@@ -4101,6 +4139,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
4101
4139
|
]
|
|
4102
4140
|
};
|
|
4103
4141
|
}
|
|
4142
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
4143
|
+
const taskFile = String(row.task_file);
|
|
4144
|
+
const employees = await loadEmployees();
|
|
4145
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
4146
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
4147
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
4148
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
4149
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
4150
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
4151
|
+
const client = getClient();
|
|
4152
|
+
const agent = String(row.assigned_to);
|
|
4153
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
4154
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
4155
|
+
const currentStatus = String(row.status ?? "");
|
|
4156
|
+
if (currentStatus === "done") return;
|
|
4157
|
+
const existingResult = String(row.result ?? "");
|
|
4158
|
+
if (existingResult.includes("## Review notes")) return;
|
|
4159
|
+
let reviewerRole = "unknown";
|
|
4160
|
+
try {
|
|
4161
|
+
const emp = getEmployee(employees, reviewer);
|
|
4162
|
+
if (emp) reviewerRole = emp.role;
|
|
4163
|
+
} catch {
|
|
4164
|
+
}
|
|
4165
|
+
const taskTitle = String(row.title);
|
|
4166
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
4167
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
4168
|
+
process.stderr.write(
|
|
4169
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
4170
|
+
`
|
|
4171
|
+
);
|
|
4172
|
+
const reviewNotes = [
|
|
4173
|
+
`
|
|
4174
|
+
---
|
|
4175
|
+
## Review notes`,
|
|
4176
|
+
`Review lens: ${lens}`,
|
|
4177
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
4178
|
+
"",
|
|
4179
|
+
"### Checklist",
|
|
4180
|
+
...checklist,
|
|
4181
|
+
"",
|
|
4182
|
+
"### Verdict",
|
|
4183
|
+
"- **Approved:** mark this task as done",
|
|
4184
|
+
"- **Needs work:** re-open with notes"
|
|
4185
|
+
].join("\n");
|
|
4186
|
+
const originalTaskId = String(row.id);
|
|
4187
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
4188
|
+
await client.execute({
|
|
4189
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
4190
|
+
WHERE id = ?`,
|
|
4191
|
+
args: [updatedResult, now, originalTaskId]
|
|
4192
|
+
});
|
|
4193
|
+
orgBus.emit({
|
|
4194
|
+
type: "review_created",
|
|
4195
|
+
reviewId: originalTaskId,
|
|
4196
|
+
employee: agent,
|
|
4197
|
+
reviewer,
|
|
4198
|
+
timestamp: now
|
|
4199
|
+
});
|
|
4200
|
+
await writeNotification({
|
|
4201
|
+
agentId: agent,
|
|
4202
|
+
agentRole: String(row.assigned_to),
|
|
4203
|
+
event: "task_complete",
|
|
4204
|
+
project: String(row.project_name),
|
|
4205
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
4206
|
+
taskFile
|
|
4207
|
+
});
|
|
4208
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
4209
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
4210
|
+
if (!autoApprove) {
|
|
4211
|
+
try {
|
|
4212
|
+
const key = getSessionKey();
|
|
4213
|
+
const exeSession = getParentExe(key);
|
|
4214
|
+
if (exeSession) {
|
|
4215
|
+
sendIntercom(exeSession);
|
|
4216
|
+
}
|
|
4217
|
+
} catch {
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
if (autoApprove) {
|
|
4221
|
+
process.stderr.write(
|
|
4222
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
4223
|
+
`
|
|
4224
|
+
);
|
|
4225
|
+
await client.execute({
|
|
4226
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
4227
|
+
args: [now, originalTaskId]
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4104
4231
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
4105
4232
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
4106
4233
|
try {
|
|
@@ -5464,15 +5591,17 @@ function sendIntercom(targetSession) {
|
|
|
5464
5591
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
5465
5592
|
return "queued";
|
|
5466
5593
|
}
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5594
|
+
if (sessionState !== "idle") {
|
|
5595
|
+
try {
|
|
5596
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
5597
|
+
const agent = baseAgentName(rawAgent);
|
|
5598
|
+
const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
5599
|
+
if (existsSync14(markerPath)) {
|
|
5600
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
5601
|
+
return "debounced";
|
|
5602
|
+
}
|
|
5603
|
+
} catch {
|
|
5474
5604
|
}
|
|
5475
|
-
} catch {
|
|
5476
5605
|
}
|
|
5477
5606
|
try {
|
|
5478
5607
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -5543,6 +5672,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
5543
5672
|
try {
|
|
5544
5673
|
const sessions = transport.listSessions();
|
|
5545
5674
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
5675
|
+
try {
|
|
5676
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
5677
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
5678
|
+
if (pending instanceof Promise) {
|
|
5679
|
+
pending.then((count) => {
|
|
5680
|
+
if (count > 0) {
|
|
5681
|
+
execSync6(
|
|
5682
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5683
|
+
{ timeout: 3e3 }
|
|
5684
|
+
);
|
|
5685
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
5686
|
+
}
|
|
5687
|
+
}).catch(() => {
|
|
5688
|
+
});
|
|
5689
|
+
return true;
|
|
5690
|
+
}
|
|
5691
|
+
} catch {
|
|
5692
|
+
}
|
|
5546
5693
|
execSync6(
|
|
5547
5694
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5548
5695
|
{ timeout: 3e3 }
|
|
@@ -7192,7 +7339,7 @@ function getRecentCommits(limit = DEFAULT_COMMIT_LIMIT) {
|
|
|
7192
7339
|
return [];
|
|
7193
7340
|
}
|
|
7194
7341
|
}
|
|
7195
|
-
function
|
|
7342
|
+
function isStale2(updatedAt, staleMinutes2) {
|
|
7196
7343
|
const updated = new Date(updatedAt).getTime();
|
|
7197
7344
|
const threshold = Date.now() - staleMinutes2 * 6e4;
|
|
7198
7345
|
return updated < threshold;
|
|
@@ -7257,7 +7404,7 @@ async function sweepTasks(projectName2, options = {}) {
|
|
|
7257
7404
|
return result;
|
|
7258
7405
|
}
|
|
7259
7406
|
for (const task of tasks) {
|
|
7260
|
-
if (!
|
|
7407
|
+
if (!isStale2(task.updatedAt, staleMinutes2)) {
|
|
7261
7408
|
result.unchanged++;
|
|
7262
7409
|
continue;
|
|
7263
7410
|
}
|
package/dist/bin/scan-tasks.js
CHANGED
|
@@ -808,6 +808,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
|
808
808
|
if (!agentName) return false;
|
|
809
809
|
return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
|
|
810
810
|
}
|
|
811
|
+
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
812
|
+
if (!existsSync6(employeesPath)) {
|
|
813
|
+
return [];
|
|
814
|
+
}
|
|
815
|
+
const raw = await readFile2(employeesPath, "utf-8");
|
|
816
|
+
try {
|
|
817
|
+
return JSON.parse(raw);
|
|
818
|
+
} catch {
|
|
819
|
+
return [];
|
|
820
|
+
}
|
|
821
|
+
}
|
|
811
822
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
812
823
|
if (!existsSync6(employeesPath)) return [];
|
|
813
824
|
try {
|
|
@@ -3984,8 +3995,35 @@ var init_tasks_crud = __esm({
|
|
|
3984
3995
|
});
|
|
3985
3996
|
|
|
3986
3997
|
// src/lib/tasks-review.ts
|
|
3998
|
+
var tasks_review_exports = {};
|
|
3999
|
+
__export(tasks_review_exports, {
|
|
4000
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
4001
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
4002
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
4003
|
+
countPendingReviews: () => countPendingReviews,
|
|
4004
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
4005
|
+
formatAge: () => formatAge,
|
|
4006
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
4007
|
+
isStale: () => isStale,
|
|
4008
|
+
listPendingReviews: () => listPendingReviews
|
|
4009
|
+
});
|
|
3987
4010
|
import path14 from "path";
|
|
3988
4011
|
import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
|
|
4012
|
+
function formatAge(isoTimestamp) {
|
|
4013
|
+
if (!isoTimestamp) return "";
|
|
4014
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
4015
|
+
if (ms < 0) return "just now";
|
|
4016
|
+
const minutes = Math.floor(ms / 6e4);
|
|
4017
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
4018
|
+
const hours = Math.floor(minutes / 60);
|
|
4019
|
+
if (hours < 24) return `${hours}h ago`;
|
|
4020
|
+
const days = Math.floor(hours / 24);
|
|
4021
|
+
return `${days}d ago`;
|
|
4022
|
+
}
|
|
4023
|
+
function isStale(isoTimestamp) {
|
|
4024
|
+
if (!isoTimestamp) return false;
|
|
4025
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
4026
|
+
}
|
|
3989
4027
|
async function countPendingReviews(sessionScope) {
|
|
3990
4028
|
const client = getClient();
|
|
3991
4029
|
const scope = strictSessionScopeFilter(
|
|
@@ -4106,6 +4144,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
4106
4144
|
]
|
|
4107
4145
|
};
|
|
4108
4146
|
}
|
|
4147
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
4148
|
+
const taskFile = String(row.task_file);
|
|
4149
|
+
const employees = await loadEmployees();
|
|
4150
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
4151
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
4152
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
4153
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
4154
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
4155
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
4156
|
+
const client = getClient();
|
|
4157
|
+
const agent = String(row.assigned_to);
|
|
4158
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
4159
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
4160
|
+
const currentStatus = String(row.status ?? "");
|
|
4161
|
+
if (currentStatus === "done") return;
|
|
4162
|
+
const existingResult = String(row.result ?? "");
|
|
4163
|
+
if (existingResult.includes("## Review notes")) return;
|
|
4164
|
+
let reviewerRole = "unknown";
|
|
4165
|
+
try {
|
|
4166
|
+
const emp = getEmployee(employees, reviewer);
|
|
4167
|
+
if (emp) reviewerRole = emp.role;
|
|
4168
|
+
} catch {
|
|
4169
|
+
}
|
|
4170
|
+
const taskTitle = String(row.title);
|
|
4171
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
4172
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
4173
|
+
process.stderr.write(
|
|
4174
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
4175
|
+
`
|
|
4176
|
+
);
|
|
4177
|
+
const reviewNotes = [
|
|
4178
|
+
`
|
|
4179
|
+
---
|
|
4180
|
+
## Review notes`,
|
|
4181
|
+
`Review lens: ${lens}`,
|
|
4182
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
4183
|
+
"",
|
|
4184
|
+
"### Checklist",
|
|
4185
|
+
...checklist,
|
|
4186
|
+
"",
|
|
4187
|
+
"### Verdict",
|
|
4188
|
+
"- **Approved:** mark this task as done",
|
|
4189
|
+
"- **Needs work:** re-open with notes"
|
|
4190
|
+
].join("\n");
|
|
4191
|
+
const originalTaskId = String(row.id);
|
|
4192
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
4193
|
+
await client.execute({
|
|
4194
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
4195
|
+
WHERE id = ?`,
|
|
4196
|
+
args: [updatedResult, now, originalTaskId]
|
|
4197
|
+
});
|
|
4198
|
+
orgBus.emit({
|
|
4199
|
+
type: "review_created",
|
|
4200
|
+
reviewId: originalTaskId,
|
|
4201
|
+
employee: agent,
|
|
4202
|
+
reviewer,
|
|
4203
|
+
timestamp: now
|
|
4204
|
+
});
|
|
4205
|
+
await writeNotification({
|
|
4206
|
+
agentId: agent,
|
|
4207
|
+
agentRole: String(row.assigned_to),
|
|
4208
|
+
event: "task_complete",
|
|
4209
|
+
project: String(row.project_name),
|
|
4210
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
4211
|
+
taskFile
|
|
4212
|
+
});
|
|
4213
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
4214
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
4215
|
+
if (!autoApprove) {
|
|
4216
|
+
try {
|
|
4217
|
+
const key = getSessionKey();
|
|
4218
|
+
const exeSession = getParentExe(key);
|
|
4219
|
+
if (exeSession) {
|
|
4220
|
+
sendIntercom(exeSession);
|
|
4221
|
+
}
|
|
4222
|
+
} catch {
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
if (autoApprove) {
|
|
4226
|
+
process.stderr.write(
|
|
4227
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
4228
|
+
`
|
|
4229
|
+
);
|
|
4230
|
+
await client.execute({
|
|
4231
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
4232
|
+
args: [now, originalTaskId]
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4109
4236
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
4110
4237
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
4111
4238
|
try {
|
|
@@ -5469,15 +5596,17 @@ function sendIntercom(targetSession) {
|
|
|
5469
5596
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
5470
5597
|
return "queued";
|
|
5471
5598
|
}
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5599
|
+
if (sessionState !== "idle") {
|
|
5600
|
+
try {
|
|
5601
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
5602
|
+
const agent = baseAgentName(rawAgent);
|
|
5603
|
+
const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
5604
|
+
if (existsSync14(markerPath)) {
|
|
5605
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
5606
|
+
return "debounced";
|
|
5607
|
+
}
|
|
5608
|
+
} catch {
|
|
5479
5609
|
}
|
|
5480
|
-
} catch {
|
|
5481
5610
|
}
|
|
5482
5611
|
try {
|
|
5483
5612
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -5548,6 +5677,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
5548
5677
|
try {
|
|
5549
5678
|
const sessions = transport.listSessions();
|
|
5550
5679
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
5680
|
+
try {
|
|
5681
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
5682
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
5683
|
+
if (pending instanceof Promise) {
|
|
5684
|
+
pending.then((count) => {
|
|
5685
|
+
if (count > 0) {
|
|
5686
|
+
execSync6(
|
|
5687
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5688
|
+
{ timeout: 3e3 }
|
|
5689
|
+
);
|
|
5690
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
5691
|
+
}
|
|
5692
|
+
}).catch(() => {
|
|
5693
|
+
});
|
|
5694
|
+
return true;
|
|
5695
|
+
}
|
|
5696
|
+
} catch {
|
|
5697
|
+
}
|
|
5551
5698
|
execSync6(
|
|
5552
5699
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
5553
5700
|
{ timeout: 3e3 }
|
package/dist/bin/setup.js
CHANGED
|
@@ -4358,6 +4358,12 @@ async function cloudSync(config) {
|
|
|
4358
4358
|
pulled = pullResult.records.length;
|
|
4359
4359
|
}
|
|
4360
4360
|
}
|
|
4361
|
+
if (pulled > 0) {
|
|
4362
|
+
try {
|
|
4363
|
+
await pushToPostgres(pullResult.records);
|
|
4364
|
+
} catch {
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4361
4367
|
if (pullResult.maxVersion > lastPullVersion) {
|
|
4362
4368
|
await client.execute({
|
|
4363
4369
|
sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
|
package/dist/gateway/index.js
CHANGED
|
@@ -6794,8 +6794,35 @@ var init_tasks_crud = __esm({
|
|
|
6794
6794
|
});
|
|
6795
6795
|
|
|
6796
6796
|
// src/lib/tasks-review.ts
|
|
6797
|
+
var tasks_review_exports = {};
|
|
6798
|
+
__export(tasks_review_exports, {
|
|
6799
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
6800
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
6801
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
6802
|
+
countPendingReviews: () => countPendingReviews,
|
|
6803
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
6804
|
+
formatAge: () => formatAge,
|
|
6805
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
6806
|
+
isStale: () => isStale,
|
|
6807
|
+
listPendingReviews: () => listPendingReviews
|
|
6808
|
+
});
|
|
6797
6809
|
import path17 from "path";
|
|
6798
6810
|
import { existsSync as existsSync15, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
6811
|
+
function formatAge(isoTimestamp) {
|
|
6812
|
+
if (!isoTimestamp) return "";
|
|
6813
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
6814
|
+
if (ms < 0) return "just now";
|
|
6815
|
+
const minutes = Math.floor(ms / 6e4);
|
|
6816
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
6817
|
+
const hours = Math.floor(minutes / 60);
|
|
6818
|
+
if (hours < 24) return `${hours}h ago`;
|
|
6819
|
+
const days = Math.floor(hours / 24);
|
|
6820
|
+
return `${days}d ago`;
|
|
6821
|
+
}
|
|
6822
|
+
function isStale(isoTimestamp) {
|
|
6823
|
+
if (!isoTimestamp) return false;
|
|
6824
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
6825
|
+
}
|
|
6799
6826
|
async function countPendingReviews(sessionScope) {
|
|
6800
6827
|
const client = getClient();
|
|
6801
6828
|
const scope = strictSessionScopeFilter(
|
|
@@ -6916,6 +6943,95 @@ function getReviewChecklist(role, agent, taskSlug) {
|
|
|
6916
6943
|
]
|
|
6917
6944
|
};
|
|
6918
6945
|
}
|
|
6946
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
6947
|
+
const taskFile = String(row.task_file);
|
|
6948
|
+
const employees = await loadEmployees();
|
|
6949
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
6950
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
6951
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
6952
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
6953
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
6954
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
6955
|
+
const client = getClient();
|
|
6956
|
+
const agent = String(row.assigned_to);
|
|
6957
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
6958
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
6959
|
+
const currentStatus = String(row.status ?? "");
|
|
6960
|
+
if (currentStatus === "done") return;
|
|
6961
|
+
const existingResult = String(row.result ?? "");
|
|
6962
|
+
if (existingResult.includes("## Review notes")) return;
|
|
6963
|
+
let reviewerRole = "unknown";
|
|
6964
|
+
try {
|
|
6965
|
+
const emp = getEmployee(employees, reviewer);
|
|
6966
|
+
if (emp) reviewerRole = emp.role;
|
|
6967
|
+
} catch {
|
|
6968
|
+
}
|
|
6969
|
+
const taskTitle = String(row.title);
|
|
6970
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
6971
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
6972
|
+
process.stderr.write(
|
|
6973
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
6974
|
+
`
|
|
6975
|
+
);
|
|
6976
|
+
const reviewNotes = [
|
|
6977
|
+
`
|
|
6978
|
+
---
|
|
6979
|
+
## Review notes`,
|
|
6980
|
+
`Review lens: ${lens}`,
|
|
6981
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
6982
|
+
"",
|
|
6983
|
+
"### Checklist",
|
|
6984
|
+
...checklist,
|
|
6985
|
+
"",
|
|
6986
|
+
"### Verdict",
|
|
6987
|
+
"- **Approved:** mark this task as done",
|
|
6988
|
+
"- **Needs work:** re-open with notes"
|
|
6989
|
+
].join("\n");
|
|
6990
|
+
const originalTaskId = String(row.id);
|
|
6991
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
6992
|
+
await client.execute({
|
|
6993
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
6994
|
+
WHERE id = ?`,
|
|
6995
|
+
args: [updatedResult, now, originalTaskId]
|
|
6996
|
+
});
|
|
6997
|
+
orgBus.emit({
|
|
6998
|
+
type: "review_created",
|
|
6999
|
+
reviewId: originalTaskId,
|
|
7000
|
+
employee: agent,
|
|
7001
|
+
reviewer,
|
|
7002
|
+
timestamp: now
|
|
7003
|
+
});
|
|
7004
|
+
await writeNotification({
|
|
7005
|
+
agentId: agent,
|
|
7006
|
+
agentRole: String(row.assigned_to),
|
|
7007
|
+
event: "task_complete",
|
|
7008
|
+
project: String(row.project_name),
|
|
7009
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
7010
|
+
taskFile
|
|
7011
|
+
});
|
|
7012
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
7013
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
7014
|
+
if (!autoApprove) {
|
|
7015
|
+
try {
|
|
7016
|
+
const key = getSessionKey();
|
|
7017
|
+
const exeSession = getParentExe(key);
|
|
7018
|
+
if (exeSession) {
|
|
7019
|
+
sendIntercom(exeSession);
|
|
7020
|
+
}
|
|
7021
|
+
} catch {
|
|
7022
|
+
}
|
|
7023
|
+
}
|
|
7024
|
+
if (autoApprove) {
|
|
7025
|
+
process.stderr.write(
|
|
7026
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
7027
|
+
`
|
|
7028
|
+
);
|
|
7029
|
+
await client.execute({
|
|
7030
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
7031
|
+
args: [now, originalTaskId]
|
|
7032
|
+
});
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
6919
7035
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
6920
7036
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
6921
7037
|
try {
|
|
@@ -8279,15 +8395,17 @@ function sendIntercom(targetSession) {
|
|
|
8279
8395
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
8280
8396
|
return "queued";
|
|
8281
8397
|
}
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8398
|
+
if (sessionState !== "idle") {
|
|
8399
|
+
try {
|
|
8400
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
8401
|
+
const agent = baseAgentName(rawAgent);
|
|
8402
|
+
const markerPath = path20.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
8403
|
+
if (existsSync16(markerPath)) {
|
|
8404
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
8405
|
+
return "debounced";
|
|
8406
|
+
}
|
|
8407
|
+
} catch {
|
|
8289
8408
|
}
|
|
8290
|
-
} catch {
|
|
8291
8409
|
}
|
|
8292
8410
|
try {
|
|
8293
8411
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
@@ -8358,6 +8476,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
8358
8476
|
try {
|
|
8359
8477
|
const sessions = transport.listSessions();
|
|
8360
8478
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
8479
|
+
try {
|
|
8480
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
8481
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
8482
|
+
if (pending instanceof Promise) {
|
|
8483
|
+
pending.then((count) => {
|
|
8484
|
+
if (count > 0) {
|
|
8485
|
+
execSync6(
|
|
8486
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
8487
|
+
{ timeout: 3e3 }
|
|
8488
|
+
);
|
|
8489
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
8490
|
+
}
|
|
8491
|
+
}).catch(() => {
|
|
8492
|
+
});
|
|
8493
|
+
return true;
|
|
8494
|
+
}
|
|
8495
|
+
} catch {
|
|
8496
|
+
}
|
|
8361
8497
|
execSync6(
|
|
8362
8498
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
8363
8499
|
{ timeout: 3e3 }
|