@askexenow/exe-os 0.8.80 → 0.8.82
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 +359 -267
- package/dist/bin/backfill-responses.js +357 -265
- package/dist/bin/backfill-vectors.js +339 -264
- package/dist/bin/cleanup-stale-review-tasks.js +315 -256
- package/dist/bin/cli.js +494 -240
- package/dist/bin/exe-agent.js +141 -46
- package/dist/bin/exe-assign.js +151 -63
- package/dist/bin/exe-boot.js +294 -115
- package/dist/bin/exe-call.js +76 -51
- package/dist/bin/exe-cloud.js +58 -45
- package/dist/bin/exe-dispatch.js +434 -277
- package/dist/bin/exe-doctor.js +317 -246
- package/dist/bin/exe-export-behaviors.js +328 -248
- package/dist/bin/exe-forget.js +314 -231
- package/dist/bin/exe-gateway.js +2676 -1402
- package/dist/bin/exe-heartbeat.js +329 -264
- package/dist/bin/exe-kill.js +324 -244
- package/dist/bin/exe-launch-agent.js +574 -463
- package/dist/bin/exe-link.js +1055 -95
- package/dist/bin/exe-new-employee.js +49 -54
- package/dist/bin/exe-pending-messages.js +310 -253
- package/dist/bin/exe-pending-notifications.js +299 -228
- package/dist/bin/exe-pending-reviews.js +314 -245
- package/dist/bin/exe-rename.js +259 -195
- package/dist/bin/exe-review.js +140 -64
- package/dist/bin/exe-search.js +543 -356
- package/dist/bin/exe-session-cleanup.js +463 -382
- package/dist/bin/exe-settings.js +129 -99
- package/dist/bin/exe-start.sh +6 -6
- package/dist/bin/exe-status.js +95 -36
- package/dist/bin/exe-team.js +116 -51
- package/dist/bin/git-sweep.js +482 -307
- package/dist/bin/graph-backfill.js +357 -245
- package/dist/bin/graph-export.js +324 -244
- package/dist/bin/install.js +33 -10
- package/dist/bin/scan-tasks.js +481 -307
- package/dist/bin/setup.js +1147 -140
- package/dist/bin/shard-migrate.js +321 -241
- package/dist/bin/update.js +1 -7
- package/dist/bin/wiki-sync.js +318 -238
- package/dist/gateway/index.js +2656 -1383
- package/dist/hooks/bug-report-worker.js +641 -472
- package/dist/hooks/commit-complete.js +482 -307
- package/dist/hooks/error-recall.js +363 -135
- package/dist/hooks/exe-heartbeat-hook.js +97 -27
- package/dist/hooks/ingest-worker.js +584 -397
- package/dist/hooks/ingest.js +123 -58
- package/dist/hooks/instructions-loaded.js +212 -82
- package/dist/hooks/notification.js +200 -70
- package/dist/hooks/post-compact.js +199 -81
- package/dist/hooks/pre-compact.js +352 -140
- package/dist/hooks/pre-tool-use.js +416 -278
- package/dist/hooks/prompt-ingest-worker.js +376 -299
- package/dist/hooks/prompt-submit.js +414 -188
- package/dist/hooks/response-ingest-worker.js +408 -338
- package/dist/hooks/session-end.js +209 -83
- package/dist/hooks/session-start.js +382 -158
- package/dist/hooks/stop.js +209 -83
- package/dist/hooks/subagent-stop.js +209 -85
- package/dist/hooks/summary-worker.js +606 -510
- package/dist/index.js +2133 -855
- package/dist/lib/cloud-sync.js +1175 -184
- package/dist/lib/config.js +1 -9
- package/dist/lib/consolidation.js +71 -34
- package/dist/lib/database.js +166 -14
- package/dist/lib/device-registry.js +189 -117
- package/dist/lib/embedder.js +6 -10
- package/dist/lib/employee-templates.js +134 -39
- package/dist/lib/employees.js +30 -7
- package/dist/lib/exe-daemon-client.js +5 -7
- package/dist/lib/exe-daemon.js +514 -152
- package/dist/lib/hybrid-search.js +543 -356
- package/dist/lib/identity-templates.js +15 -15
- package/dist/lib/identity.js +19 -15
- package/dist/lib/license.js +1 -7
- package/dist/lib/messaging.js +157 -135
- package/dist/lib/reminders.js +97 -0
- package/dist/lib/schedules.js +302 -231
- package/dist/lib/skill-learning.js +33 -27
- package/dist/lib/status-brief.js +11 -14
- package/dist/lib/store.js +326 -237
- package/dist/lib/task-router.js +105 -1
- package/dist/lib/tasks.js +233 -116
- package/dist/lib/tmux-routing.js +173 -56
- package/dist/lib/ws-client.js +13 -3
- package/dist/mcp/server.js +2009 -1015
- package/dist/mcp/tools/complete-reminder.js +97 -0
- package/dist/mcp/tools/create-reminder.js +97 -0
- package/dist/mcp/tools/create-task.js +426 -262
- package/dist/mcp/tools/deactivate-behavior.js +119 -44
- package/dist/mcp/tools/list-reminders.js +97 -0
- package/dist/mcp/tools/list-tasks.js +56 -57
- package/dist/mcp/tools/send-message.js +206 -143
- package/dist/mcp/tools/update-task.js +259 -85
- package/dist/runtime/index.js +495 -316
- package/dist/tui/App.js +1128 -919
- package/package.json +2 -10
- package/src/commands/exe/afk.md +8 -8
- package/src/commands/exe/assign.md +1 -1
- package/src/commands/exe/build-adv.md +1 -1
- package/src/commands/exe/call.md +10 -10
- package/src/commands/exe/employee-heartbeat.md +9 -6
- package/src/commands/exe/heartbeat.md +5 -5
- package/src/commands/exe/intercom.md +26 -15
- package/src/commands/exe/launch.md +2 -2
- package/src/commands/exe/new-employee.md +1 -1
- package/src/commands/exe/review.md +2 -2
- package/src/commands/exe/schedule.md +1 -1
- package/src/commands/exe/sessions.md +2 -2
- package/src/commands/exe.md +22 -20
|
@@ -26,29 +26,11 @@ var init_db_retry = __esm({
|
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
// src/lib/database.ts
|
|
30
|
-
import { createClient } from "@libsql/client";
|
|
31
|
-
function getClient() {
|
|
32
|
-
if (!_resilientClient) {
|
|
33
|
-
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
34
|
-
}
|
|
35
|
-
return _resilientClient;
|
|
36
|
-
}
|
|
37
|
-
var _resilientClient;
|
|
38
|
-
var init_database = __esm({
|
|
39
|
-
"src/lib/database.ts"() {
|
|
40
|
-
"use strict";
|
|
41
|
-
init_db_retry();
|
|
42
|
-
_resilientClient = null;
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
29
|
// src/lib/config.ts
|
|
47
30
|
var config_exports = {};
|
|
48
31
|
__export(config_exports, {
|
|
49
32
|
CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
|
|
50
33
|
CONFIG_PATH: () => CONFIG_PATH,
|
|
51
|
-
COO_AGENT_NAME: () => COO_AGENT_NAME,
|
|
52
34
|
CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
|
|
53
35
|
DB_PATH: () => DB_PATH,
|
|
54
36
|
EXE_AI_DIR: () => EXE_AI_DIR,
|
|
@@ -204,7 +186,7 @@ async function loadConfigFrom(configPath) {
|
|
|
204
186
|
return { ...DEFAULT_CONFIG };
|
|
205
187
|
}
|
|
206
188
|
}
|
|
207
|
-
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH,
|
|
189
|
+
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
208
190
|
var init_config = __esm({
|
|
209
191
|
"src/lib/config.ts"() {
|
|
210
192
|
"use strict";
|
|
@@ -212,7 +194,6 @@ var init_config = __esm({
|
|
|
212
194
|
DB_PATH = path.join(EXE_AI_DIR, "memories.db");
|
|
213
195
|
MODELS_DIR = path.join(EXE_AI_DIR, "models");
|
|
214
196
|
CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
|
|
215
|
-
COO_AGENT_NAME = "exe";
|
|
216
197
|
LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
|
|
217
198
|
CURRENT_CONFIG_VERSION = 1;
|
|
218
199
|
DEFAULT_CONFIG = {
|
|
@@ -248,13 +229,7 @@ var init_config = __esm({
|
|
|
248
229
|
wikiUrl: "",
|
|
249
230
|
wikiApiKey: "",
|
|
250
231
|
wikiSyncIntervalMs: 30 * 60 * 1e3,
|
|
251
|
-
wikiWorkspaceMapping: {
|
|
252
|
-
exe: "Executive",
|
|
253
|
-
yoshi: "Engineering",
|
|
254
|
-
mari: "Marketing",
|
|
255
|
-
tom: "Engineering",
|
|
256
|
-
sasha: "Production"
|
|
257
|
-
},
|
|
232
|
+
wikiWorkspaceMapping: {},
|
|
258
233
|
wikiAutoUpdate: true,
|
|
259
234
|
wikiAutoUpdateThreshold: 0.5,
|
|
260
235
|
wikiAutoUpdateCreateNew: true,
|
|
@@ -291,15 +266,231 @@ var init_config = __esm({
|
|
|
291
266
|
}
|
|
292
267
|
});
|
|
293
268
|
|
|
294
|
-
// src/lib/
|
|
295
|
-
|
|
269
|
+
// src/lib/employees.ts
|
|
270
|
+
var employees_exports = {};
|
|
271
|
+
__export(employees_exports, {
|
|
272
|
+
COORDINATOR_ROLE: () => COORDINATOR_ROLE,
|
|
273
|
+
DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
|
|
274
|
+
EMPLOYEES_PATH: () => EMPLOYEES_PATH,
|
|
275
|
+
addEmployee: () => addEmployee,
|
|
276
|
+
canCoordinate: () => canCoordinate,
|
|
277
|
+
getCoordinatorEmployee: () => getCoordinatorEmployee,
|
|
278
|
+
getCoordinatorName: () => getCoordinatorName,
|
|
279
|
+
getEmployee: () => getEmployee,
|
|
280
|
+
getEmployeeByRole: () => getEmployeeByRole,
|
|
281
|
+
getEmployeeNamesByRole: () => getEmployeeNamesByRole,
|
|
282
|
+
hasRole: () => hasRole,
|
|
283
|
+
isCoordinatorName: () => isCoordinatorName,
|
|
284
|
+
isCoordinatorRole: () => isCoordinatorRole,
|
|
285
|
+
isMultiInstance: () => isMultiInstance,
|
|
286
|
+
loadEmployees: () => loadEmployees,
|
|
287
|
+
loadEmployeesSync: () => loadEmployeesSync,
|
|
288
|
+
normalizeRole: () => normalizeRole,
|
|
289
|
+
normalizeRosterCase: () => normalizeRosterCase,
|
|
290
|
+
registerBinSymlinks: () => registerBinSymlinks,
|
|
291
|
+
saveEmployees: () => saveEmployees,
|
|
292
|
+
validateEmployeeName: () => validateEmployeeName
|
|
293
|
+
});
|
|
294
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
295
|
+
import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
|
|
296
|
+
import { execSync } from "child_process";
|
|
296
297
|
import path2 from "path";
|
|
297
298
|
import os2 from "os";
|
|
299
|
+
function normalizeRole(role) {
|
|
300
|
+
return (role ?? "").trim().toLowerCase();
|
|
301
|
+
}
|
|
302
|
+
function isCoordinatorRole(role) {
|
|
303
|
+
return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
|
|
304
|
+
}
|
|
305
|
+
function getCoordinatorEmployee(employees) {
|
|
306
|
+
return employees.find((e) => isCoordinatorRole(e.role));
|
|
307
|
+
}
|
|
308
|
+
function getCoordinatorName(employees = loadEmployeesSync()) {
|
|
309
|
+
return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
|
|
310
|
+
}
|
|
311
|
+
function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
312
|
+
if (!agentName) return false;
|
|
313
|
+
return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
|
|
314
|
+
}
|
|
315
|
+
function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
|
|
316
|
+
return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
|
|
317
|
+
}
|
|
318
|
+
function validateEmployeeName(name) {
|
|
319
|
+
if (!name) {
|
|
320
|
+
return { valid: false, error: "Name is required" };
|
|
321
|
+
}
|
|
322
|
+
if (name.length > 32) {
|
|
323
|
+
return { valid: false, error: "Name must be 32 characters or fewer" };
|
|
324
|
+
}
|
|
325
|
+
if (!/^[a-z][a-z0-9]*$/.test(name)) {
|
|
326
|
+
return {
|
|
327
|
+
valid: false,
|
|
328
|
+
error: "Name must start with a letter and contain only lowercase alphanumeric characters"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return { valid: true };
|
|
332
|
+
}
|
|
333
|
+
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
334
|
+
if (!existsSync2(employeesPath)) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
const raw = await readFile2(employeesPath, "utf-8");
|
|
338
|
+
try {
|
|
339
|
+
return JSON.parse(raw);
|
|
340
|
+
} catch {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
|
|
345
|
+
await mkdir2(path2.dirname(employeesPath), { recursive: true });
|
|
346
|
+
await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
|
|
347
|
+
}
|
|
348
|
+
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
349
|
+
if (!existsSync2(employeesPath)) return [];
|
|
350
|
+
try {
|
|
351
|
+
return JSON.parse(readFileSync2(employeesPath, "utf-8"));
|
|
352
|
+
} catch {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function getEmployee(employees, name) {
|
|
357
|
+
return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
358
|
+
}
|
|
359
|
+
function getEmployeeByRole(employees, role) {
|
|
360
|
+
const lower = role.toLowerCase();
|
|
361
|
+
return employees.find((e) => e.role.toLowerCase() === lower);
|
|
362
|
+
}
|
|
363
|
+
function getEmployeeNamesByRole(employees, role) {
|
|
364
|
+
const lower = role.toLowerCase();
|
|
365
|
+
return employees.filter((e) => e.role.toLowerCase() === lower).map((e) => e.name);
|
|
366
|
+
}
|
|
367
|
+
function hasRole(agentName, role) {
|
|
368
|
+
const employees = loadEmployeesSync();
|
|
369
|
+
const emp = getEmployee(employees, agentName);
|
|
370
|
+
return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
|
|
371
|
+
}
|
|
372
|
+
function isMultiInstance(agentName, employees) {
|
|
373
|
+
const roster = employees ?? loadEmployeesSync();
|
|
374
|
+
const emp = getEmployee(roster, agentName);
|
|
375
|
+
if (!emp) return false;
|
|
376
|
+
return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
|
|
377
|
+
}
|
|
378
|
+
function addEmployee(employees, employee) {
|
|
379
|
+
const normalized = { ...employee, name: employee.name.toLowerCase() };
|
|
380
|
+
if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
|
|
381
|
+
throw new Error(`Employee '${normalized.name}' already exists`);
|
|
382
|
+
}
|
|
383
|
+
return [...employees, normalized];
|
|
384
|
+
}
|
|
385
|
+
async function normalizeRosterCase(rosterPath) {
|
|
386
|
+
const employees = await loadEmployees(rosterPath);
|
|
387
|
+
let changed = false;
|
|
388
|
+
for (const emp of employees) {
|
|
389
|
+
if (emp.name !== emp.name.toLowerCase()) {
|
|
390
|
+
const oldName = emp.name;
|
|
391
|
+
emp.name = emp.name.toLowerCase();
|
|
392
|
+
changed = true;
|
|
393
|
+
try {
|
|
394
|
+
const identityDir = path2.join(os2.homedir(), ".exe-os", "identity");
|
|
395
|
+
const oldPath = path2.join(identityDir, `${oldName}.md`);
|
|
396
|
+
const newPath = path2.join(identityDir, `${emp.name}.md`);
|
|
397
|
+
if (existsSync2(oldPath) && !existsSync2(newPath)) {
|
|
398
|
+
renameSync2(oldPath, newPath);
|
|
399
|
+
} else if (existsSync2(oldPath) && oldPath !== newPath) {
|
|
400
|
+
const content = readFileSync2(oldPath, "utf-8");
|
|
401
|
+
writeFileSync(newPath, content, "utf-8");
|
|
402
|
+
if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
|
|
403
|
+
unlinkSync(oldPath);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (changed) {
|
|
411
|
+
await saveEmployees(employees, rosterPath);
|
|
412
|
+
}
|
|
413
|
+
return changed;
|
|
414
|
+
}
|
|
415
|
+
function findExeBin() {
|
|
416
|
+
try {
|
|
417
|
+
return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function registerBinSymlinks(name) {
|
|
423
|
+
const created = [];
|
|
424
|
+
const skipped = [];
|
|
425
|
+
const errors = [];
|
|
426
|
+
const exeBinPath = findExeBin();
|
|
427
|
+
if (!exeBinPath) {
|
|
428
|
+
errors.push("Could not find 'exe-os' in PATH");
|
|
429
|
+
return { created, skipped, errors };
|
|
430
|
+
}
|
|
431
|
+
const binDir = path2.dirname(exeBinPath);
|
|
432
|
+
let target;
|
|
433
|
+
try {
|
|
434
|
+
target = readlinkSync(exeBinPath);
|
|
435
|
+
} catch {
|
|
436
|
+
errors.push("Could not read 'exe' symlink");
|
|
437
|
+
return { created, skipped, errors };
|
|
438
|
+
}
|
|
439
|
+
for (const suffix of ["", "-opencode"]) {
|
|
440
|
+
const linkName = `${name}${suffix}`;
|
|
441
|
+
const linkPath = path2.join(binDir, linkName);
|
|
442
|
+
if (existsSync2(linkPath)) {
|
|
443
|
+
skipped.push(linkName);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
symlinkSync(target, linkPath);
|
|
448
|
+
created.push(linkName);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return { created, skipped, errors };
|
|
454
|
+
}
|
|
455
|
+
var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
|
|
456
|
+
var init_employees = __esm({
|
|
457
|
+
"src/lib/employees.ts"() {
|
|
458
|
+
"use strict";
|
|
459
|
+
init_config();
|
|
460
|
+
EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
|
|
461
|
+
DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
|
|
462
|
+
COORDINATOR_ROLE = "COO";
|
|
463
|
+
MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// src/lib/database.ts
|
|
468
|
+
import { createClient } from "@libsql/client";
|
|
469
|
+
function getClient() {
|
|
470
|
+
if (!_resilientClient) {
|
|
471
|
+
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
472
|
+
}
|
|
473
|
+
return _resilientClient;
|
|
474
|
+
}
|
|
475
|
+
var _resilientClient;
|
|
476
|
+
var init_database = __esm({
|
|
477
|
+
"src/lib/database.ts"() {
|
|
478
|
+
"use strict";
|
|
479
|
+
init_db_retry();
|
|
480
|
+
init_employees();
|
|
481
|
+
_resilientClient = null;
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// src/lib/notifications.ts
|
|
486
|
+
import crypto from "crypto";
|
|
487
|
+
import path3 from "path";
|
|
488
|
+
import os3 from "os";
|
|
298
489
|
import {
|
|
299
|
-
readFileSync as
|
|
490
|
+
readFileSync as readFileSync3,
|
|
300
491
|
readdirSync,
|
|
301
|
-
unlinkSync,
|
|
302
|
-
existsSync as
|
|
492
|
+
unlinkSync as unlinkSync2,
|
|
493
|
+
existsSync as existsSync3,
|
|
303
494
|
rmdirSync
|
|
304
495
|
} from "fs";
|
|
305
496
|
async function writeNotification(notification) {
|
|
@@ -399,12 +590,12 @@ var init_state_bus = __esm({
|
|
|
399
590
|
});
|
|
400
591
|
|
|
401
592
|
// src/lib/session-registry.ts
|
|
402
|
-
import { readFileSync as
|
|
403
|
-
import
|
|
404
|
-
import
|
|
593
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync4 } from "fs";
|
|
594
|
+
import path4 from "path";
|
|
595
|
+
import os4 from "os";
|
|
405
596
|
function registerSession(entry) {
|
|
406
|
-
const dir =
|
|
407
|
-
if (!
|
|
597
|
+
const dir = path4.dirname(REGISTRY_PATH);
|
|
598
|
+
if (!existsSync4(dir)) {
|
|
408
599
|
mkdirSync(dir, { recursive: true });
|
|
409
600
|
}
|
|
410
601
|
const sessions = listSessions();
|
|
@@ -414,11 +605,11 @@ function registerSession(entry) {
|
|
|
414
605
|
} else {
|
|
415
606
|
sessions.push(entry);
|
|
416
607
|
}
|
|
417
|
-
|
|
608
|
+
writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
418
609
|
}
|
|
419
610
|
function listSessions() {
|
|
420
611
|
try {
|
|
421
|
-
const raw =
|
|
612
|
+
const raw = readFileSync4(REGISTRY_PATH, "utf8");
|
|
422
613
|
return JSON.parse(raw);
|
|
423
614
|
} catch {
|
|
424
615
|
return [];
|
|
@@ -428,18 +619,18 @@ var REGISTRY_PATH;
|
|
|
428
619
|
var init_session_registry = __esm({
|
|
429
620
|
"src/lib/session-registry.ts"() {
|
|
430
621
|
"use strict";
|
|
431
|
-
REGISTRY_PATH =
|
|
622
|
+
REGISTRY_PATH = path4.join(os4.homedir(), ".exe-os", "session-registry.json");
|
|
432
623
|
}
|
|
433
624
|
});
|
|
434
625
|
|
|
435
626
|
// src/lib/session-key.ts
|
|
436
|
-
import { execSync } from "child_process";
|
|
627
|
+
import { execSync as execSync2 } from "child_process";
|
|
437
628
|
function getSessionKey() {
|
|
438
629
|
if (_cached) return _cached;
|
|
439
630
|
let pid = process.ppid;
|
|
440
631
|
for (let i = 0; i < 10; i++) {
|
|
441
632
|
try {
|
|
442
|
-
const info =
|
|
633
|
+
const info = execSync2(`ps -p ${pid} -o ppid=,comm=`, {
|
|
443
634
|
encoding: "utf8",
|
|
444
635
|
timeout: 2e3
|
|
445
636
|
}).trim();
|
|
@@ -575,14 +766,14 @@ var init_transport = __esm({
|
|
|
575
766
|
});
|
|
576
767
|
|
|
577
768
|
// src/lib/cc-agent-support.ts
|
|
578
|
-
import { execSync as
|
|
769
|
+
import { execSync as execSync3 } from "child_process";
|
|
579
770
|
function _resetCcAgentSupportCache() {
|
|
580
771
|
_cachedSupport = null;
|
|
581
772
|
}
|
|
582
773
|
function claudeSupportsAgentFlag() {
|
|
583
774
|
if (_cachedSupport !== null) return _cachedSupport;
|
|
584
775
|
try {
|
|
585
|
-
const helpOutput =
|
|
776
|
+
const helpOutput = execSync3("claude --help 2>&1", {
|
|
586
777
|
encoding: "utf-8",
|
|
587
778
|
timeout: 5e3
|
|
588
779
|
});
|
|
@@ -648,17 +839,17 @@ var init_provider_table = __esm({
|
|
|
648
839
|
});
|
|
649
840
|
|
|
650
841
|
// src/lib/intercom-queue.ts
|
|
651
|
-
import { readFileSync as
|
|
652
|
-
import
|
|
653
|
-
import
|
|
842
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
843
|
+
import path5 from "path";
|
|
844
|
+
import os5 from "os";
|
|
654
845
|
function ensureDir() {
|
|
655
|
-
const dir =
|
|
656
|
-
if (!
|
|
846
|
+
const dir = path5.dirname(QUEUE_PATH);
|
|
847
|
+
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
657
848
|
}
|
|
658
849
|
function readQueue() {
|
|
659
850
|
try {
|
|
660
|
-
if (!
|
|
661
|
-
return JSON.parse(
|
|
851
|
+
if (!existsSync5(QUEUE_PATH)) return [];
|
|
852
|
+
return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
|
|
662
853
|
} catch {
|
|
663
854
|
return [];
|
|
664
855
|
}
|
|
@@ -666,8 +857,8 @@ function readQueue() {
|
|
|
666
857
|
function writeQueue(queue) {
|
|
667
858
|
ensureDir();
|
|
668
859
|
const tmp = `${QUEUE_PATH}.tmp`;
|
|
669
|
-
|
|
670
|
-
|
|
860
|
+
writeFileSync3(tmp, JSON.stringify(queue, null, 2));
|
|
861
|
+
renameSync3(tmp, QUEUE_PATH);
|
|
671
862
|
}
|
|
672
863
|
function queueIntercom(targetSession, reason) {
|
|
673
864
|
const queue = readQueue();
|
|
@@ -690,178 +881,9 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
|
690
881
|
var init_intercom_queue = __esm({
|
|
691
882
|
"src/lib/intercom-queue.ts"() {
|
|
692
883
|
"use strict";
|
|
693
|
-
QUEUE_PATH =
|
|
884
|
+
QUEUE_PATH = path5.join(os5.homedir(), ".exe-os", "intercom-queue.json");
|
|
694
885
|
TTL_MS = 60 * 60 * 1e3;
|
|
695
|
-
INTERCOM_LOG =
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
// src/lib/employees.ts
|
|
700
|
-
var employees_exports = {};
|
|
701
|
-
__export(employees_exports, {
|
|
702
|
-
EMPLOYEES_PATH: () => EMPLOYEES_PATH,
|
|
703
|
-
addEmployee: () => addEmployee,
|
|
704
|
-
getEmployee: () => getEmployee,
|
|
705
|
-
getEmployeeByRole: () => getEmployeeByRole,
|
|
706
|
-
getEmployeeNamesByRole: () => getEmployeeNamesByRole,
|
|
707
|
-
hasRole: () => hasRole,
|
|
708
|
-
isMultiInstance: () => isMultiInstance,
|
|
709
|
-
loadEmployees: () => loadEmployees,
|
|
710
|
-
loadEmployeesSync: () => loadEmployeesSync,
|
|
711
|
-
normalizeRosterCase: () => normalizeRosterCase,
|
|
712
|
-
registerBinSymlinks: () => registerBinSymlinks,
|
|
713
|
-
saveEmployees: () => saveEmployees,
|
|
714
|
-
validateEmployeeName: () => validateEmployeeName
|
|
715
|
-
});
|
|
716
|
-
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
717
|
-
import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
718
|
-
import { execSync as execSync3 } from "child_process";
|
|
719
|
-
import path5 from "path";
|
|
720
|
-
import os5 from "os";
|
|
721
|
-
function validateEmployeeName(name) {
|
|
722
|
-
if (!name) {
|
|
723
|
-
return { valid: false, error: "Name is required" };
|
|
724
|
-
}
|
|
725
|
-
if (name.length > 32) {
|
|
726
|
-
return { valid: false, error: "Name must be 32 characters or fewer" };
|
|
727
|
-
}
|
|
728
|
-
if (!/^[a-z][a-z0-9]*$/.test(name)) {
|
|
729
|
-
return {
|
|
730
|
-
valid: false,
|
|
731
|
-
error: "Name must start with a letter and contain only lowercase alphanumeric characters"
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
return { valid: true };
|
|
735
|
-
}
|
|
736
|
-
async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
|
|
737
|
-
if (!existsSync5(employeesPath)) {
|
|
738
|
-
return [];
|
|
739
|
-
}
|
|
740
|
-
const raw = await readFile2(employeesPath, "utf-8");
|
|
741
|
-
try {
|
|
742
|
-
return JSON.parse(raw);
|
|
743
|
-
} catch {
|
|
744
|
-
return [];
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
|
|
748
|
-
await mkdir2(path5.dirname(employeesPath), { recursive: true });
|
|
749
|
-
await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
|
|
750
|
-
}
|
|
751
|
-
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
752
|
-
if (!existsSync5(employeesPath)) return [];
|
|
753
|
-
try {
|
|
754
|
-
return JSON.parse(readFileSync5(employeesPath, "utf-8"));
|
|
755
|
-
} catch {
|
|
756
|
-
return [];
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
function getEmployee(employees, name) {
|
|
760
|
-
return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
761
|
-
}
|
|
762
|
-
function getEmployeeByRole(employees, role) {
|
|
763
|
-
const lower = role.toLowerCase();
|
|
764
|
-
return employees.find((e) => e.role.toLowerCase() === lower);
|
|
765
|
-
}
|
|
766
|
-
function getEmployeeNamesByRole(employees, role) {
|
|
767
|
-
const lower = role.toLowerCase();
|
|
768
|
-
return employees.filter((e) => e.role.toLowerCase() === lower).map((e) => e.name);
|
|
769
|
-
}
|
|
770
|
-
function hasRole(agentName, role) {
|
|
771
|
-
const employees = loadEmployeesSync();
|
|
772
|
-
const emp = getEmployee(employees, agentName);
|
|
773
|
-
return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
|
|
774
|
-
}
|
|
775
|
-
function isMultiInstance(agentName, employees) {
|
|
776
|
-
const roster = employees ?? loadEmployeesSync();
|
|
777
|
-
const emp = getEmployee(roster, agentName);
|
|
778
|
-
if (!emp) return false;
|
|
779
|
-
return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
|
|
780
|
-
}
|
|
781
|
-
function addEmployee(employees, employee) {
|
|
782
|
-
const normalized = { ...employee, name: employee.name.toLowerCase() };
|
|
783
|
-
if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
|
|
784
|
-
throw new Error(`Employee '${normalized.name}' already exists`);
|
|
785
|
-
}
|
|
786
|
-
return [...employees, normalized];
|
|
787
|
-
}
|
|
788
|
-
async function normalizeRosterCase(rosterPath) {
|
|
789
|
-
const employees = await loadEmployees(rosterPath);
|
|
790
|
-
let changed = false;
|
|
791
|
-
for (const emp of employees) {
|
|
792
|
-
if (emp.name !== emp.name.toLowerCase()) {
|
|
793
|
-
const oldName = emp.name;
|
|
794
|
-
emp.name = emp.name.toLowerCase();
|
|
795
|
-
changed = true;
|
|
796
|
-
try {
|
|
797
|
-
const identityDir = path5.join(os5.homedir(), ".exe-os", "identity");
|
|
798
|
-
const oldPath = path5.join(identityDir, `${oldName}.md`);
|
|
799
|
-
const newPath = path5.join(identityDir, `${emp.name}.md`);
|
|
800
|
-
if (existsSync5(oldPath) && !existsSync5(newPath)) {
|
|
801
|
-
renameSync3(oldPath, newPath);
|
|
802
|
-
} else if (existsSync5(oldPath) && oldPath !== newPath) {
|
|
803
|
-
const content = readFileSync5(oldPath, "utf-8");
|
|
804
|
-
writeFileSync3(newPath, content, "utf-8");
|
|
805
|
-
if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
|
|
806
|
-
unlinkSync2(oldPath);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
} catch {
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
if (changed) {
|
|
814
|
-
await saveEmployees(employees, rosterPath);
|
|
815
|
-
}
|
|
816
|
-
return changed;
|
|
817
|
-
}
|
|
818
|
-
function findExeBin() {
|
|
819
|
-
try {
|
|
820
|
-
return execSync3(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
|
|
821
|
-
} catch {
|
|
822
|
-
return null;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
function registerBinSymlinks(name) {
|
|
826
|
-
const created = [];
|
|
827
|
-
const skipped = [];
|
|
828
|
-
const errors = [];
|
|
829
|
-
const exeBinPath = findExeBin();
|
|
830
|
-
if (!exeBinPath) {
|
|
831
|
-
errors.push("Could not find 'exe-os' in PATH");
|
|
832
|
-
return { created, skipped, errors };
|
|
833
|
-
}
|
|
834
|
-
const binDir = path5.dirname(exeBinPath);
|
|
835
|
-
let target;
|
|
836
|
-
try {
|
|
837
|
-
target = readlinkSync(exeBinPath);
|
|
838
|
-
} catch {
|
|
839
|
-
errors.push("Could not read 'exe' symlink");
|
|
840
|
-
return { created, skipped, errors };
|
|
841
|
-
}
|
|
842
|
-
for (const suffix of ["", "-opencode"]) {
|
|
843
|
-
const linkName = `${name}${suffix}`;
|
|
844
|
-
const linkPath = path5.join(binDir, linkName);
|
|
845
|
-
if (existsSync5(linkPath)) {
|
|
846
|
-
skipped.push(linkName);
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
849
|
-
try {
|
|
850
|
-
symlinkSync(target, linkPath);
|
|
851
|
-
created.push(linkName);
|
|
852
|
-
} catch (err) {
|
|
853
|
-
errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
return { created, skipped, errors };
|
|
857
|
-
}
|
|
858
|
-
var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
|
|
859
|
-
var init_employees = __esm({
|
|
860
|
-
"src/lib/employees.ts"() {
|
|
861
|
-
"use strict";
|
|
862
|
-
init_config();
|
|
863
|
-
EMPLOYEES_PATH = path5.join(EXE_AI_DIR, "exe-employees.json");
|
|
864
|
-
MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
|
|
886
|
+
INTERCOM_LOG = path5.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
865
887
|
}
|
|
866
888
|
});
|
|
867
889
|
|
|
@@ -1076,7 +1098,7 @@ function _resetLastRelaunchCache() {
|
|
|
1076
1098
|
}
|
|
1077
1099
|
async function lastResumeCreatedAtMs(agentId) {
|
|
1078
1100
|
const client = getClient();
|
|
1079
|
-
const cmScope = sessionScopeFilter();
|
|
1101
|
+
const cmScope = sessionScopeFilter(null);
|
|
1080
1102
|
const result = await client.execute({
|
|
1081
1103
|
sql: `SELECT MAX(created_at) AS last_created_at
|
|
1082
1104
|
FROM tasks
|
|
@@ -1101,7 +1123,7 @@ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
|
1101
1123
|
const client = getClient();
|
|
1102
1124
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1103
1125
|
const context = buildResumeContext(agentId, openTasks);
|
|
1104
|
-
const rdScope = sessionScopeFilter();
|
|
1126
|
+
const rdScope = sessionScopeFilter(null);
|
|
1105
1127
|
const existing = await client.execute({
|
|
1106
1128
|
sql: `SELECT id FROM tasks
|
|
1107
1129
|
WHERE assigned_to = ?
|
|
@@ -1135,7 +1157,7 @@ async function pollCapacityDead() {
|
|
|
1135
1157
|
const transport = getTransport();
|
|
1136
1158
|
const relaunched = [];
|
|
1137
1159
|
const registered = listSessions().filter(
|
|
1138
|
-
(s) => s.agentId !== "exe"
|
|
1160
|
+
(s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
|
|
1139
1161
|
);
|
|
1140
1162
|
if (registered.length === 0) return [];
|
|
1141
1163
|
let liveSessions;
|
|
@@ -1195,7 +1217,7 @@ async function pollCapacityDead() {
|
|
|
1195
1217
|
reason: "capacity"
|
|
1196
1218
|
});
|
|
1197
1219
|
const client = getClient();
|
|
1198
|
-
const rlScope = sessionScopeFilter();
|
|
1220
|
+
const rlScope = sessionScopeFilter(null);
|
|
1199
1221
|
const openTasks = await client.execute({
|
|
1200
1222
|
sql: `SELECT id, title, priority, task_file, status
|
|
1201
1223
|
FROM tasks
|
|
@@ -1249,6 +1271,7 @@ var init_capacity_monitor = __esm({
|
|
|
1249
1271
|
init_session_kill_telemetry();
|
|
1250
1272
|
init_tmux_routing();
|
|
1251
1273
|
init_task_scope();
|
|
1274
|
+
init_employees();
|
|
1252
1275
|
CAPACITY_PATTERNS = [
|
|
1253
1276
|
/conversation is too long/i,
|
|
1254
1277
|
/maximum context length/i,
|
|
@@ -1398,7 +1421,7 @@ function employeeSessionName(employee, exeSession, instance) {
|
|
|
1398
1421
|
exeSession = root;
|
|
1399
1422
|
} else {
|
|
1400
1423
|
throw new Error(
|
|
1401
|
-
`Invalid
|
|
1424
|
+
`Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
1402
1425
|
);
|
|
1403
1426
|
}
|
|
1404
1427
|
}
|
|
@@ -1418,8 +1441,10 @@ function parseParentExe(sessionName, agentId) {
|
|
|
1418
1441
|
return match?.[1] ?? null;
|
|
1419
1442
|
}
|
|
1420
1443
|
function extractRootExe(name) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1444
|
+
if (!name) return null;
|
|
1445
|
+
if (!name.includes("-")) return name;
|
|
1446
|
+
const parts = name.split("-").filter(Boolean);
|
|
1447
|
+
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
1423
1448
|
}
|
|
1424
1449
|
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
1425
1450
|
if (!existsSync8(SESSION_CACHE)) {
|
|
@@ -1564,12 +1589,14 @@ function isSessionBusy(sessionName) {
|
|
|
1564
1589
|
return state === "thinking" || state === "tool";
|
|
1565
1590
|
}
|
|
1566
1591
|
function isExeSession(sessionName) {
|
|
1567
|
-
|
|
1592
|
+
const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
|
|
1593
|
+
const coordinatorName = getCoordinatorName();
|
|
1594
|
+
return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
|
|
1568
1595
|
}
|
|
1569
1596
|
function sendIntercom(targetSession) {
|
|
1570
1597
|
const transport = getTransport();
|
|
1571
1598
|
if (isExeSession(targetSession)) {
|
|
1572
|
-
logIntercom(`
|
|
1599
|
+
logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
|
|
1573
1600
|
return "skipped_exe";
|
|
1574
1601
|
}
|
|
1575
1602
|
if (isDebounced(targetSession)) {
|
|
@@ -1621,7 +1648,7 @@ function notifyParentExe(sessionKey) {
|
|
|
1621
1648
|
if (result === "failed") {
|
|
1622
1649
|
const rootExe = resolveExeSession();
|
|
1623
1650
|
if (rootExe && rootExe !== target) {
|
|
1624
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root
|
|
1651
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
|
|
1625
1652
|
`);
|
|
1626
1653
|
const fallback = sendIntercom(rootExe);
|
|
1627
1654
|
return fallback !== "failed";
|
|
@@ -1631,8 +1658,8 @@ function notifyParentExe(sessionKey) {
|
|
|
1631
1658
|
return true;
|
|
1632
1659
|
}
|
|
1633
1660
|
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
1634
|
-
if (employeeName === "exe") {
|
|
1635
|
-
return { status: "failed", sessionName: "", error: "
|
|
1661
|
+
if (employeeName === "exe" || isCoordinatorName(employeeName)) {
|
|
1662
|
+
return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
|
|
1636
1663
|
}
|
|
1637
1664
|
try {
|
|
1638
1665
|
assertEmployeeLimitSync();
|
|
@@ -1641,8 +1668,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1641
1668
|
return { status: "failed", sessionName: "", error: err.message };
|
|
1642
1669
|
}
|
|
1643
1670
|
}
|
|
1644
|
-
if (
|
|
1645
|
-
const bare = employeeName.
|
|
1671
|
+
if (employeeName.includes("-")) {
|
|
1672
|
+
const bare = employeeName.split("-")[0].replace(/\d+$/, "");
|
|
1646
1673
|
return {
|
|
1647
1674
|
status: "failed",
|
|
1648
1675
|
sessionName: "",
|
|
@@ -1661,7 +1688,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1661
1688
|
return {
|
|
1662
1689
|
status: "failed",
|
|
1663
1690
|
sessionName: "",
|
|
1664
|
-
error: `Invalid
|
|
1691
|
+
error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
1665
1692
|
};
|
|
1666
1693
|
}
|
|
1667
1694
|
}
|
|
@@ -1818,8 +1845,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1818
1845
|
const ctxContent = [
|
|
1819
1846
|
`## Session Context`,
|
|
1820
1847
|
`You are running in tmux session: ${sessionName}.`,
|
|
1821
|
-
`Your parent
|
|
1822
|
-
`Your employees (if any) use the -${exeSession} suffix
|
|
1848
|
+
`Your parent coordinator session is ${exeSession}.`,
|
|
1849
|
+
`Your employees (if any) use the -${exeSession} suffix.`
|
|
1823
1850
|
].join("\n");
|
|
1824
1851
|
writeFileSync5(ctxFile, ctxContent);
|
|
1825
1852
|
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
@@ -1923,6 +1950,7 @@ var init_tmux_routing = __esm({
|
|
|
1923
1950
|
init_provider_table();
|
|
1924
1951
|
init_intercom_queue();
|
|
1925
1952
|
init_plan_limits();
|
|
1953
|
+
init_employees();
|
|
1926
1954
|
SPAWN_LOCK_DIR = path8.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
1927
1955
|
SESSION_CACHE = path8.join(os6.homedir(), ".exe-os", "session-cache");
|
|
1928
1956
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
@@ -2205,6 +2233,36 @@ async function listTasks(input) {
|
|
|
2205
2233
|
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
2206
2234
|
}));
|
|
2207
2235
|
}
|
|
2236
|
+
function isTmuxSessionAlive(identifier) {
|
|
2237
|
+
if (!identifier || identifier === "unknown") return true;
|
|
2238
|
+
try {
|
|
2239
|
+
if (identifier.startsWith("%")) {
|
|
2240
|
+
const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
|
|
2241
|
+
timeout: 2e3,
|
|
2242
|
+
encoding: "utf8",
|
|
2243
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2244
|
+
});
|
|
2245
|
+
return output.split("\n").some((l) => l.trim() === identifier);
|
|
2246
|
+
} else {
|
|
2247
|
+
execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
|
|
2248
|
+
timeout: 2e3,
|
|
2249
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2250
|
+
});
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2253
|
+
} catch {
|
|
2254
|
+
if (identifier.startsWith("%")) return true;
|
|
2255
|
+
try {
|
|
2256
|
+
execSync5("tmux list-sessions", {
|
|
2257
|
+
timeout: 2e3,
|
|
2258
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2259
|
+
});
|
|
2260
|
+
return false;
|
|
2261
|
+
} catch {
|
|
2262
|
+
return true;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2208
2266
|
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
2209
2267
|
if (!taskContext) return null;
|
|
2210
2268
|
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
@@ -2267,13 +2325,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
2267
2325
|
});
|
|
2268
2326
|
if (claim.rowsAffected === 0) {
|
|
2269
2327
|
const current = await client.execute({
|
|
2270
|
-
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
2328
|
+
sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
|
|
2271
2329
|
args: [taskId]
|
|
2272
2330
|
});
|
|
2273
2331
|
const cur = current.rows[0];
|
|
2274
|
-
const
|
|
2275
|
-
const
|
|
2276
|
-
|
|
2332
|
+
const curStatus = cur?.status ?? "unknown";
|
|
2333
|
+
const claimedBySession = cur?.assigned_tmux ?? "";
|
|
2334
|
+
const assignedBy = cur?.assigned_by ?? "";
|
|
2335
|
+
if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
|
|
2336
|
+
process.stderr.write(
|
|
2337
|
+
`[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
|
|
2338
|
+
`
|
|
2339
|
+
);
|
|
2340
|
+
await client.execute({
|
|
2341
|
+
sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
|
|
2342
|
+
args: [now, taskId]
|
|
2343
|
+
});
|
|
2344
|
+
const retried = await client.execute({
|
|
2345
|
+
sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
|
|
2346
|
+
args: [tmuxSession, now, taskId]
|
|
2347
|
+
});
|
|
2348
|
+
if (retried.rowsAffected > 0) {
|
|
2349
|
+
try {
|
|
2350
|
+
await writeCheckpoint({
|
|
2351
|
+
taskId,
|
|
2352
|
+
step: "reclaimed_dead_session",
|
|
2353
|
+
contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
|
|
2354
|
+
});
|
|
2355
|
+
} catch {
|
|
2356
|
+
}
|
|
2357
|
+
return { row, taskFile, now, taskId };
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
|
|
2361
|
+
process.stderr.write(
|
|
2362
|
+
`[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
|
|
2363
|
+
`
|
|
2364
|
+
);
|
|
2365
|
+
await client.execute({
|
|
2366
|
+
sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
|
|
2367
|
+
args: [tmuxSession, now, taskId]
|
|
2368
|
+
});
|
|
2369
|
+
try {
|
|
2370
|
+
await writeCheckpoint({
|
|
2371
|
+
taskId,
|
|
2372
|
+
step: "assigner_override",
|
|
2373
|
+
contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
|
|
2374
|
+
});
|
|
2375
|
+
} catch {
|
|
2376
|
+
}
|
|
2377
|
+
return { row, taskFile, now, taskId };
|
|
2378
|
+
}
|
|
2379
|
+
const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
|
|
2380
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
|
|
2277
2381
|
}
|
|
2278
2382
|
try {
|
|
2279
2383
|
await writeCheckpoint({
|
|
@@ -2371,7 +2475,7 @@ var init_tasks_crud = __esm({
|
|
|
2371
2475
|
"use strict";
|
|
2372
2476
|
init_database();
|
|
2373
2477
|
init_task_scope();
|
|
2374
|
-
DELEGATION_KEYWORDS = /parallel|delegate|wave|
|
|
2478
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
|
|
2375
2479
|
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
2376
2480
|
}
|
|
2377
2481
|
});
|
|
@@ -2728,7 +2832,7 @@ function findSessionForProject(projectName) {
|
|
|
2728
2832
|
const sessions = listSessions();
|
|
2729
2833
|
for (const s of sessions) {
|
|
2730
2834
|
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
2731
|
-
if (proj === projectName && s.agentId === "exe") return s;
|
|
2835
|
+
if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
|
|
2732
2836
|
}
|
|
2733
2837
|
return null;
|
|
2734
2838
|
}
|
|
@@ -2768,12 +2872,13 @@ var init_session_scope = __esm({
|
|
|
2768
2872
|
init_session_registry();
|
|
2769
2873
|
init_project_name();
|
|
2770
2874
|
init_tmux_routing();
|
|
2875
|
+
init_employees();
|
|
2771
2876
|
}
|
|
2772
2877
|
});
|
|
2773
2878
|
|
|
2774
2879
|
// src/lib/tasks-notify.ts
|
|
2775
2880
|
async function dispatchTaskToEmployee(input) {
|
|
2776
|
-
if (input.assignedTo === "exe") return { dispatched: "skipped" };
|
|
2881
|
+
if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
|
|
2777
2882
|
let crossProject = false;
|
|
2778
2883
|
if (input.projectName) {
|
|
2779
2884
|
try {
|
|
@@ -3216,6 +3321,24 @@ async function updateTask(input) {
|
|
|
3216
3321
|
});
|
|
3217
3322
|
} catch {
|
|
3218
3323
|
}
|
|
3324
|
+
const assignedAgent = String(row.assigned_to);
|
|
3325
|
+
if (!isCoordinatorName(assignedAgent)) {
|
|
3326
|
+
try {
|
|
3327
|
+
const draftClient = getClient();
|
|
3328
|
+
if (input.status === "done") {
|
|
3329
|
+
await draftClient.execute({
|
|
3330
|
+
sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
|
|
3331
|
+
args: [assignedAgent]
|
|
3332
|
+
});
|
|
3333
|
+
} else if (input.status === "cancelled") {
|
|
3334
|
+
await draftClient.execute({
|
|
3335
|
+
sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
|
|
3336
|
+
args: [assignedAgent]
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
} catch {
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3219
3342
|
try {
|
|
3220
3343
|
const client = getClient();
|
|
3221
3344
|
const cascaded = await client.execute({
|
|
@@ -3234,8 +3357,8 @@ async function updateTask(input) {
|
|
|
3234
3357
|
}
|
|
3235
3358
|
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
3236
3359
|
if (isTerminal) {
|
|
3237
|
-
const
|
|
3238
|
-
if (!
|
|
3360
|
+
const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
|
|
3361
|
+
if (!isCoordinator) {
|
|
3239
3362
|
notifyTaskDone();
|
|
3240
3363
|
}
|
|
3241
3364
|
await markTaskNotificationsRead(taskFile);
|
|
@@ -3259,7 +3382,7 @@ async function updateTask(input) {
|
|
|
3259
3382
|
}
|
|
3260
3383
|
}
|
|
3261
3384
|
}
|
|
3262
|
-
if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
|
|
3385
|
+
if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
3263
3386
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
3264
3387
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
3265
3388
|
taskId,
|
|
@@ -3275,7 +3398,7 @@ async function updateTask(input) {
|
|
|
3275
3398
|
});
|
|
3276
3399
|
}
|
|
3277
3400
|
let nextTask;
|
|
3278
|
-
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
3401
|
+
if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
|
|
3279
3402
|
try {
|
|
3280
3403
|
nextTask = await findNextTask(String(row.assigned_to));
|
|
3281
3404
|
} catch {
|
|
@@ -3302,12 +3425,14 @@ async function updateTask(input) {
|
|
|
3302
3425
|
async function deleteTask(taskId, baseDir) {
|
|
3303
3426
|
const client = getClient();
|
|
3304
3427
|
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
3305
|
-
const
|
|
3428
|
+
const coordinatorName = getCoordinatorName();
|
|
3429
|
+
const reviewer = assignedBy || coordinatorName;
|
|
3306
3430
|
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
3307
3431
|
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
3432
|
+
const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
|
|
3308
3433
|
await client.execute({
|
|
3309
|
-
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
|
|
3310
|
-
args: [reviewFile, `exe/exe/${reviewSlug}.md`]
|
|
3434
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
|
|
3435
|
+
args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
|
|
3311
3436
|
});
|
|
3312
3437
|
await markAsReadByTaskFile(taskFile);
|
|
3313
3438
|
await markAsReadByTaskFile(reviewFile);
|
|
@@ -3319,6 +3444,7 @@ var init_tasks = __esm({
|
|
|
3319
3444
|
init_config();
|
|
3320
3445
|
init_notifications();
|
|
3321
3446
|
init_state_bus();
|
|
3447
|
+
init_employees();
|
|
3322
3448
|
init_tasks_crud();
|
|
3323
3449
|
init_tasks_review();
|
|
3324
3450
|
init_tasks_crud();
|
|
@@ -3342,8 +3468,51 @@ import path14 from "path";
|
|
|
3342
3468
|
init_session_key();
|
|
3343
3469
|
|
|
3344
3470
|
// src/adapters/claude/active-agent.ts
|
|
3471
|
+
init_employees();
|
|
3345
3472
|
var CACHE_DIR = path14.join(EXE_AI_DIR, "session-cache");
|
|
3346
3473
|
var STALE_MS = 24 * 60 * 60 * 1e3;
|
|
3474
|
+
function isNameWithOptionalInstance(candidate, baseName) {
|
|
3475
|
+
if (candidate === baseName) return true;
|
|
3476
|
+
if (!candidate.startsWith(baseName)) return false;
|
|
3477
|
+
return /^\d+$/.test(candidate.slice(baseName.length));
|
|
3478
|
+
}
|
|
3479
|
+
function resolveEmployeeFromSessionPrefix(prefix, employees) {
|
|
3480
|
+
const sorted = [...employees].sort((a, b) => b.name.length - a.name.length);
|
|
3481
|
+
for (const employee of sorted) {
|
|
3482
|
+
if (isNameWithOptionalInstance(prefix, employee.name)) {
|
|
3483
|
+
return { agentId: employee.name, agentRole: employee.role };
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
return null;
|
|
3487
|
+
}
|
|
3488
|
+
function resolveActiveAgentFromTmuxSession(sessionName) {
|
|
3489
|
+
const employees = loadEmployeesSync();
|
|
3490
|
+
const coordinator = getCoordinatorEmployee(employees);
|
|
3491
|
+
const coordinatorName = coordinator?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
|
|
3492
|
+
if (isNameWithOptionalInstance(sessionName, coordinatorName)) {
|
|
3493
|
+
return {
|
|
3494
|
+
agentId: coordinatorName,
|
|
3495
|
+
agentRole: coordinator?.role ?? "COO"
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
if (isNameWithOptionalInstance(sessionName, DEFAULT_COORDINATOR_TEMPLATE_NAME)) {
|
|
3499
|
+
return {
|
|
3500
|
+
agentId: coordinator?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME,
|
|
3501
|
+
agentRole: coordinator?.role ?? "COO"
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
if (sessionName.includes("-")) {
|
|
3505
|
+
const prefix = sessionName.split("-")[0] ?? "";
|
|
3506
|
+
const employee = resolveEmployeeFromSessionPrefix(prefix, employees);
|
|
3507
|
+
if (employee) return employee;
|
|
3508
|
+
const legacy = prefix.match(/^([a-zA-Z]+)\d*$/);
|
|
3509
|
+
if (legacy?.[1] && legacy[1] !== DEFAULT_COORDINATOR_TEMPLATE_NAME) {
|
|
3510
|
+
const emp = getEmployee(employees, legacy[1]);
|
|
3511
|
+
return { agentId: emp?.name ?? legacy[1], agentRole: emp?.role ?? "employee" };
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return null;
|
|
3515
|
+
}
|
|
3347
3516
|
function getMarkerPath() {
|
|
3348
3517
|
return path14.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
|
|
3349
3518
|
}
|
|
@@ -3380,13 +3549,8 @@ function getActiveAgent() {
|
|
|
3380
3549
|
"tmux display-message -p '#{session_name}' 2>/dev/null",
|
|
3381
3550
|
{ encoding: "utf8", timeout: 2e3 }
|
|
3382
3551
|
).trim();
|
|
3383
|
-
const
|
|
3384
|
-
if (
|
|
3385
|
-
return { agentId: empMatch[1], agentRole: "employee" };
|
|
3386
|
-
}
|
|
3387
|
-
if (/^exe\d+$/.test(sessionName)) {
|
|
3388
|
-
return { agentId: "exe", agentRole: "COO" };
|
|
3389
|
-
}
|
|
3552
|
+
const resolved = resolveActiveAgentFromTmuxSession(sessionName);
|
|
3553
|
+
if (resolved) return resolved;
|
|
3390
3554
|
} catch {
|
|
3391
3555
|
}
|
|
3392
3556
|
return {
|