@askexenow/exe-os 0.8.41 → 0.8.43

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 (76) 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 +1345 -660
  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 +2518 -1798
  9. package/dist/bin/exe-call.js +39 -1
  10. package/dist/bin/exe-cloud.js +15 -1
  11. package/dist/bin/exe-dispatch.js +39 -2
  12. package/dist/bin/exe-doctor.js +790 -633
  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 +2500 -1877
  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 +28 -2
  20. package/dist/bin/exe-new-employee.js +25 -3
  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 +147 -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 +154 -3
  27. package/dist/bin/exe-session-cleanup.js +2466 -413
  28. package/dist/bin/exe-status.js +474 -317
  29. package/dist/bin/exe-team.js +474 -317
  30. package/dist/bin/git-sweep.js +2690 -150
  31. package/dist/bin/graph-backfill.js +794 -637
  32. package/dist/bin/graph-export.js +798 -641
  33. package/dist/bin/scan-tasks.js +2951 -44
  34. package/dist/bin/setup.js +62 -26
  35. package/dist/bin/shard-migrate.js +792 -637
  36. package/dist/bin/wiki-sync.js +794 -637
  37. package/dist/gateway/index.js +2504 -1895
  38. package/dist/hooks/bug-report-worker.js +2118 -576
  39. package/dist/hooks/commit-complete.js +2689 -149
  40. package/dist/hooks/error-recall.js +154 -3
  41. package/dist/hooks/ingest-worker.js +1439 -815
  42. package/dist/hooks/instructions-loaded.js +151 -0
  43. package/dist/hooks/notification.js +153 -2
  44. package/dist/hooks/post-compact.js +164 -0
  45. package/dist/hooks/pre-compact.js +3073 -101
  46. package/dist/hooks/pre-tool-use.js +151 -0
  47. package/dist/hooks/prompt-ingest-worker.js +1714 -1537
  48. package/dist/hooks/prompt-submit.js +2658 -1113
  49. package/dist/hooks/response-ingest-worker.js +170 -6
  50. package/dist/hooks/session-end.js +153 -2
  51. package/dist/hooks/session-start.js +154 -3
  52. package/dist/hooks/stop.js +151 -0
  53. package/dist/hooks/subagent-stop.js +151 -0
  54. package/dist/hooks/summary-worker.js +179 -7
  55. package/dist/index.js +278 -100
  56. package/dist/lib/cloud-sync.js +28 -2
  57. package/dist/lib/consolidation.js +69 -2
  58. package/dist/lib/database.js +19 -0
  59. package/dist/lib/device-registry.js +19 -0
  60. package/dist/lib/employee-templates.js +20 -1
  61. package/dist/lib/exe-daemon.js +236 -16
  62. package/dist/lib/hybrid-search.js +154 -3
  63. package/dist/lib/license.js +15 -1
  64. package/dist/lib/messaging.js +39 -2
  65. package/dist/lib/schedules.js +792 -637
  66. package/dist/lib/store.js +796 -636
  67. package/dist/lib/tasks.js +1614 -1091
  68. package/dist/lib/tmux-routing.js +149 -9
  69. package/dist/mcp/server.js +1825 -1138
  70. package/dist/mcp/tools/create-task.js +2280 -828
  71. package/dist/mcp/tools/list-tasks.js +2788 -159
  72. package/dist/mcp/tools/send-message.js +39 -2
  73. package/dist/mcp/tools/update-task.js +64 -0
  74. package/dist/runtime/index.js +235 -67
  75. package/dist/tui/App.js +1452 -644
  76. 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,
@@ -2128,6 +2202,71 @@ var init_shard_manager = __esm({
2128
2202
  }
2129
2203
  });
2130
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
+
2131
2270
  // src/lib/store.ts
2132
2271
  var store_exports = {};
2133
2272
  __export(store_exports, {
@@ -2207,6 +2346,11 @@ async function initStore(options) {
2207
2346
  "version-query"
2208
2347
  );
2209
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
+ }
2210
2354
  }
2211
2355
  function classifyTier(record) {
2212
2356
  if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
@@ -2248,6 +2392,12 @@ async function writeMemory(record) {
2248
2392
  supersedes_id: record.supersedes_id ?? null
2249
2393
  };
2250
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
+ });
2251
2401
  const MAX_PENDING = 1e3;
2252
2402
  if (_pendingRecords.length > MAX_PENDING) {
2253
2403
  const dropped = _pendingRecords.length - MAX_PENDING;
@@ -2593,6 +2743,7 @@ var init_store = __esm({
2593
2743
  init_database();
2594
2744
  init_keychain();
2595
2745
  init_config();
2746
+ init_state_bus();
2596
2747
  INIT_MAX_RETRIES = 3;
2597
2748
  INIT_RETRY_DELAY_MS = 1e3;
2598
2749
  _pendingRecords = [];
@@ -3132,7 +3283,7 @@ var init_employees = __esm({
3132
3283
 
3133
3284
  // src/lib/license.ts
3134
3285
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
3135
- import { randomUUID as randomUUID10 } from "crypto";
3286
+ import { randomUUID as randomUUID11 } from "crypto";
3136
3287
  import path9 from "path";
3137
3288
  import { jwtVerify, importSPKI } from "jose";
3138
3289
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
@@ -3231,2156 +3382,2607 @@ var init_plan_limits = __esm({
3231
3382
  }
3232
3383
  });
3233
3384
 
3234
- // src/lib/tmux-routing.ts
3235
- import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
3236
- 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";
3237
3387
  import path11 from "path";
3238
3388
  import os6 from "os";
3239
- import { fileURLToPath as fileURLToPath2 } from "url";
3240
- import { unlinkSync as unlinkSync2 } from "fs";
3241
- function spawnLockPath(sessionName) {
3242
- return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3243
- }
3244
- 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) {
3245
3397
  try {
3246
- process.kill(pid, 0);
3247
- return true;
3248
- } catch {
3249
- return false;
3250
- }
3251
- }
3252
- function acquireSpawnLock2(sessionName) {
3253
- if (!existsSync10(SPAWN_LOCK_DIR)) {
3254
- mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3255
- }
3256
- const lockFile = spawnLockPath(sessionName);
3257
- if (existsSync10(lockFile)) {
3258
- try {
3259
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
3260
- const age = Date.now() - lock.timestamp;
3261
- if (isProcessAlive(lock.pid) && age < 6e4) {
3262
- return false;
3263
- }
3264
- } catch {
3265
- }
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
+ `);
3266
3418
  }
3267
- writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3268
- return true;
3269
3419
  }
3270
- function releaseSpawnLock2(sessionName) {
3420
+ async function markAsReadByTaskFile(taskFile) {
3271
3421
  try {
3272
- 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
+ });
3273
3427
  } catch {
3274
3428
  }
3275
3429
  }
3276
- function resolveBehaviorsExporterScript() {
3277
- try {
3278
- const thisFile = fileURLToPath2(import.meta.url);
3279
- const scriptPath = path11.join(
3280
- path11.dirname(thisFile),
3281
- "..",
3282
- "bin",
3283
- "exe-export-behaviors.js"
3284
- );
3285
- return existsSync10(scriptPath) ? scriptPath : null;
3286
- } catch {
3287
- return null;
3430
+ var init_notifications = __esm({
3431
+ "src/lib/notifications.ts"() {
3432
+ "use strict";
3433
+ init_database();
3288
3434
  }
3289
- }
3290
- function exportBehaviorsSync(agentId, projectName, sessionKey) {
3291
- const script = resolveBehaviorsExporterScript();
3292
- if (!script) return null;
3435
+ });
3436
+
3437
+ // src/lib/session-kill-telemetry.ts
3438
+ import crypto4 from "crypto";
3439
+ async function recordSessionKill(input) {
3293
3440
  try {
3294
- const output = execFileSync2(
3295
- process.execPath,
3296
- [script, agentId, projectName, sessionKey],
3297
- { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
3298
- ).trim();
3299
- 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
+ });
3300
3457
  } catch (err) {
3301
3458
  process.stderr.write(
3302
- `[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)}
3303
3460
  `
3304
3461
  );
3305
- return null;
3306
- }
3307
- }
3308
- function getMySession() {
3309
- return getTransport().getMySession();
3310
- }
3311
- function employeeSessionName(employee, exeSession, instance) {
3312
- const suffix = instance != null && instance > 0 ? String(instance) : "";
3313
- return `${employee}${suffix}-${exeSession}`;
3314
- }
3315
- function extractRootExe(name) {
3316
- const match = name.match(/(exe\d+)$/);
3317
- return match?.[1] ?? null;
3318
- }
3319
- function getParentExe(sessionKey) {
3320
- try {
3321
- const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3322
- return data.parentExe || null;
3323
- } catch {
3324
- return null;
3325
- }
3326
- }
3327
- function getDispatchedBy(sessionKey) {
3328
- try {
3329
- const data = JSON.parse(readFileSync9(
3330
- path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3331
- "utf8"
3332
- ));
3333
- return data.dispatchedBy ?? data.parentExe ?? null;
3334
- } catch {
3335
- return null;
3336
3462
  }
3337
3463
  }
3338
- function resolveExeSession() {
3339
- const mySession = getMySession();
3340
- if (!mySession) return null;
3341
- try {
3342
- const key = getSessionKey();
3343
- const parentExe = getParentExe(key);
3344
- if (parentExe) {
3345
- return extractRootExe(parentExe) ?? parentExe;
3346
- }
3347
- } catch {
3464
+ var init_session_kill_telemetry = __esm({
3465
+ "src/lib/session-kill-telemetry.ts"() {
3466
+ "use strict";
3467
+ init_database();
3348
3468
  }
3349
- return extractRootExe(mySession) ?? mySession;
3350
- }
3351
- function isEmployeeAlive(sessionName) {
3352
- return getTransport().isAlive(sessionName);
3353
- }
3354
- function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
3355
- const base = employeeSessionName(employeeName, exeSession);
3356
- if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
3357
- for (let i = 2; i <= maxInstances; i++) {
3358
- const candidate = employeeSessionName(employeeName, exeSession, i);
3359
- 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));
3360
3485
  }
3361
- return null;
3362
- }
3363
- function readDebounceState() {
3364
- try {
3365
- if (!existsSync10(DEBOUNCE_FILE)) return {};
3366
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
3367
- } catch {
3368
- 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`);
3369
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 };
3370
3506
  }
3371
- function writeDebounceState(state) {
3372
- try {
3373
- if (!existsSync10(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3374
- writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
3375
- } catch {
3376
- }
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;
3377
3513
  }
3378
- function isDebounced(targetSession) {
3379
- const state = readDebounceState();
3380
- const lastSent = state[targetSession] ?? 0;
3381
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
3514
+ function slugify(title) {
3515
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3382
3516
  }
3383
- function recordDebounce(targetSession) {
3384
- const state = readDebounceState();
3385
- state[targetSession] = Date.now();
3386
- const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
3387
- for (const key of Object.keys(state)) {
3388
- 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
+ );
3389
3542
  }
3390
- writeDebounceState(state);
3391
- }
3392
- function logIntercom(msg) {
3393
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
3394
- `;
3395
- process.stderr.write(`[intercom] ${msg}
3396
- `);
3397
- try {
3398
- appendFileSync(INTERCOM_LOG2, line);
3399
- } 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
+ );
3400
3557
  }
3558
+ throw new Error(`Task not found: ${identifier}`);
3401
3559
  }
3402
- function getSessionState(sessionName) {
3403
- const transport = getTransport();
3404
- if (!transport.isAlive(sessionName)) return "offline";
3405
- try {
3406
- const pane = transport.capturePane(sessionName, 5);
3407
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
3408
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
3409
- 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
+ );
3410
3592
  }
3593
+ throw err;
3411
3594
  }
3412
- if (/Running…/.test(pane)) return "tool";
3413
- if (BUSY_PATTERN.test(pane)) return "thinking";
3414
- return "idle";
3415
- } catch {
3416
- return "offline";
3417
3595
  }
3418
- }
3419
- function isExeSession(sessionName) {
3420
- return /^exe\d*$/.test(sessionName);
3421
- }
3422
- function sendIntercom(targetSession) {
3423
- const transport = getTransport();
3424
- if (isExeSession(targetSession)) {
3425
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
3426
- 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.`;
3427
3603
  }
3428
- if (isDebounced(targetSession)) {
3429
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
3430
- 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
+ }
3431
3612
  }
3613
+ const complexity = input.complexity ?? "standard";
3614
+ let sessionScope = null;
3432
3615
  try {
3433
- const sessions = transport.listSessions();
3434
- if (!sessions.includes(targetSession)) {
3435
- logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
3436
- return "failed";
3437
- }
3438
- const sessionState = getSessionState(targetSession);
3439
- if (sessionState === "no_claude") {
3440
- queueIntercom(targetSession, "claude not running in session");
3441
- recordDebounce(targetSession);
3442
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3443
- return "queued";
3444
- }
3445
- if (sessionState === "thinking" || sessionState === "tool") {
3446
- queueIntercom(targetSession, "session busy at send time");
3447
- recordDebounce(targetSession);
3448
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
3449
- return "queued";
3450
- }
3451
- if (transport.isPaneInCopyMode(targetSession)) {
3452
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
3453
- transport.sendKeys(targetSession, "q");
3454
- }
3455
- transport.sendKeys(targetSession, "/exe-intercom");
3456
- recordDebounce(targetSession);
3457
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
3458
- return "delivered";
3616
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3617
+ sessionScope = resolveExeSession2();
3459
3618
  } catch {
3460
- logIntercom(`FAIL \u2192 ${targetSession}`);
3461
- return "failed";
3462
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
+ };
3463
3663
  }
3464
- function notifyParentExe(sessionKey) {
3465
- const target = getDispatchedBy(sessionKey);
3466
- if (!target) {
3467
- process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
3468
- `);
3469
- 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);
3470
3671
  }
3471
- process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
3472
- `);
3473
- const result = sendIntercom(target);
3474
- if (result === "failed") {
3475
- const rootExe = resolveExeSession();
3476
- if (rootExe && rootExe !== target) {
3477
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3478
- `);
3479
- const fallback = sendIntercom(rootExe);
3480
- return fallback !== "failed";
3481
- }
3482
- 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')");
3483
3677
  }
3484
- return true;
3485
- }
3486
- function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3487
- if (employeeName === "exe") {
3488
- 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);
3489
3685
  }
3490
3686
  try {
3491
- assertEmployeeLimitSync();
3492
- } catch (err) {
3493
- if (err instanceof PlanLimitError) {
3494
- 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);
3495
3692
  }
3693
+ } catch {
3496
3694
  }
3497
- if (/-exe\d*$/.test(employeeName)) {
3498
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
3499
- return {
3500
- status: "failed",
3501
- sessionName: "",
3502
- error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
3503
- };
3504
- }
3505
- let effectiveInstance = opts?.instance;
3506
- if (effectiveInstance === void 0 && opts?.autoInstance) {
3507
- const free = findFreeInstance(
3508
- employeeName,
3509
- exeSession,
3510
- opts.maxAutoInstances ?? 10
3511
- );
3512
- if (free === null) {
3513
- return {
3514
- status: "failed",
3515
- sessionName: employeeSessionName(employeeName, exeSession),
3516
- error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
3517
- };
3518
- }
3519
- effectiveInstance = free === 0 ? void 0 : free;
3520
- }
3521
- const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
3522
- if (isEmployeeAlive(sessionName)) {
3523
- const result2 = sendIntercom(sessionName);
3524
- if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
3525
- return { status: "intercom_sent", sessionName };
3526
- }
3527
- if (result2 === "delivered") {
3528
- return { status: "intercom_unprocessed", sessionName };
3529
- }
3530
- return { status: "failed", sessionName, error: "intercom delivery failed" };
3531
- }
3532
- const spawnOpts = { ...opts, instance: effectiveInstance };
3533
- const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
3534
- if (result.error) {
3535
- return { status: "failed", sessionName, error: result.error };
3536
- }
3537
- 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
+ }));
3538
3717
  }
3539
- function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3540
- const transport = getTransport();
3541
- const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3542
- const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3543
- const logDir = path11.join(os6.homedir(), ".exe-os", "session-logs");
3544
- const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3545
- if (!existsSync10(logDir)) {
3546
- mkdirSync6(logDir, { recursive: true });
3547
- }
3548
- transport.kill(sessionName);
3549
- let cleanupSuffix = "";
3718
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
3719
+ if (!taskContext) return null;
3720
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
3550
3721
  try {
3551
- const thisFile = fileURLToPath2(import.meta.url);
3552
- const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3553
- if (existsSync10(cleanupScript)) {
3554
- cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
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.";
3555
3735
  }
3736
+ return null;
3556
3737
  } catch {
3738
+ return null;
3557
3739
  }
3558
- try {
3559
- const claudeJsonPath = path11.join(os6.homedir(), ".claude.json");
3560
- let claudeJson = {};
3561
- try {
3562
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
3563
- } catch {
3564
- }
3565
- if (!claudeJson.projects) claudeJson.projects = {};
3566
- const projects = claudeJson.projects;
3567
- const trustDir = opts?.cwd ?? projectDir;
3568
- if (!projects[trustDir]) projects[trustDir] = {};
3569
- projects[trustDir].hasTrustDialogAccepted = true;
3570
- writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3571
- } catch {
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-")) {
3748
+ process.stderr.write(
3749
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
3750
+ `
3751
+ );
3572
3752
  }
3573
- try {
3574
- const settingsDir = path11.join(os6.homedir(), ".claude", "projects");
3575
- const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3576
- const projSettingsDir = path11.join(settingsDir, normalizedKey);
3577
- const settingsPath = path11.join(projSettingsDir, "settings.json");
3578
- let settings = {};
3579
- try {
3580
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
3581
- } catch {
3582
- }
3583
- const perms = settings.permissions ?? {};
3584
- const allow = perms.allow ?? [];
3585
- const toolNames = [
3586
- "recall_my_memory",
3587
- "store_memory",
3588
- "create_task",
3589
- "update_task",
3590
- "list_tasks",
3591
- "get_task",
3592
- "ask_team_memory",
3593
- "store_behavior",
3594
- "get_identity",
3595
- "send_message"
3596
- ];
3597
- const requiredTools = expandDualPrefixTools(toolNames);
3598
- let changed = false;
3599
- for (const tool of requiredTools) {
3600
- if (!allow.includes(tool)) {
3601
- allow.push(tool);
3602
- changed = true;
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
+ `);
3603
3767
  }
3604
3768
  }
3605
- if (changed) {
3606
- perms.allow = allow;
3607
- settings.permissions = perms;
3608
- mkdirSync6(projSettingsDir, { recursive: true });
3609
- writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3610
- }
3611
- } catch {
3612
3769
  }
3613
- const spawnCwd = opts?.cwd ?? projectDir;
3614
- const useExeAgent = !!(opts?.model && opts?.provider);
3615
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
3616
- const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
3617
- let identityFlag = "";
3618
- let behaviorsFlag = "";
3619
- let legacyFallbackWarned = false;
3620
- if (!useExeAgent && !useBinSymlink) {
3621
- const identityPath = path11.join(
3622
- os6.homedir(),
3623
- ".exe-os",
3624
- "identity",
3625
- `${employeeName}.md`
3626
- );
3627
- _resetCcAgentSupportCache();
3628
- const hasAgentFlag = claudeSupportsAgentFlag();
3629
- if (hasAgentFlag) {
3630
- identityFlag = ` --agent ${employeeName}`;
3631
- } else if (existsSync10(identityPath)) {
3632
- identityFlag = ` --append-system-prompt-file ${identityPath}`;
3633
- legacyFallbackWarned = true;
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}`);
3634
3787
  }
3635
- const behaviorsFile = exportBehaviorsSync(
3636
- employeeName,
3637
- path11.basename(spawnCwd),
3638
- sessionName
3639
- );
3640
- if (behaviorsFile) {
3641
- behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
3788
+ try {
3789
+ await writeCheckpoint({
3790
+ taskId,
3791
+ step: "claimed",
3792
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3793
+ });
3794
+ } catch {
3642
3795
  }
3796
+ return { row, taskFile, now, taskId };
3643
3797
  }
3644
- if (legacyFallbackWarned) {
3645
- process.stderr.write(
3646
- `[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.
3647
- `
3648
- );
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
+ });
3649
3808
  }
3650
- let sessionContextFlag = "";
3651
3809
  try {
3652
- const ctxDir = path11.join(os6.homedir(), ".exe-os", "session-cache");
3653
- mkdirSync6(ctxDir, { recursive: true });
3654
- const ctxFile = path11.join(ctxDir, `session-context-${sessionName}.md`);
3655
- const ctxContent = [
3656
- `## Session Context`,
3657
- `You are running in tmux session: ${sessionName}.`,
3658
- `Your parent exe session is ${exeSession}.`,
3659
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
3660
- ].join("\n");
3661
- writeFileSync4(ctxFile, ctxContent);
3662
- sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
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
+ });
3663
3815
  } catch {
3664
3816
  }
