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