@askexenow/exe-os 0.9.7 → 0.9.9
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 +953 -105
- package/dist/bin/backfill-responses.js +952 -104
- package/dist/bin/backfill-vectors.js +956 -108
- package/dist/bin/cleanup-stale-review-tasks.js +802 -58
- package/dist/bin/cli.js +2292 -1070
- package/dist/bin/exe-agent-config.js +157 -101
- package/dist/bin/exe-agent.js +55 -29
- package/dist/bin/exe-assign.js +940 -92
- package/dist/bin/exe-boot.js +1424 -442
- package/dist/bin/exe-call.js +240 -141
- package/dist/bin/exe-cloud.js +198 -70
- package/dist/bin/exe-dispatch.js +951 -192
- package/dist/bin/exe-doctor.js +791 -51
- package/dist/bin/exe-export-behaviors.js +790 -42
- package/dist/bin/exe-forget.js +771 -31
- package/dist/bin/exe-gateway.js +1592 -521
- package/dist/bin/exe-heartbeat.js +850 -109
- package/dist/bin/exe-kill.js +783 -35
- package/dist/bin/exe-launch-agent.js +1030 -107
- package/dist/bin/exe-link.js +916 -110
- package/dist/bin/exe-new-employee.js +526 -217
- package/dist/bin/exe-pending-messages.js +1046 -62
- package/dist/bin/exe-pending-notifications.js +1318 -111
- package/dist/bin/exe-pending-reviews.js +1040 -72
- package/dist/bin/exe-rename.js +772 -59
- package/dist/bin/exe-review.js +772 -32
- package/dist/bin/exe-search.js +982 -128
- package/dist/bin/exe-session-cleanup.js +1180 -306
- package/dist/bin/exe-settings.js +185 -105
- package/dist/bin/exe-start-codex.js +886 -132
- package/dist/bin/exe-start-opencode.js +873 -119
- package/dist/bin/exe-status.js +803 -59
- package/dist/bin/exe-team.js +772 -32
- package/dist/bin/git-sweep.js +1046 -223
- package/dist/bin/graph-backfill.js +779 -31
- package/dist/bin/graph-export.js +785 -37
- package/dist/bin/install.js +632 -200
- package/dist/bin/scan-tasks.js +1055 -232
- package/dist/bin/setup.js +1419 -320
- package/dist/bin/shard-migrate.js +783 -35
- package/dist/bin/update.js +138 -49
- package/dist/bin/wiki-sync.js +782 -34
- package/dist/gateway/index.js +1444 -449
- package/dist/hooks/bug-report-worker.js +1141 -269
- package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
- package/dist/hooks/commit-complete.js +1044 -221
- package/dist/hooks/error-recall.js +989 -135
- package/dist/hooks/exe-heartbeat-hook.js +99 -75
- package/dist/hooks/ingest-worker.js +4176 -3226
- package/dist/hooks/ingest.js +920 -168
- package/dist/hooks/instructions-loaded.js +874 -70
- package/dist/hooks/notification.js +860 -56
- package/dist/hooks/post-compact.js +881 -73
- package/dist/hooks/pre-compact.js +1050 -227
- package/dist/hooks/pre-tool-use.js +1084 -159
- package/dist/hooks/prompt-ingest-worker.js +1089 -164
- package/dist/hooks/prompt-submit.js +1469 -515
- package/dist/hooks/response-ingest-worker.js +1104 -179
- package/dist/hooks/session-end.js +1085 -251
- package/dist/hooks/session-start.js +1241 -231
- package/dist/hooks/stop.js +935 -109
- package/dist/hooks/subagent-stop.js +881 -73
- package/dist/hooks/summary-worker.js +1323 -307
- package/dist/index.js +1449 -452
- package/dist/lib/agent-config.js +28 -6
- package/dist/lib/cloud-sync.js +909 -115
- package/dist/lib/config.js +30 -10
- package/dist/lib/consolidation.js +42 -9
- package/dist/lib/database.js +739 -33
- package/dist/lib/db-daemon-client.js +73 -19
- package/dist/lib/db.js +2359 -0
- package/dist/lib/device-registry.js +760 -47
- package/dist/lib/embedder.js +201 -73
- package/dist/lib/employee-templates.js +30 -4
- package/dist/lib/employees.js +290 -86
- package/dist/lib/exe-daemon-client.js +187 -83
- package/dist/lib/exe-daemon.js +1696 -616
- package/dist/lib/hybrid-search.js +982 -128
- package/dist/lib/identity.js +43 -13
- package/dist/lib/license.js +133 -48
- package/dist/lib/messaging.js +167 -80
- package/dist/lib/reminders.js +35 -5
- package/dist/lib/schedules.js +772 -32
- package/dist/lib/skill-learning.js +54 -7
- package/dist/lib/store.js +779 -31
- package/dist/lib/task-router.js +94 -73
- package/dist/lib/tasks.js +298 -225
- package/dist/lib/tmux-routing.js +246 -172
- package/dist/lib/token-spend.js +52 -14
- package/dist/mcp/server.js +2893 -850
- package/dist/mcp/tools/complete-reminder.js +35 -5
- package/dist/mcp/tools/create-reminder.js +35 -5
- package/dist/mcp/tools/create-task.js +507 -323
- package/dist/mcp/tools/deactivate-behavior.js +40 -10
- package/dist/mcp/tools/list-reminders.js +35 -5
- package/dist/mcp/tools/list-tasks.js +277 -104
- package/dist/mcp/tools/send-message.js +129 -56
- package/dist/mcp/tools/update-task.js +1864 -188
- package/dist/runtime/index.js +1083 -259
- package/dist/tui/App.js +1501 -434
- package/package.json +3 -2
|
@@ -32,9 +32,34 @@ var init_db_retry = __esm({
|
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
// src/lib/secure-files.ts
|
|
36
|
+
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
37
|
+
import { chmod, mkdir } from "fs/promises";
|
|
38
|
+
async function ensurePrivateDir(dirPath) {
|
|
39
|
+
await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
40
|
+
try {
|
|
41
|
+
await chmod(dirPath, PRIVATE_DIR_MODE);
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function enforcePrivateFile(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
await chmod(filePath, PRIVATE_FILE_MODE);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
|
|
52
|
+
var init_secure_files = __esm({
|
|
53
|
+
"src/lib/secure-files.ts"() {
|
|
54
|
+
"use strict";
|
|
55
|
+
PRIVATE_DIR_MODE = 448;
|
|
56
|
+
PRIVATE_FILE_MODE = 384;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
35
60
|
// src/lib/config.ts
|
|
36
|
-
import { readFile, writeFile
|
|
37
|
-
import { readFileSync, existsSync, renameSync } from "fs";
|
|
61
|
+
import { readFile, writeFile } from "fs/promises";
|
|
62
|
+
import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
|
|
38
63
|
import path from "path";
|
|
39
64
|
import os from "os";
|
|
40
65
|
function resolveDataDir() {
|
|
@@ -42,7 +67,7 @@ function resolveDataDir() {
|
|
|
42
67
|
if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
|
|
43
68
|
const newDir = path.join(os.homedir(), ".exe-os");
|
|
44
69
|
const legacyDir = path.join(os.homedir(), ".exe-mem");
|
|
45
|
-
if (!
|
|
70
|
+
if (!existsSync2(newDir) && existsSync2(legacyDir)) {
|
|
46
71
|
try {
|
|
47
72
|
renameSync(legacyDir, newDir);
|
|
48
73
|
process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
|
|
@@ -105,9 +130,9 @@ function normalizeAutoUpdate(raw) {
|
|
|
105
130
|
}
|
|
106
131
|
async function loadConfig() {
|
|
107
132
|
const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
|
|
108
|
-
await
|
|
133
|
+
await ensurePrivateDir(dir);
|
|
109
134
|
const configPath = path.join(dir, "config.json");
|
|
110
|
-
if (!
|
|
135
|
+
if (!existsSync2(configPath)) {
|
|
111
136
|
return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
|
|
112
137
|
}
|
|
113
138
|
const raw = await readFile(configPath, "utf-8");
|
|
@@ -120,6 +145,7 @@ async function loadConfig() {
|
|
|
120
145
|
`);
|
|
121
146
|
try {
|
|
122
147
|
await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
|
|
148
|
+
await enforcePrivateFile(configPath);
|
|
123
149
|
} catch {
|
|
124
150
|
}
|
|
125
151
|
}
|
|
@@ -139,6 +165,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
|
|
|
139
165
|
var init_config = __esm({
|
|
140
166
|
"src/lib/config.ts"() {
|
|
141
167
|
"use strict";
|
|
168
|
+
init_secure_files();
|
|
142
169
|
EXE_AI_DIR = resolveDataDir();
|
|
143
170
|
DB_PATH = path.join(EXE_AI_DIR, "memories.db");
|
|
144
171
|
MODELS_DIR = path.join(EXE_AI_DIR, "models");
|
|
@@ -217,7 +244,7 @@ var init_config = __esm({
|
|
|
217
244
|
|
|
218
245
|
// src/lib/employees.ts
|
|
219
246
|
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
220
|
-
import { existsSync as
|
|
247
|
+
import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
|
|
221
248
|
import { execSync } from "child_process";
|
|
222
249
|
import path2 from "path";
|
|
223
250
|
import os2 from "os";
|
|
@@ -241,7 +268,7 @@ function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
|
|
|
241
268
|
return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
|
|
242
269
|
}
|
|
243
270
|
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
244
|
-
if (!
|
|
271
|
+
if (!existsSync3(employeesPath)) return [];
|
|
245
272
|
try {
|
|
246
273
|
return JSON.parse(readFileSync2(employeesPath, "utf-8"));
|
|
247
274
|
} catch {
|
|
@@ -259,7 +286,13 @@ function baseAgentName(name, employees) {
|
|
|
259
286
|
if (getEmployee(roster, base)) return base;
|
|
260
287
|
return name;
|
|
261
288
|
}
|
|
262
|
-
|
|
289
|
+
function isMultiInstance(agentName, employees) {
|
|
290
|
+
const roster = employees ?? loadEmployeesSync();
|
|
291
|
+
const emp = getEmployee(roster, agentName);
|
|
292
|
+
if (!emp) return false;
|
|
293
|
+
return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
|
|
294
|
+
}
|
|
295
|
+
var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES, IDENTITY_DIR;
|
|
263
296
|
var init_employees = __esm({
|
|
264
297
|
"src/lib/employees.ts"() {
|
|
265
298
|
"use strict";
|
|
@@ -267,15 +300,41 @@ var init_employees = __esm({
|
|
|
267
300
|
EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
|
|
268
301
|
DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
|
|
269
302
|
COORDINATOR_ROLE = "COO";
|
|
303
|
+
MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
|
|
304
|
+
IDENTITY_DIR = path2.join(EXE_AI_DIR, "identity");
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// src/lib/database-adapter.ts
|
|
309
|
+
import os3 from "os";
|
|
310
|
+
import path3 from "path";
|
|
311
|
+
import { createRequire } from "module";
|
|
312
|
+
import { pathToFileURL } from "url";
|
|
313
|
+
var BOOLEAN_COLUMNS_BY_TABLE, BOOLEAN_COLUMN_NAMES;
|
|
314
|
+
var init_database_adapter = __esm({
|
|
315
|
+
"src/lib/database-adapter.ts"() {
|
|
316
|
+
"use strict";
|
|
317
|
+
BOOLEAN_COLUMNS_BY_TABLE = {
|
|
318
|
+
memories: /* @__PURE__ */ new Set(["has_error", "draft"]),
|
|
319
|
+
behaviors: /* @__PURE__ */ new Set(["active"]),
|
|
320
|
+
notifications: /* @__PURE__ */ new Set(["read"]),
|
|
321
|
+
users: /* @__PURE__ */ new Set(["has_personal_memory"])
|
|
322
|
+
};
|
|
323
|
+
BOOLEAN_COLUMN_NAMES = new Set(
|
|
324
|
+
Object.values(BOOLEAN_COLUMNS_BY_TABLE).flatMap((cols) => [...cols])
|
|
325
|
+
);
|
|
270
326
|
}
|
|
271
327
|
});
|
|
272
328
|
|
|
273
329
|
// src/lib/database.ts
|
|
274
330
|
import { createClient } from "@libsql/client";
|
|
275
331
|
function getClient() {
|
|
276
|
-
if (!
|
|
332
|
+
if (!_adapterClient) {
|
|
277
333
|
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
278
334
|
}
|
|
335
|
+
if (process.env.DATABASE_URL) {
|
|
336
|
+
return _adapterClient;
|
|
337
|
+
}
|
|
279
338
|
if (process.env.EXE_IS_DAEMON === "1") {
|
|
280
339
|
return _resilientClient;
|
|
281
340
|
}
|
|
@@ -284,127 +343,45 @@ function getClient() {
|
|
|
284
343
|
}
|
|
285
344
|
return _resilientClient;
|
|
286
345
|
}
|
|
287
|
-
var _resilientClient, _daemonClient;
|
|
346
|
+
var _resilientClient, _daemonClient, _adapterClient;
|
|
288
347
|
var init_database = __esm({
|
|
289
348
|
"src/lib/database.ts"() {
|
|
290
349
|
"use strict";
|
|
291
350
|
init_db_retry();
|
|
292
351
|
init_employees();
|
|
352
|
+
init_database_adapter();
|
|
293
353
|
_resilientClient = null;
|
|
294
354
|
_daemonClient = null;
|
|
355
|
+
_adapterClient = null;
|
|
295
356
|
}
|
|
296
357
|
});
|
|
297
358
|
|
|
298
|
-
// src/lib/
|
|
299
|
-
import
|
|
300
|
-
import
|
|
301
|
-
import
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
314
|
-
await client.execute({
|
|
315
|
-
sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
|
|
316
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
|
317
|
-
args: [
|
|
318
|
-
id,
|
|
319
|
-
notification.agentId,
|
|
320
|
-
notification.agentRole,
|
|
321
|
-
notification.event,
|
|
322
|
-
notification.project,
|
|
323
|
-
notification.summary,
|
|
324
|
-
notification.taskFile ?? null,
|
|
325
|
-
now
|
|
326
|
-
]
|
|
327
|
-
});
|
|
328
|
-
} catch (err) {
|
|
329
|
-
process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
|
|
330
|
-
`);
|
|
359
|
+
// src/lib/session-registry.ts
|
|
360
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
361
|
+
import path4 from "path";
|
|
362
|
+
import os4 from "os";
|
|
363
|
+
function registerSession(entry) {
|
|
364
|
+
const dir = path4.dirname(REGISTRY_PATH);
|
|
365
|
+
if (!existsSync4(dir)) {
|
|
366
|
+
mkdirSync2(dir, { recursive: true });
|
|
367
|
+
}
|
|
368
|
+
const sessions = listSessions();
|
|
369
|
+
const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
|
|
370
|
+
if (idx >= 0) {
|
|
371
|
+
sessions[idx] = entry;
|
|
372
|
+
} else {
|
|
373
|
+
sessions.push(entry);
|
|
331
374
|
}
|
|
375
|
+
writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
332
376
|
}
|
|
333
|
-
|
|
377
|
+
function listSessions() {
|
|
334
378
|
try {
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
|
|
338
|
-
args: [taskFile]
|
|
339
|
-
});
|
|
379
|
+
const raw = readFileSync3(REGISTRY_PATH, "utf8");
|
|
380
|
+
return JSON.parse(raw);
|
|
340
381
|
} catch {
|
|
382
|
+
return [];
|
|
341
383
|
}
|
|
342
384
|
}
|
|
343
|
-
var init_notifications = __esm({
|
|
344
|
-
"src/lib/notifications.ts"() {
|
|
345
|
-
"use strict";
|
|
346
|
-
init_database();
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
// src/lib/state-bus.ts
|
|
351
|
-
var StateBus, orgBus;
|
|
352
|
-
var init_state_bus = __esm({
|
|
353
|
-
"src/lib/state-bus.ts"() {
|
|
354
|
-
"use strict";
|
|
355
|
-
StateBus = class {
|
|
356
|
-
handlers = /* @__PURE__ */ new Map();
|
|
357
|
-
globalHandlers = /* @__PURE__ */ new Set();
|
|
358
|
-
/** Emit an event to all subscribers */
|
|
359
|
-
emit(event) {
|
|
360
|
-
const typeHandlers = this.handlers.get(event.type);
|
|
361
|
-
if (typeHandlers) {
|
|
362
|
-
for (const handler of typeHandlers) {
|
|
363
|
-
try {
|
|
364
|
-
handler(event);
|
|
365
|
-
} catch {
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
for (const handler of this.globalHandlers) {
|
|
370
|
-
try {
|
|
371
|
-
handler(event);
|
|
372
|
-
} catch {
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
/** Subscribe to a specific event type */
|
|
377
|
-
on(type, handler) {
|
|
378
|
-
if (!this.handlers.has(type)) {
|
|
379
|
-
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
380
|
-
}
|
|
381
|
-
this.handlers.get(type).add(handler);
|
|
382
|
-
}
|
|
383
|
-
/** Subscribe to ALL events */
|
|
384
|
-
onAny(handler) {
|
|
385
|
-
this.globalHandlers.add(handler);
|
|
386
|
-
}
|
|
387
|
-
/** Unsubscribe from a specific event type */
|
|
388
|
-
off(type, handler) {
|
|
389
|
-
this.handlers.get(type)?.delete(handler);
|
|
390
|
-
}
|
|
391
|
-
/** Unsubscribe from ALL events */
|
|
392
|
-
offAny(handler) {
|
|
393
|
-
this.globalHandlers.delete(handler);
|
|
394
|
-
}
|
|
395
|
-
/** Remove all listeners */
|
|
396
|
-
clear() {
|
|
397
|
-
this.handlers.clear();
|
|
398
|
-
this.globalHandlers.clear();
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
orgBus = new StateBus();
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// src/lib/session-registry.ts
|
|
406
|
-
import path4 from "path";
|
|
407
|
-
import os4 from "os";
|
|
408
385
|
var REGISTRY_PATH;
|
|
409
386
|
var init_session_registry = __esm({
|
|
410
387
|
"src/lib/session-registry.ts"() {
|
|
@@ -590,13 +567,40 @@ var init_transport = __esm({
|
|
|
590
567
|
|
|
591
568
|
// src/lib/cc-agent-support.ts
|
|
592
569
|
import { execSync as execSync3 } from "child_process";
|
|
570
|
+
function _resetCcAgentSupportCache() {
|
|
571
|
+
_cachedSupport = null;
|
|
572
|
+
}
|
|
573
|
+
function claudeSupportsAgentFlag() {
|
|
574
|
+
if (_cachedSupport !== null) return _cachedSupport;
|
|
575
|
+
try {
|
|
576
|
+
const helpOutput = execSync3("claude --help 2>&1", {
|
|
577
|
+
encoding: "utf-8",
|
|
578
|
+
timeout: 5e3
|
|
579
|
+
});
|
|
580
|
+
_cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
|
|
581
|
+
} catch {
|
|
582
|
+
_cachedSupport = false;
|
|
583
|
+
}
|
|
584
|
+
return _cachedSupport;
|
|
585
|
+
}
|
|
586
|
+
var _cachedSupport;
|
|
593
587
|
var init_cc_agent_support = __esm({
|
|
594
588
|
"src/lib/cc-agent-support.ts"() {
|
|
595
589
|
"use strict";
|
|
590
|
+
_cachedSupport = null;
|
|
596
591
|
}
|
|
597
592
|
});
|
|
598
593
|
|
|
599
594
|
// src/lib/mcp-prefix.ts
|
|
595
|
+
function expandDualPrefixTools(shortNames) {
|
|
596
|
+
const out = [];
|
|
597
|
+
for (const name of shortNames) {
|
|
598
|
+
for (const prefix of MCP_TOOL_PREFIXES) {
|
|
599
|
+
out.push(prefix + name);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
600
604
|
var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
|
|
601
605
|
var init_mcp_prefix = __esm({
|
|
602
606
|
"src/lib/mcp-prefix.ts"() {
|
|
@@ -611,9 +615,26 @@ var init_mcp_prefix = __esm({
|
|
|
611
615
|
});
|
|
612
616
|
|
|
613
617
|
// src/lib/provider-table.ts
|
|
618
|
+
function detectActiveProvider(env = process.env) {
|
|
619
|
+
const baseUrl = env.ANTHROPIC_BASE_URL;
|
|
620
|
+
if (!baseUrl) return DEFAULT_PROVIDER;
|
|
621
|
+
for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
|
|
622
|
+
if (cfg.baseUrl === baseUrl) return name;
|
|
623
|
+
}
|
|
624
|
+
return DEFAULT_PROVIDER;
|
|
625
|
+
}
|
|
626
|
+
var PROVIDER_TABLE, DEFAULT_PROVIDER;
|
|
614
627
|
var init_provider_table = __esm({
|
|
615
628
|
"src/lib/provider-table.ts"() {
|
|
616
629
|
"use strict";
|
|
630
|
+
PROVIDER_TABLE = {
|
|
631
|
+
opencode: {
|
|
632
|
+
baseUrl: "https://opencode.ai/zen/go",
|
|
633
|
+
apiKeyEnv: "OPENCODE_API_KEY",
|
|
634
|
+
defaultModel: "minimax-m2.7"
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
DEFAULT_PROVIDER = "default";
|
|
617
638
|
}
|
|
618
639
|
});
|
|
619
640
|
|
|
@@ -645,10 +666,10 @@ var init_runtime_table = __esm({
|
|
|
645
666
|
});
|
|
646
667
|
|
|
647
668
|
// src/lib/agent-config.ts
|
|
648
|
-
import { readFileSync as readFileSync4, writeFileSync as
|
|
669
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
|
|
649
670
|
import path5 from "path";
|
|
650
671
|
function loadAgentConfig() {
|
|
651
|
-
if (!
|
|
672
|
+
if (!existsSync5(AGENT_CONFIG_PATH)) return {};
|
|
652
673
|
try {
|
|
653
674
|
return JSON.parse(readFileSync4(AGENT_CONFIG_PATH, "utf-8"));
|
|
654
675
|
} catch {
|
|
@@ -669,6 +690,7 @@ var init_agent_config = __esm({
|
|
|
669
690
|
"use strict";
|
|
670
691
|
init_config();
|
|
671
692
|
init_runtime_table();
|
|
693
|
+
init_secure_files();
|
|
672
694
|
AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
|
|
673
695
|
DEFAULT_MODELS = {
|
|
674
696
|
claude: "claude-opus-4",
|
|
@@ -687,16 +709,16 @@ __export(intercom_queue_exports, {
|
|
|
687
709
|
queueIntercom: () => queueIntercom,
|
|
688
710
|
readQueue: () => readQueue
|
|
689
711
|
});
|
|
690
|
-
import { readFileSync as readFileSync5, writeFileSync as
|
|
712
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
691
713
|
import path6 from "path";
|
|
692
714
|
import os5 from "os";
|
|
693
715
|
function ensureDir() {
|
|
694
716
|
const dir = path6.dirname(QUEUE_PATH);
|
|
695
|
-
if (!
|
|
717
|
+
if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
|
|
696
718
|
}
|
|
697
719
|
function readQueue() {
|
|
698
720
|
try {
|
|
699
|
-
if (!
|
|
721
|
+
if (!existsSync6(QUEUE_PATH)) return [];
|
|
700
722
|
return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
|
|
701
723
|
} catch {
|
|
702
724
|
return [];
|
|
@@ -705,7 +727,7 @@ function readQueue() {
|
|
|
705
727
|
function writeQueue(queue) {
|
|
706
728
|
ensureDir();
|
|
707
729
|
const tmp = `${QUEUE_PATH}.tmp`;
|
|
708
|
-
|
|
730
|
+
writeFileSync4(tmp, JSON.stringify(queue, null, 2));
|
|
709
731
|
renameSync3(tmp, QUEUE_PATH);
|
|
710
732
|
}
|
|
711
733
|
function queueIntercom(targetSession, reason) {
|
|
@@ -725,7 +747,7 @@ function queueIntercom(targetSession, reason) {
|
|
|
725
747
|
}
|
|
726
748
|
writeQueue(queue);
|
|
727
749
|
}
|
|
728
|
-
function drainQueue(
|
|
750
|
+
function drainQueue(isSessionBusy2, sendKeys) {
|
|
729
751
|
const queue = readQueue();
|
|
730
752
|
if (queue.length === 0) return { drained: 0, failed: 0 };
|
|
731
753
|
const remaining = [];
|
|
@@ -739,7 +761,7 @@ function drainQueue(isSessionBusy, sendKeys) {
|
|
|
739
761
|
continue;
|
|
740
762
|
}
|
|
741
763
|
try {
|
|
742
|
-
if (!
|
|
764
|
+
if (!isSessionBusy2(item.targetSession)) {
|
|
743
765
|
const success = sendKeys(item.targetSession);
|
|
744
766
|
if (success) {
|
|
745
767
|
logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
|
|
@@ -805,11 +827,14 @@ var init_intercom_queue = __esm({
|
|
|
805
827
|
});
|
|
806
828
|
|
|
807
829
|
// src/lib/license.ts
|
|
808
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
830
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
809
831
|
import { randomUUID } from "crypto";
|
|
832
|
+
import { createRequire as createRequire2 } from "module";
|
|
833
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
834
|
+
import os6 from "os";
|
|
810
835
|
import path7 from "path";
|
|
811
836
|
import { jwtVerify, importSPKI } from "jose";
|
|
812
|
-
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
|
|
837
|
+
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
813
838
|
var init_license = __esm({
|
|
814
839
|
"src/lib/license.ts"() {
|
|
815
840
|
"use strict";
|
|
@@ -817,13 +842,77 @@ var init_license = __esm({
|
|
|
817
842
|
LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
|
|
818
843
|
CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
|
|
819
844
|
DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
|
|
845
|
+
PLAN_LIMITS = {
|
|
846
|
+
free: { devices: 1, employees: 1, memories: 5e3 },
|
|
847
|
+
pro: { devices: 3, employees: 5, memories: 1e5 },
|
|
848
|
+
team: { devices: 10, employees: 20, memories: 1e6 },
|
|
849
|
+
agency: { devices: 50, employees: 100, memories: 1e7 },
|
|
850
|
+
enterprise: { devices: -1, employees: -1, memories: -1 }
|
|
851
|
+
};
|
|
820
852
|
}
|
|
821
853
|
});
|
|
822
854
|
|
|
823
855
|
// src/lib/plan-limits.ts
|
|
824
|
-
import { readFileSync as readFileSync7, existsSync as
|
|
856
|
+
import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
|
|
825
857
|
import path8 from "path";
|
|
826
|
-
|
|
858
|
+
function getLicenseSync() {
|
|
859
|
+
try {
|
|
860
|
+
if (!existsSync8(CACHE_PATH2)) return freeLicense();
|
|
861
|
+
const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
|
|
862
|
+
if (!raw.token || typeof raw.token !== "string") return freeLicense();
|
|
863
|
+
const parts = raw.token.split(".");
|
|
864
|
+
if (parts.length !== 3) return freeLicense();
|
|
865
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
866
|
+
const plan = payload.plan ?? "free";
|
|
867
|
+
const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
|
|
868
|
+
return {
|
|
869
|
+
valid: true,
|
|
870
|
+
plan,
|
|
871
|
+
email: payload.sub ?? "",
|
|
872
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
|
|
873
|
+
deviceLimit: limits.devices,
|
|
874
|
+
employeeLimit: limits.employees,
|
|
875
|
+
memoryLimit: limits.memories
|
|
876
|
+
};
|
|
877
|
+
} catch {
|
|
878
|
+
return freeLicense();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function freeLicense() {
|
|
882
|
+
const limits = PLAN_LIMITS.free;
|
|
883
|
+
return {
|
|
884
|
+
valid: true,
|
|
885
|
+
plan: "free",
|
|
886
|
+
email: "",
|
|
887
|
+
expiresAt: null,
|
|
888
|
+
deviceLimit: limits.devices,
|
|
889
|
+
employeeLimit: limits.employees,
|
|
890
|
+
memoryLimit: limits.memories
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function assertEmployeeLimitSync(rosterPath) {
|
|
894
|
+
const license = getLicenseSync();
|
|
895
|
+
if (license.employeeLimit < 0) return;
|
|
896
|
+
const filePath = rosterPath ?? EMPLOYEES_PATH;
|
|
897
|
+
let count = 0;
|
|
898
|
+
try {
|
|
899
|
+
if (existsSync8(filePath)) {
|
|
900
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
901
|
+
const employees = JSON.parse(raw);
|
|
902
|
+
count = Array.isArray(employees) ? employees.length : 0;
|
|
903
|
+
}
|
|
904
|
+
} catch {
|
|
905
|
+
throw new PlanLimitError(
|
|
906
|
+
`Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (count >= license.employeeLimit) {
|
|
910
|
+
throw new PlanLimitError(
|
|
911
|
+
`Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
var PlanLimitError, CACHE_PATH2;
|
|
827
916
|
var init_plan_limits = __esm({
|
|
828
917
|
"src/lib/plan-limits.ts"() {
|
|
829
918
|
"use strict";
|
|
@@ -831,25 +920,487 @@ var init_plan_limits = __esm({
|
|
|
831
920
|
init_employees();
|
|
832
921
|
init_license();
|
|
833
922
|
init_config();
|
|
923
|
+
PlanLimitError = class extends Error {
|
|
924
|
+
constructor(message) {
|
|
925
|
+
super(message);
|
|
926
|
+
this.name = "PlanLimitError";
|
|
927
|
+
}
|
|
928
|
+
};
|
|
834
929
|
CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
|
|
835
930
|
}
|
|
836
931
|
});
|
|
837
932
|
|
|
933
|
+
// src/lib/session-kill-telemetry.ts
|
|
934
|
+
import crypto from "crypto";
|
|
935
|
+
async function recordSessionKill(input) {
|
|
936
|
+
try {
|
|
937
|
+
const client = getClient();
|
|
938
|
+
await client.execute({
|
|
939
|
+
sql: `INSERT INTO session_kills
|
|
940
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
941
|
+
ticks_idle, estimated_tokens_saved)
|
|
942
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
943
|
+
args: [
|
|
944
|
+
crypto.randomUUID(),
|
|
945
|
+
input.sessionName,
|
|
946
|
+
input.agentId,
|
|
947
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
948
|
+
input.reason,
|
|
949
|
+
input.ticksIdle ?? null,
|
|
950
|
+
input.estimatedTokensSaved ?? null
|
|
951
|
+
]
|
|
952
|
+
});
|
|
953
|
+
} catch (err) {
|
|
954
|
+
process.stderr.write(
|
|
955
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
956
|
+
`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
var init_session_kill_telemetry = __esm({
|
|
961
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
962
|
+
"use strict";
|
|
963
|
+
init_database();
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// src/lib/capacity-monitor.ts
|
|
968
|
+
var capacity_monitor_exports = {};
|
|
969
|
+
__export(capacity_monitor_exports, {
|
|
970
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
971
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
972
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
973
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
974
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
975
|
+
extractContextPercent: () => extractContextPercent,
|
|
976
|
+
isAtCapacity: () => isAtCapacity,
|
|
977
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
978
|
+
pollCapacityDead: () => pollCapacityDead
|
|
979
|
+
});
|
|
980
|
+
function resumeTaskTitle(agentId) {
|
|
981
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
982
|
+
}
|
|
983
|
+
function buildResumeContext(agentId, openTasks) {
|
|
984
|
+
const taskList = openTasks.map(
|
|
985
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
986
|
+
).join("\n");
|
|
987
|
+
return [
|
|
988
|
+
"## Context",
|
|
989
|
+
"",
|
|
990
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
991
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
992
|
+
"",
|
|
993
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
994
|
+
"",
|
|
995
|
+
taskList,
|
|
996
|
+
"",
|
|
997
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
998
|
+
].join("\n");
|
|
999
|
+
}
|
|
1000
|
+
function filterPaneContent(paneOutput) {
|
|
1001
|
+
return paneOutput.split("\n").filter((line) => {
|
|
1002
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
1003
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
1004
|
+
if (line.includes(marker)) return false;
|
|
1005
|
+
}
|
|
1006
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
1007
|
+
if (re.test(line)) return false;
|
|
1008
|
+
}
|
|
1009
|
+
return true;
|
|
1010
|
+
}).join("\n");
|
|
1011
|
+
}
|
|
1012
|
+
function extractContextPercent(paneOutput) {
|
|
1013
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
1014
|
+
if (!match) return null;
|
|
1015
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
1016
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1017
|
+
}
|
|
1018
|
+
function isAtCapacity(paneOutput) {
|
|
1019
|
+
const filtered = filterPaneContent(paneOutput);
|
|
1020
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
1021
|
+
}
|
|
1022
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
1023
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
1024
|
+
if (pendingSince === void 0) {
|
|
1025
|
+
_pendingCapacityKill.set(agentId, now);
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
1029
|
+
_pendingCapacityKill.set(agentId, now);
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
_pendingCapacityKill.delete(agentId);
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
function _resetPendingCapacityKills() {
|
|
1036
|
+
_pendingCapacityKill.clear();
|
|
1037
|
+
}
|
|
1038
|
+
function _resetLastRelaunchCache() {
|
|
1039
|
+
_lastRelaunch.clear();
|
|
1040
|
+
}
|
|
1041
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
1042
|
+
const client = getClient();
|
|
1043
|
+
const cmScope = sessionScopeFilter(null);
|
|
1044
|
+
const result = await client.execute({
|
|
1045
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
1046
|
+
FROM tasks
|
|
1047
|
+
WHERE assigned_to = ? AND title LIKE ?${cmScope.sql}`,
|
|
1048
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`, ...cmScope.args]
|
|
1049
|
+
});
|
|
1050
|
+
const raw = result.rows[0]?.last_created_at;
|
|
1051
|
+
if (raw === null || raw === void 0) return null;
|
|
1052
|
+
const parsed = Date.parse(String(raw));
|
|
1053
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1054
|
+
}
|
|
1055
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
1056
|
+
const cached = _lastRelaunch.get(agentId);
|
|
1057
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
1058
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
1059
|
+
if (persisted === null) return false;
|
|
1060
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
1061
|
+
_lastRelaunch.set(agentId, persisted);
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
1065
|
+
const client = getClient();
|
|
1066
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1067
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
1068
|
+
const rdScope = sessionScopeFilter(null);
|
|
1069
|
+
const existing = await client.execute({
|
|
1070
|
+
sql: `SELECT id FROM tasks
|
|
1071
|
+
WHERE assigned_to = ?
|
|
1072
|
+
AND title LIKE ?
|
|
1073
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})${rdScope.sql}
|
|
1074
|
+
ORDER BY created_at DESC
|
|
1075
|
+
LIMIT 1`,
|
|
1076
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES, ...rdScope.args]
|
|
1077
|
+
});
|
|
1078
|
+
if (existing.rows.length > 0) {
|
|
1079
|
+
const taskId = String(existing.rows[0].id);
|
|
1080
|
+
await client.execute({
|
|
1081
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
1082
|
+
args: [context, now, taskId]
|
|
1083
|
+
});
|
|
1084
|
+
return { created: false, taskId };
|
|
1085
|
+
}
|
|
1086
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
1087
|
+
const task = await createTask2({
|
|
1088
|
+
title: resumeTaskTitle(agentId),
|
|
1089
|
+
assignedTo: agentId,
|
|
1090
|
+
assignedBy: "system",
|
|
1091
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
1092
|
+
priority: "p0",
|
|
1093
|
+
context,
|
|
1094
|
+
baseDir: projectDir
|
|
1095
|
+
});
|
|
1096
|
+
return { created: true, taskId: task.id };
|
|
1097
|
+
}
|
|
1098
|
+
async function pollCapacityDead() {
|
|
1099
|
+
const transport = getTransport();
|
|
1100
|
+
const relaunched = [];
|
|
1101
|
+
const registered = listSessions().filter(
|
|
1102
|
+
(s) => !isCoordinatorName(s.agentId)
|
|
1103
|
+
);
|
|
1104
|
+
if (registered.length === 0) return [];
|
|
1105
|
+
let liveSessions;
|
|
1106
|
+
try {
|
|
1107
|
+
liveSessions = transport.listSessions();
|
|
1108
|
+
} catch {
|
|
1109
|
+
return [];
|
|
1110
|
+
}
|
|
1111
|
+
for (const entry of registered) {
|
|
1112
|
+
const { windowName, agentId, projectDir } = entry;
|
|
1113
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
1114
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
1115
|
+
let pane;
|
|
1116
|
+
try {
|
|
1117
|
+
pane = transport.capturePane(windowName, 15);
|
|
1118
|
+
} catch {
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (!isAtCapacity(pane)) continue;
|
|
1122
|
+
const ctxPct = extractContextPercent(pane);
|
|
1123
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
1124
|
+
process.stderr.write(
|
|
1125
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
1126
|
+
`
|
|
1127
|
+
);
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
if (!confirmCapacityKill(agentId)) {
|
|
1131
|
+
process.stderr.write(
|
|
1132
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
1133
|
+
`
|
|
1134
|
+
);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
1138
|
+
if (!verify.atCapacity) {
|
|
1139
|
+
process.stderr.write(
|
|
1140
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
1141
|
+
`
|
|
1142
|
+
);
|
|
1143
|
+
void recordSessionKill({
|
|
1144
|
+
sessionName: windowName,
|
|
1145
|
+
agentId,
|
|
1146
|
+
reason: "capacity_false_positive_blocked"
|
|
1147
|
+
});
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
process.stderr.write(
|
|
1151
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
1152
|
+
`
|
|
1153
|
+
);
|
|
1154
|
+
try {
|
|
1155
|
+
transport.kill(windowName);
|
|
1156
|
+
void recordSessionKill({
|
|
1157
|
+
sessionName: windowName,
|
|
1158
|
+
agentId,
|
|
1159
|
+
reason: "capacity"
|
|
1160
|
+
});
|
|
1161
|
+
const client = getClient();
|
|
1162
|
+
const rlScope = sessionScopeFilter(null);
|
|
1163
|
+
const openTasks = await client.execute({
|
|
1164
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
1165
|
+
FROM tasks
|
|
1166
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')${rlScope.sql}
|
|
1167
|
+
ORDER BY
|
|
1168
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
1169
|
+
created_at ASC
|
|
1170
|
+
LIMIT 10`,
|
|
1171
|
+
args: [agentId, ...rlScope.args]
|
|
1172
|
+
});
|
|
1173
|
+
if (openTasks.rows.length === 0) {
|
|
1174
|
+
process.stderr.write(
|
|
1175
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
1176
|
+
`
|
|
1177
|
+
);
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
const { created } = await createOrRefreshResumeTask(
|
|
1181
|
+
agentId,
|
|
1182
|
+
projectDir,
|
|
1183
|
+
openTasks.rows
|
|
1184
|
+
);
|
|
1185
|
+
if (created) {
|
|
1186
|
+
await writeNotification({
|
|
1187
|
+
agentId: "system",
|
|
1188
|
+
agentRole: "daemon",
|
|
1189
|
+
event: "capacity_relaunch",
|
|
1190
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
1191
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
1195
|
+
if (created) relaunched.push(agentId);
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
process.stderr.write(
|
|
1198
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
1199
|
+
`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return relaunched;
|
|
1204
|
+
}
|
|
1205
|
+
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;
|
|
1206
|
+
var init_capacity_monitor = __esm({
|
|
1207
|
+
"src/lib/capacity-monitor.ts"() {
|
|
1208
|
+
"use strict";
|
|
1209
|
+
init_session_registry();
|
|
1210
|
+
init_transport();
|
|
1211
|
+
init_notifications();
|
|
1212
|
+
init_database();
|
|
1213
|
+
init_session_kill_telemetry();
|
|
1214
|
+
init_tmux_routing();
|
|
1215
|
+
init_task_scope();
|
|
1216
|
+
init_employees();
|
|
1217
|
+
CAPACITY_PATTERNS = [
|
|
1218
|
+
/conversation is too long/i,
|
|
1219
|
+
/maximum context length/i,
|
|
1220
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
1221
|
+
/reached.*(?:token|context).*limit/i
|
|
1222
|
+
];
|
|
1223
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
1224
|
+
CONTENT_LINE_MARKERS = [
|
|
1225
|
+
"RESUME:",
|
|
1226
|
+
"intercom",
|
|
1227
|
+
"capacity-monitor",
|
|
1228
|
+
"CAPACITY_PATTERNS",
|
|
1229
|
+
"isAtCapacity",
|
|
1230
|
+
"CONTENT_LINE_MARKERS",
|
|
1231
|
+
"pollCapacityDead",
|
|
1232
|
+
"confirmCapacityKill",
|
|
1233
|
+
"session_kills",
|
|
1234
|
+
"capacity-monitor.test"
|
|
1235
|
+
];
|
|
1236
|
+
SOURCE_CODE_MARKERS = [
|
|
1237
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
1238
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
1239
|
+
];
|
|
1240
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
1241
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
1242
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
1243
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
1244
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
1245
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
1246
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
1247
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
1248
|
+
CTX_FLOOR_PERCENT = 50;
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
|
|
838
1252
|
// src/lib/tmux-routing.ts
|
|
1253
|
+
var tmux_routing_exports = {};
|
|
1254
|
+
__export(tmux_routing_exports, {
|
|
1255
|
+
acquireSpawnLock: () => acquireSpawnLock,
|
|
1256
|
+
employeeSessionName: () => employeeSessionName,
|
|
1257
|
+
ensureEmployee: () => ensureEmployee,
|
|
1258
|
+
extractRootExe: () => extractRootExe,
|
|
1259
|
+
findFreeInstance: () => findFreeInstance,
|
|
1260
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
1261
|
+
getMySession: () => getMySession,
|
|
1262
|
+
getParentExe: () => getParentExe,
|
|
1263
|
+
getSessionState: () => getSessionState,
|
|
1264
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
1265
|
+
isExeSession: () => isExeSession,
|
|
1266
|
+
isSessionBusy: () => isSessionBusy,
|
|
1267
|
+
notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
|
|
1268
|
+
notifyParentExe: () => notifyParentExe,
|
|
1269
|
+
parseParentExe: () => parseParentExe,
|
|
1270
|
+
registerParentExe: () => registerParentExe,
|
|
1271
|
+
releaseSpawnLock: () => releaseSpawnLock,
|
|
1272
|
+
resolveExeSession: () => resolveExeSession,
|
|
1273
|
+
sendIntercom: () => sendIntercom,
|
|
1274
|
+
spawnEmployee: () => spawnEmployee,
|
|
1275
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
1276
|
+
});
|
|
839
1277
|
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
840
|
-
import { readFileSync as readFileSync8, writeFileSync as
|
|
1278
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync } from "fs";
|
|
841
1279
|
import path9 from "path";
|
|
842
|
-
import
|
|
1280
|
+
import os7 from "os";
|
|
843
1281
|
import { fileURLToPath } from "url";
|
|
1282
|
+
import { unlinkSync as unlinkSync2 } from "fs";
|
|
1283
|
+
function spawnLockPath(sessionName) {
|
|
1284
|
+
return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
1285
|
+
}
|
|
1286
|
+
function isProcessAlive(pid) {
|
|
1287
|
+
try {
|
|
1288
|
+
process.kill(pid, 0);
|
|
1289
|
+
return true;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function acquireSpawnLock(sessionName) {
|
|
1295
|
+
if (!existsSync9(SPAWN_LOCK_DIR)) {
|
|
1296
|
+
mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
|
|
1297
|
+
}
|
|
1298
|
+
const lockFile = spawnLockPath(sessionName);
|
|
1299
|
+
if (existsSync9(lockFile)) {
|
|
1300
|
+
try {
|
|
1301
|
+
const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
|
|
1302
|
+
const age = Date.now() - lock.timestamp;
|
|
1303
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
function releaseSpawnLock(sessionName) {
|
|
1313
|
+
try {
|
|
1314
|
+
unlinkSync2(spawnLockPath(sessionName));
|
|
1315
|
+
} catch {
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function resolveBehaviorsExporterScript() {
|
|
1319
|
+
try {
|
|
1320
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
1321
|
+
const scriptPath = path9.join(
|
|
1322
|
+
path9.dirname(thisFile),
|
|
1323
|
+
"..",
|
|
1324
|
+
"bin",
|
|
1325
|
+
"exe-export-behaviors.js"
|
|
1326
|
+
);
|
|
1327
|
+
return existsSync9(scriptPath) ? scriptPath : null;
|
|
1328
|
+
} catch {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
1333
|
+
const script = resolveBehaviorsExporterScript();
|
|
1334
|
+
if (!script) return null;
|
|
1335
|
+
try {
|
|
1336
|
+
const output = execFileSync2(
|
|
1337
|
+
process.execPath,
|
|
1338
|
+
[script, agentId, projectName, sessionKey],
|
|
1339
|
+
{ encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
|
|
1340
|
+
).trim();
|
|
1341
|
+
return output.length > 0 ? output : null;
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
process.stderr.write(
|
|
1344
|
+
`[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
1345
|
+
`
|
|
1346
|
+
);
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
844
1350
|
function getMySession() {
|
|
845
1351
|
return getTransport().getMySession();
|
|
846
1352
|
}
|
|
1353
|
+
function isRootSession(name) {
|
|
1354
|
+
return name.length > 0 && !name.includes("-");
|
|
1355
|
+
}
|
|
1356
|
+
function employeeSessionName(employee, exeSession, instance) {
|
|
1357
|
+
if (!isRootSession(exeSession)) {
|
|
1358
|
+
const root = extractRootExe(exeSession);
|
|
1359
|
+
if (root) {
|
|
1360
|
+
process.stderr.write(
|
|
1361
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root session, using "${root}" instead
|
|
1362
|
+
`
|
|
1363
|
+
);
|
|
1364
|
+
exeSession = root;
|
|
1365
|
+
} else {
|
|
1366
|
+
throw new Error(
|
|
1367
|
+
`Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
1372
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
1373
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
1374
|
+
throw new Error(
|
|
1375
|
+
`Invalid session name "${name}" \u2014 must match {agent}-{rootSession} or {agent}{instance}-{rootSession}`
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
return name;
|
|
1379
|
+
}
|
|
1380
|
+
function parseParentExe(sessionName, agentId) {
|
|
1381
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1382
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
1383
|
+
const match = sessionName.match(regex);
|
|
1384
|
+
return match?.[1] ?? null;
|
|
1385
|
+
}
|
|
847
1386
|
function extractRootExe(name) {
|
|
848
1387
|
if (!name) return null;
|
|
849
1388
|
if (!name.includes("-")) return name;
|
|
850
1389
|
const parts = name.split("-").filter(Boolean);
|
|
851
1390
|
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
852
1391
|
}
|
|
1392
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
1393
|
+
if (!existsSync9(SESSION_CACHE)) {
|
|
1394
|
+
mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
1395
|
+
}
|
|
1396
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
1397
|
+
const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
1398
|
+
writeFileSync6(filePath, JSON.stringify({
|
|
1399
|
+
parentExe: rootExe,
|
|
1400
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
1401
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1402
|
+
}));
|
|
1403
|
+
}
|
|
853
1404
|
function getParentExe(sessionKey) {
|
|
854
1405
|
try {
|
|
855
1406
|
const data = JSON.parse(readFileSync8(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
@@ -891,9 +1442,47 @@ function resolveExeSession() {
|
|
|
891
1442
|
}
|
|
892
1443
|
return fromSessionName ?? mySession;
|
|
893
1444
|
}
|
|
1445
|
+
function isEmployeeAlive(sessionName) {
|
|
1446
|
+
return getTransport().isAlive(sessionName);
|
|
1447
|
+
}
|
|
1448
|
+
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
1449
|
+
const base = employeeSessionName(employeeName, exeSession);
|
|
1450
|
+
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
1451
|
+
for (let i = 2; i <= maxInstances; i++) {
|
|
1452
|
+
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
1453
|
+
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
1454
|
+
}
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
1458
|
+
const transport = getTransport();
|
|
1459
|
+
if (!transport.isAlive(sessionName)) {
|
|
1460
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
1461
|
+
}
|
|
1462
|
+
let pane;
|
|
1463
|
+
try {
|
|
1464
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
return {
|
|
1467
|
+
atCapacity: false,
|
|
1468
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
1472
|
+
if (!isAtCapacity2(pane)) {
|
|
1473
|
+
return {
|
|
1474
|
+
atCapacity: false,
|
|
1475
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
atCapacity: true,
|
|
1480
|
+
reason: "capacity banner matched in recent pane output"
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
894
1483
|
function readDebounceState() {
|
|
895
1484
|
try {
|
|
896
|
-
if (!
|
|
1485
|
+
if (!existsSync9(DEBOUNCE_FILE)) return {};
|
|
897
1486
|
const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
|
|
898
1487
|
const state = {};
|
|
899
1488
|
for (const [key, val] of Object.entries(raw)) {
|
|
@@ -910,8 +1499,8 @@ function readDebounceState() {
|
|
|
910
1499
|
}
|
|
911
1500
|
function writeDebounceState(state) {
|
|
912
1501
|
try {
|
|
913
|
-
if (!
|
|
914
|
-
|
|
1502
|
+
if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
1503
|
+
writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
|
|
915
1504
|
} catch {
|
|
916
1505
|
}
|
|
917
1506
|
}
|
|
@@ -968,6 +1557,10 @@ function getSessionState(sessionName) {
|
|
|
968
1557
|
return "offline";
|
|
969
1558
|
}
|
|
970
1559
|
}
|
|
1560
|
+
function isSessionBusy(sessionName) {
|
|
1561
|
+
const state = getSessionState(sessionName);
|
|
1562
|
+
return state === "thinking" || state === "tool";
|
|
1563
|
+
}
|
|
971
1564
|
function isExeSession(sessionName) {
|
|
972
1565
|
const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
|
|
973
1566
|
const coordinatorName = getCoordinatorName();
|
|
@@ -1006,7 +1599,7 @@ function sendIntercom(targetSession) {
|
|
|
1006
1599
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
1007
1600
|
const agent = baseAgentName(rawAgent);
|
|
1008
1601
|
const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
1009
|
-
if (
|
|
1602
|
+
if (existsSync9(markerPath)) {
|
|
1010
1603
|
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
|
|
1011
1604
|
return "debounced";
|
|
1012
1605
|
}
|
|
@@ -1016,8 +1609,8 @@ function sendIntercom(targetSession) {
|
|
|
1016
1609
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
1017
1610
|
const agent = baseAgentName(rawAgent);
|
|
1018
1611
|
const taskDir = path9.join(process.cwd(), "exe", agent);
|
|
1019
|
-
if (
|
|
1020
|
-
const files =
|
|
1612
|
+
if (existsSync9(taskDir)) {
|
|
1613
|
+
const files = readdirSync(taskDir).filter(
|
|
1021
1614
|
(f) => f.endsWith(".md") && f !== "DONE.txt"
|
|
1022
1615
|
);
|
|
1023
1616
|
if (files.length === 0) {
|
|
@@ -1076,7 +1669,353 @@ function notifyParentExe(sessionKey) {
|
|
|
1076
1669
|
}
|
|
1077
1670
|
return true;
|
|
1078
1671
|
}
|
|
1079
|
-
|
|
1672
|
+
function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
|
|
1673
|
+
const transport = getTransport();
|
|
1674
|
+
try {
|
|
1675
|
+
const sessions = transport.listSessions();
|
|
1676
|
+
if (!sessions.includes(coordinatorSession)) return false;
|
|
1677
|
+
execSync4(
|
|
1678
|
+
`tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
|
|
1679
|
+
{ timeout: 3e3 }
|
|
1680
|
+
);
|
|
1681
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
|
|
1682
|
+
return true;
|
|
1683
|
+
} catch {
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
1688
|
+
if (isCoordinatorName(employeeName)) {
|
|
1689
|
+
return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
|
|
1690
|
+
}
|
|
1691
|
+
try {
|
|
1692
|
+
assertEmployeeLimitSync();
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
if (err instanceof PlanLimitError) {
|
|
1695
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (employeeName.includes("-")) {
|
|
1699
|
+
const bare = employeeName.split("-")[0].replace(/\d+$/, "");
|
|
1700
|
+
return {
|
|
1701
|
+
status: "failed",
|
|
1702
|
+
sessionName: "",
|
|
1703
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
if (!isRootSession(exeSession)) {
|
|
1707
|
+
const root = extractRootExe(exeSession);
|
|
1708
|
+
if (root) {
|
|
1709
|
+
process.stderr.write(
|
|
1710
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root session). Auto-correcting to "${root}".
|
|
1711
|
+
`
|
|
1712
|
+
);
|
|
1713
|
+
exeSession = root;
|
|
1714
|
+
} else {
|
|
1715
|
+
return {
|
|
1716
|
+
status: "failed",
|
|
1717
|
+
sessionName: "",
|
|
1718
|
+
error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
let effectiveInstance = opts?.instance;
|
|
1723
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
1724
|
+
const free = findFreeInstance(
|
|
1725
|
+
employeeName,
|
|
1726
|
+
exeSession,
|
|
1727
|
+
opts.maxAutoInstances ?? 10
|
|
1728
|
+
);
|
|
1729
|
+
if (free === null) {
|
|
1730
|
+
return {
|
|
1731
|
+
status: "failed",
|
|
1732
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
1733
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
1737
|
+
}
|
|
1738
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
1739
|
+
if (isEmployeeAlive(sessionName)) {
|
|
1740
|
+
const result2 = sendIntercom(sessionName);
|
|
1741
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
1742
|
+
return { status: "intercom_sent", sessionName };
|
|
1743
|
+
}
|
|
1744
|
+
if (result2 === "delivered") {
|
|
1745
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
1746
|
+
}
|
|
1747
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
1748
|
+
}
|
|
1749
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
1750
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
1751
|
+
if (result.error) {
|
|
1752
|
+
return { status: "failed", sessionName, error: result.error };
|
|
1753
|
+
}
|
|
1754
|
+
return { status: "spawned", sessionName };
|
|
1755
|
+
}
|
|
1756
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
1757
|
+
const transport = getTransport();
|
|
1758
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
1759
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
1760
|
+
const logDir = path9.join(os7.homedir(), ".exe-os", "session-logs");
|
|
1761
|
+
const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
1762
|
+
if (!existsSync9(logDir)) {
|
|
1763
|
+
mkdirSync5(logDir, { recursive: true });
|
|
1764
|
+
}
|
|
1765
|
+
transport.kill(sessionName);
|
|
1766
|
+
let cleanupSuffix = "";
|
|
1767
|
+
try {
|
|
1768
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
1769
|
+
const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
1770
|
+
if (existsSync9(cleanupScript)) {
|
|
1771
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
1772
|
+
}
|
|
1773
|
+
} catch {
|
|
1774
|
+
}
|
|
1775
|
+
try {
|
|
1776
|
+
const claudeJsonPath = path9.join(os7.homedir(), ".claude.json");
|
|
1777
|
+
let claudeJson = {};
|
|
1778
|
+
try {
|
|
1779
|
+
claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
|
|
1780
|
+
} catch {
|
|
1781
|
+
}
|
|
1782
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
1783
|
+
const projects = claudeJson.projects;
|
|
1784
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
1785
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
1786
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
1787
|
+
writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
1788
|
+
} catch {
|
|
1789
|
+
}
|
|
1790
|
+
try {
|
|
1791
|
+
const settingsDir = path9.join(os7.homedir(), ".claude", "projects");
|
|
1792
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
1793
|
+
const projSettingsDir = path9.join(settingsDir, normalizedKey);
|
|
1794
|
+
const settingsPath = path9.join(projSettingsDir, "settings.json");
|
|
1795
|
+
let settings = {};
|
|
1796
|
+
try {
|
|
1797
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
|
|
1798
|
+
} catch {
|
|
1799
|
+
}
|
|
1800
|
+
const perms = settings.permissions ?? {};
|
|
1801
|
+
const allow = perms.allow ?? [];
|
|
1802
|
+
const toolNames = [
|
|
1803
|
+
"recall_my_memory",
|
|
1804
|
+
"store_memory",
|
|
1805
|
+
"create_task",
|
|
1806
|
+
"update_task",
|
|
1807
|
+
"list_tasks",
|
|
1808
|
+
"get_task",
|
|
1809
|
+
"ask_team_memory",
|
|
1810
|
+
"store_behavior",
|
|
1811
|
+
"get_identity",
|
|
1812
|
+
"send_message"
|
|
1813
|
+
];
|
|
1814
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
1815
|
+
let changed = false;
|
|
1816
|
+
for (const tool of requiredTools) {
|
|
1817
|
+
if (!allow.includes(tool)) {
|
|
1818
|
+
allow.push(tool);
|
|
1819
|
+
changed = true;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
if (changed) {
|
|
1823
|
+
perms.allow = allow;
|
|
1824
|
+
settings.permissions = perms;
|
|
1825
|
+
mkdirSync5(projSettingsDir, { recursive: true });
|
|
1826
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1827
|
+
}
|
|
1828
|
+
} catch {
|
|
1829
|
+
}
|
|
1830
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
1831
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
1832
|
+
const agentRtConfig = getAgentRuntime(employeeName);
|
|
1833
|
+
const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
|
|
1834
|
+
const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
|
|
1835
|
+
const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
1836
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
1837
|
+
let identityFlag = "";
|
|
1838
|
+
let behaviorsFlag = "";
|
|
1839
|
+
let legacyFallbackWarned = false;
|
|
1840
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
1841
|
+
const identityPath = path9.join(
|
|
1842
|
+
os7.homedir(),
|
|
1843
|
+
".exe-os",
|
|
1844
|
+
"identity",
|
|
1845
|
+
`${employeeName}.md`
|
|
1846
|
+
);
|
|
1847
|
+
_resetCcAgentSupportCache();
|
|
1848
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
1849
|
+
if (hasAgentFlag) {
|
|
1850
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
1851
|
+
} else if (existsSync9(identityPath)) {
|
|
1852
|
+
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
1853
|
+
legacyFallbackWarned = true;
|
|
1854
|
+
}
|
|
1855
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
1856
|
+
employeeName,
|
|
1857
|
+
path9.basename(spawnCwd),
|
|
1858
|
+
sessionName
|
|
1859
|
+
);
|
|
1860
|
+
if (behaviorsFile) {
|
|
1861
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (legacyFallbackWarned) {
|
|
1865
|
+
process.stderr.write(
|
|
1866
|
+
`[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.
|
|
1867
|
+
`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
let sessionContextFlag = "";
|
|
1871
|
+
try {
|
|
1872
|
+
const ctxDir = path9.join(os7.homedir(), ".exe-os", "session-cache");
|
|
1873
|
+
mkdirSync5(ctxDir, { recursive: true });
|
|
1874
|
+
const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
|
|
1875
|
+
const ctxContent = [
|
|
1876
|
+
`## Session Context`,
|
|
1877
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
1878
|
+
`Your parent coordinator session is ${exeSession}.`,
|
|
1879
|
+
`Your employees (if any) use the -${exeSession} suffix.`
|
|
1880
|
+
].join("\n");
|
|
1881
|
+
writeFileSync6(ctxFile, ctxContent);
|
|
1882
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
1883
|
+
} catch {
|
|
1884
|
+
}
|
|
1885
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
1886
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
1887
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
1888
|
+
if (cfg?.apiKeyEnv) {
|
|
1889
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
1890
|
+
if (keyVal) {
|
|
1891
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
if (useCodex) {
|
|
1896
|
+
const codexCfg = RUNTIME_TABLE.codex;
|
|
1897
|
+
if (codexCfg?.apiKeyEnv) {
|
|
1898
|
+
const keyVal = process.env[codexCfg.apiKeyEnv];
|
|
1899
|
+
if (keyVal) {
|
|
1900
|
+
envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
|
|
1904
|
+
}
|
|
1905
|
+
if (useOpencode) {
|
|
1906
|
+
const ocCfg = PROVIDER_TABLE.opencode;
|
|
1907
|
+
if (ocCfg?.apiKeyEnv) {
|
|
1908
|
+
const keyVal = process.env[ocCfg.apiKeyEnv];
|
|
1909
|
+
if (keyVal) {
|
|
1910
|
+
envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
|
|
1914
|
+
}
|
|
1915
|
+
if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
|
|
1916
|
+
const defaultClaudeModel = DEFAULT_MODELS.claude;
|
|
1917
|
+
if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
|
|
1918
|
+
envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
let spawnCommand;
|
|
1922
|
+
if (useExeAgent) {
|
|
1923
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
1924
|
+
} else if (useCodex) {
|
|
1925
|
+
process.stderr.write(
|
|
1926
|
+
`[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
|
|
1927
|
+
`
|
|
1928
|
+
);
|
|
1929
|
+
spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName} --session ${sessionName}${cleanupSuffix}`;
|
|
1930
|
+
} else if (useOpencode) {
|
|
1931
|
+
const binName = `${employeeName}-opencode`;
|
|
1932
|
+
process.stderr.write(
|
|
1933
|
+
`[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
|
|
1934
|
+
`
|
|
1935
|
+
);
|
|
1936
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
1937
|
+
} else if (useBinSymlink) {
|
|
1938
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
1939
|
+
process.stderr.write(
|
|
1940
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
1941
|
+
`
|
|
1942
|
+
);
|
|
1943
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
1944
|
+
} else {
|
|
1945
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
1946
|
+
}
|
|
1947
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
1948
|
+
cwd: spawnCwd,
|
|
1949
|
+
command: spawnCommand
|
|
1950
|
+
});
|
|
1951
|
+
if (spawnResult.error) {
|
|
1952
|
+
releaseSpawnLock(sessionName);
|
|
1953
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
1954
|
+
}
|
|
1955
|
+
transport.pipeLog(sessionName, logFile);
|
|
1956
|
+
try {
|
|
1957
|
+
const mySession = getMySession();
|
|
1958
|
+
const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
1959
|
+
writeFileSync6(dispatchInfo, JSON.stringify({
|
|
1960
|
+
dispatchedBy: mySession,
|
|
1961
|
+
rootExe: exeSession,
|
|
1962
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
|
|
1963
|
+
runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
|
|
1964
|
+
model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
|
|
1965
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1966
|
+
}));
|
|
1967
|
+
} catch {
|
|
1968
|
+
}
|
|
1969
|
+
let booted = false;
|
|
1970
|
+
for (let i = 0; i < 30; i++) {
|
|
1971
|
+
try {
|
|
1972
|
+
execSync4("sleep 0.5");
|
|
1973
|
+
} catch {
|
|
1974
|
+
}
|
|
1975
|
+
try {
|
|
1976
|
+
const pane = transport.capturePane(sessionName);
|
|
1977
|
+
if (useExeAgent) {
|
|
1978
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
1979
|
+
booted = true;
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
} else if (useCodex) {
|
|
1983
|
+
if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
|
|
1984
|
+
booted = true;
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
} else {
|
|
1988
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
1989
|
+
booted = true;
|
|
1990
|
+
break;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
} catch {
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
if (!booted) {
|
|
1997
|
+
releaseSpawnLock(sessionName);
|
|
1998
|
+
const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
|
|
1999
|
+
return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
|
|
2000
|
+
}
|
|
2001
|
+
if (!useExeAgent && !useCodex) {
|
|
2002
|
+
try {
|
|
2003
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
2004
|
+
} catch {
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
registerSession({
|
|
2008
|
+
windowName: sessionName,
|
|
2009
|
+
agentId: employeeName,
|
|
2010
|
+
projectDir: spawnCwd,
|
|
2011
|
+
parentExe: exeSession,
|
|
2012
|
+
pid: 0,
|
|
2013
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2014
|
+
});
|
|
2015
|
+
releaseSpawnLock(sessionName);
|
|
2016
|
+
return { sessionName };
|
|
2017
|
+
}
|
|
2018
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
1080
2019
|
var init_tmux_routing = __esm({
|
|
1081
2020
|
"src/lib/tmux-routing.ts"() {
|
|
1082
2021
|
"use strict";
|
|
@@ -1091,11 +2030,14 @@ var init_tmux_routing = __esm({
|
|
|
1091
2030
|
init_intercom_queue();
|
|
1092
2031
|
init_plan_limits();
|
|
1093
2032
|
init_employees();
|
|
1094
|
-
SPAWN_LOCK_DIR = path9.join(
|
|
1095
|
-
SESSION_CACHE = path9.join(
|
|
2033
|
+
SPAWN_LOCK_DIR = path9.join(os7.homedir(), ".exe-os", "spawn-locks");
|
|
2034
|
+
SESSION_CACHE = path9.join(os7.homedir(), ".exe-os", "session-cache");
|
|
2035
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
2036
|
+
VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
2037
|
+
VERIFY_PANE_LINES = 200;
|
|
1096
2038
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
1097
2039
|
CODEX_DEBOUNCE_MS = 12e4;
|
|
1098
|
-
INTERCOM_LOG2 = path9.join(
|
|
2040
|
+
INTERCOM_LOG2 = path9.join(os7.homedir(), ".exe-os", "intercom.log");
|
|
1099
2041
|
DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
|
|
1100
2042
|
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
1101
2043
|
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
|
|
@@ -1119,6 +2061,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
|
|
|
1119
2061
|
args: [scope]
|
|
1120
2062
|
};
|
|
1121
2063
|
}
|
|
2064
|
+
function strictSessionScopeFilter(sessionScope, tableAlias) {
|
|
2065
|
+
const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
|
|
2066
|
+
if (!scope) return { sql: "", args: [] };
|
|
2067
|
+
const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
|
|
2068
|
+
return {
|
|
2069
|
+
sql: ` AND ${col} = ?`,
|
|
2070
|
+
args: [scope]
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
1122
2073
|
var init_task_scope = __esm({
|
|
1123
2074
|
"src/lib/task-scope.ts"() {
|
|
1124
2075
|
"use strict";
|
|
@@ -1126,13 +2077,125 @@ var init_task_scope = __esm({
|
|
|
1126
2077
|
}
|
|
1127
2078
|
});
|
|
1128
2079
|
|
|
2080
|
+
// src/lib/notifications.ts
|
|
2081
|
+
import crypto2 from "crypto";
|
|
2082
|
+
import path10 from "path";
|
|
2083
|
+
import os8 from "os";
|
|
2084
|
+
import {
|
|
2085
|
+
readFileSync as readFileSync9,
|
|
2086
|
+
readdirSync as readdirSync2,
|
|
2087
|
+
unlinkSync as unlinkSync3,
|
|
2088
|
+
existsSync as existsSync10,
|
|
2089
|
+
rmdirSync
|
|
2090
|
+
} from "fs";
|
|
2091
|
+
async function writeNotification(notification) {
|
|
2092
|
+
try {
|
|
2093
|
+
const client = getClient();
|
|
2094
|
+
const id = crypto2.randomUUID();
|
|
2095
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2096
|
+
const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
|
|
2097
|
+
await client.execute({
|
|
2098
|
+
sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
|
|
2099
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
|
2100
|
+
args: [
|
|
2101
|
+
id,
|
|
2102
|
+
notification.agentId,
|
|
2103
|
+
notification.agentRole,
|
|
2104
|
+
notification.event,
|
|
2105
|
+
notification.project,
|
|
2106
|
+
notification.summary,
|
|
2107
|
+
notification.taskFile ?? null,
|
|
2108
|
+
sessionScope,
|
|
2109
|
+
now
|
|
2110
|
+
]
|
|
2111
|
+
});
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
|
|
2114
|
+
`);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
async function markAsReadByTaskFile(taskFile, sessionScope) {
|
|
2118
|
+
try {
|
|
2119
|
+
const client = getClient();
|
|
2120
|
+
const scope = strictSessionScopeFilter(sessionScope);
|
|
2121
|
+
await client.execute({
|
|
2122
|
+
sql: `UPDATE notifications SET read = 1
|
|
2123
|
+
WHERE task_file = ? AND read = 0${scope.sql}`,
|
|
2124
|
+
args: [taskFile, ...scope.args]
|
|
2125
|
+
});
|
|
2126
|
+
} catch {
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
var init_notifications = __esm({
|
|
2130
|
+
"src/lib/notifications.ts"() {
|
|
2131
|
+
"use strict";
|
|
2132
|
+
init_database();
|
|
2133
|
+
init_task_scope();
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
// src/lib/state-bus.ts
|
|
2138
|
+
var StateBus, orgBus;
|
|
2139
|
+
var init_state_bus = __esm({
|
|
2140
|
+
"src/lib/state-bus.ts"() {
|
|
2141
|
+
"use strict";
|
|
2142
|
+
StateBus = class {
|
|
2143
|
+
handlers = /* @__PURE__ */ new Map();
|
|
2144
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
2145
|
+
/** Emit an event to all subscribers */
|
|
2146
|
+
emit(event) {
|
|
2147
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
2148
|
+
if (typeHandlers) {
|
|
2149
|
+
for (const handler of typeHandlers) {
|
|
2150
|
+
try {
|
|
2151
|
+
handler(event);
|
|
2152
|
+
} catch {
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
for (const handler of this.globalHandlers) {
|
|
2157
|
+
try {
|
|
2158
|
+
handler(event);
|
|
2159
|
+
} catch {
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
/** Subscribe to a specific event type */
|
|
2164
|
+
on(type, handler) {
|
|
2165
|
+
if (!this.handlers.has(type)) {
|
|
2166
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
2167
|
+
}
|
|
2168
|
+
this.handlers.get(type).add(handler);
|
|
2169
|
+
}
|
|
2170
|
+
/** Subscribe to ALL events */
|
|
2171
|
+
onAny(handler) {
|
|
2172
|
+
this.globalHandlers.add(handler);
|
|
2173
|
+
}
|
|
2174
|
+
/** Unsubscribe from a specific event type */
|
|
2175
|
+
off(type, handler) {
|
|
2176
|
+
this.handlers.get(type)?.delete(handler);
|
|
2177
|
+
}
|
|
2178
|
+
/** Unsubscribe from ALL events */
|
|
2179
|
+
offAny(handler) {
|
|
2180
|
+
this.globalHandlers.delete(handler);
|
|
2181
|
+
}
|
|
2182
|
+
/** Remove all listeners */
|
|
2183
|
+
clear() {
|
|
2184
|
+
this.handlers.clear();
|
|
2185
|
+
this.globalHandlers.clear();
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
orgBus = new StateBus();
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
|
|
1129
2192
|
// src/lib/tasks-crud.ts
|
|
1130
|
-
import
|
|
1131
|
-
import
|
|
1132
|
-
import
|
|
2193
|
+
import crypto3 from "crypto";
|
|
2194
|
+
import path11 from "path";
|
|
2195
|
+
import os9 from "os";
|
|
1133
2196
|
import { execSync as execSync5 } from "child_process";
|
|
1134
2197
|
import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
|
|
1135
|
-
import { existsSync as
|
|
2198
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
1136
2199
|
async function writeCheckpoint(input) {
|
|
1137
2200
|
const client = getClient();
|
|
1138
2201
|
const row = await resolveTask(client, input.taskId);
|
|
@@ -1163,6 +2226,16 @@ async function writeCheckpoint(input) {
|
|
|
1163
2226
|
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
1164
2227
|
return { checkpointCount };
|
|
1165
2228
|
}
|
|
2229
|
+
function extractParentFromContext(contextBody) {
|
|
2230
|
+
if (!contextBody) return null;
|
|
2231
|
+
const match = contextBody.match(
|
|
2232
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
2233
|
+
);
|
|
2234
|
+
return match ? match[1].toLowerCase() : null;
|
|
2235
|
+
}
|
|
2236
|
+
function slugify(title) {
|
|
2237
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2238
|
+
}
|
|
1166
2239
|
function buildKeywordIndex() {
|
|
1167
2240
|
const idx = /* @__PURE__ */ new Map();
|
|
1168
2241
|
for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
|
|
@@ -1174,6 +2247,24 @@ function buildKeywordIndex() {
|
|
|
1174
2247
|
}
|
|
1175
2248
|
return idx;
|
|
1176
2249
|
}
|
|
2250
|
+
function checkLaneAffinity(title, context, assigneeName) {
|
|
2251
|
+
const employees = loadEmployeesSync();
|
|
2252
|
+
const employee = employees.find((e) => e.name === assigneeName);
|
|
2253
|
+
if (!employee) return void 0;
|
|
2254
|
+
const assigneeRole = employee.role;
|
|
2255
|
+
const text = `${title} ${context}`.toLowerCase();
|
|
2256
|
+
const matchedRoles = /* @__PURE__ */ new Set();
|
|
2257
|
+
for (const [keyword, roles] of KEYWORD_INDEX) {
|
|
2258
|
+
if (text.includes(keyword)) {
|
|
2259
|
+
for (const role of roles) matchedRoles.add(role);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
if (matchedRoles.size === 0) return void 0;
|
|
2263
|
+
if (matchedRoles.has(assigneeRole)) return void 0;
|
|
2264
|
+
if (assigneeRole === "COO") return void 0;
|
|
2265
|
+
const expectedRoles = Array.from(matchedRoles).join(" or ");
|
|
2266
|
+
return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
|
|
2267
|
+
}
|
|
1177
2268
|
async function resolveTask(client, identifier, scopeSession) {
|
|
1178
2269
|
const scope = sessionScopeFilter(scopeSession);
|
|
1179
2270
|
let result = await client.execute({
|
|
@@ -1218,6 +2309,207 @@ async function resolveTask(client, identifier, scopeSession) {
|
|
|
1218
2309
|
}
|
|
1219
2310
|
throw new Error(`Task not found: ${identifier}`);
|
|
1220
2311
|
}
|
|
2312
|
+
async function createTaskCore(input) {
|
|
2313
|
+
const client = getClient();
|
|
2314
|
+
const id = crypto3.randomUUID();
|
|
2315
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2316
|
+
const slug = slugify(input.title);
|
|
2317
|
+
let earlySessionScope = null;
|
|
2318
|
+
try {
|
|
2319
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
2320
|
+
earlySessionScope = resolveExeSession2();
|
|
2321
|
+
} catch {
|
|
2322
|
+
}
|
|
2323
|
+
const scope = earlySessionScope ?? "default";
|
|
2324
|
+
const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
|
|
2325
|
+
let blockedById = null;
|
|
2326
|
+
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
2327
|
+
if (input.blockedBy) {
|
|
2328
|
+
const blocker = await resolveTask(client, input.blockedBy);
|
|
2329
|
+
blockedById = String(blocker.id);
|
|
2330
|
+
}
|
|
2331
|
+
let parentTaskId = null;
|
|
2332
|
+
let parentRef = input.parentTaskId;
|
|
2333
|
+
if (!parentRef) {
|
|
2334
|
+
const extracted = extractParentFromContext(input.context);
|
|
2335
|
+
if (extracted) {
|
|
2336
|
+
parentRef = extracted;
|
|
2337
|
+
process.stderr.write(
|
|
2338
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
if (parentRef) {
|
|
2343
|
+
try {
|
|
2344
|
+
const parent = await resolveTask(client, parentRef);
|
|
2345
|
+
parentTaskId = String(parent.id);
|
|
2346
|
+
} catch (err) {
|
|
2347
|
+
if (!input.parentTaskId) {
|
|
2348
|
+
throw new Error(
|
|
2349
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
throw err;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
let warning;
|
|
2356
|
+
const dupScope = sessionScopeFilter();
|
|
2357
|
+
const dupCheck = await client.execute({
|
|
2358
|
+
sql: `SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')${dupScope.sql}`,
|
|
2359
|
+
args: [input.title, input.assignedTo, ...dupScope.args]
|
|
2360
|
+
});
|
|
2361
|
+
if (dupCheck.rows.length > 0) {
|
|
2362
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
2363
|
+
}
|
|
2364
|
+
if (!process.env.DISABLE_LANE_AFFINITY) {
|
|
2365
|
+
const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
|
|
2366
|
+
if (laneWarning) {
|
|
2367
|
+
warning = warning ? `${warning}
|
|
2368
|
+
${laneWarning}` : laneWarning;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (input.baseDir) {
|
|
2372
|
+
try {
|
|
2373
|
+
await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
2374
|
+
await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
2375
|
+
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
2376
|
+
await ensureGitignoreExe(input.baseDir);
|
|
2377
|
+
} catch {
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
const complexity = input.complexity ?? "standard";
|
|
2381
|
+
const sessionScope = earlySessionScope;
|
|
2382
|
+
await client.execute({
|
|
2383
|
+
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)
|
|
2384
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2385
|
+
args: [
|
|
2386
|
+
id,
|
|
2387
|
+
input.title,
|
|
2388
|
+
input.assignedTo,
|
|
2389
|
+
input.assignedBy,
|
|
2390
|
+
input.projectName,
|
|
2391
|
+
input.priority,
|
|
2392
|
+
initialStatus,
|
|
2393
|
+
taskFile,
|
|
2394
|
+
blockedById,
|
|
2395
|
+
parentTaskId,
|
|
2396
|
+
input.reviewer ?? null,
|
|
2397
|
+
input.context,
|
|
2398
|
+
complexity,
|
|
2399
|
+
input.budgetTokens ?? null,
|
|
2400
|
+
input.budgetFallbackModel ?? null,
|
|
2401
|
+
0,
|
|
2402
|
+
null,
|
|
2403
|
+
sessionScope,
|
|
2404
|
+
now,
|
|
2405
|
+
now
|
|
2406
|
+
]
|
|
2407
|
+
});
|
|
2408
|
+
if (input.baseDir) {
|
|
2409
|
+
try {
|
|
2410
|
+
const EXE_OS_DIR = path11.join(os9.homedir(), ".exe-os");
|
|
2411
|
+
const mdPath = path11.join(EXE_OS_DIR, taskFile);
|
|
2412
|
+
const mdDir = path11.dirname(mdPath);
|
|
2413
|
+
if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
|
|
2414
|
+
const reviewer = input.reviewer ?? input.assignedBy;
|
|
2415
|
+
const mdContent = `# ${input.title}
|
|
2416
|
+
|
|
2417
|
+
## MANDATORY: When done
|
|
2418
|
+
|
|
2419
|
+
You MUST call update_task with status "done" and a result summary when finished.
|
|
2420
|
+
If you skip this, your reviewer will not know you're done and your work won't be reviewed.
|
|
2421
|
+
Do NOT let a failed commit or any error prevent you from calling update_task(done).
|
|
2422
|
+
|
|
2423
|
+
**ID:** ${id}
|
|
2424
|
+
**Status:** ${initialStatus}
|
|
2425
|
+
**Priority:** ${input.priority}
|
|
2426
|
+
**Assigned by:** ${input.assignedBy}
|
|
2427
|
+
**Assigned to:** ${input.assignedTo}
|
|
2428
|
+
**Project:** ${input.projectName}
|
|
2429
|
+
**Created:** ${now.split("T")[0]}${parentTaskId ? `
|
|
2430
|
+
**Parent task:** ${parentTaskId}` : ""}
|
|
2431
|
+
**Reviewer:** ${reviewer}
|
|
2432
|
+
|
|
2433
|
+
## Context
|
|
2434
|
+
|
|
2435
|
+
${input.context}
|
|
2436
|
+
`;
|
|
2437
|
+
await writeFile3(mdPath, mdContent, "utf-8");
|
|
2438
|
+
} catch (err) {
|
|
2439
|
+
process.stderr.write(
|
|
2440
|
+
`[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
|
|
2441
|
+
`
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
return {
|
|
2446
|
+
id,
|
|
2447
|
+
title: input.title,
|
|
2448
|
+
assignedTo: input.assignedTo,
|
|
2449
|
+
assignedBy: input.assignedBy,
|
|
2450
|
+
projectName: input.projectName,
|
|
2451
|
+
priority: input.priority,
|
|
2452
|
+
status: initialStatus,
|
|
2453
|
+
taskFile,
|
|
2454
|
+
createdAt: now,
|
|
2455
|
+
updatedAt: now,
|
|
2456
|
+
warning,
|
|
2457
|
+
budgetTokens: input.budgetTokens ?? null,
|
|
2458
|
+
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
2459
|
+
tokensUsed: 0,
|
|
2460
|
+
tokensWarnedAt: null
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
async function listTasks(input) {
|
|
2464
|
+
const client = getClient();
|
|
2465
|
+
const conditions = [];
|
|
2466
|
+
const args = [];
|
|
2467
|
+
if (input.assignedTo) {
|
|
2468
|
+
conditions.push("assigned_to = ?");
|
|
2469
|
+
args.push(input.assignedTo);
|
|
2470
|
+
}
|
|
2471
|
+
if (input.status) {
|
|
2472
|
+
conditions.push("status = ?");
|
|
2473
|
+
args.push(input.status);
|
|
2474
|
+
} else {
|
|
2475
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
2476
|
+
}
|
|
2477
|
+
if (input.projectName) {
|
|
2478
|
+
conditions.push("project_name = ?");
|
|
2479
|
+
args.push(input.projectName);
|
|
2480
|
+
}
|
|
2481
|
+
if (input.priority) {
|
|
2482
|
+
conditions.push("priority = ?");
|
|
2483
|
+
args.push(input.priority);
|
|
2484
|
+
}
|
|
2485
|
+
const scope = sessionScopeFilter();
|
|
2486
|
+
if (scope.sql) {
|
|
2487
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
2488
|
+
args.push(...scope.args);
|
|
2489
|
+
}
|
|
2490
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2491
|
+
const result = await client.execute({
|
|
2492
|
+
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`,
|
|
2493
|
+
args
|
|
2494
|
+
});
|
|
2495
|
+
return result.rows.map((r) => ({
|
|
2496
|
+
id: String(r.id),
|
|
2497
|
+
title: String(r.title),
|
|
2498
|
+
assignedTo: String(r.assigned_to),
|
|
2499
|
+
assignedBy: String(r.assigned_by),
|
|
2500
|
+
projectName: String(r.project_name),
|
|
2501
|
+
priority: String(r.priority),
|
|
2502
|
+
status: String(r.status),
|
|
2503
|
+
taskFile: String(r.task_file),
|
|
2504
|
+
createdAt: String(r.created_at),
|
|
2505
|
+
updatedAt: String(r.updated_at),
|
|
2506
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
2507
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
2508
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
2509
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
2510
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
2511
|
+
}));
|
|
2512
|
+
}
|
|
1221
2513
|
function isTmuxSessionAlive(identifier) {
|
|
1222
2514
|
if (!identifier || identifier === "unknown") return true;
|
|
1223
2515
|
try {
|
|
@@ -1389,7 +2681,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
1389
2681
|
await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
|
|
1390
2682
|
} catch {
|
|
1391
2683
|
}
|
|
1392
|
-
if (input.status === "done" || input.status === "cancelled") {
|
|
2684
|
+
if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
|
|
1393
2685
|
try {
|
|
1394
2686
|
const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
|
|
1395
2687
|
clearQueueForAgent2(String(row.assigned_to));
|
|
@@ -1406,6 +2698,65 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
1406
2698
|
}
|
|
1407
2699
|
return { row, taskFile, now, taskId };
|
|
1408
2700
|
}
|
|
2701
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
2702
|
+
const client = getClient();
|
|
2703
|
+
const row = await resolveTask(client, taskId);
|
|
2704
|
+
const id = String(row.id);
|
|
2705
|
+
const taskFile = String(row.task_file);
|
|
2706
|
+
const assignedTo = String(row.assigned_to);
|
|
2707
|
+
const assignedBy = String(row.assigned_by);
|
|
2708
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
2709
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
2710
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
2711
|
+
}
|
|
2712
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
2713
|
+
const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
2714
|
+
try {
|
|
2715
|
+
if (existsSync11(archPath)) return;
|
|
2716
|
+
const template = [
|
|
2717
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
2718
|
+
"",
|
|
2719
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
2720
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2721
|
+
"",
|
|
2722
|
+
"## Overview",
|
|
2723
|
+
"",
|
|
2724
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
2725
|
+
"",
|
|
2726
|
+
"## Key Components",
|
|
2727
|
+
"",
|
|
2728
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
2729
|
+
"",
|
|
2730
|
+
"## Data Flow",
|
|
2731
|
+
"",
|
|
2732
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
2733
|
+
"",
|
|
2734
|
+
"## Invariants",
|
|
2735
|
+
"",
|
|
2736
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
2737
|
+
"",
|
|
2738
|
+
"## Dependencies",
|
|
2739
|
+
"",
|
|
2740
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
2741
|
+
""
|
|
2742
|
+
].join("\n");
|
|
2743
|
+
await writeFile3(archPath, template, "utf-8");
|
|
2744
|
+
} catch {
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
async function ensureGitignoreExe(baseDir) {
|
|
2748
|
+
const gitignorePath = path11.join(baseDir, ".gitignore");
|
|
2749
|
+
try {
|
|
2750
|
+
if (existsSync11(gitignorePath)) {
|
|
2751
|
+
const content = readFileSync10(gitignorePath, "utf-8");
|
|
2752
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
2753
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
2754
|
+
} else {
|
|
2755
|
+
await writeFile3(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
2756
|
+
}
|
|
2757
|
+
} catch {
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
1409
2760
|
var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
1410
2761
|
var init_tasks_crud = __esm({
|
|
1411
2762
|
"src/lib/tasks-crud.ts"() {
|
|
@@ -1428,8 +2779,128 @@ var init_tasks_crud = __esm({
|
|
|
1428
2779
|
});
|
|
1429
2780
|
|
|
1430
2781
|
// src/lib/tasks-review.ts
|
|
1431
|
-
import
|
|
1432
|
-
import { existsSync as
|
|
2782
|
+
import path12 from "path";
|
|
2783
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
2784
|
+
async function countPendingReviews(sessionScope) {
|
|
2785
|
+
const client = getClient();
|
|
2786
|
+
const scope = strictSessionScopeFilter(
|
|
2787
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2788
|
+
);
|
|
2789
|
+
const result = await client.execute({
|
|
2790
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
2791
|
+
WHERE status = 'needs_review'${scope.sql}`,
|
|
2792
|
+
args: [...scope.args]
|
|
2793
|
+
});
|
|
2794
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
2795
|
+
}
|
|
2796
|
+
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
2797
|
+
const client = getClient();
|
|
2798
|
+
const scope = strictSessionScopeFilter(
|
|
2799
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2800
|
+
);
|
|
2801
|
+
const result = await client.execute({
|
|
2802
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
2803
|
+
WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
|
|
2804
|
+
args: [sinceIso, ...scope.args]
|
|
2805
|
+
});
|
|
2806
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
2807
|
+
}
|
|
2808
|
+
async function listPendingReviews(limit, sessionScope) {
|
|
2809
|
+
const client = getClient();
|
|
2810
|
+
const scope = strictSessionScopeFilter(
|
|
2811
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2812
|
+
);
|
|
2813
|
+
const result = await client.execute({
|
|
2814
|
+
sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
|
|
2815
|
+
WHERE status = 'needs_review'${scope.sql}
|
|
2816
|
+
ORDER BY updated_at ASC LIMIT ?`,
|
|
2817
|
+
args: [...scope.args, limit]
|
|
2818
|
+
});
|
|
2819
|
+
return result.rows;
|
|
2820
|
+
}
|
|
2821
|
+
async function cleanupOrphanedReviews() {
|
|
2822
|
+
const client = getClient();
|
|
2823
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2824
|
+
const r1 = await client.execute({
|
|
2825
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
2826
|
+
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
2827
|
+
AND assigned_by = 'system'
|
|
2828
|
+
AND title LIKE 'Review:%'
|
|
2829
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
|
|
2830
|
+
args: [now]
|
|
2831
|
+
});
|
|
2832
|
+
const r1b = await client.execute({
|
|
2833
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
2834
|
+
WHERE status IN ('open', 'needs_review')
|
|
2835
|
+
AND title LIKE 'Review:%completed%'
|
|
2836
|
+
AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
|
|
2837
|
+
args: [now]
|
|
2838
|
+
});
|
|
2839
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
2840
|
+
const r2 = await client.execute({
|
|
2841
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
2842
|
+
WHERE status = 'needs_review'
|
|
2843
|
+
AND result IS NOT NULL
|
|
2844
|
+
AND updated_at < ?`,
|
|
2845
|
+
args: [now, staleThreshold]
|
|
2846
|
+
});
|
|
2847
|
+
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
2848
|
+
if (total > 0) {
|
|
2849
|
+
process.stderr.write(
|
|
2850
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
2851
|
+
`
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
return total;
|
|
2855
|
+
}
|
|
2856
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
2857
|
+
const roleLower = role.toLowerCase();
|
|
2858
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
2859
|
+
return {
|
|
2860
|
+
lens: "Code Quality (Engineer)",
|
|
2861
|
+
checklist: [
|
|
2862
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
2863
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
2864
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
2865
|
+
"4. Any regressions in the test suite?"
|
|
2866
|
+
]
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
2870
|
+
return {
|
|
2871
|
+
lens: "Architecture (CTO)",
|
|
2872
|
+
checklist: [
|
|
2873
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
2874
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
2875
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
2876
|
+
"4. Security implications? Any new attack surface?",
|
|
2877
|
+
"5. Does it scale? Performance considerations?",
|
|
2878
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
2879
|
+
]
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
2883
|
+
return {
|
|
2884
|
+
lens: "Strategic (COO)",
|
|
2885
|
+
checklist: [
|
|
2886
|
+
"1. Does this serve the project mission?",
|
|
2887
|
+
"2. Is this the right work at the right time?",
|
|
2888
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
2889
|
+
"4. Any cross-project implications?"
|
|
2890
|
+
]
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
return {
|
|
2894
|
+
lens: "General",
|
|
2895
|
+
checklist: [
|
|
2896
|
+
"1. Read the original task's acceptance criteria",
|
|
2897
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
2898
|
+
"3. Verify code changes match requirements",
|
|
2899
|
+
"4. Check if tests were added/updated",
|
|
2900
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
2901
|
+
]
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
1433
2904
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
1434
2905
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
1435
2906
|
try {
|
|
@@ -1474,11 +2945,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
|
1474
2945
|
);
|
|
1475
2946
|
}
|
|
1476
2947
|
try {
|
|
1477
|
-
const cacheDir =
|
|
1478
|
-
if (
|
|
2948
|
+
const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
|
|
2949
|
+
if (existsSync12(cacheDir)) {
|
|
1479
2950
|
for (const f of readdirSync3(cacheDir)) {
|
|
1480
2951
|
if (f.startsWith("review-notified-")) {
|
|
1481
|
-
|
|
2952
|
+
unlinkSync4(path12.join(cacheDir, f));
|
|
1482
2953
|
}
|
|
1483
2954
|
}
|
|
1484
2955
|
}
|
|
@@ -1495,11 +2966,12 @@ var init_tasks_review = __esm({
|
|
|
1495
2966
|
init_tmux_routing();
|
|
1496
2967
|
init_session_key();
|
|
1497
2968
|
init_state_bus();
|
|
2969
|
+
init_task_scope();
|
|
1498
2970
|
}
|
|
1499
2971
|
});
|
|
1500
2972
|
|
|
1501
2973
|
// src/lib/tasks-chain.ts
|
|
1502
|
-
import
|
|
2974
|
+
import path13 from "path";
|
|
1503
2975
|
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
1504
2976
|
async function cascadeUnblock(taskId, baseDir, now) {
|
|
1505
2977
|
const client = getClient();
|
|
@@ -1516,7 +2988,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
|
|
|
1516
2988
|
});
|
|
1517
2989
|
for (const ur of unblockedRows.rows) {
|
|
1518
2990
|
try {
|
|
1519
|
-
const ubFile =
|
|
2991
|
+
const ubFile = path13.join(baseDir, String(ur.task_file));
|
|
1520
2992
|
let ubContent = await readFile3(ubFile, "utf-8");
|
|
1521
2993
|
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
1522
2994
|
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
@@ -1551,7 +3023,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
|
|
|
1551
3023
|
const scScope = sessionScopeFilter();
|
|
1552
3024
|
const remaining = await client.execute({
|
|
1553
3025
|
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
1554
|
-
WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
|
|
3026
|
+
WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
|
|
1555
3027
|
args: [parentTaskId, ...scScope.args]
|
|
1556
3028
|
});
|
|
1557
3029
|
const cnt = Number(remaining.rows[0]?.cnt ?? 1);
|
|
@@ -1583,7 +3055,152 @@ var init_tasks_chain = __esm({
|
|
|
1583
3055
|
}
|
|
1584
3056
|
});
|
|
1585
3057
|
|
|
3058
|
+
// src/lib/project-name.ts
|
|
3059
|
+
import { execSync as execSync6 } from "child_process";
|
|
3060
|
+
import path14 from "path";
|
|
3061
|
+
function getProjectName(cwd) {
|
|
3062
|
+
const dir = cwd ?? process.cwd();
|
|
3063
|
+
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
3064
|
+
try {
|
|
3065
|
+
let repoRoot;
|
|
3066
|
+
try {
|
|
3067
|
+
const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
|
|
3068
|
+
cwd: dir,
|
|
3069
|
+
encoding: "utf8",
|
|
3070
|
+
timeout: 2e3,
|
|
3071
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3072
|
+
}).trim();
|
|
3073
|
+
repoRoot = path14.dirname(gitCommonDir);
|
|
3074
|
+
} catch {
|
|
3075
|
+
repoRoot = execSync6("git rev-parse --show-toplevel", {
|
|
3076
|
+
cwd: dir,
|
|
3077
|
+
encoding: "utf8",
|
|
3078
|
+
timeout: 2e3,
|
|
3079
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3080
|
+
}).trim();
|
|
3081
|
+
}
|
|
3082
|
+
_cached2 = path14.basename(repoRoot);
|
|
3083
|
+
_cachedCwd = dir;
|
|
3084
|
+
return _cached2;
|
|
3085
|
+
} catch {
|
|
3086
|
+
_cached2 = path14.basename(dir);
|
|
3087
|
+
_cachedCwd = dir;
|
|
3088
|
+
return _cached2;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
var _cached2, _cachedCwd;
|
|
3092
|
+
var init_project_name = __esm({
|
|
3093
|
+
"src/lib/project-name.ts"() {
|
|
3094
|
+
"use strict";
|
|
3095
|
+
_cached2 = null;
|
|
3096
|
+
_cachedCwd = null;
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
|
|
3100
|
+
// src/lib/session-scope.ts
|
|
3101
|
+
var session_scope_exports = {};
|
|
3102
|
+
__export(session_scope_exports, {
|
|
3103
|
+
assertSessionScope: () => assertSessionScope,
|
|
3104
|
+
findSessionForProject: () => findSessionForProject,
|
|
3105
|
+
getSessionProject: () => getSessionProject
|
|
3106
|
+
});
|
|
3107
|
+
function getSessionProject(sessionName) {
|
|
3108
|
+
const sessions = listSessions();
|
|
3109
|
+
const entry = sessions.find((s) => s.windowName === sessionName);
|
|
3110
|
+
if (!entry) return null;
|
|
3111
|
+
const parts = entry.projectDir.split("/").filter(Boolean);
|
|
3112
|
+
return parts[parts.length - 1] ?? null;
|
|
3113
|
+
}
|
|
3114
|
+
function findSessionForProject(projectName) {
|
|
3115
|
+
const sessions = listSessions();
|
|
3116
|
+
for (const s of sessions) {
|
|
3117
|
+
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
3118
|
+
if (proj === projectName && isCoordinatorName(s.agentId)) return s;
|
|
3119
|
+
}
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
function assertSessionScope(actionType, targetProject) {
|
|
3123
|
+
try {
|
|
3124
|
+
const currentProject = getProjectName();
|
|
3125
|
+
const exeSession = resolveExeSession();
|
|
3126
|
+
if (!exeSession) {
|
|
3127
|
+
return { allowed: true, reason: "no_session" };
|
|
3128
|
+
}
|
|
3129
|
+
if (currentProject === targetProject) {
|
|
3130
|
+
return {
|
|
3131
|
+
allowed: true,
|
|
3132
|
+
reason: "same_session",
|
|
3133
|
+
currentProject,
|
|
3134
|
+
targetProject
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
process.stderr.write(
|
|
3138
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
3139
|
+
`
|
|
3140
|
+
);
|
|
3141
|
+
return {
|
|
3142
|
+
allowed: false,
|
|
3143
|
+
reason: "cross_session_denied",
|
|
3144
|
+
currentProject,
|
|
3145
|
+
targetProject,
|
|
3146
|
+
targetSession: findSessionForProject(targetProject)?.windowName
|
|
3147
|
+
};
|
|
3148
|
+
} catch {
|
|
3149
|
+
return { allowed: true, reason: "no_session" };
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
var init_session_scope = __esm({
|
|
3153
|
+
"src/lib/session-scope.ts"() {
|
|
3154
|
+
"use strict";
|
|
3155
|
+
init_session_registry();
|
|
3156
|
+
init_project_name();
|
|
3157
|
+
init_tmux_routing();
|
|
3158
|
+
init_employees();
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
|
|
1586
3162
|
// src/lib/tasks-notify.ts
|
|
3163
|
+
async function dispatchTaskToEmployee(input) {
|
|
3164
|
+
if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
|
|
3165
|
+
let crossProject = false;
|
|
3166
|
+
if (input.projectName) {
|
|
3167
|
+
try {
|
|
3168
|
+
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
3169
|
+
const check = assertSessionScope2("dispatch_task", input.projectName);
|
|
3170
|
+
if (check.reason === "cross_session_denied") {
|
|
3171
|
+
crossProject = true;
|
|
3172
|
+
return { dispatched: "skipped", crossProject: true };
|
|
3173
|
+
}
|
|
3174
|
+
} catch {
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
try {
|
|
3178
|
+
const transport = getTransport();
|
|
3179
|
+
const exeSession = resolveExeSession();
|
|
3180
|
+
if (!exeSession) return { dispatched: "session_missing" };
|
|
3181
|
+
const sessionName = employeeSessionName(input.assignedTo, exeSession);
|
|
3182
|
+
if (transport.isAlive(sessionName)) {
|
|
3183
|
+
const result = sendIntercom(sessionName);
|
|
3184
|
+
const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
|
|
3185
|
+
return { dispatched, session: sessionName, crossProject };
|
|
3186
|
+
} else {
|
|
3187
|
+
const projectDir = input.projectDir ?? process.cwd();
|
|
3188
|
+
const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
|
|
3189
|
+
autoInstance: isMultiInstance(input.assignedTo)
|
|
3190
|
+
});
|
|
3191
|
+
if (result.status === "failed") {
|
|
3192
|
+
process.stderr.write(
|
|
3193
|
+
`[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
|
|
3194
|
+
`
|
|
3195
|
+
);
|
|
3196
|
+
return { dispatched: "session_missing" };
|
|
3197
|
+
}
|
|
3198
|
+
return { dispatched: "spawned", session: result.sessionName, crossProject };
|
|
3199
|
+
}
|
|
3200
|
+
} catch {
|
|
3201
|
+
return { dispatched: "session_missing" };
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
1587
3204
|
function notifyTaskDone() {
|
|
1588
3205
|
try {
|
|
1589
3206
|
const key = getSessionKey();
|
|
@@ -1609,10 +3226,10 @@ var init_tasks_notify = __esm({
|
|
|
1609
3226
|
});
|
|
1610
3227
|
|
|
1611
3228
|
// src/lib/behaviors.ts
|
|
1612
|
-
import
|
|
3229
|
+
import crypto4 from "crypto";
|
|
1613
3230
|
async function storeBehavior(opts) {
|
|
1614
3231
|
const client = getClient();
|
|
1615
|
-
const id =
|
|
3232
|
+
const id = crypto4.randomUUID();
|
|
1616
3233
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1617
3234
|
await client.execute({
|
|
1618
3235
|
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
@@ -1641,7 +3258,7 @@ __export(skill_learning_exports, {
|
|
|
1641
3258
|
storeTrajectory: () => storeTrajectory,
|
|
1642
3259
|
sweepTrajectories: () => sweepTrajectories
|
|
1643
3260
|
});
|
|
1644
|
-
import
|
|
3261
|
+
import crypto5 from "crypto";
|
|
1645
3262
|
async function extractTrajectory(taskId, agentId) {
|
|
1646
3263
|
const client = getClient();
|
|
1647
3264
|
const result = await client.execute({
|
|
@@ -1670,11 +3287,11 @@ async function extractTrajectory(taskId, agentId) {
|
|
|
1670
3287
|
return signature;
|
|
1671
3288
|
}
|
|
1672
3289
|
function hashSignature(signature) {
|
|
1673
|
-
return
|
|
3290
|
+
return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
1674
3291
|
}
|
|
1675
3292
|
async function storeTrajectory(opts) {
|
|
1676
3293
|
const client = getClient();
|
|
1677
|
-
const id =
|
|
3294
|
+
const id = crypto5.randomUUID();
|
|
1678
3295
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1679
3296
|
const signatureHash = hashSignature(opts.signature);
|
|
1680
3297
|
await client.execute({
|
|
@@ -1919,29 +3536,63 @@ var init_skill_learning = __esm({
|
|
|
1919
3536
|
});
|
|
1920
3537
|
|
|
1921
3538
|
// src/lib/tasks.ts
|
|
1922
|
-
|
|
1923
|
-
|
|
3539
|
+
var tasks_exports = {};
|
|
3540
|
+
__export(tasks_exports, {
|
|
3541
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
3542
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
3543
|
+
countPendingReviews: () => countPendingReviews,
|
|
3544
|
+
createTask: () => createTask,
|
|
3545
|
+
createTaskCore: () => createTaskCore,
|
|
3546
|
+
deleteTask: () => deleteTask,
|
|
3547
|
+
deleteTaskCore: () => deleteTaskCore,
|
|
3548
|
+
ensureArchitectureDoc: () => ensureArchitectureDoc,
|
|
3549
|
+
ensureGitignoreExe: () => ensureGitignoreExe,
|
|
3550
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
3551
|
+
listPendingReviews: () => listPendingReviews,
|
|
3552
|
+
listTasks: () => listTasks,
|
|
3553
|
+
resolveTask: () => resolveTask,
|
|
3554
|
+
slugify: () => slugify,
|
|
3555
|
+
updateTask: () => updateTask,
|
|
3556
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
3557
|
+
writeCheckpoint: () => writeCheckpoint
|
|
3558
|
+
});
|
|
3559
|
+
import path15 from "path";
|
|
3560
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
|
|
3561
|
+
async function createTask(input) {
|
|
3562
|
+
const result = await createTaskCore(input);
|
|
3563
|
+
if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
|
|
3564
|
+
dispatchTaskToEmployee({
|
|
3565
|
+
assignedTo: input.assignedTo,
|
|
3566
|
+
title: input.title,
|
|
3567
|
+
priority: input.priority,
|
|
3568
|
+
taskFile: result.taskFile,
|
|
3569
|
+
initialStatus: result.status,
|
|
3570
|
+
projectName: input.projectName
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
return result;
|
|
3574
|
+
}
|
|
1924
3575
|
async function updateTask(input) {
|
|
1925
3576
|
const { row, taskFile, now, taskId } = await updateTaskStatus(input);
|
|
1926
3577
|
try {
|
|
1927
3578
|
const agent = String(row.assigned_to);
|
|
1928
|
-
const cacheDir =
|
|
1929
|
-
const cachePath =
|
|
3579
|
+
const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
|
|
3580
|
+
const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
|
|
1930
3581
|
if (input.status === "in_progress") {
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
3582
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
3583
|
+
writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
3584
|
+
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
|
|
1934
3585
|
try {
|
|
1935
|
-
|
|
3586
|
+
unlinkSync5(cachePath);
|
|
1936
3587
|
} catch {
|
|
1937
3588
|
}
|
|
1938
3589
|
}
|
|
1939
3590
|
} catch {
|
|
1940
3591
|
}
|
|
1941
|
-
if (input.status === "done") {
|
|
3592
|
+
if (input.status === "done" || input.status === "closed") {
|
|
1942
3593
|
await cleanupReviewFile(row, taskFile, input.baseDir);
|
|
1943
3594
|
}
|
|
1944
|
-
if (input.status === "done" || input.status === "cancelled") {
|
|
3595
|
+
if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
|
|
1945
3596
|
try {
|
|
1946
3597
|
const client = getClient();
|
|
1947
3598
|
const taskTitle = String(row.title);
|
|
@@ -1957,7 +3608,7 @@ async function updateTask(input) {
|
|
|
1957
3608
|
if (!isCoordinatorName(assignedAgent)) {
|
|
1958
3609
|
try {
|
|
1959
3610
|
const draftClient = getClient();
|
|
1960
|
-
if (input.status === "done") {
|
|
3611
|
+
if (input.status === "done" || input.status === "closed") {
|
|
1961
3612
|
await draftClient.execute({
|
|
1962
3613
|
sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
|
|
1963
3614
|
args: [assignedAgent]
|
|
@@ -1974,7 +3625,7 @@ async function updateTask(input) {
|
|
|
1974
3625
|
try {
|
|
1975
3626
|
const client = getClient();
|
|
1976
3627
|
const cascaded = await client.execute({
|
|
1977
|
-
sql: `UPDATE tasks SET status = '
|
|
3628
|
+
sql: `UPDATE tasks SET status = 'closed', updated_at = ?
|
|
1978
3629
|
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
1979
3630
|
args: [now, taskId]
|
|
1980
3631
|
});
|
|
@@ -1987,14 +3638,14 @@ async function updateTask(input) {
|
|
|
1987
3638
|
} catch {
|
|
1988
3639
|
}
|
|
1989
3640
|
}
|
|
1990
|
-
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
3641
|
+
const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
|
|
1991
3642
|
if (isTerminal) {
|
|
1992
3643
|
const isCoordinator = isCoordinatorName(String(row.assigned_to));
|
|
1993
3644
|
if (!isCoordinator) {
|
|
1994
3645
|
notifyTaskDone();
|
|
1995
3646
|
}
|
|
1996
3647
|
await markTaskNotificationsRead(taskFile);
|
|
1997
|
-
if (input.status === "done") {
|
|
3648
|
+
if (input.status === "done" || input.status === "closed") {
|
|
1998
3649
|
try {
|
|
1999
3650
|
await cascadeUnblock(taskId, input.baseDir, now);
|
|
2000
3651
|
} catch {
|
|
@@ -2014,7 +3665,7 @@ async function updateTask(input) {
|
|
|
2014
3665
|
}
|
|
2015
3666
|
}
|
|
2016
3667
|
}
|
|
2017
|
-
if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
3668
|
+
if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
2018
3669
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
2019
3670
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
2020
3671
|
taskId,
|
|
@@ -2054,6 +3705,21 @@ async function updateTask(input) {
|
|
|
2054
3705
|
nextTask
|
|
2055
3706
|
};
|
|
2056
3707
|
}
|
|
3708
|
+
async function deleteTask(taskId, baseDir) {
|
|
3709
|
+
const client = getClient();
|
|
3710
|
+
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
3711
|
+
const coordinatorName = getCoordinatorName();
|
|
3712
|
+
const reviewer = assignedBy || coordinatorName;
|
|
3713
|
+
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
3714
|
+
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
3715
|
+
const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
|
|
3716
|
+
await client.execute({
|
|
3717
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
|
|
3718
|
+
args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
|
|
3719
|
+
});
|
|
3720
|
+
await markAsReadByTaskFile(taskFile);
|
|
3721
|
+
await markAsReadByTaskFile(reviewFile);
|
|
3722
|
+
}
|
|
2057
3723
|
var init_tasks = __esm({
|
|
2058
3724
|
"src/lib/tasks.ts"() {
|
|
2059
3725
|
"use strict";
|
|
@@ -2081,9 +3747,9 @@ __export(active_agent_exports, {
|
|
|
2081
3747
|
resolveActiveAgentFromTmuxSession: () => resolveActiveAgentFromTmuxSession,
|
|
2082
3748
|
writeActiveAgent: () => writeActiveAgent
|
|
2083
3749
|
});
|
|
2084
|
-
import { readFileSync as
|
|
2085
|
-
import { execSync as
|
|
2086
|
-
import
|
|
3750
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync6, readdirSync as readdirSync4 } from "fs";
|
|
3751
|
+
import { execSync as execSync7 } from "child_process";
|
|
3752
|
+
import path16 from "path";
|
|
2087
3753
|
function isNameWithOptionalInstance(candidate, baseName) {
|
|
2088
3754
|
if (candidate === baseName) return true;
|
|
2089
3755
|
if (!candidate.startsWith(baseName)) return false;
|
|
@@ -2127,12 +3793,12 @@ function resolveActiveAgentFromTmuxSession(sessionName) {
|
|
|
2127
3793
|
return null;
|
|
2128
3794
|
}
|
|
2129
3795
|
function getMarkerPath() {
|
|
2130
|
-
return
|
|
3796
|
+
return path16.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
|
|
2131
3797
|
}
|
|
2132
3798
|
function writeActiveAgent(agentId, agentRole) {
|
|
2133
3799
|
try {
|
|
2134
|
-
|
|
2135
|
-
|
|
3800
|
+
mkdirSync7(CACHE_DIR, { recursive: true });
|
|
3801
|
+
writeFileSync8(
|
|
2136
3802
|
getMarkerPath(),
|
|
2137
3803
|
JSON.stringify({ agentId, agentRole, startedAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
2138
3804
|
);
|
|
@@ -2141,21 +3807,21 @@ function writeActiveAgent(agentId, agentRole) {
|
|
|
2141
3807
|
}
|
|
2142
3808
|
function clearActiveAgent() {
|
|
2143
3809
|
try {
|
|
2144
|
-
|
|
3810
|
+
unlinkSync6(getMarkerPath());
|
|
2145
3811
|
} catch {
|
|
2146
3812
|
}
|
|
2147
3813
|
}
|
|
2148
3814
|
function getActiveAgent() {
|
|
2149
3815
|
try {
|
|
2150
3816
|
const markerPath = getMarkerPath();
|
|
2151
|
-
const raw =
|
|
3817
|
+
const raw = readFileSync11(markerPath, "utf8");
|
|
2152
3818
|
const data = JSON.parse(raw);
|
|
2153
3819
|
if (data.agentId) {
|
|
2154
3820
|
if (data.startedAt) {
|
|
2155
3821
|
const age = Date.now() - new Date(data.startedAt).getTime();
|
|
2156
3822
|
if (age > STALE_MS) {
|
|
2157
3823
|
try {
|
|
2158
|
-
|
|
3824
|
+
unlinkSync6(markerPath);
|
|
2159
3825
|
} catch {
|
|
2160
3826
|
}
|
|
2161
3827
|
} else {
|
|
@@ -2174,7 +3840,7 @@ function getActiveAgent() {
|
|
|
2174
3840
|
} catch {
|
|
2175
3841
|
}
|
|
2176
3842
|
try {
|
|
2177
|
-
const sessionName =
|
|
3843
|
+
const sessionName = execSync7(
|
|
2178
3844
|
"tmux display-message -p '#{session_name}' 2>/dev/null",
|
|
2179
3845
|
{ encoding: "utf8", timeout: 2e3 }
|
|
2180
3846
|
).trim();
|
|
@@ -2196,14 +3862,14 @@ function getAllActiveAgents() {
|
|
|
2196
3862
|
const key = file.slice("active-agent-".length, -".json".length);
|
|
2197
3863
|
if (key === "undefined") continue;
|
|
2198
3864
|
try {
|
|
2199
|
-
const raw =
|
|
3865
|
+
const raw = readFileSync11(path16.join(CACHE_DIR, file), "utf8");
|
|
2200
3866
|
const data = JSON.parse(raw);
|
|
2201
3867
|
if (!data.agentId) continue;
|
|
2202
3868
|
if (data.startedAt) {
|
|
2203
3869
|
const age = Date.now() - new Date(data.startedAt).getTime();
|
|
2204
3870
|
if (age > STALE_MS) {
|
|
2205
3871
|
try {
|
|
2206
|
-
|
|
3872
|
+
unlinkSync6(path16.join(CACHE_DIR, file));
|
|
2207
3873
|
} catch {
|
|
2208
3874
|
}
|
|
2209
3875
|
continue;
|
|
@@ -2226,11 +3892,11 @@ function getAllActiveAgents() {
|
|
|
2226
3892
|
function cleanupSessionMarkers() {
|
|
2227
3893
|
const key = getSessionKey();
|
|
2228
3894
|
try {
|
|
2229
|
-
|
|
3895
|
+
unlinkSync6(path16.join(CACHE_DIR, `active-agent-${key}.json`));
|
|
2230
3896
|
} catch {
|
|
2231
3897
|
}
|
|
2232
3898
|
try {
|
|
2233
|
-
|
|
3899
|
+
unlinkSync6(path16.join(CACHE_DIR, "active-agent-undefined.json"));
|
|
2234
3900
|
} catch {
|
|
2235
3901
|
}
|
|
2236
3902
|
}
|
|
@@ -2241,7 +3907,7 @@ var init_active_agent = __esm({
|
|
|
2241
3907
|
init_config();
|
|
2242
3908
|
init_session_key();
|
|
2243
3909
|
init_employees();
|
|
2244
|
-
CACHE_DIR =
|
|
3910
|
+
CACHE_DIR = path16.join(EXE_AI_DIR, "session-cache");
|
|
2245
3911
|
STALE_MS = 24 * 60 * 60 * 1e3;
|
|
2246
3912
|
}
|
|
2247
3913
|
});
|
|
@@ -2260,7 +3926,7 @@ function registerUpdateTask(server) {
|
|
|
2260
3926
|
description: "Update task status. Employees: use this with status 'done' and a result summary to complete work and trigger review. Accepts UUID, slug (filename), or title substring.",
|
|
2261
3927
|
inputSchema: {
|
|
2262
3928
|
task_id: z.string().describe("Task identifier \u2014 UUID, slug (e.g. 'fix-auth-bug'), or title substring"),
|
|
2263
|
-
status: z.enum(["open", "in_progress", "done", "needs_review", "blocked", "cancelled"]).describe("New status"),
|
|
3929
|
+
status: z.enum(["open", "in_progress", "done", "needs_review", "blocked", "cancelled", "closed"]).describe("New status"),
|
|
2264
3930
|
result: z.string().optional().describe("Result summary (include when status=done)")
|
|
2265
3931
|
}
|
|
2266
3932
|
},
|
|
@@ -2350,7 +4016,17 @@ function registerUpdateTask(server) {
|
|
|
2350
4016
|
}
|
|
2351
4017
|
let text = `Task "${task.title}" marked ${task.status}.
|
|
2352
4018
|
File: ${task.taskFile}`;
|
|
2353
|
-
const isTerminal = status === "done" || status === "needs_review";
|
|
4019
|
+
const isTerminal = status === "done" || status === "needs_review" || status === "closed";
|
|
4020
|
+
if (isTerminal && task.assignedBy) {
|
|
4021
|
+
try {
|
|
4022
|
+
const { notifyCoordinatorTaskCompletion: notifyCoordinatorTaskCompletion2, resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
4023
|
+
const coordinatorSession = resolveExeSession2();
|
|
4024
|
+
if (coordinatorSession) {
|
|
4025
|
+
notifyCoordinatorTaskCompletion2(coordinatorSession, callerAgentId ?? "agent", task.title);
|
|
4026
|
+
}
|
|
4027
|
+
} catch {
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
2354
4030
|
if (isTerminal && task.nextTask) {
|
|
2355
4031
|
text += `
|
|
2356
4032
|
|