@askexenow/exe-os 0.8.40 → 0.8.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/bin/backfill-conversations.js +805 -642
  2. package/dist/bin/backfill-responses.js +804 -641
  3. package/dist/bin/backfill-vectors.js +791 -634
  4. package/dist/bin/cleanup-stale-review-tasks.js +788 -631
  5. package/dist/bin/cli.js +1376 -659
  6. package/dist/bin/exe-agent.js +20 -1
  7. package/dist/bin/exe-assign.js +1503 -1343
  8. package/dist/bin/exe-boot.js +2549 -1784
  9. package/dist/bin/exe-call.js +39 -1
  10. package/dist/bin/exe-cloud.js +12 -2
  11. package/dist/bin/exe-dispatch.js +39 -2
  12. package/dist/bin/exe-doctor.js +791 -634
  13. package/dist/bin/exe-export-behaviors.js +792 -637
  14. package/dist/bin/exe-forget.js +145 -0
  15. package/dist/bin/exe-gateway.js +2501 -1846
  16. package/dist/bin/exe-heartbeat.js +147 -1
  17. package/dist/bin/exe-kill.js +795 -640
  18. package/dist/bin/exe-launch-agent.js +2168 -2008
  19. package/dist/bin/exe-link.js +44 -12
  20. package/dist/bin/exe-new-employee.js +6 -2
  21. package/dist/bin/exe-pending-messages.js +146 -1
  22. package/dist/bin/exe-pending-notifications.js +788 -631
  23. package/dist/bin/exe-pending-reviews.js +176 -1
  24. package/dist/bin/exe-rename.js +23 -0
  25. package/dist/bin/exe-review.js +490 -327
  26. package/dist/bin/exe-search.js +157 -4
  27. package/dist/bin/exe-session-cleanup.js +2487 -403
  28. package/dist/bin/exe-settings.js +2 -1
  29. package/dist/bin/exe-status.js +474 -317
  30. package/dist/bin/exe-team.js +474 -317
  31. package/dist/bin/git-sweep.js +2691 -151
  32. package/dist/bin/graph-backfill.js +794 -637
  33. package/dist/bin/graph-export.js +798 -641
  34. package/dist/bin/scan-tasks.js +2951 -44
  35. package/dist/bin/setup.js +50 -26
  36. package/dist/bin/shard-migrate.js +792 -637
  37. package/dist/bin/wiki-sync.js +794 -637
  38. package/dist/gateway/index.js +2542 -1887
  39. package/dist/hooks/bug-report-worker.js +2118 -576
  40. package/dist/hooks/commit-complete.js +2690 -150
  41. package/dist/hooks/error-recall.js +157 -4
  42. package/dist/hooks/ingest-worker.js +1455 -803
  43. package/dist/hooks/instructions-loaded.js +151 -0
  44. package/dist/hooks/notification.js +153 -2
  45. package/dist/hooks/post-compact.js +164 -0
  46. package/dist/hooks/pre-compact.js +3073 -101
  47. package/dist/hooks/pre-tool-use.js +151 -0
  48. package/dist/hooks/prompt-ingest-worker.js +1670 -1509
  49. package/dist/hooks/prompt-submit.js +2650 -1074
  50. package/dist/hooks/response-ingest-worker.js +154 -6
  51. package/dist/hooks/session-end.js +153 -2
  52. package/dist/hooks/session-start.js +157 -4
  53. package/dist/hooks/stop.js +151 -0
  54. package/dist/hooks/subagent-stop.js +155 -2
  55. package/dist/hooks/summary-worker.js +190 -21
  56. package/dist/index.js +326 -102
  57. package/dist/lib/cloud-sync.js +31 -10
  58. package/dist/lib/config.js +2 -0
  59. package/dist/lib/consolidation.js +69 -2
  60. package/dist/lib/database.js +19 -0
  61. package/dist/lib/device-registry.js +19 -0
  62. package/dist/lib/embedder.js +3 -1
  63. package/dist/lib/employee-templates.js +20 -1
  64. package/dist/lib/exe-daemon.js +285 -18
  65. package/dist/lib/hybrid-search.js +157 -4
  66. package/dist/lib/messaging.js +39 -2
  67. package/dist/lib/schedules.js +792 -637
  68. package/dist/lib/store.js +796 -636
  69. package/dist/lib/tasks.js +1485 -918
  70. package/dist/lib/tmux-routing.js +194 -10
  71. package/dist/mcp/server.js +1643 -924
  72. package/dist/mcp/tools/create-task.js +2283 -829
  73. package/dist/mcp/tools/list-tasks.js +2788 -159
  74. package/dist/mcp/tools/send-message.js +39 -2
  75. package/dist/mcp/tools/update-task.js +79 -0
  76. package/dist/runtime/index.js +280 -68
  77. package/dist/tui/App.js +1485 -645
  78. package/package.json +3 -2
