@askexenow/exe-os 0.8.40 → 0.8.42
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 +1376 -659
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2549 -1784
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +12 -2
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +791 -634
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2501 -1846
- 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 +44 -12
- package/dist/bin/exe-new-employee.js +6 -2
- 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 +176 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +157 -4
- package/dist/bin/exe-session-cleanup.js +2487 -403
- package/dist/bin/exe-settings.js +2 -1
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2691 -151
- 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 +50 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2542 -1887
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2690 -150
- package/dist/hooks/error-recall.js +157 -4
- package/dist/hooks/ingest-worker.js +1455 -803
- 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 +1670 -1509
- package/dist/hooks/prompt-submit.js +2650 -1074
- package/dist/hooks/response-ingest-worker.js +154 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +157 -4
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +155 -2
- package/dist/hooks/summary-worker.js +190 -21
- package/dist/index.js +326 -102
- package/dist/lib/cloud-sync.js +31 -10
- package/dist/lib/config.js +2 -0
- 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/embedder.js +3 -1
- package/dist/lib/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +285 -18
- package/dist/lib/hybrid-search.js +157 -4
- 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 +1485 -918
- package/dist/lib/tmux-routing.js +194 -10
- package/dist/mcp/server.js +1643 -924
- package/dist/mcp/tools/create-task.js +2283 -829
- 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 +79 -0
- package/dist/runtime/index.js +280 -68
- package/dist/tui/App.js +1485 -645
- package/package.json +3 -2
|
@@ -416,6 +416,13 @@ async function ensureSchema() {
|
|
|
416
416
|
});
|
|
417
417
|
} catch {
|
|
418
418
|
}
|
|
419
|
+
try {
|
|
420
|
+
await client.execute({
|
|
421
|
+
sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
|
|
422
|
+
args: []
|
|
423
|
+
});
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
419
426
|
try {
|
|
420
427
|
await client.execute({
|
|
421
428
|
sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
|
|
@@ -862,6 +869,18 @@ async function ensureSchema() {
|
|
|
862
869
|
CREATE INDEX IF NOT EXISTS idx_session_kills_agent
|
|
863
870
|
ON session_kills(agent_id);
|
|
864
871
|
`);
|
|
872
|
+
await client.execute(`
|
|
873
|
+
CREATE TABLE IF NOT EXISTS global_procedures (
|
|
874
|
+
id TEXT PRIMARY KEY,
|
|
875
|
+
title TEXT NOT NULL,
|
|
876
|
+
content TEXT NOT NULL,
|
|
877
|
+
priority TEXT NOT NULL DEFAULT 'p0',
|
|
878
|
+
domain TEXT,
|
|
879
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
880
|
+
created_at TEXT NOT NULL,
|
|
881
|
+
updated_at TEXT NOT NULL
|
|
882
|
+
)
|
|
883
|
+
`);
|
|
865
884
|
await client.executeMultiple(`
|
|
866
885
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
867
886
|
id TEXT PRIMARY KEY,
|
|
@@ -1024,6 +1043,7 @@ var config_exports = {};
|
|
|
1024
1043
|
__export(config_exports, {
|
|
1025
1044
|
CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
|
|
1026
1045
|
CONFIG_PATH: () => CONFIG_PATH,
|
|
1046
|
+
COO_AGENT_NAME: () => COO_AGENT_NAME,
|
|
1027
1047
|
CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
|
|
1028
1048
|
DB_PATH: () => DB_PATH,
|
|
1029
1049
|
EXE_AI_DIR: () => EXE_AI_DIR,
|
|
@@ -1179,7 +1199,7 @@ async function loadConfigFrom(configPath) {
|
|
|
1179
1199
|
return { ...DEFAULT_CONFIG };
|
|
1180
1200
|
}
|
|
1181
1201
|
}
|
|
1182
|
-
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
1202
|
+
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
1183
1203
|
var init_config = __esm({
|
|
1184
1204
|
"src/lib/config.ts"() {
|
|
1185
1205
|
"use strict";
|
|
@@ -1187,6 +1207,7 @@ var init_config = __esm({
|
|
|
1187
1207
|
DB_PATH = path4.join(EXE_AI_DIR, "memories.db");
|
|
1188
1208
|
MODELS_DIR = path4.join(EXE_AI_DIR, "models");
|
|
1189
1209
|
CONFIG_PATH = path4.join(EXE_AI_DIR, "config.json");
|
|
1210
|
+
COO_AGENT_NAME = "exe";
|
|
1190
1211
|
LEGACY_LANCE_PATH = path4.join(EXE_AI_DIR, "local.lance");
|
|
1191
1212
|
CURRENT_CONFIG_VERSION = 1;
|
|
1192
1213
|
DEFAULT_CONFIG = {
|
|
@@ -1265,6 +1286,61 @@ var init_config = __esm({
|
|
|
1265
1286
|
}
|
|
1266
1287
|
});
|
|
1267
1288
|
|
|
1289
|
+
// src/lib/state-bus.ts
|
|
1290
|
+
var StateBus, orgBus;
|
|
1291
|
+
var init_state_bus = __esm({
|
|
1292
|
+
"src/lib/state-bus.ts"() {
|
|
1293
|
+
"use strict";
|
|
1294
|
+
StateBus = class {
|
|
1295
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1296
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
1297
|
+
/** Emit an event to all subscribers */
|
|
1298
|
+
emit(event) {
|
|
1299
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
1300
|
+
if (typeHandlers) {
|
|
1301
|
+
for (const handler of typeHandlers) {
|
|
1302
|
+
try {
|
|
1303
|
+
handler(event);
|
|
1304
|
+
} catch {
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
for (const handler of this.globalHandlers) {
|
|
1309
|
+
try {
|
|
1310
|
+
handler(event);
|
|
1311
|
+
} catch {
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
/** Subscribe to a specific event type */
|
|
1316
|
+
on(type, handler) {
|
|
1317
|
+
if (!this.handlers.has(type)) {
|
|
1318
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
1319
|
+
}
|
|
1320
|
+
this.handlers.get(type).add(handler);
|
|
1321
|
+
}
|
|
1322
|
+
/** Subscribe to ALL events */
|
|
1323
|
+
onAny(handler) {
|
|
1324
|
+
this.globalHandlers.add(handler);
|
|
1325
|
+
}
|
|
1326
|
+
/** Unsubscribe from a specific event type */
|
|
1327
|
+
off(type, handler) {
|
|
1328
|
+
this.handlers.get(type)?.delete(handler);
|
|
1329
|
+
}
|
|
1330
|
+
/** Unsubscribe from ALL events */
|
|
1331
|
+
offAny(handler) {
|
|
1332
|
+
this.globalHandlers.delete(handler);
|
|
1333
|
+
}
|
|
1334
|
+
/** Remove all listeners */
|
|
1335
|
+
clear() {
|
|
1336
|
+
this.handlers.clear();
|
|
1337
|
+
this.globalHandlers.clear();
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
orgBus = new StateBus();
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1268
1344
|
// src/lib/shard-manager.ts
|
|
1269
1345
|
var shard_manager_exports = {};
|
|
1270
1346
|
__export(shard_manager_exports, {
|
|
@@ -1506,6 +1582,71 @@ var init_shard_manager = __esm({
|
|
|
1506
1582
|
}
|
|
1507
1583
|
});
|
|
1508
1584
|
|
|
1585
|
+
// src/lib/global-procedures.ts
|
|
1586
|
+
var global_procedures_exports = {};
|
|
1587
|
+
__export(global_procedures_exports, {
|
|
1588
|
+
deactivateGlobalProcedure: () => deactivateGlobalProcedure,
|
|
1589
|
+
getGlobalProceduresBlock: () => getGlobalProceduresBlock,
|
|
1590
|
+
loadGlobalProcedures: () => loadGlobalProcedures,
|
|
1591
|
+
storeGlobalProcedure: () => storeGlobalProcedure
|
|
1592
|
+
});
|
|
1593
|
+
import { randomUUID } from "crypto";
|
|
1594
|
+
async function loadGlobalProcedures() {
|
|
1595
|
+
const client = getClient();
|
|
1596
|
+
const result = await client.execute({
|
|
1597
|
+
sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
|
|
1598
|
+
args: []
|
|
1599
|
+
});
|
|
1600
|
+
const procedures = result.rows;
|
|
1601
|
+
if (procedures.length > 0) {
|
|
1602
|
+
_cache = procedures.map((p) => `### ${p.title}
|
|
1603
|
+
${p.content}`).join("\n\n");
|
|
1604
|
+
} else {
|
|
1605
|
+
_cache = "";
|
|
1606
|
+
}
|
|
1607
|
+
_cacheLoaded = true;
|
|
1608
|
+
return procedures;
|
|
1609
|
+
}
|
|
1610
|
+
function getGlobalProceduresBlock() {
|
|
1611
|
+
if (!_cacheLoaded) return "";
|
|
1612
|
+
if (!_cache) return "";
|
|
1613
|
+
return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
|
|
1614
|
+
|
|
1615
|
+
${_cache}
|
|
1616
|
+
`;
|
|
1617
|
+
}
|
|
1618
|
+
async function storeGlobalProcedure(input2) {
|
|
1619
|
+
const id = randomUUID();
|
|
1620
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1621
|
+
const client = getClient();
|
|
1622
|
+
await client.execute({
|
|
1623
|
+
sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
|
|
1624
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
1625
|
+
args: [id, input2.title, input2.content, input2.priority ?? "p0", input2.domain ?? null, now, now]
|
|
1626
|
+
});
|
|
1627
|
+
await loadGlobalProcedures();
|
|
1628
|
+
return id;
|
|
1629
|
+
}
|
|
1630
|
+
async function deactivateGlobalProcedure(id) {
|
|
1631
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1632
|
+
const client = getClient();
|
|
1633
|
+
const result = await client.execute({
|
|
1634
|
+
sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
|
|
1635
|
+
args: [now, id]
|
|
1636
|
+
});
|
|
1637
|
+
await loadGlobalProcedures();
|
|
1638
|
+
return result.rowsAffected > 0;
|
|
1639
|
+
}
|
|
1640
|
+
var _cache, _cacheLoaded;
|
|
1641
|
+
var init_global_procedures = __esm({
|
|
1642
|
+
"src/lib/global-procedures.ts"() {
|
|
1643
|
+
"use strict";
|
|
1644
|
+
init_database();
|
|
1645
|
+
_cache = "";
|
|
1646
|
+
_cacheLoaded = false;
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1509
1650
|
// src/lib/notifications.ts
|
|
1510
1651
|
import crypto3 from "crypto";
|
|
1511
1652
|
import path6 from "path";
|
|
@@ -1592,7 +1733,7 @@ var init_employees = __esm({
|
|
|
1592
1733
|
|
|
1593
1734
|
// src/lib/license.ts
|
|
1594
1735
|
import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
|
|
1595
|
-
import { randomUUID } from "crypto";
|
|
1736
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1596
1737
|
import path8 from "path";
|
|
1597
1738
|
import { jwtVerify, importSPKI } from "jose";
|
|
1598
1739
|
async function fetchRetry(url, init) {
|
|
@@ -1619,7 +1760,7 @@ function loadDeviceId() {
|
|
|
1619
1760
|
}
|
|
1620
1761
|
} catch {
|
|
1621
1762
|
}
|
|
1622
|
-
const id =
|
|
1763
|
+
const id = randomUUID2();
|
|
1623
1764
|
mkdirSync2(EXE_AI_DIR, { recursive: true });
|
|
1624
1765
|
writeFileSync(DEVICE_ID_PATH, id, "utf8");
|
|
1625
1766
|
return id;
|
|
@@ -1863,7 +2004,7 @@ var init_plan_limits = __esm({
|
|
|
1863
2004
|
// src/lib/exe-daemon-client.ts
|
|
1864
2005
|
import net from "net";
|
|
1865
2006
|
import { spawn } from "child_process";
|
|
1866
|
-
import { randomUUID as
|
|
2007
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1867
2008
|
import { existsSync as existsSync9, unlinkSync as unlinkSync2, readFileSync as readFileSync7, openSync, closeSync, statSync as statSync2 } from "fs";
|
|
1868
2009
|
import path10 from "path";
|
|
1869
2010
|
import { fileURLToPath } from "url";
|
|
@@ -2055,7 +2196,7 @@ function sendRequest(texts, priority) {
|
|
|
2055
2196
|
resolve({ error: "Not connected" });
|
|
2056
2197
|
return;
|
|
2057
2198
|
}
|
|
2058
|
-
const id =
|
|
2199
|
+
const id = randomUUID3();
|
|
2059
2200
|
const timer = setTimeout(() => {
|
|
2060
2201
|
_pending.delete(id);
|
|
2061
2202
|
resolve({ error: "Request timeout" });
|
|
@@ -2073,7 +2214,7 @@ function sendRequest(texts, priority) {
|
|
|
2073
2214
|
async function pingDaemon() {
|
|
2074
2215
|
if (!_socket || !_connected) return null;
|
|
2075
2216
|
return new Promise((resolve) => {
|
|
2076
|
-
const id =
|
|
2217
|
+
const id = randomUUID3();
|
|
2077
2218
|
const timer = setTimeout(() => {
|
|
2078
2219
|
_pending.delete(id);
|
|
2079
2220
|
resolve(null);
|
|
@@ -2269,463 +2410,62 @@ var init_embedder = __esm({
|
|
|
2269
2410
|
}
|
|
2270
2411
|
});
|
|
2271
2412
|
|
|
2272
|
-
// src/lib/
|
|
2273
|
-
import
|
|
2413
|
+
// src/lib/session-registry.ts
|
|
2414
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync10 } from "fs";
|
|
2274
2415
|
import path11 from "path";
|
|
2275
|
-
import
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
const row = await resolveTask(client, input2.taskId);
|
|
2281
|
-
const taskId = String(row.id);
|
|
2282
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2283
|
-
const blockedByIds = [];
|
|
2284
|
-
if (row.blocked_by) {
|
|
2285
|
-
blockedByIds.push(String(row.blocked_by));
|
|
2416
|
+
import os4 from "os";
|
|
2417
|
+
function registerSession(entry) {
|
|
2418
|
+
const dir = path11.dirname(REGISTRY_PATH);
|
|
2419
|
+
if (!existsSync10(dir)) {
|
|
2420
|
+
mkdirSync3(dir, { recursive: true });
|
|
2286
2421
|
}
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
};
|
|
2294
|
-
const result = await client.execute({
|
|
2295
|
-
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
2296
|
-
args: [JSON.stringify(checkpoint), now, taskId]
|
|
2297
|
-
});
|
|
2298
|
-
if (result.rowsAffected === 0) {
|
|
2299
|
-
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
2422
|
+
const sessions = listSessions();
|
|
2423
|
+
const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
|
|
2424
|
+
if (idx >= 0) {
|
|
2425
|
+
sessions[idx] = entry;
|
|
2426
|
+
} else {
|
|
2427
|
+
sessions.push(entry);
|
|
2300
2428
|
}
|
|
2301
|
-
|
|
2302
|
-
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
2303
|
-
args: [taskId]
|
|
2304
|
-
});
|
|
2305
|
-
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
2306
|
-
return { checkpointCount };
|
|
2307
|
-
}
|
|
2308
|
-
function extractParentFromContext(contextBody) {
|
|
2309
|
-
if (!contextBody) return null;
|
|
2310
|
-
const match = contextBody.match(
|
|
2311
|
-
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
2312
|
-
);
|
|
2313
|
-
return match ? match[1].toLowerCase() : null;
|
|
2314
|
-
}
|
|
2315
|
-
function slugify(title) {
|
|
2316
|
-
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2429
|
+
writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
2317
2430
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
result = await client.execute({
|
|
2325
|
-
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
2326
|
-
args: [`%${identifier}%`]
|
|
2327
|
-
});
|
|
2328
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
2329
|
-
if (result.rows.length > 1) {
|
|
2330
|
-
const exact = result.rows.filter(
|
|
2331
|
-
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
2332
|
-
);
|
|
2333
|
-
if (exact.length === 1) return exact[0];
|
|
2334
|
-
const candidates = exact.length > 1 ? exact : result.rows;
|
|
2335
|
-
const active = candidates.filter(
|
|
2336
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
2337
|
-
);
|
|
2338
|
-
if (active.length === 1) return active[0];
|
|
2339
|
-
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
2340
|
-
throw new Error(
|
|
2341
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
2342
|
-
);
|
|
2343
|
-
}
|
|
2344
|
-
result = await client.execute({
|
|
2345
|
-
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
2346
|
-
args: [`%${identifier}%`]
|
|
2347
|
-
});
|
|
2348
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
2349
|
-
if (result.rows.length > 1) {
|
|
2350
|
-
const active = result.rows.filter(
|
|
2351
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
2352
|
-
);
|
|
2353
|
-
if (active.length === 1) return active[0];
|
|
2354
|
-
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
2355
|
-
throw new Error(
|
|
2356
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
2357
|
-
);
|
|
2431
|
+
function listSessions() {
|
|
2432
|
+
try {
|
|
2433
|
+
const raw = readFileSync8(REGISTRY_PATH, "utf8");
|
|
2434
|
+
return JSON.parse(raw);
|
|
2435
|
+
} catch {
|
|
2436
|
+
return [];
|
|
2358
2437
|
}
|
|
2359
|
-
throw new Error(`Task not found: ${identifier}`);
|
|
2360
2438
|
}
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
|
|
2367
|
-
let blockedById = null;
|
|
2368
|
-
const initialStatus = input2.blockedBy ? "blocked" : "open";
|
|
2369
|
-
if (input2.blockedBy) {
|
|
2370
|
-
const blocker = await resolveTask(client, input2.blockedBy);
|
|
2371
|
-
blockedById = String(blocker.id);
|
|
2372
|
-
}
|
|
2373
|
-
let parentTaskId = null;
|
|
2374
|
-
let parentRef = input2.parentTaskId;
|
|
2375
|
-
if (!parentRef) {
|
|
2376
|
-
const extracted = extractParentFromContext(input2.context);
|
|
2377
|
-
if (extracted) {
|
|
2378
|
-
parentRef = extracted;
|
|
2379
|
-
process.stderr.write(
|
|
2380
|
-
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
2381
|
-
);
|
|
2382
|
-
}
|
|
2439
|
+
var REGISTRY_PATH;
|
|
2440
|
+
var init_session_registry = __esm({
|
|
2441
|
+
"src/lib/session-registry.ts"() {
|
|
2442
|
+
"use strict";
|
|
2443
|
+
REGISTRY_PATH = path11.join(os4.homedir(), ".exe-os", "session-registry.json");
|
|
2383
2444
|
}
|
|
2384
|
-
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
// src/lib/session-key.ts
|
|
2448
|
+
import { execSync as execSync4 } from "child_process";
|
|
2449
|
+
function getSessionKey() {
|
|
2450
|
+
if (_cached2) return _cached2;
|
|
2451
|
+
let pid = process.ppid;
|
|
2452
|
+
for (let i = 0; i < 10; i++) {
|
|
2385
2453
|
try {
|
|
2386
|
-
const
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2454
|
+
const info = execSync4(`ps -p ${pid} -o ppid=,comm=`, {
|
|
2455
|
+
encoding: "utf8",
|
|
2456
|
+
timeout: 2e3
|
|
2457
|
+
}).trim();
|
|
2458
|
+
const match = info.match(/^\s*(\d+)\s+(.+)$/);
|
|
2459
|
+
if (!match) break;
|
|
2460
|
+
const [, ppid, cmd] = match;
|
|
2461
|
+
if (cmd === "claude" || cmd.endsWith("/claude")) {
|
|
2462
|
+
_cached2 = String(pid);
|
|
2463
|
+
return _cached2;
|
|
2393
2464
|
}
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
}
|
|
2397
|
-
let warning;
|
|
2398
|
-
const dupCheck = await client.execute({
|
|
2399
|
-
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
2400
|
-
args: [input2.title, input2.assignedTo]
|
|
2401
|
-
});
|
|
2402
|
-
if (dupCheck.rows.length > 0) {
|
|
2403
|
-
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
2404
|
-
}
|
|
2405
|
-
if (input2.baseDir) {
|
|
2406
|
-
try {
|
|
2407
|
-
await mkdir4(path11.join(input2.baseDir, "exe", "output"), { recursive: true });
|
|
2408
|
-
await mkdir4(path11.join(input2.baseDir, "exe", "research"), { recursive: true });
|
|
2409
|
-
await ensureArchitectureDoc(input2.baseDir, input2.projectName);
|
|
2410
|
-
await ensureGitignoreExe(input2.baseDir);
|
|
2465
|
+
pid = parseInt(ppid, 10);
|
|
2466
|
+
if (pid <= 1) break;
|
|
2411
2467
|
} catch {
|
|
2412
|
-
|
|
2413
|
-
}
|
|
2414
|
-
const complexity = input2.complexity ?? "standard";
|
|
2415
|
-
await client.execute({
|
|
2416
|
-
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, created_at, updated_at)
|
|
2417
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2418
|
-
args: [
|
|
2419
|
-
id,
|
|
2420
|
-
input2.title,
|
|
2421
|
-
input2.assignedTo,
|
|
2422
|
-
input2.assignedBy,
|
|
2423
|
-
input2.projectName,
|
|
2424
|
-
input2.priority,
|
|
2425
|
-
initialStatus,
|
|
2426
|
-
taskFile,
|
|
2427
|
-
blockedById,
|
|
2428
|
-
parentTaskId,
|
|
2429
|
-
input2.reviewer ?? null,
|
|
2430
|
-
input2.context,
|
|
2431
|
-
complexity,
|
|
2432
|
-
input2.budgetTokens ?? null,
|
|
2433
|
-
input2.budgetFallbackModel ?? null,
|
|
2434
|
-
0,
|
|
2435
|
-
null,
|
|
2436
|
-
now,
|
|
2437
|
-
now
|
|
2438
|
-
]
|
|
2439
|
-
});
|
|
2440
|
-
return {
|
|
2441
|
-
id,
|
|
2442
|
-
title: input2.title,
|
|
2443
|
-
assignedTo: input2.assignedTo,
|
|
2444
|
-
assignedBy: input2.assignedBy,
|
|
2445
|
-
projectName: input2.projectName,
|
|
2446
|
-
priority: input2.priority,
|
|
2447
|
-
status: initialStatus,
|
|
2448
|
-
taskFile,
|
|
2449
|
-
createdAt: now,
|
|
2450
|
-
updatedAt: now,
|
|
2451
|
-
warning,
|
|
2452
|
-
budgetTokens: input2.budgetTokens ?? null,
|
|
2453
|
-
budgetFallbackModel: input2.budgetFallbackModel ?? null,
|
|
2454
|
-
tokensUsed: 0,
|
|
2455
|
-
tokensWarnedAt: null
|
|
2456
|
-
};
|
|
2457
|
-
}
|
|
2458
|
-
async function listTasks(input2) {
|
|
2459
|
-
const client = getClient();
|
|
2460
|
-
const conditions = [];
|
|
2461
|
-
const args = [];
|
|
2462
|
-
if (input2.assignedTo) {
|
|
2463
|
-
conditions.push("assigned_to = ?");
|
|
2464
|
-
args.push(input2.assignedTo);
|
|
2465
|
-
}
|
|
2466
|
-
if (input2.status) {
|
|
2467
|
-
conditions.push("status = ?");
|
|
2468
|
-
args.push(input2.status);
|
|
2469
|
-
} else {
|
|
2470
|
-
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
2471
|
-
}
|
|
2472
|
-
if (input2.projectName) {
|
|
2473
|
-
conditions.push("project_name = ?");
|
|
2474
|
-
args.push(input2.projectName);
|
|
2475
|
-
}
|
|
2476
|
-
if (input2.priority) {
|
|
2477
|
-
conditions.push("priority = ?");
|
|
2478
|
-
args.push(input2.priority);
|
|
2479
|
-
}
|
|
2480
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2481
|
-
const result = await client.execute({
|
|
2482
|
-
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`,
|
|
2483
|
-
args
|
|
2484
|
-
});
|
|
2485
|
-
return result.rows.map((r) => ({
|
|
2486
|
-
id: String(r.id),
|
|
2487
|
-
title: String(r.title),
|
|
2488
|
-
assignedTo: String(r.assigned_to),
|
|
2489
|
-
assignedBy: String(r.assigned_by),
|
|
2490
|
-
projectName: String(r.project_name),
|
|
2491
|
-
priority: String(r.priority),
|
|
2492
|
-
status: String(r.status),
|
|
2493
|
-
taskFile: String(r.task_file),
|
|
2494
|
-
createdAt: String(r.created_at),
|
|
2495
|
-
updatedAt: String(r.updated_at),
|
|
2496
|
-
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
2497
|
-
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
2498
|
-
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
2499
|
-
tokensUsed: Number(r.tokens_used ?? 0),
|
|
2500
|
-
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
2501
|
-
}));
|
|
2502
|
-
}
|
|
2503
|
-
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
2504
|
-
if (!taskContext) return null;
|
|
2505
|
-
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
2506
|
-
try {
|
|
2507
|
-
const since = new Date(taskCreatedAt).toISOString();
|
|
2508
|
-
const branch = execSync4(
|
|
2509
|
-
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
2510
|
-
{ encoding: "utf8", timeout: 3e3 }
|
|
2511
|
-
).trim();
|
|
2512
|
-
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
2513
|
-
const commitCount = execSync4(
|
|
2514
|
-
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
2515
|
-
{ encoding: "utf8", timeout: 5e3 }
|
|
2516
|
-
).trim();
|
|
2517
|
-
const count = parseInt(commitCount, 10);
|
|
2518
|
-
if (count === 0) {
|
|
2519
|
-
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
2520
|
-
}
|
|
2521
|
-
return null;
|
|
2522
|
-
} catch {
|
|
2523
|
-
return null;
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
async function updateTaskStatus(input2) {
|
|
2527
|
-
const client = getClient();
|
|
2528
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2529
|
-
const row = await resolveTask(client, input2.taskId);
|
|
2530
|
-
const taskId = String(row.id);
|
|
2531
|
-
const taskFile = String(row.task_file);
|
|
2532
|
-
if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
2533
|
-
process.stderr.write(
|
|
2534
|
-
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
2535
|
-
`
|
|
2536
|
-
);
|
|
2537
|
-
}
|
|
2538
|
-
if (input2.status === "done") {
|
|
2539
|
-
const existingRow = await client.execute({
|
|
2540
|
-
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
2541
|
-
args: [taskId]
|
|
2542
|
-
});
|
|
2543
|
-
if (existingRow.rows.length > 0) {
|
|
2544
|
-
const ctx = existingRow.rows[0];
|
|
2545
|
-
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
2546
|
-
if (warning) {
|
|
2547
|
-
input2.result = input2.result ? `\u26A0\uFE0F ${warning}
|
|
2548
|
-
|
|
2549
|
-
${input2.result}` : `\u26A0\uFE0F ${warning}`;
|
|
2550
|
-
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
2551
|
-
`);
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
if (input2.status === "in_progress") {
|
|
2556
|
-
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
2557
|
-
const claim = await client.execute({
|
|
2558
|
-
sql: `UPDATE tasks
|
|
2559
|
-
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
2560
|
-
WHERE id = ? AND status = 'open'`,
|
|
2561
|
-
args: [tmuxSession, now, taskId]
|
|
2562
|
-
});
|
|
2563
|
-
if (claim.rowsAffected === 0) {
|
|
2564
|
-
const current = await client.execute({
|
|
2565
|
-
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
2566
|
-
args: [taskId]
|
|
2567
|
-
});
|
|
2568
|
-
const cur = current.rows[0];
|
|
2569
|
-
const status = cur?.status ?? "unknown";
|
|
2570
|
-
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
2571
|
-
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
2572
|
-
}
|
|
2573
|
-
try {
|
|
2574
|
-
await writeCheckpoint({
|
|
2575
|
-
taskId,
|
|
2576
|
-
step: "claimed",
|
|
2577
|
-
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
2578
|
-
});
|
|
2579
|
-
} catch {
|
|
2580
|
-
}
|
|
2581
|
-
return { row, taskFile, now, taskId };
|
|
2582
|
-
}
|
|
2583
|
-
if (input2.result) {
|
|
2584
|
-
await client.execute({
|
|
2585
|
-
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
2586
|
-
args: [input2.status, input2.result, now, taskId]
|
|
2587
|
-
});
|
|
2588
|
-
} else {
|
|
2589
|
-
await client.execute({
|
|
2590
|
-
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
2591
|
-
args: [input2.status, now, taskId]
|
|
2592
|
-
});
|
|
2593
|
-
}
|
|
2594
|
-
try {
|
|
2595
|
-
await writeCheckpoint({
|
|
2596
|
-
taskId,
|
|
2597
|
-
step: `status_transition:${input2.status}`,
|
|
2598
|
-
contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
|
|
2599
|
-
});
|
|
2600
|
-
} catch {
|
|
2601
|
-
}
|
|
2602
|
-
return { row, taskFile, now, taskId };
|
|
2603
|
-
}
|
|
2604
|
-
async function deleteTaskCore(taskId, _baseDir) {
|
|
2605
|
-
const client = getClient();
|
|
2606
|
-
const row = await resolveTask(client, taskId);
|
|
2607
|
-
const id = String(row.id);
|
|
2608
|
-
const taskFile = String(row.task_file);
|
|
2609
|
-
const assignedTo = String(row.assigned_to);
|
|
2610
|
-
const assignedBy = String(row.assigned_by);
|
|
2611
|
-
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
2612
|
-
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
2613
|
-
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
2614
|
-
}
|
|
2615
|
-
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
2616
|
-
const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
2617
|
-
try {
|
|
2618
|
-
if (existsSync10(archPath)) return;
|
|
2619
|
-
const template = [
|
|
2620
|
-
`# ${projectName} \u2014 System Architecture`,
|
|
2621
|
-
"",
|
|
2622
|
-
"> Employees: read this before every task. Update it when you change system structure.",
|
|
2623
|
-
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2624
|
-
"",
|
|
2625
|
-
"## Overview",
|
|
2626
|
-
"",
|
|
2627
|
-
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
2628
|
-
"",
|
|
2629
|
-
"## Key Components",
|
|
2630
|
-
"",
|
|
2631
|
-
"<!-- List the major modules, services, or subsystems. -->",
|
|
2632
|
-
"",
|
|
2633
|
-
"## Data Flow",
|
|
2634
|
-
"",
|
|
2635
|
-
"<!-- How does data move through the system? What writes where? -->",
|
|
2636
|
-
"",
|
|
2637
|
-
"## Invariants",
|
|
2638
|
-
"",
|
|
2639
|
-
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
2640
|
-
"",
|
|
2641
|
-
"## Dependencies",
|
|
2642
|
-
"",
|
|
2643
|
-
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
2644
|
-
""
|
|
2645
|
-
].join("\n");
|
|
2646
|
-
await writeFile4(archPath, template, "utf-8");
|
|
2647
|
-
} catch {
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
async function ensureGitignoreExe(baseDir) {
|
|
2651
|
-
const gitignorePath = path11.join(baseDir, ".gitignore");
|
|
2652
|
-
try {
|
|
2653
|
-
if (existsSync10(gitignorePath)) {
|
|
2654
|
-
const content = readFileSync8(gitignorePath, "utf-8");
|
|
2655
|
-
if (/^\/?exe\/?$/m.test(content)) return;
|
|
2656
|
-
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
2657
|
-
} else {
|
|
2658
|
-
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
2659
|
-
}
|
|
2660
|
-
} catch {
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
2664
|
-
var init_tasks_crud = __esm({
|
|
2665
|
-
"src/lib/tasks-crud.ts"() {
|
|
2666
|
-
"use strict";
|
|
2667
|
-
init_database();
|
|
2668
|
-
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
2669
|
-
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
2670
|
-
}
|
|
2671
|
-
});
|
|
2672
|
-
|
|
2673
|
-
// src/lib/session-registry.ts
|
|
2674
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync11 } from "fs";
|
|
2675
|
-
import path12 from "path";
|
|
2676
|
-
import os4 from "os";
|
|
2677
|
-
function registerSession(entry) {
|
|
2678
|
-
const dir = path12.dirname(REGISTRY_PATH);
|
|
2679
|
-
if (!existsSync11(dir)) {
|
|
2680
|
-
mkdirSync3(dir, { recursive: true });
|
|
2681
|
-
}
|
|
2682
|
-
const sessions = listSessions();
|
|
2683
|
-
const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
|
|
2684
|
-
if (idx >= 0) {
|
|
2685
|
-
sessions[idx] = entry;
|
|
2686
|
-
} else {
|
|
2687
|
-
sessions.push(entry);
|
|
2688
|
-
}
|
|
2689
|
-
writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
2690
|
-
}
|
|
2691
|
-
function listSessions() {
|
|
2692
|
-
try {
|
|
2693
|
-
const raw = readFileSync9(REGISTRY_PATH, "utf8");
|
|
2694
|
-
return JSON.parse(raw);
|
|
2695
|
-
} catch {
|
|
2696
|
-
return [];
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
var REGISTRY_PATH;
|
|
2700
|
-
var init_session_registry = __esm({
|
|
2701
|
-
"src/lib/session-registry.ts"() {
|
|
2702
|
-
"use strict";
|
|
2703
|
-
REGISTRY_PATH = path12.join(os4.homedir(), ".exe-os", "session-registry.json");
|
|
2704
|
-
}
|
|
2705
|
-
});
|
|
2706
|
-
|
|
2707
|
-
// src/lib/session-key.ts
|
|
2708
|
-
import { execSync as execSync5 } from "child_process";
|
|
2709
|
-
function getSessionKey() {
|
|
2710
|
-
if (_cached2) return _cached2;
|
|
2711
|
-
let pid = process.ppid;
|
|
2712
|
-
for (let i = 0; i < 10; i++) {
|
|
2713
|
-
try {
|
|
2714
|
-
const info = execSync5(`ps -p ${pid} -o ppid=,comm=`, {
|
|
2715
|
-
encoding: "utf8",
|
|
2716
|
-
timeout: 2e3
|
|
2717
|
-
}).trim();
|
|
2718
|
-
const match = info.match(/^\s*(\d+)\s+(.+)$/);
|
|
2719
|
-
if (!match) break;
|
|
2720
|
-
const [, ppid, cmd] = match;
|
|
2721
|
-
if (cmd === "claude" || cmd.endsWith("/claude")) {
|
|
2722
|
-
_cached2 = String(pid);
|
|
2723
|
-
return _cached2;
|
|
2724
|
-
}
|
|
2725
|
-
pid = parseInt(ppid, 10);
|
|
2726
|
-
if (pid <= 1) break;
|
|
2727
|
-
} catch {
|
|
2728
|
-
break;
|
|
2468
|
+
break;
|
|
2729
2469
|
}
|
|
2730
2470
|
}
|
|
2731
2471
|
_cached2 = process.env.CLAUDE_CODE_SSE_PORT ?? String(process.ppid);
|
|
@@ -2847,14 +2587,14 @@ var init_transport = __esm({
|
|
|
2847
2587
|
});
|
|
2848
2588
|
|
|
2849
2589
|
// src/lib/cc-agent-support.ts
|
|
2850
|
-
import { execSync as
|
|
2590
|
+
import { execSync as execSync5 } from "child_process";
|
|
2851
2591
|
function _resetCcAgentSupportCache() {
|
|
2852
2592
|
_cachedSupport = null;
|
|
2853
2593
|
}
|
|
2854
2594
|
function claudeSupportsAgentFlag() {
|
|
2855
2595
|
if (_cachedSupport !== null) return _cachedSupport;
|
|
2856
2596
|
try {
|
|
2857
|
-
const helpOutput =
|
|
2597
|
+
const helpOutput = execSync5("claude --help 2>&1", {
|
|
2858
2598
|
encoding: "utf-8",
|
|
2859
2599
|
timeout: 5e3
|
|
2860
2600
|
});
|
|
@@ -2897,17 +2637,17 @@ var init_provider_table = __esm({
|
|
|
2897
2637
|
});
|
|
2898
2638
|
|
|
2899
2639
|
// src/lib/intercom-queue.ts
|
|
2900
|
-
import { readFileSync as
|
|
2901
|
-
import
|
|
2640
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync11, mkdirSync as mkdirSync4 } from "fs";
|
|
2641
|
+
import path12 from "path";
|
|
2902
2642
|
import os5 from "os";
|
|
2903
2643
|
function ensureDir() {
|
|
2904
|
-
const dir =
|
|
2905
|
-
if (!
|
|
2644
|
+
const dir = path12.dirname(QUEUE_PATH);
|
|
2645
|
+
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
|
|
2906
2646
|
}
|
|
2907
2647
|
function readQueue() {
|
|
2908
2648
|
try {
|
|
2909
|
-
if (!
|
|
2910
|
-
return JSON.parse(
|
|
2649
|
+
if (!existsSync11(QUEUE_PATH)) return [];
|
|
2650
|
+
return JSON.parse(readFileSync9(QUEUE_PATH, "utf8"));
|
|
2911
2651
|
} catch {
|
|
2912
2652
|
return [];
|
|
2913
2653
|
}
|
|
@@ -2939,21 +2679,358 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
|
2939
2679
|
var init_intercom_queue = __esm({
|
|
2940
2680
|
"src/lib/intercom-queue.ts"() {
|
|
2941
2681
|
"use strict";
|
|
2942
|
-
QUEUE_PATH =
|
|
2682
|
+
QUEUE_PATH = path12.join(os5.homedir(), ".exe-os", "intercom-queue.json");
|
|
2943
2683
|
TTL_MS = 60 * 60 * 1e3;
|
|
2944
|
-
INTERCOM_LOG =
|
|
2684
|
+
INTERCOM_LOG = path12.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
// src/lib/session-kill-telemetry.ts
|
|
2689
|
+
import crypto4 from "crypto";
|
|
2690
|
+
async function recordSessionKill(input2) {
|
|
2691
|
+
try {
|
|
2692
|
+
const client = getClient();
|
|
2693
|
+
await client.execute({
|
|
2694
|
+
sql: `INSERT INTO session_kills
|
|
2695
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
2696
|
+
ticks_idle, estimated_tokens_saved)
|
|
2697
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2698
|
+
args: [
|
|
2699
|
+
crypto4.randomUUID(),
|
|
2700
|
+
input2.sessionName,
|
|
2701
|
+
input2.agentId,
|
|
2702
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2703
|
+
input2.reason,
|
|
2704
|
+
input2.ticksIdle ?? null,
|
|
2705
|
+
input2.estimatedTokensSaved ?? null
|
|
2706
|
+
]
|
|
2707
|
+
});
|
|
2708
|
+
} catch (err) {
|
|
2709
|
+
process.stderr.write(
|
|
2710
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
2711
|
+
`
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
var init_session_kill_telemetry = __esm({
|
|
2716
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
2717
|
+
"use strict";
|
|
2718
|
+
init_database();
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
|
|
2722
|
+
// src/lib/capacity-monitor.ts
|
|
2723
|
+
var capacity_monitor_exports = {};
|
|
2724
|
+
__export(capacity_monitor_exports, {
|
|
2725
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
2726
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
2727
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
2728
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
2729
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
2730
|
+
extractContextPercent: () => extractContextPercent,
|
|
2731
|
+
isAtCapacity: () => isAtCapacity,
|
|
2732
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
2733
|
+
pollCapacityDead: () => pollCapacityDead
|
|
2734
|
+
});
|
|
2735
|
+
function resumeTaskTitle(agentId) {
|
|
2736
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
2737
|
+
}
|
|
2738
|
+
function buildResumeContext(agentId, openTasks) {
|
|
2739
|
+
const taskList = openTasks.map(
|
|
2740
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
2741
|
+
).join("\n");
|
|
2742
|
+
return [
|
|
2743
|
+
"## Context",
|
|
2744
|
+
"",
|
|
2745
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
2746
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
2747
|
+
"",
|
|
2748
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
2749
|
+
"",
|
|
2750
|
+
taskList,
|
|
2751
|
+
"",
|
|
2752
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
2753
|
+
].join("\n");
|
|
2754
|
+
}
|
|
2755
|
+
function filterPaneContent(paneOutput) {
|
|
2756
|
+
return paneOutput.split("\n").filter((line) => {
|
|
2757
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
2758
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
2759
|
+
if (line.includes(marker)) return false;
|
|
2760
|
+
}
|
|
2761
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
2762
|
+
if (re.test(line)) return false;
|
|
2763
|
+
}
|
|
2764
|
+
return true;
|
|
2765
|
+
}).join("\n");
|
|
2766
|
+
}
|
|
2767
|
+
function extractContextPercent(paneOutput) {
|
|
2768
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
2769
|
+
if (!match) return null;
|
|
2770
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
2771
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2772
|
+
}
|
|
2773
|
+
function isAtCapacity(paneOutput) {
|
|
2774
|
+
const filtered = filterPaneContent(paneOutput);
|
|
2775
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
2776
|
+
}
|
|
2777
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
2778
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
2779
|
+
if (pendingSince === void 0) {
|
|
2780
|
+
_pendingCapacityKill.set(agentId, now);
|
|
2781
|
+
return false;
|
|
2782
|
+
}
|
|
2783
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
2784
|
+
_pendingCapacityKill.set(agentId, now);
|
|
2785
|
+
return false;
|
|
2786
|
+
}
|
|
2787
|
+
_pendingCapacityKill.delete(agentId);
|
|
2788
|
+
return true;
|
|
2789
|
+
}
|
|
2790
|
+
function _resetPendingCapacityKills() {
|
|
2791
|
+
_pendingCapacityKill.clear();
|
|
2792
|
+
}
|
|
2793
|
+
function _resetLastRelaunchCache() {
|
|
2794
|
+
_lastRelaunch.clear();
|
|
2795
|
+
}
|
|
2796
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
2797
|
+
const client = getClient();
|
|
2798
|
+
const result = await client.execute({
|
|
2799
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
2800
|
+
FROM tasks
|
|
2801
|
+
WHERE assigned_to = ? AND title LIKE ?`,
|
|
2802
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`]
|
|
2803
|
+
});
|
|
2804
|
+
const raw = result.rows[0]?.last_created_at;
|
|
2805
|
+
if (raw === null || raw === void 0) return null;
|
|
2806
|
+
const parsed = Date.parse(String(raw));
|
|
2807
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2808
|
+
}
|
|
2809
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
2810
|
+
const cached = _lastRelaunch.get(agentId);
|
|
2811
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
2812
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
2813
|
+
if (persisted === null) return false;
|
|
2814
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
2815
|
+
_lastRelaunch.set(agentId, persisted);
|
|
2816
|
+
return true;
|
|
2817
|
+
}
|
|
2818
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
2819
|
+
const client = getClient();
|
|
2820
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2821
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
2822
|
+
const existing = await client.execute({
|
|
2823
|
+
sql: `SELECT id FROM tasks
|
|
2824
|
+
WHERE assigned_to = ?
|
|
2825
|
+
AND title LIKE ?
|
|
2826
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
|
|
2827
|
+
ORDER BY created_at DESC
|
|
2828
|
+
LIMIT 1`,
|
|
2829
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
2830
|
+
});
|
|
2831
|
+
if (existing.rows.length > 0) {
|
|
2832
|
+
const taskId = String(existing.rows[0].id);
|
|
2833
|
+
await client.execute({
|
|
2834
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
2835
|
+
args: [context, now, taskId]
|
|
2836
|
+
});
|
|
2837
|
+
return { created: false, taskId };
|
|
2838
|
+
}
|
|
2839
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
2840
|
+
const task = await createTask2({
|
|
2841
|
+
title: resumeTaskTitle(agentId),
|
|
2842
|
+
assignedTo: agentId,
|
|
2843
|
+
assignedBy: "system",
|
|
2844
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
2845
|
+
priority: "p0",
|
|
2846
|
+
context,
|
|
2847
|
+
baseDir: projectDir
|
|
2848
|
+
});
|
|
2849
|
+
return { created: true, taskId: task.id };
|
|
2850
|
+
}
|
|
2851
|
+
async function pollCapacityDead() {
|
|
2852
|
+
const transport = getTransport();
|
|
2853
|
+
const relaunched = [];
|
|
2854
|
+
const registered = listSessions().filter(
|
|
2855
|
+
(s) => s.agentId !== "exe"
|
|
2856
|
+
);
|
|
2857
|
+
if (registered.length === 0) return [];
|
|
2858
|
+
let liveSessions;
|
|
2859
|
+
try {
|
|
2860
|
+
liveSessions = transport.listSessions();
|
|
2861
|
+
} catch {
|
|
2862
|
+
return [];
|
|
2863
|
+
}
|
|
2864
|
+
for (const entry of registered) {
|
|
2865
|
+
const { windowName, agentId, projectDir } = entry;
|
|
2866
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
2867
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
2868
|
+
let pane;
|
|
2869
|
+
try {
|
|
2870
|
+
pane = transport.capturePane(windowName, 15);
|
|
2871
|
+
} catch {
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
if (!isAtCapacity(pane)) continue;
|
|
2875
|
+
const ctxPct = extractContextPercent(pane);
|
|
2876
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
2877
|
+
process.stderr.write(
|
|
2878
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
2879
|
+
`
|
|
2880
|
+
);
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
if (!confirmCapacityKill(agentId)) {
|
|
2884
|
+
process.stderr.write(
|
|
2885
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
2886
|
+
`
|
|
2887
|
+
);
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
2891
|
+
if (!verify.atCapacity) {
|
|
2892
|
+
process.stderr.write(
|
|
2893
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
2894
|
+
`
|
|
2895
|
+
);
|
|
2896
|
+
void recordSessionKill({
|
|
2897
|
+
sessionName: windowName,
|
|
2898
|
+
agentId,
|
|
2899
|
+
reason: "capacity_false_positive_blocked"
|
|
2900
|
+
});
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
process.stderr.write(
|
|
2904
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
2905
|
+
`
|
|
2906
|
+
);
|
|
2907
|
+
try {
|
|
2908
|
+
transport.kill(windowName);
|
|
2909
|
+
void recordSessionKill({
|
|
2910
|
+
sessionName: windowName,
|
|
2911
|
+
agentId,
|
|
2912
|
+
reason: "capacity"
|
|
2913
|
+
});
|
|
2914
|
+
const client = getClient();
|
|
2915
|
+
const openTasks = await client.execute({
|
|
2916
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
2917
|
+
FROM tasks
|
|
2918
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')
|
|
2919
|
+
ORDER BY
|
|
2920
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
2921
|
+
created_at ASC
|
|
2922
|
+
LIMIT 10`,
|
|
2923
|
+
args: [agentId]
|
|
2924
|
+
});
|
|
2925
|
+
if (openTasks.rows.length === 0) {
|
|
2926
|
+
process.stderr.write(
|
|
2927
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
2928
|
+
`
|
|
2929
|
+
);
|
|
2930
|
+
continue;
|
|
2931
|
+
}
|
|
2932
|
+
const { created } = await createOrRefreshResumeTask(
|
|
2933
|
+
agentId,
|
|
2934
|
+
projectDir,
|
|
2935
|
+
openTasks.rows
|
|
2936
|
+
);
|
|
2937
|
+
if (created) {
|
|
2938
|
+
await writeNotification({
|
|
2939
|
+
agentId: "system",
|
|
2940
|
+
agentRole: "daemon",
|
|
2941
|
+
event: "capacity_relaunch",
|
|
2942
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
2943
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
2947
|
+
if (created) relaunched.push(agentId);
|
|
2948
|
+
} catch (err) {
|
|
2949
|
+
process.stderr.write(
|
|
2950
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
2951
|
+
`
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return relaunched;
|
|
2956
|
+
}
|
|
2957
|
+
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;
|
|
2958
|
+
var init_capacity_monitor = __esm({
|
|
2959
|
+
"src/lib/capacity-monitor.ts"() {
|
|
2960
|
+
"use strict";
|
|
2961
|
+
init_session_registry();
|
|
2962
|
+
init_transport();
|
|
2963
|
+
init_notifications();
|
|
2964
|
+
init_database();
|
|
2965
|
+
init_session_kill_telemetry();
|
|
2966
|
+
init_tmux_routing();
|
|
2967
|
+
CAPACITY_PATTERNS = [
|
|
2968
|
+
/conversation is too long/i,
|
|
2969
|
+
/maximum context length/i,
|
|
2970
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
2971
|
+
/reached.*(?:token|context).*limit/i
|
|
2972
|
+
];
|
|
2973
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
2974
|
+
CONTENT_LINE_MARKERS = [
|
|
2975
|
+
"RESUME:",
|
|
2976
|
+
"intercom",
|
|
2977
|
+
"capacity-monitor",
|
|
2978
|
+
"CAPACITY_PATTERNS",
|
|
2979
|
+
"isAtCapacity",
|
|
2980
|
+
"CONTENT_LINE_MARKERS",
|
|
2981
|
+
"pollCapacityDead",
|
|
2982
|
+
"confirmCapacityKill",
|
|
2983
|
+
"session_kills",
|
|
2984
|
+
"capacity-monitor.test"
|
|
2985
|
+
];
|
|
2986
|
+
SOURCE_CODE_MARKERS = [
|
|
2987
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
2988
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
2989
|
+
];
|
|
2990
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
2991
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
2992
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
2993
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
2994
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
2995
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
2996
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
2997
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
2998
|
+
CTX_FLOOR_PERCENT = 50;
|
|
2945
2999
|
}
|
|
2946
3000
|
});
|
|
2947
3001
|
|
|
2948
3002
|
// src/lib/tmux-routing.ts
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3003
|
+
var tmux_routing_exports = {};
|
|
3004
|
+
__export(tmux_routing_exports, {
|
|
3005
|
+
acquireSpawnLock: () => acquireSpawnLock2,
|
|
3006
|
+
employeeSessionName: () => employeeSessionName,
|
|
3007
|
+
ensureEmployee: () => ensureEmployee,
|
|
3008
|
+
extractRootExe: () => extractRootExe,
|
|
3009
|
+
findFreeInstance: () => findFreeInstance,
|
|
3010
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
3011
|
+
getMySession: () => getMySession,
|
|
3012
|
+
getParentExe: () => getParentExe,
|
|
3013
|
+
getSessionState: () => getSessionState,
|
|
3014
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
3015
|
+
isExeSession: () => isExeSession,
|
|
3016
|
+
isSessionBusy: () => isSessionBusy,
|
|
3017
|
+
notifyParentExe: () => notifyParentExe,
|
|
3018
|
+
parseParentExe: () => parseParentExe,
|
|
3019
|
+
registerParentExe: () => registerParentExe,
|
|
3020
|
+
releaseSpawnLock: () => releaseSpawnLock2,
|
|
3021
|
+
resolveExeSession: () => resolveExeSession,
|
|
3022
|
+
sendIntercom: () => sendIntercom,
|
|
3023
|
+
spawnEmployee: () => spawnEmployee,
|
|
3024
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
3025
|
+
});
|
|
3026
|
+
import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
|
|
3027
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync12, appendFileSync } from "fs";
|
|
3028
|
+
import path13 from "path";
|
|
2952
3029
|
import os6 from "os";
|
|
2953
3030
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2954
3031
|
import { unlinkSync as unlinkSync3 } from "fs";
|
|
2955
3032
|
function spawnLockPath(sessionName) {
|
|
2956
|
-
return
|
|
3033
|
+
return path13.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
2957
3034
|
}
|
|
2958
3035
|
function isProcessAlive(pid) {
|
|
2959
3036
|
try {
|
|
@@ -2964,13 +3041,13 @@ function isProcessAlive(pid) {
|
|
|
2964
3041
|
}
|
|
2965
3042
|
}
|
|
2966
3043
|
function acquireSpawnLock2(sessionName) {
|
|
2967
|
-
if (!
|
|
3044
|
+
if (!existsSync12(SPAWN_LOCK_DIR)) {
|
|
2968
3045
|
mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
|
|
2969
3046
|
}
|
|
2970
3047
|
const lockFile = spawnLockPath(sessionName);
|
|
2971
|
-
if (
|
|
3048
|
+
if (existsSync12(lockFile)) {
|
|
2972
3049
|
try {
|
|
2973
|
-
const lock = JSON.parse(
|
|
3050
|
+
const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
|
|
2974
3051
|
const age = Date.now() - lock.timestamp;
|
|
2975
3052
|
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
2976
3053
|
return false;
|
|
@@ -2990,13 +3067,13 @@ function releaseSpawnLock2(sessionName) {
|
|
|
2990
3067
|
function resolveBehaviorsExporterScript() {
|
|
2991
3068
|
try {
|
|
2992
3069
|
const thisFile = fileURLToPath2(import.meta.url);
|
|
2993
|
-
const scriptPath =
|
|
2994
|
-
|
|
3070
|
+
const scriptPath = path13.join(
|
|
3071
|
+
path13.dirname(thisFile),
|
|
2995
3072
|
"..",
|
|
2996
3073
|
"bin",
|
|
2997
3074
|
"exe-export-behaviors.js"
|
|
2998
3075
|
);
|
|
2999
|
-
return
|
|
3076
|
+
return existsSync12(scriptPath) ? scriptPath : null;
|
|
3000
3077
|
} catch {
|
|
3001
3078
|
return null;
|
|
3002
3079
|
}
|
|
@@ -3023,16 +3100,54 @@ function getMySession() {
|
|
|
3023
3100
|
return getTransport().getMySession();
|
|
3024
3101
|
}
|
|
3025
3102
|
function employeeSessionName(employee, exeSession, instance) {
|
|
3103
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
3104
|
+
const root = extractRootExe(exeSession);
|
|
3105
|
+
if (root) {
|
|
3106
|
+
process.stderr.write(
|
|
3107
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
|
|
3108
|
+
`
|
|
3109
|
+
);
|
|
3110
|
+
exeSession = root;
|
|
3111
|
+
} else {
|
|
3112
|
+
throw new Error(
|
|
3113
|
+
`Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3026
3117
|
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
3027
|
-
|
|
3118
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
3119
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
3120
|
+
throw new Error(
|
|
3121
|
+
`Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
return name;
|
|
3125
|
+
}
|
|
3126
|
+
function parseParentExe(sessionName, agentId) {
|
|
3127
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3128
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
3129
|
+
const match = sessionName.match(regex);
|
|
3130
|
+
return match?.[1] ?? null;
|
|
3028
3131
|
}
|
|
3029
3132
|
function extractRootExe(name) {
|
|
3030
3133
|
const match = name.match(/(exe\d+)$/);
|
|
3031
3134
|
return match?.[1] ?? null;
|
|
3032
3135
|
}
|
|
3136
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
3137
|
+
if (!existsSync12(SESSION_CACHE)) {
|
|
3138
|
+
mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
3139
|
+
}
|
|
3140
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
3141
|
+
const filePath = path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
3142
|
+
writeFileSync4(filePath, JSON.stringify({
|
|
3143
|
+
parentExe: rootExe,
|
|
3144
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
3145
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3146
|
+
}));
|
|
3147
|
+
}
|
|
3033
3148
|
function getParentExe(sessionKey) {
|
|
3034
3149
|
try {
|
|
3035
|
-
const data = JSON.parse(
|
|
3150
|
+
const data = JSON.parse(readFileSync10(path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
3036
3151
|
return data.parentExe || null;
|
|
3037
3152
|
} catch {
|
|
3038
3153
|
return null;
|
|
@@ -3040,8 +3155,8 @@ function getParentExe(sessionKey) {
|
|
|
3040
3155
|
}
|
|
3041
3156
|
function getDispatchedBy(sessionKey) {
|
|
3042
3157
|
try {
|
|
3043
|
-
const data = JSON.parse(
|
|
3044
|
-
|
|
3158
|
+
const data = JSON.parse(readFileSync10(
|
|
3159
|
+
path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
3045
3160
|
"utf8"
|
|
3046
3161
|
));
|
|
3047
3162
|
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
@@ -3072,19 +3187,45 @@ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive =
|
|
|
3072
3187
|
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
3073
3188
|
if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
|
|
3074
3189
|
}
|
|
3075
|
-
return null;
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
3193
|
+
const transport = getTransport();
|
|
3194
|
+
if (!transport.isAlive(sessionName)) {
|
|
3195
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
3196
|
+
}
|
|
3197
|
+
let pane;
|
|
3198
|
+
try {
|
|
3199
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
3200
|
+
} catch (err) {
|
|
3201
|
+
return {
|
|
3202
|
+
atCapacity: false,
|
|
3203
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
3207
|
+
if (!isAtCapacity2(pane)) {
|
|
3208
|
+
return {
|
|
3209
|
+
atCapacity: false,
|
|
3210
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
return {
|
|
3214
|
+
atCapacity: true,
|
|
3215
|
+
reason: "capacity banner matched in recent pane output"
|
|
3216
|
+
};
|
|
3076
3217
|
}
|
|
3077
3218
|
function readDebounceState() {
|
|
3078
3219
|
try {
|
|
3079
|
-
if (!
|
|
3080
|
-
return JSON.parse(
|
|
3220
|
+
if (!existsSync12(DEBOUNCE_FILE)) return {};
|
|
3221
|
+
return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
|
|
3081
3222
|
} catch {
|
|
3082
3223
|
return {};
|
|
3083
3224
|
}
|
|
3084
3225
|
}
|
|
3085
3226
|
function writeDebounceState(state) {
|
|
3086
3227
|
try {
|
|
3087
|
-
if (!
|
|
3228
|
+
if (!existsSync12(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
3088
3229
|
writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
|
|
3089
3230
|
} catch {
|
|
3090
3231
|
}
|
|
@@ -3123,365 +3264,804 @@ function getSessionState(sessionName) {
|
|
|
3123
3264
|
return "no_claude";
|
|
3124
3265
|
}
|
|
3125
3266
|
}
|
|
3126
|
-
if (/Running…/.test(pane)) return "tool";
|
|
3127
|
-
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
3128
|
-
return "idle";
|
|
3129
|
-
} catch {
|
|
3130
|
-
return "offline";
|
|
3267
|
+
if (/Running…/.test(pane)) return "tool";
|
|
3268
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
3269
|
+
return "idle";
|
|
3270
|
+
} catch {
|
|
3271
|
+
return "offline";
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
function isSessionBusy(sessionName) {
|
|
3275
|
+
const state = getSessionState(sessionName);
|
|
3276
|
+
return state === "thinking" || state === "tool";
|
|
3277
|
+
}
|
|
3278
|
+
function isExeSession(sessionName) {
|
|
3279
|
+
return /^exe\d*$/.test(sessionName);
|
|
3280
|
+
}
|
|
3281
|
+
function sendIntercom(targetSession) {
|
|
3282
|
+
const transport = getTransport();
|
|
3283
|
+
if (isExeSession(targetSession)) {
|
|
3284
|
+
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
3285
|
+
return "skipped_exe";
|
|
3286
|
+
}
|
|
3287
|
+
if (isDebounced(targetSession)) {
|
|
3288
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
|
|
3289
|
+
return "debounced";
|
|
3290
|
+
}
|
|
3291
|
+
try {
|
|
3292
|
+
const sessions = transport.listSessions();
|
|
3293
|
+
if (!sessions.includes(targetSession)) {
|
|
3294
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
3295
|
+
return "failed";
|
|
3296
|
+
}
|
|
3297
|
+
const sessionState = getSessionState(targetSession);
|
|
3298
|
+
if (sessionState === "no_claude") {
|
|
3299
|
+
queueIntercom(targetSession, "claude not running in session");
|
|
3300
|
+
recordDebounce(targetSession);
|
|
3301
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
3302
|
+
return "queued";
|
|
3303
|
+
}
|
|
3304
|
+
if (sessionState === "thinking" || sessionState === "tool") {
|
|
3305
|
+
queueIntercom(targetSession, "session busy at send time");
|
|
3306
|
+
recordDebounce(targetSession);
|
|
3307
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
3308
|
+
return "queued";
|
|
3309
|
+
}
|
|
3310
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
3311
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
3312
|
+
transport.sendKeys(targetSession, "q");
|
|
3313
|
+
}
|
|
3314
|
+
transport.sendKeys(targetSession, "/exe-intercom");
|
|
3315
|
+
recordDebounce(targetSession);
|
|
3316
|
+
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
3317
|
+
return "delivered";
|
|
3318
|
+
} catch {
|
|
3319
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
3320
|
+
return "failed";
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
function notifyParentExe(sessionKey) {
|
|
3324
|
+
const target = getDispatchedBy(sessionKey);
|
|
3325
|
+
if (!target) {
|
|
3326
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
3327
|
+
`);
|
|
3328
|
+
return false;
|
|
3329
|
+
}
|
|
3330
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
3331
|
+
`);
|
|
3332
|
+
const result = sendIntercom(target);
|
|
3333
|
+
if (result === "failed") {
|
|
3334
|
+
const rootExe = resolveExeSession();
|
|
3335
|
+
if (rootExe && rootExe !== target) {
|
|
3336
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
3337
|
+
`);
|
|
3338
|
+
const fallback = sendIntercom(rootExe);
|
|
3339
|
+
return fallback !== "failed";
|
|
3340
|
+
}
|
|
3341
|
+
return false;
|
|
3342
|
+
}
|
|
3343
|
+
return true;
|
|
3344
|
+
}
|
|
3345
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
3346
|
+
if (employeeName === "exe") {
|
|
3347
|
+
return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
|
|
3348
|
+
}
|
|
3349
|
+
try {
|
|
3350
|
+
assertEmployeeLimitSync();
|
|
3351
|
+
} catch (err) {
|
|
3352
|
+
if (err instanceof PlanLimitError) {
|
|
3353
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
if (/-exe\d*$/.test(employeeName)) {
|
|
3357
|
+
const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
|
|
3358
|
+
return {
|
|
3359
|
+
status: "failed",
|
|
3360
|
+
sessionName: "",
|
|
3361
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
3365
|
+
const root = extractRootExe(exeSession);
|
|
3366
|
+
if (root) {
|
|
3367
|
+
process.stderr.write(
|
|
3368
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
|
|
3369
|
+
`
|
|
3370
|
+
);
|
|
3371
|
+
exeSession = root;
|
|
3372
|
+
} else {
|
|
3373
|
+
return {
|
|
3374
|
+
status: "failed",
|
|
3375
|
+
sessionName: "",
|
|
3376
|
+
error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
let effectiveInstance = opts?.instance;
|
|
3381
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
3382
|
+
const free = findFreeInstance(
|
|
3383
|
+
employeeName,
|
|
3384
|
+
exeSession,
|
|
3385
|
+
opts.maxAutoInstances ?? 10
|
|
3386
|
+
);
|
|
3387
|
+
if (free === null) {
|
|
3388
|
+
return {
|
|
3389
|
+
status: "failed",
|
|
3390
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
3391
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
3395
|
+
}
|
|
3396
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
3397
|
+
if (isEmployeeAlive(sessionName)) {
|
|
3398
|
+
const result2 = sendIntercom(sessionName);
|
|
3399
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
3400
|
+
return { status: "intercom_sent", sessionName };
|
|
3401
|
+
}
|
|
3402
|
+
if (result2 === "delivered") {
|
|
3403
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
3404
|
+
}
|
|
3405
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
3406
|
+
}
|
|
3407
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
3408
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
3409
|
+
if (result.error) {
|
|
3410
|
+
return { status: "failed", sessionName, error: result.error };
|
|
3411
|
+
}
|
|
3412
|
+
return { status: "spawned", sessionName };
|
|
3413
|
+
}
|
|
3414
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
3415
|
+
const transport = getTransport();
|
|
3416
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
3417
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
3418
|
+
const logDir = path13.join(os6.homedir(), ".exe-os", "session-logs");
|
|
3419
|
+
const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
3420
|
+
if (!existsSync12(logDir)) {
|
|
3421
|
+
mkdirSync5(logDir, { recursive: true });
|
|
3422
|
+
}
|
|
3423
|
+
transport.kill(sessionName);
|
|
3424
|
+
let cleanupSuffix = "";
|
|
3425
|
+
try {
|
|
3426
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
3427
|
+
const cleanupScript = path13.join(path13.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
3428
|
+
if (existsSync12(cleanupScript)) {
|
|
3429
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
3430
|
+
}
|
|
3431
|
+
} catch {
|
|
3432
|
+
}
|
|
3433
|
+
try {
|
|
3434
|
+
const claudeJsonPath = path13.join(os6.homedir(), ".claude.json");
|
|
3435
|
+
let claudeJson = {};
|
|
3436
|
+
try {
|
|
3437
|
+
claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
|
|
3438
|
+
} catch {
|
|
3439
|
+
}
|
|
3440
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
3441
|
+
const projects = claudeJson.projects;
|
|
3442
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
3443
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
3444
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
3445
|
+
writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
3446
|
+
} catch {
|
|
3447
|
+
}
|
|
3448
|
+
try {
|
|
3449
|
+
const settingsDir = path13.join(os6.homedir(), ".claude", "projects");
|
|
3450
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
3451
|
+
const projSettingsDir = path13.join(settingsDir, normalizedKey);
|
|
3452
|
+
const settingsPath = path13.join(projSettingsDir, "settings.json");
|
|
3453
|
+
let settings = {};
|
|
3454
|
+
try {
|
|
3455
|
+
settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
|
|
3456
|
+
} catch {
|
|
3457
|
+
}
|
|
3458
|
+
const perms = settings.permissions ?? {};
|
|
3459
|
+
const allow = perms.allow ?? [];
|
|
3460
|
+
const toolNames = [
|
|
3461
|
+
"recall_my_memory",
|
|
3462
|
+
"store_memory",
|
|
3463
|
+
"create_task",
|
|
3464
|
+
"update_task",
|
|
3465
|
+
"list_tasks",
|
|
3466
|
+
"get_task",
|
|
3467
|
+
"ask_team_memory",
|
|
3468
|
+
"store_behavior",
|
|
3469
|
+
"get_identity",
|
|
3470
|
+
"send_message"
|
|
3471
|
+
];
|
|
3472
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
3473
|
+
let changed = false;
|
|
3474
|
+
for (const tool of requiredTools) {
|
|
3475
|
+
if (!allow.includes(tool)) {
|
|
3476
|
+
allow.push(tool);
|
|
3477
|
+
changed = true;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
if (changed) {
|
|
3481
|
+
perms.allow = allow;
|
|
3482
|
+
settings.permissions = perms;
|
|
3483
|
+
mkdirSync5(projSettingsDir, { recursive: true });
|
|
3484
|
+
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3485
|
+
}
|
|
3486
|
+
} catch {
|
|
3487
|
+
}
|
|
3488
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
3489
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
3490
|
+
const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
3491
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
3492
|
+
let identityFlag = "";
|
|
3493
|
+
let behaviorsFlag = "";
|
|
3494
|
+
let legacyFallbackWarned = false;
|
|
3495
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
3496
|
+
const identityPath = path13.join(
|
|
3497
|
+
os6.homedir(),
|
|
3498
|
+
".exe-os",
|
|
3499
|
+
"identity",
|
|
3500
|
+
`${employeeName}.md`
|
|
3501
|
+
);
|
|
3502
|
+
_resetCcAgentSupportCache();
|
|
3503
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
3504
|
+
if (hasAgentFlag) {
|
|
3505
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
3506
|
+
} else if (existsSync12(identityPath)) {
|
|
3507
|
+
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
3508
|
+
legacyFallbackWarned = true;
|
|
3509
|
+
}
|
|
3510
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
3511
|
+
employeeName,
|
|
3512
|
+
path13.basename(spawnCwd),
|
|
3513
|
+
sessionName
|
|
3514
|
+
);
|
|
3515
|
+
if (behaviorsFile) {
|
|
3516
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
if (legacyFallbackWarned) {
|
|
3520
|
+
process.stderr.write(
|
|
3521
|
+
`[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.
|
|
3522
|
+
`
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
let sessionContextFlag = "";
|
|
3526
|
+
try {
|
|
3527
|
+
const ctxDir = path13.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3528
|
+
mkdirSync5(ctxDir, { recursive: true });
|
|
3529
|
+
const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
|
|
3530
|
+
const ctxContent = [
|
|
3531
|
+
`## Session Context`,
|
|
3532
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
3533
|
+
`Your parent exe session is ${exeSession}.`,
|
|
3534
|
+
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
3535
|
+
].join("\n");
|
|
3536
|
+
writeFileSync4(ctxFile, ctxContent);
|
|
3537
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
3538
|
+
} catch {
|
|
3539
|
+
}
|
|
3540
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
3541
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
3542
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
3543
|
+
if (cfg?.apiKeyEnv) {
|
|
3544
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
3545
|
+
if (keyVal) {
|
|
3546
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3131
3549
|
}
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3550
|
+
let spawnCommand;
|
|
3551
|
+
if (useExeAgent) {
|
|
3552
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
3553
|
+
} else if (useBinSymlink) {
|
|
3554
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
3555
|
+
process.stderr.write(
|
|
3556
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
3557
|
+
`
|
|
3558
|
+
);
|
|
3559
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
3560
|
+
} else {
|
|
3561
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
3141
3562
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3563
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
3564
|
+
cwd: spawnCwd,
|
|
3565
|
+
command: spawnCommand
|
|
3566
|
+
});
|
|
3567
|
+
if (spawnResult.error) {
|
|
3568
|
+
releaseSpawnLock2(sessionName);
|
|
3569
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
3145
3570
|
}
|
|
3571
|
+
transport.pipeLog(sessionName, logFile);
|
|
3146
3572
|
try {
|
|
3147
|
-
const
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
3163
|
-
return "queued";
|
|
3573
|
+
const mySession = getMySession();
|
|
3574
|
+
const dispatchInfo = path13.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
3575
|
+
writeFileSync4(dispatchInfo, JSON.stringify({
|
|
3576
|
+
dispatchedBy: mySession,
|
|
3577
|
+
rootExe: exeSession,
|
|
3578
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
3579
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3580
|
+
}));
|
|
3581
|
+
} catch {
|
|
3582
|
+
}
|
|
3583
|
+
let booted = false;
|
|
3584
|
+
for (let i = 0; i < 30; i++) {
|
|
3585
|
+
try {
|
|
3586
|
+
execSync6("sleep 0.5");
|
|
3587
|
+
} catch {
|
|
3164
3588
|
}
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3589
|
+
try {
|
|
3590
|
+
const pane = transport.capturePane(sessionName);
|
|
3591
|
+
if (useExeAgent) {
|
|
3592
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
3593
|
+
booted = true;
|
|
3594
|
+
break;
|
|
3595
|
+
}
|
|
3596
|
+
} else {
|
|
3597
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
3598
|
+
booted = true;
|
|
3599
|
+
break;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
} catch {
|
|
3168
3603
|
}
|
|
3169
|
-
transport.sendKeys(targetSession, "/exe-intercom");
|
|
3170
|
-
recordDebounce(targetSession);
|
|
3171
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
3172
|
-
return "delivered";
|
|
3173
|
-
} catch {
|
|
3174
|
-
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
3175
|
-
return "failed";
|
|
3176
3604
|
}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
if (!target) {
|
|
3181
|
-
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
3182
|
-
`);
|
|
3183
|
-
return false;
|
|
3605
|
+
if (!booted) {
|
|
3606
|
+
releaseSpawnLock2(sessionName);
|
|
3607
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
3184
3608
|
}
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
const rootExe = resolveExeSession();
|
|
3190
|
-
if (rootExe && rootExe !== target) {
|
|
3191
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
3192
|
-
`);
|
|
3193
|
-
const fallback = sendIntercom(rootExe);
|
|
3194
|
-
return fallback !== "failed";
|
|
3609
|
+
if (!useExeAgent) {
|
|
3610
|
+
try {
|
|
3611
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
3612
|
+
} catch {
|
|
3195
3613
|
}
|
|
3196
|
-
return false;
|
|
3197
3614
|
}
|
|
3198
|
-
|
|
3615
|
+
registerSession({
|
|
3616
|
+
windowName: sessionName,
|
|
3617
|
+
agentId: employeeName,
|
|
3618
|
+
projectDir: spawnCwd,
|
|
3619
|
+
parentExe: exeSession,
|
|
3620
|
+
pid: 0,
|
|
3621
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3622
|
+
});
|
|
3623
|
+
releaseSpawnLock2(sessionName);
|
|
3624
|
+
return { sessionName };
|
|
3199
3625
|
}
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3626
|
+
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;
|
|
3627
|
+
var init_tmux_routing = __esm({
|
|
3628
|
+
"src/lib/tmux-routing.ts"() {
|
|
3629
|
+
"use strict";
|
|
3630
|
+
init_session_registry();
|
|
3631
|
+
init_session_key();
|
|
3632
|
+
init_transport();
|
|
3633
|
+
init_cc_agent_support();
|
|
3634
|
+
init_mcp_prefix();
|
|
3635
|
+
init_provider_table();
|
|
3636
|
+
init_intercom_queue();
|
|
3637
|
+
init_plan_limits();
|
|
3638
|
+
SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
3639
|
+
SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3640
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
3641
|
+
VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
|
|
3642
|
+
VERIFY_PANE_LINES = 200;
|
|
3643
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
3644
|
+
INTERCOM_LOG2 = path13.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
3645
|
+
DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3646
|
+
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3647
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
3203
3648
|
}
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3649
|
+
});
|
|
3650
|
+
|
|
3651
|
+
// src/lib/tasks-crud.ts
|
|
3652
|
+
import crypto5 from "crypto";
|
|
3653
|
+
import path14 from "path";
|
|
3654
|
+
import { execSync as execSync7 } from "child_process";
|
|
3655
|
+
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
3656
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
3657
|
+
async function writeCheckpoint(input2) {
|
|
3658
|
+
const client = getClient();
|
|
3659
|
+
const row = await resolveTask(client, input2.taskId);
|
|
3660
|
+
const taskId = String(row.id);
|
|
3661
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3662
|
+
const blockedByIds = [];
|
|
3663
|
+
if (row.blocked_by) {
|
|
3664
|
+
blockedByIds.push(String(row.blocked_by));
|
|
3210
3665
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3666
|
+
const checkpoint = {
|
|
3667
|
+
step: input2.step,
|
|
3668
|
+
context_summary: input2.contextSummary,
|
|
3669
|
+
files_touched: input2.filesTouched ?? [],
|
|
3670
|
+
blocked_by_ids: blockedByIds,
|
|
3671
|
+
last_checkpoint_at: now
|
|
3672
|
+
};
|
|
3673
|
+
const result = await client.execute({
|
|
3674
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
3675
|
+
args: [JSON.stringify(checkpoint), now, taskId]
|
|
3676
|
+
});
|
|
3677
|
+
if (result.rowsAffected === 0) {
|
|
3678
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
3218
3679
|
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3680
|
+
const countResult = await client.execute({
|
|
3681
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
3682
|
+
args: [taskId]
|
|
3683
|
+
});
|
|
3684
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
3685
|
+
return { checkpointCount };
|
|
3686
|
+
}
|
|
3687
|
+
function extractParentFromContext(contextBody) {
|
|
3688
|
+
if (!contextBody) return null;
|
|
3689
|
+
const match = contextBody.match(
|
|
3690
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
3691
|
+
);
|
|
3692
|
+
return match ? match[1].toLowerCase() : null;
|
|
3693
|
+
}
|
|
3694
|
+
function slugify(title) {
|
|
3695
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
3696
|
+
}
|
|
3697
|
+
async function resolveTask(client, identifier) {
|
|
3698
|
+
let result = await client.execute({
|
|
3699
|
+
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
3700
|
+
args: [identifier]
|
|
3701
|
+
});
|
|
3702
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3703
|
+
result = await client.execute({
|
|
3704
|
+
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
3705
|
+
args: [`%${identifier}%`]
|
|
3706
|
+
});
|
|
3707
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3708
|
+
if (result.rows.length > 1) {
|
|
3709
|
+
const exact = result.rows.filter(
|
|
3710
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
3711
|
+
);
|
|
3712
|
+
if (exact.length === 1) return exact[0];
|
|
3713
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
3714
|
+
const active = candidates.filter(
|
|
3715
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3716
|
+
);
|
|
3717
|
+
if (active.length === 1) return active[0];
|
|
3718
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3719
|
+
throw new Error(
|
|
3720
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3225
3721
|
);
|
|
3226
|
-
if (free === null) {
|
|
3227
|
-
return {
|
|
3228
|
-
status: "failed",
|
|
3229
|
-
sessionName: employeeSessionName(employeeName, exeSession),
|
|
3230
|
-
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
3231
|
-
};
|
|
3232
|
-
}
|
|
3233
|
-
effectiveInstance = free === 0 ? void 0 : free;
|
|
3234
|
-
}
|
|
3235
|
-
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
3236
|
-
if (isEmployeeAlive(sessionName)) {
|
|
3237
|
-
const result2 = sendIntercom(sessionName);
|
|
3238
|
-
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
3239
|
-
return { status: "intercom_sent", sessionName };
|
|
3240
|
-
}
|
|
3241
|
-
if (result2 === "delivered") {
|
|
3242
|
-
return { status: "intercom_unprocessed", sessionName };
|
|
3243
|
-
}
|
|
3244
|
-
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
3245
3722
|
}
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3723
|
+
result = await client.execute({
|
|
3724
|
+
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
3725
|
+
args: [`%${identifier}%`]
|
|
3726
|
+
});
|
|
3727
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3728
|
+
if (result.rows.length > 1) {
|
|
3729
|
+
const active = result.rows.filter(
|
|
3730
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3731
|
+
);
|
|
3732
|
+
if (active.length === 1) return active[0];
|
|
3733
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3734
|
+
throw new Error(
|
|
3735
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3736
|
+
);
|
|
3250
3737
|
}
|
|
3251
|
-
|
|
3738
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
3252
3739
|
}
|
|
3253
|
-
function
|
|
3254
|
-
const
|
|
3255
|
-
const
|
|
3256
|
-
const
|
|
3257
|
-
const
|
|
3258
|
-
const
|
|
3259
|
-
|
|
3260
|
-
|
|
3740
|
+
async function createTaskCore(input2) {
|
|
3741
|
+
const client = getClient();
|
|
3742
|
+
const id = crypto5.randomUUID();
|
|
3743
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3744
|
+
const slug = slugify(input2.title);
|
|
3745
|
+
const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
|
|
3746
|
+
let blockedById = null;
|
|
3747
|
+
const initialStatus = input2.blockedBy ? "blocked" : "open";
|
|
3748
|
+
if (input2.blockedBy) {
|
|
3749
|
+
const blocker = await resolveTask(client, input2.blockedBy);
|
|
3750
|
+
blockedById = String(blocker.id);
|
|
3261
3751
|
}
|
|
3262
|
-
|
|
3263
|
-
let
|
|
3264
|
-
|
|
3265
|
-
const
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3752
|
+
let parentTaskId = null;
|
|
3753
|
+
let parentRef = input2.parentTaskId;
|
|
3754
|
+
if (!parentRef) {
|
|
3755
|
+
const extracted = extractParentFromContext(input2.context);
|
|
3756
|
+
if (extracted) {
|
|
3757
|
+
parentRef = extracted;
|
|
3758
|
+
process.stderr.write(
|
|
3759
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
3760
|
+
);
|
|
3269
3761
|
}
|
|
3270
|
-
} catch {
|
|
3271
3762
|
}
|
|
3272
|
-
|
|
3273
|
-
const claudeJsonPath = path14.join(os6.homedir(), ".claude.json");
|
|
3274
|
-
let claudeJson = {};
|
|
3763
|
+
if (parentRef) {
|
|
3275
3764
|
try {
|
|
3276
|
-
|
|
3277
|
-
|
|
3765
|
+
const parent = await resolveTask(client, parentRef);
|
|
3766
|
+
parentTaskId = String(parent.id);
|
|
3767
|
+
} catch (err) {
|
|
3768
|
+
if (!input2.parentTaskId) {
|
|
3769
|
+
throw new Error(
|
|
3770
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
3771
|
+
);
|
|
3772
|
+
}
|
|
3773
|
+
throw err;
|
|
3278
3774
|
}
|
|
3279
|
-
if (!claudeJson.projects) claudeJson.projects = {};
|
|
3280
|
-
const projects = claudeJson.projects;
|
|
3281
|
-
const trustDir = opts?.cwd ?? projectDir;
|
|
3282
|
-
if (!projects[trustDir]) projects[trustDir] = {};
|
|
3283
|
-
projects[trustDir].hasTrustDialogAccepted = true;
|
|
3284
|
-
writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
3285
|
-
} catch {
|
|
3286
3775
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3776
|
+
let warning;
|
|
3777
|
+
const dupCheck = await client.execute({
|
|
3778
|
+
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
3779
|
+
args: [input2.title, input2.assignedTo]
|
|
3780
|
+
});
|
|
3781
|
+
if (dupCheck.rows.length > 0) {
|
|
3782
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
3783
|
+
}
|
|
3784
|
+
if (input2.baseDir) {
|
|
3293
3785
|
try {
|
|
3294
|
-
|
|
3786
|
+
await mkdir4(path14.join(input2.baseDir, "exe", "output"), { recursive: true });
|
|
3787
|
+
await mkdir4(path14.join(input2.baseDir, "exe", "research"), { recursive: true });
|
|
3788
|
+
await ensureArchitectureDoc(input2.baseDir, input2.projectName);
|
|
3789
|
+
await ensureGitignoreExe(input2.baseDir);
|
|
3295
3790
|
} catch {
|
|
3296
3791
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
"update_task",
|
|
3304
|
-
"list_tasks",
|
|
3305
|
-
"get_task",
|
|
3306
|
-
"ask_team_memory",
|
|
3307
|
-
"store_behavior",
|
|
3308
|
-
"get_identity",
|
|
3309
|
-
"send_message"
|
|
3310
|
-
];
|
|
3311
|
-
const requiredTools = expandDualPrefixTools(toolNames);
|
|
3312
|
-
let changed = false;
|
|
3313
|
-
for (const tool of requiredTools) {
|
|
3314
|
-
if (!allow.includes(tool)) {
|
|
3315
|
-
allow.push(tool);
|
|
3316
|
-
changed = true;
|
|
3317
|
-
}
|
|
3318
|
-
}
|
|
3319
|
-
if (changed) {
|
|
3320
|
-
perms.allow = allow;
|
|
3321
|
-
settings.permissions = perms;
|
|
3322
|
-
mkdirSync5(projSettingsDir, { recursive: true });
|
|
3323
|
-
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3324
|
-
}
|
|
3792
|
+
}
|
|
3793
|
+
const complexity = input2.complexity ?? "standard";
|
|
3794
|
+
let sessionScope = null;
|
|
3795
|
+
try {
|
|
3796
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3797
|
+
sessionScope = resolveExeSession2();
|
|
3325
3798
|
} catch {
|
|
3326
3799
|
}
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3800
|
+
await client.execute({
|
|
3801
|
+
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)
|
|
3802
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3803
|
+
args: [
|
|
3804
|
+
id,
|
|
3805
|
+
input2.title,
|
|
3806
|
+
input2.assignedTo,
|
|
3807
|
+
input2.assignedBy,
|
|
3808
|
+
input2.projectName,
|
|
3809
|
+
input2.priority,
|
|
3810
|
+
initialStatus,
|
|
3811
|
+
taskFile,
|
|
3812
|
+
blockedById,
|
|
3813
|
+
parentTaskId,
|
|
3814
|
+
input2.reviewer ?? null,
|
|
3815
|
+
input2.context,
|
|
3816
|
+
complexity,
|
|
3817
|
+
input2.budgetTokens ?? null,
|
|
3818
|
+
input2.budgetFallbackModel ?? null,
|
|
3819
|
+
0,
|
|
3820
|
+
null,
|
|
3821
|
+
sessionScope,
|
|
3822
|
+
now,
|
|
3823
|
+
now
|
|
3824
|
+
]
|
|
3825
|
+
});
|
|
3826
|
+
return {
|
|
3827
|
+
id,
|
|
3828
|
+
title: input2.title,
|
|
3829
|
+
assignedTo: input2.assignedTo,
|
|
3830
|
+
assignedBy: input2.assignedBy,
|
|
3831
|
+
projectName: input2.projectName,
|
|
3832
|
+
priority: input2.priority,
|
|
3833
|
+
status: initialStatus,
|
|
3834
|
+
taskFile,
|
|
3835
|
+
createdAt: now,
|
|
3836
|
+
updatedAt: now,
|
|
3837
|
+
warning,
|
|
3838
|
+
budgetTokens: input2.budgetTokens ?? null,
|
|
3839
|
+
budgetFallbackModel: input2.budgetFallbackModel ?? null,
|
|
3840
|
+
tokensUsed: 0,
|
|
3841
|
+
tokensWarnedAt: null
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3844
|
+
async function listTasks(input2) {
|
|
3845
|
+
const client = getClient();
|
|
3846
|
+
const conditions = [];
|
|
3847
|
+
const args = [];
|
|
3848
|
+
if (input2.assignedTo) {
|
|
3849
|
+
conditions.push("assigned_to = ?");
|
|
3850
|
+
args.push(input2.assignedTo);
|
|
3357
3851
|
}
|
|
3358
|
-
if (
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
);
|
|
3852
|
+
if (input2.status) {
|
|
3853
|
+
conditions.push("status = ?");
|
|
3854
|
+
args.push(input2.status);
|
|
3855
|
+
} else {
|
|
3856
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
3857
|
+
}
|
|
3858
|
+
if (input2.projectName) {
|
|
3859
|
+
conditions.push("project_name = ?");
|
|
3860
|
+
args.push(input2.projectName);
|
|
3861
|
+
}
|
|
3862
|
+
if (input2.priority) {
|
|
3863
|
+
conditions.push("priority = ?");
|
|
3864
|
+
args.push(input2.priority);
|
|
3363
3865
|
}
|
|
3364
|
-
let sessionContextFlag = "";
|
|
3365
3866
|
try {
|
|
3366
|
-
const
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
`Your parent exe session is ${exeSession}.`,
|
|
3373
|
-
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
3374
|
-
].join("\n");
|
|
3375
|
-
writeFileSync4(ctxFile, ctxContent);
|
|
3376
|
-
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
3867
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3868
|
+
const session = resolveExeSession2();
|
|
3869
|
+
if (session) {
|
|
3870
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
3871
|
+
args.push(session);
|
|
3872
|
+
}
|
|
3377
3873
|
} catch {
|
|
3378
3874
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3875
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3876
|
+
const result = await client.execute({
|
|
3877
|
+
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`,
|
|
3878
|
+
args
|
|
3879
|
+
});
|
|
3880
|
+
return result.rows.map((r) => ({
|
|
3881
|
+
id: String(r.id),
|
|
3882
|
+
title: String(r.title),
|
|
3883
|
+
assignedTo: String(r.assigned_to),
|
|
3884
|
+
assignedBy: String(r.assigned_by),
|
|
3885
|
+
projectName: String(r.project_name),
|
|
3886
|
+
priority: String(r.priority),
|
|
3887
|
+
status: String(r.status),
|
|
3888
|
+
taskFile: String(r.task_file),
|
|
3889
|
+
createdAt: String(r.created_at),
|
|
3890
|
+
updatedAt: String(r.updated_at),
|
|
3891
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
3892
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
3893
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
3894
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
3895
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
3896
|
+
}));
|
|
3897
|
+
}
|
|
3898
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
3899
|
+
if (!taskContext) return null;
|
|
3900
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
3901
|
+
try {
|
|
3902
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
3903
|
+
const branch = execSync7(
|
|
3904
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
3905
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
3906
|
+
).trim();
|
|
3907
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
3908
|
+
const commitCount = execSync7(
|
|
3909
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
3910
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
3911
|
+
).trim();
|
|
3912
|
+
const count = parseInt(commitCount, 10);
|
|
3913
|
+
if (count === 0) {
|
|
3914
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
3387
3915
|
}
|
|
3916
|
+
return null;
|
|
3917
|
+
} catch {
|
|
3918
|
+
return null;
|
|
3388
3919
|
}
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3920
|
+
}
|
|
3921
|
+
async function updateTaskStatus(input2) {
|
|
3922
|
+
const client = getClient();
|
|
3923
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3924
|
+
const row = await resolveTask(client, input2.taskId);
|
|
3925
|
+
const taskId = String(row.id);
|
|
3926
|
+
const taskFile = String(row.task_file);
|
|
3927
|
+
if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
3394
3928
|
process.stderr.write(
|
|
3395
|
-
`[
|
|
3929
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
3396
3930
|
`
|
|
3397
3931
|
);
|
|
3398
|
-
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
3399
|
-
} else {
|
|
3400
|
-
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
3401
|
-
}
|
|
3402
|
-
const spawnResult = transport.spawn(sessionName, {
|
|
3403
|
-
cwd: spawnCwd,
|
|
3404
|
-
command: spawnCommand
|
|
3405
|
-
});
|
|
3406
|
-
if (spawnResult.error) {
|
|
3407
|
-
releaseSpawnLock2(sessionName);
|
|
3408
|
-
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
3409
3932
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3933
|
+
if (input2.status === "done") {
|
|
3934
|
+
const existingRow = await client.execute({
|
|
3935
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
3936
|
+
args: [taskId]
|
|
3937
|
+
});
|
|
3938
|
+
if (existingRow.rows.length > 0) {
|
|
3939
|
+
const ctx = existingRow.rows[0];
|
|
3940
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
3941
|
+
if (warning) {
|
|
3942
|
+
input2.result = input2.result ? `\u26A0\uFE0F ${warning}
|
|
3943
|
+
|
|
3944
|
+
${input2.result}` : `\u26A0\uFE0F ${warning}`;
|
|
3945
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
3946
|
+
`);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3421
3949
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3950
|
+
if (input2.status === "in_progress") {
|
|
3951
|
+
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
3952
|
+
const claim = await client.execute({
|
|
3953
|
+
sql: `UPDATE tasks
|
|
3954
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
3955
|
+
WHERE id = ? AND status = 'open'`,
|
|
3956
|
+
args: [tmuxSession, now, taskId]
|
|
3957
|
+
});
|
|
3958
|
+
if (claim.rowsAffected === 0) {
|
|
3959
|
+
const current = await client.execute({
|
|
3960
|
+
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
3961
|
+
args: [taskId]
|
|
3962
|
+
});
|
|
3963
|
+
const cur = current.rows[0];
|
|
3964
|
+
const status = cur?.status ?? "unknown";
|
|
3965
|
+
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
3966
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
3427
3967
|
}
|
|
3428
3968
|
try {
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
}
|
|
3435
|
-
} else {
|
|
3436
|
-
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
3437
|
-
booted = true;
|
|
3438
|
-
break;
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3969
|
+
await writeCheckpoint({
|
|
3970
|
+
taskId,
|
|
3971
|
+
step: "claimed",
|
|
3972
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
3973
|
+
});
|
|
3441
3974
|
} catch {
|
|
3442
3975
|
}
|
|
3976
|
+
return { row, taskFile, now, taskId };
|
|
3443
3977
|
}
|
|
3444
|
-
if (
|
|
3445
|
-
|
|
3446
|
-
|
|
3978
|
+
if (input2.result) {
|
|
3979
|
+
await client.execute({
|
|
3980
|
+
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
3981
|
+
args: [input2.status, input2.result, now, taskId]
|
|
3982
|
+
});
|
|
3983
|
+
} else {
|
|
3984
|
+
await client.execute({
|
|
3985
|
+
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
3986
|
+
args: [input2.status, now, taskId]
|
|
3987
|
+
});
|
|
3447
3988
|
}
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3989
|
+
try {
|
|
3990
|
+
await writeCheckpoint({
|
|
3991
|
+
taskId,
|
|
3992
|
+
step: `status_transition:${input2.status}`,
|
|
3993
|
+
contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
|
|
3994
|
+
});
|
|
3995
|
+
} catch {
|
|
3996
|
+
}
|
|
3997
|
+
return { row, taskFile, now, taskId };
|
|
3998
|
+
}
|
|
3999
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
4000
|
+
const client = getClient();
|
|
4001
|
+
const row = await resolveTask(client, taskId);
|
|
4002
|
+
const id = String(row.id);
|
|
4003
|
+
const taskFile = String(row.task_file);
|
|
4004
|
+
const assignedTo = String(row.assigned_to);
|
|
4005
|
+
const assignedBy = String(row.assigned_by);
|
|
4006
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
4007
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
4008
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
4009
|
+
}
|
|
4010
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
4011
|
+
const archPath = path14.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
4012
|
+
try {
|
|
4013
|
+
if (existsSync13(archPath)) return;
|
|
4014
|
+
const template = [
|
|
4015
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
4016
|
+
"",
|
|
4017
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
4018
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
4019
|
+
"",
|
|
4020
|
+
"## Overview",
|
|
4021
|
+
"",
|
|
4022
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
4023
|
+
"",
|
|
4024
|
+
"## Key Components",
|
|
4025
|
+
"",
|
|
4026
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
4027
|
+
"",
|
|
4028
|
+
"## Data Flow",
|
|
4029
|
+
"",
|
|
4030
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
4031
|
+
"",
|
|
4032
|
+
"## Invariants",
|
|
4033
|
+
"",
|
|
4034
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
4035
|
+
"",
|
|
4036
|
+
"## Dependencies",
|
|
4037
|
+
"",
|
|
4038
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
4039
|
+
""
|
|
4040
|
+
].join("\n");
|
|
4041
|
+
await writeFile4(archPath, template, "utf-8");
|
|
4042
|
+
} catch {
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
async function ensureGitignoreExe(baseDir) {
|
|
4046
|
+
const gitignorePath = path14.join(baseDir, ".gitignore");
|
|
4047
|
+
try {
|
|
4048
|
+
if (existsSync13(gitignorePath)) {
|
|
4049
|
+
const content = readFileSync11(gitignorePath, "utf-8");
|
|
4050
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
4051
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
4052
|
+
} else {
|
|
4053
|
+
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
3452
4054
|
}
|
|
4055
|
+
} catch {
|
|
3453
4056
|
}
|
|
3454
|
-
registerSession({
|
|
3455
|
-
windowName: sessionName,
|
|
3456
|
-
agentId: employeeName,
|
|
3457
|
-
projectDir: spawnCwd,
|
|
3458
|
-
parentExe: exeSession,
|
|
3459
|
-
pid: 0,
|
|
3460
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3461
|
-
});
|
|
3462
|
-
releaseSpawnLock2(sessionName);
|
|
3463
|
-
return { sessionName };
|
|
3464
4057
|
}
|
|
3465
|
-
var
|
|
3466
|
-
var
|
|
3467
|
-
"src/lib/
|
|
4058
|
+
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
4059
|
+
var init_tasks_crud = __esm({
|
|
4060
|
+
"src/lib/tasks-crud.ts"() {
|
|
3468
4061
|
"use strict";
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
init_cc_agent_support();
|
|
3473
|
-
init_mcp_prefix();
|
|
3474
|
-
init_provider_table();
|
|
3475
|
-
init_intercom_queue();
|
|
3476
|
-
init_plan_limits();
|
|
3477
|
-
SPAWN_LOCK_DIR = path14.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
3478
|
-
SESSION_CACHE = path14.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3479
|
-
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
3480
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
3481
|
-
INTERCOM_LOG2 = path14.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
3482
|
-
DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3483
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3484
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
4062
|
+
init_database();
|
|
4063
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
4064
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
3485
4065
|
}
|
|
3486
4066
|
});
|
|
3487
4067
|
|
|
@@ -3515,6 +4095,34 @@ async function listPendingReviews(limit) {
|
|
|
3515
4095
|
});
|
|
3516
4096
|
return result.rows;
|
|
3517
4097
|
}
|
|
4098
|
+
async function cleanupOrphanedReviews() {
|
|
4099
|
+
const client = getClient();
|
|
4100
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4101
|
+
const r1 = await client.execute({
|
|
4102
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
4103
|
+
WHERE status = 'needs_review'
|
|
4104
|
+
AND assigned_by = 'system'
|
|
4105
|
+
AND title LIKE 'Review:%'
|
|
4106
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
|
|
4107
|
+
args: [now]
|
|
4108
|
+
});
|
|
4109
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
4110
|
+
const r2 = await client.execute({
|
|
4111
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
4112
|
+
WHERE status = 'needs_review'
|
|
4113
|
+
AND result IS NOT NULL
|
|
4114
|
+
AND updated_at < ?`,
|
|
4115
|
+
args: [now, staleThreshold]
|
|
4116
|
+
});
|
|
4117
|
+
const total = r1.rowsAffected + r2.rowsAffected;
|
|
4118
|
+
if (total > 0) {
|
|
4119
|
+
process.stderr.write(
|
|
4120
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
|
|
4121
|
+
`
|
|
4122
|
+
);
|
|
4123
|
+
}
|
|
4124
|
+
return total;
|
|
4125
|
+
}
|
|
3518
4126
|
function getReviewChecklist(role, agent, taskSlug) {
|
|
3519
4127
|
const roleLower = role.toLowerCase();
|
|
3520
4128
|
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
@@ -3628,6 +4236,7 @@ var init_tasks_review = __esm({
|
|
|
3628
4236
|
init_tasks_crud();
|
|
3629
4237
|
init_tmux_routing();
|
|
3630
4238
|
init_session_key();
|
|
4239
|
+
init_state_bus();
|
|
3631
4240
|
}
|
|
3632
4241
|
});
|
|
3633
4242
|
|
|
@@ -3750,13 +4359,12 @@ function assertSessionScope(actionType, targetProject) {
|
|
|
3750
4359
|
};
|
|
3751
4360
|
}
|
|
3752
4361
|
process.stderr.write(
|
|
3753
|
-
`[session-scope]
|
|
4362
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
3754
4363
|
`
|
|
3755
4364
|
);
|
|
3756
4365
|
return {
|
|
3757
|
-
allowed:
|
|
3758
|
-
|
|
3759
|
-
reason: "cross_session_granted",
|
|
4366
|
+
allowed: false,
|
|
4367
|
+
reason: "cross_session_denied",
|
|
3760
4368
|
currentProject,
|
|
3761
4369
|
targetProject,
|
|
3762
4370
|
targetSession: findSessionForProject(targetProject)?.windowName
|
|
@@ -3782,8 +4390,9 @@ async function dispatchTaskToEmployee(input2) {
|
|
|
3782
4390
|
try {
|
|
3783
4391
|
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
3784
4392
|
const check = assertSessionScope2("dispatch_task", input2.projectName);
|
|
3785
|
-
if (check.reason === "
|
|
4393
|
+
if (check.reason === "cross_session_denied") {
|
|
3786
4394
|
crossProject = true;
|
|
4395
|
+
return { dispatched: "skipped", crossProject: true };
|
|
3787
4396
|
}
|
|
3788
4397
|
} catch {
|
|
3789
4398
|
}
|
|
@@ -3840,10 +4449,10 @@ var init_tasks_notify = __esm({
|
|
|
3840
4449
|
});
|
|
3841
4450
|
|
|
3842
4451
|
// src/lib/behaviors.ts
|
|
3843
|
-
import
|
|
4452
|
+
import crypto6 from "crypto";
|
|
3844
4453
|
async function storeBehavior(opts) {
|
|
3845
4454
|
const client = getClient();
|
|
3846
|
-
const id =
|
|
4455
|
+
const id = crypto6.randomUUID();
|
|
3847
4456
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3848
4457
|
await client.execute({
|
|
3849
4458
|
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
@@ -3872,7 +4481,7 @@ __export(skill_learning_exports, {
|
|
|
3872
4481
|
storeTrajectory: () => storeTrajectory,
|
|
3873
4482
|
sweepTrajectories: () => sweepTrajectories
|
|
3874
4483
|
});
|
|
3875
|
-
import
|
|
4484
|
+
import crypto7 from "crypto";
|
|
3876
4485
|
async function extractTrajectory(taskId, agentId) {
|
|
3877
4486
|
const client = getClient();
|
|
3878
4487
|
const result = await client.execute({
|
|
@@ -3901,11 +4510,11 @@ async function extractTrajectory(taskId, agentId) {
|
|
|
3901
4510
|
return signature;
|
|
3902
4511
|
}
|
|
3903
4512
|
function hashSignature(signature) {
|
|
3904
|
-
return
|
|
4513
|
+
return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
3905
4514
|
}
|
|
3906
4515
|
async function storeTrajectory(opts) {
|
|
3907
4516
|
const client = getClient();
|
|
3908
|
-
const id =
|
|
4517
|
+
const id = crypto7.randomUUID();
|
|
3909
4518
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3910
4519
|
const signatureHash = hashSignature(opts.signature);
|
|
3911
4520
|
await client.execute({
|
|
@@ -4152,6 +4761,7 @@ var init_skill_learning = __esm({
|
|
|
4152
4761
|
// src/lib/tasks.ts
|
|
4153
4762
|
var tasks_exports = {};
|
|
4154
4763
|
__export(tasks_exports, {
|
|
4764
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
4155
4765
|
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
4156
4766
|
countPendingReviews: () => countPendingReviews,
|
|
4157
4767
|
createTask: () => createTask,
|
|
@@ -4217,6 +4827,21 @@ async function updateTask(input2) {
|
|
|
4217
4827
|
});
|
|
4218
4828
|
} catch {
|
|
4219
4829
|
}
|
|
4830
|
+
try {
|
|
4831
|
+
const client = getClient();
|
|
4832
|
+
const cascaded = await client.execute({
|
|
4833
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
4834
|
+
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
4835
|
+
args: [now, taskId]
|
|
4836
|
+
});
|
|
4837
|
+
if (cascaded.rowsAffected > 0) {
|
|
4838
|
+
process.stderr.write(
|
|
4839
|
+
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
4840
|
+
`
|
|
4841
|
+
);
|
|
4842
|
+
}
|
|
4843
|
+
} catch {
|
|
4844
|
+
}
|
|
4220
4845
|
}
|
|
4221
4846
|
const isTerminal = input2.status === "done" || input2.status === "needs_review";
|
|
4222
4847
|
if (isTerminal) {
|
|
@@ -4230,6 +4855,13 @@ async function updateTask(input2) {
|
|
|
4230
4855
|
await cascadeUnblock(taskId, input2.baseDir, now);
|
|
4231
4856
|
} catch {
|
|
4232
4857
|
}
|
|
4858
|
+
orgBus.emit({
|
|
4859
|
+
type: "task_completed",
|
|
4860
|
+
taskId,
|
|
4861
|
+
employee: String(row.assigned_to),
|
|
4862
|
+
result: input2.result ?? "",
|
|
4863
|
+
timestamp: now
|
|
4864
|
+
});
|
|
4233
4865
|
if (row.parent_task_id) {
|
|
4234
4866
|
try {
|
|
4235
4867
|
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
@@ -4297,6 +4929,7 @@ var init_tasks = __esm({
|
|
|
4297
4929
|
init_database();
|
|
4298
4930
|
init_config();
|
|
4299
4931
|
init_notifications();
|
|
4932
|
+
init_state_bus();
|
|
4300
4933
|
init_tasks_crud();
|
|
4301
4934
|
init_tasks_review();
|
|
4302
4935
|
init_tasks_crud();
|
|
@@ -4365,7 +4998,7 @@ var init_worker_gate = __esm({
|
|
|
4365
4998
|
});
|
|
4366
4999
|
|
|
4367
5000
|
// src/adapters/claude/hooks/ingest-worker.ts
|
|
4368
|
-
import
|
|
5001
|
+
import crypto8 from "crypto";
|
|
4369
5002
|
import { execSync as execSync8 } from "child_process";
|
|
4370
5003
|
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
4371
5004
|
import path19 from "path";
|
|
@@ -4524,6 +5157,7 @@ async function getMasterKey() {
|
|
|
4524
5157
|
|
|
4525
5158
|
// src/lib/store.ts
|
|
4526
5159
|
init_config();
|
|
5160
|
+
init_state_bus();
|
|
4527
5161
|
var INIT_MAX_RETRIES = 3;
|
|
4528
5162
|
var INIT_RETRY_DELAY_MS = 1e3;
|
|
4529
5163
|
function isBusyError2(err) {
|
|
@@ -4594,6 +5228,11 @@ async function initStore(options) {
|
|
|
4594
5228
|
"version-query"
|
|
4595
5229
|
);
|
|
4596
5230
|
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
5231
|
+
try {
|
|
5232
|
+
const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
|
|
5233
|
+
await loadGlobalProcedures2();
|
|
5234
|
+
} catch {
|
|
5235
|
+
}
|
|
4597
5236
|
}
|
|
4598
5237
|
function classifyTier(record) {
|
|
4599
5238
|
if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
|
|
@@ -4635,6 +5274,12 @@ async function writeMemory(record) {
|
|
|
4635
5274
|
supersedes_id: record.supersedes_id ?? null
|
|
4636
5275
|
};
|
|
4637
5276
|
_pendingRecords.push(dbRow);
|
|
5277
|
+
orgBus.emit({
|
|
5278
|
+
type: "memory_stored",
|
|
5279
|
+
agentId: record.agent_id,
|
|
5280
|
+
project: record.project_name,
|
|
5281
|
+
timestamp: record.timestamp
|
|
5282
|
+
});
|
|
4638
5283
|
const MAX_PENDING = 1e3;
|
|
4639
5284
|
if (_pendingRecords.length > MAX_PENDING) {
|
|
4640
5285
|
const dropped = _pendingRecords.length - MAX_PENDING;
|
|
@@ -4996,7 +5641,14 @@ process.stdin.on("data", (chunk) => {
|
|
|
4996
5641
|
});
|
|
4997
5642
|
process.stdin.on("end", async () => {
|
|
4998
5643
|
try {
|
|
4999
|
-
|
|
5644
|
+
let data;
|
|
5645
|
+
try {
|
|
5646
|
+
data = JSON.parse(input);
|
|
5647
|
+
} catch (parseErr) {
|
|
5648
|
+
process.stderr.write(`[ingest-worker] skipped: truncated stdin (${input.length} bytes)
|
|
5649
|
+
`);
|
|
5650
|
+
process.exit(0);
|
|
5651
|
+
}
|
|
5000
5652
|
const rawText = extractSemanticText(data.tool_name, data.tool_input, data.tool_response);
|
|
5001
5653
|
if (rawText.length < 50) {
|
|
5002
5654
|
process.exit(0);
|
|
@@ -5036,7 +5688,7 @@ process.stdin.on("end", async () => {
|
|
|
5036
5688
|
}
|
|
5037
5689
|
const agentId = process.env.AGENT_ID;
|
|
5038
5690
|
await writeMemory({
|
|
5039
|
-
id:
|
|
5691
|
+
id: crypto8.randomUUID(),
|
|
5040
5692
|
agent_id: agentId,
|
|
5041
5693
|
agent_role: process.env.AGENT_ROLE ?? "unknown",
|
|
5042
5694
|
session_id: data.session_id,
|
|
@@ -5146,7 +5798,7 @@ process.stdin.on("end", async () => {
|
|
|
5146
5798
|
});
|
|
5147
5799
|
const assignmentText = `TASK ASSIGNED: ${title} \u2192 ${employee} [${priority}] in ${projectName}`;
|
|
5148
5800
|
await writeMemory({
|
|
5149
|
-
id:
|
|
5801
|
+
id: crypto8.randomUUID(),
|
|
5150
5802
|
agent_id: assignedBy,
|
|
5151
5803
|
agent_role: "COO",
|
|
5152
5804
|
session_id: data.session_id,
|