@askexenow/exe-os 0.8.40 → 0.8.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/backfill-conversations.js +805 -642
- package/dist/bin/backfill-responses.js +804 -641
- package/dist/bin/backfill-vectors.js +791 -634
- package/dist/bin/cleanup-stale-review-tasks.js +788 -631
- package/dist/bin/cli.js +1376 -659
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2549 -1784
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +12 -2
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +791 -634
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2501 -1846
- package/dist/bin/exe-heartbeat.js +147 -1
- package/dist/bin/exe-kill.js +795 -640
- package/dist/bin/exe-launch-agent.js +2168 -2008
- package/dist/bin/exe-link.js +44 -12
- package/dist/bin/exe-new-employee.js +6 -2
- package/dist/bin/exe-pending-messages.js +146 -1
- package/dist/bin/exe-pending-notifications.js +788 -631
- package/dist/bin/exe-pending-reviews.js +176 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +157 -4
- package/dist/bin/exe-session-cleanup.js +2487 -403
- package/dist/bin/exe-settings.js +2 -1
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2691 -151
- package/dist/bin/graph-backfill.js +794 -637
- package/dist/bin/graph-export.js +798 -641
- package/dist/bin/scan-tasks.js +2951 -44
- package/dist/bin/setup.js +50 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2542 -1887
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2690 -150
- package/dist/hooks/error-recall.js +157 -4
- package/dist/hooks/ingest-worker.js +1455 -803
- package/dist/hooks/instructions-loaded.js +151 -0
- package/dist/hooks/notification.js +153 -2
- package/dist/hooks/post-compact.js +164 -0
- package/dist/hooks/pre-compact.js +3073 -101
- package/dist/hooks/pre-tool-use.js +151 -0
- package/dist/hooks/prompt-ingest-worker.js +1670 -1509
- package/dist/hooks/prompt-submit.js +2650 -1074
- package/dist/hooks/response-ingest-worker.js +154 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +157 -4
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +155 -2
- package/dist/hooks/summary-worker.js +190 -21
- package/dist/index.js +326 -102
- package/dist/lib/cloud-sync.js +31 -10
- package/dist/lib/config.js +2 -0
- package/dist/lib/consolidation.js +69 -2
- package/dist/lib/database.js +19 -0
- package/dist/lib/device-registry.js +19 -0
- package/dist/lib/embedder.js +3 -1
- package/dist/lib/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +285 -18
- package/dist/lib/hybrid-search.js +157 -4
- package/dist/lib/messaging.js +39 -2
- package/dist/lib/schedules.js +792 -637
- package/dist/lib/store.js +796 -636
- package/dist/lib/tasks.js +1485 -918
- package/dist/lib/tmux-routing.js +194 -10
- package/dist/mcp/server.js +1643 -924
- package/dist/mcp/tools/create-task.js +2283 -829
- package/dist/mcp/tools/list-tasks.js +2788 -159
- package/dist/mcp/tools/send-message.js +39 -2
- package/dist/mcp/tools/update-task.js +79 -0
- package/dist/runtime/index.js +280 -68
- package/dist/tui/App.js +1485 -645
- package/package.json +3 -2
package/dist/bin/exe-gateway.js
CHANGED
|
@@ -26,6 +26,61 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
26
26
|
};
|
|
27
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
28
|
|
|
29
|
+
// src/lib/state-bus.ts
|
|
30
|
+
var StateBus, orgBus;
|
|
31
|
+
var init_state_bus = __esm({
|
|
32
|
+
"src/lib/state-bus.ts"() {
|
|
33
|
+
"use strict";
|
|
34
|
+
StateBus = class {
|
|
35
|
+
handlers = /* @__PURE__ */ new Map();
|
|
36
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
37
|
+
/** Emit an event to all subscribers */
|
|
38
|
+
emit(event) {
|
|
39
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
40
|
+
if (typeHandlers) {
|
|
41
|
+
for (const handler of typeHandlers) {
|
|
42
|
+
try {
|
|
43
|
+
handler(event);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const handler of this.globalHandlers) {
|
|
49
|
+
try {
|
|
50
|
+
handler(event);
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Subscribe to a specific event type */
|
|
56
|
+
on(type, handler) {
|
|
57
|
+
if (!this.handlers.has(type)) {
|
|
58
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
59
|
+
}
|
|
60
|
+
this.handlers.get(type).add(handler);
|
|
61
|
+
}
|
|
62
|
+
/** Subscribe to ALL events */
|
|
63
|
+
onAny(handler) {
|
|
64
|
+
this.globalHandlers.add(handler);
|
|
65
|
+
}
|
|
66
|
+
/** Unsubscribe from a specific event type */
|
|
67
|
+
off(type, handler) {
|
|
68
|
+
this.handlers.get(type)?.delete(handler);
|
|
69
|
+
}
|
|
70
|
+
/** Unsubscribe from ALL events */
|
|
71
|
+
offAny(handler) {
|
|
72
|
+
this.globalHandlers.delete(handler);
|
|
73
|
+
}
|
|
74
|
+
/** Remove all listeners */
|
|
75
|
+
clear() {
|
|
76
|
+
this.handlers.clear();
|
|
77
|
+
this.globalHandlers.clear();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
orgBus = new StateBus();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
29
84
|
// src/gateway/crm-bridge.ts
|
|
30
85
|
var crm_bridge_exports = {};
|
|
31
86
|
__export(crm_bridge_exports, {
|
|
@@ -575,6 +630,13 @@ async function ensureSchema() {
|
|
|
575
630
|
});
|
|
576
631
|
} catch {
|
|
577
632
|
}
|
|
633
|
+
try {
|
|
634
|
+
await client.execute({
|
|
635
|
+
sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
|
|
636
|
+
args: []
|
|
637
|
+
});
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
578
640
|
try {
|
|
579
641
|
await client.execute({
|
|
580
642
|
sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
|
|
@@ -1021,6 +1083,18 @@ async function ensureSchema() {
|
|
|
1021
1083
|
CREATE INDEX IF NOT EXISTS idx_session_kills_agent
|
|
1022
1084
|
ON session_kills(agent_id);
|
|
1023
1085
|
`);
|
|
1086
|
+
await client.execute(`
|
|
1087
|
+
CREATE TABLE IF NOT EXISTS global_procedures (
|
|
1088
|
+
id TEXT PRIMARY KEY,
|
|
1089
|
+
title TEXT NOT NULL,
|
|
1090
|
+
content TEXT NOT NULL,
|
|
1091
|
+
priority TEXT NOT NULL DEFAULT 'p0',
|
|
1092
|
+
domain TEXT,
|
|
1093
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
1094
|
+
created_at TEXT NOT NULL,
|
|
1095
|
+
updated_at TEXT NOT NULL
|
|
1096
|
+
)
|
|
1097
|
+
`);
|
|
1024
1098
|
await client.executeMultiple(`
|
|
1025
1099
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
1026
1100
|
id TEXT PRIMARY KEY,
|
|
@@ -1192,6 +1266,7 @@ var config_exports = {};
|
|
|
1192
1266
|
__export(config_exports, {
|
|
1193
1267
|
CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
|
|
1194
1268
|
CONFIG_PATH: () => CONFIG_PATH,
|
|
1269
|
+
COO_AGENT_NAME: () => COO_AGENT_NAME,
|
|
1195
1270
|
CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
|
|
1196
1271
|
DB_PATH: () => DB_PATH,
|
|
1197
1272
|
EXE_AI_DIR: () => EXE_AI_DIR,
|
|
@@ -1347,7 +1422,7 @@ async function loadConfigFrom(configPath) {
|
|
|
1347
1422
|
return { ...DEFAULT_CONFIG };
|
|
1348
1423
|
}
|
|
1349
1424
|
}
|
|
1350
|
-
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
1425
|
+
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
1351
1426
|
var init_config = __esm({
|
|
1352
1427
|
"src/lib/config.ts"() {
|
|
1353
1428
|
"use strict";
|
|
@@ -1355,6 +1430,7 @@ var init_config = __esm({
|
|
|
1355
1430
|
DB_PATH = path.join(EXE_AI_DIR, "memories.db");
|
|
1356
1431
|
MODELS_DIR = path.join(EXE_AI_DIR, "models");
|
|
1357
1432
|
CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
|
|
1433
|
+
COO_AGENT_NAME = "exe";
|
|
1358
1434
|
LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
|
|
1359
1435
|
CURRENT_CONFIG_VERSION = 1;
|
|
1360
1436
|
DEFAULT_CONFIG = {
|
|
@@ -2133,6 +2209,71 @@ var init_shard_manager = __esm({
|
|
|
2133
2209
|
}
|
|
2134
2210
|
});
|
|
2135
2211
|
|
|
2212
|
+
// src/lib/global-procedures.ts
|
|
2213
|
+
var global_procedures_exports = {};
|
|
2214
|
+
__export(global_procedures_exports, {
|
|
2215
|
+
deactivateGlobalProcedure: () => deactivateGlobalProcedure,
|
|
2216
|
+
getGlobalProceduresBlock: () => getGlobalProceduresBlock,
|
|
2217
|
+
loadGlobalProcedures: () => loadGlobalProcedures,
|
|
2218
|
+
storeGlobalProcedure: () => storeGlobalProcedure
|
|
2219
|
+
});
|
|
2220
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2221
|
+
async function loadGlobalProcedures() {
|
|
2222
|
+
const client = getClient();
|
|
2223
|
+
const result = await client.execute({
|
|
2224
|
+
sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
|
|
2225
|
+
args: []
|
|
2226
|
+
});
|
|
2227
|
+
const procedures = result.rows;
|
|
2228
|
+
if (procedures.length > 0) {
|
|
2229
|
+
_cache = procedures.map((p) => `### ${p.title}
|
|
2230
|
+
${p.content}`).join("\n\n");
|
|
2231
|
+
} else {
|
|
2232
|
+
_cache = "";
|
|
2233
|
+
}
|
|
2234
|
+
_cacheLoaded = true;
|
|
2235
|
+
return procedures;
|
|
2236
|
+
}
|
|
2237
|
+
function getGlobalProceduresBlock() {
|
|
2238
|
+
if (!_cacheLoaded) return "";
|
|
2239
|
+
if (!_cache) return "";
|
|
2240
|
+
return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
|
|
2241
|
+
|
|
2242
|
+
${_cache}
|
|
2243
|
+
`;
|
|
2244
|
+
}
|
|
2245
|
+
async function storeGlobalProcedure(input) {
|
|
2246
|
+
const id = randomUUID2();
|
|
2247
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2248
|
+
const client = getClient();
|
|
2249
|
+
await client.execute({
|
|
2250
|
+
sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
|
|
2251
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
2252
|
+
args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
|
|
2253
|
+
});
|
|
2254
|
+
await loadGlobalProcedures();
|
|
2255
|
+
return id;
|
|
2256
|
+
}
|
|
2257
|
+
async function deactivateGlobalProcedure(id) {
|
|
2258
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2259
|
+
const client = getClient();
|
|
2260
|
+
const result = await client.execute({
|
|
2261
|
+
sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
|
|
2262
|
+
args: [now, id]
|
|
2263
|
+
});
|
|
2264
|
+
await loadGlobalProcedures();
|
|
2265
|
+
return result.rowsAffected > 0;
|
|
2266
|
+
}
|
|
2267
|
+
var _cache, _cacheLoaded;
|
|
2268
|
+
var init_global_procedures = __esm({
|
|
2269
|
+
"src/lib/global-procedures.ts"() {
|
|
2270
|
+
"use strict";
|
|
2271
|
+
init_database();
|
|
2272
|
+
_cache = "";
|
|
2273
|
+
_cacheLoaded = false;
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2136
2277
|
// src/lib/store.ts
|
|
2137
2278
|
var store_exports = {};
|
|
2138
2279
|
__export(store_exports, {
|
|
@@ -2212,6 +2353,11 @@ async function initStore(options) {
|
|
|
2212
2353
|
"version-query"
|
|
2213
2354
|
);
|
|
2214
2355
|
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
2356
|
+
try {
|
|
2357
|
+
const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
|
|
2358
|
+
await loadGlobalProcedures2();
|
|
2359
|
+
} catch {
|
|
2360
|
+
}
|
|
2215
2361
|
}
|
|
2216
2362
|
function classifyTier(record) {
|
|
2217
2363
|
if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
|
|
@@ -2253,6 +2399,12 @@ async function writeMemory(record) {
|
|
|
2253
2399
|
supersedes_id: record.supersedes_id ?? null
|
|
2254
2400
|
};
|
|
2255
2401
|
_pendingRecords.push(dbRow);
|
|
2402
|
+
orgBus.emit({
|
|
2403
|
+
type: "memory_stored",
|
|
2404
|
+
agentId: record.agent_id,
|
|
2405
|
+
project: record.project_name,
|
|
2406
|
+
timestamp: record.timestamp
|
|
2407
|
+
});
|
|
2256
2408
|
const MAX_PENDING = 1e3;
|
|
2257
2409
|
if (_pendingRecords.length > MAX_PENDING) {
|
|
2258
2410
|
const dropped = _pendingRecords.length - MAX_PENDING;
|
|
@@ -2598,6 +2750,7 @@ var init_store = __esm({
|
|
|
2598
2750
|
init_database();
|
|
2599
2751
|
init_keychain();
|
|
2600
2752
|
init_config();
|
|
2753
|
+
init_state_bus();
|
|
2601
2754
|
INIT_MAX_RETRIES = 3;
|
|
2602
2755
|
INIT_RETRY_DELAY_MS = 1e3;
|
|
2603
2756
|
_pendingRecords = [];
|
|
@@ -2769,7 +2922,7 @@ __export(license_exports, {
|
|
|
2769
2922
|
validateLicense: () => validateLicense
|
|
2770
2923
|
});
|
|
2771
2924
|
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
2772
|
-
import { randomUUID as
|
|
2925
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2773
2926
|
import path5 from "path";
|
|
2774
2927
|
import { jwtVerify, importSPKI } from "jose";
|
|
2775
2928
|
async function fetchRetry(url, init) {
|
|
@@ -2796,7 +2949,7 @@ function loadDeviceId() {
|
|
|
2796
2949
|
}
|
|
2797
2950
|
} catch {
|
|
2798
2951
|
}
|
|
2799
|
-
const id =
|
|
2952
|
+
const id = randomUUID3();
|
|
2800
2953
|
mkdirSync2(EXE_AI_DIR, { recursive: true });
|
|
2801
2954
|
writeFileSync(DEVICE_ID_PATH, id, "utf8");
|
|
2802
2955
|
return id;
|
|
@@ -3097,7 +3250,7 @@ var whatsapp_exports = {};
|
|
|
3097
3250
|
__export(whatsapp_exports, {
|
|
3098
3251
|
WhatsAppAdapter: () => WhatsAppAdapter
|
|
3099
3252
|
});
|
|
3100
|
-
import { randomUUID as
|
|
3253
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3101
3254
|
import { homedir } from "os";
|
|
3102
3255
|
import { join } from "path";
|
|
3103
3256
|
import { mkdirSync as mkdirSync3 } from "fs";
|
|
@@ -3283,7 +3436,7 @@ var init_whatsapp = __esm({
|
|
|
3283
3436
|
const location = this.extractLocation(msg.message);
|
|
3284
3437
|
const dataCategory = location ? "location" : "message";
|
|
3285
3438
|
return {
|
|
3286
|
-
messageId: msg.key.id ??
|
|
3439
|
+
messageId: msg.key.id ?? randomUUID4(),
|
|
3287
3440
|
platform: "whatsapp",
|
|
3288
3441
|
senderId,
|
|
3289
3442
|
senderName: msg.pushName ?? void 0,
|
|
@@ -3328,7 +3481,7 @@ var init_whatsapp = __esm({
|
|
|
3328
3481
|
}
|
|
3329
3482
|
const timestamp = receipt.readTimestamp ?? receipt.receiptTimestamp ?? Date.now() / 1e3;
|
|
3330
3483
|
return {
|
|
3331
|
-
messageId:
|
|
3484
|
+
messageId: randomUUID4(),
|
|
3332
3485
|
platform: "whatsapp",
|
|
3333
3486
|
senderId: remoteJid.replace("@s.whatsapp.net", "").replace("@g.us", ""),
|
|
3334
3487
|
channelId: remoteJid,
|
|
@@ -3351,7 +3504,7 @@ var init_whatsapp = __esm({
|
|
|
3351
3504
|
const phone = id.replace("@s.whatsapp.net", "").replace("@g.us", "");
|
|
3352
3505
|
const name = contact.name ?? contact.notify ?? phone;
|
|
3353
3506
|
return {
|
|
3354
|
-
messageId:
|
|
3507
|
+
messageId: randomUUID4(),
|
|
3355
3508
|
platform: "whatsapp",
|
|
3356
3509
|
senderId: phone,
|
|
3357
3510
|
senderName: name,
|
|
@@ -3375,7 +3528,7 @@ var init_whatsapp = __esm({
|
|
|
3375
3528
|
const participants = (group.participants ?? []).map((p) => p.id ?? p);
|
|
3376
3529
|
const admins = (group.participants ?? []).filter((p) => p.admin === "admin" || p.admin === "superadmin").map((p) => p.id ?? p);
|
|
3377
3530
|
return {
|
|
3378
|
-
messageId:
|
|
3531
|
+
messageId: randomUUID4(),
|
|
3379
3532
|
platform: "whatsapp",
|
|
3380
3533
|
senderId: groupId,
|
|
3381
3534
|
channelId: groupId,
|
|
@@ -3400,7 +3553,7 @@ var init_whatsapp = __esm({
|
|
|
3400
3553
|
if (!reactionData) return null;
|
|
3401
3554
|
const remoteJid = key.remoteJid ?? "";
|
|
3402
3555
|
return {
|
|
3403
|
-
messageId:
|
|
3556
|
+
messageId: randomUUID4(),
|
|
3404
3557
|
platform: "whatsapp",
|
|
3405
3558
|
senderId: reactionData.key?.participant ?? reactionData.key?.remoteJid?.replace("@s.whatsapp.net", "") ?? "",
|
|
3406
3559
|
channelId: remoteJid,
|
|
@@ -3422,7 +3575,7 @@ var init_whatsapp = __esm({
|
|
|
3422
3575
|
if (!chatId) return null;
|
|
3423
3576
|
const caller = call.from?.replace("@s.whatsapp.net", "") ?? "";
|
|
3424
3577
|
return {
|
|
3425
|
-
messageId:
|
|
3578
|
+
messageId: randomUUID4(),
|
|
3426
3579
|
platform: "whatsapp",
|
|
3427
3580
|
senderId: caller,
|
|
3428
3581
|
channelId: chatId,
|
|
@@ -3768,7 +3921,7 @@ var slack_exports = {};
|
|
|
3768
3921
|
__export(slack_exports, {
|
|
3769
3922
|
SlackAdapter: () => SlackAdapter
|
|
3770
3923
|
});
|
|
3771
|
-
import { randomUUID as
|
|
3924
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3772
3925
|
var SlackAdapter;
|
|
3773
3926
|
var init_slack = __esm({
|
|
3774
3927
|
"src/gateway/adapters/slack.ts"() {
|
|
@@ -3810,7 +3963,7 @@ var init_slack = __esm({
|
|
|
3810
3963
|
if (event.subtype) return;
|
|
3811
3964
|
const isGroup = event.channel_type !== "im";
|
|
3812
3965
|
const normalized = {
|
|
3813
|
-
messageId: event.client_msg_id ?? event.ts ??
|
|
3966
|
+
messageId: event.client_msg_id ?? event.ts ?? randomUUID5(),
|
|
3814
3967
|
platform: "slack",
|
|
3815
3968
|
senderId: event.user ?? "",
|
|
3816
3969
|
channelId: event.channel ?? "",
|
|
@@ -3872,7 +4025,7 @@ var init_slack = __esm({
|
|
|
3872
4025
|
if (!event.text) return;
|
|
3873
4026
|
const isGroup = event.channel_type !== "im";
|
|
3874
4027
|
const normalized = {
|
|
3875
|
-
messageId: event.ts ??
|
|
4028
|
+
messageId: event.ts ?? randomUUID5(),
|
|
3876
4029
|
platform: "slack",
|
|
3877
4030
|
senderId: event.user ?? "",
|
|
3878
4031
|
senderName: event.user_profile?.display_name ?? event.user_profile?.real_name ?? void 0,
|
|
@@ -4095,7 +4248,7 @@ var email_exports = {};
|
|
|
4095
4248
|
__export(email_exports, {
|
|
4096
4249
|
EmailAdapter: () => EmailAdapter
|
|
4097
4250
|
});
|
|
4098
|
-
import { randomUUID as
|
|
4251
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
4099
4252
|
import { createTransport } from "nodemailer";
|
|
4100
4253
|
function extractEmailAddress(from) {
|
|
4101
4254
|
const match = from.match(/<([^>]+)>/);
|
|
@@ -4177,7 +4330,7 @@ var init_email = __esm({
|
|
|
4177
4330
|
const senderEmail = extractEmailAddress(from);
|
|
4178
4331
|
const media = this.extractMedia(payload);
|
|
4179
4332
|
const normalized = {
|
|
4180
|
-
messageId: payload.message_id ??
|
|
4333
|
+
messageId: payload.message_id ?? randomUUID6(),
|
|
4181
4334
|
platform: "email",
|
|
4182
4335
|
senderId: senderEmail,
|
|
4183
4336
|
senderName: extractSenderName(from),
|
|
@@ -4238,7 +4391,7 @@ var webhook_exports = {};
|
|
|
4238
4391
|
__export(webhook_exports, {
|
|
4239
4392
|
WebhookAdapter: () => WebhookAdapter
|
|
4240
4393
|
});
|
|
4241
|
-
import { randomUUID as
|
|
4394
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
4242
4395
|
function resolvePath(obj, path20) {
|
|
4243
4396
|
let current = obj;
|
|
4244
4397
|
for (const segment of path20.split(".")) {
|
|
@@ -4294,7 +4447,7 @@ var init_webhook = __esm({
|
|
|
4294
4447
|
if (!text || !senderId) return;
|
|
4295
4448
|
const channelId = this.fieldMap.channelId ? String(resolvePath(rawPayload, this.fieldMap.channelId) ?? senderId) : senderId;
|
|
4296
4449
|
const senderName = this.fieldMap.senderName ? String(resolvePath(rawPayload, this.fieldMap.senderName) ?? "") || void 0 : void 0;
|
|
4297
|
-
const messageId = this.fieldMap.messageId ? String(resolvePath(rawPayload, this.fieldMap.messageId) ??
|
|
4450
|
+
const messageId = this.fieldMap.messageId ? String(resolvePath(rawPayload, this.fieldMap.messageId) ?? randomUUID7()) : randomUUID7();
|
|
4298
4451
|
const timestamp = this.fieldMap.timestamp ? String(resolvePath(rawPayload, this.fieldMap.timestamp) ?? (/* @__PURE__ */ new Date()).toISOString()) : (/* @__PURE__ */ new Date()).toISOString();
|
|
4299
4452
|
const normalized = {
|
|
4300
4453
|
messageId,
|
|
@@ -4797,2115 +4950,2607 @@ var init_plan_limits = __esm({
|
|
|
4797
4950
|
}
|
|
4798
4951
|
});
|
|
4799
4952
|
|
|
4800
|
-
// src/lib/
|
|
4801
|
-
import
|
|
4802
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, existsSync as existsSync10, appendFileSync } from "fs";
|
|
4953
|
+
// src/lib/notifications.ts
|
|
4954
|
+
import crypto3 from "crypto";
|
|
4803
4955
|
import path11 from "path";
|
|
4804
4956
|
import os6 from "os";
|
|
4805
|
-
import {
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4957
|
+
import {
|
|
4958
|
+
readFileSync as readFileSync9,
|
|
4959
|
+
readdirSync as readdirSync2,
|
|
4960
|
+
unlinkSync as unlinkSync2,
|
|
4961
|
+
existsSync as existsSync10,
|
|
4962
|
+
rmdirSync
|
|
4963
|
+
} from "fs";
|
|
4964
|
+
async function writeNotification(notification) {
|
|
4811
4965
|
try {
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4966
|
+
const client = getClient();
|
|
4967
|
+
const id = crypto3.randomUUID();
|
|
4968
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4969
|
+
await client.execute({
|
|
4970
|
+
sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
|
|
4971
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
|
4972
|
+
args: [
|
|
4973
|
+
id,
|
|
4974
|
+
notification.agentId,
|
|
4975
|
+
notification.agentRole,
|
|
4976
|
+
notification.event,
|
|
4977
|
+
notification.project,
|
|
4978
|
+
notification.summary,
|
|
4979
|
+
notification.taskFile ?? null,
|
|
4980
|
+
now
|
|
4981
|
+
]
|
|
4982
|
+
});
|
|
4983
|
+
} catch (err) {
|
|
4984
|
+
process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
|
|
4985
|
+
`);
|
|
4832
4986
|
}
|
|
4833
|
-
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
4834
|
-
return true;
|
|
4835
4987
|
}
|
|
4836
|
-
function
|
|
4988
|
+
async function markAsReadByTaskFile(taskFile) {
|
|
4837
4989
|
try {
|
|
4838
|
-
|
|
4990
|
+
const client = getClient();
|
|
4991
|
+
await client.execute({
|
|
4992
|
+
sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
|
|
4993
|
+
args: [taskFile]
|
|
4994
|
+
});
|
|
4839
4995
|
} catch {
|
|
4840
4996
|
}
|
|
4841
4997
|
}
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
path11.dirname(thisFile),
|
|
4847
|
-
"..",
|
|
4848
|
-
"bin",
|
|
4849
|
-
"exe-export-behaviors.js"
|
|
4850
|
-
);
|
|
4851
|
-
return existsSync10(scriptPath) ? scriptPath : null;
|
|
4852
|
-
} catch {
|
|
4853
|
-
return null;
|
|
4998
|
+
var init_notifications = __esm({
|
|
4999
|
+
"src/lib/notifications.ts"() {
|
|
5000
|
+
"use strict";
|
|
5001
|
+
init_database();
|
|
4854
5002
|
}
|
|
4855
|
-
}
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
5003
|
+
});
|
|
5004
|
+
|
|
5005
|
+
// src/lib/session-kill-telemetry.ts
|
|
5006
|
+
import crypto4 from "crypto";
|
|
5007
|
+
async function recordSessionKill(input) {
|
|
4859
5008
|
try {
|
|
4860
|
-
const
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
5009
|
+
const client = getClient();
|
|
5010
|
+
await client.execute({
|
|
5011
|
+
sql: `INSERT INTO session_kills
|
|
5012
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
5013
|
+
ticks_idle, estimated_tokens_saved)
|
|
5014
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
5015
|
+
args: [
|
|
5016
|
+
crypto4.randomUUID(),
|
|
5017
|
+
input.sessionName,
|
|
5018
|
+
input.agentId,
|
|
5019
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
5020
|
+
input.reason,
|
|
5021
|
+
input.ticksIdle ?? null,
|
|
5022
|
+
input.estimatedTokensSaved ?? null
|
|
5023
|
+
]
|
|
5024
|
+
});
|
|
4866
5025
|
} catch (err) {
|
|
4867
5026
|
process.stderr.write(
|
|
4868
|
-
`[
|
|
5027
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
4869
5028
|
`
|
|
4870
5029
|
);
|
|
4871
|
-
return null;
|
|
4872
5030
|
}
|
|
4873
5031
|
}
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
4879
|
-
return `${employee}${suffix}-${exeSession}`;
|
|
4880
|
-
}
|
|
4881
|
-
function extractRootExe(name) {
|
|
4882
|
-
const match = name.match(/(exe\d+)$/);
|
|
4883
|
-
return match?.[1] ?? null;
|
|
4884
|
-
}
|
|
4885
|
-
function getParentExe(sessionKey) {
|
|
4886
|
-
try {
|
|
4887
|
-
const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
4888
|
-
return data.parentExe || null;
|
|
4889
|
-
} catch {
|
|
4890
|
-
return null;
|
|
5032
|
+
var init_session_kill_telemetry = __esm({
|
|
5033
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
5034
|
+
"use strict";
|
|
5035
|
+
init_database();
|
|
4891
5036
|
}
|
|
4892
|
-
}
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5037
|
+
});
|
|
5038
|
+
|
|
5039
|
+
// src/lib/tasks-crud.ts
|
|
5040
|
+
import crypto5 from "crypto";
|
|
5041
|
+
import path12 from "path";
|
|
5042
|
+
import { execSync as execSync4 } from "child_process";
|
|
5043
|
+
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
5044
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
5045
|
+
async function writeCheckpoint(input) {
|
|
5046
|
+
const client = getClient();
|
|
5047
|
+
const row = await resolveTask(client, input.taskId);
|
|
5048
|
+
const taskId = String(row.id);
|
|
5049
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5050
|
+
const blockedByIds = [];
|
|
5051
|
+
if (row.blocked_by) {
|
|
5052
|
+
blockedByIds.push(String(row.blocked_by));
|
|
4902
5053
|
}
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
}
|
|
5054
|
+
const checkpoint = {
|
|
5055
|
+
step: input.step,
|
|
5056
|
+
context_summary: input.contextSummary,
|
|
5057
|
+
files_touched: input.filesTouched ?? [],
|
|
5058
|
+
blocked_by_ids: blockedByIds,
|
|
5059
|
+
last_checkpoint_at: now
|
|
5060
|
+
};
|
|
5061
|
+
const result = await client.execute({
|
|
5062
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
5063
|
+
args: [JSON.stringify(checkpoint), now, taskId]
|
|
5064
|
+
});
|
|
5065
|
+
if (result.rowsAffected === 0) {
|
|
5066
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
4914
5067
|
}
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
5068
|
+
const countResult = await client.execute({
|
|
5069
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
5070
|
+
args: [taskId]
|
|
5071
|
+
});
|
|
5072
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
5073
|
+
return { checkpointCount };
|
|
4919
5074
|
}
|
|
4920
|
-
function
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
}
|
|
4927
|
-
return null;
|
|
5075
|
+
function extractParentFromContext(contextBody) {
|
|
5076
|
+
if (!contextBody) return null;
|
|
5077
|
+
const match = contextBody.match(
|
|
5078
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
5079
|
+
);
|
|
5080
|
+
return match ? match[1].toLowerCase() : null;
|
|
4928
5081
|
}
|
|
4929
|
-
function
|
|
4930
|
-
|
|
4931
|
-
if (!existsSync10(DEBOUNCE_FILE)) return {};
|
|
4932
|
-
return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
|
|
4933
|
-
} catch {
|
|
4934
|
-
return {};
|
|
4935
|
-
}
|
|
5082
|
+
function slugify(title) {
|
|
5083
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4936
5084
|
}
|
|
4937
|
-
function
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
}
|
|
5085
|
+
async function resolveTask(client, identifier) {
|
|
5086
|
+
let result = await client.execute({
|
|
5087
|
+
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
5088
|
+
args: [identifier]
|
|
5089
|
+
});
|
|
5090
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
5091
|
+
result = await client.execute({
|
|
5092
|
+
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
5093
|
+
args: [`%${identifier}%`]
|
|
5094
|
+
});
|
|
5095
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
5096
|
+
if (result.rows.length > 1) {
|
|
5097
|
+
const exact = result.rows.filter(
|
|
5098
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
5099
|
+
);
|
|
5100
|
+
if (exact.length === 1) return exact[0];
|
|
5101
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
5102
|
+
const active = candidates.filter(
|
|
5103
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
5104
|
+
);
|
|
5105
|
+
if (active.length === 1) return active[0];
|
|
5106
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
5107
|
+
throw new Error(
|
|
5108
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
5109
|
+
);
|
|
4942
5110
|
}
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
5111
|
+
result = await client.execute({
|
|
5112
|
+
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
5113
|
+
args: [`%${identifier}%`]
|
|
5114
|
+
});
|
|
5115
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
5116
|
+
if (result.rows.length > 1) {
|
|
5117
|
+
const active = result.rows.filter(
|
|
5118
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
5119
|
+
);
|
|
5120
|
+
if (active.length === 1) return active[0];
|
|
5121
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
5122
|
+
throw new Error(
|
|
5123
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
5124
|
+
);
|
|
4955
5125
|
}
|
|
4956
|
-
|
|
5126
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
4957
5127
|
}
|
|
4958
|
-
function
|
|
4959
|
-
const
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
5128
|
+
async function createTaskCore(input) {
|
|
5129
|
+
const client = getClient();
|
|
5130
|
+
const id = crypto5.randomUUID();
|
|
5131
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5132
|
+
const slug = slugify(input.title);
|
|
5133
|
+
const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
|
|
5134
|
+
let blockedById = null;
|
|
5135
|
+
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
5136
|
+
if (input.blockedBy) {
|
|
5137
|
+
const blocker = await resolveTask(client, input.blockedBy);
|
|
5138
|
+
blockedById = String(blocker.id);
|
|
4966
5139
|
}
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
5140
|
+
let parentTaskId = null;
|
|
5141
|
+
let parentRef = input.parentTaskId;
|
|
5142
|
+
if (!parentRef) {
|
|
5143
|
+
const extracted = extractParentFromContext(input.context);
|
|
5144
|
+
if (extracted) {
|
|
5145
|
+
parentRef = extracted;
|
|
5146
|
+
process.stderr.write(
|
|
5147
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
5148
|
+
);
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
if (parentRef) {
|
|
5152
|
+
try {
|
|
5153
|
+
const parent = await resolveTask(client, parentRef);
|
|
5154
|
+
parentTaskId = String(parent.id);
|
|
5155
|
+
} catch (err) {
|
|
5156
|
+
if (!input.parentTaskId) {
|
|
5157
|
+
throw new Error(
|
|
5158
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
5159
|
+
);
|
|
4976
5160
|
}
|
|
5161
|
+
throw err;
|
|
4977
5162
|
}
|
|
4978
|
-
if (/Running…/.test(pane)) return "tool";
|
|
4979
|
-
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
4980
|
-
return "idle";
|
|
4981
|
-
} catch {
|
|
4982
|
-
return "offline";
|
|
4983
5163
|
}
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
4992
|
-
return "skipped_exe";
|
|
5164
|
+
let warning;
|
|
5165
|
+
const dupCheck = await client.execute({
|
|
5166
|
+
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
5167
|
+
args: [input.title, input.assignedTo]
|
|
5168
|
+
});
|
|
5169
|
+
if (dupCheck.rows.length > 0) {
|
|
5170
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
4993
5171
|
}
|
|
4994
|
-
if (
|
|
4995
|
-
|
|
4996
|
-
|
|
5172
|
+
if (input.baseDir) {
|
|
5173
|
+
try {
|
|
5174
|
+
await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
5175
|
+
await mkdir4(path12.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
5176
|
+
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
5177
|
+
await ensureGitignoreExe(input.baseDir);
|
|
5178
|
+
} catch {
|
|
5179
|
+
}
|
|
4997
5180
|
}
|
|
5181
|
+
const complexity = input.complexity ?? "standard";
|
|
5182
|
+
let sessionScope = null;
|
|
4998
5183
|
try {
|
|
4999
|
-
const
|
|
5000
|
-
|
|
5001
|
-
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
5002
|
-
return "failed";
|
|
5003
|
-
}
|
|
5004
|
-
const sessionState = getSessionState(targetSession);
|
|
5005
|
-
if (sessionState === "no_claude") {
|
|
5006
|
-
queueIntercom(targetSession, "claude not running in session");
|
|
5007
|
-
recordDebounce(targetSession);
|
|
5008
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
5009
|
-
return "queued";
|
|
5010
|
-
}
|
|
5011
|
-
if (sessionState === "thinking" || sessionState === "tool") {
|
|
5012
|
-
queueIntercom(targetSession, "session busy at send time");
|
|
5013
|
-
recordDebounce(targetSession);
|
|
5014
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
5015
|
-
return "queued";
|
|
5016
|
-
}
|
|
5017
|
-
if (transport.isPaneInCopyMode(targetSession)) {
|
|
5018
|
-
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
5019
|
-
transport.sendKeys(targetSession, "q");
|
|
5020
|
-
}
|
|
5021
|
-
transport.sendKeys(targetSession, "/exe-intercom");
|
|
5022
|
-
recordDebounce(targetSession);
|
|
5023
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
5024
|
-
return "delivered";
|
|
5184
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
5185
|
+
sessionScope = resolveExeSession2();
|
|
5025
5186
|
} catch {
|
|
5026
|
-
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
5027
|
-
return "failed";
|
|
5028
5187
|
}
|
|
5188
|
+
await client.execute({
|
|
5189
|
+
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)
|
|
5190
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5191
|
+
args: [
|
|
5192
|
+
id,
|
|
5193
|
+
input.title,
|
|
5194
|
+
input.assignedTo,
|
|
5195
|
+
input.assignedBy,
|
|
5196
|
+
input.projectName,
|
|
5197
|
+
input.priority,
|
|
5198
|
+
initialStatus,
|
|
5199
|
+
taskFile,
|
|
5200
|
+
blockedById,
|
|
5201
|
+
parentTaskId,
|
|
5202
|
+
input.reviewer ?? null,
|
|
5203
|
+
input.context,
|
|
5204
|
+
complexity,
|
|
5205
|
+
input.budgetTokens ?? null,
|
|
5206
|
+
input.budgetFallbackModel ?? null,
|
|
5207
|
+
0,
|
|
5208
|
+
null,
|
|
5209
|
+
sessionScope,
|
|
5210
|
+
now,
|
|
5211
|
+
now
|
|
5212
|
+
]
|
|
5213
|
+
});
|
|
5214
|
+
return {
|
|
5215
|
+
id,
|
|
5216
|
+
title: input.title,
|
|
5217
|
+
assignedTo: input.assignedTo,
|
|
5218
|
+
assignedBy: input.assignedBy,
|
|
5219
|
+
projectName: input.projectName,
|
|
5220
|
+
priority: input.priority,
|
|
5221
|
+
status: initialStatus,
|
|
5222
|
+
taskFile,
|
|
5223
|
+
createdAt: now,
|
|
5224
|
+
updatedAt: now,
|
|
5225
|
+
warning,
|
|
5226
|
+
budgetTokens: input.budgetTokens ?? null,
|
|
5227
|
+
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
5228
|
+
tokensUsed: 0,
|
|
5229
|
+
tokensWarnedAt: null
|
|
5230
|
+
};
|
|
5029
5231
|
}
|
|
5030
|
-
function
|
|
5031
|
-
const
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5232
|
+
async function listTasks(input) {
|
|
5233
|
+
const client = getClient();
|
|
5234
|
+
const conditions = [];
|
|
5235
|
+
const args = [];
|
|
5236
|
+
if (input.assignedTo) {
|
|
5237
|
+
conditions.push("assigned_to = ?");
|
|
5238
|
+
args.push(input.assignedTo);
|
|
5036
5239
|
}
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
if (rootExe && rootExe !== target) {
|
|
5043
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
5044
|
-
`);
|
|
5045
|
-
const fallback = sendIntercom(rootExe);
|
|
5046
|
-
return fallback !== "failed";
|
|
5047
|
-
}
|
|
5048
|
-
return false;
|
|
5240
|
+
if (input.status) {
|
|
5241
|
+
conditions.push("status = ?");
|
|
5242
|
+
args.push(input.status);
|
|
5243
|
+
} else {
|
|
5244
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
5049
5245
|
}
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5246
|
+
if (input.projectName) {
|
|
5247
|
+
conditions.push("project_name = ?");
|
|
5248
|
+
args.push(input.projectName);
|
|
5249
|
+
}
|
|
5250
|
+
if (input.priority) {
|
|
5251
|
+
conditions.push("priority = ?");
|
|
5252
|
+
args.push(input.priority);
|
|
5055
5253
|
}
|
|
5056
5254
|
try {
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
if (
|
|
5060
|
-
|
|
5255
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
5256
|
+
const session = resolveExeSession2();
|
|
5257
|
+
if (session) {
|
|
5258
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
5259
|
+
args.push(session);
|
|
5061
5260
|
}
|
|
5261
|
+
} catch {
|
|
5062
5262
|
}
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5263
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
5264
|
+
const result = await client.execute({
|
|
5265
|
+
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`,
|
|
5266
|
+
args
|
|
5267
|
+
});
|
|
5268
|
+
return result.rows.map((r) => ({
|
|
5269
|
+
id: String(r.id),
|
|
5270
|
+
title: String(r.title),
|
|
5271
|
+
assignedTo: String(r.assigned_to),
|
|
5272
|
+
assignedBy: String(r.assigned_by),
|
|
5273
|
+
projectName: String(r.project_name),
|
|
5274
|
+
priority: String(r.priority),
|
|
5275
|
+
status: String(r.status),
|
|
5276
|
+
taskFile: String(r.task_file),
|
|
5277
|
+
createdAt: String(r.created_at),
|
|
5278
|
+
updatedAt: String(r.updated_at),
|
|
5279
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
5280
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
5281
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
5282
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
5283
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
5284
|
+
}));
|
|
5285
|
+
}
|
|
5286
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
5287
|
+
if (!taskContext) return null;
|
|
5288
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
5289
|
+
try {
|
|
5290
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
5291
|
+
const branch = execSync4(
|
|
5292
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
5293
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
5294
|
+
).trim();
|
|
5295
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
5296
|
+
const commitCount = execSync4(
|
|
5297
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
5298
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
5299
|
+
).trim();
|
|
5300
|
+
const count = parseInt(commitCount, 10);
|
|
5301
|
+
if (count === 0) {
|
|
5302
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
5303
|
+
}
|
|
5304
|
+
return null;
|
|
5305
|
+
} catch {
|
|
5306
|
+
return null;
|
|
5070
5307
|
}
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5308
|
+
}
|
|
5309
|
+
async function updateTaskStatus(input) {
|
|
5310
|
+
const client = getClient();
|
|
5311
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5312
|
+
const row = await resolveTask(client, input.taskId);
|
|
5313
|
+
const taskId = String(row.id);
|
|
5314
|
+
const taskFile = String(row.task_file);
|
|
5315
|
+
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
5316
|
+
process.stderr.write(
|
|
5317
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
5318
|
+
`
|
|
5077
5319
|
);
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5320
|
+
}
|
|
5321
|
+
if (input.status === "done") {
|
|
5322
|
+
const existingRow = await client.execute({
|
|
5323
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
5324
|
+
args: [taskId]
|
|
5325
|
+
});
|
|
5326
|
+
if (existingRow.rows.length > 0) {
|
|
5327
|
+
const ctx = existingRow.rows[0];
|
|
5328
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
5329
|
+
if (warning) {
|
|
5330
|
+
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
5331
|
+
|
|
5332
|
+
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
5333
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
5334
|
+
`);
|
|
5335
|
+
}
|
|
5084
5336
|
}
|
|
5085
|
-
effectiveInstance = free === 0 ? void 0 : free;
|
|
5086
5337
|
}
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
const
|
|
5090
|
-
|
|
5091
|
-
|
|
5338
|
+
if (input.status === "in_progress") {
|
|
5339
|
+
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
5340
|
+
const claim = await client.execute({
|
|
5341
|
+
sql: `UPDATE tasks
|
|
5342
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
5343
|
+
WHERE id = ? AND status = 'open'`,
|
|
5344
|
+
args: [tmuxSession, now, taskId]
|
|
5345
|
+
});
|
|
5346
|
+
if (claim.rowsAffected === 0) {
|
|
5347
|
+
const current = await client.execute({
|
|
5348
|
+
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
5349
|
+
args: [taskId]
|
|
5350
|
+
});
|
|
5351
|
+
const cur = current.rows[0];
|
|
5352
|
+
const status = cur?.status ?? "unknown";
|
|
5353
|
+
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
5354
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
5092
5355
|
}
|
|
5093
|
-
|
|
5094
|
-
|
|
5356
|
+
try {
|
|
5357
|
+
await writeCheckpoint({
|
|
5358
|
+
taskId,
|
|
5359
|
+
step: "claimed",
|
|
5360
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
5361
|
+
});
|
|
5362
|
+
} catch {
|
|
5095
5363
|
}
|
|
5096
|
-
return {
|
|
5097
|
-
}
|
|
5098
|
-
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
5099
|
-
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
5100
|
-
if (result.error) {
|
|
5101
|
-
return { status: "failed", sessionName, error: result.error };
|
|
5364
|
+
return { row, taskFile, now, taskId };
|
|
5102
5365
|
}
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5366
|
+
if (input.result) {
|
|
5367
|
+
await client.execute({
|
|
5368
|
+
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
5369
|
+
args: [input.status, input.result, now, taskId]
|
|
5370
|
+
});
|
|
5371
|
+
} else {
|
|
5372
|
+
await client.execute({
|
|
5373
|
+
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
5374
|
+
args: [input.status, now, taskId]
|
|
5375
|
+
});
|
|
5113
5376
|
}
|
|
5114
|
-
transport.kill(sessionName);
|
|
5115
|
-
let cleanupSuffix = "";
|
|
5116
5377
|
try {
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
}
|
|
5378
|
+
await writeCheckpoint({
|
|
5379
|
+
taskId,
|
|
5380
|
+
step: `status_transition:${input.status}`,
|
|
5381
|
+
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
5382
|
+
});
|
|
5122
5383
|
} catch {
|
|
5123
5384
|
}
|
|
5385
|
+
return { row, taskFile, now, taskId };
|
|
5386
|
+
}
|
|
5387
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
5388
|
+
const client = getClient();
|
|
5389
|
+
const row = await resolveTask(client, taskId);
|
|
5390
|
+
const id = String(row.id);
|
|
5391
|
+
const taskFile = String(row.task_file);
|
|
5392
|
+
const assignedTo = String(row.assigned_to);
|
|
5393
|
+
const assignedBy = String(row.assigned_by);
|
|
5394
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
5395
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
5396
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
5397
|
+
}
|
|
5398
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
5399
|
+
const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
5124
5400
|
try {
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5401
|
+
if (existsSync11(archPath)) return;
|
|
5402
|
+
const template = [
|
|
5403
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
5404
|
+
"",
|
|
5405
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
5406
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
5407
|
+
"",
|
|
5408
|
+
"## Overview",
|
|
5409
|
+
"",
|
|
5410
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
5411
|
+
"",
|
|
5412
|
+
"## Key Components",
|
|
5413
|
+
"",
|
|
5414
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
5415
|
+
"",
|
|
5416
|
+
"## Data Flow",
|
|
5417
|
+
"",
|
|
5418
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
5419
|
+
"",
|
|
5420
|
+
"## Invariants",
|
|
5421
|
+
"",
|
|
5422
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
5423
|
+
"",
|
|
5424
|
+
"## Dependencies",
|
|
5425
|
+
"",
|
|
5426
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
5427
|
+
""
|
|
5428
|
+
].join("\n");
|
|
5429
|
+
await writeFile4(archPath, template, "utf-8");
|
|
5137
5430
|
} catch {
|
|
5138
5431
|
}
|
|
5432
|
+
}
|
|
5433
|
+
async function ensureGitignoreExe(baseDir) {
|
|
5434
|
+
const gitignorePath = path12.join(baseDir, ".gitignore");
|
|
5139
5435
|
try {
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
|
|
5147
|
-
} catch {
|
|
5148
|
-
}
|
|
5149
|
-
const perms = settings.permissions ?? {};
|
|
5150
|
-
const allow = perms.allow ?? [];
|
|
5151
|
-
const toolNames = [
|
|
5152
|
-
"recall_my_memory",
|
|
5153
|
-
"store_memory",
|
|
5154
|
-
"create_task",
|
|
5155
|
-
"update_task",
|
|
5156
|
-
"list_tasks",
|
|
5157
|
-
"get_task",
|
|
5158
|
-
"ask_team_memory",
|
|
5159
|
-
"store_behavior",
|
|
5160
|
-
"get_identity",
|
|
5161
|
-
"send_message"
|
|
5162
|
-
];
|
|
5163
|
-
const requiredTools = expandDualPrefixTools(toolNames);
|
|
5164
|
-
let changed = false;
|
|
5165
|
-
for (const tool of requiredTools) {
|
|
5166
|
-
if (!allow.includes(tool)) {
|
|
5167
|
-
allow.push(tool);
|
|
5168
|
-
changed = true;
|
|
5169
|
-
}
|
|
5170
|
-
}
|
|
5171
|
-
if (changed) {
|
|
5172
|
-
perms.allow = allow;
|
|
5173
|
-
settings.permissions = perms;
|
|
5174
|
-
mkdirSync6(projSettingsDir, { recursive: true });
|
|
5175
|
-
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
5436
|
+
if (existsSync11(gitignorePath)) {
|
|
5437
|
+
const content = readFileSync10(gitignorePath, "utf-8");
|
|
5438
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
5439
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
5440
|
+
} else {
|
|
5441
|
+
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
5176
5442
|
}
|
|
5177
5443
|
} catch {
|
|
5178
5444
|
}
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
const identityPath = path11.join(
|
|
5188
|
-
os6.homedir(),
|
|
5189
|
-
".exe-os",
|
|
5190
|
-
"identity",
|
|
5191
|
-
`${employeeName}.md`
|
|
5192
|
-
);
|
|
5193
|
-
_resetCcAgentSupportCache();
|
|
5194
|
-
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
5195
|
-
if (hasAgentFlag) {
|
|
5196
|
-
identityFlag = ` --agent ${employeeName}`;
|
|
5197
|
-
} else if (existsSync10(identityPath)) {
|
|
5198
|
-
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
5199
|
-
legacyFallbackWarned = true;
|
|
5200
|
-
}
|
|
5201
|
-
const behaviorsFile = exportBehaviorsSync(
|
|
5202
|
-
employeeName,
|
|
5203
|
-
path11.basename(spawnCwd),
|
|
5204
|
-
sessionName
|
|
5205
|
-
);
|
|
5206
|
-
if (behaviorsFile) {
|
|
5207
|
-
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
5208
|
-
}
|
|
5445
|
+
}
|
|
5446
|
+
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
5447
|
+
var init_tasks_crud = __esm({
|
|
5448
|
+
"src/lib/tasks-crud.ts"() {
|
|
5449
|
+
"use strict";
|
|
5450
|
+
init_database();
|
|
5451
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
5452
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
5209
5453
|
}
|
|
5210
|
-
|
|
5454
|
+
});
|
|
5455
|
+
|
|
5456
|
+
// src/lib/tasks-review.ts
|
|
5457
|
+
import path13 from "path";
|
|
5458
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
5459
|
+
async function countPendingReviews() {
|
|
5460
|
+
const client = getClient();
|
|
5461
|
+
const result = await client.execute({
|
|
5462
|
+
sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
|
|
5463
|
+
args: []
|
|
5464
|
+
});
|
|
5465
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
5466
|
+
}
|
|
5467
|
+
async function countNewPendingReviewsSince(sinceIso) {
|
|
5468
|
+
const client = getClient();
|
|
5469
|
+
const result = await client.execute({
|
|
5470
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
5471
|
+
WHERE status = 'needs_review' AND updated_at > ?`,
|
|
5472
|
+
args: [sinceIso]
|
|
5473
|
+
});
|
|
5474
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
5475
|
+
}
|
|
5476
|
+
async function listPendingReviews(limit) {
|
|
5477
|
+
const client = getClient();
|
|
5478
|
+
const result = await client.execute({
|
|
5479
|
+
sql: `SELECT title, assigned_to, project_name FROM tasks
|
|
5480
|
+
WHERE status = 'needs_review'
|
|
5481
|
+
ORDER BY priority ASC, created_at DESC LIMIT ?`,
|
|
5482
|
+
args: [limit]
|
|
5483
|
+
});
|
|
5484
|
+
return result.rows;
|
|
5485
|
+
}
|
|
5486
|
+
async function cleanupOrphanedReviews() {
|
|
5487
|
+
const client = getClient();
|
|
5488
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5489
|
+
const r1 = await client.execute({
|
|
5490
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
5491
|
+
WHERE status = 'needs_review'
|
|
5492
|
+
AND assigned_by = 'system'
|
|
5493
|
+
AND title LIKE 'Review:%'
|
|
5494
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
|
|
5495
|
+
args: [now]
|
|
5496
|
+
});
|
|
5497
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
5498
|
+
const r2 = await client.execute({
|
|
5499
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
5500
|
+
WHERE status = 'needs_review'
|
|
5501
|
+
AND result IS NOT NULL
|
|
5502
|
+
AND updated_at < ?`,
|
|
5503
|
+
args: [now, staleThreshold]
|
|
5504
|
+
});
|
|
5505
|
+
const total = r1.rowsAffected + r2.rowsAffected;
|
|
5506
|
+
if (total > 0) {
|
|
5211
5507
|
process.stderr.write(
|
|
5212
|
-
`[
|
|
5508
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
|
|
5213
5509
|
`
|
|
5214
5510
|
);
|
|
5215
5511
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5512
|
+
return total;
|
|
5513
|
+
}
|
|
5514
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
5515
|
+
const roleLower = role.toLowerCase();
|
|
5516
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
5517
|
+
return {
|
|
5518
|
+
lens: "Code Quality (Engineer)",
|
|
5519
|
+
checklist: [
|
|
5520
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
5521
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
5522
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
5523
|
+
"4. Any regressions in the test suite?"
|
|
5524
|
+
]
|
|
5525
|
+
};
|
|
5230
5526
|
}
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5527
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
5528
|
+
return {
|
|
5529
|
+
lens: "Architecture (CTO)",
|
|
5530
|
+
checklist: [
|
|
5531
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
5532
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
5533
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
5534
|
+
"4. Security implications? Any new attack surface?",
|
|
5535
|
+
"5. Does it scale? Performance considerations?",
|
|
5536
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
5537
|
+
]
|
|
5538
|
+
};
|
|
5539
|
+
}
|
|
5540
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
5541
|
+
return {
|
|
5542
|
+
lens: "Strategic (COO)",
|
|
5543
|
+
checklist: [
|
|
5544
|
+
"1. Does this serve the project mission?",
|
|
5545
|
+
"2. Is this the right work at the right time?",
|
|
5546
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
5547
|
+
"4. Any cross-project implications?"
|
|
5548
|
+
]
|
|
5549
|
+
};
|
|
5550
|
+
}
|
|
5551
|
+
return {
|
|
5552
|
+
lens: "General",
|
|
5553
|
+
checklist: [
|
|
5554
|
+
"1. Read the original task's acceptance criteria",
|
|
5555
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
5556
|
+
"3. Verify code changes match requirements",
|
|
5557
|
+
"4. Check if tests were added/updated",
|
|
5558
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
5559
|
+
]
|
|
5560
|
+
};
|
|
5561
|
+
}
|
|
5562
|
+
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
5563
|
+
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
5564
|
+
try {
|
|
5565
|
+
const client = getClient();
|
|
5566
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5567
|
+
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
5568
|
+
if (parentId) {
|
|
5569
|
+
const result = await client.execute({
|
|
5570
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
5571
|
+
args: [now, parentId]
|
|
5572
|
+
});
|
|
5573
|
+
if (result.rowsAffected > 0) {
|
|
5574
|
+
process.stderr.write(
|
|
5575
|
+
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
5576
|
+
`
|
|
5577
|
+
);
|
|
5578
|
+
}
|
|
5579
|
+
} else {
|
|
5580
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
5581
|
+
const reviewPrefix = fileName.replace(".md", "");
|
|
5582
|
+
const parts = reviewPrefix.split("-");
|
|
5583
|
+
if (parts.length >= 3 && parts[0] === "review") {
|
|
5584
|
+
const agent = parts[1];
|
|
5585
|
+
const slug = parts.slice(2).join("-");
|
|
5586
|
+
const originalTaskFile = `exe/${agent}/${slug}.md`;
|
|
5587
|
+
const result = await client.execute({
|
|
5588
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
|
|
5589
|
+
args: [now, originalTaskFile]
|
|
5590
|
+
});
|
|
5591
|
+
if (result.rowsAffected > 0) {
|
|
5592
|
+
process.stderr.write(
|
|
5593
|
+
`[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
|
|
5594
|
+
`
|
|
5595
|
+
);
|
|
5596
|
+
}
|
|
5238
5597
|
}
|
|
5239
5598
|
}
|
|
5240
|
-
}
|
|
5241
|
-
let spawnCommand;
|
|
5242
|
-
if (useExeAgent) {
|
|
5243
|
-
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
5244
|
-
} else if (useBinSymlink) {
|
|
5245
|
-
const binName = `${employeeName}-${ccProvider}`;
|
|
5599
|
+
} catch (err) {
|
|
5246
5600
|
process.stderr.write(
|
|
5247
|
-
`[
|
|
5601
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
5248
5602
|
`
|
|
5249
5603
|
);
|
|
5250
|
-
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
5251
|
-
} else {
|
|
5252
|
-
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
5253
|
-
}
|
|
5254
|
-
const spawnResult = transport.spawn(sessionName, {
|
|
5255
|
-
cwd: spawnCwd,
|
|
5256
|
-
command: spawnCommand
|
|
5257
|
-
});
|
|
5258
|
-
if (spawnResult.error) {
|
|
5259
|
-
releaseSpawnLock2(sessionName);
|
|
5260
|
-
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
5261
5604
|
}
|
|
5262
|
-
transport.pipeLog(sessionName, logFile);
|
|
5263
5605
|
try {
|
|
5264
|
-
const
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
5270
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5271
|
-
}));
|
|
5272
|
-
} catch {
|
|
5273
|
-
}
|
|
5274
|
-
let booted = false;
|
|
5275
|
-
for (let i = 0; i < 30; i++) {
|
|
5276
|
-
try {
|
|
5277
|
-
execSync4("sleep 0.5");
|
|
5278
|
-
} catch {
|
|
5279
|
-
}
|
|
5280
|
-
try {
|
|
5281
|
-
const pane = transport.capturePane(sessionName);
|
|
5282
|
-
if (useExeAgent) {
|
|
5283
|
-
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
5284
|
-
booted = true;
|
|
5285
|
-
break;
|
|
5286
|
-
}
|
|
5287
|
-
} else {
|
|
5288
|
-
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
5289
|
-
booted = true;
|
|
5290
|
-
break;
|
|
5606
|
+
const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
|
|
5607
|
+
if (existsSync12(cacheDir)) {
|
|
5608
|
+
for (const f of readdirSync3(cacheDir)) {
|
|
5609
|
+
if (f.startsWith("review-notified-")) {
|
|
5610
|
+
unlinkSync3(path13.join(cacheDir, f));
|
|
5291
5611
|
}
|
|
5292
5612
|
}
|
|
5293
|
-
} catch {
|
|
5294
|
-
}
|
|
5295
|
-
}
|
|
5296
|
-
if (!booted) {
|
|
5297
|
-
releaseSpawnLock2(sessionName);
|
|
5298
|
-
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
5299
|
-
}
|
|
5300
|
-
if (!useExeAgent) {
|
|
5301
|
-
try {
|
|
5302
|
-
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
5303
|
-
} catch {
|
|
5304
5613
|
}
|
|
5614
|
+
} catch {
|
|
5305
5615
|
}
|
|
5306
|
-
registerSession({
|
|
5307
|
-
windowName: sessionName,
|
|
5308
|
-
agentId: employeeName,
|
|
5309
|
-
projectDir: spawnCwd,
|
|
5310
|
-
parentExe: exeSession,
|
|
5311
|
-
pid: 0,
|
|
5312
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5313
|
-
});
|
|
5314
|
-
releaseSpawnLock2(sessionName);
|
|
5315
|
-
return { sessionName };
|
|
5316
5616
|
}
|
|
5317
|
-
var
|
|
5318
|
-
|
|
5319
|
-
"src/lib/tmux-routing.ts"() {
|
|
5617
|
+
var init_tasks_review = __esm({
|
|
5618
|
+
"src/lib/tasks-review.ts"() {
|
|
5320
5619
|
"use strict";
|
|
5321
|
-
|
|
5620
|
+
init_database();
|
|
5621
|
+
init_config();
|
|
5622
|
+
init_employees();
|
|
5623
|
+
init_notifications();
|
|
5624
|
+
init_tasks_crud();
|
|
5625
|
+
init_tmux_routing();
|
|
5322
5626
|
init_session_key();
|
|
5323
|
-
|
|
5324
|
-
init_cc_agent_support();
|
|
5325
|
-
init_mcp_prefix();
|
|
5326
|
-
init_provider_table();
|
|
5327
|
-
init_intercom_queue();
|
|
5328
|
-
init_plan_limits();
|
|
5329
|
-
SPAWN_LOCK_DIR = path11.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
5330
|
-
SESSION_CACHE = path11.join(os6.homedir(), ".exe-os", "session-cache");
|
|
5331
|
-
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
5332
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
5333
|
-
INTERCOM_LOG2 = path11.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
5334
|
-
DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
|
|
5335
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
5336
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
5627
|
+
init_state_bus();
|
|
5337
5628
|
}
|
|
5338
5629
|
});
|
|
5339
5630
|
|
|
5340
|
-
// src/lib/
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
getFailedMessages: () => getFailedMessages,
|
|
5345
|
-
getMessageStatus: () => getMessageStatus,
|
|
5346
|
-
getPendingMessages: () => getPendingMessages,
|
|
5347
|
-
getReadMessages: () => getReadMessages,
|
|
5348
|
-
getUnacknowledgedMessages: () => getUnacknowledgedMessages,
|
|
5349
|
-
markAcknowledged: () => markAcknowledged,
|
|
5350
|
-
markFailed: () => markFailed,
|
|
5351
|
-
markProcessed: () => markProcessed,
|
|
5352
|
-
markRead: () => markRead,
|
|
5353
|
-
retryPendingMessages: () => retryPendingMessages,
|
|
5354
|
-
sendMessage: () => sendMessage,
|
|
5355
|
-
setWsClientSend: () => setWsClientSend
|
|
5356
|
-
});
|
|
5357
|
-
import crypto3 from "crypto";
|
|
5358
|
-
function generateUlid() {
|
|
5359
|
-
const timestamp = Date.now().toString(36).padStart(10, "0");
|
|
5360
|
-
const random = crypto3.randomBytes(10).toString("hex").slice(0, 16);
|
|
5361
|
-
return (timestamp + random).toUpperCase();
|
|
5362
|
-
}
|
|
5363
|
-
function rowToMessage(row) {
|
|
5364
|
-
return {
|
|
5365
|
-
id: row.id,
|
|
5366
|
-
fromAgent: row.from_agent,
|
|
5367
|
-
fromDevice: row.from_device,
|
|
5368
|
-
targetAgent: row.target_agent,
|
|
5369
|
-
targetProject: row.target_project ?? null,
|
|
5370
|
-
targetDevice: row.target_device,
|
|
5371
|
-
content: row.content,
|
|
5372
|
-
priority: row.priority ?? "normal",
|
|
5373
|
-
status: row.status ?? "pending",
|
|
5374
|
-
serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
|
|
5375
|
-
retryCount: Number(row.retry_count ?? 0),
|
|
5376
|
-
createdAt: row.created_at,
|
|
5377
|
-
deliveredAt: row.delivered_at ?? null,
|
|
5378
|
-
processedAt: row.processed_at ?? null,
|
|
5379
|
-
failedAt: row.failed_at ?? null,
|
|
5380
|
-
failureReason: row.failure_reason ?? null
|
|
5381
|
-
};
|
|
5382
|
-
}
|
|
5383
|
-
async function sendMessage(input) {
|
|
5631
|
+
// src/lib/tasks-chain.ts
|
|
5632
|
+
import path14 from "path";
|
|
5633
|
+
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
5634
|
+
async function cascadeUnblock(taskId, baseDir, now) {
|
|
5384
5635
|
const client = getClient();
|
|
5385
|
-
const
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
|
|
5390
|
-
VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
|
|
5391
|
-
args: [
|
|
5392
|
-
id,
|
|
5393
|
-
input.fromAgent,
|
|
5394
|
-
input.targetAgent,
|
|
5395
|
-
input.targetProject ?? null,
|
|
5396
|
-
targetDevice,
|
|
5397
|
-
input.content,
|
|
5398
|
-
input.priority ?? "normal",
|
|
5399
|
-
now
|
|
5400
|
-
]
|
|
5636
|
+
const unblocked = await client.execute({
|
|
5637
|
+
sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
|
|
5638
|
+
WHERE blocked_by = ? AND status = 'blocked'`,
|
|
5639
|
+
args: [now, taskId]
|
|
5401
5640
|
});
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5641
|
+
if (baseDir && unblocked.rowsAffected > 0) {
|
|
5642
|
+
const unblockedRows = await client.execute({
|
|
5643
|
+
sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
|
|
5644
|
+
args: [now]
|
|
5645
|
+
});
|
|
5646
|
+
for (const ur of unblockedRows.rows) {
|
|
5647
|
+
try {
|
|
5648
|
+
const ubFile = path14.join(baseDir, String(ur.task_file));
|
|
5649
|
+
let ubContent = await readFile4(ubFile, "utf-8");
|
|
5650
|
+
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
5651
|
+
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
5652
|
+
await writeFile5(ubFile, ubContent, "utf-8");
|
|
5653
|
+
} catch {
|
|
5654
|
+
}
|
|
5407
5655
|
}
|
|
5408
|
-
} catch {
|
|
5409
5656
|
}
|
|
5410
|
-
const result = await client.execute({
|
|
5411
|
-
sql: "SELECT * FROM messages WHERE id = ?",
|
|
5412
|
-
args: [id]
|
|
5413
|
-
});
|
|
5414
|
-
return rowToMessage(result.rows[0]);
|
|
5415
|
-
}
|
|
5416
|
-
function setWsClientSend(fn) {
|
|
5417
|
-
_wsClientSend = fn;
|
|
5418
5657
|
}
|
|
5419
|
-
async function
|
|
5658
|
+
async function findNextTask(assignedTo) {
|
|
5420
5659
|
const client = getClient();
|
|
5421
|
-
const
|
|
5422
|
-
sql:
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
if (msg.status !== "pending") return false;
|
|
5428
|
-
if (!_wsClientSend) {
|
|
5429
|
-
return false;
|
|
5430
|
-
}
|
|
5431
|
-
const payload = JSON.stringify({
|
|
5432
|
-
id: msg.id,
|
|
5433
|
-
fromAgent: msg.fromAgent,
|
|
5434
|
-
targetAgent: msg.targetAgent,
|
|
5435
|
-
targetProject: msg.targetProject,
|
|
5436
|
-
content: msg.content,
|
|
5437
|
-
priority: msg.priority,
|
|
5438
|
-
createdAt: msg.createdAt
|
|
5660
|
+
const nextResult = await client.execute({
|
|
5661
|
+
sql: `SELECT title, task_file, priority FROM tasks
|
|
5662
|
+
WHERE assigned_to = ? AND status = 'open'
|
|
5663
|
+
ORDER BY priority ASC, created_at ASC
|
|
5664
|
+
LIMIT 1`,
|
|
5665
|
+
args: [assignedTo]
|
|
5439
5666
|
});
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5667
|
+
if (nextResult.rows.length === 1) {
|
|
5668
|
+
const nr = nextResult.rows[0];
|
|
5669
|
+
return {
|
|
5670
|
+
title: String(nr.title),
|
|
5671
|
+
priority: String(nr.priority),
|
|
5672
|
+
taskFile: String(nr.task_file)
|
|
5673
|
+
};
|
|
5447
5674
|
}
|
|
5448
|
-
return
|
|
5675
|
+
return void 0;
|
|
5449
5676
|
}
|
|
5450
|
-
async function
|
|
5677
|
+
async function checkSubtaskCompletion(parentTaskId, projectName) {
|
|
5451
5678
|
const client = getClient();
|
|
5452
|
-
const
|
|
5453
|
-
sql:
|
|
5454
|
-
|
|
5679
|
+
const remaining = await client.execute({
|
|
5680
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
5681
|
+
WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
|
|
5682
|
+
args: [parentTaskId]
|
|
5455
5683
|
});
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
try {
|
|
5462
|
-
const exeSession = resolveExeSession();
|
|
5463
|
-
if (!exeSession) {
|
|
5464
|
-
throw new Error("No exe session found");
|
|
5465
|
-
}
|
|
5466
|
-
const ensureResult = ensureEmployee(targetAgent, exeSession, process.cwd());
|
|
5467
|
-
if (ensureResult.status === "failed") {
|
|
5468
|
-
throw new Error(ensureResult.error ?? "ensureEmployee failed");
|
|
5469
|
-
}
|
|
5470
|
-
await client.execute({
|
|
5471
|
-
sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
|
|
5472
|
-
args: [now, messageId]
|
|
5684
|
+
const cnt = Number(remaining.rows[0]?.cnt ?? 1);
|
|
5685
|
+
if (cnt === 0) {
|
|
5686
|
+
const parentRow = await client.execute({
|
|
5687
|
+
sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
|
|
5688
|
+
args: [parentTaskId]
|
|
5473
5689
|
});
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5690
|
+
if (parentRow.rows.length === 1) {
|
|
5691
|
+
const pr = parentRow.rows[0];
|
|
5692
|
+
const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
|
|
5693
|
+
await writeNotification({
|
|
5694
|
+
agentId: String(pr.assigned_to),
|
|
5695
|
+
agentRole: "system",
|
|
5696
|
+
event: "subtasks_complete",
|
|
5697
|
+
project: parentProject,
|
|
5698
|
+
summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
|
|
5699
|
+
taskFile: String(pr.task_file)
|
|
5483
5700
|
});
|
|
5484
5701
|
}
|
|
5485
|
-
return false;
|
|
5486
5702
|
}
|
|
5487
5703
|
}
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5704
|
+
var init_tasks_chain = __esm({
|
|
5705
|
+
"src/lib/tasks-chain.ts"() {
|
|
5706
|
+
"use strict";
|
|
5707
|
+
init_database();
|
|
5708
|
+
init_notifications();
|
|
5709
|
+
}
|
|
5710
|
+
});
|
|
5711
|
+
|
|
5712
|
+
// src/lib/project-name.ts
|
|
5713
|
+
import { execSync as execSync5 } from "child_process";
|
|
5714
|
+
import path15 from "path";
|
|
5715
|
+
function getProjectName(cwd) {
|
|
5716
|
+
const dir = cwd ?? process.cwd();
|
|
5717
|
+
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
5718
|
+
try {
|
|
5719
|
+
let repoRoot;
|
|
5720
|
+
try {
|
|
5721
|
+
const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
|
|
5722
|
+
cwd: dir,
|
|
5723
|
+
encoding: "utf8",
|
|
5724
|
+
timeout: 2e3,
|
|
5725
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5726
|
+
}).trim();
|
|
5727
|
+
repoRoot = path15.dirname(gitCommonDir);
|
|
5728
|
+
} catch {
|
|
5729
|
+
repoRoot = execSync5("git rev-parse --show-toplevel", {
|
|
5730
|
+
cwd: dir,
|
|
5731
|
+
encoding: "utf8",
|
|
5732
|
+
timeout: 2e3,
|
|
5733
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5734
|
+
}).trim();
|
|
5735
|
+
}
|
|
5736
|
+
_cached2 = path15.basename(repoRoot);
|
|
5737
|
+
_cachedCwd = dir;
|
|
5738
|
+
return _cached2;
|
|
5739
|
+
} catch {
|
|
5740
|
+
_cached2 = path15.basename(dir);
|
|
5741
|
+
_cachedCwd = dir;
|
|
5742
|
+
return _cached2;
|
|
5743
|
+
}
|
|
5497
5744
|
}
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5745
|
+
var _cached2, _cachedCwd;
|
|
5746
|
+
var init_project_name = __esm({
|
|
5747
|
+
"src/lib/project-name.ts"() {
|
|
5748
|
+
"use strict";
|
|
5749
|
+
_cached2 = null;
|
|
5750
|
+
_cachedCwd = null;
|
|
5751
|
+
}
|
|
5752
|
+
});
|
|
5753
|
+
|
|
5754
|
+
// src/lib/session-scope.ts
|
|
5755
|
+
var session_scope_exports = {};
|
|
5756
|
+
__export(session_scope_exports, {
|
|
5757
|
+
assertSessionScope: () => assertSessionScope,
|
|
5758
|
+
findSessionForProject: () => findSessionForProject,
|
|
5759
|
+
getSessionProject: () => getSessionProject
|
|
5760
|
+
});
|
|
5761
|
+
function getSessionProject(sessionName) {
|
|
5762
|
+
const sessions = listSessions();
|
|
5763
|
+
const entry = sessions.find((s) => s.windowName === sessionName);
|
|
5764
|
+
if (!entry) return null;
|
|
5765
|
+
const parts = entry.projectDir.split("/").filter(Boolean);
|
|
5766
|
+
return parts[parts.length - 1] ?? null;
|
|
5504
5767
|
}
|
|
5505
|
-
|
|
5506
|
-
const
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
}
|
|
5768
|
+
function findSessionForProject(projectName) {
|
|
5769
|
+
const sessions = listSessions();
|
|
5770
|
+
for (const s of sessions) {
|
|
5771
|
+
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
5772
|
+
if (proj === projectName && s.agentId === "exe") return s;
|
|
5773
|
+
}
|
|
5774
|
+
return null;
|
|
5511
5775
|
}
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
}
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
}
|
|
5527
|
-
async function getUnacknowledgedMessages(targetAgent) {
|
|
5528
|
-
const client = getClient();
|
|
5529
|
-
const result = await client.execute({
|
|
5530
|
-
sql: `SELECT * FROM messages
|
|
5531
|
-
WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
|
|
5532
|
-
ORDER BY id`,
|
|
5533
|
-
args: [targetAgent]
|
|
5534
|
-
});
|
|
5535
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
5536
|
-
}
|
|
5537
|
-
async function getReadMessages(targetAgent) {
|
|
5538
|
-
const client = getClient();
|
|
5539
|
-
const result = await client.execute({
|
|
5540
|
-
sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
|
|
5541
|
-
args: [targetAgent]
|
|
5542
|
-
});
|
|
5543
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
5544
|
-
}
|
|
5545
|
-
async function markFailed(messageId, reason) {
|
|
5546
|
-
const client = getClient();
|
|
5547
|
-
await client.execute({
|
|
5548
|
-
sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
|
|
5549
|
-
args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
|
|
5550
|
-
});
|
|
5551
|
-
}
|
|
5552
|
-
async function getFailedMessages() {
|
|
5553
|
-
const client = getClient();
|
|
5554
|
-
const result = await client.execute({
|
|
5555
|
-
sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
|
|
5556
|
-
args: []
|
|
5557
|
-
});
|
|
5558
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
5559
|
-
}
|
|
5560
|
-
async function retryPendingMessages() {
|
|
5561
|
-
const client = getClient();
|
|
5562
|
-
const result = await client.execute({
|
|
5563
|
-
sql: `SELECT * FROM messages
|
|
5564
|
-
WHERE status = 'pending' AND retry_count < ?
|
|
5565
|
-
ORDER BY id`,
|
|
5566
|
-
args: [MAX_RETRIES2]
|
|
5567
|
-
});
|
|
5568
|
-
let delivered = 0;
|
|
5569
|
-
for (const row of result.rows) {
|
|
5570
|
-
const msg = rowToMessage(row);
|
|
5571
|
-
try {
|
|
5572
|
-
const success = await deliverLocalMessage(msg.id);
|
|
5573
|
-
if (success) delivered++;
|
|
5574
|
-
} catch {
|
|
5776
|
+
function assertSessionScope(actionType, targetProject) {
|
|
5777
|
+
try {
|
|
5778
|
+
const currentProject = getProjectName();
|
|
5779
|
+
const exeSession = resolveExeSession();
|
|
5780
|
+
if (!exeSession) {
|
|
5781
|
+
return { allowed: true, reason: "no_session" };
|
|
5782
|
+
}
|
|
5783
|
+
if (currentProject === targetProject) {
|
|
5784
|
+
return {
|
|
5785
|
+
allowed: true,
|
|
5786
|
+
reason: "same_session",
|
|
5787
|
+
currentProject,
|
|
5788
|
+
targetProject
|
|
5789
|
+
};
|
|
5575
5790
|
}
|
|
5791
|
+
process.stderr.write(
|
|
5792
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
5793
|
+
`
|
|
5794
|
+
);
|
|
5795
|
+
return {
|
|
5796
|
+
allowed: false,
|
|
5797
|
+
reason: "cross_session_denied",
|
|
5798
|
+
currentProject,
|
|
5799
|
+
targetProject,
|
|
5800
|
+
targetSession: findSessionForProject(targetProject)?.windowName
|
|
5801
|
+
};
|
|
5802
|
+
} catch {
|
|
5803
|
+
return { allowed: true, reason: "no_session" };
|
|
5576
5804
|
}
|
|
5577
|
-
return delivered;
|
|
5578
5805
|
}
|
|
5579
|
-
var
|
|
5580
|
-
|
|
5581
|
-
"src/lib/messaging.ts"() {
|
|
5806
|
+
var init_session_scope = __esm({
|
|
5807
|
+
"src/lib/session-scope.ts"() {
|
|
5582
5808
|
"use strict";
|
|
5583
|
-
|
|
5809
|
+
init_session_registry();
|
|
5810
|
+
init_project_name();
|
|
5584
5811
|
init_tmux_routing();
|
|
5585
|
-
MAX_RETRIES2 = 10;
|
|
5586
|
-
_wsClientSend = null;
|
|
5587
5812
|
}
|
|
5588
5813
|
});
|
|
5589
5814
|
|
|
5590
|
-
// src/lib/
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5815
|
+
// src/lib/tasks-notify.ts
|
|
5816
|
+
async function dispatchTaskToEmployee(input) {
|
|
5817
|
+
if (input.assignedTo === "exe") return { dispatched: "skipped" };
|
|
5818
|
+
let crossProject = false;
|
|
5819
|
+
if (input.projectName) {
|
|
5820
|
+
try {
|
|
5821
|
+
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
5822
|
+
const check = assertSessionScope2("dispatch_task", input.projectName);
|
|
5823
|
+
if (check.reason === "cross_session_denied") {
|
|
5824
|
+
crossProject = true;
|
|
5825
|
+
return { dispatched: "skipped", crossProject: true };
|
|
5826
|
+
}
|
|
5827
|
+
} catch {
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5602
5830
|
try {
|
|
5603
|
-
const
|
|
5604
|
-
const
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5831
|
+
const transport = getTransport();
|
|
5832
|
+
const exeSession = resolveExeSession();
|
|
5833
|
+
if (!exeSession) return { dispatched: "session_missing" };
|
|
5834
|
+
const sessionName = employeeSessionName(input.assignedTo, exeSession);
|
|
5835
|
+
if (transport.isAlive(sessionName)) {
|
|
5836
|
+
const result = sendIntercom(sessionName);
|
|
5837
|
+
const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
|
|
5838
|
+
return { dispatched, session: sessionName, crossProject };
|
|
5839
|
+
} else {
|
|
5840
|
+
const projectDir = input.projectDir ?? process.cwd();
|
|
5841
|
+
const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
|
|
5842
|
+
autoInstance: isMultiInstance(input.assignedTo)
|
|
5843
|
+
});
|
|
5844
|
+
if (result.status === "failed") {
|
|
5845
|
+
process.stderr.write(
|
|
5846
|
+
`[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
|
|
5847
|
+
`
|
|
5848
|
+
);
|
|
5849
|
+
return { dispatched: "session_missing" };
|
|
5850
|
+
}
|
|
5851
|
+
return { dispatched: "spawned", session: result.sessionName, crossProject };
|
|
5852
|
+
}
|
|
5853
|
+
} catch {
|
|
5854
|
+
return { dispatched: "session_missing" };
|
|
5623
5855
|
}
|
|
5624
5856
|
}
|
|
5625
|
-
|
|
5857
|
+
function notifyTaskDone() {
|
|
5626
5858
|
try {
|
|
5627
|
-
const
|
|
5628
|
-
|
|
5629
|
-
sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
|
|
5630
|
-
args: [taskFile]
|
|
5631
|
-
});
|
|
5859
|
+
const key = getSessionKey();
|
|
5860
|
+
if (key && !process.env.VITEST) notifyParentExe(key);
|
|
5632
5861
|
} catch {
|
|
5633
5862
|
}
|
|
5634
5863
|
}
|
|
5635
|
-
|
|
5636
|
-
|
|
5864
|
+
async function markTaskNotificationsRead(taskFile) {
|
|
5865
|
+
try {
|
|
5866
|
+
await markAsReadByTaskFile(taskFile);
|
|
5867
|
+
} catch {
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
var init_tasks_notify = __esm({
|
|
5871
|
+
"src/lib/tasks-notify.ts"() {
|
|
5637
5872
|
"use strict";
|
|
5638
|
-
|
|
5873
|
+
init_tmux_routing();
|
|
5874
|
+
init_session_key();
|
|
5875
|
+
init_notifications();
|
|
5876
|
+
init_transport();
|
|
5877
|
+
init_employees();
|
|
5639
5878
|
}
|
|
5640
5879
|
});
|
|
5641
5880
|
|
|
5642
|
-
// src/lib/
|
|
5643
|
-
import
|
|
5644
|
-
|
|
5645
|
-
import { execSync as execSync5 } from "child_process";
|
|
5646
|
-
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
5647
|
-
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
5648
|
-
async function writeCheckpoint(input) {
|
|
5881
|
+
// src/lib/behaviors.ts
|
|
5882
|
+
import crypto6 from "crypto";
|
|
5883
|
+
async function storeBehavior(opts) {
|
|
5649
5884
|
const client = getClient();
|
|
5650
|
-
const
|
|
5651
|
-
const taskId = String(row.id);
|
|
5885
|
+
const id = crypto6.randomUUID();
|
|
5652
5886
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
const checkpoint = {
|
|
5658
|
-
step: input.step,
|
|
5659
|
-
context_summary: input.contextSummary,
|
|
5660
|
-
files_touched: input.filesTouched ?? [],
|
|
5661
|
-
blocked_by_ids: blockedByIds,
|
|
5662
|
-
last_checkpoint_at: now
|
|
5663
|
-
};
|
|
5664
|
-
const result = await client.execute({
|
|
5665
|
-
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
5666
|
-
args: [JSON.stringify(checkpoint), now, taskId]
|
|
5887
|
+
await client.execute({
|
|
5888
|
+
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
5889
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
5890
|
+
args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
|
|
5667
5891
|
});
|
|
5668
|
-
|
|
5669
|
-
|
|
5892
|
+
return id;
|
|
5893
|
+
}
|
|
5894
|
+
var init_behaviors = __esm({
|
|
5895
|
+
"src/lib/behaviors.ts"() {
|
|
5896
|
+
"use strict";
|
|
5897
|
+
init_database();
|
|
5670
5898
|
}
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5899
|
+
});
|
|
5900
|
+
|
|
5901
|
+
// src/lib/skill-learning.ts
|
|
5902
|
+
var skill_learning_exports = {};
|
|
5903
|
+
__export(skill_learning_exports, {
|
|
5904
|
+
captureAndLearn: () => captureAndLearn,
|
|
5905
|
+
captureTrajectory: () => captureTrajectory,
|
|
5906
|
+
editDistance: () => editDistance,
|
|
5907
|
+
extractSkill: () => extractSkill,
|
|
5908
|
+
extractTrajectory: () => extractTrajectory,
|
|
5909
|
+
findSimilarTrajectories: () => findSimilarTrajectories,
|
|
5910
|
+
hashSignature: () => hashSignature,
|
|
5911
|
+
storeTrajectory: () => storeTrajectory,
|
|
5912
|
+
sweepTrajectories: () => sweepTrajectories
|
|
5913
|
+
});
|
|
5914
|
+
import crypto7 from "crypto";
|
|
5915
|
+
async function extractTrajectory(taskId, agentId) {
|
|
5916
|
+
const client = getClient();
|
|
5917
|
+
const result = await client.execute({
|
|
5918
|
+
sql: `SELECT tool_name, raw_text
|
|
5919
|
+
FROM memories
|
|
5920
|
+
WHERE task_id = ? AND agent_id = ?
|
|
5921
|
+
ORDER BY timestamp ASC`,
|
|
5922
|
+
args: [taskId, agentId]
|
|
5674
5923
|
});
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
}
|
|
5685
|
-
function slugify(title) {
|
|
5686
|
-
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
5687
|
-
}
|
|
5688
|
-
async function resolveTask(client, identifier) {
|
|
5689
|
-
let result = await client.execute({
|
|
5690
|
-
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
5691
|
-
args: [identifier]
|
|
5692
|
-
});
|
|
5693
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
5694
|
-
result = await client.execute({
|
|
5695
|
-
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
5696
|
-
args: [`%${identifier}%`]
|
|
5697
|
-
});
|
|
5698
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
5699
|
-
if (result.rows.length > 1) {
|
|
5700
|
-
const exact = result.rows.filter(
|
|
5701
|
-
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
5702
|
-
);
|
|
5703
|
-
if (exact.length === 1) return exact[0];
|
|
5704
|
-
const candidates = exact.length > 1 ? exact : result.rows;
|
|
5705
|
-
const active = candidates.filter(
|
|
5706
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
5707
|
-
);
|
|
5708
|
-
if (active.length === 1) return active[0];
|
|
5709
|
-
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
5710
|
-
throw new Error(
|
|
5711
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
5712
|
-
);
|
|
5713
|
-
}
|
|
5714
|
-
result = await client.execute({
|
|
5715
|
-
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
5716
|
-
args: [`%${identifier}%`]
|
|
5924
|
+
if (result.rows.length === 0) return [];
|
|
5925
|
+
const rawTools = result.rows.map((r) => {
|
|
5926
|
+
const toolName = String(r.tool_name);
|
|
5927
|
+
if (toolName === "Bash") {
|
|
5928
|
+
const text = String(r.raw_text);
|
|
5929
|
+
const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
|
|
5930
|
+
return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
|
|
5931
|
+
}
|
|
5932
|
+
return toolName;
|
|
5717
5933
|
});
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
if (active.length === 1) return active[0];
|
|
5724
|
-
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
5725
|
-
throw new Error(
|
|
5726
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
5727
|
-
);
|
|
5934
|
+
const signature = [];
|
|
5935
|
+
for (const tool of rawTools) {
|
|
5936
|
+
if (signature.length === 0 || signature[signature.length - 1] !== tool) {
|
|
5937
|
+
signature.push(tool);
|
|
5938
|
+
}
|
|
5728
5939
|
}
|
|
5729
|
-
|
|
5940
|
+
return signature;
|
|
5730
5941
|
}
|
|
5731
|
-
|
|
5942
|
+
function hashSignature(signature) {
|
|
5943
|
+
return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
5944
|
+
}
|
|
5945
|
+
async function storeTrajectory(opts) {
|
|
5732
5946
|
const client = getClient();
|
|
5733
|
-
const id =
|
|
5947
|
+
const id = crypto7.randomUUID();
|
|
5734
5948
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5735
|
-
const
|
|
5736
|
-
const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
|
|
5737
|
-
let blockedById = null;
|
|
5738
|
-
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
5739
|
-
if (input.blockedBy) {
|
|
5740
|
-
const blocker = await resolveTask(client, input.blockedBy);
|
|
5741
|
-
blockedById = String(blocker.id);
|
|
5742
|
-
}
|
|
5743
|
-
let parentTaskId = null;
|
|
5744
|
-
let parentRef = input.parentTaskId;
|
|
5745
|
-
if (!parentRef) {
|
|
5746
|
-
const extracted = extractParentFromContext(input.context);
|
|
5747
|
-
if (extracted) {
|
|
5748
|
-
parentRef = extracted;
|
|
5749
|
-
process.stderr.write(
|
|
5750
|
-
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
5751
|
-
);
|
|
5752
|
-
}
|
|
5753
|
-
}
|
|
5754
|
-
if (parentRef) {
|
|
5755
|
-
try {
|
|
5756
|
-
const parent = await resolveTask(client, parentRef);
|
|
5757
|
-
parentTaskId = String(parent.id);
|
|
5758
|
-
} catch (err) {
|
|
5759
|
-
if (!input.parentTaskId) {
|
|
5760
|
-
throw new Error(
|
|
5761
|
-
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
5762
|
-
);
|
|
5763
|
-
}
|
|
5764
|
-
throw err;
|
|
5765
|
-
}
|
|
5766
|
-
}
|
|
5767
|
-
let warning;
|
|
5768
|
-
const dupCheck = await client.execute({
|
|
5769
|
-
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
5770
|
-
args: [input.title, input.assignedTo]
|
|
5771
|
-
});
|
|
5772
|
-
if (dupCheck.rows.length > 0) {
|
|
5773
|
-
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
5774
|
-
}
|
|
5775
|
-
if (input.baseDir) {
|
|
5776
|
-
try {
|
|
5777
|
-
await mkdir4(path13.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
5778
|
-
await mkdir4(path13.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
5779
|
-
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
5780
|
-
await ensureGitignoreExe(input.baseDir);
|
|
5781
|
-
} catch {
|
|
5782
|
-
}
|
|
5783
|
-
}
|
|
5784
|
-
const complexity = input.complexity ?? "standard";
|
|
5949
|
+
const signatureHash = hashSignature(opts.signature);
|
|
5785
5950
|
await client.execute({
|
|
5786
|
-
sql: `INSERT INTO
|
|
5787
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
5951
|
+
sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
|
|
5952
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5788
5953
|
args: [
|
|
5789
5954
|
id,
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
blockedById,
|
|
5798
|
-
parentTaskId,
|
|
5799
|
-
input.reviewer ?? null,
|
|
5800
|
-
input.context,
|
|
5801
|
-
complexity,
|
|
5802
|
-
input.budgetTokens ?? null,
|
|
5803
|
-
input.budgetFallbackModel ?? null,
|
|
5804
|
-
0,
|
|
5805
|
-
null,
|
|
5806
|
-
now,
|
|
5955
|
+
opts.taskId,
|
|
5956
|
+
opts.agentId,
|
|
5957
|
+
opts.projectName,
|
|
5958
|
+
opts.taskTitle,
|
|
5959
|
+
JSON.stringify(opts.signature),
|
|
5960
|
+
signatureHash,
|
|
5961
|
+
opts.signature.length,
|
|
5807
5962
|
now
|
|
5808
5963
|
]
|
|
5809
5964
|
});
|
|
5810
|
-
return
|
|
5811
|
-
id,
|
|
5812
|
-
title: input.title,
|
|
5813
|
-
assignedTo: input.assignedTo,
|
|
5814
|
-
assignedBy: input.assignedBy,
|
|
5815
|
-
projectName: input.projectName,
|
|
5816
|
-
priority: input.priority,
|
|
5817
|
-
status: initialStatus,
|
|
5818
|
-
taskFile,
|
|
5819
|
-
createdAt: now,
|
|
5820
|
-
updatedAt: now,
|
|
5821
|
-
warning,
|
|
5822
|
-
budgetTokens: input.budgetTokens ?? null,
|
|
5823
|
-
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
5824
|
-
tokensUsed: 0,
|
|
5825
|
-
tokensWarnedAt: null
|
|
5826
|
-
};
|
|
5965
|
+
return id;
|
|
5827
5966
|
}
|
|
5828
|
-
async function
|
|
5967
|
+
async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
|
|
5829
5968
|
const client = getClient();
|
|
5830
|
-
const
|
|
5831
|
-
const args = [];
|
|
5832
|
-
if (input.assignedTo) {
|
|
5833
|
-
conditions.push("assigned_to = ?");
|
|
5834
|
-
args.push(input.assignedTo);
|
|
5835
|
-
}
|
|
5836
|
-
if (input.status) {
|
|
5837
|
-
conditions.push("status = ?");
|
|
5838
|
-
args.push(input.status);
|
|
5839
|
-
} else {
|
|
5840
|
-
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
5841
|
-
}
|
|
5842
|
-
if (input.projectName) {
|
|
5843
|
-
conditions.push("project_name = ?");
|
|
5844
|
-
args.push(input.projectName);
|
|
5845
|
-
}
|
|
5846
|
-
if (input.priority) {
|
|
5847
|
-
conditions.push("priority = ?");
|
|
5848
|
-
args.push(input.priority);
|
|
5849
|
-
}
|
|
5850
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
5969
|
+
const hash = hashSignature(signature);
|
|
5851
5970
|
const result = await client.execute({
|
|
5852
|
-
sql: `SELECT
|
|
5853
|
-
|
|
5971
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
5972
|
+
FROM trajectories
|
|
5973
|
+
WHERE signature_hash = ?
|
|
5974
|
+
ORDER BY created_at DESC
|
|
5975
|
+
LIMIT 20`,
|
|
5976
|
+
args: [hash]
|
|
5854
5977
|
});
|
|
5855
|
-
|
|
5978
|
+
const mapRow = (r) => ({
|
|
5856
5979
|
id: String(r.id),
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
assignedBy: String(r.assigned_by),
|
|
5980
|
+
taskId: String(r.task_id),
|
|
5981
|
+
agentId: String(r.agent_id),
|
|
5860
5982
|
projectName: String(r.project_name),
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5983
|
+
taskTitle: String(r.task_title),
|
|
5984
|
+
signature: JSON.parse(String(r.signature)),
|
|
5985
|
+
signatureHash: String(r.signature_hash),
|
|
5986
|
+
toolCount: Number(r.tool_count),
|
|
5987
|
+
skillId: r.skill_id ? String(r.skill_id) : null,
|
|
5988
|
+
createdAt: String(r.created_at)
|
|
5989
|
+
});
|
|
5990
|
+
const matches = result.rows.map(mapRow);
|
|
5991
|
+
if (matches.length >= threshold) return matches;
|
|
5992
|
+
const nearResult = await client.execute({
|
|
5993
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
5994
|
+
FROM trajectories
|
|
5995
|
+
WHERE tool_count BETWEEN ? AND ?
|
|
5996
|
+
AND signature_hash != ?
|
|
5997
|
+
ORDER BY created_at DESC
|
|
5998
|
+
LIMIT 50`,
|
|
5999
|
+
args: [
|
|
6000
|
+
Math.max(1, signature.length - 3),
|
|
6001
|
+
signature.length + 3,
|
|
6002
|
+
hash
|
|
6003
|
+
]
|
|
6004
|
+
});
|
|
6005
|
+
for (const r of nearResult.rows) {
|
|
6006
|
+
const candidateSig = JSON.parse(String(r.signature));
|
|
6007
|
+
if (editDistance(signature, candidateSig) <= 2) {
|
|
6008
|
+
matches.push(mapRow(r));
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
return matches;
|
|
5872
6012
|
}
|
|
5873
|
-
function
|
|
5874
|
-
|
|
5875
|
-
if (
|
|
5876
|
-
|
|
5877
|
-
const since = new Date(taskCreatedAt).toISOString();
|
|
5878
|
-
const branch = execSync5(
|
|
5879
|
-
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
5880
|
-
{ encoding: "utf8", timeout: 3e3 }
|
|
5881
|
-
).trim();
|
|
5882
|
-
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
5883
|
-
const commitCount = execSync5(
|
|
5884
|
-
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
5885
|
-
{ encoding: "utf8", timeout: 5e3 }
|
|
5886
|
-
).trim();
|
|
5887
|
-
const count = parseInt(commitCount, 10);
|
|
5888
|
-
if (count === 0) {
|
|
5889
|
-
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
5890
|
-
}
|
|
5891
|
-
return null;
|
|
5892
|
-
} catch {
|
|
5893
|
-
return null;
|
|
6013
|
+
async function captureTrajectory(opts) {
|
|
6014
|
+
const signature = await extractTrajectory(opts.taskId, opts.agentId);
|
|
6015
|
+
if (signature.length < 3) {
|
|
6016
|
+
return { trajectoryId: "", similarCount: 0, similar: [] };
|
|
5894
6017
|
}
|
|
6018
|
+
const trajectoryId = await storeTrajectory({
|
|
6019
|
+
taskId: opts.taskId,
|
|
6020
|
+
agentId: opts.agentId,
|
|
6021
|
+
projectName: opts.projectName,
|
|
6022
|
+
taskTitle: opts.taskTitle,
|
|
6023
|
+
signature
|
|
6024
|
+
});
|
|
6025
|
+
const similar = await findSimilarTrajectories(
|
|
6026
|
+
signature,
|
|
6027
|
+
opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
|
|
6028
|
+
);
|
|
6029
|
+
return { trajectoryId, similarCount: similar.length, similar };
|
|
5895
6030
|
}
|
|
5896
|
-
|
|
5897
|
-
const
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
process.stderr.write(
|
|
5904
|
-
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
5905
|
-
`
|
|
5906
|
-
);
|
|
5907
|
-
}
|
|
5908
|
-
if (input.status === "done") {
|
|
5909
|
-
const existingRow = await client.execute({
|
|
5910
|
-
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
5911
|
-
args: [taskId]
|
|
5912
|
-
});
|
|
5913
|
-
if (existingRow.rows.length > 0) {
|
|
5914
|
-
const ctx = existingRow.rows[0];
|
|
5915
|
-
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
5916
|
-
if (warning) {
|
|
5917
|
-
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
6031
|
+
function buildExtractionPrompt(trajectories) {
|
|
6032
|
+
const items = trajectories.map((t, i) => {
|
|
6033
|
+
const sig = t.signature.join(" \u2192 ");
|
|
6034
|
+
return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
|
|
6035
|
+
Signature: ${sig}`;
|
|
6036
|
+
}).join("\n\n");
|
|
6037
|
+
return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
|
|
5918
6038
|
|
|
5919
|
-
${
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
args: [tmuxSession, now, taskId]
|
|
5932
|
-
});
|
|
5933
|
-
if (claim.rowsAffected === 0) {
|
|
5934
|
-
const current = await client.execute({
|
|
5935
|
-
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
5936
|
-
args: [taskId]
|
|
5937
|
-
});
|
|
5938
|
-
const cur = current.rows[0];
|
|
5939
|
-
const status = cur?.status ?? "unknown";
|
|
5940
|
-
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
5941
|
-
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
5942
|
-
}
|
|
5943
|
-
try {
|
|
5944
|
-
await writeCheckpoint({
|
|
5945
|
-
taskId,
|
|
5946
|
-
step: "claimed",
|
|
5947
|
-
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
5948
|
-
});
|
|
5949
|
-
} catch {
|
|
5950
|
-
}
|
|
5951
|
-
return { row, taskFile, now, taskId };
|
|
5952
|
-
}
|
|
5953
|
-
if (input.result) {
|
|
5954
|
-
await client.execute({
|
|
5955
|
-
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
5956
|
-
args: [input.status, input.result, now, taskId]
|
|
5957
|
-
});
|
|
5958
|
-
} else {
|
|
5959
|
-
await client.execute({
|
|
5960
|
-
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
5961
|
-
args: [input.status, now, taskId]
|
|
5962
|
-
});
|
|
5963
|
-
}
|
|
5964
|
-
try {
|
|
5965
|
-
await writeCheckpoint({
|
|
5966
|
-
taskId,
|
|
5967
|
-
step: `status_transition:${input.status}`,
|
|
5968
|
-
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
5969
|
-
});
|
|
5970
|
-
} catch {
|
|
5971
|
-
}
|
|
5972
|
-
return { row, taskFile, now, taskId };
|
|
5973
|
-
}
|
|
5974
|
-
async function deleteTaskCore(taskId, _baseDir) {
|
|
5975
|
-
const client = getClient();
|
|
5976
|
-
const row = await resolveTask(client, taskId);
|
|
5977
|
-
const id = String(row.id);
|
|
5978
|
-
const taskFile = String(row.task_file);
|
|
5979
|
-
const assignedTo = String(row.assigned_to);
|
|
5980
|
-
const assignedBy = String(row.assigned_by);
|
|
5981
|
-
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
5982
|
-
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
5983
|
-
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
6039
|
+
${items}
|
|
6040
|
+
|
|
6041
|
+
Extract the reusable procedure. Format your response EXACTLY like this:
|
|
6042
|
+
|
|
6043
|
+
SKILL: {name \u2014 short, descriptive}
|
|
6044
|
+
TRIGGER: {when to use this \u2014 one sentence}
|
|
6045
|
+
STEPS:
|
|
6046
|
+
1. ...
|
|
6047
|
+
2. ...
|
|
6048
|
+
PITFALLS: {common mistakes to avoid}
|
|
6049
|
+
|
|
6050
|
+
Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
|
|
5984
6051
|
}
|
|
5985
|
-
async function
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
"
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
""
|
|
6015
|
-
].join("\n");
|
|
6016
|
-
await writeFile4(archPath, template, "utf-8");
|
|
6017
|
-
} catch {
|
|
6052
|
+
async function extractSkill(trajectories, model) {
|
|
6053
|
+
if (trajectories.length === 0) return null;
|
|
6054
|
+
const config2 = await loadConfig();
|
|
6055
|
+
const skillModel = model ?? config2.skillModel;
|
|
6056
|
+
const Anthropic2 = (await import("@anthropic-ai/sdk")).default;
|
|
6057
|
+
const client = new Anthropic2();
|
|
6058
|
+
const prompt = buildExtractionPrompt(trajectories);
|
|
6059
|
+
const response = await client.messages.create({
|
|
6060
|
+
model: skillModel,
|
|
6061
|
+
max_tokens: 500,
|
|
6062
|
+
messages: [{ role: "user", content: prompt }]
|
|
6063
|
+
});
|
|
6064
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
6065
|
+
const skillText = textBlock?.text;
|
|
6066
|
+
if (!skillText) return null;
|
|
6067
|
+
const agentId = trajectories[0].agentId;
|
|
6068
|
+
const projectName = trajectories[0].projectName;
|
|
6069
|
+
const skillId = await storeBehavior({
|
|
6070
|
+
agentId,
|
|
6071
|
+
content: skillText,
|
|
6072
|
+
domain: "skill",
|
|
6073
|
+
projectName
|
|
6074
|
+
});
|
|
6075
|
+
const dbClient = getClient();
|
|
6076
|
+
for (const t of trajectories) {
|
|
6077
|
+
await dbClient.execute({
|
|
6078
|
+
sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
|
|
6079
|
+
args: [skillId, t.id]
|
|
6080
|
+
});
|
|
6018
6081
|
}
|
|
6082
|
+
process.stderr.write(
|
|
6083
|
+
`[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
|
|
6084
|
+
`
|
|
6085
|
+
);
|
|
6086
|
+
return skillId;
|
|
6019
6087
|
}
|
|
6020
|
-
async function
|
|
6021
|
-
const gitignorePath = path13.join(baseDir, ".gitignore");
|
|
6088
|
+
async function captureAndLearn(opts) {
|
|
6022
6089
|
try {
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6090
|
+
const config2 = await loadConfig();
|
|
6091
|
+
if (!config2.skillLearning) return;
|
|
6092
|
+
const { trajectoryId, similarCount, similar } = await captureTrajectory({
|
|
6093
|
+
...opts,
|
|
6094
|
+
skillThreshold: config2.skillThreshold
|
|
6095
|
+
});
|
|
6096
|
+
if (!trajectoryId) return;
|
|
6097
|
+
if (similarCount >= config2.skillThreshold) {
|
|
6098
|
+
const unprocessed = similar.filter((t) => !t.skillId);
|
|
6099
|
+
if (unprocessed.length >= config2.skillThreshold) {
|
|
6100
|
+
extractSkill(unprocessed, config2.skillModel).catch((err) => {
|
|
6101
|
+
process.stderr.write(
|
|
6102
|
+
`[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
|
|
6103
|
+
`
|
|
6104
|
+
);
|
|
6105
|
+
});
|
|
6106
|
+
}
|
|
6029
6107
|
}
|
|
6030
|
-
} catch {
|
|
6108
|
+
} catch (err) {
|
|
6109
|
+
process.stderr.write(
|
|
6110
|
+
`[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
|
|
6111
|
+
`
|
|
6112
|
+
);
|
|
6031
6113
|
}
|
|
6032
6114
|
}
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
init_database();
|
|
6038
|
-
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
6039
|
-
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
6040
|
-
}
|
|
6041
|
-
});
|
|
6042
|
-
|
|
6043
|
-
// src/lib/tasks-review.ts
|
|
6044
|
-
import path14 from "path";
|
|
6045
|
-
import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
6046
|
-
async function countPendingReviews() {
|
|
6115
|
+
async function sweepTrajectories(threshold, model) {
|
|
6116
|
+
const config2 = await loadConfig();
|
|
6117
|
+
if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
|
|
6118
|
+
const t = threshold ?? config2.skillThreshold;
|
|
6047
6119
|
const client = getClient();
|
|
6048
6120
|
const result = await client.execute({
|
|
6049
|
-
sql:
|
|
6050
|
-
|
|
6121
|
+
sql: `SELECT signature_hash, COUNT(*) as cnt
|
|
6122
|
+
FROM trajectories
|
|
6123
|
+
WHERE skill_id IS NULL
|
|
6124
|
+
GROUP BY signature_hash
|
|
6125
|
+
HAVING cnt >= ?
|
|
6126
|
+
ORDER BY cnt DESC
|
|
6127
|
+
LIMIT 10`,
|
|
6128
|
+
args: [t]
|
|
6051
6129
|
});
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
6081
|
-
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
6082
|
-
"4. Any regressions in the test suite?"
|
|
6083
|
-
]
|
|
6084
|
-
};
|
|
6085
|
-
}
|
|
6086
|
-
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
6087
|
-
return {
|
|
6088
|
-
lens: "Architecture (CTO)",
|
|
6089
|
-
checklist: [
|
|
6090
|
-
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
6091
|
-
"2. Is it backward compatible? Any breaking changes?",
|
|
6092
|
-
"3. Does it introduce technical debt? Is that debt justified?",
|
|
6093
|
-
"4. Security implications? Any new attack surface?",
|
|
6094
|
-
"5. Does it scale? Performance considerations?",
|
|
6095
|
-
"6. Coordination: does this affect other employees' work or other projects?"
|
|
6096
|
-
]
|
|
6097
|
-
};
|
|
6098
|
-
}
|
|
6099
|
-
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
6100
|
-
return {
|
|
6101
|
-
lens: "Strategic (COO)",
|
|
6102
|
-
checklist: [
|
|
6103
|
-
"1. Does this serve the project mission?",
|
|
6104
|
-
"2. Is this the right work at the right time?",
|
|
6105
|
-
"3. Does the architectural assessment make sense for the business?",
|
|
6106
|
-
"4. Any cross-project implications?"
|
|
6107
|
-
]
|
|
6108
|
-
};
|
|
6109
|
-
}
|
|
6110
|
-
return {
|
|
6111
|
-
lens: "General",
|
|
6112
|
-
checklist: [
|
|
6113
|
-
"1. Read the original task's acceptance criteria",
|
|
6114
|
-
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
6115
|
-
"3. Verify code changes match requirements",
|
|
6116
|
-
"4. Check if tests were added/updated",
|
|
6117
|
-
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
6118
|
-
]
|
|
6119
|
-
};
|
|
6120
|
-
}
|
|
6121
|
-
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
6122
|
-
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
6123
|
-
try {
|
|
6124
|
-
const client = getClient();
|
|
6125
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6126
|
-
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
6127
|
-
if (parentId) {
|
|
6128
|
-
const result = await client.execute({
|
|
6129
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
6130
|
-
args: [now, parentId]
|
|
6131
|
-
});
|
|
6132
|
-
if (result.rowsAffected > 0) {
|
|
6133
|
-
process.stderr.write(
|
|
6134
|
-
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
6135
|
-
`
|
|
6136
|
-
);
|
|
6137
|
-
}
|
|
6138
|
-
} else {
|
|
6139
|
-
const fileName = taskFile.split("/").pop() ?? "";
|
|
6140
|
-
const reviewPrefix = fileName.replace(".md", "");
|
|
6141
|
-
const parts = reviewPrefix.split("-");
|
|
6142
|
-
if (parts.length >= 3 && parts[0] === "review") {
|
|
6143
|
-
const agent = parts[1];
|
|
6144
|
-
const slug = parts.slice(2).join("-");
|
|
6145
|
-
const originalTaskFile = `exe/${agent}/${slug}.md`;
|
|
6146
|
-
const result = await client.execute({
|
|
6147
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
|
|
6148
|
-
args: [now, originalTaskFile]
|
|
6149
|
-
});
|
|
6150
|
-
if (result.rowsAffected > 0) {
|
|
6151
|
-
process.stderr.write(
|
|
6152
|
-
`[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
|
|
6153
|
-
`
|
|
6154
|
-
);
|
|
6155
|
-
}
|
|
6156
|
-
}
|
|
6130
|
+
let clustersProcessed = 0;
|
|
6131
|
+
let skillsExtracted = 0;
|
|
6132
|
+
for (const row of result.rows) {
|
|
6133
|
+
const hash = String(row.signature_hash);
|
|
6134
|
+
const trajResult = await client.execute({
|
|
6135
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
|
|
6136
|
+
FROM trajectories
|
|
6137
|
+
WHERE signature_hash = ? AND skill_id IS NULL
|
|
6138
|
+
ORDER BY created_at DESC
|
|
6139
|
+
LIMIT 10`,
|
|
6140
|
+
args: [hash]
|
|
6141
|
+
});
|
|
6142
|
+
const trajectories = trajResult.rows.map((r) => ({
|
|
6143
|
+
id: String(r.id),
|
|
6144
|
+
taskId: String(r.task_id),
|
|
6145
|
+
agentId: String(r.agent_id),
|
|
6146
|
+
projectName: String(r.project_name),
|
|
6147
|
+
taskTitle: String(r.task_title),
|
|
6148
|
+
signature: JSON.parse(String(r.signature)),
|
|
6149
|
+
signatureHash: String(r.signature_hash),
|
|
6150
|
+
toolCount: Number(r.tool_count),
|
|
6151
|
+
skillId: null,
|
|
6152
|
+
createdAt: String(r.created_at)
|
|
6153
|
+
}));
|
|
6154
|
+
if (trajectories.length >= t) {
|
|
6155
|
+
clustersProcessed++;
|
|
6156
|
+
const skillId = await extractSkill(trajectories, model ?? config2.skillModel);
|
|
6157
|
+
if (skillId) skillsExtracted++;
|
|
6157
6158
|
}
|
|
6158
|
-
} catch (err) {
|
|
6159
|
-
process.stderr.write(
|
|
6160
|
-
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
6161
|
-
`
|
|
6162
|
-
);
|
|
6163
6159
|
}
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6160
|
+
return { clustersProcessed, skillsExtracted };
|
|
6161
|
+
}
|
|
6162
|
+
function editDistance(a, b) {
|
|
6163
|
+
const m = a.length;
|
|
6164
|
+
const n = b.length;
|
|
6165
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
6166
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
6167
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
6168
|
+
for (let i = 1; i <= m; i++) {
|
|
6169
|
+
for (let j = 1; j <= n; j++) {
|
|
6170
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
6171
|
+
dp[i][j] = Math.min(
|
|
6172
|
+
dp[i - 1][j] + 1,
|
|
6173
|
+
dp[i][j - 1] + 1,
|
|
6174
|
+
dp[i - 1][j - 1] + cost
|
|
6175
|
+
);
|
|
6172
6176
|
}
|
|
6173
|
-
} catch {
|
|
6174
6177
|
}
|
|
6178
|
+
return dp[m][n];
|
|
6175
6179
|
}
|
|
6176
|
-
var
|
|
6177
|
-
|
|
6180
|
+
var DEFAULT_SKILL_THRESHOLD;
|
|
6181
|
+
var init_skill_learning = __esm({
|
|
6182
|
+
"src/lib/skill-learning.ts"() {
|
|
6178
6183
|
"use strict";
|
|
6179
6184
|
init_database();
|
|
6185
|
+
init_behaviors();
|
|
6180
6186
|
init_config();
|
|
6181
|
-
|
|
6182
|
-
init_notifications();
|
|
6183
|
-
init_tasks_crud();
|
|
6184
|
-
init_tmux_routing();
|
|
6185
|
-
init_session_key();
|
|
6187
|
+
DEFAULT_SKILL_THRESHOLD = 3;
|
|
6186
6188
|
}
|
|
6187
6189
|
});
|
|
6188
6190
|
|
|
6189
|
-
// src/lib/tasks
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6191
|
+
// src/lib/tasks.ts
|
|
6192
|
+
var tasks_exports = {};
|
|
6193
|
+
__export(tasks_exports, {
|
|
6194
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
6195
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
6196
|
+
countPendingReviews: () => countPendingReviews,
|
|
6197
|
+
createTask: () => createTask,
|
|
6198
|
+
createTaskCore: () => createTaskCore,
|
|
6199
|
+
deleteTask: () => deleteTask,
|
|
6200
|
+
deleteTaskCore: () => deleteTaskCore,
|
|
6201
|
+
ensureArchitectureDoc: () => ensureArchitectureDoc,
|
|
6202
|
+
ensureGitignoreExe: () => ensureGitignoreExe,
|
|
6203
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
6204
|
+
listPendingReviews: () => listPendingReviews,
|
|
6205
|
+
listTasks: () => listTasks,
|
|
6206
|
+
resolveTask: () => resolveTask,
|
|
6207
|
+
slugify: () => slugify,
|
|
6208
|
+
updateTask: () => updateTask,
|
|
6209
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
6210
|
+
writeCheckpoint: () => writeCheckpoint
|
|
6211
|
+
});
|
|
6212
|
+
import path16 from "path";
|
|
6213
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
|
|
6214
|
+
async function createTask(input) {
|
|
6215
|
+
const result = await createTaskCore(input);
|
|
6216
|
+
if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
|
|
6217
|
+
dispatchTaskToEmployee({
|
|
6218
|
+
assignedTo: input.assignedTo,
|
|
6219
|
+
title: input.title,
|
|
6220
|
+
priority: input.priority,
|
|
6221
|
+
taskFile: result.taskFile,
|
|
6222
|
+
initialStatus: result.status,
|
|
6223
|
+
projectName: input.projectName
|
|
6203
6224
|
});
|
|
6204
|
-
|
|
6225
|
+
}
|
|
6226
|
+
return result;
|
|
6227
|
+
}
|
|
6228
|
+
async function updateTask(input) {
|
|
6229
|
+
const { row, taskFile, now, taskId } = await updateTaskStatus(input);
|
|
6230
|
+
try {
|
|
6231
|
+
const agent = String(row.assigned_to);
|
|
6232
|
+
const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
|
|
6233
|
+
const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
|
|
6234
|
+
if (input.status === "in_progress") {
|
|
6235
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
6236
|
+
writeFileSync4(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
6237
|
+
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
6205
6238
|
try {
|
|
6206
|
-
|
|
6207
|
-
let ubContent = await readFile4(ubFile, "utf-8");
|
|
6208
|
-
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
6209
|
-
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
6210
|
-
await writeFile5(ubFile, ubContent, "utf-8");
|
|
6239
|
+
unlinkSync4(cachePath);
|
|
6211
6240
|
} catch {
|
|
6212
6241
|
}
|
|
6213
6242
|
}
|
|
6243
|
+
} catch {
|
|
6214
6244
|
}
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
const client = getClient();
|
|
6218
|
-
const nextResult = await client.execute({
|
|
6219
|
-
sql: `SELECT title, task_file, priority FROM tasks
|
|
6220
|
-
WHERE assigned_to = ? AND status = 'open'
|
|
6221
|
-
ORDER BY priority ASC, created_at ASC
|
|
6222
|
-
LIMIT 1`,
|
|
6223
|
-
args: [assignedTo]
|
|
6224
|
-
});
|
|
6225
|
-
if (nextResult.rows.length === 1) {
|
|
6226
|
-
const nr = nextResult.rows[0];
|
|
6227
|
-
return {
|
|
6228
|
-
title: String(nr.title),
|
|
6229
|
-
priority: String(nr.priority),
|
|
6230
|
-
taskFile: String(nr.task_file)
|
|
6231
|
-
};
|
|
6245
|
+
if (input.status === "done") {
|
|
6246
|
+
await cleanupReviewFile(row, taskFile, input.baseDir);
|
|
6232
6247
|
}
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
const cnt = Number(remaining.rows[0]?.cnt ?? 1);
|
|
6243
|
-
if (cnt === 0) {
|
|
6244
|
-
const parentRow = await client.execute({
|
|
6245
|
-
sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
|
|
6246
|
-
args: [parentTaskId]
|
|
6247
|
-
});
|
|
6248
|
-
if (parentRow.rows.length === 1) {
|
|
6249
|
-
const pr = parentRow.rows[0];
|
|
6250
|
-
const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
|
|
6251
|
-
await writeNotification({
|
|
6252
|
-
agentId: String(pr.assigned_to),
|
|
6253
|
-
agentRole: "system",
|
|
6254
|
-
event: "subtasks_complete",
|
|
6255
|
-
project: parentProject,
|
|
6256
|
-
summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
|
|
6257
|
-
taskFile: String(pr.task_file)
|
|
6248
|
+
if (input.status === "done" || input.status === "cancelled") {
|
|
6249
|
+
try {
|
|
6250
|
+
const client = getClient();
|
|
6251
|
+
const taskTitle = String(row.title);
|
|
6252
|
+
const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
6253
|
+
await client.execute({
|
|
6254
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
6255
|
+
WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
|
|
6256
|
+
args: [now, `%left '${escaped}' as in\\_progress%`]
|
|
6258
6257
|
});
|
|
6258
|
+
} catch {
|
|
6259
|
+
}
|
|
6260
|
+
try {
|
|
6261
|
+
const client = getClient();
|
|
6262
|
+
const cascaded = await client.execute({
|
|
6263
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
6264
|
+
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
6265
|
+
args: [now, taskId]
|
|
6266
|
+
});
|
|
6267
|
+
if (cascaded.rowsAffected > 0) {
|
|
6268
|
+
process.stderr.write(
|
|
6269
|
+
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
6270
|
+
`
|
|
6271
|
+
);
|
|
6272
|
+
}
|
|
6273
|
+
} catch {
|
|
6259
6274
|
}
|
|
6260
6275
|
}
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6276
|
+
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
6277
|
+
if (isTerminal) {
|
|
6278
|
+
const isExe = String(row.assigned_to) === "exe";
|
|
6279
|
+
if (!isExe) {
|
|
6280
|
+
notifyTaskDone();
|
|
6281
|
+
}
|
|
6282
|
+
await markTaskNotificationsRead(taskFile);
|
|
6283
|
+
if (input.status === "done") {
|
|
6284
|
+
try {
|
|
6285
|
+
await cascadeUnblock(taskId, input.baseDir, now);
|
|
6286
|
+
} catch {
|
|
6287
|
+
}
|
|
6288
|
+
orgBus.emit({
|
|
6289
|
+
type: "task_completed",
|
|
6290
|
+
taskId,
|
|
6291
|
+
employee: String(row.assigned_to),
|
|
6292
|
+
result: input.result ?? "",
|
|
6293
|
+
timestamp: now
|
|
6294
|
+
});
|
|
6295
|
+
if (row.parent_task_id) {
|
|
6296
|
+
try {
|
|
6297
|
+
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
6298
|
+
} catch {
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6267
6302
|
}
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6303
|
+
if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
|
|
6304
|
+
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
6305
|
+
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
6306
|
+
taskId,
|
|
6307
|
+
agentId: String(row.assigned_to),
|
|
6308
|
+
projectName: String(row.project_name),
|
|
6309
|
+
taskTitle: String(row.title)
|
|
6310
|
+
})
|
|
6311
|
+
).catch((err) => {
|
|
6312
|
+
process.stderr.write(
|
|
6313
|
+
`[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
|
|
6314
|
+
`
|
|
6315
|
+
);
|
|
6316
|
+
});
|
|
6317
|
+
}
|
|
6318
|
+
let nextTask;
|
|
6319
|
+
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
6278
6320
|
try {
|
|
6279
|
-
|
|
6280
|
-
cwd: dir,
|
|
6281
|
-
encoding: "utf8",
|
|
6282
|
-
timeout: 2e3,
|
|
6283
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
6284
|
-
}).trim();
|
|
6285
|
-
repoRoot = path16.dirname(gitCommonDir);
|
|
6321
|
+
nextTask = await findNextTask(String(row.assigned_to));
|
|
6286
6322
|
} catch {
|
|
6287
|
-
repoRoot = execSync6("git rev-parse --show-toplevel", {
|
|
6288
|
-
cwd: dir,
|
|
6289
|
-
encoding: "utf8",
|
|
6290
|
-
timeout: 2e3,
|
|
6291
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
6292
|
-
}).trim();
|
|
6293
6323
|
}
|
|
6294
|
-
_cached2 = path16.basename(repoRoot);
|
|
6295
|
-
_cachedCwd = dir;
|
|
6296
|
-
return _cached2;
|
|
6297
|
-
} catch {
|
|
6298
|
-
_cached2 = path16.basename(dir);
|
|
6299
|
-
_cachedCwd = dir;
|
|
6300
|
-
return _cached2;
|
|
6301
6324
|
}
|
|
6325
|
+
return {
|
|
6326
|
+
id: String(row.id),
|
|
6327
|
+
title: String(row.title),
|
|
6328
|
+
assignedTo: String(row.assigned_to),
|
|
6329
|
+
assignedBy: String(row.assigned_by),
|
|
6330
|
+
projectName: String(row.project_name),
|
|
6331
|
+
priority: String(row.priority),
|
|
6332
|
+
status: input.status,
|
|
6333
|
+
taskFile,
|
|
6334
|
+
createdAt: String(row.created_at),
|
|
6335
|
+
updatedAt: now,
|
|
6336
|
+
budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
|
|
6337
|
+
budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
|
|
6338
|
+
tokensUsed: Number(row.tokens_used ?? 0),
|
|
6339
|
+
tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
|
|
6340
|
+
nextTask
|
|
6341
|
+
};
|
|
6302
6342
|
}
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6343
|
+
async function deleteTask(taskId, baseDir) {
|
|
6344
|
+
const client = getClient();
|
|
6345
|
+
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
6346
|
+
const reviewer = assignedBy || "exe";
|
|
6347
|
+
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
6348
|
+
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
6349
|
+
await client.execute({
|
|
6350
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
|
|
6351
|
+
args: [reviewFile, `exe/exe/${reviewSlug}.md`]
|
|
6352
|
+
});
|
|
6353
|
+
await markAsReadByTaskFile(taskFile);
|
|
6354
|
+
await markAsReadByTaskFile(reviewFile);
|
|
6355
|
+
}
|
|
6356
|
+
var init_tasks = __esm({
|
|
6357
|
+
"src/lib/tasks.ts"() {
|
|
6306
6358
|
"use strict";
|
|
6307
|
-
|
|
6308
|
-
|
|
6359
|
+
init_database();
|
|
6360
|
+
init_config();
|
|
6361
|
+
init_notifications();
|
|
6362
|
+
init_state_bus();
|
|
6363
|
+
init_tasks_crud();
|
|
6364
|
+
init_tasks_review();
|
|
6365
|
+
init_tasks_crud();
|
|
6366
|
+
init_tasks_chain();
|
|
6367
|
+
init_tasks_review();
|
|
6368
|
+
init_tasks_notify();
|
|
6309
6369
|
}
|
|
6310
6370
|
});
|
|
6311
6371
|
|
|
6312
|
-
// src/lib/
|
|
6313
|
-
var
|
|
6314
|
-
__export(
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6372
|
+
// src/lib/capacity-monitor.ts
|
|
6373
|
+
var capacity_monitor_exports = {};
|
|
6374
|
+
__export(capacity_monitor_exports, {
|
|
6375
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
6376
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
6377
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
6378
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
6379
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
6380
|
+
extractContextPercent: () => extractContextPercent,
|
|
6381
|
+
isAtCapacity: () => isAtCapacity,
|
|
6382
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
6383
|
+
pollCapacityDead: () => pollCapacityDead
|
|
6318
6384
|
});
|
|
6319
|
-
function
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
const
|
|
6324
|
-
|
|
6385
|
+
function resumeTaskTitle(agentId) {
|
|
6386
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
6387
|
+
}
|
|
6388
|
+
function buildResumeContext(agentId, openTasks) {
|
|
6389
|
+
const taskList = openTasks.map(
|
|
6390
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
6391
|
+
).join("\n");
|
|
6392
|
+
return [
|
|
6393
|
+
"## Context",
|
|
6394
|
+
"",
|
|
6395
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
6396
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
6397
|
+
"",
|
|
6398
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
6399
|
+
"",
|
|
6400
|
+
taskList,
|
|
6401
|
+
"",
|
|
6402
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
6403
|
+
].join("\n");
|
|
6404
|
+
}
|
|
6405
|
+
function filterPaneContent(paneOutput) {
|
|
6406
|
+
return paneOutput.split("\n").filter((line) => {
|
|
6407
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
6408
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
6409
|
+
if (line.includes(marker)) return false;
|
|
6410
|
+
}
|
|
6411
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
6412
|
+
if (re.test(line)) return false;
|
|
6413
|
+
}
|
|
6414
|
+
return true;
|
|
6415
|
+
}).join("\n");
|
|
6416
|
+
}
|
|
6417
|
+
function extractContextPercent(paneOutput) {
|
|
6418
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
6419
|
+
if (!match) return null;
|
|
6420
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
6421
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
6422
|
+
}
|
|
6423
|
+
function isAtCapacity(paneOutput) {
|
|
6424
|
+
const filtered = filterPaneContent(paneOutput);
|
|
6425
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
6426
|
+
}
|
|
6427
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
6428
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
6429
|
+
if (pendingSince === void 0) {
|
|
6430
|
+
_pendingCapacityKill.set(agentId, now);
|
|
6431
|
+
return false;
|
|
6432
|
+
}
|
|
6433
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
6434
|
+
_pendingCapacityKill.set(agentId, now);
|
|
6435
|
+
return false;
|
|
6436
|
+
}
|
|
6437
|
+
_pendingCapacityKill.delete(agentId);
|
|
6438
|
+
return true;
|
|
6325
6439
|
}
|
|
6326
|
-
function
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6440
|
+
function _resetPendingCapacityKills() {
|
|
6441
|
+
_pendingCapacityKill.clear();
|
|
6442
|
+
}
|
|
6443
|
+
function _resetLastRelaunchCache() {
|
|
6444
|
+
_lastRelaunch.clear();
|
|
6445
|
+
}
|
|
6446
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
6447
|
+
const client = getClient();
|
|
6448
|
+
const result = await client.execute({
|
|
6449
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
6450
|
+
FROM tasks
|
|
6451
|
+
WHERE assigned_to = ? AND title LIKE ?`,
|
|
6452
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`]
|
|
6453
|
+
});
|
|
6454
|
+
const raw = result.rows[0]?.last_created_at;
|
|
6455
|
+
if (raw === null || raw === void 0) return null;
|
|
6456
|
+
const parsed = Date.parse(String(raw));
|
|
6457
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
6458
|
+
}
|
|
6459
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
6460
|
+
const cached = _lastRelaunch.get(agentId);
|
|
6461
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
6462
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
6463
|
+
if (persisted === null) return false;
|
|
6464
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
6465
|
+
_lastRelaunch.set(agentId, persisted);
|
|
6466
|
+
return true;
|
|
6467
|
+
}
|
|
6468
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
6469
|
+
const client = getClient();
|
|
6470
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6471
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
6472
|
+
const existing = await client.execute({
|
|
6473
|
+
sql: `SELECT id FROM tasks
|
|
6474
|
+
WHERE assigned_to = ?
|
|
6475
|
+
AND title LIKE ?
|
|
6476
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
|
|
6477
|
+
ORDER BY created_at DESC
|
|
6478
|
+
LIMIT 1`,
|
|
6479
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
6480
|
+
});
|
|
6481
|
+
if (existing.rows.length > 0) {
|
|
6482
|
+
const taskId = String(existing.rows[0].id);
|
|
6483
|
+
await client.execute({
|
|
6484
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
6485
|
+
args: [context, now, taskId]
|
|
6486
|
+
});
|
|
6487
|
+
return { created: false, taskId };
|
|
6331
6488
|
}
|
|
6332
|
-
|
|
6489
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
6490
|
+
const task = await createTask2({
|
|
6491
|
+
title: resumeTaskTitle(agentId),
|
|
6492
|
+
assignedTo: agentId,
|
|
6493
|
+
assignedBy: "system",
|
|
6494
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
6495
|
+
priority: "p0",
|
|
6496
|
+
context,
|
|
6497
|
+
baseDir: projectDir
|
|
6498
|
+
});
|
|
6499
|
+
return { created: true, taskId: task.id };
|
|
6333
6500
|
}
|
|
6334
|
-
function
|
|
6501
|
+
async function pollCapacityDead() {
|
|
6502
|
+
const transport = getTransport();
|
|
6503
|
+
const relaunched = [];
|
|
6504
|
+
const registered = listSessions().filter(
|
|
6505
|
+
(s) => s.agentId !== "exe"
|
|
6506
|
+
);
|
|
6507
|
+
if (registered.length === 0) return [];
|
|
6508
|
+
let liveSessions;
|
|
6335
6509
|
try {
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6510
|
+
liveSessions = transport.listSessions();
|
|
6511
|
+
} catch {
|
|
6512
|
+
return [];
|
|
6513
|
+
}
|
|
6514
|
+
for (const entry of registered) {
|
|
6515
|
+
const { windowName, agentId, projectDir } = entry;
|
|
6516
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
6517
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
6518
|
+
let pane;
|
|
6519
|
+
try {
|
|
6520
|
+
pane = transport.capturePane(windowName, 15);
|
|
6521
|
+
} catch {
|
|
6522
|
+
continue;
|
|
6340
6523
|
}
|
|
6341
|
-
if (
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6524
|
+
if (!isAtCapacity(pane)) continue;
|
|
6525
|
+
const ctxPct = extractContextPercent(pane);
|
|
6526
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
6527
|
+
process.stderr.write(
|
|
6528
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
6529
|
+
`
|
|
6530
|
+
);
|
|
6531
|
+
continue;
|
|
6532
|
+
}
|
|
6533
|
+
if (!confirmCapacityKill(agentId)) {
|
|
6534
|
+
process.stderr.write(
|
|
6535
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
6536
|
+
`
|
|
6537
|
+
);
|
|
6538
|
+
continue;
|
|
6539
|
+
}
|
|
6540
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
6541
|
+
if (!verify.atCapacity) {
|
|
6542
|
+
process.stderr.write(
|
|
6543
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
6544
|
+
`
|
|
6545
|
+
);
|
|
6546
|
+
void recordSessionKill({
|
|
6547
|
+
sessionName: windowName,
|
|
6548
|
+
agentId,
|
|
6549
|
+
reason: "capacity_false_positive_blocked"
|
|
6550
|
+
});
|
|
6551
|
+
continue;
|
|
6348
6552
|
}
|
|
6349
6553
|
process.stderr.write(
|
|
6350
|
-
`[
|
|
6554
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
6351
6555
|
`
|
|
6352
6556
|
);
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6557
|
+
try {
|
|
6558
|
+
transport.kill(windowName);
|
|
6559
|
+
void recordSessionKill({
|
|
6560
|
+
sessionName: windowName,
|
|
6561
|
+
agentId,
|
|
6562
|
+
reason: "capacity"
|
|
6563
|
+
});
|
|
6564
|
+
const client = getClient();
|
|
6565
|
+
const openTasks = await client.execute({
|
|
6566
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
6567
|
+
FROM tasks
|
|
6568
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')
|
|
6569
|
+
ORDER BY
|
|
6570
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
6571
|
+
created_at ASC
|
|
6572
|
+
LIMIT 10`,
|
|
6573
|
+
args: [agentId]
|
|
6574
|
+
});
|
|
6575
|
+
if (openTasks.rows.length === 0) {
|
|
6576
|
+
process.stderr.write(
|
|
6577
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
6578
|
+
`
|
|
6579
|
+
);
|
|
6580
|
+
continue;
|
|
6581
|
+
}
|
|
6582
|
+
const { created } = await createOrRefreshResumeTask(
|
|
6583
|
+
agentId,
|
|
6584
|
+
projectDir,
|
|
6585
|
+
openTasks.rows
|
|
6586
|
+
);
|
|
6587
|
+
if (created) {
|
|
6588
|
+
await writeNotification({
|
|
6589
|
+
agentId: "system",
|
|
6590
|
+
agentRole: "daemon",
|
|
6591
|
+
event: "capacity_relaunch",
|
|
6592
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
6593
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
6594
|
+
});
|
|
6595
|
+
}
|
|
6596
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
6597
|
+
if (created) relaunched.push(agentId);
|
|
6598
|
+
} catch (err) {
|
|
6599
|
+
process.stderr.write(
|
|
6600
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
6601
|
+
`
|
|
6602
|
+
);
|
|
6603
|
+
}
|
|
6363
6604
|
}
|
|
6605
|
+
return relaunched;
|
|
6364
6606
|
}
|
|
6365
|
-
var
|
|
6366
|
-
|
|
6607
|
+
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;
|
|
6608
|
+
var init_capacity_monitor = __esm({
|
|
6609
|
+
"src/lib/capacity-monitor.ts"() {
|
|
6367
6610
|
"use strict";
|
|
6368
6611
|
init_session_registry();
|
|
6369
|
-
|
|
6612
|
+
init_transport();
|
|
6613
|
+
init_notifications();
|
|
6614
|
+
init_database();
|
|
6615
|
+
init_session_kill_telemetry();
|
|
6370
6616
|
init_tmux_routing();
|
|
6617
|
+
CAPACITY_PATTERNS = [
|
|
6618
|
+
/conversation is too long/i,
|
|
6619
|
+
/maximum context length/i,
|
|
6620
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
6621
|
+
/reached.*(?:token|context).*limit/i
|
|
6622
|
+
];
|
|
6623
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
6624
|
+
CONTENT_LINE_MARKERS = [
|
|
6625
|
+
"RESUME:",
|
|
6626
|
+
"intercom",
|
|
6627
|
+
"capacity-monitor",
|
|
6628
|
+
"CAPACITY_PATTERNS",
|
|
6629
|
+
"isAtCapacity",
|
|
6630
|
+
"CONTENT_LINE_MARKERS",
|
|
6631
|
+
"pollCapacityDead",
|
|
6632
|
+
"confirmCapacityKill",
|
|
6633
|
+
"session_kills",
|
|
6634
|
+
"capacity-monitor.test"
|
|
6635
|
+
];
|
|
6636
|
+
SOURCE_CODE_MARKERS = [
|
|
6637
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
6638
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
6639
|
+
];
|
|
6640
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
6641
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
6642
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
6643
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
6644
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
6645
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
6646
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
6647
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
6648
|
+
CTX_FLOOR_PERCENT = 50;
|
|
6371
6649
|
}
|
|
6372
6650
|
});
|
|
6373
6651
|
|
|
6374
|
-
// src/lib/
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6652
|
+
// src/lib/tmux-routing.ts
|
|
6653
|
+
var tmux_routing_exports = {};
|
|
6654
|
+
__export(tmux_routing_exports, {
|
|
6655
|
+
acquireSpawnLock: () => acquireSpawnLock2,
|
|
6656
|
+
employeeSessionName: () => employeeSessionName,
|
|
6657
|
+
ensureEmployee: () => ensureEmployee,
|
|
6658
|
+
extractRootExe: () => extractRootExe,
|
|
6659
|
+
findFreeInstance: () => findFreeInstance,
|
|
6660
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
6661
|
+
getMySession: () => getMySession,
|
|
6662
|
+
getParentExe: () => getParentExe,
|
|
6663
|
+
getSessionState: () => getSessionState,
|
|
6664
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
6665
|
+
isExeSession: () => isExeSession,
|
|
6666
|
+
isSessionBusy: () => isSessionBusy,
|
|
6667
|
+
notifyParentExe: () => notifyParentExe,
|
|
6668
|
+
parseParentExe: () => parseParentExe,
|
|
6669
|
+
registerParentExe: () => registerParentExe,
|
|
6670
|
+
releaseSpawnLock: () => releaseSpawnLock2,
|
|
6671
|
+
resolveExeSession: () => resolveExeSession,
|
|
6672
|
+
sendIntercom: () => sendIntercom,
|
|
6673
|
+
spawnEmployee: () => spawnEmployee,
|
|
6674
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
6675
|
+
});
|
|
6676
|
+
import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
|
|
6677
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
|
|
6678
|
+
import path17 from "path";
|
|
6679
|
+
import os7 from "os";
|
|
6680
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6681
|
+
import { unlinkSync as unlinkSync5 } from "fs";
|
|
6682
|
+
function spawnLockPath(sessionName) {
|
|
6683
|
+
return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
6684
|
+
}
|
|
6685
|
+
function isProcessAlive(pid) {
|
|
6686
|
+
try {
|
|
6687
|
+
process.kill(pid, 0);
|
|
6688
|
+
return true;
|
|
6689
|
+
} catch {
|
|
6690
|
+
return false;
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
function acquireSpawnLock2(sessionName) {
|
|
6694
|
+
if (!existsSync13(SPAWN_LOCK_DIR)) {
|
|
6695
|
+
mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
|
|
6696
|
+
}
|
|
6697
|
+
const lockFile = spawnLockPath(sessionName);
|
|
6698
|
+
if (existsSync13(lockFile)) {
|
|
6379
6699
|
try {
|
|
6380
|
-
const
|
|
6381
|
-
const
|
|
6382
|
-
if (
|
|
6383
|
-
|
|
6700
|
+
const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
|
|
6701
|
+
const age = Date.now() - lock.timestamp;
|
|
6702
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
6703
|
+
return false;
|
|
6384
6704
|
}
|
|
6385
6705
|
} catch {
|
|
6386
6706
|
}
|
|
6387
6707
|
}
|
|
6708
|
+
writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
6709
|
+
return true;
|
|
6710
|
+
}
|
|
6711
|
+
function releaseSpawnLock2(sessionName) {
|
|
6388
6712
|
try {
|
|
6389
|
-
|
|
6390
|
-
const exeSession = resolveExeSession();
|
|
6391
|
-
if (!exeSession) return { dispatched: "session_missing" };
|
|
6392
|
-
const sessionName = employeeSessionName(input.assignedTo, exeSession);
|
|
6393
|
-
if (transport.isAlive(sessionName)) {
|
|
6394
|
-
const result = sendIntercom(sessionName);
|
|
6395
|
-
const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
|
|
6396
|
-
return { dispatched, session: sessionName, crossProject };
|
|
6397
|
-
} else {
|
|
6398
|
-
const projectDir = input.projectDir ?? process.cwd();
|
|
6399
|
-
const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
|
|
6400
|
-
autoInstance: isMultiInstance(input.assignedTo)
|
|
6401
|
-
});
|
|
6402
|
-
if (result.status === "failed") {
|
|
6403
|
-
process.stderr.write(
|
|
6404
|
-
`[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
|
|
6405
|
-
`
|
|
6406
|
-
);
|
|
6407
|
-
return { dispatched: "session_missing" };
|
|
6408
|
-
}
|
|
6409
|
-
return { dispatched: "spawned", session: result.sessionName, crossProject };
|
|
6410
|
-
}
|
|
6713
|
+
unlinkSync5(spawnLockPath(sessionName));
|
|
6411
6714
|
} catch {
|
|
6412
|
-
return { dispatched: "session_missing" };
|
|
6413
6715
|
}
|
|
6414
6716
|
}
|
|
6415
|
-
function
|
|
6717
|
+
function resolveBehaviorsExporterScript() {
|
|
6416
6718
|
try {
|
|
6417
|
-
const
|
|
6418
|
-
|
|
6719
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
6720
|
+
const scriptPath = path17.join(
|
|
6721
|
+
path17.dirname(thisFile),
|
|
6722
|
+
"..",
|
|
6723
|
+
"bin",
|
|
6724
|
+
"exe-export-behaviors.js"
|
|
6725
|
+
);
|
|
6726
|
+
return existsSync13(scriptPath) ? scriptPath : null;
|
|
6419
6727
|
} catch {
|
|
6728
|
+
return null;
|
|
6420
6729
|
}
|
|
6421
6730
|
}
|
|
6422
|
-
|
|
6731
|
+
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
6732
|
+
const script = resolveBehaviorsExporterScript();
|
|
6733
|
+
if (!script) return null;
|
|
6423
6734
|
try {
|
|
6424
|
-
|
|
6425
|
-
|
|
6735
|
+
const output = execFileSync2(
|
|
6736
|
+
process.execPath,
|
|
6737
|
+
[script, agentId, projectName, sessionKey],
|
|
6738
|
+
{ encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
|
|
6739
|
+
).trim();
|
|
6740
|
+
return output.length > 0 ? output : null;
|
|
6741
|
+
} catch (err) {
|
|
6742
|
+
process.stderr.write(
|
|
6743
|
+
`[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
6744
|
+
`
|
|
6745
|
+
);
|
|
6746
|
+
return null;
|
|
6426
6747
|
}
|
|
6427
6748
|
}
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
"use strict";
|
|
6431
|
-
init_tmux_routing();
|
|
6432
|
-
init_session_key();
|
|
6433
|
-
init_notifications();
|
|
6434
|
-
init_transport();
|
|
6435
|
-
init_employees();
|
|
6436
|
-
}
|
|
6437
|
-
});
|
|
6438
|
-
|
|
6439
|
-
// src/lib/behaviors.ts
|
|
6440
|
-
import crypto6 from "crypto";
|
|
6441
|
-
async function storeBehavior(opts) {
|
|
6442
|
-
const client = getClient();
|
|
6443
|
-
const id = crypto6.randomUUID();
|
|
6444
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6445
|
-
await client.execute({
|
|
6446
|
-
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
6447
|
-
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
6448
|
-
args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
|
|
6449
|
-
});
|
|
6450
|
-
return id;
|
|
6749
|
+
function getMySession() {
|
|
6750
|
+
return getTransport().getMySession();
|
|
6451
6751
|
}
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
}
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
extractSkill: () => extractSkill,
|
|
6466
|
-
extractTrajectory: () => extractTrajectory,
|
|
6467
|
-
findSimilarTrajectories: () => findSimilarTrajectories,
|
|
6468
|
-
hashSignature: () => hashSignature,
|
|
6469
|
-
storeTrajectory: () => storeTrajectory,
|
|
6470
|
-
sweepTrajectories: () => sweepTrajectories
|
|
6471
|
-
});
|
|
6472
|
-
import crypto7 from "crypto";
|
|
6473
|
-
async function extractTrajectory(taskId, agentId) {
|
|
6474
|
-
const client = getClient();
|
|
6475
|
-
const result = await client.execute({
|
|
6476
|
-
sql: `SELECT tool_name, raw_text
|
|
6477
|
-
FROM memories
|
|
6478
|
-
WHERE task_id = ? AND agent_id = ?
|
|
6479
|
-
ORDER BY timestamp ASC`,
|
|
6480
|
-
args: [taskId, agentId]
|
|
6481
|
-
});
|
|
6482
|
-
if (result.rows.length === 0) return [];
|
|
6483
|
-
const rawTools = result.rows.map((r) => {
|
|
6484
|
-
const toolName = String(r.tool_name);
|
|
6485
|
-
if (toolName === "Bash") {
|
|
6486
|
-
const text = String(r.raw_text);
|
|
6487
|
-
const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
|
|
6488
|
-
return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
|
|
6489
|
-
}
|
|
6490
|
-
return toolName;
|
|
6491
|
-
});
|
|
6492
|
-
const signature = [];
|
|
6493
|
-
for (const tool of rawTools) {
|
|
6494
|
-
if (signature.length === 0 || signature[signature.length - 1] !== tool) {
|
|
6495
|
-
signature.push(tool);
|
|
6752
|
+
function employeeSessionName(employee, exeSession, instance) {
|
|
6753
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
6754
|
+
const root = extractRootExe(exeSession);
|
|
6755
|
+
if (root) {
|
|
6756
|
+
process.stderr.write(
|
|
6757
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
|
|
6758
|
+
`
|
|
6759
|
+
);
|
|
6760
|
+
exeSession = root;
|
|
6761
|
+
} else {
|
|
6762
|
+
throw new Error(
|
|
6763
|
+
`Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
|
|
6764
|
+
);
|
|
6496
6765
|
}
|
|
6497
6766
|
}
|
|
6498
|
-
|
|
6767
|
+
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
6768
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
6769
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
6770
|
+
throw new Error(
|
|
6771
|
+
`Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
|
|
6772
|
+
);
|
|
6773
|
+
}
|
|
6774
|
+
return name;
|
|
6499
6775
|
}
|
|
6500
|
-
function
|
|
6501
|
-
|
|
6776
|
+
function parseParentExe(sessionName, agentId) {
|
|
6777
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6778
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
6779
|
+
const match = sessionName.match(regex);
|
|
6780
|
+
return match?.[1] ?? null;
|
|
6502
6781
|
}
|
|
6503
|
-
|
|
6504
|
-
const
|
|
6505
|
-
|
|
6506
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6507
|
-
const signatureHash = hashSignature(opts.signature);
|
|
6508
|
-
await client.execute({
|
|
6509
|
-
sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
|
|
6510
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
6511
|
-
args: [
|
|
6512
|
-
id,
|
|
6513
|
-
opts.taskId,
|
|
6514
|
-
opts.agentId,
|
|
6515
|
-
opts.projectName,
|
|
6516
|
-
opts.taskTitle,
|
|
6517
|
-
JSON.stringify(opts.signature),
|
|
6518
|
-
signatureHash,
|
|
6519
|
-
opts.signature.length,
|
|
6520
|
-
now
|
|
6521
|
-
]
|
|
6522
|
-
});
|
|
6523
|
-
return id;
|
|
6782
|
+
function extractRootExe(name) {
|
|
6783
|
+
const match = name.match(/(exe\d+)$/);
|
|
6784
|
+
return match?.[1] ?? null;
|
|
6524
6785
|
}
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
const result = await client.execute({
|
|
6529
|
-
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
6530
|
-
FROM trajectories
|
|
6531
|
-
WHERE signature_hash = ?
|
|
6532
|
-
ORDER BY created_at DESC
|
|
6533
|
-
LIMIT 20`,
|
|
6534
|
-
args: [hash]
|
|
6535
|
-
});
|
|
6536
|
-
const mapRow = (r) => ({
|
|
6537
|
-
id: String(r.id),
|
|
6538
|
-
taskId: String(r.task_id),
|
|
6539
|
-
agentId: String(r.agent_id),
|
|
6540
|
-
projectName: String(r.project_name),
|
|
6541
|
-
taskTitle: String(r.task_title),
|
|
6542
|
-
signature: JSON.parse(String(r.signature)),
|
|
6543
|
-
signatureHash: String(r.signature_hash),
|
|
6544
|
-
toolCount: Number(r.tool_count),
|
|
6545
|
-
skillId: r.skill_id ? String(r.skill_id) : null,
|
|
6546
|
-
createdAt: String(r.created_at)
|
|
6547
|
-
});
|
|
6548
|
-
const matches = result.rows.map(mapRow);
|
|
6549
|
-
if (matches.length >= threshold) return matches;
|
|
6550
|
-
const nearResult = await client.execute({
|
|
6551
|
-
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
6552
|
-
FROM trajectories
|
|
6553
|
-
WHERE tool_count BETWEEN ? AND ?
|
|
6554
|
-
AND signature_hash != ?
|
|
6555
|
-
ORDER BY created_at DESC
|
|
6556
|
-
LIMIT 50`,
|
|
6557
|
-
args: [
|
|
6558
|
-
Math.max(1, signature.length - 3),
|
|
6559
|
-
signature.length + 3,
|
|
6560
|
-
hash
|
|
6561
|
-
]
|
|
6562
|
-
});
|
|
6563
|
-
for (const r of nearResult.rows) {
|
|
6564
|
-
const candidateSig = JSON.parse(String(r.signature));
|
|
6565
|
-
if (editDistance(signature, candidateSig) <= 2) {
|
|
6566
|
-
matches.push(mapRow(r));
|
|
6567
|
-
}
|
|
6786
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
6787
|
+
if (!existsSync13(SESSION_CACHE)) {
|
|
6788
|
+
mkdirSync7(SESSION_CACHE, { recursive: true });
|
|
6568
6789
|
}
|
|
6569
|
-
|
|
6790
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
6791
|
+
const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
6792
|
+
writeFileSync5(filePath, JSON.stringify({
|
|
6793
|
+
parentExe: rootExe,
|
|
6794
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
6795
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6796
|
+
}));
|
|
6570
6797
|
}
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
return
|
|
6798
|
+
function getParentExe(sessionKey) {
|
|
6799
|
+
try {
|
|
6800
|
+
const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
6801
|
+
return data.parentExe || null;
|
|
6802
|
+
} catch {
|
|
6803
|
+
return null;
|
|
6575
6804
|
}
|
|
6576
|
-
const trajectoryId = await storeTrajectory({
|
|
6577
|
-
taskId: opts.taskId,
|
|
6578
|
-
agentId: opts.agentId,
|
|
6579
|
-
projectName: opts.projectName,
|
|
6580
|
-
taskTitle: opts.taskTitle,
|
|
6581
|
-
signature
|
|
6582
|
-
});
|
|
6583
|
-
const similar = await findSimilarTrajectories(
|
|
6584
|
-
signature,
|
|
6585
|
-
opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
|
|
6586
|
-
);
|
|
6587
|
-
return { trajectoryId, similarCount: similar.length, similar };
|
|
6588
6805
|
}
|
|
6589
|
-
function
|
|
6590
|
-
|
|
6591
|
-
const
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
Extract the reusable procedure. Format your response EXACTLY like this:
|
|
6600
|
-
|
|
6601
|
-
SKILL: {name \u2014 short, descriptive}
|
|
6602
|
-
TRIGGER: {when to use this \u2014 one sentence}
|
|
6603
|
-
STEPS:
|
|
6604
|
-
1. ...
|
|
6605
|
-
2. ...
|
|
6606
|
-
PITFALLS: {common mistakes to avoid}
|
|
6607
|
-
|
|
6608
|
-
Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
|
|
6806
|
+
function getDispatchedBy(sessionKey) {
|
|
6807
|
+
try {
|
|
6808
|
+
const data = JSON.parse(readFileSync11(
|
|
6809
|
+
path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
6810
|
+
"utf8"
|
|
6811
|
+
));
|
|
6812
|
+
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
6813
|
+
} catch {
|
|
6814
|
+
return null;
|
|
6815
|
+
}
|
|
6609
6816
|
}
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
messages: [{ role: "user", content: prompt }]
|
|
6621
|
-
});
|
|
6622
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
6623
|
-
const skillText = textBlock?.text;
|
|
6624
|
-
if (!skillText) return null;
|
|
6625
|
-
const agentId = trajectories[0].agentId;
|
|
6626
|
-
const projectName = trajectories[0].projectName;
|
|
6627
|
-
const skillId = await storeBehavior({
|
|
6628
|
-
agentId,
|
|
6629
|
-
content: skillText,
|
|
6630
|
-
domain: "skill",
|
|
6631
|
-
projectName
|
|
6632
|
-
});
|
|
6633
|
-
const dbClient = getClient();
|
|
6634
|
-
for (const t of trajectories) {
|
|
6635
|
-
await dbClient.execute({
|
|
6636
|
-
sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
|
|
6637
|
-
args: [skillId, t.id]
|
|
6638
|
-
});
|
|
6817
|
+
function resolveExeSession() {
|
|
6818
|
+
const mySession = getMySession();
|
|
6819
|
+
if (!mySession) return null;
|
|
6820
|
+
try {
|
|
6821
|
+
const key = getSessionKey();
|
|
6822
|
+
const parentExe = getParentExe(key);
|
|
6823
|
+
if (parentExe) {
|
|
6824
|
+
return extractRootExe(parentExe) ?? parentExe;
|
|
6825
|
+
}
|
|
6826
|
+
} catch {
|
|
6639
6827
|
}
|
|
6640
|
-
|
|
6641
|
-
|
|
6828
|
+
return extractRootExe(mySession) ?? mySession;
|
|
6829
|
+
}
|
|
6830
|
+
function isEmployeeAlive(sessionName) {
|
|
6831
|
+
return getTransport().isAlive(sessionName);
|
|
6832
|
+
}
|
|
6833
|
+
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
6834
|
+
const base = employeeSessionName(employeeName, exeSession);
|
|
6835
|
+
if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
|
|
6836
|
+
for (let i = 2; i <= maxInstances; i++) {
|
|
6837
|
+
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
6838
|
+
if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
|
|
6839
|
+
}
|
|
6840
|
+
return null;
|
|
6841
|
+
}
|
|
6842
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
6843
|
+
const transport = getTransport();
|
|
6844
|
+
if (!transport.isAlive(sessionName)) {
|
|
6845
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
6846
|
+
}
|
|
6847
|
+
let pane;
|
|
6848
|
+
try {
|
|
6849
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
6850
|
+
} catch (err) {
|
|
6851
|
+
return {
|
|
6852
|
+
atCapacity: false,
|
|
6853
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
6854
|
+
};
|
|
6855
|
+
}
|
|
6856
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
6857
|
+
if (!isAtCapacity2(pane)) {
|
|
6858
|
+
return {
|
|
6859
|
+
atCapacity: false,
|
|
6860
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
6861
|
+
};
|
|
6862
|
+
}
|
|
6863
|
+
return {
|
|
6864
|
+
atCapacity: true,
|
|
6865
|
+
reason: "capacity banner matched in recent pane output"
|
|
6866
|
+
};
|
|
6867
|
+
}
|
|
6868
|
+
function readDebounceState() {
|
|
6869
|
+
try {
|
|
6870
|
+
if (!existsSync13(DEBOUNCE_FILE)) return {};
|
|
6871
|
+
return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
|
|
6872
|
+
} catch {
|
|
6873
|
+
return {};
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
function writeDebounceState(state) {
|
|
6877
|
+
try {
|
|
6878
|
+
if (!existsSync13(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
|
|
6879
|
+
writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
|
|
6880
|
+
} catch {
|
|
6881
|
+
}
|
|
6882
|
+
}
|
|
6883
|
+
function isDebounced(targetSession) {
|
|
6884
|
+
const state = readDebounceState();
|
|
6885
|
+
const lastSent = state[targetSession] ?? 0;
|
|
6886
|
+
return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
|
|
6887
|
+
}
|
|
6888
|
+
function recordDebounce(targetSession) {
|
|
6889
|
+
const state = readDebounceState();
|
|
6890
|
+
state[targetSession] = Date.now();
|
|
6891
|
+
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
6892
|
+
for (const key of Object.keys(state)) {
|
|
6893
|
+
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
6894
|
+
}
|
|
6895
|
+
writeDebounceState(state);
|
|
6896
|
+
}
|
|
6897
|
+
function logIntercom(msg) {
|
|
6898
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
6899
|
+
`;
|
|
6900
|
+
process.stderr.write(`[intercom] ${msg}
|
|
6901
|
+
`);
|
|
6902
|
+
try {
|
|
6903
|
+
appendFileSync(INTERCOM_LOG2, line);
|
|
6904
|
+
} catch {
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
function getSessionState(sessionName) {
|
|
6908
|
+
const transport = getTransport();
|
|
6909
|
+
if (!transport.isAlive(sessionName)) return "offline";
|
|
6910
|
+
try {
|
|
6911
|
+
const pane = transport.capturePane(sessionName, 5);
|
|
6912
|
+
if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
|
|
6913
|
+
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
6914
|
+
return "no_claude";
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
if (/Running…/.test(pane)) return "tool";
|
|
6918
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
6919
|
+
return "idle";
|
|
6920
|
+
} catch {
|
|
6921
|
+
return "offline";
|
|
6922
|
+
}
|
|
6923
|
+
}
|
|
6924
|
+
function isSessionBusy(sessionName) {
|
|
6925
|
+
const state = getSessionState(sessionName);
|
|
6926
|
+
return state === "thinking" || state === "tool";
|
|
6927
|
+
}
|
|
6928
|
+
function isExeSession(sessionName) {
|
|
6929
|
+
return /^exe\d*$/.test(sessionName);
|
|
6930
|
+
}
|
|
6931
|
+
function sendIntercom(targetSession) {
|
|
6932
|
+
const transport = getTransport();
|
|
6933
|
+
if (isExeSession(targetSession)) {
|
|
6934
|
+
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
6935
|
+
return "skipped_exe";
|
|
6936
|
+
}
|
|
6937
|
+
if (isDebounced(targetSession)) {
|
|
6938
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
|
|
6939
|
+
return "debounced";
|
|
6940
|
+
}
|
|
6941
|
+
try {
|
|
6942
|
+
const sessions = transport.listSessions();
|
|
6943
|
+
if (!sessions.includes(targetSession)) {
|
|
6944
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
6945
|
+
return "failed";
|
|
6946
|
+
}
|
|
6947
|
+
const sessionState = getSessionState(targetSession);
|
|
6948
|
+
if (sessionState === "no_claude") {
|
|
6949
|
+
queueIntercom(targetSession, "claude not running in session");
|
|
6950
|
+
recordDebounce(targetSession);
|
|
6951
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
6952
|
+
return "queued";
|
|
6953
|
+
}
|
|
6954
|
+
if (sessionState === "thinking" || sessionState === "tool") {
|
|
6955
|
+
queueIntercom(targetSession, "session busy at send time");
|
|
6956
|
+
recordDebounce(targetSession);
|
|
6957
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
6958
|
+
return "queued";
|
|
6959
|
+
}
|
|
6960
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
6961
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
6962
|
+
transport.sendKeys(targetSession, "q");
|
|
6963
|
+
}
|
|
6964
|
+
transport.sendKeys(targetSession, "/exe-intercom");
|
|
6965
|
+
recordDebounce(targetSession);
|
|
6966
|
+
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
6967
|
+
return "delivered";
|
|
6968
|
+
} catch {
|
|
6969
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
6970
|
+
return "failed";
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
function notifyParentExe(sessionKey) {
|
|
6974
|
+
const target = getDispatchedBy(sessionKey);
|
|
6975
|
+
if (!target) {
|
|
6976
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
6977
|
+
`);
|
|
6978
|
+
return false;
|
|
6979
|
+
}
|
|
6980
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
6981
|
+
`);
|
|
6982
|
+
const result = sendIntercom(target);
|
|
6983
|
+
if (result === "failed") {
|
|
6984
|
+
const rootExe = resolveExeSession();
|
|
6985
|
+
if (rootExe && rootExe !== target) {
|
|
6986
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
6987
|
+
`);
|
|
6988
|
+
const fallback = sendIntercom(rootExe);
|
|
6989
|
+
return fallback !== "failed";
|
|
6990
|
+
}
|
|
6991
|
+
return false;
|
|
6992
|
+
}
|
|
6993
|
+
return true;
|
|
6994
|
+
}
|
|
6995
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
6996
|
+
if (employeeName === "exe") {
|
|
6997
|
+
return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
|
|
6998
|
+
}
|
|
6999
|
+
try {
|
|
7000
|
+
assertEmployeeLimitSync();
|
|
7001
|
+
} catch (err) {
|
|
7002
|
+
if (err instanceof PlanLimitError) {
|
|
7003
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
7004
|
+
}
|
|
7005
|
+
}
|
|
7006
|
+
if (/-exe\d*$/.test(employeeName)) {
|
|
7007
|
+
const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
|
|
7008
|
+
return {
|
|
7009
|
+
status: "failed",
|
|
7010
|
+
sessionName: "",
|
|
7011
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
7012
|
+
};
|
|
7013
|
+
}
|
|
7014
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
7015
|
+
const root = extractRootExe(exeSession);
|
|
7016
|
+
if (root) {
|
|
7017
|
+
process.stderr.write(
|
|
7018
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
|
|
6642
7019
|
`
|
|
6643
|
-
|
|
6644
|
-
|
|
7020
|
+
);
|
|
7021
|
+
exeSession = root;
|
|
7022
|
+
} else {
|
|
7023
|
+
return {
|
|
7024
|
+
status: "failed",
|
|
7025
|
+
sessionName: "",
|
|
7026
|
+
error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
|
|
7027
|
+
};
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
let effectiveInstance = opts?.instance;
|
|
7031
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
7032
|
+
const free = findFreeInstance(
|
|
7033
|
+
employeeName,
|
|
7034
|
+
exeSession,
|
|
7035
|
+
opts.maxAutoInstances ?? 10
|
|
7036
|
+
);
|
|
7037
|
+
if (free === null) {
|
|
7038
|
+
return {
|
|
7039
|
+
status: "failed",
|
|
7040
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
7041
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
7042
|
+
};
|
|
7043
|
+
}
|
|
7044
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
7045
|
+
}
|
|
7046
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
7047
|
+
if (isEmployeeAlive(sessionName)) {
|
|
7048
|
+
const result2 = sendIntercom(sessionName);
|
|
7049
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
7050
|
+
return { status: "intercom_sent", sessionName };
|
|
7051
|
+
}
|
|
7052
|
+
if (result2 === "delivered") {
|
|
7053
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
7054
|
+
}
|
|
7055
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
7056
|
+
}
|
|
7057
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
7058
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
7059
|
+
if (result.error) {
|
|
7060
|
+
return { status: "failed", sessionName, error: result.error };
|
|
7061
|
+
}
|
|
7062
|
+
return { status: "spawned", sessionName };
|
|
6645
7063
|
}
|
|
6646
|
-
|
|
7064
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
7065
|
+
const transport = getTransport();
|
|
7066
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
7067
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
7068
|
+
const logDir = path17.join(os7.homedir(), ".exe-os", "session-logs");
|
|
7069
|
+
const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
7070
|
+
if (!existsSync13(logDir)) {
|
|
7071
|
+
mkdirSync7(logDir, { recursive: true });
|
|
7072
|
+
}
|
|
7073
|
+
transport.kill(sessionName);
|
|
7074
|
+
let cleanupSuffix = "";
|
|
6647
7075
|
try {
|
|
6648
|
-
const
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
7076
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
7077
|
+
const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
7078
|
+
if (existsSync13(cleanupScript)) {
|
|
7079
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
7080
|
+
}
|
|
7081
|
+
} catch {
|
|
7082
|
+
}
|
|
7083
|
+
try {
|
|
7084
|
+
const claudeJsonPath = path17.join(os7.homedir(), ".claude.json");
|
|
7085
|
+
let claudeJson = {};
|
|
7086
|
+
try {
|
|
7087
|
+
claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
|
|
7088
|
+
} catch {
|
|
7089
|
+
}
|
|
7090
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
7091
|
+
const projects = claudeJson.projects;
|
|
7092
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
7093
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
7094
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
7095
|
+
writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
7096
|
+
} catch {
|
|
7097
|
+
}
|
|
7098
|
+
try {
|
|
7099
|
+
const settingsDir = path17.join(os7.homedir(), ".claude", "projects");
|
|
7100
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
7101
|
+
const projSettingsDir = path17.join(settingsDir, normalizedKey);
|
|
7102
|
+
const settingsPath = path17.join(projSettingsDir, "settings.json");
|
|
7103
|
+
let settings = {};
|
|
7104
|
+
try {
|
|
7105
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
|
|
7106
|
+
} catch {
|
|
7107
|
+
}
|
|
7108
|
+
const perms = settings.permissions ?? {};
|
|
7109
|
+
const allow = perms.allow ?? [];
|
|
7110
|
+
const toolNames = [
|
|
7111
|
+
"recall_my_memory",
|
|
7112
|
+
"store_memory",
|
|
7113
|
+
"create_task",
|
|
7114
|
+
"update_task",
|
|
7115
|
+
"list_tasks",
|
|
7116
|
+
"get_task",
|
|
7117
|
+
"ask_team_memory",
|
|
7118
|
+
"store_behavior",
|
|
7119
|
+
"get_identity",
|
|
7120
|
+
"send_message"
|
|
7121
|
+
];
|
|
7122
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
7123
|
+
let changed = false;
|
|
7124
|
+
for (const tool of requiredTools) {
|
|
7125
|
+
if (!allow.includes(tool)) {
|
|
7126
|
+
allow.push(tool);
|
|
7127
|
+
changed = true;
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
if (changed) {
|
|
7131
|
+
perms.allow = allow;
|
|
7132
|
+
settings.permissions = perms;
|
|
7133
|
+
mkdirSync7(projSettingsDir, { recursive: true });
|
|
7134
|
+
writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
7135
|
+
}
|
|
7136
|
+
} catch {
|
|
7137
|
+
}
|
|
7138
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
7139
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
7140
|
+
const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
7141
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
7142
|
+
let identityFlag = "";
|
|
7143
|
+
let behaviorsFlag = "";
|
|
7144
|
+
let legacyFallbackWarned = false;
|
|
7145
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
7146
|
+
const identityPath = path17.join(
|
|
7147
|
+
os7.homedir(),
|
|
7148
|
+
".exe-os",
|
|
7149
|
+
"identity",
|
|
7150
|
+
`${employeeName}.md`
|
|
7151
|
+
);
|
|
7152
|
+
_resetCcAgentSupportCache();
|
|
7153
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
7154
|
+
if (hasAgentFlag) {
|
|
7155
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
7156
|
+
} else if (existsSync13(identityPath)) {
|
|
7157
|
+
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
7158
|
+
legacyFallbackWarned = true;
|
|
7159
|
+
}
|
|
7160
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
7161
|
+
employeeName,
|
|
7162
|
+
path17.basename(spawnCwd),
|
|
7163
|
+
sessionName
|
|
7164
|
+
);
|
|
7165
|
+
if (behaviorsFile) {
|
|
7166
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
if (legacyFallbackWarned) {
|
|
7170
|
+
process.stderr.write(
|
|
7171
|
+
`[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.
|
|
6661
7172
|
`
|
|
6662
|
-
|
|
6663
|
-
|
|
7173
|
+
);
|
|
7174
|
+
}
|
|
7175
|
+
let sessionContextFlag = "";
|
|
7176
|
+
try {
|
|
7177
|
+
const ctxDir = path17.join(os7.homedir(), ".exe-os", "session-cache");
|
|
7178
|
+
mkdirSync7(ctxDir, { recursive: true });
|
|
7179
|
+
const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
|
|
7180
|
+
const ctxContent = [
|
|
7181
|
+
`## Session Context`,
|
|
7182
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
7183
|
+
`Your parent exe session is ${exeSession}.`,
|
|
7184
|
+
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
7185
|
+
].join("\n");
|
|
7186
|
+
writeFileSync5(ctxFile, ctxContent);
|
|
7187
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
7188
|
+
} catch {
|
|
7189
|
+
}
|
|
7190
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
7191
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
7192
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
7193
|
+
if (cfg?.apiKeyEnv) {
|
|
7194
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
7195
|
+
if (keyVal) {
|
|
7196
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
6664
7197
|
}
|
|
6665
7198
|
}
|
|
6666
|
-
}
|
|
7199
|
+
}
|
|
7200
|
+
let spawnCommand;
|
|
7201
|
+
if (useExeAgent) {
|
|
7202
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
7203
|
+
} else if (useBinSymlink) {
|
|
7204
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
6667
7205
|
process.stderr.write(
|
|
6668
|
-
`[
|
|
7206
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
6669
7207
|
`
|
|
6670
7208
|
);
|
|
7209
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
7210
|
+
} else {
|
|
7211
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
6671
7212
|
}
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
|
|
6676
|
-
const t = threshold ?? config2.skillThreshold;
|
|
6677
|
-
const client = getClient();
|
|
6678
|
-
const result = await client.execute({
|
|
6679
|
-
sql: `SELECT signature_hash, COUNT(*) as cnt
|
|
6680
|
-
FROM trajectories
|
|
6681
|
-
WHERE skill_id IS NULL
|
|
6682
|
-
GROUP BY signature_hash
|
|
6683
|
-
HAVING cnt >= ?
|
|
6684
|
-
ORDER BY cnt DESC
|
|
6685
|
-
LIMIT 10`,
|
|
6686
|
-
args: [t]
|
|
7213
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
7214
|
+
cwd: spawnCwd,
|
|
7215
|
+
command: spawnCommand
|
|
6687
7216
|
});
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
id: String(r.id),
|
|
6702
|
-
taskId: String(r.task_id),
|
|
6703
|
-
agentId: String(r.agent_id),
|
|
6704
|
-
projectName: String(r.project_name),
|
|
6705
|
-
taskTitle: String(r.task_title),
|
|
6706
|
-
signature: JSON.parse(String(r.signature)),
|
|
6707
|
-
signatureHash: String(r.signature_hash),
|
|
6708
|
-
toolCount: Number(r.tool_count),
|
|
6709
|
-
skillId: null,
|
|
6710
|
-
createdAt: String(r.created_at)
|
|
7217
|
+
if (spawnResult.error) {
|
|
7218
|
+
releaseSpawnLock2(sessionName);
|
|
7219
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
7220
|
+
}
|
|
7221
|
+
transport.pipeLog(sessionName, logFile);
|
|
7222
|
+
try {
|
|
7223
|
+
const mySession = getMySession();
|
|
7224
|
+
const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
7225
|
+
writeFileSync5(dispatchInfo, JSON.stringify({
|
|
7226
|
+
dispatchedBy: mySession,
|
|
7227
|
+
rootExe: exeSession,
|
|
7228
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
7229
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6711
7230
|
}));
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
7231
|
+
} catch {
|
|
7232
|
+
}
|
|
7233
|
+
let booted = false;
|
|
7234
|
+
for (let i = 0; i < 30; i++) {
|
|
7235
|
+
try {
|
|
7236
|
+
execSync6("sleep 0.5");
|
|
7237
|
+
} catch {
|
|
7238
|
+
}
|
|
7239
|
+
try {
|
|
7240
|
+
const pane = transport.capturePane(sessionName);
|
|
7241
|
+
if (useExeAgent) {
|
|
7242
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
7243
|
+
booted = true;
|
|
7244
|
+
break;
|
|
7245
|
+
}
|
|
7246
|
+
} else {
|
|
7247
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
7248
|
+
booted = true;
|
|
7249
|
+
break;
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
7252
|
+
} catch {
|
|
6716
7253
|
}
|
|
6717
7254
|
}
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
for (let i = 1; i <= m; i++) {
|
|
6727
|
-
for (let j = 1; j <= n; j++) {
|
|
6728
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
6729
|
-
dp[i][j] = Math.min(
|
|
6730
|
-
dp[i - 1][j] + 1,
|
|
6731
|
-
dp[i][j - 1] + 1,
|
|
6732
|
-
dp[i - 1][j - 1] + cost
|
|
6733
|
-
);
|
|
7255
|
+
if (!booted) {
|
|
7256
|
+
releaseSpawnLock2(sessionName);
|
|
7257
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
7258
|
+
}
|
|
7259
|
+
if (!useExeAgent) {
|
|
7260
|
+
try {
|
|
7261
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
7262
|
+
} catch {
|
|
6734
7263
|
}
|
|
6735
7264
|
}
|
|
6736
|
-
|
|
7265
|
+
registerSession({
|
|
7266
|
+
windowName: sessionName,
|
|
7267
|
+
agentId: employeeName,
|
|
7268
|
+
projectDir: spawnCwd,
|
|
7269
|
+
parentExe: exeSession,
|
|
7270
|
+
pid: 0,
|
|
7271
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7272
|
+
});
|
|
7273
|
+
releaseSpawnLock2(sessionName);
|
|
7274
|
+
return { sessionName };
|
|
6737
7275
|
}
|
|
6738
|
-
var
|
|
6739
|
-
var
|
|
6740
|
-
"src/lib/
|
|
7276
|
+
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;
|
|
7277
|
+
var init_tmux_routing = __esm({
|
|
7278
|
+
"src/lib/tmux-routing.ts"() {
|
|
6741
7279
|
"use strict";
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
7280
|
+
init_session_registry();
|
|
7281
|
+
init_session_key();
|
|
7282
|
+
init_transport();
|
|
7283
|
+
init_cc_agent_support();
|
|
7284
|
+
init_mcp_prefix();
|
|
7285
|
+
init_provider_table();
|
|
7286
|
+
init_intercom_queue();
|
|
7287
|
+
init_plan_limits();
|
|
7288
|
+
SPAWN_LOCK_DIR = path17.join(os7.homedir(), ".exe-os", "spawn-locks");
|
|
7289
|
+
SESSION_CACHE = path17.join(os7.homedir(), ".exe-os", "session-cache");
|
|
7290
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
7291
|
+
VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
|
|
7292
|
+
VERIFY_PANE_LINES = 200;
|
|
7293
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
7294
|
+
INTERCOM_LOG2 = path17.join(os7.homedir(), ".exe-os", "intercom.log");
|
|
7295
|
+
DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
|
|
7296
|
+
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
7297
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
6746
7298
|
}
|
|
6747
7299
|
});
|
|
6748
7300
|
|
|
6749
|
-
// src/lib/
|
|
6750
|
-
var
|
|
6751
|
-
__export(
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
updateTask: () => updateTask,
|
|
6766
|
-
updateTaskStatus: () => updateTaskStatus,
|
|
6767
|
-
writeCheckpoint: () => writeCheckpoint
|
|
7301
|
+
// src/lib/messaging.ts
|
|
7302
|
+
var messaging_exports = {};
|
|
7303
|
+
__export(messaging_exports, {
|
|
7304
|
+
deliverLocalMessage: () => deliverLocalMessage,
|
|
7305
|
+
getFailedMessages: () => getFailedMessages,
|
|
7306
|
+
getMessageStatus: () => getMessageStatus,
|
|
7307
|
+
getPendingMessages: () => getPendingMessages,
|
|
7308
|
+
getReadMessages: () => getReadMessages,
|
|
7309
|
+
getUnacknowledgedMessages: () => getUnacknowledgedMessages,
|
|
7310
|
+
markAcknowledged: () => markAcknowledged,
|
|
7311
|
+
markFailed: () => markFailed,
|
|
7312
|
+
markProcessed: () => markProcessed,
|
|
7313
|
+
markRead: () => markRead,
|
|
7314
|
+
retryPendingMessages: () => retryPendingMessages,
|
|
7315
|
+
sendMessage: () => sendMessage,
|
|
7316
|
+
setWsClientSend: () => setWsClientSend
|
|
6768
7317
|
});
|
|
6769
|
-
import
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
const
|
|
6773
|
-
|
|
6774
|
-
dispatchTaskToEmployee({
|
|
6775
|
-
assignedTo: input.assignedTo,
|
|
6776
|
-
title: input.title,
|
|
6777
|
-
priority: input.priority,
|
|
6778
|
-
taskFile: result.taskFile,
|
|
6779
|
-
initialStatus: result.status,
|
|
6780
|
-
projectName: input.projectName
|
|
6781
|
-
});
|
|
6782
|
-
}
|
|
6783
|
-
return result;
|
|
7318
|
+
import crypto8 from "crypto";
|
|
7319
|
+
function generateUlid() {
|
|
7320
|
+
const timestamp = Date.now().toString(36).padStart(10, "0");
|
|
7321
|
+
const random = crypto8.randomBytes(10).toString("hex").slice(0, 16);
|
|
7322
|
+
return (timestamp + random).toUpperCase();
|
|
6784
7323
|
}
|
|
6785
|
-
|
|
6786
|
-
|
|
7324
|
+
function rowToMessage(row) {
|
|
7325
|
+
return {
|
|
7326
|
+
id: row.id,
|
|
7327
|
+
fromAgent: row.from_agent,
|
|
7328
|
+
fromDevice: row.from_device,
|
|
7329
|
+
targetAgent: row.target_agent,
|
|
7330
|
+
targetProject: row.target_project ?? null,
|
|
7331
|
+
targetDevice: row.target_device,
|
|
7332
|
+
content: row.content,
|
|
7333
|
+
priority: row.priority ?? "normal",
|
|
7334
|
+
status: row.status ?? "pending",
|
|
7335
|
+
serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
|
|
7336
|
+
retryCount: Number(row.retry_count ?? 0),
|
|
7337
|
+
createdAt: row.created_at,
|
|
7338
|
+
deliveredAt: row.delivered_at ?? null,
|
|
7339
|
+
processedAt: row.processed_at ?? null,
|
|
7340
|
+
failedAt: row.failed_at ?? null,
|
|
7341
|
+
failureReason: row.failure_reason ?? null
|
|
7342
|
+
};
|
|
7343
|
+
}
|
|
7344
|
+
async function sendMessage(input) {
|
|
7345
|
+
const client = getClient();
|
|
7346
|
+
const id = generateUlid();
|
|
7347
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7348
|
+
const targetDevice = input.targetDevice ?? "local";
|
|
7349
|
+
await client.execute({
|
|
7350
|
+
sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
|
|
7351
|
+
VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
|
|
7352
|
+
args: [
|
|
7353
|
+
id,
|
|
7354
|
+
input.fromAgent,
|
|
7355
|
+
input.targetAgent,
|
|
7356
|
+
input.targetProject ?? null,
|
|
7357
|
+
targetDevice,
|
|
7358
|
+
input.content,
|
|
7359
|
+
input.priority ?? "normal",
|
|
7360
|
+
now
|
|
7361
|
+
]
|
|
7362
|
+
});
|
|
6787
7363
|
try {
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
mkdirSync7(cacheDir, { recursive: true });
|
|
6793
|
-
writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
6794
|
-
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
6795
|
-
try {
|
|
6796
|
-
unlinkSync5(cachePath);
|
|
6797
|
-
} catch {
|
|
6798
|
-
}
|
|
7364
|
+
if (targetDevice !== "local") {
|
|
7365
|
+
await deliverCrossMachineMessage(id, targetDevice);
|
|
7366
|
+
} else {
|
|
7367
|
+
await deliverLocalMessage(id);
|
|
6799
7368
|
}
|
|
6800
7369
|
} catch {
|
|
6801
7370
|
}
|
|
6802
|
-
|
|
6803
|
-
|
|
7371
|
+
const result = await client.execute({
|
|
7372
|
+
sql: "SELECT * FROM messages WHERE id = ?",
|
|
7373
|
+
args: [id]
|
|
7374
|
+
});
|
|
7375
|
+
return rowToMessage(result.rows[0]);
|
|
7376
|
+
}
|
|
7377
|
+
function setWsClientSend(fn) {
|
|
7378
|
+
_wsClientSend = fn;
|
|
7379
|
+
}
|
|
7380
|
+
async function deliverCrossMachineMessage(messageId, targetDevice) {
|
|
7381
|
+
const client = getClient();
|
|
7382
|
+
const result = await client.execute({
|
|
7383
|
+
sql: "SELECT * FROM messages WHERE id = ?",
|
|
7384
|
+
args: [messageId]
|
|
7385
|
+
});
|
|
7386
|
+
if (result.rows.length === 0) return false;
|
|
7387
|
+
const msg = rowToMessage(result.rows[0]);
|
|
7388
|
+
if (msg.status !== "pending") return false;
|
|
7389
|
+
if (!_wsClientSend) {
|
|
7390
|
+
return false;
|
|
6804
7391
|
}
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
7392
|
+
const payload = JSON.stringify({
|
|
7393
|
+
id: msg.id,
|
|
7394
|
+
fromAgent: msg.fromAgent,
|
|
7395
|
+
targetAgent: msg.targetAgent,
|
|
7396
|
+
targetProject: msg.targetProject,
|
|
7397
|
+
content: msg.content,
|
|
7398
|
+
priority: msg.priority,
|
|
7399
|
+
createdAt: msg.createdAt
|
|
7400
|
+
});
|
|
7401
|
+
const sent = _wsClientSend(targetDevice, payload);
|
|
7402
|
+
if (sent) {
|
|
7403
|
+
await client.execute({
|
|
7404
|
+
sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
|
|
7405
|
+
args: [messageId]
|
|
7406
|
+
});
|
|
7407
|
+
return true;
|
|
6817
7408
|
}
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
7409
|
+
return false;
|
|
7410
|
+
}
|
|
7411
|
+
async function deliverLocalMessage(messageId) {
|
|
7412
|
+
const client = getClient();
|
|
7413
|
+
const result = await client.execute({
|
|
7414
|
+
sql: "SELECT * FROM messages WHERE id = ?",
|
|
7415
|
+
args: [messageId]
|
|
7416
|
+
});
|
|
7417
|
+
if (result.rows.length === 0) return false;
|
|
7418
|
+
const msg = rowToMessage(result.rows[0]);
|
|
7419
|
+
if (msg.status !== "pending") return false;
|
|
7420
|
+
const targetAgent = msg.targetAgent;
|
|
7421
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7422
|
+
try {
|
|
7423
|
+
const exeSession = resolveExeSession();
|
|
7424
|
+
if (!exeSession) {
|
|
7425
|
+
throw new Error("No exe session found");
|
|
6823
7426
|
}
|
|
6824
|
-
|
|
6825
|
-
if (
|
|
6826
|
-
|
|
6827
|
-
await cascadeUnblock(taskId, input.baseDir, now);
|
|
6828
|
-
} catch {
|
|
6829
|
-
}
|
|
6830
|
-
if (row.parent_task_id) {
|
|
6831
|
-
try {
|
|
6832
|
-
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
6833
|
-
} catch {
|
|
6834
|
-
}
|
|
6835
|
-
}
|
|
7427
|
+
const ensureResult = ensureEmployee(targetAgent, exeSession, process.cwd());
|
|
7428
|
+
if (ensureResult.status === "failed") {
|
|
7429
|
+
throw new Error(ensureResult.error ?? "ensureEmployee failed");
|
|
6836
7430
|
}
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
6841
|
-
taskId,
|
|
6842
|
-
agentId: String(row.assigned_to),
|
|
6843
|
-
projectName: String(row.project_name),
|
|
6844
|
-
taskTitle: String(row.title)
|
|
6845
|
-
})
|
|
6846
|
-
).catch((err) => {
|
|
6847
|
-
process.stderr.write(
|
|
6848
|
-
`[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
|
|
6849
|
-
`
|
|
6850
|
-
);
|
|
7431
|
+
await client.execute({
|
|
7432
|
+
sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
|
|
7433
|
+
args: [now, messageId]
|
|
6851
7434
|
});
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
}
|
|
7435
|
+
return true;
|
|
7436
|
+
} catch {
|
|
7437
|
+
const newRetryCount = msg.retryCount + 1;
|
|
7438
|
+
if (newRetryCount >= MAX_RETRIES2) {
|
|
7439
|
+
await markFailed(messageId, "session unavailable after 10 retries");
|
|
7440
|
+
} else {
|
|
7441
|
+
await client.execute({
|
|
7442
|
+
sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
|
|
7443
|
+
args: [newRetryCount, messageId]
|
|
7444
|
+
});
|
|
6858
7445
|
}
|
|
7446
|
+
return false;
|
|
6859
7447
|
}
|
|
6860
|
-
return {
|
|
6861
|
-
id: String(row.id),
|
|
6862
|
-
title: String(row.title),
|
|
6863
|
-
assignedTo: String(row.assigned_to),
|
|
6864
|
-
assignedBy: String(row.assigned_by),
|
|
6865
|
-
projectName: String(row.project_name),
|
|
6866
|
-
priority: String(row.priority),
|
|
6867
|
-
status: input.status,
|
|
6868
|
-
taskFile,
|
|
6869
|
-
createdAt: String(row.created_at),
|
|
6870
|
-
updatedAt: now,
|
|
6871
|
-
budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
|
|
6872
|
-
budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
|
|
6873
|
-
tokensUsed: Number(row.tokens_used ?? 0),
|
|
6874
|
-
tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
|
|
6875
|
-
nextTask
|
|
6876
|
-
};
|
|
6877
7448
|
}
|
|
6878
|
-
async function
|
|
7449
|
+
async function getPendingMessages(targetAgent) {
|
|
7450
|
+
const client = getClient();
|
|
7451
|
+
const result = await client.execute({
|
|
7452
|
+
sql: `SELECT * FROM messages
|
|
7453
|
+
WHERE target_agent = ? AND status IN ('pending', 'delivered')
|
|
7454
|
+
ORDER BY id`,
|
|
7455
|
+
args: [targetAgent]
|
|
7456
|
+
});
|
|
7457
|
+
return result.rows.map((row) => rowToMessage(row));
|
|
7458
|
+
}
|
|
7459
|
+
async function markRead(messageId) {
|
|
6879
7460
|
const client = getClient();
|
|
6880
|
-
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
6881
|
-
const reviewer = assignedBy || "exe";
|
|
6882
|
-
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
6883
|
-
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
6884
7461
|
await client.execute({
|
|
6885
|
-
sql: "
|
|
6886
|
-
args: [
|
|
7462
|
+
sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
|
|
7463
|
+
args: [messageId]
|
|
6887
7464
|
});
|
|
6888
|
-
await markAsReadByTaskFile(taskFile);
|
|
6889
|
-
await markAsReadByTaskFile(reviewFile);
|
|
6890
7465
|
}
|
|
6891
|
-
|
|
6892
|
-
|
|
7466
|
+
async function markAcknowledged(messageId) {
|
|
7467
|
+
const client = getClient();
|
|
7468
|
+
await client.execute({
|
|
7469
|
+
sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
|
|
7470
|
+
args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
|
|
7471
|
+
});
|
|
7472
|
+
}
|
|
7473
|
+
async function markProcessed(messageId) {
|
|
7474
|
+
const client = getClient();
|
|
7475
|
+
await client.execute({
|
|
7476
|
+
sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
|
|
7477
|
+
args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
|
|
7478
|
+
});
|
|
7479
|
+
}
|
|
7480
|
+
async function getMessageStatus(messageId) {
|
|
7481
|
+
const client = getClient();
|
|
7482
|
+
const result = await client.execute({
|
|
7483
|
+
sql: "SELECT status FROM messages WHERE id = ?",
|
|
7484
|
+
args: [messageId]
|
|
7485
|
+
});
|
|
7486
|
+
return result.rows[0]?.status ?? null;
|
|
7487
|
+
}
|
|
7488
|
+
async function getUnacknowledgedMessages(targetAgent) {
|
|
7489
|
+
const client = getClient();
|
|
7490
|
+
const result = await client.execute({
|
|
7491
|
+
sql: `SELECT * FROM messages
|
|
7492
|
+
WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
|
|
7493
|
+
ORDER BY id`,
|
|
7494
|
+
args: [targetAgent]
|
|
7495
|
+
});
|
|
7496
|
+
return result.rows.map((row) => rowToMessage(row));
|
|
7497
|
+
}
|
|
7498
|
+
async function getReadMessages(targetAgent) {
|
|
7499
|
+
const client = getClient();
|
|
7500
|
+
const result = await client.execute({
|
|
7501
|
+
sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
|
|
7502
|
+
args: [targetAgent]
|
|
7503
|
+
});
|
|
7504
|
+
return result.rows.map((row) => rowToMessage(row));
|
|
7505
|
+
}
|
|
7506
|
+
async function markFailed(messageId, reason) {
|
|
7507
|
+
const client = getClient();
|
|
7508
|
+
await client.execute({
|
|
7509
|
+
sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
|
|
7510
|
+
args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
|
|
7511
|
+
});
|
|
7512
|
+
}
|
|
7513
|
+
async function getFailedMessages() {
|
|
7514
|
+
const client = getClient();
|
|
7515
|
+
const result = await client.execute({
|
|
7516
|
+
sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
|
|
7517
|
+
args: []
|
|
7518
|
+
});
|
|
7519
|
+
return result.rows.map((row) => rowToMessage(row));
|
|
7520
|
+
}
|
|
7521
|
+
async function retryPendingMessages() {
|
|
7522
|
+
const client = getClient();
|
|
7523
|
+
const result = await client.execute({
|
|
7524
|
+
sql: `SELECT * FROM messages
|
|
7525
|
+
WHERE status = 'pending' AND retry_count < ?
|
|
7526
|
+
ORDER BY id`,
|
|
7527
|
+
args: [MAX_RETRIES2]
|
|
7528
|
+
});
|
|
7529
|
+
let delivered = 0;
|
|
7530
|
+
for (const row of result.rows) {
|
|
7531
|
+
const msg = rowToMessage(row);
|
|
7532
|
+
try {
|
|
7533
|
+
const success = await deliverLocalMessage(msg.id);
|
|
7534
|
+
if (success) delivered++;
|
|
7535
|
+
} catch {
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
return delivered;
|
|
7539
|
+
}
|
|
7540
|
+
var MAX_RETRIES2, _wsClientSend;
|
|
7541
|
+
var init_messaging = __esm({
|
|
7542
|
+
"src/lib/messaging.ts"() {
|
|
6893
7543
|
"use strict";
|
|
6894
7544
|
init_database();
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
init_tasks_review();
|
|
6899
|
-
init_tasks_crud();
|
|
6900
|
-
init_tasks_chain();
|
|
6901
|
-
init_tasks_review();
|
|
6902
|
-
init_tasks_notify();
|
|
7545
|
+
init_tmux_routing();
|
|
7546
|
+
MAX_RETRIES2 = 10;
|
|
7547
|
+
_wsClientSend = null;
|
|
6903
7548
|
}
|
|
6904
7549
|
});
|
|
6905
7550
|
|
|
6906
7551
|
// src/automation/trigger-engine.ts
|
|
6907
7552
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
|
|
6908
|
-
import { randomUUID as
|
|
7553
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
6909
7554
|
import path18 from "path";
|
|
6910
7555
|
import os8 from "os";
|
|
6911
7556
|
function substituteTemplate(template, record) {
|
|
@@ -7450,6 +8095,9 @@ function sendJson(res, status, data) {
|
|
|
7450
8095
|
res.end(JSON.stringify(data));
|
|
7451
8096
|
}
|
|
7452
8097
|
|
|
8098
|
+
// src/gateway/gateway.ts
|
|
8099
|
+
init_state_bus();
|
|
8100
|
+
|
|
7453
8101
|
// src/gateway/router.ts
|
|
7454
8102
|
function matchesPlatform(msgPlatform, matchPlatform) {
|
|
7455
8103
|
if (!matchPlatform) return true;
|
|
@@ -7748,6 +8396,13 @@ var Gateway = class {
|
|
|
7748
8396
|
console.log(
|
|
7749
8397
|
`[gateway] ${msg.platform}/${msg.senderId} \u2192 ${route.employee} (${route.routeName})`
|
|
7750
8398
|
);
|
|
8399
|
+
orgBus.emit({
|
|
8400
|
+
type: "gateway_message",
|
|
8401
|
+
platform: msg.platform,
|
|
8402
|
+
senderId: msg.senderId,
|
|
8403
|
+
botId: route.employee,
|
|
8404
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8405
|
+
});
|
|
7751
8406
|
const bot = this.botRegistry.get(route.employee);
|
|
7752
8407
|
if (!bot) {
|
|
7753
8408
|
console.error(`[gateway] No bot registered for target: ${route.employee}`);
|