@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/lib/tasks.js
CHANGED
|
@@ -264,6 +264,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
|
264
264
|
if (!agentName) return false;
|
|
265
265
|
return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
|
|
266
266
|
}
|
|
267
|
+
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
268
|
+
if (!existsSync3(employeesPath)) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const raw = await readFile2(employeesPath, "utf-8");
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(raw);
|
|
274
|
+
} catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
267
278
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
268
279
|
if (!existsSync3(employeesPath)) return [];
|
|
269
280
|
try {
|
|
@@ -1246,6 +1257,369 @@ var init_capacity_monitor = __esm({
|
|
|
1246
1257
|
}
|
|
1247
1258
|
});
|
|
1248
1259
|
|
|
1260
|
+
// src/lib/state-bus.ts
|
|
1261
|
+
var StateBus, orgBus;
|
|
1262
|
+
var init_state_bus = __esm({
|
|
1263
|
+
"src/lib/state-bus.ts"() {
|
|
1264
|
+
"use strict";
|
|
1265
|
+
StateBus = class {
|
|
1266
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1267
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
1268
|
+
/** Emit an event to all subscribers */
|
|
1269
|
+
emit(event) {
|
|
1270
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
1271
|
+
if (typeHandlers) {
|
|
1272
|
+
for (const handler of typeHandlers) {
|
|
1273
|
+
try {
|
|
1274
|
+
handler(event);
|
|
1275
|
+
} catch {
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
for (const handler of this.globalHandlers) {
|
|
1280
|
+
try {
|
|
1281
|
+
handler(event);
|
|
1282
|
+
} catch {
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/** Subscribe to a specific event type */
|
|
1287
|
+
on(type, handler) {
|
|
1288
|
+
if (!this.handlers.has(type)) {
|
|
1289
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
1290
|
+
}
|
|
1291
|
+
this.handlers.get(type).add(handler);
|
|
1292
|
+
}
|
|
1293
|
+
/** Subscribe to ALL events */
|
|
1294
|
+
onAny(handler) {
|
|
1295
|
+
this.globalHandlers.add(handler);
|
|
1296
|
+
}
|
|
1297
|
+
/** Unsubscribe from a specific event type */
|
|
1298
|
+
off(type, handler) {
|
|
1299
|
+
this.handlers.get(type)?.delete(handler);
|
|
1300
|
+
}
|
|
1301
|
+
/** Unsubscribe from ALL events */
|
|
1302
|
+
offAny(handler) {
|
|
1303
|
+
this.globalHandlers.delete(handler);
|
|
1304
|
+
}
|
|
1305
|
+
/** Remove all listeners */
|
|
1306
|
+
clear() {
|
|
1307
|
+
this.handlers.clear();
|
|
1308
|
+
this.globalHandlers.clear();
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
orgBus = new StateBus();
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
// src/lib/tasks-review.ts
|
|
1316
|
+
var tasks_review_exports = {};
|
|
1317
|
+
__export(tasks_review_exports, {
|
|
1318
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
1319
|
+
cleanupReviewFile: () => cleanupReviewFile,
|
|
1320
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
1321
|
+
countPendingReviews: () => countPendingReviews,
|
|
1322
|
+
createReviewForCompletedTask: () => createReviewForCompletedTask,
|
|
1323
|
+
formatAge: () => formatAge,
|
|
1324
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
1325
|
+
isStale: () => isStale,
|
|
1326
|
+
listPendingReviews: () => listPendingReviews
|
|
1327
|
+
});
|
|
1328
|
+
import path9 from "path";
|
|
1329
|
+
import { existsSync as existsSync9, readdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
1330
|
+
function formatAge(isoTimestamp) {
|
|
1331
|
+
if (!isoTimestamp) return "";
|
|
1332
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
1333
|
+
if (ms < 0) return "just now";
|
|
1334
|
+
const minutes = Math.floor(ms / 6e4);
|
|
1335
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1336
|
+
const hours = Math.floor(minutes / 60);
|
|
1337
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1338
|
+
const days = Math.floor(hours / 24);
|
|
1339
|
+
return `${days}d ago`;
|
|
1340
|
+
}
|
|
1341
|
+
function isStale(isoTimestamp) {
|
|
1342
|
+
if (!isoTimestamp) return false;
|
|
1343
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
1344
|
+
}
|
|
1345
|
+
async function countPendingReviews(sessionScope) {
|
|
1346
|
+
const client = getClient();
|
|
1347
|
+
const scope = strictSessionScopeFilter(
|
|
1348
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
1349
|
+
);
|
|
1350
|
+
const result = await client.execute({
|
|
1351
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
1352
|
+
WHERE status = 'needs_review'${scope.sql}`,
|
|
1353
|
+
args: [...scope.args]
|
|
1354
|
+
});
|
|
1355
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
1356
|
+
}
|
|
1357
|
+
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
1358
|
+
const client = getClient();
|
|
1359
|
+
const scope = strictSessionScopeFilter(
|
|
1360
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
1361
|
+
);
|
|
1362
|
+
const result = await client.execute({
|
|
1363
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
1364
|
+
WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
|
|
1365
|
+
args: [sinceIso, ...scope.args]
|
|
1366
|
+
});
|
|
1367
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
1368
|
+
}
|
|
1369
|
+
async function listPendingReviews(limit, sessionScope) {
|
|
1370
|
+
const client = getClient();
|
|
1371
|
+
const scope = strictSessionScopeFilter(
|
|
1372
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
1373
|
+
);
|
|
1374
|
+
const result = await client.execute({
|
|
1375
|
+
sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
|
|
1376
|
+
WHERE status = 'needs_review'${scope.sql}
|
|
1377
|
+
ORDER BY updated_at ASC LIMIT ?`,
|
|
1378
|
+
args: [...scope.args, limit]
|
|
1379
|
+
});
|
|
1380
|
+
return result.rows;
|
|
1381
|
+
}
|
|
1382
|
+
async function cleanupOrphanedReviews() {
|
|
1383
|
+
const client = getClient();
|
|
1384
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1385
|
+
const r1 = await client.execute({
|
|
1386
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
1387
|
+
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
1388
|
+
AND assigned_by = 'system'
|
|
1389
|
+
AND title LIKE 'Review:%'
|
|
1390
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
|
|
1391
|
+
args: [now]
|
|
1392
|
+
});
|
|
1393
|
+
const r1b = await client.execute({
|
|
1394
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
1395
|
+
WHERE status IN ('open', 'needs_review')
|
|
1396
|
+
AND title LIKE 'Review:%completed%'
|
|
1397
|
+
AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
|
|
1398
|
+
args: [now]
|
|
1399
|
+
});
|
|
1400
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
1401
|
+
const r2 = await client.execute({
|
|
1402
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
1403
|
+
WHERE status = 'needs_review'
|
|
1404
|
+
AND result IS NOT NULL
|
|
1405
|
+
AND updated_at < ?`,
|
|
1406
|
+
args: [now, staleThreshold]
|
|
1407
|
+
});
|
|
1408
|
+
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
1409
|
+
if (total > 0) {
|
|
1410
|
+
process.stderr.write(
|
|
1411
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
1412
|
+
`
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
return total;
|
|
1416
|
+
}
|
|
1417
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
1418
|
+
const roleLower = role.toLowerCase();
|
|
1419
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
1420
|
+
return {
|
|
1421
|
+
lens: "Code Quality (Engineer)",
|
|
1422
|
+
checklist: [
|
|
1423
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
1424
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
1425
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
1426
|
+
"4. Any regressions in the test suite?"
|
|
1427
|
+
]
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
1431
|
+
return {
|
|
1432
|
+
lens: "Architecture (CTO)",
|
|
1433
|
+
checklist: [
|
|
1434
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
1435
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
1436
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
1437
|
+
"4. Security implications? Any new attack surface?",
|
|
1438
|
+
"5. Does it scale? Performance considerations?",
|
|
1439
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
1440
|
+
]
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
1444
|
+
return {
|
|
1445
|
+
lens: "Strategic (COO)",
|
|
1446
|
+
checklist: [
|
|
1447
|
+
"1. Does this serve the project mission?",
|
|
1448
|
+
"2. Is this the right work at the right time?",
|
|
1449
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
1450
|
+
"4. Any cross-project implications?"
|
|
1451
|
+
]
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
lens: "General",
|
|
1456
|
+
checklist: [
|
|
1457
|
+
"1. Read the original task's acceptance criteria",
|
|
1458
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
1459
|
+
"3. Verify code changes match requirements",
|
|
1460
|
+
"4. Check if tests were added/updated",
|
|
1461
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
1462
|
+
]
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
1466
|
+
const taskFile = String(row.task_file);
|
|
1467
|
+
const employees = await loadEmployees();
|
|
1468
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
1469
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
1470
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
1471
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
1472
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
1473
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
1474
|
+
const client = getClient();
|
|
1475
|
+
const agent = String(row.assigned_to);
|
|
1476
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
1477
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
1478
|
+
const currentStatus = String(row.status ?? "");
|
|
1479
|
+
if (currentStatus === "done") return;
|
|
1480
|
+
const existingResult = String(row.result ?? "");
|
|
1481
|
+
if (existingResult.includes("## Review notes")) return;
|
|
1482
|
+
let reviewerRole = "unknown";
|
|
1483
|
+
try {
|
|
1484
|
+
const emp = getEmployee(employees, reviewer);
|
|
1485
|
+
if (emp) reviewerRole = emp.role;
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
const taskTitle = String(row.title);
|
|
1489
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
1490
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
1491
|
+
process.stderr.write(
|
|
1492
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
1493
|
+
`
|
|
1494
|
+
);
|
|
1495
|
+
const reviewNotes = [
|
|
1496
|
+
`
|
|
1497
|
+
---
|
|
1498
|
+
## Review notes`,
|
|
1499
|
+
`Review lens: ${lens}`,
|
|
1500
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
1501
|
+
"",
|
|
1502
|
+
"### Checklist",
|
|
1503
|
+
...checklist,
|
|
1504
|
+
"",
|
|
1505
|
+
"### Verdict",
|
|
1506
|
+
"- **Approved:** mark this task as done",
|
|
1507
|
+
"- **Needs work:** re-open with notes"
|
|
1508
|
+
].join("\n");
|
|
1509
|
+
const originalTaskId = String(row.id);
|
|
1510
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
1511
|
+
await client.execute({
|
|
1512
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
1513
|
+
WHERE id = ?`,
|
|
1514
|
+
args: [updatedResult, now, originalTaskId]
|
|
1515
|
+
});
|
|
1516
|
+
orgBus.emit({
|
|
1517
|
+
type: "review_created",
|
|
1518
|
+
reviewId: originalTaskId,
|
|
1519
|
+
employee: agent,
|
|
1520
|
+
reviewer,
|
|
1521
|
+
timestamp: now
|
|
1522
|
+
});
|
|
1523
|
+
await writeNotification({
|
|
1524
|
+
agentId: agent,
|
|
1525
|
+
agentRole: String(row.assigned_to),
|
|
1526
|
+
event: "task_complete",
|
|
1527
|
+
project: String(row.project_name),
|
|
1528
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
1529
|
+
taskFile
|
|
1530
|
+
});
|
|
1531
|
+
const originalPriority = String(row.priority).toLowerCase();
|
|
1532
|
+
const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
|
|
1533
|
+
if (!autoApprove) {
|
|
1534
|
+
try {
|
|
1535
|
+
const key = getSessionKey();
|
|
1536
|
+
const exeSession = getParentExe(key);
|
|
1537
|
+
if (exeSession) {
|
|
1538
|
+
sendIntercom(exeSession);
|
|
1539
|
+
}
|
|
1540
|
+
} catch {
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (autoApprove) {
|
|
1544
|
+
process.stderr.write(
|
|
1545
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
1546
|
+
`
|
|
1547
|
+
);
|
|
1548
|
+
await client.execute({
|
|
1549
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
|
|
1550
|
+
args: [now, originalTaskId]
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
1555
|
+
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
1556
|
+
try {
|
|
1557
|
+
const client = getClient();
|
|
1558
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1559
|
+
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
1560
|
+
if (parentId) {
|
|
1561
|
+
const result = await client.execute({
|
|
1562
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
1563
|
+
args: [now, parentId]
|
|
1564
|
+
});
|
|
1565
|
+
if (result.rowsAffected > 0) {
|
|
1566
|
+
process.stderr.write(
|
|
1567
|
+
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
1568
|
+
`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
} else {
|
|
1572
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
1573
|
+
const reviewPrefix = fileName.replace(".md", "");
|
|
1574
|
+
const parts = reviewPrefix.split("-");
|
|
1575
|
+
if (parts.length >= 3 && parts[0] === "review") {
|
|
1576
|
+
const agent = parts[1];
|
|
1577
|
+
const slug = parts.slice(2).join("-");
|
|
1578
|
+
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
1579
|
+
const result = await client.execute({
|
|
1580
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
1581
|
+
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
1582
|
+
});
|
|
1583
|
+
if (result.rowsAffected > 0) {
|
|
1584
|
+
process.stderr.write(
|
|
1585
|
+
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
1586
|
+
`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
process.stderr.write(
|
|
1593
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
1594
|
+
`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
try {
|
|
1598
|
+
const cacheDir = path9.join(EXE_AI_DIR, "session-cache");
|
|
1599
|
+
if (existsSync9(cacheDir)) {
|
|
1600
|
+
for (const f of readdirSync(cacheDir)) {
|
|
1601
|
+
if (f.startsWith("review-notified-")) {
|
|
1602
|
+
unlinkSync2(path9.join(cacheDir, f));
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
} catch {
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
var init_tasks_review = __esm({
|
|
1610
|
+
"src/lib/tasks-review.ts"() {
|
|
1611
|
+
"use strict";
|
|
1612
|
+
init_database();
|
|
1613
|
+
init_config();
|
|
1614
|
+
init_employees();
|
|
1615
|
+
init_notifications();
|
|
1616
|
+
init_tmux_routing();
|
|
1617
|
+
init_session_key();
|
|
1618
|
+
init_state_bus();
|
|
1619
|
+
init_task_scope();
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1249
1623
|
// src/lib/tmux-routing.ts
|
|
1250
1624
|
var tmux_routing_exports = {};
|
|
1251
1625
|
__export(tmux_routing_exports, {
|
|
@@ -1272,13 +1646,13 @@ __export(tmux_routing_exports, {
|
|
|
1272
1646
|
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
1273
1647
|
});
|
|
1274
1648
|
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
1275
|
-
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as
|
|
1276
|
-
import
|
|
1649
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync, readdirSync as readdirSync2 } from "fs";
|
|
1650
|
+
import path10 from "path";
|
|
1277
1651
|
import os7 from "os";
|
|
1278
1652
|
import { fileURLToPath } from "url";
|
|
1279
|
-
import { unlinkSync as
|
|
1653
|
+
import { unlinkSync as unlinkSync3 } from "fs";
|
|
1280
1654
|
function spawnLockPath(sessionName) {
|
|
1281
|
-
return
|
|
1655
|
+
return path10.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
1282
1656
|
}
|
|
1283
1657
|
function isProcessAlive(pid) {
|
|
1284
1658
|
try {
|
|
@@ -1289,11 +1663,11 @@ function isProcessAlive(pid) {
|
|
|
1289
1663
|
}
|
|
1290
1664
|
}
|
|
1291
1665
|
function acquireSpawnLock(sessionName) {
|
|
1292
|
-
if (!
|
|
1666
|
+
if (!existsSync10(SPAWN_LOCK_DIR)) {
|
|
1293
1667
|
mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
|
|
1294
1668
|
}
|
|
1295
1669
|
const lockFile = spawnLockPath(sessionName);
|
|
1296
|
-
if (
|
|
1670
|
+
if (existsSync10(lockFile)) {
|
|
1297
1671
|
try {
|
|
1298
1672
|
const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
|
|
1299
1673
|
const age = Date.now() - lock.timestamp;
|
|
@@ -1308,20 +1682,20 @@ function acquireSpawnLock(sessionName) {
|
|
|
1308
1682
|
}
|
|
1309
1683
|
function releaseSpawnLock(sessionName) {
|
|
1310
1684
|
try {
|
|
1311
|
-
|
|
1685
|
+
unlinkSync3(spawnLockPath(sessionName));
|
|
1312
1686
|
} catch {
|
|
1313
1687
|
}
|
|
1314
1688
|
}
|
|
1315
1689
|
function resolveBehaviorsExporterScript() {
|
|
1316
1690
|
try {
|
|
1317
1691
|
const thisFile = fileURLToPath(import.meta.url);
|
|
1318
|
-
const scriptPath =
|
|
1319
|
-
|
|
1692
|
+
const scriptPath = path10.join(
|
|
1693
|
+
path10.dirname(thisFile),
|
|
1320
1694
|
"..",
|
|
1321
1695
|
"bin",
|
|
1322
1696
|
"exe-export-behaviors.js"
|
|
1323
1697
|
);
|
|
1324
|
-
return
|
|
1698
|
+
return existsSync10(scriptPath) ? scriptPath : null;
|
|
1325
1699
|
} catch {
|
|
1326
1700
|
return null;
|
|
1327
1701
|
}
|
|
@@ -1387,11 +1761,11 @@ function extractRootExe(name) {
|
|
|
1387
1761
|
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
1388
1762
|
}
|
|
1389
1763
|
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
1390
|
-
if (!
|
|
1764
|
+
if (!existsSync10(SESSION_CACHE)) {
|
|
1391
1765
|
mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
1392
1766
|
}
|
|
1393
1767
|
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
1394
|
-
const filePath =
|
|
1768
|
+
const filePath = path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
1395
1769
|
writeFileSync6(filePath, JSON.stringify({
|
|
1396
1770
|
parentExe: rootExe,
|
|
1397
1771
|
dispatchedBy: dispatchedBy || rootExe,
|
|
@@ -1400,7 +1774,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
|
1400
1774
|
}
|
|
1401
1775
|
function getParentExe(sessionKey) {
|
|
1402
1776
|
try {
|
|
1403
|
-
const data = JSON.parse(readFileSync8(
|
|
1777
|
+
const data = JSON.parse(readFileSync8(path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
1404
1778
|
return data.parentExe || null;
|
|
1405
1779
|
} catch {
|
|
1406
1780
|
return null;
|
|
@@ -1409,7 +1783,7 @@ function getParentExe(sessionKey) {
|
|
|
1409
1783
|
function getDispatchedBy(sessionKey) {
|
|
1410
1784
|
try {
|
|
1411
1785
|
const data = JSON.parse(readFileSync8(
|
|
1412
|
-
|
|
1786
|
+
path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
1413
1787
|
"utf8"
|
|
1414
1788
|
));
|
|
1415
1789
|
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
@@ -1479,7 +1853,7 @@ async function verifyPaneAtCapacity(sessionName) {
|
|
|
1479
1853
|
}
|
|
1480
1854
|
function readDebounceState() {
|
|
1481
1855
|
try {
|
|
1482
|
-
if (!
|
|
1856
|
+
if (!existsSync10(DEBOUNCE_FILE)) return {};
|
|
1483
1857
|
const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
|
|
1484
1858
|
const state = {};
|
|
1485
1859
|
for (const [key, val] of Object.entries(raw)) {
|
|
@@ -1496,7 +1870,7 @@ function readDebounceState() {
|
|
|
1496
1870
|
}
|
|
1497
1871
|
function writeDebounceState(state) {
|
|
1498
1872
|
try {
|
|
1499
|
-
if (!
|
|
1873
|
+
if (!existsSync10(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
1500
1874
|
writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
|
|
1501
1875
|
} catch {
|
|
1502
1876
|
}
|
|
@@ -1592,22 +1966,24 @@ function sendIntercom(targetSession) {
|
|
|
1592
1966
|
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
1593
1967
|
return "queued";
|
|
1594
1968
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1969
|
+
if (sessionState !== "idle") {
|
|
1970
|
+
try {
|
|
1971
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
1972
|
+
const agent = baseAgentName(rawAgent);
|
|
1973
|
+
const markerPath = path10.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
1974
|
+
if (existsSync10(markerPath)) {
|
|
1975
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
1976
|
+
return "debounced";
|
|
1977
|
+
}
|
|
1978
|
+
} catch {
|
|
1602
1979
|
}
|
|
1603
|
-
} catch {
|
|
1604
1980
|
}
|
|
1605
1981
|
try {
|
|
1606
1982
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
1607
1983
|
const agent = baseAgentName(rawAgent);
|
|
1608
|
-
const taskDir =
|
|
1609
|
-
if (
|
|
1610
|
-
const files =
|
|
1984
|
+
const taskDir = path10.join(process.cwd(), "exe", agent);
|
|
1985
|
+
if (existsSync10(taskDir)) {
|
|
1986
|
+
const files = readdirSync2(taskDir).filter(
|
|
1611
1987
|
(f) => f.endsWith(".md") && f !== "DONE.txt"
|
|
1612
1988
|
);
|
|
1613
1989
|
if (files.length === 0) {
|
|
@@ -1671,6 +2047,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
|
|
|
1671
2047
|
try {
|
|
1672
2048
|
const sessions = transport.listSessions();
|
|
1673
2049
|
if (!sessions.includes(coordinatorSession)) return false;
|
|
2050
|
+
try {
|
|
2051
|
+
const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
|
|
2052
|
+
const pending = countPendingReviews2(coordinatorSession);
|
|
2053
|
+
if (pending instanceof Promise) {
|
|
2054
|
+
pending.then((count) => {
|
|
2055
|
+
if (count > 0) {
|
|
2056
|
+
execSync4(
|
|
2057
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
2058
|
+
{ timeout: 3e3 }
|
|
2059
|
+
);
|
|
2060
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
|
|
2061
|
+
}
|
|
2062
|
+
}).catch(() => {
|
|
2063
|
+
});
|
|
2064
|
+
return true;
|
|
2065
|
+
}
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
1674
2068
|
execSync4(
|
|
1675
2069
|
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
1676
2070
|
{ timeout: 3e3 }
|
|
@@ -1754,23 +2148,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1754
2148
|
const transport = getTransport();
|
|
1755
2149
|
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
1756
2150
|
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
1757
|
-
const logDir =
|
|
1758
|
-
const logFile =
|
|
1759
|
-
if (!
|
|
2151
|
+
const logDir = path10.join(os7.homedir(), ".exe-os", "session-logs");
|
|
2152
|
+
const logFile = path10.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
2153
|
+
if (!existsSync10(logDir)) {
|
|
1760
2154
|
mkdirSync5(logDir, { recursive: true });
|
|
1761
2155
|
}
|
|
1762
2156
|
transport.kill(sessionName);
|
|
1763
2157
|
let cleanupSuffix = "";
|
|
1764
2158
|
try {
|
|
1765
2159
|
const thisFile = fileURLToPath(import.meta.url);
|
|
1766
|
-
const cleanupScript =
|
|
1767
|
-
if (
|
|
2160
|
+
const cleanupScript = path10.join(path10.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
2161
|
+
if (existsSync10(cleanupScript)) {
|
|
1768
2162
|
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
1769
2163
|
}
|
|
1770
2164
|
} catch {
|
|
1771
2165
|
}
|
|
1772
2166
|
try {
|
|
1773
|
-
const claudeJsonPath =
|
|
2167
|
+
const claudeJsonPath = path10.join(os7.homedir(), ".claude.json");
|
|
1774
2168
|
let claudeJson = {};
|
|
1775
2169
|
try {
|
|
1776
2170
|
claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
|
|
@@ -1785,10 +2179,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1785
2179
|
} catch {
|
|
1786
2180
|
}
|
|
1787
2181
|
try {
|
|
1788
|
-
const settingsDir =
|
|
2182
|
+
const settingsDir = path10.join(os7.homedir(), ".claude", "projects");
|
|
1789
2183
|
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
1790
|
-
const projSettingsDir =
|
|
1791
|
-
const settingsPath =
|
|
2184
|
+
const projSettingsDir = path10.join(settingsDir, normalizedKey);
|
|
2185
|
+
const settingsPath = path10.join(projSettingsDir, "settings.json");
|
|
1792
2186
|
let settings = {};
|
|
1793
2187
|
try {
|
|
1794
2188
|
settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
|
|
@@ -1835,7 +2229,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1835
2229
|
let behaviorsFlag = "";
|
|
1836
2230
|
let legacyFallbackWarned = false;
|
|
1837
2231
|
if (!useExeAgent && !useBinSymlink) {
|
|
1838
|
-
const identityPath =
|
|
2232
|
+
const identityPath = path10.join(
|
|
1839
2233
|
os7.homedir(),
|
|
1840
2234
|
".exe-os",
|
|
1841
2235
|
"identity",
|
|
@@ -1845,13 +2239,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1845
2239
|
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
1846
2240
|
if (hasAgentFlag) {
|
|
1847
2241
|
identityFlag = ` --agent ${employeeName}`;
|
|
1848
|
-
} else if (
|
|
2242
|
+
} else if (existsSync10(identityPath)) {
|
|
1849
2243
|
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
1850
2244
|
legacyFallbackWarned = true;
|
|
1851
2245
|
}
|
|
1852
2246
|
const behaviorsFile = exportBehaviorsSync(
|
|
1853
2247
|
employeeName,
|
|
1854
|
-
|
|
2248
|
+
path10.basename(spawnCwd),
|
|
1855
2249
|
sessionName
|
|
1856
2250
|
);
|
|
1857
2251
|
if (behaviorsFile) {
|
|
@@ -1866,9 +2260,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1866
2260
|
}
|
|
1867
2261
|
let sessionContextFlag = "";
|
|
1868
2262
|
try {
|
|
1869
|
-
const ctxDir =
|
|
2263
|
+
const ctxDir = path10.join(os7.homedir(), ".exe-os", "session-cache");
|
|
1870
2264
|
mkdirSync5(ctxDir, { recursive: true });
|
|
1871
|
-
const ctxFile =
|
|
2265
|
+
const ctxFile = path10.join(ctxDir, `session-context-${sessionName}.md`);
|
|
1872
2266
|
const ctxContent = [
|
|
1873
2267
|
`## Session Context`,
|
|
1874
2268
|
`You are running in tmux session: ${sessionName}.`,
|
|
@@ -1952,7 +2346,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1952
2346
|
transport.pipeLog(sessionName, logFile);
|
|
1953
2347
|
try {
|
|
1954
2348
|
const mySession = getMySession();
|
|
1955
|
-
const dispatchInfo =
|
|
2349
|
+
const dispatchInfo = path10.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
1956
2350
|
writeFileSync6(dispatchInfo, JSON.stringify({
|
|
1957
2351
|
dispatchedBy: mySession,
|
|
1958
2352
|
rootExe: exeSession,
|
|
@@ -2027,15 +2421,15 @@ var init_tmux_routing = __esm({
|
|
|
2027
2421
|
init_intercom_queue();
|
|
2028
2422
|
init_plan_limits();
|
|
2029
2423
|
init_employees();
|
|
2030
|
-
SPAWN_LOCK_DIR =
|
|
2031
|
-
SESSION_CACHE =
|
|
2424
|
+
SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
|
|
2425
|
+
SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
|
|
2032
2426
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
2033
2427
|
VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
2034
2428
|
VERIFY_PANE_LINES = 200;
|
|
2035
2429
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
2036
2430
|
CODEX_DEBOUNCE_MS = 12e4;
|
|
2037
|
-
INTERCOM_LOG2 =
|
|
2038
|
-
DEBOUNCE_FILE =
|
|
2431
|
+
INTERCOM_LOG2 = path10.join(os7.homedir(), ".exe-os", "intercom.log");
|
|
2432
|
+
DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
|
|
2039
2433
|
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
2040
2434
|
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
|
|
2041
2435
|
}
|
|
@@ -2076,13 +2470,13 @@ var init_task_scope = __esm({
|
|
|
2076
2470
|
|
|
2077
2471
|
// src/lib/notifications.ts
|
|
2078
2472
|
import crypto2 from "crypto";
|
|
2079
|
-
import
|
|
2473
|
+
import path11 from "path";
|
|
2080
2474
|
import os8 from "os";
|
|
2081
2475
|
import {
|
|
2082
2476
|
readFileSync as readFileSync9,
|
|
2083
|
-
readdirSync as
|
|
2084
|
-
unlinkSync as
|
|
2085
|
-
existsSync as
|
|
2477
|
+
readdirSync as readdirSync3,
|
|
2478
|
+
unlinkSync as unlinkSync4,
|
|
2479
|
+
existsSync as existsSync11,
|
|
2086
2480
|
rmdirSync
|
|
2087
2481
|
} from "fs";
|
|
2088
2482
|
async function writeNotification(notification) {
|
|
@@ -2131,64 +2525,9 @@ var init_notifications = __esm({
|
|
|
2131
2525
|
}
|
|
2132
2526
|
});
|
|
2133
2527
|
|
|
2134
|
-
// src/lib/state-bus.ts
|
|
2135
|
-
var StateBus, orgBus;
|
|
2136
|
-
var init_state_bus = __esm({
|
|
2137
|
-
"src/lib/state-bus.ts"() {
|
|
2138
|
-
"use strict";
|
|
2139
|
-
StateBus = class {
|
|
2140
|
-
handlers = /* @__PURE__ */ new Map();
|
|
2141
|
-
globalHandlers = /* @__PURE__ */ new Set();
|
|
2142
|
-
/** Emit an event to all subscribers */
|
|
2143
|
-
emit(event) {
|
|
2144
|
-
const typeHandlers = this.handlers.get(event.type);
|
|
2145
|
-
if (typeHandlers) {
|
|
2146
|
-
for (const handler of typeHandlers) {
|
|
2147
|
-
try {
|
|
2148
|
-
handler(event);
|
|
2149
|
-
} catch {
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
for (const handler of this.globalHandlers) {
|
|
2154
|
-
try {
|
|
2155
|
-
handler(event);
|
|
2156
|
-
} catch {
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
/** Subscribe to a specific event type */
|
|
2161
|
-
on(type, handler) {
|
|
2162
|
-
if (!this.handlers.has(type)) {
|
|
2163
|
-
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
2164
|
-
}
|
|
2165
|
-
this.handlers.get(type).add(handler);
|
|
2166
|
-
}
|
|
2167
|
-
/** Subscribe to ALL events */
|
|
2168
|
-
onAny(handler) {
|
|
2169
|
-
this.globalHandlers.add(handler);
|
|
2170
|
-
}
|
|
2171
|
-
/** Unsubscribe from a specific event type */
|
|
2172
|
-
off(type, handler) {
|
|
2173
|
-
this.handlers.get(type)?.delete(handler);
|
|
2174
|
-
}
|
|
2175
|
-
/** Unsubscribe from ALL events */
|
|
2176
|
-
offAny(handler) {
|
|
2177
|
-
this.globalHandlers.delete(handler);
|
|
2178
|
-
}
|
|
2179
|
-
/** Remove all listeners */
|
|
2180
|
-
clear() {
|
|
2181
|
-
this.handlers.clear();
|
|
2182
|
-
this.globalHandlers.clear();
|
|
2183
|
-
}
|
|
2184
|
-
};
|
|
2185
|
-
orgBus = new StateBus();
|
|
2186
|
-
}
|
|
2187
|
-
});
|
|
2188
|
-
|
|
2189
2528
|
// src/lib/project-name.ts
|
|
2190
2529
|
import { execSync as execSync5 } from "child_process";
|
|
2191
|
-
import
|
|
2530
|
+
import path12 from "path";
|
|
2192
2531
|
function getProjectName(cwd) {
|
|
2193
2532
|
const dir = cwd ?? process.cwd();
|
|
2194
2533
|
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
@@ -2201,7 +2540,7 @@ function getProjectName(cwd) {
|
|
|
2201
2540
|
timeout: 2e3,
|
|
2202
2541
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2203
2542
|
}).trim();
|
|
2204
|
-
repoRoot =
|
|
2543
|
+
repoRoot = path12.dirname(gitCommonDir);
|
|
2205
2544
|
} catch {
|
|
2206
2545
|
repoRoot = execSync5("git rev-parse --show-toplevel", {
|
|
2207
2546
|
cwd: dir,
|
|
@@ -2210,11 +2549,11 @@ function getProjectName(cwd) {
|
|
|
2210
2549
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2211
2550
|
}).trim();
|
|
2212
2551
|
}
|
|
2213
|
-
_cached2 =
|
|
2552
|
+
_cached2 = path12.basename(repoRoot);
|
|
2214
2553
|
_cachedCwd = dir;
|
|
2215
2554
|
return _cached2;
|
|
2216
2555
|
} catch {
|
|
2217
|
-
_cached2 =
|
|
2556
|
+
_cached2 = path12.basename(dir);
|
|
2218
2557
|
_cachedCwd = dir;
|
|
2219
2558
|
return _cached2;
|
|
2220
2559
|
}
|
|
@@ -2292,11 +2631,11 @@ var init_session_scope = __esm({
|
|
|
2292
2631
|
|
|
2293
2632
|
// src/lib/tasks-crud.ts
|
|
2294
2633
|
import crypto3 from "crypto";
|
|
2295
|
-
import
|
|
2634
|
+
import path13 from "path";
|
|
2296
2635
|
import os9 from "os";
|
|
2297
2636
|
import { execSync as execSync6 } from "child_process";
|
|
2298
2637
|
import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
|
|
2299
|
-
import { existsSync as
|
|
2638
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
2300
2639
|
async function writeCheckpoint(input) {
|
|
2301
2640
|
const client = getClient();
|
|
2302
2641
|
const row = await resolveTask(client, input.taskId);
|
|
@@ -2490,8 +2829,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
|
|
|
2490
2829
|
}
|
|
2491
2830
|
if (input.baseDir) {
|
|
2492
2831
|
try {
|
|
2493
|
-
await mkdir3(
|
|
2494
|
-
await mkdir3(
|
|
2832
|
+
await mkdir3(path13.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
2833
|
+
await mkdir3(path13.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
2495
2834
|
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
2496
2835
|
await ensureGitignoreExe(input.baseDir);
|
|
2497
2836
|
} catch {
|
|
@@ -2527,10 +2866,10 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
|
|
|
2527
2866
|
});
|
|
2528
2867
|
if (input.baseDir) {
|
|
2529
2868
|
try {
|
|
2530
|
-
const EXE_OS_DIR =
|
|
2531
|
-
const mdPath =
|
|
2532
|
-
const mdDir =
|
|
2533
|
-
if (!
|
|
2869
|
+
const EXE_OS_DIR = path13.join(os9.homedir(), ".exe-os");
|
|
2870
|
+
const mdPath = path13.join(EXE_OS_DIR, taskFile);
|
|
2871
|
+
const mdDir = path13.dirname(mdPath);
|
|
2872
|
+
if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
|
|
2534
2873
|
const reviewer = input.reviewer ?? input.assignedBy;
|
|
2535
2874
|
const mdContent = `# ${input.title}
|
|
2536
2875
|
|
|
@@ -2830,9 +3169,9 @@ async function deleteTaskCore(taskId, _baseDir) {
|
|
|
2830
3169
|
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
2831
3170
|
}
|
|
2832
3171
|
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
2833
|
-
const archPath =
|
|
3172
|
+
const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
2834
3173
|
try {
|
|
2835
|
-
if (
|
|
3174
|
+
if (existsSync12(archPath)) return;
|
|
2836
3175
|
const template = [
|
|
2837
3176
|
`# ${projectName} \u2014 System Architecture`,
|
|
2838
3177
|
"",
|
|
@@ -2865,9 +3204,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
|
|
|
2865
3204
|
}
|
|
2866
3205
|
}
|
|
2867
3206
|
async function ensureGitignoreExe(baseDir) {
|
|
2868
|
-
const gitignorePath =
|
|
3207
|
+
const gitignorePath = path13.join(baseDir, ".gitignore");
|
|
2869
3208
|
try {
|
|
2870
|
-
if (
|
|
3209
|
+
if (existsSync12(gitignorePath)) {
|
|
2871
3210
|
const content = readFileSync10(gitignorePath, "utf-8");
|
|
2872
3211
|
if (/^\/?exe\/?$/m.test(content)) return;
|
|
2873
3212
|
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
@@ -2898,198 +3237,6 @@ var init_tasks_crud = __esm({
|
|
|
2898
3237
|
}
|
|
2899
3238
|
});
|
|
2900
3239
|
|
|
2901
|
-
// src/lib/tasks-review.ts
|
|
2902
|
-
import path13 from "path";
|
|
2903
|
-
import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
2904
|
-
async function countPendingReviews(sessionScope) {
|
|
2905
|
-
const client = getClient();
|
|
2906
|
-
const scope = strictSessionScopeFilter(
|
|
2907
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2908
|
-
);
|
|
2909
|
-
const result = await client.execute({
|
|
2910
|
-
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
2911
|
-
WHERE status = 'needs_review'${scope.sql}`,
|
|
2912
|
-
args: [...scope.args]
|
|
2913
|
-
});
|
|
2914
|
-
return Number(result.rows[0]?.cnt) || 0;
|
|
2915
|
-
}
|
|
2916
|
-
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
2917
|
-
const client = getClient();
|
|
2918
|
-
const scope = strictSessionScopeFilter(
|
|
2919
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2920
|
-
);
|
|
2921
|
-
const result = await client.execute({
|
|
2922
|
-
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
2923
|
-
WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
|
|
2924
|
-
args: [sinceIso, ...scope.args]
|
|
2925
|
-
});
|
|
2926
|
-
return Number(result.rows[0]?.cnt) || 0;
|
|
2927
|
-
}
|
|
2928
|
-
async function listPendingReviews(limit, sessionScope) {
|
|
2929
|
-
const client = getClient();
|
|
2930
|
-
const scope = strictSessionScopeFilter(
|
|
2931
|
-
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2932
|
-
);
|
|
2933
|
-
const result = await client.execute({
|
|
2934
|
-
sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
|
|
2935
|
-
WHERE status = 'needs_review'${scope.sql}
|
|
2936
|
-
ORDER BY updated_at ASC LIMIT ?`,
|
|
2937
|
-
args: [...scope.args, limit]
|
|
2938
|
-
});
|
|
2939
|
-
return result.rows;
|
|
2940
|
-
}
|
|
2941
|
-
async function cleanupOrphanedReviews() {
|
|
2942
|
-
const client = getClient();
|
|
2943
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2944
|
-
const r1 = await client.execute({
|
|
2945
|
-
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
2946
|
-
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
2947
|
-
AND assigned_by = 'system'
|
|
2948
|
-
AND title LIKE 'Review:%'
|
|
2949
|
-
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
|
|
2950
|
-
args: [now]
|
|
2951
|
-
});
|
|
2952
|
-
const r1b = await client.execute({
|
|
2953
|
-
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
2954
|
-
WHERE status IN ('open', 'needs_review')
|
|
2955
|
-
AND title LIKE 'Review:%completed%'
|
|
2956
|
-
AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
|
|
2957
|
-
args: [now]
|
|
2958
|
-
});
|
|
2959
|
-
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
2960
|
-
const r2 = await client.execute({
|
|
2961
|
-
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
2962
|
-
WHERE status = 'needs_review'
|
|
2963
|
-
AND result IS NOT NULL
|
|
2964
|
-
AND updated_at < ?`,
|
|
2965
|
-
args: [now, staleThreshold]
|
|
2966
|
-
});
|
|
2967
|
-
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
2968
|
-
if (total > 0) {
|
|
2969
|
-
process.stderr.write(
|
|
2970
|
-
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
2971
|
-
`
|
|
2972
|
-
);
|
|
2973
|
-
}
|
|
2974
|
-
return total;
|
|
2975
|
-
}
|
|
2976
|
-
function getReviewChecklist(role, agent, taskSlug) {
|
|
2977
|
-
const roleLower = role.toLowerCase();
|
|
2978
|
-
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
2979
|
-
return {
|
|
2980
|
-
lens: "Code Quality (Engineer)",
|
|
2981
|
-
checklist: [
|
|
2982
|
-
"1. Do all tests pass? Any new tests needed?",
|
|
2983
|
-
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
2984
|
-
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
2985
|
-
"4. Any regressions in the test suite?"
|
|
2986
|
-
]
|
|
2987
|
-
};
|
|
2988
|
-
}
|
|
2989
|
-
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
2990
|
-
return {
|
|
2991
|
-
lens: "Architecture (CTO)",
|
|
2992
|
-
checklist: [
|
|
2993
|
-
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
2994
|
-
"2. Is it backward compatible? Any breaking changes?",
|
|
2995
|
-
"3. Does it introduce technical debt? Is that debt justified?",
|
|
2996
|
-
"4. Security implications? Any new attack surface?",
|
|
2997
|
-
"5. Does it scale? Performance considerations?",
|
|
2998
|
-
"6. Coordination: does this affect other employees' work or other projects?"
|
|
2999
|
-
]
|
|
3000
|
-
};
|
|
3001
|
-
}
|
|
3002
|
-
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
3003
|
-
return {
|
|
3004
|
-
lens: "Strategic (COO)",
|
|
3005
|
-
checklist: [
|
|
3006
|
-
"1. Does this serve the project mission?",
|
|
3007
|
-
"2. Is this the right work at the right time?",
|
|
3008
|
-
"3. Does the architectural assessment make sense for the business?",
|
|
3009
|
-
"4. Any cross-project implications?"
|
|
3010
|
-
]
|
|
3011
|
-
};
|
|
3012
|
-
}
|
|
3013
|
-
return {
|
|
3014
|
-
lens: "General",
|
|
3015
|
-
checklist: [
|
|
3016
|
-
"1. Read the original task's acceptance criteria",
|
|
3017
|
-
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
3018
|
-
"3. Verify code changes match requirements",
|
|
3019
|
-
"4. Check if tests were added/updated",
|
|
3020
|
-
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
3021
|
-
]
|
|
3022
|
-
};
|
|
3023
|
-
}
|
|
3024
|
-
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
3025
|
-
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
3026
|
-
try {
|
|
3027
|
-
const client = getClient();
|
|
3028
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3029
|
-
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
3030
|
-
if (parentId) {
|
|
3031
|
-
const result = await client.execute({
|
|
3032
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
3033
|
-
args: [now, parentId]
|
|
3034
|
-
});
|
|
3035
|
-
if (result.rowsAffected > 0) {
|
|
3036
|
-
process.stderr.write(
|
|
3037
|
-
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
3038
|
-
`
|
|
3039
|
-
);
|
|
3040
|
-
}
|
|
3041
|
-
} else {
|
|
3042
|
-
const fileName = taskFile.split("/").pop() ?? "";
|
|
3043
|
-
const reviewPrefix = fileName.replace(".md", "");
|
|
3044
|
-
const parts = reviewPrefix.split("-");
|
|
3045
|
-
if (parts.length >= 3 && parts[0] === "review") {
|
|
3046
|
-
const agent = parts[1];
|
|
3047
|
-
const slug = parts.slice(2).join("-");
|
|
3048
|
-
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
3049
|
-
const result = await client.execute({
|
|
3050
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
3051
|
-
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
3052
|
-
});
|
|
3053
|
-
if (result.rowsAffected > 0) {
|
|
3054
|
-
process.stderr.write(
|
|
3055
|
-
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
3056
|
-
`
|
|
3057
|
-
);
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
} catch (err) {
|
|
3062
|
-
process.stderr.write(
|
|
3063
|
-
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
3064
|
-
`
|
|
3065
|
-
);
|
|
3066
|
-
}
|
|
3067
|
-
try {
|
|
3068
|
-
const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
|
|
3069
|
-
if (existsSync12(cacheDir)) {
|
|
3070
|
-
for (const f of readdirSync3(cacheDir)) {
|
|
3071
|
-
if (f.startsWith("review-notified-")) {
|
|
3072
|
-
unlinkSync4(path13.join(cacheDir, f));
|
|
3073
|
-
}
|
|
3074
|
-
}
|
|
3075
|
-
}
|
|
3076
|
-
} catch {
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
var init_tasks_review = __esm({
|
|
3080
|
-
"src/lib/tasks-review.ts"() {
|
|
3081
|
-
"use strict";
|
|
3082
|
-
init_database();
|
|
3083
|
-
init_config();
|
|
3084
|
-
init_employees();
|
|
3085
|
-
init_notifications();
|
|
3086
|
-
init_tmux_routing();
|
|
3087
|
-
init_session_key();
|
|
3088
|
-
init_state_bus();
|
|
3089
|
-
init_task_scope();
|
|
3090
|
-
}
|
|
3091
|
-
});
|
|
3092
|
-
|
|
3093
3240
|
// src/lib/tasks-chain.ts
|
|
3094
3241
|
import path14 from "path";
|
|
3095
3242
|
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|