@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.
Files changed (78) hide show
  1. package/dist/bin/backfill-conversations.js +805 -642
  2. package/dist/bin/backfill-responses.js +804 -641
  3. package/dist/bin/backfill-vectors.js +791 -634
  4. package/dist/bin/cleanup-stale-review-tasks.js +788 -631
  5. package/dist/bin/cli.js +1376 -659
  6. package/dist/bin/exe-agent.js +20 -1
  7. package/dist/bin/exe-assign.js +1503 -1343
  8. package/dist/bin/exe-boot.js +2549 -1784
  9. package/dist/bin/exe-call.js +39 -1
  10. package/dist/bin/exe-cloud.js +12 -2
  11. package/dist/bin/exe-dispatch.js +39 -2
  12. package/dist/bin/exe-doctor.js +791 -634
  13. package/dist/bin/exe-export-behaviors.js +792 -637
  14. package/dist/bin/exe-forget.js +145 -0
  15. package/dist/bin/exe-gateway.js +2501 -1846
  16. package/dist/bin/exe-heartbeat.js +147 -1
  17. package/dist/bin/exe-kill.js +795 -640
  18. package/dist/bin/exe-launch-agent.js +2168 -2008
  19. package/dist/bin/exe-link.js +44 -12
  20. package/dist/bin/exe-new-employee.js +6 -2
  21. package/dist/bin/exe-pending-messages.js +146 -1
  22. package/dist/bin/exe-pending-notifications.js +788 -631
  23. package/dist/bin/exe-pending-reviews.js +176 -1
  24. package/dist/bin/exe-rename.js +23 -0
  25. package/dist/bin/exe-review.js +490 -327
  26. package/dist/bin/exe-search.js +157 -4
  27. package/dist/bin/exe-session-cleanup.js +2487 -403
  28. package/dist/bin/exe-settings.js +2 -1
  29. package/dist/bin/exe-status.js +474 -317
  30. package/dist/bin/exe-team.js +474 -317
  31. package/dist/bin/git-sweep.js +2691 -151
  32. package/dist/bin/graph-backfill.js +794 -637
  33. package/dist/bin/graph-export.js +798 -641
  34. package/dist/bin/scan-tasks.js +2951 -44
  35. package/dist/bin/setup.js +50 -26
  36. package/dist/bin/shard-migrate.js +792 -637
  37. package/dist/bin/wiki-sync.js +794 -637
  38. package/dist/gateway/index.js +2542 -1887
  39. package/dist/hooks/bug-report-worker.js +2118 -576
  40. package/dist/hooks/commit-complete.js +2690 -150
  41. package/dist/hooks/error-recall.js +157 -4
  42. package/dist/hooks/ingest-worker.js +1455 -803
  43. package/dist/hooks/instructions-loaded.js +151 -0
  44. package/dist/hooks/notification.js +153 -2
  45. package/dist/hooks/post-compact.js +164 -0
  46. package/dist/hooks/pre-compact.js +3073 -101
  47. package/dist/hooks/pre-tool-use.js +151 -0
  48. package/dist/hooks/prompt-ingest-worker.js +1670 -1509
  49. package/dist/hooks/prompt-submit.js +2650 -1074
  50. package/dist/hooks/response-ingest-worker.js +154 -6
  51. package/dist/hooks/session-end.js +153 -2
  52. package/dist/hooks/session-start.js +157 -4
  53. package/dist/hooks/stop.js +151 -0
  54. package/dist/hooks/subagent-stop.js +155 -2
  55. package/dist/hooks/summary-worker.js +190 -21
  56. package/dist/index.js +326 -102
  57. package/dist/lib/cloud-sync.js +31 -10
  58. package/dist/lib/config.js +2 -0
  59. package/dist/lib/consolidation.js +69 -2
  60. package/dist/lib/database.js +19 -0
  61. package/dist/lib/device-registry.js +19 -0
  62. package/dist/lib/embedder.js +3 -1
  63. package/dist/lib/employee-templates.js +20 -1
  64. package/dist/lib/exe-daemon.js +285 -18
  65. package/dist/lib/hybrid-search.js +157 -4
  66. package/dist/lib/messaging.js +39 -2
  67. package/dist/lib/schedules.js +792 -637
  68. package/dist/lib/store.js +796 -636
  69. package/dist/lib/tasks.js +1485 -918
  70. package/dist/lib/tmux-routing.js +194 -10
  71. package/dist/mcp/server.js +1643 -924
  72. package/dist/mcp/tools/create-task.js +2283 -829
  73. package/dist/mcp/tools/list-tasks.js +2788 -159
  74. package/dist/mcp/tools/send-message.js +39 -2
  75. package/dist/mcp/tools/update-task.js +79 -0
  76. package/dist/runtime/index.js +280 -68
  77. package/dist/tui/App.js +1485 -645
  78. package/package.json +3 -2
@@ -19,6 +19,61 @@ var __copyProps = (to, from, except, desc) => {
19
19
  };
20
20
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
21
 
22
+ // src/lib/state-bus.ts
23
+ var StateBus, orgBus;
24
+ var init_state_bus = __esm({
25
+ "src/lib/state-bus.ts"() {
26
+ "use strict";
27
+ StateBus = class {
28
+ handlers = /* @__PURE__ */ new Map();
29
+ globalHandlers = /* @__PURE__ */ new Set();
30
+ /** Emit an event to all subscribers */
31
+ emit(event) {
32
+ const typeHandlers = this.handlers.get(event.type);
33
+ if (typeHandlers) {
34
+ for (const handler of typeHandlers) {
35
+ try {
36
+ handler(event);
37
+ } catch {
38
+ }
39
+ }
40
+ }
41
+ for (const handler of this.globalHandlers) {
42
+ try {
43
+ handler(event);
44
+ } catch {
45
+ }
46
+ }
47
+ }
48
+ /** Subscribe to a specific event type */
49
+ on(type, handler) {
50
+ if (!this.handlers.has(type)) {
51
+ this.handlers.set(type, /* @__PURE__ */ new Set());
52
+ }
53
+ this.handlers.get(type).add(handler);
54
+ }
55
+ /** Subscribe to ALL events */
56
+ onAny(handler) {
57
+ this.globalHandlers.add(handler);
58
+ }
59
+ /** Unsubscribe from a specific event type */
60
+ off(type, handler) {
61
+ this.handlers.get(type)?.delete(handler);
62
+ }
63
+ /** Unsubscribe from ALL events */
64
+ offAny(handler) {
65
+ this.globalHandlers.delete(handler);
66
+ }
67
+ /** Remove all listeners */
68
+ clear() {
69
+ this.handlers.clear();
70
+ this.globalHandlers.clear();
71
+ }
72
+ };
73
+ orgBus = new StateBus();
74
+ }
75
+ });
76
+
22
77
  // src/gateway/crm-bridge.ts
23
78
  var crm_bridge_exports = {};
24
79
  __export(crm_bridge_exports, {
@@ -568,6 +623,13 @@ async function ensureSchema() {
568
623
  });
569
624
  } catch {
570
625
  }