@@ -31,6 +31,7 @@ var config_exports = {};
31
31
  __export(config_exports, {
32
32
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
33
33
  CONFIG_PATH: () => CONFIG_PATH,
34
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
34
35
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
35
36
  DB_PATH: () => DB_PATH,
36
37
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -186,7 +187,7 @@ async function loadConfigFrom(configPath) {
186
187
  return { ...DEFAULT_CONFIG };
187
188
  }
188
189
  }
189
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
190
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
190
191
  var init_config = __esm({
191
192
  "src/lib/config.ts"() {
192
193
  "use strict";
@@ -194,6 +195,7 @@ var init_config = __esm({
194
195
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
195
196
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
196
197
  CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
198
+ COO_AGENT_NAME = "exe";
197
199
  LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
198
200
  CURRENT_CONFIG_VERSION = 1;
199
201
  DEFAULT_CONFIG = {
@@ -359,13 +361,6 @@ var init_employees = __esm({
359
361
  }
360
362
  });
361
363
 
362
- // src/types/memory.ts
363
- var init_memory = __esm({
364
- "src/types/memory.ts"() {
365
- "use strict";
366
- }
367
- });
368
-
369
364
  // src/lib/db-retry.ts
370
365
  function isBusyError(err) {
371
366
  if (err instanceof Error) {
@@ -662,6 +657,13 @@ async function ensureSchema() {
662
657
  });
663
658
  } catch {
664
659
  }
660
+ try {
661
+ await client.execute({
662
+ sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
663
+ args: []
664
+ });
665
+ } catch {
666
+ }
665
667
  try {
666
668
  await client.execute({
667
669
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -1108,6 +1110,18 @@ async function ensureSchema() {
1108
1110
  CREATE INDEX IF NOT EXISTS idx_session_kills_agent
1109
1111
  ON session_kills(agent_id);
1110
1112
  `);
1113
+ await client.execute(`
1114
+ CREATE TABLE IF NOT EXISTS global_procedures (
1115
+ id TEXT PRIMARY KEY,
1116
+ title TEXT NOT NULL,
1117
+ content TEXT NOT NULL,
1118
+ priority TEXT NOT NULL DEFAULT 'p0',
1119
+ domain TEXT,
1120
+ active INTEGER NOT NULL DEFAULT 1,
1121
+ created_at TEXT NOT NULL,
1122
+ updated_at TEXT NOT NULL
1123
+ )
1124
+ `);
1111
1125
  await client.executeMultiple(`
1112
1126
  CREATE TABLE IF NOT EXISTS conversations (
1113
1127
  id TEXT PRIMARY KEY,
@@ -1257,6 +1271,78 @@ var init_database = __esm({
1257
1271
  }
1258
1272
  });
1259
1273
 
1274
+ // src/lib/global-procedures.ts
1275
+ var global_procedures_exports = {};
1276
+ __export(global_procedures_exports, {
1277
+ deactivateGlobalProcedure: () => deactivateGlobalProcedure,
1278
+ getGlobalProceduresBlock: () => getGlobalProceduresBlock,
1279
+ loadGlobalProcedures: () => loadGlobalProcedures,
1280
+ storeGlobalProcedure: () => storeGlobalProcedure
1281
+ });
1282
+ import { randomUUID } from "crypto";
1283
+ async function loadGlobalProcedures() {
1284
+ const client = getClient();
1285
+ const result = await client.execute({
1286
+ sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
1287
+ args: []
1288
+ });
1289
+ const procedures = result.rows;
1290
+ if (procedures.length > 0) {
1291
+ _cache = procedures.map((p) => `### ${p.title}
1292
+ ${p.content}`).join("\n\n");
1293
+ } else {
1294
+ _cache = "";
1295
+ }
1296
+ _cacheLoaded = true;
1297
+ return procedures;
1298
+ }
1299
+ function getGlobalProceduresBlock() {
1300
+ if (!_cacheLoaded) return "";
1301
+ if (!_cache) return "";
1302
+ return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
1303
+
1304
+ ${_cache}
1305
+ `;
1306
+ }
1307
+ async function storeGlobalProcedure(input) {
1308
+ const id = randomUUID();
1309
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1310
+ const client = getClient();
1311
+ await client.execute({
1312
+ sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
1313
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
1314
+ args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
1315
+ });
1316
+ await loadGlobalProcedures();
1317
+ return id;
1318
+ }
1319
+ async function deactivateGlobalProcedure(id) {
1320
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1321
+ const client = getClient();
1322
+ const result = await client.execute({
1323
+ sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
1324
+ args: [now, id]
1325
+ });
1326
+ await loadGlobalProcedures();
1327
+ return result.rowsAffected > 0;
1328
+ }
1329
+ var _cache, _cacheLoaded;
1330
+ var init_global_procedures = __esm({
1331
+ "src/lib/global-procedures.ts"() {
1332
+ "use strict";
1333
+ init_database();
1334
+ _cache = "";
1335
+ _cacheLoaded = false;
1336
+ }
1337
+ });
1338
+
1339
+ // src/types/memory.ts
1340
+ var init_memory = __esm({
1341
+ "src/types/memory.ts"() {
1342
+ "use strict";
1343
+ }
1344
+ });
1345
+
1260
1346
  // src/lib/keychain.ts
1261
1347
  var keychain_exports = {};
1262
1348
  __export(keychain_exports, {
@@ -1404,6 +1490,61 @@ var init_keychain = __esm({
1404
1490
  }
1405
1491
  });
1406
1492
 
1493
+ // src/lib/state-bus.ts
1494
+ var StateBus, orgBus;
1495
+ var init_state_bus = __esm({
1496
+ "src/lib/state-bus.ts"() {
1497
+ "use strict";
1498
+ StateBus = class {
1499
+ handlers = /* @__PURE__ */ new Map();
1500
+ globalHandlers = /* @__PURE__ */ new Set();
1501
+ /** Emit an event to all subscribers */
1502
+ emit(event) {
1503
+ const typeHandlers = this.handlers.get(event.type);
1504
+ if (typeHandlers) {
1505
+ for (const handler of typeHandlers) {
1506
+ try {
1507
+ handler(event);
1508
+ } catch {
1509
+ }
1510
+ }
1511
+ }
1512
+ for (const handler of this.globalHandlers) {
1513
+ try {
1514
+ handler(event);
1515
+ } catch {
1516
+ }
1517
+ }
1518
+ }
1519
+ /** Subscribe to a specific event type */
1520
+ on(type, handler) {
1521
+ if (!this.handlers.has(type)) {
1522
+ this.handlers.set(type, /* @__PURE__ */ new Set());
1523
+ }
1524
+ this.handlers.get(type).add(handler);
1525
+ }
1526
+ /** Subscribe to ALL events */
1527
+ onAny(handler) {
1528
+ this.globalHandlers.add(handler);
1529
+ }
1530
+ /** Unsubscribe from a specific event type */
1531
+ off(type, handler) {
1532
+ this.handlers.get(type)?.delete(handler);
1533
+ }
1534
+ /** Unsubscribe from ALL events */
1535
+ offAny(handler) {
1536
+ this.globalHandlers.delete(handler);
1537
+ }
1538
+ /** Remove all listeners */
1539
+ clear() {
1540
+ this.handlers.clear();
1541
+ this.globalHandlers.clear();
1542
+ }
1543
+ };
1544
+ orgBus = new StateBus();
1545
+ }
1546
+ });
1547
+
1407
1548
  // src/lib/shard-manager.ts
1408
1549
  var shard_manager_exports = {};
1409
1550
  __export(shard_manager_exports, {
@@ -1708,6 +1849,11 @@ async function initStore(options) {
1708
1849
  "version-query"
1709
1850
  );
1710
1851
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1852
+ try {
1853
+ const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
1854
+ await loadGlobalProcedures2();
1855
+ } catch {
1856
+ }
1711
1857
  }
1712
1858
  var INIT_MAX_RETRIES, INIT_RETRY_DELAY_MS, _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
1713
1859
  var init_store = __esm({
@@ -1717,6 +1863,7 @@ var init_store = __esm({
1717
1863
  init_database();
1718
1864
  init_keychain();
1719
1865
  init_config();
1866
+ init_state_bus();
1720
1867
  INIT_MAX_RETRIES = 3;
1721
1868
  INIT_RETRY_DELAY_MS = 1e3;
1722
1869
  _pendingRecords = [];
@@ -2243,7 +2390,7 @@ __export(license_exports, {
2243
2390
  validateLicense: () => validateLicense
2244
2391
  });
2245
2392
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
2246
- import { randomUUID } from "crypto";
2393
+ import { randomUUID as randomUUID2 } from "crypto";
2247
2394
  import path9 from "path";
2248
2395
  import { jwtVerify, importSPKI } from "jose";
2249
2396
  async function fetchRetry(url, init) {
@@ -2270,7 +2417,7 @@ function loadDeviceId() {
2270
2417
  }
2271
2418
  } catch {
2272
2419
  }
2273
- const id = randomUUID();
2420
+ const id = randomUUID2();
2274
2421
  mkdirSync5(EXE_AI_DIR, { recursive: true });
2275
2422
  writeFileSync4(DEVICE_ID_PATH, id, "utf8");
2276
2423
  return id;
@@ -2652,678 +2799,780 @@ var init_plan_limits = __esm({
2652
2799
  }
2653
2800
  });
2654
2801
 
2655
- // src/lib/tmux-routing.ts
2656
- import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
2657
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, existsSync as existsSync10, appendFileSync } from "fs";
2658
- import path11 from "path";
2659
- import os6 from "os";
2660
- import { fileURLToPath as fileURLToPath2 } from "url";
2661
- import { unlinkSync as unlinkSync3 } from "fs";
2662
- function spawnLockPath(sessionName) {
2663
- return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2664
- }
2665
- function isProcessAlive(pid) {
2666
- try {
2667
- process.kill(pid, 0);
2668
- return true;
2669
- } catch {
2670
- return false;
2671
- }
2672
- }
2673
- function acquireSpawnLock(sessionName) {
2674
- if (!existsSync10(SPAWN_LOCK_DIR)) {
2675
- mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
2676
- }
2677
- const lockFile = spawnLockPath(sessionName);
2678
- if (existsSync10(lockFile)) {
2679
- try {
2680
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
2681
- const age = Date.now() - lock.timestamp;
2682
- if (isProcessAlive(lock.pid) && age < 6e4) {
2683
- return false;
2684
- }
2685
- } catch {
2686
- }
2687
- }
2688
- writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
2689
- return true;
2690
- }
2691
- function releaseSpawnLock(sessionName) {
2692
- try {
2693
- unlinkSync3(spawnLockPath(sessionName));
2694
- } catch {
2695
- }
2696
- }
2697
- function resolveBehaviorsExporterScript() {
2698
- try {
2699
- const thisFile = fileURLToPath2(import.meta.url);
2700
- const scriptPath = path11.join(
2701
- path11.dirname(thisFile),
2702
- "..",
2703
- "bin",
2704
- "exe-export-behaviors.js"
2705
- );
2706
- return existsSync10(scriptPath) ? scriptPath : null;
2707
- } catch {
2708
- return null;
2709
- }
2710
- }
2711
- function exportBehaviorsSync(agentId, projectName, sessionKey) {
2712
- const script = resolveBehaviorsExporterScript();
2713
- if (!script) return null;
2802
+ // src/lib/session-kill-telemetry.ts
2803
+ var session_kill_telemetry_exports = {};
2804
+ __export(session_kill_telemetry_exports, {
2805
+ IDLE_KILL_MIN_LIVE_SESSIONS: () => IDLE_KILL_MIN_LIVE_SESSIONS,
2806
+ IDLE_KILL_STREAK_META_KEY: () => IDLE_KILL_STREAK_META_KEY,
2807
+ IDLE_KILL_SUSPECT_DAY_THRESHOLD: () => IDLE_KILL_SUSPECT_DAY_THRESHOLD,
2808
+ TOKENS_PER_IDLE_MINUTE: () => TOKENS_PER_IDLE_MINUTE,
2809
+ computeIdleKillSuspectStreak: () => computeIdleKillSuspectStreak,
2810
+ countKillsSince: () => countKillsSince,
2811
+ parseStreakState: () => parseStreakState,
2812
+ recordSessionKill: () => recordSessionKill,
2813
+ sumTokensSavedSince: () => sumTokensSavedSince
2814
+ });
2815
+ import crypto3 from "crypto";
2816
+ async function recordSessionKill(input) {
2714
2817
  try {
2715
- const output = execFileSync2(
2716
- process.execPath,
2717
- [script, agentId, projectName, sessionKey],
2718
- { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
2719
- ).trim();
2720
- return output.length > 0 ? output : null;
2818
+ const client = getClient();
2819
+ await client.execute({
2820
+ sql: `INSERT INTO session_kills
2821
+ (id, session_name, agent_id, killed_at, reason,
2822
+ ticks_idle, estimated_tokens_saved)
2823
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
2824
+ args: [
2825
+ crypto3.randomUUID(),
2826
+ input.sessionName,
2827
+ input.agentId,
2828
+ (/* @__PURE__ */ new Date()).toISOString(),
2829
+ input.reason,
2830
+ input.ticksIdle ?? null,
2831
+ input.estimatedTokensSaved ?? null
2832
+ ]
2833
+ });
2721
2834
  } catch (err) {
2722
2835
  process.stderr.write(
2723
- `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
2836
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
2724
2837
  `
2725
2838
  );
2726
- return null;
2727
- }
2728
- }
2729
- function getMySession() {
2730
- return getTransport().getMySession();
2731
- }
2732
- function employeeSessionName(employee, exeSession, instance) {
2733
- const suffix = instance != null && instance > 0 ? String(instance) : "";
2734
- return `${employee}${suffix}-${exeSession}`;
2735
- }
2736
- function extractRootExe(name) {
2737
- const match = name.match(/(exe\d+)$/);
2738
- return match?.[1] ?? null;
2739
- }
2740
- function getParentExe(sessionKey) {
2741
- try {
2742
- const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
2743
- return data.parentExe || null;
2744
- } catch {
2745
- return null;
2746
2839
  }
2747
2840
  }
2748
- function getDispatchedBy(sessionKey) {
2841
+ async function countKillsSince(sinceISO) {
2749
2842
  try {
2750
- const data = JSON.parse(readFileSync9(
2751
- path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
2752
- "utf8"
2753
- ));
2754
- return data.dispatchedBy ?? data.parentExe ?? null;
2843
+ const client = getClient();
2844
+ const result = await client.execute({
2845
+ sql: `SELECT COUNT(*) AS n FROM session_kills WHERE killed_at >= ?`,
2846
+ args: [sinceISO]
2847
+ });
2848
+ const row = result.rows[0];
2849
+ return row ? Number(row.n) : 0;
2755
2850
  } catch {
2756
- return null;
2851
+ return 0;
2757
2852
  }
2758
2853
  }
2759
- function resolveExeSession() {
2760
- const mySession = getMySession();
2761
- if (!mySession) return null;
2854
+ function parseStreakState(raw) {
2855
+ if (!raw) return { lastDate: null, streak: 0 };
2762
2856
  try {
2763
- const key = getSessionKey();
2764
- const parentExe = getParentExe(key);
2765
- if (parentExe) {
2766
- return extractRootExe(parentExe) ?? parentExe;
2767
- }
2857
+ const parsed = JSON.parse(raw);
2858
+ return {
2859
+ lastDate: typeof parsed.lastDate === "string" ? parsed.lastDate : null,
2860
+ streak: typeof parsed.streak === "number" ? parsed.streak : 0
2861
+ };
2768
2862
  } catch {
2863
+ return { lastDate: null, streak: 0 };
2769
2864
  }
2770
- return extractRootExe(mySession) ?? mySession;
2771
2865
  }
2772
- function isEmployeeAlive(sessionName) {
2773
- return getTransport().isAlive(sessionName);
2866
+ function nextStreakState(prev, qualifiesToday, todayDate) {
2867
+ if (!qualifiesToday) return { lastDate: todayDate, streak: 0 };
2868
+ if (prev.lastDate === todayDate) return prev;
2869
+ return { lastDate: todayDate, streak: prev.streak + 1 };
2774
2870
  }
2775
- function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
2776
- const base = employeeSessionName(employeeName, exeSession);
2777
- if (!isAlive(base) && acquireSpawnLock(base)) return 0;
2778
- for (let i = 2; i <= maxInstances; i++) {
2779
- const candidate = employeeSessionName(employeeName, exeSession, i);
2780
- if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
2781
- }
2782
- return null;
2871
+ function computeIdleKillSuspectStreak(prev, killsToday, liveSessions, todayDate) {
2872
+ const qualifies = killsToday === 0 && liveSessions >= IDLE_KILL_MIN_LIVE_SESSIONS;
2873
+ const state = nextStreakState(prev, qualifies, todayDate);
2874
+ return {
2875
+ state,
2876
+ suspect: state.streak >= IDLE_KILL_SUSPECT_DAY_THRESHOLD
2877
+ };
2783
2878
  }
2784
- function readDebounceState() {
2879
+ async function sumTokensSavedSince(sinceISO) {
2785
2880
  try {
2786
- if (!existsSync10(DEBOUNCE_FILE)) return {};
2787
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
2881
+ const client = getClient();
2882
+ const result = await client.execute({
2883
+ sql: `SELECT COALESCE(SUM(estimated_tokens_saved), 0) AS total
2884
+ FROM session_kills
2885
+ WHERE killed_at >= ?`,
2886
+ args: [sinceISO]
2887
+ });
2888
+ const row = result.rows[0];
2889
+ return row ? Number(row.total) : 0;
2788
2890
  } catch {
2789
- return {};
2891
+ return 0;
2790
2892
  }
2791
2893
  }
2792
- function writeDebounceState(state) {
2793
- try {
2794
- if (!existsSync10(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
2795
- writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
2796
- } catch {
2894
+ var TOKENS_PER_IDLE_MINUTE, IDLE_KILL_STREAK_META_KEY, IDLE_KILL_SUSPECT_DAY_THRESHOLD, IDLE_KILL_MIN_LIVE_SESSIONS;
2895
+ var init_session_kill_telemetry = __esm({
2896
+ "src/lib/session-kill-telemetry.ts"() {
2897
+ "use strict";
2898
+ init_database();
2899
+ TOKENS_PER_IDLE_MINUTE = 50;
2900
+ IDLE_KILL_STREAK_META_KEY = "idle_kill_suspect_streak";
2901
+ IDLE_KILL_SUSPECT_DAY_THRESHOLD = 3;
2902
+ IDLE_KILL_MIN_LIVE_SESSIONS = 5;
2797
2903
  }
2798
- }
2799
- function isDebounced(targetSession) {
2800
- const state = readDebounceState();
2801
- const lastSent = state[targetSession] ?? 0;
2802
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
2803
- }
2804
- function recordDebounce(targetSession) {
2805
- const state = readDebounceState();
2806
- state[targetSession] = Date.now();
2807
- const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
2808
- for (const key of Object.keys(state)) {
2809
- if ((state[key] ?? 0) < cutoff) delete state[key];
2904
+ });
2905
+
2906
+ // src/lib/tasks-crud.ts
2907
+ import crypto4 from "crypto";
2908
+ import path11 from "path";
2909
+ import { execSync as execSync6 } from "child_process";
2910
+ import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
2911
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
2912
+ async function writeCheckpoint(input) {
2913
+ const client = getClient();
2914
+ const row = await resolveTask(client, input.taskId);
2915
+ const taskId = String(row.id);
2916
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2917
+ const blockedByIds = [];
2918
+ if (row.blocked_by) {
2919
+ blockedByIds.push(String(row.blocked_by));
2810
2920
  }
2811
- writeDebounceState(state);
2812
- }
2813
- function logIntercom(msg) {
2814
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
2815
- `;
2816
- process.stderr.write(`[intercom] ${msg}
2817
- `);
2818
- try {
2819
- appendFileSync(INTERCOM_LOG2, line);
2820
- } catch {
2921
+ const checkpoint = {
2922
+ step: input.step,
2923
+ context_summary: input.contextSummary,
2924
+ files_touched: input.filesTouched ?? [],
2925
+ blocked_by_ids: blockedByIds,
2926
+ last_checkpoint_at: now
2927
+ };
2928
+ const result = await client.execute({
2929
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
2930
+ args: [JSON.stringify(checkpoint), now, taskId]
2931
+ });
2932
+ if (result.rowsAffected === 0) {
2933
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
2821
2934
  }
2935
+ const countResult = await client.execute({
2936
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
2937
+ args: [taskId]
2938
+ });
2939
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
2940
+ return { checkpointCount };
2822
2941
  }
2823
- function getSessionState(sessionName) {
2824
- const transport = getTransport();
2825
- if (!transport.isAlive(sessionName)) return "offline";
2826
- try {
2827
- const pane = transport.capturePane(sessionName, 5);
2828
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
2829
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
2830
- return "no_claude";
2831
- }
2832
- }
2833
- if (/Running…/.test(pane)) return "tool";
2834
- if (BUSY_PATTERN.test(pane)) return "thinking";
2835
- return "idle";
2836
- } catch {
2837
- return "offline";
2838
- }
2942
+ function extractParentFromContext(contextBody) {
2943
+ if (!contextBody) return null;
2944
+ const match = contextBody.match(
2945
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
2946
+ );
2947
+ return match ? match[1].toLowerCase() : null;
2839
2948
  }
2840
- function isExeSession(sessionName) {
2841
- return /^exe\d*$/.test(sessionName);
2949
+ function slugify(title) {
2950
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2842
2951
  }
2843
- function sendIntercom(targetSession) {
2844
- const transport = getTransport();
2845
- if (isExeSession(targetSession)) {
2846
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
2847
- return "skipped_exe";
2848
- }
2849
- if (isDebounced(targetSession)) {
2850
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
2851
- return "debounced";
2952
+ async function resolveTask(client, identifier) {
2953
+ let result = await client.execute({
2954
+ sql: "SELECT * FROM tasks WHERE id = ?",
2955
+ args: [identifier]
2956
+ });
2957
+ if (result.rows.length === 1) return result.rows[0];
2958
+ result = await client.execute({
2959
+ sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
2960
+ args: [`%${identifier}%`]
2961
+ });
2962
+ if (result.rows.length === 1) return result.rows[0];
2963
+ if (result.rows.length > 1) {
2964
+ const exact = result.rows.filter(
2965
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
2966
+ );
2967
+ if (exact.length === 1) return exact[0];
2968
+ const candidates = exact.length > 1 ? exact : result.rows;
2969
+ const active = candidates.filter(
2970
+ (r) => !["done", "cancelled"].includes(String(r.status))
2971
+ );
2972
+ if (active.length === 1) return active[0];
2973
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
2974
+ throw new Error(
2975
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
2976
+ );
2852
2977
  }
2853
- try {
2854
- const sessions = transport.listSessions();
2855
- if (!sessions.includes(targetSession)) {
2856
- logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
2857
- return "failed";
2858
- }
2859
- const sessionState = getSessionState(targetSession);
2860
- if (sessionState === "no_claude") {
2861
- queueIntercom(targetSession, "claude not running in session");
2862
- recordDebounce(targetSession);
2863
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
2864
- return "queued";
2865
- }
2866
- if (sessionState === "thinking" || sessionState === "tool") {
2867
- queueIntercom(targetSession, "session busy at send time");
2868
- recordDebounce(targetSession);
2869
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
2870
- return "queued";
2871
- }
2872
- if (transport.isPaneInCopyMode(targetSession)) {
2873
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
2874
- transport.sendKeys(targetSession, "q");
2875
- }
2876
- transport.sendKeys(targetSession, "/exe-intercom");
2877
- recordDebounce(targetSession);
2878
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
2879
- return "delivered";
2880
- } catch {
2881
- logIntercom(`FAIL \u2192 ${targetSession}`);
2882
- return "failed";
2978
+ result = await client.execute({
2979
+ sql: "SELECT * FROM tasks WHERE title LIKE ?",
2980
+ args: [`%${identifier}%`]
2981
+ });
2982
+ if (result.rows.length === 1) return result.rows[0];
2983
+ if (result.rows.length > 1) {
2984
+ const active = result.rows.filter(
2985
+ (r) => !["done", "cancelled"].includes(String(r.status))
2986
+ );
2987
+ if (active.length === 1) return active[0];
2988
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
2989
+ throw new Error(
2990
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
2991
+ );
2883
2992
  }
2993
+ throw new Error(`Task not found: ${identifier}`);
2884
2994
  }
2885
- function notifyParentExe(sessionKey) {
2886
- const target = getDispatchedBy(sessionKey);
2887
- if (!target) {
2888
- process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
2889
- `);
2890
- return false;
2995
+ async function createTaskCore(input) {
2996
+ const client = getClient();
2997
+ const id = crypto4.randomUUID();
2998
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2999
+ const slug = slugify(input.title);
3000
+ const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
3001
+ let blockedById = null;
3002
+ const initialStatus = input.blockedBy ? "blocked" : "open";
3003
+ if (input.blockedBy) {
3004
+ const blocker = await resolveTask(client, input.blockedBy);
3005
+ blockedById = String(blocker.id);
2891
3006
  }
2892
- process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
2893
- `);
2894
- const result = sendIntercom(target);
2895
- if (result === "failed") {
2896
- const rootExe = resolveExeSession();
2897
- if (rootExe && rootExe !== target) {
2898
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
2899
- `);
2900
- const fallback = sendIntercom(rootExe);
2901
- return fallback !== "failed";
3007
+ let parentTaskId = null;
3008
+ let parentRef = input.parentTaskId;
3009
+ if (!parentRef) {
3010
+ const extracted = extractParentFromContext(input.context);
3011
+ if (extracted) {
3012
+ parentRef = extracted;
3013
+ process.stderr.write(
3014
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
3015
+ );
2902
3016
  }
2903
- return false;
2904
- }
2905
- return true;
2906
- }
2907
- function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2908
- if (employeeName === "exe") {
2909
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
2910
3017
  }
2911
- try {
2912
- assertEmployeeLimitSync();
2913
- } catch (err) {
2914
- if (err instanceof PlanLimitError) {
2915
- return { status: "failed", sessionName: "", error: err.message };
3018
+ if (parentRef) {
3019
+ try {
3020
+ const parent = await resolveTask(client, parentRef);
3021
+ parentTaskId = String(parent.id);
3022
+ } catch (err) {
3023
+ if (!input.parentTaskId) {
3024
+ throw new Error(
3025
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
3026
+ );
3027
+ }
3028
+ throw err;
2916
3029
  }
2917
3030
  }
2918
- if (/-exe\d*$/.test(employeeName)) {
2919
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
2920
- return {
2921
- status: "failed",
2922
- sessionName: "",
2923
- error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
2924
- };
3031
+ let warning;
3032
+ const dupCheck = await client.execute({
3033
+ sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
3034
+ args: [input.title, input.assignedTo]
3035
+ });
3036
+ if (dupCheck.rows.length > 0) {
3037
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
2925
3038
  }
2926
- let effectiveInstance = opts?.instance;
2927
- if (effectiveInstance === void 0 && opts?.autoInstance) {
2928
- const free = findFreeInstance(
2929
- employeeName,
2930
- exeSession,
2931
- opts.maxAutoInstances ?? 10
2932
- );
2933
- if (free === null) {
2934
- return {
2935
- status: "failed",
2936
- sessionName: employeeSessionName(employeeName, exeSession),
2937
- error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
2938
- };
3039
+ if (input.baseDir) {
3040
+ try {
3041
+ await mkdir4(path11.join(input.baseDir, "exe", "output"), { recursive: true });
3042
+ await mkdir4(path11.join(input.baseDir, "exe", "research"), { recursive: true });
3043
+ await ensureArchitectureDoc(input.baseDir, input.projectName);
3044
+ await ensureGitignoreExe(input.baseDir);
3045
+ } catch {
2939
3046
  }
2940
- effectiveInstance = free === 0 ? void 0 : free;
2941
3047
  }
2942
- const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
2943
- if (isEmployeeAlive(sessionName)) {
2944
- const result2 = sendIntercom(sessionName);
2945
- if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
2946
- return { status: "intercom_sent", sessionName };
2947
- }
2948
- if (result2 === "delivered") {
2949
- return { status: "intercom_unprocessed", sessionName };
2950
- }
2951
- return { status: "failed", sessionName, error: "intercom delivery failed" };
2952
- }
2953
- const spawnOpts = { ...opts, instance: effectiveInstance };
2954
- const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
2955
- if (result.error) {
2956
- return { status: "failed", sessionName, error: result.error };
3048
+ const complexity = input.complexity ?? "standard";
3049
+ let sessionScope = null;
3050
+ try {
3051
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3052
+ sessionScope = resolveExeSession2();
3053
+ } catch {
2957
3054
  }
2958
- return { status: "spawned", sessionName };
3055
+ await client.execute({
3056
+ 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)
3057
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3058
+ args: [
3059
+ id,
3060
+ input.title,
3061
+ input.assignedTo,
3062
+ input.assignedBy,
3063
+ input.projectName,
3064
+ input.priority,
3065
+ initialStatus,
3066
+ taskFile,
3067
+ blockedById,
3068
+ parentTaskId,
3069
+ input.reviewer ?? null,
3070
+ input.context,
3071
+ complexity,
3072
+ input.budgetTokens ?? null,
3073
+ input.budgetFallbackModel ?? null,
3074
+ 0,
3075
+ null,
3076
+ sessionScope,
3077
+ now,
3078
+ now
3079
+ ]
3080
+ });
3081
+ return {
3082
+ id,
3083
+ title: input.title,
3084
+ assignedTo: input.assignedTo,
3085
+ assignedBy: input.assignedBy,
3086
+ projectName: input.projectName,
3087
+ priority: input.priority,
3088
+ status: initialStatus,
3089
+ taskFile,
3090
+ createdAt: now,
3091
+ updatedAt: now,
3092
+ warning,
3093
+ budgetTokens: input.budgetTokens ?? null,
3094
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
3095
+ tokensUsed: 0,
3096
+ tokensWarnedAt: null
3097
+ };
2959
3098
  }
2960
- function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2961
- const transport = getTransport();
2962
- const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
2963
- const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
2964
- const logDir = path11.join(os6.homedir(), ".exe-os", "session-logs");
2965
- const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
2966
- if (!existsSync10(logDir)) {
2967
- mkdirSync6(logDir, { recursive: true });
3099
+ async function listTasks(input) {
3100
+ const client = getClient();
3101
+ const conditions = [];
3102
+ const args = [];
3103
+ if (input.assignedTo) {
3104
+ conditions.push("assigned_to = ?");
3105
+ args.push(input.assignedTo);
2968
3106
  }
2969
- transport.kill(sessionName);
2970
- let cleanupSuffix = "";
2971
- try {
2972
- const thisFile = fileURLToPath2(import.meta.url);
2973
- const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
2974
- if (existsSync10(cleanupScript)) {
2975
- cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
2976
- }
2977
- } catch {
3107
+ if (input.status) {
3108
+ conditions.push("status = ?");
3109
+ args.push(input.status);
3110
+ } else {
3111
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
3112
+ }
3113
+ if (input.projectName) {
3114
+ conditions.push("project_name = ?");
3115
+ args.push(input.projectName);
3116
+ }
3117
+ if (input.priority) {
3118
+ conditions.push("priority = ?");
3119
+ args.push(input.priority);
2978
3120
  }
2979
3121
  try {
2980
- const claudeJsonPath = path11.join(os6.homedir(), ".claude.json");
2981
- let claudeJson = {};
2982
- try {
2983
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
2984
- } catch {
3122
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3123
+ const session = resolveExeSession2();
3124
+ if (session) {
3125
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
3126
+ args.push(session);
2985
3127
  }
2986
- if (!claudeJson.projects) claudeJson.projects = {};
2987
- const projects = claudeJson.projects;
2988
- const trustDir = opts?.cwd ?? projectDir;
2989
- if (!projects[trustDir]) projects[trustDir] = {};
2990
- projects[trustDir].hasTrustDialogAccepted = true;
2991
- writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
2992
3128
  } catch {
2993
3129
  }
3130
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3131
+ const result = await client.execute({
3132
+ 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`,
3133
+ args
3134
+ });
3135
+ return result.rows.map((r) => ({
3136
+ id: String(r.id),
3137
+ title: String(r.title),
3138
+ assignedTo: String(r.assigned_to),
3139
+ assignedBy: String(r.assigned_by),
3140
+ projectName: String(r.project_name),
3141
+ priority: String(r.priority),
3142
+ status: String(r.status),
3143
+ taskFile: String(r.task_file),
3144
+ createdAt: String(r.created_at),
3145
+ updatedAt: String(r.updated_at),
3146
+ checkpointCount: Number(r.checkpoint_count ?? 0),
3147
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
3148
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
3149
+ tokensUsed: Number(r.tokens_used ?? 0),
3150
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
3151
+ }));
3152
+ }
3153
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
3154
+ if (!taskContext) return null;
3155
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
2994
3156
  try {
2995
- const settingsDir = path11.join(os6.homedir(), ".claude", "projects");
2996
- const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
2997
- const projSettingsDir = path11.join(settingsDir, normalizedKey);
2998
- const settingsPath = path11.join(projSettingsDir, "settings.json");
2999
- let settings = {};
3000
- try {
3001
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
3002
- } catch {
3003
- }
3004
- const perms = settings.permissions ?? {};
3005
- const allow = perms.allow ?? [];
3006
- const toolNames = [
3007
- "recall_my_memory",
3008
- "store_memory",
3009
- "create_task",
3010
- "update_task",
3011
- "list_tasks",
3012
- "get_task",
3013
- "ask_team_memory",
3014
- "store_behavior",
3015
- "get_identity",
3016
- "send_message"
3017
- ];
3018
- const requiredTools = expandDualPrefixTools(toolNames);
3019
- let changed = false;
3020
- for (const tool of requiredTools) {
3021
- if (!allow.includes(tool)) {
3022
- allow.push(tool);
3023
- changed = true;
3024
- }
3025
- }
3026
- if (changed) {
3027
- perms.allow = allow;
3028
- settings.permissions = perms;
3029
- mkdirSync6(projSettingsDir, { recursive: true });
3030
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3157
+ const since = new Date(taskCreatedAt).toISOString();
3158
+ const branch = execSync6(
3159
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
3160
+ { encoding: "utf8", timeout: 3e3 }
3161
+ ).trim();
3162
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
3163
+ const commitCount = execSync6(
3164
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
3165
+ { encoding: "utf8", timeout: 5e3 }
3166
+ ).trim();
3167
+ const count = parseInt(commitCount, 10);
3168
+ if (count === 0) {
3169
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
3031
3170
  }
3171
+ return null;
3032
3172
  } catch {
3173
+ return null;
3033
3174
  }
3034
- const spawnCwd = opts?.cwd ?? projectDir;
3035
- const useExeAgent = !!(opts?.model && opts?.provider);
3036
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
3037
- const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
3038
- let identityFlag = "";
3039
- let behaviorsFlag = "";
3040
- let legacyFallbackWarned = false;
3041
- if (!useExeAgent && !useBinSymlink) {
3042
- const identityPath = path11.join(
3043
- os6.homedir(),
3044
- ".exe-os",
3045
- "identity",
3046
- `${employeeName}.md`
3047
- );
3048
- _resetCcAgentSupportCache();
3049
- const hasAgentFlag = claudeSupportsAgentFlag();
3050
- if (hasAgentFlag) {
3051
- identityFlag = ` --agent ${employeeName}`;
3052
- } else if (existsSync10(identityPath)) {
3053
- identityFlag = ` --append-system-prompt-file ${identityPath}`;
3054
- legacyFallbackWarned = true;
3055
- }
3056
- const behaviorsFile = exportBehaviorsSync(
3057
- employeeName,
3058
- path11.basename(spawnCwd),
3059
- sessionName
3060
- );
3061
- if (behaviorsFile) {
3062
- behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
3063
- }
3064
- }
3065
- if (legacyFallbackWarned) {
3175
+ }
3176
+ async function updateTaskStatus(input) {
3177
+ const client = getClient();
3178
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3179
+ const row = await resolveTask(client, input.taskId);
3180
+ const taskId = String(row.id);
3181
+ const taskFile = String(row.task_file);
3182
+ if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
3066
3183
  process.stderr.write(
3067
- `[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.
3184
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
3068
3185
  `
3069
3186
  );
3070
3187
  }
3071
- let sessionContextFlag = "";
3072
- try {
3073
- const ctxDir = path11.join(os6.homedir(), ".exe-os", "session-cache");
3074
- mkdirSync6(ctxDir, { recursive: true });
3075
- const ctxFile = path11.join(ctxDir, `session-context-${sessionName}.md`);
3076
- const ctxContent = [
3077
- `## Session Context`,
3078
- `You are running in tmux session: ${sessionName}.`,
3079
- `Your parent exe session is ${exeSession}.`,
3080
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
3081
- ].join("\n");
3082
- writeFileSync5(ctxFile, ctxContent);
3083
- sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
3084
- } catch {
3085
- }
3086
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
3087
- if (ccProvider !== DEFAULT_PROVIDER) {
3088
- const cfg = PROVIDER_TABLE[ccProvider];
3089
- if (cfg?.apiKeyEnv) {
3090
- const keyVal = process.env[cfg.apiKeyEnv];
3091
- if (keyVal) {
3092
- envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
3188
+ if (input.status === "done") {
3189
+ const existingRow = await client.execute({
3190
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
3191
+ args: [taskId]
3192
+ });
3193
+ if (existingRow.rows.length > 0) {
3194
+ const ctx = existingRow.rows[0];
3195
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
3196
+ if (warning) {
3197
+ input.result = input.result ? `\u26A0\uFE0F ${warning}
3198
+
3199
+ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3200
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
3201
+ `);
3093
3202
  }
3094
3203
  }
3095
3204
  }
3096
- let spawnCommand;
3097
- if (useExeAgent) {
3098
- spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
3099
- } else if (useBinSymlink) {
3100
- const binName = `${employeeName}-${ccProvider}`;
3101
- process.stderr.write(
3102
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
3103
- `
3104
- );
3105
- spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
3106
- } else {
3107
- spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
3108
- }
3109
- const spawnResult = transport.spawn(sessionName, {
3110
- cwd: spawnCwd,
3111
- command: spawnCommand
3112
- });
3113
- if (spawnResult.error) {
3114
- releaseSpawnLock(sessionName);
3115
- return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3116
- }
3117
- transport.pipeLog(sessionName, logFile);
3118
- try {
3119
- const mySession = getMySession();
3120
- const dispatchInfo = path11.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
3121
- writeFileSync5(dispatchInfo, JSON.stringify({
3122
- dispatchedBy: mySession,
3123
- rootExe: exeSession,
3124
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
3125
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
3126
- }));
3127
- } catch {
3128
- }
3129
- let booted = false;
3130
- for (let i = 0; i < 30; i++) {
3131
- try {
3132
- execSync6("sleep 0.5");
3133
- } catch {
3205
+ if (input.status === "in_progress") {
3206
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
3207
+ const claim = await client.execute({
3208
+ sql: `UPDATE tasks
3209
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
3210
+ WHERE id = ? AND status = 'open'`,
3211
+ args: [tmuxSession, now, taskId]
3212
+ });
3213
+ if (claim.rowsAffected === 0) {
3214
+ const current = await client.execute({
3215
+ sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
3216
+ args: [taskId]
3217
+ });
3218
+ const cur = current.rows[0];
3219
+ const status = cur?.status ?? "unknown";
3220
+ const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
3221
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
3134
3222
  }
3135
3223
  try {
3136
- const pane = transport.capturePane(sessionName);
3137
- if (useExeAgent) {
3138
- if (pane.includes("[exe-agent]") || pane.includes("online")) {
3139
- booted = true;
3140
- break;
3141
- }
3142
- } else {
3143
- if (pane.includes("Claude Code") || pane.includes("\u276F")) {
3144
- booted = true;
3145
- break;
3146
- }
3147
- }
3224
+ await writeCheckpoint({
3225
+ taskId,
3226
+ step: "claimed",
3227
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3228
+ });
3148
3229
  } catch {
3149
3230
  }
3231
+ return { row, taskFile, now, taskId };
3150
3232
  }
3151
- if (!booted) {
3152
- releaseSpawnLock(sessionName);
3153
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3233
+ if (input.result) {
3234
+ await client.execute({
3235
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
3236
+ args: [input.status, input.result, now, taskId]
3237
+ });
3238
+ } else {
3239
+ await client.execute({
3240
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
3241
+ args: [input.status, now, taskId]
3242
+ });
3154
3243
  }
3155
- if (!useExeAgent) {
3156
- try {
3157
- transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
3158
- } catch {
3159
- }
3244
+ try {
3245
+ await writeCheckpoint({
3246
+ taskId,
3247
+ step: `status_transition:${input.status}`,
3248
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
3249
+ });
3250
+ } catch {
3160
3251
  }
3161
- registerSession({
3162
- windowName: sessionName,
3163
- agentId: employeeName,
3164
- projectDir: spawnCwd,
3165
- parentExe: exeSession,
3166
- pid: 0,
3167
- registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3168
- });
3169
- releaseSpawnLock(sessionName);
3170
- return { sessionName };
3252
+ return { row, taskFile, now, taskId };
3171
3253
  }
3172
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3173
- var init_tmux_routing = __esm({
3174
- "src/lib/tmux-routing.ts"() {
3175
- "use strict";
3176
- init_session_registry();
3177
- init_session_key();
3178
- init_transport();
3179
- init_cc_agent_support();
3180
- init_mcp_prefix();
3181
- init_provider_table();
3182
- init_intercom_queue();
3183
- init_plan_limits();
3184
- SPAWN_LOCK_DIR = path11.join(os6.homedir(), ".exe-os", "spawn-locks");
3185
- SESSION_CACHE = path11.join(os6.homedir(), ".exe-os", "session-cache");
3186
- BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3187
- INTERCOM_DEBOUNCE_MS = 3e4;
3188
- INTERCOM_LOG2 = path11.join(os6.homedir(), ".exe-os", "intercom.log");
3189
- DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
3190
- DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3191
- BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3192
- }
3193
- });
3194
-
3195
- // src/lib/task-scanner.ts
3196
- var task_scanner_exports = {};
3197
- __export(task_scanner_exports, {
3198
- PRIORITY_RE: () => PRIORITY_RE,
3199
- STATUS_RE: () => STATUS_RE,
3200
- TITLE_RE: () => TITLE_RE,
3201
- formatJson: () => formatJson,
3202
- formatMandatory: () => formatMandatory,
3203
- formatText: () => formatText,
3204
- scanAgentTasks: () => scanAgentTasks
3205
- });
3206
- import { readdirSync as readdirSync4, readFileSync as readFileSync10, existsSync as existsSync11, statSync } from "fs";
3207
- import { execSync as execSync7 } from "child_process";
3208
- import path12 from "path";
3209
- function getProjectRoot() {
3254
+ async function deleteTaskCore(taskId, _baseDir) {
3255
+ const client = getClient();
3256
+ const row = await resolveTask(client, taskId);
3257
+ const id = String(row.id);
3258
+ const taskFile = String(row.task_file);
3259
+ const assignedTo = String(row.assigned_to);
3260
+ const assignedBy = String(row.assigned_by);
3261
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
3262
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
3263
+ return { taskFile, assignedTo, assignedBy, taskSlug };
3264
+ }
3265
+ async function ensureArchitectureDoc(baseDir, projectName) {
3266
+ const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
3210
3267
  try {
3211
- return execSync7("git rev-parse --show-toplevel", {
3212
- encoding: "utf8",
3213
- stdio: ["pipe", "pipe", "pipe"],
3214
- timeout: 5e3
3215
- }).trim();
3268
+ if (existsSync10(archPath)) return;
3269
+ const template = [
3270
+ `# ${projectName} \u2014 System Architecture`,
3271
+ "",
3272
+ "> Employees: read this before every task. Update it when you change system structure.",
3273
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
3274
+ "",
3275
+ "## Overview",
3276
+ "",
3277
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
3278
+ "",
3279
+ "## Key Components",
3280
+ "",
3281
+ "<!-- List the major modules, services, or subsystems. -->",
3282
+ "",
3283
+ "## Data Flow",
3284
+ "",
3285
+ "<!-- How does data move through the system? What writes where? -->",
3286
+ "",
3287
+ "## Invariants",
3288
+ "",
3289
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
3290
+ "",
3291
+ "## Dependencies",
3292
+ "",
3293
+ "<!-- What depends on what? If I change X, what else is affected? -->",
3294
+ ""
3295
+ ].join("\n");
3296
+ await writeFile4(archPath, template, "utf-8");
3216
3297
  } catch {
3217
- return process.cwd();
3218
3298
  }
3219
3299
  }
3220
- function scanAgentTasks(agentId) {
3221
- const taskDir = path12.join(getProjectRoot(), "exe", agentId);
3222
- const open = [];
3223
- const inProgress = [];
3224
- let done = 0;
3225
- let total = 0;
3226
- if (!existsSync11(taskDir)) return { open, inProgress, done, total };
3300
+ async function ensureGitignoreExe(baseDir) {
3301
+ const gitignorePath = path11.join(baseDir, ".gitignore");
3227
3302
  try {
3228
- const files = readdirSync4(taskDir).filter((f) => f.endsWith(".md"));
3229
- total = files.length;
3230
- for (const f of files) {
3231
- try {
3232
- const content = readFileSync10(path12.join(taskDir, f), "utf8");
3233
- const statusMatch = content.match(STATUS_RE);
3234
- const status = statusMatch ? statusMatch[1].toLowerCase() : null;
3235
- if (status === "done") {
3236
- done++;
3237
- continue;
3238
- }
3239
- if (status !== "open" && status !== "in_progress") continue;
3240
- const priMatch = content.match(PRIORITY_RE);
3241
- const titleMatch = content.match(TITLE_RE);
3242
- const task = {
3243
- file: f,
3244
- title: titleMatch ? titleMatch[1] : f.replace(".md", ""),
3245
- priority: priMatch ? priMatch[1] : "P2",
3246
- status,
3247
- slug: f.replace(".md", "")
3248
- };
3249
- if (status === "in_progress") {
3250
- inProgress.push(task);
3251
- } else {
3252
- open.push(task);
3253
- }
3254
- } catch {
3255
- }
3303
+ if (existsSync10(gitignorePath)) {
3304
+ const content = readFileSync9(gitignorePath, "utf-8");
3305
+ if (/^\/?exe\/?$/m.test(content)) return;
3306
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3307
+ } else {
3308
+ await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
3256
3309
  }
3257
3310
  } catch {
3258
3311
  }
3259
- open.sort((a, b) => a.priority.localeCompare(b.priority));
3260
- inProgress.sort((a, b) => a.priority.localeCompare(b.priority));
3261
- return { open, inProgress, done, total };
3262
3312
  }
3263
- function formatText(agentId, result) {
3264
- const lines = [];
3265
- if (result.inProgress.length > 0) {
3266
- lines.push(`IN_PROGRESS (${result.inProgress.length}):`);
3267
- for (const t of result.inProgress) {
3268
- lines.push(` [${t.priority}] ${t.title} \u2014 exe/${agentId}/${t.file}`);
3269
- }
3270
- }
3271
- if (result.open.length > 0) {
3272
- lines.push(`OPEN (${result.open.length}):`);
3273
- for (const t of result.open) {
3274
- lines.push(` [${t.priority}] ${t.title} \u2014 exe/${agentId}/${t.file}`);
3275
- }
3313
+ var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3314
+ var init_tasks_crud = __esm({
3315
+ "src/lib/tasks-crud.ts"() {
3316
+ "use strict";
3317
+ init_database();
3318
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
3319
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3276
3320
  }
3277
- lines.push(`DONE: ${result.done}`);
3278
- return lines.join("\n");
3321
+ });
3322
+
3323
+ // src/lib/tasks-review.ts
3324
+ import path12 from "path";
3325
+ import { existsSync as existsSync11, readdirSync as readdirSync4, unlinkSync as unlinkSync3 } from "fs";
3326
+ async function countPendingReviews() {
3327
+ const client = getClient();
3328
+ const result = await client.execute({
3329
+ sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3330
+ args: []
3331
+ });
3332
+ return Number(result.rows[0]?.cnt) || 0;
3279
3333
  }
3280
- function formatMandatory(agentId, result) {
3281
- const { open, inProgress } = result;
3282
- if (open.length === 0 && inProgress.length === 0) return "";
3283
- const lines = [];
3284
- if (inProgress.length > 0) {
3285
- const current = inProgress[0];
3286
- let stale = false;
3287
- try {
3288
- const stat = statSync(path12.join(getProjectRoot(), "exe", agentId, current.file));
3289
- const ageMin = (Date.now() - stat.mtimeMs) / 6e4;
3290
- if (ageMin > 30) stale = true;
3291
- } catch {
3292
- }
3293
- if (stale) {
3294
- lines.push(`MANDATORY: Update task status for: ${current.title} [${current.priority}] (exe/${agentId}/${current.file})`);
3295
- lines.push("This task has been in_progress for over 30 minutes without updates.");
3296
- lines.push("If work is done, mark done. If blocked, update status to blocked.");
3297
- } else {
3298
- lines.push(`Continue working on: ${current.title} [${current.priority}] (exe/${agentId}/${current.file})`);
3299
- }
3300
- if (open.length > 0) {
3301
- lines.push("Queued: " + open.map((t) => `${t.title} [${t.priority}]`).join(", "));
3302
- }
3303
- } else {
3304
- const top = open[0];
3305
- lines.push(`MANDATORY: You have ${open.length} unstarted task(s).`);
3306
- lines.push(`Highest priority: ${top.title} [${top.priority}]`);
3307
- lines.push(`File: exe/${agentId}/${top.file}`);
3308
- lines.push("Read this task file and START WORKING NOW.");
3334
+ async function countNewPendingReviewsSince(sinceIso) {
3335
+ const client = getClient();
3336
+ const result = await client.execute({
3337
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3338
+ WHERE status = 'needs_review' AND updated_at > ?`,
3339
+ args: [sinceIso]
3340
+ });
3341
+ return Number(result.rows[0]?.cnt) || 0;
3342
+ }
3343
+ async function listPendingReviews(limit) {
3344
+ const client = getClient();
3345
+ const result = await client.execute({
3346
+ sql: `SELECT title, assigned_to, project_name FROM tasks
3347
+ WHERE status = 'needs_review'
3348
+ ORDER BY priority ASC, created_at DESC LIMIT ?`,
3349
+ args: [limit]
3350
+ });
3351
+ return result.rows;
3352
+ }
3353
+ async function cleanupOrphanedReviews() {
3354
+ const client = getClient();
3355
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3356
+ const r1 = await client.execute({
3357
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3358
+ WHERE status = 'needs_review'
3359
+ AND assigned_by = 'system'
3360
+ AND title LIKE 'Review:%'
3361
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3362
+ args: [now]
3363
+ });
3364
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
3365
+ const r2 = await client.execute({
3366
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3367
+ WHERE status = 'needs_review'
3368
+ AND result IS NOT NULL
3369
+ AND updated_at < ?`,
3370
+ args: [now, staleThreshold]
3371
+ });
3372
+ const total = r1.rowsAffected + r2.rowsAffected;
3373
+ if (total > 0) {
3374
+ process.stderr.write(
3375
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
3376
+ `
3377
+ );
3309
3378
  }
3310
- return lines.join("\n");
3379
+ return total;
3311
3380
  }
3312
- function formatJson(result) {
3313
- return JSON.stringify({
3314
- open: result.open.map((t) => ({ file: t.file, title: t.title, priority: t.priority })),
3315
- in_progress: result.inProgress.map((t) => ({ file: t.file, title: t.title, priority: t.priority })),
3316
- done: result.done,
3317
- total: result.total
3381
+ function getReviewChecklist(role, agent, taskSlug) {
3382
+ const roleLower = role.toLowerCase();
3383
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
3384
+ return {
3385
+ lens: "Code Quality (Engineer)",
3386
+ checklist: [
3387
+ "1. Do all tests pass? Any new tests needed?",
3388
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
3389
+ "3. Does it follow existing patterns and conventions in the codebase?",
3390
+ "4. Any regressions in the test suite?"
3391
+ ]
3392
+ };
3393
+ }
3394
+ if (roleLower === "cto" || roleLower.includes("architect")) {
3395
+ return {
3396
+ lens: "Architecture (CTO)",
3397
+ checklist: [
3398
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
3399
+ "2. Is it backward compatible? Any breaking changes?",
3400
+ "3. Does it introduce technical debt? Is that debt justified?",
3401
+ "4. Security implications? Any new attack surface?",
3402
+ "5. Does it scale? Performance considerations?",
3403
+ "6. Coordination: does this affect other employees' work or other projects?"
3404
+ ]
3405
+ };
3406
+ }
3407
+ if (roleLower === "coo" || roleLower.includes("operations")) {
3408
+ return {
3409
+ lens: "Strategic (COO)",
3410
+ checklist: [
3411
+ "1. Does this serve the project mission?",
3412
+ "2. Is this the right work at the right time?",
3413
+ "3. Does the architectural assessment make sense for the business?",
3414
+ "4. Any cross-project implications?"
3415
+ ]
3416
+ };
3417
+ }
3418
+ return {
3419
+ lens: "General",
3420
+ checklist: [
3421
+ "1. Read the original task's acceptance criteria",
3422
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
3423
+ "3. Verify code changes match requirements",
3424
+ "4. Check if tests were added/updated",
3425
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
3426
+ ]
3427
+ };
3428
+ }
3429
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
3430
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3431
+ try {
3432
+ const client = getClient();
3433
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3434
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3435
+ if (parentId) {
3436
+ const result = await client.execute({
3437
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3438
+ args: [now, parentId]
3439
+ });
3440
+ if (result.rowsAffected > 0) {
3441
+ process.stderr.write(
3442
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3443
+ `
3444
+ );
3445
+ }
3446
+ } else {
3447
+ const fileName = taskFile.split("/").pop() ?? "";
3448
+ const reviewPrefix = fileName.replace(".md", "");
3449
+ const parts = reviewPrefix.split("-");
3450
+ if (parts.length >= 3 && parts[0] === "review") {
3451
+ const agent = parts[1];
3452
+ const slug = parts.slice(2).join("-");
3453
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
3454
+ const result = await client.execute({
3455
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
3456
+ args: [now, originalTaskFile]
3457
+ });
3458
+ if (result.rowsAffected > 0) {
3459
+ process.stderr.write(
3460
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
3461
+ `
3462
+ );
3463
+ }
3464
+ }
3465
+ }
3466
+ } catch (err) {
3467
+ process.stderr.write(
3468
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
3469
+ `
3470
+ );
3471
+ }
3472
+ try {
3473
+ const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3474
+ if (existsSync11(cacheDir)) {
3475
+ for (const f of readdirSync4(cacheDir)) {
3476
+ if (f.startsWith("review-notified-")) {
3477
+ unlinkSync3(path12.join(cacheDir, f));
3478
+ }
3479
+ }
3480
+ }
3481
+ } catch {
3482
+ }
3483
+ }
3484
+ var init_tasks_review = __esm({
3485
+ "src/lib/tasks-review.ts"() {
3486
+ "use strict";
3487
+ init_database();
3488
+ init_config();
3489
+ init_employees();
3490
+ init_notifications();
3491
+ init_tasks_crud();
3492
+ init_tmux_routing();
3493
+ init_session_key();
3494
+ init_state_bus();
3495
+ }
3496
+ });
3497
+
3498
+ // src/lib/tasks-chain.ts
3499
+ import path13 from "path";
3500
+ import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
3501
+ async function cascadeUnblock(taskId, baseDir, now) {
3502
+ const client = getClient();
3503
+ const unblocked = await client.execute({
3504
+ sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
3505
+ WHERE blocked_by = ? AND status = 'blocked'`,
3506
+ args: [now, taskId]
3318
3507
  });
3508
+ if (baseDir && unblocked.rowsAffected > 0) {
3509
+ const unblockedRows = await client.execute({
3510
+ sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
3511
+ args: [now]
3512
+ });
3513
+ for (const ur of unblockedRows.rows) {
3514
+ try {
3515
+ const ubFile = path13.join(baseDir, String(ur.task_file));
3516
+ let ubContent = await readFile4(ubFile, "utf-8");
3517
+ ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3518
+ ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
3519
+ await writeFile5(ubFile, ubContent, "utf-8");
3520
+ } catch {
3521
+ }
3522
+ }
3523
+ }
3319
3524
  }
3320
- var STATUS_RE, PRIORITY_RE, TITLE_RE;
3321
- var init_task_scanner = __esm({
3322
- "src/lib/task-scanner.ts"() {
3525
+ async function findNextTask(assignedTo) {
3526
+ const client = getClient();
3527
+ const nextResult = await client.execute({
3528
+ sql: `SELECT title, task_file, priority FROM tasks
3529
+ WHERE assigned_to = ? AND status = 'open'
3530
+ ORDER BY priority ASC, created_at ASC
3531
+ LIMIT 1`,
3532
+ args: [assignedTo]
3533
+ });
3534
+ if (nextResult.rows.length === 1) {
3535
+ const nr = nextResult.rows[0];
3536
+ return {
3537
+ title: String(nr.title),
3538
+ priority: String(nr.priority),
3539
+ taskFile: String(nr.task_file)
3540
+ };
3541
+ }
3542
+ return void 0;
3543
+ }
3544
+ async function checkSubtaskCompletion(parentTaskId, projectName) {
3545
+ const client = getClient();
3546
+ const remaining = await client.execute({
3547
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3548
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
3549
+ args: [parentTaskId]
3550
+ });
3551
+ const cnt = Number(remaining.rows[0]?.cnt ?? 1);
3552
+ if (cnt === 0) {
3553
+ const parentRow = await client.execute({
3554
+ sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
3555
+ args: [parentTaskId]
3556
+ });
3557
+ if (parentRow.rows.length === 1) {
3558
+ const pr = parentRow.rows[0];
3559
+ const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
3560
+ await writeNotification({
3561
+ agentId: String(pr.assigned_to),
3562
+ agentRole: "system",
3563
+ event: "subtasks_complete",
3564
+ project: parentProject,
3565
+ summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
3566
+ taskFile: String(pr.task_file)
3567
+ });
3568
+ }
3569
+ }
3570
+ }
3571
+ var init_tasks_chain = __esm({
3572
+ "src/lib/tasks-chain.ts"() {
3323
3573
  "use strict";
3324
- STATUS_RE = /^\*\*Status:\*\*\s*(\w+)/m;
3325
- PRIORITY_RE = /^\*\*Priority:\*\*\s*(\w+)/m;
3326
- TITLE_RE = /^# (.+)/m;
3574
+ init_database();
3575
+ init_notifications();
3327
3576
  }
3328
3577
  });
3329
3578
 
@@ -3333,34 +3582,34 @@ __export(project_name_exports, {
3333
3582
  _resetCache: () => _resetCache,
3334
3583
  getProjectName: () => getProjectName
3335
3584
  });
3336
- import { execSync as execSync8 } from "child_process";
3337
- import path13 from "path";
3585
+ import { execSync as execSync7 } from "child_process";
3586
+ import path14 from "path";
3338
3587
  function getProjectName(cwd) {
3339
3588
  const dir = cwd ?? process.cwd();
3340
3589
  if (_cached2 && _cachedCwd === dir) return _cached2;
3341
3590
  try {
3342
3591
  let repoRoot;
3343
3592
  try {
3344
- const gitCommonDir = execSync8("git rev-parse --path-format=absolute --git-common-dir", {
3593
+ const gitCommonDir = execSync7("git rev-parse --path-format=absolute --git-common-dir", {
3345
3594
  cwd: dir,
3346
3595
  encoding: "utf8",
3347
3596
  timeout: 2e3,
3348
3597
  stdio: ["pipe", "pipe", "pipe"]
3349
3598
  }).trim();
3350
- repoRoot = path13.dirname(gitCommonDir);
3599
+ repoRoot = path14.dirname(gitCommonDir);
3351
3600
  } catch {
3352
- repoRoot = execSync8("git rev-parse --show-toplevel", {
3601
+ repoRoot = execSync7("git rev-parse --show-toplevel", {
3353
3602
  cwd: dir,
3354
3603
  encoding: "utf8",
3355
3604
  timeout: 2e3,
3356
3605
  stdio: ["pipe", "pipe", "pipe"]
3357
3606
  }).trim();
3358
3607
  }
3359
- _cached2 = path13.basename(repoRoot);
3608
+ _cached2 = path14.basename(repoRoot);
3360
3609
  _cachedCwd = dir;
3361
3610
  return _cached2;
3362
3611
  } catch {
3363
- _cached2 = path13.basename(dir);
3612
+ _cached2 = path14.basename(dir);
3364
3613
  _cachedCwd = dir;
3365
3614
  return _cached2;
3366
3615
  }
@@ -3378,1329 +3627,1685 @@ var init_project_name = __esm({
3378
3627
  }
3379
3628
  });
3380
3629
 
3381
- // src/lib/tasks-crud.ts
3382
- import crypto3 from "crypto";
3383
- import path14 from "path";
3384
- import { execSync as execSync9 } from "child_process";
3385
- import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3386
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3387
- async function writeCheckpoint(input) {
3388
- const client = getClient();
3389
- const row = await resolveTask(client, input.taskId);
3390
- const taskId = String(row.id);
3391
- const now = (/* @__PURE__ */ new Date()).toISOString();
3392
- const blockedByIds = [];
3393
- if (row.blocked_by) {
3394
- blockedByIds.push(String(row.blocked_by));
3395
- }
3396
- const checkpoint = {
3397
- step: input.step,
3398
- context_summary: input.contextSummary,
3399
- files_touched: input.filesTouched ?? [],
3400
- blocked_by_ids: blockedByIds,
3401
- last_checkpoint_at: now
3402
- };
3403
- const result = await client.execute({
3404
- sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
3405
- args: [JSON.stringify(checkpoint), now, taskId]
3406
- });
3407
- if (result.rowsAffected === 0) {
3408
- throw new Error(`Checkpoint write failed: task ${taskId} not found`);
3409
- }
3410
- const countResult = await client.execute({
3411
- sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
3412
- args: [taskId]
3413
- });
3414
- const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
3415
- return { checkpointCount };
3416
- }
3417
- function extractParentFromContext(contextBody) {
3418
- if (!contextBody) return null;
3419
- const match = contextBody.match(
3420
- /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
3421
- );
3422
- return match ? match[1].toLowerCase() : null;
3423
- }
3424
- function slugify(title) {
3425
- return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3630
+ // src/lib/session-scope.ts
3631
+ var session_scope_exports = {};
3632
+ __export(session_scope_exports, {
3633
+ assertSessionScope: () => assertSessionScope,
3634
+ findSessionForProject: () => findSessionForProject,
3635
+ getSessionProject: () => getSessionProject
3636
+ });
3637
+ function getSessionProject(sessionName) {
3638
+ const sessions = listSessions();
3639
+ const entry = sessions.find((s) => s.windowName === sessionName);
3640
+ if (!entry) return null;
3641
+ const parts = entry.projectDir.split("/").filter(Boolean);
3642
+ return parts[parts.length - 1] ?? null;
3426
3643
  }
3427
- async function resolveTask(client, identifier) {
3428
- let result = await client.execute({
3429
- sql: "SELECT * FROM tasks WHERE id = ?",
3430
- args: [identifier]
3431
- });
3432
- if (result.rows.length === 1) return result.rows[0];
3433
- result = await client.execute({
3434
- sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
3435
- args: [`%${identifier}%`]
3436
- });
3437
- if (result.rows.length === 1) return result.rows[0];
3438
- if (result.rows.length > 1) {
3439
- const exact = result.rows.filter(
3440
- (r) => String(r.task_file).endsWith(`/${identifier}.md`)
3441
- );
3442
- if (exact.length === 1) return exact[0];
3443
- const candidates = exact.length > 1 ? exact : result.rows;
3444
- const active = candidates.filter(
3445
- (r) => !["done", "cancelled"].includes(String(r.status))
3446
- );
3447
- if (active.length === 1) return active[0];
3448
- const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
3449
- throw new Error(
3450
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3451
- );
3644
+ function findSessionForProject(projectName) {
3645
+ const sessions = listSessions();
3646
+ for (const s of sessions) {
3647
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
3648
+ if (proj === projectName && s.agentId === "exe") return s;
3452
3649
  }
3453
- result = await client.execute({
3454
- sql: "SELECT * FROM tasks WHERE title LIKE ?",
3455
- args: [`%${identifier}%`]
3456
- });
3457
- if (result.rows.length === 1) return result.rows[0];
3458
- if (result.rows.length > 1) {
3459
- const active = result.rows.filter(
3460
- (r) => !["done", "cancelled"].includes(String(r.status))
3461
- );
3462
- if (active.length === 1) return active[0];
3463
- const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
3464
- throw new Error(
3465
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3650
+ return null;
3651
+ }
3652
+ function assertSessionScope(actionType, targetProject) {
3653
+ try {
3654
+ const currentProject = getProjectName();
3655
+ const exeSession = resolveExeSession();
3656
+ if (!exeSession) {
3657
+ return { allowed: true, reason: "no_session" };
3658
+ }
3659
+ if (currentProject === targetProject) {
3660
+ return {
3661
+ allowed: true,
3662
+ reason: "same_session",
3663
+ currentProject,
3664
+ targetProject
3665
+ };
3666
+ }
3667
+ process.stderr.write(
3668
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3669
+ `
3466
3670
  );
3671
+ return {
3672
+ allowed: false,
3673
+ reason: "cross_session_denied",
3674
+ currentProject,
3675
+ targetProject,
3676
+ targetSession: findSessionForProject(targetProject)?.windowName
3677
+ };
3678
+ } catch {
3679
+ return { allowed: true, reason: "no_session" };
3467
3680
  }
3468
- throw new Error(`Task not found: ${identifier}`);
3469
3681
  }
3470
- async function createTaskCore(input) {
3471
- const client = getClient();
3472
- const id = crypto3.randomUUID();
3473
- const now = (/* @__PURE__ */ new Date()).toISOString();
3474
- const slug = slugify(input.title);
3475
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
3476
- let blockedById = null;
3477
- const initialStatus = input.blockedBy ? "blocked" : "open";
3478
- if (input.blockedBy) {
3479
- const blocker = await resolveTask(client, input.blockedBy);
3480
- blockedById = String(blocker.id);
3682
+ var init_session_scope = __esm({
3683
+ "src/lib/session-scope.ts"() {
3684
+ "use strict";
3685
+ init_session_registry();
3686
+ init_project_name();
3687
+ init_tmux_routing();
3481
3688
  }
3482
- let parentTaskId = null;
3483
- let parentRef = input.parentTaskId;
3484
- if (!parentRef) {
3485
- const extracted = extractParentFromContext(input.context);
3486
- if (extracted) {
3487
- parentRef = extracted;
3488
- process.stderr.write(
3489
- "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
3490
- );
3689
+ });
3690
+
3691
+ // src/lib/tasks-notify.ts
3692
+ async function dispatchTaskToEmployee(input) {
3693
+ if (input.assignedTo === "exe") return { dispatched: "skipped" };
3694
+ let crossProject = false;
3695
+ if (input.projectName) {
3696
+ try {
3697
+ const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
3698
+ const check = assertSessionScope2("dispatch_task", input.projectName);
3699
+ if (check.reason === "cross_session_denied") {
3700
+ crossProject = true;
3701
+ return { dispatched: "skipped", crossProject: true };
3702
+ }
3703
+ } catch {
3491
3704
  }
3492
3705
  }
3493
- if (parentRef) {
3494
- try {
3495
- const parent = await resolveTask(client, parentRef);
3496
- parentTaskId = String(parent.id);
3497
- } catch (err) {
3498
- if (!input.parentTaskId) {
3499
- throw new Error(
3500
- `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
3706
+ try {
3707
+ const transport = getTransport();
3708
+ const exeSession = resolveExeSession();
3709
+ if (!exeSession) return { dispatched: "session_missing" };
3710
+ const sessionName = employeeSessionName(input.assignedTo, exeSession);
3711
+ if (transport.isAlive(sessionName)) {
3712
+ const result = sendIntercom(sessionName);
3713
+ const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
3714
+ return { dispatched, session: sessionName, crossProject };
3715
+ } else {
3716
+ const projectDir = input.projectDir ?? process.cwd();
3717
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
3718
+ autoInstance: isMultiInstance(input.assignedTo)
3719
+ });
3720
+ if (result.status === "failed") {
3721
+ process.stderr.write(
3722
+ `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
3723
+ `
3501
3724
  );
3725
+ return { dispatched: "session_missing" };
3502
3726
  }
3503
- throw err;
3727
+ return { dispatched: "spawned", session: result.sessionName, crossProject };
3504
3728
  }
3729
+ } catch {
3730
+ return { dispatched: "session_missing" };
3505
3731
  }
3506
- let warning;
3507
- const dupCheck = await client.execute({
3508
- sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
3509
- args: [input.title, input.assignedTo]
3510
- });
3511
- if (dupCheck.rows.length > 0) {
3512
- warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
3732
+ }
3733
+ function notifyTaskDone() {
3734
+ try {
3735
+ const key = getSessionKey();
3736
+ if (key && !process.env.VITEST) notifyParentExe(key);
3737
+ } catch {
3513
3738
  }
3514
- if (input.baseDir) {
3515
- try {
3516
- await mkdir4(path14.join(input.baseDir, "exe", "output"), { recursive: true });
3517
- await mkdir4(path14.join(input.baseDir, "exe", "research"), { recursive: true });
3518
- await ensureArchitectureDoc(input.baseDir, input.projectName);
3519
- await ensureGitignoreExe(input.baseDir);
3520
- } catch {
3521
- }
3739
+ }
3740
+ async function markTaskNotificationsRead(taskFile) {
3741
+ try {
3742
+ await markAsReadByTaskFile(taskFile);
3743
+ } catch {
3522
3744
  }
3523
- const complexity = input.complexity ?? "standard";
3524
- await client.execute({
3525
- 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)
3526
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3527
- args: [
3528
- id,
3529
- input.title,
3530
- input.assignedTo,
3531
- input.assignedBy,
3532
- input.projectName,
3533
- input.priority,
3534
- initialStatus,
3535
- taskFile,
3536
- blockedById,
3537
- parentTaskId,
3538
- input.reviewer ?? null,
3539
- input.context,
3540
- complexity,
3541
- input.budgetTokens ?? null,
3542
- input.budgetFallbackModel ?? null,
3543
- 0,
3544
- null,
3545
- now,
3546
- now
3547
- ]
3548
- });
3549
- return {
3550
- id,
3551
- title: input.title,
3552
- assignedTo: input.assignedTo,
3553
- assignedBy: input.assignedBy,
3554
- projectName: input.projectName,
3555
- priority: input.priority,
3556
- status: initialStatus,
3557
- taskFile,
3558
- createdAt: now,
3559
- updatedAt: now,
3560
- warning,
3561
- budgetTokens: input.budgetTokens ?? null,
3562
- budgetFallbackModel: input.budgetFallbackModel ?? null,
3563
- tokensUsed: 0,
3564
- tokensWarnedAt: null
3565
- };
3566
3745
  }
3567
- async function listTasks(input) {
3568
- const client = getClient();
3569
- const conditions = [];
3570
- const args = [];
3571
- if (input.assignedTo) {
3572
- conditions.push("assigned_to = ?");
3573
- args.push(input.assignedTo);
3746
+ var init_tasks_notify = __esm({
3747
+ "src/lib/tasks-notify.ts"() {
3748
+ "use strict";
3749
+ init_tmux_routing();
3750
+ init_session_key();
3751
+ init_notifications();
3752
+ init_transport();
3753
+ init_employees();
3574
3754
  }
3575
- if (input.status) {
3576
- conditions.push("status = ?");
3577
- args.push(input.status);
3578
- } else {
3579
- conditions.push("status IN ('open', 'in_progress', 'blocked')");
3580
- }
3581
- if (input.projectName) {
3582
- conditions.push("project_name = ?");
3583
- args.push(input.projectName);
3584
- }
3585
- if (input.priority) {
3586
- conditions.push("priority = ?");
3587
- args.push(input.priority);
3588
- }
3589
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3590
- const result = await client.execute({
3591
- 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`,
3592
- args
3593
- });
3594
- return result.rows.map((r) => ({
3595
- id: String(r.id),
3596
- title: String(r.title),
3597
- assignedTo: String(r.assigned_to),
3598
- assignedBy: String(r.assigned_by),
3599
- projectName: String(r.project_name),
3600
- priority: String(r.priority),
3601
- status: String(r.status),
3602
- taskFile: String(r.task_file),
3603
- createdAt: String(r.created_at),
3604
- updatedAt: String(r.updated_at),
3605
- checkpointCount: Number(r.checkpoint_count ?? 0),
3606
- budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
3607
- budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
3608
- tokensUsed: Number(r.tokens_used ?? 0),
3609
- tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
3610
- }));
3611
- }
3612
- function checkStaleCompletion(taskContext, taskCreatedAt) {
3613
- if (!taskContext) return null;
3614
- if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
3615
- try {
3616
- const since = new Date(taskCreatedAt).toISOString();
3617
- const branch = execSync9(
3618
- "git rev-parse --abbrev-ref HEAD 2>/dev/null",
3619
- { encoding: "utf8", timeout: 3e3 }
3620
- ).trim();
3621
- const branchArg = branch && branch !== "HEAD" ? branch : "";
3622
- const commitCount = execSync9(
3623
- `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
3624
- { encoding: "utf8", timeout: 5e3 }
3625
- ).trim();
3626
- const count = parseInt(commitCount, 10);
3627
- if (count === 0) {
3628
- return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
3629
- }
3630
- return null;
3631
- } catch {
3632
- return null;
3633
- }
3634
- }
3635
- async function updateTaskStatus(input) {
3755
+ });
3756
+
3757
+ // src/lib/behaviors.ts
3758
+ import crypto5 from "crypto";
3759
+ async function storeBehavior(opts) {
3636
3760
  const client = getClient();
3761
+ const id = crypto5.randomUUID();
3637
3762
  const now = (/* @__PURE__ */ new Date()).toISOString();
3638
- const row = await resolveTask(client, input.taskId);
3639
- const taskId = String(row.id);
3640
- const taskFile = String(row.task_file);
3641
- if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
3642
- process.stderr.write(
3643
- `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
3644
- `
3645
- );
3763
+ await client.execute({
3764
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
3765
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
3766
+ args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
3767
+ });
3768
+ return id;
3769
+ }
3770
+ var init_behaviors = __esm({
3771
+ "src/lib/behaviors.ts"() {
3772
+ "use strict";
3773
+ init_database();
3646
3774
  }
3647
- if (input.status === "done") {
3648
- const existingRow = await client.execute({
3649
- sql: "SELECT context, created_at FROM tasks WHERE id = ?",
3650
- args: [taskId]
3651
- });
3652
- if (existingRow.rows.length > 0) {
3653
- const ctx = existingRow.rows[0];
3654
- const warning = checkStaleCompletion(ctx.context, ctx.created_at);
3655
- if (warning) {
3656
- input.result = input.result ? `\u26A0\uFE0F ${warning}
3775
+ });
3657
3776
 
3658
- ${input.result}` : `\u26A0\uFE0F ${warning}`;
3659
- process.stderr.write(`[tasks] ${warning} (task: ${taskId})
3660
- `);
3661
- }
3662
- }
3663
- }
3664
- if (input.status === "in_progress") {
3665
- const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
3666
- const claim = await client.execute({
3667
- sql: `UPDATE tasks
3668
- SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
3669
- WHERE id = ? AND status = 'open'`,
3670
- args: [tmuxSession, now, taskId]
3671
- });
3672
- if (claim.rowsAffected === 0) {
3673
- const current = await client.execute({
3674
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
3675
- args: [taskId]
3676
- });
3677
- const cur = current.rows[0];
3678
- const status = cur?.status ?? "unknown";
3679
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
3680
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
3777
+ // src/lib/skill-learning.ts
3778
+ var skill_learning_exports = {};
3779
+ __export(skill_learning_exports, {
3780
+ captureAndLearn: () => captureAndLearn,
3781
+ captureTrajectory: () => captureTrajectory,
3782
+ editDistance: () => editDistance,
3783
+ extractSkill: () => extractSkill,
3784
+ extractTrajectory: () => extractTrajectory,
3785
+ findSimilarTrajectories: () => findSimilarTrajectories,
3786
+ hashSignature: () => hashSignature,
3787
+ storeTrajectory: () => storeTrajectory,
3788
+ sweepTrajectories: () => sweepTrajectories
3789
+ });
3790
+ import crypto6 from "crypto";
3791
+ async function extractTrajectory(taskId, agentId) {
3792
+ const client = getClient();
3793
+ const result = await client.execute({
3794
+ sql: `SELECT tool_name, raw_text
3795
+ FROM memories
3796
+ WHERE task_id = ? AND agent_id = ?
3797
+ ORDER BY timestamp ASC`,
3798
+ args: [taskId, agentId]
3799
+ });
3800
+ if (result.rows.length === 0) return [];
3801
+ const rawTools = result.rows.map((r) => {
3802
+ const toolName = String(r.tool_name);
3803
+ if (toolName === "Bash") {
3804
+ const text = String(r.raw_text);
3805
+ const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
3806
+ return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
3681
3807
  }
3682
- try {
3683
- await writeCheckpoint({
3684
- taskId,
3685
- step: "claimed",
3686
- contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3687
- });
3688
- } catch {
3808
+ return toolName;
3809
+ });
3810
+ const signature = [];
3811
+ for (const tool of rawTools) {
3812
+ if (signature.length === 0 || signature[signature.length - 1] !== tool) {
3813
+ signature.push(tool);
3689
3814
  }
3690
- return { row, taskFile, now, taskId };
3691
- }
3692
- if (input.result) {
3693
- await client.execute({
3694
- sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
3695
- args: [input.status, input.result, now, taskId]
3696
- });
3697
- } else {
3698
- await client.execute({
3699
- sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
3700
- args: [input.status, now, taskId]
3701
- });
3702
- }
3703
- try {
3704
- await writeCheckpoint({
3705
- taskId,
3706
- step: `status_transition:${input.status}`,
3707
- contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
3708
- });
3709
- } catch {
3710
3815
  }
3711
- return { row, taskFile, now, taskId };
3816
+ return signature;
3712
3817
  }
3713
- async function deleteTaskCore(taskId, _baseDir) {
3714
- const client = getClient();
3715
- const row = await resolveTask(client, taskId);
3716
- const id = String(row.id);
3717
- const taskFile = String(row.task_file);
3718
- const assignedTo = String(row.assigned_to);
3719
- const assignedBy = String(row.assigned_by);
3720
- await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
3721
- const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
3722
- return { taskFile, assignedTo, assignedBy, taskSlug };
3818
+ function hashSignature(signature) {
3819
+ return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
3723
3820
  }
3724
- async function ensureArchitectureDoc(baseDir, projectName) {
3725
- const archPath = path14.join(baseDir, "exe", "ARCHITECTURE.md");
3726
- try {
3727
- if (existsSync12(archPath)) return;
3728
- const template = [
3729
- `# ${projectName} \u2014 System Architecture`,
3730
- "",
3731
- "> Employees: read this before every task. Update it when you change system structure.",
3732
- `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
3733
- "",
3734
- "## Overview",
3735
- "",
3736
- "<!-- Describe what this system does, its main components, and how they connect. -->",
3737
- "",
3738
- "## Key Components",
3739
- "",
3740
- "<!-- List the major modules, services, or subsystems. -->",
3741
- "",
3742
- "## Data Flow",
3743
- "",
3744
- "<!-- How does data move through the system? What writes where? -->",
3745
- "",
3746
- "## Invariants",
3747
- "",
3748
- "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
3749
- "",
3750
- "## Dependencies",
3751
- "",
3752
- "<!-- What depends on what? If I change X, what else is affected? -->",
3753
- ""
3754
- ].join("\n");
3755
- await writeFile4(archPath, template, "utf-8");
3756
- } catch {
3757
- }
3821
+ async function storeTrajectory(opts) {
3822
+ const client = getClient();
3823
+ const id = crypto6.randomUUID();
3824
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3825
+ const signatureHash = hashSignature(opts.signature);
3826
+ await client.execute({
3827
+ sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
3828
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3829
+ args: [
3830
+ id,
3831
+ opts.taskId,
3832
+ opts.agentId,
3833
+ opts.projectName,
3834
+ opts.taskTitle,
3835
+ JSON.stringify(opts.signature),
3836
+ signatureHash,
3837
+ opts.signature.length,
3838
+ now
3839
+ ]
3840
+ });
3841
+ return id;
3758
3842
  }
3759
- async function ensureGitignoreExe(baseDir) {
3760
- const gitignorePath = path14.join(baseDir, ".gitignore");
3761
- try {
3762
- if (existsSync12(gitignorePath)) {
3763
- const content = readFileSync11(gitignorePath, "utf-8");
3764
- if (/^\/?exe\/?$/m.test(content)) return;
3765
- await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3766
- } else {
3767
- await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
3843
+ async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
3844
+ const client = getClient();
3845
+ const hash = hashSignature(signature);
3846
+ const result = await client.execute({
3847
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
3848
+ FROM trajectories
3849
+ WHERE signature_hash = ?
3850
+ ORDER BY created_at DESC
3851
+ LIMIT 20`,
3852
+ args: [hash]
3853
+ });
3854
+ const mapRow = (r) => ({
3855
+ id: String(r.id),
3856
+ taskId: String(r.task_id),
3857
+ agentId: String(r.agent_id),
3858
+ projectName: String(r.project_name),
3859
+ taskTitle: String(r.task_title),
3860
+ signature: JSON.parse(String(r.signature)),
3861
+ signatureHash: String(r.signature_hash),
3862
+ toolCount: Number(r.tool_count),
3863
+ skillId: r.skill_id ? String(r.skill_id) : null,
3864
+ createdAt: String(r.created_at)
3865
+ });
3866
+ const matches = result.rows.map(mapRow);
3867
+ if (matches.length >= threshold) return matches;
3868
+ const nearResult = await client.execute({
3869
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
3870
+ FROM trajectories
3871
+ WHERE tool_count BETWEEN ? AND ?
3872
+ AND signature_hash != ?
3873
+ ORDER BY created_at DESC
3874
+ LIMIT 50`,
3875
+ args: [
3876
+ Math.max(1, signature.length - 3),
3877
+ signature.length + 3,
3878
+ hash
3879
+ ]
3880
+ });
3881
+ for (const r of nearResult.rows) {
3882
+ const candidateSig = JSON.parse(String(r.signature));
3883
+ if (editDistance(signature, candidateSig) <= 2) {
3884
+ matches.push(mapRow(r));
3768
3885
  }
3769
- } catch {
3770
3886
  }
3887
+ return matches;
3771
3888
  }
3772
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3773
- var init_tasks_crud = __esm({
3774
- "src/lib/tasks-crud.ts"() {
3775
- "use strict";
3776
- init_database();
3777
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
3778
- TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3889
+ async function captureTrajectory(opts) {
3890
+ const signature = await extractTrajectory(opts.taskId, opts.agentId);
3891
+ if (signature.length < 3) {
3892
+ return { trajectoryId: "", similarCount: 0, similar: [] };
3779
3893
  }
3780
- });
3781
-
3782
- // src/lib/tasks-review.ts
3783
- import path15 from "path";
3784
- import { existsSync as existsSync13, readdirSync as readdirSync5, unlinkSync as unlinkSync4 } from "fs";
3785
- async function countPendingReviews() {
3786
- const client = getClient();
3787
- const result = await client.execute({
3788
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3789
- args: []
3894
+ const trajectoryId = await storeTrajectory({
3895
+ taskId: opts.taskId,
3896
+ agentId: opts.agentId,
3897
+ projectName: opts.projectName,
3898
+ taskTitle: opts.taskTitle,
3899
+ signature
3790
3900
  });
3791
- return Number(result.rows[0]?.cnt) || 0;
3901
+ const similar = await findSimilarTrajectories(
3902
+ signature,
3903
+ opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
3904
+ );
3905
+ return { trajectoryId, similarCount: similar.length, similar };
3792
3906
  }
3793
- async function countNewPendingReviewsSince(sinceIso) {
3794
- const client = getClient();
3795
- const result = await client.execute({
3796
- sql: `SELECT COUNT(*) as cnt FROM tasks
3797
- WHERE status = 'needs_review' AND updated_at > ?`,
3798
- args: [sinceIso]
3799
- });
3800
- return Number(result.rows[0]?.cnt) || 0;
3907
+ function buildExtractionPrompt(trajectories) {
3908
+ const items = trajectories.map((t, i) => {
3909
+ const sig = t.signature.join(" \u2192 ");
3910
+ return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
3911
+ Signature: ${sig}`;
3912
+ }).join("\n\n");
3913
+ return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
3914
+
3915
+ ${items}
3916
+
3917
+ Extract the reusable procedure. Format your response EXACTLY like this:
3918
+
3919
+ SKILL: {name \u2014 short, descriptive}
3920
+ TRIGGER: {when to use this \u2014 one sentence}
3921
+ STEPS:
3922
+ 1. ...
3923
+ 2. ...
3924
+ PITFALLS: {common mistakes to avoid}
3925
+
3926
+ Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
3801
3927
  }
3802
- async function listPendingReviews(limit) {
3803
- const client = getClient();
3804
- const result = await client.execute({
3805
- sql: `SELECT title, assigned_to, project_name FROM tasks
3806
- WHERE status = 'needs_review'
3807
- ORDER BY priority ASC, created_at DESC LIMIT ?`,
3808
- args: [limit]
3928
+ async function extractSkill(trajectories, model) {
3929
+ if (trajectories.length === 0) return null;
3930
+ const config = await loadConfig();
3931
+ const skillModel = model ?? config.skillModel;
3932
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
3933
+ const client = new Anthropic();
3934
+ const prompt = buildExtractionPrompt(trajectories);
3935
+ const response = await client.messages.create({
3936
+ model: skillModel,
3937
+ max_tokens: 500,
3938
+ messages: [{ role: "user", content: prompt }]
3809
3939
  });
3810
- return result.rows;
3811
- }
3812
- function getReviewChecklist(role, agent, taskSlug) {
3813
- const roleLower = role.toLowerCase();
3814
- if (roleLower.includes("engineer") || roleLower === "principal engineer") {
3815
- return {
3816
- lens: "Code Quality (Engineer)",
3817
- checklist: [
3818
- "1. Do all tests pass? Any new tests needed?",
3819
- "2. Is the code clean \u2014 no dead code, no TODOs left?",
3820
- "3. Does it follow existing patterns and conventions in the codebase?",
3821
- "4. Any regressions in the test suite?"
3822
- ]
3823
- };
3824
- }
3825
- if (roleLower === "cto" || roleLower.includes("architect")) {
3826
- return {
3827
- lens: "Architecture (CTO)",
3828
- checklist: [
3829
- "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
3830
- "2. Is it backward compatible? Any breaking changes?",
3831
- "3. Does it introduce technical debt? Is that debt justified?",
3832
- "4. Security implications? Any new attack surface?",
3833
- "5. Does it scale? Performance considerations?",
3834
- "6. Coordination: does this affect other employees' work or other projects?"
3835
- ]
3836
- };
3837
- }
3838
- if (roleLower === "coo" || roleLower.includes("operations")) {
3839
- return {
3840
- lens: "Strategic (COO)",
3841
- checklist: [
3842
- "1. Does this serve the project mission?",
3843
- "2. Is this the right work at the right time?",
3844
- "3. Does the architectural assessment make sense for the business?",
3845
- "4. Any cross-project implications?"
3846
- ]
3847
- };
3940
+ const textBlock = response.content.find((b) => b.type === "text");
3941
+ const skillText = textBlock?.text;
3942
+ if (!skillText) return null;
3943
+ const agentId = trajectories[0].agentId;
3944
+ const projectName = trajectories[0].projectName;
3945
+ const skillId = await storeBehavior({
3946
+ agentId,
3947
+ content: skillText,
3948
+ domain: "skill",
3949
+ projectName
3950
+ });
3951
+ const dbClient = getClient();
3952
+ for (const t of trajectories) {
3953
+ await dbClient.execute({
3954
+ sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
3955
+ args: [skillId, t.id]
3956
+ });
3848
3957
  }
3849
- return {
3850
- lens: "General",
3851
- checklist: [
3852
- "1. Read the original task's acceptance criteria",
3853
- `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
3854
- "3. Verify code changes match requirements",
3855
- "4. Check if tests were added/updated",
3856
- `5. Look for output files in exe/output/${agent}-${taskSlug}*`
3857
- ]
3858
- };
3958
+ process.stderr.write(
3959
+ `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
3960
+ `
3961
+ );
3962
+ return skillId;
3859
3963
  }
3860
- async function cleanupReviewFile(row, taskFile, _baseDir) {
3861
- if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3964
+ async function captureAndLearn(opts) {
3862
3965
  try {
3863
- const client = getClient();
3864
- const now = (/* @__PURE__ */ new Date()).toISOString();
3865
- const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3866
- if (parentId) {
3867
- const result = await client.execute({
3868
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3869
- args: [now, parentId]
3870
- });
3871
- if (result.rowsAffected > 0) {
3872
- process.stderr.write(
3873
- `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3874
- `
3875
- );
3876
- }
3877
- } else {
3878
- const fileName = taskFile.split("/").pop() ?? "";
3879
- const reviewPrefix = fileName.replace(".md", "");
3880
- const parts = reviewPrefix.split("-");
3881
- if (parts.length >= 3 && parts[0] === "review") {
3882
- const agent = parts[1];
3883
- const slug = parts.slice(2).join("-");
3884
- const originalTaskFile = `exe/${agent}/${slug}.md`;
3885
- const result = await client.execute({
3886
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
3887
- args: [now, originalTaskFile]
3888
- });
3889
- if (result.rowsAffected > 0) {
3966
+ const config = await loadConfig();
3967
+ if (!config.skillLearning) return;
3968
+ const { trajectoryId, similarCount, similar } = await captureTrajectory({
3969
+ ...opts,
3970
+ skillThreshold: config.skillThreshold
3971
+ });
3972
+ if (!trajectoryId) return;
3973
+ if (similarCount >= config.skillThreshold) {
3974
+ const unprocessed = similar.filter((t) => !t.skillId);
3975
+ if (unprocessed.length >= config.skillThreshold) {
3976
+ extractSkill(unprocessed, config.skillModel).catch((err) => {
3890
3977
  process.stderr.write(
3891
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
3978
+ `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
3892
3979
  `
3893
3980
  );
3894
- }
3981
+ });
3895
3982
  }
3896
3983
  }
3897
- } catch (err) {
3898
- process.stderr.write(
3899
- `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
3900
- `
3901
- );
3984
+ } catch (err) {
3985
+ process.stderr.write(
3986
+ `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
3987
+ `
3988
+ );
3989
+ }
3990
+ }
3991
+ async function sweepTrajectories(threshold, model) {
3992
+ const config = await loadConfig();
3993
+ if (!config.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
3994
+ const t = threshold ?? config.skillThreshold;
3995
+ const client = getClient();
3996
+ const result = await client.execute({
3997
+ sql: `SELECT signature_hash, COUNT(*) as cnt
3998
+ FROM trajectories
3999
+ WHERE skill_id IS NULL
4000
+ GROUP BY signature_hash
4001
+ HAVING cnt >= ?
4002
+ ORDER BY cnt DESC
4003
+ LIMIT 10`,
4004
+ args: [t]
4005
+ });
4006
+ let clustersProcessed = 0;
4007
+ let skillsExtracted = 0;
4008
+ for (const row of result.rows) {
4009
+ const hash = String(row.signature_hash);
4010
+ const trajResult = await client.execute({
4011
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
4012
+ FROM trajectories
4013
+ WHERE signature_hash = ? AND skill_id IS NULL
4014
+ ORDER BY created_at DESC
4015
+ LIMIT 10`,
4016
+ args: [hash]
4017
+ });
4018
+ const trajectories = trajResult.rows.map((r) => ({
4019
+ id: String(r.id),
4020
+ taskId: String(r.task_id),
4021
+ agentId: String(r.agent_id),
4022
+ projectName: String(r.project_name),
4023
+ taskTitle: String(r.task_title),
4024
+ signature: JSON.parse(String(r.signature)),
4025
+ signatureHash: String(r.signature_hash),
4026
+ toolCount: Number(r.tool_count),
4027
+ skillId: null,
4028
+ createdAt: String(r.created_at)
4029
+ }));
4030
+ if (trajectories.length >= t) {
4031
+ clustersProcessed++;
4032
+ const skillId = await extractSkill(trajectories, model ?? config.skillModel);
4033
+ if (skillId) skillsExtracted++;
4034
+ }
3902
4035
  }
3903
- try {
3904
- const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
3905
- if (existsSync13(cacheDir)) {
3906
- for (const f of readdirSync5(cacheDir)) {
3907
- if (f.startsWith("review-notified-")) {
3908
- unlinkSync4(path15.join(cacheDir, f));
3909
- }
3910
- }
4036
+ return { clustersProcessed, skillsExtracted };
4037
+ }
4038
+ function editDistance(a, b) {
4039
+ const m = a.length;
4040
+ const n = b.length;
4041
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
4042
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
4043
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
4044
+ for (let i = 1; i <= m; i++) {
4045
+ for (let j = 1; j <= n; j++) {
4046
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4047
+ dp[i][j] = Math.min(
4048
+ dp[i - 1][j] + 1,
4049
+ dp[i][j - 1] + 1,
4050
+ dp[i - 1][j - 1] + cost
4051
+ );
3911
4052
  }
3912
- } catch {
3913
4053
  }
4054
+ return dp[m][n];
3914
4055
  }
3915
- var init_tasks_review = __esm({
3916
- "src/lib/tasks-review.ts"() {
4056
+ var DEFAULT_SKILL_THRESHOLD;
4057
+ var init_skill_learning = __esm({
4058
+ "src/lib/skill-learning.ts"() {
3917
4059
  "use strict";
3918
4060
  init_database();
4061
+ init_behaviors();
3919
4062
  init_config();
3920
- init_employees();
3921
- init_notifications();
3922
- init_tasks_crud();
3923
- init_tmux_routing();
3924
- init_session_key();
4063
+ DEFAULT_SKILL_THRESHOLD = 3;
3925
4064
  }
3926
4065
  });
3927
4066
 
3928
- // src/lib/tasks-chain.ts
3929
- import path16 from "path";
3930
- import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
3931
- async function cascadeUnblock(taskId, baseDir, now) {
3932
- const client = getClient();
3933
- const unblocked = await client.execute({
3934
- sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
3935
- WHERE blocked_by = ? AND status = 'blocked'`,
3936
- args: [now, taskId]
3937
- });
3938
- if (baseDir && unblocked.rowsAffected > 0) {
3939
- const unblockedRows = await client.execute({
3940
- sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
3941
- args: [now]
4067
+ // src/lib/tasks.ts
4068
+ var tasks_exports = {};
4069
+ __export(tasks_exports, {
4070
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
4071
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
4072
+ countPendingReviews: () => countPendingReviews,
4073
+ createTask: () => createTask,
4074
+ createTaskCore: () => createTaskCore,
4075
+ deleteTask: () => deleteTask,
4076
+ deleteTaskCore: () => deleteTaskCore,
4077
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
4078
+ ensureGitignoreExe: () => ensureGitignoreExe,
4079
+ getReviewChecklist: () => getReviewChecklist,
4080
+ listPendingReviews: () => listPendingReviews,
4081
+ listTasks: () => listTasks,
4082
+ resolveTask: () => resolveTask,
4083
+ slugify: () => slugify,
4084
+ updateTask: () => updateTask,
4085
+ updateTaskStatus: () => updateTaskStatus,
4086
+ writeCheckpoint: () => writeCheckpoint
4087
+ });
4088
+ import path15 from "path";
4089
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
4090
+ async function createTask(input) {
4091
+ const result = await createTaskCore(input);
4092
+ if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
4093
+ dispatchTaskToEmployee({
4094
+ assignedTo: input.assignedTo,
4095
+ title: input.title,
4096
+ priority: input.priority,
4097
+ taskFile: result.taskFile,
4098
+ initialStatus: result.status,
4099
+ projectName: input.projectName
3942
4100
  });
3943
- for (const ur of unblockedRows.rows) {
4101
+ }
4102
+ return result;
4103
+ }
4104
+ async function updateTask(input) {
4105
+ const { row, taskFile, now, taskId } = await updateTaskStatus(input);
4106
+ try {
4107
+ const agent = String(row.assigned_to);
4108
+ const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
4109
+ const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
4110
+ if (input.status === "in_progress") {
4111
+ mkdirSync6(cacheDir, { recursive: true });
4112
+ writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4113
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3944
4114
  try {
3945
- const ubFile = path16.join(baseDir, String(ur.task_file));
3946
- let ubContent = await readFile4(ubFile, "utf-8");
3947
- ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3948
- ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
3949
- await writeFile5(ubFile, ubContent, "utf-8");
4115
+ unlinkSync4(cachePath);
3950
4116
  } catch {
3951
4117
  }
3952
4118
  }
4119
+ } catch {
3953
4120
  }
3954
- }
3955
- async function findNextTask(assignedTo) {
3956
- const client = getClient();
3957
- const nextResult = await client.execute({
3958
- sql: `SELECT title, task_file, priority FROM tasks
3959
- WHERE assigned_to = ? AND status = 'open'
3960
- ORDER BY priority ASC, created_at ASC
3961
- LIMIT 1`,
3962
- args: [assignedTo]
3963
- });
3964
- if (nextResult.rows.length === 1) {
3965
- const nr = nextResult.rows[0];
3966
- return {
3967
- title: String(nr.title),
3968
- priority: String(nr.priority),
3969
- taskFile: String(nr.task_file)
3970
- };
4121
+ if (input.status === "done") {
4122
+ await cleanupReviewFile(row, taskFile, input.baseDir);
3971
4123
  }
3972
- return void 0;
3973
- }
3974
- async function checkSubtaskCompletion(parentTaskId, projectName) {
3975
- const client = getClient();
3976
- const remaining = await client.execute({
3977
- sql: `SELECT COUNT(*) as cnt FROM tasks
3978
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
3979
- args: [parentTaskId]
3980
- });
3981
- const cnt = Number(remaining.rows[0]?.cnt ?? 1);
3982
- if (cnt === 0) {
3983
- const parentRow = await client.execute({
3984
- sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
3985
- args: [parentTaskId]
3986
- });
3987
- if (parentRow.rows.length === 1) {
3988
- const pr = parentRow.rows[0];
3989
- const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
3990
- await writeNotification({
3991
- agentId: String(pr.assigned_to),
3992
- agentRole: "system",
3993
- event: "subtasks_complete",
3994
- project: parentProject,
3995
- summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
3996
- taskFile: String(pr.task_file)
4124
+ if (input.status === "done" || input.status === "cancelled") {
4125
+ try {
4126
+ const client = getClient();
4127
+ const taskTitle = String(row.title);
4128
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
4129
+ await client.execute({
4130
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
4131
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
4132
+ args: [now, `%left '${escaped}' as in\\_progress%`]
4133
+ });
4134
+ } catch {
4135
+ }
4136
+ try {
4137
+ const client = getClient();
4138
+ const cascaded = await client.execute({
4139
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
4140
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
4141
+ args: [now, taskId]
4142
+ });
4143
+ if (cascaded.rowsAffected > 0) {
4144
+ process.stderr.write(
4145
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
4146
+ `
4147
+ );
4148
+ }
4149
+ } catch {
4150
+ }
4151
+ }
4152
+ const isTerminal = input.status === "done" || input.status === "needs_review";
4153
+ if (isTerminal) {
4154
+ const isExe = String(row.assigned_to) === "exe";
4155
+ if (!isExe) {
4156
+ notifyTaskDone();
4157
+ }
4158
+ await markTaskNotificationsRead(taskFile);
4159
+ if (input.status === "done") {
4160
+ try {
4161
+ await cascadeUnblock(taskId, input.baseDir, now);
4162
+ } catch {
4163
+ }
4164
+ orgBus.emit({
4165
+ type: "task_completed",
4166
+ taskId,
4167
+ employee: String(row.assigned_to),
4168
+ result: input.result ?? "",
4169
+ timestamp: now
3997
4170
  });
4171
+ if (row.parent_task_id) {
4172
+ try {
4173
+ await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
4174
+ } catch {
4175
+ }
4176
+ }
4177
+ }
4178
+ }
4179
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
4180
+ Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4181
+ ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4182
+ taskId,
4183
+ agentId: String(row.assigned_to),
4184
+ projectName: String(row.project_name),
4185
+ taskTitle: String(row.title)
4186
+ })
4187
+ ).catch((err) => {
4188
+ process.stderr.write(
4189
+ `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
4190
+ `
4191
+ );
4192
+ });
4193
+ }
4194
+ let nextTask;
4195
+ if (isTerminal && String(row.assigned_to) !== "exe") {
4196
+ try {
4197
+ nextTask = await findNextTask(String(row.assigned_to));
4198
+ } catch {
3998
4199
  }
3999
4200
  }
4201
+ return {
4202
+ id: String(row.id),
4203
+ title: String(row.title),
4204
+ assignedTo: String(row.assigned_to),
4205
+ assignedBy: String(row.assigned_by),
4206
+ projectName: String(row.project_name),
4207
+ priority: String(row.priority),
4208
+ status: input.status,
4209
+ taskFile,
4210
+ createdAt: String(row.created_at),
4211
+ updatedAt: now,
4212
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
4213
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
4214
+ tokensUsed: Number(row.tokens_used ?? 0),
4215
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
4216
+ nextTask
4217
+ };
4000
4218
  }
4001
- var init_tasks_chain = __esm({
4002
- "src/lib/tasks-chain.ts"() {
4219
+ async function deleteTask(taskId, baseDir) {
4220
+ const client = getClient();
4221
+ const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
4222
+ const reviewer = assignedBy || "exe";
4223
+ const reviewSlug = `review-${assignedTo}-${taskSlug}`;
4224
+ const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
4225
+ await client.execute({
4226
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
4227
+ args: [reviewFile, `exe/exe/${reviewSlug}.md`]
4228
+ });
4229
+ await markAsReadByTaskFile(taskFile);
4230
+ await markAsReadByTaskFile(reviewFile);
4231
+ }
4232
+ var init_tasks = __esm({
4233
+ "src/lib/tasks.ts"() {
4003
4234
  "use strict";
4004
4235
  init_database();
4236
+ init_config();
4005
4237
  init_notifications();
4238
+ init_state_bus();
4239
+ init_tasks_crud();
4240
+ init_tasks_review();
4241
+ init_tasks_crud();
4242
+ init_tasks_chain();
4243
+ init_tasks_review();
4244
+ init_tasks_notify();
4006
4245
  }
4007
4246
  });
4008
4247
 
4009
- // src/lib/session-scope.ts
4010
- var session_scope_exports = {};
4011
- __export(session_scope_exports, {
4012
- assertSessionScope: () => assertSessionScope,
4013
- findSessionForProject: () => findSessionForProject,
4014
- getSessionProject: () => getSessionProject
4248
+ // src/lib/capacity-monitor.ts
4249
+ var capacity_monitor_exports = {};
4250
+ __export(capacity_monitor_exports, {
4251
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
4252
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
4253
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
4254
+ confirmCapacityKill: () => confirmCapacityKill,
4255
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
4256
+ extractContextPercent: () => extractContextPercent,
4257
+ isAtCapacity: () => isAtCapacity,
4258
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
4259
+ pollCapacityDead: () => pollCapacityDead
4015
4260
  });
4016
- function getSessionProject(sessionName) {
4017
- const sessions = listSessions();
4018
- const entry = sessions.find((s) => s.windowName === sessionName);
4019
- if (!entry) return null;
4020
- const parts = entry.projectDir.split("/").filter(Boolean);
4021
- return parts[parts.length - 1] ?? null;
4022
- }
4023
- function findSessionForProject(projectName) {
4024
- const sessions = listSessions();
4025
- for (const s of sessions) {
4026
- const proj = s.projectDir.split("/").filter(Boolean).pop();
4027
- if (proj === projectName && s.agentId === "exe") return s;
4261
+ function resumeTaskTitle(agentId) {
4262
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
4263
+ }
4264
+ function buildResumeContext(agentId, openTasks) {
4265
+ const taskList = openTasks.map(
4266
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
4267
+ ).join("\n");
4268
+ return [
4269
+ "## Context",
4270
+ "",
4271
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
4272
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
4273
+ "",
4274
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
4275
+ "",
4276
+ taskList,
4277
+ "",
4278
+ "Read each task file and chain through them. Build and commit after each one."
4279
+ ].join("\n");
4280
+ }
4281
+ function filterPaneContent(paneOutput) {
4282
+ return paneOutput.split("\n").filter((line) => {
4283
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
4284
+ for (const marker of CONTENT_LINE_MARKERS) {
4285
+ if (line.includes(marker)) return false;
4286
+ }
4287
+ for (const re of SOURCE_CODE_MARKERS) {
4288
+ if (re.test(line)) return false;
4289
+ }
4290
+ return true;
4291
+ }).join("\n");
4292
+ }
4293
+ function extractContextPercent(paneOutput) {
4294
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
4295
+ if (!match) return null;
4296
+ const parsed = Number.parseInt(match[2], 10);
4297
+ return Number.isFinite(parsed) ? parsed : null;
4298
+ }
4299
+ function isAtCapacity(paneOutput) {
4300
+ const filtered = filterPaneContent(paneOutput);
4301
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
4302
+ }
4303
+ function confirmCapacityKill(agentId, now = Date.now()) {
4304
+ const pendingSince = _pendingCapacityKill.get(agentId);
4305
+ if (pendingSince === void 0) {
4306
+ _pendingCapacityKill.set(agentId, now);
4307
+ return false;
4028
4308
  }
4029
- return null;
4309
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
4310
+ _pendingCapacityKill.set(agentId, now);
4311
+ return false;
4312
+ }
4313
+ _pendingCapacityKill.delete(agentId);
4314
+ return true;
4030
4315
  }
4031
- function assertSessionScope(actionType, targetProject) {
4316
+ function _resetPendingCapacityKills() {
4317
+ _pendingCapacityKill.clear();
4318
+ }
4319
+ function _resetLastRelaunchCache() {
4320
+ _lastRelaunch.clear();
4321
+ }
4322
+ async function lastResumeCreatedAtMs(agentId) {
4323
+ const client = getClient();
4324
+ const result = await client.execute({
4325
+ sql: `SELECT MAX(created_at) AS last_created_at
4326
+ FROM tasks
4327
+ WHERE assigned_to = ? AND title LIKE ?`,
4328
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`]
4329
+ });
4330
+ const raw = result.rows[0]?.last_created_at;
4331
+ if (raw === null || raw === void 0) return null;
4332
+ const parsed = Date.parse(String(raw));
4333
+ return Number.isNaN(parsed) ? null : parsed;
4334
+ }
4335
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
4336
+ const cached = _lastRelaunch.get(agentId);
4337
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
4338
+ const persisted = await lastResumeCreatedAtMs(agentId);
4339
+ if (persisted === null) return false;
4340
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
4341
+ _lastRelaunch.set(agentId, persisted);
4342
+ return true;
4343
+ }
4344
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
4345
+ const client = getClient();
4346
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4347
+ const context = buildResumeContext(agentId, openTasks);
4348
+ const existing = await client.execute({
4349
+ sql: `SELECT id FROM tasks
4350
+ WHERE assigned_to = ?
4351
+ AND title LIKE ?
4352
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
4353
+ ORDER BY created_at DESC
4354
+ LIMIT 1`,
4355
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
4356
+ });
4357
+ if (existing.rows.length > 0) {
4358
+ const taskId = String(existing.rows[0].id);
4359
+ await client.execute({
4360
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
4361
+ args: [context, now, taskId]
4362
+ });
4363
+ return { created: false, taskId };
4364
+ }
4365
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
4366
+ const task = await createTask2({
4367
+ title: resumeTaskTitle(agentId),
4368
+ assignedTo: agentId,
4369
+ assignedBy: "system",
4370
+ projectName: projectDir.split("/").pop() ?? "unknown",
4371
+ priority: "p0",
4372
+ context,
4373
+ baseDir: projectDir
4374
+ });
4375
+ return { created: true, taskId: task.id };
4376
+ }
4377
+ async function pollCapacityDead() {
4378
+ const transport = getTransport();
4379
+ const relaunched = [];
4380
+ const registered = listSessions().filter(
4381
+ (s) => s.agentId !== "exe"
4382
+ );
4383
+ if (registered.length === 0) return [];
4384
+ let liveSessions;
4032
4385
  try {
4033
- const currentProject = getProjectName();
4034
- const exeSession = resolveExeSession();
4035
- if (!exeSession) {
4036
- return { allowed: true, reason: "no_session" };
4386
+ liveSessions = transport.listSessions();
4387
+ } catch {
4388
+ return [];
4389
+ }
4390
+ for (const entry of registered) {
4391
+ const { windowName, agentId, projectDir } = entry;
4392
+ if (!liveSessions.includes(windowName)) continue;
4393
+ if (await isWithinRelaunchCooldown(agentId)) continue;
4394
+ let pane;
4395
+ try {
4396
+ pane = transport.capturePane(windowName, 15);
4397
+ } catch {
4398
+ continue;
4037
4399
  }
4038
- if (currentProject === targetProject) {
4039
- return {
4040
- allowed: true,
4041
- reason: "same_session",
4042
- currentProject,
4043
- targetProject
4044
- };
4400
+ if (!isAtCapacity(pane)) continue;
4401
+ const ctxPct = extractContextPercent(pane);
4402
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
4403
+ process.stderr.write(
4404
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
4405
+ `
4406
+ );
4407
+ continue;
4408
+ }
4409
+ if (!confirmCapacityKill(agentId)) {
4410
+ process.stderr.write(
4411
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
4412
+ `
4413
+ );
4414
+ continue;
4415
+ }
4416
+ const verify = await verifyPaneAtCapacity(windowName);
4417
+ if (!verify.atCapacity) {
4418
+ process.stderr.write(
4419
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
4420
+ `
4421
+ );
4422
+ void recordSessionKill({
4423
+ sessionName: windowName,
4424
+ agentId,
4425
+ reason: "capacity_false_positive_blocked"
4426
+ });
4427
+ continue;
4045
4428
  }
4046
4429
  process.stderr.write(
4047
- `[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
4430
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
4048
4431
  `
4049
4432
  );
4050
- return {
4051
- allowed: true,
4052
- // v1: warn-only, don't block
4053
- reason: "cross_session_granted",
4054
- currentProject,
4055
- targetProject,
4056
- targetSession: findSessionForProject(targetProject)?.windowName
4057
- };
4058
- } catch {
4059
- return { allowed: true, reason: "no_session" };
4433
+ try {
4434
+ transport.kill(windowName);
4435
+ void recordSessionKill({
4436
+ sessionName: windowName,
4437
+ agentId,
4438
+ reason: "capacity"
4439
+ });
4440
+ const client = getClient();
4441
+ const openTasks = await client.execute({
4442
+ sql: `SELECT id, title, priority, task_file, status
4443
+ FROM tasks
4444
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')
4445
+ ORDER BY
4446
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
4447
+ created_at ASC
4448
+ LIMIT 10`,
4449
+ args: [agentId]
4450
+ });
4451
+ if (openTasks.rows.length === 0) {
4452
+ process.stderr.write(
4453
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
4454
+ `
4455
+ );
4456
+ continue;
4457
+ }
4458
+ const { created } = await createOrRefreshResumeTask(
4459
+ agentId,
4460
+ projectDir,
4461
+ openTasks.rows
4462
+ );
4463
+ if (created) {
4464
+ await writeNotification({
4465
+ agentId: "system",
4466
+ agentRole: "daemon",
4467
+ event: "capacity_relaunch",
4468
+ project: projectDir.split("/").pop() ?? "unknown",
4469
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
4470
+ });
4471
+ }
4472
+ _lastRelaunch.set(agentId, Date.now());
4473
+ if (created) relaunched.push(agentId);
4474
+ } catch (err) {
4475
+ process.stderr.write(
4476
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
4477
+ `
4478
+ );
4479
+ }
4060
4480
  }
4481
+ return relaunched;
4061
4482
  }
4062
- var init_session_scope = __esm({
4063
- "src/lib/session-scope.ts"() {
4483
+ 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;
4484
+ var init_capacity_monitor = __esm({
4485
+ "src/lib/capacity-monitor.ts"() {
4064
4486
  "use strict";
4065
4487
  init_session_registry();
4066
- init_project_name();
4488
+ init_transport();
4489
+ init_notifications();
4490
+ init_database();
4491
+ init_session_kill_telemetry();
4067
4492
  init_tmux_routing();
4493
+ CAPACITY_PATTERNS = [
4494
+ /conversation is too long/i,
4495
+ /maximum context length/i,
4496
+ /context window.*(?:limit|exceed|full)/i,
4497
+ /reached.*(?:token|context).*limit/i
4498
+ ];
4499
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
4500
+ CONTENT_LINE_MARKERS = [
4501
+ "RESUME:",
4502
+ "intercom",
4503
+ "capacity-monitor",
4504
+ "CAPACITY_PATTERNS",
4505
+ "isAtCapacity",
4506
+ "CONTENT_LINE_MARKERS",
4507
+ "pollCapacityDead",
4508
+ "confirmCapacityKill",
4509
+ "session_kills",
4510
+ "capacity-monitor.test"
4511
+ ];
4512
+ SOURCE_CODE_MARKERS = [
4513
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
4514
+ /(?:maximum context length|conversation is too long).*["'`/]/i
4515
+ ];
4516
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
4517
+ _lastRelaunch = /* @__PURE__ */ new Map();
4518
+ RESUME_TITLE_PREFIX = "RESUME:";
4519
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
4520
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
4521
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
4522
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
4523
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
4524
+ CTX_FLOOR_PERCENT = 50;
4068
4525
  }
4069
4526
  });
4070
4527
 
4071
- // src/lib/tasks-notify.ts
4072
- async function dispatchTaskToEmployee(input) {
4073
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
4074
- let crossProject = false;
4075
- if (input.projectName) {
4528
+ // src/lib/tmux-routing.ts
4529
+ var tmux_routing_exports = {};
4530
+ __export(tmux_routing_exports, {
4531
+ acquireSpawnLock: () => acquireSpawnLock,
4532
+ employeeSessionName: () => employeeSessionName,
4533
+ ensureEmployee: () => ensureEmployee,
4534
+ extractRootExe: () => extractRootExe,
4535
+ findFreeInstance: () => findFreeInstance,
4536
+ getDispatchedBy: () => getDispatchedBy,
4537
+ getMySession: () => getMySession,
4538
+ getParentExe: () => getParentExe,
4539
+ getSessionState: () => getSessionState,
4540
+ isEmployeeAlive: () => isEmployeeAlive,
4541
+ isExeSession: () => isExeSession,
4542
+ isSessionBusy: () => isSessionBusy,
4543
+ notifyParentExe: () => notifyParentExe,
4544
+ parseParentExe: () => parseParentExe,
4545
+ registerParentExe: () => registerParentExe,
4546
+ releaseSpawnLock: () => releaseSpawnLock,
4547
+ resolveExeSession: () => resolveExeSession,
4548
+ sendIntercom: () => sendIntercom,
4549
+ spawnEmployee: () => spawnEmployee,
4550
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
4551
+ });
4552
+ import { execFileSync as execFileSync2, execSync as execSync8 } from "child_process";
4553
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync12, appendFileSync } from "fs";
4554
+ import path16 from "path";
4555
+ import os6 from "os";
4556
+ import { fileURLToPath as fileURLToPath2 } from "url";
4557
+ import { unlinkSync as unlinkSync5 } from "fs";
4558
+ function spawnLockPath(sessionName) {
4559
+ return path16.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4560
+ }
4561
+ function isProcessAlive(pid) {
4562
+ try {
4563
+ process.kill(pid, 0);
4564
+ return true;
4565
+ } catch {
4566
+ return false;
4567
+ }
4568
+ }
4569
+ function acquireSpawnLock(sessionName) {
4570
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
4571
+ mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
4572
+ }
4573
+ const lockFile = spawnLockPath(sessionName);
4574
+ if (existsSync12(lockFile)) {
4076
4575
  try {
4077
- const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
4078
- const check = assertSessionScope2("dispatch_task", input.projectName);
4079
- if (check.reason === "cross_session_granted") {
4080
- crossProject = true;
4576
+ const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
4577
+ const age = Date.now() - lock.timestamp;
4578
+ if (isProcessAlive(lock.pid) && age < 6e4) {
4579
+ return false;
4081
4580
  }
4082
4581
  } catch {
4083
4582
  }
4084
4583
  }
4584
+ writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4585
+ return true;
4586
+ }
4587
+ function releaseSpawnLock(sessionName) {
4085
4588
  try {
4086
- const transport = getTransport();
4087
- const exeSession = resolveExeSession();
4088
- if (!exeSession) return { dispatched: "session_missing" };
4089
- const sessionName = employeeSessionName(input.assignedTo, exeSession);
4090
- if (transport.isAlive(sessionName)) {
4091
- const result = sendIntercom(sessionName);
4092
- const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
4093
- return { dispatched, session: sessionName, crossProject };
4094
- } else {
4095
- const projectDir = input.projectDir ?? process.cwd();
4096
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
4097
- autoInstance: isMultiInstance(input.assignedTo)
4098
- });
4099
- if (result.status === "failed") {
4100
- process.stderr.write(
4101
- `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
4589
+ unlinkSync5(spawnLockPath(sessionName));
4590
+ } catch {
4591
+ }
4592
+ }
4593
+ function resolveBehaviorsExporterScript() {
4594
+ try {
4595
+ const thisFile = fileURLToPath2(import.meta.url);
4596
+ const scriptPath = path16.join(
4597
+ path16.dirname(thisFile),
4598
+ "..",
4599
+ "bin",
4600
+ "exe-export-behaviors.js"
4601
+ );
4602
+ return existsSync12(scriptPath) ? scriptPath : null;
4603
+ } catch {
4604
+ return null;
4605
+ }
4606
+ }
4607
+ function exportBehaviorsSync(agentId, projectName, sessionKey) {
4608
+ const script = resolveBehaviorsExporterScript();
4609
+ if (!script) return null;
4610
+ try {
4611
+ const output = execFileSync2(
4612
+ process.execPath,
4613
+ [script, agentId, projectName, sessionKey],
4614
+ { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
4615
+ ).trim();
4616
+ return output.length > 0 ? output : null;
4617
+ } catch (err) {
4618
+ process.stderr.write(
4619
+ `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
4102
4620
  `
4103
- );
4104
- return { dispatched: "session_missing" };
4105
- }
4106
- return { dispatched: "spawned", session: result.sessionName, crossProject };
4621
+ );
4622
+ return null;
4623
+ }
4624
+ }
4625
+ function getMySession() {
4626
+ return getTransport().getMySession();
4627
+ }
4628
+ function employeeSessionName(employee, exeSession, instance) {
4629
+ if (!/^exe\d+$/.test(exeSession)) {
4630
+ const root = extractRootExe(exeSession);
4631
+ if (root) {
4632
+ process.stderr.write(
4633
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
4634
+ `
4635
+ );
4636
+ exeSession = root;
4637
+ } else {
4638
+ throw new Error(
4639
+ `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
4640
+ );
4641
+ }
4642
+ }
4643
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
4644
+ const name = `${employee}${suffix}-${exeSession}`;
4645
+ if (!VALID_SESSION_NAME.test(name)) {
4646
+ throw new Error(
4647
+ `Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
4648
+ );
4649
+ }
4650
+ return name;
4651
+ }
4652
+ function parseParentExe(sessionName, agentId) {
4653
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4654
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
4655
+ const match = sessionName.match(regex);
4656
+ return match?.[1] ?? null;
4657
+ }
4658
+ function extractRootExe(name) {
4659
+ const match = name.match(/(exe\d+)$/);
4660
+ return match?.[1] ?? null;
4661
+ }
4662
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
4663
+ if (!existsSync12(SESSION_CACHE)) {
4664
+ mkdirSync7(SESSION_CACHE, { recursive: true });
4665
+ }
4666
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
4667
+ const filePath = path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4668
+ writeFileSync6(filePath, JSON.stringify({
4669
+ parentExe: rootExe,
4670
+ dispatchedBy: dispatchedBy || rootExe,
4671
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4672
+ }));
4673
+ }
4674
+ function getParentExe(sessionKey) {
4675
+ try {
4676
+ const data = JSON.parse(readFileSync10(path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4677
+ return data.parentExe || null;
4678
+ } catch {
4679
+ return null;
4680
+ }
4681
+ }
4682
+ function getDispatchedBy(sessionKey) {
4683
+ try {
4684
+ const data = JSON.parse(readFileSync10(
4685
+ path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4686
+ "utf8"
4687
+ ));
4688
+ return data.dispatchedBy ?? data.parentExe ?? null;
4689
+ } catch {
4690
+ return null;
4691
+ }
4692
+ }
4693
+ function resolveExeSession() {
4694
+ const mySession = getMySession();
4695
+ if (!mySession) return null;
4696
+ try {
4697
+ const key = getSessionKey();
4698
+ const parentExe = getParentExe(key);
4699
+ if (parentExe) {
4700
+ return extractRootExe(parentExe) ?? parentExe;
4107
4701
  }
4108
4702
  } catch {
4109
- return { dispatched: "session_missing" };
4110
4703
  }
4704
+ return extractRootExe(mySession) ?? mySession;
4705
+ }
4706
+ function isEmployeeAlive(sessionName) {
4707
+ return getTransport().isAlive(sessionName);
4708
+ }
4709
+ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4710
+ const base = employeeSessionName(employeeName, exeSession);
4711
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
4712
+ for (let i = 2; i <= maxInstances; i++) {
4713
+ const candidate = employeeSessionName(employeeName, exeSession, i);
4714
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
4715
+ }
4716
+ return null;
4111
4717
  }
4112
- function notifyTaskDone() {
4718
+ async function verifyPaneAtCapacity(sessionName) {
4719
+ const transport = getTransport();
4720
+ if (!transport.isAlive(sessionName)) {
4721
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
4722
+ }
4723
+ let pane;
4113
4724
  try {
4114
- const key = getSessionKey();
4115
- if (key && !process.env.VITEST) notifyParentExe(key);
4116
- } catch {
4725
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
4726
+ } catch (err) {
4727
+ return {
4728
+ atCapacity: false,
4729
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
4730
+ };
4117
4731
  }
4732
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
4733
+ if (!isAtCapacity2(pane)) {
4734
+ return {
4735
+ atCapacity: false,
4736
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
4737
+ };
4738
+ }
4739
+ return {
4740
+ atCapacity: true,
4741
+ reason: "capacity banner matched in recent pane output"
4742
+ };
4118
4743
  }
4119
- async function markTaskNotificationsRead(taskFile) {
4744
+ function readDebounceState() {
4120
4745
  try {
4121
- await markAsReadByTaskFile(taskFile);
4746
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
4747
+ return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
4122
4748
  } catch {
4749
+ return {};
4123
4750
  }
4124
4751
  }
4125
- var init_tasks_notify = __esm({
4126
- "src/lib/tasks-notify.ts"() {
4127
- "use strict";
4128
- init_tmux_routing();
4129
- init_session_key();
4130
- init_notifications();
4131
- init_transport();
4132
- init_employees();
4133
- }
4134
- });
4135
-
4136
- // src/lib/behaviors.ts
4137
- import crypto4 from "crypto";
4138
- async function storeBehavior(opts) {
4139
- const client = getClient();
4140
- const id = crypto4.randomUUID();
4141
- const now = (/* @__PURE__ */ new Date()).toISOString();
4142
- await client.execute({
4143
- sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
4144
- VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
4145
- args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
4146
- });
4147
- return id;
4148
- }
4149
- var init_behaviors = __esm({
4150
- "src/lib/behaviors.ts"() {
4151
- "use strict";
4152
- init_database();
4153
- }
4154
- });
4155
-
4156
- // src/lib/skill-learning.ts
4157
- var skill_learning_exports = {};
4158
- __export(skill_learning_exports, {
4159
- captureAndLearn: () => captureAndLearn,
4160
- captureTrajectory: () => captureTrajectory,
4161
- editDistance: () => editDistance,
4162
- extractSkill: () => extractSkill,
4163
- extractTrajectory: () => extractTrajectory,
4164
- findSimilarTrajectories: () => findSimilarTrajectories,
4165
- hashSignature: () => hashSignature,
4166
- storeTrajectory: () => storeTrajectory,
4167
- sweepTrajectories: () => sweepTrajectories
4168
- });
4169
- import crypto5 from "crypto";
4170
- async function extractTrajectory(taskId, agentId) {
4171
- const client = getClient();
4172
- const result = await client.execute({
4173
- sql: `SELECT tool_name, raw_text
4174
- FROM memories
4175
- WHERE task_id = ? AND agent_id = ?
4176
- ORDER BY timestamp ASC`,
4177
- args: [taskId, agentId]
4178
- });
4179
- if (result.rows.length === 0) return [];
4180
- const rawTools = result.rows.map((r) => {
4181
- const toolName = String(r.tool_name);
4182
- if (toolName === "Bash") {
4183
- const text = String(r.raw_text);
4184
- const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
4185
- return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
4186
- }
4187
- return toolName;
4188
- });
4189
- const signature = [];
4190
- for (const tool of rawTools) {
4191
- if (signature.length === 0 || signature[signature.length - 1] !== tool) {
4192
- signature.push(tool);
4193
- }
4752
+ function writeDebounceState(state) {
4753
+ try {
4754
+ if (!existsSync12(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
4755
+ writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
4756
+ } catch {
4194
4757
  }
4195
- return signature;
4196
- }
4197
- function hashSignature(signature) {
4198
- return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4199
4758
  }
4200
- async function storeTrajectory(opts) {
4201
- const client = getClient();
4202
- const id = crypto5.randomUUID();
4203
- const now = (/* @__PURE__ */ new Date()).toISOString();
4204
- const signatureHash = hashSignature(opts.signature);
4205
- await client.execute({
4206
- sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
4207
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4208
- args: [
4209
- id,
4210
- opts.taskId,
4211
- opts.agentId,
4212
- opts.projectName,
4213
- opts.taskTitle,
4214
- JSON.stringify(opts.signature),
4215
- signatureHash,
4216
- opts.signature.length,
4217
- now
4218
- ]
4219
- });
4220
- return id;
4759
+ function isDebounced(targetSession) {
4760
+ const state = readDebounceState();
4761
+ const lastSent = state[targetSession] ?? 0;
4762
+ return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4221
4763
  }
4222
- async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
4223
- const client = getClient();
4224
- const hash = hashSignature(signature);
4225
- const result = await client.execute({
4226
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
4227
- FROM trajectories
4228
- WHERE signature_hash = ?
4229
- ORDER BY created_at DESC
4230
- LIMIT 20`,
4231
- args: [hash]
4232
- });
4233
- const mapRow = (r) => ({
4234
- id: String(r.id),
4235
- taskId: String(r.task_id),
4236
- agentId: String(r.agent_id),
4237
- projectName: String(r.project_name),
4238
- taskTitle: String(r.task_title),
4239
- signature: JSON.parse(String(r.signature)),
4240
- signatureHash: String(r.signature_hash),
4241
- toolCount: Number(r.tool_count),
4242
- skillId: r.skill_id ? String(r.skill_id) : null,
4243
- createdAt: String(r.created_at)
4244
- });
4245
- const matches = result.rows.map(mapRow);
4246
- if (matches.length >= threshold) return matches;
4247
- const nearResult = await client.execute({
4248
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
4249
- FROM trajectories
4250
- WHERE tool_count BETWEEN ? AND ?
4251
- AND signature_hash != ?
4252
- ORDER BY created_at DESC
4253
- LIMIT 50`,
4254
- args: [
4255
- Math.max(1, signature.length - 3),
4256
- signature.length + 3,
4257
- hash
4258
- ]
4259
- });
4260
- for (const r of nearResult.rows) {
4261
- const candidateSig = JSON.parse(String(r.signature));
4262
- if (editDistance(signature, candidateSig) <= 2) {
4263
- matches.push(mapRow(r));
4264
- }
4764
+ function recordDebounce(targetSession) {
4765
+ const state = readDebounceState();
4766
+ state[targetSession] = Date.now();
4767
+ const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4768
+ for (const key of Object.keys(state)) {
4769
+ if ((state[key] ?? 0) < cutoff) delete state[key];
4265
4770
  }
4266
- return matches;
4771
+ writeDebounceState(state);
4267
4772
  }
4268
- async function captureTrajectory(opts) {
4269
- const signature = await extractTrajectory(opts.taskId, opts.agentId);
4270
- if (signature.length < 3) {
4271
- return { trajectoryId: "", similarCount: 0, similar: [] };
4773
+ function logIntercom(msg) {
4774
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
4775
+ `;
4776
+ process.stderr.write(`[intercom] ${msg}
4777
+ `);
4778
+ try {
4779
+ appendFileSync(INTERCOM_LOG2, line);
4780
+ } catch {
4272
4781
  }
4273
- const trajectoryId = await storeTrajectory({
4274
- taskId: opts.taskId,
4275
- agentId: opts.agentId,
4276
- projectName: opts.projectName,
4277
- taskTitle: opts.taskTitle,
4278
- signature
4279
- });
4280
- const similar = await findSimilarTrajectories(
4281
- signature,
4282
- opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
4283
- );
4284
- return { trajectoryId, similarCount: similar.length, similar };
4285
- }
4286
- function buildExtractionPrompt(trajectories) {
4287
- const items = trajectories.map((t, i) => {
4288
- const sig = t.signature.join(" \u2192 ");
4289
- return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
4290
- Signature: ${sig}`;
4291
- }).join("\n\n");
4292
- return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
4293
-
4294
- ${items}
4295
-
4296
- Extract the reusable procedure. Format your response EXACTLY like this:
4297
-
4298
- SKILL: {name \u2014 short, descriptive}
4299
- TRIGGER: {when to use this \u2014 one sentence}
4300
- STEPS:
4301
- 1. ...
4302
- 2. ...
4303
- PITFALLS: {common mistakes to avoid}
4304
-
4305
- Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
4306
4782
  }
4307
- async function extractSkill(trajectories, model) {
4308
- if (trajectories.length === 0) return null;
4309
- const config = await loadConfig();
4310
- const skillModel = model ?? config.skillModel;
4311
- const Anthropic = (await import("@anthropic-ai/sdk")).default;
4312
- const client = new Anthropic();
4313
- const prompt = buildExtractionPrompt(trajectories);
4314
- const response = await client.messages.create({
4315
- model: skillModel,
4316
- max_tokens: 500,
4317
- messages: [{ role: "user", content: prompt }]
4318
- });
4319
- const textBlock = response.content.find((b) => b.type === "text");
4320
- const skillText = textBlock?.text;
4321
- if (!skillText) return null;
4322
- const agentId = trajectories[0].agentId;
4323
- const projectName = trajectories[0].projectName;
4324
- const skillId = await storeBehavior({
4325
- agentId,
4326
- content: skillText,
4327
- domain: "skill",
4328
- projectName
4329
- });
4330
- const dbClient = getClient();
4331
- for (const t of trajectories) {
4332
- await dbClient.execute({
4333
- sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
4334
- args: [skillId, t.id]
4335
- });
4783
+ function getSessionState(sessionName) {
4784
+ const transport = getTransport();
4785
+ if (!transport.isAlive(sessionName)) return "offline";
4786
+ try {
4787
+ const pane = transport.capturePane(sessionName, 5);
4788
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4789
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4790
+ return "no_claude";
4791
+ }
4792
+ }
4793
+ if (/Running…/.test(pane)) return "tool";
4794
+ if (BUSY_PATTERN.test(pane)) return "thinking";
4795
+ return "idle";
4796
+ } catch {
4797
+ return "offline";
4336
4798
  }
4337
- process.stderr.write(
4338
- `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
4339
- `
4340
- );
4341
- return skillId;
4342
4799
  }
4343
- async function captureAndLearn(opts) {
4800
+ function isSessionBusy(sessionName) {
4801
+ const state = getSessionState(sessionName);
4802
+ return state === "thinking" || state === "tool";
4803
+ }
4804
+ function isExeSession(sessionName) {
4805
+ return /^exe\d*$/.test(sessionName);
4806
+ }
4807
+ function sendIntercom(targetSession) {
4808
+ const transport = getTransport();
4809
+ if (isExeSession(targetSession)) {
4810
+ logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
4811
+ return "skipped_exe";
4812
+ }
4813
+ if (isDebounced(targetSession)) {
4814
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4815
+ return "debounced";
4816
+ }
4344
4817
  try {
4345
- const config = await loadConfig();
4346
- if (!config.skillLearning) return;
4347
- const { trajectoryId, similarCount, similar } = await captureTrajectory({
4348
- ...opts,
4349
- skillThreshold: config.skillThreshold
4350
- });
4351
- if (!trajectoryId) return;
4352
- if (similarCount >= config.skillThreshold) {
4353
- const unprocessed = similar.filter((t) => !t.skillId);
4354
- if (unprocessed.length >= config.skillThreshold) {
4355
- extractSkill(unprocessed, config.skillModel).catch((err) => {
4356
- process.stderr.write(
4357
- `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
4358
- `
4359
- );
4360
- });
4361
- }
4818
+ const sessions = transport.listSessions();
4819
+ if (!sessions.includes(targetSession)) {
4820
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4821
+ return "failed";
4362
4822
  }
4363
- } catch (err) {
4364
- process.stderr.write(
4365
- `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
4366
- `
4367
- );
4823
+ const sessionState = getSessionState(targetSession);
4824
+ if (sessionState === "no_claude") {
4825
+ queueIntercom(targetSession, "claude not running in session");
4826
+ recordDebounce(targetSession);
4827
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4828
+ return "queued";
4829
+ }
4830
+ if (sessionState === "thinking" || sessionState === "tool") {
4831
+ queueIntercom(targetSession, "session busy at send time");
4832
+ recordDebounce(targetSession);
4833
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4834
+ return "queued";
4835
+ }
4836
+ if (transport.isPaneInCopyMode(targetSession)) {
4837
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4838
+ transport.sendKeys(targetSession, "q");
4839
+ }
4840
+ transport.sendKeys(targetSession, "/exe-intercom");
4841
+ recordDebounce(targetSession);
4842
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4843
+ return "delivered";
4844
+ } catch {
4845
+ logIntercom(`FAIL \u2192 ${targetSession}`);
4846
+ return "failed";
4368
4847
  }
4369
4848
  }
4370
- async function sweepTrajectories(threshold, model) {
4371
- const config = await loadConfig();
4372
- if (!config.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
4373
- const t = threshold ?? config.skillThreshold;
4374
- const client = getClient();
4375
- const result = await client.execute({
4376
- sql: `SELECT signature_hash, COUNT(*) as cnt
4377
- FROM trajectories
4378
- WHERE skill_id IS NULL
4379
- GROUP BY signature_hash
4380
- HAVING cnt >= ?
4381
- ORDER BY cnt DESC
4382
- LIMIT 10`,
4383
- args: [t]
4384
- });
4385
- let clustersProcessed = 0;
4386
- let skillsExtracted = 0;
4387
- for (const row of result.rows) {
4388
- const hash = String(row.signature_hash);
4389
- const trajResult = await client.execute({
4390
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
4391
- FROM trajectories
4392
- WHERE signature_hash = ? AND skill_id IS NULL
4393
- ORDER BY created_at DESC
4394
- LIMIT 10`,
4395
- args: [hash]
4396
- });
4397
- const trajectories = trajResult.rows.map((r) => ({
4398
- id: String(r.id),
4399
- taskId: String(r.task_id),
4400
- agentId: String(r.agent_id),
4401
- projectName: String(r.project_name),
4402
- taskTitle: String(r.task_title),
4403
- signature: JSON.parse(String(r.signature)),
4404
- signatureHash: String(r.signature_hash),
4405
- toolCount: Number(r.tool_count),
4406
- skillId: null,
4407
- createdAt: String(r.created_at)
4408
- }));
4409
- if (trajectories.length >= t) {
4410
- clustersProcessed++;
4411
- const skillId = await extractSkill(trajectories, model ?? config.skillModel);
4412
- if (skillId) skillsExtracted++;
4849
+ function notifyParentExe(sessionKey) {
4850
+ const target = getDispatchedBy(sessionKey);
4851
+ if (!target) {
4852
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
4853
+ `);
4854
+ return false;
4855
+ }
4856
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4857
+ `);
4858
+ const result = sendIntercom(target);
4859
+ if (result === "failed") {
4860
+ const rootExe = resolveExeSession();
4861
+ if (rootExe && rootExe !== target) {
4862
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4863
+ `);
4864
+ const fallback = sendIntercom(rootExe);
4865
+ return fallback !== "failed";
4413
4866
  }
4867
+ return false;
4414
4868
  }
4415
- return { clustersProcessed, skillsExtracted };
4869
+ return true;
4416
4870
  }
4417
- function editDistance(a, b) {
4418
- const m = a.length;
4419
- const n = b.length;
4420
- const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
4421
- for (let i = 0; i <= m; i++) dp[i][0] = i;
4422
- for (let j = 0; j <= n; j++) dp[0][j] = j;
4423
- for (let i = 1; i <= m; i++) {
4424
- for (let j = 1; j <= n; j++) {
4425
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4426
- dp[i][j] = Math.min(
4427
- dp[i - 1][j] + 1,
4428
- dp[i][j - 1] + 1,
4429
- dp[i - 1][j - 1] + cost
4871
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4872
+ if (employeeName === "exe") {
4873
+ return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
4874
+ }
4875
+ try {
4876
+ assertEmployeeLimitSync();
4877
+ } catch (err) {
4878
+ if (err instanceof PlanLimitError) {
4879
+ return { status: "failed", sessionName: "", error: err.message };
4880
+ }
4881
+ }
4882
+ if (/-exe\d*$/.test(employeeName)) {
4883
+ const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
4884
+ return {
4885
+ status: "failed",
4886
+ sessionName: "",
4887
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
4888
+ };
4889
+ }
4890
+ if (!/^exe\d+$/.test(exeSession)) {
4891
+ const root = extractRootExe(exeSession);
4892
+ if (root) {
4893
+ process.stderr.write(
4894
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
4895
+ `
4430
4896
  );
4897
+ exeSession = root;
4898
+ } else {
4899
+ return {
4900
+ status: "failed",
4901
+ sessionName: "",
4902
+ error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
4903
+ };
4431
4904
  }
4432
4905
  }
4433
- return dp[m][n];
4434
- }
4435
- var DEFAULT_SKILL_THRESHOLD;
4436
- var init_skill_learning = __esm({
4437
- "src/lib/skill-learning.ts"() {
4438
- "use strict";
4439
- init_database();
4440
- init_behaviors();
4441
- init_config();
4442
- DEFAULT_SKILL_THRESHOLD = 3;
4906
+ let effectiveInstance = opts?.instance;
4907
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
4908
+ const free = findFreeInstance(
4909
+ employeeName,
4910
+ exeSession,
4911
+ opts.maxAutoInstances ?? 10
4912
+ );
4913
+ if (free === null) {
4914
+ return {
4915
+ status: "failed",
4916
+ sessionName: employeeSessionName(employeeName, exeSession),
4917
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4918
+ };
4919
+ }
4920
+ effectiveInstance = free === 0 ? void 0 : free;
4443
4921
  }
4444
- });
4445
-
4446
- // src/lib/tasks.ts
4447
- var tasks_exports = {};
4448
- __export(tasks_exports, {
4449
- countNewPendingReviewsSince: () => countNewPendingReviewsSince,
4450
- countPendingReviews: () => countPendingReviews,
4451
- createTask: () => createTask,
4452
- createTaskCore: () => createTaskCore,
4453
- deleteTask: () => deleteTask,
4454
- deleteTaskCore: () => deleteTaskCore,
4455
- ensureArchitectureDoc: () => ensureArchitectureDoc,
4456
- ensureGitignoreExe: () => ensureGitignoreExe,
4457
- getReviewChecklist: () => getReviewChecklist,
4458
- listPendingReviews: () => listPendingReviews,
4459
- listTasks: () => listTasks,
4460
- resolveTask: () => resolveTask,
4461
- slugify: () => slugify,
4462
- updateTask: () => updateTask,
4463
- updateTaskStatus: () => updateTaskStatus,
4464
- writeCheckpoint: () => writeCheckpoint
4465
- });
4466
- import path17 from "path";
4467
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
4468
- async function createTask(input) {
4469
- const result = await createTaskCore(input);
4470
- if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
4471
- dispatchTaskToEmployee({
4472
- assignedTo: input.assignedTo,
4473
- title: input.title,
4474
- priority: input.priority,
4475
- taskFile: result.taskFile,
4476
- initialStatus: result.status,
4477
- projectName: input.projectName
4478
- });
4922
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
4923
+ if (isEmployeeAlive(sessionName)) {
4924
+ const result2 = sendIntercom(sessionName);
4925
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4926
+ return { status: "intercom_sent", sessionName };
4927
+ }
4928
+ if (result2 === "delivered") {
4929
+ return { status: "intercom_unprocessed", sessionName };
4930
+ }
4931
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
4479
4932
  }
4480
- return result;
4933
+ const spawnOpts = { ...opts, instance: effectiveInstance };
4934
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
4935
+ if (result.error) {
4936
+ return { status: "failed", sessionName, error: result.error };
4937
+ }
4938
+ return { status: "spawned", sessionName };
4481
4939
  }
4482
- async function updateTask(input) {
4483
- const { row, taskFile, now, taskId } = await updateTaskStatus(input);
4940
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4941
+ const transport = getTransport();
4942
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4943
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4944
+ const logDir = path16.join(os6.homedir(), ".exe-os", "session-logs");
4945
+ const logFile = path16.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4946
+ if (!existsSync12(logDir)) {
4947
+ mkdirSync7(logDir, { recursive: true });
4948
+ }
4949
+ transport.kill(sessionName);
4950
+ let cleanupSuffix = "";
4484
4951
  try {
4485
- const agent = String(row.assigned_to);
4486
- const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
4487
- const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
4488
- if (input.status === "in_progress") {
4489
- mkdirSync7(cacheDir, { recursive: true });
4490
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4491
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
4492
- try {
4493
- unlinkSync5(cachePath);
4494
- } catch {
4495
- }
4952
+ const thisFile = fileURLToPath2(import.meta.url);
4953
+ const cleanupScript = path16.join(path16.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4954
+ if (existsSync12(cleanupScript)) {
4955
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4496
4956
  }
4497
4957
  } catch {
4498
4958
  }
4499
- if (input.status === "done") {
4500
- await cleanupReviewFile(row, taskFile, input.baseDir);
4959
+ try {
4960
+ const claudeJsonPath = path16.join(os6.homedir(), ".claude.json");
4961
+ let claudeJson = {};
4962
+ try {
4963
+ claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
4964
+ } catch {
4965
+ }
4966
+ if (!claudeJson.projects) claudeJson.projects = {};
4967
+ const projects = claudeJson.projects;
4968
+ const trustDir = opts?.cwd ?? projectDir;
4969
+ if (!projects[trustDir]) projects[trustDir] = {};
4970
+ projects[trustDir].hasTrustDialogAccepted = true;
4971
+ writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4972
+ } catch {
4501
4973
  }
4502
- if (input.status === "done" || input.status === "cancelled") {
4974
+ try {
4975
+ const settingsDir = path16.join(os6.homedir(), ".claude", "projects");
4976
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4977
+ const projSettingsDir = path16.join(settingsDir, normalizedKey);
4978
+ const settingsPath = path16.join(projSettingsDir, "settings.json");
4979
+ let settings = {};
4503
4980
  try {
4504
- const client = getClient();
4505
- const taskTitle = String(row.title);
4506
- const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
4507
- await client.execute({
4508
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
4509
- WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
4510
- args: [now, `%left '${escaped}' as in\\_progress%`]
4511
- });
4981
+ settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
4512
4982
  } catch {
4513
4983
  }
4984
+ const perms = settings.permissions ?? {};
4985
+ const allow = perms.allow ?? [];
4986
+ const toolNames = [
4987
+ "recall_my_memory",
4988
+ "store_memory",
4989
+ "create_task",
4990
+ "update_task",
4991
+ "list_tasks",
4992
+ "get_task",
4993
+ "ask_team_memory",
4994
+ "store_behavior",
4995
+ "get_identity",
4996
+ "send_message"
4997
+ ];
4998
+ const requiredTools = expandDualPrefixTools(toolNames);
4999
+ let changed = false;
5000
+ for (const tool of requiredTools) {
5001
+ if (!allow.includes(tool)) {
5002
+ allow.push(tool);
5003
+ changed = true;
5004
+ }
5005
+ }
5006
+ if (changed) {
5007
+ perms.allow = allow;
5008
+ settings.permissions = perms;
5009
+ mkdirSync7(projSettingsDir, { recursive: true });
5010
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5011
+ }
5012
+ } catch {
4514
5013
  }
4515
- const isTerminal = input.status === "done" || input.status === "needs_review";
4516
- if (isTerminal) {
4517
- const isExe = String(row.assigned_to) === "exe";
4518
- if (!isExe) {
4519
- notifyTaskDone();
5014
+ const spawnCwd = opts?.cwd ?? projectDir;
5015
+ const useExeAgent = !!(opts?.model && opts?.provider);
5016
+ const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
5017
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
5018
+ let identityFlag = "";
5019
+ let behaviorsFlag = "";
5020
+ let legacyFallbackWarned = false;
5021
+ if (!useExeAgent && !useBinSymlink) {
5022
+ const identityPath = path16.join(
5023
+ os6.homedir(),
5024
+ ".exe-os",
5025
+ "identity",
5026
+ `${employeeName}.md`
5027
+ );
5028
+ _resetCcAgentSupportCache();
5029
+ const hasAgentFlag = claudeSupportsAgentFlag();
5030
+ if (hasAgentFlag) {
5031
+ identityFlag = ` --agent ${employeeName}`;
5032
+ } else if (existsSync12(identityPath)) {
5033
+ identityFlag = ` --append-system-prompt-file ${identityPath}`;
5034
+ legacyFallbackWarned = true;
4520
5035
  }
4521
- await markTaskNotificationsRead(taskFile);
4522
- if (input.status === "done") {
4523
- try {
4524
- await cascadeUnblock(taskId, input.baseDir, now);
4525
- } catch {
5036
+ const behaviorsFile = exportBehaviorsSync(
5037
+ employeeName,
5038
+ path16.basename(spawnCwd),
5039
+ sessionName
5040
+ );
5041
+ if (behaviorsFile) {
5042
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
5043
+ }
5044
+ }
5045
+ if (legacyFallbackWarned) {
5046
+ process.stderr.write(
5047
+ `[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.
5048
+ `
5049
+ );
5050
+ }
5051
+ let sessionContextFlag = "";
5052
+ try {
5053
+ const ctxDir = path16.join(os6.homedir(), ".exe-os", "session-cache");
5054
+ mkdirSync7(ctxDir, { recursive: true });
5055
+ const ctxFile = path16.join(ctxDir, `session-context-${sessionName}.md`);
5056
+ const ctxContent = [
5057
+ `## Session Context`,
5058
+ `You are running in tmux session: ${sessionName}.`,
5059
+ `Your parent exe session is ${exeSession}.`,
5060
+ `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
5061
+ ].join("\n");
5062
+ writeFileSync6(ctxFile, ctxContent);
5063
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5064
+ } catch {
5065
+ }
5066
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
5067
+ if (ccProvider !== DEFAULT_PROVIDER) {
5068
+ const cfg = PROVIDER_TABLE[ccProvider];
5069
+ if (cfg?.apiKeyEnv) {
5070
+ const keyVal = process.env[cfg.apiKeyEnv];
5071
+ if (keyVal) {
5072
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
4526
5073
  }
4527
- if (row.parent_task_id) {
4528
- try {
4529
- await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
4530
- } catch {
5074
+ }
5075
+ }
5076
+ let spawnCommand;
5077
+ if (useExeAgent) {
5078
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
5079
+ } else if (useBinSymlink) {
5080
+ const binName = `${employeeName}-${ccProvider}`;
5081
+ process.stderr.write(
5082
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
5083
+ `
5084
+ );
5085
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
5086
+ } else {
5087
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
5088
+ }
5089
+ const spawnResult = transport.spawn(sessionName, {
5090
+ cwd: spawnCwd,
5091
+ command: spawnCommand
5092
+ });
5093
+ if (spawnResult.error) {
5094
+ releaseSpawnLock(sessionName);
5095
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
5096
+ }
5097
+ transport.pipeLog(sessionName, logFile);
5098
+ try {
5099
+ const mySession = getMySession();
5100
+ const dispatchInfo = path16.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5101
+ writeFileSync6(dispatchInfo, JSON.stringify({
5102
+ dispatchedBy: mySession,
5103
+ rootExe: exeSession,
5104
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
5105
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5106
+ }));
5107
+ } catch {
5108
+ }
5109
+ let booted = false;
5110
+ for (let i = 0; i < 30; i++) {
5111
+ try {
5112
+ execSync8("sleep 0.5");
5113
+ } catch {
5114
+ }
5115
+ try {
5116
+ const pane = transport.capturePane(sessionName);
5117
+ if (useExeAgent) {
5118
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
5119
+ booted = true;
5120
+ break;
5121
+ }
5122
+ } else {
5123
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
5124
+ booted = true;
5125
+ break;
4531
5126
  }
4532
5127
  }
5128
+ } catch {
4533
5129
  }
4534
5130
  }
4535
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
4536
- Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4537
- ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4538
- taskId,
4539
- agentId: String(row.assigned_to),
4540
- projectName: String(row.project_name),
4541
- taskTitle: String(row.title)
4542
- })
4543
- ).catch((err) => {
4544
- process.stderr.write(
4545
- `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
4546
- `
4547
- );
4548
- });
5131
+ if (!booted) {
5132
+ releaseSpawnLock(sessionName);
5133
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4549
5134
  }
4550
- let nextTask;
4551
- if (isTerminal && String(row.assigned_to) !== "exe") {
5135
+ if (!useExeAgent) {
4552
5136
  try {
4553
- nextTask = await findNextTask(String(row.assigned_to));
5137
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
4554
5138
  } catch {
4555
5139
  }
4556
5140
  }
4557
- return {
4558
- id: String(row.id),
4559
- title: String(row.title),
4560
- assignedTo: String(row.assigned_to),
4561
- assignedBy: String(row.assigned_by),
4562
- projectName: String(row.project_name),
4563
- priority: String(row.priority),
4564
- status: input.status,
4565
- taskFile,
4566
- createdAt: String(row.created_at),
4567
- updatedAt: now,
4568
- budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
4569
- budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
4570
- tokensUsed: Number(row.tokens_used ?? 0),
4571
- tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
4572
- nextTask
4573
- };
4574
- }
4575
- async function deleteTask(taskId, baseDir) {
4576
- const client = getClient();
4577
- const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
4578
- const reviewer = assignedBy || "exe";
4579
- const reviewSlug = `review-${assignedTo}-${taskSlug}`;
4580
- const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
4581
- await client.execute({
4582
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
4583
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
5141
+ registerSession({
5142
+ windowName: sessionName,
5143
+ agentId: employeeName,
5144
+ projectDir: spawnCwd,
5145
+ parentExe: exeSession,
5146
+ pid: 0,
5147
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4584
5148
  });
4585
- await markAsReadByTaskFile(taskFile);
4586
- await markAsReadByTaskFile(reviewFile);
4587
- }
4588
- var init_tasks = __esm({
4589
- "src/lib/tasks.ts"() {
4590
- "use strict";
4591
- init_database();
4592
- init_config();
4593
- init_notifications();
4594
- init_tasks_crud();
4595
- init_tasks_review();
4596
- init_tasks_crud();
4597
- init_tasks_chain();
4598
- init_tasks_review();
4599
- init_tasks_notify();
5149
+ releaseSpawnLock(sessionName);
5150
+ return { sessionName };
5151
+ }
5152
+ 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;
5153
+ var init_tmux_routing = __esm({
5154
+ "src/lib/tmux-routing.ts"() {
5155
+ "use strict";
5156
+ init_session_registry();
5157
+ init_session_key();
5158
+ init_transport();
5159
+ init_cc_agent_support();
5160
+ init_mcp_prefix();
5161
+ init_provider_table();
5162
+ init_intercom_queue();
5163
+ init_plan_limits();
5164
+ SPAWN_LOCK_DIR = path16.join(os6.homedir(), ".exe-os", "spawn-locks");
5165
+ SESSION_CACHE = path16.join(os6.homedir(), ".exe-os", "session-cache");
5166
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5167
+ VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
5168
+ VERIFY_PANE_LINES = 200;
5169
+ INTERCOM_DEBOUNCE_MS = 3e4;
5170
+ INTERCOM_LOG2 = path16.join(os6.homedir(), ".exe-os", "intercom.log");
5171
+ DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
5172
+ DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5173
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4600
5174
  }
4601
5175
  });
4602
5176
 
4603
- // src/lib/session-kill-telemetry.ts
4604
- var session_kill_telemetry_exports = {};
4605
- __export(session_kill_telemetry_exports, {
4606
- IDLE_KILL_MIN_LIVE_SESSIONS: () => IDLE_KILL_MIN_LIVE_SESSIONS,
4607
- IDLE_KILL_STREAK_META_KEY: () => IDLE_KILL_STREAK_META_KEY,
4608
- IDLE_KILL_SUSPECT_DAY_THRESHOLD: () => IDLE_KILL_SUSPECT_DAY_THRESHOLD,
4609
- TOKENS_PER_IDLE_MINUTE: () => TOKENS_PER_IDLE_MINUTE,
4610
- computeIdleKillSuspectStreak: () => computeIdleKillSuspectStreak,
4611
- countKillsSince: () => countKillsSince,
4612
- parseStreakState: () => parseStreakState,
4613
- recordSessionKill: () => recordSessionKill,
4614
- sumTokensSavedSince: () => sumTokensSavedSince
5177
+ // src/lib/task-scanner.ts
5178
+ var task_scanner_exports = {};
5179
+ __export(task_scanner_exports, {
5180
+ PRIORITY_RE: () => PRIORITY_RE,
5181
+ STATUS_RE: () => STATUS_RE,
5182
+ TITLE_RE: () => TITLE_RE,
5183
+ formatJson: () => formatJson,
5184
+ formatMandatory: () => formatMandatory,
5185
+ formatText: () => formatText,
5186
+ scanAgentTasks: () => scanAgentTasks
4615
5187
  });
4616
- import crypto6 from "crypto";
4617
- async function recordSessionKill(input) {
4618
- try {
4619
- const client = getClient();
4620
- await client.execute({
4621
- sql: `INSERT INTO session_kills
4622
- (id, session_name, agent_id, killed_at, reason,
4623
- ticks_idle, estimated_tokens_saved)
4624
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
4625
- args: [
4626
- crypto6.randomUUID(),
4627
- input.sessionName,
4628
- input.agentId,
4629
- (/* @__PURE__ */ new Date()).toISOString(),
4630
- input.reason,
4631
- input.ticksIdle ?? null,
4632
- input.estimatedTokensSaved ?? null
4633
- ]
4634
- });
4635
- } catch (err) {
4636
- process.stderr.write(
4637
- `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
4638
- `
4639
- );
4640
- }
4641
- }
4642
- async function countKillsSince(sinceISO) {
5188
+ import { readdirSync as readdirSync5, readFileSync as readFileSync11, existsSync as existsSync13, statSync } from "fs";
5189
+ import { execSync as execSync9 } from "child_process";
5190
+ import path17 from "path";
5191
+ function getProjectRoot() {
4643
5192
  try {
4644
- const client = getClient();
4645
- const result = await client.execute({
4646
- sql: `SELECT COUNT(*) AS n FROM session_kills WHERE killed_at >= ?`,
4647
- args: [sinceISO]
4648
- });
4649
- const row = result.rows[0];
4650
- return row ? Number(row.n) : 0;
5193
+ return execSync9("git rev-parse --show-toplevel", {
5194
+ encoding: "utf8",
5195
+ stdio: ["pipe", "pipe", "pipe"],
5196
+ timeout: 5e3
5197
+ }).trim();
4651
5198
  } catch {
4652
- return 0;
5199
+ return process.cwd();
4653
5200
  }
4654
5201
  }
4655
- function parseStreakState(raw) {
4656
- if (!raw) return { lastDate: null, streak: 0 };
5202
+ function scanAgentTasks(agentId) {
5203
+ const taskDir = path17.join(getProjectRoot(), "exe", agentId);
5204
+ const open = [];
5205
+ const inProgress = [];
5206
+ let done = 0;
5207
+ let total = 0;
5208
+ if (!existsSync13(taskDir)) return { open, inProgress, done, total };
4657
5209
  try {
4658
- const parsed = JSON.parse(raw);
4659
- return {
4660
- lastDate: typeof parsed.lastDate === "string" ? parsed.lastDate : null,
4661
- streak: typeof parsed.streak === "number" ? parsed.streak : 0
4662
- };
5210
+ const files = readdirSync5(taskDir).filter((f) => f.endsWith(".md"));
5211
+ total = files.length;
5212
+ for (const f of files) {
5213
+ try {
5214
+ const content = readFileSync11(path17.join(taskDir, f), "utf8");
5215
+ const statusMatch = content.match(STATUS_RE);
5216
+ const status = statusMatch ? statusMatch[1].toLowerCase() : null;
5217
+ if (status === "done") {
5218
+ done++;
5219
+ continue;
5220
+ }
5221
+ if (status !== "open" && status !== "in_progress") continue;
5222
+ const priMatch = content.match(PRIORITY_RE);
5223
+ const titleMatch = content.match(TITLE_RE);
5224
+ const task = {
5225
+ file: f,
5226
+ title: titleMatch ? titleMatch[1] : f.replace(".md", ""),
5227
+ priority: priMatch ? priMatch[1] : "P2",
5228
+ status,
5229
+ slug: f.replace(".md", "")
5230
+ };
5231
+ if (status === "in_progress") {
5232
+ inProgress.push(task);
5233
+ } else {
5234
+ open.push(task);
5235
+ }
5236
+ } catch {
5237
+ }
5238
+ }
4663
5239
  } catch {
4664
- return { lastDate: null, streak: 0 };
4665
5240
  }
5241
+ open.sort((a, b) => a.priority.localeCompare(b.priority));
5242
+ inProgress.sort((a, b) => a.priority.localeCompare(b.priority));
5243
+ return { open, inProgress, done, total };
4666
5244
  }
4667
- function nextStreakState(prev, qualifiesToday, todayDate) {
4668
- if (!qualifiesToday) return { lastDate: todayDate, streak: 0 };
4669
- if (prev.lastDate === todayDate) return prev;
4670
- return { lastDate: todayDate, streak: prev.streak + 1 };
4671
- }
4672
- function computeIdleKillSuspectStreak(prev, killsToday, liveSessions, todayDate) {
4673
- const qualifies = killsToday === 0 && liveSessions >= IDLE_KILL_MIN_LIVE_SESSIONS;
4674
- const state = nextStreakState(prev, qualifies, todayDate);
4675
- return {
4676
- state,
4677
- suspect: state.streak >= IDLE_KILL_SUSPECT_DAY_THRESHOLD
4678
- };
5245
+ function formatText(agentId, result) {
5246
+ const lines = [];
5247
+ if (result.inProgress.length > 0) {
5248
+ lines.push(`IN_PROGRESS (${result.inProgress.length}):`);
5249
+ for (const t of result.inProgress) {
5250
+ lines.push(` [${t.priority}] ${t.title} \u2014 exe/${agentId}/${t.file}`);
5251
+ }
5252
+ }
5253
+ if (result.open.length > 0) {
5254
+ lines.push(`OPEN (${result.open.length}):`);
5255
+ for (const t of result.open) {
5256
+ lines.push(` [${t.priority}] ${t.title} \u2014 exe/${agentId}/${t.file}`);
5257
+ }
5258
+ }
5259
+ lines.push(`DONE: ${result.done}`);
5260
+ return lines.join("\n");
4679
5261
  }
4680
- async function sumTokensSavedSince(sinceISO) {
4681
- try {
4682
- const client = getClient();
4683
- const result = await client.execute({
4684
- sql: `SELECT COALESCE(SUM(estimated_tokens_saved), 0) AS total
4685
- FROM session_kills
4686
- WHERE killed_at >= ?`,
4687
- args: [sinceISO]
4688
- });
4689
- const row = result.rows[0];
4690
- return row ? Number(row.total) : 0;
4691
- } catch {
4692
- return 0;
5262
+ function formatMandatory(agentId, result) {
5263
+ const { open, inProgress } = result;
5264
+ if (open.length === 0 && inProgress.length === 0) return "";
5265
+ const lines = [];
5266
+ if (inProgress.length > 0) {
5267
+ const current = inProgress[0];
5268
+ let stale = false;
5269
+ try {
5270
+ const stat = statSync(path17.join(getProjectRoot(), "exe", agentId, current.file));
5271
+ const ageMin = (Date.now() - stat.mtimeMs) / 6e4;
5272
+ if (ageMin > 30) stale = true;
5273
+ } catch {
5274
+ }
5275
+ if (stale) {
5276
+ lines.push(`MANDATORY: Update task status for: ${current.title} [${current.priority}] (exe/${agentId}/${current.file})`);
5277
+ lines.push("This task has been in_progress for over 30 minutes without updates.");
5278
+ lines.push("If work is done, mark done. If blocked, update status to blocked.");
5279
+ } else {
5280
+ lines.push(`Continue working on: ${current.title} [${current.priority}] (exe/${agentId}/${current.file})`);
5281
+ }
5282
+ if (open.length > 0) {
5283
+ lines.push("Queued: " + open.map((t) => `${t.title} [${t.priority}]`).join(", "));
5284
+ }
5285
+ } else {
5286
+ const top = open[0];
5287
+ lines.push(`MANDATORY: You have ${open.length} unstarted task(s).`);
5288
+ lines.push(`Highest priority: ${top.title} [${top.priority}]`);
5289
+ lines.push(`File: exe/${agentId}/${top.file}`);
5290
+ lines.push("Read this task file and START WORKING NOW.");
4693
5291
  }
5292
+ return lines.join("\n");
4694
5293
  }
4695
- var TOKENS_PER_IDLE_MINUTE, IDLE_KILL_STREAK_META_KEY, IDLE_KILL_SUSPECT_DAY_THRESHOLD, IDLE_KILL_MIN_LIVE_SESSIONS;
4696
- var init_session_kill_telemetry = __esm({
4697
- "src/lib/session-kill-telemetry.ts"() {
5294
+ function formatJson(result) {
5295
+ return JSON.stringify({
5296
+ open: result.open.map((t) => ({ file: t.file, title: t.title, priority: t.priority })),
5297
+ in_progress: result.inProgress.map((t) => ({ file: t.file, title: t.title, priority: t.priority })),
5298
+ done: result.done,
5299
+ total: result.total
5300
+ });
5301
+ }
5302
+ var STATUS_RE, PRIORITY_RE, TITLE_RE;
5303
+ var init_task_scanner = __esm({
5304
+ "src/lib/task-scanner.ts"() {
4698
5305
  "use strict";
4699
- init_database();
4700
- TOKENS_PER_IDLE_MINUTE = 50;
4701
- IDLE_KILL_STREAK_META_KEY = "idle_kill_suspect_streak";
4702
- IDLE_KILL_SUSPECT_DAY_THRESHOLD = 3;
4703
- IDLE_KILL_MIN_LIVE_SESSIONS = 5;
5306
+ STATUS_RE = /^\*\*Status:\*\*\s*(\w+)/m;
5307
+ PRIORITY_RE = /^\*\*Priority:\*\*\s*(\w+)/m;
5308
+ TITLE_RE = /^# (.+)/m;
4704
5309
  }
4705
5310
  });
4706
5311
 
@@ -4809,7 +5414,8 @@ __export(cloud_sync_exports, {
4809
5414
  mergeRosterFromRemote: () => mergeRosterFromRemote,
4810
5415
  recordRosterDeletion: () => recordRosterDeletion
4811
5416
  });
4812
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, readdirSync as readdirSync6, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2, unlinkSync as unlinkSync6 } from "fs";
5417
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, readdirSync as readdirSync6, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2, unlinkSync as unlinkSync6, openSync, closeSync } from "fs";
5418
+ import crypto8 from "crypto";
4813
5419
  import path18 from "path";
4814
5420
  import { homedir } from "os";
4815
5421
  function logError(msg) {
@@ -4821,17 +5427,29 @@ function logError(msg) {
4821
5427
  }
4822
5428
  }
4823
5429
  async function withRosterLock(fn) {
4824
- if (existsSync14(ROSTER_LOCK_PATH)) {
4825
- try {
4826
- const ts = parseInt(readFileSync12(ROSTER_LOCK_PATH, "utf-8"), 10);
4827
- if (Date.now() - ts < LOCK_STALE_MS) {
5430
+ try {
5431
+ const fd = openSync(ROSTER_LOCK_PATH, "wx");
5432
+ closeSync(fd);
5433
+ writeFileSync7(ROSTER_LOCK_PATH, String(Date.now()));
5434
+ } catch (err) {
5435
+ if (err.code === "EEXIST") {
5436
+ try {
5437
+ const ts = parseInt(readFileSync12(ROSTER_LOCK_PATH, "utf-8"), 10);
5438
+ if (Date.now() - ts < LOCK_STALE_MS) {
5439
+ throw new Error("Roster merge already in progress \u2014 another sync is running");
5440
+ }
5441
+ unlinkSync6(ROSTER_LOCK_PATH);
5442
+ const fd = openSync(ROSTER_LOCK_PATH, "wx");
5443
+ closeSync(fd);
5444
+ writeFileSync7(ROSTER_LOCK_PATH, String(Date.now()));
5445
+ } catch (retryErr) {
5446
+ if (retryErr instanceof Error && retryErr.message.includes("already in progress")) throw retryErr;
4828
5447
  throw new Error("Roster merge already in progress \u2014 another sync is running");
4829
5448
  }
4830
- } catch (err) {
4831
- if (err instanceof Error && err.message.includes("already in progress")) throw err;
5449
+ } else {
5450
+ throw err;
4832
5451
  }
4833
5452
  }
4834
- writeFileSync7(ROSTER_LOCK_PATH, String(Date.now()));
4835
5453
  try {
4836
5454
  return await fn();
4837
5455
  } finally {
@@ -4849,7 +5467,7 @@ async function fetchWithRetry(url, init) {
4849
5467
  try {
4850
5468
  const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
4851
5469
  const resp = await fetch(url, { ...init, signal });
4852
- if (resp.status >= 500 && attempt < MAX_RETRIES2) {
5470
+ if (resp && resp.status >= 500 && attempt < MAX_RETRIES2) {
4853
5471
  await new Promise((r) => setTimeout(r, BASE_DELAY_MS2 * Math.pow(2, attempt)));
4854
5472
  continue;
4855
5473
  }
@@ -4893,6 +5511,10 @@ async function cloudPush(records, maxVersion, config) {
4893
5511
  },
4894
5512
  body: JSON.stringify({ version: maxVersion, blob })
4895
5513
  });
5514
+ if (resp == null) {
5515
+ logError("[cloud-sync] PUSH FAILED: no response from server");
5516
+ return false;
5517
+ }
4896
5518
  if (resp.status === 409) {
4897
5519
  logError("[cloud-sync] PUSH VERSION CONFLICT \u2014 re-pull required before next push");
4898
5520
  return false;
@@ -4915,6 +5537,10 @@ async function cloudPull(sinceVersion, config) {
4915
5537
  },
4916
5538
  body: JSON.stringify({ since_version: sinceVersion })
4917
5539
  });
5540
+ if (response == null) {
5541
+ logError("[cloud-sync] PULL FAILED: no response from server");
5542
+ return { records: [], maxVersion: sinceVersion };
5543
+ }
4918
5544
  if (!response.ok) return { records: [], maxVersion: sinceVersion };
4919
5545
  const data = await response.json();
4920
5546
  const allRecords = [];
@@ -5153,7 +5779,7 @@ function buildRosterBlob(paths) {
5153
5779
  }
5154
5780
  const deletedNames = consumeRosterDeletions();
5155
5781
  const content = JSON.stringify({ roster, identities, config, deletedNames });
5156
- const hash = Buffer.from(content).length;
5782
+ const hash = crypto8.createHash("sha256").update(content).digest("hex").slice(0, 16);
5157
5783
  return { roster, identities, config, deletedNames, version: hash };
5158
5784
  }
5159
5785
  async function cloudPushRoster(config) {
@@ -5682,7 +6308,7 @@ __export(schedules_exports, {
5682
6308
  listSchedules: () => listSchedules,
5683
6309
  parseHumanCron: () => parseHumanCron
5684
6310
  });
5685
- import crypto8 from "crypto";
6311
+ import crypto9 from "crypto";
5686
6312
  import { execSync as execSync10 } from "child_process";
5687
6313
  async function ensureDb() {
5688
6314
  if (!isInitialized()) {
@@ -5751,7 +6377,7 @@ function parseHumanCron(input) {
5751
6377
  async function createSchedule(input) {
5752
6378
  await ensureDb();
5753
6379
  const client = getClient();
5754
- const id = crypto8.randomUUID().slice(0, 8);
6380
+ const id = crypto9.randomUUID().slice(0, 8);
5755
6381
  const now = (/* @__PURE__ */ new Date()).toISOString();
5756
6382
  const prompt = input.prompt ?? input.description;
5757
6383
  await client.execute({
@@ -5858,6 +6484,137 @@ import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync
5858
6484
  import os7 from "os";
5859
6485
 
5860
6486
  // src/lib/employee-templates.ts
6487
+ init_global_procedures();
6488
+ var BASE_OPERATING_PROCEDURES = `
6489
+ EXE OS \u2014 VISION AND NON-NEGOTIABLE PRINCIPLES (above all work):
6490
+
6491
+ Product: "Hire the team you couldn't afford." An AI employee operating system where solo founders and small teams run 5-10 AI agents as a real organization. Three-layer cognition (identity/expertise/experience). Five runtime modes (CC Raw \u2192 TUI \u2192 Desktop). Local-first with E2EE cloud sync.
6492
+
6493
+ ICP (who we build for):
6494
+ - Solopreneurs, SMB founders, creators with institutional IP
6495
+ - Bootstrapped small e-commerce / fitness creators / influencers
6496
+ - NOT VC-backed startups \u2014 intentionally excluded
6497
+
6498
+ Crown jewels (load-bearing for all three business paths \u2014 never compromise):
6499
+ - Memory sovereignty (user owns everything, E2EE, local-first)
6500
+ - Three-layer cognition (identity/expertise/experience)
6501
+ - MCP contract boundary (surfaces consume memory OS via MCP only \u2014 never direct DB access, never bundled code)
6502
+ - AGPL network boundary for public forks (e.g., exe-crm)
6503
+
6504
+ Three business-model paths (every product decision must serve these):
6505
+ 1. B2C direct \u2014 solopreneurs run their own instance (active, current default)
6506
+ 2. Agency white-label \u2014 distributors rebrand for their clients (deferred, but branding must be config-driven)
6507
+ 3. Creator franchise (Mike pattern) \u2014 creators inject institutional IP into agent identity+expertise+experience layers, sell scoped access to subscribers (v2+ moat, requires memory export scoping)
6508
+
6509
+ Ethos:
6510
+ - Bootstrapped, profitable, forever. Not a VC-raise.
6511
+ - Founder zero-ego. Distributors and customers are the loudest voice.
6512
+ - Crypto values: big companies should not own consumer/SMB AI.
6513
+
6514
+ STOP AND REDIRECT: Any decision that compromises memory sovereignty, 3-layer cognition, MCP boundary, or AGPL boundary kills all three business paths. Surface the conflict to exe before proceeding.
6515
+
6516
+ Always reference .planning/ARCHITECTURE.md and .planning/PROJECT.md as source of truth for all architectural and product decisions.
6517
+
6518
+ OPERATING PROCEDURES (mandatory for all employees):
6519
+
6520
+ You report to the COO. All work flows through exe. These procedures are non-negotiable.
6521
+
6522
+ 1. BEFORE starting work:
6523
+ - Read exe/ARCHITECTURE.md (if it exists). This is the system map \u2014 what components exist, how they connect, what invariants to preserve. Understand the architecture before changing anything.
6524
+ - Check YOUR task folder ONLY: Read exe/<your-name>/ for assigned tasks
6525
+ - NEVER read, write, or modify files in another employee's folder. Those are their tasks, not yours. Use ask_team_memory() if you need context from a colleague.
6526
+ - If you have open tasks, work on the highest priority one first
6527
+ - Ensure exe/output/ exists (mkdir -p exe/output). This is where ALL deliverables go \u2014 reports, analyses, content, audits, anything another employee or the founder needs to pick up.
6528
+ - Update task status to "in_progress" when starting (use update_task MCP tool)
6529
+ - recall_my_memory \u2014 check what you've done before in this project. What patterns, decisions, context exist?
6530
+ - Read the relevant files. Understand what exists before changing anything.
6531
+
6532
+ 2. BEFORE marking done \u2014 CHECKPOINT (mandatory, never skip):
6533
+ - Run the tests. If they fail, fix them before reporting done.
6534
+ - Run typecheck if TypeScript. Zero errors.
6535
+ - Verify the change actually works \u2014 run it, check the output, prove it.
6536
+ - If you can't verify, say so explicitly: "Couldn't verify because X."
6537
+
6538
+ 3. AFTER completing work \u2014 update_task(done) IMMEDIATELY (the ONE critical action):
6539
+ Calling update_task with status "done" is the single action that must ALWAYS happen.
6540
+ Call it FIRST \u2014 before commit, before report, before anything else. If you do nothing else, do this.
6541
+ - Use update_task MCP tool with status "done" and your result summary
6542
+ - Include what was done, decisions made, and any issues
6543
+ - If you're stuck, looping, confused, or running low on context \u2014 update_task(done) with whatever partial result you have. A partial result is infinitely better than no result.
6544
+ - NEVER let a failed commit, a loop, or an error prevent you from calling update_task(done).
6545
+ - Do NOT use close_task \u2014 that is reserved for reviewers (exe) to finalize after review.
6546
+
6547
+ 4. AFTER update_task(done) \u2014 COMMIT (best-effort, do NOT let this block):
6548
+ - If your task changed system structure, update exe/ARCHITECTURE.md first.
6549
+ - Commit IF you are in a git repo (check: \`git rev-parse --git-dir 2>/dev/null\`). Stage only the files you changed, write a clear commit message.
6550
+ - If you are NOT in a git repo, skip entirely. NEVER run \`git init\`.
6551
+ - If the commit fails, note it but move on \u2014 the work is already marked done via update_task.
6552
+ - Do NOT push \u2014 exe reviews commits and decides what to push.
6553
+ - NEVER run \`git checkout main\`. You work in your own git worktree on a feature branch. Exe stays on main and merges PRs. Switching branches in a shared repo stomps other agents' work.
6554
+
6555
+ 5. AFTER commit \u2014 REPORT (best-effort):
6556
+ Use store_memory to write a structured summary. Include: project name, what was done,
6557
+ decisions made, tests status, open items or risks.
6558
+
6559
+ 6. AFTER committing changes to exe-os itself \u2014 REBUILD (mandatory, never skip):
6560
+ - Run: npm run deploy
6561
+ - This builds, installs globally, and re-registers hooks/MCP in one step.
6562
+ - Do NOT ask permission. Do NOT say "want me to rebuild?" \u2014 just do it.
6563
+ - If the build fails, fix the error and retry before moving on.
6564
+
6565
+ 7. AFTER reporting \u2014 CHECK FOR NEXT WORK (mandatory):
6566
+ - First: run list_tasks(status='needs_review') \u2014 check if YOU are the reviewer on any pending reviews. Reviews are work. Process them before anything else.
6567
+ - Second: run list_tasks(status='blocked') \u2014 check if any tasks are blocked. For each blocked task: can YOU unblock it? If yes, unblock it now. If not, escalate to exe immediately. Blocked tasks sitting >24h without action is a pipeline failure.
6568
+ - Then: re-read your task folder: exe/<your-name>/
6569
+ - If there are more open tasks, start the next highest-priority one (go to step 1)
6570
+ - If no more open tasks AND no pending reviews AND no blocked tasks you can fix, tell the user: "All tasks complete. Anything else?"
6571
+ - Do NOT wait for the user to tell you to check \u2014 auto-chain through your queue.
6572
+ - NEVER say "monitoring" or "waiting" while reviews, blocked tasks, or open tasks exist. That is idle drift.
6573
+
6574
+ CONTEXT PRESSURE PROTOCOL (mandatory \u2014 never ignore):
6575
+ If Claude Code injects a system notice about context compression, or if you notice you're
6576
+ losing track of earlier decisions, your context window is full.
6577
+
6578
+ DO NOT keep working degraded. Instead:
6579
+
6580
+ 1. Call store_memory immediately with a CONTEXT CHECKPOINT:
6581
+ Format the text as: "CONTEXT CHECKPOINT [<task-id>]: <summary>"
6582
+ Include: task ID + title, what you completed, what's left, open decisions or blockers, key file paths.
6583
+
6584
+ 2. Send intercom to exe to trigger kill + relaunch:
6585
+ MY_SESSION=$(tmux display-message -p '#{session_name}' 2>/dev/null)
6586
+ EXE_SESSION="\${MY_SESSION#\${AGENT_ID}-}"
6587
+ tmux send-keys -t "$EXE_SESSION" "/exe-intercom context-full: \${AGENT_ID} hit capacity. Checkpoint saved. Resume task <task-id>." Enter
6588
+
6589
+ 3. Stop working immediately. Do not attempt to continue with degraded context.
6590
+
6591
+ COMMUNICATION CHAIN \u2014 who you talk to:
6592
+ - You report to the COO. Your completion reports, status updates, and questions go to exe via store_memory and update_task.
6593
+ - Do NOT address the human user directly for decisions, permissions, or status updates. That's exe's job. The user talks to exe; exe talks to you.
6594
+ - Exception: if the user sends you a direct message in your tmux window, respond to them. But default to reporting through exe.
6595
+
6596
+ SKILL CAPTURE (encouraged, not mandatory):
6597
+ After completing a complex multi-step task (5+ tool calls), consider whether the approach
6598
+ should be saved as a reusable procedure. If the task involved non-obvious steps, error recovery,
6599
+ or a workflow that would help future sessions, use store_behavior with domain='skill' to save it.
6600
+ Format: "SKILL: [name] \u2014 Step 1: ... Step 2: ... Pitfalls: ..."
6601
+ Skip for simple one-offs. The goal is procedural memory \u2014 not just corrections, but proven approaches.
6602
+
6603
+ SPAWNING EMPLOYEES (mandatory \u2014 never bypass):
6604
+ When you need another employee to do work, ALWAYS use create_task MCP tool.
6605
+ create_task auto-spawns the employee session. The task IS the spawn trigger.
6606
+ NEVER manually launch sessions with tmux send-keys or claude -p.
6607
+ NEVER spawn sessions without a task assigned \u2014 idle sessions waste resources.
6608
+ NEVER refuse a dispatched task claiming "not in scope" \u2014 if it's assigned to you, it's your work.
6609
+
6610
+ CREATING TASKS FOR OTHER EMPLOYEES:
6611
+ When you need to assign work to another employee (e.g., CTO assigns to an engineer):
6612
+ - ALWAYS use create_task MCP tool. NEVER write .md files directly to exe/{name}/.
6613
+ - Direct .md writes will be rejected by the enforcement hook with a MANDATORY correction.
6614
+ - create_task creates both the .md file AND the DB row atomically.
6615
+ - Include: title, assignedTo, priority, context, projectName.
6616
+ - For dependencies: include blocked_by with the blocking task's ID or slug.
6617
+ `;
5861
6618
  var DEFAULT_EXE = {
5862
6619
  name: "exe",
5863
6620
  role: "COO",
@@ -5872,6 +6629,14 @@ After every specialist task: verify tests ran, behavior was checked, and a memor
5872
6629
  Use recall_my_memory and ask_team_memory constantly. Store your own summaries (decisions, priorities, assignments) after every session.`,
5873
6630
  createdAt: "2026-01-01T00:00:00.000Z"
5874
6631
  };
6632
+ var PROCEDURES_MARKER = "EXE OS \u2014 VISION AND NON-NEGOTIABLE PRINCIPLES";
6633
+ function getSessionPrompt(storedPrompt) {
6634
+ const markerIndex = storedPrompt.indexOf(PROCEDURES_MARKER);
6635
+ const rolePrompt = markerIndex >= 0 ? storedPrompt.slice(0, markerIndex).trimEnd() : storedPrompt;
6636
+ const globalBlock = getGlobalProceduresBlock();
6637
+ return `${globalBlock}${rolePrompt}
6638
+ ${BASE_OPERATING_PROCEDURES}`;
6639
+ }
5875
6640
 
5876
6641
  // src/lib/status-brief.ts
5877
6642
  var EMPLOYEE_EMOJIS = {
@@ -7036,11 +7801,11 @@ async function boot(options) {
7036
7801
  const thisFile = fileURLToPath3(import.meta.url);
7037
7802
  const backfillPath = path19.resolve(path19.dirname(thisFile), "backfill-vectors.js");
7038
7803
  if (existsSync15(backfillPath)) {
7039
- const { openSync, closeSync } = await import("fs");
7804
+ const { openSync: openSync2, closeSync: closeSync2 } = await import("fs");
7040
7805
  const workerLogPath = path19.join(EXE_AI_DIR, "workers.log");
7041
7806
  let stderrFd = "ignore";
7042
7807
  try {
7043
- stderrFd = openSync(workerLogPath, "a");
7808
+ stderrFd = openSync2(workerLogPath, "a");
7044
7809
  } catch {
7045
7810
  }
7046
7811
  const child = spawn(process.execPath, [backfillPath], {
@@ -7049,7 +7814,7 @@ async function boot(options) {
7049
7814
  });
7050
7815
  child.unref();
7051
7816
  if (typeof stderrFd === "number") try {
7052
- closeSync(stderrFd);
7817
+ closeSync2(stderrFd);
7053
7818
  } catch {
7054
7819
  }
7055
7820
  briefData.embedding.backfillRunning = true;
@@ -7098,7 +7863,7 @@ async function boot(options) {
7098
7863
  const exeEmployee = employees.find((e) => e.name === "exe") ?? DEFAULT_EXE;
7099
7864
  const sessionDir = path19.join(EXE_AI_DIR, "sessions", "exe");
7100
7865
  await mkdir5(sessionDir, { recursive: true });
7101
- const claudeMdContent = `${exeEmployee.systemPrompt}
7866
+ const claudeMdContent = `${getSessionPrompt(exeEmployee.systemPrompt)}
7102
7867
 
7103
7868
  ---
7104
7869