3665
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
3666
- if (ccProvider !== DEFAULT_PROVIDER) {
3667
- const cfg = PROVIDER_TABLE[ccProvider];
3668
- if (cfg?.apiKeyEnv) {
3669
- const keyVal = process.env[cfg.apiKeyEnv];
3670
- if (keyVal) {
3671
- envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
3672
- }
3673
- }
3674
- }
3675
- let spawnCommand;
3676
- if (useExeAgent) {
3677
- spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
3678
- } else if (useBinSymlink) {
3679
- const binName = `${employeeName}-${ccProvider}`;
3680
- process.stderr.write(
3681
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
3682
- `
3683
- );
3684
- spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
3685
- } else {
3686
- spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
3687
- }
3688
- const spawnResult = transport.spawn(sessionName, {
3689
- cwd: spawnCwd,
3690
- command: spawnCommand
3691
- });
3692
- if (spawnResult.error) {
3693
- releaseSpawnLock2(sessionName);
3694
- return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3695
- }
3696
- transport.pipeLog(sessionName, logFile);
3697
- try {
3698
- const mySession = getMySession();
3699
- const dispatchInfo = path11.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
3700
- writeFileSync4(dispatchInfo, JSON.stringify({
3701
- dispatchedBy: mySession,
3702
- rootExe: exeSession,
3703
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
3704
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
3705
- }));
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");
3706
3862
  } catch {
3707
3863
  }
3708
- let booted = false;
3709
- for (let i = 0; i < 30; i++) {
3710
- try {
3711
- execSync4("sleep 0.5");
3712
- } catch {
3713
- }
3714
- try {
3715
- const pane = transport.capturePane(sessionName);
3716
- if (useExeAgent) {
3717
- if (pane.includes("[exe-agent]") || pane.includes("online")) {
3718
- booted = true;
3719
- break;
3720
- }
3721
- } else {
3722
- if (pane.includes("Claude Code") || pane.includes("\u276F")) {
3723
- booted = true;
3724
- break;
3725
- }
3726
- }
3727
- } catch {
3728
- }
3729
- }
3730
- if (!booted) {
3731
- releaseSpawnLock2(sessionName);
3732
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3733
- }
3734
- if (!useExeAgent) {
3735
- try {
3736
- transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
3737
- } 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");
3738
3874
  }
3875
+ } catch {
3739
3876
  }
3740
- registerSession({
3741
- windowName: sessionName,
3742
- agentId: employeeName,
3743
- projectDir: spawnCwd,
3744
- parentExe: exeSession,
3745
- pid: 0,
3746
- registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3747
- });
3748
- releaseSpawnLock2(sessionName);
3749
- return { sessionName };
3750
3877
  }
3751
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3752
- var init_tmux_routing = __esm({
3753
- "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"() {
3754
3881
  "use strict";
3755
- init_session_registry();
3756
- init_session_key();
3757
- init_transport();
3758
- init_cc_agent_support();
3759
- init_mcp_prefix();
3760
- init_provider_table();
3761
- init_intercom_queue();
3762
- init_plan_limits();
3763
- SPAWN_LOCK_DIR = path11.join(os6.homedir(), ".exe-os", "spawn-locks");
3764
- SESSION_CACHE = path11.join(os6.homedir(), ".exe-os", "session-cache");
3765
- BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3766
- INTERCOM_DEBOUNCE_MS = 3e4;
3767
- INTERCOM_LOG2 = path11.join(os6.homedir(), ".exe-os", "intercom.log");
3768
- DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
3769
- DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3770
- BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3882
+ init_database();
3883
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
3884
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3771
3885
  }
3772
3886
  });
3773
3887
 
3774
- // src/lib/messaging.ts
3775
- var messaging_exports = {};
3776
- __export(messaging_exports, {
3777
- deliverLocalMessage: () => deliverLocalMessage,
3778
- getFailedMessages: () => getFailedMessages,
3779
- getMessageStatus: () => getMessageStatus,
3780
- getPendingMessages: () => getPendingMessages,
3781
- getReadMessages: () => getReadMessages,
3782
- getUnacknowledgedMessages: () => getUnacknowledgedMessages,
3783
- markAcknowledged: () => markAcknowledged,
3784
- markFailed: () => markFailed,
3785
- markProcessed: () => markProcessed,
3786
- markRead: () => markRead,
3787
- retryPendingMessages: () => retryPendingMessages,
3788
- sendMessage: () => sendMessage,
3789
- setWsClientSend: () => setWsClientSend
3790
- });
3791
- import crypto3 from "crypto";
3792
- function generateUlid() {
3793
- const timestamp = Date.now().toString(36).padStart(10, "0");
3794
- const random = crypto3.randomBytes(10).toString("hex").slice(0, 16);
3795
- return (timestamp + random).toUpperCase();
3796
- }
3797
- function rowToMessage(row) {
3798
- return {
3799
- id: row.id,
3800
- fromAgent: row.from_agent,
3801
- fromDevice: row.from_device,
3802
- targetAgent: row.target_agent,
3803
- targetProject: row.target_project ?? null,
3804
- targetDevice: row.target_device,
3805
- content: row.content,
3806
- priority: row.priority ?? "normal",
3807
- status: row.status ?? "pending",
3808
- serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
3809
- retryCount: Number(row.retry_count ?? 0),
3810
- createdAt: row.created_at,
3811
- deliveredAt: row.delivered_at ?? null,
3812
- processedAt: row.processed_at ?? null,
3813
- failedAt: row.failed_at ?? null,
3814
- failureReason: row.failure_reason ?? null
3815
- };
3816
- }
3817
- 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() {
3818
3892
  const client = getClient();
3819
- const id = generateUlid();
3820
- const now = (/* @__PURE__ */ new Date()).toISOString();
3821
- const targetDevice = input.targetDevice ?? "local";
3822
- await client.execute({
3823
- sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
3824
- VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
3825
- args: [
3826
- id,
3827
- input.fromAgent,
3828
- input.targetAgent,
3829
- input.targetProject ?? null,
3830
- targetDevice,
3831
- input.content,
3832
- input.priority ?? "normal",
3833
- now
3834
- ]
3835
- });
3836
- try {
3837
- if (targetDevice !== "local") {
3838
- await deliverCrossMachineMessage(id, targetDevice);
3839
- } else {
3840
- await deliverLocalMessage(id);
3841
- }
3842
- } catch {
3843
- }
3844
3893
  const result = await client.execute({
3845
- sql: "SELECT * FROM messages WHERE id = ?",
3846
- args: [id]
3894
+ sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3895
+ args: []
3847
3896
  });
3848
- return rowToMessage(result.rows[0]);
3849
- }
3850
- function setWsClientSend(fn) {
3851
- _wsClientSend = fn;
3897
+ return Number(result.rows[0]?.cnt) || 0;
3852
3898
  }
3853
- async function deliverCrossMachineMessage(messageId, targetDevice) {
3899
+ async function countNewPendingReviewsSince(sinceIso) {
3854
3900
  const client = getClient();
3855
3901
  const result = await client.execute({
3856
- sql: "SELECT * FROM messages WHERE id = ?",
3857
- args: [messageId]
3858
- });
3859
- if (result.rows.length === 0) return false;
3860
- const msg = rowToMessage(result.rows[0]);
3861
- if (msg.status !== "pending") return false;
3862
- if (!_wsClientSend) {
3863
- return false;
3864
- }
3865
- const payload = JSON.stringify({
3866
- id: msg.id,
3867
- fromAgent: msg.fromAgent,
3868
- targetAgent: msg.targetAgent,
3869
- targetProject: msg.targetProject,
3870
- content: msg.content,
3871
- priority: msg.priority,
3872
- createdAt: msg.createdAt
3902
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3903
+ WHERE status = 'needs_review' AND updated_at > ?`,
3904
+ args: [sinceIso]
3873
3905
  });
