@askexenow/exe-os 0.9.8 → 0.9.10
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 +222 -49
- package/dist/bin/backfill-responses.js +221 -48
- package/dist/bin/backfill-vectors.js +225 -52
- package/dist/bin/cleanup-stale-review-tasks.js +150 -28
- package/dist/bin/cli.js +1411 -953
- package/dist/bin/exe-agent-config.js +36 -8
- package/dist/bin/exe-agent.js +14 -4
- package/dist/bin/exe-assign.js +221 -48
- package/dist/bin/exe-boot.js +913 -543
- package/dist/bin/exe-call.js +41 -13
- package/dist/bin/exe-cloud.js +163 -58
- package/dist/bin/exe-dispatch.js +418 -262
- package/dist/bin/exe-doctor.js +145 -27
- package/dist/bin/exe-export-behaviors.js +141 -23
- package/dist/bin/exe-forget.js +137 -19
- package/dist/bin/exe-gateway.js +793 -485
- package/dist/bin/exe-heartbeat.js +227 -108
- package/dist/bin/exe-kill.js +138 -20
- package/dist/bin/exe-launch-agent.js +172 -39
- package/dist/bin/exe-link.js +291 -100
- package/dist/bin/exe-new-employee.js +214 -106
- package/dist/bin/exe-pending-messages.js +395 -33
- package/dist/bin/exe-pending-notifications.js +684 -99
- package/dist/bin/exe-pending-reviews.js +420 -74
- package/dist/bin/exe-rename.js +147 -49
- package/dist/bin/exe-review.js +138 -20
- package/dist/bin/exe-search.js +240 -69
- package/dist/bin/exe-session-cleanup.js +566 -357
- package/dist/bin/exe-settings.js +61 -17
- package/dist/bin/exe-start-codex.js +158 -39
- package/dist/bin/exe-start-opencode.js +157 -38
- package/dist/bin/exe-status.js +151 -29
- package/dist/bin/exe-team.js +138 -20
- package/dist/bin/git-sweep.js +530 -319
- package/dist/bin/graph-backfill.js +137 -19
- package/dist/bin/graph-export.js +140 -22
- package/dist/bin/install.js +90 -61
- package/dist/bin/scan-tasks.js +547 -336
- package/dist/bin/setup.js +564 -293
- package/dist/bin/shard-migrate.js +139 -21
- package/dist/bin/update.js +138 -49
- package/dist/bin/wiki-sync.js +137 -19
- package/dist/gateway/index.js +649 -417
- package/dist/hooks/bug-report-worker.js +486 -316
- package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
- package/dist/hooks/commit-complete.js +528 -317
- package/dist/hooks/error-recall.js +245 -74
- package/dist/hooks/exe-heartbeat-hook.js +16 -6
- package/dist/hooks/ingest-worker.js +3442 -3157
- package/dist/hooks/ingest.js +832 -97
- package/dist/hooks/instructions-loaded.js +227 -54
- package/dist/hooks/notification.js +216 -43
- package/dist/hooks/post-compact.js +239 -62
- package/dist/hooks/pre-compact.js +534 -323
- package/dist/hooks/pre-tool-use.js +268 -90
- package/dist/hooks/prompt-ingest-worker.js +352 -102
- package/dist/hooks/prompt-submit.js +614 -382
- package/dist/hooks/response-ingest-worker.js +372 -122
- package/dist/hooks/session-end.js +569 -347
- package/dist/hooks/session-start.js +313 -127
- package/dist/hooks/stop.js +293 -98
- package/dist/hooks/subagent-stop.js +239 -62
- package/dist/hooks/summary-worker.js +568 -236
- package/dist/index.js +664 -431
- package/dist/lib/agent-config.js +28 -6
- package/dist/lib/cloud-sync.js +284 -105
- package/dist/lib/config.js +30 -10
- package/dist/lib/consolidation.js +16 -6
- package/dist/lib/database.js +123 -25
- package/dist/lib/db-daemon-client.js +73 -19
- package/dist/lib/db.js +123 -25
- package/dist/lib/device-registry.js +133 -35
- package/dist/lib/embedder.js +107 -32
- package/dist/lib/employee-templates.js +14 -4
- package/dist/lib/employees.js +41 -13
- package/dist/lib/exe-daemon-client.js +88 -22
- package/dist/lib/exe-daemon.js +1049 -680
- package/dist/lib/hybrid-search.js +240 -69
- package/dist/lib/identity.js +18 -8
- package/dist/lib/license.js +133 -48
- package/dist/lib/messaging.js +116 -56
- package/dist/lib/reminders.js +14 -4
- package/dist/lib/schedules.js +137 -19
- package/dist/lib/skill-learning.js +33 -6
- package/dist/lib/store.js +137 -19
- package/dist/lib/task-router.js +14 -4
- package/dist/lib/tasks.js +422 -357
- package/dist/lib/tmux-routing.js +314 -248
- package/dist/lib/token-spend.js +26 -8
- package/dist/mcp/server.js +1408 -672
- package/dist/mcp/tools/complete-reminder.js +14 -4
- package/dist/mcp/tools/create-reminder.js +14 -4
- package/dist/mcp/tools/create-task.js +448 -371
- package/dist/mcp/tools/deactivate-behavior.js +16 -6
- package/dist/mcp/tools/list-reminders.js +14 -4
- package/dist/mcp/tools/list-tasks.js +123 -107
- package/dist/mcp/tools/send-message.js +75 -29
- package/dist/mcp/tools/update-task.js +1983 -315
- package/dist/runtime/index.js +567 -355
- package/dist/tui/App.js +887 -531
- package/package.json +4 -4
|
@@ -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,6 +300,7 @@ 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"]);
|
|
270
304
|
IDENTITY_DIR = path2.join(EXE_AI_DIR, "identity");
|
|
271
305
|
}
|
|
272
306
|
});
|
|
@@ -322,121 +356,37 @@ var init_database = __esm({
|
|
|
322
356
|
}
|
|
323
357
|
});
|
|
324
358
|
|
|
325
|
-
// src/lib/
|
|
326
|
-
import
|
|
359
|
+
// src/lib/session-registry.ts
|
|
360
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
327
361
|
import path4 from "path";
|
|
328
362
|
import os4 from "os";
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
341
|
-
await client.execute({
|
|
342
|
-
sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
|
|
343
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
|
344
|
-
args: [
|
|
345
|
-
id,
|
|
346
|
-
notification.agentId,
|
|
347
|
-
notification.agentRole,
|
|
348
|
-
notification.event,
|
|
349
|
-
notification.project,
|
|
350
|
-
notification.summary,
|
|
351
|
-
notification.taskFile ?? null,
|
|
352
|
-
now
|
|
353
|
-
]
|
|
354
|
-
});
|
|
355
|
-
} catch (err) {
|
|
356
|
-
process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
|
|
357
|
-
`);
|
|
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);
|
|
358
374
|
}
|
|
375
|
+
writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
359
376
|
}
|
|
360
|
-
|
|
377
|
+
function listSessions() {
|
|
361
378
|
try {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
|
|
365
|
-
args: [taskFile]
|
|
366
|
-
});
|
|
379
|
+
const raw = readFileSync3(REGISTRY_PATH, "utf8");
|
|
380
|
+
return JSON.parse(raw);
|
|
367
381
|
} catch {
|
|
382
|
+
return [];
|
|
368
383
|
}
|
|
369
384
|
}
|
|
370
|
-
var init_notifications = __esm({
|
|
371
|
-
"src/lib/notifications.ts"() {
|
|
372
|
-
"use strict";
|
|
373
|
-
init_database();
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// src/lib/state-bus.ts
|
|
378
|
-
var StateBus, orgBus;
|
|
379
|
-
var init_state_bus = __esm({
|
|
380
|
-
"src/lib/state-bus.ts"() {
|
|
381
|
-
"use strict";
|
|
382
|
-
StateBus = class {
|
|
383
|
-
handlers = /* @__PURE__ */ new Map();
|
|
384
|
-
globalHandlers = /* @__PURE__ */ new Set();
|
|
385
|
-
/** Emit an event to all subscribers */
|
|
386
|
-
emit(event) {
|
|
387
|
-
const typeHandlers = this.handlers.get(event.type);
|
|
388
|
-
if (typeHandlers) {
|
|
389
|
-
for (const handler of typeHandlers) {
|
|
390
|
-
try {
|
|
391
|
-
handler(event);
|
|
392
|
-
} catch {
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
for (const handler of this.globalHandlers) {
|
|
397
|
-
try {
|
|
398
|
-
handler(event);
|
|
399
|
-
} catch {
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/** Subscribe to a specific event type */
|
|
404
|
-
on(type, handler) {
|
|
405
|
-
if (!this.handlers.has(type)) {
|
|
406
|
-
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
407
|
-
}
|
|
408
|
-
this.handlers.get(type).add(handler);
|
|
409
|
-
}
|
|
410
|
-
/** Subscribe to ALL events */
|
|
411
|
-
onAny(handler) {
|
|
412
|
-
this.globalHandlers.add(handler);
|
|
413
|
-
}
|
|
414
|
-
/** Unsubscribe from a specific event type */
|
|
415
|
-
off(type, handler) {
|
|
416
|
-
this.handlers.get(type)?.delete(handler);
|
|
417
|
-
}
|
|
418
|
-
/** Unsubscribe from ALL events */
|
|
419
|
-
offAny(handler) {
|
|
420
|
-
this.globalHandlers.delete(handler);
|
|
421
|
-
}
|
|
422
|
-
/** Remove all listeners */
|
|
423
|
-
clear() {
|
|
424
|
-
this.handlers.clear();
|
|
425
|
-
this.globalHandlers.clear();
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
orgBus = new StateBus();
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
// src/lib/session-registry.ts
|
|
433
|
-
import path5 from "path";
|
|
434
|
-
import os5 from "os";
|
|
435
385
|
var REGISTRY_PATH;
|
|
436
386
|
var init_session_registry = __esm({
|
|
437
387
|
"src/lib/session-registry.ts"() {
|
|
438
388
|
"use strict";
|
|
439
|
-
REGISTRY_PATH =
|
|
389
|
+
REGISTRY_PATH = path4.join(os4.homedir(), ".exe-os", "session-registry.json");
|
|
440
390
|
}
|
|
441
391
|
});
|
|
442
392
|
|
|
@@ -617,13 +567,40 @@ var init_transport = __esm({
|
|
|
617
567
|
|
|
618
568
|
// src/lib/cc-agent-support.ts
|
|
619
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;
|
|
620
587
|
var init_cc_agent_support = __esm({
|
|
621
588
|
"src/lib/cc-agent-support.ts"() {
|
|
622
589
|
"use strict";
|
|
590
|
+
_cachedSupport = null;
|
|
623
591
|
}
|
|
624
592
|
});
|
|
625
593
|
|
|
626
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
|
+
}
|
|
627
604
|
var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
|
|
628
605
|
var init_mcp_prefix = __esm({
|
|
629
606
|
"src/lib/mcp-prefix.ts"() {
|
|
@@ -638,9 +615,26 @@ var init_mcp_prefix = __esm({
|
|
|
638
615
|
});
|
|
639
616
|
|
|
640
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;
|
|
641
627
|
var init_provider_table = __esm({
|
|
642
628
|
"src/lib/provider-table.ts"() {
|
|
643
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";
|
|
644
638
|
}
|
|
645
639
|
});
|
|
646
640
|
|
|
@@ -672,10 +666,10 @@ var init_runtime_table = __esm({
|
|
|
672
666
|
});
|
|
673
667
|
|
|
674
668
|
// src/lib/agent-config.ts
|
|
675
|
-
import { readFileSync as readFileSync4, writeFileSync as
|
|
676
|
-
import
|
|
669
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
|
|
670
|
+
import path5 from "path";
|
|
677
671
|
function loadAgentConfig() {
|
|
678
|
-
if (!
|
|
672
|
+
if (!existsSync5(AGENT_CONFIG_PATH)) return {};
|
|
679
673
|
try {
|
|
680
674
|
return JSON.parse(readFileSync4(AGENT_CONFIG_PATH, "utf-8"));
|
|
681
675
|
} catch {
|
|
@@ -696,7 +690,8 @@ var init_agent_config = __esm({
|
|
|
696
690
|
"use strict";
|
|
697
691
|
init_config();
|
|
698
692
|
init_runtime_table();
|
|
699
|
-
|
|
693
|
+
init_secure_files();
|
|
694
|
+
AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
|
|
700
695
|
DEFAULT_MODELS = {
|
|
701
696
|
claude: "claude-opus-4",
|
|
702
697
|
codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
|
|
@@ -714,16 +709,16 @@ __export(intercom_queue_exports, {
|
|
|
714
709
|
queueIntercom: () => queueIntercom,
|
|
715
710
|
readQueue: () => readQueue
|
|
716
711
|
});
|
|
717
|
-
import { readFileSync as readFileSync5, writeFileSync as
|
|
718
|
-
import
|
|
719
|
-
import
|
|
712
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
713
|
+
import path6 from "path";
|
|
714
|
+
import os5 from "os";
|
|
720
715
|
function ensureDir() {
|
|
721
|
-
const dir =
|
|
722
|
-
if (!
|
|
716
|
+
const dir = path6.dirname(QUEUE_PATH);
|
|
717
|
+
if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
|
|
723
718
|
}
|
|
724
719
|
function readQueue() {
|
|
725
720
|
try {
|
|
726
|
-
if (!
|
|
721
|
+
if (!existsSync6(QUEUE_PATH)) return [];
|
|
727
722
|
return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
|
|
728
723
|
} catch {
|
|
729
724
|
return [];
|
|
@@ -732,7 +727,7 @@ function readQueue() {
|
|
|
732
727
|
function writeQueue(queue) {
|
|
733
728
|
ensureDir();
|
|
734
729
|
const tmp = `${QUEUE_PATH}.tmp`;
|
|
735
|
-
|
|
730
|
+
writeFileSync4(tmp, JSON.stringify(queue, null, 2));
|
|
736
731
|
renameSync3(tmp, QUEUE_PATH);
|
|
737
732
|
}
|
|
738
733
|
function queueIntercom(targetSession, reason) {
|
|
@@ -752,7 +747,7 @@ function queueIntercom(targetSession, reason) {
|
|
|
752
747
|
}
|
|
753
748
|
writeQueue(queue);
|
|
754
749
|
}
|
|
755
|
-
function drainQueue(
|
|
750
|
+
function drainQueue(isSessionBusy2, sendKeys) {
|
|
756
751
|
const queue = readQueue();
|
|
757
752
|
if (queue.length === 0) return { drained: 0, failed: 0 };
|
|
758
753
|
const remaining = [];
|
|
@@ -766,7 +761,7 @@ function drainQueue(isSessionBusy, sendKeys) {
|
|
|
766
761
|
continue;
|
|
767
762
|
}
|
|
768
763
|
try {
|
|
769
|
-
if (!
|
|
764
|
+
if (!isSessionBusy2(item.targetSession)) {
|
|
770
765
|
const success = sendKeys(item.targetSession);
|
|
771
766
|
if (success) {
|
|
772
767
|
logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
|
|
@@ -824,33 +819,100 @@ var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
|
|
|
824
819
|
var init_intercom_queue = __esm({
|
|
825
820
|
"src/lib/intercom-queue.ts"() {
|
|
826
821
|
"use strict";
|
|
827
|
-
QUEUE_PATH =
|
|
822
|
+
QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
|
|
828
823
|
MAX_RETRIES = 5;
|
|
829
824
|
TTL_MS = 60 * 60 * 1e3;
|
|
830
|
-
INTERCOM_LOG =
|
|
825
|
+
INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
831
826
|
}
|
|
832
827
|
});
|
|
833
828
|
|
|
834
829
|
// src/lib/license.ts
|
|
835
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
830
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
836
831
|
import { randomUUID } from "crypto";
|
|
837
|
-
import
|
|
832
|
+
import { createRequire as createRequire2 } from "module";
|
|
833
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
834
|
+
import os6 from "os";
|
|
835
|
+
import path7 from "path";
|
|
838
836
|
import { jwtVerify, importSPKI } from "jose";
|
|
839
|
-
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
|
|
837
|
+
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
840
838
|
var init_license = __esm({
|
|
841
839
|
"src/lib/license.ts"() {
|
|
842
840
|
"use strict";
|
|
843
841
|
init_config();
|
|
844
|
-
LICENSE_PATH =
|
|
845
|
-
CACHE_PATH =
|
|
846
|
-
DEVICE_ID_PATH =
|
|
842
|
+
LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
|
|
843
|
+
CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
|
|
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
|
+
};
|
|
847
852
|
}
|
|
848
853
|
});
|
|
849
854
|
|
|
850
855
|
// src/lib/plan-limits.ts
|
|
851
|
-
import { readFileSync as readFileSync7, existsSync as
|
|
852
|
-
import
|
|
853
|
-
|
|
856
|
+
import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
|
|
857
|
+
import path8 from "path";
|
|
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;
|
|
854
916
|
var init_plan_limits = __esm({
|
|
855
917
|
"src/lib/plan-limits.ts"() {
|
|
856
918
|
"use strict";
|
|
@@ -858,90 +920,590 @@ var init_plan_limits = __esm({
|
|
|
858
920
|
init_employees();
|
|
859
921
|
init_license();
|
|
860
922
|
init_config();
|
|
861
|
-
|
|
923
|
+
PlanLimitError = class extends Error {
|
|
924
|
+
constructor(message) {
|
|
925
|
+
super(message);
|
|
926
|
+
this.name = "PlanLimitError";
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
|
|
862
930
|
}
|
|
863
931
|
});
|
|
864
932
|
|
|
865
|
-
// src/lib/
|
|
866
|
-
import
|
|
867
|
-
|
|
868
|
-
import path10 from "path";
|
|
869
|
-
import os7 from "os";
|
|
870
|
-
import { fileURLToPath } from "url";
|
|
871
|
-
function getMySession() {
|
|
872
|
-
return getTransport().getMySession();
|
|
873
|
-
}
|
|
874
|
-
function extractRootExe(name) {
|
|
875
|
-
if (!name) return null;
|
|
876
|
-
if (!name.includes("-")) return name;
|
|
877
|
-
const parts = name.split("-").filter(Boolean);
|
|
878
|
-
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
879
|
-
}
|
|
880
|
-
function getParentExe(sessionKey) {
|
|
933
|
+
// src/lib/session-kill-telemetry.ts
|
|
934
|
+
import crypto from "crypto";
|
|
935
|
+
async function recordSessionKill(input) {
|
|
881
936
|
try {
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
+
);
|
|
886
958
|
}
|
|
887
959
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
"utf8"
|
|
893
|
-
));
|
|
894
|
-
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
895
|
-
} catch {
|
|
896
|
-
return null;
|
|
960
|
+
var init_session_kill_telemetry = __esm({
|
|
961
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
962
|
+
"use strict";
|
|
963
|
+
init_database();
|
|
897
964
|
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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;
|
|
916
1005
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
return fromSessionName ?? mySession;
|
|
920
|
-
}
|
|
921
|
-
function readDebounceState() {
|
|
922
|
-
try {
|
|
923
|
-
if (!existsSync8(DEBOUNCE_FILE)) return {};
|
|
924
|
-
const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
|
|
925
|
-
const state = {};
|
|
926
|
-
for (const [key, val] of Object.entries(raw)) {
|
|
927
|
-
if (typeof val === "number") {
|
|
928
|
-
state[key] = { lastSent: val, pending: 0 };
|
|
929
|
-
} else if (val && typeof val === "object" && "lastSent" in val) {
|
|
930
|
-
state[key] = val;
|
|
931
|
-
}
|
|
1006
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
1007
|
+
if (re.test(line)) return false;
|
|
932
1008
|
}
|
|
933
|
-
return
|
|
934
|
-
}
|
|
935
|
-
|
|
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;
|
|
936
1027
|
}
|
|
1028
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
1029
|
+
_pendingCapacityKill.set(agentId, now);
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
_pendingCapacityKill.delete(agentId);
|
|
1033
|
+
return true;
|
|
937
1034
|
}
|
|
938
|
-
function
|
|
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;
|
|
939
1106
|
try {
|
|
940
|
-
|
|
941
|
-
writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
|
|
1107
|
+
liveSessions = transport.listSessions();
|
|
942
1108
|
} catch {
|
|
1109
|
+
return [];
|
|
943
1110
|
}
|
|
944
|
-
|
|
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
|
+
|
|
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
|
+
});
|
|
1277
|
+
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
1278
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync } from "fs";
|
|
1279
|
+
import path9 from "path";
|
|
1280
|
+
import os7 from "os";
|
|
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
|
+
}
|
|
1350
|
+
function getMySession() {
|
|
1351
|
+
return getTransport().getMySession();
|
|
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
|
+
}
|
|
1386
|
+
function extractRootExe(name) {
|
|
1387
|
+
if (!name) return null;
|
|
1388
|
+
if (!name.includes("-")) return name;
|
|
1389
|
+
const parts = name.split("-").filter(Boolean);
|
|
1390
|
+
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
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
|
+
}
|
|
1404
|
+
function getParentExe(sessionKey) {
|
|
1405
|
+
try {
|
|
1406
|
+
const data = JSON.parse(readFileSync8(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
1407
|
+
return data.parentExe || null;
|
|
1408
|
+
} catch {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function getDispatchedBy(sessionKey) {
|
|
1413
|
+
try {
|
|
1414
|
+
const data = JSON.parse(readFileSync8(
|
|
1415
|
+
path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
1416
|
+
"utf8"
|
|
1417
|
+
));
|
|
1418
|
+
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
1419
|
+
} catch {
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function resolveExeSession() {
|
|
1424
|
+
const mySession = getMySession();
|
|
1425
|
+
if (!mySession) return null;
|
|
1426
|
+
const fromSessionName = extractRootExe(mySession);
|
|
1427
|
+
try {
|
|
1428
|
+
const key = getSessionKey();
|
|
1429
|
+
const parentExe = getParentExe(key);
|
|
1430
|
+
if (parentExe) {
|
|
1431
|
+
const fromCache = extractRootExe(parentExe) ?? parentExe;
|
|
1432
|
+
if (fromSessionName && fromCache !== fromSessionName) {
|
|
1433
|
+
process.stderr.write(
|
|
1434
|
+
`[tmux-routing] WARN: cache says "${fromCache}" but session name says "${fromSessionName}". Trusting session name.
|
|
1435
|
+
`
|
|
1436
|
+
);
|
|
1437
|
+
return fromSessionName;
|
|
1438
|
+
}
|
|
1439
|
+
return fromCache;
|
|
1440
|
+
}
|
|
1441
|
+
} catch {
|
|
1442
|
+
}
|
|
1443
|
+
return fromSessionName ?? mySession;
|
|
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
|
+
}
|
|
1483
|
+
function readDebounceState() {
|
|
1484
|
+
try {
|
|
1485
|
+
if (!existsSync9(DEBOUNCE_FILE)) return {};
|
|
1486
|
+
const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
|
|
1487
|
+
const state = {};
|
|
1488
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
1489
|
+
if (typeof val === "number") {
|
|
1490
|
+
state[key] = { lastSent: val, pending: 0 };
|
|
1491
|
+
} else if (val && typeof val === "object" && "lastSent" in val) {
|
|
1492
|
+
state[key] = val;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return state;
|
|
1496
|
+
} catch {
|
|
1497
|
+
return {};
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function writeDebounceState(state) {
|
|
1501
|
+
try {
|
|
1502
|
+
if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
1503
|
+
writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
|
|
1504
|
+
} catch {
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
945
1507
|
function isDebounced(targetSession) {
|
|
946
1508
|
const state = readDebounceState();
|
|
947
1509
|
const entry = state[targetSession];
|
|
@@ -995,6 +1557,10 @@ function getSessionState(sessionName) {
|
|
|
995
1557
|
return "offline";
|
|
996
1558
|
}
|
|
997
1559
|
}
|
|
1560
|
+
function isSessionBusy(sessionName) {
|
|
1561
|
+
const state = getSessionState(sessionName);
|
|
1562
|
+
return state === "thinking" || state === "tool";
|
|
1563
|
+
}
|
|
998
1564
|
function isExeSession(sessionName) {
|
|
999
1565
|
const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
|
|
1000
1566
|
const coordinatorName = getCoordinatorName();
|
|
@@ -1032,8 +1598,8 @@ function sendIntercom(targetSession) {
|
|
|
1032
1598
|
try {
|
|
1033
1599
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
1034
1600
|
const agent = baseAgentName(rawAgent);
|
|
1035
|
-
const markerPath =
|
|
1036
|
-
if (
|
|
1601
|
+
const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
1602
|
+
if (existsSync9(markerPath)) {
|
|
1037
1603
|
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
|
|
1038
1604
|
return "debounced";
|
|
1039
1605
|
}
|
|
@@ -1042,9 +1608,9 @@ function sendIntercom(targetSession) {
|
|
|
1042
1608
|
try {
|
|
1043
1609
|
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
1044
1610
|
const agent = baseAgentName(rawAgent);
|
|
1045
|
-
const taskDir =
|
|
1046
|
-
if (
|
|
1047
|
-
const files =
|
|
1611
|
+
const taskDir = path9.join(process.cwd(), "exe", agent);
|
|
1612
|
+
if (existsSync9(taskDir)) {
|
|
1613
|
+
const files = readdirSync(taskDir).filter(
|
|
1048
1614
|
(f) => f.endsWith(".md") && f !== "DONE.txt"
|
|
1049
1615
|
);
|
|
1050
1616
|
if (files.length === 0) {
|
|
@@ -1088,78 +1654,652 @@ function notifyParentExe(sessionKey) {
|
|
|
1088
1654
|
`);
|
|
1089
1655
|
return false;
|
|
1090
1656
|
}
|
|
1091
|
-
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
1092
|
-
`);
|
|
1093
|
-
const result = sendIntercom(target);
|
|
1094
|
-
if (result === "failed") {
|
|
1095
|
-
const rootExe = resolveExeSession();
|
|
1096
|
-
if (rootExe && rootExe !== target) {
|
|
1097
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
|
|
1098
|
-
`);
|
|
1099
|
-
const fallback = sendIntercom(rootExe);
|
|
1100
|
-
return fallback !== "failed";
|
|
1657
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
1658
|
+
`);
|
|
1659
|
+
const result = sendIntercom(target);
|
|
1660
|
+
if (result === "failed") {
|
|
1661
|
+
const rootExe = resolveExeSession();
|
|
1662
|
+
if (rootExe && rootExe !== target) {
|
|
1663
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
|
|
1664
|
+
`);
|
|
1665
|
+
const fallback = sendIntercom(rootExe);
|
|
1666
|
+
return fallback !== "failed";
|
|
1667
|
+
}
|
|
1668
|
+
return false;
|
|
1669
|
+
}
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
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;
|
|
2019
|
+
var init_tmux_routing = __esm({
|
|
2020
|
+
"src/lib/tmux-routing.ts"() {
|
|
2021
|
+
"use strict";
|
|
2022
|
+
init_session_registry();
|
|
2023
|
+
init_session_key();
|
|
2024
|
+
init_transport();
|
|
2025
|
+
init_cc_agent_support();
|
|
2026
|
+
init_mcp_prefix();
|
|
2027
|
+
init_provider_table();
|
|
2028
|
+
init_agent_config();
|
|
2029
|
+
init_runtime_table();
|
|
2030
|
+
init_intercom_queue();
|
|
2031
|
+
init_plan_limits();
|
|
2032
|
+
init_employees();
|
|
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;
|
|
2038
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
2039
|
+
CODEX_DEBOUNCE_MS = 12e4;
|
|
2040
|
+
INTERCOM_LOG2 = path9.join(os7.homedir(), ".exe-os", "intercom.log");
|
|
2041
|
+
DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
|
|
2042
|
+
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
2043
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
// src/lib/task-scope.ts
|
|
2048
|
+
function getCurrentSessionScope() {
|
|
2049
|
+
try {
|
|
2050
|
+
return resolveExeSession();
|
|
2051
|
+
} catch {
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function sessionScopeFilter(sessionScope, tableAlias) {
|
|
2056
|
+
const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
|
|
2057
|
+
if (!scope) return { sql: "", args: [] };
|
|
2058
|
+
const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
|
|
2059
|
+
return {
|
|
2060
|
+
sql: ` AND (${col} IS NULL OR ${col} = ?)`,
|
|
2061
|
+
args: [scope]
|
|
2062
|
+
};
|
|
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
|
+
}
|
|
2073
|
+
var init_task_scope = __esm({
|
|
2074
|
+
"src/lib/task-scope.ts"() {
|
|
2075
|
+
"use strict";
|
|
2076
|
+
init_tmux_routing();
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
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
|
+
|
|
2192
|
+
// src/lib/project-name.ts
|
|
2193
|
+
import { execSync as execSync5 } from "child_process";
|
|
2194
|
+
import path11 from "path";
|
|
2195
|
+
function getProjectName(cwd) {
|
|
2196
|
+
const dir = cwd ?? process.cwd();
|
|
2197
|
+
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
2198
|
+
try {
|
|
2199
|
+
let repoRoot;
|
|
2200
|
+
try {
|
|
2201
|
+
const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
|
|
2202
|
+
cwd: dir,
|
|
2203
|
+
encoding: "utf8",
|
|
2204
|
+
timeout: 2e3,
|
|
2205
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2206
|
+
}).trim();
|
|
2207
|
+
repoRoot = path11.dirname(gitCommonDir);
|
|
2208
|
+
} catch {
|
|
2209
|
+
repoRoot = execSync5("git rev-parse --show-toplevel", {
|
|
2210
|
+
cwd: dir,
|
|
2211
|
+
encoding: "utf8",
|
|
2212
|
+
timeout: 2e3,
|
|
2213
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2214
|
+
}).trim();
|
|
1101
2215
|
}
|
|
1102
|
-
|
|
2216
|
+
_cached2 = path11.basename(repoRoot);
|
|
2217
|
+
_cachedCwd = dir;
|
|
2218
|
+
return _cached2;
|
|
2219
|
+
} catch {
|
|
2220
|
+
_cached2 = path11.basename(dir);
|
|
2221
|
+
_cachedCwd = dir;
|
|
2222
|
+
return _cached2;
|
|
1103
2223
|
}
|
|
1104
|
-
return true;
|
|
1105
2224
|
}
|
|
1106
|
-
var
|
|
1107
|
-
var
|
|
1108
|
-
"src/lib/
|
|
2225
|
+
var _cached2, _cachedCwd;
|
|
2226
|
+
var init_project_name = __esm({
|
|
2227
|
+
"src/lib/project-name.ts"() {
|
|
1109
2228
|
"use strict";
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
init_transport();
|
|
1113
|
-
init_cc_agent_support();
|
|
1114
|
-
init_mcp_prefix();
|
|
1115
|
-
init_provider_table();
|
|
1116
|
-
init_agent_config();
|
|
1117
|
-
init_runtime_table();
|
|
1118
|
-
init_intercom_queue();
|
|
1119
|
-
init_plan_limits();
|
|
1120
|
-
init_employees();
|
|
1121
|
-
SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
|
|
1122
|
-
SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
|
|
1123
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
1124
|
-
CODEX_DEBOUNCE_MS = 12e4;
|
|
1125
|
-
INTERCOM_LOG2 = path10.join(os7.homedir(), ".exe-os", "intercom.log");
|
|
1126
|
-
DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
|
|
1127
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
1128
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
|
|
2229
|
+
_cached2 = null;
|
|
2230
|
+
_cachedCwd = null;
|
|
1129
2231
|
}
|
|
1130
2232
|
});
|
|
1131
2233
|
|
|
1132
|
-
// src/lib/
|
|
1133
|
-
|
|
2234
|
+
// src/lib/session-scope.ts
|
|
2235
|
+
var session_scope_exports = {};
|
|
2236
|
+
__export(session_scope_exports, {
|
|
2237
|
+
assertSessionScope: () => assertSessionScope,
|
|
2238
|
+
findSessionForProject: () => findSessionForProject,
|
|
2239
|
+
getSessionProject: () => getSessionProject
|
|
2240
|
+
});
|
|
2241
|
+
function getSessionProject(sessionName) {
|
|
2242
|
+
const sessions = listSessions();
|
|
2243
|
+
const entry = sessions.find((s) => s.windowName === sessionName);
|
|
2244
|
+
if (!entry) return null;
|
|
2245
|
+
const parts = entry.projectDir.split("/").filter(Boolean);
|
|
2246
|
+
return parts[parts.length - 1] ?? null;
|
|
2247
|
+
}
|
|
2248
|
+
function findSessionForProject(projectName) {
|
|
2249
|
+
const sessions = listSessions();
|
|
2250
|
+
for (const s of sessions) {
|
|
2251
|
+
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
2252
|
+
if (proj === projectName && isCoordinatorName(s.agentId)) return s;
|
|
2253
|
+
}
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
function assertSessionScope(actionType, targetProject) {
|
|
1134
2257
|
try {
|
|
1135
|
-
|
|
2258
|
+
const currentProject = getProjectName();
|
|
2259
|
+
const exeSession = resolveExeSession();
|
|
2260
|
+
if (!exeSession) {
|
|
2261
|
+
return { allowed: true, reason: "no_session" };
|
|
2262
|
+
}
|
|
2263
|
+
if (currentProject === targetProject) {
|
|
2264
|
+
return {
|
|
2265
|
+
allowed: true,
|
|
2266
|
+
reason: "same_session",
|
|
2267
|
+
currentProject,
|
|
2268
|
+
targetProject
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
process.stderr.write(
|
|
2272
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
2273
|
+
`
|
|
2274
|
+
);
|
|
2275
|
+
return {
|
|
2276
|
+
allowed: false,
|
|
2277
|
+
reason: "cross_session_denied",
|
|
2278
|
+
currentProject,
|
|
2279
|
+
targetProject,
|
|
2280
|
+
targetSession: findSessionForProject(targetProject)?.windowName
|
|
2281
|
+
};
|
|
1136
2282
|
} catch {
|
|
1137
|
-
return
|
|
2283
|
+
return { allowed: true, reason: "no_session" };
|
|
1138
2284
|
}
|
|
1139
2285
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
if (!scope) return { sql: "", args: [] };
|
|
1143
|
-
const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
|
|
1144
|
-
return {
|
|
1145
|
-
sql: ` AND (${col} IS NULL OR ${col} = ?)`,
|
|
1146
|
-
args: [scope]
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
var init_task_scope = __esm({
|
|
1150
|
-
"src/lib/task-scope.ts"() {
|
|
2286
|
+
var init_session_scope = __esm({
|
|
2287
|
+
"src/lib/session-scope.ts"() {
|
|
1151
2288
|
"use strict";
|
|
2289
|
+
init_session_registry();
|
|
2290
|
+
init_project_name();
|
|
1152
2291
|
init_tmux_routing();
|
|
2292
|
+
init_employees();
|
|
1153
2293
|
}
|
|
1154
2294
|
});
|
|
1155
2295
|
|
|
1156
2296
|
// src/lib/tasks-crud.ts
|
|
1157
|
-
import
|
|
1158
|
-
import
|
|
1159
|
-
import
|
|
1160
|
-
import { execSync as
|
|
2297
|
+
import crypto3 from "crypto";
|
|
2298
|
+
import path12 from "path";
|
|
2299
|
+
import os9 from "os";
|
|
2300
|
+
import { execSync as execSync6 } from "child_process";
|
|
1161
2301
|
import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
|
|
1162
|
-
import { existsSync as
|
|
2302
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
1163
2303
|
async function writeCheckpoint(input) {
|
|
1164
2304
|
const client = getClient();
|
|
1165
2305
|
const row = await resolveTask(client, input.taskId);
|
|
@@ -1190,6 +2330,16 @@ async function writeCheckpoint(input) {
|
|
|
1190
2330
|
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
1191
2331
|
return { checkpointCount };
|
|
1192
2332
|
}
|
|
2333
|
+
function extractParentFromContext(contextBody) {
|
|
2334
|
+
if (!contextBody) return null;
|
|
2335
|
+
const match = contextBody.match(
|
|
2336
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
2337
|
+
);
|
|
2338
|
+
return match ? match[1].toLowerCase() : null;
|
|
2339
|
+
}
|
|
2340
|
+
function slugify(title) {
|
|
2341
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2342
|
+
}
|
|
1193
2343
|
function buildKeywordIndex() {
|
|
1194
2344
|
const idx = /* @__PURE__ */ new Map();
|
|
1195
2345
|
for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
|
|
@@ -1201,6 +2351,24 @@ function buildKeywordIndex() {
|
|
|
1201
2351
|
}
|
|
1202
2352
|
return idx;
|
|
1203
2353
|
}
|
|
2354
|
+
function checkLaneAffinity(title, context, assigneeName) {
|
|
2355
|
+
const employees = loadEmployeesSync();
|
|
2356
|
+
const employee = employees.find((e) => e.name === assigneeName);
|
|
2357
|
+
if (!employee) return void 0;
|
|
2358
|
+
const assigneeRole = employee.role;
|
|
2359
|
+
const text = `${title} ${context}`.toLowerCase();
|
|
2360
|
+
const matchedRoles = /* @__PURE__ */ new Set();
|
|
2361
|
+
for (const [keyword, roles] of KEYWORD_INDEX) {
|
|
2362
|
+
if (text.includes(keyword)) {
|
|
2363
|
+
for (const role of roles) matchedRoles.add(role);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (matchedRoles.size === 0) return void 0;
|
|
2367
|
+
if (matchedRoles.has(assigneeRole)) return void 0;
|
|
2368
|
+
if (assigneeRole === "COO") return void 0;
|
|
2369
|
+
const expectedRoles = Array.from(matchedRoles).join(" or ");
|
|
2370
|
+
return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
|
|
2371
|
+
}
|
|
1204
2372
|
async function resolveTask(client, identifier, scopeSession) {
|
|
1205
2373
|
const scope = sessionScopeFilter(scopeSession);
|
|
1206
2374
|
let result = await client.execute({
|
|
@@ -1245,18 +2413,238 @@ async function resolveTask(client, identifier, scopeSession) {
|
|
|
1245
2413
|
}
|
|
1246
2414
|
throw new Error(`Task not found: ${identifier}`);
|
|
1247
2415
|
}
|
|
2416
|
+
async function createTaskCore(input) {
|
|
2417
|
+
const client = getClient();
|
|
2418
|
+
const id = crypto3.randomUUID();
|
|
2419
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2420
|
+
const slug = slugify(input.title);
|
|
2421
|
+
let earlySessionScope = null;
|
|
2422
|
+
let scopeMismatchWarning;
|
|
2423
|
+
try {
|
|
2424
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
2425
|
+
const resolved = resolveExeSession2();
|
|
2426
|
+
if (resolved && input.projectName) {
|
|
2427
|
+
const { getSessionProject: getSessionProject2 } = await Promise.resolve().then(() => (init_session_scope(), session_scope_exports));
|
|
2428
|
+
const sessionProject = getSessionProject2(resolved);
|
|
2429
|
+
if (sessionProject && sessionProject !== input.projectName) {
|
|
2430
|
+
scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
|
|
2431
|
+
process.stderr.write(`[create_task] ${scopeMismatchWarning}
|
|
2432
|
+
`);
|
|
2433
|
+
earlySessionScope = null;
|
|
2434
|
+
} else {
|
|
2435
|
+
earlySessionScope = resolved;
|
|
2436
|
+
}
|
|
2437
|
+
} else {
|
|
2438
|
+
earlySessionScope = resolved;
|
|
2439
|
+
}
|
|
2440
|
+
} catch {
|
|
2441
|
+
}
|
|
2442
|
+
const scope = earlySessionScope ?? "default";
|
|
2443
|
+
const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
|
|
2444
|
+
let blockedById = null;
|
|
2445
|
+
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
2446
|
+
if (input.blockedBy) {
|
|
2447
|
+
const blocker = await resolveTask(client, input.blockedBy);
|
|
2448
|
+
blockedById = String(blocker.id);
|
|
2449
|
+
}
|
|
2450
|
+
let parentTaskId = null;
|
|
2451
|
+
let parentRef = input.parentTaskId;
|
|
2452
|
+
if (!parentRef) {
|
|
2453
|
+
const extracted = extractParentFromContext(input.context);
|
|
2454
|
+
if (extracted) {
|
|
2455
|
+
parentRef = extracted;
|
|
2456
|
+
process.stderr.write(
|
|
2457
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
if (parentRef) {
|
|
2462
|
+
try {
|
|
2463
|
+
const parent = await resolveTask(client, parentRef);
|
|
2464
|
+
parentTaskId = String(parent.id);
|
|
2465
|
+
} catch (err) {
|
|
2466
|
+
if (!input.parentTaskId) {
|
|
2467
|
+
throw new Error(
|
|
2468
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
throw err;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
let warning;
|
|
2475
|
+
const dupScope = sessionScopeFilter();
|
|
2476
|
+
const dupCheck = await client.execute({
|
|
2477
|
+
sql: `SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')${dupScope.sql}`,
|
|
2478
|
+
args: [input.title, input.assignedTo, ...dupScope.args]
|
|
2479
|
+
});
|
|
2480
|
+
if (dupCheck.rows.length > 0) {
|
|
2481
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
2482
|
+
}
|
|
2483
|
+
if (!process.env.DISABLE_LANE_AFFINITY) {
|
|
2484
|
+
const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
|
|
2485
|
+
if (laneWarning) {
|
|
2486
|
+
warning = warning ? `${warning}
|
|
2487
|
+
${laneWarning}` : laneWarning;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
if (scopeMismatchWarning) {
|
|
2491
|
+
warning = warning ? `${warning}
|
|
2492
|
+
${scopeMismatchWarning}` : scopeMismatchWarning;
|
|
2493
|
+
}
|
|
2494
|
+
if (input.baseDir) {
|
|
2495
|
+
try {
|
|
2496
|
+
await mkdir3(path12.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
2497
|
+
await mkdir3(path12.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
2498
|
+
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
2499
|
+
await ensureGitignoreExe(input.baseDir);
|
|
2500
|
+
} catch {
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
const complexity = input.complexity ?? "standard";
|
|
2504
|
+
const sessionScope = earlySessionScope;
|
|
2505
|
+
await client.execute({
|
|
2506
|
+
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)
|
|
2507
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2508
|
+
args: [
|
|
2509
|
+
id,
|
|
2510
|
+
input.title,
|
|
2511
|
+
input.assignedTo,
|
|
2512
|
+
input.assignedBy,
|
|
2513
|
+
input.projectName,
|
|
2514
|
+
input.priority,
|
|
2515
|
+
initialStatus,
|
|
2516
|
+
taskFile,
|
|
2517
|
+
blockedById,
|
|
2518
|
+
parentTaskId,
|
|
2519
|
+
input.reviewer ?? null,
|
|
2520
|
+
input.context,
|
|
2521
|
+
complexity,
|
|
2522
|
+
input.budgetTokens ?? null,
|
|
2523
|
+
input.budgetFallbackModel ?? null,
|
|
2524
|
+
0,
|
|
2525
|
+
null,
|
|
2526
|
+
sessionScope,
|
|
2527
|
+
now,
|
|
2528
|
+
now
|
|
2529
|
+
]
|
|
2530
|
+
});
|
|
2531
|
+
if (input.baseDir) {
|
|
2532
|
+
try {
|
|
2533
|
+
const EXE_OS_DIR = path12.join(os9.homedir(), ".exe-os");
|
|
2534
|
+
const mdPath = path12.join(EXE_OS_DIR, taskFile);
|
|
2535
|
+
const mdDir = path12.dirname(mdPath);
|
|
2536
|
+
if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
|
|
2537
|
+
const reviewer = input.reviewer ?? input.assignedBy;
|
|
2538
|
+
const mdContent = `# ${input.title}
|
|
2539
|
+
|
|
2540
|
+
## MANDATORY: When done
|
|
2541
|
+
|
|
2542
|
+
You MUST call update_task with status "done" and a result summary when finished.
|
|
2543
|
+
If you skip this, your reviewer will not know you're done and your work won't be reviewed.
|
|
2544
|
+
Do NOT let a failed commit or any error prevent you from calling update_task(done).
|
|
2545
|
+
|
|
2546
|
+
**ID:** ${id}
|
|
2547
|
+
**Status:** ${initialStatus}
|
|
2548
|
+
**Priority:** ${input.priority}
|
|
2549
|
+
**Assigned by:** ${input.assignedBy}
|
|
2550
|
+
**Assigned to:** ${input.assignedTo}
|
|
2551
|
+
**Project:** ${input.projectName}
|
|
2552
|
+
**Created:** ${now.split("T")[0]}${parentTaskId ? `
|
|
2553
|
+
**Parent task:** ${parentTaskId}` : ""}
|
|
2554
|
+
**Reviewer:** ${reviewer}
|
|
2555
|
+
|
|
2556
|
+
## Context
|
|
2557
|
+
|
|
2558
|
+
${input.context}
|
|
2559
|
+
`;
|
|
2560
|
+
await writeFile3(mdPath, mdContent, "utf-8");
|
|
2561
|
+
} catch (err) {
|
|
2562
|
+
process.stderr.write(
|
|
2563
|
+
`[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
|
|
2564
|
+
`
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return {
|
|
2569
|
+
id,
|
|
2570
|
+
title: input.title,
|
|
2571
|
+
assignedTo: input.assignedTo,
|
|
2572
|
+
assignedBy: input.assignedBy,
|
|
2573
|
+
projectName: input.projectName,
|
|
2574
|
+
priority: input.priority,
|
|
2575
|
+
status: initialStatus,
|
|
2576
|
+
taskFile,
|
|
2577
|
+
createdAt: now,
|
|
2578
|
+
updatedAt: now,
|
|
2579
|
+
warning,
|
|
2580
|
+
budgetTokens: input.budgetTokens ?? null,
|
|
2581
|
+
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
2582
|
+
tokensUsed: 0,
|
|
2583
|
+
tokensWarnedAt: null
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
async function listTasks(input) {
|
|
2587
|
+
const client = getClient();
|
|
2588
|
+
const conditions = [];
|
|
2589
|
+
const args = [];
|
|
2590
|
+
if (input.assignedTo) {
|
|
2591
|
+
conditions.push("assigned_to = ?");
|
|
2592
|
+
args.push(input.assignedTo);
|
|
2593
|
+
}
|
|
2594
|
+
if (input.status) {
|
|
2595
|
+
conditions.push("status = ?");
|
|
2596
|
+
args.push(input.status);
|
|
2597
|
+
} else {
|
|
2598
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
2599
|
+
}
|
|
2600
|
+
if (input.projectName) {
|
|
2601
|
+
conditions.push("project_name = ?");
|
|
2602
|
+
args.push(input.projectName);
|
|
2603
|
+
}
|
|
2604
|
+
if (input.priority) {
|
|
2605
|
+
conditions.push("priority = ?");
|
|
2606
|
+
args.push(input.priority);
|
|
2607
|
+
}
|
|
2608
|
+
const scope = sessionScopeFilter();
|
|
2609
|
+
if (scope.sql) {
|
|
2610
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
2611
|
+
args.push(...scope.args);
|
|
2612
|
+
}
|
|
2613
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2614
|
+
const result = await client.execute({
|
|
2615
|
+
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`,
|
|
2616
|
+
args
|
|
2617
|
+
});
|
|
2618
|
+
return result.rows.map((r) => ({
|
|
2619
|
+
id: String(r.id),
|
|
2620
|
+
title: String(r.title),
|
|
2621
|
+
assignedTo: String(r.assigned_to),
|
|
2622
|
+
assignedBy: String(r.assigned_by),
|
|
2623
|
+
projectName: String(r.project_name),
|
|
2624
|
+
priority: String(r.priority),
|
|
2625
|
+
status: String(r.status),
|
|
2626
|
+
taskFile: String(r.task_file),
|
|
2627
|
+
createdAt: String(r.created_at),
|
|
2628
|
+
updatedAt: String(r.updated_at),
|
|
2629
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
2630
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
2631
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
2632
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
2633
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
2634
|
+
}));
|
|
2635
|
+
}
|
|
1248
2636
|
function isTmuxSessionAlive(identifier) {
|
|
1249
2637
|
if (!identifier || identifier === "unknown") return true;
|
|
1250
2638
|
try {
|
|
1251
2639
|
if (identifier.startsWith("%")) {
|
|
1252
|
-
const output =
|
|
2640
|
+
const output = execSync6("tmux list-panes -a -F '#{pane_id}'", {
|
|
1253
2641
|
timeout: 2e3,
|
|
1254
2642
|
encoding: "utf8",
|
|
1255
2643
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1256
2644
|
});
|
|
1257
2645
|
return output.split("\n").some((l) => l.trim() === identifier);
|
|
1258
2646
|
} else {
|
|
1259
|
-
|
|
2647
|
+
execSync6(`tmux has-session -t ${JSON.stringify(identifier)}`, {
|
|
1260
2648
|
timeout: 2e3,
|
|
1261
2649
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1262
2650
|
});
|
|
@@ -1265,7 +2653,7 @@ function isTmuxSessionAlive(identifier) {
|
|
|
1265
2653
|
} catch {
|
|
1266
2654
|
if (identifier.startsWith("%")) return true;
|
|
1267
2655
|
try {
|
|
1268
|
-
|
|
2656
|
+
execSync6("tmux list-sessions", {
|
|
1269
2657
|
timeout: 2e3,
|
|
1270
2658
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1271
2659
|
});
|
|
@@ -1280,12 +2668,12 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
|
1280
2668
|
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
1281
2669
|
try {
|
|
1282
2670
|
const since = new Date(taskCreatedAt).toISOString();
|
|
1283
|
-
const branch =
|
|
2671
|
+
const branch = execSync6(
|
|
1284
2672
|
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
1285
2673
|
{ encoding: "utf8", timeout: 3e3 }
|
|
1286
2674
|
).trim();
|
|
1287
2675
|
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
1288
|
-
const commitCount =
|
|
2676
|
+
const commitCount = execSync6(
|
|
1289
2677
|
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
1290
2678
|
{ encoding: "utf8", timeout: 5e3 }
|
|
1291
2679
|
).trim();
|
|
@@ -1416,7 +2804,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
1416
2804
|
await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
|
|
1417
2805
|
} catch {
|
|
1418
2806
|
}
|
|
1419
|
-
if (input.status === "done" || input.status === "cancelled") {
|
|
2807
|
+
if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
|
|
1420
2808
|
try {
|
|
1421
2809
|
const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
|
|
1422
2810
|
clearQueueForAgent2(String(row.assigned_to));
|
|
@@ -1433,6 +2821,65 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
1433
2821
|
}
|
|
1434
2822
|
return { row, taskFile, now, taskId };
|
|
1435
2823
|
}
|
|
2824
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
2825
|
+
const client = getClient();
|
|
2826
|
+
const row = await resolveTask(client, taskId);
|
|
2827
|
+
const id = String(row.id);
|
|
2828
|
+
const taskFile = String(row.task_file);
|
|
2829
|
+
const assignedTo = String(row.assigned_to);
|
|
2830
|
+
const assignedBy = String(row.assigned_by);
|
|
2831
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
2832
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
2833
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
2834
|
+
}
|
|
2835
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
2836
|
+
const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
2837
|
+
try {
|
|
2838
|
+
if (existsSync11(archPath)) return;
|
|
2839
|
+
const template = [
|
|
2840
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
2841
|
+
"",
|
|
2842
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
2843
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2844
|
+
"",
|
|
2845
|
+
"## Overview",
|
|
2846
|
+
"",
|
|
2847
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
2848
|
+
"",
|
|
2849
|
+
"## Key Components",
|
|
2850
|
+
"",
|
|
2851
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
2852
|
+
"",
|
|
2853
|
+
"## Data Flow",
|
|
2854
|
+
"",
|
|
2855
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
2856
|
+
"",
|
|
2857
|
+
"## Invariants",
|
|
2858
|
+
"",
|
|
2859
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
2860
|
+
"",
|
|
2861
|
+
"## Dependencies",
|
|
2862
|
+
"",
|
|
2863
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
2864
|
+
""
|
|
2865
|
+
].join("\n");
|
|
2866
|
+
await writeFile3(archPath, template, "utf-8");
|
|
2867
|
+
} catch {
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
async function ensureGitignoreExe(baseDir) {
|
|
2871
|
+
const gitignorePath = path12.join(baseDir, ".gitignore");
|
|
2872
|
+
try {
|
|
2873
|
+
if (existsSync11(gitignorePath)) {
|
|
2874
|
+
const content = readFileSync10(gitignorePath, "utf-8");
|
|
2875
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
2876
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
2877
|
+
} else {
|
|
2878
|
+
await writeFile3(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
2879
|
+
}
|
|
2880
|
+
} catch {
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
1436
2883
|
var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
1437
2884
|
var init_tasks_crud = __esm({
|
|
1438
2885
|
"src/lib/tasks-crud.ts"() {
|
|
@@ -1455,8 +2902,128 @@ var init_tasks_crud = __esm({
|
|
|
1455
2902
|
});
|
|
1456
2903
|
|
|
1457
2904
|
// src/lib/tasks-review.ts
|
|
1458
|
-
import
|
|
1459
|
-
import { existsSync as
|
|
2905
|
+
import path13 from "path";
|
|
2906
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
2907
|
+
async function countPendingReviews(sessionScope) {
|
|
2908
|
+
const client = getClient();
|
|
2909
|
+
const scope = strictSessionScopeFilter(
|
|
2910
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2911
|
+
);
|
|
2912
|
+
const result = await client.execute({
|
|
2913
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
2914
|
+
WHERE status = 'needs_review'${scope.sql}`,
|
|
2915
|
+
args: [...scope.args]
|
|
2916
|
+
});
|
|
2917
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
2918
|
+
}
|
|
2919
|
+
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
2920
|
+
const client = getClient();
|
|
2921
|
+
const scope = strictSessionScopeFilter(
|
|
2922
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2923
|
+
);
|
|
2924
|
+
const result = await client.execute({
|
|
2925
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
2926
|
+
WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
|
|
2927
|
+
args: [sinceIso, ...scope.args]
|
|
2928
|
+
});
|
|
2929
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
2930
|
+
}
|
|
2931
|
+
async function listPendingReviews(limit, sessionScope) {
|
|
2932
|
+
const client = getClient();
|
|
2933
|
+
const scope = strictSessionScopeFilter(
|
|
2934
|
+
sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
|
|
2935
|
+
);
|
|
2936
|
+
const result = await client.execute({
|
|
2937
|
+
sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
|
|
2938
|
+
WHERE status = 'needs_review'${scope.sql}
|
|
2939
|
+
ORDER BY updated_at ASC LIMIT ?`,
|
|
2940
|
+
args: [...scope.args, limit]
|
|
2941
|
+
});
|
|
2942
|
+
return result.rows;
|
|
2943
|
+
}
|
|
2944
|
+
async function cleanupOrphanedReviews() {
|
|
2945
|
+
const client = getClient();
|
|
2946
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2947
|
+
const r1 = await client.execute({
|
|
2948
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
2949
|
+
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
2950
|
+
AND assigned_by = 'system'
|
|
2951
|
+
AND title LIKE 'Review:%'
|
|
2952
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
|
|
2953
|
+
args: [now]
|
|
2954
|
+
});
|
|
2955
|
+
const r1b = await client.execute({
|
|
2956
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
2957
|
+
WHERE status IN ('open', 'needs_review')
|
|
2958
|
+
AND title LIKE 'Review:%completed%'
|
|
2959
|
+
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')))`,
|
|
2960
|
+
args: [now]
|
|
2961
|
+
});
|
|
2962
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
2963
|
+
const r2 = await client.execute({
|
|
2964
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
2965
|
+
WHERE status = 'needs_review'
|
|
2966
|
+
AND result IS NOT NULL
|
|
2967
|
+
AND updated_at < ?`,
|
|
2968
|
+
args: [now, staleThreshold]
|
|
2969
|
+
});
|
|
2970
|
+
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
2971
|
+
if (total > 0) {
|
|
2972
|
+
process.stderr.write(
|
|
2973
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
2974
|
+
`
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
return total;
|
|
2978
|
+
}
|
|
2979
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
2980
|
+
const roleLower = role.toLowerCase();
|
|
2981
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
2982
|
+
return {
|
|
2983
|
+
lens: "Code Quality (Engineer)",
|
|
2984
|
+
checklist: [
|
|
2985
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
2986
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
2987
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
2988
|
+
"4. Any regressions in the test suite?"
|
|
2989
|
+
]
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
2993
|
+
return {
|
|
2994
|
+
lens: "Architecture (CTO)",
|
|
2995
|
+
checklist: [
|
|
2996
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
2997
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
2998
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
2999
|
+
"4. Security implications? Any new attack surface?",
|
|
3000
|
+
"5. Does it scale? Performance considerations?",
|
|
3001
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
3002
|
+
]
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
3006
|
+
return {
|
|
3007
|
+
lens: "Strategic (COO)",
|
|
3008
|
+
checklist: [
|
|
3009
|
+
"1. Does this serve the project mission?",
|
|
3010
|
+
"2. Is this the right work at the right time?",
|
|
3011
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
3012
|
+
"4. Any cross-project implications?"
|
|
3013
|
+
]
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
return {
|
|
3017
|
+
lens: "General",
|
|
3018
|
+
checklist: [
|
|
3019
|
+
"1. Read the original task's acceptance criteria",
|
|
3020
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
3021
|
+
"3. Verify code changes match requirements",
|
|
3022
|
+
"4. Check if tests were added/updated",
|
|
3023
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
3024
|
+
]
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
1460
3027
|
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
1461
3028
|
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
1462
3029
|
try {
|
|
@@ -1501,11 +3068,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
|
1501
3068
|
);
|
|
1502
3069
|
}
|
|
1503
3070
|
try {
|
|
1504
|
-
const cacheDir =
|
|
1505
|
-
if (
|
|
3071
|
+
const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
|
|
3072
|
+
if (existsSync12(cacheDir)) {
|
|
1506
3073
|
for (const f of readdirSync3(cacheDir)) {
|
|
1507
3074
|
if (f.startsWith("review-notified-")) {
|
|
1508
|
-
|
|
3075
|
+
unlinkSync4(path13.join(cacheDir, f));
|
|
1509
3076
|
}
|
|
1510
3077
|
}
|
|
1511
3078
|
}
|
|
@@ -1522,11 +3089,12 @@ var init_tasks_review = __esm({
|
|
|
1522
3089
|
init_tmux_routing();
|
|
1523
3090
|
init_session_key();
|
|
1524
3091
|
init_state_bus();
|
|
3092
|
+
init_task_scope();
|
|
1525
3093
|
}
|
|
1526
3094
|
});
|
|
1527
3095
|
|
|
1528
3096
|
// src/lib/tasks-chain.ts
|
|
1529
|
-
import
|
|
3097
|
+
import path14 from "path";
|
|
1530
3098
|
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
1531
3099
|
async function cascadeUnblock(taskId, baseDir, now) {
|
|
1532
3100
|
const client = getClient();
|
|
@@ -1543,7 +3111,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
|
|
|
1543
3111
|
});
|
|
1544
3112
|
for (const ur of unblockedRows.rows) {
|
|
1545
3113
|
try {
|
|
1546
|
-
const ubFile =
|
|
3114
|
+
const ubFile = path14.join(baseDir, String(ur.task_file));
|
|
1547
3115
|
let ubContent = await readFile3(ubFile, "utf-8");
|
|
1548
3116
|
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
1549
3117
|
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
@@ -1578,7 +3146,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
|
|
|
1578
3146
|
const scScope = sessionScopeFilter();
|
|
1579
3147
|
const remaining = await client.execute({
|
|
1580
3148
|
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
1581
|
-
WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
|
|
3149
|
+
WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
|
|
1582
3150
|
args: [parentTaskId, ...scScope.args]
|
|
1583
3151
|
});
|
|
1584
3152
|
const cnt = Number(remaining.rows[0]?.cnt ?? 1);
|
|
@@ -1611,6 +3179,47 @@ var init_tasks_chain = __esm({
|
|
|
1611
3179
|
});
|
|
1612
3180
|
|
|
1613
3181
|
// src/lib/tasks-notify.ts
|
|
3182
|
+
async function dispatchTaskToEmployee(input) {
|
|
3183
|
+
if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
|
|
3184
|
+
let crossProject = false;
|
|
3185
|
+
if (input.projectName) {
|
|
3186
|
+
try {
|
|
3187
|
+
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
3188
|
+
const check = assertSessionScope2("dispatch_task", input.projectName);
|
|
3189
|
+
if (check.reason === "cross_session_denied") {
|
|
3190
|
+
crossProject = true;
|
|
3191
|
+
return { dispatched: "skipped", crossProject: true };
|
|
3192
|
+
}
|
|
3193
|
+
} catch {
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
try {
|
|
3197
|
+
const transport = getTransport();
|
|
3198
|
+
const exeSession = resolveExeSession();
|
|
3199
|
+
if (!exeSession) return { dispatched: "session_missing" };
|
|
3200
|
+
const sessionName = employeeSessionName(input.assignedTo, exeSession);
|
|
3201
|
+
if (transport.isAlive(sessionName)) {
|
|
3202
|
+
const result = sendIntercom(sessionName);
|
|
3203
|
+
const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
|
|
3204
|
+
return { dispatched, session: sessionName, crossProject };
|
|
3205
|
+
} else {
|
|
3206
|
+
const projectDir = input.projectDir ?? process.cwd();
|
|
3207
|
+
const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
|
|
3208
|
+
autoInstance: isMultiInstance(input.assignedTo)
|
|
3209
|
+
});
|
|
3210
|
+
if (result.status === "failed") {
|
|
3211
|
+
process.stderr.write(
|
|
3212
|
+
`[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
|
|
3213
|
+
`
|
|
3214
|
+
);
|
|
3215
|
+
return { dispatched: "session_missing" };
|
|
3216
|
+
}
|
|
3217
|
+
return { dispatched: "spawned", session: result.sessionName, crossProject };
|
|
3218
|
+
}
|
|
3219
|
+
} catch {
|
|
3220
|
+
return { dispatched: "session_missing" };
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
1614
3223
|
function notifyTaskDone() {
|
|
1615
3224
|
try {
|
|
1616
3225
|
const key = getSessionKey();
|
|
@@ -1636,10 +3245,10 @@ var init_tasks_notify = __esm({
|
|
|
1636
3245
|
});
|
|
1637
3246
|
|
|
1638
3247
|
// src/lib/behaviors.ts
|
|
1639
|
-
import
|
|
3248
|
+
import crypto4 from "crypto";
|
|
1640
3249
|
async function storeBehavior(opts) {
|
|
1641
3250
|
const client = getClient();
|
|
1642
|
-
const id =
|
|
3251
|
+
const id = crypto4.randomUUID();
|
|
1643
3252
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1644
3253
|
await client.execute({
|
|
1645
3254
|
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
@@ -1668,7 +3277,7 @@ __export(skill_learning_exports, {
|
|
|
1668
3277
|
storeTrajectory: () => storeTrajectory,
|
|
1669
3278
|
sweepTrajectories: () => sweepTrajectories
|
|
1670
3279
|
});
|
|
1671
|
-
import
|
|
3280
|
+
import crypto5 from "crypto";
|
|
1672
3281
|
async function extractTrajectory(taskId, agentId) {
|
|
1673
3282
|
const client = getClient();
|
|
1674
3283
|
const result = await client.execute({
|
|
@@ -1697,11 +3306,11 @@ async function extractTrajectory(taskId, agentId) {
|
|
|
1697
3306
|
return signature;
|
|
1698
3307
|
}
|
|
1699
3308
|
function hashSignature(signature) {
|
|
1700
|
-
return
|
|
3309
|
+
return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
1701
3310
|
}
|
|
1702
3311
|
async function storeTrajectory(opts) {
|
|
1703
3312
|
const client = getClient();
|
|
1704
|
-
const id =
|
|
3313
|
+
const id = crypto5.randomUUID();
|
|
1705
3314
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1706
3315
|
const signatureHash = hashSignature(opts.signature);
|
|
1707
3316
|
await client.execute({
|
|
@@ -1946,29 +3555,63 @@ var init_skill_learning = __esm({
|
|
|
1946
3555
|
});
|
|
1947
3556
|
|
|
1948
3557
|
// src/lib/tasks.ts
|
|
1949
|
-
|
|
1950
|
-
|
|
3558
|
+
var tasks_exports = {};
|
|
3559
|
+
__export(tasks_exports, {
|
|
3560
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
3561
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
3562
|
+
countPendingReviews: () => countPendingReviews,
|
|
3563
|
+
createTask: () => createTask,
|
|
3564
|
+
createTaskCore: () => createTaskCore,
|
|
3565
|
+
deleteTask: () => deleteTask,
|
|
3566
|
+
deleteTaskCore: () => deleteTaskCore,
|
|
3567
|
+
ensureArchitectureDoc: () => ensureArchitectureDoc,
|
|
3568
|
+
ensureGitignoreExe: () => ensureGitignoreExe,
|
|
3569
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
3570
|
+
listPendingReviews: () => listPendingReviews,
|
|
3571
|
+
listTasks: () => listTasks,
|
|
3572
|
+
resolveTask: () => resolveTask,
|
|
3573
|
+
slugify: () => slugify,
|
|
3574
|
+
updateTask: () => updateTask,
|
|
3575
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
3576
|
+
writeCheckpoint: () => writeCheckpoint
|
|
3577
|
+
});
|
|
3578
|
+
import path15 from "path";
|
|
3579
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
|
|
3580
|
+
async function createTask(input) {
|
|
3581
|
+
const result = await createTaskCore(input);
|
|
3582
|
+
if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
|
|
3583
|
+
dispatchTaskToEmployee({
|
|
3584
|
+
assignedTo: input.assignedTo,
|
|
3585
|
+
title: input.title,
|
|
3586
|
+
priority: input.priority,
|
|
3587
|
+
taskFile: result.taskFile,
|
|
3588
|
+
initialStatus: result.status,
|
|
3589
|
+
projectName: input.projectName
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
return result;
|
|
3593
|
+
}
|
|
1951
3594
|
async function updateTask(input) {
|
|
1952
3595
|
const { row, taskFile, now, taskId } = await updateTaskStatus(input);
|
|
1953
3596
|
try {
|
|
1954
3597
|
const agent = String(row.assigned_to);
|
|
1955
|
-
const cacheDir =
|
|
1956
|
-
const cachePath =
|
|
3598
|
+
const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
|
|
3599
|
+
const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
|
|
1957
3600
|
if (input.status === "in_progress") {
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
3601
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
3602
|
+
writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
3603
|
+
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
|
|
1961
3604
|
try {
|
|
1962
|
-
|
|
3605
|
+
unlinkSync5(cachePath);
|
|
1963
3606
|
} catch {
|
|
1964
3607
|
}
|
|
1965
3608
|
}
|
|
1966
3609
|
} catch {
|
|
1967
3610
|
}
|
|
1968
|
-
if (input.status === "done") {
|
|
3611
|
+
if (input.status === "done" || input.status === "closed") {
|
|
1969
3612
|
await cleanupReviewFile(row, taskFile, input.baseDir);
|
|
1970
3613
|
}
|
|
1971
|
-
if (input.status === "done" || input.status === "cancelled") {
|
|
3614
|
+
if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
|
|
1972
3615
|
try {
|
|
1973
3616
|
const client = getClient();
|
|
1974
3617
|
const taskTitle = String(row.title);
|
|
@@ -1984,7 +3627,7 @@ async function updateTask(input) {
|
|
|
1984
3627
|
if (!isCoordinatorName(assignedAgent)) {
|
|
1985
3628
|
try {
|
|
1986
3629
|
const draftClient = getClient();
|
|
1987
|
-
if (input.status === "done") {
|
|
3630
|
+
if (input.status === "done" || input.status === "closed") {
|
|
1988
3631
|
await draftClient.execute({
|
|
1989
3632
|
sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
|
|
1990
3633
|
args: [assignedAgent]
|
|
@@ -2001,7 +3644,7 @@ async function updateTask(input) {
|
|
|
2001
3644
|
try {
|
|
2002
3645
|
const client = getClient();
|
|
2003
3646
|
const cascaded = await client.execute({
|
|
2004
|
-
sql: `UPDATE tasks SET status = '
|
|
3647
|
+
sql: `UPDATE tasks SET status = 'closed', updated_at = ?
|
|
2005
3648
|
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
2006
3649
|
args: [now, taskId]
|
|
2007
3650
|
});
|
|
@@ -2014,14 +3657,14 @@ async function updateTask(input) {
|
|
|
2014
3657
|
} catch {
|
|
2015
3658
|
}
|
|
2016
3659
|
}
|
|
2017
|
-
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
3660
|
+
const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
|
|
2018
3661
|
if (isTerminal) {
|
|
2019
3662
|
const isCoordinator = isCoordinatorName(String(row.assigned_to));
|
|
2020
3663
|
if (!isCoordinator) {
|
|
2021
3664
|
notifyTaskDone();
|
|
2022
3665
|
}
|
|
2023
3666
|
await markTaskNotificationsRead(taskFile);
|
|
2024
|
-
if (input.status === "done") {
|
|
3667
|
+
if (input.status === "done" || input.status === "closed") {
|
|
2025
3668
|
try {
|
|
2026
3669
|
await cascadeUnblock(taskId, input.baseDir, now);
|
|
2027
3670
|
} catch {
|
|
@@ -2041,7 +3684,7 @@ async function updateTask(input) {
|
|
|
2041
3684
|
}
|
|
2042
3685
|
}
|
|
2043
3686
|
}
|
|
2044
|
-
if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
3687
|
+
if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
2045
3688
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
2046
3689
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
2047
3690
|
taskId,
|
|
@@ -2081,6 +3724,21 @@ async function updateTask(input) {
|
|
|
2081
3724
|
nextTask
|
|
2082
3725
|
};
|
|
2083
3726
|
}
|
|
3727
|
+
async function deleteTask(taskId, baseDir) {
|
|
3728
|
+
const client = getClient();
|
|
3729
|
+
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
3730
|
+
const coordinatorName = getCoordinatorName();
|
|
3731
|
+
const reviewer = assignedBy || coordinatorName;
|
|
3732
|
+
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
3733
|
+
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
3734
|
+
const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
|
|
3735
|
+
await client.execute({
|
|
3736
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
|
|
3737
|
+
args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
|
|
3738
|
+
});
|
|
3739
|
+
await markAsReadByTaskFile(taskFile);
|
|
3740
|
+
await markAsReadByTaskFile(reviewFile);
|
|
3741
|
+
}
|
|
2084
3742
|
var init_tasks = __esm({
|
|
2085
3743
|
"src/lib/tasks.ts"() {
|
|
2086
3744
|
"use strict";
|
|
@@ -2108,9 +3766,9 @@ __export(active_agent_exports, {
|
|
|
2108
3766
|
resolveActiveAgentFromTmuxSession: () => resolveActiveAgentFromTmuxSession,
|
|
2109
3767
|
writeActiveAgent: () => writeActiveAgent
|
|
2110
3768
|
});
|
|
2111
|
-
import { readFileSync as
|
|
2112
|
-
import { execSync as
|
|
2113
|
-
import
|
|
3769
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync6, readdirSync as readdirSync4 } from "fs";
|
|
3770
|
+
import { execSync as execSync7 } from "child_process";
|
|
3771
|
+
import path16 from "path";
|
|
2114
3772
|
function isNameWithOptionalInstance(candidate, baseName) {
|
|
2115
3773
|
if (candidate === baseName) return true;
|
|
2116
3774
|
if (!candidate.startsWith(baseName)) return false;
|
|
@@ -2154,12 +3812,12 @@ function resolveActiveAgentFromTmuxSession(sessionName) {
|
|
|
2154
3812
|
return null;
|
|
2155
3813
|
}
|
|
2156
3814
|
function getMarkerPath() {
|
|
2157
|
-
return
|
|
3815
|
+
return path16.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
|
|
2158
3816
|
}
|
|
2159
3817
|
function writeActiveAgent(agentId, agentRole) {
|
|
2160
3818
|
try {
|
|
2161
|
-
|
|
2162
|
-
|
|
3819
|
+
mkdirSync7(CACHE_DIR, { recursive: true });
|
|
3820
|
+
writeFileSync8(
|
|
2163
3821
|
getMarkerPath(),
|
|
2164
3822
|
JSON.stringify({ agentId, agentRole, startedAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
2165
3823
|
);
|
|
@@ -2168,21 +3826,21 @@ function writeActiveAgent(agentId, agentRole) {
|
|
|
2168
3826
|
}
|
|
2169
3827
|
function clearActiveAgent() {
|
|
2170
3828
|
try {
|
|
2171
|
-
|
|
3829
|
+
unlinkSync6(getMarkerPath());
|
|
2172
3830
|
} catch {
|
|
2173
3831
|
}
|
|
2174
3832
|
}
|
|
2175
3833
|
function getActiveAgent() {
|
|
2176
3834
|
try {
|
|
2177
3835
|
const markerPath = getMarkerPath();
|
|
2178
|
-
const raw =
|
|
3836
|
+
const raw = readFileSync11(markerPath, "utf8");
|
|
2179
3837
|
const data = JSON.parse(raw);
|
|
2180
3838
|
if (data.agentId) {
|
|
2181
3839
|
if (data.startedAt) {
|
|
2182
3840
|
const age = Date.now() - new Date(data.startedAt).getTime();
|
|
2183
3841
|
if (age > STALE_MS) {
|
|
2184
3842
|
try {
|
|
2185
|
-
|
|
3843
|
+
unlinkSync6(markerPath);
|
|
2186
3844
|
} catch {
|
|
2187
3845
|
}
|
|
2188
3846
|
} else {
|
|
@@ -2201,7 +3859,7 @@ function getActiveAgent() {
|
|
|
2201
3859
|
} catch {
|
|
2202
3860
|
}
|
|
2203
3861
|
try {
|
|
2204
|
-
const sessionName =
|
|
3862
|
+
const sessionName = execSync7(
|
|
2205
3863
|
"tmux display-message -p '#{session_name}' 2>/dev/null",
|
|
2206
3864
|
{ encoding: "utf8", timeout: 2e3 }
|
|
2207
3865
|
).trim();
|
|
@@ -2223,14 +3881,14 @@ function getAllActiveAgents() {
|
|
|
2223
3881
|
const key = file.slice("active-agent-".length, -".json".length);
|
|
2224
3882
|
if (key === "undefined") continue;
|
|
2225
3883
|
try {
|
|
2226
|
-
const raw =
|
|
3884
|
+
const raw = readFileSync11(path16.join(CACHE_DIR, file), "utf8");
|
|
2227
3885
|
const data = JSON.parse(raw);
|
|
2228
3886
|
if (!data.agentId) continue;
|
|
2229
3887
|
if (data.startedAt) {
|
|
2230
3888
|
const age = Date.now() - new Date(data.startedAt).getTime();
|
|
2231
3889
|
if (age > STALE_MS) {
|
|
2232
3890
|
try {
|
|
2233
|
-
|
|
3891
|
+
unlinkSync6(path16.join(CACHE_DIR, file));
|
|
2234
3892
|
} catch {
|
|
2235
3893
|
}
|
|
2236
3894
|
continue;
|
|
@@ -2253,11 +3911,11 @@ function getAllActiveAgents() {
|
|
|
2253
3911
|
function cleanupSessionMarkers() {
|
|
2254
3912
|
const key = getSessionKey();
|
|
2255
3913
|
try {
|
|
2256
|
-
|
|
3914
|
+
unlinkSync6(path16.join(CACHE_DIR, `active-agent-${key}.json`));
|
|
2257
3915
|
} catch {
|
|
2258
3916
|
}
|
|
2259
3917
|
try {
|
|
2260
|
-
|
|
3918
|
+
unlinkSync6(path16.join(CACHE_DIR, "active-agent-undefined.json"));
|
|
2261
3919
|
} catch {
|
|
2262
3920
|
}
|
|
2263
3921
|
}
|
|
@@ -2268,7 +3926,7 @@ var init_active_agent = __esm({
|
|
|
2268
3926
|
init_config();
|
|
2269
3927
|
init_session_key();
|
|
2270
3928
|
init_employees();
|
|
2271
|
-
CACHE_DIR =
|
|
3929
|
+
CACHE_DIR = path16.join(EXE_AI_DIR, "session-cache");
|
|
2272
3930
|
STALE_MS = 24 * 60 * 60 * 1e3;
|
|
2273
3931
|
}
|
|
2274
3932
|
});
|
|
@@ -2287,7 +3945,7 @@ function registerUpdateTask(server) {
|
|
|
2287
3945
|
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.",
|
|
2288
3946
|
inputSchema: {
|
|
2289
3947
|
task_id: z.string().describe("Task identifier \u2014 UUID, slug (e.g. 'fix-auth-bug'), or title substring"),
|
|
2290
|
-
status: z.enum(["open", "in_progress", "done", "needs_review", "blocked", "cancelled"]).describe("New status"),
|
|
3948
|
+
status: z.enum(["open", "in_progress", "done", "needs_review", "blocked", "cancelled", "closed"]).describe("New status"),
|
|
2291
3949
|
result: z.string().optional().describe("Result summary (include when status=done)")
|
|
2292
3950
|
}
|
|
2293
3951
|
},
|
|
@@ -2377,7 +4035,17 @@ function registerUpdateTask(server) {
|
|
|
2377
4035
|
}
|
|
2378
4036
|
let text = `Task "${task.title}" marked ${task.status}.
|
|
2379
4037
|
File: ${task.taskFile}`;
|
|
2380
|
-
const isTerminal = status === "done" || status === "needs_review";
|
|
4038
|
+
const isTerminal = status === "done" || status === "needs_review" || status === "closed";
|
|
4039
|
+
if (isTerminal && task.assignedBy) {
|
|
4040
|
+
try {
|
|
4041
|
+
const { notifyCoordinatorTaskCompletion: notifyCoordinatorTaskCompletion2, resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
4042
|
+
const coordinatorSession = resolveExeSession2();
|
|
4043
|
+
if (coordinatorSession) {
|
|
4044
|
+
notifyCoordinatorTaskCompletion2(coordinatorSession, callerAgentId ?? "agent", task.title);
|
|
4045
|
+
}
|
|
4046
|
+
} catch {
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
2381
4049
|
if (isTerminal && task.nextTask) {
|
|
2382
4050
|
text += `
|
|
2383
4051
|
|