@askexenow/exe-os 0.8.41 → 0.8.43
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/backfill-conversations.js +805 -642
- package/dist/bin/backfill-responses.js +804 -641
- package/dist/bin/backfill-vectors.js +791 -634
- package/dist/bin/cleanup-stale-review-tasks.js +788 -631
- package/dist/bin/cli.js +1345 -660
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2518 -1798
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +15 -1
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +790 -633
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2500 -1877
- package/dist/bin/exe-heartbeat.js +147 -1
- package/dist/bin/exe-kill.js +795 -640
- package/dist/bin/exe-launch-agent.js +2168 -2008
- package/dist/bin/exe-link.js +28 -2
- package/dist/bin/exe-new-employee.js +25 -3
- package/dist/bin/exe-pending-messages.js +146 -1
- package/dist/bin/exe-pending-notifications.js +788 -631
- package/dist/bin/exe-pending-reviews.js +147 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +154 -3
- package/dist/bin/exe-session-cleanup.js +2466 -413
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2690 -150
- package/dist/bin/graph-backfill.js +794 -637
- package/dist/bin/graph-export.js +798 -641
- package/dist/bin/scan-tasks.js +2951 -44
- package/dist/bin/setup.js +62 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2504 -1895
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2689 -149
- package/dist/hooks/error-recall.js +154 -3
- package/dist/hooks/ingest-worker.js +1439 -815
- package/dist/hooks/instructions-loaded.js +151 -0
- package/dist/hooks/notification.js +153 -2
- package/dist/hooks/post-compact.js +164 -0
- package/dist/hooks/pre-compact.js +3073 -101
- package/dist/hooks/pre-tool-use.js +151 -0
- package/dist/hooks/prompt-ingest-worker.js +1714 -1537
- package/dist/hooks/prompt-submit.js +2658 -1113
- package/dist/hooks/response-ingest-worker.js +170 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +154 -3
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +151 -0
- package/dist/hooks/summary-worker.js +179 -7
- package/dist/index.js +278 -100
- package/dist/lib/cloud-sync.js +28 -2
- package/dist/lib/consolidation.js +69 -2
- package/dist/lib/database.js +19 -0
- package/dist/lib/device-registry.js +19 -0
- package/dist/lib/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +236 -16
- package/dist/lib/hybrid-search.js +154 -3
- package/dist/lib/license.js +15 -1
- package/dist/lib/messaging.js +39 -2
- package/dist/lib/schedules.js +792 -637
- package/dist/lib/store.js +796 -636
- package/dist/lib/tasks.js +1614 -1091
- package/dist/lib/tmux-routing.js +149 -9
- package/dist/mcp/server.js +1825 -1138
- package/dist/mcp/tools/create-task.js +2280 -828
- package/dist/mcp/tools/list-tasks.js +2788 -159
- package/dist/mcp/tools/send-message.js +39 -2
- package/dist/mcp/tools/update-task.js +64 -0
- package/dist/runtime/index.js +235 -67
- package/dist/tui/App.js +1452 -644
- package/package.json +3 -2
package/dist/bin/exe-boot.js
CHANGED
|
@@ -361,13 +361,6 @@ var init_employees = __esm({
|
|
|
361
361
|
}
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
-
// src/types/memory.ts
|
|
365
|
-
var init_memory = __esm({
|
|
366
|
-
"src/types/memory.ts"() {
|
|
367
|
-
"use strict";
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
364
|
// src/lib/db-retry.ts
|
|
372
365
|
function isBusyError(err) {
|
|
373
366
|
if (err instanceof Error) {
|
|
@@ -664,6 +657,13 @@ async function ensureSchema() {
|
|
|
664
657
|
});
|
|
665
658
|
} catch {
|
|
666
659
|
}
|
|
660
|
+
try {
|
|
661
|
+
await client.execute({
|
|
662
|
+
sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
|
|
663
|
+
args: []
|
|
664
|
+
});
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
667
|
try {
|
|
668
668
|
await client.execute({
|
|
669
669
|
sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
|
|
@@ -1110,6 +1110,18 @@ async function ensureSchema() {
|
|
|
1110
1110
|
CREATE INDEX IF NOT EXISTS idx_session_kills_agent
|
|
1111
1111
|
ON session_kills(agent_id);
|
|
1112
1112
|
`);
|
|
1113
|
+
await client.execute(`
|
|
1114
|
+
CREATE TABLE IF NOT EXISTS global_procedures (
|
|
1115
|
+
id TEXT PRIMARY KEY,
|
|
1116
|
+
title TEXT NOT NULL,
|
|
1117
|
+
content TEXT NOT NULL,
|
|
1118
|
+
priority TEXT NOT NULL DEFAULT 'p0',
|
|
1119
|
+
domain TEXT,
|
|
1120
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
1121
|
+
created_at TEXT NOT NULL,
|
|
1122
|
+
updated_at TEXT NOT NULL
|
|
1123
|
+
)
|
|
1124
|
+
`);
|
|
1113
1125
|
await client.executeMultiple(`
|
|
1114
1126
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
1115
1127
|
id TEXT PRIMARY KEY,
|
|
@@ -1259,6 +1271,78 @@ var init_database = __esm({
|
|
|
1259
1271
|
}
|
|
1260
1272
|
});
|
|
1261
1273
|
|
|
1274
|
+
// src/lib/global-procedures.ts
|
|
1275
|
+
var global_procedures_exports = {};
|
|
1276
|
+
__export(global_procedures_exports, {
|
|
1277
|
+
deactivateGlobalProcedure: () => deactivateGlobalProcedure,
|
|
1278
|
+
getGlobalProceduresBlock: () => getGlobalProceduresBlock,
|
|
1279
|
+
loadGlobalProcedures: () => loadGlobalProcedures,
|
|
1280
|
+
storeGlobalProcedure: () => storeGlobalProcedure
|
|
1281
|
+
});
|
|
1282
|
+
import { randomUUID } from "crypto";
|
|
1283
|
+
async function loadGlobalProcedures() {
|
|
1284
|
+
const client = getClient();
|
|
1285
|
+
const result = await client.execute({
|
|
1286
|
+
sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
|
|
1287
|
+
args: []
|
|
1288
|
+
});
|
|
1289
|
+
const procedures = result.rows;
|
|
1290
|
+
if (procedures.length > 0) {
|
|
1291
|
+
_cache = procedures.map((p) => `### ${p.title}
|
|
1292
|
+
${p.content}`).join("\n\n");
|
|
1293
|
+
} else {
|
|
1294
|
+
_cache = "";
|
|
1295
|
+
}
|
|
1296
|
+
_cacheLoaded = true;
|
|
1297
|
+
return procedures;
|
|
1298
|
+
}
|
|
1299
|
+
function getGlobalProceduresBlock() {
|
|
1300
|
+
if (!_cacheLoaded) return "";
|
|
1301
|
+
if (!_cache) return "";
|
|
1302
|
+
return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
|
|
1303
|
+
|
|
1304
|
+
${_cache}
|
|
1305
|
+
`;
|
|
1306
|
+
}
|
|
1307
|
+
async function storeGlobalProcedure(input) {
|
|
1308
|
+
const id = randomUUID();
|
|
1309
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1310
|
+
const client = getClient();
|
|
1311
|
+
await client.execute({
|
|
1312
|
+
sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
|
|
1313
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
1314
|
+
args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
|
|
1315
|
+
});
|
|
1316
|
+
await loadGlobalProcedures();
|
|
1317
|
+
return id;
|
|
1318
|
+
}
|
|
1319
|
+
async function deactivateGlobalProcedure(id) {
|
|
1320
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1321
|
+
const client = getClient();
|
|
1322
|
+
const result = await client.execute({
|
|
1323
|
+
sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
|
|
1324
|
+
args: [now, id]
|
|
1325
|
+
});
|
|
1326
|
+
await loadGlobalProcedures();
|
|
1327
|
+
return result.rowsAffected > 0;
|
|
1328
|
+
}
|
|
1329
|
+
var _cache, _cacheLoaded;
|
|
1330
|
+
var init_global_procedures = __esm({
|
|
1331
|
+
"src/lib/global-procedures.ts"() {
|
|
1332
|
+
"use strict";
|
|
1333
|
+
init_database();
|
|
1334
|
+
_cache = "";
|
|
1335
|
+
_cacheLoaded = false;
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
// src/types/memory.ts
|
|
1340
|
+
var init_memory = __esm({
|
|
1341
|
+
"src/types/memory.ts"() {
|
|
1342
|
+
"use strict";
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1262
1346
|
// src/lib/keychain.ts
|
|
1263
1347
|
var keychain_exports = {};
|
|
1264
1348
|
__export(keychain_exports, {
|
|
@@ -1406,6 +1490,61 @@ var init_keychain = __esm({
|
|
|
1406
1490
|
}
|
|
1407
1491
|
});
|
|
1408
1492
|
|
|
1493
|
+
// src/lib/state-bus.ts
|
|
1494
|
+
var StateBus, orgBus;
|
|
1495
|
+
var init_state_bus = __esm({
|
|
1496
|
+
"src/lib/state-bus.ts"() {
|
|
1497
|
+
"use strict";
|
|
1498
|
+
StateBus = class {
|
|
1499
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1500
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
1501
|
+
/** Emit an event to all subscribers */
|
|
1502
|
+
emit(event) {
|
|
1503
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
1504
|
+
if (typeHandlers) {
|
|
1505
|
+
for (const handler of typeHandlers) {
|
|
1506
|
+
try {
|
|
1507
|
+
handler(event);
|
|
1508
|
+
} catch {
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
for (const handler of this.globalHandlers) {
|
|
1513
|
+
try {
|
|
1514
|
+
handler(event);
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
/** Subscribe to a specific event type */
|
|
1520
|
+
on(type, handler) {
|
|
1521
|
+
if (!this.handlers.has(type)) {
|
|
1522
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
1523
|
+
}
|
|
1524
|
+
this.handlers.get(type).add(handler);
|
|
1525
|
+
}
|
|
1526
|
+
/** Subscribe to ALL events */
|
|
1527
|
+
onAny(handler) {
|
|
1528
|
+
this.globalHandlers.add(handler);
|
|
1529
|
+
}
|
|
1530
|
+
/** Unsubscribe from a specific event type */
|
|
1531
|
+
off(type, handler) {
|
|
1532
|
+
this.handlers.get(type)?.delete(handler);
|
|
1533
|
+
}
|
|
1534
|
+
/** Unsubscribe from ALL events */
|
|
1535
|
+
offAny(handler) {
|
|
1536
|
+
this.globalHandlers.delete(handler);
|
|
1537
|
+
}
|
|
1538
|
+
/** Remove all listeners */
|
|
1539
|
+
clear() {
|
|
1540
|
+
this.handlers.clear();
|
|
1541
|
+
this.globalHandlers.clear();
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
orgBus = new StateBus();
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1409
1548
|
// src/lib/shard-manager.ts
|
|
1410
1549
|
var shard_manager_exports = {};
|
|
1411
1550
|
__export(shard_manager_exports, {
|
|
@@ -1710,6 +1849,11 @@ async function initStore(options) {
|
|
|
1710
1849
|
"version-query"
|
|
1711
1850
|
);
|
|
1712
1851
|
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
1852
|
+
try {
|
|
1853
|
+
const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
|
|
1854
|
+
await loadGlobalProcedures2();
|
|
1855
|
+
} catch {
|
|
1856
|
+
}
|
|
1713
1857
|
}
|
|
1714
1858
|
var INIT_MAX_RETRIES, INIT_RETRY_DELAY_MS, _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
|
|
1715
1859
|
var init_store = __esm({
|
|
@@ -1719,6 +1863,7 @@ var init_store = __esm({
|
|
|
1719
1863
|
init_database();
|
|
1720
1864
|
init_keychain();
|
|
1721
1865
|
init_config();
|
|
1866
|
+
init_state_bus();
|
|
1722
1867
|
INIT_MAX_RETRIES = 3;
|
|
1723
1868
|
INIT_RETRY_DELAY_MS = 1e3;
|
|
1724
1869
|
_pendingRecords = [];
|
|
@@ -2245,7 +2390,7 @@ __export(license_exports, {
|
|
|
2245
2390
|
validateLicense: () => validateLicense
|
|
2246
2391
|
});
|
|
2247
2392
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
2248
|
-
import { randomUUID } from "crypto";
|
|
2393
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2249
2394
|
import path9 from "path";
|
|
2250
2395
|
import { jwtVerify, importSPKI } from "jose";
|
|
2251
2396
|
async function fetchRetry(url, init) {
|
|
@@ -2272,7 +2417,7 @@ function loadDeviceId() {
|
|
|
2272
2417
|
}
|
|
2273
2418
|
} catch {
|
|
2274
2419
|
}
|
|
2275
|
-
const id =
|
|
2420
|
+
const id = randomUUID2();
|
|
2276
2421
|
mkdirSync5(EXE_AI_DIR, { recursive: true });
|
|
2277
2422
|
writeFileSync4(DEVICE_ID_PATH, id, "utf8");
|
|
2278
2423
|
return id;
|
|
@@ -2387,7 +2532,21 @@ function getCacheAgeMs() {
|
|
|
2387
2532
|
}
|
|
2388
2533
|
}
|
|
2389
2534
|
async function checkLicense() {
|
|
2390
|
-
|
|
2535
|
+
let key = loadLicense();
|
|
2536
|
+
if (!key) {
|
|
2537
|
+
try {
|
|
2538
|
+
const configPath = path9.join(EXE_AI_DIR, "config.json");
|
|
2539
|
+
if (existsSync8(configPath)) {
|
|
2540
|
+
const raw = JSON.parse(readFileSync7(configPath, "utf8"));
|
|
2541
|
+
const cloud = raw.cloud;
|
|
2542
|
+
if (cloud?.apiKey) {
|
|
2543
|
+
key = cloud.apiKey;
|
|
2544
|
+
saveLicense(key);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
} catch {
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2391
2550
|
if (!key) return FREE_LICENSE;
|
|
2392
2551
|
const cached = await getCachedLicense();
|
|
2393
2552
|
if (cached && getCacheAgeMs() < CACHE_MAX_AGE_MS) return cached;
|
|
@@ -2654,678 +2813,780 @@ var init_plan_limits = __esm({
|
|
|
2654
2813
|
}
|
|
2655
2814
|
});
|
|
2656
2815
|
|
|
2657
|
-
// src/lib/
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
return false;
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
function acquireSpawnLock(sessionName) {
|
|
2676
|
-
if (!existsSync10(SPAWN_LOCK_DIR)) {
|
|
2677
|
-
mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
|
|
2678
|
-
}
|
|
2679
|
-
const lockFile = spawnLockPath(sessionName);
|
|
2680
|
-
if (existsSync10(lockFile)) {
|
|
2681
|
-
try {
|
|
2682
|
-
const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
|
|
2683
|
-
const age = Date.now() - lock.timestamp;
|
|
2684
|
-
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
2685
|
-
return false;
|
|
2686
|
-
}
|
|
2687
|
-
} catch {
|
|
2688
|
-
}
|
|
2689
|
-
}
|
|
2690
|
-
writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
2691
|
-
return true;
|
|
2692
|
-
}
|
|
2693
|
-
function releaseSpawnLock(sessionName) {
|
|
2694
|
-
try {
|
|
2695
|
-
unlinkSync3(spawnLockPath(sessionName));
|
|
2696
|
-
} catch {
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
function resolveBehaviorsExporterScript() {
|
|
2700
|
-
try {
|
|
2701
|
-
const thisFile = fileURLToPath2(import.meta.url);
|
|
2702
|
-
const scriptPath = path11.join(
|
|
2703
|
-
path11.dirname(thisFile),
|
|
2704
|
-
"..",
|
|
2705
|
-
"bin",
|
|
2706
|
-
"exe-export-behaviors.js"
|
|
2707
|
-
);
|
|
2708
|
-
return existsSync10(scriptPath) ? scriptPath : null;
|
|
2709
|
-
} catch {
|
|
2710
|
-
return null;
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
2714
|
-
const script = resolveBehaviorsExporterScript();
|
|
2715
|
-
if (!script) return null;
|
|
2816
|
+
// src/lib/session-kill-telemetry.ts
|
|
2817
|
+
var session_kill_telemetry_exports = {};
|
|
2818
|
+
__export(session_kill_telemetry_exports, {
|
|
2819
|
+
IDLE_KILL_MIN_LIVE_SESSIONS: () => IDLE_KILL_MIN_LIVE_SESSIONS,
|
|
2820
|
+
IDLE_KILL_STREAK_META_KEY: () => IDLE_KILL_STREAK_META_KEY,
|
|
2821
|
+
IDLE_KILL_SUSPECT_DAY_THRESHOLD: () => IDLE_KILL_SUSPECT_DAY_THRESHOLD,
|
|
2822
|
+
TOKENS_PER_IDLE_MINUTE: () => TOKENS_PER_IDLE_MINUTE,
|
|
2823
|
+
computeIdleKillSuspectStreak: () => computeIdleKillSuspectStreak,
|
|
2824
|
+
countKillsSince: () => countKillsSince,
|
|
2825
|
+
parseStreakState: () => parseStreakState,
|
|
2826
|
+
recordSessionKill: () => recordSessionKill,
|
|
2827
|
+
sumTokensSavedSince: () => sumTokensSavedSince
|
|
2828
|
+
});
|
|
2829
|
+
import crypto3 from "crypto";
|
|
2830
|
+
async function recordSessionKill(input) {
|
|
2716
2831
|
try {
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2832
|
+
const client = getClient();
|
|
2833
|
+
await client.execute({
|
|
2834
|
+
sql: `INSERT INTO session_kills
|
|
2835
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
2836
|
+
ticks_idle, estimated_tokens_saved)
|
|
2837
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2838
|
+
args: [
|
|
2839
|
+
crypto3.randomUUID(),
|
|
2840
|
+
input.sessionName,
|
|
2841
|
+
input.agentId,
|
|
2842
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2843
|
+
input.reason,
|
|
2844
|
+
input.ticksIdle ?? null,
|
|
2845
|
+
input.estimatedTokensSaved ?? null
|
|
2846
|
+
]
|
|
2847
|
+
});
|
|
2723
2848
|
} catch (err) {
|
|
2724
2849
|
process.stderr.write(
|
|
2725
|
-
`[
|
|
2850
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
2726
2851
|
`
|
|
2727
2852
|
);
|
|
2728
|
-
return null;
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
function getMySession() {
|
|
2732
|
-
return getTransport().getMySession();
|
|
2733
|
-
}
|
|
2734
|
-
function employeeSessionName(employee, exeSession, instance) {
|
|
2735
|
-
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
2736
|
-
return `${employee}${suffix}-${exeSession}`;
|
|
2737
|
-
}
|
|
2738
|
-
function extractRootExe(name) {
|
|
2739
|
-
const match = name.match(/(exe\d+)$/);
|
|
2740
|
-
return match?.[1] ?? null;
|
|
2741
|
-
}
|
|
2742
|
-
function getParentExe(sessionKey) {
|
|
2743
|
-
try {
|
|
2744
|
-
const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
2745
|
-
return data.parentExe || null;
|
|
2746
|
-
} catch {
|
|
2747
|
-
return null;
|
|
2748
2853
|
}
|
|
2749
2854
|
}
|
|
2750
|
-
function
|
|
2855
|
+
async function countKillsSince(sinceISO) {
|
|
2751
2856
|
try {
|
|
2752
|
-
const
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2857
|
+
const client = getClient();
|
|
2858
|
+
const result = await client.execute({
|
|
2859
|
+
sql: `SELECT COUNT(*) AS n FROM session_kills WHERE killed_at >= ?`,
|
|
2860
|
+
args: [sinceISO]
|
|
2861
|
+
});
|
|
2862
|
+
const row = result.rows[0];
|
|
2863
|
+
return row ? Number(row.n) : 0;
|
|
2757
2864
|
} catch {
|
|
2758
|
-
return
|
|
2865
|
+
return 0;
|
|
2759
2866
|
}
|
|
2760
2867
|
}
|
|
2761
|
-
function
|
|
2762
|
-
|
|
2763
|
-
if (!mySession) return null;
|
|
2868
|
+
function parseStreakState(raw) {
|
|
2869
|
+
if (!raw) return { lastDate: null, streak: 0 };
|
|
2764
2870
|
try {
|
|
2765
|
-
const
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
}
|
|
2871
|
+
const parsed = JSON.parse(raw);
|
|
2872
|
+
return {
|
|
2873
|
+
lastDate: typeof parsed.lastDate === "string" ? parsed.lastDate : null,
|
|
2874
|
+
streak: typeof parsed.streak === "number" ? parsed.streak : 0
|
|
2875
|
+
};
|
|
2770
2876
|
} catch {
|
|
2877
|
+
return { lastDate: null, streak: 0 };
|
|
2771
2878
|
}
|
|
2772
|
-
return extractRootExe(mySession) ?? mySession;
|
|
2773
2879
|
}
|
|
2774
|
-
function
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
const base = employeeSessionName(employeeName, exeSession);
|
|
2779
|
-
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
2780
|
-
for (let i = 2; i <= maxInstances; i++) {
|
|
2781
|
-
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
2782
|
-
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
2783
|
-
}
|
|
2784
|
-
return null;
|
|
2880
|
+
function nextStreakState(prev, qualifiesToday, todayDate) {
|
|
2881
|
+
if (!qualifiesToday) return { lastDate: todayDate, streak: 0 };
|
|
2882
|
+
if (prev.lastDate === todayDate) return prev;
|
|
2883
|
+
return { lastDate: todayDate, streak: prev.streak + 1 };
|
|
2785
2884
|
}
|
|
2786
|
-
function
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
}
|
|
2885
|
+
function computeIdleKillSuspectStreak(prev, killsToday, liveSessions, todayDate) {
|
|
2886
|
+
const qualifies = killsToday === 0 && liveSessions >= IDLE_KILL_MIN_LIVE_SESSIONS;
|
|
2887
|
+
const state = nextStreakState(prev, qualifies, todayDate);
|
|
2888
|
+
return {
|
|
2889
|
+
state,
|
|
2890
|
+
suspect: state.streak >= IDLE_KILL_SUSPECT_DAY_THRESHOLD
|
|
2891
|
+
};
|
|
2793
2892
|
}
|
|
2794
|
-
function
|
|
2893
|
+
async function sumTokensSavedSince(sinceISO) {
|
|
2795
2894
|
try {
|
|
2796
|
-
|
|
2797
|
-
|
|
2895
|
+
const client = getClient();
|
|
2896
|
+
const result = await client.execute({
|
|
2897
|
+
sql: `SELECT COALESCE(SUM(estimated_tokens_saved), 0) AS total
|
|
2898
|
+
FROM session_kills
|
|
2899
|
+
WHERE killed_at >= ?`,
|
|
2900
|
+
args: [sinceISO]
|
|
2901
|
+
});
|
|
2902
|
+
const row = result.rows[0];
|
|
2903
|
+
return row ? Number(row.total) : 0;
|
|
2798
2904
|
} catch {
|
|
2905
|
+
return 0;
|
|
2799
2906
|
}
|
|
2800
2907
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
for (const key of Object.keys(state)) {
|
|
2811
|
-
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
2908
|
+
var TOKENS_PER_IDLE_MINUTE, IDLE_KILL_STREAK_META_KEY, IDLE_KILL_SUSPECT_DAY_THRESHOLD, IDLE_KILL_MIN_LIVE_SESSIONS;
|
|
2909
|
+
var init_session_kill_telemetry = __esm({
|
|
2910
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
2911
|
+
"use strict";
|
|
2912
|
+
init_database();
|
|
2913
|
+
TOKENS_PER_IDLE_MINUTE = 50;
|
|
2914
|
+
IDLE_KILL_STREAK_META_KEY = "idle_kill_suspect_streak";
|
|
2915
|
+
IDLE_KILL_SUSPECT_DAY_THRESHOLD = 3;
|
|
2916
|
+
IDLE_KILL_MIN_LIVE_SESSIONS = 5;
|
|
2812
2917
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2918
|
+
});
|
|
2919
|
+
|
|
2920
|
+
// src/lib/tasks-crud.ts
|
|
2921
|
+
import crypto4 from "crypto";
|
|
2922
|
+
import path11 from "path";
|
|
2923
|
+
import { execSync as execSync6 } from "child_process";
|
|
2924
|
+
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
2925
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
2926
|
+
async function writeCheckpoint(input) {
|
|
2927
|
+
const client = getClient();
|
|
2928
|
+
const row = await resolveTask(client, input.taskId);
|
|
2929
|
+
const taskId = String(row.id);
|
|
2930
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2931
|
+
const blockedByIds = [];
|
|
2932
|
+
if (row.blocked_by) {
|
|
2933
|
+
blockedByIds.push(String(row.blocked_by));
|
|
2823
2934
|
}
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
return "idle";
|
|
2838
|
-
} catch {
|
|
2839
|
-
return "offline";
|
|
2935
|
+
const checkpoint = {
|
|
2936
|
+
step: input.step,
|
|
2937
|
+
context_summary: input.contextSummary,
|
|
2938
|
+
files_touched: input.filesTouched ?? [],
|
|
2939
|
+
blocked_by_ids: blockedByIds,
|
|
2940
|
+
last_checkpoint_at: now
|
|
2941
|
+
};
|
|
2942
|
+
const result = await client.execute({
|
|
2943
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
2944
|
+
args: [JSON.stringify(checkpoint), now, taskId]
|
|
2945
|
+
});
|
|
2946
|
+
if (result.rowsAffected === 0) {
|
|
2947
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
2840
2948
|
}
|
|
2949
|
+
const countResult = await client.execute({
|
|
2950
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
2951
|
+
args: [taskId]
|
|
2952
|
+
});
|
|
2953
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
2954
|
+
return { checkpointCount };
|
|
2841
2955
|
}
|
|
2842
|
-
function
|
|
2843
|
-
|
|
2956
|
+
function extractParentFromContext(contextBody) {
|
|
2957
|
+
if (!contextBody) return null;
|
|
2958
|
+
const match = contextBody.match(
|
|
2959
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
2960
|
+
);
|
|
2961
|
+
return match ? match[1].toLowerCase() : null;
|
|
2844
2962
|
}
|
|
2845
|
-
function
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2963
|
+
function slugify(title) {
|
|
2964
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2965
|
+
}
|
|
2966
|
+
async function resolveTask(client, identifier) {
|
|
2967
|
+
let result = await client.execute({
|
|
2968
|
+
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
2969
|
+
args: [identifier]
|
|
2970
|
+
});
|
|
2971
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
2972
|
+
result = await client.execute({
|
|
2973
|
+
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
2974
|
+
args: [`%${identifier}%`]
|
|
2975
|
+
});
|
|
2976
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
2977
|
+
if (result.rows.length > 1) {
|
|
2978
|
+
const exact = result.rows.filter(
|
|
2979
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
2980
|
+
);
|
|
2981
|
+
if (exact.length === 1) return exact[0];
|
|
2982
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
2983
|
+
const active = candidates.filter(
|
|
2984
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
2985
|
+
);
|
|
2986
|
+
if (active.length === 1) return active[0];
|
|
2987
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
2988
|
+
throw new Error(
|
|
2989
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
2990
|
+
);
|
|
2854
2991
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
const
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
queueIntercom(targetSession, "session busy at send time");
|
|
2870
|
-
recordDebounce(targetSession);
|
|
2871
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
2872
|
-
return "queued";
|
|
2873
|
-
}
|
|
2874
|
-
if (transport.isPaneInCopyMode(targetSession)) {
|
|
2875
|
-
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
2876
|
-
transport.sendKeys(targetSession, "q");
|
|
2877
|
-
}
|
|
2878
|
-
transport.sendKeys(targetSession, "/exe-intercom");
|
|
2879
|
-
recordDebounce(targetSession);
|
|
2880
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
2881
|
-
return "delivered";
|
|
2882
|
-
} catch {
|
|
2883
|
-
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
2884
|
-
return "failed";
|
|
2992
|
+
result = await client.execute({
|
|
2993
|
+
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
2994
|
+
args: [`%${identifier}%`]
|
|
2995
|
+
});
|
|
2996
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
2997
|
+
if (result.rows.length > 1) {
|
|
2998
|
+
const active = result.rows.filter(
|
|
2999
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3000
|
+
);
|
|
3001
|
+
if (active.length === 1) return active[0];
|
|
3002
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3003
|
+
throw new Error(
|
|
3004
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3005
|
+
);
|
|
2885
3006
|
}
|
|
3007
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
2886
3008
|
}
|
|
2887
|
-
function
|
|
2888
|
-
const
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
3009
|
+
async function createTaskCore(input) {
|
|
3010
|
+
const client = getClient();
|
|
3011
|
+
const id = crypto4.randomUUID();
|
|
3012
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3013
|
+
const slug = slugify(input.title);
|
|
3014
|
+
const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
|
|
3015
|
+
let blockedById = null;
|
|
3016
|
+
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
3017
|
+
if (input.blockedBy) {
|
|
3018
|
+
const blocker = await resolveTask(client, input.blockedBy);
|
|
3019
|
+
blockedById = String(blocker.id);
|
|
2893
3020
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
process.stderr.write(
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
return fallback !== "failed";
|
|
3021
|
+
let parentTaskId = null;
|
|
3022
|
+
let parentRef = input.parentTaskId;
|
|
3023
|
+
if (!parentRef) {
|
|
3024
|
+
const extracted = extractParentFromContext(input.context);
|
|
3025
|
+
if (extracted) {
|
|
3026
|
+
parentRef = extracted;
|
|
3027
|
+
process.stderr.write(
|
|
3028
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
3029
|
+
);
|
|
2904
3030
|
}
|
|
2905
|
-
return false;
|
|
2906
3031
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3032
|
+
if (parentRef) {
|
|
3033
|
+
try {
|
|
3034
|
+
const parent = await resolveTask(client, parentRef);
|
|
3035
|
+
parentTaskId = String(parent.id);
|
|
3036
|
+
} catch (err) {
|
|
3037
|
+
if (!input.parentTaskId) {
|
|
3038
|
+
throw new Error(
|
|
3039
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
throw err;
|
|
3043
|
+
}
|
|
2912
3044
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3045
|
+
let warning;
|
|
3046
|
+
const dupCheck = await client.execute({
|
|
3047
|
+
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
3048
|
+
args: [input.title, input.assignedTo]
|
|
3049
|
+
});
|
|
3050
|
+
if (dupCheck.rows.length > 0) {
|
|
3051
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
3052
|
+
}
|
|
3053
|
+
if (input.baseDir) {
|
|
3054
|
+
try {
|
|
3055
|
+
await mkdir4(path11.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
3056
|
+
await mkdir4(path11.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
3057
|
+
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
3058
|
+
await ensureGitignoreExe(input.baseDir);
|
|
3059
|
+
} catch {
|
|
2918
3060
|
}
|
|
2919
3061
|
}
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
};
|
|
3062
|
+
const complexity = input.complexity ?? "standard";
|
|
3063
|
+
let sessionScope = null;
|
|
3064
|
+
try {
|
|
3065
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3066
|
+
sessionScope = resolveExeSession2();
|
|
3067
|
+
} catch {
|
|
2927
3068
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3069
|
+
await client.execute({
|
|
3070
|
+
sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
|
|
3071
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3072
|
+
args: [
|
|
3073
|
+
id,
|
|
3074
|
+
input.title,
|
|
3075
|
+
input.assignedTo,
|
|
3076
|
+
input.assignedBy,
|
|
3077
|
+
input.projectName,
|
|
3078
|
+
input.priority,
|
|
3079
|
+
initialStatus,
|
|
3080
|
+
taskFile,
|
|
3081
|
+
blockedById,
|
|
3082
|
+
parentTaskId,
|
|
3083
|
+
input.reviewer ?? null,
|
|
3084
|
+
input.context,
|
|
3085
|
+
complexity,
|
|
3086
|
+
input.budgetTokens ?? null,
|
|
3087
|
+
input.budgetFallbackModel ?? null,
|
|
3088
|
+
0,
|
|
3089
|
+
null,
|
|
3090
|
+
sessionScope,
|
|
3091
|
+
now,
|
|
3092
|
+
now
|
|
3093
|
+
]
|
|
3094
|
+
});
|
|
3095
|
+
return {
|
|
3096
|
+
id,
|
|
3097
|
+
title: input.title,
|
|
3098
|
+
assignedTo: input.assignedTo,
|
|
3099
|
+
assignedBy: input.assignedBy,
|
|
3100
|
+
projectName: input.projectName,
|
|
3101
|
+
priority: input.priority,
|
|
3102
|
+
status: initialStatus,
|
|
3103
|
+
taskFile,
|
|
3104
|
+
createdAt: now,
|
|
3105
|
+
updatedAt: now,
|
|
3106
|
+
warning,
|
|
3107
|
+
budgetTokens: input.budgetTokens ?? null,
|
|
3108
|
+
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
3109
|
+
tokensUsed: 0,
|
|
3110
|
+
tokensWarnedAt: null
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
async function listTasks(input) {
|
|
3114
|
+
const client = getClient();
|
|
3115
|
+
const conditions = [];
|
|
3116
|
+
const args = [];
|
|
3117
|
+
if (input.assignedTo) {
|
|
3118
|
+
conditions.push("assigned_to = ?");
|
|
3119
|
+
args.push(input.assignedTo);
|
|
2954
3120
|
}
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3121
|
+
if (input.status) {
|
|
3122
|
+
conditions.push("status = ?");
|
|
3123
|
+
args.push(input.status);
|
|
3124
|
+
} else {
|
|
3125
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
2959
3126
|
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
const transport = getTransport();
|
|
2964
|
-
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
2965
|
-
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
2966
|
-
const logDir = path11.join(os6.homedir(), ".exe-os", "session-logs");
|
|
2967
|
-
const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
2968
|
-
if (!existsSync10(logDir)) {
|
|
2969
|
-
mkdirSync6(logDir, { recursive: true });
|
|
3127
|
+
if (input.projectName) {
|
|
3128
|
+
conditions.push("project_name = ?");
|
|
3129
|
+
args.push(input.projectName);
|
|
2970
3130
|
}
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
const thisFile = fileURLToPath2(import.meta.url);
|
|
2975
|
-
const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
2976
|
-
if (existsSync10(cleanupScript)) {
|
|
2977
|
-
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
2978
|
-
}
|
|
2979
|
-
} catch {
|
|
3131
|
+
if (input.priority) {
|
|
3132
|
+
conditions.push("priority = ?");
|
|
3133
|
+
args.push(input.priority);
|
|
2980
3134
|
}
|
|
2981
3135
|
try {
|
|
2982
|
-
const
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3136
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3137
|
+
const session = resolveExeSession2();
|
|
3138
|
+
if (session) {
|
|
3139
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
3140
|
+
args.push(session);
|
|
2987
3141
|
}
|
|
2988
|
-
if (!claudeJson.projects) claudeJson.projects = {};
|
|
2989
|
-
const projects = claudeJson.projects;
|
|
2990
|
-
const trustDir = opts?.cwd ?? projectDir;
|
|
2991
|
-
if (!projects[trustDir]) projects[trustDir] = {};
|
|
2992
|
-
projects[trustDir].hasTrustDialogAccepted = true;
|
|
2993
|
-
writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
2994
3142
|
} catch {
|
|
2995
3143
|
}
|
|
3144
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3145
|
+
const result = await client.execute({
|
|
3146
|
+
sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC LIMIT 1000`,
|
|
3147
|
+
args
|
|
3148
|
+
});
|
|
3149
|
+
return result.rows.map((r) => ({
|
|
3150
|
+
id: String(r.id),
|
|
3151
|
+
title: String(r.title),
|
|
3152
|
+
assignedTo: String(r.assigned_to),
|
|
3153
|
+
assignedBy: String(r.assigned_by),
|
|
3154
|
+
projectName: String(r.project_name),
|
|
3155
|
+
priority: String(r.priority),
|
|
3156
|
+
status: String(r.status),
|
|
3157
|
+
taskFile: String(r.task_file),
|
|
3158
|
+
createdAt: String(r.created_at),
|
|
3159
|
+
updatedAt: String(r.updated_at),
|
|
3160
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
3161
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
3162
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
3163
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
3164
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
3165
|
+
}));
|
|
3166
|
+
}
|
|
3167
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
3168
|
+
if (!taskContext) return null;
|
|
3169
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
2996
3170
|
try {
|
|
2997
|
-
const
|
|
2998
|
-
const
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
const
|
|
3008
|
-
|
|
3009
|
-
"
|
|
3010
|
-
"store_memory",
|
|
3011
|
-
"create_task",
|
|
3012
|
-
"update_task",
|
|
3013
|
-
"list_tasks",
|
|
3014
|
-
"get_task",
|
|
3015
|
-
"ask_team_memory",
|
|
3016
|
-
"store_behavior",
|
|
3017
|
-
"get_identity",
|
|
3018
|
-
"send_message"
|
|
3019
|
-
];
|
|
3020
|
-
const requiredTools = expandDualPrefixTools(toolNames);
|
|
3021
|
-
let changed = false;
|
|
3022
|
-
for (const tool of requiredTools) {
|
|
3023
|
-
if (!allow.includes(tool)) {
|
|
3024
|
-
allow.push(tool);
|
|
3025
|
-
changed = true;
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
if (changed) {
|
|
3029
|
-
perms.allow = allow;
|
|
3030
|
-
settings.permissions = perms;
|
|
3031
|
-
mkdirSync6(projSettingsDir, { recursive: true });
|
|
3032
|
-
writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3171
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
3172
|
+
const branch = execSync6(
|
|
3173
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
3174
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
3175
|
+
).trim();
|
|
3176
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
3177
|
+
const commitCount = execSync6(
|
|
3178
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
3179
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
3180
|
+
).trim();
|
|
3181
|
+
const count = parseInt(commitCount, 10);
|
|
3182
|
+
if (count === 0) {
|
|
3183
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
3033
3184
|
}
|
|
3185
|
+
return null;
|
|
3034
3186
|
} catch {
|
|
3187
|
+
return null;
|
|
3035
3188
|
}
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
const
|
|
3039
|
-
const
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
if (
|
|
3044
|
-
const identityPath = path11.join(
|
|
3045
|
-
os6.homedir(),
|
|
3046
|
-
".exe-os",
|
|
3047
|
-
"identity",
|
|
3048
|
-
`${employeeName}.md`
|
|
3049
|
-
);
|
|
3050
|
-
_resetCcAgentSupportCache();
|
|
3051
|
-
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
3052
|
-
if (hasAgentFlag) {
|
|
3053
|
-
identityFlag = ` --agent ${employeeName}`;
|
|
3054
|
-
} else if (existsSync10(identityPath)) {
|
|
3055
|
-
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
3056
|
-
legacyFallbackWarned = true;
|
|
3057
|
-
}
|
|
3058
|
-
const behaviorsFile = exportBehaviorsSync(
|
|
3059
|
-
employeeName,
|
|
3060
|
-
path11.basename(spawnCwd),
|
|
3061
|
-
sessionName
|
|
3062
|
-
);
|
|
3063
|
-
if (behaviorsFile) {
|
|
3064
|
-
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
3065
|
-
}
|
|
3066
|
-
}
|
|
3067
|
-
if (legacyFallbackWarned) {
|
|
3189
|
+
}
|
|
3190
|
+
async function updateTaskStatus(input) {
|
|
3191
|
+
const client = getClient();
|
|
3192
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3193
|
+
const row = await resolveTask(client, input.taskId);
|
|
3194
|
+
const taskId = String(row.id);
|
|
3195
|
+
const taskFile = String(row.task_file);
|
|
3196
|
+
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
3068
3197
|
process.stderr.write(
|
|
3069
|
-
`[
|
|
3198
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
3070
3199
|
`
|
|
3071
3200
|
);
|
|
3072
3201
|
}
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
}
|
|
3088
|
-
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
3089
|
-
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
3090
|
-
const cfg = PROVIDER_TABLE[ccProvider];
|
|
3091
|
-
if (cfg?.apiKeyEnv) {
|
|
3092
|
-
const keyVal = process.env[cfg.apiKeyEnv];
|
|
3093
|
-
if (keyVal) {
|
|
3094
|
-
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
3202
|
+
if (input.status === "done") {
|
|
3203
|
+
const existingRow = await client.execute({
|
|
3204
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
3205
|
+
args: [taskId]
|
|
3206
|
+
});
|
|
3207
|
+
if (existingRow.rows.length > 0) {
|
|
3208
|
+
const ctx = existingRow.rows[0];
|
|
3209
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
3210
|
+
if (warning) {
|
|
3211
|
+
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
3212
|
+
|
|
3213
|
+
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
3214
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
3215
|
+
`);
|
|
3095
3216
|
}
|
|
3096
3217
|
}
|
|
3097
3218
|
}
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
)
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
if (spawnResult.error) {
|
|
3116
|
-
releaseSpawnLock(sessionName);
|
|
3117
|
-
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
3118
|
-
}
|
|
3119
|
-
transport.pipeLog(sessionName, logFile);
|
|
3120
|
-
try {
|
|
3121
|
-
const mySession = getMySession();
|
|
3122
|
-
const dispatchInfo = path11.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
3123
|
-
writeFileSync5(dispatchInfo, JSON.stringify({
|
|
3124
|
-
dispatchedBy: mySession,
|
|
3125
|
-
rootExe: exeSession,
|
|
3126
|
-
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
3127
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3128
|
-
}));
|
|
3129
|
-
} catch {
|
|
3130
|
-
}
|
|
3131
|
-
let booted = false;
|
|
3132
|
-
for (let i = 0; i < 30; i++) {
|
|
3133
|
-
try {
|
|
3134
|
-
execSync6("sleep 0.5");
|
|
3135
|
-
} catch {
|
|
3219
|
+
if (input.status === "in_progress") {
|
|
3220
|
+
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
3221
|
+
const claim = await client.execute({
|
|
3222
|
+
sql: `UPDATE tasks
|
|
3223
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
3224
|
+
WHERE id = ? AND status = 'open'`,
|
|
3225
|
+
args: [tmuxSession, now, taskId]
|
|
3226
|
+
});
|
|
3227
|
+
if (claim.rowsAffected === 0) {
|
|
3228
|
+
const current = await client.execute({
|
|
3229
|
+
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
3230
|
+
args: [taskId]
|
|
3231
|
+
});
|
|
3232
|
+
const cur = current.rows[0];
|
|
3233
|
+
const status = cur?.status ?? "unknown";
|
|
3234
|
+
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
3235
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
3136
3236
|
}
|
|
3137
3237
|
try {
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
}
|
|
3144
|
-
} else {
|
|
3145
|
-
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
3146
|
-
booted = true;
|
|
3147
|
-
break;
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3238
|
+
await writeCheckpoint({
|
|
3239
|
+
taskId,
|
|
3240
|
+
step: "claimed",
|
|
3241
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
3242
|
+
});
|
|
3150
3243
|
} catch {
|
|
3151
3244
|
}
|
|
3245
|
+
return { row, taskFile, now, taskId };
|
|
3152
3246
|
}
|
|
3153
|
-
if (
|
|
3154
|
-
|
|
3155
|
-
|
|
3247
|
+
if (input.result) {
|
|
3248
|
+
await client.execute({
|
|
3249
|
+
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
3250
|
+
args: [input.status, input.result, now, taskId]
|
|
3251
|
+
});
|
|
3252
|
+
} else {
|
|
3253
|
+
await client.execute({
|
|
3254
|
+
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
3255
|
+
args: [input.status, now, taskId]
|
|
3256
|
+
});
|
|
3156
3257
|
}
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3258
|
+
try {
|
|
3259
|
+
await writeCheckpoint({
|
|
3260
|
+
taskId,
|
|
3261
|
+
step: `status_transition:${input.status}`,
|
|
3262
|
+
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
3263
|
+
});
|
|
3264
|
+
} catch {
|
|
3162
3265
|
}
|
|
3163
|
-
|
|
3164
|
-
windowName: sessionName,
|
|
3165
|
-
agentId: employeeName,
|
|
3166
|
-
projectDir: spawnCwd,
|
|
3167
|
-
parentExe: exeSession,
|
|
3168
|
-
pid: 0,
|
|
3169
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3170
|
-
});
|
|
3171
|
-
releaseSpawnLock(sessionName);
|
|
3172
|
-
return { sessionName };
|
|
3266
|
+
return { row, taskFile, now, taskId };
|
|
3173
3267
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
SESSION_CACHE = path11.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3188
|
-
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
3189
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
3190
|
-
INTERCOM_LOG2 = path11.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
3191
|
-
DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3192
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3193
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
3194
|
-
}
|
|
3195
|
-
});
|
|
3196
|
-
|
|
3197
|
-
// src/lib/task-scanner.ts
|
|
3198
|
-
var task_scanner_exports = {};
|
|
3199
|
-
__export(task_scanner_exports, {
|
|
3200
|
-
PRIORITY_RE: () => PRIORITY_RE,
|
|
3201
|
-
STATUS_RE: () => STATUS_RE,
|
|
3202
|
-
TITLE_RE: () => TITLE_RE,
|
|
3203
|
-
formatJson: () => formatJson,
|
|
3204
|
-
formatMandatory: () => formatMandatory,
|
|
3205
|
-
formatText: () => formatText,
|
|
3206
|
-
scanAgentTasks: () => scanAgentTasks
|
|
3207
|
-
});
|
|
3208
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync10, existsSync as existsSync11, statSync } from "fs";
|
|
3209
|
-
import { execSync as execSync7 } from "child_process";
|
|
3210
|
-
import path12 from "path";
|
|
3211
|
-
function getProjectRoot() {
|
|
3268
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
3269
|
+
const client = getClient();
|
|
3270
|
+
const row = await resolveTask(client, taskId);
|
|
3271
|
+
const id = String(row.id);
|
|
3272
|
+
const taskFile = String(row.task_file);
|
|
3273
|
+
const assignedTo = String(row.assigned_to);
|
|
3274
|
+
const assignedBy = String(row.assigned_by);
|
|
3275
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
3276
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
3277
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
3278
|
+
}
|
|
3279
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
3280
|
+
const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
3212
3281
|
try {
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3282
|
+
if (existsSync10(archPath)) return;
|
|
3283
|
+
const template = [
|
|
3284
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
3285
|
+
"",
|
|
3286
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
3287
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
3288
|
+
"",
|
|
3289
|
+
"## Overview",
|
|
3290
|
+
"",
|
|
3291
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
3292
|
+
"",
|
|
3293
|
+
"## Key Components",
|
|
3294
|
+
"",
|
|
3295
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
3296
|
+
"",
|
|
3297
|
+
"## Data Flow",
|
|
3298
|
+
"",
|
|
3299
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
3300
|
+
"",
|
|
3301
|
+
"## Invariants",
|
|
3302
|
+
"",
|
|
3303
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
3304
|
+
"",
|
|
3305
|
+
"## Dependencies",
|
|
3306
|
+
"",
|
|
3307
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
3308
|
+
""
|
|
3309
|
+
].join("\n");
|
|
3310
|
+
await writeFile4(archPath, template, "utf-8");
|
|
3218
3311
|
} catch {
|
|
3219
|
-
return process.cwd();
|
|
3220
3312
|
}
|
|
3221
3313
|
}
|
|
3222
|
-
function
|
|
3223
|
-
const
|
|
3224
|
-
const open = [];
|
|
3225
|
-
const inProgress = [];
|
|
3226
|
-
let done = 0;
|
|
3227
|
-
let total = 0;
|
|
3228
|
-
if (!existsSync11(taskDir)) return { open, inProgress, done, total };
|
|
3314
|
+
async function ensureGitignoreExe(baseDir) {
|
|
3315
|
+
const gitignorePath = path11.join(baseDir, ".gitignore");
|
|
3229
3316
|
try {
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
const status = statusMatch ? statusMatch[1].toLowerCase() : null;
|
|
3237
|
-
if (status === "done") {
|
|
3238
|
-
done++;
|
|
3239
|
-
continue;
|
|
3240
|
-
}
|
|
3241
|
-
if (status !== "open" && status !== "in_progress") continue;
|
|
3242
|
-
const priMatch = content.match(PRIORITY_RE);
|
|
3243
|
-
const titleMatch = content.match(TITLE_RE);
|
|
3244
|
-
const task = {
|
|
3245
|
-
file: f,
|
|
3246
|
-
title: titleMatch ? titleMatch[1] : f.replace(".md", ""),
|
|
3247
|
-
priority: priMatch ? priMatch[1] : "P2",
|
|
3248
|
-
status,
|
|
3249
|
-
slug: f.replace(".md", "")
|
|
3250
|
-
};
|
|
3251
|
-
if (status === "in_progress") {
|
|
3252
|
-
inProgress.push(task);
|
|
3253
|
-
} else {
|
|
3254
|
-
open.push(task);
|
|
3255
|
-
}
|
|
3256
|
-
} catch {
|
|
3257
|
-
}
|
|
3317
|
+
if (existsSync10(gitignorePath)) {
|
|
3318
|
+
const content = readFileSync9(gitignorePath, "utf-8");
|
|
3319
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
3320
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
3321
|
+
} else {
|
|
3322
|
+
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
3258
3323
|
}
|
|
3259
3324
|
} catch {
|
|
3260
3325
|
}
|
|
3261
|
-
open.sort((a, b) => a.priority.localeCompare(b.priority));
|
|
3262
|
-
inProgress.sort((a, b) => a.priority.localeCompare(b.priority));
|
|
3263
|
-
return { open, inProgress, done, total };
|
|
3264
3326
|
}
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
}
|
|
3273
|
-
if (result.open.length > 0) {
|
|
3274
|
-
lines.push(`OPEN (${result.open.length}):`);
|
|
3275
|
-
for (const t of result.open) {
|
|
3276
|
-
lines.push(` [${t.priority}] ${t.title} \u2014 exe/${agentId}/${t.file}`);
|
|
3277
|
-
}
|
|
3327
|
+
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
3328
|
+
var init_tasks_crud = __esm({
|
|
3329
|
+
"src/lib/tasks-crud.ts"() {
|
|
3330
|
+
"use strict";
|
|
3331
|
+
init_database();
|
|
3332
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
3333
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
3278
3334
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3335
|
+
});
|
|
3336
|
+
|
|
3337
|
+
// src/lib/tasks-review.ts
|
|
3338
|
+
import path12 from "path";
|
|
3339
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
3340
|
+
async function countPendingReviews() {
|
|
3341
|
+
const client = getClient();
|
|
3342
|
+
const result = await client.execute({
|
|
3343
|
+
sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
|
|
3344
|
+
args: []
|
|
3345
|
+
});
|
|
3346
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3281
3347
|
}
|
|
3282
|
-
function
|
|
3283
|
-
const
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3348
|
+
async function countNewPendingReviewsSince(sinceIso) {
|
|
3349
|
+
const client = getClient();
|
|
3350
|
+
const result = await client.execute({
|
|
3351
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3352
|
+
WHERE status = 'needs_review' AND updated_at > ?`,
|
|
3353
|
+
args: [sinceIso]
|
|
3354
|
+
});
|
|
3355
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3356
|
+
}
|
|
3357
|
+
async function listPendingReviews(limit) {
|
|
3358
|
+
const client = getClient();
|
|
3359
|
+
const result = await client.execute({
|
|
3360
|
+
sql: `SELECT title, assigned_to, project_name FROM tasks
|
|
3361
|
+
WHERE status = 'needs_review'
|
|
3362
|
+
ORDER BY priority ASC, created_at DESC LIMIT ?`,
|
|
3363
|
+
args: [limit]
|
|
3364
|
+
});
|
|
3365
|
+
return result.rows;
|
|
3366
|
+
}
|
|
3367
|
+
async function cleanupOrphanedReviews() {
|
|
3368
|
+
const client = getClient();
|
|
3369
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3370
|
+
const r1 = await client.execute({
|
|
3371
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3372
|
+
WHERE status = 'needs_review'
|
|
3373
|
+
AND assigned_by = 'system'
|
|
3374
|
+
AND title LIKE 'Review:%'
|
|
3375
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
|
|
3376
|
+
args: [now]
|
|
3377
|
+
});
|
|
3378
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
3379
|
+
const r2 = await client.execute({
|
|
3380
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3381
|
+
WHERE status = 'needs_review'
|
|
3382
|
+
AND result IS NOT NULL
|
|
3383
|
+
AND updated_at < ?`,
|
|
3384
|
+
args: [now, staleThreshold]
|
|
3385
|
+
});
|
|
3386
|
+
const total = r1.rowsAffected + r2.rowsAffected;
|
|
3387
|
+
if (total > 0) {
|
|
3388
|
+
process.stderr.write(
|
|
3389
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
|
|
3390
|
+
`
|
|
3391
|
+
);
|
|
3392
|
+
}
|
|
3393
|
+
return total;
|
|
3394
|
+
}
|
|
3395
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
3396
|
+
const roleLower = role.toLowerCase();
|
|
3397
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
3398
|
+
return {
|
|
3399
|
+
lens: "Code Quality (Engineer)",
|
|
3400
|
+
checklist: [
|
|
3401
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
3402
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
3403
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
3404
|
+
"4. Any regressions in the test suite?"
|
|
3405
|
+
]
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
3409
|
+
return {
|
|
3410
|
+
lens: "Architecture (CTO)",
|
|
3411
|
+
checklist: [
|
|
3412
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
3413
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
3414
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
3415
|
+
"4. Security implications? Any new attack surface?",
|
|
3416
|
+
"5. Does it scale? Performance considerations?",
|
|
3417
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
3418
|
+
]
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
3422
|
+
return {
|
|
3423
|
+
lens: "Strategic (COO)",
|
|
3424
|
+
checklist: [
|
|
3425
|
+
"1. Does this serve the project mission?",
|
|
3426
|
+
"2. Is this the right work at the right time?",
|
|
3427
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
3428
|
+
"4. Any cross-project implications?"
|
|
3429
|
+
]
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
return {
|
|
3433
|
+
lens: "General",
|
|
3434
|
+
checklist: [
|
|
3435
|
+
"1. Read the original task's acceptance criteria",
|
|
3436
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
3437
|
+
"3. Verify code changes match requirements",
|
|
3438
|
+
"4. Check if tests were added/updated",
|
|
3439
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
3440
|
+
]
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
3444
|
+
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
3445
|
+
try {
|
|
3446
|
+
const client = getClient();
|
|
3447
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3448
|
+
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
3449
|
+
if (parentId) {
|
|
3450
|
+
const result = await client.execute({
|
|
3451
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
3452
|
+
args: [now, parentId]
|
|
3453
|
+
});
|
|
3454
|
+
if (result.rowsAffected > 0) {
|
|
3455
|
+
process.stderr.write(
|
|
3456
|
+
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
3457
|
+
`
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3299
3460
|
} else {
|
|
3300
|
-
|
|
3461
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
3462
|
+
const reviewPrefix = fileName.replace(".md", "");
|
|
3463
|
+
const parts = reviewPrefix.split("-");
|
|
3464
|
+
if (parts.length >= 3 && parts[0] === "review") {
|
|
3465
|
+
const agent = parts[1];
|
|
3466
|
+
const slug = parts.slice(2).join("-");
|
|
3467
|
+
const originalTaskFile = `exe/${agent}/${slug}.md`;
|
|
3468
|
+
const result = await client.execute({
|
|
3469
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
|
|
3470
|
+
args: [now, originalTaskFile]
|
|
3471
|
+
});
|
|
3472
|
+
if (result.rowsAffected > 0) {
|
|
3473
|
+
process.stderr.write(
|
|
3474
|
+
`[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
|
|
3475
|
+
`
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3301
3479
|
}
|
|
3302
|
-
|
|
3303
|
-
|
|
3480
|
+
} catch (err) {
|
|
3481
|
+
process.stderr.write(
|
|
3482
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
3483
|
+
`
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
try {
|
|
3487
|
+
const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
|
|
3488
|
+
if (existsSync11(cacheDir)) {
|
|
3489
|
+
for (const f of readdirSync4(cacheDir)) {
|
|
3490
|
+
if (f.startsWith("review-notified-")) {
|
|
3491
|
+
unlinkSync3(path12.join(cacheDir, f));
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3304
3494
|
}
|
|
3305
|
-
}
|
|
3306
|
-
const top = open[0];
|
|
3307
|
-
lines.push(`MANDATORY: You have ${open.length} unstarted task(s).`);
|
|
3308
|
-
lines.push(`Highest priority: ${top.title} [${top.priority}]`);
|
|
3309
|
-
lines.push(`File: exe/${agentId}/${top.file}`);
|
|
3310
|
-
lines.push("Read this task file and START WORKING NOW.");
|
|
3495
|
+
} catch {
|
|
3311
3496
|
}
|
|
3312
|
-
return lines.join("\n");
|
|
3313
3497
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3498
|
+
var init_tasks_review = __esm({
|
|
3499
|
+
"src/lib/tasks-review.ts"() {
|
|
3500
|
+
"use strict";
|
|
3501
|
+
init_database();
|
|
3502
|
+
init_config();
|
|
3503
|
+
init_employees();
|
|
3504
|
+
init_notifications();
|
|
3505
|
+
init_tasks_crud();
|
|
3506
|
+
init_tmux_routing();
|
|
3507
|
+
init_session_key();
|
|
3508
|
+
init_state_bus();
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
// src/lib/tasks-chain.ts
|
|
3513
|
+
import path13 from "path";
|
|
3514
|
+
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
3515
|
+
async function cascadeUnblock(taskId, baseDir, now) {
|
|
3516
|
+
const client = getClient();
|
|
3517
|
+
const unblocked = await client.execute({
|
|
3518
|
+
sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
|
|
3519
|
+
WHERE blocked_by = ? AND status = 'blocked'`,
|
|
3520
|
+
args: [now, taskId]
|
|
3521
|
+
});
|
|
3522
|
+
if (baseDir && unblocked.rowsAffected > 0) {
|
|
3523
|
+
const unblockedRows = await client.execute({
|
|
3524
|
+
sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
|
|
3525
|
+
args: [now]
|
|
3526
|
+
});
|
|
3527
|
+
for (const ur of unblockedRows.rows) {
|
|
3528
|
+
try {
|
|
3529
|
+
const ubFile = path13.join(baseDir, String(ur.task_file));
|
|
3530
|
+
let ubContent = await readFile4(ubFile, "utf-8");
|
|
3531
|
+
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
3532
|
+
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
3533
|
+
await writeFile5(ubFile, ubContent, "utf-8");
|
|
3534
|
+
} catch {
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
async function findNextTask(assignedTo) {
|
|
3540
|
+
const client = getClient();
|
|
3541
|
+
const nextResult = await client.execute({
|
|
3542
|
+
sql: `SELECT title, task_file, priority FROM tasks
|
|
3543
|
+
WHERE assigned_to = ? AND status = 'open'
|
|
3544
|
+
ORDER BY priority ASC, created_at ASC
|
|
3545
|
+
LIMIT 1`,
|
|
3546
|
+
args: [assignedTo]
|
|
3320
3547
|
});
|
|
3548
|
+
if (nextResult.rows.length === 1) {
|
|
3549
|
+
const nr = nextResult.rows[0];
|
|
3550
|
+
return {
|
|
3551
|
+
title: String(nr.title),
|
|
3552
|
+
priority: String(nr.priority),
|
|
3553
|
+
taskFile: String(nr.task_file)
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
return void 0;
|
|
3321
3557
|
}
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3558
|
+
async function checkSubtaskCompletion(parentTaskId, projectName) {
|
|
3559
|
+
const client = getClient();
|
|
3560
|
+
const remaining = await client.execute({
|
|
3561
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3562
|
+
WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
|
|
3563
|
+
args: [parentTaskId]
|
|
3564
|
+
});
|
|
3565
|
+
const cnt = Number(remaining.rows[0]?.cnt ?? 1);
|
|
3566
|
+
if (cnt === 0) {
|
|
3567
|
+
const parentRow = await client.execute({
|
|
3568
|
+
sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
|
|
3569
|
+
args: [parentTaskId]
|
|
3570
|
+
});
|
|
3571
|
+
if (parentRow.rows.length === 1) {
|
|
3572
|
+
const pr = parentRow.rows[0];
|
|
3573
|
+
const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
|
|
3574
|
+
await writeNotification({
|
|
3575
|
+
agentId: String(pr.assigned_to),
|
|
3576
|
+
agentRole: "system",
|
|
3577
|
+
event: "subtasks_complete",
|
|
3578
|
+
project: parentProject,
|
|
3579
|
+
summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
|
|
3580
|
+
taskFile: String(pr.task_file)
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
var init_tasks_chain = __esm({
|
|
3586
|
+
"src/lib/tasks-chain.ts"() {
|
|
3325
3587
|
"use strict";
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
TITLE_RE = /^# (.+)/m;
|
|
3588
|
+
init_database();
|
|
3589
|
+
init_notifications();
|
|
3329
3590
|
}
|
|
3330
3591
|
});
|
|
3331
3592
|
|
|
@@ -3335,34 +3596,34 @@ __export(project_name_exports, {
|
|
|
3335
3596
|
_resetCache: () => _resetCache,
|
|
3336
3597
|
getProjectName: () => getProjectName
|
|
3337
3598
|
});
|
|
3338
|
-
import { execSync as
|
|
3339
|
-
import
|
|
3599
|
+
import { execSync as execSync7 } from "child_process";
|
|
3600
|
+
import path14 from "path";
|
|
3340
3601
|
function getProjectName(cwd) {
|
|
3341
3602
|
const dir = cwd ?? process.cwd();
|
|
3342
3603
|
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
3343
3604
|
try {
|
|
3344
3605
|
let repoRoot;
|
|
3345
3606
|
try {
|
|
3346
|
-
const gitCommonDir =
|
|
3607
|
+
const gitCommonDir = execSync7("git rev-parse --path-format=absolute --git-common-dir", {
|
|
3347
3608
|
cwd: dir,
|
|
3348
3609
|
encoding: "utf8",
|
|
3349
3610
|
timeout: 2e3,
|
|
3350
3611
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3351
3612
|
}).trim();
|
|
3352
|
-
repoRoot =
|
|
3613
|
+
repoRoot = path14.dirname(gitCommonDir);
|
|
3353
3614
|
} catch {
|
|
3354
|
-
repoRoot =
|
|
3615
|
+
repoRoot = execSync7("git rev-parse --show-toplevel", {
|
|
3355
3616
|
cwd: dir,
|
|
3356
3617
|
encoding: "utf8",
|
|
3357
3618
|
timeout: 2e3,
|
|
3358
3619
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3359
3620
|
}).trim();
|
|
3360
3621
|
}
|
|
3361
|
-
_cached2 =
|
|
3622
|
+
_cached2 = path14.basename(repoRoot);
|
|
3362
3623
|
_cachedCwd = dir;
|
|
3363
3624
|
return _cached2;
|
|
3364
3625
|
} catch {
|
|
3365
|
-
_cached2 =
|
|
3626
|
+
_cached2 = path14.basename(dir);
|
|
3366
3627
|
_cachedCwd = dir;
|
|
3367
3628
|
return _cached2;
|
|
3368
3629
|
}
|
|
@@ -3380,1373 +3641,1685 @@ var init_project_name = __esm({
|
|
|
3380
3641
|
}
|
|
3381
3642
|
});
|
|
3382
3643
|
|
|
3383
|
-
// src/lib/
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
const
|
|
3392
|
-
const
|
|
3393
|
-
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
blockedByIds.push(String(row.blocked_by));
|
|
3397
|
-
}
|
|
3398
|
-
const checkpoint = {
|
|
3399
|
-
step: input.step,
|
|
3400
|
-
context_summary: input.contextSummary,
|
|
3401
|
-
files_touched: input.filesTouched ?? [],
|
|
3402
|
-
blocked_by_ids: blockedByIds,
|
|
3403
|
-
last_checkpoint_at: now
|
|
3404
|
-
};
|
|
3405
|
-
const result = await client.execute({
|
|
3406
|
-
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
3407
|
-
args: [JSON.stringify(checkpoint), now, taskId]
|
|
3408
|
-
});
|
|
3409
|
-
if (result.rowsAffected === 0) {
|
|
3410
|
-
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
3411
|
-
}
|
|
3412
|
-
const countResult = await client.execute({
|
|
3413
|
-
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
3414
|
-
args: [taskId]
|
|
3415
|
-
});
|
|
3416
|
-
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
3417
|
-
return { checkpointCount };
|
|
3418
|
-
}
|
|
3419
|
-
function extractParentFromContext(contextBody) {
|
|
3420
|
-
if (!contextBody) return null;
|
|
3421
|
-
const match = contextBody.match(
|
|
3422
|
-
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
3423
|
-
);
|
|
3424
|
-
return match ? match[1].toLowerCase() : null;
|
|
3425
|
-
}
|
|
3426
|
-
function slugify(title) {
|
|
3427
|
-
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
3644
|
+
// src/lib/session-scope.ts
|
|
3645
|
+
var session_scope_exports = {};
|
|
3646
|
+
__export(session_scope_exports, {
|
|
3647
|
+
assertSessionScope: () => assertSessionScope,
|
|
3648
|
+
findSessionForProject: () => findSessionForProject,
|
|
3649
|
+
getSessionProject: () => getSessionProject
|
|
3650
|
+
});
|
|
3651
|
+
function getSessionProject(sessionName) {
|
|
3652
|
+
const sessions = listSessions();
|
|
3653
|
+
const entry = sessions.find((s) => s.windowName === sessionName);
|
|
3654
|
+
if (!entry) return null;
|
|
3655
|
+
const parts = entry.projectDir.split("/").filter(Boolean);
|
|
3656
|
+
return parts[parts.length - 1] ?? null;
|
|
3428
3657
|
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
3435
|
-
result = await client.execute({
|
|
3436
|
-
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
3437
|
-
args: [`%${identifier}%`]
|
|
3438
|
-
});
|
|
3439
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
3440
|
-
if (result.rows.length > 1) {
|
|
3441
|
-
const exact = result.rows.filter(
|
|
3442
|
-
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
3443
|
-
);
|
|
3444
|
-
if (exact.length === 1) return exact[0];
|
|
3445
|
-
const candidates = exact.length > 1 ? exact : result.rows;
|
|
3446
|
-
const active = candidates.filter(
|
|
3447
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3448
|
-
);
|
|
3449
|
-
if (active.length === 1) return active[0];
|
|
3450
|
-
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3451
|
-
throw new Error(
|
|
3452
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3453
|
-
);
|
|
3658
|
+
function findSessionForProject(projectName) {
|
|
3659
|
+
const sessions = listSessions();
|
|
3660
|
+
for (const s of sessions) {
|
|
3661
|
+
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
3662
|
+
if (proj === projectName && s.agentId === "exe") return s;
|
|
3454
3663
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
if (
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3664
|
+
return null;
|
|
3665
|
+
}
|
|
3666
|
+
function assertSessionScope(actionType, targetProject) {
|
|
3667
|
+
try {
|
|
3668
|
+
const currentProject = getProjectName();
|
|
3669
|
+
const exeSession = resolveExeSession();
|
|
3670
|
+
if (!exeSession) {
|
|
3671
|
+
return { allowed: true, reason: "no_session" };
|
|
3672
|
+
}
|
|
3673
|
+
if (currentProject === targetProject) {
|
|
3674
|
+
return {
|
|
3675
|
+
allowed: true,
|
|
3676
|
+
reason: "same_session",
|
|
3677
|
+
currentProject,
|
|
3678
|
+
targetProject
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
process.stderr.write(
|
|
3682
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
3683
|
+
`
|
|
3468
3684
|
);
|
|
3685
|
+
return {
|
|
3686
|
+
allowed: false,
|
|
3687
|
+
reason: "cross_session_denied",
|
|
3688
|
+
currentProject,
|
|
3689
|
+
targetProject,
|
|
3690
|
+
targetSession: findSessionForProject(targetProject)?.windowName
|
|
3691
|
+
};
|
|
3692
|
+
} catch {
|
|
3693
|
+
return { allowed: true, reason: "no_session" };
|
|
3469
3694
|
}
|
|
3470
|
-
throw new Error(`Task not found: ${identifier}`);
|
|
3471
3695
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
let blockedById = null;
|
|
3479
|
-
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
3480
|
-
if (input.blockedBy) {
|
|
3481
|
-
const blocker = await resolveTask(client, input.blockedBy);
|
|
3482
|
-
blockedById = String(blocker.id);
|
|
3696
|
+
var init_session_scope = __esm({
|
|
3697
|
+
"src/lib/session-scope.ts"() {
|
|
3698
|
+
"use strict";
|
|
3699
|
+
init_session_registry();
|
|
3700
|
+
init_project_name();
|
|
3701
|
+
init_tmux_routing();
|
|
3483
3702
|
}
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
);
|
|
3703
|
+
});
|
|
3704
|
+
|
|
3705
|
+
// src/lib/tasks-notify.ts
|
|
3706
|
+
async function dispatchTaskToEmployee(input) {
|
|
3707
|
+
if (input.assignedTo === "exe") return { dispatched: "skipped" };
|
|
3708
|
+
let crossProject = false;
|
|
3709
|
+
if (input.projectName) {
|
|
3710
|
+
try {
|
|
3711
|
+
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
3712
|
+
const check = assertSessionScope2("dispatch_task", input.projectName);
|
|
3713
|
+
if (check.reason === "cross_session_denied") {
|
|
3714
|
+
crossProject = true;
|
|
3715
|
+
return { dispatched: "skipped", crossProject: true };
|
|
3716
|
+
}
|
|
3717
|
+
} catch {
|
|
3493
3718
|
}
|
|
3494
3719
|
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3720
|
+
try {
|
|
3721
|
+
const transport = getTransport();
|
|
3722
|
+
const exeSession = resolveExeSession();
|
|
3723
|
+
if (!exeSession) return { dispatched: "session_missing" };
|
|
3724
|
+
const sessionName = employeeSessionName(input.assignedTo, exeSession);
|
|
3725
|
+
if (transport.isAlive(sessionName)) {
|
|
3726
|
+
const result = sendIntercom(sessionName);
|
|
3727
|
+
const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
|
|
3728
|
+
return { dispatched, session: sessionName, crossProject };
|
|
3729
|
+
} else {
|
|
3730
|
+
const projectDir = input.projectDir ?? process.cwd();
|
|
3731
|
+
const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
|
|
3732
|
+
autoInstance: isMultiInstance(input.assignedTo)
|
|
3733
|
+
});
|
|
3734
|
+
if (result.status === "failed") {
|
|
3735
|
+
process.stderr.write(
|
|
3736
|
+
`[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
|
|
3737
|
+
`
|
|
3503
3738
|
);
|
|
3739
|
+
return { dispatched: "session_missing" };
|
|
3504
3740
|
}
|
|
3505
|
-
|
|
3741
|
+
return { dispatched: "spawned", session: result.sessionName, crossProject };
|
|
3506
3742
|
}
|
|
3743
|
+
} catch {
|
|
3744
|
+
return { dispatched: "session_missing" };
|
|
3507
3745
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
3746
|
+
}
|
|
3747
|
+
function notifyTaskDone() {
|
|
3748
|
+
try {
|
|
3749
|
+
const key = getSessionKey();
|
|
3750
|
+
if (key && !process.env.VITEST) notifyParentExe(key);
|
|
3751
|
+
} catch {
|
|
3515
3752
|
}
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
await ensureGitignoreExe(input.baseDir);
|
|
3522
|
-
} catch {
|
|
3523
|
-
}
|
|
3753
|
+
}
|
|
3754
|
+
async function markTaskNotificationsRead(taskFile) {
|
|
3755
|
+
try {
|
|
3756
|
+
await markAsReadByTaskFile(taskFile);
|
|
3757
|
+
} catch {
|
|
3524
3758
|
}
|
|
3525
|
-
|
|
3759
|
+
}
|
|
3760
|
+
var init_tasks_notify = __esm({
|
|
3761
|
+
"src/lib/tasks-notify.ts"() {
|
|
3762
|
+
"use strict";
|
|
3763
|
+
init_tmux_routing();
|
|
3764
|
+
init_session_key();
|
|
3765
|
+
init_notifications();
|
|
3766
|
+
init_transport();
|
|
3767
|
+
init_employees();
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
|
|
3771
|
+
// src/lib/behaviors.ts
|
|
3772
|
+
import crypto5 from "crypto";
|
|
3773
|
+
async function storeBehavior(opts) {
|
|
3774
|
+
const client = getClient();
|
|
3775
|
+
const id = crypto5.randomUUID();
|
|
3776
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3526
3777
|
await client.execute({
|
|
3527
|
-
sql: `INSERT INTO
|
|
3528
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
3529
|
-
args: [
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3778
|
+
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
3779
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
3780
|
+
args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
|
|
3781
|
+
});
|
|
3782
|
+
return id;
|
|
3783
|
+
}
|
|
3784
|
+
var init_behaviors = __esm({
|
|
3785
|
+
"src/lib/behaviors.ts"() {
|
|
3786
|
+
"use strict";
|
|
3787
|
+
init_database();
|
|
3788
|
+
}
|
|
3789
|
+
});
|
|
3790
|
+
|
|
3791
|
+
// src/lib/skill-learning.ts
|
|
3792
|
+
var skill_learning_exports = {};
|
|
3793
|
+
__export(skill_learning_exports, {
|
|
3794
|
+
captureAndLearn: () => captureAndLearn,
|
|
3795
|
+
captureTrajectory: () => captureTrajectory,
|
|
3796
|
+
editDistance: () => editDistance,
|
|
3797
|
+
extractSkill: () => extractSkill,
|
|
3798
|
+
extractTrajectory: () => extractTrajectory,
|
|
3799
|
+
findSimilarTrajectories: () => findSimilarTrajectories,
|
|
3800
|
+
hashSignature: () => hashSignature,
|
|
3801
|
+
storeTrajectory: () => storeTrajectory,
|
|
3802
|
+
sweepTrajectories: () => sweepTrajectories
|
|
3803
|
+
});
|
|
3804
|
+
import crypto6 from "crypto";
|
|
3805
|
+
async function extractTrajectory(taskId, agentId) {
|
|
3806
|
+
const client = getClient();
|
|
3807
|
+
const result = await client.execute({
|
|
3808
|
+
sql: `SELECT tool_name, raw_text
|
|
3809
|
+
FROM memories
|
|
3810
|
+
WHERE task_id = ? AND agent_id = ?
|
|
3811
|
+
ORDER BY timestamp ASC`,
|
|
3812
|
+
args: [taskId, agentId]
|
|
3813
|
+
});
|
|
3814
|
+
if (result.rows.length === 0) return [];
|
|
3815
|
+
const rawTools = result.rows.map((r) => {
|
|
3816
|
+
const toolName = String(r.tool_name);
|
|
3817
|
+
if (toolName === "Bash") {
|
|
3818
|
+
const text = String(r.raw_text);
|
|
3819
|
+
const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
|
|
3820
|
+
return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
|
|
3821
|
+
}
|
|
3822
|
+
return toolName;
|
|
3823
|
+
});
|
|
3824
|
+
const signature = [];
|
|
3825
|
+
for (const tool of rawTools) {
|
|
3826
|
+
if (signature.length === 0 || signature[signature.length - 1] !== tool) {
|
|
3827
|
+
signature.push(tool);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
return signature;
|
|
3831
|
+
}
|
|
3832
|
+
function hashSignature(signature) {
|
|
3833
|
+
return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
3834
|
+
}
|
|
3835
|
+
async function storeTrajectory(opts) {
|
|
3836
|
+
const client = getClient();
|
|
3837
|
+
const id = crypto6.randomUUID();
|
|
3838
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3839
|
+
const signatureHash = hashSignature(opts.signature);
|
|
3840
|
+
await client.execute({
|
|
3841
|
+
sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
|
|
3842
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3843
|
+
args: [
|
|
3844
|
+
id,
|
|
3845
|
+
opts.taskId,
|
|
3846
|
+
opts.agentId,
|
|
3847
|
+
opts.projectName,
|
|
3848
|
+
opts.taskTitle,
|
|
3849
|
+
JSON.stringify(opts.signature),
|
|
3850
|
+
signatureHash,
|
|
3851
|
+
opts.signature.length,
|
|
3548
3852
|
now
|
|
3549
3853
|
]
|
|
3550
3854
|
});
|
|
3551
|
-
return
|
|
3552
|
-
id,
|
|
3553
|
-
title: input.title,
|
|
3554
|
-
assignedTo: input.assignedTo,
|
|
3555
|
-
assignedBy: input.assignedBy,
|
|
3556
|
-
projectName: input.projectName,
|
|
3557
|
-
priority: input.priority,
|
|
3558
|
-
status: initialStatus,
|
|
3559
|
-
taskFile,
|
|
3560
|
-
createdAt: now,
|
|
3561
|
-
updatedAt: now,
|
|
3562
|
-
warning,
|
|
3563
|
-
budgetTokens: input.budgetTokens ?? null,
|
|
3564
|
-
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
3565
|
-
tokensUsed: 0,
|
|
3566
|
-
tokensWarnedAt: null
|
|
3567
|
-
};
|
|
3855
|
+
return id;
|
|
3568
3856
|
}
|
|
3569
|
-
async function
|
|
3857
|
+
async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
|
|
3570
3858
|
const client = getClient();
|
|
3571
|
-
const
|
|
3572
|
-
const args = [];
|
|
3573
|
-
if (input.assignedTo) {
|
|
3574
|
-
conditions.push("assigned_to = ?");
|
|
3575
|
-
args.push(input.assignedTo);
|
|
3576
|
-
}
|
|
3577
|
-
if (input.status) {
|
|
3578
|
-
conditions.push("status = ?");
|
|
3579
|
-
args.push(input.status);
|
|
3580
|
-
} else {
|
|
3581
|
-
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
3582
|
-
}
|
|
3583
|
-
if (input.projectName) {
|
|
3584
|
-
conditions.push("project_name = ?");
|
|
3585
|
-
args.push(input.projectName);
|
|
3586
|
-
}
|
|
3587
|
-
if (input.priority) {
|
|
3588
|
-
conditions.push("priority = ?");
|
|
3589
|
-
args.push(input.priority);
|
|
3590
|
-
}
|
|
3591
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3859
|
+
const hash = hashSignature(signature);
|
|
3592
3860
|
const result = await client.execute({
|
|
3593
|
-
sql: `SELECT
|
|
3594
|
-
|
|
3861
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
3862
|
+
FROM trajectories
|
|
3863
|
+
WHERE signature_hash = ?
|
|
3864
|
+
ORDER BY created_at DESC
|
|
3865
|
+
LIMIT 20`,
|
|
3866
|
+
args: [hash]
|
|
3595
3867
|
});
|
|
3596
|
-
|
|
3868
|
+
const mapRow = (r) => ({
|
|
3597
3869
|
id: String(r.id),
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
assignedBy: String(r.assigned_by),
|
|
3870
|
+
taskId: String(r.task_id),
|
|
3871
|
+
agentId: String(r.agent_id),
|
|
3601
3872
|
projectName: String(r.project_name),
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
const count = parseInt(commitCount, 10);
|
|
3629
|
-
if (count === 0) {
|
|
3630
|
-
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
3873
|
+
taskTitle: String(r.task_title),
|
|
3874
|
+
signature: JSON.parse(String(r.signature)),
|
|
3875
|
+
signatureHash: String(r.signature_hash),
|
|
3876
|
+
toolCount: Number(r.tool_count),
|
|
3877
|
+
skillId: r.skill_id ? String(r.skill_id) : null,
|
|
3878
|
+
createdAt: String(r.created_at)
|
|
3879
|
+
});
|
|
3880
|
+
const matches = result.rows.map(mapRow);
|
|
3881
|
+
if (matches.length >= threshold) return matches;
|
|
3882
|
+
const nearResult = await client.execute({
|
|
3883
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
3884
|
+
FROM trajectories
|
|
3885
|
+
WHERE tool_count BETWEEN ? AND ?
|
|
3886
|
+
AND signature_hash != ?
|
|
3887
|
+
ORDER BY created_at DESC
|
|
3888
|
+
LIMIT 50`,
|
|
3889
|
+
args: [
|
|
3890
|
+
Math.max(1, signature.length - 3),
|
|
3891
|
+
signature.length + 3,
|
|
3892
|
+
hash
|
|
3893
|
+
]
|
|
3894
|
+
});
|
|
3895
|
+
for (const r of nearResult.rows) {
|
|
3896
|
+
const candidateSig = JSON.parse(String(r.signature));
|
|
3897
|
+
if (editDistance(signature, candidateSig) <= 2) {
|
|
3898
|
+
matches.push(mapRow(r));
|
|
3631
3899
|
}
|
|
3632
|
-
return null;
|
|
3633
|
-
} catch {
|
|
3634
|
-
return null;
|
|
3635
3900
|
}
|
|
3901
|
+
return matches;
|
|
3636
3902
|
}
|
|
3637
|
-
async function
|
|
3638
|
-
const
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
const taskId = String(row.id);
|
|
3642
|
-
const taskFile = String(row.task_file);
|
|
3643
|
-
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
3644
|
-
process.stderr.write(
|
|
3645
|
-
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
3646
|
-
`
|
|
3647
|
-
);
|
|
3903
|
+
async function captureTrajectory(opts) {
|
|
3904
|
+
const signature = await extractTrajectory(opts.taskId, opts.agentId);
|
|
3905
|
+
if (signature.length < 3) {
|
|
3906
|
+
return { trajectoryId: "", similarCount: 0, similar: [] };
|
|
3648
3907
|
}
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3908
|
+
const trajectoryId = await storeTrajectory({
|
|
3909
|
+
taskId: opts.taskId,
|
|
3910
|
+
agentId: opts.agentId,
|
|
3911
|
+
projectName: opts.projectName,
|
|
3912
|
+
taskTitle: opts.taskTitle,
|
|
3913
|
+
signature
|
|
3914
|
+
});
|
|
3915
|
+
const similar = await findSimilarTrajectories(
|
|
3916
|
+
signature,
|
|
3917
|
+
opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
|
|
3918
|
+
);
|
|
3919
|
+
return { trajectoryId, similarCount: similar.length, similar };
|
|
3920
|
+
}
|
|
3921
|
+
function buildExtractionPrompt(trajectories) {
|
|
3922
|
+
const items = trajectories.map((t, i) => {
|
|
3923
|
+
const sig = t.signature.join(" \u2192 ");
|
|
3924
|
+
return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
|
|
3925
|
+
Signature: ${sig}`;
|
|
3926
|
+
}).join("\n\n");
|
|
3927
|
+
return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
|
|
3659
3928
|
|
|
3660
|
-
${
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
}
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
3702
|
-
args: [input.status, now, taskId]
|
|
3929
|
+
${items}
|
|
3930
|
+
|
|
3931
|
+
Extract the reusable procedure. Format your response EXACTLY like this:
|
|
3932
|
+
|
|
3933
|
+
SKILL: {name \u2014 short, descriptive}
|
|
3934
|
+
TRIGGER: {when to use this \u2014 one sentence}
|
|
3935
|
+
STEPS:
|
|
3936
|
+
1. ...
|
|
3937
|
+
2. ...
|
|
3938
|
+
PITFALLS: {common mistakes to avoid}
|
|
3939
|
+
|
|
3940
|
+
Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
|
|
3941
|
+
}
|
|
3942
|
+
async function extractSkill(trajectories, model) {
|
|
3943
|
+
if (trajectories.length === 0) return null;
|
|
3944
|
+
const config = await loadConfig();
|
|
3945
|
+
const skillModel = model ?? config.skillModel;
|
|
3946
|
+
const Anthropic = (await import("@anthropic-ai/sdk")).default;
|
|
3947
|
+
const client = new Anthropic();
|
|
3948
|
+
const prompt = buildExtractionPrompt(trajectories);
|
|
3949
|
+
const response = await client.messages.create({
|
|
3950
|
+
model: skillModel,
|
|
3951
|
+
max_tokens: 500,
|
|
3952
|
+
messages: [{ role: "user", content: prompt }]
|
|
3953
|
+
});
|
|
3954
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
3955
|
+
const skillText = textBlock?.text;
|
|
3956
|
+
if (!skillText) return null;
|
|
3957
|
+
const agentId = trajectories[0].agentId;
|
|
3958
|
+
const projectName = trajectories[0].projectName;
|
|
3959
|
+
const skillId = await storeBehavior({
|
|
3960
|
+
agentId,
|
|
3961
|
+
content: skillText,
|
|
3962
|
+
domain: "skill",
|
|
3963
|
+
projectName
|
|
3964
|
+
});
|
|
3965
|
+
const dbClient = getClient();
|
|
3966
|
+
for (const t of trajectories) {
|
|
3967
|
+
await dbClient.execute({
|
|
3968
|
+
sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
|
|
3969
|
+
args: [skillId, t.id]
|
|
3703
3970
|
});
|
|
3704
3971
|
}
|
|
3972
|
+
process.stderr.write(
|
|
3973
|
+
`[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
|
|
3974
|
+
`
|
|
3975
|
+
);
|
|
3976
|
+
return skillId;
|
|
3977
|
+
}
|
|
3978
|
+
async function captureAndLearn(opts) {
|
|
3705
3979
|
try {
|
|
3706
|
-
await
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3980
|
+
const config = await loadConfig();
|
|
3981
|
+
if (!config.skillLearning) return;
|
|
3982
|
+
const { trajectoryId, similarCount, similar } = await captureTrajectory({
|
|
3983
|
+
...opts,
|
|
3984
|
+
skillThreshold: config.skillThreshold
|
|
3710
3985
|
});
|
|
3711
|
-
|
|
3986
|
+
if (!trajectoryId) return;
|
|
3987
|
+
if (similarCount >= config.skillThreshold) {
|
|
3988
|
+
const unprocessed = similar.filter((t) => !t.skillId);
|
|
3989
|
+
if (unprocessed.length >= config.skillThreshold) {
|
|
3990
|
+
extractSkill(unprocessed, config.skillModel).catch((err) => {
|
|
3991
|
+
process.stderr.write(
|
|
3992
|
+
`[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
|
|
3993
|
+
`
|
|
3994
|
+
);
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
} catch (err) {
|
|
3999
|
+
process.stderr.write(
|
|
4000
|
+
`[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
|
|
4001
|
+
`
|
|
4002
|
+
);
|
|
3712
4003
|
}
|
|
3713
|
-
return { row, taskFile, now, taskId };
|
|
3714
4004
|
}
|
|
3715
|
-
async function
|
|
4005
|
+
async function sweepTrajectories(threshold, model) {
|
|
4006
|
+
const config = await loadConfig();
|
|
4007
|
+
if (!config.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
|
|
4008
|
+
const t = threshold ?? config.skillThreshold;
|
|
3716
4009
|
const client = getClient();
|
|
3717
|
-
const
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
const
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
].join("\n");
|
|
3757
|
-
await writeFile4(archPath, template, "utf-8");
|
|
3758
|
-
} catch {
|
|
4010
|
+
const result = await client.execute({
|
|
4011
|
+
sql: `SELECT signature_hash, COUNT(*) as cnt
|
|
4012
|
+
FROM trajectories
|
|
4013
|
+
WHERE skill_id IS NULL
|
|
4014
|
+
GROUP BY signature_hash
|
|
4015
|
+
HAVING cnt >= ?
|
|
4016
|
+
ORDER BY cnt DESC
|
|
4017
|
+
LIMIT 10`,
|
|
4018
|
+
args: [t]
|
|
4019
|
+
});
|
|
4020
|
+
let clustersProcessed = 0;
|
|
4021
|
+
let skillsExtracted = 0;
|
|
4022
|
+
for (const row of result.rows) {
|
|
4023
|
+
const hash = String(row.signature_hash);
|
|
4024
|
+
const trajResult = await client.execute({
|
|
4025
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
|
|
4026
|
+
FROM trajectories
|
|
4027
|
+
WHERE signature_hash = ? AND skill_id IS NULL
|
|
4028
|
+
ORDER BY created_at DESC
|
|
4029
|
+
LIMIT 10`,
|
|
4030
|
+
args: [hash]
|
|
4031
|
+
});
|
|
4032
|
+
const trajectories = trajResult.rows.map((r) => ({
|
|
4033
|
+
id: String(r.id),
|
|
4034
|
+
taskId: String(r.task_id),
|
|
4035
|
+
agentId: String(r.agent_id),
|
|
4036
|
+
projectName: String(r.project_name),
|
|
4037
|
+
taskTitle: String(r.task_title),
|
|
4038
|
+
signature: JSON.parse(String(r.signature)),
|
|
4039
|
+
signatureHash: String(r.signature_hash),
|
|
4040
|
+
toolCount: Number(r.tool_count),
|
|
4041
|
+
skillId: null,
|
|
4042
|
+
createdAt: String(r.created_at)
|
|
4043
|
+
}));
|
|
4044
|
+
if (trajectories.length >= t) {
|
|
4045
|
+
clustersProcessed++;
|
|
4046
|
+
const skillId = await extractSkill(trajectories, model ?? config.skillModel);
|
|
4047
|
+
if (skillId) skillsExtracted++;
|
|
4048
|
+
}
|
|
3759
4049
|
}
|
|
4050
|
+
return { clustersProcessed, skillsExtracted };
|
|
3760
4051
|
}
|
|
3761
|
-
|
|
3762
|
-
const
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
4052
|
+
function editDistance(a, b) {
|
|
4053
|
+
const m = a.length;
|
|
4054
|
+
const n = b.length;
|
|
4055
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
4056
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
4057
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
4058
|
+
for (let i = 1; i <= m; i++) {
|
|
4059
|
+
for (let j = 1; j <= n; j++) {
|
|
4060
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4061
|
+
dp[i][j] = Math.min(
|
|
4062
|
+
dp[i - 1][j] + 1,
|
|
4063
|
+
dp[i][j - 1] + 1,
|
|
4064
|
+
dp[i - 1][j - 1] + cost
|
|
4065
|
+
);
|
|
3770
4066
|
}
|
|
3771
|
-
} catch {
|
|
3772
4067
|
}
|
|
4068
|
+
return dp[m][n];
|
|
3773
4069
|
}
|
|
3774
|
-
var
|
|
3775
|
-
var
|
|
3776
|
-
"src/lib/
|
|
4070
|
+
var DEFAULT_SKILL_THRESHOLD;
|
|
4071
|
+
var init_skill_learning = __esm({
|
|
4072
|
+
"src/lib/skill-learning.ts"() {
|
|
3777
4073
|
"use strict";
|
|
3778
4074
|
init_database();
|
|
3779
|
-
|
|
3780
|
-
|
|
4075
|
+
init_behaviors();
|
|
4076
|
+
init_config();
|
|
4077
|
+
DEFAULT_SKILL_THRESHOLD = 3;
|
|
3781
4078
|
}
|
|
3782
4079
|
});
|
|
3783
4080
|
|
|
3784
|
-
// src/lib/tasks
|
|
4081
|
+
// src/lib/tasks.ts
|
|
4082
|
+
var tasks_exports = {};
|
|
4083
|
+
__export(tasks_exports, {
|
|
4084
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
4085
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
4086
|
+
countPendingReviews: () => countPendingReviews,
|
|
4087
|
+
createTask: () => createTask,
|
|
4088
|
+
createTaskCore: () => createTaskCore,
|
|
4089
|
+
deleteTask: () => deleteTask,
|
|
4090
|
+
deleteTaskCore: () => deleteTaskCore,
|
|
4091
|
+
ensureArchitectureDoc: () => ensureArchitectureDoc,
|
|
4092
|
+
ensureGitignoreExe: () => ensureGitignoreExe,
|
|
4093
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
4094
|
+
listPendingReviews: () => listPendingReviews,
|
|
4095
|
+
listTasks: () => listTasks,
|
|
4096
|
+
resolveTask: () => resolveTask,
|
|
4097
|
+
slugify: () => slugify,
|
|
4098
|
+
updateTask: () => updateTask,
|
|
4099
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
4100
|
+
writeCheckpoint: () => writeCheckpoint
|
|
4101
|
+
});
|
|
3785
4102
|
import path15 from "path";
|
|
3786
|
-
import {
|
|
3787
|
-
async function
|
|
3788
|
-
const
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
4103
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
|
|
4104
|
+
async function createTask(input) {
|
|
4105
|
+
const result = await createTaskCore(input);
|
|
4106
|
+
if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
|
|
4107
|
+
dispatchTaskToEmployee({
|
|
4108
|
+
assignedTo: input.assignedTo,
|
|
4109
|
+
title: input.title,
|
|
4110
|
+
priority: input.priority,
|
|
4111
|
+
taskFile: result.taskFile,
|
|
4112
|
+
initialStatus: result.status,
|
|
4113
|
+
projectName: input.projectName
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4116
|
+
return result;
|
|
3794
4117
|
}
|
|
3795
|
-
async function
|
|
3796
|
-
const
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
}
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
});
|
|
3812
|
-
return result.rows;
|
|
3813
|
-
}
|
|
3814
|
-
async function cleanupOrphanedReviews() {
|
|
3815
|
-
const client = getClient();
|
|
3816
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3817
|
-
const r1 = await client.execute({
|
|
3818
|
-
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3819
|
-
WHERE status = 'needs_review'
|
|
3820
|
-
AND assigned_by = 'system'
|
|
3821
|
-
AND title LIKE 'Review:%'
|
|
3822
|
-
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
|
|
3823
|
-
args: [now]
|
|
3824
|
-
});
|
|
3825
|
-
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
3826
|
-
const r2 = await client.execute({
|
|
3827
|
-
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3828
|
-
WHERE status = 'needs_review'
|
|
3829
|
-
AND result IS NOT NULL
|
|
3830
|
-
AND updated_at < ?`,
|
|
3831
|
-
args: [now, staleThreshold]
|
|
3832
|
-
});
|
|
3833
|
-
const total = r1.rowsAffected + r2.rowsAffected;
|
|
3834
|
-
if (total > 0) {
|
|
3835
|
-
process.stderr.write(
|
|
3836
|
-
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
|
|
3837
|
-
`
|
|
3838
|
-
);
|
|
3839
|
-
}
|
|
3840
|
-
return total;
|
|
3841
|
-
}
|
|
3842
|
-
function getReviewChecklist(role, agent, taskSlug) {
|
|
3843
|
-
const roleLower = role.toLowerCase();
|
|
3844
|
-
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
3845
|
-
return {
|
|
3846
|
-
lens: "Code Quality (Engineer)",
|
|
3847
|
-
checklist: [
|
|
3848
|
-
"1. Do all tests pass? Any new tests needed?",
|
|
3849
|
-
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
3850
|
-
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
3851
|
-
"4. Any regressions in the test suite?"
|
|
3852
|
-
]
|
|
3853
|
-
};
|
|
3854
|
-
}
|
|
3855
|
-
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
3856
|
-
return {
|
|
3857
|
-
lens: "Architecture (CTO)",
|
|
3858
|
-
checklist: [
|
|
3859
|
-
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
3860
|
-
"2. Is it backward compatible? Any breaking changes?",
|
|
3861
|
-
"3. Does it introduce technical debt? Is that debt justified?",
|
|
3862
|
-
"4. Security implications? Any new attack surface?",
|
|
3863
|
-
"5. Does it scale? Performance considerations?",
|
|
3864
|
-
"6. Coordination: does this affect other employees' work or other projects?"
|
|
3865
|
-
]
|
|
3866
|
-
};
|
|
4118
|
+
async function updateTask(input) {
|
|
4119
|
+
const { row, taskFile, now, taskId } = await updateTaskStatus(input);
|
|
4120
|
+
try {
|
|
4121
|
+
const agent = String(row.assigned_to);
|
|
4122
|
+
const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
|
|
4123
|
+
const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
|
|
4124
|
+
if (input.status === "in_progress") {
|
|
4125
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
4126
|
+
writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
4127
|
+
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
4128
|
+
try {
|
|
4129
|
+
unlinkSync4(cachePath);
|
|
4130
|
+
} catch {
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
} catch {
|
|
3867
4134
|
}
|
|
3868
|
-
if (
|
|
3869
|
-
|
|
3870
|
-
lens: "Strategic (COO)",
|
|
3871
|
-
checklist: [
|
|
3872
|
-
"1. Does this serve the project mission?",
|
|
3873
|
-
"2. Is this the right work at the right time?",
|
|
3874
|
-
"3. Does the architectural assessment make sense for the business?",
|
|
3875
|
-
"4. Any cross-project implications?"
|
|
3876
|
-
]
|
|
3877
|
-
};
|
|
4135
|
+
if (input.status === "done") {
|
|
4136
|
+
await cleanupReviewFile(row, taskFile, input.baseDir);
|
|
3878
4137
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
};
|
|
3889
|
-
}
|
|
3890
|
-
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
3891
|
-
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
3892
|
-
try {
|
|
3893
|
-
const client = getClient();
|
|
3894
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3895
|
-
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
3896
|
-
if (parentId) {
|
|
3897
|
-
const result = await client.execute({
|
|
3898
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
3899
|
-
args: [now, parentId]
|
|
4138
|
+
if (input.status === "done" || input.status === "cancelled") {
|
|
4139
|
+
try {
|
|
4140
|
+
const client = getClient();
|
|
4141
|
+
const taskTitle = String(row.title);
|
|
4142
|
+
const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
4143
|
+
await client.execute({
|
|
4144
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
4145
|
+
WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
|
|
4146
|
+
args: [now, `%left '${escaped}' as in\\_progress%`]
|
|
3900
4147
|
});
|
|
3901
|
-
|
|
4148
|
+
} catch {
|
|
4149
|
+
}
|
|
4150
|
+
try {
|
|
4151
|
+
const client = getClient();
|
|
4152
|
+
const cascaded = await client.execute({
|
|
4153
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
4154
|
+
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
4155
|
+
args: [now, taskId]
|
|
4156
|
+
});
|
|
4157
|
+
if (cascaded.rowsAffected > 0) {
|
|
3902
4158
|
process.stderr.write(
|
|
3903
|
-
`[
|
|
4159
|
+
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
3904
4160
|
`
|
|
3905
4161
|
);
|
|
3906
4162
|
}
|
|
3907
|
-
}
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
4163
|
+
} catch {
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
4167
|
+
if (isTerminal) {
|
|
4168
|
+
const isExe = String(row.assigned_to) === "exe";
|
|
4169
|
+
if (!isExe) {
|
|
4170
|
+
notifyTaskDone();
|
|
4171
|
+
}
|
|
4172
|
+
await markTaskNotificationsRead(taskFile);
|
|
4173
|
+
if (input.status === "done") {
|
|
4174
|
+
try {
|
|
4175
|
+
await cascadeUnblock(taskId, input.baseDir, now);
|
|
4176
|
+
} catch {
|
|
4177
|
+
}
|
|
4178
|
+
orgBus.emit({
|
|
4179
|
+
type: "task_completed",
|
|
4180
|
+
taskId,
|
|
4181
|
+
employee: String(row.assigned_to),
|
|
4182
|
+
result: input.result ?? "",
|
|
4183
|
+
timestamp: now
|
|
4184
|
+
});
|
|
4185
|
+
if (row.parent_task_id) {
|
|
4186
|
+
try {
|
|
4187
|
+
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
4188
|
+
} catch {
|
|
3924
4189
|
}
|
|
3925
4190
|
}
|
|
3926
4191
|
}
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
|
|
4192
|
+
}
|
|
4193
|
+
if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
|
|
4194
|
+
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
4195
|
+
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
4196
|
+
taskId,
|
|
4197
|
+
agentId: String(row.assigned_to),
|
|
4198
|
+
projectName: String(row.project_name),
|
|
4199
|
+
taskTitle: String(row.title)
|
|
4200
|
+
})
|
|
4201
|
+
).catch((err) => {
|
|
4202
|
+
process.stderr.write(
|
|
4203
|
+
`[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
|
|
3930
4204
|
`
|
|
3931
|
-
|
|
4205
|
+
);
|
|
4206
|
+
});
|
|
3932
4207
|
}
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
unlinkSync4(path15.join(cacheDir, f));
|
|
3939
|
-
}
|
|
3940
|
-
}
|
|
4208
|
+
let nextTask;
|
|
4209
|
+
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
4210
|
+
try {
|
|
4211
|
+
nextTask = await findNextTask(String(row.assigned_to));
|
|
4212
|
+
} catch {
|
|
3941
4213
|
}
|
|
3942
|
-
} catch {
|
|
3943
4214
|
}
|
|
4215
|
+
return {
|
|
4216
|
+
id: String(row.id),
|
|
4217
|
+
title: String(row.title),
|
|
4218
|
+
assignedTo: String(row.assigned_to),
|
|
4219
|
+
assignedBy: String(row.assigned_by),
|
|
4220
|
+
projectName: String(row.project_name),
|
|
4221
|
+
priority: String(row.priority),
|
|
4222
|
+
status: input.status,
|
|
4223
|
+
taskFile,
|
|
4224
|
+
createdAt: String(row.created_at),
|
|
4225
|
+
updatedAt: now,
|
|
4226
|
+
budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
|
|
4227
|
+
budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
|
|
4228
|
+
tokensUsed: Number(row.tokens_used ?? 0),
|
|
4229
|
+
tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
|
|
4230
|
+
nextTask
|
|
4231
|
+
};
|
|
3944
4232
|
}
|
|
3945
|
-
|
|
3946
|
-
|
|
4233
|
+
async function deleteTask(taskId, baseDir) {
|
|
4234
|
+
const client = getClient();
|
|
4235
|
+
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
4236
|
+
const reviewer = assignedBy || "exe";
|
|
4237
|
+
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
4238
|
+
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
4239
|
+
await client.execute({
|
|
4240
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
|
|
4241
|
+
args: [reviewFile, `exe/exe/${reviewSlug}.md`]
|
|
4242
|
+
});
|
|
4243
|
+
await markAsReadByTaskFile(taskFile);
|
|
4244
|
+
await markAsReadByTaskFile(reviewFile);
|
|
4245
|
+
}
|
|
4246
|
+
var init_tasks = __esm({
|
|
4247
|
+
"src/lib/tasks.ts"() {
|
|
3947
4248
|
"use strict";
|
|
3948
4249
|
init_database();
|
|
3949
4250
|
init_config();
|
|
3950
|
-
init_employees();
|
|
3951
4251
|
init_notifications();
|
|
4252
|
+
init_state_bus();
|
|
3952
4253
|
init_tasks_crud();
|
|
3953
|
-
|
|
3954
|
-
|
|
4254
|
+
init_tasks_review();
|
|
4255
|
+
init_tasks_crud();
|
|
4256
|
+
init_tasks_chain();
|
|
4257
|
+
init_tasks_review();
|
|
4258
|
+
init_tasks_notify();
|
|
3955
4259
|
}
|
|
3956
4260
|
});
|
|
3957
4261
|
|
|
3958
|
-
// src/lib/
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4262
|
+
// src/lib/capacity-monitor.ts
|
|
4263
|
+
var capacity_monitor_exports = {};
|
|
4264
|
+
__export(capacity_monitor_exports, {
|
|
4265
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
4266
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
4267
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
4268
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
4269
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
4270
|
+
extractContextPercent: () => extractContextPercent,
|
|
4271
|
+
isAtCapacity: () => isAtCapacity,
|
|
4272
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
4273
|
+
pollCapacityDead: () => pollCapacityDead
|
|
4274
|
+
});
|
|
4275
|
+
function resumeTaskTitle(agentId) {
|
|
4276
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
4277
|
+
}
|
|
4278
|
+
function buildResumeContext(agentId, openTasks) {
|
|
4279
|
+
const taskList = openTasks.map(
|
|
4280
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
4281
|
+
).join("\n");
|
|
4282
|
+
return [
|
|
4283
|
+
"## Context",
|
|
4284
|
+
"",
|
|
4285
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
4286
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
4287
|
+
"",
|
|
4288
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
4289
|
+
"",
|
|
4290
|
+
taskList,
|
|
4291
|
+
"",
|
|
4292
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
4293
|
+
].join("\n");
|
|
4294
|
+
}
|
|
4295
|
+
function filterPaneContent(paneOutput) {
|
|
4296
|
+
return paneOutput.split("\n").filter((line) => {
|
|
4297
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
4298
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
4299
|
+
if (line.includes(marker)) return false;
|
|
4300
|
+
}
|
|
4301
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
4302
|
+
if (re.test(line)) return false;
|
|
3982
4303
|
}
|
|
4304
|
+
return true;
|
|
4305
|
+
}).join("\n");
|
|
4306
|
+
}
|
|
4307
|
+
function extractContextPercent(paneOutput) {
|
|
4308
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
4309
|
+
if (!match) return null;
|
|
4310
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
4311
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
4312
|
+
}
|
|
4313
|
+
function isAtCapacity(paneOutput) {
|
|
4314
|
+
const filtered = filterPaneContent(paneOutput);
|
|
4315
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
4316
|
+
}
|
|
4317
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
4318
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
4319
|
+
if (pendingSince === void 0) {
|
|
4320
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4321
|
+
return false;
|
|
4322
|
+
}
|
|
4323
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
4324
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4325
|
+
return false;
|
|
3983
4326
|
}
|
|
4327
|
+
_pendingCapacityKill.delete(agentId);
|
|
4328
|
+
return true;
|
|
3984
4329
|
}
|
|
3985
|
-
|
|
4330
|
+
function _resetPendingCapacityKills() {
|
|
4331
|
+
_pendingCapacityKill.clear();
|
|
4332
|
+
}
|
|
4333
|
+
function _resetLastRelaunchCache() {
|
|
4334
|
+
_lastRelaunch.clear();
|
|
4335
|
+
}
|
|
4336
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
3986
4337
|
const client = getClient();
|
|
3987
|
-
const
|
|
3988
|
-
sql: `SELECT
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
args: [assignedTo]
|
|
4338
|
+
const result = await client.execute({
|
|
4339
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
4340
|
+
FROM tasks
|
|
4341
|
+
WHERE assigned_to = ? AND title LIKE ?`,
|
|
4342
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`]
|
|
3993
4343
|
});
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4344
|
+
const raw = result.rows[0]?.last_created_at;
|
|
4345
|
+
if (raw === null || raw === void 0) return null;
|
|
4346
|
+
const parsed = Date.parse(String(raw));
|
|
4347
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
4348
|
+
}
|
|
4349
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
4350
|
+
const cached = _lastRelaunch.get(agentId);
|
|
4351
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
4352
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
4353
|
+
if (persisted === null) return false;
|
|
4354
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
4355
|
+
_lastRelaunch.set(agentId, persisted);
|
|
4356
|
+
return true;
|
|
4003
4357
|
}
|
|
4004
|
-
async function
|
|
4358
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
4005
4359
|
const client = getClient();
|
|
4006
|
-
const
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4360
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4361
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
4362
|
+
const existing = await client.execute({
|
|
4363
|
+
sql: `SELECT id FROM tasks
|
|
4364
|
+
WHERE assigned_to = ?
|
|
4365
|
+
AND title LIKE ?
|
|
4366
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
|
|
4367
|
+
ORDER BY created_at DESC
|
|
4368
|
+
LIMIT 1`,
|
|
4369
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
4010
4370
|
});
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
sql: `
|
|
4015
|
-
args: [
|
|
4371
|
+
if (existing.rows.length > 0) {
|
|
4372
|
+
const taskId = String(existing.rows[0].id);
|
|
4373
|
+
await client.execute({
|
|
4374
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
4375
|
+
args: [context, now, taskId]
|
|
4016
4376
|
});
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4377
|
+
return { created: false, taskId };
|
|
4378
|
+
}
|
|
4379
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
4380
|
+
const task = await createTask2({
|
|
4381
|
+
title: resumeTaskTitle(agentId),
|
|
4382
|
+
assignedTo: agentId,
|
|
4383
|
+
assignedBy: "system",
|
|
4384
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
4385
|
+
priority: "p0",
|
|
4386
|
+
context,
|
|
4387
|
+
baseDir: projectDir
|
|
4388
|
+
});
|
|
4389
|
+
return { created: true, taskId: task.id };
|
|
4390
|
+
}
|
|
4391
|
+
async function pollCapacityDead() {
|
|
4392
|
+
const transport = getTransport();
|
|
4393
|
+
const relaunched = [];
|
|
4394
|
+
const registered = listSessions().filter(
|
|
4395
|
+
(s) => s.agentId !== "exe"
|
|
4396
|
+
);
|
|
4397
|
+
if (registered.length === 0) return [];
|
|
4398
|
+
let liveSessions;
|
|
4399
|
+
try {
|
|
4400
|
+
liveSessions = transport.listSessions();
|
|
4401
|
+
} catch {
|
|
4402
|
+
return [];
|
|
4403
|
+
}
|
|
4404
|
+
for (const entry of registered) {
|
|
4405
|
+
const { windowName, agentId, projectDir } = entry;
|
|
4406
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
4407
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
4408
|
+
let pane;
|
|
4409
|
+
try {
|
|
4410
|
+
pane = transport.capturePane(windowName, 15);
|
|
4411
|
+
} catch {
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
if (!isAtCapacity(pane)) continue;
|
|
4415
|
+
const ctxPct = extractContextPercent(pane);
|
|
4416
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
4417
|
+
process.stderr.write(
|
|
4418
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
4419
|
+
`
|
|
4420
|
+
);
|
|
4421
|
+
continue;
|
|
4422
|
+
}
|
|
4423
|
+
if (!confirmCapacityKill(agentId)) {
|
|
4424
|
+
process.stderr.write(
|
|
4425
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
4426
|
+
`
|
|
4427
|
+
);
|
|
4428
|
+
continue;
|
|
4429
|
+
}
|
|
4430
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
4431
|
+
if (!verify.atCapacity) {
|
|
4432
|
+
process.stderr.write(
|
|
4433
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
4434
|
+
`
|
|
4435
|
+
);
|
|
4436
|
+
void recordSessionKill({
|
|
4437
|
+
sessionName: windowName,
|
|
4438
|
+
agentId,
|
|
4439
|
+
reason: "capacity_false_positive_blocked"
|
|
4440
|
+
});
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
process.stderr.write(
|
|
4444
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
4445
|
+
`
|
|
4446
|
+
);
|
|
4447
|
+
try {
|
|
4448
|
+
transport.kill(windowName);
|
|
4449
|
+
void recordSessionKill({
|
|
4450
|
+
sessionName: windowName,
|
|
4451
|
+
agentId,
|
|
4452
|
+
reason: "capacity"
|
|
4453
|
+
});
|
|
4454
|
+
const client = getClient();
|
|
4455
|
+
const openTasks = await client.execute({
|
|
4456
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
4457
|
+
FROM tasks
|
|
4458
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')
|
|
4459
|
+
ORDER BY
|
|
4460
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
4461
|
+
created_at ASC
|
|
4462
|
+
LIMIT 10`,
|
|
4463
|
+
args: [agentId]
|
|
4027
4464
|
});
|
|
4465
|
+
if (openTasks.rows.length === 0) {
|
|
4466
|
+
process.stderr.write(
|
|
4467
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
4468
|
+
`
|
|
4469
|
+
);
|
|
4470
|
+
continue;
|
|
4471
|
+
}
|
|
4472
|
+
const { created } = await createOrRefreshResumeTask(
|
|
4473
|
+
agentId,
|
|
4474
|
+
projectDir,
|
|
4475
|
+
openTasks.rows
|
|
4476
|
+
);
|
|
4477
|
+
if (created) {
|
|
4478
|
+
await writeNotification({
|
|
4479
|
+
agentId: "system",
|
|
4480
|
+
agentRole: "daemon",
|
|
4481
|
+
event: "capacity_relaunch",
|
|
4482
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
4483
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
4487
|
+
if (created) relaunched.push(agentId);
|
|
4488
|
+
} catch (err) {
|
|
4489
|
+
process.stderr.write(
|
|
4490
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
4491
|
+
`
|
|
4492
|
+
);
|
|
4028
4493
|
}
|
|
4029
4494
|
}
|
|
4495
|
+
return relaunched;
|
|
4030
4496
|
}
|
|
4031
|
-
var
|
|
4032
|
-
|
|
4497
|
+
var CAPACITY_PATTERNS, CONTENT_LINE_PREFIX, CONTENT_LINE_MARKERS, SOURCE_CODE_MARKERS, RELAUNCH_COOLDOWN_MS, _lastRelaunch, RESUME_TITLE_PREFIX, RESUME_TITLE_LIKE_PATTERN, RESUME_ACTIVE_STATUSES, CONFIRMATION_WINDOW_MS, _pendingCapacityKill, CC_CONTEXT_BAR_RE, CTX_FLOOR_PERCENT;
|
|
4498
|
+
var init_capacity_monitor = __esm({
|
|
4499
|
+
"src/lib/capacity-monitor.ts"() {
|
|
4033
4500
|
"use strict";
|
|
4034
|
-
|
|
4501
|
+
init_session_registry();
|
|
4502
|
+
init_transport();
|
|
4035
4503
|
init_notifications();
|
|
4504
|
+
init_database();
|
|
4505
|
+
init_session_kill_telemetry();
|
|
4506
|
+
init_tmux_routing();
|
|
4507
|
+
CAPACITY_PATTERNS = [
|
|
4508
|
+
/conversation is too long/i,
|
|
4509
|
+
/maximum context length/i,
|
|
4510
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
4511
|
+
/reached.*(?:token|context).*limit/i
|
|
4512
|
+
];
|
|
4513
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
4514
|
+
CONTENT_LINE_MARKERS = [
|
|
4515
|
+
"RESUME:",
|
|
4516
|
+
"intercom",
|
|
4517
|
+
"capacity-monitor",
|
|
4518
|
+
"CAPACITY_PATTERNS",
|
|
4519
|
+
"isAtCapacity",
|
|
4520
|
+
"CONTENT_LINE_MARKERS",
|
|
4521
|
+
"pollCapacityDead",
|
|
4522
|
+
"confirmCapacityKill",
|
|
4523
|
+
"session_kills",
|
|
4524
|
+
"capacity-monitor.test"
|
|
4525
|
+
];
|
|
4526
|
+
SOURCE_CODE_MARKERS = [
|
|
4527
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
4528
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
4529
|
+
];
|
|
4530
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
4531
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
4532
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
4533
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
4534
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
4535
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
4536
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
4537
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
4538
|
+
CTX_FLOOR_PERCENT = 50;
|
|
4036
4539
|
}
|
|
4037
4540
|
});
|
|
4038
4541
|
|
|
4039
|
-
// src/lib/
|
|
4040
|
-
var
|
|
4041
|
-
__export(
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4542
|
+
// src/lib/tmux-routing.ts
|
|
4543
|
+
var tmux_routing_exports = {};
|
|
4544
|
+
__export(tmux_routing_exports, {
|
|
4545
|
+
acquireSpawnLock: () => acquireSpawnLock,
|
|
4546
|
+
employeeSessionName: () => employeeSessionName,
|
|
4547
|
+
ensureEmployee: () => ensureEmployee,
|
|
4548
|
+
extractRootExe: () => extractRootExe,
|
|
4549
|
+
findFreeInstance: () => findFreeInstance,
|
|
4550
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
4551
|
+
getMySession: () => getMySession,
|
|
4552
|
+
getParentExe: () => getParentExe,
|
|
4553
|
+
getSessionState: () => getSessionState,
|
|
4554
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
4555
|
+
isExeSession: () => isExeSession,
|
|
4556
|
+
isSessionBusy: () => isSessionBusy,
|
|
4557
|
+
notifyParentExe: () => notifyParentExe,
|
|
4558
|
+
parseParentExe: () => parseParentExe,
|
|
4559
|
+
registerParentExe: () => registerParentExe,
|
|
4560
|
+
releaseSpawnLock: () => releaseSpawnLock,
|
|
4561
|
+
resolveExeSession: () => resolveExeSession,
|
|
4562
|
+
sendIntercom: () => sendIntercom,
|
|
4563
|
+
spawnEmployee: () => spawnEmployee,
|
|
4564
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
4045
4565
|
});
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
const sessions = listSessions();
|
|
4055
|
-
for (const s of sessions) {
|
|
4056
|
-
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
4057
|
-
if (proj === projectName && s.agentId === "exe") return s;
|
|
4058
|
-
}
|
|
4059
|
-
return null;
|
|
4566
|
+
import { execFileSync as execFileSync2, execSync as execSync8 } from "child_process";
|
|
4567
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync12, appendFileSync } from "fs";
|
|
4568
|
+
import path16 from "path";
|
|
4569
|
+
import os6 from "os";
|
|
4570
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4571
|
+
import { unlinkSync as unlinkSync5 } from "fs";
|
|
4572
|
+
function spawnLockPath(sessionName) {
|
|
4573
|
+
return path16.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
4060
4574
|
}
|
|
4061
|
-
function
|
|
4575
|
+
function isProcessAlive(pid) {
|
|
4062
4576
|
try {
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
if (!exeSession) {
|
|
4066
|
-
return { allowed: true, reason: "no_session" };
|
|
4067
|
-
}
|
|
4068
|
-
if (currentProject === targetProject) {
|
|
4069
|
-
return {
|
|
4070
|
-
allowed: true,
|
|
4071
|
-
reason: "same_session",
|
|
4072
|
-
currentProject,
|
|
4073
|
-
targetProject
|
|
4074
|
-
};
|
|
4075
|
-
}
|
|
4076
|
-
process.stderr.write(
|
|
4077
|
-
`[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
4078
|
-
`
|
|
4079
|
-
);
|
|
4080
|
-
return {
|
|
4081
|
-
allowed: true,
|
|
4082
|
-
// v1: warn-only, don't block
|
|
4083
|
-
reason: "cross_session_granted",
|
|
4084
|
-
currentProject,
|
|
4085
|
-
targetProject,
|
|
4086
|
-
targetSession: findSessionForProject(targetProject)?.windowName
|
|
4087
|
-
};
|
|
4577
|
+
process.kill(pid, 0);
|
|
4578
|
+
return true;
|
|
4088
4579
|
} catch {
|
|
4089
|
-
return
|
|
4580
|
+
return false;
|
|
4090
4581
|
}
|
|
4091
4582
|
}
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
init_session_registry();
|
|
4096
|
-
init_project_name();
|
|
4097
|
-
init_tmux_routing();
|
|
4583
|
+
function acquireSpawnLock(sessionName) {
|
|
4584
|
+
if (!existsSync12(SPAWN_LOCK_DIR)) {
|
|
4585
|
+
mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
|
|
4098
4586
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
// src/lib/tasks-notify.ts
|
|
4102
|
-
async function dispatchTaskToEmployee(input) {
|
|
4103
|
-
if (input.assignedTo === "exe") return { dispatched: "skipped" };
|
|
4104
|
-
let crossProject = false;
|
|
4105
|
-
if (input.projectName) {
|
|
4587
|
+
const lockFile = spawnLockPath(sessionName);
|
|
4588
|
+
if (existsSync12(lockFile)) {
|
|
4106
4589
|
try {
|
|
4107
|
-
const
|
|
4108
|
-
const
|
|
4109
|
-
if (
|
|
4110
|
-
|
|
4590
|
+
const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
|
|
4591
|
+
const age = Date.now() - lock.timestamp;
|
|
4592
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
4593
|
+
return false;
|
|
4111
4594
|
}
|
|
4112
4595
|
} catch {
|
|
4113
4596
|
}
|
|
4114
4597
|
}
|
|
4598
|
+
writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
4599
|
+
return true;
|
|
4600
|
+
}
|
|
4601
|
+
function releaseSpawnLock(sessionName) {
|
|
4115
4602
|
try {
|
|
4116
|
-
|
|
4117
|
-
const exeSession = resolveExeSession();
|
|
4118
|
-
if (!exeSession) return { dispatched: "session_missing" };
|
|
4119
|
-
const sessionName = employeeSessionName(input.assignedTo, exeSession);
|
|
4120
|
-
if (transport.isAlive(sessionName)) {
|
|
4121
|
-
const result = sendIntercom(sessionName);
|
|
4122
|
-
const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
|
|
4123
|
-
return { dispatched, session: sessionName, crossProject };
|
|
4124
|
-
} else {
|
|
4125
|
-
const projectDir = input.projectDir ?? process.cwd();
|
|
4126
|
-
const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
|
|
4127
|
-
autoInstance: isMultiInstance(input.assignedTo)
|
|
4128
|
-
});
|
|
4129
|
-
if (result.status === "failed") {
|
|
4130
|
-
process.stderr.write(
|
|
4131
|
-
`[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
|
|
4132
|
-
`
|
|
4133
|
-
);
|
|
4134
|
-
return { dispatched: "session_missing" };
|
|
4135
|
-
}
|
|
4136
|
-
return { dispatched: "spawned", session: result.sessionName, crossProject };
|
|
4137
|
-
}
|
|
4603
|
+
unlinkSync5(spawnLockPath(sessionName));
|
|
4138
4604
|
} catch {
|
|
4139
|
-
return { dispatched: "session_missing" };
|
|
4140
4605
|
}
|
|
4141
4606
|
}
|
|
4142
|
-
function
|
|
4607
|
+
function resolveBehaviorsExporterScript() {
|
|
4143
4608
|
try {
|
|
4144
|
-
const
|
|
4145
|
-
|
|
4609
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
4610
|
+
const scriptPath = path16.join(
|
|
4611
|
+
path16.dirname(thisFile),
|
|
4612
|
+
"..",
|
|
4613
|
+
"bin",
|
|
4614
|
+
"exe-export-behaviors.js"
|
|
4615
|
+
);
|
|
4616
|
+
return existsSync12(scriptPath) ? scriptPath : null;
|
|
4146
4617
|
} catch {
|
|
4618
|
+
return null;
|
|
4147
4619
|
}
|
|
4148
4620
|
}
|
|
4149
|
-
|
|
4621
|
+
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
4622
|
+
const script = resolveBehaviorsExporterScript();
|
|
4623
|
+
if (!script) return null;
|
|
4150
4624
|
try {
|
|
4151
|
-
|
|
4152
|
-
|
|
4625
|
+
const output = execFileSync2(
|
|
4626
|
+
process.execPath,
|
|
4627
|
+
[script, agentId, projectName, sessionKey],
|
|
4628
|
+
{ encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
|
|
4629
|
+
).trim();
|
|
4630
|
+
return output.length > 0 ? output : null;
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
process.stderr.write(
|
|
4633
|
+
`[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
4634
|
+
`
|
|
4635
|
+
);
|
|
4636
|
+
return null;
|
|
4153
4637
|
}
|
|
4154
4638
|
}
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
"use strict";
|
|
4158
|
-
init_tmux_routing();
|
|
4159
|
-
init_session_key();
|
|
4160
|
-
init_notifications();
|
|
4161
|
-
init_transport();
|
|
4162
|
-
init_employees();
|
|
4163
|
-
}
|
|
4164
|
-
});
|
|
4165
|
-
|
|
4166
|
-
// src/lib/behaviors.ts
|
|
4167
|
-
import crypto4 from "crypto";
|
|
4168
|
-
async function storeBehavior(opts) {
|
|
4169
|
-
const client = getClient();
|
|
4170
|
-
const id = crypto4.randomUUID();
|
|
4171
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4172
|
-
await client.execute({
|
|
4173
|
-
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
4174
|
-
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
4175
|
-
args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
|
|
4176
|
-
});
|
|
4177
|
-
return id;
|
|
4639
|
+
function getMySession() {
|
|
4640
|
+
return getTransport().getMySession();
|
|
4178
4641
|
}
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
}
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
extractSkill: () => extractSkill,
|
|
4193
|
-
extractTrajectory: () => extractTrajectory,
|
|
4194
|
-
findSimilarTrajectories: () => findSimilarTrajectories,
|
|
4195
|
-
hashSignature: () => hashSignature,
|
|
4196
|
-
storeTrajectory: () => storeTrajectory,
|
|
4197
|
-
sweepTrajectories: () => sweepTrajectories
|
|
4198
|
-
});
|
|
4199
|
-
import crypto5 from "crypto";
|
|
4200
|
-
async function extractTrajectory(taskId, agentId) {
|
|
4201
|
-
const client = getClient();
|
|
4202
|
-
const result = await client.execute({
|
|
4203
|
-
sql: `SELECT tool_name, raw_text
|
|
4204
|
-
FROM memories
|
|
4205
|
-
WHERE task_id = ? AND agent_id = ?
|
|
4206
|
-
ORDER BY timestamp ASC`,
|
|
4207
|
-
args: [taskId, agentId]
|
|
4208
|
-
});
|
|
4209
|
-
if (result.rows.length === 0) return [];
|
|
4210
|
-
const rawTools = result.rows.map((r) => {
|
|
4211
|
-
const toolName = String(r.tool_name);
|
|
4212
|
-
if (toolName === "Bash") {
|
|
4213
|
-
const text = String(r.raw_text);
|
|
4214
|
-
const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
|
|
4215
|
-
return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
|
|
4216
|
-
}
|
|
4217
|
-
return toolName;
|
|
4218
|
-
});
|
|
4219
|
-
const signature = [];
|
|
4220
|
-
for (const tool of rawTools) {
|
|
4221
|
-
if (signature.length === 0 || signature[signature.length - 1] !== tool) {
|
|
4222
|
-
signature.push(tool);
|
|
4642
|
+
function employeeSessionName(employee, exeSession, instance) {
|
|
4643
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
4644
|
+
const root = extractRootExe(exeSession);
|
|
4645
|
+
if (root) {
|
|
4646
|
+
process.stderr.write(
|
|
4647
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
|
|
4648
|
+
`
|
|
4649
|
+
);
|
|
4650
|
+
exeSession = root;
|
|
4651
|
+
} else {
|
|
4652
|
+
throw new Error(
|
|
4653
|
+
`Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
|
|
4654
|
+
);
|
|
4223
4655
|
}
|
|
4224
4656
|
}
|
|
4225
|
-
|
|
4657
|
+
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
4658
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
4659
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
4660
|
+
throw new Error(
|
|
4661
|
+
`Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
|
|
4662
|
+
);
|
|
4663
|
+
}
|
|
4664
|
+
return name;
|
|
4226
4665
|
}
|
|
4227
|
-
function
|
|
4228
|
-
|
|
4666
|
+
function parseParentExe(sessionName, agentId) {
|
|
4667
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4668
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
4669
|
+
const match = sessionName.match(regex);
|
|
4670
|
+
return match?.[1] ?? null;
|
|
4229
4671
|
}
|
|
4230
|
-
|
|
4231
|
-
const
|
|
4232
|
-
|
|
4233
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4234
|
-
const signatureHash = hashSignature(opts.signature);
|
|
4235
|
-
await client.execute({
|
|
4236
|
-
sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
|
|
4237
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4238
|
-
args: [
|
|
4239
|
-
id,
|
|
4240
|
-
opts.taskId,
|
|
4241
|
-
opts.agentId,
|
|
4242
|
-
opts.projectName,
|
|
4243
|
-
opts.taskTitle,
|
|
4244
|
-
JSON.stringify(opts.signature),
|
|
4245
|
-
signatureHash,
|
|
4246
|
-
opts.signature.length,
|
|
4247
|
-
now
|
|
4248
|
-
]
|
|
4249
|
-
});
|
|
4250
|
-
return id;
|
|
4672
|
+
function extractRootExe(name) {
|
|
4673
|
+
const match = name.match(/(exe\d+)$/);
|
|
4674
|
+
return match?.[1] ?? null;
|
|
4251
4675
|
}
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
const result = await client.execute({
|
|
4256
|
-
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
4257
|
-
FROM trajectories
|
|
4258
|
-
WHERE signature_hash = ?
|
|
4259
|
-
ORDER BY created_at DESC
|
|
4260
|
-
LIMIT 20`,
|
|
4261
|
-
args: [hash]
|
|
4262
|
-
});
|
|
4263
|
-
const mapRow = (r) => ({
|
|
4264
|
-
id: String(r.id),
|
|
4265
|
-
taskId: String(r.task_id),
|
|
4266
|
-
agentId: String(r.agent_id),
|
|
4267
|
-
projectName: String(r.project_name),
|
|
4268
|
-
taskTitle: String(r.task_title),
|
|
4269
|
-
signature: JSON.parse(String(r.signature)),
|
|
4270
|
-
signatureHash: String(r.signature_hash),
|
|
4271
|
-
toolCount: Number(r.tool_count),
|
|
4272
|
-
skillId: r.skill_id ? String(r.skill_id) : null,
|
|
4273
|
-
createdAt: String(r.created_at)
|
|
4274
|
-
});
|
|
4275
|
-
const matches = result.rows.map(mapRow);
|
|
4276
|
-
if (matches.length >= threshold) return matches;
|
|
4277
|
-
const nearResult = await client.execute({
|
|
4278
|
-
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
4279
|
-
FROM trajectories
|
|
4280
|
-
WHERE tool_count BETWEEN ? AND ?
|
|
4281
|
-
AND signature_hash != ?
|
|
4282
|
-
ORDER BY created_at DESC
|
|
4283
|
-
LIMIT 50`,
|
|
4284
|
-
args: [
|
|
4285
|
-
Math.max(1, signature.length - 3),
|
|
4286
|
-
signature.length + 3,
|
|
4287
|
-
hash
|
|
4288
|
-
]
|
|
4289
|
-
});
|
|
4290
|
-
for (const r of nearResult.rows) {
|
|
4291
|
-
const candidateSig = JSON.parse(String(r.signature));
|
|
4292
|
-
if (editDistance(signature, candidateSig) <= 2) {
|
|
4293
|
-
matches.push(mapRow(r));
|
|
4294
|
-
}
|
|
4676
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
4677
|
+
if (!existsSync12(SESSION_CACHE)) {
|
|
4678
|
+
mkdirSync7(SESSION_CACHE, { recursive: true });
|
|
4295
4679
|
}
|
|
4296
|
-
|
|
4680
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
4681
|
+
const filePath = path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
4682
|
+
writeFileSync6(filePath, JSON.stringify({
|
|
4683
|
+
parentExe: rootExe,
|
|
4684
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
4685
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4686
|
+
}));
|
|
4297
4687
|
}
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
return
|
|
4688
|
+
function getParentExe(sessionKey) {
|
|
4689
|
+
try {
|
|
4690
|
+
const data = JSON.parse(readFileSync10(path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
4691
|
+
return data.parentExe || null;
|
|
4692
|
+
} catch {
|
|
4693
|
+
return null;
|
|
4302
4694
|
}
|
|
4303
|
-
const trajectoryId = await storeTrajectory({
|
|
4304
|
-
taskId: opts.taskId,
|
|
4305
|
-
agentId: opts.agentId,
|
|
4306
|
-
projectName: opts.projectName,
|
|
4307
|
-
taskTitle: opts.taskTitle,
|
|
4308
|
-
signature
|
|
4309
|
-
});
|
|
4310
|
-
const similar = await findSimilarTrajectories(
|
|
4311
|
-
signature,
|
|
4312
|
-
opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
|
|
4313
|
-
);
|
|
4314
|
-
return { trajectoryId, similarCount: similar.length, similar };
|
|
4315
|
-
}
|
|
4316
|
-
function buildExtractionPrompt(trajectories) {
|
|
4317
|
-
const items = trajectories.map((t, i) => {
|
|
4318
|
-
const sig = t.signature.join(" \u2192 ");
|
|
4319
|
-
return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
|
|
4320
|
-
Signature: ${sig}`;
|
|
4321
|
-
}).join("\n\n");
|
|
4322
|
-
return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
|
|
4323
|
-
|
|
4324
|
-
${items}
|
|
4325
|
-
|
|
4326
|
-
Extract the reusable procedure. Format your response EXACTLY like this:
|
|
4327
|
-
|
|
4328
|
-
SKILL: {name \u2014 short, descriptive}
|
|
4329
|
-
TRIGGER: {when to use this \u2014 one sentence}
|
|
4330
|
-
STEPS:
|
|
4331
|
-
1. ...
|
|
4332
|
-
2. ...
|
|
4333
|
-
PITFALLS: {common mistakes to avoid}
|
|
4334
|
-
|
|
4335
|
-
Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
|
|
4336
4695
|
}
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
max_tokens: 500,
|
|
4347
|
-
messages: [{ role: "user", content: prompt }]
|
|
4348
|
-
});
|
|
4349
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
4350
|
-
const skillText = textBlock?.text;
|
|
4351
|
-
if (!skillText) return null;
|
|
4352
|
-
const agentId = trajectories[0].agentId;
|
|
4353
|
-
const projectName = trajectories[0].projectName;
|
|
4354
|
-
const skillId = await storeBehavior({
|
|
4355
|
-
agentId,
|
|
4356
|
-
content: skillText,
|
|
4357
|
-
domain: "skill",
|
|
4358
|
-
projectName
|
|
4359
|
-
});
|
|
4360
|
-
const dbClient = getClient();
|
|
4361
|
-
for (const t of trajectories) {
|
|
4362
|
-
await dbClient.execute({
|
|
4363
|
-
sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
|
|
4364
|
-
args: [skillId, t.id]
|
|
4365
|
-
});
|
|
4696
|
+
function getDispatchedBy(sessionKey) {
|
|
4697
|
+
try {
|
|
4698
|
+
const data = JSON.parse(readFileSync10(
|
|
4699
|
+
path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
4700
|
+
"utf8"
|
|
4701
|
+
));
|
|
4702
|
+
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
4703
|
+
} catch {
|
|
4704
|
+
return null;
|
|
4366
4705
|
}
|
|
4367
|
-
process.stderr.write(
|
|
4368
|
-
`[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
|
|
4369
|
-
`
|
|
4370
|
-
);
|
|
4371
|
-
return skillId;
|
|
4372
4706
|
}
|
|
4373
|
-
|
|
4707
|
+
function resolveExeSession() {
|
|
4708
|
+
const mySession = getMySession();
|
|
4709
|
+
if (!mySession) return null;
|
|
4374
4710
|
try {
|
|
4375
|
-
const
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
skillThreshold: config.skillThreshold
|
|
4380
|
-
});
|
|
4381
|
-
if (!trajectoryId) return;
|
|
4382
|
-
if (similarCount >= config.skillThreshold) {
|
|
4383
|
-
const unprocessed = similar.filter((t) => !t.skillId);
|
|
4384
|
-
if (unprocessed.length >= config.skillThreshold) {
|
|
4385
|
-
extractSkill(unprocessed, config.skillModel).catch((err) => {
|
|
4386
|
-
process.stderr.write(
|
|
4387
|
-
`[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
|
|
4388
|
-
`
|
|
4389
|
-
);
|
|
4390
|
-
});
|
|
4391
|
-
}
|
|
4711
|
+
const key = getSessionKey();
|
|
4712
|
+
const parentExe = getParentExe(key);
|
|
4713
|
+
if (parentExe) {
|
|
4714
|
+
return extractRootExe(parentExe) ?? parentExe;
|
|
4392
4715
|
}
|
|
4393
|
-
} catch
|
|
4394
|
-
process.stderr.write(
|
|
4395
|
-
`[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
|
|
4396
|
-
`
|
|
4397
|
-
);
|
|
4716
|
+
} catch {
|
|
4398
4717
|
}
|
|
4718
|
+
return extractRootExe(mySession) ?? mySession;
|
|
4399
4719
|
}
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
const
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4720
|
+
function isEmployeeAlive(sessionName) {
|
|
4721
|
+
return getTransport().isAlive(sessionName);
|
|
4722
|
+
}
|
|
4723
|
+
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
4724
|
+
const base = employeeSessionName(employeeName, exeSession);
|
|
4725
|
+
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
4726
|
+
for (let i = 2; i <= maxInstances; i++) {
|
|
4727
|
+
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
4728
|
+
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
4729
|
+
}
|
|
4730
|
+
return null;
|
|
4731
|
+
}
|
|
4732
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
4733
|
+
const transport = getTransport();
|
|
4734
|
+
if (!transport.isAlive(sessionName)) {
|
|
4735
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
4736
|
+
}
|
|
4737
|
+
let pane;
|
|
4738
|
+
try {
|
|
4739
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
4740
|
+
} catch (err) {
|
|
4741
|
+
return {
|
|
4742
|
+
atCapacity: false,
|
|
4743
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
4747
|
+
if (!isAtCapacity2(pane)) {
|
|
4748
|
+
return {
|
|
4749
|
+
atCapacity: false,
|
|
4750
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4753
|
+
return {
|
|
4754
|
+
atCapacity: true,
|
|
4755
|
+
reason: "capacity banner matched in recent pane output"
|
|
4756
|
+
};
|
|
4757
|
+
}
|
|
4758
|
+
function readDebounceState() {
|
|
4759
|
+
try {
|
|
4760
|
+
if (!existsSync12(DEBOUNCE_FILE)) return {};
|
|
4761
|
+
return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
|
|
4762
|
+
} catch {
|
|
4763
|
+
return {};
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
function writeDebounceState(state) {
|
|
4767
|
+
try {
|
|
4768
|
+
if (!existsSync12(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
|
|
4769
|
+
writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
|
|
4770
|
+
} catch {
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
function isDebounced(targetSession) {
|
|
4774
|
+
const state = readDebounceState();
|
|
4775
|
+
const lastSent = state[targetSession] ?? 0;
|
|
4776
|
+
return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
|
|
4777
|
+
}
|
|
4778
|
+
function recordDebounce(targetSession) {
|
|
4779
|
+
const state = readDebounceState();
|
|
4780
|
+
state[targetSession] = Date.now();
|
|
4781
|
+
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
4782
|
+
for (const key of Object.keys(state)) {
|
|
4783
|
+
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
4784
|
+
}
|
|
4785
|
+
writeDebounceState(state);
|
|
4786
|
+
}
|
|
4787
|
+
function logIntercom(msg) {
|
|
4788
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
4789
|
+
`;
|
|
4790
|
+
process.stderr.write(`[intercom] ${msg}
|
|
4791
|
+
`);
|
|
4792
|
+
try {
|
|
4793
|
+
appendFileSync(INTERCOM_LOG2, line);
|
|
4794
|
+
} catch {
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
function getSessionState(sessionName) {
|
|
4798
|
+
const transport = getTransport();
|
|
4799
|
+
if (!transport.isAlive(sessionName)) return "offline";
|
|
4800
|
+
try {
|
|
4801
|
+
const pane = transport.capturePane(sessionName, 5);
|
|
4802
|
+
if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
|
|
4803
|
+
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
4804
|
+
return "no_claude";
|
|
4805
|
+
}
|
|
4443
4806
|
}
|
|
4807
|
+
if (/Running…/.test(pane)) return "tool";
|
|
4808
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
4809
|
+
return "idle";
|
|
4810
|
+
} catch {
|
|
4811
|
+
return "offline";
|
|
4444
4812
|
}
|
|
4445
|
-
return { clustersProcessed, skillsExtracted };
|
|
4446
4813
|
}
|
|
4447
|
-
function
|
|
4448
|
-
const
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4814
|
+
function isSessionBusy(sessionName) {
|
|
4815
|
+
const state = getSessionState(sessionName);
|
|
4816
|
+
return state === "thinking" || state === "tool";
|
|
4817
|
+
}
|
|
4818
|
+
function isExeSession(sessionName) {
|
|
4819
|
+
return /^exe\d*$/.test(sessionName);
|
|
4820
|
+
}
|
|
4821
|
+
function sendIntercom(targetSession) {
|
|
4822
|
+
const transport = getTransport();
|
|
4823
|
+
if (isExeSession(targetSession)) {
|
|
4824
|
+
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
4825
|
+
return "skipped_exe";
|
|
4826
|
+
}
|
|
4827
|
+
if (isDebounced(targetSession)) {
|
|
4828
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
|
|
4829
|
+
return "debounced";
|
|
4830
|
+
}
|
|
4831
|
+
try {
|
|
4832
|
+
const sessions = transport.listSessions();
|
|
4833
|
+
if (!sessions.includes(targetSession)) {
|
|
4834
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
4835
|
+
return "failed";
|
|
4836
|
+
}
|
|
4837
|
+
const sessionState = getSessionState(targetSession);
|
|
4838
|
+
if (sessionState === "no_claude") {
|
|
4839
|
+
queueIntercom(targetSession, "claude not running in session");
|
|
4840
|
+
recordDebounce(targetSession);
|
|
4841
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
4842
|
+
return "queued";
|
|
4843
|
+
}
|
|
4844
|
+
if (sessionState === "thinking" || sessionState === "tool") {
|
|
4845
|
+
queueIntercom(targetSession, "session busy at send time");
|
|
4846
|
+
recordDebounce(targetSession);
|
|
4847
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
4848
|
+
return "queued";
|
|
4849
|
+
}
|
|
4850
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
4851
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
4852
|
+
transport.sendKeys(targetSession, "q");
|
|
4461
4853
|
}
|
|
4854
|
+
transport.sendKeys(targetSession, "/exe-intercom");
|
|
4855
|
+
recordDebounce(targetSession);
|
|
4856
|
+
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
4857
|
+
return "delivered";
|
|
4858
|
+
} catch {
|
|
4859
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
4860
|
+
return "failed";
|
|
4462
4861
|
}
|
|
4463
|
-
return dp[m][n];
|
|
4464
4862
|
}
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
init_config();
|
|
4472
|
-
DEFAULT_SKILL_THRESHOLD = 3;
|
|
4863
|
+
function notifyParentExe(sessionKey) {
|
|
4864
|
+
const target = getDispatchedBy(sessionKey);
|
|
4865
|
+
if (!target) {
|
|
4866
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
4867
|
+
`);
|
|
4868
|
+
return false;
|
|
4473
4869
|
}
|
|
4474
|
-
}
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4870
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
4871
|
+
`);
|
|
4872
|
+
const result = sendIntercom(target);
|
|
4873
|
+
if (result === "failed") {
|
|
4874
|
+
const rootExe = resolveExeSession();
|
|
4875
|
+
if (rootExe && rootExe !== target) {
|
|
4876
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
4877
|
+
`);
|
|
4878
|
+
const fallback = sendIntercom(rootExe);
|
|
4879
|
+
return fallback !== "failed";
|
|
4880
|
+
}
|
|
4881
|
+
return false;
|
|
4882
|
+
}
|
|
4883
|
+
return true;
|
|
4884
|
+
}
|
|
4885
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
4886
|
+
if (employeeName === "exe") {
|
|
4887
|
+
return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
|
|
4888
|
+
}
|
|
4889
|
+
try {
|
|
4890
|
+
assertEmployeeLimitSync();
|
|
4891
|
+
} catch (err) {
|
|
4892
|
+
if (err instanceof PlanLimitError) {
|
|
4893
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
if (/-exe\d*$/.test(employeeName)) {
|
|
4897
|
+
const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
|
|
4898
|
+
return {
|
|
4899
|
+
status: "failed",
|
|
4900
|
+
sessionName: "",
|
|
4901
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
4902
|
+
};
|
|
4903
|
+
}
|
|
4904
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
4905
|
+
const root = extractRootExe(exeSession);
|
|
4906
|
+
if (root) {
|
|
4907
|
+
process.stderr.write(
|
|
4908
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
|
|
4909
|
+
`
|
|
4910
|
+
);
|
|
4911
|
+
exeSession = root;
|
|
4912
|
+
} else {
|
|
4913
|
+
return {
|
|
4914
|
+
status: "failed",
|
|
4915
|
+
sessionName: "",
|
|
4916
|
+
error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
|
|
4917
|
+
};
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
let effectiveInstance = opts?.instance;
|
|
4921
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
4922
|
+
const free = findFreeInstance(
|
|
4923
|
+
employeeName,
|
|
4924
|
+
exeSession,
|
|
4925
|
+
opts.maxAutoInstances ?? 10
|
|
4926
|
+
);
|
|
4927
|
+
if (free === null) {
|
|
4928
|
+
return {
|
|
4929
|
+
status: "failed",
|
|
4930
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
4931
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
4932
|
+
};
|
|
4933
|
+
}
|
|
4934
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
4935
|
+
}
|
|
4936
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
4937
|
+
if (isEmployeeAlive(sessionName)) {
|
|
4938
|
+
const result2 = sendIntercom(sessionName);
|
|
4939
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
4940
|
+
return { status: "intercom_sent", sessionName };
|
|
4941
|
+
}
|
|
4942
|
+
if (result2 === "delivered") {
|
|
4943
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
4944
|
+
}
|
|
4945
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
4946
|
+
}
|
|
4947
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
4948
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
4949
|
+
if (result.error) {
|
|
4950
|
+
return { status: "failed", sessionName, error: result.error };
|
|
4510
4951
|
}
|
|
4511
|
-
return
|
|
4952
|
+
return { status: "spawned", sessionName };
|
|
4512
4953
|
}
|
|
4513
|
-
|
|
4514
|
-
const
|
|
4954
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
4955
|
+
const transport = getTransport();
|
|
4956
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
4957
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
4958
|
+
const logDir = path16.join(os6.homedir(), ".exe-os", "session-logs");
|
|
4959
|
+
const logFile = path16.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
4960
|
+
if (!existsSync12(logDir)) {
|
|
4961
|
+
mkdirSync7(logDir, { recursive: true });
|
|
4962
|
+
}
|
|
4963
|
+
transport.kill(sessionName);
|
|
4964
|
+
let cleanupSuffix = "";
|
|
4515
4965
|
try {
|
|
4516
|
-
const
|
|
4517
|
-
const
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
mkdirSync7(cacheDir, { recursive: true });
|
|
4521
|
-
writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
4522
|
-
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
4523
|
-
try {
|
|
4524
|
-
unlinkSync5(cachePath);
|
|
4525
|
-
} catch {
|
|
4526
|
-
}
|
|
4966
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
4967
|
+
const cleanupScript = path16.join(path16.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
4968
|
+
if (existsSync12(cleanupScript)) {
|
|
4969
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
4527
4970
|
}
|
|
4528
4971
|
} catch {
|
|
4529
4972
|
}
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
if (input.status === "done" || input.status === "cancelled") {
|
|
4973
|
+
try {
|
|
4974
|
+
const claudeJsonPath = path16.join(os6.homedir(), ".claude.json");
|
|
4975
|
+
let claudeJson = {};
|
|
4534
4976
|
try {
|
|
4535
|
-
|
|
4536
|
-
const taskTitle = String(row.title);
|
|
4537
|
-
const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
4538
|
-
await client.execute({
|
|
4539
|
-
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
4540
|
-
WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
|
|
4541
|
-
args: [now, `%left '${escaped}' as in\\_progress%`]
|
|
4542
|
-
});
|
|
4977
|
+
claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
|
|
4543
4978
|
} catch {
|
|
4544
4979
|
}
|
|
4980
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
4981
|
+
const projects = claudeJson.projects;
|
|
4982
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
4983
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
4984
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
4985
|
+
writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
4986
|
+
} catch {
|
|
4987
|
+
}
|
|
4988
|
+
try {
|
|
4989
|
+
const settingsDir = path16.join(os6.homedir(), ".claude", "projects");
|
|
4990
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
4991
|
+
const projSettingsDir = path16.join(settingsDir, normalizedKey);
|
|
4992
|
+
const settingsPath = path16.join(projSettingsDir, "settings.json");
|
|
4993
|
+
let settings = {};
|
|
4545
4994
|
try {
|
|
4546
|
-
|
|
4547
|
-
const cascaded = await client.execute({
|
|
4548
|
-
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
4549
|
-
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
4550
|
-
args: [now, taskId]
|
|
4551
|
-
});
|
|
4552
|
-
if (cascaded.rowsAffected > 0) {
|
|
4553
|
-
process.stderr.write(
|
|
4554
|
-
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
4555
|
-
`
|
|
4556
|
-
);
|
|
4557
|
-
}
|
|
4995
|
+
settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
|
|
4558
4996
|
} catch {
|
|
4559
4997
|
}
|
|
4998
|
+
const perms = settings.permissions ?? {};
|
|
4999
|
+
const allow = perms.allow ?? [];
|
|
5000
|
+
const toolNames = [
|
|
5001
|
+
"recall_my_memory",
|
|
5002
|
+
"store_memory",
|
|
5003
|
+
"create_task",
|
|
5004
|
+
"update_task",
|
|
5005
|
+
"list_tasks",
|
|
5006
|
+
"get_task",
|
|
5007
|
+
"ask_team_memory",
|
|
5008
|
+
"store_behavior",
|
|
5009
|
+
"get_identity",
|
|
5010
|
+
"send_message"
|
|
5011
|
+
];
|
|
5012
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
5013
|
+
let changed = false;
|
|
5014
|
+
for (const tool of requiredTools) {
|
|
5015
|
+
if (!allow.includes(tool)) {
|
|
5016
|
+
allow.push(tool);
|
|
5017
|
+
changed = true;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
if (changed) {
|
|
5021
|
+
perms.allow = allow;
|
|
5022
|
+
settings.permissions = perms;
|
|
5023
|
+
mkdirSync7(projSettingsDir, { recursive: true });
|
|
5024
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
5025
|
+
}
|
|
5026
|
+
} catch {
|
|
4560
5027
|
}
|
|
4561
|
-
const
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
5028
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
5029
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
5030
|
+
const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
5031
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
5032
|
+
let identityFlag = "";
|
|
5033
|
+
let behaviorsFlag = "";
|
|
5034
|
+
let legacyFallbackWarned = false;
|
|
5035
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
5036
|
+
const identityPath = path16.join(
|
|
5037
|
+
os6.homedir(),
|
|
5038
|
+
".exe-os",
|
|
5039
|
+
"identity",
|
|
5040
|
+
`${employeeName}.md`
|
|
5041
|
+
);
|
|
5042
|
+
_resetCcAgentSupportCache();
|
|
5043
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
5044
|
+
if (hasAgentFlag) {
|
|
5045
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
5046
|
+
} else if (existsSync12(identityPath)) {
|
|
5047
|
+
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
5048
|
+
legacyFallbackWarned = true;
|
|
4566
5049
|
}
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
5050
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
5051
|
+
employeeName,
|
|
5052
|
+
path16.basename(spawnCwd),
|
|
5053
|
+
sessionName
|
|
5054
|
+
);
|
|
5055
|
+
if (behaviorsFile) {
|
|
5056
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
if (legacyFallbackWarned) {
|
|
5060
|
+
process.stderr.write(
|
|
5061
|
+
`[tmux-routing] claude --agent not supported by installed CC. Falling back to --append-system-prompt-file for ${employeeName}. Upgrade Claude Code to enable native --agent launch.
|
|
5062
|
+
`
|
|
5063
|
+
);
|
|
5064
|
+
}
|
|
5065
|
+
let sessionContextFlag = "";
|
|
5066
|
+
try {
|
|
5067
|
+
const ctxDir = path16.join(os6.homedir(), ".exe-os", "session-cache");
|
|
5068
|
+
mkdirSync7(ctxDir, { recursive: true });
|
|
5069
|
+
const ctxFile = path16.join(ctxDir, `session-context-${sessionName}.md`);
|
|
5070
|
+
const ctxContent = [
|
|
5071
|
+
`## Session Context`,
|
|
5072
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
5073
|
+
`Your parent exe session is ${exeSession}.`,
|
|
5074
|
+
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
5075
|
+
].join("\n");
|
|
5076
|
+
writeFileSync6(ctxFile, ctxContent);
|
|
5077
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
5078
|
+
} catch {
|
|
5079
|
+
}
|
|
5080
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
5081
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
5082
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
5083
|
+
if (cfg?.apiKeyEnv) {
|
|
5084
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
5085
|
+
if (keyVal) {
|
|
5086
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
4572
5087
|
}
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
let spawnCommand;
|
|
5091
|
+
if (useExeAgent) {
|
|
5092
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
5093
|
+
} else if (useBinSymlink) {
|
|
5094
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
5095
|
+
process.stderr.write(
|
|
5096
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
5097
|
+
`
|
|
5098
|
+
);
|
|
5099
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
5100
|
+
} else {
|
|
5101
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
5102
|
+
}
|
|
5103
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
5104
|
+
cwd: spawnCwd,
|
|
5105
|
+
command: spawnCommand
|
|
5106
|
+
});
|
|
5107
|
+
if (spawnResult.error) {
|
|
5108
|
+
releaseSpawnLock(sessionName);
|
|
5109
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
5110
|
+
}
|
|
5111
|
+
transport.pipeLog(sessionName, logFile);
|
|
5112
|
+
try {
|
|
5113
|
+
const mySession = getMySession();
|
|
5114
|
+
const dispatchInfo = path16.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
5115
|
+
writeFileSync6(dispatchInfo, JSON.stringify({
|
|
5116
|
+
dispatchedBy: mySession,
|
|
5117
|
+
rootExe: exeSession,
|
|
5118
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
5119
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5120
|
+
}));
|
|
5121
|
+
} catch {
|
|
5122
|
+
}
|
|
5123
|
+
let booted = false;
|
|
5124
|
+
for (let i = 0; i < 30; i++) {
|
|
5125
|
+
try {
|
|
5126
|
+
execSync8("sleep 0.5");
|
|
5127
|
+
} catch {
|
|
5128
|
+
}
|
|
5129
|
+
try {
|
|
5130
|
+
const pane = transport.capturePane(sessionName);
|
|
5131
|
+
if (useExeAgent) {
|
|
5132
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
5133
|
+
booted = true;
|
|
5134
|
+
break;
|
|
5135
|
+
}
|
|
5136
|
+
} else {
|
|
5137
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
5138
|
+
booted = true;
|
|
5139
|
+
break;
|
|
4577
5140
|
}
|
|
4578
5141
|
}
|
|
5142
|
+
} catch {
|
|
4579
5143
|
}
|
|
4580
5144
|
}
|
|
4581
|
-
if (
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
taskId,
|
|
4585
|
-
agentId: String(row.assigned_to),
|
|
4586
|
-
projectName: String(row.project_name),
|
|
4587
|
-
taskTitle: String(row.title)
|
|
4588
|
-
})
|
|
4589
|
-
).catch((err) => {
|
|
4590
|
-
process.stderr.write(
|
|
4591
|
-
`[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
|
|
4592
|
-
`
|
|
4593
|
-
);
|
|
4594
|
-
});
|
|
5145
|
+
if (!booted) {
|
|
5146
|
+
releaseSpawnLock(sessionName);
|
|
5147
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
4595
5148
|
}
|
|
4596
|
-
|
|
4597
|
-
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
5149
|
+
if (!useExeAgent) {
|
|
4598
5150
|
try {
|
|
4599
|
-
|
|
5151
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
4600
5152
|
} catch {
|
|
4601
5153
|
}
|
|
4602
5154
|
}
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
status: input.status,
|
|
4611
|
-
taskFile,
|
|
4612
|
-
createdAt: String(row.created_at),
|
|
4613
|
-
updatedAt: now,
|
|
4614
|
-
budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
|
|
4615
|
-
budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
|
|
4616
|
-
tokensUsed: Number(row.tokens_used ?? 0),
|
|
4617
|
-
tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
|
|
4618
|
-
nextTask
|
|
4619
|
-
};
|
|
4620
|
-
}
|
|
4621
|
-
async function deleteTask(taskId, baseDir) {
|
|
4622
|
-
const client = getClient();
|
|
4623
|
-
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
4624
|
-
const reviewer = assignedBy || "exe";
|
|
4625
|
-
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
4626
|
-
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
4627
|
-
await client.execute({
|
|
4628
|
-
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
|
|
4629
|
-
args: [reviewFile, `exe/exe/${reviewSlug}.md`]
|
|
5155
|
+
registerSession({
|
|
5156
|
+
windowName: sessionName,
|
|
5157
|
+
agentId: employeeName,
|
|
5158
|
+
projectDir: spawnCwd,
|
|
5159
|
+
parentExe: exeSession,
|
|
5160
|
+
pid: 0,
|
|
5161
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4630
5162
|
});
|
|
4631
|
-
|
|
4632
|
-
|
|
5163
|
+
releaseSpawnLock(sessionName);
|
|
5164
|
+
return { sessionName };
|
|
4633
5165
|
}
|
|
4634
|
-
var
|
|
4635
|
-
|
|
5166
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
5167
|
+
var init_tmux_routing = __esm({
|
|
5168
|
+
"src/lib/tmux-routing.ts"() {
|
|
4636
5169
|
"use strict";
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
5170
|
+
init_session_registry();
|
|
5171
|
+
init_session_key();
|
|
5172
|
+
init_transport();
|
|
5173
|
+
init_cc_agent_support();
|
|
5174
|
+
init_mcp_prefix();
|
|
5175
|
+
init_provider_table();
|
|
5176
|
+
init_intercom_queue();
|
|
5177
|
+
init_plan_limits();
|
|
5178
|
+
SPAWN_LOCK_DIR = path16.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
5179
|
+
SESSION_CACHE = path16.join(os6.homedir(), ".exe-os", "session-cache");
|
|
5180
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
5181
|
+
VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
|
|
5182
|
+
VERIFY_PANE_LINES = 200;
|
|
5183
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
5184
|
+
INTERCOM_LOG2 = path16.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
5185
|
+
DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
|
|
5186
|
+
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
5187
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
4646
5188
|
}
|
|
4647
5189
|
});
|
|
4648
5190
|
|
|
4649
|
-
// src/lib/
|
|
4650
|
-
var
|
|
4651
|
-
__export(
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
recordSessionKill: () => recordSessionKill,
|
|
4660
|
-
sumTokensSavedSince: () => sumTokensSavedSince
|
|
5191
|
+
// src/lib/task-scanner.ts
|
|
5192
|
+
var task_scanner_exports = {};
|
|
5193
|
+
__export(task_scanner_exports, {
|
|
5194
|
+
PRIORITY_RE: () => PRIORITY_RE,
|
|
5195
|
+
STATUS_RE: () => STATUS_RE,
|
|
5196
|
+
TITLE_RE: () => TITLE_RE,
|
|
5197
|
+
formatJson: () => formatJson,
|
|
5198
|
+
formatMandatory: () => formatMandatory,
|
|
5199
|
+
formatText: () => formatText,
|
|
5200
|
+
scanAgentTasks: () => scanAgentTasks
|
|
4661
5201
|
});
|
|
4662
|
-
import
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
await client.execute({
|
|
4667
|
-
sql: `INSERT INTO session_kills
|
|
4668
|
-
(id, session_name, agent_id, killed_at, reason,
|
|
4669
|
-
ticks_idle, estimated_tokens_saved)
|
|
4670
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
4671
|
-
args: [
|
|
4672
|
-
crypto6.randomUUID(),
|
|
4673
|
-
input.sessionName,
|
|
4674
|
-
input.agentId,
|
|
4675
|
-
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4676
|
-
input.reason,
|
|
4677
|
-
input.ticksIdle ?? null,
|
|
4678
|
-
input.estimatedTokensSaved ?? null
|
|
4679
|
-
]
|
|
4680
|
-
});
|
|
4681
|
-
} catch (err) {
|
|
4682
|
-
process.stderr.write(
|
|
4683
|
-
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
4684
|
-
`
|
|
4685
|
-
);
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
4688
|
-
async function countKillsSince(sinceISO) {
|
|
5202
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync11, existsSync as existsSync13, statSync } from "fs";
|
|
5203
|
+
import { execSync as execSync9 } from "child_process";
|
|
5204
|
+
import path17 from "path";
|
|
5205
|
+
function getProjectRoot() {
|
|
4689
5206
|
try {
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
});
|
|
4695
|
-
const row = result.rows[0];
|
|
4696
|
-
return row ? Number(row.n) : 0;
|
|
5207
|
+
return execSync9("git rev-parse --show-toplevel", {
|
|
5208
|
+
encoding: "utf8",
|
|
5209
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5210
|
+
timeout: 5e3
|
|
5211
|
+
}).trim();
|
|
4697
5212
|
} catch {
|
|
4698
|
-
return
|
|
5213
|
+
return process.cwd();
|
|
4699
5214
|
}
|
|
4700
5215
|
}
|
|
4701
|
-
function
|
|
4702
|
-
|
|
5216
|
+
function scanAgentTasks(agentId) {
|
|
5217
|
+
const taskDir = path17.join(getProjectRoot(), "exe", agentId);
|
|
5218
|
+
const open = [];
|
|
5219
|
+
const inProgress = [];
|
|
5220
|
+
let done = 0;
|
|
5221
|
+
let total = 0;
|
|
5222
|
+
if (!existsSync13(taskDir)) return { open, inProgress, done, total };
|
|
4703
5223
|
try {
|
|
4704
|
-
const
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
5224
|
+
const files = readdirSync5(taskDir).filter((f) => f.endsWith(".md"));
|
|
5225
|
+
total = files.length;
|
|
5226
|
+
for (const f of files) {
|
|
5227
|
+
try {
|
|
5228
|
+
const content = readFileSync11(path17.join(taskDir, f), "utf8");
|
|
5229
|
+
const statusMatch = content.match(STATUS_RE);
|
|
5230
|
+
const status = statusMatch ? statusMatch[1].toLowerCase() : null;
|
|
5231
|
+
if (status === "done") {
|
|
5232
|
+
done++;
|
|
5233
|
+
continue;
|
|
5234
|
+
}
|
|
5235
|
+
if (status !== "open" && status !== "in_progress") continue;
|
|
5236
|
+
const priMatch = content.match(PRIORITY_RE);
|
|
5237
|
+
const titleMatch = content.match(TITLE_RE);
|
|
5238
|
+
const task = {
|
|
5239
|
+
file: f,
|
|
5240
|
+
title: titleMatch ? titleMatch[1] : f.replace(".md", ""),
|
|
5241
|
+
priority: priMatch ? priMatch[1] : "P2",
|
|
5242
|
+
status,
|
|
5243
|
+
slug: f.replace(".md", "")
|
|
5244
|
+
};
|
|
5245
|
+
if (status === "in_progress") {
|
|
5246
|
+
inProgress.push(task);
|
|
5247
|
+
} else {
|
|
5248
|
+
open.push(task);
|
|
5249
|
+
}
|
|
5250
|
+
} catch {
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
4709
5253
|
} catch {
|
|
4710
|
-
return { lastDate: null, streak: 0 };
|
|
4711
5254
|
}
|
|
5255
|
+
open.sort((a, b) => a.priority.localeCompare(b.priority));
|
|
5256
|
+
inProgress.sort((a, b) => a.priority.localeCompare(b.priority));
|
|
5257
|
+
return { open, inProgress, done, total };
|
|
4712
5258
|
}
|
|
4713
|
-
function
|
|
4714
|
-
|
|
4715
|
-
if (
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
};
|
|
5259
|
+
function formatText(agentId, result) {
|
|
5260
|
+
const lines = [];
|
|
5261
|
+
if (result.inProgress.length > 0) {
|
|
5262
|
+
lines.push(`IN_PROGRESS (${result.inProgress.length}):`);
|
|
5263
|
+
for (const t of result.inProgress) {
|
|
5264
|
+
lines.push(` [${t.priority}] ${t.title} \u2014 exe/${agentId}/${t.file}`);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
if (result.open.length > 0) {
|
|
5268
|
+
lines.push(`OPEN (${result.open.length}):`);
|
|
5269
|
+
for (const t of result.open) {
|
|
5270
|
+
lines.push(` [${t.priority}] ${t.title} \u2014 exe/${agentId}/${t.file}`);
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
lines.push(`DONE: ${result.done}`);
|
|
5274
|
+
return lines.join("\n");
|
|
4725
5275
|
}
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
5276
|
+
function formatMandatory(agentId, result) {
|
|
5277
|
+
const { open, inProgress } = result;
|
|
5278
|
+
if (open.length === 0 && inProgress.length === 0) return "";
|
|
5279
|
+
const lines = [];
|
|
5280
|
+
if (inProgress.length > 0) {
|
|
5281
|
+
const current = inProgress[0];
|
|
5282
|
+
let stale = false;
|
|
5283
|
+
try {
|
|
5284
|
+
const stat = statSync(path17.join(getProjectRoot(), "exe", agentId, current.file));
|
|
5285
|
+
const ageMin = (Date.now() - stat.mtimeMs) / 6e4;
|
|
5286
|
+
if (ageMin > 30) stale = true;
|
|
5287
|
+
} catch {
|
|
5288
|
+
}
|
|
5289
|
+
if (stale) {
|
|
5290
|
+
lines.push(`MANDATORY: Update task status for: ${current.title} [${current.priority}] (exe/${agentId}/${current.file})`);
|
|
5291
|
+
lines.push("This task has been in_progress for over 30 minutes without updates.");
|
|
5292
|
+
lines.push("If work is done, mark done. If blocked, update status to blocked.");
|
|
5293
|
+
} else {
|
|
5294
|
+
lines.push(`Continue working on: ${current.title} [${current.priority}] (exe/${agentId}/${current.file})`);
|
|
5295
|
+
}
|
|
5296
|
+
if (open.length > 0) {
|
|
5297
|
+
lines.push("Queued: " + open.map((t) => `${t.title} [${t.priority}]`).join(", "));
|
|
5298
|
+
}
|
|
5299
|
+
} else {
|
|
5300
|
+
const top = open[0];
|
|
5301
|
+
lines.push(`MANDATORY: You have ${open.length} unstarted task(s).`);
|
|
5302
|
+
lines.push(`Highest priority: ${top.title} [${top.priority}]`);
|
|
5303
|
+
lines.push(`File: exe/${agentId}/${top.file}`);
|
|
5304
|
+
lines.push("Read this task file and START WORKING NOW.");
|
|
4739
5305
|
}
|
|
5306
|
+
return lines.join("\n");
|
|
4740
5307
|
}
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
5308
|
+
function formatJson(result) {
|
|
5309
|
+
return JSON.stringify({
|
|
5310
|
+
open: result.open.map((t) => ({ file: t.file, title: t.title, priority: t.priority })),
|
|
5311
|
+
in_progress: result.inProgress.map((t) => ({ file: t.file, title: t.title, priority: t.priority })),
|
|
5312
|
+
done: result.done,
|
|
5313
|
+
total: result.total
|
|
5314
|
+
});
|
|
5315
|
+
}
|
|
5316
|
+
var STATUS_RE, PRIORITY_RE, TITLE_RE;
|
|
5317
|
+
var init_task_scanner = __esm({
|
|
5318
|
+
"src/lib/task-scanner.ts"() {
|
|
4744
5319
|
"use strict";
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
IDLE_KILL_SUSPECT_DAY_THRESHOLD = 3;
|
|
4749
|
-
IDLE_KILL_MIN_LIVE_SESSIONS = 5;
|
|
5320
|
+
STATUS_RE = /^\*\*Status:\*\*\s*(\w+)/m;
|
|
5321
|
+
PRIORITY_RE = /^\*\*Priority:\*\*\s*(\w+)/m;
|
|
5322
|
+
TITLE_RE = /^# (.+)/m;
|
|
4750
5323
|
}
|
|
4751
5324
|
});
|
|
4752
5325
|
|
|
@@ -4908,7 +5481,7 @@ async function fetchWithRetry(url, init) {
|
|
|
4908
5481
|
try {
|
|
4909
5482
|
const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
4910
5483
|
const resp = await fetch(url, { ...init, signal });
|
|
4911
|
-
if (resp.status >= 500 && attempt < MAX_RETRIES2) {
|
|
5484
|
+
if (resp && resp.status >= 500 && attempt < MAX_RETRIES2) {
|
|
4912
5485
|
await new Promise((r) => setTimeout(r, BASE_DELAY_MS2 * Math.pow(2, attempt)));
|
|
4913
5486
|
continue;
|
|
4914
5487
|
}
|
|
@@ -4952,6 +5525,10 @@ async function cloudPush(records, maxVersion, config) {
|
|
|
4952
5525
|
},
|
|
4953
5526
|
body: JSON.stringify({ version: maxVersion, blob })
|
|
4954
5527
|
});
|
|
5528
|
+
if (resp == null) {
|
|
5529
|
+
logError("[cloud-sync] PUSH FAILED: no response from server");
|
|
5530
|
+
return false;
|
|
5531
|
+
}
|
|
4955
5532
|
if (resp.status === 409) {
|
|
4956
5533
|
logError("[cloud-sync] PUSH VERSION CONFLICT \u2014 re-pull required before next push");
|
|
4957
5534
|
return false;
|
|
@@ -4974,6 +5551,10 @@ async function cloudPull(sinceVersion, config) {
|
|
|
4974
5551
|
},
|
|
4975
5552
|
body: JSON.stringify({ since_version: sinceVersion })
|
|
4976
5553
|
});
|
|
5554
|
+
if (response == null) {
|
|
5555
|
+
logError("[cloud-sync] PULL FAILED: no response from server");
|
|
5556
|
+
return { records: [], maxVersion: sinceVersion };
|
|
5557
|
+
}
|
|
4977
5558
|
if (!response.ok) return { records: [], maxVersion: sinceVersion };
|
|
4978
5559
|
const data = await response.json();
|
|
4979
5560
|
const allRecords = [];
|
|
@@ -5917,6 +6498,137 @@ import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync
|
|
|
5917
6498
|
import os7 from "os";
|
|
5918
6499
|
|
|
5919
6500
|
// src/lib/employee-templates.ts
|
|
6501
|
+
init_global_procedures();
|
|
6502
|
+
var BASE_OPERATING_PROCEDURES = `
|
|
6503
|
+
EXE OS \u2014 VISION AND NON-NEGOTIABLE PRINCIPLES (above all work):
|
|
6504
|
+
|
|
6505
|
+
Product: "Hire the team you couldn't afford." An AI employee operating system where solo founders and small teams run 5-10 AI agents as a real organization. Three-layer cognition (identity/expertise/experience). Five runtime modes (CC Raw \u2192 TUI \u2192 Desktop). Local-first with E2EE cloud sync.
|
|
6506
|
+
|
|
6507
|
+
ICP (who we build for):
|
|
6508
|
+
- Solopreneurs, SMB founders, creators with institutional IP
|
|
6509
|
+
- Bootstrapped small e-commerce / fitness creators / influencers
|
|
6510
|
+
- NOT VC-backed startups \u2014 intentionally excluded
|
|
6511
|
+
|
|
6512
|
+
Crown jewels (load-bearing for all three business paths \u2014 never compromise):
|
|
6513
|
+
- Memory sovereignty (user owns everything, E2EE, local-first)
|
|
6514
|
+
- Three-layer cognition (identity/expertise/experience)
|
|
6515
|
+
- MCP contract boundary (surfaces consume memory OS via MCP only \u2014 never direct DB access, never bundled code)
|
|
6516
|
+
- AGPL network boundary for public forks (e.g., exe-crm)
|
|
6517
|
+
|
|
6518
|
+
Three business-model paths (every product decision must serve these):
|
|
6519
|
+
1. B2C direct \u2014 solopreneurs run their own instance (active, current default)
|
|
6520
|
+
2. Agency white-label \u2014 distributors rebrand for their clients (deferred, but branding must be config-driven)
|
|
6521
|
+
3. Creator franchise (Mike pattern) \u2014 creators inject institutional IP into agent identity+expertise+experience layers, sell scoped access to subscribers (v2+ moat, requires memory export scoping)
|
|
6522
|
+
|
|
6523
|
+
Ethos:
|
|
6524
|
+
- Bootstrapped, profitable, forever. Not a VC-raise.
|
|
6525
|
+
- Founder zero-ego. Distributors and customers are the loudest voice.
|
|
6526
|
+
- Crypto values: big companies should not own consumer/SMB AI.
|
|
6527
|
+
|
|
6528
|
+
STOP AND REDIRECT: Any decision that compromises memory sovereignty, 3-layer cognition, MCP boundary, or AGPL boundary kills all three business paths. Surface the conflict to exe before proceeding.
|
|
6529
|
+
|
|
6530
|
+
Always reference .planning/ARCHITECTURE.md and .planning/PROJECT.md as source of truth for all architectural and product decisions.
|
|
6531
|
+
|
|
6532
|
+
OPERATING PROCEDURES (mandatory for all employees):
|
|
6533
|
+
|
|
6534
|
+
You report to the COO. All work flows through exe. These procedures are non-negotiable.
|
|
6535
|
+
|
|
6536
|
+
1. BEFORE starting work:
|
|
6537
|
+
- Read exe/ARCHITECTURE.md (if it exists). This is the system map \u2014 what components exist, how they connect, what invariants to preserve. Understand the architecture before changing anything.
|
|
6538
|
+
- Check YOUR task folder ONLY: Read exe/<your-name>/ for assigned tasks
|
|
6539
|
+
- NEVER read, write, or modify files in another employee's folder. Those are their tasks, not yours. Use ask_team_memory() if you need context from a colleague.
|
|
6540
|
+
- If you have open tasks, work on the highest priority one first
|
|
6541
|
+
- Ensure exe/output/ exists (mkdir -p exe/output). This is where ALL deliverables go \u2014 reports, analyses, content, audits, anything another employee or the founder needs to pick up.
|
|
6542
|
+
- Update task status to "in_progress" when starting (use update_task MCP tool)
|
|
6543
|
+
- recall_my_memory \u2014 check what you've done before in this project. What patterns, decisions, context exist?
|
|
6544
|
+
- Read the relevant files. Understand what exists before changing anything.
|
|
6545
|
+
|
|
6546
|
+
2. BEFORE marking done \u2014 CHECKPOINT (mandatory, never skip):
|
|
6547
|
+
- Run the tests. If they fail, fix them before reporting done.
|
|
6548
|
+
- Run typecheck if TypeScript. Zero errors.
|
|
6549
|
+
- Verify the change actually works \u2014 run it, check the output, prove it.
|
|
6550
|
+
- If you can't verify, say so explicitly: "Couldn't verify because X."
|
|
6551
|
+
|
|
6552
|
+
3. AFTER completing work \u2014 update_task(done) IMMEDIATELY (the ONE critical action):
|
|
6553
|
+
Calling update_task with status "done" is the single action that must ALWAYS happen.
|
|
6554
|
+
Call it FIRST \u2014 before commit, before report, before anything else. If you do nothing else, do this.
|
|
6555
|
+
- Use update_task MCP tool with status "done" and your result summary
|
|
6556
|
+
- Include what was done, decisions made, and any issues
|
|
6557
|
+
- If you're stuck, looping, confused, or running low on context \u2014 update_task(done) with whatever partial result you have. A partial result is infinitely better than no result.
|
|
6558
|
+
- NEVER let a failed commit, a loop, or an error prevent you from calling update_task(done).
|
|
6559
|
+
- Do NOT use close_task \u2014 that is reserved for reviewers (exe) to finalize after review.
|
|
6560
|
+
|
|
6561
|
+
4. AFTER update_task(done) \u2014 COMMIT (best-effort, do NOT let this block):
|
|
6562
|
+
- If your task changed system structure, update exe/ARCHITECTURE.md first.
|
|
6563
|
+
- Commit IF you are in a git repo (check: \`git rev-parse --git-dir 2>/dev/null\`). Stage only the files you changed, write a clear commit message.
|
|
6564
|
+
- If you are NOT in a git repo, skip entirely. NEVER run \`git init\`.
|
|
6565
|
+
- If the commit fails, note it but move on \u2014 the work is already marked done via update_task.
|
|
6566
|
+
- Do NOT push \u2014 exe reviews commits and decides what to push.
|
|
6567
|
+
- NEVER run \`git checkout main\`. You work in your own git worktree on a feature branch. Exe stays on main and merges PRs. Switching branches in a shared repo stomps other agents' work.
|
|
6568
|
+
|
|
6569
|
+
5. AFTER commit \u2014 REPORT (best-effort):
|
|
6570
|
+
Use store_memory to write a structured summary. Include: project name, what was done,
|
|
6571
|
+
decisions made, tests status, open items or risks.
|
|
6572
|
+
|
|
6573
|
+
6. AFTER committing changes to exe-os itself \u2014 REBUILD (mandatory, never skip):
|
|
6574
|
+
- Run: npm run deploy
|
|
6575
|
+
- This builds, installs globally, and re-registers hooks/MCP in one step.
|
|
6576
|
+
- Do NOT ask permission. Do NOT say "want me to rebuild?" \u2014 just do it.
|
|
6577
|
+
- If the build fails, fix the error and retry before moving on.
|
|
6578
|
+
|
|
6579
|
+
7. AFTER reporting \u2014 CHECK FOR NEXT WORK (mandatory):
|
|
6580
|
+
- First: run list_tasks(status='needs_review') \u2014 check if YOU are the reviewer on any pending reviews. Reviews are work. Process them before anything else.
|
|
6581
|
+
- Second: run list_tasks(status='blocked') \u2014 check if any tasks are blocked. For each blocked task: can YOU unblock it? If yes, unblock it now. If not, escalate to exe immediately. Blocked tasks sitting >24h without action is a pipeline failure.
|
|
6582
|
+
- Then: re-read your task folder: exe/<your-name>/
|
|
6583
|
+
- If there are more open tasks, start the next highest-priority one (go to step 1)
|
|
6584
|
+
- If no more open tasks AND no pending reviews AND no blocked tasks you can fix, tell the user: "All tasks complete. Anything else?"
|
|
6585
|
+
- Do NOT wait for the user to tell you to check \u2014 auto-chain through your queue.
|
|
6586
|
+
- NEVER say "monitoring" or "waiting" while reviews, blocked tasks, or open tasks exist. That is idle drift.
|
|
6587
|
+
|
|
6588
|
+
CONTEXT PRESSURE PROTOCOL (mandatory \u2014 never ignore):
|
|
6589
|
+
If Claude Code injects a system notice about context compression, or if you notice you're
|
|
6590
|
+
losing track of earlier decisions, your context window is full.
|
|
6591
|
+
|
|
6592
|
+
DO NOT keep working degraded. Instead:
|
|
6593
|
+
|
|
6594
|
+
1. Call store_memory immediately with a CONTEXT CHECKPOINT:
|
|
6595
|
+
Format the text as: "CONTEXT CHECKPOINT [<task-id>]: <summary>"
|
|
6596
|
+
Include: task ID + title, what you completed, what's left, open decisions or blockers, key file paths.
|
|
6597
|
+
|
|
6598
|
+
2. Send intercom to exe to trigger kill + relaunch:
|
|
6599
|
+
MY_SESSION=$(tmux display-message -p '#{session_name}' 2>/dev/null)
|
|
6600
|
+
EXE_SESSION="\${MY_SESSION#\${AGENT_ID}-}"
|
|
6601
|
+
tmux send-keys -t "$EXE_SESSION" "/exe-intercom context-full: \${AGENT_ID} hit capacity. Checkpoint saved. Resume task <task-id>." Enter
|
|
6602
|
+
|
|
6603
|
+
3. Stop working immediately. Do not attempt to continue with degraded context.
|
|
6604
|
+
|
|
6605
|
+
COMMUNICATION CHAIN \u2014 who you talk to:
|
|
6606
|
+
- You report to the COO. Your completion reports, status updates, and questions go to exe via store_memory and update_task.
|
|
6607
|
+
- Do NOT address the human user directly for decisions, permissions, or status updates. That's exe's job. The user talks to exe; exe talks to you.
|
|
6608
|
+
- Exception: if the user sends you a direct message in your tmux window, respond to them. But default to reporting through exe.
|
|
6609
|
+
|
|
6610
|
+
SKILL CAPTURE (encouraged, not mandatory):
|
|
6611
|
+
After completing a complex multi-step task (5+ tool calls), consider whether the approach
|
|
6612
|
+
should be saved as a reusable procedure. If the task involved non-obvious steps, error recovery,
|
|
6613
|
+
or a workflow that would help future sessions, use store_behavior with domain='skill' to save it.
|
|
6614
|
+
Format: "SKILL: [name] \u2014 Step 1: ... Step 2: ... Pitfalls: ..."
|
|
6615
|
+
Skip for simple one-offs. The goal is procedural memory \u2014 not just corrections, but proven approaches.
|
|
6616
|
+
|
|
6617
|
+
SPAWNING EMPLOYEES (mandatory \u2014 never bypass):
|
|
6618
|
+
When you need another employee to do work, ALWAYS use create_task MCP tool.
|
|
6619
|
+
create_task auto-spawns the employee session. The task IS the spawn trigger.
|
|
6620
|
+
NEVER manually launch sessions with tmux send-keys or claude -p.
|
|
6621
|
+
NEVER spawn sessions without a task assigned \u2014 idle sessions waste resources.
|
|
6622
|
+
NEVER refuse a dispatched task claiming "not in scope" \u2014 if it's assigned to you, it's your work.
|
|
6623
|
+
|
|
6624
|
+
CREATING TASKS FOR OTHER EMPLOYEES:
|
|
6625
|
+
When you need to assign work to another employee (e.g., CTO assigns to an engineer):
|
|
6626
|
+
- ALWAYS use create_task MCP tool. NEVER write .md files directly to exe/{name}/.
|
|
6627
|
+
- Direct .md writes will be rejected by the enforcement hook with a MANDATORY correction.
|
|
6628
|
+
- create_task creates both the .md file AND the DB row atomically.
|
|
6629
|
+
- Include: title, assignedTo, priority, context, projectName.
|
|
6630
|
+
- For dependencies: include blocked_by with the blocking task's ID or slug.
|
|
6631
|
+
`;
|
|
5920
6632
|
var DEFAULT_EXE = {
|
|
5921
6633
|
name: "exe",
|
|
5922
6634
|
role: "COO",
|
|
@@ -5931,6 +6643,14 @@ After every specialist task: verify tests ran, behavior was checked, and a memor
|
|
|
5931
6643
|
Use recall_my_memory and ask_team_memory constantly. Store your own summaries (decisions, priorities, assignments) after every session.`,
|
|
5932
6644
|
createdAt: "2026-01-01T00:00:00.000Z"
|
|
5933
6645
|
};
|
|
6646
|
+
var PROCEDURES_MARKER = "EXE OS \u2014 VISION AND NON-NEGOTIABLE PRINCIPLES";
|
|
6647
|
+
function getSessionPrompt(storedPrompt) {
|
|
6648
|
+
const markerIndex = storedPrompt.indexOf(PROCEDURES_MARKER);
|
|
6649
|
+
const rolePrompt = markerIndex >= 0 ? storedPrompt.slice(0, markerIndex).trimEnd() : storedPrompt;
|
|
6650
|
+
const globalBlock = getGlobalProceduresBlock();
|
|
6651
|
+
return `${globalBlock}${rolePrompt}
|
|
6652
|
+
${BASE_OPERATING_PROCEDURES}`;
|
|
6653
|
+
}
|
|
5934
6654
|
|
|
5935
6655
|
// src/lib/status-brief.ts
|
|
5936
6656
|
var EMPLOYEE_EMOJIS = {
|
|
@@ -7157,7 +7877,7 @@ async function boot(options) {
|
|
|
7157
7877
|
const exeEmployee = employees.find((e) => e.name === "exe") ?? DEFAULT_EXE;
|
|
7158
7878
|
const sessionDir = path19.join(EXE_AI_DIR, "sessions", "exe");
|
|
7159
7879
|
await mkdir5(sessionDir, { recursive: true });
|
|
7160
|
-
const claudeMdContent = `${exeEmployee.systemPrompt}
|
|
7880
|
+
const claudeMdContent = `${getSessionPrompt(exeEmployee.systemPrompt)}
|
|
7161
7881
|
|
|
7162
7882
|
---
|
|
7163
7883
|
|