3874
- const sent = _wsClientSend(targetDevice, payload);
3875
- if (sent) {
3876
- await client.execute({
3877
- sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
3878
- args: [messageId]
3879
- });
3880
- return true;
3881
- }
3882
- return false;
3906
+ return Number(result.rows[0]?.cnt) || 0;
3883
3907
  }
3884
- async function deliverLocalMessage(messageId) {
3908
+ async function listPendingReviews(limit) {
3885
3909
  const client = getClient();
3886
3910
  const result = await client.execute({
3887
- sql: "SELECT * FROM messages WHERE id = ?",
3888
- 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]
3889
3915
  });
3890
- if (result.rows.length === 0) return false;
3891
- const msg = rowToMessage(result.rows[0]);
3892
- if (msg.status !== "pending") return false;
3893
- const targetAgent = msg.targetAgent;
3916
+ return result.rows;
3917
+ }
3918
+ async function cleanupOrphanedReviews() {
3919
+ const client = getClient();
3894
3920
  const now = (/* @__PURE__ */ new Date()).toISOString();
3895
- try {
3896
- const exeSession = resolveExeSession();
3897
- if (!exeSession) {
3898
- throw new Error("No exe session found");
3899
- }
3900
- const ensureResult = ensureEmployee(targetAgent, exeSession, process.cwd());
3901
- if (ensureResult.status === "failed") {
3902
- throw new Error(ensureResult.error ?? "ensureEmployee failed");
3903
- }
3904
- await client.execute({
3905
- sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
3906
- args: [now, messageId]
3907
- });
3908
- return true;
3909
- } catch {
3910
- const newRetryCount = msg.retryCount + 1;
3911
- if (newRetryCount >= MAX_RETRIES2) {
3912
- await markFailed(messageId, "session unavailable after 10 retries");
3913
- } else {
3914
- await client.execute({
3915
- sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
3916
- args: [newRetryCount, messageId]
3917
- });
3918
- }
3919
- return false;
3920
- }
3921
- }
3922
- async function getPendingMessages(targetAgent) {
3923
- const client = getClient();
3924
- const result = await client.execute({
3925
- sql: `SELECT * FROM messages
3926
- WHERE target_agent = ? AND status IN ('pending', 'delivered')
3927
- ORDER BY id`,
3928
- args: [targetAgent]
3929
- });
3930
- return result.rows.map((row) => rowToMessage(row));
3931
- }
3932
- async function markRead(messageId) {
3933
- const client = getClient();
3934
- await client.execute({
3935
- sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
3936
- args: [messageId]
3937
- });
3938
- }
3939
- async function markAcknowledged(messageId) {
3940
- const client = getClient();
3941
- await client.execute({
3942
- sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
3943
- args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
3944
- });
3945
- }
3946
- async function markProcessed(messageId) {
3947
- const client = getClient();
3948
- await client.execute({
3949
- sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
3950
- args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
3951
- });
3952
- }
3953
- async function getMessageStatus(messageId) {
3954
- const client = getClient();
3955
- const result = await client.execute({
3956
- sql: "SELECT status FROM messages WHERE id = ?",
3957
- args: [messageId]
3958
- });
3959
- return result.rows[0]?.status ?? null;
3960
- }
3961
- async function getUnacknowledgedMessages(targetAgent) {
3962
- const client = getClient();
3963
- const result = await client.execute({
3964
- sql: `SELECT * FROM messages
3965
- WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
3966
- ORDER BY id`,
3967
- args: [targetAgent]
3968
- });
3969
- return result.rows.map((row) => rowToMessage(row));
3970
- }
3971
- async function getReadMessages(targetAgent) {
3972
- const client = getClient();
3973
- const result = await client.execute({
3974
- sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
3975
- args: [targetAgent]
3976
- });
3977
- return result.rows.map((row) => rowToMessage(row));
3978
- }
3979
- async function markFailed(messageId, reason) {
3980
- const client = getClient();
3981
- await client.execute({
3982
- sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
3983
- args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
3984
- });
3985
- }
3986
- async function getFailedMessages() {
3987
- const client = getClient();
3988
- const result = await client.execute({
3989
- sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
3990
- 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]
3991
3928
  });
3992
- return result.rows.map((row) => rowToMessage(row));
3993
- }
3994
- async function retryPendingMessages() {
3995
- const client = getClient();
3996
- const result = await client.execute({
3997
- sql: `SELECT * FROM messages
3998
- WHERE status = 'pending' AND retry_count < ?
3999
- ORDER BY id`,
4000
- 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]
4001
3936
  });
4002
- let delivered = 0;
4003
- for (const row of result.rows) {
4004
- const msg = rowToMessage(row);
4005
- try {
4006
- const success = await deliverLocalMessage(msg.id);
4007
- if (success) delivered++;
4008
- } catch {
4009
- }
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
+ );
4010
3943
  }
4011
- return delivered;
3944
+ return total;
4012
3945
  }
4013
- var MAX_RETRIES2, _wsClientSend;
4014
- var init_messaging = __esm({
4015
- "src/lib/messaging.ts"() {
4016
- "use strict";
4017
- init_database();
4018
- init_tmux_routing();
4019
- MAX_RETRIES2 = 10;
4020
- _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
+ };
4021
3958
  }
4022
- });
4023
-
4024
- // src/lib/notifications.ts
4025
- import crypto4 from "crypto";
4026
- import path12 from "path";
4027
- import os7 from "os";
4028
- import {
4029
- readFileSync as readFileSync10,
4030
- readdirSync as readdirSync2,
4031
- unlinkSync as unlinkSync3,
4032
- existsSync as existsSync11,
4033
- rmdirSync
4034
- } from "fs";
4035
- 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;
4036
3996
  try {
4037
3997
  const client = getClient();
4038
- const id = crypto4.randomUUID();
4039
3998
  const now = (/* @__PURE__ */ new Date()).toISOString();
4040
- await client.execute({
4041
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
4042
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
4043
- args: [
4044
- id,
4045
- notification.agentId,
4046
- notification.agentRole,
4047
- notification.event,
4048
- notification.project,
4049
- notification.summary,
4050
- notification.taskFile ?? null,
4051
- now
4052
- ]
4053
- });
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
+ }
4054
4031
  } catch (err) {
4055
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
4056
- `);
4032
+ process.stderr.write(
4033
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
4034
+ `
4035
+ );
4057
4036
  }
4058
- }
4059
- async function markAsReadByTaskFile(taskFile) {
4060
4037
  try {
4061
- const client = getClient();
4062
- await client.execute({
4063
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
4064
- args: [taskFile]
4065
- });
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
+ }
4066
4046
  } catch {
4067
4047
  }
4068
4048
  }
4069
- var init_notifications = __esm({
4070
- "src/lib/notifications.ts"() {
4049
+ var init_tasks_review = __esm({
4050
+ "src/lib/tasks-review.ts"() {
4071
4051
  "use strict";
4072
4052
  init_database();
4073
- }
4074
- });
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();
4060
+ }
4061
+ });
4075
4062
 
4076
- // src/lib/tasks-crud.ts
4077
- import crypto5 from "crypto";
4078
- import path13 from "path";
4079
- import { execSync as execSync5 } from "child_process";
4080
- import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
4081
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
4082
- 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) {
4083
4067
  const client = getClient();
4084
- const row = await resolveTask(client, input.taskId);
4085
- const taskId = String(row.id);
4086
- const now = (/* @__PURE__ */ new Date()).toISOString();
4087
- const blockedByIds = [];
4088
- if (row.blocked_by) {
4089
- blockedByIds.push(String(row.blocked_by));
4090
- }
4091
- const checkpoint = {
4092
- step: input.step,
4093
- context_summary: input.contextSummary,
4094
- files_touched: input.filesTouched ?? [],
4095
- blocked_by_ids: blockedByIds,
4096
- last_checkpoint_at: now
4097
- };
4098
- const result = await client.execute({
4099
- sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
4100
- 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]
4101
4072
  });
4102
- if (result.rowsAffected === 0) {
4103
- 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
+ }
4104
4088
  }
4105
- const countResult = await client.execute({
4106
- sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
4107
- args: [taskId]
4108
- });
4109
- const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
4110
- return { checkpointCount };
4111
- }
4112
- function extractParentFromContext(contextBody) {
4113
- if (!contextBody) return null;
4114
- const match = contextBody.match(
4115
- /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
4116
- );
4117
- return match ? match[1].toLowerCase() : null;
4118
4089
  }
4119
- function slugify(title) {
4120
- return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4121
- }
4122
- async function resolveTask(client, identifier) {
4123
- let result = await client.execute({
4124
- sql: "SELECT * FROM tasks WHERE id = ?",
4125
- args: [identifier]
4126
- });
4127
- if (result.rows.length === 1) return result.rows[0];
4128
- result = await client.execute({
4129
- sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
4130
- args: [`%${identifier}%`]
4131
- });
4132
- if (result.rows.length === 1) return result.rows[0];
4133
- if (result.rows.length > 1) {
4134
- const exact = result.rows.filter(
4135
- (r) => String(r.task_file).endsWith(`/${identifier}.md`)
4136
- );
4137
- if (exact.length === 1) return exact[0];
4138
- const candidates = exact.length > 1 ? exact : result.rows;
4139
- const active = candidates.filter(
4140
- (r) => !["done", "cancelled"].includes(String(r.status))
4141
- );
4142
- if (active.length === 1) return active[0];
4143
- const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
4144
- throw new Error(
4145
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
4146
- );
4147
- }
4148
- result = await client.execute({
4149
- sql: "SELECT * FROM tasks WHERE title LIKE ?",
4150
- 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]
4151
4098
  });