626
+ try {
627
+ await client.execute({
628
+ sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
629
+ args: []
630
+ });
631
+ } catch {
632
+ }
571
633
  try {
572
634
  await client.execute({
573
635
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -1014,6 +1076,18 @@ async function ensureSchema() {
1014
1076
  CREATE INDEX IF NOT EXISTS idx_session_kills_agent
1015
1077
  ON session_kills(agent_id);
1016
1078
  `);
1079
+ await client.execute(`
1080
+ CREATE TABLE IF NOT EXISTS global_procedures (
1081
+ id TEXT PRIMARY KEY,
1082
+ title TEXT NOT NULL,
1083
+ content TEXT NOT NULL,
1084
+ priority TEXT NOT NULL DEFAULT 'p0',
1085
+ domain TEXT,
1086
+ active INTEGER NOT NULL DEFAULT 1,
1087
+ created_at TEXT NOT NULL,
1088
+ updated_at TEXT NOT NULL
1089
+ )
1090
+ `);
1017
1091
  await client.executeMultiple(`
1018
1092
  CREATE TABLE IF NOT EXISTS conversations (
1019
1093
  id TEXT PRIMARY KEY,
@@ -1185,6 +1259,7 @@ var config_exports = {};
1185
1259
  __export(config_exports, {
1186
1260
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
1187
1261
  CONFIG_PATH: () => CONFIG_PATH,
1262
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
1188
1263
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
1189
1264
  DB_PATH: () => DB_PATH,
1190
1265
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -1340,7 +1415,7 @@ async function loadConfigFrom(configPath) {
1340
1415
  return { ...DEFAULT_CONFIG };
1341
1416
  }
1342
1417
  }
1343
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1418
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1344
1419
  var init_config = __esm({
1345
1420
  "src/lib/config.ts"() {
1346
1421
  "use strict";
@@ -1348,6 +1423,7 @@ var init_config = __esm({
1348
1423
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
1349
1424
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
1350
1425
  CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
1426
+ COO_AGENT_NAME = "exe";
1351
1427
  LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
1352
1428
  CURRENT_CONFIG_VERSION = 1;
1353
1429
  DEFAULT_CONFIG = {
@@ -2126,6 +2202,71 @@ var init_shard_manager = __esm({
2126
2202
  }
2127
2203
  });
2128
2204
 
2205
+ // src/lib/global-procedures.ts
2206
+ var global_procedures_exports = {};
2207
+ __export(global_procedures_exports, {
2208
+ deactivateGlobalProcedure: () => deactivateGlobalProcedure,
2209
+ getGlobalProceduresBlock: () => getGlobalProceduresBlock,
2210
+ loadGlobalProcedures: () => loadGlobalProcedures,
2211
+ storeGlobalProcedure: () => storeGlobalProcedure
2212
+ });
2213
+ import { randomUUID as randomUUID2 } from "crypto";
2214
+ async function loadGlobalProcedures() {
2215
+ const client = getClient();
2216
+ const result = await client.execute({
2217
+ sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
2218
+ args: []
2219
+ });
2220
+ const procedures = result.rows;
2221
+ if (procedures.length > 0) {
2222
+ _cache = procedures.map((p) => `### ${p.title}
2223
+ ${p.content}`).join("\n\n");
2224
+ } else {
2225
+ _cache = "";
2226
+ }
2227
+ _cacheLoaded = true;
2228
+ return procedures;
2229
+ }
2230
+ function getGlobalProceduresBlock() {
2231
+ if (!_cacheLoaded) return "";
2232
+ if (!_cache) return "";
2233
+ return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
2234
+
2235
+ ${_cache}
2236
+ `;
2237
+ }
2238
+ async function storeGlobalProcedure(input) {
2239
+ const id = randomUUID2();
2240
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2241
+ const client = getClient();
2242
+ await client.execute({
2243
+ sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
2244
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
2245
+ args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
2246
+ });
2247
+ await loadGlobalProcedures();
2248
+ return id;
2249
+ }
2250
+ async function deactivateGlobalProcedure(id) {
2251
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2252
+ const client = getClient();
2253
+ const result = await client.execute({
2254
+ sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
2255
+ args: [now, id]
2256
+ });
2257
+ await loadGlobalProcedures();
2258
+ return result.rowsAffected > 0;
2259
+ }
2260
+ var _cache, _cacheLoaded;
2261
+ var init_global_procedures = __esm({
2262
+ "src/lib/global-procedures.ts"() {
2263
+ "use strict";
2264
+ init_database();
2265
+ _cache = "";
2266
+ _cacheLoaded = false;
2267
+ }
2268
+ });
2269
+
2129
2270
  // src/lib/store.ts
2130
2271
  var store_exports = {};
2131
2272
  __export(store_exports, {
@@ -2205,6 +2346,11 @@ async function initStore(options) {
2205
2346
  "version-query"
2206
2347
  );
2207
2348
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
2349
+ try {
2350
+ const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
2351
+ await loadGlobalProcedures2();
2352
+ } catch {
2353
+ }
2208
2354
  }
2209
2355
  function classifyTier(record) {
2210
2356
  if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
@@ -2246,6 +2392,12 @@ async function writeMemory(record) {
2246
2392
  supersedes_id: record.supersedes_id ?? null
2247
2393
  };
2248
2394
  _pendingRecords.push(dbRow);
2395
+ orgBus.emit({
2396
+ type: "memory_stored",
2397
+ agentId: record.agent_id,
2398
+ project: record.project_name,
2399
+ timestamp: record.timestamp
2400
+ });
2249
2401
  const MAX_PENDING = 1e3;
2250
2402
  if (_pendingRecords.length > MAX_PENDING) {
2251
2403
  const dropped = _pendingRecords.length - MAX_PENDING;
@@ -2591,6 +2743,7 @@ var init_store = __esm({
2591
2743
  init_database();
2592
2744
  init_keychain();
2593
2745
  init_config();
2746
+ init_state_bus();
2594
2747
  INIT_MAX_RETRIES = 3;
2595
2748
  INIT_RETRY_DELAY_MS = 1e3;
2596
2749
  _pendingRecords = [];
@@ -3130,7 +3283,7 @@ var init_employees = __esm({
3130
3283
 
3131
3284
  // src/lib/license.ts
3132
3285
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
3133
- import { randomUUID as randomUUID10 } from "crypto";
3286
+ import { randomUUID as randomUUID11 } from "crypto";
3134
3287
  import path9 from "path";
3135
3288
  import { jwtVerify, importSPKI } from "jose";
3136
3289
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
@@ -3229,2112 +3382,2607 @@ var init_plan_limits = __esm({
3229
3382
  }
3230
3383
  });
3231
3384
 
3232
- // src/lib/tmux-routing.ts
3233
- import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
3234
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, existsSync as existsSync10, appendFileSync } from "fs";
3385
+ // src/lib/notifications.ts
3386
+ import crypto3 from "crypto";
3235
3387
  import path11 from "path";
3236
3388
  import os6 from "os";
3237
- import { fileURLToPath as fileURLToPath2 } from "url";
3238
- import { unlinkSync as unlinkSync2 } from "fs";
3239
- function spawnLockPath(sessionName) {
3240
- return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3241
- }
3242
- function isProcessAlive(pid) {
3389
+ import {
3390
+ readFileSync as readFileSync9,
3391
+ readdirSync as readdirSync2,
3392
+ unlinkSync as unlinkSync2,
3393
+ existsSync as existsSync10,
3394
+ rmdirSync
3395
+ } from "fs";
3396
+ async function writeNotification(notification) {
3243
3397
  try {
3244
- process.kill(pid, 0);
3245
- return true;
3246
- } catch {
3247
- return false;
3248
- }
3249
- }
3250
- function acquireSpawnLock2(sessionName) {
3251
- if (!existsSync10(SPAWN_LOCK_DIR)) {
3252
- mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3253
- }
3254
- const lockFile = spawnLockPath(sessionName);
3255
- if (existsSync10(lockFile)) {
3256
- try {
3257
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
3258
- const age = Date.now() - lock.timestamp;
3259
- if (isProcessAlive(lock.pid) && age < 6e4) {
3260
- return false;
3261
- }
3262
- } catch {
3263
- }
3398
+ const client = getClient();
3399
+ const id = crypto3.randomUUID();
3400
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3401
+ await client.execute({
3402
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
3403
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3404
+ args: [
3405
+ id,
3406
+ notification.agentId,
3407
+ notification.agentRole,
3408
+ notification.event,
3409
+ notification.project,
3410
+ notification.summary,
3411
+ notification.taskFile ?? null,
3412
+ now
3413
+ ]
3414
+ });
3415
+ } catch (err) {
3416
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
3417
+ `);
3264
3418
  }
3265
- writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3266
- return true;
3267
3419
  }
3268
- function releaseSpawnLock2(sessionName) {
3420
+ async function markAsReadByTaskFile(taskFile) {
3269
3421
  try {
3270
- unlinkSync2(spawnLockPath(sessionName));
3422
+ const client = getClient();
3423
+ await client.execute({
3424
+ sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
3425
+ args: [taskFile]
3426
+ });
3271
3427
  } catch {
3272
3428
  }
3273
3429
  }
3274
- function resolveBehaviorsExporterScript() {
3275
- try {
3276
- const thisFile = fileURLToPath2(import.meta.url);
3277
- const scriptPath = path11.join(
3278
- path11.dirname(thisFile),
3279
- "..",
3280
- "bin",
3281
- "exe-export-behaviors.js"
3282
- );
3283
- return existsSync10(scriptPath) ? scriptPath : null;
3284
- } catch {
3285
- return null;
3430
+ var init_notifications = __esm({
3431
+ "src/lib/notifications.ts"() {
3432
+ "use strict";
3433
+ init_database();
3286
3434
  }
3287
- }
3288
- function exportBehaviorsSync(agentId, projectName, sessionKey) {
3289
- const script = resolveBehaviorsExporterScript();
3290
- if (!script) return null;
3435
+ });
3436
+
3437
+ // src/lib/session-kill-telemetry.ts
3438
+ import crypto4 from "crypto";
3439
+ async function recordSessionKill(input) {
3291
3440
  try {
3292
- const output = execFileSync2(
3293
- process.execPath,
3294
- [script, agentId, projectName, sessionKey],
3295
- { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
3296
- ).trim();
3297
- return output.length > 0 ? output : null;
3441
+ const client = getClient();
3442
+ await client.execute({
3443
+ sql: `INSERT INTO session_kills
3444
+ (id, session_name, agent_id, killed_at, reason,
3445
+ ticks_idle, estimated_tokens_saved)
3446
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
3447
+ args: [
3448
+ crypto4.randomUUID(),
3449
+ input.sessionName,
3450
+ input.agentId,
3451
+ (/* @__PURE__ */ new Date()).toISOString(),
3452
+ input.reason,
3453
+ input.ticksIdle ?? null,
3454
+ input.estimatedTokensSaved ?? null
3455
+ ]
3456
+ });
3298
3457
  } catch (err) {
3299
3458
  process.stderr.write(
3300
- `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
3459
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
3301
3460
  `
3302
3461
  );
3303
- return null;
3304
- }
3305
- }
3306
- function getMySession() {
3307
- return getTransport().getMySession();
3308
- }
3309
- function employeeSessionName(employee, exeSession, instance) {
3310
- const suffix = instance != null && instance > 0 ? String(instance) : "";
3311
- return `${employee}${suffix}-${exeSession}`;
3312
- }
3313
- function extractRootExe(name) {
3314
- const match = name.match(/(exe\d+)$/);
3315
- return match?.[1] ?? null;
3316
- }
3317
- function getParentExe(sessionKey) {
3318
- try {
3319
- const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3320
- return data.parentExe || null;
3321
- } catch {
3322
- return null;
3323
- }
3324
- }
3325
- function getDispatchedBy(sessionKey) {
3326
- try {
3327
- const data = JSON.parse(readFileSync9(
3328
- path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3329
- "utf8"
3330
- ));
3331
- return data.dispatchedBy ?? data.parentExe ?? null;
3332
- } catch {
3333
- return null;
3334
3462
  }
3335
3463
  }
3336
- function resolveExeSession() {
3337
- const mySession = getMySession();
3338
- if (!mySession) return null;
3339
- try {
3340
- const key = getSessionKey();
3341
- const parentExe = getParentExe(key);
3342
- if (parentExe) {
3343
- return extractRootExe(parentExe) ?? parentExe;
3344
- }
3345
- } catch {
3464
+ var init_session_kill_telemetry = __esm({
3465
+ "src/lib/session-kill-telemetry.ts"() {
3466
+ "use strict";
3467
+ init_database();
3346
3468
  }
3347
- return extractRootExe(mySession) ?? mySession;
3348
- }
3349
- function isEmployeeAlive(sessionName) {
3350
- return getTransport().isAlive(sessionName);
3351
- }
3352
- function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
3353
- const base = employeeSessionName(employeeName, exeSession);
3354
- if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
3355
- for (let i = 2; i <= maxInstances; i++) {
3356
- const candidate = employeeSessionName(employeeName, exeSession, i);
3357
- if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
3469
+ });
3470
+
3471
+ // src/lib/tasks-crud.ts
3472
+ import crypto5 from "crypto";
3473
+ import path12 from "path";
3474
+ import { execSync as execSync4 } from "child_process";
3475
+ import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3476
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3477
+ async function writeCheckpoint(input) {
3478
+ const client = getClient();
3479
+ const row = await resolveTask(client, input.taskId);
3480
+ const taskId = String(row.id);
3481
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3482
+ const blockedByIds = [];
3483
+ if (row.blocked_by) {
3484
+ blockedByIds.push(String(row.blocked_by));
3358
3485
  }
3359
- return null;
3360
- }
3361
- function readDebounceState() {
3362
- try {
3363
- if (!existsSync10(DEBOUNCE_FILE)) return {};
3364
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
3365
- } catch {
3366
- return {};
3486
+ const checkpoint = {
3487
+ step: input.step,
3488
+ context_summary: input.contextSummary,
3489
+ files_touched: input.filesTouched ?? [],
3490
+ blocked_by_ids: blockedByIds,
3491
+ last_checkpoint_at: now
3492
+ };
3493
+ const result = await client.execute({
3494
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
3495
+ args: [JSON.stringify(checkpoint), now, taskId]
3496
+ });
3497
+ if (result.rowsAffected === 0) {
3498
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
3367
3499
  }
3500
+ const countResult = await client.execute({
3501
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
3502
+ args: [taskId]
3503
+ });
3504
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
3505
+ return { checkpointCount };
3368
3506
  }
3369
- function writeDebounceState(state) {
3370
- try {
3371
- if (!existsSync10(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3372
- writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
3373
- } catch {
3374
- }
3507
+ function extractParentFromContext(contextBody) {
3508
+ if (!contextBody) return null;
3509
+ const match = contextBody.match(
3510
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
3511
+ );
3512
+ return match ? match[1].toLowerCase() : null;
3375
3513
  }
3376
- function isDebounced(targetSession) {
3377
- const state = readDebounceState();
3378
- const lastSent = state[targetSession] ?? 0;
3379
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
3514
+ function slugify(title) {
3515
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3380
3516
  }
3381
- function recordDebounce(targetSession) {
3382
- const state = readDebounceState();
3383
- state[targetSession] = Date.now();
3384
- const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
3385
- for (const key of Object.keys(state)) {
3386
- if ((state[key] ?? 0) < cutoff) delete state[key];
3517
+ async function resolveTask(client, identifier) {
3518
+ let result = await client.execute({
3519
+ sql: "SELECT * FROM tasks WHERE id = ?",
3520
+ args: [identifier]
3521
+ });
3522
+ if (result.rows.length === 1) return result.rows[0];
3523
+ result = await client.execute({
3524
+ sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
3525
+ args: [`%${identifier}%`]
3526
+ });
3527
+ if (result.rows.length === 1) return result.rows[0];
3528
+ if (result.rows.length > 1) {
3529
+ const exact = result.rows.filter(
3530
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
3531
+ );
3532
+ if (exact.length === 1) return exact[0];
3533
+ const candidates = exact.length > 1 ? exact : result.rows;
3534
+ const active = candidates.filter(
3535
+ (r) => !["done", "cancelled"].includes(String(r.status))
3536
+ );
3537
+ if (active.length === 1) return active[0];
3538
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
3539
+ throw new Error(
3540
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3541
+ );
3387
3542
  }
3388
- writeDebounceState(state);
3389
- }
3390
- function logIntercom(msg) {
3391
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
3392
- `;
3393
- process.stderr.write(`[intercom] ${msg}
3394
- `);
3395
- try {
3396
- appendFileSync(INTERCOM_LOG2, line);
3397
- } catch {
3543
+ result = await client.execute({
3544
+ sql: "SELECT * FROM tasks WHERE title LIKE ?",
3545
+ args: [`%${identifier}%`]
3546
+ });
3547
+ if (result.rows.length === 1) return result.rows[0];
3548
+ if (result.rows.length > 1) {
3549
+ const active = result.rows.filter(
3550
+ (r) => !["done", "cancelled"].includes(String(r.status))
3551
+ );
3552
+ if (active.length === 1) return active[0];
3553
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
3554
+ throw new Error(
3555
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3556
+ );
3398
3557
  }
3558
+ throw new Error(`Task not found: ${identifier}`);
3399
3559
  }
3400
- function getSessionState(sessionName) {
3401
- const transport = getTransport();
3402
- if (!transport.isAlive(sessionName)) return "offline";
3403
- try {
3404
- const pane = transport.capturePane(sessionName, 5);
3405
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
3406
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
3407
- return "no_claude";
3560
+ async function createTaskCore(input) {
3561
+ const client = getClient();
3562
+ const id = crypto5.randomUUID();
3563
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3564
+ const slug = slugify(input.title);
3565
+ const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
3566
+ let blockedById = null;
3567
+ const initialStatus = input.blockedBy ? "blocked" : "open";
3568
+ if (input.blockedBy) {
3569
+ const blocker = await resolveTask(client, input.blockedBy);
3570
+ blockedById = String(blocker.id);
3571
+ }
3572
+ let parentTaskId = null;
3573
+ let parentRef = input.parentTaskId;
3574
+ if (!parentRef) {
3575
+ const extracted = extractParentFromContext(input.context);
3576
+ if (extracted) {
3577
+ parentRef = extracted;
3578
+ process.stderr.write(
3579
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
3580
+ );
3581
+ }
3582
+ }
3583
+ if (parentRef) {
3584
+ try {
3585
+ const parent = await resolveTask(client, parentRef);
3586
+ parentTaskId = String(parent.id);
3587
+ } catch (err) {
3588
+ if (!input.parentTaskId) {
3589
+ throw new Error(
3590
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
3591
+ );
3408
3592
  }
3593
+ throw err;
3409
3594
  }
3410
- if (/Running…/.test(pane)) return "tool";
3411
- if (BUSY_PATTERN.test(pane)) return "thinking";
3412
- return "idle";
3413
- } catch {
3414
- return "offline";
3415
3595
  }
3416
- }
3417
- function isExeSession(sessionName) {
3418
- return /^exe\d*$/.test(sessionName);
3419
- }
3420
- function sendIntercom(targetSession) {
3421
- const transport = getTransport();
3422
- if (isExeSession(targetSession)) {
3423
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
3424
- return "skipped_exe";
3596
+ let warning;
3597
+ const dupCheck = await client.execute({
3598
+ sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
3599
+ args: [input.title, input.assignedTo]
3600
+ });
3601
+ if (dupCheck.rows.length > 0) {
3602
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
3425
3603
  }
3426
- if (isDebounced(targetSession)) {
3427
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
3428
- return "debounced";
3604
+ if (input.baseDir) {
3605
+ try {
3606
+ await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
3607
+ await mkdir4(path12.join(input.baseDir, "exe", "research"), { recursive: true });
3608
+ await ensureArchitectureDoc(input.baseDir, input.projectName);
3609
+ await ensureGitignoreExe(input.baseDir);
3610
+ } catch {
3611
+ }
3429
3612
  }
3613
+ const complexity = input.complexity ?? "standard";
3614
+ let sessionScope = null;
3430
3615
  try {
3431
- const sessions = transport.listSessions();
3432
- if (!sessions.includes(targetSession)) {
3433
- logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
3434
- return "failed";
3435
- }
3436
- const sessionState = getSessionState(targetSession);
3437
- if (sessionState === "no_claude") {
3438
- queueIntercom(targetSession, "claude not running in session");
3439
- recordDebounce(targetSession);
3440
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3441
- return "queued";
3442
- }
3443
- if (sessionState === "thinking" || sessionState === "tool") {
3444
- queueIntercom(targetSession, "session busy at send time");
3445
- recordDebounce(targetSession);
3446
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
3447
- return "queued";
3448
- }
3449
- if (transport.isPaneInCopyMode(targetSession)) {
3450
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
3451
- transport.sendKeys(targetSession, "q");
3452
- }
3453
- transport.sendKeys(targetSession, "/exe-intercom");
3454
- recordDebounce(targetSession);
3455
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
3456
- return "delivered";
3616
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3617
+ sessionScope = resolveExeSession2();
3457
3618
  } catch {
3458
- logIntercom(`FAIL \u2192 ${targetSession}`);
3459
- return "failed";
3460
3619
  }
3620
+ await client.execute({
3621
+ 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)
3622
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3623
+ args: [
3624
+ id,
3625
+ input.title,
3626
+ input.assignedTo,
3627
+ input.assignedBy,
3628
+ input.projectName,
3629
+ input.priority,
3630
+ initialStatus,
3631
+ taskFile,
3632
+ blockedById,
3633
+ parentTaskId,
3634
+ input.reviewer ?? null,
3635
+ input.context,
3636
+ complexity,
3637
+ input.budgetTokens ?? null,
3638
+ input.budgetFallbackModel ?? null,
3639
+ 0,
3640
+ null,
3641
+ sessionScope,
3642
+ now,
3643
+ now
3644
+ ]
3645
+ });
3646
+ return {
3647
+ id,
3648
+ title: input.title,
3649
+ assignedTo: input.assignedTo,
3650
+ assignedBy: input.assignedBy,
3651
+ projectName: input.projectName,
3652
+ priority: input.priority,
3653
+ status: initialStatus,
3654
+ taskFile,
3655
+ createdAt: now,
3656
+ updatedAt: now,
3657
+ warning,
3658
+ budgetTokens: input.budgetTokens ?? null,
3659
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
3660
+ tokensUsed: 0,
3661
+ tokensWarnedAt: null
3662
+ };
3461
3663
  }
3462
- function notifyParentExe(sessionKey) {
3463
- const target = getDispatchedBy(sessionKey);
3464
- if (!target) {
3465
- process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
3466
- `);
3467
- return false;
3664
+ async function listTasks(input) {
3665
+ const client = getClient();
3666
+ const conditions = [];
3667
+ const args = [];
3668
+ if (input.assignedTo) {
3669
+ conditions.push("assigned_to = ?");
3670
+ args.push(input.assignedTo);
3468
3671
  }
3469
- process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
3470
- `);
3471
- const result = sendIntercom(target);
3472
- if (result === "failed") {
3473
- const rootExe = resolveExeSession();
3474
- if (rootExe && rootExe !== target) {
3475
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3476
- `);
3477
- const fallback = sendIntercom(rootExe);
3478
- return fallback !== "failed";
3479
- }
3480
- return false;
3672
+ if (input.status) {
3673
+ conditions.push("status = ?");
3674
+ args.push(input.status);
3675
+ } else {
3676
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
3481
3677
  }
3482
- return true;
3483
- }
3484
- function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3485
- if (employeeName === "exe") {
3486
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
3678
+ if (input.projectName) {
3679
+ conditions.push("project_name = ?");
3680
+ args.push(input.projectName);
3681
+ }
3682
+ if (input.priority) {
3683
+ conditions.push("priority = ?");
3684
+ args.push(input.priority);
3487
3685
  }
3488
3686
  try {
3489
- assertEmployeeLimitSync();
3490
- } catch (err) {
3491
- if (err instanceof PlanLimitError) {
3492
- return { status: "failed", sessionName: "", error: err.message };
3687
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3688
+ const session = resolveExeSession2();
3689
+ if (session) {
3690
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
3691
+ args.push(session);
3493
3692
  }
3693
+ } catch {
3494
3694
  }
3495
- if (/-exe\d*$/.test(employeeName)) {
3496
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
3497
- return {
3498
- status: "failed",
3499
- sessionName: "",
3500
- error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
3501
- };
3502
- }
3503
- let effectiveInstance = opts?.instance;
3504
- if (effectiveInstance === void 0 && opts?.autoInstance) {
3505
- const free = findFreeInstance(
3506
- employeeName,
3507
- exeSession,
3508
- opts.maxAutoInstances ?? 10
3509
- );
3510
- if (free === null) {
3511
- return {
3512
- status: "failed",
3513
- sessionName: employeeSessionName(employeeName, exeSession),
3514
- error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
3515
- };
3516
- }
3517
- effectiveInstance = free === 0 ? void 0 : free;
3518
- }
3519
- const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
3520
- if (isEmployeeAlive(sessionName)) {
3521
- const result2 = sendIntercom(sessionName);
3522
- if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
3523
- return { status: "intercom_sent", sessionName };
3524
- }
3525
- if (result2 === "delivered") {
3526
- return { status: "intercom_unprocessed", sessionName };
3527
- }
3528
- return { status: "failed", sessionName, error: "intercom delivery failed" };
3529
- }
3530
- const spawnOpts = { ...opts, instance: effectiveInstance };
3531
- const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
3532
- if (result.error) {
3533
- return { status: "failed", sessionName, error: result.error };
3534
- }
3535
- return { status: "spawned", sessionName };
3695
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3696
+ const result = await client.execute({
3697
+ 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`,
3698
+ args
3699
+ });
3700
+ return result.rows.map((r) => ({
3701
+ id: String(r.id),
3702
+ title: String(r.title),
3703
+ assignedTo: String(r.assigned_to),
3704
+ assignedBy: String(r.assigned_by),
3705
+ projectName: String(r.project_name),
3706
+ priority: String(r.priority),
3707
+ status: String(r.status),
3708
+ taskFile: String(r.task_file),
3709
+ createdAt: String(r.created_at),
3710
+ updatedAt: String(r.updated_at),
3711
+ checkpointCount: Number(r.checkpoint_count ?? 0),
3712
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
3713
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
3714
+ tokensUsed: Number(r.tokens_used ?? 0),
3715
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
3716
+ }));
3536
3717
  }
3537
- function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3538
- const transport = getTransport();
3539
- const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3540
- const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3541
- const logDir = path11.join(os6.homedir(), ".exe-os", "session-logs");
3542
- const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3543
- if (!existsSync10(logDir)) {
3544
- mkdirSync6(logDir, { recursive: true });
3545
- }
3546
- transport.kill(sessionName);
3547
- let cleanupSuffix = "";
3548
- try {
3549
- const thisFile = fileURLToPath2(import.meta.url);
3550
- const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3551
- if (existsSync10(cleanupScript)) {
3552
- cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
3553
- }
3554
- } catch {
3555
- }
3556
- try {
3557
- const claudeJsonPath = path11.join(os6.homedir(), ".claude.json");
3558
- let claudeJson = {};
3559
- try {
3560
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
3561
- } catch {
3562
- }
3563
- if (!claudeJson.projects) claudeJson.projects = {};
3564
- const projects = claudeJson.projects;
3565
- const trustDir = opts?.cwd ?? projectDir;
3566
- if (!projects[trustDir]) projects[trustDir] = {};
3567
- projects[trustDir].hasTrustDialogAccepted = true;
3568
- writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3569
- } catch {
3570
- }
3718
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
3719
+ if (!taskContext) return null;
3720
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
3571
3721
  try {
3572
- const settingsDir = path11.join(os6.homedir(), ".claude", "projects");
3573
- const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3574
- const projSettingsDir = path11.join(settingsDir, normalizedKey);
3575
- const settingsPath = path11.join(projSettingsDir, "settings.json");
3576
- let settings = {};
3577
- try {
3578
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
3579
- } catch {
3580
- }
3581
- const perms = settings.permissions ?? {};
3582
- const allow = perms.allow ?? [];
3583
- const toolNames = [
3584
- "recall_my_memory",
3585
- "store_memory",
3586
- "create_task",
3587
- "update_task",
3588
- "list_tasks",
3589
- "get_task",
3590
- "ask_team_memory",
3591
- "store_behavior",
3592
- "get_identity",
3593
- "send_message"
3594
- ];
3595
- const requiredTools = expandDualPrefixTools(toolNames);
3596
- let changed = false;
3597
- for (const tool of requiredTools) {
3598
- if (!allow.includes(tool)) {
3599
- allow.push(tool);
3600
- changed = true;
3601
- }
3602
- }
3603
- if (changed) {
3604
- perms.allow = allow;
3605
- settings.permissions = perms;
3606
- mkdirSync6(projSettingsDir, { recursive: true });
3607
- writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3722
+ const since = new Date(taskCreatedAt).toISOString();
3723
+ const branch = execSync4(
3724
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
3725
+ { encoding: "utf8", timeout: 3e3 }
3726
+ ).trim();
3727
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
3728
+ const commitCount = execSync4(
3729
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
3730
+ { encoding: "utf8", timeout: 5e3 }
3731
+ ).trim();
3732
+ const count = parseInt(commitCount, 10);
3733
+ if (count === 0) {
3734
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
3608
3735
  }
3736
+ return null;
3609
3737
  } catch {
3738
+ return null;
3610
3739
  }
3611
- const spawnCwd = opts?.cwd ?? projectDir;
3612
- const useExeAgent = !!(opts?.model && opts?.provider);
3613
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
3614
- const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
3615
- let identityFlag = "";
3616
- let behaviorsFlag = "";
3617
- let legacyFallbackWarned = false;
3618
- if (!useExeAgent && !useBinSymlink) {
3619
- const identityPath = path11.join(
3620
- os6.homedir(),
3621
- ".exe-os",
3622
- "identity",
3623
- `${employeeName}.md`
3624
- );
3625
- _resetCcAgentSupportCache();
3626
- const hasAgentFlag = claudeSupportsAgentFlag();
3627
- if (hasAgentFlag) {
3628
- identityFlag = ` --agent ${employeeName}`;
3629
- } else if (existsSync10(identityPath)) {
3630
- identityFlag = ` --append-system-prompt-file ${identityPath}`;
3631
- legacyFallbackWarned = true;
3632
- }
3633
- const behaviorsFile = exportBehaviorsSync(
3634
- employeeName,
3635
- path11.basename(spawnCwd),
3636
- sessionName
3637
- );
3638
- if (behaviorsFile) {
3639
- behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
3640
- }
3641
- }
3642
- if (legacyFallbackWarned) {
3740
+ }
3741
+ async function updateTaskStatus(input) {
3742
+ const client = getClient();
3743
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3744
+ const row = await resolveTask(client, input.taskId);
3745
+ const taskId = String(row.id);
3746
+ const taskFile = String(row.task_file);
3747
+ if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
3643
3748
  process.stderr.write(
3644
- `[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.
3749
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
3645
3750
  `
3646
3751
  );
3647
3752
  }
3648
- let sessionContextFlag = "";
3649
- try {
3650
- const ctxDir = path11.join(os6.homedir(), ".exe-os", "session-cache");
3651
- mkdirSync6(ctxDir, { recursive: true });
3652
- const ctxFile = path11.join(ctxDir, `session-context-${sessionName}.md`);
3653
- const ctxContent = [
3654
- `## Session Context`,
3655
- `You are running in tmux session: ${sessionName}.`,
3656
- `Your parent exe session is ${exeSession}.`,
3657
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
3658
- ].join("\n");
3659
- writeFileSync4(ctxFile, ctxContent);
3660
- sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
3661
- } catch {
3662
- }
3663
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
3664
- if (ccProvider !== DEFAULT_PROVIDER) {
3665
- const cfg = PROVIDER_TABLE[ccProvider];
3666
- if (cfg?.apiKeyEnv) {
3667
- const keyVal = process.env[cfg.apiKeyEnv];
3668
- if (keyVal) {
3669
- envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
3753
+ if (input.status === "done") {
3754
+ const existingRow = await client.execute({
3755
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
3756
+ args: [taskId]
3757
+ });
3758
+ if (existingRow.rows.length > 0) {
3759
+ const ctx = existingRow.rows[0];
3760
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
3761
+ if (warning) {
3762
+ input.result = input.result ? `\u26A0\uFE0F ${warning}
3763
+
3764
+ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3765
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
3766
+ `);
3670
3767
  }
3671
3768
  }
3672
3769
  }
3673
- let spawnCommand;
3674
- if (useExeAgent) {
3675
- spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
3676
- } else if (useBinSymlink) {
3677
- const binName = `${employeeName}-${ccProvider}`;
3678
- process.stderr.write(
3679
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
3680
- `
3681
- );
3682
- spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
3683
- } else {
3684
- spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
3770
+ if (input.status === "in_progress") {
3771
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
3772
+ const claim = await client.execute({
3773
+ sql: `UPDATE tasks
3774
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
3775
+ WHERE id = ? AND status = 'open'`,
3776
+ args: [tmuxSession, now, taskId]
3777
+ });
3778
+ if (claim.rowsAffected === 0) {
3779
+ const current = await client.execute({
3780
+ sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
3781
+ args: [taskId]
3782
+ });
3783
+ const cur = current.rows[0];
3784
+ const status = cur?.status ?? "unknown";
3785
+ const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
3786
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
3787
+ }
3788
+ try {
3789
+ await writeCheckpoint({
3790
+ taskId,
3791
+ step: "claimed",
3792
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3793
+ });
3794
+ } catch {
3795
+ }
3796
+ return { row, taskFile, now, taskId };
3685
3797
  }
3686
- const spawnResult = transport.spawn(sessionName, {
3687
- cwd: spawnCwd,
3688
- command: spawnCommand
3689
- });
3690
- if (spawnResult.error) {
3691
- releaseSpawnLock2(sessionName);
3692
- return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3798
+ if (input.result) {
3799
+ await client.execute({
3800
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
3801
+ args: [input.status, input.result, now, taskId]
3802
+ });
3803
+ } else {
3804
+ await client.execute({
3805
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
3806
+ args: [input.status, now, taskId]
3807
+ });
3693
3808
  }
3694
- transport.pipeLog(sessionName, logFile);
3695
3809
  try {
3696
- const mySession = getMySession();
3697
- const dispatchInfo = path11.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
3698
- writeFileSync4(dispatchInfo, JSON.stringify({
3699
- dispatchedBy: mySession,
3700
- rootExe: exeSession,
3701
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
3702
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
3703
- }));
3810
+ await writeCheckpoint({
3811
+ taskId,
3812
+ step: `status_transition:${input.status}`,
3813
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
3814
+ });
3704
3815
  } catch {
3705
3816
  }
3706
- let booted = false;
3707
- for (let i = 0; i < 30; i++) {
3708
- try {
3709
- execSync4("sleep 0.5");
3710
- } catch {
3711
- }
3712
- try {
3713
- const pane = transport.capturePane(sessionName);
3714
- if (useExeAgent) {
3715
- if (pane.includes("[exe-agent]") || pane.includes("online")) {
3716
- booted = true;
3717
- break;
3718
- }
3719
- } else {
3720
- if (pane.includes("Claude Code") || pane.includes("\u276F")) {
3721
- booted = true;
3722
- break;
3723
- }
3724
- }
3725
- } catch {
3726
- }
3727
- }
3728
- if (!booted) {
3729
- releaseSpawnLock2(sessionName);
3730
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3817
+ return { row, taskFile, now, taskId };
3818
+ }
3819
+ async function deleteTaskCore(taskId, _baseDir) {
3820
+ const client = getClient();
3821
+ const row = await resolveTask(client, taskId);
3822
+ const id = String(row.id);
3823
+ const taskFile = String(row.task_file);
3824
+ const assignedTo = String(row.assigned_to);
3825
+ const assignedBy = String(row.assigned_by);
3826
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
3827
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
3828
+ return { taskFile, assignedTo, assignedBy, taskSlug };
3829
+ }
3830
+ async function ensureArchitectureDoc(baseDir, projectName) {
3831
+ const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
3832
+ try {
3833
+ if (existsSync11(archPath)) return;
3834
+ const template = [
3835
+ `# ${projectName} \u2014 System Architecture`,
3836
+ "",
3837
+ "> Employees: read this before every task. Update it when you change system structure.",
3838
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
3839
+ "",
3840
+ "## Overview",
3841
+ "",
3842
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
3843
+ "",
3844
+ "## Key Components",
3845
+ "",
3846
+ "<!-- List the major modules, services, or subsystems. -->",
3847
+ "",
3848
+ "## Data Flow",
3849
+ "",
3850
+ "<!-- How does data move through the system? What writes where? -->",
3851
+ "",
3852
+ "## Invariants",
3853
+ "",
3854
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
3855
+ "",
3856
+ "## Dependencies",
3857
+ "",
3858
+ "<!-- What depends on what? If I change X, what else is affected? -->",
3859
+ ""
3860
+ ].join("\n");
3861
+ await writeFile4(archPath, template, "utf-8");
3862
+ } catch {
3731
3863
  }
3732
- if (!useExeAgent) {
3733
- try {
3734
- transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
3735
- } catch {
3864
+ }
3865
+ async function ensureGitignoreExe(baseDir) {
3866
+ const gitignorePath = path12.join(baseDir, ".gitignore");
3867
+ try {
3868
+ if (existsSync11(gitignorePath)) {
3869
+ const content = readFileSync10(gitignorePath, "utf-8");
3870
+ if (/^\/?exe\/?$/m.test(content)) return;
3871
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3872
+ } else {
3873
+ await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
3736
3874
  }
3875
+ } catch {
3737
3876
  }
3738
- registerSession({
3739
- windowName: sessionName,
3740
- agentId: employeeName,
3741
- projectDir: spawnCwd,
3742
- parentExe: exeSession,
3743
- pid: 0,
3744
- registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3745
- });
3746
- releaseSpawnLock2(sessionName);
3747
- return { sessionName };
3748
3877
  }
3749
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3750
- var init_tmux_routing = __esm({
3751
- "src/lib/tmux-routing.ts"() {
3878
+ var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3879
+ var init_tasks_crud = __esm({
3880
+ "src/lib/tasks-crud.ts"() {
3752
3881
  "use strict";
3753
- init_session_registry();
3754
- init_session_key();
3755
- init_transport();
3756
- init_cc_agent_support();
3757
- init_mcp_prefix();
3758
- init_provider_table();
3759
- init_intercom_queue();
3760
- init_plan_limits();
3761
- SPAWN_LOCK_DIR = path11.join(os6.homedir(), ".exe-os", "spawn-locks");
3762
- SESSION_CACHE = path11.join(os6.homedir(), ".exe-os", "session-cache");
3763
- BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3764
- INTERCOM_DEBOUNCE_MS = 3e4;
3765
- INTERCOM_LOG2 = path11.join(os6.homedir(), ".exe-os", "intercom.log");
3766
- DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
3767
- DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3768
- BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3882
+ init_database();
3883
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
3884
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3769
3885
  }
3770
3886
  });
3771
3887
 
3772
- // src/lib/messaging.ts
3773
- var messaging_exports = {};
3774
- __export(messaging_exports, {
3775
- deliverLocalMessage: () => deliverLocalMessage,
3776
- getFailedMessages: () => getFailedMessages,
3777
- getMessageStatus: () => getMessageStatus,
3778
- getPendingMessages: () => getPendingMessages,
3779
- getReadMessages: () => getReadMessages,
3780
- getUnacknowledgedMessages: () => getUnacknowledgedMessages,
3781
- markAcknowledged: () => markAcknowledged,
3782
- markFailed: () => markFailed,
3783
- markProcessed: () => markProcessed,
3784
- markRead: () => markRead,
3785
- retryPendingMessages: () => retryPendingMessages,
3786
- sendMessage: () => sendMessage,
3787
- setWsClientSend: () => setWsClientSend
3788
- });
3789
- import crypto3 from "crypto";
3790
- function generateUlid() {
3791
- const timestamp = Date.now().toString(36).padStart(10, "0");
3792
- const random = crypto3.randomBytes(10).toString("hex").slice(0, 16);
3793
- return (timestamp + random).toUpperCase();
3794
- }
3795
- function rowToMessage(row) {
3796
- return {
3797
- id: row.id,
3798
- fromAgent: row.from_agent,
3799
- fromDevice: row.from_device,
3800
- targetAgent: row.target_agent,
3801
- targetProject: row.target_project ?? null,
3802
- targetDevice: row.target_device,
3803
- content: row.content,
3804
- priority: row.priority ?? "normal",
3805
- status: row.status ?? "pending",
3806
- serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
3807
- retryCount: Number(row.retry_count ?? 0),
3808
- createdAt: row.created_at,
3809
- deliveredAt: row.delivered_at ?? null,
3810
- processedAt: row.processed_at ?? null,
3811
- failedAt: row.failed_at ?? null,
3812
- failureReason: row.failure_reason ?? null
3813
- };
3814
- }
3815
- async function sendMessage(input) {
3888
+ // src/lib/tasks-review.ts
3889
+ import path13 from "path";
3890
+ import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
3891
+ async function countPendingReviews() {
3816
3892
  const client = getClient();
3817
- const id = generateUlid();
3818
- const now = (/* @__PURE__ */ new Date()).toISOString();
3819
- const targetDevice = input.targetDevice ?? "local";
3820
- await client.execute({
3821
- sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
3822
- VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
3823
- args: [
3824
- id,
3825
- input.fromAgent,
3826
- input.targetAgent,
3827
- input.targetProject ?? null,
3828
- targetDevice,
3829
- input.content,
3830
- input.priority ?? "normal",
3831
- now
3832
- ]
3833
- });
3834
- try {
3835
- if (targetDevice !== "local") {
3836
- await deliverCrossMachineMessage(id, targetDevice);
3837
- } else {
3838
- await deliverLocalMessage(id);
3839
- }
3840
- } catch {
3841
- }
3842
3893
  const result = await client.execute({
3843
- sql: "SELECT * FROM messages WHERE id = ?",
3844
- args: [id]
3894
+ sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3895
+ args: []
3845
3896
  });
3846
- return rowToMessage(result.rows[0]);
3847
- }
3848
- function setWsClientSend(fn) {
3849
- _wsClientSend = fn;
3897
+ return Number(result.rows[0]?.cnt) || 0;
3850
3898
  }
3851
- async function deliverCrossMachineMessage(messageId, targetDevice) {
3899
+ async function countNewPendingReviewsSince(sinceIso) {
3852
3900
  const client = getClient();
3853
3901
  const result = await client.execute({
3854
- sql: "SELECT * FROM messages WHERE id = ?",
3855
- args: [messageId]
3856
- });
3857
- if (result.rows.length === 0) return false;
3858
- const msg = rowToMessage(result.rows[0]);
3859
- if (msg.status !== "pending") return false;
3860
- if (!_wsClientSend) {
3861
- return false;
3862
- }
3863
- const payload = JSON.stringify({
3864
- id: msg.id,
3865
- fromAgent: msg.fromAgent,
3866
- targetAgent: msg.targetAgent,
3867
- targetProject: msg.targetProject,
3868
- content: msg.content,
3869
- priority: msg.priority,
3870
- createdAt: msg.createdAt
3902
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3903
+ WHERE status = 'needs_review' AND updated_at > ?`,
3904
+ args: [sinceIso]
3871
3905
  });
3872
- const sent = _wsClientSend(targetDevice, payload);
3873
- if (sent) {
3874
- await client.execute({
3875
- sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
3876
- args: [messageId]
3877
- });
3878
- return true;
3879
- }
3880
- return false;
3906
+ return Number(result.rows[0]?.cnt) || 0;
3881
3907
  }
3882
- async function deliverLocalMessage(messageId) {
3908
+ async function listPendingReviews(limit) {
3883
3909
  const client = getClient();
3884
3910
  const result = await client.execute({
3885
- sql: "SELECT * FROM messages WHERE id = ?",
3886
- args: [messageId]
3911
+ sql: `SELECT title, assigned_to, project_name FROM tasks
3912
+ WHERE status = 'needs_review'
3913
+ ORDER BY priority ASC, created_at DESC LIMIT ?`,
3914
+ args: [limit]
3887
3915
  });
3888
- if (result.rows.length === 0) return false;
3889
- const msg = rowToMessage(result.rows[0]);
3890
- if (msg.status !== "pending") return false;
3891
- const targetAgent = msg.targetAgent;
3916
+ return result.rows;
3917
+ }
3918
+ async function cleanupOrphanedReviews() {
3919
+ const client = getClient();
3892
3920
  const now = (/* @__PURE__ */ new Date()).toISOString();
3893
- try {
3894
- const exeSession = resolveExeSession();
3895
- if (!exeSession) {
3896
- throw new Error("No exe session found");
3897
- }
3898
- const ensureResult = ensureEmployee(targetAgent, exeSession, process.cwd());
3899
- if (ensureResult.status === "failed") {
3900
- throw new Error(ensureResult.error ?? "ensureEmployee failed");
3901
- }
3902
- await client.execute({
3903
- sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
3904
- args: [now, messageId]
3905
- });
3906
- return true;
3907
- } catch {
3908
- const newRetryCount = msg.retryCount + 1;
3909
- if (newRetryCount >= MAX_RETRIES2) {
3910
- await markFailed(messageId, "session unavailable after 10 retries");
3911
- } else {
3912
- await client.execute({
3913
- sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
3914
- args: [newRetryCount, messageId]
3915
- });
3916
- }
3917
- return false;
3918
- }
3919
- }
3920
- async function getPendingMessages(targetAgent) {
3921
- const client = getClient();
3922
- const result = await client.execute({
3923
- sql: `SELECT * FROM messages
3924
- WHERE target_agent = ? AND status IN ('pending', 'delivered')
3925
- ORDER BY id`,
3926
- args: [targetAgent]
3927
- });
3928
- return result.rows.map((row) => rowToMessage(row));
3929
- }
3930
- async function markRead(messageId) {
3931
- const client = getClient();
3932
- await client.execute({
3933
- sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
3934
- args: [messageId]
3935
- });
3936
- }
3937
- async function markAcknowledged(messageId) {
3938
- const client = getClient();
3939
- await client.execute({
3940
- sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
3941
- args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
3942
- });
3943
- }
3944
- async function markProcessed(messageId) {
3945
- const client = getClient();
3946
- await client.execute({
3947
- sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
3948
- args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
3949
- });
3950
- }
3951
- async function getMessageStatus(messageId) {
3952
- const client = getClient();
3953
- const result = await client.execute({
3954
- sql: "SELECT status FROM messages WHERE id = ?",
3955
- args: [messageId]
3956
- });
3957
- return result.rows[0]?.status ?? null;
3958
- }
3959
- async function getUnacknowledgedMessages(targetAgent) {
3960
- const client = getClient();
3961
- const result = await client.execute({
3962
- sql: `SELECT * FROM messages
3963
- WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
3964
- ORDER BY id`,
3965
- args: [targetAgent]
3966
- });
3967
- return result.rows.map((row) => rowToMessage(row));
3968
- }
3969
- async function getReadMessages(targetAgent) {
3970
- const client = getClient();
3971
- const result = await client.execute({
3972
- sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
3973
- args: [targetAgent]
3974
- });
3975
- return result.rows.map((row) => rowToMessage(row));
3976
- }
3977
- async function markFailed(messageId, reason) {
3978
- const client = getClient();
3979
- await client.execute({
3980
- sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
3981
- args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
3982
- });
3983
- }
3984
- async function getFailedMessages() {
3985
- const client = getClient();
3986
- const result = await client.execute({
3987
- sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
3988
- args: []
3921
+ const r1 = await client.execute({
3922
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3923
+ WHERE status = 'needs_review'
3924
+ AND assigned_by = 'system'
3925
+ AND title LIKE 'Review:%'
3926
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3927
+ args: [now]
3989
3928
  });
3990
- return result.rows.map((row) => rowToMessage(row));
3991
- }
3992
- async function retryPendingMessages() {
3993
- const client = getClient();
3994
- const result = await client.execute({
3995
- sql: `SELECT * FROM messages
3996
- WHERE status = 'pending' AND retry_count < ?
3997
- ORDER BY id`,
3998
- args: [MAX_RETRIES2]
3929
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
3930
+ const r2 = await client.execute({
3931
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3932
+ WHERE status = 'needs_review'
3933
+ AND result IS NOT NULL
3934
+ AND updated_at < ?`,
3935
+ args: [now, staleThreshold]
3999
3936
  });
4000
- let delivered = 0;
4001
- for (const row of result.rows) {
4002
- const msg = rowToMessage(row);
4003
- try {
4004
- const success = await deliverLocalMessage(msg.id);
4005
- if (success) delivered++;
4006
- } catch {
4007
- }
3937
+ const total = r1.rowsAffected + r2.rowsAffected;
3938
+ if (total > 0) {
3939
+ process.stderr.write(
3940
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
3941
+ `
3942
+ );
4008
3943
  }
4009
- return delivered;
3944
+ return total;
4010
3945
  }
4011
- var MAX_RETRIES2, _wsClientSend;
4012
- var init_messaging = __esm({
4013
- "src/lib/messaging.ts"() {
4014
- "use strict";
4015
- init_database();
4016
- init_tmux_routing();
4017
- MAX_RETRIES2 = 10;
4018
- _wsClientSend = null;
3946
+ function getReviewChecklist(role, agent, taskSlug) {
3947
+ const roleLower = role.toLowerCase();
3948
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
3949
+ return {
3950
+ lens: "Code Quality (Engineer)",
3951
+ checklist: [
3952
+ "1. Do all tests pass? Any new tests needed?",
3953
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
3954
+ "3. Does it follow existing patterns and conventions in the codebase?",
3955
+ "4. Any regressions in the test suite?"
3956
+ ]
3957
+ };
4019
3958
  }
4020
- });
4021
-
4022
- // src/lib/notifications.ts
4023
- import crypto4 from "crypto";
4024
- import path12 from "path";
4025
- import os7 from "os";
4026
- import {
4027
- readFileSync as readFileSync10,
4028
- readdirSync as readdirSync2,
4029
- unlinkSync as unlinkSync3,
4030
- existsSync as existsSync11,
4031
- rmdirSync
4032
- } from "fs";
4033
- async function writeNotification(notification) {
3959
+ if (roleLower === "cto" || roleLower.includes("architect")) {
3960
+ return {
3961
+ lens: "Architecture (CTO)",
3962
+ checklist: [
3963
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
3964
+ "2. Is it backward compatible? Any breaking changes?",
3965
+ "3. Does it introduce technical debt? Is that debt justified?",
3966
+ "4. Security implications? Any new attack surface?",
3967
+ "5. Does it scale? Performance considerations?",
3968
+ "6. Coordination: does this affect other employees' work or other projects?"
3969
+ ]
3970
+ };
3971
+ }
3972
+ if (roleLower === "coo" || roleLower.includes("operations")) {
3973
+ return {
3974
+ lens: "Strategic (COO)",
3975
+ checklist: [
3976
+ "1. Does this serve the project mission?",
3977
+ "2. Is this the right work at the right time?",
3978
+ "3. Does the architectural assessment make sense for the business?",
3979
+ "4. Any cross-project implications?"
3980
+ ]
3981
+ };
3982
+ }
3983
+ return {
3984
+ lens: "General",
3985
+ checklist: [
3986
+ "1. Read the original task's acceptance criteria",
3987
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
3988
+ "3. Verify code changes match requirements",
3989
+ "4. Check if tests were added/updated",
3990
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
3991
+ ]
3992
+ };
3993
+ }
3994
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
3995
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4034
3996
  try {
4035
3997
  const client = getClient();
4036
- const id = crypto4.randomUUID();
4037
3998
  const now = (/* @__PURE__ */ new Date()).toISOString();
4038
- await client.execute({
4039
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
4040
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
4041
- args: [
4042
- id,
4043
- notification.agentId,
4044
- notification.agentRole,
4045
- notification.event,
4046
- notification.project,
4047
- notification.summary,
4048
- notification.taskFile ?? null,
4049
- now
4050
- ]
4051
- });
3999
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
4000
+ if (parentId) {
4001
+ const result = await client.execute({
4002
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
4003
+ args: [now, parentId]
4004
+ });
4005
+ if (result.rowsAffected > 0) {
4006
+ process.stderr.write(
4007
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
4008
+ `
4009
+ );
4010
+ }
4011
+ } else {
4012
+ const fileName = taskFile.split("/").pop() ?? "";
4013
+ const reviewPrefix = fileName.replace(".md", "");
4014
+ const parts = reviewPrefix.split("-");
4015
+ if (parts.length >= 3 && parts[0] === "review") {
4016
+ const agent = parts[1];
4017
+ const slug = parts.slice(2).join("-");
4018
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
4019
+ const result = await client.execute({
4020
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
4021
+ args: [now, originalTaskFile]
4022
+ });
4023
+ if (result.rowsAffected > 0) {
4024
+ process.stderr.write(
4025
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
4026
+ `
4027
+ );
4028
+ }
4029
+ }
4030
+ }
4052
4031
  } catch (err) {
4053
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
4054
- `);
4032
+ process.stderr.write(
4033
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
4034
+ `
4035
+ );
4055
4036
  }
4056
- }
4057
- async function markAsReadByTaskFile(taskFile) {
4058
4037
  try {
4059
- const client = getClient();
4060
- await client.execute({
4061
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
4062
- args: [taskFile]
4063
- });
4038
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
4039
+ if (existsSync12(cacheDir)) {
4040
+ for (const f of readdirSync3(cacheDir)) {
4041
+ if (f.startsWith("review-notified-")) {
4042
+ unlinkSync3(path13.join(cacheDir, f));
4043
+ }
4044
+ }
4045
+ }
4064
4046
  } catch {
4065
4047
  }
4066
4048
  }
4067
- var init_notifications = __esm({
4068
- "src/lib/notifications.ts"() {
4049
+ var init_tasks_review = __esm({
4050
+ "src/lib/tasks-review.ts"() {
4069
4051
  "use strict";
4070
4052
  init_database();
4053
+ init_config();
4054
+ init_employees();
4055
+ init_notifications();
4056
+ init_tasks_crud();
4057
+ init_tmux_routing();
4058
+ init_session_key();
4059
+ init_state_bus();
4071
4060
  }
4072
4061
  });
4073
4062
 
4074
- // src/lib/tasks-crud.ts
4075
- import crypto5 from "crypto";
4076
- import path13 from "path";
4077
- import { execSync as execSync5 } from "child_process";
4078
- import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
4079
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
4080
- async function writeCheckpoint(input) {
4063
+ // src/lib/tasks-chain.ts
4064
+ import path14 from "path";
4065
+ import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
4066
+ async function cascadeUnblock(taskId, baseDir, now) {
4081
4067
  const client = getClient();
4082
- const row = await resolveTask(client, input.taskId);
4083
- const taskId = String(row.id);
4084
- const now = (/* @__PURE__ */ new Date()).toISOString();
4085
- const blockedByIds = [];
4086
- if (row.blocked_by) {
4087
- blockedByIds.push(String(row.blocked_by));
4088
- }
4089
- const checkpoint = {
4090
- step: input.step,
4091
- context_summary: input.contextSummary,
4092
- files_touched: input.filesTouched ?? [],
4093
- blocked_by_ids: blockedByIds,
4094
- last_checkpoint_at: now
4095
- };
4096
- const result = await client.execute({
4097
- sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
4098
- args: [JSON.stringify(checkpoint), now, taskId]
4068
+ const unblocked = await client.execute({
4069
+ sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
4070
+ WHERE blocked_by = ? AND status = 'blocked'`,
4071
+ args: [now, taskId]
4099
4072
  });
4100
- if (result.rowsAffected === 0) {
4101
- throw new Error(`Checkpoint write failed: task ${taskId} not found`);
4073
+ if (baseDir && unblocked.rowsAffected > 0) {
4074
+ const unblockedRows = await client.execute({
4075
+ sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
4076
+ args: [now]
4077
+ });
4078
+ for (const ur of unblockedRows.rows) {
4079
+ try {
4080
+ const ubFile = path14.join(baseDir, String(ur.task_file));
4081
+ let ubContent = await readFile4(ubFile, "utf-8");
4082
+ ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
4083
+ ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
4084
+ await writeFile5(ubFile, ubContent, "utf-8");
4085
+ } catch {
4086
+ }
4087
+ }
4102
4088
  }
4103
- const countResult = await client.execute({
4104
- sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
4105
- args: [taskId]
4106
- });
4107
- const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
4108
- return { checkpointCount };
4109
- }
4110
- function extractParentFromContext(contextBody) {
4111
- if (!contextBody) return null;
4112
- const match = contextBody.match(
4113
- /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
4114
- );
4115
- return match ? match[1].toLowerCase() : null;
4116
- }
4117
- function slugify(title) {
4118
- return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4119
4089
  }
4120
- async function resolveTask(client, identifier) {
4121
- let result = await client.execute({
4122
- sql: "SELECT * FROM tasks WHERE id = ?",
4123
- args: [identifier]
4124
- });
4125
- if (result.rows.length === 1) return result.rows[0];
4126
- result = await client.execute({
4127
- sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
4128
- args: [`%${identifier}%`]
4129
- });
4130
- if (result.rows.length === 1) return result.rows[0];
4131
- if (result.rows.length > 1) {
4132
- const exact = result.rows.filter(
4133
- (r) => String(r.task_file).endsWith(`/${identifier}.md`)
4134
- );
4135
- if (exact.length === 1) return exact[0];
4136
- const candidates = exact.length > 1 ? exact : result.rows;
4137
- const active = candidates.filter(
4138
- (r) => !["done", "cancelled"].includes(String(r.status))
4139
- );
4140
- if (active.length === 1) return active[0];
4141
- const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
4142
- throw new Error(
4143
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
4144
- );
4145
- }
4146
- result = await client.execute({
4147
- sql: "SELECT * FROM tasks WHERE title LIKE ?",
4148
- args: [`%${identifier}%`]
4090
+ async function findNextTask(assignedTo) {
4091
+ const client = getClient();
4092
+ const nextResult = await client.execute({
4093
+ sql: `SELECT title, task_file, priority FROM tasks
4094
+ WHERE assigned_to = ? AND status = 'open'
4095
+ ORDER BY priority ASC, created_at ASC
4096
+ LIMIT 1`,
4097
+ args: [assignedTo]
4149
4098
  });
4150
- if (result.rows.length === 1) return result.rows[0];
4151
- if (result.rows.length > 1) {
4152
- const active = result.rows.filter(
4153
- (r) => !["done", "cancelled"].includes(String(r.status))
4154
- );
4155
- if (active.length === 1) return active[0];
4156
- const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
4157
- throw new Error(
4158
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
4159
- );
4099
+ if (nextResult.rows.length === 1) {
4100
+ const nr = nextResult.rows[0];
4101
+ return {
4102
+ title: String(nr.title),
4103
+ priority: String(nr.priority),
4104
+ taskFile: String(nr.task_file)
4105
+ };
4160
4106
  }
4161
- throw new Error(`Task not found: ${identifier}`);
4107
+ return void 0;
4162
4108
  }
4163
- async function createTaskCore(input) {
4109
+ async function checkSubtaskCompletion(parentTaskId, projectName) {
4164
4110
  const client = getClient();
4165
- const id = crypto5.randomUUID();
4166
- const now = (/* @__PURE__ */ new Date()).toISOString();
4167
- const slug = slugify(input.title);
4168
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
4169
- let blockedById = null;
4170
- const initialStatus = input.blockedBy ? "blocked" : "open";
4171
- if (input.blockedBy) {
4172
- const blocker = await resolveTask(client, input.blockedBy);
4173
- blockedById = String(blocker.id);
4174
- }
4175
- let parentTaskId = null;
4176
- let parentRef = input.parentTaskId;
4177
- if (!parentRef) {
4178
- const extracted = extractParentFromContext(input.context);
4179
- if (extracted) {
4180
- parentRef = extracted;
4181
- process.stderr.write(
4182
- "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
4183
- );
4184
- }
4185
- }
4186
- if (parentRef) {
4187
- try {
4188
- const parent = await resolveTask(client, parentRef);
4189
- parentTaskId = String(parent.id);
4190
- } catch (err) {
4191
- if (!input.parentTaskId) {
4192
- throw new Error(
4193
- `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
4194
- );
4195
- }
4196
- throw err;
4111
+ const remaining = await client.execute({
4112
+ sql: `SELECT COUNT(*) as cnt FROM tasks
4113
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
4114
+ args: [parentTaskId]
4115
+ });
4116
+ const cnt = Number(remaining.rows[0]?.cnt ?? 1);
4117
+ if (cnt === 0) {
4118
+ const parentRow = await client.execute({
4119
+ sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
4120
+ args: [parentTaskId]
4121
+ });
4122
+ if (parentRow.rows.length === 1) {
4123
+ const pr = parentRow.rows[0];
4124
+ const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
4125
+ await writeNotification({
4126
+ agentId: String(pr.assigned_to),
4127
+ agentRole: "system",
4128
+ event: "subtasks_complete",
4129
+ project: parentProject,
4130
+ summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
4131
+ taskFile: String(pr.task_file)
4132
+ });
4197
4133
  }
4198
4134
  }
4199
- let warning;
4200
- const dupCheck = await client.execute({
4201
- sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
4202
- args: [input.title, input.assignedTo]
4203
- });
4204
- if (dupCheck.rows.length > 0) {
4205
- warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
4135
+ }
4136
+ var init_tasks_chain = __esm({
4137
+ "src/lib/tasks-chain.ts"() {
4138
+ "use strict";
4139
+ init_database();
4140
+ init_notifications();
4206
4141
  }
4207
- if (input.baseDir) {
4142
+ });
4143
+
4144
+ // src/lib/project-name.ts
4145
+ import { execSync as execSync5 } from "child_process";
4146
+ import path15 from "path";
4147
+ function getProjectName(cwd) {
4148
+ const dir = cwd ?? process.cwd();
4149
+ if (_cached2 && _cachedCwd === dir) return _cached2;
4150
+ try {
4151
+ let repoRoot;
4208
4152
  try {
4209
- await mkdir4(path13.join(input.baseDir, "exe", "output"), { recursive: true });
4210
- await mkdir4(path13.join(input.baseDir, "exe", "research"), { recursive: true });
4211
- await ensureArchitectureDoc(input.baseDir, input.projectName);
4212
- await ensureGitignoreExe(input.baseDir);
4153
+ const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
4154
+ cwd: dir,
4155
+ encoding: "utf8",
4156
+ timeout: 2e3,
4157
+ stdio: ["pipe", "pipe", "pipe"]
4158
+ }).trim();
4159
+ repoRoot = path15.dirname(gitCommonDir);
4213
4160
  } catch {
4161
+ repoRoot = execSync5("git rev-parse --show-toplevel", {
4162
+ cwd: dir,
4163
+ encoding: "utf8",
4164
+ timeout: 2e3,
4165
+ stdio: ["pipe", "pipe", "pipe"]
4166
+ }).trim();
4214
4167
  }
4168
+ _cached2 = path15.basename(repoRoot);
4169
+ _cachedCwd = dir;
4170
+ return _cached2;
4171
+ } catch {
4172
+ _cached2 = path15.basename(dir);
4173
+ _cachedCwd = dir;
4174
+ return _cached2;
4215
4175
  }
4216
- const complexity = input.complexity ?? "standard";
4217
- await client.execute({
4218
- 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)
4219
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4220
- args: [
4221
- id,
4222
- input.title,
4223
- input.assignedTo,
4224
- input.assignedBy,
4225
- input.projectName,
4226
- input.priority,
4227
- initialStatus,
4228
- taskFile,
4229
- blockedById,
4230
- parentTaskId,
4231
- input.reviewer ?? null,
4232
- input.context,
4233
- complexity,
4234
- input.budgetTokens ?? null,
4235
- input.budgetFallbackModel ?? null,
4236
- 0,
4237
- null,
4238
- now,
4239
- now
4240
- ]
4241
- });
4242
- return {
4243
- id,
4244
- title: input.title,
4245
- assignedTo: input.assignedTo,
4246
- assignedBy: input.assignedBy,
4247
- projectName: input.projectName,
4248
- priority: input.priority,
4249
- status: initialStatus,
4250
- taskFile,
4251
- createdAt: now,
4252
- updatedAt: now,
4253
- warning,
4254
- budgetTokens: input.budgetTokens ?? null,
4255
- budgetFallbackModel: input.budgetFallbackModel ?? null,
4256
- tokensUsed: 0,
4257
- tokensWarnedAt: null
4258
- };
4259
4176
  }
4260
- async function listTasks(input) {
4261
- const client = getClient();
4262
- const conditions = [];
4263
- const args = [];
4264
- if (input.assignedTo) {
4265
- conditions.push("assigned_to = ?");
4266
- args.push(input.assignedTo);
4177
+ var _cached2, _cachedCwd;
4178
+ var init_project_name = __esm({
4179
+ "src/lib/project-name.ts"() {
4180
+ "use strict";
4181
+ _cached2 = null;
4182
+ _cachedCwd = null;
4267
4183
  }
4268
- if (input.status) {
4269
- conditions.push("status = ?");
4270
- args.push(input.status);
4271
- } else {
4272
- conditions.push("status IN ('open', 'in_progress', 'blocked')");
4273
- }
4274
- if (input.projectName) {
4275
- conditions.push("project_name = ?");
4276
- args.push(input.projectName);
4277
- }
4278
- if (input.priority) {
4279
- conditions.push("priority = ?");
4280
- args.push(input.priority);
4184
+ });
4185
+
4186
+ // src/lib/session-scope.ts
4187
+ var session_scope_exports = {};
4188
+ __export(session_scope_exports, {
4189
+ assertSessionScope: () => assertSessionScope,
4190
+ findSessionForProject: () => findSessionForProject,
4191
+ getSessionProject: () => getSessionProject
4192
+ });
4193
+ function getSessionProject(sessionName) {
4194
+ const sessions = listSessions();
4195
+ const entry = sessions.find((s) => s.windowName === sessionName);
4196
+ if (!entry) return null;
4197
+ const parts = entry.projectDir.split("/").filter(Boolean);
4198
+ return parts[parts.length - 1] ?? null;
4199
+ }
4200
+ function findSessionForProject(projectName) {
4201
+ const sessions = listSessions();
4202
+ for (const s of sessions) {
4203
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
4204
+ if (proj === projectName && s.agentId === "exe") return s;
4281
4205
  }
4282
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4283
- const result = await client.execute({
4284
- 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`,
4285
- args
4286
- });
4287
- return result.rows.map((r) => ({
4288
- id: String(r.id),
4289
- title: String(r.title),
4290
- assignedTo: String(r.assigned_to),
4291
- assignedBy: String(r.assigned_by),
4292
- projectName: String(r.project_name),
4293
- priority: String(r.priority),
4294
- status: String(r.status),
4295
- taskFile: String(r.task_file),
4296
- createdAt: String(r.created_at),
4297
- updatedAt: String(r.updated_at),
4298
- checkpointCount: Number(r.checkpoint_count ?? 0),
4299
- budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
4300
- budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
4301
- tokensUsed: Number(r.tokens_used ?? 0),
4302
- tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
4303
- }));
4206
+ return null;
4304
4207
  }
4305
- function checkStaleCompletion(taskContext, taskCreatedAt) {
4306
- if (!taskContext) return null;
4307
- if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
4208
+ function assertSessionScope(actionType, targetProject) {
4308
4209
  try {
4309
- const since = new Date(taskCreatedAt).toISOString();
4310
- const branch = execSync5(
4311
- "git rev-parse --abbrev-ref HEAD 2>/dev/null",
4312
- { encoding: "utf8", timeout: 3e3 }
4313
- ).trim();
4314
- const branchArg = branch && branch !== "HEAD" ? branch : "";
4315
- const commitCount = execSync5(
4316
- `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
4317
- { encoding: "utf8", timeout: 5e3 }
4318
- ).trim();
4319
- const count = parseInt(commitCount, 10);
4320
- if (count === 0) {
4321
- return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
4210
+ const currentProject = getProjectName();
4211
+ const exeSession = resolveExeSession();
4212
+ if (!exeSession) {
4213
+ return { allowed: true, reason: "no_session" };
4214
+ }
4215
+ if (currentProject === targetProject) {
4216
+ return {
4217
+ allowed: true,
4218
+ reason: "same_session",
4219
+ currentProject,
4220
+ targetProject
4221
+ };
4322
4222
  }
4323
- return null;
4324
- } catch {
4325
- return null;
4326
- }
4327
- }
4328
- async function updateTaskStatus(input) {
4329
- const client = getClient();
4330
- const now = (/* @__PURE__ */ new Date()).toISOString();
4331
- const row = await resolveTask(client, input.taskId);
4332
- const taskId = String(row.id);
4333
- const taskFile = String(row.task_file);
4334
- if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
4335
4223
  process.stderr.write(
4336
- `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
4224
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
4337
4225
  `
4338
4226
  );
4227
+ return {
4228
+ allowed: false,
4229
+ reason: "cross_session_denied",
4230
+ currentProject,
4231
+ targetProject,
4232
+ targetSession: findSessionForProject(targetProject)?.windowName
4233
+ };
4234
+ } catch {
4235
+ return { allowed: true, reason: "no_session" };
4339
4236
  }
4340
- if (input.status === "done") {
4341
- const existingRow = await client.execute({
4342
- sql: "SELECT context, created_at FROM tasks WHERE id = ?",
4343
- args: [taskId]
4344
- });
4345
- if (existingRow.rows.length > 0) {
4346
- const ctx = existingRow.rows[0];
4347
- const warning = checkStaleCompletion(ctx.context, ctx.created_at);
4348
- if (warning) {
4349
- input.result = input.result ? `\u26A0\uFE0F ${warning}
4350
-
4351
- ${input.result}` : `\u26A0\uFE0F ${warning}`;
4352
- process.stderr.write(`[tasks] ${warning} (task: ${taskId})
4353
- `);
4354
- }
4355
- }
4237
+ }
4238
+ var init_session_scope = __esm({
4239
+ "src/lib/session-scope.ts"() {
4240
+ "use strict";
4241
+ init_session_registry();
4242
+ init_project_name();
4243
+ init_tmux_routing();
4356
4244
  }
4357
- if (input.status === "in_progress") {
4358
- const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
4359
- const claim = await client.execute({
4360
- sql: `UPDATE tasks
4361
- SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
4362
- WHERE id = ? AND status = 'open'`,
4363
- args: [tmuxSession, now, taskId]
4364
- });
4365
- if (claim.rowsAffected === 0) {
4366
- const current = await client.execute({
4367
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
4368
- args: [taskId]
4369
- });
4370
- const cur = current.rows[0];
4371
- const status = cur?.status ?? "unknown";
4372
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
4373
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
4374
- }
4245
+ });
4246
+
4247
+ // src/lib/tasks-notify.ts
4248
+ async function dispatchTaskToEmployee(input) {
4249
+ if (input.assignedTo === "exe") return { dispatched: "skipped" };
4250
+ let crossProject = false;
4251
+ if (input.projectName) {
4375
4252
  try {
4376
- await writeCheckpoint({
4377
- taskId,
4378
- step: "claimed",
4379
- contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
4380
- });
4253
+ const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
4254
+ const check = assertSessionScope2("dispatch_task", input.projectName);
4255
+ if (check.reason === "cross_session_denied") {
4256
+ crossProject = true;
4257
+ return { dispatched: "skipped", crossProject: true };
4258
+ }
4381
4259
  } catch {
4382
4260
  }
4383
- return { row, taskFile, now, taskId };
4384
- }
4385
- if (input.result) {
4386
- await client.execute({
4387
- sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
4388
- args: [input.status, input.result, now, taskId]
4389
- });
4390
- } else {
4391
- await client.execute({
4392
- sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
4393
- args: [input.status, now, taskId]
4394
- });
4395
4261
  }
4396
4262
  try {
4397
- await writeCheckpoint({
4398
- taskId,
4399
- step: `status_transition:${input.status}`,
4400
- contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
4401
- });
4263
+ const transport = getTransport();
4264
+ const exeSession = resolveExeSession();
4265
+ if (!exeSession) return { dispatched: "session_missing" };
4266
+ const sessionName = employeeSessionName(input.assignedTo, exeSession);
4267
+ if (transport.isAlive(sessionName)) {
4268
+ const result = sendIntercom(sessionName);
4269
+ const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
4270
+ return { dispatched, session: sessionName, crossProject };
4271
+ } else {
4272
+ const projectDir = input.projectDir ?? process.cwd();
4273
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
4274
+ autoInstance: isMultiInstance(input.assignedTo)
4275
+ });
4276
+ if (result.status === "failed") {
4277
+ process.stderr.write(
4278
+ `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
4279
+ `
4280
+ );
4281
+ return { dispatched: "session_missing" };
4282
+ }
4283
+ return { dispatched: "spawned", session: result.sessionName, crossProject };
4284
+ }
4402
4285
  } catch {
4286
+ return { dispatched: "session_missing" };
4403
4287
  }
4404
- return { row, taskFile, now, taskId };
4405
- }
4406
- async function deleteTaskCore(taskId, _baseDir) {
4407
- const client = getClient();
4408
- const row = await resolveTask(client, taskId);
4409
- const id = String(row.id);
4410
- const taskFile = String(row.task_file);
4411
- const assignedTo = String(row.assigned_to);
4412
- const assignedBy = String(row.assigned_by);
4413
- await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
4414
- const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
4415
- return { taskFile, assignedTo, assignedBy, taskSlug };
4416
4288
  }
4417
- async function ensureArchitectureDoc(baseDir, projectName) {
4418
- const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
4289
+ function notifyTaskDone() {
4419
4290
  try {
4420
- if (existsSync12(archPath)) return;
4421
- const template = [
4422
- `# ${projectName} \u2014 System Architecture`,
4423
- "",
4424
- "> Employees: read this before every task. Update it when you change system structure.",
4425
- `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
4426
- "",
4427
- "## Overview",
4428
- "",
4429
- "<!-- Describe what this system does, its main components, and how they connect. -->",
4430
- "",
4431
- "## Key Components",
4432
- "",
4433
- "<!-- List the major modules, services, or subsystems. -->",
4434
- "",
4435
- "## Data Flow",
4436
- "",
4437
- "<!-- How does data move through the system? What writes where? -->",
4438
- "",
4439
- "## Invariants",
4440
- "",
4441
- "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
4442
- "",
4443
- "## Dependencies",
4444
- "",
4445
- "<!-- What depends on what? If I change X, what else is affected? -->",
4446
- ""
4447
- ].join("\n");
4448
- await writeFile4(archPath, template, "utf-8");
4291
+ const key = getSessionKey();
4292
+ if (key && !process.env.VITEST) notifyParentExe(key);
4449
4293
  } catch {
4450
4294
  }
4451
4295
  }
4452
- async function ensureGitignoreExe(baseDir) {
4453
- const gitignorePath = path13.join(baseDir, ".gitignore");
4296
+ async function markTaskNotificationsRead(taskFile) {
4454
4297
  try {
4455
- if (existsSync12(gitignorePath)) {
4456
- const content = readFileSync11(gitignorePath, "utf-8");
4457
- if (/^\/?exe\/?$/m.test(content)) return;
4458
- await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
4459
- } else {
4460
- await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
4461
- }
4298
+ await markAsReadByTaskFile(taskFile);
4462
4299
  } catch {
4463
4300
  }
4464
4301
  }
4465
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4466
- var init_tasks_crud = __esm({
4467
- "src/lib/tasks-crud.ts"() {
4302
+ var init_tasks_notify = __esm({
4303
+ "src/lib/tasks-notify.ts"() {
4468
4304
  "use strict";
4469
- init_database();
4470
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
4471
- TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4305
+ init_tmux_routing();
4306
+ init_session_key();
4307
+ init_notifications();
4308
+ init_transport();
4309
+ init_employees();
4472
4310
  }
4473
4311
  });
4474
4312
 
4475
- // src/lib/tasks-review.ts
4476
- import path14 from "path";
4477
- import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
4478
- async function countPendingReviews() {
4479
- const client = getClient();
4480
- const result = await client.execute({
4481
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
4482
- args: []
4483
- });
4484
- return Number(result.rows[0]?.cnt) || 0;
4485
- }
4486
- async function countNewPendingReviewsSince(sinceIso) {
4487
- const client = getClient();
4488
- const result = await client.execute({
4489
- sql: `SELECT COUNT(*) as cnt FROM tasks
4490
- WHERE status = 'needs_review' AND updated_at > ?`,
4491
- args: [sinceIso]
4492
- });
4493
- return Number(result.rows[0]?.cnt) || 0;
4494
- }
4495
- async function listPendingReviews(limit) {
4313
+ // src/lib/behaviors.ts
4314
+ import crypto6 from "crypto";
4315
+ async function storeBehavior(opts) {
4496
4316
  const client = getClient();
4497
- const result = await client.execute({
4498
- sql: `SELECT title, assigned_to, project_name FROM tasks
4499
- WHERE status = 'needs_review'
4500
- ORDER BY priority ASC, created_at DESC LIMIT ?`,
4501
- args: [limit]
4317
+ const id = crypto6.randomUUID();
4318
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4319
+ await client.execute({
4320
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
4321
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
4322
+ args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
4502
4323
  });
4503
- return result.rows;
4504
- }
4505
- function getReviewChecklist(role, agent, taskSlug) {
4506
- const roleLower = role.toLowerCase();
4507
- if (roleLower.includes("engineer") || roleLower === "principal engineer") {
4508
- return {
4509
- lens: "Code Quality (Engineer)",
4510
- checklist: [
4511
- "1. Do all tests pass? Any new tests needed?",
4512
- "2. Is the code clean \u2014 no dead code, no TODOs left?",
4513
- "3. Does it follow existing patterns and conventions in the codebase?",
4514
- "4. Any regressions in the test suite?"
4515
- ]
4516
- };
4517
- }
4518
- if (roleLower === "cto" || roleLower.includes("architect")) {
4519
- return {
4520
- lens: "Architecture (CTO)",
4521
- checklist: [
4522
- "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
4523
- "2. Is it backward compatible? Any breaking changes?",
4524
- "3. Does it introduce technical debt? Is that debt justified?",
4525
- "4. Security implications? Any new attack surface?",
4526
- "5. Does it scale? Performance considerations?",
4527
- "6. Coordination: does this affect other employees' work or other projects?"
4528
- ]
4529
- };
4530
- }
4531
- if (roleLower === "coo" || roleLower.includes("operations")) {
4532
- return {
4533
- lens: "Strategic (COO)",
4534
- checklist: [
4535
- "1. Does this serve the project mission?",
4536
- "2. Is this the right work at the right time?",
4537
- "3. Does the architectural assessment make sense for the business?",
4538
- "4. Any cross-project implications?"
4539
- ]
4540
- };
4541
- }
4542
- return {
4543
- lens: "General",
4544
- checklist: [
4545
- "1. Read the original task's acceptance criteria",
4546
- `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
4547
- "3. Verify code changes match requirements",
4548
- "4. Check if tests were added/updated",
4549
- `5. Look for output files in exe/output/${agent}-${taskSlug}*`
4550
- ]
4551
- };
4552
- }
4553
- async function cleanupReviewFile(row, taskFile, _baseDir) {
4554
- if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4555
- try {
4556
- const client = getClient();
4557
- const now = (/* @__PURE__ */ new Date()).toISOString();
4558
- const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
4559
- if (parentId) {
4560
- const result = await client.execute({
4561
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
4562
- args: [now, parentId]
4563
- });
4564
- if (result.rowsAffected > 0) {
4565
- process.stderr.write(
4566
- `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
4567
- `
4568
- );
4569
- }
4570
- } else {
4571
- const fileName = taskFile.split("/").pop() ?? "";
4572
- const reviewPrefix = fileName.replace(".md", "");
4573
- const parts = reviewPrefix.split("-");
4574
- if (parts.length >= 3 && parts[0] === "review") {
4575
- const agent = parts[1];
4576
- const slug = parts.slice(2).join("-");
4577
- const originalTaskFile = `exe/${agent}/${slug}.md`;
4578
- const result = await client.execute({
4579
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
4580
- args: [now, originalTaskFile]
4581
- });
4582
- if (result.rowsAffected > 0) {
4583
- process.stderr.write(
4584
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
4585
- `
4586
- );
4587
- }
4588
- }
4589
- }
4590
- } catch (err) {
4591
- process.stderr.write(
4592
- `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
4593
- `
4594
- );
4595
- }
4596
- try {
4597
- const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
4598
- if (existsSync13(cacheDir)) {
4599
- for (const f of readdirSync3(cacheDir)) {
4600
- if (f.startsWith("review-notified-")) {
4601
- unlinkSync4(path14.join(cacheDir, f));
4602
- }
4603
- }
4604
- }
4605
- } catch {
4606
- }
4324
+ return id;
4607
4325
  }
4608
- var init_tasks_review = __esm({
4609
- "src/lib/tasks-review.ts"() {
4326
+ var init_behaviors = __esm({
4327
+ "src/lib/behaviors.ts"() {
4610
4328
  "use strict";
4611
4329
  init_database();
4612
- init_config();
4613
- init_employees();
4614
- init_notifications();
4615
- init_tasks_crud();
4616
- init_tmux_routing();
4617
- init_session_key();
4618
4330
  }
4619
4331
  });
4620
4332
 
4621
- // src/lib/tasks-chain.ts
4622
- import path15 from "path";
4623
- import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
4624
- async function cascadeUnblock(taskId, baseDir, now) {
4333
+ // src/lib/skill-learning.ts
4334
+ var skill_learning_exports = {};
4335
+ __export(skill_learning_exports, {
4336
+ captureAndLearn: () => captureAndLearn,
4337
+ captureTrajectory: () => captureTrajectory,
4338
+ editDistance: () => editDistance,
4339
+ extractSkill: () => extractSkill,
4340
+ extractTrajectory: () => extractTrajectory,
4341
+ findSimilarTrajectories: () => findSimilarTrajectories,
4342
+ hashSignature: () => hashSignature,
4343
+ storeTrajectory: () => storeTrajectory,
4344
+ sweepTrajectories: () => sweepTrajectories
4345
+ });
4346
+ import crypto7 from "crypto";
4347
+ async function extractTrajectory(taskId, agentId) {
4625
4348
  const client = getClient();
4626
- const unblocked = await client.execute({
4627
- sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
4628
- WHERE blocked_by = ? AND status = 'blocked'`,
4629
- args: [now, taskId]
4349
+ const result = await client.execute({
4350
+ sql: `SELECT tool_name, raw_text
4351
+ FROM memories
4352
+ WHERE task_id = ? AND agent_id = ?
4353
+ ORDER BY timestamp ASC`,
4354
+ args: [taskId, agentId]
4630
4355
  });
4631
- if (baseDir && unblocked.rowsAffected > 0) {
4632
- const unblockedRows = await client.execute({
4633
- sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
4634
- args: [now]
4635
- });
4636
- for (const ur of unblockedRows.rows) {
4637
- try {
4638
- const ubFile = path15.join(baseDir, String(ur.task_file));
4639
- let ubContent = await readFile4(ubFile, "utf-8");
4640
- ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
4641
- ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
4642
- await writeFile5(ubFile, ubContent, "utf-8");
4643
- } catch {
4644
- }
4356
+ if (result.rows.length === 0) return [];
4357
+ const rawTools = result.rows.map((r) => {
4358
+ const toolName = String(r.tool_name);
4359
+ if (toolName === "Bash") {
4360
+ const text = String(r.raw_text);
4361
+ const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
4362
+ return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
4363
+ }
4364
+ return toolName;
4365
+ });
4366
+ const signature = [];
4367
+ for (const tool of rawTools) {
4368
+ if (signature.length === 0 || signature[signature.length - 1] !== tool) {
4369
+ signature.push(tool);
4645
4370
  }
4646
4371
  }
4372
+ return signature;
4647
4373
  }
4648
- async function findNextTask(assignedTo) {
4374
+ function hashSignature(signature) {
4375
+ return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4376
+ }
4377
+ async function storeTrajectory(opts) {
4649
4378
  const client = getClient();
4650
- const nextResult = await client.execute({
4651
- sql: `SELECT title, task_file, priority FROM tasks
4652
- WHERE assigned_to = ? AND status = 'open'
4653
- ORDER BY priority ASC, created_at ASC
4654
- LIMIT 1`,
4655
- args: [assignedTo]
4379
+ const id = crypto7.randomUUID();
4380
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4381
+ const signatureHash = hashSignature(opts.signature);
4382
+ await client.execute({
4383
+ sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
4384
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4385
+ args: [
4386
+ id,
4387
+ opts.taskId,
4388
+ opts.agentId,
4389
+ opts.projectName,
4390
+ opts.taskTitle,
4391
+ JSON.stringify(opts.signature),
4392
+ signatureHash,
4393
+ opts.signature.length,
4394
+ now
4395
+ ]
4656
4396
  });
4657
- if (nextResult.rows.length === 1) {
4658
- const nr = nextResult.rows[0];
4659
- return {
4660
- title: String(nr.title),
4661
- priority: String(nr.priority),
4662
- taskFile: String(nr.task_file)
4663
- };
4664
- }
4665
- return void 0;
4397
+ return id;
4666
4398
  }
4667
- async function checkSubtaskCompletion(parentTaskId, projectName) {
4399
+ async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
4668
4400
  const client = getClient();
4669
- const remaining = await client.execute({
4670
- sql: `SELECT COUNT(*) as cnt FROM tasks
4671
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
4672
- args: [parentTaskId]
4401
+ const hash = hashSignature(signature);
4402
+ const result = await client.execute({
4403
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
4404
+ FROM trajectories
4405
+ WHERE signature_hash = ?
4406
+ ORDER BY created_at DESC
4407
+ LIMIT 20`,
4408
+ args: [hash]
4673
4409
  });
4674
- const cnt = Number(remaining.rows[0]?.cnt ?? 1);
4675
- if (cnt === 0) {
4676
- const parentRow = await client.execute({
4677
- sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
4678
- args: [parentTaskId]
4679
- });
4680
- if (parentRow.rows.length === 1) {
4681
- const pr = parentRow.rows[0];
4682
- const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
4683
- await writeNotification({
4684
- agentId: String(pr.assigned_to),
4685
- agentRole: "system",
4686
- event: "subtasks_complete",
4687
- project: parentProject,
4688
- summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
4689
- taskFile: String(pr.task_file)
4690
- });
4410
+ const mapRow = (r) => ({
4411
+ id: String(r.id),
4412
+ taskId: String(r.task_id),
4413
+ agentId: String(r.agent_id),
4414
+ projectName: String(r.project_name),
4415
+ taskTitle: String(r.task_title),
4416
+ signature: JSON.parse(String(r.signature)),
4417
+ signatureHash: String(r.signature_hash),
4418
+ toolCount: Number(r.tool_count),
4419
+ skillId: r.skill_id ? String(r.skill_id) : null,
4420
+ createdAt: String(r.created_at)
4421
+ });
4422
+ const matches = result.rows.map(mapRow);
4423
+ if (matches.length >= threshold) return matches;
4424
+ const nearResult = await client.execute({
4425
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
4426
+ FROM trajectories
4427
+ WHERE tool_count BETWEEN ? AND ?
4428
+ AND signature_hash != ?
4429
+ ORDER BY created_at DESC
4430
+ LIMIT 50`,
4431
+ args: [
4432
+ Math.max(1, signature.length - 3),
4433
+ signature.length + 3,
4434
+ hash
4435
+ ]
4436
+ });
4437
+ for (const r of nearResult.rows) {
4438
+ const candidateSig = JSON.parse(String(r.signature));
4439
+ if (editDistance(signature, candidateSig) <= 2) {
4440
+ matches.push(mapRow(r));
4691
4441
  }
4692
4442
  }
4443
+ return matches;
4693
4444
  }
4694
- var init_tasks_chain = __esm({
4695
- "src/lib/tasks-chain.ts"() {
4696
- "use strict";
4697
- init_database();
4698
- init_notifications();
4699
- }
4700
- });
4701
-
4702
- // src/lib/project-name.ts
4703
- import { execSync as execSync6 } from "child_process";
4704
- import path16 from "path";
4705
- function getProjectName(cwd) {
4706
- const dir = cwd ?? process.cwd();
4707
- if (_cached2 && _cachedCwd === dir) return _cached2;
4708
- try {
4709
- let repoRoot;
4710
- try {
4711
- const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
4712
- cwd: dir,
4713
- encoding: "utf8",
4714
- timeout: 2e3,
4715
- stdio: ["pipe", "pipe", "pipe"]
4716
- }).trim();
4717
- repoRoot = path16.dirname(gitCommonDir);
4718
- } catch {
4719
- repoRoot = execSync6("git rev-parse --show-toplevel", {
4720
- cwd: dir,
4721
- encoding: "utf8",
4722
- timeout: 2e3,
4723
- stdio: ["pipe", "pipe", "pipe"]
4724
- }).trim();
4725
- }
4726
- _cached2 = path16.basename(repoRoot);
4727
- _cachedCwd = dir;
4728
- return _cached2;
4729
- } catch {
4730
- _cached2 = path16.basename(dir);
4731
- _cachedCwd = dir;
4732
- return _cached2;
4445
+ async function captureTrajectory(opts) {
4446
+ const signature = await extractTrajectory(opts.taskId, opts.agentId);
4447
+ if (signature.length < 3) {
4448
+ return { trajectoryId: "", similarCount: 0, similar: [] };
4733
4449
  }
4450
+ const trajectoryId = await storeTrajectory({
4451
+ taskId: opts.taskId,
4452
+ agentId: opts.agentId,
4453
+ projectName: opts.projectName,
4454
+ taskTitle: opts.taskTitle,
4455
+ signature
4456
+ });
4457
+ const similar = await findSimilarTrajectories(
4458
+ signature,
4459
+ opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
4460
+ );
4461
+ return { trajectoryId, similarCount: similar.length, similar };
4734
4462
  }
4735
- var _cached2, _cachedCwd;
4736
- var init_project_name = __esm({
4737
- "src/lib/project-name.ts"() {
4738
- "use strict";
4739
- _cached2 = null;
4740
- _cachedCwd = null;
4741
- }
4742
- });
4463
+ function buildExtractionPrompt(trajectories) {
4464
+ const items = trajectories.map((t, i) => {
4465
+ const sig = t.signature.join(" \u2192 ");
4466
+ return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
4467
+ Signature: ${sig}`;
4468
+ }).join("\n\n");
4469
+ return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
4743
4470
 
4744
- // src/lib/session-scope.ts
4745
- var session_scope_exports = {};
4746
- __export(session_scope_exports, {
4747
- assertSessionScope: () => assertSessionScope,
4748
- findSessionForProject: () => findSessionForProject,
4749
- getSessionProject: () => getSessionProject
4750
- });
4751
- function getSessionProject(sessionName) {
4752
- const sessions = listSessions();
4753
- const entry = sessions.find((s) => s.windowName === sessionName);
4754
- if (!entry) return null;
4755
- const parts = entry.projectDir.split("/").filter(Boolean);
4756
- return parts[parts.length - 1] ?? null;
4471
+ ${items}
4472
+
4473
+ Extract the reusable procedure. Format your response EXACTLY like this:
4474
+
4475
+ SKILL: {name \u2014 short, descriptive}
4476
+ TRIGGER: {when to use this \u2014 one sentence}
4477
+ STEPS:
4478
+ 1. ...
4479
+ 2. ...
4480
+ PITFALLS: {common mistakes to avoid}
4481
+
4482
+ Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
4757
4483
  }
4758
- function findSessionForProject(projectName) {
4759
- const sessions = listSessions();
4760
- for (const s of sessions) {
4761
- const proj = s.projectDir.split("/").filter(Boolean).pop();
4762
- if (proj === projectName && s.agentId === "exe") return s;
4484
+ async function extractSkill(trajectories, model) {
4485
+ if (trajectories.length === 0) return null;
4486
+ const config2 = await loadConfig();
4487
+ const skillModel = model ?? config2.skillModel;
4488
+ const Anthropic4 = (await import("@anthropic-ai/sdk")).default;
4489
+ const client = new Anthropic4();
4490
+ const prompt = buildExtractionPrompt(trajectories);
4491
+ const response = await client.messages.create({
4492
+ model: skillModel,
4493
+ max_tokens: 500,
4494
+ messages: [{ role: "user", content: prompt }]
4495
+ });
4496
+ const textBlock = response.content.find((b) => b.type === "text");
4497
+ const skillText = textBlock?.text;
4498
+ if (!skillText) return null;
4499
+ const agentId = trajectories[0].agentId;
4500
+ const projectName = trajectories[0].projectName;
4501
+ const skillId = await storeBehavior({
4502
+ agentId,
4503
+ content: skillText,
4504
+ domain: "skill",
4505
+ projectName
4506
+ });
4507
+ const dbClient = getClient();
4508
+ for (const t of trajectories) {
4509
+ await dbClient.execute({
4510
+ sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
4511
+ args: [skillId, t.id]
4512
+ });
4763
4513
  }
4764
- return null;
4514
+ process.stderr.write(
4515
+ `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
4516
+ `
4517
+ );
4518
+ return skillId;
4765
4519
  }
4766
- function assertSessionScope(actionType, targetProject) {
4520
+ async function captureAndLearn(opts) {
4767
4521
  try {
4768
- const currentProject = getProjectName();
4769
- const exeSession = resolveExeSession();
4770
- if (!exeSession) {
4771
- return { allowed: true, reason: "no_session" };
4772
- }
4773
- if (currentProject === targetProject) {
4774
- return {
4775
- allowed: true,
4776
- reason: "same_session",
4777
- currentProject,
4778
- targetProject
4779
- };
4522
+ const config2 = await loadConfig();
4523
+ if (!config2.skillLearning) return;
4524
+ const { trajectoryId, similarCount, similar } = await captureTrajectory({
4525
+ ...opts,
4526
+ skillThreshold: config2.skillThreshold
4527
+ });
4528
+ if (!trajectoryId) return;
4529
+ if (similarCount >= config2.skillThreshold) {
4530
+ const unprocessed = similar.filter((t) => !t.skillId);
4531
+ if (unprocessed.length >= config2.skillThreshold) {
4532
+ extractSkill(unprocessed, config2.skillModel).catch((err) => {
4533
+ process.stderr.write(
4534
+ `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
4535
+ `
4536
+ );
4537
+ });
4538
+ }
4780
4539
  }
4540
+ } catch (err) {
4781
4541
  process.stderr.write(
4782
- `[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
4542
+ `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
4783
4543
  `
4784
4544
  );
4785
- return {
4786
- allowed: true,
4787
- // v1: warn-only, don't block
4788
- reason: "cross_session_granted",
4789
- currentProject,
4790
- targetProject,
4791
- targetSession: findSessionForProject(targetProject)?.windowName
4792
- };
4793
- } catch {
4794
- return { allowed: true, reason: "no_session" };
4795
4545
  }
4796
4546
  }
4797
- var init_session_scope = __esm({
4798
- "src/lib/session-scope.ts"() {
4799
- "use strict";
4800
- init_session_registry();
4801
- init_project_name();
4802
- init_tmux_routing();
4803
- }
4804
- });
4805
-
4806
- // src/lib/tasks-notify.ts
4807
- async function dispatchTaskToEmployee(input) {
4808
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
4809
- let crossProject = false;
4810
- if (input.projectName) {
4811
- try {
4812
- const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
4813
- const check = assertSessionScope2("dispatch_task", input.projectName);
4814
- if (check.reason === "cross_session_granted") {
4815
- crossProject = true;
4816
- }
4817
- } catch {
4818
- }
4819
- }
4820
- try {
4821
- const transport = getTransport();
4822
- const exeSession = resolveExeSession();
4823
- if (!exeSession) return { dispatched: "session_missing" };
4824
- const sessionName = employeeSessionName(input.assignedTo, exeSession);
4825
- if (transport.isAlive(sessionName)) {
4826
- const result = sendIntercom(sessionName);
4827
- const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
4828
- return { dispatched, session: sessionName, crossProject };
4829
- } else {
4830
- const projectDir = input.projectDir ?? process.cwd();
4831
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
4832
- autoInstance: isMultiInstance(input.assignedTo)
4547
+ async function sweepTrajectories(threshold, model) {
4548
+ const config2 = await loadConfig();
4549
+ if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
4550
+ const t = threshold ?? config2.skillThreshold;
4551
+ const client = getClient();
4552
+ const result = await client.execute({
4553
+ sql: `SELECT signature_hash, COUNT(*) as cnt
4554
+ FROM trajectories
4555
+ WHERE skill_id IS NULL
4556
+ GROUP BY signature_hash
4557
+ HAVING cnt >= ?
4558
+ ORDER BY cnt DESC
4559
+ LIMIT 10`,
4560
+ args: [t]
4561
+ });
4562
+ let clustersProcessed = 0;
4563
+ let skillsExtracted = 0;
4564
+ for (const row of result.rows) {
4565
+ const hash = String(row.signature_hash);
4566
+ const trajResult = await client.execute({
4567
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
4568
+ FROM trajectories
4569
+ WHERE signature_hash = ? AND skill_id IS NULL
4570
+ ORDER BY created_at DESC
4571
+ LIMIT 10`,
4572
+ args: [hash]
4573
+ });
4574
+ const trajectories = trajResult.rows.map((r) => ({
4575
+ id: String(r.id),
4576
+ taskId: String(r.task_id),
4577
+ agentId: String(r.agent_id),
4578
+ projectName: String(r.project_name),
4579
+ taskTitle: String(r.task_title),
4580
+ signature: JSON.parse(String(r.signature)),
4581
+ signatureHash: String(r.signature_hash),
4582
+ toolCount: Number(r.tool_count),
4583
+ skillId: null,
4584
+ createdAt: String(r.created_at)
4585
+ }));
4586
+ if (trajectories.length >= t) {
4587
+ clustersProcessed++;
4588
+ const skillId = await extractSkill(trajectories, model ?? config2.skillModel);
4589
+ if (skillId) skillsExtracted++;
4590
+ }
4591
+ }
4592
+ return { clustersProcessed, skillsExtracted };
4593
+ }
4594
+ function editDistance(a, b) {
4595
+ const m = a.length;
4596
+ const n = b.length;
4597
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
4598
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
4599
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
4600
+ for (let i = 1; i <= m; i++) {
4601
+ for (let j = 1; j <= n; j++) {
4602
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4603
+ dp[i][j] = Math.min(
4604
+ dp[i - 1][j] + 1,
4605
+ dp[i][j - 1] + 1,
4606
+ dp[i - 1][j - 1] + cost
4607
+ );
4608
+ }
4609
+ }
4610
+ return dp[m][n];
4611
+ }
4612
+ var DEFAULT_SKILL_THRESHOLD;
4613
+ var init_skill_learning = __esm({
4614
+ "src/lib/skill-learning.ts"() {
4615
+ "use strict";
4616
+ init_database();
4617
+ init_behaviors();
4618
+ init_config();
4619
+ DEFAULT_SKILL_THRESHOLD = 3;
4620
+ }
4621
+ });
4622
+
4623
+ // src/lib/tasks.ts
4624
+ var tasks_exports = {};
4625
+ __export(tasks_exports, {
4626
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
4627
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
4628
+ countPendingReviews: () => countPendingReviews,
4629
+ createTask: () => createTask,
4630
+ createTaskCore: () => createTaskCore,
4631
+ deleteTask: () => deleteTask,
4632
+ deleteTaskCore: () => deleteTaskCore,
4633
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
4634
+ ensureGitignoreExe: () => ensureGitignoreExe,
4635
+ getReviewChecklist: () => getReviewChecklist,
4636
+ listPendingReviews: () => listPendingReviews,
4637
+ listTasks: () => listTasks,
4638
+ resolveTask: () => resolveTask,
4639
+ slugify: () => slugify,
4640
+ updateTask: () => updateTask,
4641
+ updateTaskStatus: () => updateTaskStatus,
4642
+ writeCheckpoint: () => writeCheckpoint
4643
+ });
4644
+ import path16 from "path";
4645
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
4646
+ async function createTask(input) {
4647
+ const result = await createTaskCore(input);
4648
+ if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
4649
+ dispatchTaskToEmployee({
4650
+ assignedTo: input.assignedTo,
4651
+ title: input.title,
4652
+ priority: input.priority,
4653
+ taskFile: result.taskFile,
4654
+ initialStatus: result.status,
4655
+ projectName: input.projectName
4656
+ });
4657
+ }
4658
+ return result;
4659
+ }
4660
+ async function updateTask(input) {
4661
+ const { row, taskFile, now, taskId } = await updateTaskStatus(input);
4662
+ try {
4663
+ const agent = String(row.assigned_to);
4664
+ const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
4665
+ const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
4666
+ if (input.status === "in_progress") {
4667
+ mkdirSync6(cacheDir, { recursive: true });
4668
+ writeFileSync4(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4669
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
4670
+ try {
4671
+ unlinkSync4(cachePath);
4672
+ } catch {
4673
+ }
4674
+ }
4675
+ } catch {
4676
+ }
4677
+ if (input.status === "done") {
4678
+ await cleanupReviewFile(row, taskFile, input.baseDir);
4679
+ }
4680
+ if (input.status === "done" || input.status === "cancelled") {
4681
+ try {
4682
+ const client = getClient();
4683
+ const taskTitle = String(row.title);
4684
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
4685
+ await client.execute({
4686
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
4687
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
4688
+ args: [now, `%left '${escaped}' as in\\_progress%`]
4833
4689
  });
4834
- if (result.status === "failed") {
4690
+ } catch {
4691
+ }
4692
+ try {
4693
+ const client = getClient();
4694
+ const cascaded = await client.execute({
4695
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
4696
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
4697
+ args: [now, taskId]
4698
+ });
4699
+ if (cascaded.rowsAffected > 0) {
4835
4700
  process.stderr.write(
4836
- `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
4701
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
4837
4702
  `
4838
4703
  );
4839
- return { dispatched: "session_missing" };
4840
4704
  }
4841
- return { dispatched: "spawned", session: result.sessionName, crossProject };
4705
+ } catch {
4842
4706
  }
4843
- } catch {
4844
- return { dispatched: "session_missing" };
4845
4707
  }
4846
- }
4847
- function notifyTaskDone() {
4848
- try {
4849
- const key = getSessionKey();
4850
- if (key && !process.env.VITEST) notifyParentExe(key);
4851
- } catch {
4708
+ const isTerminal = input.status === "done" || input.status === "needs_review";
4709
+ if (isTerminal) {
4710
+ const isExe = String(row.assigned_to) === "exe";
4711
+ if (!isExe) {
4712
+ notifyTaskDone();
4713
+ }
4714
+ await markTaskNotificationsRead(taskFile);
4715
+ if (input.status === "done") {
4716
+ try {
4717
+ await cascadeUnblock(taskId, input.baseDir, now);
4718
+ } catch {
4719
+ }
4720
+ orgBus.emit({
4721
+ type: "task_completed",
4722
+ taskId,
4723
+ employee: String(row.assigned_to),
4724
+ result: input.result ?? "",
4725
+ timestamp: now
4726
+ });
4727
+ if (row.parent_task_id) {
4728
+ try {
4729
+ await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
4730
+ } catch {
4731
+ }
4732
+ }
4733
+ }
4852
4734
  }
4853
- }
4854
- async function markTaskNotificationsRead(taskFile) {
4855
- try {
4856
- await markAsReadByTaskFile(taskFile);
4857
- } catch {
4735
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
4736
+ Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4737
+ ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4738
+ taskId,
4739
+ agentId: String(row.assigned_to),
4740
+ projectName: String(row.project_name),
4741
+ taskTitle: String(row.title)
4742
+ })
4743
+ ).catch((err) => {
4744
+ process.stderr.write(
4745
+ `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
4746
+ `
4747
+ );
4748
+ });
4858
4749
  }
4859
- }
4860
- var init_tasks_notify = __esm({
4861
- "src/lib/tasks-notify.ts"() {
4862
- "use strict";
4863
- init_tmux_routing();
4864
- init_session_key();
4865
- init_notifications();
4866
- init_transport();
4867
- init_employees();
4750
+ let nextTask;
4751
+ if (isTerminal && String(row.assigned_to) !== "exe") {
4752
+ try {
4753
+ nextTask = await findNextTask(String(row.assigned_to));
4754
+ } catch {
4755
+ }
4868
4756
  }
4869
- });
4870
-
4871
- // src/lib/behaviors.ts
4872
- import crypto6 from "crypto";
4873
- async function storeBehavior(opts) {
4757
+ return {
4758
+ id: String(row.id),
4759
+ title: String(row.title),
4760
+ assignedTo: String(row.assigned_to),
4761
+ assignedBy: String(row.assigned_by),
4762
+ projectName: String(row.project_name),
4763
+ priority: String(row.priority),
4764
+ status: input.status,
4765
+ taskFile,
4766
+ createdAt: String(row.created_at),
4767
+ updatedAt: now,
4768
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
4769
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
4770
+ tokensUsed: Number(row.tokens_used ?? 0),
4771
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
4772
+ nextTask
4773
+ };
4774
+ }
4775
+ async function deleteTask(taskId, baseDir) {
4874
4776
  const client = getClient();
4875
- const id = crypto6.randomUUID();
4876
- const now = (/* @__PURE__ */ new Date()).toISOString();
4777
+ const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
4778
+ const reviewer = assignedBy || "exe";
4779
+ const reviewSlug = `review-${assignedTo}-${taskSlug}`;
4780
+ const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
4877
4781
  await client.execute({
4878
- sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
4879
- VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
4880
- args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
4782
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
4783
+ args: [reviewFile, `exe/exe/${reviewSlug}.md`]
4881
4784
  });
4882
- return id;
4785
+ await markAsReadByTaskFile(taskFile);
4786
+ await markAsReadByTaskFile(reviewFile);
4883
4787
  }
4884
- var init_behaviors = __esm({
4885
- "src/lib/behaviors.ts"() {
4788
+ var init_tasks = __esm({
4789
+ "src/lib/tasks.ts"() {
4886
4790
  "use strict";
4887
4791
  init_database();
4792
+ init_config();
4793
+ init_notifications();
4794
+ init_state_bus();
4795
+ init_tasks_crud();
4796
+ init_tasks_review();
4797
+ init_tasks_crud();
4798
+ init_tasks_chain();
4799
+ init_tasks_review();
4800
+ init_tasks_notify();
4888
4801
  }
4889
4802
  });
4890
4803
 
4891
- // src/lib/skill-learning.ts
4892
- var skill_learning_exports = {};
4893
- __export(skill_learning_exports, {
4894
- captureAndLearn: () => captureAndLearn,
4895
- captureTrajectory: () => captureTrajectory,
4896
- editDistance: () => editDistance,
4897
- extractSkill: () => extractSkill,
4898
- extractTrajectory: () => extractTrajectory,
4899
- findSimilarTrajectories: () => findSimilarTrajectories,
4900
- hashSignature: () => hashSignature,
4901
- storeTrajectory: () => storeTrajectory,
4902
- sweepTrajectories: () => sweepTrajectories
4804
+ // src/lib/capacity-monitor.ts
4805
+ var capacity_monitor_exports = {};
4806
+ __export(capacity_monitor_exports, {
4807
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
4808
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
4809
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
4810
+ confirmCapacityKill: () => confirmCapacityKill,
4811
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
4812
+ extractContextPercent: () => extractContextPercent,
4813
+ isAtCapacity: () => isAtCapacity,
4814
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
4815
+ pollCapacityDead: () => pollCapacityDead
4903
4816
  });
4904
- import crypto7 from "crypto";
4905
- async function extractTrajectory(taskId, agentId) {
4906
- const client = getClient();
4907
- const result = await client.execute({
4908
- sql: `SELECT tool_name, raw_text
4909
- FROM memories
4910
- WHERE task_id = ? AND agent_id = ?
4911
- ORDER BY timestamp ASC`,
4912
- args: [taskId, agentId]
4913
- });
4914
- if (result.rows.length === 0) return [];
4915
- const rawTools = result.rows.map((r) => {
4916
- const toolName = String(r.tool_name);
4917
- if (toolName === "Bash") {
4918
- const text = String(r.raw_text);
4919
- const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
4920
- return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
4921
- }
4922
- return toolName;
4923
- });
4924
- const signature = [];
4925
- for (const tool of rawTools) {
4926
- if (signature.length === 0 || signature[signature.length - 1] !== tool) {
4927
- signature.push(tool);
4817
+ function resumeTaskTitle(agentId) {
4818
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
4819
+ }
4820
+ function buildResumeContext(agentId, openTasks) {
4821
+ const taskList = openTasks.map(
4822
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
4823
+ ).join("\n");
4824
+ return [
4825
+ "## Context",
4826
+ "",
4827
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
4828
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
4829
+ "",
4830
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
4831
+ "",
4832
+ taskList,
4833
+ "",
4834
+ "Read each task file and chain through them. Build and commit after each one."
4835
+ ].join("\n");
4836
+ }
4837
+ function filterPaneContent(paneOutput) {
4838
+ return paneOutput.split("\n").filter((line) => {
4839
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
4840
+ for (const marker of CONTENT_LINE_MARKERS) {
4841
+ if (line.includes(marker)) return false;
4842
+ }
4843
+ for (const re of SOURCE_CODE_MARKERS) {
4844
+ if (re.test(line)) return false;
4928
4845
  }
4846
+ return true;
4847
+ }).join("\n");
4848
+ }
4849
+ function extractContextPercent(paneOutput) {
4850
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
4851
+ if (!match) return null;
4852
+ const parsed = Number.parseInt(match[2], 10);
4853
+ return Number.isFinite(parsed) ? parsed : null;
4854
+ }
4855
+ function isAtCapacity(paneOutput) {
4856
+ const filtered = filterPaneContent(paneOutput);
4857
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
4858
+ }
4859
+ function confirmCapacityKill(agentId, now = Date.now()) {
4860
+ const pendingSince = _pendingCapacityKill.get(agentId);
4861
+ if (pendingSince === void 0) {
4862
+ _pendingCapacityKill.set(agentId, now);
4863
+ return false;
4929
4864
  }
4930
- return signature;
4865
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
4866
+ _pendingCapacityKill.set(agentId, now);
4867
+ return false;
4868
+ }
4869
+ _pendingCapacityKill.delete(agentId);
4870
+ return true;
4931
4871
  }
4932
- function hashSignature(signature) {
4933
- return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4872
+ function _resetPendingCapacityKills() {
4873
+ _pendingCapacityKill.clear();
4934
4874
  }
4935
- async function storeTrajectory(opts) {
4936
- const client = getClient();
4937
- const id = crypto7.randomUUID();
4938
- const now = (/* @__PURE__ */ new Date()).toISOString();
4939
- const signatureHash = hashSignature(opts.signature);
4940
- await client.execute({
4941
- sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
4942
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4943
- args: [
4944
- id,
4945
- opts.taskId,
4946
- opts.agentId,
4947
- opts.projectName,
4948
- opts.taskTitle,
4949
- JSON.stringify(opts.signature),
4950
- signatureHash,
4951
- opts.signature.length,
4952
- now
4953
- ]
4954
- });
4955
- return id;
4875
+ function _resetLastRelaunchCache() {
4876
+ _lastRelaunch.clear();
4956
4877
  }
4957
- async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
4878
+ async function lastResumeCreatedAtMs(agentId) {
4958
4879
  const client = getClient();
4959
- const hash = hashSignature(signature);
4960
4880
  const result = await client.execute({
4961
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
4962
- FROM trajectories
4963
- WHERE signature_hash = ?
4964
- ORDER BY created_at DESC
4965
- LIMIT 20`,
4966
- args: [hash]
4967
- });
4968
- const mapRow = (r) => ({
4969
- id: String(r.id),
4970
- taskId: String(r.task_id),
4971
- agentId: String(r.agent_id),
4972
- projectName: String(r.project_name),
4973
- taskTitle: String(r.task_title),
4974
- signature: JSON.parse(String(r.signature)),
4975
- signatureHash: String(r.signature_hash),
4976
- toolCount: Number(r.tool_count),
4977
- skillId: r.skill_id ? String(r.skill_id) : null,
4978
- createdAt: String(r.created_at)
4881
+ sql: `SELECT MAX(created_at) AS last_created_at
4882
+ FROM tasks
4883
+ WHERE assigned_to = ? AND title LIKE ?`,
4884
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`]
4979
4885
  });
4980
- const matches = result.rows.map(mapRow);
4981
- if (matches.length >= threshold) return matches;
4982
- const nearResult = await client.execute({
4983
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
4984
- FROM trajectories
4985
- WHERE tool_count BETWEEN ? AND ?
4986
- AND signature_hash != ?
4886
+ const raw = result.rows[0]?.last_created_at;
4887
+ if (raw === null || raw === void 0) return null;
4888
+ const parsed = Date.parse(String(raw));
4889
+ return Number.isNaN(parsed) ? null : parsed;
4890
+ }
4891
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
4892
+ const cached = _lastRelaunch.get(agentId);
4893
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
4894
+ const persisted = await lastResumeCreatedAtMs(agentId);
4895
+ if (persisted === null) return false;
4896
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
4897
+ _lastRelaunch.set(agentId, persisted);
4898
+ return true;
4899
+ }
4900
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
4901
+ const client = getClient();
4902
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4903
+ const context = buildResumeContext(agentId, openTasks);
4904
+ const existing = await client.execute({
4905
+ sql: `SELECT id FROM tasks
4906
+ WHERE assigned_to = ?
4907
+ AND title LIKE ?
4908
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
4987
4909
  ORDER BY created_at DESC
4988
- LIMIT 50`,
4989
- args: [
4990
- Math.max(1, signature.length - 3),
4991
- signature.length + 3,
4992
- hash
4993
- ]
4910
+ LIMIT 1`,
4911
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
4994
4912
  });
4995
- for (const r of nearResult.rows) {
4996
- const candidateSig = JSON.parse(String(r.signature));
4997
- if (editDistance(signature, candidateSig) <= 2) {
4998
- matches.push(mapRow(r));
4999
- }
5000
- }
5001
- return matches;
5002
- }
5003
- async function captureTrajectory(opts) {
5004
- const signature = await extractTrajectory(opts.taskId, opts.agentId);
5005
- if (signature.length < 3) {
5006
- return { trajectoryId: "", similarCount: 0, similar: [] };
4913
+ if (existing.rows.length > 0) {
4914
+ const taskId = String(existing.rows[0].id);
4915
+ await client.execute({
4916
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
4917
+ args: [context, now, taskId]
4918
+ });
4919
+ return { created: false, taskId };
5007
4920
  }
5008
- const trajectoryId = await storeTrajectory({
5009
- taskId: opts.taskId,
5010
- agentId: opts.agentId,
5011
- projectName: opts.projectName,
5012
- taskTitle: opts.taskTitle,
5013
- signature
4921
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
4922
+ const task = await createTask2({
4923
+ title: resumeTaskTitle(agentId),
4924
+ assignedTo: agentId,
4925
+ assignedBy: "system",
4926
+ projectName: projectDir.split("/").pop() ?? "unknown",
4927
+ priority: "p0",
4928
+ context,
4929
+ baseDir: projectDir
5014
4930
  });
5015
- const similar = await findSimilarTrajectories(
5016
- signature,
5017
- opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
5018
- );
5019
- return { trajectoryId, similarCount: similar.length, similar };
5020
- }
5021
- function buildExtractionPrompt(trajectories) {
5022
- const items = trajectories.map((t, i) => {
5023
- const sig = t.signature.join(" \u2192 ");
5024
- return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
5025
- Signature: ${sig}`;
5026
- }).join("\n\n");
5027
- return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
5028
-
5029
- ${items}
5030
-
5031
- Extract the reusable procedure. Format your response EXACTLY like this:
5032
-
5033
- SKILL: {name \u2014 short, descriptive}
5034
- TRIGGER: {when to use this \u2014 one sentence}
5035
- STEPS:
5036
- 1. ...
5037
- 2. ...
5038
- PITFALLS: {common mistakes to avoid}
5039
-
5040
- Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
4931
+ return { created: true, taskId: task.id };
5041
4932
  }
5042
- async function extractSkill(trajectories, model) {
5043
- if (trajectories.length === 0) return null;
5044
- const config2 = await loadConfig();
5045
- const skillModel = model ?? config2.skillModel;
5046
- const Anthropic4 = (await import("@anthropic-ai/sdk")).default;
5047
- const client = new Anthropic4();
5048
- const prompt = buildExtractionPrompt(trajectories);
5049
- const response = await client.messages.create({
5050
- model: skillModel,
5051
- max_tokens: 500,
5052
- messages: [{ role: "user", content: prompt }]
5053
- });
5054
- const textBlock = response.content.find((b) => b.type === "text");
5055
- const skillText = textBlock?.text;
5056
- if (!skillText) return null;
5057
- const agentId = trajectories[0].agentId;
5058
- const projectName = trajectories[0].projectName;
5059
- const skillId = await storeBehavior({
5060
- agentId,
5061
- content: skillText,
5062
- domain: "skill",
5063
- projectName
5064
- });
5065
- const dbClient = getClient();
5066
- for (const t of trajectories) {
5067
- await dbClient.execute({
5068
- sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
5069
- args: [skillId, t.id]
5070
- });
5071
- }
5072
- process.stderr.write(
5073
- `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
5074
- `
4933
+ async function pollCapacityDead() {
4934
+ const transport = getTransport();
4935
+ const relaunched = [];
4936
+ const registered = listSessions().filter(
4937
+ (s) => s.agentId !== "exe"
5075
4938
  );
5076
- return skillId;
5077
- }
5078
- async function captureAndLearn(opts) {
4939
+ if (registered.length === 0) return [];
4940
+ let liveSessions;
5079
4941
  try {
5080
- const config2 = await loadConfig();
5081
- if (!config2.skillLearning) return;
5082
- const { trajectoryId, similarCount, similar } = await captureTrajectory({
5083
- ...opts,
5084
- skillThreshold: config2.skillThreshold
5085
- });
5086
- if (!trajectoryId) return;
5087
- if (similarCount >= config2.skillThreshold) {
5088
- const unprocessed = similar.filter((t) => !t.skillId);
5089
- if (unprocessed.length >= config2.skillThreshold) {
5090
- extractSkill(unprocessed, config2.skillModel).catch((err) => {
5091
- process.stderr.write(
5092
- `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
4942
+ liveSessions = transport.listSessions();
4943
+ } catch {
4944
+ return [];
4945
+ }
4946
+ for (const entry of registered) {
4947
+ const { windowName, agentId, projectDir } = entry;
4948
+ if (!liveSessions.includes(windowName)) continue;
4949
+ if (await isWithinRelaunchCooldown(agentId)) continue;
4950
+ let pane;
4951
+ try {
4952
+ pane = transport.capturePane(windowName, 15);
4953
+ } catch {
4954
+ continue;
4955
+ }
4956
+ if (!isAtCapacity(pane)) continue;
4957
+ const ctxPct = extractContextPercent(pane);
4958
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
4959
+ process.stderr.write(
4960
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
5093
4961
  `
5094
- );
5095
- });
5096
- }
4962
+ );
4963
+ continue;
4964
+ }
4965
+ if (!confirmCapacityKill(agentId)) {
4966
+ process.stderr.write(
4967
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
4968
+ `
4969
+ );
4970
+ continue;
4971
+ }
4972
+ const verify = await verifyPaneAtCapacity(windowName);
4973
+ if (!verify.atCapacity) {
4974
+ process.stderr.write(
4975
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
4976
+ `
4977
+ );
4978
+ void recordSessionKill({
4979
+ sessionName: windowName,
4980
+ agentId,
4981
+ reason: "capacity_false_positive_blocked"
4982
+ });
4983
+ continue;
5097
4984
  }
5098
- } catch (err) {
5099
4985
  process.stderr.write(
5100
- `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
4986
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
5101
4987
  `
5102
4988
  );
4989
+ try {
4990
+ transport.kill(windowName);
4991
+ void recordSessionKill({
4992
+ sessionName: windowName,
4993
+ agentId,
4994
+ reason: "capacity"
4995
+ });
4996
+ const client = getClient();
4997
+ const openTasks = await client.execute({
4998
+ sql: `SELECT id, title, priority, task_file, status
4999
+ FROM tasks
5000
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')
5001
+ ORDER BY
5002
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
5003
+ created_at ASC
5004
+ LIMIT 10`,
5005
+ args: [agentId]
5006
+ });
5007
+ if (openTasks.rows.length === 0) {
5008
+ process.stderr.write(
5009
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
5010
+ `
5011
+ );
5012
+ continue;
5013
+ }
5014
+ const { created } = await createOrRefreshResumeTask(
5015
+ agentId,
5016
+ projectDir,
5017
+ openTasks.rows
5018
+ );
5019
+ if (created) {
5020
+ await writeNotification({
5021
+ agentId: "system",
5022
+ agentRole: "daemon",
5023
+ event: "capacity_relaunch",
5024
+ project: projectDir.split("/").pop() ?? "unknown",
5025
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
5026
+ });
5027
+ }
5028
+ _lastRelaunch.set(agentId, Date.now());
5029
+ if (created) relaunched.push(agentId);
5030
+ } catch (err) {
5031
+ process.stderr.write(
5032
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
5033
+ `
5034
+ );
5035
+ }
5103
5036
  }
5037
+ return relaunched;
5104
5038
  }
5105
- async function sweepTrajectories(threshold, model) {
5106
- const config2 = await loadConfig();
5107
- if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
5108
- const t = threshold ?? config2.skillThreshold;
5109
- const client = getClient();
5110
- const result = await client.execute({
5111
- sql: `SELECT signature_hash, COUNT(*) as cnt
5112
- FROM trajectories
5113
- WHERE skill_id IS NULL
5114
- GROUP BY signature_hash
5115
- HAVING cnt >= ?
5116
- ORDER BY cnt DESC
5117
- LIMIT 10`,
5118
- args: [t]
5039
+ 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;
5040
+ var init_capacity_monitor = __esm({
5041
+ "src/lib/capacity-monitor.ts"() {
5042
+ "use strict";
5043
+ init_session_registry();
5044
+ init_transport();
5045
+ init_notifications();
5046
+ init_database();
5047
+ init_session_kill_telemetry();
5048
+ init_tmux_routing();
5049
+ CAPACITY_PATTERNS = [
5050
+ /conversation is too long/i,
5051
+ /maximum context length/i,
5052
+ /context window.*(?:limit|exceed|full)/i,
5053
+ /reached.*(?:token|context).*limit/i
5054
+ ];
5055
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
5056
+ CONTENT_LINE_MARKERS = [
5057
+ "RESUME:",
5058
+ "intercom",
5059
+ "capacity-monitor",
5060
+ "CAPACITY_PATTERNS",
5061
+ "isAtCapacity",
5062
+ "CONTENT_LINE_MARKERS",
5063
+ "pollCapacityDead",
5064
+ "confirmCapacityKill",
5065
+ "session_kills",
5066
+ "capacity-monitor.test"
5067
+ ];
5068
+ SOURCE_CODE_MARKERS = [
5069
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
5070
+ /(?:maximum context length|conversation is too long).*["'`/]/i
5071
+ ];
5072
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
5073
+ _lastRelaunch = /* @__PURE__ */ new Map();
5074
+ RESUME_TITLE_PREFIX = "RESUME:";
5075
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
5076
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
5077
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
5078
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
5079
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
5080
+ CTX_FLOOR_PERCENT = 50;
5081
+ }
5082
+ });
5083
+
5084
+ // src/lib/tmux-routing.ts
5085
+ var tmux_routing_exports = {};
5086
+ __export(tmux_routing_exports, {
5087
+ acquireSpawnLock: () => acquireSpawnLock2,
5088
+ employeeSessionName: () => employeeSessionName,
5089
+ ensureEmployee: () => ensureEmployee,
5090
+ extractRootExe: () => extractRootExe,
5091
+ findFreeInstance: () => findFreeInstance,
5092
+ getDispatchedBy: () => getDispatchedBy,
5093
+ getMySession: () => getMySession,
5094
+ getParentExe: () => getParentExe,
5095
+ getSessionState: () => getSessionState,
5096
+ isEmployeeAlive: () => isEmployeeAlive,
5097
+ isExeSession: () => isExeSession,
5098
+ isSessionBusy: () => isSessionBusy,
5099
+ notifyParentExe: () => notifyParentExe,
5100
+ parseParentExe: () => parseParentExe,
5101
+ registerParentExe: () => registerParentExe,
5102
+ releaseSpawnLock: () => releaseSpawnLock2,
5103
+ resolveExeSession: () => resolveExeSession,
5104
+ sendIntercom: () => sendIntercom,
5105
+ spawnEmployee: () => spawnEmployee,
5106
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
5107
+ });
5108
+ import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
5109
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
5110
+ import path17 from "path";
5111
+ import os7 from "os";
5112
+ import { fileURLToPath as fileURLToPath2 } from "url";
5113
+ import { unlinkSync as unlinkSync5 } from "fs";
5114
+ function spawnLockPath(sessionName) {
5115
+ return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5116
+ }
5117
+ function isProcessAlive(pid) {
5118
+ try {
5119
+ process.kill(pid, 0);
5120
+ return true;
5121
+ } catch {
5122
+ return false;
5123
+ }
5124
+ }
5125
+ function acquireSpawnLock2(sessionName) {
5126
+ if (!existsSync13(SPAWN_LOCK_DIR)) {
5127
+ mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
5128
+ }
5129
+ const lockFile = spawnLockPath(sessionName);
5130
+ if (existsSync13(lockFile)) {
5131
+ try {
5132
+ const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
5133
+ const age = Date.now() - lock.timestamp;
5134
+ if (isProcessAlive(lock.pid) && age < 6e4) {
5135
+ return false;
5136
+ }
5137
+ } catch {
5138
+ }
5139
+ }
5140
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5141
+ return true;
5142
+ }
5143
+ function releaseSpawnLock2(sessionName) {
5144
+ try {
5145
+ unlinkSync5(spawnLockPath(sessionName));
5146
+ } catch {
5147
+ }
5148
+ }
5149
+ function resolveBehaviorsExporterScript() {
5150
+ try {
5151
+ const thisFile = fileURLToPath2(import.meta.url);
5152
+ const scriptPath = path17.join(
5153
+ path17.dirname(thisFile),
5154
+ "..",
5155
+ "bin",
5156
+ "exe-export-behaviors.js"
5157
+ );
5158
+ return existsSync13(scriptPath) ? scriptPath : null;
5159
+ } catch {
5160
+ return null;
5161
+ }
5162
+ }
5163
+ function exportBehaviorsSync(agentId, projectName, sessionKey) {
5164
+ const script = resolveBehaviorsExporterScript();
5165
+ if (!script) return null;
5166
+ try {
5167
+ const output = execFileSync2(
5168
+ process.execPath,
5169
+ [script, agentId, projectName, sessionKey],
5170
+ { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
5171
+ ).trim();
5172
+ return output.length > 0 ? output : null;
5173
+ } catch (err) {
5174
+ process.stderr.write(
5175
+ `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
5176
+ `
5177
+ );
5178
+ return null;
5179
+ }
5180
+ }
5181
+ function getMySession() {
5182
+ return getTransport().getMySession();
5183
+ }
5184
+ function employeeSessionName(employee, exeSession, instance) {
5185
+ if (!/^exe\d+$/.test(exeSession)) {
5186
+ const root = extractRootExe(exeSession);
5187
+ if (root) {
5188
+ process.stderr.write(
5189
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
5190
+ `
5191
+ );
5192
+ exeSession = root;
5193
+ } else {
5194
+ throw new Error(
5195
+ `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
5196
+ );
5197
+ }
5198
+ }
5199
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
5200
+ const name = `${employee}${suffix}-${exeSession}`;
5201
+ if (!VALID_SESSION_NAME.test(name)) {
5202
+ throw new Error(
5203
+ `Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
5204
+ );
5205
+ }
5206
+ return name;
5207
+ }
5208
+ function parseParentExe(sessionName, agentId) {
5209
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5210
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
5211
+ const match = sessionName.match(regex);
5212
+ return match?.[1] ?? null;
5213
+ }
5214
+ function extractRootExe(name) {
5215
+ const match = name.match(/(exe\d+)$/);
5216
+ return match?.[1] ?? null;
5217
+ }
5218
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5219
+ if (!existsSync13(SESSION_CACHE)) {
5220
+ mkdirSync7(SESSION_CACHE, { recursive: true });
5221
+ }
5222
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
5223
+ const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5224
+ writeFileSync5(filePath, JSON.stringify({
5225
+ parentExe: rootExe,
5226
+ dispatchedBy: dispatchedBy || rootExe,
5227
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
5228
+ }));
5229
+ }
5230
+ function getParentExe(sessionKey) {
5231
+ try {
5232
+ const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5233
+ return data.parentExe || null;
5234
+ } catch {
5235
+ return null;
5236
+ }
5237
+ }
5238
+ function getDispatchedBy(sessionKey) {
5239
+ try {
5240
+ const data = JSON.parse(readFileSync11(
5241
+ path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5242
+ "utf8"
5243
+ ));
5244
+ return data.dispatchedBy ?? data.parentExe ?? null;
5245
+ } catch {
5246
+ return null;
5247
+ }
5248
+ }
5249
+ function resolveExeSession() {
5250
+ const mySession = getMySession();
5251
+ if (!mySession) return null;
5252
+ try {
5253
+ const key = getSessionKey();
5254
+ const parentExe = getParentExe(key);
5255
+ if (parentExe) {
5256
+ return extractRootExe(parentExe) ?? parentExe;
5257
+ }
5258
+ } catch {
5259
+ }
5260
+ return extractRootExe(mySession) ?? mySession;
5261
+ }
5262
+ function isEmployeeAlive(sessionName) {
5263
+ return getTransport().isAlive(sessionName);
5264
+ }
5265
+ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
5266
+ const base = employeeSessionName(employeeName, exeSession);
5267
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
5268
+ for (let i = 2; i <= maxInstances; i++) {
5269
+ const candidate = employeeSessionName(employeeName, exeSession, i);
5270
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
5271
+ }
5272
+ return null;
5273
+ }
5274
+ async function verifyPaneAtCapacity(sessionName) {
5275
+ const transport = getTransport();
5276
+ if (!transport.isAlive(sessionName)) {
5277
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
5278
+ }
5279
+ let pane;
5280
+ try {
5281
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
5282
+ } catch (err) {
5283
+ return {
5284
+ atCapacity: false,
5285
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
5286
+ };
5287
+ }
5288
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
5289
+ if (!isAtCapacity2(pane)) {
5290
+ return {
5291
+ atCapacity: false,
5292
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
5293
+ };
5294
+ }
5295
+ return {
5296
+ atCapacity: true,
5297
+ reason: "capacity banner matched in recent pane output"
5298
+ };
5299
+ }
5300
+ function readDebounceState() {
5301
+ try {
5302
+ if (!existsSync13(DEBOUNCE_FILE)) return {};
5303
+ return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
5304
+ } catch {
5305
+ return {};
5306
+ }
5307
+ }
5308
+ function writeDebounceState(state) {
5309
+ try {
5310
+ if (!existsSync13(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
5311
+ writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
5312
+ } catch {
5313
+ }
5314
+ }
5315
+ function isDebounced(targetSession) {
5316
+ const state = readDebounceState();
5317
+ const lastSent = state[targetSession] ?? 0;
5318
+ return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
5319
+ }
5320
+ function recordDebounce(targetSession) {
5321
+ const state = readDebounceState();
5322
+ state[targetSession] = Date.now();
5323
+ const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
5324
+ for (const key of Object.keys(state)) {
5325
+ if ((state[key] ?? 0) < cutoff) delete state[key];
5326
+ }
5327
+ writeDebounceState(state);
5328
+ }
5329
+ function logIntercom(msg) {
5330
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
5331
+ `;
5332
+ process.stderr.write(`[intercom] ${msg}
5333
+ `);
5334
+ try {
5335
+ appendFileSync(INTERCOM_LOG2, line);
5336
+ } catch {
5337
+ }
5338
+ }
5339
+ function getSessionState(sessionName) {
5340
+ const transport = getTransport();
5341
+ if (!transport.isAlive(sessionName)) return "offline";
5342
+ try {
5343
+ const pane = transport.capturePane(sessionName, 5);
5344
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
5345
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
5346
+ return "no_claude";
5347
+ }
5348
+ }
5349
+ if (/Running…/.test(pane)) return "tool";
5350
+ if (BUSY_PATTERN.test(pane)) return "thinking";
5351
+ return "idle";
5352
+ } catch {
5353
+ return "offline";
5354
+ }
5355
+ }
5356
+ function isSessionBusy(sessionName) {
5357
+ const state = getSessionState(sessionName);
5358
+ return state === "thinking" || state === "tool";
5359
+ }
5360
+ function isExeSession(sessionName) {
5361
+ return /^exe\d*$/.test(sessionName);
5362
+ }
5363
+ function sendIntercom(targetSession) {
5364
+ const transport = getTransport();
5365
+ if (isExeSession(targetSession)) {
5366
+ logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
5367
+ return "skipped_exe";
5368
+ }
5369
+ if (isDebounced(targetSession)) {
5370
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
5371
+ return "debounced";
5372
+ }
5373
+ try {
5374
+ const sessions = transport.listSessions();
5375
+ if (!sessions.includes(targetSession)) {
5376
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
5377
+ return "failed";
5378
+ }
5379
+ const sessionState = getSessionState(targetSession);
5380
+ if (sessionState === "no_claude") {
5381
+ queueIntercom(targetSession, "claude not running in session");
5382
+ recordDebounce(targetSession);
5383
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
5384
+ return "queued";
5385
+ }
5386
+ if (sessionState === "thinking" || sessionState === "tool") {
5387
+ queueIntercom(targetSession, "session busy at send time");
5388
+ recordDebounce(targetSession);
5389
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
5390
+ return "queued";
5391
+ }
5392
+ if (transport.isPaneInCopyMode(targetSession)) {
5393
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
5394
+ transport.sendKeys(targetSession, "q");
5395
+ }
5396
+ transport.sendKeys(targetSession, "/exe-intercom");
5397
+ recordDebounce(targetSession);
5398
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
5399
+ return "delivered";
5400
+ } catch {
5401
+ logIntercom(`FAIL \u2192 ${targetSession}`);
5402
+ return "failed";
5403
+ }
5404
+ }
5405
+ function notifyParentExe(sessionKey) {
5406
+ const target = getDispatchedBy(sessionKey);
5407
+ if (!target) {
5408
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
5409
+ `);
5410
+ return false;
5411
+ }
5412
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
5413
+ `);
5414
+ const result = sendIntercom(target);
5415
+ if (result === "failed") {
5416
+ const rootExe = resolveExeSession();
5417
+ if (rootExe && rootExe !== target) {
5418
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
5419
+ `);
5420
+ const fallback = sendIntercom(rootExe);
5421
+ return fallback !== "failed";
5422
+ }
5423
+ return false;
5424
+ }
5425
+ return true;
5426
+ }
5427
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
5428
+ if (employeeName === "exe") {
5429
+ return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
5430
+ }
5431
+ try {
5432
+ assertEmployeeLimitSync();
5433
+ } catch (err) {
5434
+ if (err instanceof PlanLimitError) {
5435
+ return { status: "failed", sessionName: "", error: err.message };
5436
+ }
5437
+ }
5438
+ if (/-exe\d*$/.test(employeeName)) {
5439
+ const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
5440
+ return {
5441
+ status: "failed",
5442
+ sessionName: "",
5443
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
5444
+ };
5445
+ }
5446
+ if (!/^exe\d+$/.test(exeSession)) {
5447
+ const root = extractRootExe(exeSession);
5448
+ if (root) {
5449
+ process.stderr.write(
5450
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
5451
+ `
5452
+ );
5453
+ exeSession = root;
5454
+ } else {
5455
+ return {
5456
+ status: "failed",
5457
+ sessionName: "",
5458
+ error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
5459
+ };
5460
+ }
5461
+ }
5462
+ let effectiveInstance = opts?.instance;
5463
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
5464
+ const free = findFreeInstance(
5465
+ employeeName,
5466
+ exeSession,
5467
+ opts.maxAutoInstances ?? 10
5468
+ );
5469
+ if (free === null) {
5470
+ return {
5471
+ status: "failed",
5472
+ sessionName: employeeSessionName(employeeName, exeSession),
5473
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
5474
+ };
5475
+ }
5476
+ effectiveInstance = free === 0 ? void 0 : free;
5477
+ }
5478
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
5479
+ if (isEmployeeAlive(sessionName)) {
5480
+ const result2 = sendIntercom(sessionName);
5481
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
5482
+ return { status: "intercom_sent", sessionName };
5483
+ }
5484
+ if (result2 === "delivered") {
5485
+ return { status: "intercom_unprocessed", sessionName };
5486
+ }
5487
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
5488
+ }
5489
+ const spawnOpts = { ...opts, instance: effectiveInstance };
5490
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
5491
+ if (result.error) {
5492
+ return { status: "failed", sessionName, error: result.error };
5493
+ }
5494
+ return { status: "spawned", sessionName };
5495
+ }
5496
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5497
+ const transport = getTransport();
5498
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
5499
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
5500
+ const logDir = path17.join(os7.homedir(), ".exe-os", "session-logs");
5501
+ const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5502
+ if (!existsSync13(logDir)) {
5503
+ mkdirSync7(logDir, { recursive: true });
5504
+ }
5505
+ transport.kill(sessionName);
5506
+ let cleanupSuffix = "";
5507
+ try {
5508
+ const thisFile = fileURLToPath2(import.meta.url);
5509
+ const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5510
+ if (existsSync13(cleanupScript)) {
5511
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5512
+ }
5513
+ } catch {
5514
+ }
5515
+ try {
5516
+ const claudeJsonPath = path17.join(os7.homedir(), ".claude.json");
5517
+ let claudeJson = {};
5518
+ try {
5519
+ claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
5520
+ } catch {
5521
+ }
5522
+ if (!claudeJson.projects) claudeJson.projects = {};
5523
+ const projects = claudeJson.projects;
5524
+ const trustDir = opts?.cwd ?? projectDir;
5525
+ if (!projects[trustDir]) projects[trustDir] = {};
5526
+ projects[trustDir].hasTrustDialogAccepted = true;
5527
+ writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5528
+ } catch {
5529
+ }
5530
+ try {
5531
+ const settingsDir = path17.join(os7.homedir(), ".claude", "projects");
5532
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
5533
+ const projSettingsDir = path17.join(settingsDir, normalizedKey);
5534
+ const settingsPath = path17.join(projSettingsDir, "settings.json");
5535
+ let settings = {};
5536
+ try {
5537
+ settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
5538
+ } catch {
5539
+ }
5540
+ const perms = settings.permissions ?? {};
5541
+ const allow = perms.allow ?? [];
5542
+ const toolNames = [
5543
+ "recall_my_memory",
5544
+ "store_memory",
5545
+ "create_task",
5546
+ "update_task",
5547
+ "list_tasks",
5548
+ "get_task",
5549
+ "ask_team_memory",
5550
+ "store_behavior",
5551
+ "get_identity",
5552
+ "send_message"
5553
+ ];
5554
+ const requiredTools = expandDualPrefixTools(toolNames);
5555
+ let changed = false;
5556
+ for (const tool of requiredTools) {
5557
+ if (!allow.includes(tool)) {
5558
+ allow.push(tool);
5559
+ changed = true;
5560
+ }
5561
+ }
5562
+ if (changed) {
5563
+ perms.allow = allow;
5564
+ settings.permissions = perms;
5565
+ mkdirSync7(projSettingsDir, { recursive: true });
5566
+ writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5567
+ }
5568
+ } catch {
5569
+ }
5570
+ const spawnCwd = opts?.cwd ?? projectDir;
5571
+ const useExeAgent = !!(opts?.model && opts?.provider);
5572
+ const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
5573
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
5574
+ let identityFlag = "";
5575
+ let behaviorsFlag = "";
5576
+ let legacyFallbackWarned = false;
5577
+ if (!useExeAgent && !useBinSymlink) {
5578
+ const identityPath = path17.join(
5579
+ os7.homedir(),
5580
+ ".exe-os",
5581
+ "identity",
5582
+ `${employeeName}.md`
5583
+ );
5584
+ _resetCcAgentSupportCache();
5585
+ const hasAgentFlag = claudeSupportsAgentFlag();
5586
+ if (hasAgentFlag) {
5587
+ identityFlag = ` --agent ${employeeName}`;
5588
+ } else if (existsSync13(identityPath)) {
5589
+ identityFlag = ` --append-system-prompt-file ${identityPath}`;
5590
+ legacyFallbackWarned = true;
5591
+ }
5592
+ const behaviorsFile = exportBehaviorsSync(
5593
+ employeeName,
5594
+ path17.basename(spawnCwd),
5595
+ sessionName
5596
+ );
5597
+ if (behaviorsFile) {
5598
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
5599
+ }
5600
+ }
5601
+ if (legacyFallbackWarned) {
5602
+ process.stderr.write(
5603
+ `[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.
5604
+ `
5605
+ );
5606
+ }
5607
+ let sessionContextFlag = "";
5608
+ try {
5609
+ const ctxDir = path17.join(os7.homedir(), ".exe-os", "session-cache");
5610
+ mkdirSync7(ctxDir, { recursive: true });
5611
+ const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
5612
+ const ctxContent = [
5613
+ `## Session Context`,
5614
+ `You are running in tmux session: ${sessionName}.`,
5615
+ `Your parent exe session is ${exeSession}.`,
5616
+ `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
5617
+ ].join("\n");
5618
+ writeFileSync5(ctxFile, ctxContent);
5619
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5620
+ } catch {
5621
+ }
5622
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
5623
+ if (ccProvider !== DEFAULT_PROVIDER) {
5624
+ const cfg = PROVIDER_TABLE[ccProvider];
5625
+ if (cfg?.apiKeyEnv) {
5626
+ const keyVal = process.env[cfg.apiKeyEnv];
5627
+ if (keyVal) {
5628
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
5629
+ }
5630
+ }
5631
+ }
5632
+ let spawnCommand;
5633
+ if (useExeAgent) {
5634
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
5635
+ } else if (useBinSymlink) {
5636
+ const binName = `${employeeName}-${ccProvider}`;
5637
+ process.stderr.write(
5638
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
5639
+ `
5640
+ );
5641
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
5642
+ } else {
5643
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
5644
+ }
5645
+ const spawnResult = transport.spawn(sessionName, {
5646
+ cwd: spawnCwd,
5647
+ command: spawnCommand
5119
5648
  });
5120
- let clustersProcessed = 0;
5121
- let skillsExtracted = 0;
5122
- for (const row of result.rows) {
5123
- const hash = String(row.signature_hash);
5124
- const trajResult = await client.execute({
5125
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
5126
- FROM trajectories
5127
- WHERE signature_hash = ? AND skill_id IS NULL
5128
- ORDER BY created_at DESC
5129
- LIMIT 10`,
5130
- args: [hash]
5131
- });
5132
- const trajectories = trajResult.rows.map((r) => ({
5133
- id: String(r.id),
5134
- taskId: String(r.task_id),
5135
- agentId: String(r.agent_id),
5136
- projectName: String(r.project_name),
5137
- taskTitle: String(r.task_title),
5138
- signature: JSON.parse(String(r.signature)),
5139
- signatureHash: String(r.signature_hash),
5140
- toolCount: Number(r.tool_count),
5141
- skillId: null,
5142
- createdAt: String(r.created_at)
5649
+ if (spawnResult.error) {
5650
+ releaseSpawnLock2(sessionName);
5651
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
5652
+ }
5653
+ transport.pipeLog(sessionName, logFile);
5654
+ try {
5655
+ const mySession = getMySession();
5656
+ const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5657
+ writeFileSync5(dispatchInfo, JSON.stringify({
5658
+ dispatchedBy: mySession,
5659
+ rootExe: exeSession,
5660
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
5661
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5143
5662
  }));
5144
- if (trajectories.length >= t) {
5145
- clustersProcessed++;
5146
- const skillId = await extractSkill(trajectories, model ?? config2.skillModel);
5147
- if (skillId) skillsExtracted++;
5663
+ } catch {
5664
+ }
5665
+ let booted = false;
5666
+ for (let i = 0; i < 30; i++) {
5667
+ try {
5668
+ execSync6("sleep 0.5");
5669
+ } catch {
5670
+ }
5671
+ try {
5672
+ const pane = transport.capturePane(sessionName);
5673
+ if (useExeAgent) {
5674
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
5675
+ booted = true;
5676
+ break;
5677
+ }
5678
+ } else {
5679
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
5680
+ booted = true;
5681
+ break;
5682
+ }
5683
+ }
5684
+ } catch {
5148
5685
  }
5149
5686
  }
5150
- return { clustersProcessed, skillsExtracted };
5151
- }
5152
- function editDistance(a, b) {
5153
- const m = a.length;
5154
- const n = b.length;
5155
- const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
5156
- for (let i = 0; i <= m; i++) dp[i][0] = i;
5157
- for (let j = 0; j <= n; j++) dp[0][j] = j;
5158
- for (let i = 1; i <= m; i++) {
5159
- for (let j = 1; j <= n; j++) {
5160
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
5161
- dp[i][j] = Math.min(
5162
- dp[i - 1][j] + 1,
5163
- dp[i][j - 1] + 1,
5164
- dp[i - 1][j - 1] + cost
5165
- );
5687
+ if (!booted) {
5688
+ releaseSpawnLock2(sessionName);
5689
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
5690
+ }
5691
+ if (!useExeAgent) {
5692
+ try {
5693
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
5694
+ } catch {
5166
5695
  }
5167
5696
  }
5168
- return dp[m][n];
5697
+ registerSession({
5698
+ windowName: sessionName,
5699
+ agentId: employeeName,
5700
+ projectDir: spawnCwd,
5701
+ parentExe: exeSession,
5702
+ pid: 0,
5703
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
5704
+ });
5705
+ releaseSpawnLock2(sessionName);
5706
+ return { sessionName };
5169
5707
  }
5170
- var DEFAULT_SKILL_THRESHOLD;
5171
- var init_skill_learning = __esm({
5172
- "src/lib/skill-learning.ts"() {
5708
+ 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;
5709
+ var init_tmux_routing = __esm({
5710
+ "src/lib/tmux-routing.ts"() {
5173
5711
  "use strict";
5174
- init_database();
5175
- init_behaviors();
5176
- init_config();
5177
- DEFAULT_SKILL_THRESHOLD = 3;
5712
+ init_session_registry();
5713
+ init_session_key();
5714
+ init_transport();
5715
+ init_cc_agent_support();
5716
+ init_mcp_prefix();
5717
+ init_provider_table();
5718
+ init_intercom_queue();
5719
+ init_plan_limits();
5720
+ SPAWN_LOCK_DIR = path17.join(os7.homedir(), ".exe-os", "spawn-locks");
5721
+ SESSION_CACHE = path17.join(os7.homedir(), ".exe-os", "session-cache");
5722
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5723
+ VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
5724
+ VERIFY_PANE_LINES = 200;
5725
+ INTERCOM_DEBOUNCE_MS = 3e4;
5726
+ INTERCOM_LOG2 = path17.join(os7.homedir(), ".exe-os", "intercom.log");
5727
+ DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
5728
+ DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5729
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
5178
5730
  }
5179
5731
  });
5180
5732
 
5181
- // src/lib/tasks.ts
5182
- var tasks_exports = {};
5183
- __export(tasks_exports, {
5184
- countNewPendingReviewsSince: () => countNewPendingReviewsSince,
5185
- countPendingReviews: () => countPendingReviews,
5186
- createTask: () => createTask,
5187
- createTaskCore: () => createTaskCore,
5188
- deleteTask: () => deleteTask,
5189
- deleteTaskCore: () => deleteTaskCore,
5190
- ensureArchitectureDoc: () => ensureArchitectureDoc,
5191
- ensureGitignoreExe: () => ensureGitignoreExe,
5192
- getReviewChecklist: () => getReviewChecklist,
5193
- listPendingReviews: () => listPendingReviews,
5194
- listTasks: () => listTasks,
5195
- resolveTask: () => resolveTask,
5196
- slugify: () => slugify,
5197
- updateTask: () => updateTask,
5198
- updateTaskStatus: () => updateTaskStatus,
5199
- writeCheckpoint: () => writeCheckpoint
5733
+ // src/lib/messaging.ts
5734
+ var messaging_exports = {};
5735
+ __export(messaging_exports, {
5736
+ deliverLocalMessage: () => deliverLocalMessage,
5737
+ getFailedMessages: () => getFailedMessages,
5738
+ getMessageStatus: () => getMessageStatus,
5739
+ getPendingMessages: () => getPendingMessages,
5740
+ getReadMessages: () => getReadMessages,
5741
+ getUnacknowledgedMessages: () => getUnacknowledgedMessages,
5742
+ markAcknowledged: () => markAcknowledged,
5743
+ markFailed: () => markFailed,
5744
+ markProcessed: () => markProcessed,
5745
+ markRead: () => markRead,
5746
+ retryPendingMessages: () => retryPendingMessages,
5747
+ sendMessage: () => sendMessage,
5748
+ setWsClientSend: () => setWsClientSend
5200
5749
  });
5201
- import path17 from "path";
5202
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
5203
- async function createTask(input) {
5204
- const result = await createTaskCore(input);
5205
- if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
5206
- dispatchTaskToEmployee({
5207
- assignedTo: input.assignedTo,
5208
- title: input.title,
5209
- priority: input.priority,
5210
- taskFile: result.taskFile,
5211
- initialStatus: result.status,
5212
- projectName: input.projectName
5750
+ import crypto8 from "crypto";
5751
+ function generateUlid() {
5752
+ const timestamp = Date.now().toString(36).padStart(10, "0");
5753
+ const random = crypto8.randomBytes(10).toString("hex").slice(0, 16);
5754
+ return (timestamp + random).toUpperCase();
5755
+ }
5756
+ function rowToMessage(row) {
5757
+ return {
5758
+ id: row.id,
5759
+ fromAgent: row.from_agent,
5760
+ fromDevice: row.from_device,
5761
+ targetAgent: row.target_agent,
5762
+ targetProject: row.target_project ?? null,
5763
+ targetDevice: row.target_device,
5764
+ content: row.content,
5765
+ priority: row.priority ?? "normal",
5766
+ status: row.status ?? "pending",
5767
+ serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
5768
+ retryCount: Number(row.retry_count ?? 0),
5769
+ createdAt: row.created_at,
5770
+ deliveredAt: row.delivered_at ?? null,
5771
+ processedAt: row.processed_at ?? null,
5772
+ failedAt: row.failed_at ?? null,
5773
+ failureReason: row.failure_reason ?? null
5774
+ };
5775
+ }
5776
+ async function sendMessage(input) {
5777
+ const client = getClient();
5778
+ const id = generateUlid();
5779
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5780
+ const targetDevice = input.targetDevice ?? "local";
5781
+ await client.execute({
5782
+ sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
5783
+ VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
5784
+ args: [
5785
+ id,
5786
+ input.fromAgent,
5787
+ input.targetAgent,
5788
+ input.targetProject ?? null,
5789
+ targetDevice,
5790
+ input.content,
5791
+ input.priority ?? "normal",
5792
+ now
5793
+ ]
5794
+ });
5795
+ try {
5796
+ if (targetDevice !== "local") {
5797
+ await deliverCrossMachineMessage(id, targetDevice);
5798
+ } else {
5799
+ await deliverLocalMessage(id);
5800
+ }
5801
+ } catch {
5802
+ }
5803
+ const result = await client.execute({
5804
+ sql: "SELECT * FROM messages WHERE id = ?",
5805
+ args: [id]
5806
+ });
5807
+ return rowToMessage(result.rows[0]);
5808
+ }
5809
+ function setWsClientSend(fn) {
5810
+ _wsClientSend = fn;
5811
+ }
5812
+ async function deliverCrossMachineMessage(messageId, targetDevice) {
5813
+ const client = getClient();
5814
+ const result = await client.execute({
5815
+ sql: "SELECT * FROM messages WHERE id = ?",
5816
+ args: [messageId]
5817
+ });
5818
+ if (result.rows.length === 0) return false;
5819
+ const msg = rowToMessage(result.rows[0]);
5820
+ if (msg.status !== "pending") return false;
5821
+ if (!_wsClientSend) {
5822
+ return false;
5823
+ }
5824
+ const payload = JSON.stringify({
5825
+ id: msg.id,
5826
+ fromAgent: msg.fromAgent,
5827
+ targetAgent: msg.targetAgent,
5828
+ targetProject: msg.targetProject,
5829
+ content: msg.content,
5830
+ priority: msg.priority,
5831
+ createdAt: msg.createdAt
5832
+ });
5833
+ const sent = _wsClientSend(targetDevice, payload);
5834
+ if (sent) {
5835
+ await client.execute({
5836
+ sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
5837
+ args: [messageId]
5213
5838
  });
5839
+ return true;
5214
5840
  }
5215
- return result;
5841
+ return false;
5216
5842
  }
5217
- async function updateTask(input) {
5218
- const { row, taskFile, now, taskId } = await updateTaskStatus(input);
5843
+ async function deliverLocalMessage(messageId) {
5844
+ const client = getClient();
5845
+ const result = await client.execute({
5846
+ sql: "SELECT * FROM messages WHERE id = ?",
5847
+ args: [messageId]
5848
+ });
5849
+ if (result.rows.length === 0) return false;
5850
+ const msg = rowToMessage(result.rows[0]);
5851
+ if (msg.status !== "pending") return false;
5852
+ const targetAgent = msg.targetAgent;
5853
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5219
5854
  try {
5220
- const agent = String(row.assigned_to);
5221
- const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
5222
- const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
5223
- if (input.status === "in_progress") {
5224
- mkdirSync7(cacheDir, { recursive: true });
5225
- writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5226
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
5227
- try {
5228
- unlinkSync5(cachePath);
5229
- } catch {
5230
- }
5855
+ const exeSession = resolveExeSession();
5856
+ if (!exeSession) {
5857
+ throw new Error("No exe session found");
5858
+ }
5859
+ const ensureResult = ensureEmployee(targetAgent, exeSession, process.cwd());
5860
+ if (ensureResult.status === "failed") {
5861
+ throw new Error(ensureResult.error ?? "ensureEmployee failed");
5231
5862
  }
5863
+ await client.execute({
5864
+ sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
5865
+ args: [now, messageId]
5866
+ });
5867
+ return true;
5232
5868
  } catch {
5233
- }
5234
- if (input.status === "done") {
5235
- await cleanupReviewFile(row, taskFile, input.baseDir);
5236
- }
5237
- if (input.status === "done" || input.status === "cancelled") {
5238
- try {
5239
- const client = getClient();
5240
- const taskTitle = String(row.title);
5241
- const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
5869
+ const newRetryCount = msg.retryCount + 1;
5870
+ if (newRetryCount >= MAX_RETRIES2) {
5871
+ await markFailed(messageId, "session unavailable after 10 retries");
5872
+ } else {
5242
5873
  await client.execute({
5243
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
5244
- WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
5245
- args: [now, `%left '${escaped}' as in\\_progress%`]
5874
+ sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
5875
+ args: [newRetryCount, messageId]
5246
5876
  });
5247
- } catch {
5248
- }
5249
- }
5250
- const isTerminal = input.status === "done" || input.status === "needs_review";
5251
- if (isTerminal) {
5252
- const isExe = String(row.assigned_to) === "exe";
5253
- if (!isExe) {
5254
- notifyTaskDone();
5255
- }
5256
- await markTaskNotificationsRead(taskFile);
5257
- if (input.status === "done") {
5258
- try {
5259
- await cascadeUnblock(taskId, input.baseDir, now);
5260
- } catch {
5261
- }
5262
- if (row.parent_task_id) {
5263
- try {
5264
- await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
5265
- } catch {
5266
- }
5267
- }
5268
- }
5269
- }
5270
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
5271
- Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
5272
- ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
5273
- taskId,
5274
- agentId: String(row.assigned_to),
5275
- projectName: String(row.project_name),
5276
- taskTitle: String(row.title)
5277
- })
5278
- ).catch((err) => {
5279
- process.stderr.write(
5280
- `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
5281
- `
5282
- );
5283
- });
5284
- }
5285
- let nextTask;
5286
- if (isTerminal && String(row.assigned_to) !== "exe") {
5287
- try {
5288
- nextTask = await findNextTask(String(row.assigned_to));
5289
- } catch {
5290
5877
  }
5878
+ return false;
5291
5879
  }
5292
- return {
5293
- id: String(row.id),
5294
- title: String(row.title),
5295
- assignedTo: String(row.assigned_to),
5296
- assignedBy: String(row.assigned_by),
5297
- projectName: String(row.project_name),
5298
- priority: String(row.priority),
5299
- status: input.status,
5300
- taskFile,
5301
- createdAt: String(row.created_at),
5302
- updatedAt: now,
5303
- budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
5304
- budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
5305
- tokensUsed: Number(row.tokens_used ?? 0),
5306
- tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
5307
- nextTask
5308
- };
5309
5880
  }
5310
- async function deleteTask(taskId, baseDir) {
5881
+ async function getPendingMessages(targetAgent) {
5882
+ const client = getClient();
5883
+ const result = await client.execute({
5884
+ sql: `SELECT * FROM messages
5885
+ WHERE target_agent = ? AND status IN ('pending', 'delivered')
5886
+ ORDER BY id`,
5887
+ args: [targetAgent]
5888
+ });
5889
+ return result.rows.map((row) => rowToMessage(row));
5890
+ }
5891
+ async function markRead(messageId) {
5311
5892
  const client = getClient();
5312
- const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
5313
- const reviewer = assignedBy || "exe";
5314
- const reviewSlug = `review-${assignedTo}-${taskSlug}`;
5315
- const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
5316
5893
  await client.execute({
5317
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
5318
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
5894
+ sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
5895
+ args: [messageId]
5319
5896
  });
5320
- await markAsReadByTaskFile(taskFile);
5321
- await markAsReadByTaskFile(reviewFile);
5322
5897
  }
5323
- var init_tasks = __esm({
5324
- "src/lib/tasks.ts"() {
5898
+ async function markAcknowledged(messageId) {
5899
+ const client = getClient();
5900
+ await client.execute({
5901
+ sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
5902
+ args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
5903
+ });
5904
+ }
5905
+ async function markProcessed(messageId) {
5906
+ const client = getClient();
5907
+ await client.execute({
5908
+ sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
5909
+ args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
5910
+ });
5911
+ }
5912
+ async function getMessageStatus(messageId) {
5913
+ const client = getClient();
5914
+ const result = await client.execute({
5915
+ sql: "SELECT status FROM messages WHERE id = ?",
5916
+ args: [messageId]
5917
+ });
5918
+ return result.rows[0]?.status ?? null;
5919
+ }
5920
+ async function getUnacknowledgedMessages(targetAgent) {
5921
+ const client = getClient();
5922
+ const result = await client.execute({
5923
+ sql: `SELECT * FROM messages
5924
+ WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
5925
+ ORDER BY id`,
5926
+ args: [targetAgent]
5927
+ });
5928
+ return result.rows.map((row) => rowToMessage(row));
5929
+ }
5930
+ async function getReadMessages(targetAgent) {
5931
+ const client = getClient();
5932
+ const result = await client.execute({
5933
+ sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
5934
+ args: [targetAgent]
5935
+ });
5936
+ return result.rows.map((row) => rowToMessage(row));
5937
+ }
5938
+ async function markFailed(messageId, reason) {
5939
+ const client = getClient();
5940
+ await client.execute({
5941
+ sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
5942
+ args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
5943
+ });
5944
+ }
5945
+ async function getFailedMessages() {
5946
+ const client = getClient();
5947
+ const result = await client.execute({
5948
+ sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
5949
+ args: []
5950
+ });
5951
+ return result.rows.map((row) => rowToMessage(row));
5952
+ }
5953
+ async function retryPendingMessages() {
5954
+ const client = getClient();
5955
+ const result = await client.execute({
5956
+ sql: `SELECT * FROM messages
5957
+ WHERE status = 'pending' AND retry_count < ?
5958
+ ORDER BY id`,
5959
+ args: [MAX_RETRIES2]
5960
+ });
5961
+ let delivered = 0;
5962
+ for (const row of result.rows) {
5963
+ const msg = rowToMessage(row);
5964
+ try {
5965
+ const success = await deliverLocalMessage(msg.id);
5966
+ if (success) delivered++;
5967
+ } catch {
5968
+ }
5969
+ }
5970
+ return delivered;
5971
+ }
5972
+ var MAX_RETRIES2, _wsClientSend;
5973
+ var init_messaging = __esm({
5974
+ "src/lib/messaging.ts"() {
5325
5975
  "use strict";
5326
5976
  init_database();
5327
- init_config();
5328
- init_notifications();
5329
- init_tasks_crud();
5330
- init_tasks_review();
5331
- init_tasks_crud();
5332
- init_tasks_chain();
5333
- init_tasks_review();
5334
- init_tasks_notify();
5977
+ init_tmux_routing();
5978
+ MAX_RETRIES2 = 10;
5979
+ _wsClientSend = null;
5335
5980
  }
5336
5981
  });
5337
5982
 
5983
+ // src/gateway/gateway.ts
5984
+ init_state_bus();
5985
+
5338
5986
  // src/gateway/router.ts
5339
5987
  function matchesPlatform(msgPlatform, matchPlatform) {
5340
5988
  if (!matchPlatform) return true;
@@ -5771,6 +6419,13 @@ var Gateway = class {
5771
6419
  console.log(
5772
6420
  `[gateway] ${msg.platform}/${msg.senderId} \u2192 ${route.employee} (${route.routeName})`
5773
6421
  );
6422
+ orgBus.emit({
6423
+ type: "gateway_message",
6424
+ platform: msg.platform,
6425
+ senderId: msg.senderId,
6426
+ botId: route.employee,
6427
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6428
+ });
5774
6429
  const bot = this.botRegistry.get(route.employee);
5775
6430
  if (!bot) {
5776
6431
  console.error(`[gateway] No bot registered for target: ${route.employee}`);
@@ -6564,7 +7219,7 @@ var AnthropicProvider = class {
6564
7219
 
6565
7220
  // src/gateway/providers/openai-compat.ts
6566
7221
  import OpenAI from "openai";
6567
- import { randomUUID as randomUUID2 } from "crypto";
7222
+ import { randomUUID as randomUUID3 } from "crypto";
6568
7223
  var OpenAICompatProvider = class {
6569
7224
  name;
6570
7225
  client;
@@ -6677,7 +7332,7 @@ var OpenAICompatProvider = class {
6677
7332
  }
6678
7333
  content.push({
6679
7334
  type: "tool_use",
6680
- id: call.id ?? randomUUID2(),
7335
+ id: call.id ?? randomUUID3(),
6681
7336
  name: fn.name,
6682
7337
  input
6683
7338
  });
@@ -6699,7 +7354,7 @@ var OpenAICompatProvider = class {
6699
7354
  };
6700
7355
 
6701
7356
  // src/gateway/providers/ollama.ts
6702
- import { randomUUID as randomUUID3 } from "crypto";
7357
+ import { randomUUID as randomUUID4 } from "crypto";
6703
7358
  var OllamaProvider = class {
6704
7359
  name;
6705
7360
  host;
@@ -6768,7 +7423,7 @@ var OllamaProvider = class {
6768
7423
  for (const call of data.message.tool_calls) {
6769
7424
  content.push({
6770
7425
  type: "tool_use",
6771
- id: randomUUID3(),
7426
+ id: randomUUID4(),
6772
7427
  name: call.function.name,
6773
7428
  input: call.function.arguments
6774
7429
  });
@@ -6790,7 +7445,7 @@ var OllamaProvider = class {
6790
7445
  };
6791
7446
 
6792
7447
  // src/gateway/adapters/whatsapp.ts
6793
- import { randomUUID as randomUUID4 } from "crypto";
7448
+ import { randomUUID as randomUUID5 } from "crypto";
6794
7449
  import { homedir } from "os";
6795
7450
  import { join } from "path";
6796
7451
  import { mkdirSync as mkdirSync2 } from "fs";
@@ -6972,7 +7627,7 @@ var WhatsAppAdapter = class {
6972
7627
  const location = this.extractLocation(msg.message);
6973
7628
  const dataCategory = location ? "location" : "message";
6974
7629
  return {
6975
- messageId: msg.key.id ?? randomUUID4(),
7630
+ messageId: msg.key.id ?? randomUUID5(),
6976
7631
  platform: "whatsapp",
6977
7632
  senderId,
6978
7633
  senderName: msg.pushName ?? void 0,
@@ -7017,7 +7672,7 @@ var WhatsAppAdapter = class {
7017
7672
  }
7018
7673
  const timestamp = receipt.readTimestamp ?? receipt.receiptTimestamp ?? Date.now() / 1e3;
7019
7674
  return {
7020
- messageId: randomUUID4(),
7675
+ messageId: randomUUID5(),
7021
7676
  platform: "whatsapp",
7022
7677
  senderId: remoteJid.replace("@s.whatsapp.net", "").replace("@g.us", ""),
7023
7678
  channelId: remoteJid,
@@ -7040,7 +7695,7 @@ var WhatsAppAdapter = class {
7040
7695
  const phone = id.replace("@s.whatsapp.net", "").replace("@g.us", "");
7041
7696
  const name = contact.name ?? contact.notify ?? phone;
7042
7697
  return {
7043
- messageId: randomUUID4(),
7698
+ messageId: randomUUID5(),
7044
7699
  platform: "whatsapp",
7045
7700
  senderId: phone,
7046
7701
  senderName: name,
@@ -7064,7 +7719,7 @@ var WhatsAppAdapter = class {
7064
7719
  const participants = (group.participants ?? []).map((p) => p.id ?? p);
7065
7720
  const admins = (group.participants ?? []).filter((p) => p.admin === "admin" || p.admin === "superadmin").map((p) => p.id ?? p);
7066
7721
  return {
7067
- messageId: randomUUID4(),
7722
+ messageId: randomUUID5(),
7068
7723
  platform: "whatsapp",
7069
7724
  senderId: groupId,
7070
7725
  channelId: groupId,
@@ -7089,7 +7744,7 @@ var WhatsAppAdapter = class {
7089
7744
  if (!reactionData) return null;
7090
7745
  const remoteJid = key.remoteJid ?? "";
7091
7746
  return {
7092
- messageId: randomUUID4(),
7747
+ messageId: randomUUID5(),
7093
7748
  platform: "whatsapp",
7094
7749
  senderId: reactionData.key?.participant ?? reactionData.key?.remoteJid?.replace("@s.whatsapp.net", "") ?? "",
7095
7750
  channelId: remoteJid,
@@ -7111,7 +7766,7 @@ var WhatsAppAdapter = class {
7111
7766
  if (!chatId) return null;
7112
7767
  const caller = call.from?.replace("@s.whatsapp.net", "") ?? "";
7113
7768
  return {
7114
- messageId: randomUUID4(),
7769
+ messageId: randomUUID5(),
7115
7770
  platform: "whatsapp",
7116
7771
  senderId: caller,
7117
7772
  channelId: chatId,
@@ -7154,7 +7809,7 @@ var WhatsAppAdapter = class {
7154
7809
  };
7155
7810
 
7156
7811
  // src/gateway/adapters/signal.ts
7157
- import { randomUUID as randomUUID5 } from "crypto";
7812
+ import { randomUUID as randomUUID6 } from "crypto";
7158
7813
  var DEFAULT_TIMEOUT_MS = 1e4;
7159
7814
  var SignalAdapter = class {
7160
7815
  platform = "signal";
@@ -7239,7 +7894,7 @@ var SignalAdapter = class {
7239
7894
  }
7240
7895
  }
7241
7896
  async rpcRequest(method, params) {
7242
- const id = randomUUID5();
7897
+ const id = randomUUID6();
7243
7898
  const res = await fetch(`${this.baseUrl}/api/v1/rpc`, {
7244
7899
  method: "POST",
7245
7900
  headers: { "Content-Type": "application/json" },
@@ -7329,7 +7984,7 @@ ${val}` : val;
7329
7984
  if (envelope.reactionMessage) {
7330
7985
  const rm = envelope.reactionMessage;
7331
7986
  const normalized2 = {
7332
- messageId: randomUUID5(),
7987
+ messageId: randomUUID6(),
7333
7988
  platform: "signal",
7334
7989
  senderId,
7335
7990
  senderName: envelope.sourceName ?? void 0,
@@ -7357,7 +8012,7 @@ ${val}` : val;
7357
8012
  const rcpt = envelope.receiptMessage;
7358
8013
  for (const ts of rcpt.timestamps) {
7359
8014
  const normalized2 = {
7360
- messageId: randomUUID5(),
8015
+ messageId: randomUUID6(),
7361
8016
  platform: "signal",
7362
8017
  senderId,
7363
8018
  senderName: envelope.sourceName ?? void 0,
@@ -7387,7 +8042,7 @@ ${val}` : val;
7387
8042
  const dm2 = em.dataMessage;
7388
8043
  const isGroup2 = !!dm2.groupInfo?.groupId;
7389
8044
  const normalized2 = {
7390
- messageId: String(dm2.timestamp ?? randomUUID5()),
8045
+ messageId: String(dm2.timestamp ?? randomUUID6()),
7391
8046
  platform: "signal",
7392
8047
  senderId,
7393
8048
  senderName: envelope.sourceName ?? void 0,
@@ -7413,7 +8068,7 @@ ${val}` : val;
7413
8068
  const dm = envelope.dataMessage;
7414
8069
  const isGroup = !!dm.groupInfo?.groupId;
7415
8070
  const normalized = {
7416
- messageId: String(dm.timestamp ?? randomUUID5()),
8071
+ messageId: String(dm.timestamp ?? randomUUID6()),
7417
8072
  platform: "signal",
7418
8073
  senderId,
7419
8074
  senderName: envelope.sourceName ?? void 0,
@@ -7454,7 +8109,7 @@ ${val}` : val;
7454
8109
  if (!phone) continue;
7455
8110
  const name = contact.name ?? contact.profileName ?? phone;
7456
8111
  const normalized = {
7457
- messageId: randomUUID5(),
8112
+ messageId: randomUUID6(),
7458
8113
  platform: "signal",
7459
8114
  senderId: phone,
7460
8115
  senderName: name,
@@ -7483,7 +8138,7 @@ ${val}` : val;
7483
8138
  if (!Array.isArray(groups)) return;
7484
8139
  for (const group of groups) {
7485
8140
  const normalized = {
7486
- messageId: randomUUID5(),
8141
+ messageId: randomUUID6(),
7487
8142
  platform: "signal",
7488
8143
  senderId: `group:${group.id}`,
7489
8144
  channelId: `group:${group.id}`,
@@ -7518,7 +8173,7 @@ ${val}` : val;
7518
8173
  };
7519
8174
 
7520
8175
  // src/gateway/adapters/webchat.ts
7521
- import { randomUUID as randomUUID6 } from "crypto";
8176
+ import { randomUUID as randomUUID7 } from "crypto";
7522
8177
  import { createServer } from "http";
7523
8178
  var WebChatAdapter = class {
7524
8179
  platform = "webchat";
@@ -7614,7 +8269,7 @@ var WebChatAdapter = class {
7614
8269
  res.end(JSON.stringify({ error: "No message text" }));
7615
8270
  return;
7616
8271
  }
7617
- const requestId = randomUUID6();
8272
+ const requestId = randomUUID7();
7618
8273
  const sessionId = parsed.sessionId ?? this.extractSessionId(req);
7619
8274
  const normalized = {
7620
8275
  messageId: requestId,
@@ -7657,7 +8312,7 @@ var WebChatAdapter = class {
7657
8312
  extractSessionId(req) {
7658
8313
  const cookies = req.headers.cookie ?? "";
7659
8314
  const match = cookies.match(/exe_session=([^;]+)/);
7660
- return match?.[1] ?? `anon-${randomUUID6().slice(0, 8)}`;
8315
+ return match?.[1] ?? `anon-${randomUUID7().slice(0, 8)}`;
7661
8316
  }
7662
8317
  };
7663
8318
 
@@ -7939,7 +8594,7 @@ var DiscordAdapter = class {
7939
8594
  };
7940
8595
 
7941
8596
  // src/gateway/adapters/slack.ts
7942
- import { randomUUID as randomUUID7 } from "crypto";
8597
+ import { randomUUID as randomUUID8 } from "crypto";
7943
8598
  var SlackAdapter = class {
7944
8599
  platform = "slack";
7945
8600
  webClient = null;
@@ -7977,7 +8632,7 @@ var SlackAdapter = class {
7977
8632
  if (event.subtype) return;
7978
8633
  const isGroup = event.channel_type !== "im";
7979
8634
  const normalized = {
7980
- messageId: event.client_msg_id ?? event.ts ?? randomUUID7(),
8635
+ messageId: event.client_msg_id ?? event.ts ?? randomUUID8(),
7981
8636
  platform: "slack",
7982
8637
  senderId: event.user ?? "",
7983
8638
  channelId: event.channel ?? "",
@@ -8039,7 +8694,7 @@ var SlackAdapter = class {
8039
8694
  if (!event.text) return;
8040
8695
  const isGroup = event.channel_type !== "im";
8041
8696
  const normalized = {
8042
- messageId: event.ts ?? randomUUID7(),
8697
+ messageId: event.ts ?? randomUUID8(),
8043
8698
  platform: "slack",
8044
8699
  senderId: event.user ?? "",
8045
8700
  senderName: event.user_profile?.display_name ?? event.user_profile?.real_name ?? void 0,
@@ -8424,7 +9079,7 @@ var FailoverExhaustedError = class extends Error {
8424
9079
  };
8425
9080
 
8426
9081
  // src/gateway/session-store.ts
8427
- import { randomUUID as randomUUID8 } from "crypto";
9082
+ import { randomUUID as randomUUID9 } from "crypto";
8428
9083
  var DEFAULT_CONFIG3 = {
8429
9084
  idleTimeoutMs: 30 * 6e4,
8430
9085
  maxMessages: 100
@@ -8451,7 +9106,7 @@ var SessionStore = class {
8451
9106
  existing.status = "closed";
8452
9107
  }
8453
9108
  const session = {
8454
- sessionId: randomUUID8(),
9109
+ sessionId: randomUUID9(),
8455
9110
  customerId,
8456
9111
  botId,
8457
9112
  platform,
@@ -8848,7 +9503,7 @@ function formatAlert(alert) {
8848
9503
  }
8849
9504
 
8850
9505
  // src/gateway/customer-store.ts
8851
- import { randomUUID as randomUUID9 } from "crypto";
9506
+ import { randomUUID as randomUUID10 } from "crypto";
8852
9507
  var CustomerStore = class {
8853
9508
  customers = /* @__PURE__ */ new Map();
8854
9509
  identities = /* @__PURE__ */ new Map();
@@ -8867,7 +9522,7 @@ var CustomerStore = class {
8867
9522
  return customer2;
8868
9523
  }
8869
9524
  const customer = {
8870
- id: randomUUID9(),
9525
+ id: randomUUID10(),
8871
9526
  firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
8872
9527
  lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
8873
9528
  interactionCount: 1
@@ -8927,7 +9582,7 @@ async function ensureCRMContact(info) {
8927
9582
 
8928
9583
  // src/automation/trigger-engine.ts
8929
9584
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
8930
- import { randomUUID as randomUUID11 } from "crypto";
9585
+ import { randomUUID as randomUUID12 } from "crypto";
8931
9586
  import path18 from "path";
8932
9587
  import os8 from "os";
8933
9588
  var TRIGGERS_PATH = path18.join(os8.homedir(), ".exe-os", "triggers.json");