@askexenow/exe-os 0.8.41 → 0.8.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/backfill-conversations.js +805 -642
- package/dist/bin/backfill-responses.js +804 -641
- package/dist/bin/backfill-vectors.js +791 -634
- package/dist/bin/cleanup-stale-review-tasks.js +788 -631
- package/dist/bin/cli.js +1345 -660
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2518 -1798
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +15 -1
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +790 -633
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2500 -1877
- package/dist/bin/exe-heartbeat.js +147 -1
- package/dist/bin/exe-kill.js +795 -640
- package/dist/bin/exe-launch-agent.js +2168 -2008
- package/dist/bin/exe-link.js +28 -2
- package/dist/bin/exe-new-employee.js +25 -3
- package/dist/bin/exe-pending-messages.js +146 -1
- package/dist/bin/exe-pending-notifications.js +788 -631
- package/dist/bin/exe-pending-reviews.js +147 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +154 -3
- package/dist/bin/exe-session-cleanup.js +2466 -413
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2690 -150
- package/dist/bin/graph-backfill.js +794 -637
- package/dist/bin/graph-export.js +798 -641
- package/dist/bin/scan-tasks.js +2951 -44
- package/dist/bin/setup.js +62 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2504 -1895
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2689 -149
- package/dist/hooks/error-recall.js +154 -3
- package/dist/hooks/ingest-worker.js +1439 -815
- package/dist/hooks/instructions-loaded.js +151 -0
- package/dist/hooks/notification.js +153 -2
- package/dist/hooks/post-compact.js +164 -0
- package/dist/hooks/pre-compact.js +3073 -101
- package/dist/hooks/pre-tool-use.js +151 -0
- package/dist/hooks/prompt-ingest-worker.js +1714 -1537
- package/dist/hooks/prompt-submit.js +2658 -1113
- package/dist/hooks/response-ingest-worker.js +170 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +154 -3
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +151 -0
- package/dist/hooks/summary-worker.js +179 -7
- package/dist/index.js +278 -100
- package/dist/lib/cloud-sync.js +28 -2
- package/dist/lib/consolidation.js +69 -2
- package/dist/lib/database.js +19 -0
- package/dist/lib/device-registry.js +19 -0
- package/dist/lib/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +236 -16
- package/dist/lib/hybrid-search.js +154 -3
- package/dist/lib/license.js +15 -1
- package/dist/lib/messaging.js +39 -2
- package/dist/lib/schedules.js +792 -637
- package/dist/lib/store.js +796 -636
- package/dist/lib/tasks.js +1614 -1091
- package/dist/lib/tmux-routing.js +149 -9
- package/dist/mcp/server.js +1825 -1138
- package/dist/mcp/tools/create-task.js +2280 -828
- package/dist/mcp/tools/list-tasks.js +2788 -159
- package/dist/mcp/tools/send-message.js +39 -2
- package/dist/mcp/tools/update-task.js +64 -0
- package/dist/runtime/index.js +235 -67
- package/dist/tui/App.js +1452 -644
- package/package.json +3 -2
|
@@ -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,
|
|
@@ -1267,6 +1286,61 @@ var init_config = __esm({
|
|
|
1267
1286
|
}
|
|
1268
1287
|
});
|
|
1269
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
|
+
|
|
1270
1344
|
// src/lib/shard-manager.ts
|
|
1271
1345
|
var shard_manager_exports = {};
|
|
1272
1346
|
__export(shard_manager_exports, {
|
|
@@ -1508,6 +1582,71 @@ var init_shard_manager = __esm({
|
|
|
1508
1582
|
}
|
|
1509
1583
|
});
|
|
1510
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
|
+
|
|
1511
1650
|
// src/lib/notifications.ts
|
|
1512
1651
|
import crypto3 from "crypto";
|
|
1513
1652
|
import path6 from "path";
|
|
@@ -1594,7 +1733,7 @@ var init_employees = __esm({
|
|
|
1594
1733
|
|
|
1595
1734
|
// src/lib/license.ts
|
|
1596
1735
|
import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
|
|
1597
|
-
import { randomUUID } from "crypto";
|
|
1736
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1598
1737
|
import path8 from "path";
|
|
1599
1738
|
import { jwtVerify, importSPKI } from "jose";
|
|
1600
1739
|
async function fetchRetry(url, init) {
|
|
@@ -1621,7 +1760,7 @@ function loadDeviceId() {
|
|
|
1621
1760
|
}
|
|
1622
1761
|
} catch {
|
|
1623
1762
|
}
|
|
1624
|
-
const id =
|
|
1763
|
+
const id = randomUUID2();
|
|
1625
1764
|
mkdirSync2(EXE_AI_DIR, { recursive: true });
|
|
1626
1765
|
writeFileSync(DEVICE_ID_PATH, id, "utf8");
|
|
1627
1766
|
return id;
|
|
@@ -1634,6 +1773,10 @@ function loadLicense() {
|
|
|
1634
1773
|
return null;
|
|
1635
1774
|
}
|
|
1636
1775
|
}
|
|
1776
|
+
function saveLicense(apiKey) {
|
|
1777
|
+
mkdirSync2(EXE_AI_DIR, { recursive: true });
|
|
1778
|
+
writeFileSync(LICENSE_PATH, apiKey.trim(), { encoding: "utf8", mode: 384 });
|
|
1779
|
+
}
|
|
1637
1780
|
async function verifyLicenseJwt(token) {
|
|
1638
1781
|
try {
|
|
1639
1782
|
const key = await importSPKI(LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG);
|
|
@@ -1723,7 +1866,21 @@ function getCacheAgeMs() {
|
|
|
1723
1866
|
}
|
|
1724
1867
|
}
|
|
1725
1868
|
async function checkLicense() {
|
|
1726
|
-
|
|
1869
|
+
let key = loadLicense();
|
|
1870
|
+
if (!key) {
|
|
1871
|
+
try {
|
|
1872
|
+
const configPath = path8.join(EXE_AI_DIR, "config.json");
|
|
1873
|
+
if (existsSync7(configPath)) {
|
|
1874
|
+
const raw = JSON.parse(readFileSync5(configPath, "utf8"));
|
|
1875
|
+
const cloud = raw.cloud;
|
|
1876
|
+
if (cloud?.apiKey) {
|
|
1877
|
+
key = cloud.apiKey;
|
|
1878
|
+
saveLicense(key);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1727
1884
|
if (!key) return FREE_LICENSE;
|
|
1728
1885
|
const cached = await getCachedLicense();
|
|
1729
1886
|
if (cached && getCacheAgeMs() < CACHE_MAX_AGE_MS) return cached;
|
|
@@ -1865,7 +2022,7 @@ var init_plan_limits = __esm({
|
|
|
1865
2022
|
// src/lib/exe-daemon-client.ts
|
|
1866
2023
|
import net from "net";
|
|
1867
2024
|
import { spawn } from "child_process";
|
|
1868
|
-
import { randomUUID as
|
|
2025
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1869
2026
|
import { existsSync as existsSync9, unlinkSync as unlinkSync2, readFileSync as readFileSync7, openSync, closeSync, statSync as statSync2 } from "fs";
|
|
1870
2027
|
import path10 from "path";
|
|
1871
2028
|
import { fileURLToPath } from "url";
|
|
@@ -2057,7 +2214,7 @@ function sendRequest(texts, priority) {
|
|
|
2057
2214
|
resolve({ error: "Not connected" });
|
|
2058
2215
|
return;
|
|
2059
2216
|
}
|
|
2060
|
-
const id =
|
|
2217
|
+
const id = randomUUID3();
|
|
2061
2218
|
const timer = setTimeout(() => {
|
|
2062
2219
|
_pending.delete(id);
|
|
2063
2220
|
resolve({ error: "Request timeout" });
|
|
@@ -2075,7 +2232,7 @@ function sendRequest(texts, priority) {
|
|
|
2075
2232
|
async function pingDaemon() {
|
|
2076
2233
|
if (!_socket || !_connected) return null;
|
|
2077
2234
|
return new Promise((resolve) => {
|
|
2078
|
-
const id =
|
|
2235
|
+
const id = randomUUID3();
|
|
2079
2236
|
const timer = setTimeout(() => {
|
|
2080
2237
|
_pending.delete(id);
|
|
2081
2238
|
resolve(null);
|
|
@@ -2271,463 +2428,62 @@ var init_embedder = __esm({
|
|
|
2271
2428
|
}
|
|
2272
2429
|
});
|
|
2273
2430
|
|
|
2274
|
-
// src/lib/
|
|
2275
|
-
import
|
|
2431
|
+
// src/lib/session-registry.ts
|
|
2432
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync10 } from "fs";
|
|
2276
2433
|
import path11 from "path";
|
|
2277
|
-
import
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
const row = await resolveTask(client, input2.taskId);
|
|
2283
|
-
const taskId = String(row.id);
|
|
2284
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2285
|
-
const blockedByIds = [];
|
|
2286
|
-
if (row.blocked_by) {
|
|
2287
|
-
blockedByIds.push(String(row.blocked_by));
|
|
2434
|
+
import os4 from "os";
|
|
2435
|
+
function registerSession(entry) {
|
|
2436
|
+
const dir = path11.dirname(REGISTRY_PATH);
|
|
2437
|
+
if (!existsSync10(dir)) {
|
|
2438
|
+
mkdirSync3(dir, { recursive: true });
|
|
2288
2439
|
}
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
};
|
|
2296
|
-
const result = await client.execute({
|
|
2297
|
-
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
2298
|
-
args: [JSON.stringify(checkpoint), now, taskId]
|
|
2299
|
-
});
|
|
2300
|
-
if (result.rowsAffected === 0) {
|
|
2301
|
-
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
2440
|
+
const sessions = listSessions();
|
|
2441
|
+
const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
|
|
2442
|
+
if (idx >= 0) {
|
|
2443
|
+
sessions[idx] = entry;
|
|
2444
|
+
} else {
|
|
2445
|
+
sessions.push(entry);
|
|
2302
2446
|
}
|
|
2303
|
-
|
|
2304
|
-
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
2305
|
-
args: [taskId]
|
|
2306
|
-
});
|
|
2307
|
-
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
2308
|
-
return { checkpointCount };
|
|
2309
|
-
}
|
|
2310
|
-
function extractParentFromContext(contextBody) {
|
|
2311
|
-
if (!contextBody) return null;
|
|
2312
|
-
const match = contextBody.match(
|
|
2313
|
-
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
2314
|
-
);
|
|
2315
|
-
return match ? match[1].toLowerCase() : null;
|
|
2316
|
-
}
|
|
2317
|
-
function slugify(title) {
|
|
2318
|
-
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2447
|
+
writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
2319
2448
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
|
-
result = await client.execute({
|
|
2327
|
-
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
2328
|
-
args: [`%${identifier}%`]
|
|
2329
|
-
});
|
|
2330
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
2331
|
-
if (result.rows.length > 1) {
|
|
2332
|
-
const exact = result.rows.filter(
|
|
2333
|
-
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
2334
|
-
);
|
|
2335
|
-
if (exact.length === 1) return exact[0];
|
|
2336
|
-
const candidates = exact.length > 1 ? exact : result.rows;
|
|
2337
|
-
const active = candidates.filter(
|
|
2338
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
2339
|
-
);
|
|
2340
|
-
if (active.length === 1) return active[0];
|
|
2341
|
-
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
2342
|
-
throw new Error(
|
|
2343
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
2344
|
-
);
|
|
2345
|
-
}
|
|
2346
|
-
result = await client.execute({
|
|
2347
|
-
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
2348
|
-
args: [`%${identifier}%`]
|
|
2349
|
-
});
|
|
2350
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
2351
|
-
if (result.rows.length > 1) {
|
|
2352
|
-
const active = result.rows.filter(
|
|
2353
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
2354
|
-
);
|
|
2355
|
-
if (active.length === 1) return active[0];
|
|
2356
|
-
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
2357
|
-
throw new Error(
|
|
2358
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
2359
|
-
);
|
|
2449
|
+
function listSessions() {
|
|
2450
|
+
try {
|
|
2451
|
+
const raw = readFileSync8(REGISTRY_PATH, "utf8");
|
|
2452
|
+
return JSON.parse(raw);
|
|
2453
|
+
} catch {
|
|
2454
|
+
return [];
|
|
2360
2455
|
}
|
|
2361
|
-
throw new Error(`Task not found: ${identifier}`);
|
|
2362
2456
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
|
|
2369
|
-
let blockedById = null;
|
|
2370
|
-
const initialStatus = input2.blockedBy ? "blocked" : "open";
|
|
2371
|
-
if (input2.blockedBy) {
|
|
2372
|
-
const blocker = await resolveTask(client, input2.blockedBy);
|
|
2373
|
-
blockedById = String(blocker.id);
|
|
2374
|
-
}
|
|
2375
|
-
let parentTaskId = null;
|
|
2376
|
-
let parentRef = input2.parentTaskId;
|
|
2377
|
-
if (!parentRef) {
|
|
2378
|
-
const extracted = extractParentFromContext(input2.context);
|
|
2379
|
-
if (extracted) {
|
|
2380
|
-
parentRef = extracted;
|
|
2381
|
-
process.stderr.write(
|
|
2382
|
-
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
2383
|
-
);
|
|
2384
|
-
}
|
|
2457
|
+
var REGISTRY_PATH;
|
|
2458
|
+
var init_session_registry = __esm({
|
|
2459
|
+
"src/lib/session-registry.ts"() {
|
|
2460
|
+
"use strict";
|
|
2461
|
+
REGISTRY_PATH = path11.join(os4.homedir(), ".exe-os", "session-registry.json");
|
|
2385
2462
|
}
|
|
2386
|
-
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
// src/lib/session-key.ts
|
|
2466
|
+
import { execSync as execSync4 } from "child_process";
|
|
2467
|
+
function getSessionKey() {
|
|
2468
|
+
if (_cached2) return _cached2;
|
|
2469
|
+
let pid = process.ppid;
|
|
2470
|
+
for (let i = 0; i < 10; i++) {
|
|
2387
2471
|
try {
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2472
|
+
const info = execSync4(`ps -p ${pid} -o ppid=,comm=`, {
|
|
2473
|
+
encoding: "utf8",
|
|
2474
|
+
timeout: 2e3
|
|
2475
|
+
}).trim();
|
|
2476
|
+
const match = info.match(/^\s*(\d+)\s+(.+)$/);
|
|
2477
|
+
if (!match) break;
|
|
2478
|
+
const [, ppid, cmd] = match;
|
|
2479
|
+
if (cmd === "claude" || cmd.endsWith("/claude")) {
|
|
2480
|
+
_cached2 = String(pid);
|
|
2481
|
+
return _cached2;
|
|
2395
2482
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
}
|
|
2399
|
-
let warning;
|
|
2400
|
-
const dupCheck = await client.execute({
|
|
2401
|
-
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
2402
|
-
args: [input2.title, input2.assignedTo]
|
|
2403
|
-
});
|
|
2404
|
-
if (dupCheck.rows.length > 0) {
|
|
2405
|
-
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
2406
|
-
}
|
|
2407
|
-
if (input2.baseDir) {
|
|
2408
|
-
try {
|
|
2409
|
-
await mkdir4(path11.join(input2.baseDir, "exe", "output"), { recursive: true });
|
|
2410
|
-
await mkdir4(path11.join(input2.baseDir, "exe", "research"), { recursive: true });
|
|
2411
|
-
await ensureArchitectureDoc(input2.baseDir, input2.projectName);
|
|
2412
|
-
await ensureGitignoreExe(input2.baseDir);
|
|
2483
|
+
pid = parseInt(ppid, 10);
|
|
2484
|
+
if (pid <= 1) break;
|
|
2413
2485
|
} catch {
|
|
2414
|
-
|
|
2415
|
-
}
|
|
2416
|
-
const complexity = input2.complexity ?? "standard";
|
|
2417
|
-
await client.execute({
|
|
2418
|
-
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)
|
|
2419
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2420
|
-
args: [
|
|
2421
|
-
id,
|
|
2422
|
-
input2.title,
|
|
2423
|
-
input2.assignedTo,
|
|
2424
|
-
input2.assignedBy,
|
|
2425
|
-
input2.projectName,
|
|
2426
|
-
input2.priority,
|
|
2427
|
-
initialStatus,
|
|
2428
|
-
taskFile,
|
|
2429
|
-
blockedById,
|
|
2430
|
-
parentTaskId,
|
|
2431
|
-
input2.reviewer ?? null,
|
|
2432
|
-
input2.context,
|
|
2433
|
-
complexity,
|
|
2434
|
-
input2.budgetTokens ?? null,
|
|
2435
|
-
input2.budgetFallbackModel ?? null,
|
|
2436
|
-
0,
|
|
2437
|
-
null,
|
|
2438
|
-
now,
|
|
2439
|
-
now
|
|
2440
|
-
]
|
|
2441
|
-
});
|
|
2442
|
-
return {
|
|
2443
|
-
id,
|
|
2444
|
-
title: input2.title,
|
|
2445
|
-
assignedTo: input2.assignedTo,
|
|
2446
|
-
assignedBy: input2.assignedBy,
|
|
2447
|
-
projectName: input2.projectName,
|
|
2448
|
-
priority: input2.priority,
|
|
2449
|
-
status: initialStatus,
|
|
2450
|
-
taskFile,
|
|
2451
|
-
createdAt: now,
|
|
2452
|
-
updatedAt: now,
|
|
2453
|
-
warning,
|
|
2454
|
-
budgetTokens: input2.budgetTokens ?? null,
|
|
2455
|
-
budgetFallbackModel: input2.budgetFallbackModel ?? null,
|
|
2456
|
-
tokensUsed: 0,
|
|
2457
|
-
tokensWarnedAt: null
|
|
2458
|
-
};
|
|
2459
|
-
}
|
|
2460
|
-
async function listTasks(input2) {
|
|
2461
|
-
const client = getClient();
|
|
2462
|
-
const conditions = [];
|
|
2463
|
-
const args = [];
|
|
2464
|
-
if (input2.assignedTo) {
|
|
2465
|
-
conditions.push("assigned_to = ?");
|
|
2466
|
-
args.push(input2.assignedTo);
|
|
2467
|
-
}
|
|
2468
|
-
if (input2.status) {
|
|
2469
|
-
conditions.push("status = ?");
|
|
2470
|
-
args.push(input2.status);
|
|
2471
|
-
} else {
|
|
2472
|
-
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
2473
|
-
}
|
|
2474
|
-
if (input2.projectName) {
|
|
2475
|
-
conditions.push("project_name = ?");
|
|
2476
|
-
args.push(input2.projectName);
|
|
2477
|
-
}
|
|
2478
|
-
if (input2.priority) {
|
|
2479
|
-
conditions.push("priority = ?");
|
|
2480
|
-
args.push(input2.priority);
|
|
2481
|
-
}
|
|
2482
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2483
|
-
const result = await client.execute({
|
|
2484
|
-
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`,
|
|
2485
|
-
args
|
|
2486
|
-
});
|
|
2487
|
-
return result.rows.map((r) => ({
|
|
2488
|
-
id: String(r.id),
|
|
2489
|
-
title: String(r.title),
|
|
2490
|
-
assignedTo: String(r.assigned_to),
|
|
2491
|
-
assignedBy: String(r.assigned_by),
|
|
2492
|
-
projectName: String(r.project_name),
|
|
2493
|
-
priority: String(r.priority),
|
|
2494
|
-
status: String(r.status),
|
|
2495
|
-
taskFile: String(r.task_file),
|
|
2496
|
-
createdAt: String(r.created_at),
|
|
2497
|
-
updatedAt: String(r.updated_at),
|
|
2498
|
-
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
2499
|
-
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
2500
|
-
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
2501
|
-
tokensUsed: Number(r.tokens_used ?? 0),
|
|
2502
|
-
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
2503
|
-
}));
|
|
2504
|
-
}
|
|
2505
|
-
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
2506
|
-
if (!taskContext) return null;
|
|
2507
|
-
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
2508
|
-
try {
|
|
2509
|
-
const since = new Date(taskCreatedAt).toISOString();
|
|
2510
|
-
const branch = execSync4(
|
|
2511
|
-
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
2512
|
-
{ encoding: "utf8", timeout: 3e3 }
|
|
2513
|
-
).trim();
|
|
2514
|
-
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
2515
|
-
const commitCount = execSync4(
|
|
2516
|
-
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
2517
|
-
{ encoding: "utf8", timeout: 5e3 }
|
|
2518
|
-
).trim();
|
|
2519
|
-
const count = parseInt(commitCount, 10);
|
|
2520
|
-
if (count === 0) {
|
|
2521
|
-
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
2522
|
-
}
|
|
2523
|
-
return null;
|
|
2524
|
-
} catch {
|
|
2525
|
-
return null;
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
async function updateTaskStatus(input2) {
|
|
2529
|
-
const client = getClient();
|
|
2530
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2531
|
-
const row = await resolveTask(client, input2.taskId);
|
|
2532
|
-
const taskId = String(row.id);
|
|
2533
|
-
const taskFile = String(row.task_file);
|
|
2534
|
-
if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
2535
|
-
process.stderr.write(
|
|
2536
|
-
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
2537
|
-
`
|
|
2538
|
-
);
|
|
2539
|
-
}
|
|
2540
|
-
if (input2.status === "done") {
|
|
2541
|
-
const existingRow = await client.execute({
|
|
2542
|
-
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
2543
|
-
args: [taskId]
|
|
2544
|
-
});
|
|
2545
|
-
if (existingRow.rows.length > 0) {
|
|
2546
|
-
const ctx = existingRow.rows[0];
|
|
2547
|
-
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
2548
|
-
if (warning) {
|
|
2549
|
-
input2.result = input2.result ? `\u26A0\uFE0F ${warning}
|
|
2550
|
-
|
|
2551
|
-
${input2.result}` : `\u26A0\uFE0F ${warning}`;
|
|
2552
|
-
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
2553
|
-
`);
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
if (input2.status === "in_progress") {
|
|
2558
|
-
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
2559
|
-
const claim = await client.execute({
|
|
2560
|
-
sql: `UPDATE tasks
|
|
2561
|
-
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
2562
|
-
WHERE id = ? AND status = 'open'`,
|
|
2563
|
-
args: [tmuxSession, now, taskId]
|
|
2564
|
-
});
|
|
2565
|
-
if (claim.rowsAffected === 0) {
|
|
2566
|
-
const current = await client.execute({
|
|
2567
|
-
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
2568
|
-
args: [taskId]
|
|
2569
|
-
});
|
|
2570
|
-
const cur = current.rows[0];
|
|
2571
|
-
const status = cur?.status ?? "unknown";
|
|
2572
|
-
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
2573
|
-
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
2574
|
-
}
|
|
2575
|
-
try {
|
|
2576
|
-
await writeCheckpoint({
|
|
2577
|
-
taskId,
|
|
2578
|
-
step: "claimed",
|
|
2579
|
-
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
2580
|
-
});
|
|
2581
|
-
} catch {
|
|
2582
|
-
}
|
|
2583
|
-
return { row, taskFile, now, taskId };
|
|
2584
|
-
}
|
|
2585
|
-
if (input2.result) {
|
|
2586
|
-
await client.execute({
|
|
2587
|
-
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
2588
|
-
args: [input2.status, input2.result, now, taskId]
|
|
2589
|
-
});
|
|
2590
|
-
} else {
|
|
2591
|
-
await client.execute({
|
|
2592
|
-
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
2593
|
-
args: [input2.status, now, taskId]
|
|
2594
|
-
});
|
|
2595
|
-
}
|
|
2596
|
-
try {
|
|
2597
|
-
await writeCheckpoint({
|
|
2598
|
-
taskId,
|
|
2599
|
-
step: `status_transition:${input2.status}`,
|
|
2600
|
-
contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
|
|
2601
|
-
});
|
|
2602
|
-
} catch {
|
|
2603
|
-
}
|
|
2604
|
-
return { row, taskFile, now, taskId };
|
|
2605
|
-
}
|
|
2606
|
-
async function deleteTaskCore(taskId, _baseDir) {
|
|
2607
|
-
const client = getClient();
|
|
2608
|
-
const row = await resolveTask(client, taskId);
|
|
2609
|
-
const id = String(row.id);
|
|
2610
|
-
const taskFile = String(row.task_file);
|
|
2611
|
-
const assignedTo = String(row.assigned_to);
|
|
2612
|
-
const assignedBy = String(row.assigned_by);
|
|
2613
|
-
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
2614
|
-
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
2615
|
-
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
2616
|
-
}
|
|
2617
|
-
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
2618
|
-
const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
2619
|
-
try {
|
|
2620
|
-
if (existsSync10(archPath)) return;
|
|
2621
|
-
const template = [
|
|
2622
|
-
`# ${projectName} \u2014 System Architecture`,
|
|
2623
|
-
"",
|
|
2624
|
-
"> Employees: read this before every task. Update it when you change system structure.",
|
|
2625
|
-
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2626
|
-
"",
|
|
2627
|
-
"## Overview",
|
|
2628
|
-
"",
|
|
2629
|
-
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
2630
|
-
"",
|
|
2631
|
-
"## Key Components",
|
|
2632
|
-
"",
|
|
2633
|
-
"<!-- List the major modules, services, or subsystems. -->",
|
|
2634
|
-
"",
|
|
2635
|
-
"## Data Flow",
|
|
2636
|
-
"",
|
|
2637
|
-
"<!-- How does data move through the system? What writes where? -->",
|
|
2638
|
-
"",
|
|
2639
|
-
"## Invariants",
|
|
2640
|
-
"",
|
|
2641
|
-
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
2642
|
-
"",
|
|
2643
|
-
"## Dependencies",
|
|
2644
|
-
"",
|
|
2645
|
-
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
2646
|
-
""
|
|
2647
|
-
].join("\n");
|
|
2648
|
-
await writeFile4(archPath, template, "utf-8");
|
|
2649
|
-
} catch {
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
async function ensureGitignoreExe(baseDir) {
|
|
2653
|
-
const gitignorePath = path11.join(baseDir, ".gitignore");
|
|
2654
|
-
try {
|
|
2655
|
-
if (existsSync10(gitignorePath)) {
|
|
2656
|
-
const content = readFileSync8(gitignorePath, "utf-8");
|
|
2657
|
-
if (/^\/?exe\/?$/m.test(content)) return;
|
|
2658
|
-
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
2659
|
-
} else {
|
|
2660
|
-
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
2661
|
-
}
|
|
2662
|
-
} catch {
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
2666
|
-
var init_tasks_crud = __esm({
|
|
2667
|
-
"src/lib/tasks-crud.ts"() {
|
|
2668
|
-
"use strict";
|
|
2669
|
-
init_database();
|
|
2670
|
-
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
2671
|
-
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
2672
|
-
}
|
|
2673
|
-
});
|
|
2674
|
-
|
|
2675
|
-
// src/lib/session-registry.ts
|
|
2676
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync11 } from "fs";
|
|
2677
|
-
import path12 from "path";
|
|
2678
|
-
import os4 from "os";
|
|
2679
|
-
function registerSession(entry) {
|
|
2680
|
-
const dir = path12.dirname(REGISTRY_PATH);
|
|
2681
|
-
if (!existsSync11(dir)) {
|
|
2682
|
-
mkdirSync3(dir, { recursive: true });
|
|
2683
|
-
}
|
|
2684
|
-
const sessions = listSessions();
|
|
2685
|
-
const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
|
|
2686
|
-
if (idx >= 0) {
|
|
2687
|
-
sessions[idx] = entry;
|
|
2688
|
-
} else {
|
|
2689
|
-
sessions.push(entry);
|
|
2690
|
-
}
|
|
2691
|
-
writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
2692
|
-
}
|
|
2693
|
-
function listSessions() {
|
|
2694
|
-
try {
|
|
2695
|
-
const raw = readFileSync9(REGISTRY_PATH, "utf8");
|
|
2696
|
-
return JSON.parse(raw);
|
|
2697
|
-
} catch {
|
|
2698
|
-
return [];
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
var REGISTRY_PATH;
|
|
2702
|
-
var init_session_registry = __esm({
|
|
2703
|
-
"src/lib/session-registry.ts"() {
|
|
2704
|
-
"use strict";
|
|
2705
|
-
REGISTRY_PATH = path12.join(os4.homedir(), ".exe-os", "session-registry.json");
|
|
2706
|
-
}
|
|
2707
|
-
});
|
|
2708
|
-
|
|
2709
|
-
// src/lib/session-key.ts
|
|
2710
|
-
import { execSync as execSync5 } from "child_process";
|
|
2711
|
-
function getSessionKey() {
|
|
2712
|
-
if (_cached2) return _cached2;
|
|
2713
|
-
let pid = process.ppid;
|
|
2714
|
-
for (let i = 0; i < 10; i++) {
|
|
2715
|
-
try {
|
|
2716
|
-
const info = execSync5(`ps -p ${pid} -o ppid=,comm=`, {
|
|
2717
|
-
encoding: "utf8",
|
|
2718
|
-
timeout: 2e3
|
|
2719
|
-
}).trim();
|
|
2720
|
-
const match = info.match(/^\s*(\d+)\s+(.+)$/);
|
|
2721
|
-
if (!match) break;
|
|
2722
|
-
const [, ppid, cmd] = match;
|
|
2723
|
-
if (cmd === "claude" || cmd.endsWith("/claude")) {
|
|
2724
|
-
_cached2 = String(pid);
|
|
2725
|
-
return _cached2;
|
|
2726
|
-
}
|
|
2727
|
-
pid = parseInt(ppid, 10);
|
|
2728
|
-
if (pid <= 1) break;
|
|
2729
|
-
} catch {
|
|
2730
|
-
break;
|
|
2486
|
+
break;
|
|
2731
2487
|
}
|
|
2732
2488
|
}
|
|
2733
2489
|
_cached2 = process.env.CLAUDE_CODE_SSE_PORT ?? String(process.ppid);
|
|
@@ -2849,14 +2605,14 @@ var init_transport = __esm({
|
|
|
2849
2605
|
});
|
|
2850
2606
|
|
|
2851
2607
|
// src/lib/cc-agent-support.ts
|
|
2852
|
-
import { execSync as
|
|
2608
|
+
import { execSync as execSync5 } from "child_process";
|
|
2853
2609
|
function _resetCcAgentSupportCache() {
|
|
2854
2610
|
_cachedSupport = null;
|
|
2855
2611
|
}
|
|
2856
2612
|
function claudeSupportsAgentFlag() {
|
|
2857
2613
|
if (_cachedSupport !== null) return _cachedSupport;
|
|
2858
2614
|
try {
|
|
2859
|
-
const helpOutput =
|
|
2615
|
+
const helpOutput = execSync5("claude --help 2>&1", {
|
|
2860
2616
|
encoding: "utf-8",
|
|
2861
2617
|
timeout: 5e3
|
|
2862
2618
|
});
|
|
@@ -2899,17 +2655,17 @@ var init_provider_table = __esm({
|
|
|
2899
2655
|
});
|
|
2900
2656
|
|
|
2901
2657
|
// src/lib/intercom-queue.ts
|
|
2902
|
-
import { readFileSync as
|
|
2903
|
-
import
|
|
2658
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync11, mkdirSync as mkdirSync4 } from "fs";
|
|
2659
|
+
import path12 from "path";
|
|
2904
2660
|
import os5 from "os";
|
|
2905
2661
|
function ensureDir() {
|
|
2906
|
-
const dir =
|
|
2907
|
-
if (!
|
|
2662
|
+
const dir = path12.dirname(QUEUE_PATH);
|
|
2663
|
+
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
|
|
2908
2664
|
}
|
|
2909
2665
|
function readQueue() {
|
|
2910
2666
|
try {
|
|
2911
|
-
if (!
|
|
2912
|
-
return JSON.parse(
|
|
2667
|
+
if (!existsSync11(QUEUE_PATH)) return [];
|
|
2668
|
+
return JSON.parse(readFileSync9(QUEUE_PATH, "utf8"));
|
|
2913
2669
|
} catch {
|
|
2914
2670
|
return [];
|
|
2915
2671
|
}
|
|
@@ -2941,21 +2697,358 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
|
2941
2697
|
var init_intercom_queue = __esm({
|
|
2942
2698
|
"src/lib/intercom-queue.ts"() {
|
|
2943
2699
|
"use strict";
|
|
2944
|
-
QUEUE_PATH =
|
|
2700
|
+
QUEUE_PATH = path12.join(os5.homedir(), ".exe-os", "intercom-queue.json");
|
|
2945
2701
|
TTL_MS = 60 * 60 * 1e3;
|
|
2946
|
-
INTERCOM_LOG =
|
|
2702
|
+
INTERCOM_LOG = path12.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
// src/lib/session-kill-telemetry.ts
|
|
2707
|
+
import crypto4 from "crypto";
|
|
2708
|
+
async function recordSessionKill(input2) {
|
|
2709
|
+
try {
|
|
2710
|
+
const client = getClient();
|
|
2711
|
+
await client.execute({
|
|
2712
|
+
sql: `INSERT INTO session_kills
|
|
2713
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
2714
|
+
ticks_idle, estimated_tokens_saved)
|
|
2715
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2716
|
+
args: [
|
|
2717
|
+
crypto4.randomUUID(),
|
|
2718
|
+
input2.sessionName,
|
|
2719
|
+
input2.agentId,
|
|
2720
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2721
|
+
input2.reason,
|
|
2722
|
+
input2.ticksIdle ?? null,
|
|
2723
|
+
input2.estimatedTokensSaved ?? null
|
|
2724
|
+
]
|
|
2725
|
+
});
|
|
2726
|
+
} catch (err) {
|
|
2727
|
+
process.stderr.write(
|
|
2728
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
2729
|
+
`
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
var init_session_kill_telemetry = __esm({
|
|
2734
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
2735
|
+
"use strict";
|
|
2736
|
+
init_database();
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
|
|
2740
|
+
// src/lib/capacity-monitor.ts
|
|
2741
|
+
var capacity_monitor_exports = {};
|
|
2742
|
+
__export(capacity_monitor_exports, {
|
|
2743
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
2744
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
2745
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
2746
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
2747
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
2748
|
+
extractContextPercent: () => extractContextPercent,
|
|
2749
|
+
isAtCapacity: () => isAtCapacity,
|
|
2750
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
2751
|
+
pollCapacityDead: () => pollCapacityDead
|
|
2752
|
+
});
|
|
2753
|
+
function resumeTaskTitle(agentId) {
|
|
2754
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
2755
|
+
}
|
|
2756
|
+
function buildResumeContext(agentId, openTasks) {
|
|
2757
|
+
const taskList = openTasks.map(
|
|
2758
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
2759
|
+
).join("\n");
|
|
2760
|
+
return [
|
|
2761
|
+
"## Context",
|
|
2762
|
+
"",
|
|
2763
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
2764
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
2765
|
+
"",
|
|
2766
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
2767
|
+
"",
|
|
2768
|
+
taskList,
|
|
2769
|
+
"",
|
|
2770
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
2771
|
+
].join("\n");
|
|
2772
|
+
}
|
|
2773
|
+
function filterPaneContent(paneOutput) {
|
|
2774
|
+
return paneOutput.split("\n").filter((line) => {
|
|
2775
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
2776
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
2777
|
+
if (line.includes(marker)) return false;
|
|
2778
|
+
}
|
|
2779
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
2780
|
+
if (re.test(line)) return false;
|
|
2781
|
+
}
|
|
2782
|
+
return true;
|
|
2783
|
+
}).join("\n");
|
|
2784
|
+
}
|
|
2785
|
+
function extractContextPercent(paneOutput) {
|
|
2786
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
2787
|
+
if (!match) return null;
|
|
2788
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
2789
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2790
|
+
}
|
|
2791
|
+
function isAtCapacity(paneOutput) {
|
|
2792
|
+
const filtered = filterPaneContent(paneOutput);
|
|
2793
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
2794
|
+
}
|
|
2795
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
2796
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
2797
|
+
if (pendingSince === void 0) {
|
|
2798
|
+
_pendingCapacityKill.set(agentId, now);
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
2802
|
+
_pendingCapacityKill.set(agentId, now);
|
|
2803
|
+
return false;
|
|
2804
|
+
}
|
|
2805
|
+
_pendingCapacityKill.delete(agentId);
|
|
2806
|
+
return true;
|
|
2807
|
+
}
|
|
2808
|
+
function _resetPendingCapacityKills() {
|
|
2809
|
+
_pendingCapacityKill.clear();
|
|
2810
|
+
}
|
|
2811
|
+
function _resetLastRelaunchCache() {
|
|
2812
|
+
_lastRelaunch.clear();
|
|
2813
|
+
}
|
|
2814
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
2815
|
+
const client = getClient();
|
|
2816
|
+
const result = await client.execute({
|
|
2817
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
2818
|
+
FROM tasks
|
|
2819
|
+
WHERE assigned_to = ? AND title LIKE ?`,
|
|
2820
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`]
|
|
2821
|
+
});
|
|
2822
|
+
const raw = result.rows[0]?.last_created_at;
|
|
2823
|
+
if (raw === null || raw === void 0) return null;
|
|
2824
|
+
const parsed = Date.parse(String(raw));
|
|
2825
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2826
|
+
}
|
|
2827
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
2828
|
+
const cached = _lastRelaunch.get(agentId);
|
|
2829
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
2830
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
2831
|
+
if (persisted === null) return false;
|
|
2832
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
2833
|
+
_lastRelaunch.set(agentId, persisted);
|
|
2834
|
+
return true;
|
|
2835
|
+
}
|
|
2836
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
2837
|
+
const client = getClient();
|
|
2838
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2839
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
2840
|
+
const existing = await client.execute({
|
|
2841
|
+
sql: `SELECT id FROM tasks
|
|
2842
|
+
WHERE assigned_to = ?
|
|
2843
|
+
AND title LIKE ?
|
|
2844
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
|
|
2845
|
+
ORDER BY created_at DESC
|
|
2846
|
+
LIMIT 1`,
|
|
2847
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
2848
|
+
});
|
|
2849
|
+
if (existing.rows.length > 0) {
|
|
2850
|
+
const taskId = String(existing.rows[0].id);
|
|
2851
|
+
await client.execute({
|
|
2852
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
2853
|
+
args: [context, now, taskId]
|
|
2854
|
+
});
|
|
2855
|
+
return { created: false, taskId };
|
|
2856
|
+
}
|
|
2857
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
2858
|
+
const task = await createTask2({
|
|
2859
|
+
title: resumeTaskTitle(agentId),
|
|
2860
|
+
assignedTo: agentId,
|
|
2861
|
+
assignedBy: "system",
|
|
2862
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
2863
|
+
priority: "p0",
|
|
2864
|
+
context,
|
|
2865
|
+
baseDir: projectDir
|
|
2866
|
+
});
|
|
2867
|
+
return { created: true, taskId: task.id };
|
|
2868
|
+
}
|
|
2869
|
+
async function pollCapacityDead() {
|
|
2870
|
+
const transport = getTransport();
|
|
2871
|
+
const relaunched = [];
|
|
2872
|
+
const registered = listSessions().filter(
|
|
2873
|
+
(s) => s.agentId !== "exe"
|
|
2874
|
+
);
|
|
2875
|
+
if (registered.length === 0) return [];
|
|
2876
|
+
let liveSessions;
|
|
2877
|
+
try {
|
|
2878
|
+
liveSessions = transport.listSessions();
|
|
2879
|
+
} catch {
|
|
2880
|
+
return [];
|
|
2881
|
+
}
|
|
2882
|
+
for (const entry of registered) {
|
|
2883
|
+
const { windowName, agentId, projectDir } = entry;
|
|
2884
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
2885
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
2886
|
+
let pane;
|
|
2887
|
+
try {
|
|
2888
|
+
pane = transport.capturePane(windowName, 15);
|
|
2889
|
+
} catch {
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
if (!isAtCapacity(pane)) continue;
|
|
2893
|
+
const ctxPct = extractContextPercent(pane);
|
|
2894
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
2895
|
+
process.stderr.write(
|
|
2896
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
2897
|
+
`
|
|
2898
|
+
);
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
if (!confirmCapacityKill(agentId)) {
|
|
2902
|
+
process.stderr.write(
|
|
2903
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
2904
|
+
`
|
|
2905
|
+
);
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
2909
|
+
if (!verify.atCapacity) {
|
|
2910
|
+
process.stderr.write(
|
|
2911
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
2912
|
+
`
|
|
2913
|
+
);
|
|
2914
|
+
void recordSessionKill({
|
|
2915
|
+
sessionName: windowName,
|
|
2916
|
+
agentId,
|
|
2917
|
+
reason: "capacity_false_positive_blocked"
|
|
2918
|
+
});
|
|
2919
|
+
continue;
|
|
2920
|
+
}
|
|
2921
|
+
process.stderr.write(
|
|
2922
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
2923
|
+
`
|
|
2924
|
+
);
|
|
2925
|
+
try {
|
|
2926
|
+
transport.kill(windowName);
|
|
2927
|
+
void recordSessionKill({
|
|
2928
|
+
sessionName: windowName,
|
|
2929
|
+
agentId,
|
|
2930
|
+
reason: "capacity"
|
|
2931
|
+
});
|
|
2932
|
+
const client = getClient();
|
|
2933
|
+
const openTasks = await client.execute({
|
|
2934
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
2935
|
+
FROM tasks
|
|
2936
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')
|
|
2937
|
+
ORDER BY
|
|
2938
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
2939
|
+
created_at ASC
|
|
2940
|
+
LIMIT 10`,
|
|
2941
|
+
args: [agentId]
|
|
2942
|
+
});
|
|
2943
|
+
if (openTasks.rows.length === 0) {
|
|
2944
|
+
process.stderr.write(
|
|
2945
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
2946
|
+
`
|
|
2947
|
+
);
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
const { created } = await createOrRefreshResumeTask(
|
|
2951
|
+
agentId,
|
|
2952
|
+
projectDir,
|
|
2953
|
+
openTasks.rows
|
|
2954
|
+
);
|
|
2955
|
+
if (created) {
|
|
2956
|
+
await writeNotification({
|
|
2957
|
+
agentId: "system",
|
|
2958
|
+
agentRole: "daemon",
|
|
2959
|
+
event: "capacity_relaunch",
|
|
2960
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
2961
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
2962
|
+
});
|
|
2963
|
+
}
|
|
2964
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
2965
|
+
if (created) relaunched.push(agentId);
|
|
2966
|
+
} catch (err) {
|
|
2967
|
+
process.stderr.write(
|
|
2968
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
2969
|
+
`
|
|
2970
|
+
);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
return relaunched;
|
|
2974
|
+
}
|
|
2975
|
+
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;
|
|
2976
|
+
var init_capacity_monitor = __esm({
|
|
2977
|
+
"src/lib/capacity-monitor.ts"() {
|
|
2978
|
+
"use strict";
|
|
2979
|
+
init_session_registry();
|
|
2980
|
+
init_transport();
|
|
2981
|
+
init_notifications();
|
|
2982
|
+
init_database();
|
|
2983
|
+
init_session_kill_telemetry();
|
|
2984
|
+
init_tmux_routing();
|
|
2985
|
+
CAPACITY_PATTERNS = [
|
|
2986
|
+
/conversation is too long/i,
|
|
2987
|
+
/maximum context length/i,
|
|
2988
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
2989
|
+
/reached.*(?:token|context).*limit/i
|
|
2990
|
+
];
|
|
2991
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
2992
|
+
CONTENT_LINE_MARKERS = [
|
|
2993
|
+
"RESUME:",
|
|
2994
|
+
"intercom",
|
|
2995
|
+
"capacity-monitor",
|
|
2996
|
+
"CAPACITY_PATTERNS",
|
|
2997
|
+
"isAtCapacity",
|
|
2998
|
+
"CONTENT_LINE_MARKERS",
|
|
2999
|
+
"pollCapacityDead",
|
|
3000
|
+
"confirmCapacityKill",
|
|
3001
|
+
"session_kills",
|
|
3002
|
+
"capacity-monitor.test"
|
|
3003
|
+
];
|
|
3004
|
+
SOURCE_CODE_MARKERS = [
|
|
3005
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
3006
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
3007
|
+
];
|
|
3008
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
3009
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
3010
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
3011
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
3012
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
3013
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
3014
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
3015
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
3016
|
+
CTX_FLOOR_PERCENT = 50;
|
|
2947
3017
|
}
|
|
2948
3018
|
});
|
|
2949
3019
|
|
|
2950
3020
|
// src/lib/tmux-routing.ts
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3021
|
+
var tmux_routing_exports = {};
|
|
3022
|
+
__export(tmux_routing_exports, {
|
|
3023
|
+
acquireSpawnLock: () => acquireSpawnLock2,
|
|
3024
|
+
employeeSessionName: () => employeeSessionName,
|
|
3025
|
+
ensureEmployee: () => ensureEmployee,
|
|
3026
|
+
extractRootExe: () => extractRootExe,
|
|
3027
|
+
findFreeInstance: () => findFreeInstance,
|
|
3028
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
3029
|
+
getMySession: () => getMySession,
|
|
3030
|
+
getParentExe: () => getParentExe,
|
|
3031
|
+
getSessionState: () => getSessionState,
|
|
3032
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
3033
|
+
isExeSession: () => isExeSession,
|
|
3034
|
+
isSessionBusy: () => isSessionBusy,
|
|
3035
|
+
notifyParentExe: () => notifyParentExe,
|
|
3036
|
+
parseParentExe: () => parseParentExe,
|
|
3037
|
+
registerParentExe: () => registerParentExe,
|
|
3038
|
+
releaseSpawnLock: () => releaseSpawnLock2,
|
|
3039
|
+
resolveExeSession: () => resolveExeSession,
|
|
3040
|
+
sendIntercom: () => sendIntercom,
|
|
3041
|
+
spawnEmployee: () => spawnEmployee,
|
|
3042
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
3043
|
+
});
|
|
3044
|
+
import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
|
|
3045
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync12, appendFileSync } from "fs";
|
|
3046
|
+
import path13 from "path";
|
|
2954
3047
|
import os6 from "os";
|
|
2955
3048
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2956
3049
|
import { unlinkSync as unlinkSync3 } from "fs";
|
|
2957
3050
|
function spawnLockPath(sessionName) {
|
|
2958
|
-
return
|
|
3051
|
+
return path13.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
2959
3052
|
}
|
|
2960
3053
|
function isProcessAlive(pid) {
|
|
2961
3054
|
try {
|
|
@@ -2966,13 +3059,13 @@ function isProcessAlive(pid) {
|
|
|
2966
3059
|
}
|
|
2967
3060
|
}
|
|
2968
3061
|
function acquireSpawnLock2(sessionName) {
|
|
2969
|
-
if (!
|
|
3062
|
+
if (!existsSync12(SPAWN_LOCK_DIR)) {
|
|
2970
3063
|
mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
|
|
2971
3064
|
}
|
|
2972
3065
|
const lockFile = spawnLockPath(sessionName);
|
|
2973
|
-
if (
|
|
3066
|
+
if (existsSync12(lockFile)) {
|
|
2974
3067
|
try {
|
|
2975
|
-
const lock = JSON.parse(
|
|
3068
|
+
const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
|
|
2976
3069
|
const age = Date.now() - lock.timestamp;
|
|
2977
3070
|
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
2978
3071
|
return false;
|
|
@@ -2992,13 +3085,13 @@ function releaseSpawnLock2(sessionName) {
|
|
|
2992
3085
|
function resolveBehaviorsExporterScript() {
|
|
2993
3086
|
try {
|
|
2994
3087
|
const thisFile = fileURLToPath2(import.meta.url);
|
|
2995
|
-
const scriptPath =
|
|
2996
|
-
|
|
3088
|
+
const scriptPath = path13.join(
|
|
3089
|
+
path13.dirname(thisFile),
|
|
2997
3090
|
"..",
|
|
2998
3091
|
"bin",
|
|
2999
3092
|
"exe-export-behaviors.js"
|
|
3000
3093
|
);
|
|
3001
|
-
return
|
|
3094
|
+
return existsSync12(scriptPath) ? scriptPath : null;
|
|
3002
3095
|
} catch {
|
|
3003
3096
|
return null;
|
|
3004
3097
|
}
|
|
@@ -3025,16 +3118,54 @@ function getMySession() {
|
|
|
3025
3118
|
return getTransport().getMySession();
|
|
3026
3119
|
}
|
|
3027
3120
|
function employeeSessionName(employee, exeSession, instance) {
|
|
3121
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
3122
|
+
const root = extractRootExe(exeSession);
|
|
3123
|
+
if (root) {
|
|
3124
|
+
process.stderr.write(
|
|
3125
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
|
|
3126
|
+
`
|
|
3127
|
+
);
|
|
3128
|
+
exeSession = root;
|
|
3129
|
+
} else {
|
|
3130
|
+
throw new Error(
|
|
3131
|
+
`Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3028
3135
|
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
3029
|
-
|
|
3136
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
3137
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
3138
|
+
throw new Error(
|
|
3139
|
+
`Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
return name;
|
|
3143
|
+
}
|
|
3144
|
+
function parseParentExe(sessionName, agentId) {
|
|
3145
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3146
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
3147
|
+
const match = sessionName.match(regex);
|
|
3148
|
+
return match?.[1] ?? null;
|
|
3030
3149
|
}
|
|
3031
3150
|
function extractRootExe(name) {
|
|
3032
3151
|
const match = name.match(/(exe\d+)$/);
|
|
3033
3152
|
return match?.[1] ?? null;
|
|
3034
3153
|
}
|
|
3154
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
3155
|
+
if (!existsSync12(SESSION_CACHE)) {
|
|
3156
|
+
mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
3157
|
+
}
|
|
3158
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
3159
|
+
const filePath = path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
3160
|
+
writeFileSync4(filePath, JSON.stringify({
|
|
3161
|
+
parentExe: rootExe,
|
|
3162
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
3163
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3164
|
+
}));
|
|
3165
|
+
}
|
|
3035
3166
|
function getParentExe(sessionKey) {
|
|
3036
3167
|
try {
|
|
3037
|
-
const data = JSON.parse(
|
|
3168
|
+
const data = JSON.parse(readFileSync10(path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
3038
3169
|
return data.parentExe || null;
|
|
3039
3170
|
} catch {
|
|
3040
3171
|
return null;
|
|
@@ -3042,8 +3173,8 @@ function getParentExe(sessionKey) {
|
|
|
3042
3173
|
}
|
|
3043
3174
|
function getDispatchedBy(sessionKey) {
|
|
3044
3175
|
try {
|
|
3045
|
-
const data = JSON.parse(
|
|
3046
|
-
|
|
3176
|
+
const data = JSON.parse(readFileSync10(
|
|
3177
|
+
path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
3047
3178
|
"utf8"
|
|
3048
3179
|
));
|
|
3049
3180
|
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
@@ -3074,19 +3205,45 @@ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive =
|
|
|
3074
3205
|
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
3075
3206
|
if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
|
|
3076
3207
|
}
|
|
3077
|
-
return null;
|
|
3208
|
+
return null;
|
|
3209
|
+
}
|
|
3210
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
3211
|
+
const transport = getTransport();
|
|
3212
|
+
if (!transport.isAlive(sessionName)) {
|
|
3213
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
3214
|
+
}
|
|
3215
|
+
let pane;
|
|
3216
|
+
try {
|
|
3217
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
3218
|
+
} catch (err) {
|
|
3219
|
+
return {
|
|
3220
|
+
atCapacity: false,
|
|
3221
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
3225
|
+
if (!isAtCapacity2(pane)) {
|
|
3226
|
+
return {
|
|
3227
|
+
atCapacity: false,
|
|
3228
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
return {
|
|
3232
|
+
atCapacity: true,
|
|
3233
|
+
reason: "capacity banner matched in recent pane output"
|
|
3234
|
+
};
|
|
3078
3235
|
}
|
|
3079
3236
|
function readDebounceState() {
|
|
3080
3237
|
try {
|
|
3081
|
-
if (!
|
|
3082
|
-
return JSON.parse(
|
|
3238
|
+
if (!existsSync12(DEBOUNCE_FILE)) return {};
|
|
3239
|
+
return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
|
|
3083
3240
|
} catch {
|
|
3084
3241
|
return {};
|
|
3085
3242
|
}
|
|
3086
3243
|
}
|
|
3087
3244
|
function writeDebounceState(state) {
|
|
3088
3245
|
try {
|
|
3089
|
-
if (!
|
|
3246
|
+
if (!existsSync12(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
3090
3247
|
writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
|
|
3091
3248
|
} catch {
|
|
3092
3249
|
}
|
|
@@ -3111,379 +3268,818 @@ function logIntercom(msg) {
|
|
|
3111
3268
|
process.stderr.write(`[intercom] ${msg}
|
|
3112
3269
|
`);
|
|
3113
3270
|
try {
|
|
3114
|
-
appendFileSync(INTERCOM_LOG2, line);
|
|
3271
|
+
appendFileSync(INTERCOM_LOG2, line);
|
|
3272
|
+
} catch {
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
function getSessionState(sessionName) {
|
|
3276
|
+
const transport = getTransport();
|
|
3277
|
+
if (!transport.isAlive(sessionName)) return "offline";
|
|
3278
|
+
try {
|
|
3279
|
+
const pane = transport.capturePane(sessionName, 5);
|
|
3280
|
+
if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
|
|
3281
|
+
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
3282
|
+
return "no_claude";
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
if (/Running…/.test(pane)) return "tool";
|
|
3286
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
3287
|
+
return "idle";
|
|
3288
|
+
} catch {
|
|
3289
|
+
return "offline";
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
function isSessionBusy(sessionName) {
|
|
3293
|
+
const state = getSessionState(sessionName);
|
|
3294
|
+
return state === "thinking" || state === "tool";
|
|
3295
|
+
}
|
|
3296
|
+
function isExeSession(sessionName) {
|
|
3297
|
+
return /^exe\d*$/.test(sessionName);
|
|
3298
|
+
}
|
|
3299
|
+
function sendIntercom(targetSession) {
|
|
3300
|
+
const transport = getTransport();
|
|
3301
|
+
if (isExeSession(targetSession)) {
|
|
3302
|
+
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
3303
|
+
return "skipped_exe";
|
|
3304
|
+
}
|
|
3305
|
+
if (isDebounced(targetSession)) {
|
|
3306
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
|
|
3307
|
+
return "debounced";
|
|
3308
|
+
}
|
|
3309
|
+
try {
|
|
3310
|
+
const sessions = transport.listSessions();
|
|
3311
|
+
if (!sessions.includes(targetSession)) {
|
|
3312
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
3313
|
+
return "failed";
|
|
3314
|
+
}
|
|
3315
|
+
const sessionState = getSessionState(targetSession);
|
|
3316
|
+
if (sessionState === "no_claude") {
|
|
3317
|
+
queueIntercom(targetSession, "claude not running in session");
|
|
3318
|
+
recordDebounce(targetSession);
|
|
3319
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
3320
|
+
return "queued";
|
|
3321
|
+
}
|
|
3322
|
+
if (sessionState === "thinking" || sessionState === "tool") {
|
|
3323
|
+
queueIntercom(targetSession, "session busy at send time");
|
|
3324
|
+
recordDebounce(targetSession);
|
|
3325
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
3326
|
+
return "queued";
|
|
3327
|
+
}
|
|
3328
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
3329
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
3330
|
+
transport.sendKeys(targetSession, "q");
|
|
3331
|
+
}
|
|
3332
|
+
transport.sendKeys(targetSession, "/exe-intercom");
|
|
3333
|
+
recordDebounce(targetSession);
|
|
3334
|
+
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
3335
|
+
return "delivered";
|
|
3336
|
+
} catch {
|
|
3337
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
3338
|
+
return "failed";
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
function notifyParentExe(sessionKey) {
|
|
3342
|
+
const target = getDispatchedBy(sessionKey);
|
|
3343
|
+
if (!target) {
|
|
3344
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
3345
|
+
`);
|
|
3346
|
+
return false;
|
|
3347
|
+
}
|
|
3348
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
3349
|
+
`);
|
|
3350
|
+
const result = sendIntercom(target);
|
|
3351
|
+
if (result === "failed") {
|
|
3352
|
+
const rootExe = resolveExeSession();
|
|
3353
|
+
if (rootExe && rootExe !== target) {
|
|
3354
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
3355
|
+
`);
|
|
3356
|
+
const fallback = sendIntercom(rootExe);
|
|
3357
|
+
return fallback !== "failed";
|
|
3358
|
+
}
|
|
3359
|
+
return false;
|
|
3360
|
+
}
|
|
3361
|
+
return true;
|
|
3362
|
+
}
|
|
3363
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
3364
|
+
if (employeeName === "exe") {
|
|
3365
|
+
return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
|
|
3366
|
+
}
|
|
3367
|
+
try {
|
|
3368
|
+
assertEmployeeLimitSync();
|
|
3369
|
+
} catch (err) {
|
|
3370
|
+
if (err instanceof PlanLimitError) {
|
|
3371
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
if (/-exe\d*$/.test(employeeName)) {
|
|
3375
|
+
const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
|
|
3376
|
+
return {
|
|
3377
|
+
status: "failed",
|
|
3378
|
+
sessionName: "",
|
|
3379
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
3383
|
+
const root = extractRootExe(exeSession);
|
|
3384
|
+
if (root) {
|
|
3385
|
+
process.stderr.write(
|
|
3386
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
|
|
3387
|
+
`
|
|
3388
|
+
);
|
|
3389
|
+
exeSession = root;
|
|
3390
|
+
} else {
|
|
3391
|
+
return {
|
|
3392
|
+
status: "failed",
|
|
3393
|
+
sessionName: "",
|
|
3394
|
+
error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
let effectiveInstance = opts?.instance;
|
|
3399
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
3400
|
+
const free = findFreeInstance(
|
|
3401
|
+
employeeName,
|
|
3402
|
+
exeSession,
|
|
3403
|
+
opts.maxAutoInstances ?? 10
|
|
3404
|
+
);
|
|
3405
|
+
if (free === null) {
|
|
3406
|
+
return {
|
|
3407
|
+
status: "failed",
|
|
3408
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
3409
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
3413
|
+
}
|
|
3414
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
3415
|
+
if (isEmployeeAlive(sessionName)) {
|
|
3416
|
+
const result2 = sendIntercom(sessionName);
|
|
3417
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
3418
|
+
return { status: "intercom_sent", sessionName };
|
|
3419
|
+
}
|
|
3420
|
+
if (result2 === "delivered") {
|
|
3421
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
3422
|
+
}
|
|
3423
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
3424
|
+
}
|
|
3425
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
3426
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
3427
|
+
if (result.error) {
|
|
3428
|
+
return { status: "failed", sessionName, error: result.error };
|
|
3429
|
+
}
|
|
3430
|
+
return { status: "spawned", sessionName };
|
|
3431
|
+
}
|
|
3432
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
3433
|
+
const transport = getTransport();
|
|
3434
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
3435
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
3436
|
+
const logDir = path13.join(os6.homedir(), ".exe-os", "session-logs");
|
|
3437
|
+
const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
3438
|
+
if (!existsSync12(logDir)) {
|
|
3439
|
+
mkdirSync5(logDir, { recursive: true });
|
|
3440
|
+
}
|
|
3441
|
+
transport.kill(sessionName);
|
|
3442
|
+
let cleanupSuffix = "";
|
|
3443
|
+
try {
|
|
3444
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
3445
|
+
const cleanupScript = path13.join(path13.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
3446
|
+
if (existsSync12(cleanupScript)) {
|
|
3447
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
3448
|
+
}
|
|
3449
|
+
} catch {
|
|
3450
|
+
}
|
|
3451
|
+
try {
|
|
3452
|
+
const claudeJsonPath = path13.join(os6.homedir(), ".claude.json");
|
|
3453
|
+
let claudeJson = {};
|
|
3454
|
+
try {
|
|
3455
|
+
claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
|
|
3456
|
+
} catch {
|
|
3457
|
+
}
|
|
3458
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
3459
|
+
const projects = claudeJson.projects;
|
|
3460
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
3461
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
3462
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
3463
|
+
writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
3464
|
+
} catch {
|
|
3465
|
+
}
|
|
3466
|
+
try {
|
|
3467
|
+
const settingsDir = path13.join(os6.homedir(), ".claude", "projects");
|
|
3468
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
3469
|
+
const projSettingsDir = path13.join(settingsDir, normalizedKey);
|
|
3470
|
+
const settingsPath = path13.join(projSettingsDir, "settings.json");
|
|
3471
|
+
let settings = {};
|
|
3472
|
+
try {
|
|
3473
|
+
settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
|
|
3474
|
+
} catch {
|
|
3475
|
+
}
|
|
3476
|
+
const perms = settings.permissions ?? {};
|
|
3477
|
+
const allow = perms.allow ?? [];
|
|
3478
|
+
const toolNames = [
|
|
3479
|
+
"recall_my_memory",
|
|
3480
|
+
"store_memory",
|
|
3481
|
+
"create_task",
|
|
3482
|
+
"update_task",
|
|
3483
|
+
"list_tasks",
|
|
3484
|
+
"get_task",
|
|
3485
|
+
"ask_team_memory",
|
|
3486
|
+
"store_behavior",
|
|
3487
|
+
"get_identity",
|
|
3488
|
+
"send_message"
|
|
3489
|
+
];
|
|
3490
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
3491
|
+
let changed = false;
|
|
3492
|
+
for (const tool of requiredTools) {
|
|
3493
|
+
if (!allow.includes(tool)) {
|
|
3494
|
+
allow.push(tool);
|
|
3495
|
+
changed = true;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
if (changed) {
|
|
3499
|
+
perms.allow = allow;
|
|
3500
|
+
settings.permissions = perms;
|
|
3501
|
+
mkdirSync5(projSettingsDir, { recursive: true });
|
|
3502
|
+
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3503
|
+
}
|
|
3504
|
+
} catch {
|
|
3505
|
+
}
|
|
3506
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
3507
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
3508
|
+
const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
3509
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
3510
|
+
let identityFlag = "";
|
|
3511
|
+
let behaviorsFlag = "";
|
|
3512
|
+
let legacyFallbackWarned = false;
|
|
3513
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
3514
|
+
const identityPath = path13.join(
|
|
3515
|
+
os6.homedir(),
|
|
3516
|
+
".exe-os",
|
|
3517
|
+
"identity",
|
|
3518
|
+
`${employeeName}.md`
|
|
3519
|
+
);
|
|
3520
|
+
_resetCcAgentSupportCache();
|
|
3521
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
3522
|
+
if (hasAgentFlag) {
|
|
3523
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
3524
|
+
} else if (existsSync12(identityPath)) {
|
|
3525
|
+
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
3526
|
+
legacyFallbackWarned = true;
|
|
3527
|
+
}
|
|
3528
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
3529
|
+
employeeName,
|
|
3530
|
+
path13.basename(spawnCwd),
|
|
3531
|
+
sessionName
|
|
3532
|
+
);
|
|
3533
|
+
if (behaviorsFile) {
|
|
3534
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
if (legacyFallbackWarned) {
|
|
3538
|
+
process.stderr.write(
|
|
3539
|
+
`[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.
|
|
3540
|
+
`
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
let sessionContextFlag = "";
|
|
3544
|
+
try {
|
|
3545
|
+
const ctxDir = path13.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3546
|
+
mkdirSync5(ctxDir, { recursive: true });
|
|
3547
|
+
const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
|
|
3548
|
+
const ctxContent = [
|
|
3549
|
+
`## Session Context`,
|
|
3550
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
3551
|
+
`Your parent exe session is ${exeSession}.`,
|
|
3552
|
+
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
3553
|
+
].join("\n");
|
|
3554
|
+
writeFileSync4(ctxFile, ctxContent);
|
|
3555
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
3115
3556
|
} catch {
|
|
3116
3557
|
}
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
3125
|
-
return "no_claude";
|
|
3558
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
3559
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
3560
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
3561
|
+
if (cfg?.apiKeyEnv) {
|
|
3562
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
3563
|
+
if (keyVal) {
|
|
3564
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
3126
3565
|
}
|
|
3127
3566
|
}
|
|
3128
|
-
if (/Running…/.test(pane)) return "tool";
|
|
3129
|
-
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
3130
|
-
return "idle";
|
|
3131
|
-
} catch {
|
|
3132
|
-
return "offline";
|
|
3133
3567
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3568
|
+
let spawnCommand;
|
|
3569
|
+
if (useExeAgent) {
|
|
3570
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
3571
|
+
} else if (useBinSymlink) {
|
|
3572
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
3573
|
+
process.stderr.write(
|
|
3574
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
3575
|
+
`
|
|
3576
|
+
);
|
|
3577
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
3578
|
+
} else {
|
|
3579
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
3143
3580
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3581
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
3582
|
+
cwd: spawnCwd,
|
|
3583
|
+
command: spawnCommand
|
|
3584
|
+
});
|
|
3585
|
+
if (spawnResult.error) {
|
|
3586
|
+
releaseSpawnLock2(sessionName);
|
|
3587
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
3147
3588
|
}
|
|
3589
|
+
transport.pipeLog(sessionName, logFile);
|
|
3148
3590
|
try {
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
3165
|
-
return "queued";
|
|
3591
|
+
const mySession = getMySession();
|
|
3592
|
+
const dispatchInfo = path13.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
3593
|
+
writeFileSync4(dispatchInfo, JSON.stringify({
|
|
3594
|
+
dispatchedBy: mySession,
|
|
3595
|
+
rootExe: exeSession,
|
|
3596
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
3597
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3598
|
+
}));
|
|
3599
|
+
} catch {
|
|
3600
|
+
}
|
|
3601
|
+
let booted = false;
|
|
3602
|
+
for (let i = 0; i < 30; i++) {
|
|
3603
|
+
try {
|
|
3604
|
+
execSync6("sleep 0.5");
|
|
3605
|
+
} catch {
|
|
3166
3606
|
}
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3607
|
+
try {
|
|
3608
|
+
const pane = transport.capturePane(sessionName);
|
|
3609
|
+
if (useExeAgent) {
|
|
3610
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
3611
|
+
booted = true;
|
|
3612
|
+
break;
|
|
3613
|
+
}
|
|
3614
|
+
} else {
|
|
3615
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
3616
|
+
booted = true;
|
|
3617
|
+
break;
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
} catch {
|
|
3170
3621
|
}
|
|
3171
|
-
transport.sendKeys(targetSession, "/exe-intercom");
|
|
3172
|
-
recordDebounce(targetSession);
|
|
3173
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
3174
|
-
return "delivered";
|
|
3175
|
-
} catch {
|
|
3176
|
-
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
3177
|
-
return "failed";
|
|
3178
3622
|
}
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
if (!target) {
|
|
3183
|
-
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
3184
|
-
`);
|
|
3185
|
-
return false;
|
|
3623
|
+
if (!booted) {
|
|
3624
|
+
releaseSpawnLock2(sessionName);
|
|
3625
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
3186
3626
|
}
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
const rootExe = resolveExeSession();
|
|
3192
|
-
if (rootExe && rootExe !== target) {
|
|
3193
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
3194
|
-
`);
|
|
3195
|
-
const fallback = sendIntercom(rootExe);
|
|
3196
|
-
return fallback !== "failed";
|
|
3627
|
+
if (!useExeAgent) {
|
|
3628
|
+
try {
|
|
3629
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
3630
|
+
} catch {
|
|
3197
3631
|
}
|
|
3198
|
-
return false;
|
|
3199
3632
|
}
|
|
3200
|
-
|
|
3633
|
+
registerSession({
|
|
3634
|
+
windowName: sessionName,
|
|
3635
|
+
agentId: employeeName,
|
|
3636
|
+
projectDir: spawnCwd,
|
|
3637
|
+
parentExe: exeSession,
|
|
3638
|
+
pid: 0,
|
|
3639
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3640
|
+
});
|
|
3641
|
+
releaseSpawnLock2(sessionName);
|
|
3642
|
+
return { sessionName };
|
|
3201
3643
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3644
|
+
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;
|
|
3645
|
+
var init_tmux_routing = __esm({
|
|
3646
|
+
"src/lib/tmux-routing.ts"() {
|
|
3647
|
+
"use strict";
|
|
3648
|
+
init_session_registry();
|
|
3649
|
+
init_session_key();
|
|
3650
|
+
init_transport();
|
|
3651
|
+
init_cc_agent_support();
|
|
3652
|
+
init_mcp_prefix();
|
|
3653
|
+
init_provider_table();
|
|
3654
|
+
init_intercom_queue();
|
|
3655
|
+
init_plan_limits();
|
|
3656
|
+
SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
3657
|
+
SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3658
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
3659
|
+
VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
|
|
3660
|
+
VERIFY_PANE_LINES = 200;
|
|
3661
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
3662
|
+
INTERCOM_LOG2 = path13.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
3663
|
+
DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3664
|
+
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3665
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
3205
3666
|
}
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3667
|
+
});
|
|
3668
|
+
|
|
3669
|
+
// src/lib/tasks-crud.ts
|
|
3670
|
+
import crypto5 from "crypto";
|
|
3671
|
+
import path14 from "path";
|
|
3672
|
+
import { execSync as execSync7 } from "child_process";
|
|
3673
|
+
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
3674
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
3675
|
+
async function writeCheckpoint(input2) {
|
|
3676
|
+
const client = getClient();
|
|
3677
|
+
const row = await resolveTask(client, input2.taskId);
|
|
3678
|
+
const taskId = String(row.id);
|
|
3679
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3680
|
+
const blockedByIds = [];
|
|
3681
|
+
if (row.blocked_by) {
|
|
3682
|
+
blockedByIds.push(String(row.blocked_by));
|
|
3212
3683
|
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3684
|
+
const checkpoint = {
|
|
3685
|
+
step: input2.step,
|
|
3686
|
+
context_summary: input2.contextSummary,
|
|
3687
|
+
files_touched: input2.filesTouched ?? [],
|
|
3688
|
+
blocked_by_ids: blockedByIds,
|
|
3689
|
+
last_checkpoint_at: now
|
|
3690
|
+
};
|
|
3691
|
+
const result = await client.execute({
|
|
3692
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
3693
|
+
args: [JSON.stringify(checkpoint), now, taskId]
|
|
3694
|
+
});
|
|
3695
|
+
if (result.rowsAffected === 0) {
|
|
3696
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
3220
3697
|
}
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3698
|
+
const countResult = await client.execute({
|
|
3699
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
3700
|
+
args: [taskId]
|
|
3701
|
+
});
|
|
3702
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
3703
|
+
return { checkpointCount };
|
|
3704
|
+
}
|
|
3705
|
+
function extractParentFromContext(contextBody) {
|
|
3706
|
+
if (!contextBody) return null;
|
|
3707
|
+
const match = contextBody.match(
|
|
3708
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
3709
|
+
);
|
|
3710
|
+
return match ? match[1].toLowerCase() : null;
|
|
3711
|
+
}
|
|
3712
|
+
function slugify(title) {
|
|
3713
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
3714
|
+
}
|
|
3715
|
+
async function resolveTask(client, identifier) {
|
|
3716
|
+
let result = await client.execute({
|
|
3717
|
+
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
3718
|
+
args: [identifier]
|
|
3719
|
+
});
|
|
3720
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3721
|
+
result = await client.execute({
|
|
3722
|
+
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
3723
|
+
args: [`%${identifier}%`]
|
|
3724
|
+
});
|
|
3725
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3726
|
+
if (result.rows.length > 1) {
|
|
3727
|
+
const exact = result.rows.filter(
|
|
3728
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
3729
|
+
);
|
|
3730
|
+
if (exact.length === 1) return exact[0];
|
|
3731
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
3732
|
+
const active = candidates.filter(
|
|
3733
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3734
|
+
);
|
|
3735
|
+
if (active.length === 1) return active[0];
|
|
3736
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3737
|
+
throw new Error(
|
|
3738
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3227
3739
|
);
|
|
3228
|
-
if (free === null) {
|
|
3229
|
-
return {
|
|
3230
|
-
status: "failed",
|
|
3231
|
-
sessionName: employeeSessionName(employeeName, exeSession),
|
|
3232
|
-
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
3233
|
-
};
|
|
3234
|
-
}
|
|
3235
|
-
effectiveInstance = free === 0 ? void 0 : free;
|
|
3236
|
-
}
|
|
3237
|
-
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
3238
|
-
if (isEmployeeAlive(sessionName)) {
|
|
3239
|
-
const result2 = sendIntercom(sessionName);
|
|
3240
|
-
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
3241
|
-
return { status: "intercom_sent", sessionName };
|
|
3242
|
-
}
|
|
3243
|
-
if (result2 === "delivered") {
|
|
3244
|
-
return { status: "intercom_unprocessed", sessionName };
|
|
3245
|
-
}
|
|
3246
|
-
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
3247
3740
|
}
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3741
|
+
result = await client.execute({
|
|
3742
|
+
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
3743
|
+
args: [`%${identifier}%`]
|
|
3744
|
+
});
|
|
3745
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3746
|
+
if (result.rows.length > 1) {
|
|
3747
|
+
const active = result.rows.filter(
|
|
3748
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3749
|
+
);
|
|
3750
|
+
if (active.length === 1) return active[0];
|
|
3751
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3752
|
+
throw new Error(
|
|
3753
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3754
|
+
);
|
|
3252
3755
|
}
|
|
3253
|
-
|
|
3756
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
3254
3757
|
}
|
|
3255
|
-
function
|
|
3256
|
-
const
|
|
3257
|
-
const
|
|
3258
|
-
const
|
|
3259
|
-
const
|
|
3260
|
-
const
|
|
3261
|
-
|
|
3262
|
-
|
|
3758
|
+
async function createTaskCore(input2) {
|
|
3759
|
+
const client = getClient();
|
|
3760
|
+
const id = crypto5.randomUUID();
|
|
3761
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3762
|
+
const slug = slugify(input2.title);
|
|
3763
|
+
const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
|
|
3764
|
+
let blockedById = null;
|
|
3765
|
+
const initialStatus = input2.blockedBy ? "blocked" : "open";
|
|
3766
|
+
if (input2.blockedBy) {
|
|
3767
|
+
const blocker = await resolveTask(client, input2.blockedBy);
|
|
3768
|
+
blockedById = String(blocker.id);
|
|
3263
3769
|
}
|
|
3264
|
-
|
|
3265
|
-
let
|
|
3266
|
-
|
|
3267
|
-
const
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3770
|
+
let parentTaskId = null;
|
|
3771
|
+
let parentRef = input2.parentTaskId;
|
|
3772
|
+
if (!parentRef) {
|
|
3773
|
+
const extracted = extractParentFromContext(input2.context);
|
|
3774
|
+
if (extracted) {
|
|
3775
|
+
parentRef = extracted;
|
|
3776
|
+
process.stderr.write(
|
|
3777
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
3778
|
+
);
|
|
3271
3779
|
}
|
|
3272
|
-
} catch {
|
|
3273
3780
|
}
|
|
3274
|
-
|
|
3275
|
-
const claudeJsonPath = path14.join(os6.homedir(), ".claude.json");
|
|
3276
|
-
let claudeJson = {};
|
|
3781
|
+
if (parentRef) {
|
|
3277
3782
|
try {
|
|
3278
|
-
|
|
3279
|
-
|
|
3783
|
+
const parent = await resolveTask(client, parentRef);
|
|
3784
|
+
parentTaskId = String(parent.id);
|
|
3785
|
+
} catch (err) {
|
|
3786
|
+
if (!input2.parentTaskId) {
|
|
3787
|
+
throw new Error(
|
|
3788
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3791
|
+
throw err;
|
|
3280
3792
|
}
|
|
3281
|
-
if (!claudeJson.projects) claudeJson.projects = {};
|
|
3282
|
-
const projects = claudeJson.projects;
|
|
3283
|
-
const trustDir = opts?.cwd ?? projectDir;
|
|
3284
|
-
if (!projects[trustDir]) projects[trustDir] = {};
|
|
3285
|
-
projects[trustDir].hasTrustDialogAccepted = true;
|
|
3286
|
-
writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
3287
|
-
} catch {
|
|
3288
3793
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3794
|
+
let warning;
|
|
3795
|
+
const dupCheck = await client.execute({
|
|
3796
|
+
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
3797
|
+
args: [input2.title, input2.assignedTo]
|
|
3798
|
+
});
|
|
3799
|
+
if (dupCheck.rows.length > 0) {
|
|
3800
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
3801
|
+
}
|
|
3802
|
+
if (input2.baseDir) {
|
|
3295
3803
|
try {
|
|
3296
|
-
|
|
3804
|
+
await mkdir4(path14.join(input2.baseDir, "exe", "output"), { recursive: true });
|
|
3805
|
+
await mkdir4(path14.join(input2.baseDir, "exe", "research"), { recursive: true });
|
|
3806
|
+
await ensureArchitectureDoc(input2.baseDir, input2.projectName);
|
|
3807
|
+
await ensureGitignoreExe(input2.baseDir);
|
|
3297
3808
|
} catch {
|
|
3298
3809
|
}
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
"update_task",
|
|
3306
|
-
"list_tasks",
|
|
3307
|
-
"get_task",
|
|
3308
|
-
"ask_team_memory",
|
|
3309
|
-
"store_behavior",
|
|
3310
|
-
"get_identity",
|
|
3311
|
-
"send_message"
|
|
3312
|
-
];
|
|
3313
|
-
const requiredTools = expandDualPrefixTools(toolNames);
|
|
3314
|
-
let changed = false;
|
|
3315
|
-
for (const tool of requiredTools) {
|
|
3316
|
-
if (!allow.includes(tool)) {
|
|
3317
|
-
allow.push(tool);
|
|
3318
|
-
changed = true;
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
if (changed) {
|
|
3322
|
-
perms.allow = allow;
|
|
3323
|
-
settings.permissions = perms;
|
|
3324
|
-
mkdirSync5(projSettingsDir, { recursive: true });
|
|
3325
|
-
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3326
|
-
}
|
|
3810
|
+
}
|
|
3811
|
+
const complexity = input2.complexity ?? "standard";
|
|
3812
|
+
let sessionScope = null;
|
|
3813
|
+
try {
|
|
3814
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3815
|
+
sessionScope = resolveExeSession2();
|
|
3327
3816
|
} catch {
|
|
3328
3817
|
}
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3818
|
+
await client.execute({
|
|
3819
|
+
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)
|
|
3820
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3821
|
+
args: [
|
|
3822
|
+
id,
|
|
3823
|
+
input2.title,
|
|
3824
|
+
input2.assignedTo,
|
|
3825
|
+
input2.assignedBy,
|
|
3826
|
+
input2.projectName,
|
|
3827
|
+
input2.priority,
|
|
3828
|
+
initialStatus,
|
|
3829
|
+
taskFile,
|
|
3830
|
+
blockedById,
|
|
3831
|
+
parentTaskId,
|
|
3832
|
+
input2.reviewer ?? null,
|
|
3833
|
+
input2.context,
|
|
3834
|
+
complexity,
|
|
3835
|
+
input2.budgetTokens ?? null,
|
|
3836
|
+
input2.budgetFallbackModel ?? null,
|
|
3837
|
+
0,
|
|
3838
|
+
null,
|
|
3839
|
+
sessionScope,
|
|
3840
|
+
now,
|
|
3841
|
+
now
|
|
3842
|
+
]
|
|
3843
|
+
});
|
|
3844
|
+
return {
|
|
3845
|
+
id,
|
|
3846
|
+
title: input2.title,
|
|
3847
|
+
assignedTo: input2.assignedTo,
|
|
3848
|
+
assignedBy: input2.assignedBy,
|
|
3849
|
+
projectName: input2.projectName,
|
|
3850
|
+
priority: input2.priority,
|
|
3851
|
+
status: initialStatus,
|
|
3852
|
+
taskFile,
|
|
3853
|
+
createdAt: now,
|
|
3854
|
+
updatedAt: now,
|
|
3855
|
+
warning,
|
|
3856
|
+
budgetTokens: input2.budgetTokens ?? null,
|
|
3857
|
+
budgetFallbackModel: input2.budgetFallbackModel ?? null,
|
|
3858
|
+
tokensUsed: 0,
|
|
3859
|
+
tokensWarnedAt: null
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
async function listTasks(input2) {
|
|
3863
|
+
const client = getClient();
|
|
3864
|
+
const conditions = [];
|
|
3865
|
+
const args = [];
|
|
3866
|
+
if (input2.assignedTo) {
|
|
3867
|
+
conditions.push("assigned_to = ?");
|
|
3868
|
+
args.push(input2.assignedTo);
|
|
3869
|
+
}
|
|
3870
|
+
if (input2.status) {
|
|
3871
|
+
conditions.push("status = ?");
|
|
3872
|
+
args.push(input2.status);
|
|
3873
|
+
} else {
|
|
3874
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
3875
|
+
}
|
|
3876
|
+
if (input2.projectName) {
|
|
3877
|
+
conditions.push("project_name = ?");
|
|
3878
|
+
args.push(input2.projectName);
|
|
3879
|
+
}
|
|
3880
|
+
if (input2.priority) {
|
|
3881
|
+
conditions.push("priority = ?");
|
|
3882
|
+
args.push(input2.priority);
|
|
3883
|
+
}
|
|
3884
|
+
try {
|
|
3885
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3886
|
+
const session = resolveExeSession2();
|
|
3887
|
+
if (session) {
|
|
3888
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
3889
|
+
args.push(session);
|
|
3350
3890
|
}
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3891
|
+
} catch {
|
|
3892
|
+
}
|
|
3893
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3894
|
+
const result = await client.execute({
|
|
3895
|
+
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`,
|
|
3896
|
+
args
|
|
3897
|
+
});
|
|
3898
|
+
return result.rows.map((r) => ({
|
|
3899
|
+
id: String(r.id),
|
|
3900
|
+
title: String(r.title),
|
|
3901
|
+
assignedTo: String(r.assigned_to),
|
|
3902
|
+
assignedBy: String(r.assigned_by),
|
|
3903
|
+
projectName: String(r.project_name),
|
|
3904
|
+
priority: String(r.priority),
|
|
3905
|
+
status: String(r.status),
|
|
3906
|
+
taskFile: String(r.task_file),
|
|
3907
|
+
createdAt: String(r.created_at),
|
|
3908
|
+
updatedAt: String(r.updated_at),
|
|
3909
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
3910
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
3911
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
3912
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
3913
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
3914
|
+
}));
|
|
3915
|
+
}
|
|
3916
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
3917
|
+
if (!taskContext) return null;
|
|
3918
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
3919
|
+
try {
|
|
3920
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
3921
|
+
const branch = execSync7(
|
|
3922
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
3923
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
3924
|
+
).trim();
|
|
3925
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
3926
|
+
const commitCount = execSync7(
|
|
3927
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
3928
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
3929
|
+
).trim();
|
|
3930
|
+
const count = parseInt(commitCount, 10);
|
|
3931
|
+
if (count === 0) {
|
|
3932
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
3358
3933
|
}
|
|
3934
|
+
return null;
|
|
3935
|
+
} catch {
|
|
3936
|
+
return null;
|
|
3359
3937
|
}
|
|
3360
|
-
|
|
3938
|
+
}
|
|
3939
|
+
async function updateTaskStatus(input2) {
|
|
3940
|
+
const client = getClient();
|
|
3941
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3942
|
+
const row = await resolveTask(client, input2.taskId);
|
|
3943
|
+
const taskId = String(row.id);
|
|
3944
|
+
const taskFile = String(row.task_file);
|
|
3945
|
+
if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
3361
3946
|
process.stderr.write(
|
|
3362
|
-
`[
|
|
3947
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
3363
3948
|
`
|
|
3364
3949
|
);
|
|
3365
3950
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
}
|
|
3381
|
-
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
3382
|
-
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
3383
|
-
const cfg = PROVIDER_TABLE[ccProvider];
|
|
3384
|
-
if (cfg?.apiKeyEnv) {
|
|
3385
|
-
const keyVal = process.env[cfg.apiKeyEnv];
|
|
3386
|
-
if (keyVal) {
|
|
3387
|
-
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
3951
|
+
if (input2.status === "done") {
|
|
3952
|
+
const existingRow = await client.execute({
|
|
3953
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
3954
|
+
args: [taskId]
|
|
3955
|
+
});
|
|
3956
|
+
if (existingRow.rows.length > 0) {
|
|
3957
|
+
const ctx = existingRow.rows[0];
|
|
3958
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
3959
|
+
if (warning) {
|
|
3960
|
+
input2.result = input2.result ? `\u26A0\uFE0F ${warning}
|
|
3961
|
+
|
|
3962
|
+
${input2.result}` : `\u26A0\uFE0F ${warning}`;
|
|
3963
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
3964
|
+
`);
|
|
3388
3965
|
}
|
|
3389
3966
|
}
|
|
3390
3967
|
}
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
)
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
if (spawnResult.error) {
|
|
3409
|
-
releaseSpawnLock2(sessionName);
|
|
3410
|
-
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
3411
|
-
}
|
|
3412
|
-
transport.pipeLog(sessionName, logFile);
|
|
3413
|
-
try {
|
|
3414
|
-
const mySession = getMySession();
|
|
3415
|
-
const dispatchInfo = path14.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
3416
|
-
writeFileSync4(dispatchInfo, JSON.stringify({
|
|
3417
|
-
dispatchedBy: mySession,
|
|
3418
|
-
rootExe: exeSession,
|
|
3419
|
-
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
3420
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3421
|
-
}));
|
|
3422
|
-
} catch {
|
|
3423
|
-
}
|
|
3424
|
-
let booted = false;
|
|
3425
|
-
for (let i = 0; i < 30; i++) {
|
|
3426
|
-
try {
|
|
3427
|
-
execSync7("sleep 0.5");
|
|
3428
|
-
} catch {
|
|
3968
|
+
if (input2.status === "in_progress") {
|
|
3969
|
+
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
3970
|
+
const claim = await client.execute({
|
|
3971
|
+
sql: `UPDATE tasks
|
|
3972
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
3973
|
+
WHERE id = ? AND status = 'open'`,
|
|
3974
|
+
args: [tmuxSession, now, taskId]
|
|
3975
|
+
});
|
|
3976
|
+
if (claim.rowsAffected === 0) {
|
|
3977
|
+
const current = await client.execute({
|
|
3978
|
+
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
3979
|
+
args: [taskId]
|
|
3980
|
+
});
|
|
3981
|
+
const cur = current.rows[0];
|
|
3982
|
+
const status = cur?.status ?? "unknown";
|
|
3983
|
+
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
3984
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
3429
3985
|
}
|
|
3430
3986
|
try {
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
}
|
|
3437
|
-
} else {
|
|
3438
|
-
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
3439
|
-
booted = true;
|
|
3440
|
-
break;
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3987
|
+
await writeCheckpoint({
|
|
3988
|
+
taskId,
|
|
3989
|
+
step: "claimed",
|
|
3990
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
3991
|
+
});
|
|
3443
3992
|
} catch {
|
|
3444
3993
|
}
|
|
3994
|
+
return { row, taskFile, now, taskId };
|
|
3445
3995
|
}
|
|
3446
|
-
if (
|
|
3447
|
-
|
|
3448
|
-
|
|
3996
|
+
if (input2.result) {
|
|
3997
|
+
await client.execute({
|
|
3998
|
+
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
3999
|
+
args: [input2.status, input2.result, now, taskId]
|
|
4000
|
+
});
|
|
4001
|
+
} else {
|
|
4002
|
+
await client.execute({
|
|
4003
|
+
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
4004
|
+
args: [input2.status, now, taskId]
|
|
4005
|
+
});
|
|
3449
4006
|
}
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
4007
|
+
try {
|
|
4008
|
+
await writeCheckpoint({
|
|
4009
|
+
taskId,
|
|
4010
|
+
step: `status_transition:${input2.status}`,
|
|
4011
|
+
contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
|
|
4012
|
+
});
|
|
4013
|
+
} catch {
|
|
4014
|
+
}
|
|
4015
|
+
return { row, taskFile, now, taskId };
|
|
4016
|
+
}
|
|
4017
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
4018
|
+
const client = getClient();
|
|
4019
|
+
const row = await resolveTask(client, taskId);
|
|
4020
|
+
const id = String(row.id);
|
|
4021
|
+
const taskFile = String(row.task_file);
|
|
4022
|
+
const assignedTo = String(row.assigned_to);
|
|
4023
|
+
const assignedBy = String(row.assigned_by);
|
|
4024
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
4025
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
4026
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
4027
|
+
}
|
|
4028
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
4029
|
+
const archPath = path14.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
4030
|
+
try {
|
|
4031
|
+
if (existsSync13(archPath)) return;
|
|
4032
|
+
const template = [
|
|
4033
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
4034
|
+
"",
|
|
4035
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
4036
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
4037
|
+
"",
|
|
4038
|
+
"## Overview",
|
|
4039
|
+
"",
|
|
4040
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
4041
|
+
"",
|
|
4042
|
+
"## Key Components",
|
|
4043
|
+
"",
|
|
4044
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
4045
|
+
"",
|
|
4046
|
+
"## Data Flow",
|
|
4047
|
+
"",
|
|
4048
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
4049
|
+
"",
|
|
4050
|
+
"## Invariants",
|
|
4051
|
+
"",
|
|
4052
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
4053
|
+
"",
|
|
4054
|
+
"## Dependencies",
|
|
4055
|
+
"",
|
|
4056
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
4057
|
+
""
|
|
4058
|
+
].join("\n");
|
|
4059
|
+
await writeFile4(archPath, template, "utf-8");
|
|
4060
|
+
} catch {
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
async function ensureGitignoreExe(baseDir) {
|
|
4064
|
+
const gitignorePath = path14.join(baseDir, ".gitignore");
|
|
4065
|
+
try {
|
|
4066
|
+
if (existsSync13(gitignorePath)) {
|
|
4067
|
+
const content = readFileSync11(gitignorePath, "utf-8");
|
|
4068
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
4069
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
4070
|
+
} else {
|
|
4071
|
+
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
3454
4072
|
}
|
|
4073
|
+
} catch {
|
|
3455
4074
|
}
|
|
3456
|
-
registerSession({
|
|
3457
|
-
windowName: sessionName,
|
|
3458
|
-
agentId: employeeName,
|
|
3459
|
-
projectDir: spawnCwd,
|
|
3460
|
-
parentExe: exeSession,
|
|
3461
|
-
pid: 0,
|
|
3462
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3463
|
-
});
|
|
3464
|
-
releaseSpawnLock2(sessionName);
|
|
3465
|
-
return { sessionName };
|
|
3466
4075
|
}
|
|
3467
|
-
var
|
|
3468
|
-
var
|
|
3469
|
-
"src/lib/
|
|
4076
|
+
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
4077
|
+
var init_tasks_crud = __esm({
|
|
4078
|
+
"src/lib/tasks-crud.ts"() {
|
|
3470
4079
|
"use strict";
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
init_cc_agent_support();
|
|
3475
|
-
init_mcp_prefix();
|
|
3476
|
-
init_provider_table();
|
|
3477
|
-
init_intercom_queue();
|
|
3478
|
-
init_plan_limits();
|
|
3479
|
-
SPAWN_LOCK_DIR = path14.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
3480
|
-
SESSION_CACHE = path14.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3481
|
-
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
3482
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
3483
|
-
INTERCOM_LOG2 = path14.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
3484
|
-
DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3485
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3486
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
4080
|
+
init_database();
|
|
4081
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
4082
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
3487
4083
|
}
|
|
3488
4084
|
});
|
|
3489
4085
|
|
|
@@ -3658,6 +4254,7 @@ var init_tasks_review = __esm({
|
|
|
3658
4254
|
init_tasks_crud();
|
|
3659
4255
|
init_tmux_routing();
|
|
3660
4256
|
init_session_key();
|
|
4257
|
+
init_state_bus();
|
|
3661
4258
|
}
|
|
3662
4259
|
});
|
|
3663
4260
|
|
|
@@ -3780,13 +4377,12 @@ function assertSessionScope(actionType, targetProject) {
|
|
|
3780
4377
|
};
|
|
3781
4378
|
}
|
|
3782
4379
|
process.stderr.write(
|
|
3783
|
-
`[session-scope]
|
|
4380
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
3784
4381
|
`
|
|
3785
4382
|
);
|
|
3786
4383
|
return {
|
|
3787
|
-
allowed:
|
|
3788
|
-
|
|
3789
|
-
reason: "cross_session_granted",
|
|
4384
|
+
allowed: false,
|
|
4385
|
+
reason: "cross_session_denied",
|
|
3790
4386
|
currentProject,
|
|
3791
4387
|
targetProject,
|
|
3792
4388
|
targetSession: findSessionForProject(targetProject)?.windowName
|
|
@@ -3812,8 +4408,9 @@ async function dispatchTaskToEmployee(input2) {
|
|
|
3812
4408
|
try {
|
|
3813
4409
|
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
3814
4410
|
const check = assertSessionScope2("dispatch_task", input2.projectName);
|
|
3815
|
-
if (check.reason === "
|
|
4411
|
+
if (check.reason === "cross_session_denied") {
|
|
3816
4412
|
crossProject = true;
|
|
4413
|
+
return { dispatched: "skipped", crossProject: true };
|
|
3817
4414
|
}
|
|
3818
4415
|
} catch {
|
|
3819
4416
|
}
|
|
@@ -3870,10 +4467,10 @@ var init_tasks_notify = __esm({
|
|
|
3870
4467
|
});
|
|
3871
4468
|
|
|
3872
4469
|
// src/lib/behaviors.ts
|
|
3873
|
-
import
|
|
4470
|
+
import crypto6 from "crypto";
|
|
3874
4471
|
async function storeBehavior(opts) {
|
|
3875
4472
|
const client = getClient();
|
|
3876
|
-
const id =
|
|
4473
|
+
const id = crypto6.randomUUID();
|
|
3877
4474
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3878
4475
|
await client.execute({
|
|
3879
4476
|
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
@@ -3902,7 +4499,7 @@ __export(skill_learning_exports, {
|
|
|
3902
4499
|
storeTrajectory: () => storeTrajectory,
|
|
3903
4500
|
sweepTrajectories: () => sweepTrajectories
|
|
3904
4501
|
});
|
|
3905
|
-
import
|
|
4502
|
+
import crypto7 from "crypto";
|
|
3906
4503
|
async function extractTrajectory(taskId, agentId) {
|
|
3907
4504
|
const client = getClient();
|
|
3908
4505
|
const result = await client.execute({
|
|
@@ -3931,11 +4528,11 @@ async function extractTrajectory(taskId, agentId) {
|
|
|
3931
4528
|
return signature;
|
|
3932
4529
|
}
|
|
3933
4530
|
function hashSignature(signature) {
|
|
3934
|
-
return
|
|
4531
|
+
return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
3935
4532
|
}
|
|
3936
4533
|
async function storeTrajectory(opts) {
|
|
3937
4534
|
const client = getClient();
|
|
3938
|
-
const id =
|
|
4535
|
+
const id = crypto7.randomUUID();
|
|
3939
4536
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3940
4537
|
const signatureHash = hashSignature(opts.signature);
|
|
3941
4538
|
await client.execute({
|
|
@@ -4276,6 +4873,13 @@ async function updateTask(input2) {
|
|
|
4276
4873
|
await cascadeUnblock(taskId, input2.baseDir, now);
|
|
4277
4874
|
} catch {
|
|
4278
4875
|
}
|
|
4876
|
+
orgBus.emit({
|
|
4877
|
+
type: "task_completed",
|
|
4878
|
+
taskId,
|
|
4879
|
+
employee: String(row.assigned_to),
|
|
4880
|
+
result: input2.result ?? "",
|
|
4881
|
+
timestamp: now
|
|
4882
|
+
});
|
|
4279
4883
|
if (row.parent_task_id) {
|
|
4280
4884
|
try {
|
|
4281
4885
|
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
@@ -4343,6 +4947,7 @@ var init_tasks = __esm({
|
|
|
4343
4947
|
init_database();
|
|
4344
4948
|
init_config();
|
|
4345
4949
|
init_notifications();
|
|
4950
|
+
init_state_bus();
|
|
4346
4951
|
init_tasks_crud();
|
|
4347
4952
|
init_tasks_review();
|
|
4348
4953
|
init_tasks_crud();
|
|
@@ -4411,7 +5016,7 @@ var init_worker_gate = __esm({
|
|
|
4411
5016
|
});
|
|
4412
5017
|
|
|
4413
5018
|
// src/adapters/claude/hooks/ingest-worker.ts
|
|
4414
|
-
import
|
|
5019
|
+
import crypto8 from "crypto";
|
|
4415
5020
|
import { execSync as execSync8 } from "child_process";
|
|
4416
5021
|
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
4417
5022
|
import path19 from "path";
|
|
@@ -4570,6 +5175,7 @@ async function getMasterKey() {
|
|
|
4570
5175
|
|
|
4571
5176
|
// src/lib/store.ts
|
|
4572
5177
|
init_config();
|
|
5178
|
+
init_state_bus();
|
|
4573
5179
|
var INIT_MAX_RETRIES = 3;
|
|
4574
5180
|
var INIT_RETRY_DELAY_MS = 1e3;
|
|
4575
5181
|
function isBusyError2(err) {
|
|
@@ -4640,6 +5246,11 @@ async function initStore(options) {
|
|
|
4640
5246
|
"version-query"
|
|
4641
5247
|
);
|
|
4642
5248
|
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
5249
|
+
try {
|
|
5250
|
+
const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
|
|
5251
|
+
await loadGlobalProcedures2();
|
|
5252
|
+
} catch {
|
|
5253
|
+
}
|
|
4643
5254
|
}
|
|
4644
5255
|
function classifyTier(record) {
|
|
4645
5256
|
if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
|
|
@@ -4681,6 +5292,12 @@ async function writeMemory(record) {
|
|
|
4681
5292
|
supersedes_id: record.supersedes_id ?? null
|
|
4682
5293
|
};
|
|
4683
5294
|
_pendingRecords.push(dbRow);
|
|
5295
|
+
orgBus.emit({
|
|
5296
|
+
type: "memory_stored",
|
|
5297
|
+
agentId: record.agent_id,
|
|
5298
|
+
project: record.project_name,
|
|
5299
|
+
timestamp: record.timestamp
|
|
5300
|
+
});
|
|
4684
5301
|
const MAX_PENDING = 1e3;
|
|
4685
5302
|
if (_pendingRecords.length > MAX_PENDING) {
|
|
4686
5303
|
const dropped = _pendingRecords.length - MAX_PENDING;
|
|
@@ -5042,7 +5659,14 @@ process.stdin.on("data", (chunk) => {
|
|
|
5042
5659
|
});
|
|
5043
5660
|
process.stdin.on("end", async () => {
|
|
5044
5661
|
try {
|
|
5045
|
-
|
|
5662
|
+
let data;
|
|
5663
|
+
try {
|
|
5664
|
+
data = JSON.parse(input);
|
|
5665
|
+
} catch (parseErr) {
|
|
5666
|
+
process.stderr.write(`[ingest-worker] skipped: truncated stdin (${input.length} bytes)
|
|
5667
|
+
`);
|
|
5668
|
+
process.exit(0);
|
|
5669
|
+
}
|
|
5046
5670
|
const rawText = extractSemanticText(data.tool_name, data.tool_input, data.tool_response);
|
|
5047
5671
|
if (rawText.length < 50) {
|
|
5048
5672
|
process.exit(0);
|
|
@@ -5082,7 +5706,7 @@ process.stdin.on("end", async () => {
|
|
|
5082
5706
|
}
|
|
5083
5707
|
const agentId = process.env.AGENT_ID;
|
|
5084
5708
|
await writeMemory({
|
|
5085
|
-
id:
|
|
5709
|
+
id: crypto8.randomUUID(),
|
|
5086
5710
|
agent_id: agentId,
|
|
5087
5711
|
agent_role: process.env.AGENT_ROLE ?? "unknown",
|
|
5088
5712
|
session_id: data.session_id,
|
|
@@ -5192,7 +5816,7 @@ process.stdin.on("end", async () => {
|
|
|
5192
5816
|
});
|
|
5193
5817
|
const assignmentText = `TASK ASSIGNED: ${title} \u2192 ${employee} [${priority}] in ${projectName}`;
|
|
5194
5818
|
await writeMemory({
|
|
5195
|
-
id:
|
|
5819
|
+
id: crypto8.randomUUID(),
|
|
5196
5820
|
agent_id: assignedBy,
|
|
5197
5821
|
agent_role: "COO",
|
|
5198
5822
|
session_id: data.session_id,
|