4152
- if (result.rows.length === 1) return result.rows[0];
4153
- if (result.rows.length > 1) {
4154
- const active = result.rows.filter(
4155
- (r) => !["done", "cancelled"].includes(String(r.status))
4156
- );
4157
- if (active.length === 1) return active[0];
4158
- const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
4159
- throw new Error(
4160
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
4161
- );
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
+ };
4162
4106
  }
4163
- throw new Error(`Task not found: ${identifier}`);
4107
+ return void 0;
4164
4108
  }
4165
- async function createTaskCore(input) {
4109
+ async function checkSubtaskCompletion(parentTaskId, projectName) {
4166
4110
  const client = getClient();
4167
- const id = crypto5.randomUUID();
4168
- const now = (/* @__PURE__ */ new Date()).toISOString();
4169
- const slug = slugify(input.title);
4170
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
4171
- let blockedById = null;
4172
- const initialStatus = input.blockedBy ? "blocked" : "open";
4173
- if (input.blockedBy) {
4174
- const blocker = await resolveTask(client, input.blockedBy);
4175
- blockedById = String(blocker.id);
4176
- }
4177
- let parentTaskId = null;
4178
- let parentRef = input.parentTaskId;
4179
- if (!parentRef) {
4180
- const extracted = extractParentFromContext(input.context);
4181
- if (extracted) {
4182
- parentRef = extracted;
4183
- process.stderr.write(
4184
- "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
4185
- );
4186
- }
4187
- }
4188
- if (parentRef) {
4189
- try {
4190
- const parent = await resolveTask(client, parentRef);
4191
- parentTaskId = String(parent.id);
4192
- } catch (err) {
4193
- if (!input.parentTaskId) {
4194
- throw new Error(
4195
- `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
4196
- );
4197
- }
4198
- 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
+ });
4199
4133
  }
4200
4134
  }
4201
- let warning;
4202
- const dupCheck = await client.execute({
4203
- sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
4204
- args: [input.title, input.assignedTo]
4205
- });
4206
- if (dupCheck.rows.length > 0) {
4207
- 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();
4208
4141
  }
4209
- 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;
4210
4152
  try {
4211
- await mkdir4(path13.join(input.baseDir, "exe", "output"), { recursive: true });
4212
- await mkdir4(path13.join(input.baseDir, "exe", "research"), { recursive: true });
4213
- await ensureArchitectureDoc(input.baseDir, input.projectName);
4214
- 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);
4215
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();
4216
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;
4217
4175
  }
4218
- const complexity = input.complexity ?? "standard";
4219
- await client.execute({
4220
- 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)
4221
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4222
- args: [
4223
- id,
4224
- input.title,
4225
- input.assignedTo,
4226
- input.assignedBy,
4227
- input.projectName,
4228
- input.priority,
4229
- initialStatus,
4230
- taskFile,
4231
- blockedById,
4232
- parentTaskId,
4233
- input.reviewer ?? null,
4234
- input.context,
4235
- complexity,
4236
- input.budgetTokens ?? null,
4237
- input.budgetFallbackModel ?? null,
4238
- 0,
4239
- null,
4240
- now,
4241
- now
4242
- ]
4243
- });
4244
- return {
4245
- id,
4246
- title: input.title,
4247
- assignedTo: input.assignedTo,
4248
- assignedBy: input.assignedBy,
4249
- projectName: input.projectName,
4250
- priority: input.priority,
4251
- status: initialStatus,
4252
- taskFile,
4253
- createdAt: now,
4254
- updatedAt: now,
4255
- warning,
4256
- budgetTokens: input.budgetTokens ?? null,
4257
- budgetFallbackModel: input.budgetFallbackModel ?? null,
4258
- tokensUsed: 0,
4259
- tokensWarnedAt: null
4260
- };
4261
4176
  }
4262
- async function listTasks(input) {
4263
- const client = getClient();
4264
- const conditions = [];
4265
- const args = [];
4266
- if (input.assignedTo) {
4267
- conditions.push("assigned_to = ?");
4268
- args.push(input.assignedTo);
4269
- }
4270
- if (input.status) {
4271
- conditions.push("status = ?");
4272
- args.push(input.status);
4273
- } else {
4274
- conditions.push("status IN ('open', 'in_progress', 'blocked')");
4275
- }
4276
- if (input.projectName) {
4277
- conditions.push("project_name = ?");
4278
- args.push(input.projectName);
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;
4279
4183
  }
4280
- if (input.priority) {
4281
- conditions.push("priority = ?");
4282
- 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;
4283
4205
  }
4284
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4285
- const result = await client.execute({
4286
- 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`,
4287
- args
4288
- });
4289
- return result.rows.map((r) => ({
4290
- id: String(r.id),
4291
- title: String(r.title),
4292
- assignedTo: String(r.assigned_to),
4293
- assignedBy: String(r.assigned_by),
4294
- projectName: String(r.project_name),
4295
- priority: String(r.priority),
4296
- status: String(r.status),
4297
- taskFile: String(r.task_file),
4298
- createdAt: String(r.created_at),
4299
- updatedAt: String(r.updated_at),
4300
- checkpointCount: Number(r.checkpoint_count ?? 0),
4301
- budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
4302
- budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
4303
- tokensUsed: Number(r.tokens_used ?? 0),
4304
- tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
4305
- }));
4206
+ return null;
4306
4207
  }
4307
- function checkStaleCompletion(taskContext, taskCreatedAt) {
4308
- if (!taskContext) return null;
4309
- if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
4208
+ function assertSessionScope(actionType, targetProject) {
4310
4209
  try {
4311
- const since = new Date(taskCreatedAt).toISOString();
4312
- const branch = execSync5(
4313
- "git rev-parse --abbrev-ref HEAD 2>/dev/null",
4314
- { encoding: "utf8", timeout: 3e3 }
4315
- ).trim();
4316
- const branchArg = branch && branch !== "HEAD" ? branch : "";
4317
- const commitCount = execSync5(
4318
- `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
4319
- { encoding: "utf8", timeout: 5e3 }
4320
- ).trim();
4321
- const count = parseInt(commitCount, 10);
4322
- if (count === 0) {
4323
- 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
+ };
4324
4222
  }
4325
- return null;
4326
- } catch {
4327
- return null;
4328
- }
4329
- }
4330
- async function updateTaskStatus(input) {
4331
- const client = getClient();
4332
- const now = (/* @__PURE__ */ new Date()).toISOString();
4333
- const row = await resolveTask(client, input.taskId);
4334
- const taskId = String(row.id);
4335
- const taskFile = String(row.task_file);
4336
- if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
4337
4223
  process.stderr.write(
4338
- `[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}"
4339
4225
  `
4340
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" };
4341
4236
  }
4342
- if (input.status === "done") {
4343
- const existingRow = await client.execute({
4344
- sql: "SELECT context, created_at FROM tasks WHERE id = ?",
4345
- args: [taskId]
4346
- });
4347
- if (existingRow.rows.length > 0) {
4348
- const ctx = existingRow.rows[0];
4349
- const warning = checkStaleCompletion(ctx.context, ctx.created_at);
4350
- if (warning) {
4351
- input.result = input.result ? `\u26A0\uFE0F ${warning}
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();
4244
+ }
4245
+ });
4352
4246
 
4353
- ${input.result}` : `\u26A0\uFE0F ${warning}`;
4354
- process.stderr.write(`[tasks] ${warning} (task: ${taskId})
4355
- `);
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) {
4252
+ try {
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 };
4356
4258
  }
4259
+ } catch {
4357
4260
  }
4358
4261
  }
4359
- if (input.status === "in_progress") {
4360
- const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
4361
- const claim = await client.execute({
4362
- sql: `UPDATE tasks
4363
- SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
4364
- WHERE id = ? AND status = 'open'`,
4365
- args: [tmuxSession, now, taskId]
4366
- });
4367
- if (claim.rowsAffected === 0) {
4368
- const current = await client.execute({
4369
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
4370
- args: [taskId]
4371
- });
4372
- const cur = current.rows[0];
4373
- const status = cur?.status ?? "unknown";
4374
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
4375
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
4376
- }
4377
- try {
4378
- await writeCheckpoint({
4379
- taskId,
4380
- step: "claimed",
4381
- contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
4262
+ try {
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)
4382
4275
  });
4383
- } catch {
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 };
4384
4284
  }
4385
- return { row, taskFile, now, taskId };
4285
+ } catch {
4286
+ return { dispatched: "session_missing" };
4386
4287
  }
4387
- if (input.result) {
4388
- await client.execute({
4389
- sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
4390
- args: [input.status, input.result, now, taskId]
4391
- });
4392
- } else {
4393
- await client.execute({
4394
- sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
4395
- args: [input.status, now, taskId]
4396
- });
4288
+ }
4289
+ function notifyTaskDone() {
4290
+ try {
4291
+ const key = getSessionKey();
4292
+ if (key && !process.env.VITEST) notifyParentExe(key);
4293
+ } catch {
4397
4294
  }
4295
+ }
4296
+ async function markTaskNotificationsRead(taskFile) {
4398
4297
  try {
4399
- await writeCheckpoint({
4400
- taskId,
4401
- step: `status_transition:${input.status}`,
4402
- contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
4403
- });
4298
+ await markAsReadByTaskFile(taskFile);
4404
4299
  } catch {
4405
4300
  }
4406
- return { row, taskFile, now, taskId };
4407
4301
  }
4408
- async function deleteTaskCore(taskId, _baseDir) {
4302
+ var init_tasks_notify = __esm({
4303
+ "src/lib/tasks-notify.ts"() {
4304
+ "use strict";
4305
+ init_tmux_routing();
4306
+ init_session_key();
4307
+ init_notifications();
4308
+ init_transport();
4309
+ init_employees();
4310
+ }
4311
+ });
4312
+
4313
+ // src/lib/behaviors.ts
4314
+ import crypto6 from "crypto";
4315
+ async function storeBehavior(opts) {
4409
4316
  const client = getClient();
4410
- const row = await resolveTask(client, taskId);
4411
- const id = String(row.id);
4412
- const taskFile = String(row.task_file);
4413
- const assignedTo = String(row.assigned_to);
4414
- const assignedBy = String(row.assigned_by);
4415
- await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
4416
- const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
4417
- return { taskFile, assignedTo, assignedBy, taskSlug };
4418
- }
4419
- async function ensureArchitectureDoc(baseDir, projectName) {
4420
- const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
4421
- try {
4422
- if (existsSync12(archPath)) return;
4423
- const template = [
4424
- `# ${projectName} \u2014 System Architecture`,
4425
- "",
4426
- "> Employees: read this before every task. Update it when you change system structure.",
4427
- `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
4428
- "",
4429
- "## Overview",
4430
- "",
4431
- "<!-- Describe what this system does, its main components, and how they connect. -->",
4432
- "",
4433
- "## Key Components",
4434
- "",
4435
- "<!-- List the major modules, services, or subsystems. -->",
4436
- "",
4437
- "## Data Flow",
4438
- "",
4439
- "<!-- How does data move through the system? What writes where? -->",
4440
- "",
4441
- "## Invariants",
4442
- "",
4443
- "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
4444
- "",
4445
- "## Dependencies",
4446
- "",
4447
- "<!-- What depends on what? If I change X, what else is affected? -->",
4448
- ""
4449
- ].join("\n");
4450
- await writeFile4(archPath, template, "utf-8");
4451
- } catch {
4452
- }
4453
- }
4454
- async function ensureGitignoreExe(baseDir) {
4455
- const gitignorePath = path13.join(baseDir, ".gitignore");
4456
- try {
4457
- if (existsSync12(gitignorePath)) {
4458
- const content = readFileSync11(gitignorePath, "utf-8");
4459
- if (/^\/?exe\/?$/m.test(content)) return;
4460
- await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
4461
- } else {
4462
- await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
4463
- }
4464
- } catch {
4465
- }
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]
4323
+ });
4324
+ return id;
4466
4325
  }
4467
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4468
- var init_tasks_crud = __esm({
4469
- "src/lib/tasks-crud.ts"() {
4326
+ var init_behaviors = __esm({
4327
+ "src/lib/behaviors.ts"() {
4470
4328
  "use strict";
4471
4329
  init_database();
4472
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
4473
- TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4474
4330
  }
4475
4331
  });
4476
4332
 
4477
- // src/lib/tasks-review.ts
4478
- import path14 from "path";
4479
- import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
4480
- async function countPendingReviews() {
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) {
4481
4348
  const client = getClient();
4482
4349
  const result = await client.execute({
4483
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
4484
- args: []
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]
4485
4355
  });
4486
- return Number(result.rows[0]?.cnt) || 0;
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);
4370
+ }
4371
+ }
4372
+ return signature;
4487
4373
  }
4488
- async function countNewPendingReviewsSince(sinceIso) {
4374
+ function hashSignature(signature) {
4375
+ return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4376
+ }
4377
+ async function storeTrajectory(opts) {
4489
4378
  const client = getClient();
4490
- const result = await client.execute({
4491
- sql: `SELECT COUNT(*) as cnt FROM tasks
4492
- WHERE status = 'needs_review' AND updated_at > ?`,
4493
- args: [sinceIso]
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
+ ]
4494
4396
  });
4495
- return Number(result.rows[0]?.cnt) || 0;
4397
+ return id;
4496
4398
  }
4497
- async function listPendingReviews(limit) {
4399
+ async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
4498
4400
  const client = getClient();
4401
+ const hash = hashSignature(signature);
4499
4402
  const result = await client.execute({
4500
- sql: `SELECT title, assigned_to, project_name FROM tasks
4501
- WHERE status = 'needs_review'
4502
- ORDER BY priority ASC, created_at DESC LIMIT ?`,
4503
- args: [limit]
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]
4504
4409
  });
4505
- return result.rows;
4506
- }
4507
- async function cleanupOrphanedReviews() {
4508
- const client = getClient();
4509
- const now = (/* @__PURE__ */ new Date()).toISOString();
4510
- const r1 = await client.execute({
4511
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4512
- WHERE status = 'needs_review'
4513
- AND assigned_by = 'system'
4514
- AND title LIKE 'Review:%'
4515
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
4516
- args: [now]
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)
4517
4421
  });
4518
- const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
4519
- const r2 = await client.execute({
4520
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4521
- WHERE status = 'needs_review'
4522
- AND result IS NOT NULL
4523
- AND updated_at < ?`,
4524
- args: [now, staleThreshold]
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
+ ]
4525
4436
  });
4526
- const total = r1.rowsAffected + r2.rowsAffected;
4527
- if (total > 0) {
4528
- process.stderr.write(
4529
- `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
4530
- `
4531
- );
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));
4441
+ }
4532
4442
  }
