@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
package/dist/bin/exe-dispatch.js
CHANGED
|
@@ -357,7 +357,7 @@ function wrapWithRetry(client) {
|
|
|
357
357
|
return (sql) => retryOnBusy(() => target.execute(sql), "execute");
|
|
358
358
|
}
|
|
359
359
|
if (prop === "batch") {
|
|
360
|
-
return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
|
|
360
|
+
return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
|
|
361
361
|
}
|
|
362
362
|
return Reflect.get(target, prop, receiver);
|
|
363
363
|
}
|
|
@@ -373,6 +373,240 @@ var init_db_retry = __esm({
|
|
|
373
373
|
}
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
+
// src/lib/config.ts
|
|
377
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
378
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
|
|
379
|
+
import path3 from "path";
|
|
380
|
+
import os3 from "os";
|
|
381
|
+
function resolveDataDir() {
|
|
382
|
+
if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
|
|
383
|
+
if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
|
|
384
|
+
const newDir = path3.join(os3.homedir(), ".exe-os");
|
|
385
|
+
const legacyDir = path3.join(os3.homedir(), ".exe-mem");
|
|
386
|
+
if (!existsSync3(newDir) && existsSync3(legacyDir)) {
|
|
387
|
+
try {
|
|
388
|
+
renameSync2(legacyDir, newDir);
|
|
389
|
+
process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
|
|
390
|
+
`);
|
|
391
|
+
} catch {
|
|
392
|
+
return legacyDir;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return newDir;
|
|
396
|
+
}
|
|
397
|
+
function migrateLegacyConfig(raw) {
|
|
398
|
+
if ("r2" in raw) {
|
|
399
|
+
process.stderr.write(
|
|
400
|
+
"[exe-os] Warning: config.json contains deprecated 'r2' field from v1.0. R2 sync has been replaced in v1.1. The 'r2' field will be ignored.\n"
|
|
401
|
+
);
|
|
402
|
+
delete raw.r2;
|
|
403
|
+
}
|
|
404
|
+
if ("syncIntervalMs" in raw) {
|
|
405
|
+
delete raw.syncIntervalMs;
|
|
406
|
+
}
|
|
407
|
+
return raw;
|
|
408
|
+
}
|
|
409
|
+
function migrateConfig(raw) {
|
|
410
|
+
const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
|
|
411
|
+
let currentVersion = fromVersion;
|
|
412
|
+
let migrated = false;
|
|
413
|
+
if (currentVersion > CURRENT_CONFIG_VERSION) {
|
|
414
|
+
return { config: raw, migrated: false, fromVersion };
|
|
415
|
+
}
|
|
416
|
+
for (const migration of CONFIG_MIGRATIONS) {
|
|
417
|
+
if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
|
|
418
|
+
raw = migration.migrate(raw);
|
|
419
|
+
currentVersion = migration.to;
|
|
420
|
+
migrated = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { config: raw, migrated, fromVersion };
|
|
424
|
+
}
|
|
425
|
+
function normalizeScalingRoadmap(raw) {
|
|
426
|
+
const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
|
|
427
|
+
const userRoadmap = raw.scalingRoadmap ?? {};
|
|
428
|
+
const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
|
|
429
|
+
if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
|
|
430
|
+
userAuto.enabled = raw.rerankerEnabled;
|
|
431
|
+
}
|
|
432
|
+
raw.scalingRoadmap = {
|
|
433
|
+
...userRoadmap,
|
|
434
|
+
rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function normalizeSessionLifecycle(raw) {
|
|
438
|
+
const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
|
|
439
|
+
const userSL = raw.sessionLifecycle ?? {};
|
|
440
|
+
raw.sessionLifecycle = { ...defaultSL, ...userSL };
|
|
441
|
+
}
|
|
442
|
+
function normalizeAutoUpdate(raw) {
|
|
443
|
+
const defaultAU = DEFAULT_CONFIG.autoUpdate;
|
|
444
|
+
const userAU = raw.autoUpdate ?? {};
|
|
445
|
+
raw.autoUpdate = { ...defaultAU, ...userAU };
|
|
446
|
+
}
|
|
447
|
+
async function loadConfig() {
|
|
448
|
+
const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
|
|
449
|
+
await mkdir(dir, { recursive: true });
|
|
450
|
+
const configPath = path3.join(dir, "config.json");
|
|
451
|
+
if (!existsSync3(configPath)) {
|
|
452
|
+
return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
|
|
453
|
+
}
|
|
454
|
+
const raw = await readFile(configPath, "utf-8");
|
|
455
|
+
try {
|
|
456
|
+
let parsed = JSON.parse(raw);
|
|
457
|
+
parsed = migrateLegacyConfig(parsed);
|
|
458
|
+
const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
|
|
459
|
+
if (migrated) {
|
|
460
|
+
process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
|
|
461
|
+
`);
|
|
462
|
+
try {
|
|
463
|
+
await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
normalizeScalingRoadmap(migratedCfg);
|
|
468
|
+
normalizeSessionLifecycle(migratedCfg);
|
|
469
|
+
normalizeAutoUpdate(migratedCfg);
|
|
470
|
+
const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
|
|
471
|
+
if (config.dbPath.startsWith("~")) {
|
|
472
|
+
config.dbPath = config.dbPath.replace(/^~/, os3.homedir());
|
|
473
|
+
}
|
|
474
|
+
return config;
|
|
475
|
+
} catch {
|
|
476
|
+
return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
480
|
+
var init_config = __esm({
|
|
481
|
+
"src/lib/config.ts"() {
|
|
482
|
+
"use strict";
|
|
483
|
+
EXE_AI_DIR = resolveDataDir();
|
|
484
|
+
DB_PATH = path3.join(EXE_AI_DIR, "memories.db");
|
|
485
|
+
MODELS_DIR = path3.join(EXE_AI_DIR, "models");
|
|
486
|
+
CONFIG_PATH = path3.join(EXE_AI_DIR, "config.json");
|
|
487
|
+
LEGACY_LANCE_PATH = path3.join(EXE_AI_DIR, "local.lance");
|
|
488
|
+
CURRENT_CONFIG_VERSION = 1;
|
|
489
|
+
DEFAULT_CONFIG = {
|
|
490
|
+
config_version: CURRENT_CONFIG_VERSION,
|
|
491
|
+
dbPath: DB_PATH,
|
|
492
|
+
modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
|
|
493
|
+
embeddingDim: 1024,
|
|
494
|
+
batchSize: 20,
|
|
495
|
+
flushIntervalMs: 1e4,
|
|
496
|
+
autoIngestion: true,
|
|
497
|
+
autoRetrieval: true,
|
|
498
|
+
searchMode: "hybrid",
|
|
499
|
+
hookSearchMode: "hybrid",
|
|
500
|
+
fileGrepEnabled: true,
|
|
501
|
+
splashEffect: true,
|
|
502
|
+
consolidationEnabled: true,
|
|
503
|
+
consolidationIntervalMs: 6 * 60 * 60 * 1e3,
|
|
504
|
+
consolidationModel: "claude-haiku-4-5-20251001",
|
|
505
|
+
consolidationMaxCallsPerRun: 20,
|
|
506
|
+
selfQueryRouter: true,
|
|
507
|
+
selfQueryModel: "claude-haiku-4-5-20251001",
|
|
508
|
+
rerankerEnabled: true,
|
|
509
|
+
scalingRoadmap: {
|
|
510
|
+
rerankerAutoTrigger: {
|
|
511
|
+
enabled: true,
|
|
512
|
+
broadQueryMinCardinality: 5e4,
|
|
513
|
+
fetchTopK: 150,
|
|
514
|
+
returnTopK: 5
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
graphRagEnabled: true,
|
|
518
|
+
wikiEnabled: false,
|
|
519
|
+
wikiUrl: "",
|
|
520
|
+
wikiApiKey: "",
|
|
521
|
+
wikiSyncIntervalMs: 30 * 60 * 1e3,
|
|
522
|
+
wikiWorkspaceMapping: {},
|
|
523
|
+
wikiAutoUpdate: true,
|
|
524
|
+
wikiAutoUpdateThreshold: 0.5,
|
|
525
|
+
wikiAutoUpdateCreateNew: true,
|
|
526
|
+
skillLearning: true,
|
|
527
|
+
skillThreshold: 3,
|
|
528
|
+
skillModel: "claude-haiku-4-5-20251001",
|
|
529
|
+
exeHeartbeat: {
|
|
530
|
+
enabled: true,
|
|
531
|
+
intervalSeconds: 60,
|
|
532
|
+
staleInProgressThresholdHours: 2
|
|
533
|
+
},
|
|
534
|
+
sessionLifecycle: {
|
|
535
|
+
idleKillEnabled: true,
|
|
536
|
+
idleKillTicksRequired: 3,
|
|
537
|
+
idleKillIntercomAckWindowMs: 1e4,
|
|
538
|
+
maxAutoInstances: 10
|
|
539
|
+
},
|
|
540
|
+
autoUpdate: {
|
|
541
|
+
checkOnBoot: true,
|
|
542
|
+
autoInstall: false,
|
|
543
|
+
checkIntervalMs: 24 * 60 * 60 * 1e3
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
CONFIG_MIGRATIONS = [
|
|
547
|
+
{
|
|
548
|
+
from: 0,
|
|
549
|
+
to: 1,
|
|
550
|
+
migrate: (cfg) => {
|
|
551
|
+
cfg.config_version = 1;
|
|
552
|
+
return cfg;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
];
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// src/lib/employees.ts
|
|
560
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
561
|
+
import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
562
|
+
import { execSync as execSync3 } from "child_process";
|
|
563
|
+
import path4 from "path";
|
|
564
|
+
import os4 from "os";
|
|
565
|
+
function normalizeRole(role) {
|
|
566
|
+
return (role ?? "").trim().toLowerCase();
|
|
567
|
+
}
|
|
568
|
+
function isCoordinatorRole(role) {
|
|
569
|
+
return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
|
|
570
|
+
}
|
|
571
|
+
function getCoordinatorEmployee(employees) {
|
|
572
|
+
return employees.find((e) => isCoordinatorRole(e.role));
|
|
573
|
+
}
|
|
574
|
+
function getCoordinatorName(employees = loadEmployeesSync()) {
|
|
575
|
+
return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
|
|
576
|
+
}
|
|
577
|
+
function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
|
|
578
|
+
if (!agentName) return false;
|
|
579
|
+
return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
|
|
580
|
+
}
|
|
581
|
+
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
582
|
+
if (!existsSync4(employeesPath)) return [];
|
|
583
|
+
try {
|
|
584
|
+
return JSON.parse(readFileSync4(employeesPath, "utf-8"));
|
|
585
|
+
} catch {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function getEmployee(employees, name) {
|
|
590
|
+
return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
591
|
+
}
|
|
592
|
+
function isMultiInstance(agentName, employees) {
|
|
593
|
+
const roster = employees ?? loadEmployeesSync();
|
|
594
|
+
const emp = getEmployee(roster, agentName);
|
|
595
|
+
if (!emp) return false;
|
|
596
|
+
return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
|
|
597
|
+
}
|
|
598
|
+
var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
|
|
599
|
+
var init_employees = __esm({
|
|
600
|
+
"src/lib/employees.ts"() {
|
|
601
|
+
"use strict";
|
|
602
|
+
init_config();
|
|
603
|
+
EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
|
|
604
|
+
DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
|
|
605
|
+
COORDINATOR_ROLE = "COO";
|
|
606
|
+
MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
376
610
|
// src/lib/database.ts
|
|
377
611
|
import { createClient } from "@libsql/client";
|
|
378
612
|
async function initDatabase(config) {
|
|
@@ -506,22 +740,24 @@ async function ensureSchema() {
|
|
|
506
740
|
ON behaviors(agent_id, active);
|
|
507
741
|
`);
|
|
508
742
|
try {
|
|
743
|
+
const coordinatorName = getCoordinatorName();
|
|
509
744
|
const existing = await client.execute({
|
|
510
|
-
sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id =
|
|
511
|
-
args: []
|
|
745
|
+
sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
|
|
746
|
+
args: [coordinatorName]
|
|
512
747
|
});
|
|
513
748
|
if (Number(existing.rows[0]?.cnt) === 0) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
749
|
+
const seededAt = "2026-03-25T00:00:00Z";
|
|
750
|
+
for (const [domain, content] of [
|
|
751
|
+
["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
|
|
752
|
+
["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
|
|
753
|
+
["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
|
|
754
|
+
]) {
|
|
755
|
+
await client.execute({
|
|
756
|
+
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
|
|
757
|
+
VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
|
|
758
|
+
args: [coordinatorName, domain, content, seededAt, seededAt]
|
|
759
|
+
});
|
|
760
|
+
}
|
|
525
761
|
}
|
|
526
762
|
} catch {
|
|
527
763
|
}
|
|
@@ -1213,237 +1449,49 @@ async function ensureSchema() {
|
|
|
1213
1449
|
} catch {
|
|
1214
1450
|
}
|
|
1215
1451
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
_client = null;
|
|
1223
|
-
_resilientClient = null;
|
|
1224
|
-
initTurso = initDatabase;
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
// src/lib/config.ts
|
|
1229
|
-
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
1230
|
-
import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
|
|
1231
|
-
import path3 from "path";
|
|
1232
|
-
import os3 from "os";
|
|
1233
|
-
function resolveDataDir() {
|
|
1234
|
-
if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
|
|
1235
|
-
if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
|
|
1236
|
-
const newDir = path3.join(os3.homedir(), ".exe-os");
|
|
1237
|
-
const legacyDir = path3.join(os3.homedir(), ".exe-mem");
|
|
1238
|
-
if (!existsSync3(newDir) && existsSync3(legacyDir)) {
|
|
1239
|
-
try {
|
|
1240
|
-
renameSync2(legacyDir, newDir);
|
|
1241
|
-
process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
|
|
1242
|
-
`);
|
|
1243
|
-
} catch {
|
|
1244
|
-
return legacyDir;
|
|
1245
|
-
}
|
|
1452
|
+
try {
|
|
1453
|
+
await client.execute({
|
|
1454
|
+
sql: `ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0`,
|
|
1455
|
+
args: []
|
|
1456
|
+
});
|
|
1457
|
+
} catch {
|
|
1246
1458
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
if ("r2" in raw) {
|
|
1251
|
-
process.stderr.write(
|
|
1252
|
-
"[exe-os] Warning: config.json contains deprecated 'r2' field from v1.0. R2 sync has been replaced in v1.1. The 'r2' field will be ignored.\n"
|
|
1459
|
+
try {
|
|
1460
|
+
await client.execute(
|
|
1461
|
+
`CREATE INDEX IF NOT EXISTS idx_memories_draft ON memories(draft) WHERE draft = 1`
|
|
1253
1462
|
);
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1256
|
-
if ("syncIntervalMs" in raw) {
|
|
1257
|
-
delete raw.syncIntervalMs;
|
|
1258
|
-
}
|
|
1259
|
-
return raw;
|
|
1260
|
-
}
|
|
1261
|
-
function migrateConfig(raw) {
|
|
1262
|
-
const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
|
|
1263
|
-
let currentVersion = fromVersion;
|
|
1264
|
-
let migrated = false;
|
|
1265
|
-
if (currentVersion > CURRENT_CONFIG_VERSION) {
|
|
1266
|
-
return { config: raw, migrated: false, fromVersion };
|
|
1267
|
-
}
|
|
1268
|
-
for (const migration of CONFIG_MIGRATIONS) {
|
|
1269
|
-
if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
|
|
1270
|
-
raw = migration.migrate(raw);
|
|
1271
|
-
currentVersion = migration.to;
|
|
1272
|
-
migrated = true;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
return { config: raw, migrated, fromVersion };
|
|
1276
|
-
}
|
|
1277
|
-
function normalizeScalingRoadmap(raw) {
|
|
1278
|
-
const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
|
|
1279
|
-
const userRoadmap = raw.scalingRoadmap ?? {};
|
|
1280
|
-
const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
|
|
1281
|
-
if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
|
|
1282
|
-
userAuto.enabled = raw.rerankerEnabled;
|
|
1283
|
-
}
|
|
1284
|
-
raw.scalingRoadmap = {
|
|
1285
|
-
...userRoadmap,
|
|
1286
|
-
rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
function normalizeSessionLifecycle(raw) {
|
|
1290
|
-
const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
|
|
1291
|
-
const userSL = raw.sessionLifecycle ?? {};
|
|
1292
|
-
raw.sessionLifecycle = { ...defaultSL, ...userSL };
|
|
1293
|
-
}
|
|
1294
|
-
function normalizeAutoUpdate(raw) {
|
|
1295
|
-
const defaultAU = DEFAULT_CONFIG.autoUpdate;
|
|
1296
|
-
const userAU = raw.autoUpdate ?? {};
|
|
1297
|
-
raw.autoUpdate = { ...defaultAU, ...userAU };
|
|
1298
|
-
}
|
|
1299
|
-
async function loadConfig() {
|
|
1300
|
-
const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
|
|
1301
|
-
await mkdir(dir, { recursive: true });
|
|
1302
|
-
const configPath = path3.join(dir, "config.json");
|
|
1303
|
-
if (!existsSync3(configPath)) {
|
|
1304
|
-
return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
|
|
1463
|
+
} catch {
|
|
1305
1464
|
}
|
|
1306
|
-
const raw = await readFile(configPath, "utf-8");
|
|
1307
1465
|
try {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
|
|
1313
|
-
`);
|
|
1314
|
-
try {
|
|
1315
|
-
await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
|
|
1316
|
-
} catch {
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
normalizeScalingRoadmap(migratedCfg);
|
|
1320
|
-
normalizeSessionLifecycle(migratedCfg);
|
|
1321
|
-
normalizeAutoUpdate(migratedCfg);
|
|
1322
|
-
const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
|
|
1323
|
-
if (config.dbPath.startsWith("~")) {
|
|
1324
|
-
config.dbPath = config.dbPath.replace(/^~/, os3.homedir());
|
|
1325
|
-
}
|
|
1326
|
-
return config;
|
|
1466
|
+
await client.execute({
|
|
1467
|
+
sql: `ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'`,
|
|
1468
|
+
args: []
|
|
1469
|
+
});
|
|
1327
1470
|
} catch {
|
|
1328
|
-
return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
|
|
1329
1471
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
EXE_AI_DIR = resolveDataDir();
|
|
1336
|
-
DB_PATH = path3.join(EXE_AI_DIR, "memories.db");
|
|
1337
|
-
MODELS_DIR = path3.join(EXE_AI_DIR, "models");
|
|
1338
|
-
CONFIG_PATH = path3.join(EXE_AI_DIR, "config.json");
|
|
1339
|
-
LEGACY_LANCE_PATH = path3.join(EXE_AI_DIR, "local.lance");
|
|
1340
|
-
CURRENT_CONFIG_VERSION = 1;
|
|
1341
|
-
DEFAULT_CONFIG = {
|
|
1342
|
-
config_version: CURRENT_CONFIG_VERSION,
|
|
1343
|
-
dbPath: DB_PATH,
|
|
1344
|
-
modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
|
|
1345
|
-
embeddingDim: 1024,
|
|
1346
|
-
batchSize: 20,
|
|
1347
|
-
flushIntervalMs: 1e4,
|
|
1348
|
-
autoIngestion: true,
|
|
1349
|
-
autoRetrieval: true,
|
|
1350
|
-
searchMode: "hybrid",
|
|
1351
|
-
hookSearchMode: "hybrid",
|
|
1352
|
-
fileGrepEnabled: true,
|
|
1353
|
-
splashEffect: true,
|
|
1354
|
-
consolidationEnabled: true,
|
|
1355
|
-
consolidationIntervalMs: 6 * 60 * 60 * 1e3,
|
|
1356
|
-
consolidationModel: "claude-haiku-4-5-20251001",
|
|
1357
|
-
consolidationMaxCallsPerRun: 20,
|
|
1358
|
-
selfQueryRouter: true,
|
|
1359
|
-
selfQueryModel: "claude-haiku-4-5-20251001",
|
|
1360
|
-
rerankerEnabled: true,
|
|
1361
|
-
scalingRoadmap: {
|
|
1362
|
-
rerankerAutoTrigger: {
|
|
1363
|
-
enabled: true,
|
|
1364
|
-
broadQueryMinCardinality: 5e4,
|
|
1365
|
-
fetchTopK: 150,
|
|
1366
|
-
returnTopK: 5
|
|
1367
|
-
}
|
|
1368
|
-
},
|
|
1369
|
-
graphRagEnabled: true,
|
|
1370
|
-
wikiEnabled: false,
|
|
1371
|
-
wikiUrl: "",
|
|
1372
|
-
wikiApiKey: "",
|
|
1373
|
-
wikiSyncIntervalMs: 30 * 60 * 1e3,
|
|
1374
|
-
wikiWorkspaceMapping: {
|
|
1375
|
-
exe: "Executive",
|
|
1376
|
-
yoshi: "Engineering",
|
|
1377
|
-
mari: "Marketing",
|
|
1378
|
-
tom: "Engineering",
|
|
1379
|
-
sasha: "Production"
|
|
1380
|
-
},
|
|
1381
|
-
wikiAutoUpdate: true,
|
|
1382
|
-
wikiAutoUpdateThreshold: 0.5,
|
|
1383
|
-
wikiAutoUpdateCreateNew: true,
|
|
1384
|
-
skillLearning: true,
|
|
1385
|
-
skillThreshold: 3,
|
|
1386
|
-
skillModel: "claude-haiku-4-5-20251001",
|
|
1387
|
-
exeHeartbeat: {
|
|
1388
|
-
enabled: true,
|
|
1389
|
-
intervalSeconds: 60,
|
|
1390
|
-
staleInProgressThresholdHours: 2
|
|
1391
|
-
},
|
|
1392
|
-
sessionLifecycle: {
|
|
1393
|
-
idleKillEnabled: true,
|
|
1394
|
-
idleKillTicksRequired: 3,
|
|
1395
|
-
idleKillIntercomAckWindowMs: 1e4,
|
|
1396
|
-
maxAutoInstances: 10
|
|
1397
|
-
},
|
|
1398
|
-
autoUpdate: {
|
|
1399
|
-
checkOnBoot: true,
|
|
1400
|
-
autoInstall: false,
|
|
1401
|
-
checkIntervalMs: 24 * 60 * 60 * 1e3
|
|
1402
|
-
}
|
|
1403
|
-
};
|
|
1404
|
-
CONFIG_MIGRATIONS = [
|
|
1405
|
-
{
|
|
1406
|
-
from: 0,
|
|
1407
|
-
to: 1,
|
|
1408
|
-
migrate: (cfg) => {
|
|
1409
|
-
cfg.config_version = 1;
|
|
1410
|
-
return cfg;
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
];
|
|
1472
|
+
try {
|
|
1473
|
+
await client.execute(
|
|
1474
|
+
`CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
|
|
1475
|
+
);
|
|
1476
|
+
} catch {
|
|
1414
1477
|
}
|
|
1415
|
-
});
|
|
1416
|
-
|
|
1417
|
-
// src/lib/employees.ts
|
|
1418
|
-
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1419
|
-
import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1420
|
-
import { execSync as execSync3 } from "child_process";
|
|
1421
|
-
import path4 from "path";
|
|
1422
|
-
import os4 from "os";
|
|
1423
|
-
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
1424
|
-
if (!existsSync4(employeesPath)) return [];
|
|
1425
1478
|
try {
|
|
1426
|
-
|
|
1479
|
+
await client.execute({
|
|
1480
|
+
sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
|
|
1481
|
+
args: []
|
|
1482
|
+
});
|
|
1427
1483
|
} catch {
|
|
1428
|
-
return [];
|
|
1429
1484
|
}
|
|
1430
1485
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
function isMultiInstance(agentName, employees) {
|
|
1435
|
-
const roster = employees ?? loadEmployeesSync();
|
|
1436
|
-
const emp = getEmployee(roster, agentName);
|
|
1437
|
-
if (!emp) return false;
|
|
1438
|
-
return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
|
|
1439
|
-
}
|
|
1440
|
-
var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
|
|
1441
|
-
var init_employees = __esm({
|
|
1442
|
-
"src/lib/employees.ts"() {
|
|
1486
|
+
var _client, _resilientClient, initTurso;
|
|
1487
|
+
var init_database = __esm({
|
|
1488
|
+
"src/lib/database.ts"() {
|
|
1443
1489
|
"use strict";
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1490
|
+
init_db_retry();
|
|
1491
|
+
init_employees();
|
|
1492
|
+
_client = null;
|
|
1493
|
+
_resilientClient = null;
|
|
1494
|
+
initTurso = initDatabase;
|
|
1447
1495
|
}
|
|
1448
1496
|
});
|
|
1449
1497
|
|
|
@@ -1958,6 +2006,36 @@ async function listTasks(input) {
|
|
|
1958
2006
|
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
1959
2007
|
}));
|
|
1960
2008
|
}
|
|
2009
|
+
function isTmuxSessionAlive(identifier) {
|
|
2010
|
+
if (!identifier || identifier === "unknown") return true;
|
|
2011
|
+
try {
|
|
2012
|
+
if (identifier.startsWith("%")) {
|
|
2013
|
+
const output = execSync4("tmux list-panes -a -F '#{pane_id}'", {
|
|
2014
|
+
timeout: 2e3,
|
|
2015
|
+
encoding: "utf8",
|
|
2016
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2017
|
+
});
|
|
2018
|
+
return output.split("\n").some((l) => l.trim() === identifier);
|
|
2019
|
+
} else {
|
|
2020
|
+
execSync4(`tmux has-session -t ${JSON.stringify(identifier)}`, {
|
|
2021
|
+
timeout: 2e3,
|
|
2022
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2023
|
+
});
|
|
2024
|
+
return true;
|
|
2025
|
+
}
|
|
2026
|
+
} catch {
|
|
2027
|
+
if (identifier.startsWith("%")) return true;
|
|
2028
|
+
try {
|
|
2029
|
+
execSync4("tmux list-sessions", {
|
|
2030
|
+
timeout: 2e3,
|
|
2031
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2032
|
+
});
|
|
2033
|
+
return false;
|
|
2034
|
+
} catch {
|
|
2035
|
+
return true;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
1961
2039
|
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
1962
2040
|
if (!taskContext) return null;
|
|
1963
2041
|
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
@@ -2020,13 +2098,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
2020
2098
|
});
|
|
2021
2099
|
if (claim.rowsAffected === 0) {
|
|
2022
2100
|
const current = await client.execute({
|
|
2023
|
-
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
2101
|
+
sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
|
|
2024
2102
|
args: [taskId]
|
|
2025
2103
|
});
|
|
2026
2104
|
const cur = current.rows[0];
|
|
2027
|
-
const
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2105
|
+
const curStatus = cur?.status ?? "unknown";
|
|
2106
|
+
const claimedBySession = cur?.assigned_tmux ?? "";
|
|
2107
|
+
const assignedBy = cur?.assigned_by ?? "";
|
|
2108
|
+
if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
|
|
2109
|
+
process.stderr.write(
|
|
2110
|
+
`[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
|
|
2111
|
+
`
|
|
2112
|
+
);
|
|
2113
|
+
await client.execute({
|
|
2114
|
+
sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
|
|
2115
|
+
args: [now, taskId]
|
|
2116
|
+
});
|
|
2117
|
+
const retried = await client.execute({
|
|
2118
|
+
sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
|
|
2119
|
+
args: [tmuxSession, now, taskId]
|
|
2120
|
+
});
|
|
2121
|
+
if (retried.rowsAffected > 0) {
|
|
2122
|
+
try {
|
|
2123
|
+
await writeCheckpoint({
|
|
2124
|
+
taskId,
|
|
2125
|
+
step: "reclaimed_dead_session",
|
|
2126
|
+
contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
|
|
2127
|
+
});
|
|
2128
|
+
} catch {
|
|
2129
|
+
}
|
|
2130
|
+
return { row, taskFile, now, taskId };
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
|
|
2134
|
+
process.stderr.write(
|
|
2135
|
+
`[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
|
|
2136
|
+
`
|
|
2137
|
+
);
|
|
2138
|
+
await client.execute({
|
|
2139
|
+
sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
|
|
2140
|
+
args: [tmuxSession, now, taskId]
|
|
2141
|
+
});
|
|
2142
|
+
try {
|
|
2143
|
+
await writeCheckpoint({
|
|
2144
|
+
taskId,
|
|
2145
|
+
step: "assigner_override",
|
|
2146
|
+
contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
|
|
2147
|
+
});
|
|
2148
|
+
} catch {
|
|
2149
|
+
}
|
|
2150
|
+
return { row, taskFile, now, taskId };
|
|
2151
|
+
}
|
|
2152
|
+
const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
|
|
2153
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
|
|
2030
2154
|
}
|
|
2031
2155
|
try {
|
|
2032
2156
|
await writeCheckpoint({
|
|
@@ -2124,7 +2248,7 @@ var init_tasks_crud = __esm({
|
|
|
2124
2248
|
"use strict";
|
|
2125
2249
|
init_database();
|
|
2126
2250
|
init_task_scope();
|
|
2127
|
-
DELEGATION_KEYWORDS = /parallel|delegate|wave|
|
|
2251
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
|
|
2128
2252
|
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
2129
2253
|
}
|
|
2130
2254
|
});
|
|
@@ -2481,7 +2605,7 @@ function findSessionForProject(projectName) {
|
|
|
2481
2605
|
const sessions = listSessions();
|
|
2482
2606
|
for (const s of sessions) {
|
|
2483
2607
|
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
2484
|
-
if (proj === projectName && s.agentId === "exe") return s;
|
|
2608
|
+
if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
|
|
2485
2609
|
}
|
|
2486
2610
|
return null;
|
|
2487
2611
|
}
|
|
@@ -2521,12 +2645,13 @@ var init_session_scope = __esm({
|
|
|
2521
2645
|
init_session_registry();
|
|
2522
2646
|
init_project_name();
|
|
2523
2647
|
init_tmux_routing();
|
|
2648
|
+
init_employees();
|
|
2524
2649
|
}
|
|
2525
2650
|
});
|
|
2526
2651
|
|
|
2527
2652
|
// src/lib/tasks-notify.ts
|
|
2528
2653
|
async function dispatchTaskToEmployee(input) {
|
|
2529
|
-
if (input.assignedTo === "exe") return { dispatched: "skipped" };
|
|
2654
|
+
if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
|
|
2530
2655
|
let crossProject = false;
|
|
2531
2656
|
if (input.projectName) {
|
|
2532
2657
|
try {
|
|
@@ -2969,6 +3094,24 @@ async function updateTask(input) {
|
|
|
2969
3094
|
});
|
|
2970
3095
|
} catch {
|
|
2971
3096
|
}
|
|
3097
|
+
const assignedAgent = String(row.assigned_to);
|
|
3098
|
+
if (!isCoordinatorName(assignedAgent)) {
|
|
3099
|
+
try {
|
|
3100
|
+
const draftClient = getClient();
|
|
3101
|
+
if (input.status === "done") {
|
|
3102
|
+
await draftClient.execute({
|
|
3103
|
+
sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
|
|
3104
|
+
args: [assignedAgent]
|
|
3105
|
+
});
|
|
3106
|
+
} else if (input.status === "cancelled") {
|
|
3107
|
+
await draftClient.execute({
|
|
3108
|
+
sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
|
|
3109
|
+
args: [assignedAgent]
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
} catch {
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
2972
3115
|
try {
|
|
2973
3116
|
const client = getClient();
|
|
2974
3117
|
const cascaded = await client.execute({
|
|
@@ -2987,8 +3130,8 @@ async function updateTask(input) {
|
|
|
2987
3130
|
}
|
|
2988
3131
|
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
2989
3132
|
if (isTerminal) {
|
|
2990
|
-
const
|
|
2991
|
-
if (!
|
|
3133
|
+
const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
|
|
3134
|
+
if (!isCoordinator) {
|
|
2992
3135
|
notifyTaskDone();
|
|
2993
3136
|
}
|
|
2994
3137
|
await markTaskNotificationsRead(taskFile);
|
|
@@ -3012,7 +3155,7 @@ async function updateTask(input) {
|
|
|
3012
3155
|
}
|
|
3013
3156
|
}
|
|
3014
3157
|
}
|
|
3015
|
-
if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
|
|
3158
|
+
if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
3016
3159
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
3017
3160
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
3018
3161
|
taskId,
|
|
@@ -3028,7 +3171,7 @@ async function updateTask(input) {
|
|
|
3028
3171
|
});
|
|
3029
3172
|
}
|
|
3030
3173
|
let nextTask;
|
|
3031
|
-
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
3174
|
+
if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
|
|
3032
3175
|
try {
|
|
3033
3176
|
nextTask = await findNextTask(String(row.assigned_to));
|
|
3034
3177
|
} catch {
|
|
@@ -3055,12 +3198,14 @@ async function updateTask(input) {
|
|
|
3055
3198
|
async function deleteTask(taskId, baseDir) {
|
|
3056
3199
|
const client = getClient();
|
|
3057
3200
|
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
3058
|
-
const
|
|
3201
|
+
const coordinatorName = getCoordinatorName();
|
|
3202
|
+
const reviewer = assignedBy || coordinatorName;
|
|
3059
3203
|
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
3060
3204
|
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
3205
|
+
const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
|
|
3061
3206
|
await client.execute({
|
|
3062
|
-
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
|
|
3063
|
-
args: [reviewFile, `exe/exe/${reviewSlug}.md`]
|
|
3207
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
|
|
3208
|
+
args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
|
|
3064
3209
|
});
|
|
3065
3210
|
await markAsReadByTaskFile(taskFile);
|
|
3066
3211
|
await markAsReadByTaskFile(reviewFile);
|
|
@@ -3072,6 +3217,7 @@ var init_tasks = __esm({
|
|
|
3072
3217
|
init_config();
|
|
3073
3218
|
init_notifications();
|
|
3074
3219
|
init_state_bus();
|
|
3220
|
+
init_employees();
|
|
3075
3221
|
init_tasks_crud();
|
|
3076
3222
|
init_tasks_review();
|
|
3077
3223
|
init_tasks_crud();
|
|
@@ -3157,7 +3303,7 @@ function _resetLastRelaunchCache() {
|
|
|
3157
3303
|
}
|
|
3158
3304
|
async function lastResumeCreatedAtMs(agentId) {
|
|
3159
3305
|
const client = getClient();
|
|
3160
|
-
const cmScope = sessionScopeFilter();
|
|
3306
|
+
const cmScope = sessionScopeFilter(null);
|
|
3161
3307
|
const result2 = await client.execute({
|
|
3162
3308
|
sql: `SELECT MAX(created_at) AS last_created_at
|
|
3163
3309
|
FROM tasks
|
|
@@ -3182,7 +3328,7 @@ async function createOrRefreshResumeTask(agentId, projectDir2, openTasks) {
|
|
|
3182
3328
|
const client = getClient();
|
|
3183
3329
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3184
3330
|
const context = buildResumeContext(agentId, openTasks);
|
|
3185
|
-
const rdScope = sessionScopeFilter();
|
|
3331
|
+
const rdScope = sessionScopeFilter(null);
|
|
3186
3332
|
const existing = await client.execute({
|
|
3187
3333
|
sql: `SELECT id FROM tasks
|
|
3188
3334
|
WHERE assigned_to = ?
|
|
@@ -3216,7 +3362,7 @@ async function pollCapacityDead() {
|
|
|
3216
3362
|
const transport = getTransport();
|
|
3217
3363
|
const relaunched = [];
|
|
3218
3364
|
const registered = listSessions().filter(
|
|
3219
|
-
(s) => s.agentId !== "exe"
|
|
3365
|
+
(s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
|
|
3220
3366
|
);
|
|
3221
3367
|
if (registered.length === 0) return [];
|
|
3222
3368
|
let liveSessions;
|
|
@@ -3276,7 +3422,7 @@ async function pollCapacityDead() {
|
|
|
3276
3422
|
reason: "capacity"
|
|
3277
3423
|
});
|
|
3278
3424
|
const client = getClient();
|
|
3279
|
-
const rlScope = sessionScopeFilter();
|
|
3425
|
+
const rlScope = sessionScopeFilter(null);
|
|
3280
3426
|
const openTasks = await client.execute({
|
|
3281
3427
|
sql: `SELECT id, title, priority, task_file, status
|
|
3282
3428
|
FROM tasks
|
|
@@ -3330,6 +3476,7 @@ var init_capacity_monitor = __esm({
|
|
|
3330
3476
|
init_session_kill_telemetry();
|
|
3331
3477
|
init_tmux_routing();
|
|
3332
3478
|
init_task_scope();
|
|
3479
|
+
init_employees();
|
|
3333
3480
|
CAPACITY_PATTERNS = [
|
|
3334
3481
|
/conversation is too long/i,
|
|
3335
3482
|
/maximum context length/i,
|
|
@@ -3479,7 +3626,7 @@ function employeeSessionName(employee, exeSession2, instance) {
|
|
|
3479
3626
|
exeSession2 = root;
|
|
3480
3627
|
} else {
|
|
3481
3628
|
throw new Error(
|
|
3482
|
-
`Invalid
|
|
3629
|
+
`Invalid coordinator session "${exeSession2}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
3483
3630
|
);
|
|
3484
3631
|
}
|
|
3485
3632
|
}
|
|
@@ -3499,8 +3646,10 @@ function parseParentExe(sessionName, agentId) {
|
|
|
3499
3646
|
return match?.[1] ?? null;
|
|
3500
3647
|
}
|
|
3501
3648
|
function extractRootExe(name) {
|
|
3502
|
-
|
|
3503
|
-
|
|
3649
|
+
if (!name) return null;
|
|
3650
|
+
if (!name.includes("-")) return name;
|
|
3651
|
+
const parts = name.split("-").filter(Boolean);
|
|
3652
|
+
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
3504
3653
|
}
|
|
3505
3654
|
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
3506
3655
|
if (!existsSync10(SESSION_CACHE)) {
|
|
@@ -3645,12 +3794,14 @@ function isSessionBusy(sessionName) {
|
|
|
3645
3794
|
return state === "thinking" || state === "tool";
|
|
3646
3795
|
}
|
|
3647
3796
|
function isExeSession(sessionName) {
|
|
3648
|
-
|
|
3797
|
+
const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
|
|
3798
|
+
const coordinatorName = getCoordinatorName();
|
|
3799
|
+
return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
|
|
3649
3800
|
}
|
|
3650
3801
|
function sendIntercom(targetSession) {
|
|
3651
3802
|
const transport = getTransport();
|
|
3652
3803
|
if (isExeSession(targetSession)) {
|
|
3653
|
-
logIntercom(`
|
|
3804
|
+
logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
|
|
3654
3805
|
return "skipped_exe";
|
|
3655
3806
|
}
|
|
3656
3807
|
if (isDebounced(targetSession)) {
|
|
@@ -3702,7 +3853,7 @@ function notifyParentExe(sessionKey) {
|
|
|
3702
3853
|
if (result2 === "failed") {
|
|
3703
3854
|
const rootExe = resolveExeSession();
|
|
3704
3855
|
if (rootExe && rootExe !== target) {
|
|
3705
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root
|
|
3856
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
|
|
3706
3857
|
`);
|
|
3707
3858
|
const fallback = sendIntercom(rootExe);
|
|
3708
3859
|
return fallback !== "failed";
|
|
@@ -3712,8 +3863,8 @@ function notifyParentExe(sessionKey) {
|
|
|
3712
3863
|
return true;
|
|
3713
3864
|
}
|
|
3714
3865
|
function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
|
|
3715
|
-
if (employeeName2 === "exe") {
|
|
3716
|
-
return { status: "failed", sessionName: "", error: "
|
|
3866
|
+
if (employeeName2 === "exe" || isCoordinatorName(employeeName2)) {
|
|
3867
|
+
return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
|
|
3717
3868
|
}
|
|
3718
3869
|
try {
|
|
3719
3870
|
assertEmployeeLimitSync();
|
|
@@ -3722,8 +3873,8 @@ function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
|
|
|
3722
3873
|
return { status: "failed", sessionName: "", error: err.message };
|
|
3723
3874
|
}
|
|
3724
3875
|
}
|
|
3725
|
-
if (
|
|
3726
|
-
const bare = employeeName2.
|
|
3876
|
+
if (employeeName2.includes("-")) {
|
|
3877
|
+
const bare = employeeName2.split("-")[0].replace(/\d+$/, "");
|
|
3727
3878
|
return {
|
|
3728
3879
|
status: "failed",
|
|
3729
3880
|
sessionName: "",
|
|
@@ -3742,7 +3893,7 @@ function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
|
|
|
3742
3893
|
return {
|
|
3743
3894
|
status: "failed",
|
|
3744
3895
|
sessionName: "",
|
|
3745
|
-
error: `Invalid
|
|
3896
|
+
error: `Invalid coordinator session "${exeSession2}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
3746
3897
|
};
|
|
3747
3898
|
}
|
|
3748
3899
|
}
|
|
@@ -3899,8 +4050,8 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
|
|
|
3899
4050
|
const ctxContent = [
|
|
3900
4051
|
`## Session Context`,
|
|
3901
4052
|
`You are running in tmux session: ${sessionName}.`,
|
|
3902
|
-
`Your parent
|
|
3903
|
-
`Your employees (if any) use the -${exeSession2} suffix
|
|
4053
|
+
`Your parent coordinator session is ${exeSession2}.`,
|
|
4054
|
+
`Your employees (if any) use the -${exeSession2} suffix.`
|
|
3904
4055
|
].join("\n");
|
|
3905
4056
|
writeFileSync6(ctxFile, ctxContent);
|
|
3906
4057
|
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
@@ -4004,6 +4155,7 @@ var init_tmux_routing = __esm({
|
|
|
4004
4155
|
init_provider_table();
|
|
4005
4156
|
init_intercom_queue();
|
|
4006
4157
|
init_plan_limits();
|
|
4158
|
+
init_employees();
|
|
4007
4159
|
SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
4008
4160
|
SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
|
|
4009
4161
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
@@ -4139,7 +4291,11 @@ async function ensureShardSchema(client) {
|
|
|
4139
4291
|
"ALTER TABLE memories ADD COLUMN source_path TEXT",
|
|
4140
4292
|
"ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
|
|
4141
4293
|
"ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
|
|
4142
|
-
"ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
|
|
4294
|
+
"ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
|
|
4295
|
+
// MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
|
|
4296
|
+
"ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
|
|
4297
|
+
"ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
|
|
4298
|
+
"ALTER TABLE memories ADD COLUMN trajectory TEXT"
|
|
4143
4299
|
]) {
|
|
4144
4300
|
try {
|
|
4145
4301
|
await client.execute(col);
|
|
@@ -4269,26 +4425,26 @@ var init_platform_procedures = __esm({
|
|
|
4269
4425
|
title: "What is exe-os \u2014 the operating model every agent must understand",
|
|
4270
4426
|
domain: "architecture",
|
|
4271
4427
|
priority: "p0",
|
|
4272
|
-
content: "Exe OS is an AI employee operating system. A founder runs 5-10 AI agents as a real org: COO
|
|
4428
|
+
content: "Exe OS is an AI employee operating system. A founder runs 5-10 AI agents as a real org: COO, CTO, CMO, engineers, and content production specialists. Each agent has identity, expertise, and experience layers \u2014 persistent memory that makes them better over time. All data is local-first, E2EE, owned by the user. The MCP server is the ONLY data interface \u2014 never access the DB directly."
|
|
4273
4429
|
},
|
|
4274
4430
|
{
|
|
4275
4431
|
title: "Mode 1 \u2014 how exe-os runs inside Claude Code",
|
|
4276
4432
|
domain: "architecture",
|
|
4277
4433
|
priority: "p0",
|
|
4278
|
-
content: "Mode 1: exe-os runs AS hooks + MCP + skills inside Claude Code. The founder opens CC
|
|
4434
|
+
content: "Mode 1: exe-os runs AS hooks + MCP + skills inside Claude Code. The founder opens CC and boots the COO. The COO manages employees in tmux sessions. Each coordinator session is a separate CC window/project. Employees run in their own tmux panes via create_task auto-spawn. The founder talks to the COO; the COO orchestrates the team. CC is the shell, exe-os is the brain."
|
|
4279
4435
|
},
|
|
4280
4436
|
{
|
|
4281
|
-
title: "Sessions explained \u2014
|
|
4437
|
+
title: "Sessions explained \u2014 coordinator session names and projects",
|
|
4282
4438
|
domain: "architecture",
|
|
4283
4439
|
priority: "p0",
|
|
4284
|
-
content: "Each
|
|
4440
|
+
content: "Each coordinator session is an isolated project session. One might be exe-os development, another might be exe-wiki. Each session spawns its own employees using {employee}-{coordinatorSession}. Sessions share the same memory DB but tasks are scoped to the session that created them. A founder can run multiple projects simultaneously. Sessions never interfere with each other."
|
|
4285
4441
|
},
|
|
4286
4442
|
// --- Hierarchy and dispatch ---
|
|
4287
4443
|
{
|
|
4288
4444
|
title: "Chain of command \u2014 who talks to whom",
|
|
4289
4445
|
domain: "workflow",
|
|
4290
4446
|
priority: "p0",
|
|
4291
|
-
content: "Founder
|
|
4447
|
+
content: "Founder -> COO -> CTO/CMO. CTO -> engineers. CMO -> content production. Never skip levels: the COO does not bypass managers for specialist work. Specialists report to their manager. If you need cross-team info, use ask_team_memory \u2014 don't read other agents' task folders. Each level owns dispatch downward and review upward."
|
|
4292
4448
|
},
|
|
4293
4449
|
{
|
|
4294
4450
|
title: "Single dispatch path \u2014 create_task only",
|
|
@@ -4298,30 +4454,30 @@ var init_platform_procedures = __esm({
|
|
|
4298
4454
|
},
|
|
4299
4455
|
// --- Session isolation ---
|
|
4300
4456
|
{
|
|
4301
|
-
title: "Session scoping \u2014 stay in your
|
|
4457
|
+
title: "Session scoping \u2014 stay in your coordinator boundary",
|
|
4302
4458
|
domain: "security",
|
|
4303
4459
|
priority: "p0",
|
|
4304
|
-
content: "Session scoping is mandatory. Managers dispatch to workers within their own
|
|
4460
|
+
content: "Session scoping is mandatory. Managers dispatch to workers within their own coordinator session ONLY. Employee sessions use {employee}-{coordinatorSession}. Cross-session dispatch is blocked by the system. Verify session names before dispatch. Tasks are scoped to the creating coordinator session."
|
|
4305
4461
|
},
|
|
4306
4462
|
{
|
|
4307
4463
|
title: "Session isolation \u2014 never touch another session's work",
|
|
4308
4464
|
domain: "workflow",
|
|
4309
4465
|
priority: "p0",
|
|
4310
|
-
content:
|
|
4466
|
+
content: "Sessions are isolated. A coordinator session owns ONLY tasks it dispatched. (1) Never close/update/cancel tasks from another coordinator session. (2) Never review work from a different session \u2014 report that it belongs to another session and skip. (3) Ignore other sessions' items in list_tasks results. (4) Employees inherit session: employee sessions work ONLY on their parent coordinator session's tasks. Cross-session work is a system violation."
|
|
4311
4467
|
},
|
|
4312
4468
|
// --- Engineering: session scoping in code ---
|
|
4313
4469
|
{
|
|
4314
4470
|
title: "Three-dimensional scoping \u2014 session, project, role \u2014 enforced in every query",
|
|
4315
4471
|
domain: "architecture",
|
|
4316
4472
|
priority: "p0",
|
|
4317
|
-
content: "Every DB query, notification, review count, and task operation MUST be scoped on 3 dimensions: (1) Session \u2014 filter by session_scope matching current
|
|
4473
|
+
content: "Every DB query, notification, review count, and task operation MUST be scoped on 3 dimensions: (1) Session \u2014 filter by session_scope matching the current coordinator session. (2) Project \u2014 filter by project_name. (3) Role \u2014 agents only see data at their hierarchy level. When writing ANY function that touches tasks, reviews, messages, or notifications: always accept a sessionScope parameter and pass it to the SQL WHERE clause. Unscoped queries are bugs. Test by running 2+ coordinator sessions simultaneously."
|
|
4318
4474
|
},
|
|
4319
4475
|
// --- Hard constraints ---
|
|
4320
4476
|
{
|
|
4321
4477
|
title: "What you CANNOT do in exe-os \u2014 hard constraints",
|
|
4322
4478
|
domain: "security",
|
|
4323
4479
|
priority: "p0",
|
|
4324
|
-
content: "NEVER: (1) Access the database directly \u2014 it's SQLCipher encrypted, always fails. Use MCP tools only. (2) Manually spawn tmux sessions \u2014 create_task handles it. (3) Run git checkout main \u2014 agents work in worktrees. (4) Modify another agent's in-progress task. (5) Push to remote \u2014
|
|
4480
|
+
content: "NEVER: (1) Access the database directly \u2014 it's SQLCipher encrypted, always fails. Use MCP tools only. (2) Manually spawn tmux sessions \u2014 create_task handles it. (3) Run git checkout main \u2014 agents work in worktrees. (4) Modify another agent's in-progress task. (5) Push to remote \u2014 the COO reviews and pushes. (6) Skip update_task(done) \u2014 it's the ONLY way your work gets reviewed. (7) Run git init."
|
|
4325
4481
|
},
|
|
4326
4482
|
// --- Operations ---
|
|
4327
4483
|
{
|
|
@@ -4570,6 +4726,7 @@ async function initStore(options) {
|
|
|
4570
4726
|
}
|
|
4571
4727
|
|
|
4572
4728
|
// src/bin/exe-dispatch.ts
|
|
4729
|
+
init_employees();
|
|
4573
4730
|
var employeeName = process.argv[2];
|
|
4574
4731
|
if (!employeeName) {
|
|
4575
4732
|
console.error("Usage: exe-dispatch <employeeName> [projectDir]");
|
|
@@ -4581,7 +4738,7 @@ if (!exeSession) {
|
|
|
4581
4738
|
console.log(JSON.stringify({
|
|
4582
4739
|
status: "failed",
|
|
4583
4740
|
sessionName: "",
|
|
4584
|
-
error: "Not in tmux \u2014 cannot resolve
|
|
4741
|
+
error: "Not in tmux \u2014 cannot resolve coordinator session"
|
|
4585
4742
|
}));
|
|
4586
4743
|
process.exit(1);
|
|
4587
4744
|
}
|
|
@@ -4590,7 +4747,7 @@ try {
|
|
|
4590
4747
|
await createTaskCore({
|
|
4591
4748
|
title: `Dispatch: ${employeeName} (CLI)`,
|
|
4592
4749
|
assignedTo: employeeName,
|
|
4593
|
-
assignedBy:
|
|
4750
|
+
assignedBy: getCoordinatorName(),
|
|
4594
4751
|
projectName: "exe-os",
|
|
4595
4752
|
priority: "p1",
|
|
4596
4753
|
context: "Session dispatched via exe-dispatch CLI. Agent will pick up queued tasks via intercom.",
|