@askexenow/exe-os 0.8.83 → 0.8.86
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 +746 -595
- package/dist/bin/backfill-responses.js +745 -594
- package/dist/bin/backfill-vectors.js +312 -226
- package/dist/bin/cleanup-stale-review-tasks.js +154 -21
- package/dist/bin/cli.js +14678 -12676
- package/dist/bin/exe-agent-config.js +242 -0
- package/dist/bin/exe-agent.js +100 -91
- package/dist/bin/exe-assign.js +1003 -854
- package/dist/bin/exe-boot.js +1420 -485
- package/dist/bin/exe-call.js +10 -0
- package/dist/bin/exe-cloud.js +29 -6
- package/dist/bin/exe-dispatch.js +572 -271
- package/dist/bin/exe-doctor.js +403 -6
- package/dist/bin/exe-export-behaviors.js +175 -72
- package/dist/bin/exe-forget.js +102 -3
- package/dist/bin/exe-gateway.js +796 -292
- package/dist/bin/exe-healthcheck.js +134 -1
- package/dist/bin/exe-heartbeat.js +172 -36
- package/dist/bin/exe-kill.js +175 -72
- package/dist/bin/exe-launch-agent.js +189 -76
- package/dist/bin/exe-link.js +927 -82
- package/dist/bin/exe-new-employee.js +60 -8
- package/dist/bin/exe-pending-messages.js +151 -19
- package/dist/bin/exe-pending-notifications.js +97 -2
- package/dist/bin/exe-pending-reviews.js +155 -22
- package/dist/bin/exe-rename.js +564 -23
- package/dist/bin/exe-review.js +231 -73
- package/dist/bin/exe-search.js +995 -228
- package/dist/bin/exe-session-cleanup.js +4930 -1664
- package/dist/bin/exe-settings.js +20 -5
- package/dist/bin/exe-start-codex.js +2598 -0
- package/dist/bin/exe-start.sh +15 -3
- package/dist/bin/exe-status.js +154 -21
- package/dist/bin/exe-team.js +97 -2
- package/dist/bin/git-sweep.js +1180 -363
- package/dist/bin/graph-backfill.js +175 -72
- package/dist/bin/graph-export.js +175 -72
- package/dist/bin/install.js +60 -7
- package/dist/bin/list-providers.js +1 -0
- package/dist/bin/scan-tasks.js +1185 -367
- package/dist/bin/setup.js +914 -270
- package/dist/bin/shard-migrate.js +175 -72
- package/dist/bin/update.js +1 -0
- package/dist/bin/wiki-sync.js +175 -72
- package/dist/gateway/index.js +792 -285
- package/dist/hooks/bug-report-worker.js +445 -135
- package/dist/hooks/commit-complete.js +1178 -361
- package/dist/hooks/error-recall.js +994 -228
- package/dist/hooks/ingest-worker.js +1799 -1234
- package/dist/hooks/ingest.js +3 -0
- package/dist/hooks/instructions-loaded.js +707 -97
- package/dist/hooks/notification.js +699 -89
- package/dist/hooks/post-compact.js +757 -109
- package/dist/hooks/pre-compact.js +1061 -244
- package/dist/hooks/pre-tool-use.js +787 -130
- package/dist/hooks/prompt-ingest-worker.js +242 -101
- package/dist/hooks/prompt-submit.js +1121 -299
- package/dist/hooks/response-ingest-worker.js +242 -101
- package/dist/hooks/session-end.js +4063 -397
- package/dist/hooks/session-start.js +1071 -254
- package/dist/hooks/stop.js +768 -120
- package/dist/hooks/subagent-stop.js +757 -109
- package/dist/hooks/summary-worker.js +1706 -1011
- package/dist/index.js +1821 -1098
- package/dist/lib/agent-config.js +167 -0
- package/dist/lib/cloud-sync.js +932 -88
- package/dist/lib/consolidation.js +2 -1
- package/dist/lib/database.js +642 -87
- package/dist/lib/db-daemon-client.js +503 -0
- package/dist/lib/device-registry.js +547 -7
- package/dist/lib/embedder.js +14 -28
- package/dist/lib/employee-templates.js +84 -74
- package/dist/lib/employees.js +9 -0
- package/dist/lib/exe-daemon-client.js +16 -29
- package/dist/lib/exe-daemon.js +2733 -1575
- package/dist/lib/hybrid-search.js +995 -228
- package/dist/lib/identity.js +87 -67
- package/dist/lib/keychain.js +9 -1
- package/dist/lib/messaging.js +103 -40
- package/dist/lib/reminders.js +91 -74
- package/dist/lib/runtime-table.js +16 -0
- package/dist/lib/schedules.js +96 -2
- package/dist/lib/session-wrappers.js +22 -0
- package/dist/lib/skill-learning.js +103 -85
- package/dist/lib/store.js +234 -73
- package/dist/lib/tasks.js +348 -134
- package/dist/lib/tmux-routing.js +422 -208
- package/dist/lib/token-spend.js +273 -0
- package/dist/lib/ws-client.js +11 -0
- package/dist/mcp/server.js +5742 -696
- package/dist/mcp/tools/complete-reminder.js +94 -77
- package/dist/mcp/tools/create-reminder.js +94 -77
- package/dist/mcp/tools/create-task.js +375 -152
- package/dist/mcp/tools/deactivate-behavior.js +95 -77
- package/dist/mcp/tools/list-reminders.js +94 -77
- package/dist/mcp/tools/list-tasks.js +99 -31
- package/dist/mcp/tools/send-message.js +108 -45
- package/dist/mcp/tools/update-task.js +162 -77
- package/dist/runtime/index.js +1075 -258
- package/dist/tui/App.js +1333 -506
- package/package.json +6 -1
- package/src/commands/exe/agent-config.md +27 -0
- package/src/commands/exe/cc-doctor.md +10 -0
package/dist/lib/tasks.js
CHANGED
|
@@ -266,15 +266,22 @@ function getClient() {
|
|
|
266
266
|
if (!_resilientClient) {
|
|
267
267
|
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
268
268
|
}
|
|
269
|
+
if (process.env.EXE_IS_DAEMON === "1") {
|
|
270
|
+
return _resilientClient;
|
|
271
|
+
}
|
|
272
|
+
if (_daemonClient && _daemonClient._isDaemonActive()) {
|
|
273
|
+
return _daemonClient;
|
|
274
|
+
}
|
|
269
275
|
return _resilientClient;
|
|
270
276
|
}
|
|
271
|
-
var _resilientClient;
|
|
277
|
+
var _resilientClient, _daemonClient;
|
|
272
278
|
var init_database = __esm({
|
|
273
279
|
"src/lib/database.ts"() {
|
|
274
280
|
"use strict";
|
|
275
281
|
init_db_retry();
|
|
276
282
|
init_employees();
|
|
277
283
|
_resilientClient = null;
|
|
284
|
+
_daemonClient = null;
|
|
278
285
|
}
|
|
279
286
|
});
|
|
280
287
|
|
|
@@ -634,18 +641,69 @@ var init_provider_table = __esm({
|
|
|
634
641
|
}
|
|
635
642
|
});
|
|
636
643
|
|
|
637
|
-
// src/lib/
|
|
638
|
-
|
|
644
|
+
// src/lib/runtime-table.ts
|
|
645
|
+
var RUNTIME_TABLE, DEFAULT_RUNTIME;
|
|
646
|
+
var init_runtime_table = __esm({
|
|
647
|
+
"src/lib/runtime-table.ts"() {
|
|
648
|
+
"use strict";
|
|
649
|
+
RUNTIME_TABLE = {
|
|
650
|
+
codex: {
|
|
651
|
+
binary: "codex",
|
|
652
|
+
launchMode: "exec",
|
|
653
|
+
autoApproveFlag: "--full-auto",
|
|
654
|
+
inlineFlag: "--no-alt-screen",
|
|
655
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
656
|
+
defaultModel: "gpt-5.4"
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
DEFAULT_RUNTIME = "claude";
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// src/lib/agent-config.ts
|
|
664
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
639
665
|
import path5 from "path";
|
|
666
|
+
function loadAgentConfig() {
|
|
667
|
+
if (!existsSync5(AGENT_CONFIG_PATH)) return {};
|
|
668
|
+
try {
|
|
669
|
+
return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
|
|
670
|
+
} catch {
|
|
671
|
+
return {};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function getAgentRuntime(agentId) {
|
|
675
|
+
const config = loadAgentConfig();
|
|
676
|
+
const entry = config[agentId];
|
|
677
|
+
if (entry) return entry;
|
|
678
|
+
return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
|
|
679
|
+
}
|
|
680
|
+
var AGENT_CONFIG_PATH, DEFAULT_MODELS;
|
|
681
|
+
var init_agent_config = __esm({
|
|
682
|
+
"src/lib/agent-config.ts"() {
|
|
683
|
+
"use strict";
|
|
684
|
+
init_config();
|
|
685
|
+
init_runtime_table();
|
|
686
|
+
AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
|
|
687
|
+
DEFAULT_MODELS = {
|
|
688
|
+
claude: "claude-opus-4",
|
|
689
|
+
codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
|
|
690
|
+
opencode: "minimax-m2.7"
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// src/lib/intercom-queue.ts
|
|
696
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
697
|
+
import path6 from "path";
|
|
640
698
|
import os5 from "os";
|
|
641
699
|
function ensureDir() {
|
|
642
|
-
const dir =
|
|
643
|
-
if (!
|
|
700
|
+
const dir = path6.dirname(QUEUE_PATH);
|
|
701
|
+
if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
|
|
644
702
|
}
|
|
645
703
|
function readQueue() {
|
|
646
704
|
try {
|
|
647
|
-
if (!
|
|
648
|
-
return JSON.parse(
|
|
705
|
+
if (!existsSync6(QUEUE_PATH)) return [];
|
|
706
|
+
return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
|
|
649
707
|
} catch {
|
|
650
708
|
return [];
|
|
651
709
|
}
|
|
@@ -653,7 +711,7 @@ function readQueue() {
|
|
|
653
711
|
function writeQueue(queue) {
|
|
654
712
|
ensureDir();
|
|
655
713
|
const tmp = `${QUEUE_PATH}.tmp`;
|
|
656
|
-
|
|
714
|
+
writeFileSync4(tmp, JSON.stringify(queue, null, 2));
|
|
657
715
|
renameSync3(tmp, QUEUE_PATH);
|
|
658
716
|
}
|
|
659
717
|
function queueIntercom(targetSession, reason) {
|
|
@@ -677,25 +735,25 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
|
677
735
|
var init_intercom_queue = __esm({
|
|
678
736
|
"src/lib/intercom-queue.ts"() {
|
|
679
737
|
"use strict";
|
|
680
|
-
QUEUE_PATH =
|
|
738
|
+
QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
|
|
681
739
|
TTL_MS = 60 * 60 * 1e3;
|
|
682
|
-
INTERCOM_LOG =
|
|
740
|
+
INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
683
741
|
}
|
|
684
742
|
});
|
|
685
743
|
|
|
686
744
|
// src/lib/license.ts
|
|
687
|
-
import { readFileSync as
|
|
745
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
688
746
|
import { randomUUID } from "crypto";
|
|
689
|
-
import
|
|
747
|
+
import path7 from "path";
|
|
690
748
|
import { jwtVerify, importSPKI } from "jose";
|
|
691
749
|
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
692
750
|
var init_license = __esm({
|
|
693
751
|
"src/lib/license.ts"() {
|
|
694
752
|
"use strict";
|
|
695
753
|
init_config();
|
|
696
|
-
LICENSE_PATH =
|
|
697
|
-
CACHE_PATH =
|
|
698
|
-
DEVICE_ID_PATH =
|
|
754
|
+
LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
|
|
755
|
+
CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
|
|
756
|
+
DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
|
|
699
757
|
PLAN_LIMITS = {
|
|
700
758
|
free: { devices: 1, employees: 1, memories: 5e3 },
|
|
701
759
|
pro: { devices: 3, employees: 5, memories: 1e5 },
|
|
@@ -707,12 +765,12 @@ var init_license = __esm({
|
|
|
707
765
|
});
|
|
708
766
|
|
|
709
767
|
// src/lib/plan-limits.ts
|
|
710
|
-
import { readFileSync as
|
|
711
|
-
import
|
|
768
|
+
import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
|
|
769
|
+
import path8 from "path";
|
|
712
770
|
function getLicenseSync() {
|
|
713
771
|
try {
|
|
714
|
-
if (!
|
|
715
|
-
const raw = JSON.parse(
|
|
772
|
+
if (!existsSync8(CACHE_PATH2)) return freeLicense();
|
|
773
|
+
const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
|
|
716
774
|
if (!raw.token || typeof raw.token !== "string") return freeLicense();
|
|
717
775
|
const parts = raw.token.split(".");
|
|
718
776
|
if (parts.length !== 3) return freeLicense();
|
|
@@ -750,8 +808,8 @@ function assertEmployeeLimitSync(rosterPath) {
|
|
|
750
808
|
const filePath = rosterPath ?? EMPLOYEES_PATH;
|
|
751
809
|
let count = 0;
|
|
752
810
|
try {
|
|
753
|
-
if (
|
|
754
|
-
const raw =
|
|
811
|
+
if (existsSync8(filePath)) {
|
|
812
|
+
const raw = readFileSync8(filePath, "utf8");
|
|
755
813
|
const employees = JSON.parse(raw);
|
|
756
814
|
count = Array.isArray(employees) ? employees.length : 0;
|
|
757
815
|
}
|
|
@@ -780,7 +838,7 @@ var init_plan_limits = __esm({
|
|
|
780
838
|
this.name = "PlanLimitError";
|
|
781
839
|
}
|
|
782
840
|
};
|
|
783
|
-
CACHE_PATH2 =
|
|
841
|
+
CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
|
|
784
842
|
}
|
|
785
843
|
});
|
|
786
844
|
|
|
@@ -1128,13 +1186,13 @@ __export(tmux_routing_exports, {
|
|
|
1128
1186
|
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
1129
1187
|
});
|
|
1130
1188
|
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
1131
|
-
import { readFileSync as
|
|
1132
|
-
import
|
|
1189
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync } from "fs";
|
|
1190
|
+
import path9 from "path";
|
|
1133
1191
|
import os6 from "os";
|
|
1134
1192
|
import { fileURLToPath } from "url";
|
|
1135
1193
|
import { unlinkSync as unlinkSync3 } from "fs";
|
|
1136
1194
|
function spawnLockPath(sessionName) {
|
|
1137
|
-
return
|
|
1195
|
+
return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
1138
1196
|
}
|
|
1139
1197
|
function isProcessAlive(pid) {
|
|
1140
1198
|
try {
|
|
@@ -1145,13 +1203,13 @@ function isProcessAlive(pid) {
|
|
|
1145
1203
|
}
|
|
1146
1204
|
}
|
|
1147
1205
|
function acquireSpawnLock(sessionName) {
|
|
1148
|
-
if (!
|
|
1149
|
-
|
|
1206
|
+
if (!existsSync9(SPAWN_LOCK_DIR)) {
|
|
1207
|
+
mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
|
|
1150
1208
|
}
|
|
1151
1209
|
const lockFile = spawnLockPath(sessionName);
|
|
1152
|
-
if (
|
|
1210
|
+
if (existsSync9(lockFile)) {
|
|
1153
1211
|
try {
|
|
1154
|
-
const lock = JSON.parse(
|
|
1212
|
+
const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
|
|
1155
1213
|
const age = Date.now() - lock.timestamp;
|
|
1156
1214
|
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
1157
1215
|
return false;
|
|
@@ -1159,7 +1217,7 @@ function acquireSpawnLock(sessionName) {
|
|
|
1159
1217
|
} catch {
|
|
1160
1218
|
}
|
|
1161
1219
|
}
|
|
1162
|
-
|
|
1220
|
+
writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
1163
1221
|
return true;
|
|
1164
1222
|
}
|
|
1165
1223
|
function releaseSpawnLock(sessionName) {
|
|
@@ -1171,13 +1229,13 @@ function releaseSpawnLock(sessionName) {
|
|
|
1171
1229
|
function resolveBehaviorsExporterScript() {
|
|
1172
1230
|
try {
|
|
1173
1231
|
const thisFile = fileURLToPath(import.meta.url);
|
|
1174
|
-
const scriptPath =
|
|
1175
|
-
|
|
1232
|
+
const scriptPath = path9.join(
|
|
1233
|
+
path9.dirname(thisFile),
|
|
1176
1234
|
"..",
|
|
1177
1235
|
"bin",
|
|
1178
1236
|
"exe-export-behaviors.js"
|
|
1179
1237
|
);
|
|
1180
|
-
return
|
|
1238
|
+
return existsSync9(scriptPath) ? scriptPath : null;
|
|
1181
1239
|
} catch {
|
|
1182
1240
|
return null;
|
|
1183
1241
|
}
|
|
@@ -1243,12 +1301,12 @@ function extractRootExe(name) {
|
|
|
1243
1301
|
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
1244
1302
|
}
|
|
1245
1303
|
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
1246
|
-
if (!
|
|
1247
|
-
|
|
1304
|
+
if (!existsSync9(SESSION_CACHE)) {
|
|
1305
|
+
mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
1248
1306
|
}
|
|
1249
1307
|
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
1250
|
-
const filePath =
|
|
1251
|
-
|
|
1308
|
+
const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
1309
|
+
writeFileSync6(filePath, JSON.stringify({
|
|
1252
1310
|
parentExe: rootExe,
|
|
1253
1311
|
dispatchedBy: dispatchedBy || rootExe,
|
|
1254
1312
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1256,7 +1314,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
|
1256
1314
|
}
|
|
1257
1315
|
function getParentExe(sessionKey) {
|
|
1258
1316
|
try {
|
|
1259
|
-
const data = JSON.parse(
|
|
1317
|
+
const data = JSON.parse(readFileSync9(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
1260
1318
|
return data.parentExe || null;
|
|
1261
1319
|
} catch {
|
|
1262
1320
|
return null;
|
|
@@ -1264,8 +1322,8 @@ function getParentExe(sessionKey) {
|
|
|
1264
1322
|
}
|
|
1265
1323
|
function getDispatchedBy(sessionKey) {
|
|
1266
1324
|
try {
|
|
1267
|
-
const data = JSON.parse(
|
|
1268
|
-
|
|
1325
|
+
const data = JSON.parse(readFileSync9(
|
|
1326
|
+
path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
1269
1327
|
"utf8"
|
|
1270
1328
|
));
|
|
1271
1329
|
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
@@ -1326,32 +1384,50 @@ async function verifyPaneAtCapacity(sessionName) {
|
|
|
1326
1384
|
}
|
|
1327
1385
|
function readDebounceState() {
|
|
1328
1386
|
try {
|
|
1329
|
-
if (!
|
|
1330
|
-
|
|
1387
|
+
if (!existsSync9(DEBOUNCE_FILE)) return {};
|
|
1388
|
+
const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
|
|
1389
|
+
const state = {};
|
|
1390
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
1391
|
+
if (typeof val === "number") {
|
|
1392
|
+
state[key] = { lastSent: val, pending: 0 };
|
|
1393
|
+
} else if (val && typeof val === "object" && "lastSent" in val) {
|
|
1394
|
+
state[key] = val;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return state;
|
|
1331
1398
|
} catch {
|
|
1332
1399
|
return {};
|
|
1333
1400
|
}
|
|
1334
1401
|
}
|
|
1335
1402
|
function writeDebounceState(state) {
|
|
1336
1403
|
try {
|
|
1337
|
-
if (!
|
|
1338
|
-
|
|
1404
|
+
if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
|
|
1405
|
+
writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
|
|
1339
1406
|
} catch {
|
|
1340
1407
|
}
|
|
1341
1408
|
}
|
|
1342
1409
|
function isDebounced(targetSession) {
|
|
1343
1410
|
const state = readDebounceState();
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1411
|
+
const entry = state[targetSession];
|
|
1412
|
+
const lastSent = entry?.lastSent ?? 0;
|
|
1413
|
+
if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
|
|
1414
|
+
if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
|
|
1415
|
+
state[targetSession].pending++;
|
|
1416
|
+
writeDebounceState(state);
|
|
1417
|
+
return true;
|
|
1418
|
+
}
|
|
1419
|
+
return false;
|
|
1346
1420
|
}
|
|
1347
1421
|
function recordDebounce(targetSession) {
|
|
1348
1422
|
const state = readDebounceState();
|
|
1349
|
-
state[targetSession]
|
|
1423
|
+
const batched = state[targetSession]?.pending ?? 0;
|
|
1424
|
+
state[targetSession] = { lastSent: Date.now(), pending: 0 };
|
|
1350
1425
|
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
1351
1426
|
for (const key of Object.keys(state)) {
|
|
1352
|
-
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
1427
|
+
if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
|
|
1353
1428
|
}
|
|
1354
1429
|
writeDebounceState(state);
|
|
1430
|
+
return batched;
|
|
1355
1431
|
}
|
|
1356
1432
|
function logIntercom(msg) {
|
|
1357
1433
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
@@ -1396,7 +1472,7 @@ function sendIntercom(targetSession) {
|
|
|
1396
1472
|
return "skipped_exe";
|
|
1397
1473
|
}
|
|
1398
1474
|
if (isDebounced(targetSession)) {
|
|
1399
|
-
logIntercom(`DEBOUNCE \u2192 ${targetSession} (
|
|
1475
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
|
|
1400
1476
|
return "debounced";
|
|
1401
1477
|
}
|
|
1402
1478
|
try {
|
|
@@ -1408,14 +1484,14 @@ function sendIntercom(targetSession) {
|
|
|
1408
1484
|
const sessionState = getSessionState(targetSession);
|
|
1409
1485
|
if (sessionState === "no_claude") {
|
|
1410
1486
|
queueIntercom(targetSession, "claude not running in session");
|
|
1411
|
-
recordDebounce(targetSession);
|
|
1412
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process
|
|
1487
|
+
const batched2 = recordDebounce(targetSession);
|
|
1488
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
1413
1489
|
return "queued";
|
|
1414
1490
|
}
|
|
1415
1491
|
if (sessionState === "thinking" || sessionState === "tool") {
|
|
1416
1492
|
queueIntercom(targetSession, "session busy at send time");
|
|
1417
|
-
recordDebounce(targetSession);
|
|
1418
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy
|
|
1493
|
+
const batched2 = recordDebounce(targetSession);
|
|
1494
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
1419
1495
|
return "queued";
|
|
1420
1496
|
}
|
|
1421
1497
|
if (transport.isPaneInCopyMode(targetSession)) {
|
|
@@ -1423,8 +1499,8 @@ function sendIntercom(targetSession) {
|
|
|
1423
1499
|
transport.sendKeys(targetSession, "q");
|
|
1424
1500
|
}
|
|
1425
1501
|
transport.sendKeys(targetSession, "/exe-intercom");
|
|
1426
|
-
recordDebounce(targetSession);
|
|
1427
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
1502
|
+
const batched = recordDebounce(targetSession);
|
|
1503
|
+
logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
|
|
1428
1504
|
return "delivered";
|
|
1429
1505
|
} catch {
|
|
1430
1506
|
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
@@ -1454,7 +1530,7 @@ function notifyParentExe(sessionKey) {
|
|
|
1454
1530
|
return true;
|
|
1455
1531
|
}
|
|
1456
1532
|
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
1457
|
-
if (
|
|
1533
|
+
if (isCoordinatorName(employeeName)) {
|
|
1458
1534
|
return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
|
|
1459
1535
|
}
|
|
1460
1536
|
try {
|
|
@@ -1526,26 +1602,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1526
1602
|
const transport = getTransport();
|
|
1527
1603
|
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
1528
1604
|
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
1529
|
-
const logDir =
|
|
1530
|
-
const logFile =
|
|
1531
|
-
if (!
|
|
1532
|
-
|
|
1605
|
+
const logDir = path9.join(os6.homedir(), ".exe-os", "session-logs");
|
|
1606
|
+
const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
1607
|
+
if (!existsSync9(logDir)) {
|
|
1608
|
+
mkdirSync5(logDir, { recursive: true });
|
|
1533
1609
|
}
|
|
1534
1610
|
transport.kill(sessionName);
|
|
1535
1611
|
let cleanupSuffix = "";
|
|
1536
1612
|
try {
|
|
1537
1613
|
const thisFile = fileURLToPath(import.meta.url);
|
|
1538
|
-
const cleanupScript =
|
|
1539
|
-
if (
|
|
1614
|
+
const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
1615
|
+
if (existsSync9(cleanupScript)) {
|
|
1540
1616
|
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
1541
1617
|
}
|
|
1542
1618
|
} catch {
|
|
1543
1619
|
}
|
|
1544
1620
|
try {
|
|
1545
|
-
const claudeJsonPath =
|
|
1621
|
+
const claudeJsonPath = path9.join(os6.homedir(), ".claude.json");
|
|
1546
1622
|
let claudeJson = {};
|
|
1547
1623
|
try {
|
|
1548
|
-
claudeJson = JSON.parse(
|
|
1624
|
+
claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
|
|
1549
1625
|
} catch {
|
|
1550
1626
|
}
|
|
1551
1627
|
if (!claudeJson.projects) claudeJson.projects = {};
|
|
@@ -1553,17 +1629,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1553
1629
|
const trustDir = opts?.cwd ?? projectDir;
|
|
1554
1630
|
if (!projects[trustDir]) projects[trustDir] = {};
|
|
1555
1631
|
projects[trustDir].hasTrustDialogAccepted = true;
|
|
1556
|
-
|
|
1632
|
+
writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
1557
1633
|
} catch {
|
|
1558
1634
|
}
|
|
1559
1635
|
try {
|
|
1560
|
-
const settingsDir =
|
|
1636
|
+
const settingsDir = path9.join(os6.homedir(), ".claude", "projects");
|
|
1561
1637
|
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
1562
|
-
const projSettingsDir =
|
|
1563
|
-
const settingsPath =
|
|
1638
|
+
const projSettingsDir = path9.join(settingsDir, normalizedKey);
|
|
1639
|
+
const settingsPath = path9.join(projSettingsDir, "settings.json");
|
|
1564
1640
|
let settings = {};
|
|
1565
1641
|
try {
|
|
1566
|
-
settings = JSON.parse(
|
|
1642
|
+
settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
|
|
1567
1643
|
} catch {
|
|
1568
1644
|
}
|
|
1569
1645
|
const perms = settings.permissions ?? {};
|
|
@@ -1591,20 +1667,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1591
1667
|
if (changed) {
|
|
1592
1668
|
perms.allow = allow;
|
|
1593
1669
|
settings.permissions = perms;
|
|
1594
|
-
|
|
1595
|
-
|
|
1670
|
+
mkdirSync5(projSettingsDir, { recursive: true });
|
|
1671
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1596
1672
|
}
|
|
1597
1673
|
} catch {
|
|
1598
1674
|
}
|
|
1599
1675
|
const spawnCwd = opts?.cwd ?? projectDir;
|
|
1600
1676
|
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
1601
|
-
const
|
|
1677
|
+
const agentRtConfig = getAgentRuntime(employeeName);
|
|
1678
|
+
const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
|
|
1679
|
+
const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
|
|
1680
|
+
const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
1602
1681
|
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
1603
1682
|
let identityFlag = "";
|
|
1604
1683
|
let behaviorsFlag = "";
|
|
1605
1684
|
let legacyFallbackWarned = false;
|
|
1606
1685
|
if (!useExeAgent && !useBinSymlink) {
|
|
1607
|
-
const identityPath =
|
|
1686
|
+
const identityPath = path9.join(
|
|
1608
1687
|
os6.homedir(),
|
|
1609
1688
|
".exe-os",
|
|
1610
1689
|
"identity",
|
|
@@ -1614,13 +1693,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1614
1693
|
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
1615
1694
|
if (hasAgentFlag) {
|
|
1616
1695
|
identityFlag = ` --agent ${employeeName}`;
|
|
1617
|
-
} else if (
|
|
1696
|
+
} else if (existsSync9(identityPath)) {
|
|
1618
1697
|
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
1619
1698
|
legacyFallbackWarned = true;
|
|
1620
1699
|
}
|
|
1621
1700
|
const behaviorsFile = exportBehaviorsSync(
|
|
1622
1701
|
employeeName,
|
|
1623
|
-
|
|
1702
|
+
path9.basename(spawnCwd),
|
|
1624
1703
|
sessionName
|
|
1625
1704
|
);
|
|
1626
1705
|
if (behaviorsFile) {
|
|
@@ -1635,16 +1714,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1635
1714
|
}
|
|
1636
1715
|
let sessionContextFlag = "";
|
|
1637
1716
|
try {
|
|
1638
|
-
const ctxDir =
|
|
1639
|
-
|
|
1640
|
-
const ctxFile =
|
|
1717
|
+
const ctxDir = path9.join(os6.homedir(), ".exe-os", "session-cache");
|
|
1718
|
+
mkdirSync5(ctxDir, { recursive: true });
|
|
1719
|
+
const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
|
|
1641
1720
|
const ctxContent = [
|
|
1642
1721
|
`## Session Context`,
|
|
1643
1722
|
`You are running in tmux session: ${sessionName}.`,
|
|
1644
1723
|
`Your parent coordinator session is ${exeSession}.`,
|
|
1645
1724
|
`Your employees (if any) use the -${exeSession} suffix.`
|
|
1646
1725
|
].join("\n");
|
|
1647
|
-
|
|
1726
|
+
writeFileSync6(ctxFile, ctxContent);
|
|
1648
1727
|
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
1649
1728
|
} catch {
|
|
1650
1729
|
}
|
|
@@ -1658,9 +1737,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1658
1737
|
}
|
|
1659
1738
|
}
|
|
1660
1739
|
}
|
|
1740
|
+
if (useCodex) {
|
|
1741
|
+
const codexCfg = RUNTIME_TABLE.codex;
|
|
1742
|
+
if (codexCfg?.apiKeyEnv) {
|
|
1743
|
+
const keyVal = process.env[codexCfg.apiKeyEnv];
|
|
1744
|
+
if (keyVal) {
|
|
1745
|
+
envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
|
|
1749
|
+
}
|
|
1750
|
+
if (useOpencode) {
|
|
1751
|
+
const ocCfg = PROVIDER_TABLE.opencode;
|
|
1752
|
+
if (ocCfg?.apiKeyEnv) {
|
|
1753
|
+
const keyVal = process.env[ocCfg.apiKeyEnv];
|
|
1754
|
+
if (keyVal) {
|
|
1755
|
+
envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
|
|
1759
|
+
}
|
|
1760
|
+
if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
|
|
1761
|
+
const defaultClaudeModel = DEFAULT_MODELS.claude;
|
|
1762
|
+
if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
|
|
1763
|
+
envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1661
1766
|
let spawnCommand;
|
|
1662
1767
|
if (useExeAgent) {
|
|
1663
1768
|
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
1769
|
+
} else if (useCodex) {
|
|
1770
|
+
process.stderr.write(
|
|
1771
|
+
`[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
|
|
1772
|
+
`
|
|
1773
|
+
);
|
|
1774
|
+
spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
|
|
1775
|
+
} else if (useOpencode) {
|
|
1776
|
+
const binName = `${employeeName}-opencode`;
|
|
1777
|
+
process.stderr.write(
|
|
1778
|
+
`[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
|
|
1779
|
+
`
|
|
1780
|
+
);
|
|
1781
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
1664
1782
|
} else if (useBinSymlink) {
|
|
1665
1783
|
const binName = `${employeeName}-${ccProvider}`;
|
|
1666
1784
|
process.stderr.write(
|
|
@@ -1682,11 +1800,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1682
1800
|
transport.pipeLog(sessionName, logFile);
|
|
1683
1801
|
try {
|
|
1684
1802
|
const mySession = getMySession();
|
|
1685
|
-
const dispatchInfo =
|
|
1686
|
-
|
|
1803
|
+
const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
1804
|
+
writeFileSync6(dispatchInfo, JSON.stringify({
|
|
1687
1805
|
dispatchedBy: mySession,
|
|
1688
1806
|
rootExe: exeSession,
|
|
1689
|
-
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
1807
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
|
|
1808
|
+
runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
|
|
1809
|
+
model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
|
|
1690
1810
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1691
1811
|
}));
|
|
1692
1812
|
} catch {
|
|
@@ -1704,6 +1824,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1704
1824
|
booted = true;
|
|
1705
1825
|
break;
|
|
1706
1826
|
}
|
|
1827
|
+
} else if (useCodex) {
|
|
1828
|
+
if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
|
|
1829
|
+
booted = true;
|
|
1830
|
+
break;
|
|
1831
|
+
}
|
|
1707
1832
|
} else {
|
|
1708
1833
|
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
1709
1834
|
booted = true;
|
|
@@ -1715,9 +1840,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1715
1840
|
}
|
|
1716
1841
|
if (!booted) {
|
|
1717
1842
|
releaseSpawnLock(sessionName);
|
|
1718
|
-
|
|
1843
|
+
const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
|
|
1844
|
+
return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
|
|
1719
1845
|
}
|
|
1720
|
-
if (!useExeAgent) {
|
|
1846
|
+
if (!useExeAgent && !useCodex) {
|
|
1721
1847
|
try {
|
|
1722
1848
|
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
1723
1849
|
} catch {
|
|
@@ -1744,17 +1870,19 @@ var init_tmux_routing = __esm({
|
|
|
1744
1870
|
init_cc_agent_support();
|
|
1745
1871
|
init_mcp_prefix();
|
|
1746
1872
|
init_provider_table();
|
|
1873
|
+
init_agent_config();
|
|
1874
|
+
init_runtime_table();
|
|
1747
1875
|
init_intercom_queue();
|
|
1748
1876
|
init_plan_limits();
|
|
1749
1877
|
init_employees();
|
|
1750
|
-
SPAWN_LOCK_DIR =
|
|
1751
|
-
SESSION_CACHE =
|
|
1878
|
+
SPAWN_LOCK_DIR = path9.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
1879
|
+
SESSION_CACHE = path9.join(os6.homedir(), ".exe-os", "session-cache");
|
|
1752
1880
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
1753
1881
|
VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
1754
1882
|
VERIFY_PANE_LINES = 200;
|
|
1755
1883
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
1756
|
-
INTERCOM_LOG2 =
|
|
1757
|
-
DEBOUNCE_FILE =
|
|
1884
|
+
INTERCOM_LOG2 = path9.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
1885
|
+
DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
|
|
1758
1886
|
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
1759
1887
|
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
1760
1888
|
}
|
|
@@ -1786,10 +1914,11 @@ var init_task_scope = __esm({
|
|
|
1786
1914
|
|
|
1787
1915
|
// src/lib/tasks-crud.ts
|
|
1788
1916
|
import crypto3 from "crypto";
|
|
1789
|
-
import
|
|
1917
|
+
import path10 from "path";
|
|
1918
|
+
import os7 from "os";
|
|
1790
1919
|
import { execSync as execSync5 } from "child_process";
|
|
1791
1920
|
import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
|
|
1792
|
-
import { existsSync as
|
|
1921
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
1793
1922
|
async function writeCheckpoint(input) {
|
|
1794
1923
|
const client = getClient();
|
|
1795
1924
|
const row = await resolveTask(client, input.taskId);
|
|
@@ -1830,6 +1959,35 @@ function extractParentFromContext(contextBody) {
|
|
|
1830
1959
|
function slugify(title) {
|
|
1831
1960
|
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1832
1961
|
}
|
|
1962
|
+
function buildKeywordIndex() {
|
|
1963
|
+
const idx = /* @__PURE__ */ new Map();
|
|
1964
|
+
for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
|
|
1965
|
+
for (const kw of keywords) {
|
|
1966
|
+
const existing = idx.get(kw) ?? [];
|
|
1967
|
+
existing.push(role);
|
|
1968
|
+
idx.set(kw, existing);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
return idx;
|
|
1972
|
+
}
|
|
1973
|
+
function checkLaneAffinity(title, context, assigneeName) {
|
|
1974
|
+
const employees = loadEmployeesSync();
|
|
1975
|
+
const employee = employees.find((e) => e.name === assigneeName);
|
|
1976
|
+
if (!employee) return void 0;
|
|
1977
|
+
const assigneeRole = employee.role;
|
|
1978
|
+
const text = `${title} ${context}`.toLowerCase();
|
|
1979
|
+
const matchedRoles = /* @__PURE__ */ new Set();
|
|
1980
|
+
for (const [keyword, roles] of KEYWORD_INDEX) {
|
|
1981
|
+
if (text.includes(keyword)) {
|
|
1982
|
+
for (const role of roles) matchedRoles.add(role);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
if (matchedRoles.size === 0) return void 0;
|
|
1986
|
+
if (matchedRoles.has(assigneeRole)) return void 0;
|
|
1987
|
+
if (assigneeRole === "COO") return void 0;
|
|
1988
|
+
const expectedRoles = Array.from(matchedRoles).join(" or ");
|
|
1989
|
+
return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
|
|
1990
|
+
}
|
|
1833
1991
|
async function resolveTask(client, identifier, scopeSession) {
|
|
1834
1992
|
const scope = sessionScopeFilter(scopeSession);
|
|
1835
1993
|
let result = await client.execute({
|
|
@@ -1879,7 +2037,14 @@ async function createTaskCore(input) {
|
|
|
1879
2037
|
const id = crypto3.randomUUID();
|
|
1880
2038
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1881
2039
|
const slug = slugify(input.title);
|
|
1882
|
-
|
|
2040
|
+
let earlySessionScope = null;
|
|
2041
|
+
try {
|
|
2042
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
2043
|
+
earlySessionScope = resolveExeSession2();
|
|
2044
|
+
} catch {
|
|
2045
|
+
}
|
|
2046
|
+
const scope = earlySessionScope ?? "default";
|
|
2047
|
+
const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
|
|
1883
2048
|
let blockedById = null;
|
|
1884
2049
|
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
1885
2050
|
if (input.blockedBy) {
|
|
@@ -1919,22 +2084,24 @@ async function createTaskCore(input) {
|
|
|
1919
2084
|
if (dupCheck.rows.length > 0) {
|
|
1920
2085
|
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
1921
2086
|
}
|
|
2087
|
+
if (!process.env.DISABLE_LANE_AFFINITY) {
|
|
2088
|
+
const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
|
|
2089
|
+
if (laneWarning) {
|
|
2090
|
+
warning = warning ? `${warning}
|
|
2091
|
+
${laneWarning}` : laneWarning;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
1922
2094
|
if (input.baseDir) {
|
|
1923
2095
|
try {
|
|
1924
|
-
await mkdir3(
|
|
1925
|
-
await mkdir3(
|
|
2096
|
+
await mkdir3(path10.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
2097
|
+
await mkdir3(path10.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
1926
2098
|
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
1927
2099
|
await ensureGitignoreExe(input.baseDir);
|
|
1928
2100
|
} catch {
|
|
1929
2101
|
}
|
|
1930
2102
|
}
|
|
1931
2103
|
const complexity = input.complexity ?? "standard";
|
|
1932
|
-
|
|
1933
|
-
try {
|
|
1934
|
-
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
1935
|
-
sessionScope = resolveExeSession2();
|
|
1936
|
-
} catch {
|
|
1937
|
-
}
|
|
2104
|
+
const sessionScope = earlySessionScope;
|
|
1938
2105
|
await client.execute({
|
|
1939
2106
|
sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
|
|
1940
2107
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
@@ -1961,6 +2128,43 @@ async function createTaskCore(input) {
|
|
|
1961
2128
|
now
|
|
1962
2129
|
]
|
|
1963
2130
|
});
|
|
2131
|
+
if (input.baseDir) {
|
|
2132
|
+
try {
|
|
2133
|
+
const EXE_OS_DIR = path10.join(os7.homedir(), ".exe-os");
|
|
2134
|
+
const mdPath = path10.join(EXE_OS_DIR, taskFile);
|
|
2135
|
+
const mdDir = path10.dirname(mdPath);
|
|
2136
|
+
if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
|
|
2137
|
+
const reviewer = input.reviewer ?? input.assignedBy;
|
|
2138
|
+
const mdContent = `# ${input.title}
|
|
2139
|
+
|
|
2140
|
+
**ID:** ${id}
|
|
2141
|
+
**Status:** ${initialStatus}
|
|
2142
|
+
**Priority:** ${input.priority}
|
|
2143
|
+
**Assigned by:** ${input.assignedBy}
|
|
2144
|
+
**Assigned to:** ${input.assignedTo}
|
|
2145
|
+
**Project:** ${input.projectName}
|
|
2146
|
+
**Created:** ${now.split("T")[0]}${parentTaskId ? `
|
|
2147
|
+
**Parent task:** ${parentTaskId}` : ""}
|
|
2148
|
+
**Reviewer:** ${reviewer}
|
|
2149
|
+
|
|
2150
|
+
## Context
|
|
2151
|
+
|
|
2152
|
+
${input.context}
|
|
2153
|
+
|
|
2154
|
+
## MANDATORY: When done
|
|
2155
|
+
|
|
2156
|
+
You MUST call update_task with status "done" and a result summary when finished.
|
|
2157
|
+
If you skip this, your reviewer will not know you're done and your work won't be reviewed.
|
|
2158
|
+
Do NOT let a failed commit or any error prevent you from calling update_task(done).
|
|
2159
|
+
`;
|
|
2160
|
+
await writeFile3(mdPath, mdContent, "utf-8");
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
process.stderr.write(
|
|
2163
|
+
`[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
|
|
2164
|
+
`
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
1964
2168
|
return {
|
|
1965
2169
|
id,
|
|
1966
2170
|
title: input.title,
|
|
@@ -2153,7 +2357,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
2153
2357
|
return { row, taskFile, now, taskId };
|
|
2154
2358
|
}
|
|
2155
2359
|
}
|
|
2156
|
-
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId
|
|
2360
|
+
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
|
|
2157
2361
|
process.stderr.write(
|
|
2158
2362
|
`[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
|
|
2159
2363
|
`
|
|
@@ -2218,9 +2422,9 @@ async function deleteTaskCore(taskId, _baseDir) {
|
|
|
2218
2422
|
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
2219
2423
|
}
|
|
2220
2424
|
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
2221
|
-
const archPath =
|
|
2425
|
+
const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
2222
2426
|
try {
|
|
2223
|
-
if (
|
|
2427
|
+
if (existsSync10(archPath)) return;
|
|
2224
2428
|
const template = [
|
|
2225
2429
|
`# ${projectName} \u2014 System Architecture`,
|
|
2226
2430
|
"",
|
|
@@ -2253,10 +2457,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
|
|
|
2253
2457
|
}
|
|
2254
2458
|
}
|
|
2255
2459
|
async function ensureGitignoreExe(baseDir) {
|
|
2256
|
-
const gitignorePath =
|
|
2460
|
+
const gitignorePath = path10.join(baseDir, ".gitignore");
|
|
2257
2461
|
try {
|
|
2258
|
-
if (
|
|
2259
|
-
const content =
|
|
2462
|
+
if (existsSync10(gitignorePath)) {
|
|
2463
|
+
const content = readFileSync10(gitignorePath, "utf-8");
|
|
2260
2464
|
if (/^\/?exe\/?$/m.test(content)) return;
|
|
2261
2465
|
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
2262
2466
|
} else {
|
|
@@ -2265,20 +2469,30 @@ async function ensureGitignoreExe(baseDir) {
|
|
|
2265
2469
|
} catch {
|
|
2266
2470
|
}
|
|
2267
2471
|
}
|
|
2268
|
-
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
2472
|
+
var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
2269
2473
|
var init_tasks_crud = __esm({
|
|
2270
2474
|
"src/lib/tasks-crud.ts"() {
|
|
2271
2475
|
"use strict";
|
|
2272
2476
|
init_database();
|
|
2273
2477
|
init_task_scope();
|
|
2478
|
+
init_employees();
|
|
2479
|
+
LANE_KEYWORDS = {
|
|
2480
|
+
CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
|
|
2481
|
+
CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
|
|
2482
|
+
"Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
|
|
2483
|
+
"Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
|
|
2484
|
+
"Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
|
|
2485
|
+
"AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
|
|
2486
|
+
};
|
|
2487
|
+
KEYWORD_INDEX = buildKeywordIndex();
|
|
2274
2488
|
DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
|
|
2275
2489
|
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
2276
2490
|
}
|
|
2277
2491
|
});
|
|
2278
2492
|
|
|
2279
2493
|
// src/lib/tasks-review.ts
|
|
2280
|
-
import
|
|
2281
|
-
import { existsSync as
|
|
2494
|
+
import path11 from "path";
|
|
2495
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
|
|
2282
2496
|
async function countPendingReviews(sessionScope) {
|
|
2283
2497
|
const client = getClient();
|
|
2284
2498
|
if (sessionScope) {
|
|
@@ -2300,7 +2514,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
|
2300
2514
|
const result2 = await client.execute({
|
|
2301
2515
|
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
2302
2516
|
WHERE status = 'needs_review' AND updated_at > ?
|
|
2303
|
-
AND
|
|
2517
|
+
AND session_scope = ?`,
|
|
2304
2518
|
args: [sinceIso, sessionScope]
|
|
2305
2519
|
});
|
|
2306
2520
|
return Number(result2.rows[0]?.cnt) || 0;
|
|
@@ -2318,7 +2532,7 @@ async function listPendingReviews(limit, sessionScope) {
|
|
|
2318
2532
|
const result2 = await client.execute({
|
|
2319
2533
|
sql: `SELECT title, assigned_to, project_name FROM tasks
|
|
2320
2534
|
WHERE status = 'needs_review'
|
|
2321
|
-
AND
|
|
2535
|
+
AND session_scope = ?
|
|
2322
2536
|
ORDER BY priority ASC, created_at DESC LIMIT ?`,
|
|
2323
2537
|
args: [sessionScope, limit]
|
|
2324
2538
|
});
|
|
@@ -2439,14 +2653,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
|
2439
2653
|
if (parts.length >= 3 && parts[0] === "review") {
|
|
2440
2654
|
const agent = parts[1];
|
|
2441
2655
|
const slug = parts.slice(2).join("-");
|
|
2442
|
-
const
|
|
2656
|
+
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
2443
2657
|
const result = await client.execute({
|
|
2444
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
|
|
2445
|
-
args: [now,
|
|
2658
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
2659
|
+
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
2446
2660
|
});
|
|
2447
2661
|
if (result.rowsAffected > 0) {
|
|
2448
2662
|
process.stderr.write(
|
|
2449
|
-
`[review-cleanup] Cascaded original task to done
|
|
2663
|
+
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
2450
2664
|
`
|
|
2451
2665
|
);
|
|
2452
2666
|
}
|
|
@@ -2459,11 +2673,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
|
2459
2673
|
);
|
|
2460
2674
|
}
|
|
2461
2675
|
try {
|
|
2462
|
-
const cacheDir =
|
|
2463
|
-
if (
|
|
2676
|
+
const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
|
|
2677
|
+
if (existsSync11(cacheDir)) {
|
|
2464
2678
|
for (const f of readdirSync2(cacheDir)) {
|
|
2465
2679
|
if (f.startsWith("review-notified-")) {
|
|
2466
|
-
unlinkSync4(
|
|
2680
|
+
unlinkSync4(path11.join(cacheDir, f));
|
|
2467
2681
|
}
|
|
2468
2682
|
}
|
|
2469
2683
|
}
|
|
@@ -2484,7 +2698,7 @@ var init_tasks_review = __esm({
|
|
|
2484
2698
|
});
|
|
2485
2699
|
|
|
2486
2700
|
// src/lib/tasks-chain.ts
|
|
2487
|
-
import
|
|
2701
|
+
import path12 from "path";
|
|
2488
2702
|
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
2489
2703
|
async function cascadeUnblock(taskId, baseDir, now) {
|
|
2490
2704
|
const client = getClient();
|
|
@@ -2501,7 +2715,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
|
|
|
2501
2715
|
});
|
|
2502
2716
|
for (const ur of unblockedRows.rows) {
|
|
2503
2717
|
try {
|
|
2504
|
-
const ubFile =
|
|
2718
|
+
const ubFile = path12.join(baseDir, String(ur.task_file));
|
|
2505
2719
|
let ubContent = await readFile3(ubFile, "utf-8");
|
|
2506
2720
|
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
2507
2721
|
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
@@ -2570,7 +2784,7 @@ var init_tasks_chain = __esm({
|
|
|
2570
2784
|
|
|
2571
2785
|
// src/lib/project-name.ts
|
|
2572
2786
|
import { execSync as execSync6 } from "child_process";
|
|
2573
|
-
import
|
|
2787
|
+
import path13 from "path";
|
|
2574
2788
|
function getProjectName(cwd) {
|
|
2575
2789
|
const dir = cwd ?? process.cwd();
|
|
2576
2790
|
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
@@ -2583,7 +2797,7 @@ function getProjectName(cwd) {
|
|
|
2583
2797
|
timeout: 2e3,
|
|
2584
2798
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2585
2799
|
}).trim();
|
|
2586
|
-
repoRoot =
|
|
2800
|
+
repoRoot = path13.dirname(gitCommonDir);
|
|
2587
2801
|
} catch {
|
|
2588
2802
|
repoRoot = execSync6("git rev-parse --show-toplevel", {
|
|
2589
2803
|
cwd: dir,
|
|
@@ -2592,11 +2806,11 @@ function getProjectName(cwd) {
|
|
|
2592
2806
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2593
2807
|
}).trim();
|
|
2594
2808
|
}
|
|
2595
|
-
_cached2 =
|
|
2809
|
+
_cached2 = path13.basename(repoRoot);
|
|
2596
2810
|
_cachedCwd = dir;
|
|
2597
2811
|
return _cached2;
|
|
2598
2812
|
} catch {
|
|
2599
|
-
_cached2 =
|
|
2813
|
+
_cached2 = path13.basename(dir);
|
|
2600
2814
|
_cachedCwd = dir;
|
|
2601
2815
|
return _cached2;
|
|
2602
2816
|
}
|
|
@@ -2628,7 +2842,7 @@ function findSessionForProject(projectName) {
|
|
|
2628
2842
|
const sessions = listSessions();
|
|
2629
2843
|
for (const s of sessions) {
|
|
2630
2844
|
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
2631
|
-
if (proj === projectName &&
|
|
2845
|
+
if (proj === projectName && isCoordinatorName(s.agentId)) return s;
|
|
2632
2846
|
}
|
|
2633
2847
|
return null;
|
|
2634
2848
|
}
|
|
@@ -2674,7 +2888,7 @@ var init_session_scope = __esm({
|
|
|
2674
2888
|
|
|
2675
2889
|
// src/lib/tasks-notify.ts
|
|
2676
2890
|
async function dispatchTaskToEmployee(input) {
|
|
2677
|
-
if (
|
|
2891
|
+
if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
|
|
2678
2892
|
let crossProject = false;
|
|
2679
2893
|
if (input.projectName) {
|
|
2680
2894
|
try {
|
|
@@ -3069,8 +3283,8 @@ __export(tasks_exports, {
|
|
|
3069
3283
|
updateTaskStatus: () => updateTaskStatus,
|
|
3070
3284
|
writeCheckpoint: () => writeCheckpoint
|
|
3071
3285
|
});
|
|
3072
|
-
import
|
|
3073
|
-
import { writeFileSync as
|
|
3286
|
+
import path14 from "path";
|
|
3287
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
|
|
3074
3288
|
async function createTask(input) {
|
|
3075
3289
|
const result = await createTaskCore(input);
|
|
3076
3290
|
if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
|
|
@@ -3089,11 +3303,11 @@ async function updateTask(input) {
|
|
|
3089
3303
|
const { row, taskFile, now, taskId } = await updateTaskStatus(input);
|
|
3090
3304
|
try {
|
|
3091
3305
|
const agent = String(row.assigned_to);
|
|
3092
|
-
const cacheDir =
|
|
3093
|
-
const cachePath =
|
|
3306
|
+
const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
|
|
3307
|
+
const cachePath = path14.join(cacheDir, `current-task-${agent}.json`);
|
|
3094
3308
|
if (input.status === "in_progress") {
|
|
3095
|
-
|
|
3096
|
-
|
|
3309
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
3310
|
+
writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
3097
3311
|
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
3098
3312
|
try {
|
|
3099
3313
|
unlinkSync5(cachePath);
|
|
@@ -3153,7 +3367,7 @@ async function updateTask(input) {
|
|
|
3153
3367
|
}
|
|
3154
3368
|
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
3155
3369
|
if (isTerminal) {
|
|
3156
|
-
const isCoordinator =
|
|
3370
|
+
const isCoordinator = isCoordinatorName(String(row.assigned_to));
|
|
3157
3371
|
if (!isCoordinator) {
|
|
3158
3372
|
notifyTaskDone();
|
|
3159
3373
|
}
|
|
@@ -3178,7 +3392,7 @@ async function updateTask(input) {
|
|
|
3178
3392
|
}
|
|
3179
3393
|
}
|
|
3180
3394
|
}
|
|
3181
|
-
if (input.status === "done" &&
|
|
3395
|
+
if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
3182
3396
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
3183
3397
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
3184
3398
|
taskId,
|
|
@@ -3194,7 +3408,7 @@ async function updateTask(input) {
|
|
|
3194
3408
|
});
|
|
3195
3409
|
}
|
|
3196
3410
|
let nextTask;
|
|
3197
|
-
if (isTerminal &&
|
|
3411
|
+
if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
|
|
3198
3412
|
try {
|
|
3199
3413
|
nextTask = await findNextTask(String(row.assigned_to));
|
|
3200
3414
|
} catch {
|