4533
- return total;
4443
+ return matches;
4534
4444
  }
4535
- function getReviewChecklist(role, agent, taskSlug) {
4536
- const roleLower = role.toLowerCase();
4537
- if (roleLower.includes("engineer") || roleLower === "principal engineer") {
4538
- return {
4539
- lens: "Code Quality (Engineer)",
4540
- checklist: [
4541
- "1. Do all tests pass? Any new tests needed?",
4542
- "2. Is the code clean \u2014 no dead code, no TODOs left?",
4543
- "3. Does it follow existing patterns and conventions in the codebase?",
4544
- "4. Any regressions in the test suite?"
4545
- ]
4546
- };
4547
- }
4548
- if (roleLower === "cto" || roleLower.includes("architect")) {
4549
- return {
4550
- lens: "Architecture (CTO)",
4551
- checklist: [
4552
- "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
4553
- "2. Is it backward compatible? Any breaking changes?",
4554
- "3. Does it introduce technical debt? Is that debt justified?",
4555
- "4. Security implications? Any new attack surface?",
4556
- "5. Does it scale? Performance considerations?",
4557
- "6. Coordination: does this affect other employees' work or other projects?"
4558
- ]
4559
- };
4560
- }
4561
- if (roleLower === "coo" || roleLower.includes("operations")) {
4562
- return {
4563
- lens: "Strategic (COO)",
4564
- checklist: [
4565
- "1. Does this serve the project mission?",
4566
- "2. Is this the right work at the right time?",
4567
- "3. Does the architectural assessment make sense for the business?",
4568
- "4. Any cross-project implications?"
4569
- ]
4570
- };
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: [] };
4571
4449
  }
4572
- return {
4573
- lens: "General",
4574
- checklist: [
4575
- "1. Read the original task's acceptance criteria",
4576
- `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
4577
- "3. Verify code changes match requirements",
4578
- "4. Check if tests were added/updated",
4579
- `5. Look for output files in exe/output/${agent}-${taskSlug}*`
4580
- ]
4581
- };
4582
- }
4583
- async function cleanupReviewFile(row, taskFile, _baseDir) {
4584
- if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4585
- try {
4586
- const client = getClient();
4587
- const now = (/* @__PURE__ */ new Date()).toISOString();
4588
- const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
4589
- if (parentId) {
4590
- const result = await client.execute({
4591
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
4592
- args: [now, parentId]
4593
- });
4594
- if (result.rowsAffected > 0) {
4595
- process.stderr.write(
4596
- `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
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 };
4462
+ }
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:
4470
+
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.`;
4483
+ }
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
+ });
4513
+ }
4514
+ process.stderr.write(
4515
+ `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
4597
4516
  `
4598
- );
4599
- }
4600
- } else {
4601
- const fileName = taskFile.split("/").pop() ?? "";
4602
- const reviewPrefix = fileName.replace(".md", "");
4603
- const parts = reviewPrefix.split("-");
4604
- if (parts.length >= 3 && parts[0] === "review") {
4605
- const agent = parts[1];
4606
- const slug = parts.slice(2).join("-");
4607
- const originalTaskFile = `exe/${agent}/${slug}.md`;
4608
- const result = await client.execute({
4609
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
4610
- args: [now, originalTaskFile]
4611
- });
4612
- if (result.rowsAffected > 0) {
4517
+ );
4518
+ return skillId;
4519
+ }
4520
+ async function captureAndLearn(opts) {
4521
+ try {
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) => {
4613
4533
  process.stderr.write(
4614
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
4534
+ `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
4615
4535
  `
4616
4536
  );
4617
- }
4537
+ });
4618
4538
  }
4619
4539
  }
4620
4540
  } catch (err) {
4621
4541
  process.stderr.write(
4622
- `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
4542
+ `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
4623
4543
  `
4624
4544
  );
4625
4545
  }
4626
- try {
4627
- const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
4628
- if (existsSync13(cacheDir)) {
4629
- for (const f of readdirSync3(cacheDir)) {
4630
- if (f.startsWith("review-notified-")) {
4631
- unlinkSync4(path14.join(cacheDir, f));
4632
- }
4633
- }
4546
+ }
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++;
4634
4590
  }
4635
- } catch {
4636
4591
  }
4592
+ return { clustersProcessed, skillsExtracted };
4637
4593
  }
4638
- var init_tasks_review = __esm({
4639
- "src/lib/tasks-review.ts"() {
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"() {
4640
4615
  "use strict";
4641
4616
  init_database();
4617
+ init_behaviors();
4642
4618
  init_config();
4643
- init_employees();
4644
- init_notifications();
4645
- init_tasks_crud();
4646
- init_tmux_routing();
4647
- init_session_key();
4619
+ DEFAULT_SKILL_THRESHOLD = 3;
4648
4620
  }
4649
4621
  });
4650
4622
 
4651
- // src/lib/tasks-chain.ts
4652
- import path15 from "path";
4653
- import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
4654
- async function cascadeUnblock(taskId, baseDir, now) {
4655
- const client = getClient();
4656
- const unblocked = await client.execute({
4657
- sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
4658
- WHERE blocked_by = ? AND status = 'blocked'`,
4659
- args: [now, taskId]
4660
- });
4661
- if (baseDir && unblocked.rowsAffected > 0) {
4662
- const unblockedRows = await client.execute({
4663
- sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
4664
- args: [now]
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
4665
4656
  });
4666
- for (const ur of unblockedRows.rows) {
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") {
4667
4670
  try {
4668
- const ubFile = path15.join(baseDir, String(ur.task_file));
4669
- let ubContent = await readFile4(ubFile, "utf-8");
4670
- ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
4671
- ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
4672
- await writeFile5(ubFile, ubContent, "utf-8");
4671
+ unlinkSync4(cachePath);
4673
4672
  } catch {
4674
4673
  }
4675
4674
  }
4675
+ } catch {
4676
4676
  }
4677
- }
4678
- async function findNextTask(assignedTo) {
4679
- const client = getClient();
4680
- const nextResult = await client.execute({
4681
- sql: `SELECT title, task_file, priority FROM tasks
4682
- WHERE assigned_to = ? AND status = 'open'
4683
- ORDER BY priority ASC, created_at ASC
4684
- LIMIT 1`,
4685
- args: [assignedTo]
4686
- });
4687
- if (nextResult.rows.length === 1) {
4688
- const nr = nextResult.rows[0];
4689
- return {
4690
- title: String(nr.title),
4691
- priority: String(nr.priority),
4692
- taskFile: String(nr.task_file)
4693
- };
4677
+ if (input.status === "done") {
4678
+ await cleanupReviewFile(row, taskFile, input.baseDir);
4694
4679
  }
4695
- return void 0;
4696
- }
4697
- async function checkSubtaskCompletion(parentTaskId, projectName) {
4698
- const client = getClient();
4699
- const remaining = await client.execute({
4700
- sql: `SELECT COUNT(*) as cnt FROM tasks
4701
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
4702
- args: [parentTaskId]
4703
- });
4704
- const cnt = Number(remaining.rows[0]?.cnt ?? 1);
4705
- if (cnt === 0) {
4706
- const parentRow = await client.execute({
4707
- sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
4708
- args: [parentTaskId]
4709
- });
4710
- if (parentRow.rows.length === 1) {
4711
- const pr = parentRow.rows[0];
4712
- const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
4713
- await writeNotification({
4714
- agentId: String(pr.assigned_to),
4715
- agentRole: "system",
4716
- event: "subtasks_complete",
4717
- project: parentProject,
4718
- summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
4719
- taskFile: String(pr.task_file)
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%`]
4689
+ });
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) {
4700
+ process.stderr.write(
4701
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
4702
+ `
4703
+ );
4704
+ }
4705
+ } catch {
4706
+ }
4707
+ }
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
4720
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
+ }
4721
4733
  }
4722
4734
  }
4723
- }
4724
- var init_tasks_chain = __esm({
4725
- "src/lib/tasks-chain.ts"() {
4726
- "use strict";
4727
- init_database();
4728
- init_notifications();
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
+ });
4729
4749
  }
4730
- });
4731
-
4732
- // src/lib/project-name.ts
4733
- import { execSync as execSync6 } from "child_process";
4734
- import path16 from "path";
4735
- function getProjectName(cwd) {
4736
- const dir = cwd ?? process.cwd();
4737
- if (_cached2 && _cachedCwd === dir) return _cached2;
4738
- try {
4739
- let repoRoot;
4750
+ let nextTask;
4751
+ if (isTerminal && String(row.assigned_to) !== "exe") {
4740
4752
  try {
4741
- const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
4742
- cwd: dir,
4743
- encoding: "utf8",
4744
- timeout: 2e3,
4745
- stdio: ["pipe", "pipe", "pipe"]
4746
- }).trim();
4747
- repoRoot = path16.dirname(gitCommonDir);
4753
+ nextTask = await findNextTask(String(row.assigned_to));
4748
4754
  } catch {
4749
- repoRoot = execSync6("git rev-parse --show-toplevel", {
4750
- cwd: dir,
4751
- encoding: "utf8",
4752
- timeout: 2e3,
4753
- stdio: ["pipe", "pipe", "pipe"]
4754
- }).trim();
4755
4755
  }
4756
- _cached2 = path16.basename(repoRoot);
4757
- _cachedCwd = dir;
4758
- return _cached2;
4759
- } catch {
4760
- _cached2 = path16.basename(dir);
4761
- _cachedCwd = dir;
4762
- return _cached2;
4763
4756
  }
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
+ };
4764
4774
  }
