@askexenow/exe-os 0.8.40 → 0.8.42
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 +805 -642
- package/dist/bin/backfill-responses.js +804 -641
- package/dist/bin/backfill-vectors.js +791 -634
- package/dist/bin/cleanup-stale-review-tasks.js +788 -631
- package/dist/bin/cli.js +1376 -659
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2549 -1784
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +12 -2
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +791 -634
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2501 -1846
- package/dist/bin/exe-heartbeat.js +147 -1
- package/dist/bin/exe-kill.js +795 -640
- package/dist/bin/exe-launch-agent.js +2168 -2008
- package/dist/bin/exe-link.js +44 -12
- package/dist/bin/exe-new-employee.js +6 -2
- package/dist/bin/exe-pending-messages.js +146 -1
- package/dist/bin/exe-pending-notifications.js +788 -631
- package/dist/bin/exe-pending-reviews.js +176 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +157 -4
- package/dist/bin/exe-session-cleanup.js +2487 -403
- package/dist/bin/exe-settings.js +2 -1
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2691 -151
- package/dist/bin/graph-backfill.js +794 -637
- package/dist/bin/graph-export.js +798 -641
- package/dist/bin/scan-tasks.js +2951 -44
- package/dist/bin/setup.js +50 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2542 -1887
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2690 -150
- package/dist/hooks/error-recall.js +157 -4
- package/dist/hooks/ingest-worker.js +1455 -803
- package/dist/hooks/instructions-loaded.js +151 -0
- package/dist/hooks/notification.js +153 -2
- package/dist/hooks/post-compact.js +164 -0
- package/dist/hooks/pre-compact.js +3073 -101
- package/dist/hooks/pre-tool-use.js +151 -0
- package/dist/hooks/prompt-ingest-worker.js +1670 -1509
- package/dist/hooks/prompt-submit.js +2650 -1074
- package/dist/hooks/response-ingest-worker.js +154 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +157 -4
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +155 -2
- package/dist/hooks/summary-worker.js +190 -21
- package/dist/index.js +326 -102
- package/dist/lib/cloud-sync.js +31 -10
- package/dist/lib/config.js +2 -0
- package/dist/lib/consolidation.js +69 -2
- package/dist/lib/database.js +19 -0
- package/dist/lib/device-registry.js +19 -0
- package/dist/lib/embedder.js +3 -1
- package/dist/lib/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +285 -18
- package/dist/lib/hybrid-search.js +157 -4
- package/dist/lib/messaging.js +39 -2
- package/dist/lib/schedules.js +792 -637
- package/dist/lib/store.js +796 -636
- package/dist/lib/tasks.js +1485 -918
- package/dist/lib/tmux-routing.js +194 -10
- package/dist/mcp/server.js +1643 -924
- package/dist/mcp/tools/create-task.js +2283 -829
- package/dist/mcp/tools/list-tasks.js +2788 -159
- package/dist/mcp/tools/send-message.js +39 -2
- package/dist/mcp/tools/update-task.js +79 -0
- package/dist/runtime/index.js +280 -68
- package/dist/tui/App.js +1485 -645
- package/package.json +3 -2
package/dist/mcp/server.js
CHANGED
|
@@ -39,6 +39,7 @@ var config_exports = {};
|
|
|
39
39
|
__export(config_exports, {
|
|
40
40
|
CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
|
|
41
41
|
CONFIG_PATH: () => CONFIG_PATH,
|
|
42
|
+
COO_AGENT_NAME: () => COO_AGENT_NAME,
|
|
42
43
|
CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
|
|
43
44
|
DB_PATH: () => DB_PATH,
|
|
44
45
|
EXE_AI_DIR: () => EXE_AI_DIR,
|
|
@@ -194,7 +195,7 @@ async function loadConfigFrom(configPath) {
|
|
|
194
195
|
return { ...DEFAULT_CONFIG };
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
|
-
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
198
|
+
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
198
199
|
var init_config = __esm({
|
|
199
200
|
"src/lib/config.ts"() {
|
|
200
201
|
"use strict";
|
|
@@ -202,6 +203,7 @@ var init_config = __esm({
|
|
|
202
203
|
DB_PATH = path.join(EXE_AI_DIR, "memories.db");
|
|
203
204
|
MODELS_DIR = path.join(EXE_AI_DIR, "models");
|
|
204
205
|
CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
|
|
206
|
+
COO_AGENT_NAME = "exe";
|
|
205
207
|
LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
|
|
206
208
|
CURRENT_CONFIG_VERSION = 1;
|
|
207
209
|
DEFAULT_CONFIG = {
|
|
@@ -985,6 +987,13 @@ async function ensureSchema() {
|
|
|
985
987
|
});
|
|
986
988
|
} catch {
|
|
987
989
|
}
|
|
990
|
+
try {
|
|
991
|
+
await client.execute({
|
|
992
|
+
sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
|
|
993
|
+
args: []
|
|
994
|
+
});
|
|
995
|
+
} catch {
|
|
996
|
+
}
|
|
988
997
|
try {
|
|
989
998
|
await client.execute({
|
|
990
999
|
sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
|
|
@@ -1431,6 +1440,18 @@ async function ensureSchema() {
|
|
|
1431
1440
|
CREATE INDEX IF NOT EXISTS idx_session_kills_agent
|
|
1432
1441
|
ON session_kills(agent_id);
|
|
1433
1442
|
`);
|
|
1443
|
+
await client.execute(`
|
|
1444
|
+
CREATE TABLE IF NOT EXISTS global_procedures (
|
|
1445
|
+
id TEXT PRIMARY KEY,
|
|
1446
|
+
title TEXT NOT NULL,
|
|
1447
|
+
content TEXT NOT NULL,
|
|
1448
|
+
priority TEXT NOT NULL DEFAULT 'p0',
|
|
1449
|
+
domain TEXT,
|
|
1450
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
1451
|
+
created_at TEXT NOT NULL,
|
|
1452
|
+
updated_at TEXT NOT NULL
|
|
1453
|
+
)
|
|
1454
|
+
`);
|
|
1434
1455
|
await client.executeMultiple(`
|
|
1435
1456
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
1436
1457
|
id TEXT PRIMARY KEY,
|
|
@@ -1638,6 +1659,61 @@ var init_keychain = __esm({
|
|
|
1638
1659
|
}
|
|
1639
1660
|
});
|
|
1640
1661
|
|
|
1662
|
+
// src/lib/state-bus.ts
|
|
1663
|
+
var StateBus, orgBus;
|
|
1664
|
+
var init_state_bus = __esm({
|
|
1665
|
+
"src/lib/state-bus.ts"() {
|
|
1666
|
+
"use strict";
|
|
1667
|
+
StateBus = class {
|
|
1668
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1669
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
1670
|
+
/** Emit an event to all subscribers */
|
|
1671
|
+
emit(event) {
|
|
1672
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
1673
|
+
if (typeHandlers) {
|
|
1674
|
+
for (const handler of typeHandlers) {
|
|
1675
|
+
try {
|
|
1676
|
+
handler(event);
|
|
1677
|
+
} catch {
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
for (const handler of this.globalHandlers) {
|
|
1682
|
+
try {
|
|
1683
|
+
handler(event);
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
/** Subscribe to a specific event type */
|
|
1689
|
+
on(type, handler) {
|
|
1690
|
+
if (!this.handlers.has(type)) {
|
|
1691
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
1692
|
+
}
|
|
1693
|
+
this.handlers.get(type).add(handler);
|
|
1694
|
+
}
|
|
1695
|
+
/** Subscribe to ALL events */
|
|
1696
|
+
onAny(handler) {
|
|
1697
|
+
this.globalHandlers.add(handler);
|
|
1698
|
+
}
|
|
1699
|
+
/** Unsubscribe from a specific event type */
|
|
1700
|
+
off(type, handler) {
|
|
1701
|
+
this.handlers.get(type)?.delete(handler);
|
|
1702
|
+
}
|
|
1703
|
+
/** Unsubscribe from ALL events */
|
|
1704
|
+
offAny(handler) {
|
|
1705
|
+
this.globalHandlers.delete(handler);
|
|
1706
|
+
}
|
|
1707
|
+
/** Remove all listeners */
|
|
1708
|
+
clear() {
|
|
1709
|
+
this.handlers.clear();
|
|
1710
|
+
this.globalHandlers.clear();
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
orgBus = new StateBus();
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1641
1717
|
// src/lib/shard-manager.ts
|
|
1642
1718
|
var shard_manager_exports = {};
|
|
1643
1719
|
__export(shard_manager_exports, {
|
|
@@ -1879,6 +1955,71 @@ var init_shard_manager = __esm({
|
|
|
1879
1955
|
}
|
|
1880
1956
|
});
|
|
1881
1957
|
|
|
1958
|
+
// src/lib/global-procedures.ts
|
|
1959
|
+
var global_procedures_exports = {};
|
|
1960
|
+
__export(global_procedures_exports, {
|
|
1961
|
+
deactivateGlobalProcedure: () => deactivateGlobalProcedure,
|
|
1962
|
+
getGlobalProceduresBlock: () => getGlobalProceduresBlock,
|
|
1963
|
+
loadGlobalProcedures: () => loadGlobalProcedures,
|
|
1964
|
+
storeGlobalProcedure: () => storeGlobalProcedure
|
|
1965
|
+
});
|
|
1966
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1967
|
+
async function loadGlobalProcedures() {
|
|
1968
|
+
const client = getClient();
|
|
1969
|
+
const result = await client.execute({
|
|
1970
|
+
sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
|
|
1971
|
+
args: []
|
|
1972
|
+
});
|
|
1973
|
+
const procedures = result.rows;
|
|
1974
|
+
if (procedures.length > 0) {
|
|
1975
|
+
_cache = procedures.map((p) => `### ${p.title}
|
|
1976
|
+
${p.content}`).join("\n\n");
|
|
1977
|
+
} else {
|
|
1978
|
+
_cache = "";
|
|
1979
|
+
}
|
|
1980
|
+
_cacheLoaded = true;
|
|
1981
|
+
return procedures;
|
|
1982
|
+
}
|
|
1983
|
+
function getGlobalProceduresBlock() {
|
|
1984
|
+
if (!_cacheLoaded) return "";
|
|
1985
|
+
if (!_cache) return "";
|
|
1986
|
+
return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
|
|
1987
|
+
|
|
1988
|
+
${_cache}
|
|
1989
|
+
`;
|
|
1990
|
+
}
|
|
1991
|
+
async function storeGlobalProcedure(input) {
|
|
1992
|
+
const id = randomUUID2();
|
|
1993
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1994
|
+
const client = getClient();
|
|
1995
|
+
await client.execute({
|
|
1996
|
+
sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
|
|
1997
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
1998
|
+
args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
|
|
1999
|
+
});
|
|
2000
|
+
await loadGlobalProcedures();
|
|
2001
|
+
return id;
|
|
2002
|
+
}
|
|
2003
|
+
async function deactivateGlobalProcedure(id) {
|
|
2004
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2005
|
+
const client = getClient();
|
|
2006
|
+
const result = await client.execute({
|
|
2007
|
+
sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
|
|
2008
|
+
args: [now, id]
|
|
2009
|
+
});
|
|
2010
|
+
await loadGlobalProcedures();
|
|
2011
|
+
return result.rowsAffected > 0;
|
|
2012
|
+
}
|
|
2013
|
+
var _cache, _cacheLoaded;
|
|
2014
|
+
var init_global_procedures = __esm({
|
|
2015
|
+
"src/lib/global-procedures.ts"() {
|
|
2016
|
+
"use strict";
|
|
2017
|
+
init_database();
|
|
2018
|
+
_cache = "";
|
|
2019
|
+
_cacheLoaded = false;
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
|
|
1882
2023
|
// src/lib/store.ts
|
|
1883
2024
|
var store_exports = {};
|
|
1884
2025
|
__export(store_exports, {
|
|
@@ -1958,6 +2099,11 @@ async function initStore(options) {
|
|
|
1958
2099
|
"version-query"
|
|
1959
2100
|
);
|
|
1960
2101
|
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
2102
|
+
try {
|
|
2103
|
+
const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
|
|
2104
|
+
await loadGlobalProcedures2();
|
|
2105
|
+
} catch {
|
|
2106
|
+
}
|
|
1961
2107
|
}
|
|
1962
2108
|
function classifyTier(record) {
|
|
1963
2109
|
if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
|
|
@@ -1999,6 +2145,12 @@ async function writeMemory(record) {
|
|
|
1999
2145
|
supersedes_id: record.supersedes_id ?? null
|
|
2000
2146
|
};
|
|
2001
2147
|
_pendingRecords.push(dbRow);
|
|
2148
|
+
orgBus.emit({
|
|
2149
|
+
type: "memory_stored",
|
|
2150
|
+
agentId: record.agent_id,
|
|
2151
|
+
project: record.project_name,
|
|
2152
|
+
timestamp: record.timestamp
|
|
2153
|
+
});
|
|
2002
2154
|
const MAX_PENDING = 1e3;
|
|
2003
2155
|
if (_pendingRecords.length > MAX_PENDING) {
|
|
2004
2156
|
const dropped = _pendingRecords.length - MAX_PENDING;
|
|
@@ -2344,6 +2496,7 @@ var init_store = __esm({
|
|
|
2344
2496
|
init_database();
|
|
2345
2497
|
init_keychain();
|
|
2346
2498
|
init_config();
|
|
2499
|
+
init_state_bus();
|
|
2347
2500
|
INIT_MAX_RETRIES = 3;
|
|
2348
2501
|
INIT_RETRY_DELAY_MS = 1e3;
|
|
2349
2502
|
_pendingRecords = [];
|
|
@@ -3160,7 +3313,7 @@ __export(license_exports, {
|
|
|
3160
3313
|
validateLicense: () => validateLicense
|
|
3161
3314
|
});
|
|
3162
3315
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
|
|
3163
|
-
import { randomUUID as
|
|
3316
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3164
3317
|
import path10 from "path";
|
|
3165
3318
|
import { jwtVerify, importSPKI } from "jose";
|
|
3166
3319
|
async function fetchRetry(url, init) {
|
|
@@ -3187,7 +3340,7 @@ function loadDeviceId() {
|
|
|
3187
3340
|
}
|
|
3188
3341
|
} catch {
|
|
3189
3342
|
}
|
|
3190
|
-
const id =
|
|
3343
|
+
const id = randomUUID3();
|
|
3191
3344
|
mkdirSync3(EXE_AI_DIR, { recursive: true });
|
|
3192
3345
|
writeFileSync2(DEVICE_ID_PATH, id, "utf8");
|
|
3193
3346
|
return id;
|
|
@@ -3660,462 +3813,61 @@ var init_notifications = __esm({
|
|
|
3660
3813
|
}
|
|
3661
3814
|
});
|
|
3662
3815
|
|
|
3663
|
-
// src/lib/
|
|
3664
|
-
import
|
|
3816
|
+
// src/lib/session-registry.ts
|
|
3817
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
|
|
3665
3818
|
import path15 from "path";
|
|
3666
|
-
import
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
const row = await resolveTask(client, input.taskId);
|
|
3672
|
-
const taskId = String(row.id);
|
|
3673
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3674
|
-
const blockedByIds = [];
|
|
3675
|
-
if (row.blocked_by) {
|
|
3676
|
-
blockedByIds.push(String(row.blocked_by));
|
|
3819
|
+
import os4 from "os";
|
|
3820
|
+
function registerSession(entry) {
|
|
3821
|
+
const dir = path15.dirname(REGISTRY_PATH);
|
|
3822
|
+
if (!existsSync11(dir)) {
|
|
3823
|
+
mkdirSync4(dir, { recursive: true });
|
|
3677
3824
|
}
|
|
3678
|
-
const
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
};
|
|
3685
|
-
const result = await client.execute({
|
|
3686
|
-
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
3687
|
-
args: [JSON.stringify(checkpoint), now, taskId]
|
|
3688
|
-
});
|
|
3689
|
-
if (result.rowsAffected === 0) {
|
|
3690
|
-
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
3825
|
+
const sessions = listSessions();
|
|
3826
|
+
const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
|
|
3827
|
+
if (idx >= 0) {
|
|
3828
|
+
sessions[idx] = entry;
|
|
3829
|
+
} else {
|
|
3830
|
+
sessions.push(entry);
|
|
3691
3831
|
}
|
|
3692
|
-
|
|
3693
|
-
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
3694
|
-
args: [taskId]
|
|
3695
|
-
});
|
|
3696
|
-
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
3697
|
-
return { checkpointCount };
|
|
3698
|
-
}
|
|
3699
|
-
function extractParentFromContext(contextBody) {
|
|
3700
|
-
if (!contextBody) return null;
|
|
3701
|
-
const match = contextBody.match(
|
|
3702
|
-
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
3703
|
-
);
|
|
3704
|
-
return match ? match[1].toLowerCase() : null;
|
|
3705
|
-
}
|
|
3706
|
-
function slugify(title) {
|
|
3707
|
-
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
3832
|
+
writeFileSync5(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
3708
3833
|
}
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
}
|
|
3714
|
-
|
|
3715
|
-
result = await client.execute({
|
|
3716
|
-
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
3717
|
-
args: [`%${identifier}%`]
|
|
3718
|
-
});
|
|
3719
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
3720
|
-
if (result.rows.length > 1) {
|
|
3721
|
-
const exact = result.rows.filter(
|
|
3722
|
-
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
3723
|
-
);
|
|
3724
|
-
if (exact.length === 1) return exact[0];
|
|
3725
|
-
const candidates = exact.length > 1 ? exact : result.rows;
|
|
3726
|
-
const active = candidates.filter(
|
|
3727
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3728
|
-
);
|
|
3729
|
-
if (active.length === 1) return active[0];
|
|
3730
|
-
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3731
|
-
throw new Error(
|
|
3732
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3733
|
-
);
|
|
3734
|
-
}
|
|
3735
|
-
result = await client.execute({
|
|
3736
|
-
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
3737
|
-
args: [`%${identifier}%`]
|
|
3738
|
-
});
|
|
3739
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
3740
|
-
if (result.rows.length > 1) {
|
|
3741
|
-
const active = result.rows.filter(
|
|
3742
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3743
|
-
);
|
|
3744
|
-
if (active.length === 1) return active[0];
|
|
3745
|
-
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3746
|
-
throw new Error(
|
|
3747
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3748
|
-
);
|
|
3834
|
+
function listSessions() {
|
|
3835
|
+
try {
|
|
3836
|
+
const raw = readFileSync9(REGISTRY_PATH, "utf8");
|
|
3837
|
+
return JSON.parse(raw);
|
|
3838
|
+
} catch {
|
|
3839
|
+
return [];
|
|
3749
3840
|
}
|
|
3750
|
-
throw new Error(`Task not found: ${identifier}`);
|
|
3751
3841
|
}
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
|
|
3758
|
-
let blockedById = null;
|
|
3759
|
-
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
3760
|
-
if (input.blockedBy) {
|
|
3761
|
-
const blocker = await resolveTask(client, input.blockedBy);
|
|
3762
|
-
blockedById = String(blocker.id);
|
|
3763
|
-
}
|
|
3764
|
-
let parentTaskId = null;
|
|
3765
|
-
let parentRef = input.parentTaskId;
|
|
3766
|
-
if (!parentRef) {
|
|
3767
|
-
const extracted = extractParentFromContext(input.context);
|
|
3768
|
-
if (extracted) {
|
|
3769
|
-
parentRef = extracted;
|
|
3770
|
-
process.stderr.write(
|
|
3771
|
-
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
3772
|
-
);
|
|
3773
|
-
}
|
|
3842
|
+
var REGISTRY_PATH;
|
|
3843
|
+
var init_session_registry = __esm({
|
|
3844
|
+
"src/lib/session-registry.ts"() {
|
|
3845
|
+
"use strict";
|
|
3846
|
+
REGISTRY_PATH = path15.join(os4.homedir(), ".exe-os", "session-registry.json");
|
|
3774
3847
|
}
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
await mkdir4(path15.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
3799
|
-
await mkdir4(path15.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
3800
|
-
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
3801
|
-
await ensureGitignoreExe(input.baseDir);
|
|
3802
|
-
} catch {
|
|
3803
|
-
}
|
|
3804
|
-
}
|
|
3805
|
-
const complexity = input.complexity ?? "standard";
|
|
3806
|
-
await client.execute({
|
|
3807
|
-
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, created_at, updated_at)
|
|
3808
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3809
|
-
args: [
|
|
3810
|
-
id,
|
|
3811
|
-
input.title,
|
|
3812
|
-
input.assignedTo,
|
|
3813
|
-
input.assignedBy,
|
|
3814
|
-
input.projectName,
|
|
3815
|
-
input.priority,
|
|
3816
|
-
initialStatus,
|
|
3817
|
-
taskFile,
|
|
3818
|
-
blockedById,
|
|
3819
|
-
parentTaskId,
|
|
3820
|
-
input.reviewer ?? null,
|
|
3821
|
-
input.context,
|
|
3822
|
-
complexity,
|
|
3823
|
-
input.budgetTokens ?? null,
|
|
3824
|
-
input.budgetFallbackModel ?? null,
|
|
3825
|
-
0,
|
|
3826
|
-
null,
|
|
3827
|
-
now,
|
|
3828
|
-
now
|
|
3829
|
-
]
|
|
3830
|
-
});
|
|
3831
|
-
return {
|
|
3832
|
-
id,
|
|
3833
|
-
title: input.title,
|
|
3834
|
-
assignedTo: input.assignedTo,
|
|
3835
|
-
assignedBy: input.assignedBy,
|
|
3836
|
-
projectName: input.projectName,
|
|
3837
|
-
priority: input.priority,
|
|
3838
|
-
status: initialStatus,
|
|
3839
|
-
taskFile,
|
|
3840
|
-
createdAt: now,
|
|
3841
|
-
updatedAt: now,
|
|
3842
|
-
warning,
|
|
3843
|
-
budgetTokens: input.budgetTokens ?? null,
|
|
3844
|
-
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
3845
|
-
tokensUsed: 0,
|
|
3846
|
-
tokensWarnedAt: null
|
|
3847
|
-
};
|
|
3848
|
-
}
|
|
3849
|
-
async function listTasks(input) {
|
|
3850
|
-
const client = getClient();
|
|
3851
|
-
const conditions = [];
|
|
3852
|
-
const args = [];
|
|
3853
|
-
if (input.assignedTo) {
|
|
3854
|
-
conditions.push("assigned_to = ?");
|
|
3855
|
-
args.push(input.assignedTo);
|
|
3856
|
-
}
|
|
3857
|
-
if (input.status) {
|
|
3858
|
-
conditions.push("status = ?");
|
|
3859
|
-
args.push(input.status);
|
|
3860
|
-
} else {
|
|
3861
|
-
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
3862
|
-
}
|
|
3863
|
-
if (input.projectName) {
|
|
3864
|
-
conditions.push("project_name = ?");
|
|
3865
|
-
args.push(input.projectName);
|
|
3866
|
-
}
|
|
3867
|
-
if (input.priority) {
|
|
3868
|
-
conditions.push("priority = ?");
|
|
3869
|
-
args.push(input.priority);
|
|
3870
|
-
}
|
|
3871
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3872
|
-
const result = await client.execute({
|
|
3873
|
-
sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC`,
|
|
3874
|
-
args
|
|
3875
|
-
});
|
|
3876
|
-
return result.rows.map((r) => ({
|
|
3877
|
-
id: String(r.id),
|
|
3878
|
-
title: String(r.title),
|
|
3879
|
-
assignedTo: String(r.assigned_to),
|
|
3880
|
-
assignedBy: String(r.assigned_by),
|
|
3881
|
-
projectName: String(r.project_name),
|
|
3882
|
-
priority: String(r.priority),
|
|
3883
|
-
status: String(r.status),
|
|
3884
|
-
taskFile: String(r.task_file),
|
|
3885
|
-
createdAt: String(r.created_at),
|
|
3886
|
-
updatedAt: String(r.updated_at),
|
|
3887
|
-
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
3888
|
-
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
3889
|
-
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
3890
|
-
tokensUsed: Number(r.tokens_used ?? 0),
|
|
3891
|
-
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
3892
|
-
}));
|
|
3893
|
-
}
|
|
3894
|
-
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
3895
|
-
if (!taskContext) return null;
|
|
3896
|
-
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
3897
|
-
try {
|
|
3898
|
-
const since = new Date(taskCreatedAt).toISOString();
|
|
3899
|
-
const branch = execSync6(
|
|
3900
|
-
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
3901
|
-
{ encoding: "utf8", timeout: 3e3 }
|
|
3902
|
-
).trim();
|
|
3903
|
-
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
3904
|
-
const commitCount = execSync6(
|
|
3905
|
-
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
3906
|
-
{ encoding: "utf8", timeout: 5e3 }
|
|
3907
|
-
).trim();
|
|
3908
|
-
const count = parseInt(commitCount, 10);
|
|
3909
|
-
if (count === 0) {
|
|
3910
|
-
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
3911
|
-
}
|
|
3912
|
-
return null;
|
|
3913
|
-
} catch {
|
|
3914
|
-
return null;
|
|
3915
|
-
}
|
|
3916
|
-
}
|
|
3917
|
-
async function updateTaskStatus(input) {
|
|
3918
|
-
const client = getClient();
|
|
3919
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3920
|
-
const row = await resolveTask(client, input.taskId);
|
|
3921
|
-
const taskId = String(row.id);
|
|
3922
|
-
const taskFile = String(row.task_file);
|
|
3923
|
-
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
3924
|
-
process.stderr.write(
|
|
3925
|
-
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
3926
|
-
`
|
|
3927
|
-
);
|
|
3928
|
-
}
|
|
3929
|
-
if (input.status === "done") {
|
|
3930
|
-
const existingRow = await client.execute({
|
|
3931
|
-
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
3932
|
-
args: [taskId]
|
|
3933
|
-
});
|
|
3934
|
-
if (existingRow.rows.length > 0) {
|
|
3935
|
-
const ctx = existingRow.rows[0];
|
|
3936
|
-
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
3937
|
-
if (warning) {
|
|
3938
|
-
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
3939
|
-
|
|
3940
|
-
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
3941
|
-
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
3942
|
-
`);
|
|
3943
|
-
}
|
|
3944
|
-
}
|
|
3945
|
-
}
|
|
3946
|
-
if (input.status === "in_progress") {
|
|
3947
|
-
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
3948
|
-
const claim = await client.execute({
|
|
3949
|
-
sql: `UPDATE tasks
|
|
3950
|
-
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
3951
|
-
WHERE id = ? AND status = 'open'`,
|
|
3952
|
-
args: [tmuxSession, now, taskId]
|
|
3953
|
-
});
|
|
3954
|
-
if (claim.rowsAffected === 0) {
|
|
3955
|
-
const current = await client.execute({
|
|
3956
|
-
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
3957
|
-
args: [taskId]
|
|
3958
|
-
});
|
|
3959
|
-
const cur = current.rows[0];
|
|
3960
|
-
const status = cur?.status ?? "unknown";
|
|
3961
|
-
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
3962
|
-
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
3963
|
-
}
|
|
3964
|
-
try {
|
|
3965
|
-
await writeCheckpoint({
|
|
3966
|
-
taskId,
|
|
3967
|
-
step: "claimed",
|
|
3968
|
-
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
3969
|
-
});
|
|
3970
|
-
} catch {
|
|
3971
|
-
}
|
|
3972
|
-
return { row, taskFile, now, taskId };
|
|
3973
|
-
}
|
|
3974
|
-
if (input.result) {
|
|
3975
|
-
await client.execute({
|
|
3976
|
-
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
3977
|
-
args: [input.status, input.result, now, taskId]
|
|
3978
|
-
});
|
|
3979
|
-
} else {
|
|
3980
|
-
await client.execute({
|
|
3981
|
-
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
3982
|
-
args: [input.status, now, taskId]
|
|
3983
|
-
});
|
|
3984
|
-
}
|
|
3985
|
-
try {
|
|
3986
|
-
await writeCheckpoint({
|
|
3987
|
-
taskId,
|
|
3988
|
-
step: `status_transition:${input.status}`,
|
|
3989
|
-
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
3990
|
-
});
|
|
3991
|
-
} catch {
|
|
3992
|
-
}
|
|
3993
|
-
return { row, taskFile, now, taskId };
|
|
3994
|
-
}
|
|
3995
|
-
async function deleteTaskCore(taskId, _baseDir) {
|
|
3996
|
-
const client = getClient();
|
|
3997
|
-
const row = await resolveTask(client, taskId);
|
|
3998
|
-
const id = String(row.id);
|
|
3999
|
-
const taskFile = String(row.task_file);
|
|
4000
|
-
const assignedTo = String(row.assigned_to);
|
|
4001
|
-
const assignedBy = String(row.assigned_by);
|
|
4002
|
-
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
4003
|
-
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
4004
|
-
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
4005
|
-
}
|
|
4006
|
-
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
4007
|
-
const archPath = path15.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
4008
|
-
try {
|
|
4009
|
-
if (existsSync11(archPath)) return;
|
|
4010
|
-
const template = [
|
|
4011
|
-
`# ${projectName} \u2014 System Architecture`,
|
|
4012
|
-
"",
|
|
4013
|
-
"> Employees: read this before every task. Update it when you change system structure.",
|
|
4014
|
-
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
4015
|
-
"",
|
|
4016
|
-
"## Overview",
|
|
4017
|
-
"",
|
|
4018
|
-
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
4019
|
-
"",
|
|
4020
|
-
"## Key Components",
|
|
4021
|
-
"",
|
|
4022
|
-
"<!-- List the major modules, services, or subsystems. -->",
|
|
4023
|
-
"",
|
|
4024
|
-
"## Data Flow",
|
|
4025
|
-
"",
|
|
4026
|
-
"<!-- How does data move through the system? What writes where? -->",
|
|
4027
|
-
"",
|
|
4028
|
-
"## Invariants",
|
|
4029
|
-
"",
|
|
4030
|
-
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
4031
|
-
"",
|
|
4032
|
-
"## Dependencies",
|
|
4033
|
-
"",
|
|
4034
|
-
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
4035
|
-
""
|
|
4036
|
-
].join("\n");
|
|
4037
|
-
await writeFile4(archPath, template, "utf-8");
|
|
4038
|
-
} catch {
|
|
4039
|
-
}
|
|
4040
|
-
}
|
|
4041
|
-
async function ensureGitignoreExe(baseDir) {
|
|
4042
|
-
const gitignorePath = path15.join(baseDir, ".gitignore");
|
|
4043
|
-
try {
|
|
4044
|
-
if (existsSync11(gitignorePath)) {
|
|
4045
|
-
const content = readFileSync9(gitignorePath, "utf-8");
|
|
4046
|
-
if (/^\/?exe\/?$/m.test(content)) return;
|
|
4047
|
-
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
4048
|
-
} else {
|
|
4049
|
-
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
4050
|
-
}
|
|
4051
|
-
} catch {
|
|
4052
|
-
}
|
|
4053
|
-
}
|
|
4054
|
-
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
4055
|
-
var init_tasks_crud = __esm({
|
|
4056
|
-
"src/lib/tasks-crud.ts"() {
|
|
4057
|
-
"use strict";
|
|
4058
|
-
init_database();
|
|
4059
|
-
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
4060
|
-
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
4061
|
-
}
|
|
4062
|
-
});
|
|
4063
|
-
|
|
4064
|
-
// src/lib/session-registry.ts
|
|
4065
|
-
import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync12 } from "fs";
|
|
4066
|
-
import path16 from "path";
|
|
4067
|
-
import os4 from "os";
|
|
4068
|
-
function registerSession(entry) {
|
|
4069
|
-
const dir = path16.dirname(REGISTRY_PATH);
|
|
4070
|
-
if (!existsSync12(dir)) {
|
|
4071
|
-
mkdirSync4(dir, { recursive: true });
|
|
4072
|
-
}
|
|
4073
|
-
const sessions = listSessions();
|
|
4074
|
-
const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
|
|
4075
|
-
if (idx >= 0) {
|
|
4076
|
-
sessions[idx] = entry;
|
|
4077
|
-
} else {
|
|
4078
|
-
sessions.push(entry);
|
|
4079
|
-
}
|
|
4080
|
-
writeFileSync5(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
4081
|
-
}
|
|
4082
|
-
function listSessions() {
|
|
4083
|
-
try {
|
|
4084
|
-
const raw = readFileSync10(REGISTRY_PATH, "utf8");
|
|
4085
|
-
return JSON.parse(raw);
|
|
4086
|
-
} catch {
|
|
4087
|
-
return [];
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
4090
|
-
var REGISTRY_PATH;
|
|
4091
|
-
var init_session_registry = __esm({
|
|
4092
|
-
"src/lib/session-registry.ts"() {
|
|
4093
|
-
"use strict";
|
|
4094
|
-
REGISTRY_PATH = path16.join(os4.homedir(), ".exe-os", "session-registry.json");
|
|
4095
|
-
}
|
|
4096
|
-
});
|
|
4097
|
-
|
|
4098
|
-
// src/lib/tmux-transport.ts
|
|
4099
|
-
var tmux_transport_exports = {};
|
|
4100
|
-
__export(tmux_transport_exports, {
|
|
4101
|
-
TmuxTransport: () => TmuxTransport
|
|
4102
|
-
});
|
|
4103
|
-
import { execFileSync } from "child_process";
|
|
4104
|
-
var QUIET, TmuxTransport;
|
|
4105
|
-
var init_tmux_transport = __esm({
|
|
4106
|
-
"src/lib/tmux-transport.ts"() {
|
|
4107
|
-
"use strict";
|
|
4108
|
-
QUIET = {
|
|
4109
|
-
encoding: "utf8",
|
|
4110
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4111
|
-
};
|
|
4112
|
-
TmuxTransport = class {
|
|
4113
|
-
getMySession() {
|
|
4114
|
-
try {
|
|
4115
|
-
return execFileSync("tmux", ["display-message", "-p", "#{session_name}"], QUIET).trim() || null;
|
|
4116
|
-
} catch {
|
|
4117
|
-
return null;
|
|
4118
|
-
}
|
|
3848
|
+
});
|
|
3849
|
+
|
|
3850
|
+
// src/lib/tmux-transport.ts
|
|
3851
|
+
var tmux_transport_exports = {};
|
|
3852
|
+
__export(tmux_transport_exports, {
|
|
3853
|
+
TmuxTransport: () => TmuxTransport
|
|
3854
|
+
});
|
|
3855
|
+
import { execFileSync } from "child_process";
|
|
3856
|
+
var QUIET, TmuxTransport;
|
|
3857
|
+
var init_tmux_transport = __esm({
|
|
3858
|
+
"src/lib/tmux-transport.ts"() {
|
|
3859
|
+
"use strict";
|
|
3860
|
+
QUIET = {
|
|
3861
|
+
encoding: "utf8",
|
|
3862
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3863
|
+
};
|
|
3864
|
+
TmuxTransport = class {
|
|
3865
|
+
getMySession() {
|
|
3866
|
+
try {
|
|
3867
|
+
return execFileSync("tmux", ["display-message", "-p", "#{session_name}"], QUIET).trim() || null;
|
|
3868
|
+
} catch {
|
|
3869
|
+
return null;
|
|
3870
|
+
}
|
|
4119
3871
|
}
|
|
4120
3872
|
listSessions() {
|
|
4121
3873
|
try {
|
|
@@ -4203,14 +3955,14 @@ var init_transport = __esm({
|
|
|
4203
3955
|
});
|
|
4204
3956
|
|
|
4205
3957
|
// src/lib/cc-agent-support.ts
|
|
4206
|
-
import { execSync as
|
|
3958
|
+
import { execSync as execSync6 } from "child_process";
|
|
4207
3959
|
function _resetCcAgentSupportCache() {
|
|
4208
3960
|
_cachedSupport = null;
|
|
4209
3961
|
}
|
|
4210
3962
|
function claudeSupportsAgentFlag() {
|
|
4211
3963
|
if (_cachedSupport !== null) return _cachedSupport;
|
|
4212
3964
|
try {
|
|
4213
|
-
const helpOutput =
|
|
3965
|
+
const helpOutput = execSync6("claude --help 2>&1", {
|
|
4214
3966
|
encoding: "utf-8",
|
|
4215
3967
|
timeout: 5e3
|
|
4216
3968
|
});
|
|
@@ -4276,17 +4028,17 @@ var init_provider_table = __esm({
|
|
|
4276
4028
|
});
|
|
4277
4029
|
|
|
4278
4030
|
// src/lib/intercom-queue.ts
|
|
4279
|
-
import { readFileSync as
|
|
4280
|
-
import
|
|
4031
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, renameSync as renameSync2, existsSync as existsSync12, mkdirSync as mkdirSync5 } from "fs";
|
|
4032
|
+
import path16 from "path";
|
|
4281
4033
|
import os5 from "os";
|
|
4282
4034
|
function ensureDir() {
|
|
4283
|
-
const dir =
|
|
4284
|
-
if (!
|
|
4035
|
+
const dir = path16.dirname(QUEUE_PATH);
|
|
4036
|
+
if (!existsSync12(dir)) mkdirSync5(dir, { recursive: true });
|
|
4285
4037
|
}
|
|
4286
4038
|
function readQueue() {
|
|
4287
4039
|
try {
|
|
4288
|
-
if (!
|
|
4289
|
-
return JSON.parse(
|
|
4040
|
+
if (!existsSync12(QUEUE_PATH)) return [];
|
|
4041
|
+
return JSON.parse(readFileSync10(QUEUE_PATH, "utf8"));
|
|
4290
4042
|
} catch {
|
|
4291
4043
|
return [];
|
|
4292
4044
|
}
|
|
@@ -4318,21 +4070,358 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
|
4318
4070
|
var init_intercom_queue = __esm({
|
|
4319
4071
|
"src/lib/intercom-queue.ts"() {
|
|
4320
4072
|
"use strict";
|
|
4321
|
-
QUEUE_PATH =
|
|
4073
|
+
QUEUE_PATH = path16.join(os5.homedir(), ".exe-os", "intercom-queue.json");
|
|
4322
4074
|
TTL_MS = 60 * 60 * 1e3;
|
|
4323
|
-
INTERCOM_LOG =
|
|
4075
|
+
INTERCOM_LOG = path16.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
4076
|
+
}
|
|
4077
|
+
});
|
|
4078
|
+
|
|
4079
|
+
// src/lib/session-kill-telemetry.ts
|
|
4080
|
+
import crypto6 from "crypto";
|
|
4081
|
+
async function recordSessionKill(input) {
|
|
4082
|
+
try {
|
|
4083
|
+
const client = getClient();
|
|
4084
|
+
await client.execute({
|
|
4085
|
+
sql: `INSERT INTO session_kills
|
|
4086
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
4087
|
+
ticks_idle, estimated_tokens_saved)
|
|
4088
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
4089
|
+
args: [
|
|
4090
|
+
crypto6.randomUUID(),
|
|
4091
|
+
input.sessionName,
|
|
4092
|
+
input.agentId,
|
|
4093
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4094
|
+
input.reason,
|
|
4095
|
+
input.ticksIdle ?? null,
|
|
4096
|
+
input.estimatedTokensSaved ?? null
|
|
4097
|
+
]
|
|
4098
|
+
});
|
|
4099
|
+
} catch (err) {
|
|
4100
|
+
process.stderr.write(
|
|
4101
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
4102
|
+
`
|
|
4103
|
+
);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
var init_session_kill_telemetry = __esm({
|
|
4107
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
4108
|
+
"use strict";
|
|
4109
|
+
init_database();
|
|
4110
|
+
}
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
// src/lib/capacity-monitor.ts
|
|
4114
|
+
var capacity_monitor_exports = {};
|
|
4115
|
+
__export(capacity_monitor_exports, {
|
|
4116
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
4117
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
4118
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
4119
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
4120
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
4121
|
+
extractContextPercent: () => extractContextPercent,
|
|
4122
|
+
isAtCapacity: () => isAtCapacity,
|
|
4123
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
4124
|
+
pollCapacityDead: () => pollCapacityDead
|
|
4125
|
+
});
|
|
4126
|
+
function resumeTaskTitle(agentId) {
|
|
4127
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
4128
|
+
}
|
|
4129
|
+
function buildResumeContext(agentId, openTasks) {
|
|
4130
|
+
const taskList = openTasks.map(
|
|
4131
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
4132
|
+
).join("\n");
|
|
4133
|
+
return [
|
|
4134
|
+
"## Context",
|
|
4135
|
+
"",
|
|
4136
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
4137
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
4138
|
+
"",
|
|
4139
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
4140
|
+
"",
|
|
4141
|
+
taskList,
|
|
4142
|
+
"",
|
|
4143
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
4144
|
+
].join("\n");
|
|
4145
|
+
}
|
|
4146
|
+
function filterPaneContent(paneOutput) {
|
|
4147
|
+
return paneOutput.split("\n").filter((line) => {
|
|
4148
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
4149
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
4150
|
+
if (line.includes(marker)) return false;
|
|
4151
|
+
}
|
|
4152
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
4153
|
+
if (re.test(line)) return false;
|
|
4154
|
+
}
|
|
4155
|
+
return true;
|
|
4156
|
+
}).join("\n");
|
|
4157
|
+
}
|
|
4158
|
+
function extractContextPercent(paneOutput) {
|
|
4159
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
4160
|
+
if (!match) return null;
|
|
4161
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
4162
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
4163
|
+
}
|
|
4164
|
+
function isAtCapacity(paneOutput) {
|
|
4165
|
+
const filtered = filterPaneContent(paneOutput);
|
|
4166
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
4167
|
+
}
|
|
4168
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
4169
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
4170
|
+
if (pendingSince === void 0) {
|
|
4171
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4172
|
+
return false;
|
|
4173
|
+
}
|
|
4174
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
4175
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4176
|
+
return false;
|
|
4177
|
+
}
|
|
4178
|
+
_pendingCapacityKill.delete(agentId);
|
|
4179
|
+
return true;
|
|
4180
|
+
}
|
|
4181
|
+
function _resetPendingCapacityKills() {
|
|
4182
|
+
_pendingCapacityKill.clear();
|
|
4183
|
+
}
|
|
4184
|
+
function _resetLastRelaunchCache() {
|
|
4185
|
+
_lastRelaunch.clear();
|
|
4186
|
+
}
|
|
4187
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
4188
|
+
const client = getClient();
|
|
4189
|
+
const result = await client.execute({
|
|
4190
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
4191
|
+
FROM tasks
|
|
4192
|
+
WHERE assigned_to = ? AND title LIKE ?`,
|
|
4193
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`]
|
|
4194
|
+
});
|
|
4195
|
+
const raw = result.rows[0]?.last_created_at;
|
|
4196
|
+
if (raw === null || raw === void 0) return null;
|
|
4197
|
+
const parsed = Date.parse(String(raw));
|
|
4198
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
4199
|
+
}
|
|
4200
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
4201
|
+
const cached = _lastRelaunch.get(agentId);
|
|
4202
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
4203
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
4204
|
+
if (persisted === null) return false;
|
|
4205
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
4206
|
+
_lastRelaunch.set(agentId, persisted);
|
|
4207
|
+
return true;
|
|
4208
|
+
}
|
|
4209
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
4210
|
+
const client = getClient();
|
|
4211
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4212
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
4213
|
+
const existing = await client.execute({
|
|
4214
|
+
sql: `SELECT id FROM tasks
|
|
4215
|
+
WHERE assigned_to = ?
|
|
4216
|
+
AND title LIKE ?
|
|
4217
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
|
|
4218
|
+
ORDER BY created_at DESC
|
|
4219
|
+
LIMIT 1`,
|
|
4220
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
4221
|
+
});
|
|
4222
|
+
if (existing.rows.length > 0) {
|
|
4223
|
+
const taskId = String(existing.rows[0].id);
|
|
4224
|
+
await client.execute({
|
|
4225
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
4226
|
+
args: [context, now, taskId]
|
|
4227
|
+
});
|
|
4228
|
+
return { created: false, taskId };
|
|
4229
|
+
}
|
|
4230
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
4231
|
+
const task = await createTask2({
|
|
4232
|
+
title: resumeTaskTitle(agentId),
|
|
4233
|
+
assignedTo: agentId,
|
|
4234
|
+
assignedBy: "system",
|
|
4235
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
4236
|
+
priority: "p0",
|
|
4237
|
+
context,
|
|
4238
|
+
baseDir: projectDir
|
|
4239
|
+
});
|
|
4240
|
+
return { created: true, taskId: task.id };
|
|
4241
|
+
}
|
|
4242
|
+
async function pollCapacityDead() {
|
|
4243
|
+
const transport = getTransport();
|
|
4244
|
+
const relaunched = [];
|
|
4245
|
+
const registered = listSessions().filter(
|
|
4246
|
+
(s) => s.agentId !== "exe"
|
|
4247
|
+
);
|
|
4248
|
+
if (registered.length === 0) return [];
|
|
4249
|
+
let liveSessions;
|
|
4250
|
+
try {
|
|
4251
|
+
liveSessions = transport.listSessions();
|
|
4252
|
+
} catch {
|
|
4253
|
+
return [];
|
|
4254
|
+
}
|
|
4255
|
+
for (const entry of registered) {
|
|
4256
|
+
const { windowName, agentId, projectDir } = entry;
|
|
4257
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
4258
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
4259
|
+
let pane;
|
|
4260
|
+
try {
|
|
4261
|
+
pane = transport.capturePane(windowName, 15);
|
|
4262
|
+
} catch {
|
|
4263
|
+
continue;
|
|
4264
|
+
}
|
|
4265
|
+
if (!isAtCapacity(pane)) continue;
|
|
4266
|
+
const ctxPct = extractContextPercent(pane);
|
|
4267
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
4268
|
+
process.stderr.write(
|
|
4269
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
4270
|
+
`
|
|
4271
|
+
);
|
|
4272
|
+
continue;
|
|
4273
|
+
}
|
|
4274
|
+
if (!confirmCapacityKill(agentId)) {
|
|
4275
|
+
process.stderr.write(
|
|
4276
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
4277
|
+
`
|
|
4278
|
+
);
|
|
4279
|
+
continue;
|
|
4280
|
+
}
|
|
4281
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
4282
|
+
if (!verify.atCapacity) {
|
|
4283
|
+
process.stderr.write(
|
|
4284
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
4285
|
+
`
|
|
4286
|
+
);
|
|
4287
|
+
void recordSessionKill({
|
|
4288
|
+
sessionName: windowName,
|
|
4289
|
+
agentId,
|
|
4290
|
+
reason: "capacity_false_positive_blocked"
|
|
4291
|
+
});
|
|
4292
|
+
continue;
|
|
4293
|
+
}
|
|
4294
|
+
process.stderr.write(
|
|
4295
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
4296
|
+
`
|
|
4297
|
+
);
|
|
4298
|
+
try {
|
|
4299
|
+
transport.kill(windowName);
|
|
4300
|
+
void recordSessionKill({
|
|
4301
|
+
sessionName: windowName,
|
|
4302
|
+
agentId,
|
|
4303
|
+
reason: "capacity"
|
|
4304
|
+
});
|
|
4305
|
+
const client = getClient();
|
|
4306
|
+
const openTasks = await client.execute({
|
|
4307
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
4308
|
+
FROM tasks
|
|
4309
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')
|
|
4310
|
+
ORDER BY
|
|
4311
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
4312
|
+
created_at ASC
|
|
4313
|
+
LIMIT 10`,
|
|
4314
|
+
args: [agentId]
|
|
4315
|
+
});
|
|
4316
|
+
if (openTasks.rows.length === 0) {
|
|
4317
|
+
process.stderr.write(
|
|
4318
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
4319
|
+
`
|
|
4320
|
+
);
|
|
4321
|
+
continue;
|
|
4322
|
+
}
|
|
4323
|
+
const { created } = await createOrRefreshResumeTask(
|
|
4324
|
+
agentId,
|
|
4325
|
+
projectDir,
|
|
4326
|
+
openTasks.rows
|
|
4327
|
+
);
|
|
4328
|
+
if (created) {
|
|
4329
|
+
await writeNotification({
|
|
4330
|
+
agentId: "system",
|
|
4331
|
+
agentRole: "daemon",
|
|
4332
|
+
event: "capacity_relaunch",
|
|
4333
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
4334
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
4335
|
+
});
|
|
4336
|
+
}
|
|
4337
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
4338
|
+
if (created) relaunched.push(agentId);
|
|
4339
|
+
} catch (err) {
|
|
4340
|
+
process.stderr.write(
|
|
4341
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
4342
|
+
`
|
|
4343
|
+
);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
return relaunched;
|
|
4347
|
+
}
|
|
4348
|
+
var CAPACITY_PATTERNS, CONTENT_LINE_PREFIX, CONTENT_LINE_MARKERS, SOURCE_CODE_MARKERS, RELAUNCH_COOLDOWN_MS, _lastRelaunch, RESUME_TITLE_PREFIX, RESUME_TITLE_LIKE_PATTERN, RESUME_ACTIVE_STATUSES, CONFIRMATION_WINDOW_MS, _pendingCapacityKill, CC_CONTEXT_BAR_RE, CTX_FLOOR_PERCENT;
|
|
4349
|
+
var init_capacity_monitor = __esm({
|
|
4350
|
+
"src/lib/capacity-monitor.ts"() {
|
|
4351
|
+
"use strict";
|
|
4352
|
+
init_session_registry();
|
|
4353
|
+
init_transport();
|
|
4354
|
+
init_notifications();
|
|
4355
|
+
init_database();
|
|
4356
|
+
init_session_kill_telemetry();
|
|
4357
|
+
init_tmux_routing();
|
|
4358
|
+
CAPACITY_PATTERNS = [
|
|
4359
|
+
/conversation is too long/i,
|
|
4360
|
+
/maximum context length/i,
|
|
4361
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
4362
|
+
/reached.*(?:token|context).*limit/i
|
|
4363
|
+
];
|
|
4364
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
4365
|
+
CONTENT_LINE_MARKERS = [
|
|
4366
|
+
"RESUME:",
|
|
4367
|
+
"intercom",
|
|
4368
|
+
"capacity-monitor",
|
|
4369
|
+
"CAPACITY_PATTERNS",
|
|
4370
|
+
"isAtCapacity",
|
|
4371
|
+
"CONTENT_LINE_MARKERS",
|
|
4372
|
+
"pollCapacityDead",
|
|
4373
|
+
"confirmCapacityKill",
|
|
4374
|
+
"session_kills",
|
|
4375
|
+
"capacity-monitor.test"
|
|
4376
|
+
];
|
|
4377
|
+
SOURCE_CODE_MARKERS = [
|
|
4378
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
4379
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
4380
|
+
];
|
|
4381
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
4382
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
4383
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
4384
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
4385
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
4386
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
4387
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
4388
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
4389
|
+
CTX_FLOOR_PERCENT = 50;
|
|
4324
4390
|
}
|
|
4325
4391
|
});
|
|
4326
4392
|
|
|
4327
4393
|
// src/lib/tmux-routing.ts
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4394
|
+
var tmux_routing_exports = {};
|
|
4395
|
+
__export(tmux_routing_exports, {
|
|
4396
|
+
acquireSpawnLock: () => acquireSpawnLock2,
|
|
4397
|
+
employeeSessionName: () => employeeSessionName,
|
|
4398
|
+
ensureEmployee: () => ensureEmployee,
|
|
4399
|
+
extractRootExe: () => extractRootExe,
|
|
4400
|
+
findFreeInstance: () => findFreeInstance,
|
|
4401
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
4402
|
+
getMySession: () => getMySession,
|
|
4403
|
+
getParentExe: () => getParentExe,
|
|
4404
|
+
getSessionState: () => getSessionState,
|
|
4405
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
4406
|
+
isExeSession: () => isExeSession,
|
|
4407
|
+
isSessionBusy: () => isSessionBusy,
|
|
4408
|
+
notifyParentExe: () => notifyParentExe,
|
|
4409
|
+
parseParentExe: () => parseParentExe,
|
|
4410
|
+
registerParentExe: () => registerParentExe,
|
|
4411
|
+
releaseSpawnLock: () => releaseSpawnLock2,
|
|
4412
|
+
resolveExeSession: () => resolveExeSession,
|
|
4413
|
+
sendIntercom: () => sendIntercom,
|
|
4414
|
+
spawnEmployee: () => spawnEmployee,
|
|
4415
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
4416
|
+
});
|
|
4417
|
+
import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
|
|
4418
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync13, appendFileSync } from "fs";
|
|
4419
|
+
import path17 from "path";
|
|
4331
4420
|
import os6 from "os";
|
|
4332
4421
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4333
4422
|
import { unlinkSync as unlinkSync4 } from "fs";
|
|
4334
4423
|
function spawnLockPath(sessionName) {
|
|
4335
|
-
return
|
|
4424
|
+
return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
4336
4425
|
}
|
|
4337
4426
|
function isProcessAlive(pid) {
|
|
4338
4427
|
try {
|
|
@@ -4343,13 +4432,13 @@ function isProcessAlive(pid) {
|
|
|
4343
4432
|
}
|
|
4344
4433
|
}
|
|
4345
4434
|
function acquireSpawnLock2(sessionName) {
|
|
4346
|
-
if (!
|
|
4435
|
+
if (!existsSync13(SPAWN_LOCK_DIR)) {
|
|
4347
4436
|
mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
|
|
4348
4437
|
}
|
|
4349
4438
|
const lockFile = spawnLockPath(sessionName);
|
|
4350
|
-
if (
|
|
4439
|
+
if (existsSync13(lockFile)) {
|
|
4351
4440
|
try {
|
|
4352
|
-
const lock = JSON.parse(
|
|
4441
|
+
const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
|
|
4353
4442
|
const age = Date.now() - lock.timestamp;
|
|
4354
4443
|
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
4355
4444
|
return false;
|
|
@@ -4369,13 +4458,13 @@ function releaseSpawnLock2(sessionName) {
|
|
|
4369
4458
|
function resolveBehaviorsExporterScript() {
|
|
4370
4459
|
try {
|
|
4371
4460
|
const thisFile = fileURLToPath2(import.meta.url);
|
|
4372
|
-
const scriptPath =
|
|
4373
|
-
|
|
4461
|
+
const scriptPath = path17.join(
|
|
4462
|
+
path17.dirname(thisFile),
|
|
4374
4463
|
"..",
|
|
4375
4464
|
"bin",
|
|
4376
4465
|
"exe-export-behaviors.js"
|
|
4377
4466
|
);
|
|
4378
|
-
return
|
|
4467
|
+
return existsSync13(scriptPath) ? scriptPath : null;
|
|
4379
4468
|
} catch {
|
|
4380
4469
|
return null;
|
|
4381
4470
|
}
|
|
@@ -4395,23 +4484,61 @@ function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
|
4395
4484
|
`[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
4396
4485
|
`
|
|
4397
4486
|
);
|
|
4398
|
-
return null;
|
|
4487
|
+
return null;
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
function getMySession() {
|
|
4491
|
+
return getTransport().getMySession();
|
|
4492
|
+
}
|
|
4493
|
+
function employeeSessionName(employee, exeSession, instance) {
|
|
4494
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
4495
|
+
const root = extractRootExe(exeSession);
|
|
4496
|
+
if (root) {
|
|
4497
|
+
process.stderr.write(
|
|
4498
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
|
|
4499
|
+
`
|
|
4500
|
+
);
|
|
4501
|
+
exeSession = root;
|
|
4502
|
+
} else {
|
|
4503
|
+
throw new Error(
|
|
4504
|
+
`Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
|
|
4505
|
+
);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
4509
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
4510
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
4511
|
+
throw new Error(
|
|
4512
|
+
`Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
|
|
4513
|
+
);
|
|
4399
4514
|
}
|
|
4515
|
+
return name;
|
|
4400
4516
|
}
|
|
4401
|
-
function
|
|
4402
|
-
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
return `${employee}${suffix}-${exeSession}`;
|
|
4517
|
+
function parseParentExe(sessionName, agentId) {
|
|
4518
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4519
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
4520
|
+
const match = sessionName.match(regex);
|
|
4521
|
+
return match?.[1] ?? null;
|
|
4407
4522
|
}
|
|
4408
4523
|
function extractRootExe(name) {
|
|
4409
4524
|
const match = name.match(/(exe\d+)$/);
|
|
4410
4525
|
return match?.[1] ?? null;
|
|
4411
4526
|
}
|
|
4527
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
4528
|
+
if (!existsSync13(SESSION_CACHE)) {
|
|
4529
|
+
mkdirSync6(SESSION_CACHE, { recursive: true });
|
|
4530
|
+
}
|
|
4531
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
4532
|
+
const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
4533
|
+
writeFileSync7(filePath, JSON.stringify({
|
|
4534
|
+
parentExe: rootExe,
|
|
4535
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
4536
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4537
|
+
}));
|
|
4538
|
+
}
|
|
4412
4539
|
function getParentExe(sessionKey) {
|
|
4413
4540
|
try {
|
|
4414
|
-
const data = JSON.parse(
|
|
4541
|
+
const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
4415
4542
|
return data.parentExe || null;
|
|
4416
4543
|
} catch {
|
|
4417
4544
|
return null;
|
|
@@ -4419,8 +4546,8 @@ function getParentExe(sessionKey) {
|
|
|
4419
4546
|
}
|
|
4420
4547
|
function getDispatchedBy(sessionKey) {
|
|
4421
4548
|
try {
|
|
4422
|
-
const data = JSON.parse(
|
|
4423
|
-
|
|
4549
|
+
const data = JSON.parse(readFileSync11(
|
|
4550
|
+
path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
4424
4551
|
"utf8"
|
|
4425
4552
|
));
|
|
4426
4553
|
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
@@ -4453,414 +4580,879 @@ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive =
|
|
|
4453
4580
|
}
|
|
4454
4581
|
return null;
|
|
4455
4582
|
}
|
|
4583
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
4584
|
+
const transport = getTransport();
|
|
4585
|
+
if (!transport.isAlive(sessionName)) {
|
|
4586
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
4587
|
+
}
|
|
4588
|
+
let pane;
|
|
4589
|
+
try {
|
|
4590
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
4591
|
+
} catch (err) {
|
|
4592
|
+
return {
|
|
4593
|
+
atCapacity: false,
|
|
4594
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4597
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
4598
|
+
if (!isAtCapacity2(pane)) {
|
|
4599
|
+
return {
|
|
4600
|
+
atCapacity: false,
|
|
4601
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
4602
|
+
};
|
|
4603
|
+
}
|
|
4604
|
+
return {
|
|
4605
|
+
atCapacity: true,
|
|
4606
|
+
reason: "capacity banner matched in recent pane output"
|
|
4607
|
+
};
|
|
4608
|
+
}
|
|
4456
4609
|
function readDebounceState() {
|
|
4457
4610
|
try {
|
|
4458
|
-
if (!
|
|
4459
|
-
return JSON.parse(
|
|
4611
|
+
if (!existsSync13(DEBOUNCE_FILE)) return {};
|
|
4612
|
+
return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
|
|
4613
|
+
} catch {
|
|
4614
|
+
return {};
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
function writeDebounceState(state) {
|
|
4618
|
+
try {
|
|
4619
|
+
if (!existsSync13(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
|
|
4620
|
+
writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
|
|
4621
|
+
} catch {
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
function isDebounced(targetSession) {
|
|
4625
|
+
const state = readDebounceState();
|
|
4626
|
+
const lastSent = state[targetSession] ?? 0;
|
|
4627
|
+
return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
|
|
4628
|
+
}
|
|
4629
|
+
function recordDebounce(targetSession) {
|
|
4630
|
+
const state = readDebounceState();
|
|
4631
|
+
state[targetSession] = Date.now();
|
|
4632
|
+
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
4633
|
+
for (const key of Object.keys(state)) {
|
|
4634
|
+
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
4635
|
+
}
|
|
4636
|
+
writeDebounceState(state);
|
|
4637
|
+
}
|
|
4638
|
+
function logIntercom(msg) {
|
|
4639
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
4640
|
+
`;
|
|
4641
|
+
process.stderr.write(`[intercom] ${msg}
|
|
4642
|
+
`);
|
|
4643
|
+
try {
|
|
4644
|
+
appendFileSync(INTERCOM_LOG2, line);
|
|
4645
|
+
} catch {
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
function getSessionState(sessionName) {
|
|
4649
|
+
const transport = getTransport();
|
|
4650
|
+
if (!transport.isAlive(sessionName)) return "offline";
|
|
4651
|
+
try {
|
|
4652
|
+
const pane = transport.capturePane(sessionName, 5);
|
|
4653
|
+
if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
|
|
4654
|
+
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
4655
|
+
return "no_claude";
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
if (/Running…/.test(pane)) return "tool";
|
|
4659
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
4660
|
+
return "idle";
|
|
4661
|
+
} catch {
|
|
4662
|
+
return "offline";
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
function isSessionBusy(sessionName) {
|
|
4666
|
+
const state = getSessionState(sessionName);
|
|
4667
|
+
return state === "thinking" || state === "tool";
|
|
4668
|
+
}
|
|
4669
|
+
function isExeSession(sessionName) {
|
|
4670
|
+
return /^exe\d*$/.test(sessionName);
|
|
4671
|
+
}
|
|
4672
|
+
function sendIntercom(targetSession) {
|
|
4673
|
+
const transport = getTransport();
|
|
4674
|
+
if (isExeSession(targetSession)) {
|
|
4675
|
+
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
4676
|
+
return "skipped_exe";
|
|
4677
|
+
}
|
|
4678
|
+
if (isDebounced(targetSession)) {
|
|
4679
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
|
|
4680
|
+
return "debounced";
|
|
4681
|
+
}
|
|
4682
|
+
try {
|
|
4683
|
+
const sessions = transport.listSessions();
|
|
4684
|
+
if (!sessions.includes(targetSession)) {
|
|
4685
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
4686
|
+
return "failed";
|
|
4687
|
+
}
|
|
4688
|
+
const sessionState = getSessionState(targetSession);
|
|
4689
|
+
if (sessionState === "no_claude") {
|
|
4690
|
+
queueIntercom(targetSession, "claude not running in session");
|
|
4691
|
+
recordDebounce(targetSession);
|
|
4692
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
4693
|
+
return "queued";
|
|
4694
|
+
}
|
|
4695
|
+
if (sessionState === "thinking" || sessionState === "tool") {
|
|
4696
|
+
queueIntercom(targetSession, "session busy at send time");
|
|
4697
|
+
recordDebounce(targetSession);
|
|
4698
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
4699
|
+
return "queued";
|
|
4700
|
+
}
|
|
4701
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
4702
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
4703
|
+
transport.sendKeys(targetSession, "q");
|
|
4704
|
+
}
|
|
4705
|
+
transport.sendKeys(targetSession, "/exe-intercom");
|
|
4706
|
+
recordDebounce(targetSession);
|
|
4707
|
+
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
4708
|
+
return "delivered";
|
|
4709
|
+
} catch {
|
|
4710
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
4711
|
+
return "failed";
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
function notifyParentExe(sessionKey) {
|
|
4715
|
+
const target = getDispatchedBy(sessionKey);
|
|
4716
|
+
if (!target) {
|
|
4717
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
4718
|
+
`);
|
|
4719
|
+
return false;
|
|
4720
|
+
}
|
|
4721
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
4722
|
+
`);
|
|
4723
|
+
const result = sendIntercom(target);
|
|
4724
|
+
if (result === "failed") {
|
|
4725
|
+
const rootExe = resolveExeSession();
|
|
4726
|
+
if (rootExe && rootExe !== target) {
|
|
4727
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
4728
|
+
`);
|
|
4729
|
+
const fallback = sendIntercom(rootExe);
|
|
4730
|
+
return fallback !== "failed";
|
|
4731
|
+
}
|
|
4732
|
+
return false;
|
|
4733
|
+
}
|
|
4734
|
+
return true;
|
|
4735
|
+
}
|
|
4736
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
4737
|
+
if (employeeName === "exe") {
|
|
4738
|
+
return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
|
|
4739
|
+
}
|
|
4740
|
+
try {
|
|
4741
|
+
assertEmployeeLimitSync();
|
|
4742
|
+
} catch (err) {
|
|
4743
|
+
if (err instanceof PlanLimitError) {
|
|
4744
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
if (/-exe\d*$/.test(employeeName)) {
|
|
4748
|
+
const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
|
|
4749
|
+
return {
|
|
4750
|
+
status: "failed",
|
|
4751
|
+
sessionName: "",
|
|
4752
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
4753
|
+
};
|
|
4754
|
+
}
|
|
4755
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
4756
|
+
const root = extractRootExe(exeSession);
|
|
4757
|
+
if (root) {
|
|
4758
|
+
process.stderr.write(
|
|
4759
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
|
|
4760
|
+
`
|
|
4761
|
+
);
|
|
4762
|
+
exeSession = root;
|
|
4763
|
+
} else {
|
|
4764
|
+
return {
|
|
4765
|
+
status: "failed",
|
|
4766
|
+
sessionName: "",
|
|
4767
|
+
error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
|
|
4768
|
+
};
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
let effectiveInstance = opts?.instance;
|
|
4772
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
4773
|
+
const free = findFreeInstance(
|
|
4774
|
+
employeeName,
|
|
4775
|
+
exeSession,
|
|
4776
|
+
opts.maxAutoInstances ?? 10
|
|
4777
|
+
);
|
|
4778
|
+
if (free === null) {
|
|
4779
|
+
return {
|
|
4780
|
+
status: "failed",
|
|
4781
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
4782
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
4783
|
+
};
|
|
4784
|
+
}
|
|
4785
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
4786
|
+
}
|
|
4787
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
4788
|
+
if (isEmployeeAlive(sessionName)) {
|
|
4789
|
+
const result2 = sendIntercom(sessionName);
|
|
4790
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
4791
|
+
return { status: "intercom_sent", sessionName };
|
|
4792
|
+
}
|
|
4793
|
+
if (result2 === "delivered") {
|
|
4794
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
4795
|
+
}
|
|
4796
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
4797
|
+
}
|
|
4798
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
4799
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
4800
|
+
if (result.error) {
|
|
4801
|
+
return { status: "failed", sessionName, error: result.error };
|
|
4802
|
+
}
|
|
4803
|
+
return { status: "spawned", sessionName };
|
|
4804
|
+
}
|
|
4805
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
4806
|
+
const transport = getTransport();
|
|
4807
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
4808
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
4809
|
+
const logDir = path17.join(os6.homedir(), ".exe-os", "session-logs");
|
|
4810
|
+
const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
4811
|
+
if (!existsSync13(logDir)) {
|
|
4812
|
+
mkdirSync6(logDir, { recursive: true });
|
|
4813
|
+
}
|
|
4814
|
+
transport.kill(sessionName);
|
|
4815
|
+
let cleanupSuffix = "";
|
|
4816
|
+
try {
|
|
4817
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
4818
|
+
const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
4819
|
+
if (existsSync13(cleanupScript)) {
|
|
4820
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
4821
|
+
}
|
|
4822
|
+
} catch {
|
|
4823
|
+
}
|
|
4824
|
+
try {
|
|
4825
|
+
const claudeJsonPath = path17.join(os6.homedir(), ".claude.json");
|
|
4826
|
+
let claudeJson = {};
|
|
4827
|
+
try {
|
|
4828
|
+
claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
|
|
4829
|
+
} catch {
|
|
4830
|
+
}
|
|
4831
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
4832
|
+
const projects = claudeJson.projects;
|
|
4833
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
4834
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
4835
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
4836
|
+
writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
4837
|
+
} catch {
|
|
4838
|
+
}
|
|
4839
|
+
try {
|
|
4840
|
+
const settingsDir = path17.join(os6.homedir(), ".claude", "projects");
|
|
4841
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
4842
|
+
const projSettingsDir = path17.join(settingsDir, normalizedKey);
|
|
4843
|
+
const settingsPath = path17.join(projSettingsDir, "settings.json");
|
|
4844
|
+
let settings = {};
|
|
4845
|
+
try {
|
|
4846
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
|
|
4847
|
+
} catch {
|
|
4848
|
+
}
|
|
4849
|
+
const perms = settings.permissions ?? {};
|
|
4850
|
+
const allow = perms.allow ?? [];
|
|
4851
|
+
const toolNames = [
|
|
4852
|
+
"recall_my_memory",
|
|
4853
|
+
"store_memory",
|
|
4854
|
+
"create_task",
|
|
4855
|
+
"update_task",
|
|
4856
|
+
"list_tasks",
|
|
4857
|
+
"get_task",
|
|
4858
|
+
"ask_team_memory",
|
|
4859
|
+
"store_behavior",
|
|
4860
|
+
"get_identity",
|
|
4861
|
+
"send_message"
|
|
4862
|
+
];
|
|
4863
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
4864
|
+
let changed = false;
|
|
4865
|
+
for (const tool of requiredTools) {
|
|
4866
|
+
if (!allow.includes(tool)) {
|
|
4867
|
+
allow.push(tool);
|
|
4868
|
+
changed = true;
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
if (changed) {
|
|
4872
|
+
perms.allow = allow;
|
|
4873
|
+
settings.permissions = perms;
|
|
4874
|
+
mkdirSync6(projSettingsDir, { recursive: true });
|
|
4875
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
4876
|
+
}
|
|
4460
4877
|
} catch {
|
|
4461
|
-
return {};
|
|
4462
4878
|
}
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4879
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
4880
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
4881
|
+
const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
4882
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
4883
|
+
let identityFlag = "";
|
|
4884
|
+
let behaviorsFlag = "";
|
|
4885
|
+
let legacyFallbackWarned = false;
|
|
4886
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
4887
|
+
const identityPath2 = path17.join(
|
|
4888
|
+
os6.homedir(),
|
|
4889
|
+
".exe-os",
|
|
4890
|
+
"identity",
|
|
4891
|
+
`${employeeName}.md`
|
|
4892
|
+
);
|
|
4893
|
+
_resetCcAgentSupportCache();
|
|
4894
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
4895
|
+
if (hasAgentFlag) {
|
|
4896
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
4897
|
+
} else if (existsSync13(identityPath2)) {
|
|
4898
|
+
identityFlag = ` --append-system-prompt-file ${identityPath2}`;
|
|
4899
|
+
legacyFallbackWarned = true;
|
|
4900
|
+
}
|
|
4901
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
4902
|
+
employeeName,
|
|
4903
|
+
path17.basename(spawnCwd),
|
|
4904
|
+
sessionName
|
|
4905
|
+
);
|
|
4906
|
+
if (behaviorsFile) {
|
|
4907
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
4908
|
+
}
|
|
4469
4909
|
}
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
}
|
|
4476
|
-
function recordDebounce(targetSession) {
|
|
4477
|
-
const state = readDebounceState();
|
|
4478
|
-
state[targetSession] = Date.now();
|
|
4479
|
-
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
4480
|
-
for (const key of Object.keys(state)) {
|
|
4481
|
-
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
4910
|
+
if (legacyFallbackWarned) {
|
|
4911
|
+
process.stderr.write(
|
|
4912
|
+
`[tmux-routing] claude --agent not supported by installed CC. Falling back to --append-system-prompt-file for ${employeeName}. Upgrade Claude Code to enable native --agent launch.
|
|
4913
|
+
`
|
|
4914
|
+
);
|
|
4482
4915
|
}
|
|
4483
|
-
|
|
4484
|
-
}
|
|
4485
|
-
function logIntercom(msg) {
|
|
4486
|
-
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
4487
|
-
`;
|
|
4488
|
-
process.stderr.write(`[intercom] ${msg}
|
|
4489
|
-
`);
|
|
4916
|
+
let sessionContextFlag = "";
|
|
4490
4917
|
try {
|
|
4491
|
-
|
|
4918
|
+
const ctxDir = path17.join(os6.homedir(), ".exe-os", "session-cache");
|
|
4919
|
+
mkdirSync6(ctxDir, { recursive: true });
|
|
4920
|
+
const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
|
|
4921
|
+
const ctxContent = [
|
|
4922
|
+
`## Session Context`,
|
|
4923
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
4924
|
+
`Your parent exe session is ${exeSession}.`,
|
|
4925
|
+
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
4926
|
+
].join("\n");
|
|
4927
|
+
writeFileSync7(ctxFile, ctxContent);
|
|
4928
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
4492
4929
|
} catch {
|
|
4493
4930
|
}
|
|
4494
|
-
}
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
4502
|
-
return "no_claude";
|
|
4931
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
4932
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
4933
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
4934
|
+
if (cfg?.apiKeyEnv) {
|
|
4935
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
4936
|
+
if (keyVal) {
|
|
4937
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
4503
4938
|
}
|
|
4504
4939
|
}
|
|
4505
|
-
if (/Running…/.test(pane)) return "tool";
|
|
4506
|
-
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
4507
|
-
return "idle";
|
|
4508
|
-
} catch {
|
|
4509
|
-
return "offline";
|
|
4510
4940
|
}
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
}
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4941
|
+
let spawnCommand;
|
|
4942
|
+
if (useExeAgent) {
|
|
4943
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
4944
|
+
} else if (useBinSymlink) {
|
|
4945
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
4946
|
+
process.stderr.write(
|
|
4947
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
4948
|
+
`
|
|
4949
|
+
);
|
|
4950
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
4951
|
+
} else {
|
|
4952
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
4520
4953
|
}
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4954
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
4955
|
+
cwd: spawnCwd,
|
|
4956
|
+
command: spawnCommand
|
|
4957
|
+
});
|
|
4958
|
+
if (spawnResult.error) {
|
|
4959
|
+
releaseSpawnLock2(sessionName);
|
|
4960
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
4524
4961
|
}
|
|
4962
|
+
transport.pipeLog(sessionName, logFile);
|
|
4525
4963
|
try {
|
|
4526
|
-
const
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
4542
|
-
return "queued";
|
|
4964
|
+
const mySession = getMySession();
|
|
4965
|
+
const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
4966
|
+
writeFileSync7(dispatchInfo, JSON.stringify({
|
|
4967
|
+
dispatchedBy: mySession,
|
|
4968
|
+
rootExe: exeSession,
|
|
4969
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
4970
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4971
|
+
}));
|
|
4972
|
+
} catch {
|
|
4973
|
+
}
|
|
4974
|
+
let booted = false;
|
|
4975
|
+
for (let i = 0; i < 30; i++) {
|
|
4976
|
+
try {
|
|
4977
|
+
execSync7("sleep 0.5");
|
|
4978
|
+
} catch {
|
|
4543
4979
|
}
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4980
|
+
try {
|
|
4981
|
+
const pane = transport.capturePane(sessionName);
|
|
4982
|
+
if (useExeAgent) {
|
|
4983
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
4984
|
+
booted = true;
|
|
4985
|
+
break;
|
|
4986
|
+
}
|
|
4987
|
+
} else {
|
|
4988
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
4989
|
+
booted = true;
|
|
4990
|
+
break;
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
} catch {
|
|
4547
4994
|
}
|
|
4548
|
-
transport.sendKeys(targetSession, "/exe-intercom");
|
|
4549
|
-
recordDebounce(targetSession);
|
|
4550
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
4551
|
-
return "delivered";
|
|
4552
|
-
} catch {
|
|
4553
|
-
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
4554
|
-
return "failed";
|
|
4555
4995
|
}
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
if (!target) {
|
|
4560
|
-
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
4561
|
-
`);
|
|
4562
|
-
return false;
|
|
4996
|
+
if (!booted) {
|
|
4997
|
+
releaseSpawnLock2(sessionName);
|
|
4998
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
4563
4999
|
}
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
const rootExe = resolveExeSession();
|
|
4569
|
-
if (rootExe && rootExe !== target) {
|
|
4570
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
4571
|
-
`);
|
|
4572
|
-
const fallback = sendIntercom(rootExe);
|
|
4573
|
-
return fallback !== "failed";
|
|
5000
|
+
if (!useExeAgent) {
|
|
5001
|
+
try {
|
|
5002
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
5003
|
+
} catch {
|
|
4574
5004
|
}
|
|
4575
|
-
return false;
|
|
4576
5005
|
}
|
|
4577
|
-
|
|
5006
|
+
registerSession({
|
|
5007
|
+
windowName: sessionName,
|
|
5008
|
+
agentId: employeeName,
|
|
5009
|
+
projectDir: spawnCwd,
|
|
5010
|
+
parentExe: exeSession,
|
|
5011
|
+
pid: 0,
|
|
5012
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5013
|
+
});
|
|
5014
|
+
releaseSpawnLock2(sessionName);
|
|
5015
|
+
return { sessionName };
|
|
4578
5016
|
}
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
5017
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
5018
|
+
var init_tmux_routing = __esm({
|
|
5019
|
+
"src/lib/tmux-routing.ts"() {
|
|
5020
|
+
"use strict";
|
|
5021
|
+
init_session_registry();
|
|
5022
|
+
init_session_key();
|
|
5023
|
+
init_transport();
|
|
5024
|
+
init_cc_agent_support();
|
|
5025
|
+
init_mcp_prefix();
|
|
5026
|
+
init_provider_table();
|
|
5027
|
+
init_intercom_queue();
|
|
5028
|
+
init_plan_limits();
|
|
5029
|
+
SPAWN_LOCK_DIR = path17.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
5030
|
+
SESSION_CACHE = path17.join(os6.homedir(), ".exe-os", "session-cache");
|
|
5031
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
5032
|
+
VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
|
|
5033
|
+
VERIFY_PANE_LINES = 200;
|
|
5034
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
5035
|
+
INTERCOM_LOG2 = path17.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
5036
|
+
DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
|
|
5037
|
+
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
5038
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
4582
5039
|
}
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
5040
|
+
});
|
|
5041
|
+
|
|
5042
|
+
// src/lib/tasks-crud.ts
|
|
5043
|
+
import crypto7 from "crypto";
|
|
5044
|
+
import path18 from "path";
|
|
5045
|
+
import { execSync as execSync8 } from "child_process";
|
|
5046
|
+
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
5047
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
|
|
5048
|
+
async function writeCheckpoint(input) {
|
|
5049
|
+
const client = getClient();
|
|
5050
|
+
const row = await resolveTask(client, input.taskId);
|
|
5051
|
+
const taskId = String(row.id);
|
|
5052
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5053
|
+
const blockedByIds = [];
|
|
5054
|
+
if (row.blocked_by) {
|
|
5055
|
+
blockedByIds.push(String(row.blocked_by));
|
|
4589
5056
|
}
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
5057
|
+
const checkpoint = {
|
|
5058
|
+
step: input.step,
|
|
5059
|
+
context_summary: input.contextSummary,
|
|
5060
|
+
files_touched: input.filesTouched ?? [],
|
|
5061
|
+
blocked_by_ids: blockedByIds,
|
|
5062
|
+
last_checkpoint_at: now
|
|
5063
|
+
};
|
|
5064
|
+
const result = await client.execute({
|
|
5065
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
5066
|
+
args: [JSON.stringify(checkpoint), now, taskId]
|
|
5067
|
+
});
|
|
5068
|
+
if (result.rowsAffected === 0) {
|
|
5069
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
4597
5070
|
}
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
5071
|
+
const countResult = await client.execute({
|
|
5072
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
5073
|
+
args: [taskId]
|
|
5074
|
+
});
|
|
5075
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
5076
|
+
return { checkpointCount };
|
|
5077
|
+
}
|
|
5078
|
+
function extractParentFromContext(contextBody) {
|
|
5079
|
+
if (!contextBody) return null;
|
|
5080
|
+
const match = contextBody.match(
|
|
5081
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
5082
|
+
);
|
|
5083
|
+
return match ? match[1].toLowerCase() : null;
|
|
5084
|
+
}
|
|
5085
|
+
function slugify(title) {
|
|
5086
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
5087
|
+
}
|
|
5088
|
+
async function resolveTask(client, identifier) {
|
|
5089
|
+
let result = await client.execute({
|
|
5090
|
+
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
5091
|
+
args: [identifier]
|
|
5092
|
+
});
|
|
5093
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
5094
|
+
result = await client.execute({
|
|
5095
|
+
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
5096
|
+
args: [`%${identifier}%`]
|
|
5097
|
+
});
|
|
5098
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
5099
|
+
if (result.rows.length > 1) {
|
|
5100
|
+
const exact = result.rows.filter(
|
|
5101
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
5102
|
+
);
|
|
5103
|
+
if (exact.length === 1) return exact[0];
|
|
5104
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
5105
|
+
const active = candidates.filter(
|
|
5106
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
5107
|
+
);
|
|
5108
|
+
if (active.length === 1) return active[0];
|
|
5109
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
5110
|
+
throw new Error(
|
|
5111
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
4604
5112
|
);
|
|
4605
|
-
if (free === null) {
|
|
4606
|
-
return {
|
|
4607
|
-
status: "failed",
|
|
4608
|
-
sessionName: employeeSessionName(employeeName, exeSession),
|
|
4609
|
-
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
4610
|
-
};
|
|
4611
|
-
}
|
|
4612
|
-
effectiveInstance = free === 0 ? void 0 : free;
|
|
4613
|
-
}
|
|
4614
|
-
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
4615
|
-
if (isEmployeeAlive(sessionName)) {
|
|
4616
|
-
const result2 = sendIntercom(sessionName);
|
|
4617
|
-
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
4618
|
-
return { status: "intercom_sent", sessionName };
|
|
4619
|
-
}
|
|
4620
|
-
if (result2 === "delivered") {
|
|
4621
|
-
return { status: "intercom_unprocessed", sessionName };
|
|
4622
|
-
}
|
|
4623
|
-
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
4624
5113
|
}
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
5114
|
+
result = await client.execute({
|
|
5115
|
+
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
5116
|
+
args: [`%${identifier}%`]
|
|
5117
|
+
});
|
|
5118
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
5119
|
+
if (result.rows.length > 1) {
|
|
5120
|
+
const active = result.rows.filter(
|
|
5121
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
5122
|
+
);
|
|
5123
|
+
if (active.length === 1) return active[0];
|
|
5124
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
5125
|
+
throw new Error(
|
|
5126
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
5127
|
+
);
|
|
4629
5128
|
}
|
|
4630
|
-
|
|
5129
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
4631
5130
|
}
|
|
4632
|
-
function
|
|
4633
|
-
const
|
|
4634
|
-
const
|
|
4635
|
-
const
|
|
4636
|
-
const
|
|
4637
|
-
const
|
|
4638
|
-
|
|
4639
|
-
|
|
5131
|
+
async function createTaskCore(input) {
|
|
5132
|
+
const client = getClient();
|
|
5133
|
+
const id = crypto7.randomUUID();
|
|
5134
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5135
|
+
const slug = slugify(input.title);
|
|
5136
|
+
const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
|
|
5137
|
+
let blockedById = null;
|
|
5138
|
+
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
5139
|
+
if (input.blockedBy) {
|
|
5140
|
+
const blocker = await resolveTask(client, input.blockedBy);
|
|
5141
|
+
blockedById = String(blocker.id);
|
|
4640
5142
|
}
|
|
4641
|
-
|
|
4642
|
-
let
|
|
4643
|
-
|
|
4644
|
-
const
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
5143
|
+
let parentTaskId = null;
|
|
5144
|
+
let parentRef = input.parentTaskId;
|
|
5145
|
+
if (!parentRef) {
|
|
5146
|
+
const extracted = extractParentFromContext(input.context);
|
|
5147
|
+
if (extracted) {
|
|
5148
|
+
parentRef = extracted;
|
|
5149
|
+
process.stderr.write(
|
|
5150
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
5151
|
+
);
|
|
4648
5152
|
}
|
|
4649
|
-
} catch {
|
|
4650
5153
|
}
|
|
4651
|
-
|
|
4652
|
-
const claudeJsonPath = path18.join(os6.homedir(), ".claude.json");
|
|
4653
|
-
let claudeJson = {};
|
|
5154
|
+
if (parentRef) {
|
|
4654
5155
|
try {
|
|
4655
|
-
|
|
4656
|
-
|
|
5156
|
+
const parent = await resolveTask(client, parentRef);
|
|
5157
|
+
parentTaskId = String(parent.id);
|
|
5158
|
+
} catch (err) {
|
|
5159
|
+
if (!input.parentTaskId) {
|
|
5160
|
+
throw new Error(
|
|
5161
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
5162
|
+
);
|
|
5163
|
+
}
|
|
5164
|
+
throw err;
|
|
4657
5165
|
}
|
|
4658
|
-
if (!claudeJson.projects) claudeJson.projects = {};
|
|
4659
|
-
const projects = claudeJson.projects;
|
|
4660
|
-
const trustDir = opts?.cwd ?? projectDir;
|
|
4661
|
-
if (!projects[trustDir]) projects[trustDir] = {};
|
|
4662
|
-
projects[trustDir].hasTrustDialogAccepted = true;
|
|
4663
|
-
writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
4664
|
-
} catch {
|
|
4665
5166
|
}
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
5167
|
+
let warning;
|
|
5168
|
+
const dupCheck = await client.execute({
|
|
5169
|
+
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
5170
|
+
args: [input.title, input.assignedTo]
|
|
5171
|
+
});
|
|
5172
|
+
if (dupCheck.rows.length > 0) {
|
|
5173
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
5174
|
+
}
|
|
5175
|
+
if (input.baseDir) {
|
|
4672
5176
|
try {
|
|
4673
|
-
|
|
5177
|
+
await mkdir4(path18.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
5178
|
+
await mkdir4(path18.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
5179
|
+
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
5180
|
+
await ensureGitignoreExe(input.baseDir);
|
|
4674
5181
|
} catch {
|
|
4675
5182
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
"update_task",
|
|
4683
|
-
"list_tasks",
|
|
4684
|
-
"get_task",
|
|
4685
|
-
"ask_team_memory",
|
|
4686
|
-
"store_behavior",
|
|
4687
|
-
"get_identity",
|
|
4688
|
-
"send_message"
|
|
4689
|
-
];
|
|
4690
|
-
const requiredTools = expandDualPrefixTools(toolNames);
|
|
4691
|
-
let changed = false;
|
|
4692
|
-
for (const tool of requiredTools) {
|
|
4693
|
-
if (!allow.includes(tool)) {
|
|
4694
|
-
allow.push(tool);
|
|
4695
|
-
changed = true;
|
|
4696
|
-
}
|
|
4697
|
-
}
|
|
4698
|
-
if (changed) {
|
|
4699
|
-
perms.allow = allow;
|
|
4700
|
-
settings.permissions = perms;
|
|
4701
|
-
mkdirSync6(projSettingsDir, { recursive: true });
|
|
4702
|
-
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
4703
|
-
}
|
|
5183
|
+
}
|
|
5184
|
+
const complexity = input.complexity ?? "standard";
|
|
5185
|
+
let sessionScope = null;
|
|
5186
|
+
try {
|
|
5187
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
5188
|
+
sessionScope = resolveExeSession2();
|
|
4704
5189
|
} catch {
|
|
4705
5190
|
}
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
5191
|
+
await client.execute({
|
|
5192
|
+
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)
|
|
5193
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5194
|
+
args: [
|
|
5195
|
+
id,
|
|
5196
|
+
input.title,
|
|
5197
|
+
input.assignedTo,
|
|
5198
|
+
input.assignedBy,
|
|
5199
|
+
input.projectName,
|
|
5200
|
+
input.priority,
|
|
5201
|
+
initialStatus,
|
|
5202
|
+
taskFile,
|
|
5203
|
+
blockedById,
|
|
5204
|
+
parentTaskId,
|
|
5205
|
+
input.reviewer ?? null,
|
|
5206
|
+
input.context,
|
|
5207
|
+
complexity,
|
|
5208
|
+
input.budgetTokens ?? null,
|
|
5209
|
+
input.budgetFallbackModel ?? null,
|
|
5210
|
+
0,
|
|
5211
|
+
null,
|
|
5212
|
+
sessionScope,
|
|
5213
|
+
now,
|
|
5214
|
+
now
|
|
5215
|
+
]
|
|
5216
|
+
});
|
|
5217
|
+
return {
|
|
5218
|
+
id,
|
|
5219
|
+
title: input.title,
|
|
5220
|
+
assignedTo: input.assignedTo,
|
|
5221
|
+
assignedBy: input.assignedBy,
|
|
5222
|
+
projectName: input.projectName,
|
|
5223
|
+
priority: input.priority,
|
|
5224
|
+
status: initialStatus,
|
|
5225
|
+
taskFile,
|
|
5226
|
+
createdAt: now,
|
|
5227
|
+
updatedAt: now,
|
|
5228
|
+
warning,
|
|
5229
|
+
budgetTokens: input.budgetTokens ?? null,
|
|
5230
|
+
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
5231
|
+
tokensUsed: 0,
|
|
5232
|
+
tokensWarnedAt: null
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
async function listTasks(input) {
|
|
5236
|
+
const client = getClient();
|
|
5237
|
+
const conditions = [];
|
|
5238
|
+
const args = [];
|
|
5239
|
+
if (input.assignedTo) {
|
|
5240
|
+
conditions.push("assigned_to = ?");
|
|
5241
|
+
args.push(input.assignedTo);
|
|
5242
|
+
}
|
|
5243
|
+
if (input.status) {
|
|
5244
|
+
conditions.push("status = ?");
|
|
5245
|
+
args.push(input.status);
|
|
5246
|
+
} else {
|
|
5247
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
5248
|
+
}
|
|
5249
|
+
if (input.projectName) {
|
|
5250
|
+
conditions.push("project_name = ?");
|
|
5251
|
+
args.push(input.projectName);
|
|
4736
5252
|
}
|
|
4737
|
-
if (
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
`
|
|
4741
|
-
);
|
|
5253
|
+
if (input.priority) {
|
|
5254
|
+
conditions.push("priority = ?");
|
|
5255
|
+
args.push(input.priority);
|
|
4742
5256
|
}
|
|
4743
|
-
let sessionContextFlag = "";
|
|
4744
5257
|
try {
|
|
4745
|
-
const
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
`Your parent exe session is ${exeSession}.`,
|
|
4752
|
-
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
4753
|
-
].join("\n");
|
|
4754
|
-
writeFileSync7(ctxFile, ctxContent);
|
|
4755
|
-
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
5258
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
5259
|
+
const session = resolveExeSession2();
|
|
5260
|
+
if (session) {
|
|
5261
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
5262
|
+
args.push(session);
|
|
5263
|
+
}
|
|
4756
5264
|
} catch {
|
|
4757
5265
|
}
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
5266
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
5267
|
+
const result = await client.execute({
|
|
5268
|
+
sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC LIMIT 1000`,
|
|
5269
|
+
args
|
|
5270
|
+
});
|
|
5271
|
+
return result.rows.map((r) => ({
|
|
5272
|
+
id: String(r.id),
|
|
5273
|
+
title: String(r.title),
|
|
5274
|
+
assignedTo: String(r.assigned_to),
|
|
5275
|
+
assignedBy: String(r.assigned_by),
|
|
5276
|
+
projectName: String(r.project_name),
|
|
5277
|
+
priority: String(r.priority),
|
|
5278
|
+
status: String(r.status),
|
|
5279
|
+
taskFile: String(r.task_file),
|
|
5280
|
+
createdAt: String(r.created_at),
|
|
5281
|
+
updatedAt: String(r.updated_at),
|
|
5282
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
5283
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
5284
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
5285
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
5286
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
5287
|
+
}));
|
|
5288
|
+
}
|
|
5289
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
5290
|
+
if (!taskContext) return null;
|
|
5291
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
5292
|
+
try {
|
|
5293
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
5294
|
+
const branch = execSync8(
|
|
5295
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
5296
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
5297
|
+
).trim();
|
|
5298
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
5299
|
+
const commitCount = execSync8(
|
|
5300
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
5301
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
5302
|
+
).trim();
|
|
5303
|
+
const count = parseInt(commitCount, 10);
|
|
5304
|
+
if (count === 0) {
|
|
5305
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
4766
5306
|
}
|
|
5307
|
+
return null;
|
|
5308
|
+
} catch {
|
|
5309
|
+
return null;
|
|
4767
5310
|
}
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
5311
|
+
}
|
|
5312
|
+
async function updateTaskStatus(input) {
|
|
5313
|
+
const client = getClient();
|
|
5314
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5315
|
+
const row = await resolveTask(client, input.taskId);
|
|
5316
|
+
const taskId = String(row.id);
|
|
5317
|
+
const taskFile = String(row.task_file);
|
|
5318
|
+
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
4773
5319
|
process.stderr.write(
|
|
4774
|
-
`[
|
|
5320
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
4775
5321
|
`
|
|
4776
5322
|
);
|
|
4777
|
-
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
4778
|
-
} else {
|
|
4779
|
-
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
4780
|
-
}
|
|
4781
|
-
const spawnResult = transport.spawn(sessionName, {
|
|
4782
|
-
cwd: spawnCwd,
|
|
4783
|
-
command: spawnCommand
|
|
4784
|
-
});
|
|
4785
|
-
if (spawnResult.error) {
|
|
4786
|
-
releaseSpawnLock2(sessionName);
|
|
4787
|
-
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
4788
5323
|
}
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
5324
|
+
if (input.status === "done") {
|
|
5325
|
+
const existingRow = await client.execute({
|
|
5326
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
5327
|
+
args: [taskId]
|
|
5328
|
+
});
|
|
5329
|
+
if (existingRow.rows.length > 0) {
|
|
5330
|
+
const ctx = existingRow.rows[0];
|
|
5331
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
5332
|
+
if (warning) {
|
|
5333
|
+
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
5334
|
+
|
|
5335
|
+
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
5336
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
5337
|
+
`);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
4800
5340
|
}
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
5341
|
+
if (input.status === "in_progress") {
|
|
5342
|
+
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
5343
|
+
const claim = await client.execute({
|
|
5344
|
+
sql: `UPDATE tasks
|
|
5345
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
5346
|
+
WHERE id = ? AND status = 'open'`,
|
|
5347
|
+
args: [tmuxSession, now, taskId]
|
|
5348
|
+
});
|
|
5349
|
+
if (claim.rowsAffected === 0) {
|
|
5350
|
+
const current = await client.execute({
|
|
5351
|
+
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
5352
|
+
args: [taskId]
|
|
5353
|
+
});
|
|
5354
|
+
const cur = current.rows[0];
|
|
5355
|
+
const status = cur?.status ?? "unknown";
|
|
5356
|
+
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
5357
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
4806
5358
|
}
|
|
4807
5359
|
try {
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
}
|
|
4814
|
-
} else {
|
|
4815
|
-
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
4816
|
-
booted = true;
|
|
4817
|
-
break;
|
|
4818
|
-
}
|
|
4819
|
-
}
|
|
5360
|
+
await writeCheckpoint({
|
|
5361
|
+
taskId,
|
|
5362
|
+
step: "claimed",
|
|
5363
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
5364
|
+
});
|
|
4820
5365
|
} catch {
|
|
4821
5366
|
}
|
|
5367
|
+
return { row, taskFile, now, taskId };
|
|
4822
5368
|
}
|
|
4823
|
-
if (
|
|
4824
|
-
|
|
4825
|
-
|
|
5369
|
+
if (input.result) {
|
|
5370
|
+
await client.execute({
|
|
5371
|
+
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
5372
|
+
args: [input.status, input.result, now, taskId]
|
|
5373
|
+
});
|
|
5374
|
+
} else {
|
|
5375
|
+
await client.execute({
|
|
5376
|
+
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
5377
|
+
args: [input.status, now, taskId]
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
5380
|
+
try {
|
|
5381
|
+
await writeCheckpoint({
|
|
5382
|
+
taskId,
|
|
5383
|
+
step: `status_transition:${input.status}`,
|
|
5384
|
+
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
5385
|
+
});
|
|
5386
|
+
} catch {
|
|
5387
|
+
}
|
|
5388
|
+
return { row, taskFile, now, taskId };
|
|
5389
|
+
}
|
|
5390
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
5391
|
+
const client = getClient();
|
|
5392
|
+
const row = await resolveTask(client, taskId);
|
|
5393
|
+
const id = String(row.id);
|
|
5394
|
+
const taskFile = String(row.task_file);
|
|
5395
|
+
const assignedTo = String(row.assigned_to);
|
|
5396
|
+
const assignedBy = String(row.assigned_by);
|
|
5397
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
5398
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
5399
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
5400
|
+
}
|
|
5401
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
5402
|
+
const archPath = path18.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
5403
|
+
try {
|
|
5404
|
+
if (existsSync14(archPath)) return;
|
|
5405
|
+
const template = [
|
|
5406
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
5407
|
+
"",
|
|
5408
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
5409
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
5410
|
+
"",
|
|
5411
|
+
"## Overview",
|
|
5412
|
+
"",
|
|
5413
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
5414
|
+
"",
|
|
5415
|
+
"## Key Components",
|
|
5416
|
+
"",
|
|
5417
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
5418
|
+
"",
|
|
5419
|
+
"## Data Flow",
|
|
5420
|
+
"",
|
|
5421
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
5422
|
+
"",
|
|
5423
|
+
"## Invariants",
|
|
5424
|
+
"",
|
|
5425
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
5426
|
+
"",
|
|
5427
|
+
"## Dependencies",
|
|
5428
|
+
"",
|
|
5429
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
5430
|
+
""
|
|
5431
|
+
].join("\n");
|
|
5432
|
+
await writeFile4(archPath, template, "utf-8");
|
|
5433
|
+
} catch {
|
|
4826
5434
|
}
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
5435
|
+
}
|
|
5436
|
+
async function ensureGitignoreExe(baseDir) {
|
|
5437
|
+
const gitignorePath = path18.join(baseDir, ".gitignore");
|
|
5438
|
+
try {
|
|
5439
|
+
if (existsSync14(gitignorePath)) {
|
|
5440
|
+
const content = readFileSync12(gitignorePath, "utf-8");
|
|
5441
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
5442
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
5443
|
+
} else {
|
|
5444
|
+
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
4831
5445
|
}
|
|
5446
|
+
} catch {
|
|
4832
5447
|
}
|
|
4833
|
-
registerSession({
|
|
4834
|
-
windowName: sessionName,
|
|
4835
|
-
agentId: employeeName,
|
|
4836
|
-
projectDir: spawnCwd,
|
|
4837
|
-
parentExe: exeSession,
|
|
4838
|
-
pid: 0,
|
|
4839
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4840
|
-
});
|
|
4841
|
-
releaseSpawnLock2(sessionName);
|
|
4842
|
-
return { sessionName };
|
|
4843
5448
|
}
|
|
4844
|
-
var
|
|
4845
|
-
var
|
|
4846
|
-
"src/lib/
|
|
5449
|
+
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
5450
|
+
var init_tasks_crud = __esm({
|
|
5451
|
+
"src/lib/tasks-crud.ts"() {
|
|
4847
5452
|
"use strict";
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
init_cc_agent_support();
|
|
4852
|
-
init_mcp_prefix();
|
|
4853
|
-
init_provider_table();
|
|
4854
|
-
init_intercom_queue();
|
|
4855
|
-
init_plan_limits();
|
|
4856
|
-
SPAWN_LOCK_DIR = path18.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
4857
|
-
SESSION_CACHE = path18.join(os6.homedir(), ".exe-os", "session-cache");
|
|
4858
|
-
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
4859
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
4860
|
-
INTERCOM_LOG2 = path18.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
4861
|
-
DEBOUNCE_FILE = path18.join(SESSION_CACHE, "intercom-debounce.json");
|
|
4862
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
4863
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
5453
|
+
init_database();
|
|
5454
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
5455
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
4864
5456
|
}
|
|
4865
5457
|
});
|
|
4866
5458
|
|
|
@@ -4894,6 +5486,34 @@ async function listPendingReviews(limit) {
|
|
|
4894
5486
|
});
|
|
4895
5487
|
return result.rows;
|
|
4896
5488
|
}
|
|
5489
|
+
async function cleanupOrphanedReviews() {
|
|
5490
|
+
const client = getClient();
|
|
5491
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5492
|
+
const r1 = await client.execute({
|
|
5493
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
5494
|
+
WHERE status = 'needs_review'
|
|
5495
|
+
AND assigned_by = 'system'
|
|
5496
|
+
AND title LIKE 'Review:%'
|
|
5497
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
|
|
5498
|
+
args: [now]
|
|
5499
|
+
});
|
|
5500
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
5501
|
+
const r2 = await client.execute({
|
|
5502
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
5503
|
+
WHERE status = 'needs_review'
|
|
5504
|
+
AND result IS NOT NULL
|
|
5505
|
+
AND updated_at < ?`,
|
|
5506
|
+
args: [now, staleThreshold]
|
|
5507
|
+
});
|
|
5508
|
+
const total = r1.rowsAffected + r2.rowsAffected;
|
|
5509
|
+
if (total > 0) {
|
|
5510
|
+
process.stderr.write(
|
|
5511
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
|
|
5512
|
+
`
|
|
5513
|
+
);
|
|
5514
|
+
}
|
|
5515
|
+
return total;
|
|
5516
|
+
}
|
|
4897
5517
|
function getReviewChecklist(role, agent, taskSlug) {
|
|
4898
5518
|
const roleLower = role.toLowerCase();
|
|
4899
5519
|
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
@@ -5007,6 +5627,7 @@ var init_tasks_review = __esm({
|
|
|
5007
5627
|
init_tasks_crud();
|
|
5008
5628
|
init_tmux_routing();
|
|
5009
5629
|
init_session_key();
|
|
5630
|
+
init_state_bus();
|
|
5010
5631
|
}
|
|
5011
5632
|
});
|
|
5012
5633
|
|
|
@@ -5129,13 +5750,12 @@ function assertSessionScope(actionType, targetProject) {
|
|
|
5129
5750
|
};
|
|
5130
5751
|
}
|
|
5131
5752
|
process.stderr.write(
|
|
5132
|
-
`[session-scope]
|
|
5753
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
5133
5754
|
`
|
|
5134
5755
|
);
|
|
5135
5756
|
return {
|
|
5136
|
-
allowed:
|
|
5137
|
-
|
|
5138
|
-
reason: "cross_session_granted",
|
|
5757
|
+
allowed: false,
|
|
5758
|
+
reason: "cross_session_denied",
|
|
5139
5759
|
currentProject,
|
|
5140
5760
|
targetProject,
|
|
5141
5761
|
targetSession: findSessionForProject(targetProject)?.windowName
|
|
@@ -5161,8 +5781,9 @@ async function dispatchTaskToEmployee(input) {
|
|
|
5161
5781
|
try {
|
|
5162
5782
|
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
5163
5783
|
const check = assertSessionScope2("dispatch_task", input.projectName);
|
|
5164
|
-
if (check.reason === "
|
|
5784
|
+
if (check.reason === "cross_session_denied") {
|
|
5165
5785
|
crossProject = true;
|
|
5786
|
+
return { dispatched: "skipped", crossProject: true };
|
|
5166
5787
|
}
|
|
5167
5788
|
} catch {
|
|
5168
5789
|
}
|
|
@@ -5219,10 +5840,10 @@ var init_tasks_notify = __esm({
|
|
|
5219
5840
|
});
|
|
5220
5841
|
|
|
5221
5842
|
// src/lib/behaviors.ts
|
|
5222
|
-
import
|
|
5843
|
+
import crypto8 from "crypto";
|
|
5223
5844
|
async function storeBehavior(opts) {
|
|
5224
5845
|
const client = getClient();
|
|
5225
|
-
const id =
|
|
5846
|
+
const id = crypto8.randomUUID();
|
|
5226
5847
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5227
5848
|
await client.execute({
|
|
5228
5849
|
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
@@ -5279,7 +5900,7 @@ __export(skill_learning_exports, {
|
|
|
5279
5900
|
storeTrajectory: () => storeTrajectory,
|
|
5280
5901
|
sweepTrajectories: () => sweepTrajectories
|
|
5281
5902
|
});
|
|
5282
|
-
import
|
|
5903
|
+
import crypto9 from "crypto";
|
|
5283
5904
|
async function extractTrajectory(taskId, agentId) {
|
|
5284
5905
|
const client = getClient();
|
|
5285
5906
|
const result = await client.execute({
|
|
@@ -5308,11 +5929,11 @@ async function extractTrajectory(taskId, agentId) {
|
|
|
5308
5929
|
return signature;
|
|
5309
5930
|
}
|
|
5310
5931
|
function hashSignature(signature) {
|
|
5311
|
-
return
|
|
5932
|
+
return crypto9.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
5312
5933
|
}
|
|
5313
5934
|
async function storeTrajectory(opts) {
|
|
5314
5935
|
const client = getClient();
|
|
5315
|
-
const id =
|
|
5936
|
+
const id = crypto9.randomUUID();
|
|
5316
5937
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5317
5938
|
const signatureHash = hashSignature(opts.signature);
|
|
5318
5939
|
await client.execute({
|
|
@@ -5559,6 +6180,7 @@ var init_skill_learning = __esm({
|
|
|
5559
6180
|
// src/lib/tasks.ts
|
|
5560
6181
|
var tasks_exports = {};
|
|
5561
6182
|
__export(tasks_exports, {
|
|
6183
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
5562
6184
|
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
5563
6185
|
countPendingReviews: () => countPendingReviews,
|
|
5564
6186
|
createTask: () => createTask,
|
|
@@ -5624,6 +6246,21 @@ async function updateTask(input) {
|
|
|
5624
6246
|
});
|
|
5625
6247
|
} catch {
|
|
5626
6248
|
}
|
|
6249
|
+
try {
|
|
6250
|
+
const client = getClient();
|
|
6251
|
+
const cascaded = await client.execute({
|
|
6252
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
6253
|
+
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
6254
|
+
args: [now, taskId]
|
|
6255
|
+
});
|
|
6256
|
+
if (cascaded.rowsAffected > 0) {
|
|
6257
|
+
process.stderr.write(
|
|
6258
|
+
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
6259
|
+
`
|
|
6260
|
+
);
|
|
6261
|
+
}
|
|
6262
|
+
} catch {
|
|
6263
|
+
}
|
|
5627
6264
|
}
|
|
5628
6265
|
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
5629
6266
|
if (isTerminal) {
|
|
@@ -5637,6 +6274,13 @@ async function updateTask(input) {
|
|
|
5637
6274
|
await cascadeUnblock(taskId, input.baseDir, now);
|
|
5638
6275
|
} catch {
|
|
5639
6276
|
}
|
|
6277
|
+
orgBus.emit({
|
|
6278
|
+
type: "task_completed",
|
|
6279
|
+
taskId,
|
|
6280
|
+
employee: String(row.assigned_to),
|
|
6281
|
+
result: input.result ?? "",
|
|
6282
|
+
timestamp: now
|
|
6283
|
+
});
|
|
5640
6284
|
if (row.parent_task_id) {
|
|
5641
6285
|
try {
|
|
5642
6286
|
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
@@ -5704,6 +6348,7 @@ var init_tasks = __esm({
|
|
|
5704
6348
|
init_database();
|
|
5705
6349
|
init_config();
|
|
5706
6350
|
init_notifications();
|
|
6351
|
+
init_state_bus();
|
|
5707
6352
|
init_tasks_crud();
|
|
5708
6353
|
init_tasks_review();
|
|
5709
6354
|
init_tasks_crud();
|
|
@@ -5713,87 +6358,6 @@ var init_tasks = __esm({
|
|
|
5713
6358
|
}
|
|
5714
6359
|
});
|
|
5715
6360
|
|
|
5716
|
-
// src/lib/session-kill-telemetry.ts
|
|
5717
|
-
import crypto9 from "crypto";
|
|
5718
|
-
var init_session_kill_telemetry = __esm({
|
|
5719
|
-
"src/lib/session-kill-telemetry.ts"() {
|
|
5720
|
-
"use strict";
|
|
5721
|
-
init_database();
|
|
5722
|
-
}
|
|
5723
|
-
});
|
|
5724
|
-
|
|
5725
|
-
// src/lib/capacity-monitor.ts
|
|
5726
|
-
function resumeTaskTitle(agentId) {
|
|
5727
|
-
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
5728
|
-
}
|
|
5729
|
-
function buildResumeContext(agentId, openTasks) {
|
|
5730
|
-
const taskList = openTasks.map(
|
|
5731
|
-
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
5732
|
-
).join("\n");
|
|
5733
|
-
return [
|
|
5734
|
-
"## Context",
|
|
5735
|
-
"",
|
|
5736
|
-
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
5737
|
-
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
5738
|
-
"",
|
|
5739
|
-
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
5740
|
-
"",
|
|
5741
|
-
taskList,
|
|
5742
|
-
"",
|
|
5743
|
-
"Read each task file and chain through them. Build and commit after each one."
|
|
5744
|
-
].join("\n");
|
|
5745
|
-
}
|
|
5746
|
-
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
5747
|
-
const client = getClient();
|
|
5748
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5749
|
-
const context = buildResumeContext(agentId, openTasks);
|
|
5750
|
-
const existing = await client.execute({
|
|
5751
|
-
sql: `SELECT id FROM tasks
|
|
5752
|
-
WHERE assigned_to = ?
|
|
5753
|
-
AND title LIKE ?
|
|
5754
|
-
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
|
|
5755
|
-
ORDER BY created_at DESC
|
|
5756
|
-
LIMIT 1`,
|
|
5757
|
-
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
5758
|
-
});
|
|
5759
|
-
if (existing.rows.length > 0) {
|
|
5760
|
-
const taskId = String(existing.rows[0].id);
|
|
5761
|
-
await client.execute({
|
|
5762
|
-
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
5763
|
-
args: [context, now, taskId]
|
|
5764
|
-
});
|
|
5765
|
-
return { created: false, taskId };
|
|
5766
|
-
}
|
|
5767
|
-
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
5768
|
-
const task = await createTask2({
|
|
5769
|
-
title: resumeTaskTitle(agentId),
|
|
5770
|
-
assignedTo: agentId,
|
|
5771
|
-
assignedBy: "system",
|
|
5772
|
-
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
5773
|
-
priority: "p0",
|
|
5774
|
-
context,
|
|
5775
|
-
baseDir: projectDir
|
|
5776
|
-
});
|
|
5777
|
-
return { created: true, taskId: task.id };
|
|
5778
|
-
}
|
|
5779
|
-
var RELAUNCH_COOLDOWN_MS, RESUME_TITLE_PREFIX, RESUME_TITLE_LIKE_PATTERN, RESUME_ACTIVE_STATUSES, CONFIRMATION_WINDOW_MS;
|
|
5780
|
-
var init_capacity_monitor = __esm({
|
|
5781
|
-
"src/lib/capacity-monitor.ts"() {
|
|
5782
|
-
"use strict";
|
|
5783
|
-
init_session_registry();
|
|
5784
|
-
init_transport();
|
|
5785
|
-
init_notifications();
|
|
5786
|
-
init_database();
|
|
5787
|
-
init_session_kill_telemetry();
|
|
5788
|
-
init_tmux_routing();
|
|
5789
|
-
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
5790
|
-
RESUME_TITLE_PREFIX = "RESUME:";
|
|
5791
|
-
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
5792
|
-
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
5793
|
-
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
5794
|
-
}
|
|
5795
|
-
});
|
|
5796
|
-
|
|
5797
6361
|
// src/lib/messaging.ts
|
|
5798
6362
|
import crypto10 from "crypto";
|
|
5799
6363
|
function generateUlid() {
|
|
@@ -5994,7 +6558,7 @@ __export(consolidation_exports, {
|
|
|
5994
6558
|
selectUnconsolidated: () => selectUnconsolidated,
|
|
5995
6559
|
storeConsolidation: () => storeConsolidation
|
|
5996
6560
|
});
|
|
5997
|
-
import { randomUUID as
|
|
6561
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
5998
6562
|
async function selectUnconsolidated(client, limit = 200) {
|
|
5999
6563
|
const result = await client.execute({
|
|
6000
6564
|
sql: `SELECT id, agent_id, project_name, tool_name, raw_text, timestamp
|
|
@@ -6090,7 +6654,7 @@ async function consolidateCluster(cluster, model) {
|
|
|
6090
6654
|
return textBlock?.text ?? "";
|
|
6091
6655
|
}
|
|
6092
6656
|
async function storeConsolidation(client, cluster, synthesisText, embedFn) {
|
|
6093
|
-
const consolidatedId =
|
|
6657
|
+
const consolidatedId = randomUUID5();
|
|
6094
6658
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6095
6659
|
const rawText = `CONSOLIDATION [${cluster.dateRange}, ${cluster.projectName}]:
|
|
6096
6660
|
|
|
@@ -6115,7 +6679,7 @@ ${synthesisText}`;
|
|
|
6115
6679
|
const linkStmts = sourceIds.map((sourceId) => ({
|
|
6116
6680
|
sql: `INSERT INTO consolidations (id, consolidated_memory_id, source_memory_id, created_at)
|
|
6117
6681
|
VALUES (?, ?, ?, ?)`,
|
|
6118
|
-
args: [
|
|
6682
|
+
args: [randomUUID5(), consolidatedId, sourceId, now]
|
|
6119
6683
|
}));
|
|
6120
6684
|
const placeholders = sourceIds.map(() => "?").join(",");
|
|
6121
6685
|
const markStmt = {
|
|
@@ -9340,7 +9904,7 @@ import { z as z29 } from "zod";
|
|
|
9340
9904
|
|
|
9341
9905
|
// src/automation/trigger-engine.ts
|
|
9342
9906
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync10, existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
|
|
9343
|
-
import { randomUUID as
|
|
9907
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
9344
9908
|
import path23 from "path";
|
|
9345
9909
|
import os7 from "os";
|
|
9346
9910
|
var TRIGGERS_PATH = path23.join(os7.homedir(), ".exe-os", "triggers.json");
|
|
@@ -9366,7 +9930,7 @@ function saveTriggers(triggers) {
|
|
|
9366
9930
|
function createNewTrigger(input) {
|
|
9367
9931
|
const triggers = loadTriggers();
|
|
9368
9932
|
const trigger = {
|
|
9369
|
-
id:
|
|
9933
|
+
id: randomUUID4().slice(0, 8),
|
|
9370
9934
|
...input
|
|
9371
9935
|
};
|
|
9372
9936
|
triggers.push(trigger);
|
|
@@ -9767,6 +10331,7 @@ import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as
|
|
|
9767
10331
|
import path25 from "path";
|
|
9768
10332
|
|
|
9769
10333
|
// src/lib/employee-templates.ts
|
|
10334
|
+
init_global_procedures();
|
|
9770
10335
|
var CLIENT_COO_TEMPLATE = `---
|
|
9771
10336
|
role: client-coo
|
|
9772
10337
|
title: Chief Operating Officer
|
|
@@ -11335,6 +11900,157 @@ Consolidated summaries stored as tier-1 (importance=9) memories.`
|
|
|
11335
11900
|
);
|
|
11336
11901
|
}
|
|
11337
11902
|
|
|
11903
|
+
// src/mcp/tools/store-global-procedure.ts
|
|
11904
|
+
init_global_procedures();
|
|
11905
|
+
init_active_agent();
|
|
11906
|
+
import { z as z41 } from "zod";
|
|
11907
|
+
function registerStoreGlobalProcedure(server2) {
|
|
11908
|
+
server2.registerTool(
|
|
11909
|
+
"store_global_procedure",
|
|
11910
|
+
{
|
|
11911
|
+
title: "Store Global Procedure",
|
|
11912
|
+
description: "Create an organization-wide procedure (Layer 0) that supersedes identity, expertise, and experience. Use for hard rules that every employee must follow. RESTRICTED: only exe or founder sessions.",
|
|
11913
|
+
inputSchema: {
|
|
11914
|
+
title: z41.string().describe("Short title for the procedure"),
|
|
11915
|
+
content: z41.string().max(500).describe("The procedure content \u2014 clear, actionable instruction"),
|
|
11916
|
+
priority: z41.enum(["p0", "p1", "p2"]).optional().describe("Priority tier. p0 = always (default). p1 = standard. p2 = nice-to-have."),
|
|
11917
|
+
domain: z41.string().optional().describe("Category: workflow, code-style, communication, architecture, testing, security")
|
|
11918
|
+
}
|
|
11919
|
+
},
|
|
11920
|
+
async ({ title, content, priority, domain }) => {
|
|
11921
|
+
const caller = getActiveAgent();
|
|
11922
|
+
const allowed = caller.agentId === "default" || caller.agentRole === "COO";
|
|
11923
|
+
if (!allowed) {
|
|
11924
|
+
return {
|
|
11925
|
+
content: [{
|
|
11926
|
+
type: "text",
|
|
11927
|
+
text: `Permission denied. Only exe or founder sessions can create global procedures. You are "${caller.agentId}".`
|
|
11928
|
+
}],
|
|
11929
|
+
isError: true
|
|
11930
|
+
};
|
|
11931
|
+
}
|
|
11932
|
+
const id = await storeGlobalProcedure({
|
|
11933
|
+
title,
|
|
11934
|
+
content,
|
|
11935
|
+
priority: priority ?? "p0",
|
|
11936
|
+
domain: domain ?? void 0
|
|
11937
|
+
});
|
|
11938
|
+
return {
|
|
11939
|
+
content: [{
|
|
11940
|
+
type: "text",
|
|
11941
|
+
text: `Global procedure stored.
|
|
11942
|
+
ID: ${id}
|
|
11943
|
+
Title: ${title}
|
|
11944
|
+
Priority: ${priority ?? "p0"}
|
|
11945
|
+
Domain: ${domain ?? "none"}`
|
|
11946
|
+
}]
|
|
11947
|
+
};
|
|
11948
|
+
}
|
|
11949
|
+
);
|
|
11950
|
+
}
|
|
11951
|
+
|
|
11952
|
+
// src/mcp/tools/list-global-procedures.ts
|
|
11953
|
+
init_global_procedures();
|
|
11954
|
+
function registerListGlobalProcedures(server2) {
|
|
11955
|
+
server2.registerTool(
|
|
11956
|
+
"list_global_procedures",
|
|
11957
|
+
{
|
|
11958
|
+
title: "List Global Procedures",
|
|
11959
|
+
description: "List all active organization-wide procedures (Layer 0). These supersede identity, expertise, and experience.",
|
|
11960
|
+
inputSchema: {}
|
|
11961
|
+
},
|
|
11962
|
+
async () => {
|
|
11963
|
+
const procedures = await loadGlobalProcedures();
|
|
11964
|
+
if (procedures.length === 0) {
|
|
11965
|
+
return {
|
|
11966
|
+
content: [{
|
|
11967
|
+
type: "text",
|
|
11968
|
+
text: "No active global procedures."
|
|
11969
|
+
}]
|
|
11970
|
+
};
|
|
11971
|
+
}
|
|
11972
|
+
const lines = procedures.map(
|
|
11973
|
+
(p) => `[${p.id}] (${p.priority}) ${p.title}${p.domain ? ` [${p.domain}]` : ""}
|
|
11974
|
+
${p.content}`
|
|
11975
|
+
);
|
|
11976
|
+
return {
|
|
11977
|
+
content: [{
|
|
11978
|
+
type: "text",
|
|
11979
|
+
text: `Active global procedures (${procedures.length}):
|
|
11980
|
+
|
|
11981
|
+
${lines.join("\n\n")}`
|
|
11982
|
+
}]
|
|
11983
|
+
};
|
|
11984
|
+
}
|
|
11985
|
+
);
|
|
11986
|
+
}
|
|
11987
|
+
|
|
11988
|
+
// src/mcp/tools/deactivate-global-procedure.ts
|
|
11989
|
+
init_global_procedures();
|
|
11990
|
+
init_active_agent();
|
|
11991
|
+
init_database();
|
|
11992
|
+
import { z as z42 } from "zod";
|
|
11993
|
+
function registerDeactivateGlobalProcedure(server2) {
|
|
11994
|
+
server2.registerTool(
|
|
11995
|
+
"deactivate_global_procedure",
|
|
11996
|
+
{
|
|
11997
|
+
title: "Deactivate Global Procedure",
|
|
11998
|
+
description: "Soft-delete a global procedure by setting active = 0. RESTRICTED: only exe or founder sessions. Use list_global_procedures to find the procedure ID first.",
|
|
11999
|
+
inputSchema: {
|
|
12000
|
+
procedure_id: z42.string().describe("UUID of the global procedure to deactivate")
|
|
12001
|
+
}
|
|
12002
|
+
},
|
|
12003
|
+
async ({ procedure_id }) => {
|
|
12004
|
+
const caller = getActiveAgent();
|
|
12005
|
+
const allowed = caller.agentId === "default" || caller.agentRole === "COO";
|
|
12006
|
+
if (!allowed) {
|
|
12007
|
+
return {
|
|
12008
|
+
content: [{
|
|
12009
|
+
type: "text",
|
|
12010
|
+
text: `Permission denied. Only exe or founder sessions can deactivate global procedures. You are "${caller.agentId}".`
|
|
12011
|
+
}],
|
|
12012
|
+
isError: true
|
|
12013
|
+
};
|
|
12014
|
+
}
|
|
12015
|
+
const client = getClient();
|
|
12016
|
+
const result = await client.execute({
|
|
12017
|
+
sql: "SELECT id, title, content, priority, domain FROM global_procedures WHERE id = ?",
|
|
12018
|
+
args: [procedure_id]
|
|
12019
|
+
});
|
|
12020
|
+
if (result.rows.length === 0) {
|
|
12021
|
+
return {
|
|
12022
|
+
content: [{
|
|
12023
|
+
type: "text",
|
|
12024
|
+
text: `Global procedure not found: ${procedure_id}`
|
|
12025
|
+
}],
|
|
12026
|
+
isError: true
|
|
12027
|
+
};
|
|
12028
|
+
}
|
|
12029
|
+
const row = result.rows[0];
|
|
12030
|
+
const wasActive = await deactivateGlobalProcedure(procedure_id);
|
|
12031
|
+
if (!wasActive) {
|
|
12032
|
+
return {
|
|
12033
|
+
content: [{
|
|
12034
|
+
type: "text",
|
|
12035
|
+
text: `Global procedure ${procedure_id} was already inactive.`
|
|
12036
|
+
}]
|
|
12037
|
+
};
|
|
12038
|
+
}
|
|
12039
|
+
return {
|
|
12040
|
+
content: [{
|
|
12041
|
+
type: "text",
|
|
12042
|
+
text: `Deactivated global procedure.
|
|
12043
|
+
ID: ${row.id}
|
|
12044
|
+
Title: ${row.title}
|
|
12045
|
+
Priority: ${row.priority}
|
|
12046
|
+
Domain: ${row.domain ?? "none"}
|
|
12047
|
+
Content: ${row.content}`
|
|
12048
|
+
}]
|
|
12049
|
+
};
|
|
12050
|
+
}
|
|
12051
|
+
);
|
|
12052
|
+
}
|
|
12053
|
+
|
|
11338
12054
|
// src/lib/telemetry.ts
|
|
11339
12055
|
var ENABLED = process.env.EXE_TELEMETRY === "1";
|
|
11340
12056
|
var initialized = false;
|
|
@@ -11439,6 +12155,9 @@ registerDeployClient(server);
|
|
|
11439
12155
|
registerQueryConversations(server);
|
|
11440
12156
|
registerLoadSkill(server);
|
|
11441
12157
|
registerConsolidateMemories(server);
|
|
12158
|
+
registerStoreGlobalProcedure(server);
|
|
12159
|
+
registerListGlobalProcedures(server);
|
|
12160
|
+
registerDeactivateGlobalProcedure(server);
|
|
11442
12161
|
try {
|
|
11443
12162
|
await initStore();
|
|
11444
12163
|
process.stderr.write("[exe-os] MCP server starting...\n");
|