4765
- var _cached2, _cachedCwd;
4766
- var init_project_name = __esm({
4767
- "src/lib/project-name.ts"() {
4775
+ async function deleteTask(taskId, baseDir) {
4776
+ const client = getClient();
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`;
4781
+ await client.execute({
4782
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
4783
+ args: [reviewFile, `exe/exe/${reviewSlug}.md`]
4784
+ });
4785
+ await markAsReadByTaskFile(taskFile);
4786
+ await markAsReadByTaskFile(reviewFile);
4787
+ }
4788
+ var init_tasks = __esm({
4789
+ "src/lib/tasks.ts"() {
4768
4790
  "use strict";
4769
- _cached2 = null;
4770
- _cachedCwd = null;
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();
4771
4801
  }
4772
4802
  });
4773
4803
 
4774
- // src/lib/session-scope.ts
4775
- var session_scope_exports = {};
4776
- __export(session_scope_exports, {
4777
- assertSessionScope: () => assertSessionScope,
4778
- findSessionForProject: () => findSessionForProject,
4779
- getSessionProject: () => getSessionProject
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
4780
4816
  });
4781
- function getSessionProject(sessionName) {
4782
- const sessions = listSessions();
4783
- const entry = sessions.find((s) => s.windowName === sessionName);
4784
- if (!entry) return null;
4785
- const parts = entry.projectDir.split("/").filter(Boolean);
4786
- return parts[parts.length - 1] ?? null;
4817
+ function resumeTaskTitle(agentId) {
4818
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
4787
4819
  }
4788
- function findSessionForProject(projectName) {
4789
- const sessions = listSessions();
4790
- for (const s of sessions) {
4791
- const proj = s.projectDir.split("/").filter(Boolean).pop();
4792
- if (proj === projectName && s.agentId === "exe") return s;
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;
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;
4793
4864
  }
4794
- return null;
4865
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
4866
+ _pendingCapacityKill.set(agentId, now);
4867
+ return false;
4868
+ }
4869
+ _pendingCapacityKill.delete(agentId);
4870
+ return true;
4795
4871
  }
4796
- function assertSessionScope(actionType, targetProject) {
4872
+ function _resetPendingCapacityKills() {
4873
+ _pendingCapacityKill.clear();
4874
+ }
4875
+ function _resetLastRelaunchCache() {
4876
+ _lastRelaunch.clear();
4877
+ }
4878
+ async function lastResumeCreatedAtMs(agentId) {
4879
+ const client = getClient();
4880
+ const result = await client.execute({
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} %`]
4885
+ });
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(", ")})
4909
+ ORDER BY created_at DESC
4910
+ LIMIT 1`,
4911
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
4912
+ });
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 };
4920
+ }
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
4930
+ });
4931
+ return { created: true, taskId: task.id };
4932
+ }
4933
+ async function pollCapacityDead() {
4934
+ const transport = getTransport();
4935
+ const relaunched = [];
4936
+ const registered = listSessions().filter(
4937
+ (s) => s.agentId !== "exe"
4938
+ );
4939
+ if (registered.length === 0) return [];
4940
+ let liveSessions;
4797
4941
  try {
4798
- const currentProject = getProjectName();
4799
- const exeSession = resolveExeSession();
4800
- if (!exeSession) {
4801
- return { allowed: true, reason: "no_session" };
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;
4802
4955
  }
4803
- if (currentProject === targetProject) {
4804
- return {
4805
- allowed: true,
4806
- reason: "same_session",
4807
- currentProject,
4808
- targetProject
4809
- };
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).
4961
+ `
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;
4810
4984
  }
4811
4985
  process.stderr.write(
4812
- `[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
4986
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
4813
4987
  `
4814
4988
  );
4815
- return {
4816
- allowed: true,
4817
- // v1: warn-only, don't block
4818
- reason: "cross_session_granted",
4819
- currentProject,
4820
- targetProject,
4821
- targetSession: findSessionForProject(targetProject)?.windowName
4822
- };
4823
- } catch {
4824
- return { allowed: true, reason: "no_session" };
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
+ }
4825
5036
  }
5037
+ return relaunched;
4826
5038
  }
4827
- var init_session_scope = __esm({
4828
- "src/lib/session-scope.ts"() {
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"() {
4829
5042
  "use strict";
4830
5043
  init_session_registry();
4831
- init_project_name();
5044
+ init_transport();
5045
+ init_notifications();
5046
+ init_database();
5047
+ init_session_kill_telemetry();
4832
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;
4833
5081
  }
4834
5082
  });
4835
5083
 
4836
- // src/lib/tasks-notify.ts
4837
- async function dispatchTaskToEmployee(input) {
4838
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
4839
- let crossProject = false;
4840
- if (input.projectName) {
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)) {
4841
5131
  try {
4842
- const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
4843
- const check = assertSessionScope2("dispatch_task", input.projectName);
4844
- if (check.reason === "cross_session_granted") {
4845
- crossProject = true;
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;
4846
5136
  }
4847
5137
  } catch {
4848
5138
  }
4849
5139
  }
5140
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5141
+ return true;
5142
+ }
5143
+ function releaseSpawnLock2(sessionName) {
4850
5144
  try {
4851
- const transport = getTransport();
4852
- const exeSession = resolveExeSession();
4853
- if (!exeSession) return { dispatched: "session_missing" };
4854
- const sessionName = employeeSessionName(input.assignedTo, exeSession);
4855
- if (transport.isAlive(sessionName)) {
4856
- const result = sendIntercom(sessionName);
4857
- const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
4858
- return { dispatched, session: sessionName, crossProject };
4859
- } else {
4860
- const projectDir = input.projectDir ?? process.cwd();
4861
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
4862
- autoInstance: isMultiInstance(input.assignedTo)
4863
- });
4864
- if (result.status === "failed") {
4865
- process.stderr.write(
4866
- `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
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)}
4867
5176
  `
4868
- );
4869
- return { dispatched: "session_missing" };
4870
- }
4871
- return { dispatched: "spawned", session: result.sessionName, crossProject };
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
+ );
4872
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;
4873
5234
  } catch {
4874
- return { dispatched: "session_missing" };
5235
+ return null;
4875
5236
  }
4876
5237
  }
4877
- function notifyTaskDone() {
5238
+ function getDispatchedBy(sessionKey) {
4878
5239
  try {
4879
- const key = getSessionKey();
4880
- if (key && !process.env.VITEST) notifyParentExe(key);
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;
4881
5245
  } catch {
5246
+ return null;
4882
5247
  }
4883
5248
  }
4884
- async function markTaskNotificationsRead(taskFile) {
5249
+ function resolveExeSession() {
5250
+ const mySession = getMySession();
5251
+ if (!mySession) return null;
4885
5252
  try {
4886
- await markAsReadByTaskFile(taskFile);
5253
+ const key = getSessionKey();
5254
+ const parentExe = getParentExe(key);
5255
+ if (parentExe) {
5256
+ return extractRootExe(parentExe) ?? parentExe;
5257
+ }
4887
5258
  } catch {
4888
5259
  }
5260
+ return extractRootExe(mySession) ?? mySession;
4889
5261
  }
4890
- var init_tasks_notify = __esm({
4891
- "src/lib/tasks-notify.ts"() {
4892
- "use strict";
4893
- init_tmux_routing();
4894
- init_session_key();
4895
- init_notifications();
4896
- init_transport();
4897
- init_employees();
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;
4898
5271
  }
4899
- });
4900
-
4901
- // src/lib/behaviors.ts
4902
- import crypto6 from "crypto";
4903
- async function storeBehavior(opts) {
4904
- const client = getClient();
4905
- const id = crypto6.randomUUID();
4906
- const now = (/* @__PURE__ */ new Date()).toISOString();
4907
- await client.execute({
4908
- sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
4909
- VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
4910
- args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
4911
- });
4912
- return id;
5272
+ return null;
4913
5273
  }
4914
- var init_behaviors = __esm({
4915
- "src/lib/behaviors.ts"() {
4916
- "use strict";
4917
- init_database();
5274
+ async function verifyPaneAtCapacity(sessionName) {
5275
+ const transport = getTransport();
5276
+ if (!transport.isAlive(sessionName)) {
5277
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
4918
5278
  }
4919
- });
4920
-
4921
- // src/lib/skill-learning.ts
4922
- var skill_learning_exports = {};
4923
- __export(skill_learning_exports, {
4924
- captureAndLearn: () => captureAndLearn,
4925
- captureTrajectory: () => captureTrajectory,
4926
- editDistance: () => editDistance,
4927
- extractSkill: () => extractSkill,
4928
- extractTrajectory: () => extractTrajectory,
4929
- findSimilarTrajectories: () => findSimilarTrajectories,
4930
- hashSignature: () => hashSignature,
4931
- storeTrajectory: () => storeTrajectory,
4932
- sweepTrajectories: () => sweepTrajectories
4933
- });
4934
- import crypto7 from "crypto";
4935
- async function extractTrajectory(taskId, agentId) {
4936
- const client = getClient();
4937
- const result = await client.execute({
4938
- sql: `SELECT tool_name, raw_text
4939
- FROM memories
4940
- WHERE task_id = ? AND agent_id = ?
4941
- ORDER BY timestamp ASC`,
4942
- args: [taskId, agentId]
4943
- });
4944
- if (result.rows.length === 0) return [];
4945
- const rawTools = result.rows.map((r) => {
4946
- const toolName = String(r.tool_name);
4947
- if (toolName === "Bash") {
4948
- const text = String(r.raw_text);
4949
- const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
4950
- return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
4951
- }
4952
- return toolName;
4953
- });
4954
- const signature = [];
4955
- for (const tool of rawTools) {
4956
- if (signature.length === 0 || signature[signature.length - 1] !== tool) {
4957
- signature.push(tool);
4958
- }
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
+ };
4959
5287
  }
4960
- return signature;
4961
- }
4962
- function hashSignature(signature) {
4963
- return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
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
+ };
4964
5299
  }
4965
- async function storeTrajectory(opts) {
4966
- const client = getClient();
4967
- const id = crypto7.randomUUID();
4968
- const now = (/* @__PURE__ */ new Date()).toISOString();
4969
- const signatureHash = hashSignature(opts.signature);
4970
- await client.execute({
4971
- sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
4972
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4973
- args: [
4974
- id,
4975
- opts.taskId,
4976
- opts.agentId,
4977
- opts.projectName,
4978
- opts.taskTitle,
4979
- JSON.stringify(opts.signature),
4980
- signatureHash,
4981
- opts.signature.length,
4982
- now
4983
- ]
4984
- });
4985
- return id;
5300
+ function readDebounceState() {
5301
+ try {
5302
+ if (!existsSync13(DEBOUNCE_FILE)) return {};
5303
+ return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
5304
+ } catch {
5305
+ return {};
5306
+ }
4986
5307
  }
4987
- async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
4988
- const client = getClient();
4989
- const hash = hashSignature(signature);
4990
- const result = await client.execute({
4991
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
4992
- FROM trajectories
4993
- WHERE signature_hash = ?
4994
- ORDER BY created_at DESC
4995
- LIMIT 20`,
4996
- args: [hash]
4997
- });
4998
- const mapRow = (r) => ({
4999
- id: String(r.id),
5000
- taskId: String(r.task_id),
5001
- agentId: String(r.agent_id),
5002
- projectName: String(r.project_name),
5003
- taskTitle: String(r.task_title),
5004
- signature: JSON.parse(String(r.signature)),
5005
- signatureHash: String(r.signature_hash),
5006
- toolCount: Number(r.tool_count),
5007
- skillId: r.skill_id ? String(r.skill_id) : null,
5008
- createdAt: String(r.created_at)
5009
- });
5010
- const matches = result.rows.map(mapRow);
5011
- if (matches.length >= threshold) return matches;
5012
- const nearResult = await client.execute({
5013
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
5014
- FROM trajectories
5015
- WHERE tool_count BETWEEN ? AND ?
5016
- AND signature_hash != ?
5017
- ORDER BY created_at DESC
5018
- LIMIT 50`,
5019
- args: [
5020
- Math.max(1, signature.length - 3),
5021
- signature.length + 3,
5022
- hash
5023
- ]
5024
- });
5025
- for (const r of nearResult.rows) {
5026
- const candidateSig = JSON.parse(String(r.signature));
5027
- if (editDistance(signature, candidateSig) <= 2) {
5028
- matches.push(mapRow(r));
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
+ }
5029
5348
  }
5349
+ if (/Running…/.test(pane)) return "tool";
5350
+ if (BUSY_PATTERN.test(pane)) return "thinking";
5351
+ return "idle";
5352
+ } catch {
5353
+ return "offline";
5030
5354
  }
5031
- return matches;
5032
5355
  }
5033
- async function captureTrajectory(opts) {
5034
- const signature = await extractTrajectory(opts.taskId, opts.agentId);
5035
- if (signature.length < 3) {
5036
- return { trajectoryId: "", similarCount: 0, similar: [] };
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";
5037
5403
  }
5038
- const trajectoryId = await storeTrajectory({
5039
- taskId: opts.taskId,
5040
- agentId: opts.agentId,
5041
- projectName: opts.projectName,
5042
- taskTitle: opts.taskTitle,
5043
- signature
5044
- });
5045
- const similar = await findSimilarTrajectories(
5046
- signature,
5047
- opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
5048
- );
5049
- return { trajectoryId, similarCount: similar.length, similar };
5050
5404
  }
5051
- function buildExtractionPrompt(trajectories) {
5052
- const items = trajectories.map((t, i) => {
5053
- const sig = t.signature.join(" \u2192 ");
5054
- return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
5055
- Signature: ${sig}`;
5056
- }).join("\n\n");
5057
- return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
5058
-
5059
- ${items}
5060
-
5061
- Extract the reusable procedure. Format your response EXACTLY like this:
5062
-
5063
- SKILL: {name \u2014 short, descriptive}
5064
- TRIGGER: {when to use this \u2014 one sentence}
5065
- STEPS:
5066
- 1. ...
5067
- 2. ...
5068
- PITFALLS: {common mistakes to avoid}
5069
-
5070
- Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
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;
5071
5426
  }
5072
- async function extractSkill(trajectories, model) {
5073
- if (trajectories.length === 0) return null;
5074
- const config2 = await loadConfig();
5075
- const skillModel = model ?? config2.skillModel;
5076
- const Anthropic4 = (await import("@anthropic-ai/sdk")).default;
5077
- const client = new Anthropic4();
5078
- const prompt = buildExtractionPrompt(trajectories);
5079
- const response = await client.messages.create({
5080
- model: skillModel,
5081
- max_tokens: 500,
5082
- messages: [{ role: "user", content: prompt }]
5083
- });
5084
- const textBlock = response.content.find((b) => b.type === "text");
5085
- const skillText = textBlock?.text;
5086
- if (!skillText) return null;
5087
- const agentId = trajectories[0].agentId;
5088
- const projectName = trajectories[0].projectName;
5089
- const skillId = await storeBehavior({
5090
- agentId,
5091
- content: skillText,
5092
- domain: "skill",
5093
- projectName
5094
- });
5095
- const dbClient = getClient();
5096
- for (const t of trajectories) {
5097
- await dbClient.execute({
5098
- sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
5099
- args: [skillId, t.id]
5100
- });
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" };
5101
5430
  }
5102
- process.stderr.write(
5103
- `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
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}".
5104
5451
  `
5105
- );
5106
- return skillId;
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 };
5107
5495
  }
5108
- async function captureAndLearn(opts) {
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 = "";
5109
5507
  try {
5110
- const config2 = await loadConfig();
5111
- if (!config2.skillLearning) return;
5112
- const { trajectoryId, similarCount, similar } = await captureTrajectory({
5113
- ...opts,
5114
- skillThreshold: config2.skillThreshold
5115
- });
5116
- if (!trajectoryId) return;
5117
- if (similarCount >= config2.skillThreshold) {
5118
- const unprocessed = similar.filter((t) => !t.skillId);
5119
- if (unprocessed.length >= config2.skillThreshold) {
5120
- extractSkill(unprocessed, config2.skillModel).catch((err) => {
5121
- process.stderr.write(
5122
- `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
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.
5123
5604
  `
5124
- );
5125
- });
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}`;
5126
5629
  }
5127
5630
  }
5128
- } catch (err) {
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}`;
5129
5637
  process.stderr.write(
5130
- `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
5638
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
5131
5639
  `
5132
5640
  );
5641
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
5642
+ } else {
5643
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
5133
5644
  }
5134
- }
5135
- async function sweepTrajectories(threshold, model) {
5136
- const config2 = await loadConfig();
5137
- if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
5138
- const t = threshold ?? config2.skillThreshold;
5139
- const client = getClient();
5140
- const result = await client.execute({
5141
- sql: `SELECT signature_hash, COUNT(*) as cnt
5142
- FROM trajectories
5143
- WHERE skill_id IS NULL
5144
- GROUP BY signature_hash
5145
- HAVING cnt >= ?
5146
- ORDER BY cnt DESC
5147
- LIMIT 10`,
5148
- args: [t]
5645
+ const spawnResult = transport.spawn(sessionName, {
5646
+ cwd: spawnCwd,
5647
+ command: spawnCommand
5149
5648
  });
5150
- let clustersProcessed = 0;
5151
- let skillsExtracted = 0;
5152
- for (const row of result.rows) {
5153
- const hash = String(row.signature_hash);
5154
- const trajResult = await client.execute({
5155
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
5156
- FROM trajectories
5157
- WHERE signature_hash = ? AND skill_id IS NULL
5158
- ORDER BY created_at DESC
5159
- LIMIT 10`,
5160
- args: [hash]
5161
- });
5162
- const trajectories = trajResult.rows.map((r) => ({
5163
- id: String(r.id),
5164
- taskId: String(r.task_id),
5165
- agentId: String(r.agent_id),
5166
- projectName: String(r.project_name),
5167
- taskTitle: String(r.task_title),
5168
- signature: JSON.parse(String(r.signature)),
5169
- signatureHash: String(r.signature_hash),
5170
- toolCount: Number(r.tool_count),
5171
- skillId: null,
5172
- 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()
5173
5662
  }));
5174
- if (trajectories.length >= t) {
5175
- clustersProcessed++;
5176
- const skillId = await extractSkill(trajectories, model ?? config2.skillModel);
5177
- 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 {
5178
5685
  }
5179
5686
  }
5180
- return { clustersProcessed, skillsExtracted };
5181
- }
5182
- function editDistance(a, b) {
5183
- const m = a.length;
5184
- const n = b.length;
5185
- const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
5186
- for (let i = 0; i <= m; i++) dp[i][0] = i;
5187
- for (let j = 0; j <= n; j++) dp[0][j] = j;
5188
- for (let i = 1; i <= m; i++) {
5189
- for (let j = 1; j <= n; j++) {
5190
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
5191
- dp[i][j] = Math.min(
5192
- dp[i - 1][j] + 1,
5193
- dp[i][j - 1] + 1,
5194
- dp[i - 1][j - 1] + cost
5195
- );
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 {
5196
5695
  }
5197
5696
  }
5198
- 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 };
5199
5707
  }
5200
- var DEFAULT_SKILL_THRESHOLD;
5201
- var init_skill_learning = __esm({
5202
- "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"() {
5203
5711
  "use strict";
5204
- init_database();
5205
- init_behaviors();
5206
- init_config();
5207
- 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…/;
5208
5730
  }
5209
5731
  });
5210
5732
 
5211
- // src/lib/tasks.ts
5212
- var tasks_exports = {};
5213
- __export(tasks_exports, {
5214
- cleanupOrphanedReviews: () => cleanupOrphanedReviews,
5215
- countNewPendingReviewsSince: () => countNewPendingReviewsSince,
5216
- countPendingReviews: () => countPendingReviews,
5217
- createTask: () => createTask,
5218
- createTaskCore: () => createTaskCore,
5219
- deleteTask: () => deleteTask,
5220
- deleteTaskCore: () => deleteTaskCore,
5221
- ensureArchitectureDoc: () => ensureArchitectureDoc,
5222
- ensureGitignoreExe: () => ensureGitignoreExe,
5223
- getReviewChecklist: () => getReviewChecklist,
5224
- listPendingReviews: () => listPendingReviews,
5225
- listTasks: () => listTasks,
5226
- resolveTask: () => resolveTask,
5227
- slugify: () => slugify,
5228
- updateTask: () => updateTask,
5229
- updateTaskStatus: () => updateTaskStatus,
5230
- 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
5231
5749
  });
5232
- import path17 from "path";
5233
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
5234
- async function createTask(input) {
5235
- const result = await createTaskCore(input);
5236
- if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
5237
- dispatchTaskToEmployee({
5238
- assignedTo: input.assignedTo,
5239
- title: input.title,
5240
- priority: input.priority,
5241
- taskFile: result.taskFile,
5242
- initialStatus: result.status,
5243
- 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]
5244
5838
  });
5839
+ return true;
5245
5840
  }
5246
- return result;
5841
+ return false;
5247
5842
  }
5248
- async function updateTask(input) {
5249
- 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();
5250
5854
  try {
5251
- const agent = String(row.assigned_to);
5252
- const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
5253
- const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
5254
- if (input.status === "in_progress") {
5255
- mkdirSync7(cacheDir, { recursive: true });
5256
- writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5257
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
5258
- try {
5259
- unlinkSync5(cachePath);
5260
- } catch {
5261
- }
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");
5262
5862
  }
5863
+ await client.execute({
5864
+ sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
5865
+ args: [now, messageId]
5866
+ });
5867
+ return true;
5263
5868
  } catch {
5264
- }
5265
- if (input.status === "done") {
5266
- await cleanupReviewFile(row, taskFile, input.baseDir);
5267
- }
5268
- if (input.status === "done" || input.status === "cancelled") {
5269
- try {
5270
- const client = getClient();
5271
- const taskTitle = String(row.title);
5272
- 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 {
5273
5873
  await client.execute({
5274
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
5275
- WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
5276
- args: [now, `%left '${escaped}' as in\\_progress%`]
5277
- });
5278
- } catch {
5279
- }
5280
- try {
5281
- const client = getClient();
5282
- const cascaded = await client.execute({
5283
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
5284
- WHERE parent_task_id = ? AND status = 'needs_review'`,
5285
- args: [now, taskId]
5874
+ sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
5875
+ args: [newRetryCount, messageId]
5286
5876
  });
5287
- if (cascaded.rowsAffected > 0) {
5288
- process.stderr.write(
5289
- `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
5290
- `
5291
- );
5292
- }
5293
- } catch {
5294
- }
5295
- }
5296
- const isTerminal = input.status === "done" || input.status === "needs_review";
5297
- if (isTerminal) {
5298
- const isExe = String(row.assigned_to) === "exe";
5299
- if (!isExe) {
5300
- notifyTaskDone();
5301
- }
5302
- await markTaskNotificationsRead(taskFile);
5303
- if (input.status === "done") {
5304
- try {
5305
- await cascadeUnblock(taskId, input.baseDir, now);
5306
- } catch {
5307
- }
5308
- if (row.parent_task_id) {
5309
- try {
5310
- await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
5311
- } catch {
5312
- }
5313
- }
5314
- }
5315
- }
5316
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
5317
- Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
5318
- ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
5319
- taskId,
5320
- agentId: String(row.assigned_to),
5321
- projectName: String(row.project_name),
5322
- taskTitle: String(row.title)
5323
- })
5324
- ).catch((err) => {
5325
- process.stderr.write(
5326
- `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
5327
- `
5328
- );
5329
- });
5330
- }
5331
- let nextTask;
5332
- if (isTerminal && String(row.assigned_to) !== "exe") {
5333
- try {
5334
- nextTask = await findNextTask(String(row.assigned_to));
5335
- } catch {
5336
5877
  }
5878
+ return false;
5337
5879
  }
5338
- return {
5339
- id: String(row.id),
5340
- title: String(row.title),
5341
- assignedTo: String(row.assigned_to),
5342
- assignedBy: String(row.assigned_by),
5343
- projectName: String(row.project_name),
5344
- priority: String(row.priority),
5345
- status: input.status,
5346
- taskFile,
5347
- createdAt: String(row.created_at),
5348
- updatedAt: now,
5349
- budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
5350
- budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
5351
- tokensUsed: Number(row.tokens_used ?? 0),
5352
- tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
5353
- nextTask
5354
- };
5355
5880
  }
5356
- 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) {
5357
5892
  const client = getClient();
5358
- const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
5359
- const reviewer = assignedBy || "exe";
5360
- const reviewSlug = `review-${assignedTo}-${taskSlug}`;
5361
- const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
5362
5893
  await client.execute({
5363
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
5364
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
5894
+ sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
5895
+ args: [messageId]
5365
5896
  });
5366
- await markAsReadByTaskFile(taskFile);
5367
- await markAsReadByTaskFile(reviewFile);
5368
5897
  }
5369
- var init_tasks = __esm({
5370
- "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"() {
5371
5975
  "use strict";
5372
5976
  init_database();
5373
- init_config();
5374
- init_notifications();
5375
- init_tasks_crud();
5376
- init_tasks_review();
5377
- init_tasks_crud();
5378
- init_tasks_chain();
5379
- init_tasks_review();
5380
- init_tasks_notify();
5977
+ init_tmux_routing();
5978
+ MAX_RETRIES2 = 10;
5979
+ _wsClientSend = null;
5381
5980
  }
5382
5981
  });
5383
5982
 
5983
+ // src/gateway/gateway.ts
5984
+ init_state_bus();
5985
+
5384
5986
  // src/gateway/router.ts
5385
5987
  function matchesPlatform(msgPlatform, matchPlatform) {
5386
5988
  if (!matchPlatform) return true;
@@ -5817,6 +6419,13 @@ var Gateway = class {
5817
6419
  console.log(
5818
6420
  `[gateway] ${msg.platform}/${msg.senderId} \u2192 ${route.employee} (${route.routeName})`
5819
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
+ });
5820
6429
  const bot = this.botRegistry.get(route.employee);
5821
6430
  if (!bot) {
5822
6431
  console.error(`[gateway] No bot registered for target: ${route.employee}`);
@@ -6610,7 +7219,7 @@ var AnthropicProvider = class {
6610
7219
 
6611
7220
  // src/gateway/providers/openai-compat.ts
6612
7221
  import OpenAI from "openai";
6613
- import { randomUUID as randomUUID2 } from "crypto";
7222
+ import { randomUUID as randomUUID3 } from "crypto";
6614
7223
  var OpenAICompatProvider = class {
6615
7224
  name;
6616
7225
  client;
@@ -6723,7 +7332,7 @@ var OpenAICompatProvider = class {
6723
7332
  }
6724
7333
  content.push({
6725
7334
  type: "tool_use",
6726
- id: call.id ?? randomUUID2(),
7335
+ id: call.id ?? randomUUID3(),
6727
7336
  name: fn.name,
6728
7337
  input
6729
7338
  });
@@ -6745,7 +7354,7 @@ var OpenAICompatProvider = class {
6745
7354
  };
6746
7355
 
6747
7356
  // src/gateway/providers/ollama.ts
6748
- import { randomUUID as randomUUID3 } from "crypto";
7357
+ import { randomUUID as randomUUID4 } from "crypto";
6749
7358
  var OllamaProvider = class {
6750
7359
  name;
6751
7360
  host;
@@ -6814,7 +7423,7 @@ var OllamaProvider = class {
6814
7423
  for (const call of data.message.tool_calls) {
6815
7424
  content.push({
6816
7425
  type: "tool_use",
6817
- id: randomUUID3(),
7426
+ id: randomUUID4(),
6818
7427
  name: call.function.name,
6819
7428
  input: call.function.arguments
6820
7429
  });
@@ -6836,7 +7445,7 @@ var OllamaProvider = class {
6836
7445
  };
6837
7446
 
6838
7447
  // src/gateway/adapters/whatsapp.ts
6839
- import { randomUUID as randomUUID4 } from "crypto";
7448
+ import { randomUUID as randomUUID5 } from "crypto";
6840
7449
  import { homedir } from "os";
6841
7450
  import { join } from "path";
6842
7451
  import { mkdirSync as mkdirSync2 } from "fs";
@@ -7018,7 +7627,7 @@ var WhatsAppAdapter = class {
7018
7627
  const location = this.extractLocation(msg.message);
7019
7628
  const dataCategory = location ? "location" : "message";
7020
7629
  return {
7021
- messageId: msg.key.id ?? randomUUID4(),
7630
+ messageId: msg.key.id ?? randomUUID5(),
7022
7631
  platform: "whatsapp",
7023
7632
  senderId,
7024
7633
  senderName: msg.pushName ?? void 0,
@@ -7063,7 +7672,7 @@ var WhatsAppAdapter = class {
7063
7672
  }
7064
7673
  const timestamp = receipt.readTimestamp ?? receipt.receiptTimestamp ?? Date.now() / 1e3;
7065
7674
  return {
7066
- messageId: randomUUID4(),
7675
+ messageId: randomUUID5(),
7067
7676
  platform: "whatsapp",
7068
7677
  senderId: remoteJid.replace("@s.whatsapp.net", "").replace("@g.us", ""),
7069
7678
  channelId: remoteJid,
@@ -7086,7 +7695,7 @@ var WhatsAppAdapter = class {
7086
7695
  const phone = id.replace("@s.whatsapp.net", "").replace("@g.us", "");
7087
7696
  const name = contact.name ?? contact.notify ?? phone;
7088
7697
  return {
7089
- messageId: randomUUID4(),
7698
+ messageId: randomUUID5(),
7090
7699
  platform: "whatsapp",
7091
7700
  senderId: phone,
7092
7701
  senderName: name,
@@ -7110,7 +7719,7 @@ var WhatsAppAdapter = class {
7110
7719
  const participants = (group.participants ?? []).map((p) => p.id ?? p);
7111
7720
  const admins = (group.participants ?? []).filter((p) => p.admin === "admin" || p.admin === "superadmin").map((p) => p.id ?? p);
7112
7721
  return {
7113
- messageId: randomUUID4(),
7722
+ messageId: randomUUID5(),
7114
7723
  platform: "whatsapp",
7115
7724
  senderId: groupId,
7116
7725
  channelId: groupId,
@@ -7135,7 +7744,7 @@ var WhatsAppAdapter = class {
7135
7744
  if (!reactionData) return null;
7136
7745
  const remoteJid = key.remoteJid ?? "";
7137
7746
  return {
7138
- messageId: randomUUID4(),
7747
+ messageId: randomUUID5(),
7139
7748
  platform: "whatsapp",
7140
7749
  senderId: reactionData.key?.participant ?? reactionData.key?.remoteJid?.replace("@s.whatsapp.net", "") ?? "",
7141
7750
  channelId: remoteJid,
@@ -7157,7 +7766,7 @@ var WhatsAppAdapter = class {
7157
7766
  if (!chatId) return null;
7158
7767
  const caller = call.from?.replace("@s.whatsapp.net", "") ?? "";
7159
7768
  return {
7160
- messageId: randomUUID4(),
7769
+ messageId: randomUUID5(),
7161
7770
  platform: "whatsapp",
7162
7771
  senderId: caller,
7163
7772
  channelId: chatId,
@@ -7200,7 +7809,7 @@ var WhatsAppAdapter = class {
7200
7809
  };
7201
7810
 
7202
7811
  // src/gateway/adapters/signal.ts
7203
- import { randomUUID as randomUUID5 } from "crypto";
7812
+ import { randomUUID as randomUUID6 } from "crypto";
7204
7813
  var DEFAULT_TIMEOUT_MS = 1e4;
7205
7814
  var SignalAdapter = class {
7206
7815
  platform = "signal";
@@ -7285,7 +7894,7 @@ var SignalAdapter = class {
7285
7894
  }
7286
7895
  }
7287
7896
  async rpcRequest(method, params) {
7288
- const id = randomUUID5();
7897
+ const id = randomUUID6();
7289
7898
  const res = await fetch(`${this.baseUrl}/api/v1/rpc`, {
7290
7899
  method: "POST",
7291
7900
  headers: { "Content-Type": "application/json" },
@@ -7375,7 +7984,7 @@ ${val}` : val;
7375
7984
  if (envelope.reactionMessage) {
7376
7985
  const rm = envelope.reactionMessage;
7377
7986
  const normalized2 = {
7378
- messageId: randomUUID5(),
7987
+ messageId: randomUUID6(),
7379
7988
  platform: "signal",
7380
7989
  senderId,
7381
7990
  senderName: envelope.sourceName ?? void 0,
@@ -7403,7 +8012,7 @@ ${val}` : val;
7403
8012
  const rcpt = envelope.receiptMessage;
7404
8013
  for (const ts of rcpt.timestamps) {
7405
8014
  const normalized2 = {
7406
- messageId: randomUUID5(),
8015
+ messageId: randomUUID6(),
7407
8016
  platform: "signal",
7408
8017
  senderId,
7409
8018
  senderName: envelope.sourceName ?? void 0,
@@ -7433,7 +8042,7 @@ ${val}` : val;
7433
8042
  const dm2 = em.dataMessage;
7434
8043
  const isGroup2 = !!dm2.groupInfo?.groupId;
7435
8044
  const normalized2 = {
7436
- messageId: String(dm2.timestamp ?? randomUUID5()),
8045
+ messageId: String(dm2.timestamp ?? randomUUID6()),
7437
8046
  platform: "signal",
7438
8047
  senderId,
7439
8048
  senderName: envelope.sourceName ?? void 0,
@@ -7459,7 +8068,7 @@ ${val}` : val;
7459
8068
  const dm = envelope.dataMessage;
7460
8069
  const isGroup = !!dm.groupInfo?.groupId;
7461
8070
  const normalized = {
7462
- messageId: String(dm.timestamp ?? randomUUID5()),
8071
+ messageId: String(dm.timestamp ?? randomUUID6()),
7463
8072
  platform: "signal",
7464
8073
  senderId,
7465
8074
  senderName: envelope.sourceName ?? void 0,
@@ -7500,7 +8109,7 @@ ${val}` : val;
7500
8109
  if (!phone) continue;
7501
8110
  const name = contact.name ?? contact.profileName ?? phone;
7502
8111
  const normalized = {
7503
- messageId: randomUUID5(),
8112
+ messageId: randomUUID6(),
7504
8113
  platform: "signal",
7505
8114
  senderId: phone,
7506
8115
  senderName: name,
@@ -7529,7 +8138,7 @@ ${val}` : val;
7529
8138
  if (!Array.isArray(groups)) return;
7530
8139
  for (const group of groups) {
7531
8140
  const normalized = {
7532
- messageId: randomUUID5(),
8141
+ messageId: randomUUID6(),
7533
8142
  platform: "signal",
7534
8143
  senderId: `group:${group.id}`,
7535
8144
  channelId: `group:${group.id}`,
@@ -7564,7 +8173,7 @@ ${val}` : val;
7564
8173
  };
7565
8174
 
7566
8175
  // src/gateway/adapters/webchat.ts
7567
- import { randomUUID as randomUUID6 } from "crypto";
8176
+ import { randomUUID as randomUUID7 } from "crypto";
7568
8177
  import { createServer } from "http";
7569
8178
  var WebChatAdapter = class {
7570
8179
  platform = "webchat";
@@ -7660,7 +8269,7 @@ var WebChatAdapter = class {
7660
8269
  res.end(JSON.stringify({ error: "No message text" }));
7661
8270
  return;
7662
8271
  }
7663
- const requestId = randomUUID6();
8272
+ const requestId = randomUUID7();
7664
8273
  const sessionId = parsed.sessionId ?? this.extractSessionId(req);
7665
8274
  const normalized = {
7666
8275
  messageId: requestId,
@@ -7703,7 +8312,7 @@ var WebChatAdapter = class {
7703
8312
  extractSessionId(req) {
7704
8313
  const cookies = req.headers.cookie ?? "";
7705
8314
  const match = cookies.match(/exe_session=([^;]+)/);
7706
- return match?.[1] ?? `anon-${randomUUID6().slice(0, 8)}`;
8315
+ return match?.[1] ?? `anon-${randomUUID7().slice(0, 8)}`;
7707
8316
  }
7708
8317
  };
7709
8318
 
@@ -7985,7 +8594,7 @@ var DiscordAdapter = class {
7985
8594
  };
7986
8595
 
7987
8596
  // src/gateway/adapters/slack.ts
7988
- import { randomUUID as randomUUID7 } from "crypto";
8597
+ import { randomUUID as randomUUID8 } from "crypto";
7989
8598
  var SlackAdapter = class {
7990
8599
  platform = "slack";
7991
8600
  webClient = null;
@@ -8023,7 +8632,7 @@ var SlackAdapter = class {
8023
8632
  if (event.subtype) return;
8024
8633
  const isGroup = event.channel_type !== "im";
8025
8634
  const normalized = {
8026
- messageId: event.client_msg_id ?? event.ts ?? randomUUID7(),
8635
+ messageId: event.client_msg_id ?? event.ts ?? randomUUID8(),
8027
8636
  platform: "slack",
8028
8637
  senderId: event.user ?? "",
8029
8638
  channelId: event.channel ?? "",
@@ -8085,7 +8694,7 @@ var SlackAdapter = class {
8085
8694
  if (!event.text) return;
8086
8695
  const isGroup = event.channel_type !== "im";
8087
8696
  const normalized = {
8088
- messageId: event.ts ?? randomUUID7(),
8697
+ messageId: event.ts ?? randomUUID8(),
8089
8698
  platform: "slack",
8090
8699
  senderId: event.user ?? "",
8091
8700
  senderName: event.user_profile?.display_name ?? event.user_profile?.real_name ?? void 0,
@@ -8470,7 +9079,7 @@ var FailoverExhaustedError = class extends Error {
8470
9079
  };
8471
9080
 
8472
9081
  // src/gateway/session-store.ts
8473
- import { randomUUID as randomUUID8 } from "crypto";
9082
+ import { randomUUID as randomUUID9 } from "crypto";
8474
9083
  var DEFAULT_CONFIG3 = {
8475
9084
  idleTimeoutMs: 30 * 6e4,
8476
9085
  maxMessages: 100
@@ -8497,7 +9106,7 @@ var SessionStore = class {
8497
9106
  existing.status = "closed";
8498
9107
  }
8499
9108
  const session = {
8500
- sessionId: randomUUID8(),
9109
+ sessionId: randomUUID9(),
8501
9110
  customerId,
8502
9111
  botId,
8503
9112
  platform,
@@ -8894,7 +9503,7 @@ function formatAlert(alert) {
8894
9503
  }
8895
9504
 
8896
9505
  // src/gateway/customer-store.ts
8897
- import { randomUUID as randomUUID9 } from "crypto";
9506
+ import { randomUUID as randomUUID10 } from "crypto";
8898
9507
  var CustomerStore = class {
8899
9508
  customers = /* @__PURE__ */ new Map();
8900
9509
  identities = /* @__PURE__ */ new Map();
@@ -8913,7 +9522,7 @@ var CustomerStore = class {
8913
9522
  return customer2;
8914
9523
  }
8915
9524
  const customer = {
8916
- id: randomUUID9(),
9525
+ id: randomUUID10(),
8917
9526
  firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
8918
9527
  lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
8919
9528
  interactionCount: 1
@@ -8973,7 +9582,7 @@ async function ensureCRMContact(info) {
8973
9582
 
8974
9583
  // src/automation/trigger-engine.ts
8975
9584
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
8976
- import { randomUUID as randomUUID11 } from "crypto";
9585
+ import { randomUUID as randomUUID12 } from "crypto";
8977
9586
  import path18 from "path";
8978
9587
  import os8 from "os";
8979
9588
  var TRIGGERS_PATH = path18.join(os8.homedir(), ".exe-os", "triggers.json");