@askexenow/exe-os 0.8.41 → 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 (74) 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 +1326 -655
  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 +2508 -1802
  9. package/dist/bin/exe-call.js +39 -1
  10. package/dist/bin/exe-dispatch.js +39 -2
  11. package/dist/bin/exe-doctor.js +790 -633
  12. package/dist/bin/exe-export-behaviors.js +792 -637
  13. package/dist/bin/exe-forget.js +145 -0
  14. package/dist/bin/exe-gateway.js +2487 -1878
  15. package/dist/bin/exe-heartbeat.js +147 -1
  16. package/dist/bin/exe-kill.js +795 -640
  17. package/dist/bin/exe-launch-agent.js +2168 -2008
  18. package/dist/bin/exe-link.js +9 -1
  19. package/dist/bin/exe-new-employee.js +6 -2
  20. package/dist/bin/exe-pending-messages.js +146 -1
  21. package/dist/bin/exe-pending-notifications.js +788 -631
  22. package/dist/bin/exe-pending-reviews.js +147 -1
  23. package/dist/bin/exe-rename.js +23 -0
  24. package/dist/bin/exe-review.js +490 -327
  25. package/dist/bin/exe-search.js +154 -3
  26. package/dist/bin/exe-session-cleanup.js +2466 -413
  27. package/dist/bin/exe-status.js +474 -317
  28. package/dist/bin/exe-team.js +474 -317
  29. package/dist/bin/git-sweep.js +2690 -150
  30. package/dist/bin/graph-backfill.js +794 -637
  31. package/dist/bin/graph-export.js +798 -641
  32. package/dist/bin/scan-tasks.js +2951 -44
  33. package/dist/bin/setup.js +47 -25
  34. package/dist/bin/shard-migrate.js +792 -637
  35. package/dist/bin/wiki-sync.js +794 -637
  36. package/dist/gateway/index.js +2504 -1895
  37. package/dist/hooks/bug-report-worker.js +2118 -576
  38. package/dist/hooks/commit-complete.js +2689 -149
  39. package/dist/hooks/error-recall.js +154 -3
  40. package/dist/hooks/ingest-worker.js +1420 -814
  41. package/dist/hooks/instructions-loaded.js +151 -0
  42. package/dist/hooks/notification.js +153 -2
  43. package/dist/hooks/post-compact.js +164 -0
  44. package/dist/hooks/pre-compact.js +3073 -101
  45. package/dist/hooks/pre-tool-use.js +151 -0
  46. package/dist/hooks/prompt-ingest-worker.js +1700 -1541
  47. package/dist/hooks/prompt-submit.js +2658 -1113
  48. package/dist/hooks/response-ingest-worker.js +151 -5
  49. package/dist/hooks/session-end.js +153 -2
  50. package/dist/hooks/session-start.js +154 -3
  51. package/dist/hooks/stop.js +151 -0
  52. package/dist/hooks/subagent-stop.js +151 -0
  53. package/dist/hooks/summary-worker.js +160 -6
  54. package/dist/index.js +278 -100
  55. package/dist/lib/cloud-sync.js +9 -1
  56. package/dist/lib/consolidation.js +69 -2
  57. package/dist/lib/database.js +19 -0
  58. package/dist/lib/device-registry.js +19 -0
  59. package/dist/lib/employee-templates.js +20 -1
  60. package/dist/lib/exe-daemon.js +236 -16
  61. package/dist/lib/hybrid-search.js +154 -3
  62. package/dist/lib/messaging.js +39 -2
  63. package/dist/lib/schedules.js +792 -637
  64. package/dist/lib/store.js +796 -636
  65. package/dist/lib/tasks.js +1614 -1091
  66. package/dist/lib/tmux-routing.js +149 -9
  67. package/dist/mcp/server.js +1810 -1137
  68. package/dist/mcp/tools/create-task.js +2280 -828
  69. package/dist/mcp/tools/list-tasks.js +2788 -159
  70. package/dist/mcp/tools/send-message.js +39 -2
  71. package/dist/mcp/tools/update-task.js +64 -0
  72. package/dist/runtime/index.js +235 -67
  73. package/dist/tui/App.js +1440 -646
  74. package/package.json +3 -2
@@ -416,6 +416,13 @@ async function ensureSchema() {
416
416
  });
417
417
  } catch {
418
418
  }
419
+ try {
420
+ await client.execute({
421
+ sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
422
+ args: []
423
+ });
424
+ } catch {
425
+ }
419
426
  try {
420
427
  await client.execute({
421
428
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -862,6 +869,18 @@ async function ensureSchema() {
862
869
  CREATE INDEX IF NOT EXISTS idx_session_kills_agent
863
870
  ON session_kills(agent_id);
864
871
  `);
872
+ await client.execute(`
873
+ CREATE TABLE IF NOT EXISTS global_procedures (
874
+ id TEXT PRIMARY KEY,
875
+ title TEXT NOT NULL,
876
+ content TEXT NOT NULL,
877
+ priority TEXT NOT NULL DEFAULT 'p0',
878
+ domain TEXT,
879
+ active INTEGER NOT NULL DEFAULT 1,
880
+ created_at TEXT NOT NULL,
881
+ updated_at TEXT NOT NULL
882
+ )
883
+ `);
865
884
  await client.executeMultiple(`
866
885
  CREATE TABLE IF NOT EXISTS conversations (
867
886
  id TEXT PRIMARY KEY,
@@ -1267,6 +1286,61 @@ var init_config = __esm({
1267
1286
  }
1268
1287
  });
1269
1288
 
1289
+ // src/lib/state-bus.ts
1290
+ var StateBus, orgBus;
1291
+ var init_state_bus = __esm({
1292
+ "src/lib/state-bus.ts"() {
1293
+ "use strict";
1294
+ StateBus = class {
1295
+ handlers = /* @__PURE__ */ new Map();
1296
+ globalHandlers = /* @__PURE__ */ new Set();
1297
+ /** Emit an event to all subscribers */
1298
+ emit(event) {
1299
+ const typeHandlers = this.handlers.get(event.type);
1300
+ if (typeHandlers) {
1301
+ for (const handler of typeHandlers) {
1302
+ try {
1303
+ handler(event);
1304
+ } catch {
1305
+ }
1306
+ }
1307
+ }
1308
+ for (const handler of this.globalHandlers) {
1309
+ try {
1310
+ handler(event);
1311
+ } catch {
1312
+ }
1313
+ }
1314
+ }
1315
+ /** Subscribe to a specific event type */
1316
+ on(type, handler) {
1317
+ if (!this.handlers.has(type)) {
1318
+ this.handlers.set(type, /* @__PURE__ */ new Set());
1319
+ }
1320
+ this.handlers.get(type).add(handler);
1321
+ }
1322
+ /** Subscribe to ALL events */
1323
+ onAny(handler) {
1324
+ this.globalHandlers.add(handler);
1325
+ }
1326
+ /** Unsubscribe from a specific event type */
1327
+ off(type, handler) {
1328
+ this.handlers.get(type)?.delete(handler);
1329
+ }
1330
+ /** Unsubscribe from ALL events */
1331
+ offAny(handler) {
1332
+ this.globalHandlers.delete(handler);
1333
+ }
1334
+ /** Remove all listeners */
1335
+ clear() {
1336
+ this.handlers.clear();
1337
+ this.globalHandlers.clear();
1338
+ }
1339
+ };
1340
+ orgBus = new StateBus();
1341
+ }
1342
+ });
1343
+
1270
1344
  // src/lib/shard-manager.ts
1271
1345
  var shard_manager_exports = {};
1272
1346
  __export(shard_manager_exports, {
@@ -1508,6 +1582,71 @@ var init_shard_manager = __esm({
1508
1582
  }
1509
1583
  });
1510
1584
 
1585
+ // src/lib/global-procedures.ts
1586
+ var global_procedures_exports = {};
1587
+ __export(global_procedures_exports, {
1588
+ deactivateGlobalProcedure: () => deactivateGlobalProcedure,
1589
+ getGlobalProceduresBlock: () => getGlobalProceduresBlock,
1590
+ loadGlobalProcedures: () => loadGlobalProcedures,
1591
+ storeGlobalProcedure: () => storeGlobalProcedure
1592
+ });
1593
+ import { randomUUID } from "crypto";
1594
+ async function loadGlobalProcedures() {
1595
+ const client = getClient();
1596
+ const result = await client.execute({
1597
+ sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
1598
+ args: []
1599
+ });
1600
+ const procedures = result.rows;
1601
+ if (procedures.length > 0) {
1602
+ _cache = procedures.map((p) => `### ${p.title}
1603
+ ${p.content}`).join("\n\n");
1604
+ } else {
1605
+ _cache = "";
1606
+ }
1607
+ _cacheLoaded = true;
1608
+ return procedures;
1609
+ }
1610
+ function getGlobalProceduresBlock() {
1611
+ if (!_cacheLoaded) return "";
1612
+ if (!_cache) return "";
1613
+ return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
1614
+
1615
+ ${_cache}
1616
+ `;
1617
+ }
1618
+ async function storeGlobalProcedure(input2) {
1619
+ const id = randomUUID();
1620
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1621
+ const client = getClient();
1622
+ await client.execute({
1623
+ sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
1624
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
1625
+ args: [id, input2.title, input2.content, input2.priority ?? "p0", input2.domain ?? null, now, now]
1626
+ });
1627
+ await loadGlobalProcedures();
1628
+ return id;
1629
+ }
1630
+ async function deactivateGlobalProcedure(id) {
1631
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1632
+ const client = getClient();
1633
+ const result = await client.execute({
1634
+ sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
1635
+ args: [now, id]
1636
+ });
1637
+ await loadGlobalProcedures();
1638
+ return result.rowsAffected > 0;
1639
+ }
1640
+ var _cache, _cacheLoaded;
1641
+ var init_global_procedures = __esm({
1642
+ "src/lib/global-procedures.ts"() {
1643
+ "use strict";
1644
+ init_database();
1645
+ _cache = "";
1646
+ _cacheLoaded = false;
1647
+ }
1648
+ });
1649
+
1511
1650
  // src/lib/notifications.ts
1512
1651
  import crypto3 from "crypto";
1513
1652
  import path6 from "path";
@@ -1594,7 +1733,7 @@ var init_employees = __esm({
1594
1733
 
1595
1734
  // src/lib/license.ts
1596
1735
  import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
1597
- import { randomUUID } from "crypto";
1736
+ import { randomUUID as randomUUID2 } from "crypto";
1598
1737
  import path8 from "path";
1599
1738
  import { jwtVerify, importSPKI } from "jose";
1600
1739
  async function fetchRetry(url, init) {
@@ -1621,7 +1760,7 @@ function loadDeviceId() {
1621
1760
  }
1622
1761
  } catch {
1623
1762
  }
1624
- const id = randomUUID();
1763
+ const id = randomUUID2();
1625
1764
  mkdirSync2(EXE_AI_DIR, { recursive: true });
1626
1765
  writeFileSync(DEVICE_ID_PATH, id, "utf8");
1627
1766
  return id;
@@ -1865,7 +2004,7 @@ var init_plan_limits = __esm({
1865
2004
  // src/lib/exe-daemon-client.ts
1866
2005
  import net from "net";
1867
2006
  import { spawn } from "child_process";
1868
- import { randomUUID as randomUUID2 } from "crypto";
2007
+ import { randomUUID as randomUUID3 } from "crypto";
1869
2008
  import { existsSync as existsSync9, unlinkSync as unlinkSync2, readFileSync as readFileSync7, openSync, closeSync, statSync as statSync2 } from "fs";
1870
2009
  import path10 from "path";
1871
2010
  import { fileURLToPath } from "url";
@@ -2057,7 +2196,7 @@ function sendRequest(texts, priority) {
2057
2196
  resolve({ error: "Not connected" });
2058
2197
  return;
2059
2198
  }
2060
- const id = randomUUID2();
2199
+ const id = randomUUID3();
2061
2200
  const timer = setTimeout(() => {
2062
2201
  _pending.delete(id);
2063
2202
  resolve({ error: "Request timeout" });
@@ -2075,7 +2214,7 @@ function sendRequest(texts, priority) {
2075
2214
  async function pingDaemon() {
2076
2215
  if (!_socket || !_connected) return null;
2077
2216
  return new Promise((resolve) => {
2078
- const id = randomUUID2();
2217
+ const id = randomUUID3();
2079
2218
  const timer = setTimeout(() => {
2080
2219
  _pending.delete(id);
2081
2220
  resolve(null);
@@ -2271,463 +2410,62 @@ var init_embedder = __esm({
2271
2410
  }
2272
2411
  });
2273
2412
 
2274
- // src/lib/tasks-crud.ts
2275
- import crypto4 from "crypto";
2413
+ // src/lib/session-registry.ts
2414
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync10 } from "fs";
2276
2415
  import path11 from "path";
2277
- import { execSync as execSync4 } from "child_process";
2278
- import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
2279
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
2280
- async function writeCheckpoint(input2) {
2281
- const client = getClient();
2282
- const row = await resolveTask(client, input2.taskId);
2283
- const taskId = String(row.id);
2284
- const now = (/* @__PURE__ */ new Date()).toISOString();
2285
- const blockedByIds = [];
2286
- if (row.blocked_by) {
2287
- blockedByIds.push(String(row.blocked_by));
2416
+ import os4 from "os";
2417
+ function registerSession(entry) {
2418
+ const dir = path11.dirname(REGISTRY_PATH);
2419
+ if (!existsSync10(dir)) {
2420
+ mkdirSync3(dir, { recursive: true });
2288
2421
  }
2289
- const checkpoint = {
2290
- step: input2.step,
2291
- context_summary: input2.contextSummary,
2292
- files_touched: input2.filesTouched ?? [],
2293
- blocked_by_ids: blockedByIds,
2294
- last_checkpoint_at: now
2295
- };
2296
- const result = await client.execute({
2297
- sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
2298
- args: [JSON.stringify(checkpoint), now, taskId]
2299
- });
2300
- if (result.rowsAffected === 0) {
2301
- throw new Error(`Checkpoint write failed: task ${taskId} not found`);
2422
+ const sessions = listSessions();
2423
+ const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
2424
+ if (idx >= 0) {
2425
+ sessions[idx] = entry;
2426
+ } else {
2427
+ sessions.push(entry);
2302
2428
  }
2303
- const countResult = await client.execute({
2304
- sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
2305
- args: [taskId]
2306
- });
2307
- const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
2308
- return { checkpointCount };
2309
- }
2310
- function extractParentFromContext(contextBody) {
2311
- if (!contextBody) return null;
2312
- const match = contextBody.match(
2313
- /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
2314
- );
2315
- return match ? match[1].toLowerCase() : null;
2316
- }
2317
- function slugify(title) {
2318
- return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2429
+ writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
2319
2430
  }
2320
- async function resolveTask(client, identifier) {
2321
- let result = await client.execute({
2322
- sql: "SELECT * FROM tasks WHERE id = ?",
2323
- args: [identifier]
2324
- });
2325
- if (result.rows.length === 1) return result.rows[0];
2326
- result = await client.execute({
2327
- sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
2328
- args: [`%${identifier}%`]
2329
- });
2330
- if (result.rows.length === 1) return result.rows[0];
2331
- if (result.rows.length > 1) {
2332
- const exact = result.rows.filter(
2333
- (r) => String(r.task_file).endsWith(`/${identifier}.md`)
2334
- );
2335
- if (exact.length === 1) return exact[0];
2336
- const candidates = exact.length > 1 ? exact : result.rows;
2337
- const active = candidates.filter(
2338
- (r) => !["done", "cancelled"].includes(String(r.status))
2339
- );
2340
- if (active.length === 1) return active[0];
2341
- const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
2342
- throw new Error(
2343
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
2344
- );
2345
- }
2346
- result = await client.execute({
2347
- sql: "SELECT * FROM tasks WHERE title LIKE ?",
2348
- args: [`%${identifier}%`]
2349
- });
2350
- if (result.rows.length === 1) return result.rows[0];
2351
- if (result.rows.length > 1) {
2352
- const active = result.rows.filter(
2353
- (r) => !["done", "cancelled"].includes(String(r.status))
2354
- );
2355
- if (active.length === 1) return active[0];
2356
- const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
2357
- throw new Error(
2358
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
2359
- );
2431
+ function listSessions() {
2432
+ try {
2433
+ const raw = readFileSync8(REGISTRY_PATH, "utf8");
2434
+ return JSON.parse(raw);
2435
+ } catch {
2436
+ return [];
2360
2437
  }
2361
- throw new Error(`Task not found: ${identifier}`);
2362
2438
  }
2363
- async function createTaskCore(input2) {
2364
- const client = getClient();
2365
- const id = crypto4.randomUUID();
2366
- const now = (/* @__PURE__ */ new Date()).toISOString();
2367
- const slug = slugify(input2.title);
2368
- const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
2369
- let blockedById = null;
2370
- const initialStatus = input2.blockedBy ? "blocked" : "open";
2371
- if (input2.blockedBy) {
2372
- const blocker = await resolveTask(client, input2.blockedBy);
2373
- blockedById = String(blocker.id);
2374
- }
2375
- let parentTaskId = null;
2376
- let parentRef = input2.parentTaskId;
2377
- if (!parentRef) {
2378
- const extracted = extractParentFromContext(input2.context);
2379
- if (extracted) {
2380
- parentRef = extracted;
2381
- process.stderr.write(
2382
- "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
2383
- );
2384
- }
2439
+ var REGISTRY_PATH;
2440
+ var init_session_registry = __esm({
2441
+ "src/lib/session-registry.ts"() {
2442
+ "use strict";
2443
+ REGISTRY_PATH = path11.join(os4.homedir(), ".exe-os", "session-registry.json");
2385
2444
  }
2386
- if (parentRef) {
2445
+ });
2446
+
2447
+ // src/lib/session-key.ts
2448
+ import { execSync as execSync4 } from "child_process";
2449
+ function getSessionKey() {
2450
+ if (_cached2) return _cached2;
2451
+ let pid = process.ppid;
2452
+ for (let i = 0; i < 10; i++) {
2387
2453
  try {
2388
- const parent = await resolveTask(client, parentRef);
2389
- parentTaskId = String(parent.id);
2390
- } catch (err) {
2391
- if (!input2.parentTaskId) {
2392
- throw new Error(
2393
- `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
2394
- );
2454
+ const info = execSync4(`ps -p ${pid} -o ppid=,comm=`, {
2455
+ encoding: "utf8",
2456
+ timeout: 2e3
2457
+ }).trim();
2458
+ const match = info.match(/^\s*(\d+)\s+(.+)$/);
2459
+ if (!match) break;
2460
+ const [, ppid, cmd] = match;
2461
+ if (cmd === "claude" || cmd.endsWith("/claude")) {
2462
+ _cached2 = String(pid);
2463
+ return _cached2;
2395
2464
  }
2396
- throw err;
2397
- }
2398
- }
2399
- let warning;
2400
- const dupCheck = await client.execute({
2401
- sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
2402
- args: [input2.title, input2.assignedTo]
2403
- });
2404
- if (dupCheck.rows.length > 0) {
2405
- warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
2406
- }
2407
- if (input2.baseDir) {
2408
- try {
2409
- await mkdir4(path11.join(input2.baseDir, "exe", "output"), { recursive: true });
2410
- await mkdir4(path11.join(input2.baseDir, "exe", "research"), { recursive: true });
2411
- await ensureArchitectureDoc(input2.baseDir, input2.projectName);
2412
- await ensureGitignoreExe(input2.baseDir);
2465
+ pid = parseInt(ppid, 10);
2466
+ if (pid <= 1) break;
2413
2467
  } catch {
2414
- }
2415
- }
2416
- const complexity = input2.complexity ?? "standard";
2417
- await client.execute({
2418
- 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)
2419
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2420
- args: [
2421
- id,
2422
- input2.title,
2423
- input2.assignedTo,
2424
- input2.assignedBy,
2425
- input2.projectName,
2426
- input2.priority,
2427
- initialStatus,
2428
- taskFile,
2429
- blockedById,
2430
- parentTaskId,
2431
- input2.reviewer ?? null,
2432
- input2.context,
2433
- complexity,
2434
- input2.budgetTokens ?? null,
2435
- input2.budgetFallbackModel ?? null,
2436
- 0,
2437
- null,
2438
- now,
2439
- now
2440
- ]
2441
- });
2442
- return {
2443
- id,
2444
- title: input2.title,
2445
- assignedTo: input2.assignedTo,
2446
- assignedBy: input2.assignedBy,
2447
- projectName: input2.projectName,
2448
- priority: input2.priority,
2449
- status: initialStatus,
2450
- taskFile,
2451
- createdAt: now,
2452
- updatedAt: now,
2453
- warning,
2454
- budgetTokens: input2.budgetTokens ?? null,
2455
- budgetFallbackModel: input2.budgetFallbackModel ?? null,
2456
- tokensUsed: 0,
2457
- tokensWarnedAt: null
2458
- };
2459
- }
2460
- async function listTasks(input2) {
2461
- const client = getClient();
2462
- const conditions = [];
2463
- const args = [];
2464
- if (input2.assignedTo) {
2465
- conditions.push("assigned_to = ?");
2466
- args.push(input2.assignedTo);
2467
- }
2468
- if (input2.status) {
2469
- conditions.push("status = ?");
2470
- args.push(input2.status);
2471
- } else {
2472
- conditions.push("status IN ('open', 'in_progress', 'blocked')");
2473
- }
2474
- if (input2.projectName) {
2475
- conditions.push("project_name = ?");
2476
- args.push(input2.projectName);
2477
- }
2478
- if (input2.priority) {
2479
- conditions.push("priority = ?");
2480
- args.push(input2.priority);
2481
- }
2482
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2483
- const result = await client.execute({
2484
- 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`,
2485
- args
2486
- });
2487
- return result.rows.map((r) => ({
2488
- id: String(r.id),
2489
- title: String(r.title),
2490
- assignedTo: String(r.assigned_to),
2491
- assignedBy: String(r.assigned_by),
2492
- projectName: String(r.project_name),
2493
- priority: String(r.priority),
2494
- status: String(r.status),
2495
- taskFile: String(r.task_file),
2496
- createdAt: String(r.created_at),
2497
- updatedAt: String(r.updated_at),
2498
- checkpointCount: Number(r.checkpoint_count ?? 0),
2499
- budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
2500
- budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
2501
- tokensUsed: Number(r.tokens_used ?? 0),
2502
- tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2503
- }));
2504
- }
2505
- function checkStaleCompletion(taskContext, taskCreatedAt) {
2506
- if (!taskContext) return null;
2507
- if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
2508
- try {
2509
- const since = new Date(taskCreatedAt).toISOString();
2510
- const branch = execSync4(
2511
- "git rev-parse --abbrev-ref HEAD 2>/dev/null",
2512
- { encoding: "utf8", timeout: 3e3 }
2513
- ).trim();
2514
- const branchArg = branch && branch !== "HEAD" ? branch : "";
2515
- const commitCount = execSync4(
2516
- `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
2517
- { encoding: "utf8", timeout: 5e3 }
2518
- ).trim();
2519
- const count = parseInt(commitCount, 10);
2520
- if (count === 0) {
2521
- return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
2522
- }
2523
- return null;
2524
- } catch {
2525
- return null;
2526
- }
2527
- }
2528
- async function updateTaskStatus(input2) {
2529
- const client = getClient();
2530
- const now = (/* @__PURE__ */ new Date()).toISOString();
2531
- const row = await resolveTask(client, input2.taskId);
2532
- const taskId = String(row.id);
2533
- const taskFile = String(row.task_file);
2534
- if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
2535
- process.stderr.write(
2536
- `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
2537
- `
2538
- );
2539
- }
2540
- if (input2.status === "done") {
2541
- const existingRow = await client.execute({
2542
- sql: "SELECT context, created_at FROM tasks WHERE id = ?",
2543
- args: [taskId]
2544
- });
2545
- if (existingRow.rows.length > 0) {
2546
- const ctx = existingRow.rows[0];
2547
- const warning = checkStaleCompletion(ctx.context, ctx.created_at);
2548
- if (warning) {
2549
- input2.result = input2.result ? `\u26A0\uFE0F ${warning}
2550
-
2551
- ${input2.result}` : `\u26A0\uFE0F ${warning}`;
2552
- process.stderr.write(`[tasks] ${warning} (task: ${taskId})
2553
- `);
2554
- }
2555
- }
2556
- }
2557
- if (input2.status === "in_progress") {
2558
- const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
2559
- const claim = await client.execute({
2560
- sql: `UPDATE tasks
2561
- SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
2562
- WHERE id = ? AND status = 'open'`,
2563
- args: [tmuxSession, now, taskId]
2564
- });
2565
- if (claim.rowsAffected === 0) {
2566
- const current = await client.execute({
2567
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
2568
- args: [taskId]
2569
- });
2570
- const cur = current.rows[0];
2571
- const status = cur?.status ?? "unknown";
2572
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2573
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
2574
- }
2575
- try {
2576
- await writeCheckpoint({
2577
- taskId,
2578
- step: "claimed",
2579
- contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
2580
- });
2581
- } catch {
2582
- }
2583
- return { row, taskFile, now, taskId };
2584
- }
2585
- if (input2.result) {
2586
- await client.execute({
2587
- sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
2588
- args: [input2.status, input2.result, now, taskId]
2589
- });
2590
- } else {
2591
- await client.execute({
2592
- sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
2593
- args: [input2.status, now, taskId]
2594
- });
2595
- }
2596
- try {
2597
- await writeCheckpoint({
2598
- taskId,
2599
- step: `status_transition:${input2.status}`,
2600
- contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
2601
- });
2602
- } catch {
2603
- }
2604
- return { row, taskFile, now, taskId };
2605
- }
2606
- async function deleteTaskCore(taskId, _baseDir) {
2607
- const client = getClient();
2608
- const row = await resolveTask(client, taskId);
2609
- const id = String(row.id);
2610
- const taskFile = String(row.task_file);
2611
- const assignedTo = String(row.assigned_to);
2612
- const assignedBy = String(row.assigned_by);
2613
- await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
2614
- const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
2615
- return { taskFile, assignedTo, assignedBy, taskSlug };
2616
- }
2617
- async function ensureArchitectureDoc(baseDir, projectName) {
2618
- const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
2619
- try {
2620
- if (existsSync10(archPath)) return;
2621
- const template = [
2622
- `# ${projectName} \u2014 System Architecture`,
2623
- "",
2624
- "> Employees: read this before every task. Update it when you change system structure.",
2625
- `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
2626
- "",
2627
- "## Overview",
2628
- "",
2629
- "<!-- Describe what this system does, its main components, and how they connect. -->",
2630
- "",
2631
- "## Key Components",
2632
- "",
2633
- "<!-- List the major modules, services, or subsystems. -->",
2634
- "",
2635
- "## Data Flow",
2636
- "",
2637
- "<!-- How does data move through the system? What writes where? -->",
2638
- "",
2639
- "## Invariants",
2640
- "",
2641
- "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
2642
- "",
2643
- "## Dependencies",
2644
- "",
2645
- "<!-- What depends on what? If I change X, what else is affected? -->",
2646
- ""
2647
- ].join("\n");
2648
- await writeFile4(archPath, template, "utf-8");
2649
- } catch {
2650
- }
2651
- }
2652
- async function ensureGitignoreExe(baseDir) {
2653
- const gitignorePath = path11.join(baseDir, ".gitignore");
2654
- try {
2655
- if (existsSync10(gitignorePath)) {
2656
- const content = readFileSync8(gitignorePath, "utf-8");
2657
- if (/^\/?exe\/?$/m.test(content)) return;
2658
- await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2659
- } else {
2660
- await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
2661
- }
2662
- } catch {
2663
- }
2664
- }
2665
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2666
- var init_tasks_crud = __esm({
2667
- "src/lib/tasks-crud.ts"() {
2668
- "use strict";
2669
- init_database();
2670
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
2671
- TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2672
- }
2673
- });
2674
-
2675
- // src/lib/session-registry.ts
2676
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync11 } from "fs";
2677
- import path12 from "path";
2678
- import os4 from "os";
2679
- function registerSession(entry) {
2680
- const dir = path12.dirname(REGISTRY_PATH);
2681
- if (!existsSync11(dir)) {
2682
- mkdirSync3(dir, { recursive: true });
2683
- }
2684
- const sessions = listSessions();
2685
- const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
2686
- if (idx >= 0) {
2687
- sessions[idx] = entry;
2688
- } else {
2689
- sessions.push(entry);
2690
- }
2691
- writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
2692
- }
2693
- function listSessions() {
2694
- try {
2695
- const raw = readFileSync9(REGISTRY_PATH, "utf8");
2696
- return JSON.parse(raw);
2697
- } catch {
2698
- return [];
2699
- }
2700
- }
2701
- var REGISTRY_PATH;
2702
- var init_session_registry = __esm({
2703
- "src/lib/session-registry.ts"() {
2704
- "use strict";
2705
- REGISTRY_PATH = path12.join(os4.homedir(), ".exe-os", "session-registry.json");
2706
- }
2707
- });
2708
-
2709
- // src/lib/session-key.ts
2710
- import { execSync as execSync5 } from "child_process";
2711
- function getSessionKey() {
2712
- if (_cached2) return _cached2;
2713
- let pid = process.ppid;
2714
- for (let i = 0; i < 10; i++) {
2715
- try {
2716
- const info = execSync5(`ps -p ${pid} -o ppid=,comm=`, {
2717
- encoding: "utf8",
2718
- timeout: 2e3
2719
- }).trim();
2720
- const match = info.match(/^\s*(\d+)\s+(.+)$/);
2721
- if (!match) break;
2722
- const [, ppid, cmd] = match;
2723
- if (cmd === "claude" || cmd.endsWith("/claude")) {
2724
- _cached2 = String(pid);
2725
- return _cached2;
2726
- }
2727
- pid = parseInt(ppid, 10);
2728
- if (pid <= 1) break;
2729
- } catch {
2730
- break;
2468
+ break;
2731
2469
  }
2732
2470
  }
2733
2471
  _cached2 = process.env.CLAUDE_CODE_SSE_PORT ?? String(process.ppid);
@@ -2849,14 +2587,14 @@ var init_transport = __esm({
2849
2587
  });
2850
2588
 
2851
2589
  // src/lib/cc-agent-support.ts
2852
- import { execSync as execSync6 } from "child_process";
2590
+ import { execSync as execSync5 } from "child_process";
2853
2591
  function _resetCcAgentSupportCache() {
2854
2592
  _cachedSupport = null;
2855
2593
  }
2856
2594
  function claudeSupportsAgentFlag() {
2857
2595
  if (_cachedSupport !== null) return _cachedSupport;
2858
2596
  try {
2859
- const helpOutput = execSync6("claude --help 2>&1", {
2597
+ const helpOutput = execSync5("claude --help 2>&1", {
2860
2598
  encoding: "utf-8",
2861
2599
  timeout: 5e3
2862
2600
  });
@@ -2899,17 +2637,17 @@ var init_provider_table = __esm({
2899
2637
  });
2900
2638
 
2901
2639
  // src/lib/intercom-queue.ts
2902
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync12, mkdirSync as mkdirSync4 } from "fs";
2903
- import path13 from "path";
2640
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync11, mkdirSync as mkdirSync4 } from "fs";
2641
+ import path12 from "path";
2904
2642
  import os5 from "os";
2905
2643
  function ensureDir() {
2906
- const dir = path13.dirname(QUEUE_PATH);
2907
- if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
2644
+ const dir = path12.dirname(QUEUE_PATH);
2645
+ if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
2908
2646
  }
2909
2647
  function readQueue() {
2910
2648
  try {
2911
- if (!existsSync12(QUEUE_PATH)) return [];
2912
- return JSON.parse(readFileSync10(QUEUE_PATH, "utf8"));
2649
+ if (!existsSync11(QUEUE_PATH)) return [];
2650
+ return JSON.parse(readFileSync9(QUEUE_PATH, "utf8"));
2913
2651
  } catch {
2914
2652
  return [];
2915
2653
  }
@@ -2941,21 +2679,358 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
2941
2679
  var init_intercom_queue = __esm({
2942
2680
  "src/lib/intercom-queue.ts"() {
2943
2681
  "use strict";
2944
- QUEUE_PATH = path13.join(os5.homedir(), ".exe-os", "intercom-queue.json");
2682
+ QUEUE_PATH = path12.join(os5.homedir(), ".exe-os", "intercom-queue.json");
2945
2683
  TTL_MS = 60 * 60 * 1e3;
2946
- INTERCOM_LOG = path13.join(os5.homedir(), ".exe-os", "intercom.log");
2684
+ INTERCOM_LOG = path12.join(os5.homedir(), ".exe-os", "intercom.log");
2685
+ }
2686
+ });
2687
+
2688
+ // src/lib/session-kill-telemetry.ts
2689
+ import crypto4 from "crypto";
2690
+ async function recordSessionKill(input2) {
2691
+ try {
2692
+ const client = getClient();
2693
+ await client.execute({
2694
+ sql: `INSERT INTO session_kills
2695
+ (id, session_name, agent_id, killed_at, reason,
2696
+ ticks_idle, estimated_tokens_saved)
2697
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
2698
+ args: [
2699
+ crypto4.randomUUID(),
2700
+ input2.sessionName,
2701
+ input2.agentId,
2702
+ (/* @__PURE__ */ new Date()).toISOString(),
2703
+ input2.reason,
2704
+ input2.ticksIdle ?? null,
2705
+ input2.estimatedTokensSaved ?? null
2706
+ ]
2707
+ });
2708
+ } catch (err) {
2709
+ process.stderr.write(
2710
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
2711
+ `
2712
+ );
2713
+ }
2714
+ }
2715
+ var init_session_kill_telemetry = __esm({
2716
+ "src/lib/session-kill-telemetry.ts"() {
2717
+ "use strict";
2718
+ init_database();
2719
+ }
2720
+ });
2721
+
2722
+ // src/lib/capacity-monitor.ts
2723
+ var capacity_monitor_exports = {};
2724
+ __export(capacity_monitor_exports, {
2725
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
2726
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
2727
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
2728
+ confirmCapacityKill: () => confirmCapacityKill,
2729
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
2730
+ extractContextPercent: () => extractContextPercent,
2731
+ isAtCapacity: () => isAtCapacity,
2732
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
2733
+ pollCapacityDead: () => pollCapacityDead
2734
+ });
2735
+ function resumeTaskTitle(agentId) {
2736
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
2737
+ }
2738
+ function buildResumeContext(agentId, openTasks) {
2739
+ const taskList = openTasks.map(
2740
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
2741
+ ).join("\n");
2742
+ return [
2743
+ "## Context",
2744
+ "",
2745
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
2746
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
2747
+ "",
2748
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
2749
+ "",
2750
+ taskList,
2751
+ "",
2752
+ "Read each task file and chain through them. Build and commit after each one."
2753
+ ].join("\n");
2754
+ }
2755
+ function filterPaneContent(paneOutput) {
2756
+ return paneOutput.split("\n").filter((line) => {
2757
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
2758
+ for (const marker of CONTENT_LINE_MARKERS) {
2759
+ if (line.includes(marker)) return false;
2760
+ }
2761
+ for (const re of SOURCE_CODE_MARKERS) {
2762
+ if (re.test(line)) return false;
2763
+ }
2764
+ return true;
2765
+ }).join("\n");
2766
+ }
2767
+ function extractContextPercent(paneOutput) {
2768
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
2769
+ if (!match) return null;
2770
+ const parsed = Number.parseInt(match[2], 10);
2771
+ return Number.isFinite(parsed) ? parsed : null;
2772
+ }
2773
+ function isAtCapacity(paneOutput) {
2774
+ const filtered = filterPaneContent(paneOutput);
2775
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
2776
+ }
2777
+ function confirmCapacityKill(agentId, now = Date.now()) {
2778
+ const pendingSince = _pendingCapacityKill.get(agentId);
2779
+ if (pendingSince === void 0) {
2780
+ _pendingCapacityKill.set(agentId, now);
2781
+ return false;
2782
+ }
2783
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
2784
+ _pendingCapacityKill.set(agentId, now);
2785
+ return false;
2786
+ }
2787
+ _pendingCapacityKill.delete(agentId);
2788
+ return true;
2789
+ }
2790
+ function _resetPendingCapacityKills() {
2791
+ _pendingCapacityKill.clear();
2792
+ }
2793
+ function _resetLastRelaunchCache() {
2794
+ _lastRelaunch.clear();
2795
+ }
2796
+ async function lastResumeCreatedAtMs(agentId) {
2797
+ const client = getClient();
2798
+ const result = await client.execute({
2799
+ sql: `SELECT MAX(created_at) AS last_created_at
2800
+ FROM tasks
2801
+ WHERE assigned_to = ? AND title LIKE ?`,
2802
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`]
2803
+ });
2804
+ const raw = result.rows[0]?.last_created_at;
2805
+ if (raw === null || raw === void 0) return null;
2806
+ const parsed = Date.parse(String(raw));
2807
+ return Number.isNaN(parsed) ? null : parsed;
2808
+ }
2809
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
2810
+ const cached = _lastRelaunch.get(agentId);
2811
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
2812
+ const persisted = await lastResumeCreatedAtMs(agentId);
2813
+ if (persisted === null) return false;
2814
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
2815
+ _lastRelaunch.set(agentId, persisted);
2816
+ return true;
2817
+ }
2818
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
2819
+ const client = getClient();
2820
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2821
+ const context = buildResumeContext(agentId, openTasks);
2822
+ const existing = await client.execute({
2823
+ sql: `SELECT id FROM tasks
2824
+ WHERE assigned_to = ?
2825
+ AND title LIKE ?
2826
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
2827
+ ORDER BY created_at DESC
2828
+ LIMIT 1`,
2829
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
2830
+ });
2831
+ if (existing.rows.length > 0) {
2832
+ const taskId = String(existing.rows[0].id);
2833
+ await client.execute({
2834
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
2835
+ args: [context, now, taskId]
2836
+ });
2837
+ return { created: false, taskId };
2838
+ }
2839
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
2840
+ const task = await createTask2({
2841
+ title: resumeTaskTitle(agentId),
2842
+ assignedTo: agentId,
2843
+ assignedBy: "system",
2844
+ projectName: projectDir.split("/").pop() ?? "unknown",
2845
+ priority: "p0",
2846
+ context,
2847
+ baseDir: projectDir
2848
+ });
2849
+ return { created: true, taskId: task.id };
2850
+ }
2851
+ async function pollCapacityDead() {
2852
+ const transport = getTransport();
2853
+ const relaunched = [];
2854
+ const registered = listSessions().filter(
2855
+ (s) => s.agentId !== "exe"
2856
+ );
2857
+ if (registered.length === 0) return [];
2858
+ let liveSessions;
2859
+ try {
2860
+ liveSessions = transport.listSessions();
2861
+ } catch {
2862
+ return [];
2863
+ }
2864
+ for (const entry of registered) {
2865
+ const { windowName, agentId, projectDir } = entry;
2866
+ if (!liveSessions.includes(windowName)) continue;
2867
+ if (await isWithinRelaunchCooldown(agentId)) continue;
2868
+ let pane;
2869
+ try {
2870
+ pane = transport.capturePane(windowName, 15);
2871
+ } catch {
2872
+ continue;
2873
+ }
2874
+ if (!isAtCapacity(pane)) continue;
2875
+ const ctxPct = extractContextPercent(pane);
2876
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
2877
+ process.stderr.write(
2878
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
2879
+ `
2880
+ );
2881
+ continue;
2882
+ }
2883
+ if (!confirmCapacityKill(agentId)) {
2884
+ process.stderr.write(
2885
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
2886
+ `
2887
+ );
2888
+ continue;
2889
+ }
2890
+ const verify = await verifyPaneAtCapacity(windowName);
2891
+ if (!verify.atCapacity) {
2892
+ process.stderr.write(
2893
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
2894
+ `
2895
+ );
2896
+ void recordSessionKill({
2897
+ sessionName: windowName,
2898
+ agentId,
2899
+ reason: "capacity_false_positive_blocked"
2900
+ });
2901
+ continue;
2902
+ }
2903
+ process.stderr.write(
2904
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
2905
+ `
2906
+ );
2907
+ try {
2908
+ transport.kill(windowName);
2909
+ void recordSessionKill({
2910
+ sessionName: windowName,
2911
+ agentId,
2912
+ reason: "capacity"
2913
+ });
2914
+ const client = getClient();
2915
+ const openTasks = await client.execute({
2916
+ sql: `SELECT id, title, priority, task_file, status
2917
+ FROM tasks
2918
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')
2919
+ ORDER BY
2920
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
2921
+ created_at ASC
2922
+ LIMIT 10`,
2923
+ args: [agentId]
2924
+ });
2925
+ if (openTasks.rows.length === 0) {
2926
+ process.stderr.write(
2927
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
2928
+ `
2929
+ );
2930
+ continue;
2931
+ }
2932
+ const { created } = await createOrRefreshResumeTask(
2933
+ agentId,
2934
+ projectDir,
2935
+ openTasks.rows
2936
+ );
2937
+ if (created) {
2938
+ await writeNotification({
2939
+ agentId: "system",
2940
+ agentRole: "daemon",
2941
+ event: "capacity_relaunch",
2942
+ project: projectDir.split("/").pop() ?? "unknown",
2943
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
2944
+ });
2945
+ }
2946
+ _lastRelaunch.set(agentId, Date.now());
2947
+ if (created) relaunched.push(agentId);
2948
+ } catch (err) {
2949
+ process.stderr.write(
2950
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
2951
+ `
2952
+ );
2953
+ }
2954
+ }
2955
+ return relaunched;
2956
+ }
2957
+ 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;
2958
+ var init_capacity_monitor = __esm({
2959
+ "src/lib/capacity-monitor.ts"() {
2960
+ "use strict";
2961
+ init_session_registry();
2962
+ init_transport();
2963
+ init_notifications();
2964
+ init_database();
2965
+ init_session_kill_telemetry();
2966
+ init_tmux_routing();
2967
+ CAPACITY_PATTERNS = [
2968
+ /conversation is too long/i,
2969
+ /maximum context length/i,
2970
+ /context window.*(?:limit|exceed|full)/i,
2971
+ /reached.*(?:token|context).*limit/i
2972
+ ];
2973
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
2974
+ CONTENT_LINE_MARKERS = [
2975
+ "RESUME:",
2976
+ "intercom",
2977
+ "capacity-monitor",
2978
+ "CAPACITY_PATTERNS",
2979
+ "isAtCapacity",
2980
+ "CONTENT_LINE_MARKERS",
2981
+ "pollCapacityDead",
2982
+ "confirmCapacityKill",
2983
+ "session_kills",
2984
+ "capacity-monitor.test"
2985
+ ];
2986
+ SOURCE_CODE_MARKERS = [
2987
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
2988
+ /(?:maximum context length|conversation is too long).*["'`/]/i
2989
+ ];
2990
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
2991
+ _lastRelaunch = /* @__PURE__ */ new Map();
2992
+ RESUME_TITLE_PREFIX = "RESUME:";
2993
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
2994
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
2995
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
2996
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
2997
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
2998
+ CTX_FLOOR_PERCENT = 50;
2947
2999
  }
2948
3000
  });
2949
3001
 
2950
3002
  // src/lib/tmux-routing.ts
2951
- import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
2952
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync13, appendFileSync } from "fs";
2953
- import path14 from "path";
3003
+ var tmux_routing_exports = {};
3004
+ __export(tmux_routing_exports, {
3005
+ acquireSpawnLock: () => acquireSpawnLock2,
3006
+ employeeSessionName: () => employeeSessionName,
3007
+ ensureEmployee: () => ensureEmployee,
3008
+ extractRootExe: () => extractRootExe,
3009
+ findFreeInstance: () => findFreeInstance,
3010
+ getDispatchedBy: () => getDispatchedBy,
3011
+ getMySession: () => getMySession,
3012
+ getParentExe: () => getParentExe,
3013
+ getSessionState: () => getSessionState,
3014
+ isEmployeeAlive: () => isEmployeeAlive,
3015
+ isExeSession: () => isExeSession,
3016
+ isSessionBusy: () => isSessionBusy,
3017
+ notifyParentExe: () => notifyParentExe,
3018
+ parseParentExe: () => parseParentExe,
3019
+ registerParentExe: () => registerParentExe,
3020
+ releaseSpawnLock: () => releaseSpawnLock2,
3021
+ resolveExeSession: () => resolveExeSession,
3022
+ sendIntercom: () => sendIntercom,
3023
+ spawnEmployee: () => spawnEmployee,
3024
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
3025
+ });
3026
+ import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
3027
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync12, appendFileSync } from "fs";
3028
+ import path13 from "path";
2954
3029
  import os6 from "os";
2955
3030
  import { fileURLToPath as fileURLToPath2 } from "url";
2956
3031
  import { unlinkSync as unlinkSync3 } from "fs";
2957
3032
  function spawnLockPath(sessionName) {
2958
- return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3033
+ return path13.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2959
3034
  }
2960
3035
  function isProcessAlive(pid) {
2961
3036
  try {
@@ -2966,13 +3041,13 @@ function isProcessAlive(pid) {
2966
3041
  }
2967
3042
  }
2968
3043
  function acquireSpawnLock2(sessionName) {
2969
- if (!existsSync13(SPAWN_LOCK_DIR)) {
3044
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
2970
3045
  mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
2971
3046
  }
2972
3047
  const lockFile = spawnLockPath(sessionName);
2973
- if (existsSync13(lockFile)) {
3048
+ if (existsSync12(lockFile)) {
2974
3049
  try {
2975
- const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
3050
+ const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
2976
3051
  const age = Date.now() - lock.timestamp;
2977
3052
  if (isProcessAlive(lock.pid) && age < 6e4) {
2978
3053
  return false;
@@ -2992,13 +3067,13 @@ function releaseSpawnLock2(sessionName) {
2992
3067
  function resolveBehaviorsExporterScript() {
2993
3068
  try {
2994
3069
  const thisFile = fileURLToPath2(import.meta.url);
2995
- const scriptPath = path14.join(
2996
- path14.dirname(thisFile),
3070
+ const scriptPath = path13.join(
3071
+ path13.dirname(thisFile),
2997
3072
  "..",
2998
3073
  "bin",
2999
3074
  "exe-export-behaviors.js"
3000
3075
  );
3001
- return existsSync13(scriptPath) ? scriptPath : null;
3076
+ return existsSync12(scriptPath) ? scriptPath : null;
3002
3077
  } catch {
3003
3078
  return null;
3004
3079
  }
@@ -3025,16 +3100,54 @@ function getMySession() {
3025
3100
  return getTransport().getMySession();
3026
3101
  }
3027
3102
  function employeeSessionName(employee, exeSession, instance) {
3103
+ if (!/^exe\d+$/.test(exeSession)) {
3104
+ const root = extractRootExe(exeSession);
3105
+ if (root) {
3106
+ process.stderr.write(
3107
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
3108
+ `
3109
+ );
3110
+ exeSession = root;
3111
+ } else {
3112
+ throw new Error(
3113
+ `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
3114
+ );
3115
+ }
3116
+ }
3028
3117
  const suffix = instance != null && instance > 0 ? String(instance) : "";
3029
- return `${employee}${suffix}-${exeSession}`;
3118
+ const name = `${employee}${suffix}-${exeSession}`;
3119
+ if (!VALID_SESSION_NAME.test(name)) {
3120
+ throw new Error(
3121
+ `Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
3122
+ );
3123
+ }
3124
+ return name;
3125
+ }
3126
+ function parseParentExe(sessionName, agentId) {
3127
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3128
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
3129
+ const match = sessionName.match(regex);
3130
+ return match?.[1] ?? null;
3030
3131
  }
3031
3132
  function extractRootExe(name) {
3032
3133
  const match = name.match(/(exe\d+)$/);
3033
3134
  return match?.[1] ?? null;
3034
3135
  }
3136
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3137
+ if (!existsSync12(SESSION_CACHE)) {
3138
+ mkdirSync5(SESSION_CACHE, { recursive: true });
3139
+ }
3140
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
3141
+ const filePath = path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3142
+ writeFileSync4(filePath, JSON.stringify({
3143
+ parentExe: rootExe,
3144
+ dispatchedBy: dispatchedBy || rootExe,
3145
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3146
+ }));
3147
+ }
3035
3148
  function getParentExe(sessionKey) {
3036
3149
  try {
3037
- const data = JSON.parse(readFileSync11(path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3150
+ const data = JSON.parse(readFileSync10(path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3038
3151
  return data.parentExe || null;
3039
3152
  } catch {
3040
3153
  return null;
@@ -3042,8 +3155,8 @@ function getParentExe(sessionKey) {
3042
3155
  }
3043
3156
  function getDispatchedBy(sessionKey) {
3044
3157
  try {
3045
- const data = JSON.parse(readFileSync11(
3046
- path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3158
+ const data = JSON.parse(readFileSync10(
3159
+ path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3047
3160
  "utf8"
3048
3161
  ));
3049
3162
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -3074,19 +3187,45 @@ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive =
3074
3187
  const candidate = employeeSessionName(employeeName, exeSession, i);
3075
3188
  if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
3076
3189
  }
3077
- return null;
3190
+ return null;
3191
+ }
3192
+ async function verifyPaneAtCapacity(sessionName) {
3193
+ const transport = getTransport();
3194
+ if (!transport.isAlive(sessionName)) {
3195
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
3196
+ }
3197
+ let pane;
3198
+ try {
3199
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
3200
+ } catch (err) {
3201
+ return {
3202
+ atCapacity: false,
3203
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
3204
+ };
3205
+ }
3206
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
3207
+ if (!isAtCapacity2(pane)) {
3208
+ return {
3209
+ atCapacity: false,
3210
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
3211
+ };
3212
+ }
3213
+ return {
3214
+ atCapacity: true,
3215
+ reason: "capacity banner matched in recent pane output"
3216
+ };
3078
3217
  }
3079
3218
  function readDebounceState() {
3080
3219
  try {
3081
- if (!existsSync13(DEBOUNCE_FILE)) return {};
3082
- return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
3220
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
3221
+ return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
3083
3222
  } catch {
3084
3223
  return {};
3085
3224
  }
3086
3225
  }
3087
3226
  function writeDebounceState(state) {
3088
3227
  try {
3089
- if (!existsSync13(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
3228
+ if (!existsSync12(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
3090
3229
  writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
3091
3230
  } catch {
3092
3231
  }
@@ -3111,379 +3250,818 @@ function logIntercom(msg) {
3111
3250
  process.stderr.write(`[intercom] ${msg}
3112
3251
  `);
3113
3252
  try {
3114
- appendFileSync(INTERCOM_LOG2, line);
3253
+ appendFileSync(INTERCOM_LOG2, line);
3254
+ } catch {
3255
+ }
3256
+ }
3257
+ function getSessionState(sessionName) {
3258
+ const transport = getTransport();
3259
+ if (!transport.isAlive(sessionName)) return "offline";
3260
+ try {
3261
+ const pane = transport.capturePane(sessionName, 5);
3262
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
3263
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
3264
+ return "no_claude";
3265
+ }
3266
+ }
3267
+ if (/Running…/.test(pane)) return "tool";
3268
+ if (BUSY_PATTERN.test(pane)) return "thinking";
3269
+ return "idle";
3270
+ } catch {
3271
+ return "offline";
3272
+ }
3273
+ }
3274
+ function isSessionBusy(sessionName) {
3275
+ const state = getSessionState(sessionName);
3276
+ return state === "thinking" || state === "tool";
3277
+ }
3278
+ function isExeSession(sessionName) {
3279
+ return /^exe\d*$/.test(sessionName);
3280
+ }
3281
+ function sendIntercom(targetSession) {
3282
+ const transport = getTransport();
3283
+ if (isExeSession(targetSession)) {
3284
+ logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
3285
+ return "skipped_exe";
3286
+ }
3287
+ if (isDebounced(targetSession)) {
3288
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
3289
+ return "debounced";
3290
+ }
3291
+ try {
3292
+ const sessions = transport.listSessions();
3293
+ if (!sessions.includes(targetSession)) {
3294
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
3295
+ return "failed";
3296
+ }
3297
+ const sessionState = getSessionState(targetSession);
3298
+ if (sessionState === "no_claude") {
3299
+ queueIntercom(targetSession, "claude not running in session");
3300
+ recordDebounce(targetSession);
3301
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3302
+ return "queued";
3303
+ }
3304
+ if (sessionState === "thinking" || sessionState === "tool") {
3305
+ queueIntercom(targetSession, "session busy at send time");
3306
+ recordDebounce(targetSession);
3307
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
3308
+ return "queued";
3309
+ }
3310
+ if (transport.isPaneInCopyMode(targetSession)) {
3311
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
3312
+ transport.sendKeys(targetSession, "q");
3313
+ }
3314
+ transport.sendKeys(targetSession, "/exe-intercom");
3315
+ recordDebounce(targetSession);
3316
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
3317
+ return "delivered";
3318
+ } catch {
3319
+ logIntercom(`FAIL \u2192 ${targetSession}`);
3320
+ return "failed";
3321
+ }
3322
+ }
3323
+ function notifyParentExe(sessionKey) {
3324
+ const target = getDispatchedBy(sessionKey);
3325
+ if (!target) {
3326
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
3327
+ `);
3328
+ return false;
3329
+ }
3330
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
3331
+ `);
3332
+ const result = sendIntercom(target);
3333
+ if (result === "failed") {
3334
+ const rootExe = resolveExeSession();
3335
+ if (rootExe && rootExe !== target) {
3336
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3337
+ `);
3338
+ const fallback = sendIntercom(rootExe);
3339
+ return fallback !== "failed";
3340
+ }
3341
+ return false;
3342
+ }
3343
+ return true;
3344
+ }
3345
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3346
+ if (employeeName === "exe") {
3347
+ return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
3348
+ }
3349
+ try {
3350
+ assertEmployeeLimitSync();
3351
+ } catch (err) {
3352
+ if (err instanceof PlanLimitError) {
3353
+ return { status: "failed", sessionName: "", error: err.message };
3354
+ }
3355
+ }
3356
+ if (/-exe\d*$/.test(employeeName)) {
3357
+ const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
3358
+ return {
3359
+ status: "failed",
3360
+ sessionName: "",
3361
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
3362
+ };
3363
+ }
3364
+ if (!/^exe\d+$/.test(exeSession)) {
3365
+ const root = extractRootExe(exeSession);
3366
+ if (root) {
3367
+ process.stderr.write(
3368
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
3369
+ `
3370
+ );
3371
+ exeSession = root;
3372
+ } else {
3373
+ return {
3374
+ status: "failed",
3375
+ sessionName: "",
3376
+ error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
3377
+ };
3378
+ }
3379
+ }
3380
+ let effectiveInstance = opts?.instance;
3381
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
3382
+ const free = findFreeInstance(
3383
+ employeeName,
3384
+ exeSession,
3385
+ opts.maxAutoInstances ?? 10
3386
+ );
3387
+ if (free === null) {
3388
+ return {
3389
+ status: "failed",
3390
+ sessionName: employeeSessionName(employeeName, exeSession),
3391
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
3392
+ };
3393
+ }
3394
+ effectiveInstance = free === 0 ? void 0 : free;
3395
+ }
3396
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
3397
+ if (isEmployeeAlive(sessionName)) {
3398
+ const result2 = sendIntercom(sessionName);
3399
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
3400
+ return { status: "intercom_sent", sessionName };
3401
+ }
3402
+ if (result2 === "delivered") {
3403
+ return { status: "intercom_unprocessed", sessionName };
3404
+ }
3405
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
3406
+ }
3407
+ const spawnOpts = { ...opts, instance: effectiveInstance };
3408
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
3409
+ if (result.error) {
3410
+ return { status: "failed", sessionName, error: result.error };
3411
+ }
3412
+ return { status: "spawned", sessionName };
3413
+ }
3414
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3415
+ const transport = getTransport();
3416
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3417
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3418
+ const logDir = path13.join(os6.homedir(), ".exe-os", "session-logs");
3419
+ const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3420
+ if (!existsSync12(logDir)) {
3421
+ mkdirSync5(logDir, { recursive: true });
3422
+ }
3423
+ transport.kill(sessionName);
3424
+ let cleanupSuffix = "";
3425
+ try {
3426
+ const thisFile = fileURLToPath2(import.meta.url);
3427
+ const cleanupScript = path13.join(path13.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3428
+ if (existsSync12(cleanupScript)) {
3429
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
3430
+ }
3431
+ } catch {
3432
+ }
3433
+ try {
3434
+ const claudeJsonPath = path13.join(os6.homedir(), ".claude.json");
3435
+ let claudeJson = {};
3436
+ try {
3437
+ claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
3438
+ } catch {
3439
+ }
3440
+ if (!claudeJson.projects) claudeJson.projects = {};
3441
+ const projects = claudeJson.projects;
3442
+ const trustDir = opts?.cwd ?? projectDir;
3443
+ if (!projects[trustDir]) projects[trustDir] = {};
3444
+ projects[trustDir].hasTrustDialogAccepted = true;
3445
+ writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3446
+ } catch {
3447
+ }
3448
+ try {
3449
+ const settingsDir = path13.join(os6.homedir(), ".claude", "projects");
3450
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3451
+ const projSettingsDir = path13.join(settingsDir, normalizedKey);
3452
+ const settingsPath = path13.join(projSettingsDir, "settings.json");
3453
+ let settings = {};
3454
+ try {
3455
+ settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
3456
+ } catch {
3457
+ }
3458
+ const perms = settings.permissions ?? {};
3459
+ const allow = perms.allow ?? [];
3460
+ const toolNames = [
3461
+ "recall_my_memory",
3462
+ "store_memory",
3463
+ "create_task",
3464
+ "update_task",
3465
+ "list_tasks",
3466
+ "get_task",
3467
+ "ask_team_memory",
3468
+ "store_behavior",
3469
+ "get_identity",
3470
+ "send_message"
3471
+ ];
3472
+ const requiredTools = expandDualPrefixTools(toolNames);
3473
+ let changed = false;
3474
+ for (const tool of requiredTools) {
3475
+ if (!allow.includes(tool)) {
3476
+ allow.push(tool);
3477
+ changed = true;
3478
+ }
3479
+ }
3480
+ if (changed) {
3481
+ perms.allow = allow;
3482
+ settings.permissions = perms;
3483
+ mkdirSync5(projSettingsDir, { recursive: true });
3484
+ writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3485
+ }
3486
+ } catch {
3487
+ }
3488
+ const spawnCwd = opts?.cwd ?? projectDir;
3489
+ const useExeAgent = !!(opts?.model && opts?.provider);
3490
+ const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
3491
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
3492
+ let identityFlag = "";
3493
+ let behaviorsFlag = "";
3494
+ let legacyFallbackWarned = false;
3495
+ if (!useExeAgent && !useBinSymlink) {
3496
+ const identityPath = path13.join(
3497
+ os6.homedir(),
3498
+ ".exe-os",
3499
+ "identity",
3500
+ `${employeeName}.md`
3501
+ );
3502
+ _resetCcAgentSupportCache();
3503
+ const hasAgentFlag = claudeSupportsAgentFlag();
3504
+ if (hasAgentFlag) {
3505
+ identityFlag = ` --agent ${employeeName}`;
3506
+ } else if (existsSync12(identityPath)) {
3507
+ identityFlag = ` --append-system-prompt-file ${identityPath}`;
3508
+ legacyFallbackWarned = true;
3509
+ }
3510
+ const behaviorsFile = exportBehaviorsSync(
3511
+ employeeName,
3512
+ path13.basename(spawnCwd),
3513
+ sessionName
3514
+ );
3515
+ if (behaviorsFile) {
3516
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
3517
+ }
3518
+ }
3519
+ if (legacyFallbackWarned) {
3520
+ process.stderr.write(
3521
+ `[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.
3522
+ `
3523
+ );
3524
+ }
3525
+ let sessionContextFlag = "";
3526
+ try {
3527
+ const ctxDir = path13.join(os6.homedir(), ".exe-os", "session-cache");
3528
+ mkdirSync5(ctxDir, { recursive: true });
3529
+ const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
3530
+ const ctxContent = [
3531
+ `## Session Context`,
3532
+ `You are running in tmux session: ${sessionName}.`,
3533
+ `Your parent exe session is ${exeSession}.`,
3534
+ `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
3535
+ ].join("\n");
3536
+ writeFileSync4(ctxFile, ctxContent);
3537
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
3115
3538
  } catch {
3116
3539
  }
3117
- }
3118
- function getSessionState(sessionName) {
3119
- const transport = getTransport();
3120
- if (!transport.isAlive(sessionName)) return "offline";
3121
- try {
3122
- const pane = transport.capturePane(sessionName, 5);
3123
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
3124
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
3125
- return "no_claude";
3540
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
3541
+ if (ccProvider !== DEFAULT_PROVIDER) {
3542
+ const cfg = PROVIDER_TABLE[ccProvider];
3543
+ if (cfg?.apiKeyEnv) {
3544
+ const keyVal = process.env[cfg.apiKeyEnv];
3545
+ if (keyVal) {
3546
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
3126
3547
  }
3127
3548
  }
3128
- if (/Running…/.test(pane)) return "tool";
3129
- if (BUSY_PATTERN.test(pane)) return "thinking";
3130
- return "idle";
3131
- } catch {
3132
- return "offline";
3133
3549
  }
3134
- }
3135
- function isExeSession(sessionName) {
3136
- return /^exe\d*$/.test(sessionName);
3137
- }
3138
- function sendIntercom(targetSession) {
3139
- const transport = getTransport();
3140
- if (isExeSession(targetSession)) {
3141
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
3142
- return "skipped_exe";
3550
+ let spawnCommand;
3551
+ if (useExeAgent) {
3552
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
3553
+ } else if (useBinSymlink) {
3554
+ const binName = `${employeeName}-${ccProvider}`;
3555
+ process.stderr.write(
3556
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
3557
+ `
3558
+ );
3559
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
3560
+ } else {
3561
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
3143
3562
  }
3144
- if (isDebounced(targetSession)) {
3145
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
3146
- return "debounced";
3563
+ const spawnResult = transport.spawn(sessionName, {
3564
+ cwd: spawnCwd,
3565
+ command: spawnCommand
3566
+ });
3567
+ if (spawnResult.error) {
3568
+ releaseSpawnLock2(sessionName);
3569
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3147
3570
  }
3571
+ transport.pipeLog(sessionName, logFile);
3148
3572
  try {
3149
- const sessions = transport.listSessions();
3150
- if (!sessions.includes(targetSession)) {
3151
- logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
3152
- return "failed";
3153
- }
3154
- const sessionState = getSessionState(targetSession);
3155
- if (sessionState === "no_claude") {
3156
- queueIntercom(targetSession, "claude not running in session");
3157
- recordDebounce(targetSession);
3158
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3159
- return "queued";
3160
- }
3161
- if (sessionState === "thinking" || sessionState === "tool") {
3162
- queueIntercom(targetSession, "session busy at send time");
3163
- recordDebounce(targetSession);
3164
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
3165
- return "queued";
3573
+ const mySession = getMySession();
3574
+ const dispatchInfo = path13.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
3575
+ writeFileSync4(dispatchInfo, JSON.stringify({
3576
+ dispatchedBy: mySession,
3577
+ rootExe: exeSession,
3578
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
3579
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3580
+ }));
3581
+ } catch {
3582
+ }
3583
+ let booted = false;
3584
+ for (let i = 0; i < 30; i++) {
3585
+ try {
3586
+ execSync6("sleep 0.5");
3587
+ } catch {
3166
3588
  }
3167
- if (transport.isPaneInCopyMode(targetSession)) {
3168
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
3169
- transport.sendKeys(targetSession, "q");
3589
+ try {
3590
+ const pane = transport.capturePane(sessionName);
3591
+ if (useExeAgent) {
3592
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
3593
+ booted = true;
3594
+ break;
3595
+ }
3596
+ } else {
3597
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
3598
+ booted = true;
3599
+ break;
3600
+ }
3601
+ }
3602
+ } catch {
3170
3603
  }
3171
- transport.sendKeys(targetSession, "/exe-intercom");
3172
- recordDebounce(targetSession);
3173
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
3174
- return "delivered";
3175
- } catch {
3176
- logIntercom(`FAIL \u2192 ${targetSession}`);
3177
- return "failed";
3178
3604
  }
3179
- }
3180
- function notifyParentExe(sessionKey) {
3181
- const target = getDispatchedBy(sessionKey);
3182
- if (!target) {
3183
- process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
3184
- `);
3185
- return false;
3605
+ if (!booted) {
3606
+ releaseSpawnLock2(sessionName);
3607
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3186
3608
  }
3187
- process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
3188
- `);
3189
- const result = sendIntercom(target);
3190
- if (result === "failed") {
3191
- const rootExe = resolveExeSession();
3192
- if (rootExe && rootExe !== target) {
3193
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3194
- `);
3195
- const fallback = sendIntercom(rootExe);
3196
- return fallback !== "failed";
3609
+ if (!useExeAgent) {
3610
+ try {
3611
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
3612
+ } catch {
3197
3613
  }
3198
- return false;
3199
3614
  }
3200
- return true;
3615
+ registerSession({
3616
+ windowName: sessionName,
3617
+ agentId: employeeName,
3618
+ projectDir: spawnCwd,
3619
+ parentExe: exeSession,
3620
+ pid: 0,
3621
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3622
+ });
3623
+ releaseSpawnLock2(sessionName);
3624
+ return { sessionName };
3201
3625
  }
3202
- function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3203
- if (employeeName === "exe") {
3204
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
3626
+ 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;
3627
+ var init_tmux_routing = __esm({
3628
+ "src/lib/tmux-routing.ts"() {
3629
+ "use strict";
3630
+ init_session_registry();
3631
+ init_session_key();
3632
+ init_transport();
3633
+ init_cc_agent_support();
3634
+ init_mcp_prefix();
3635
+ init_provider_table();
3636
+ init_intercom_queue();
3637
+ init_plan_limits();
3638
+ SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
3639
+ SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
3640
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3641
+ VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
3642
+ VERIFY_PANE_LINES = 200;
3643
+ INTERCOM_DEBOUNCE_MS = 3e4;
3644
+ INTERCOM_LOG2 = path13.join(os6.homedir(), ".exe-os", "intercom.log");
3645
+ DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
3646
+ DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3647
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3205
3648
  }
3206
- try {
3207
- assertEmployeeLimitSync();
3208
- } catch (err) {
3209
- if (err instanceof PlanLimitError) {
3210
- return { status: "failed", sessionName: "", error: err.message };
3211
- }
3649
+ });
3650
+
3651
+ // src/lib/tasks-crud.ts
3652
+ import crypto5 from "crypto";
3653
+ import path14 from "path";
3654
+ import { execSync as execSync7 } from "child_process";
3655
+ import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3656
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
3657
+ async function writeCheckpoint(input2) {
3658
+ const client = getClient();
3659
+ const row = await resolveTask(client, input2.taskId);
3660
+ const taskId = String(row.id);
3661
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3662
+ const blockedByIds = [];
3663
+ if (row.blocked_by) {
3664
+ blockedByIds.push(String(row.blocked_by));
3212
3665
  }
3213
- if (/-exe\d*$/.test(employeeName)) {
3214
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
3215
- return {
3216
- status: "failed",
3217
- sessionName: "",
3218
- error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
3219
- };
3666
+ const checkpoint = {
3667
+ step: input2.step,
3668
+ context_summary: input2.contextSummary,
3669
+ files_touched: input2.filesTouched ?? [],
3670
+ blocked_by_ids: blockedByIds,
3671
+ last_checkpoint_at: now
3672
+ };
3673
+ const result = await client.execute({
3674
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
3675
+ args: [JSON.stringify(checkpoint), now, taskId]
3676
+ });
3677
+ if (result.rowsAffected === 0) {
3678
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
3220
3679
  }
3221
- let effectiveInstance = opts?.instance;
3222
- if (effectiveInstance === void 0 && opts?.autoInstance) {
3223
- const free = findFreeInstance(
3224
- employeeName,
3225
- exeSession,
3226
- opts.maxAutoInstances ?? 10
3680
+ const countResult = await client.execute({
3681
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
3682
+ args: [taskId]
3683
+ });
3684
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
3685
+ return { checkpointCount };
3686
+ }
3687
+ function extractParentFromContext(contextBody) {
3688
+ if (!contextBody) return null;
3689
+ const match = contextBody.match(
3690
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
3691
+ );
3692
+ return match ? match[1].toLowerCase() : null;
3693
+ }
3694
+ function slugify(title) {
3695
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3696
+ }
3697
+ async function resolveTask(client, identifier) {
3698
+ let result = await client.execute({
3699
+ sql: "SELECT * FROM tasks WHERE id = ?",
3700
+ args: [identifier]
3701
+ });
3702
+ if (result.rows.length === 1) return result.rows[0];
3703
+ result = await client.execute({
3704
+ sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
3705
+ args: [`%${identifier}%`]
3706
+ });
3707
+ if (result.rows.length === 1) return result.rows[0];
3708
+ if (result.rows.length > 1) {
3709
+ const exact = result.rows.filter(
3710
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
3711
+ );
3712
+ if (exact.length === 1) return exact[0];
3713
+ const candidates = exact.length > 1 ? exact : result.rows;
3714
+ const active = candidates.filter(
3715
+ (r) => !["done", "cancelled"].includes(String(r.status))
3716
+ );
3717
+ if (active.length === 1) return active[0];
3718
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
3719
+ throw new Error(
3720
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3227
3721
  );
3228
- if (free === null) {
3229
- return {
3230
- status: "failed",
3231
- sessionName: employeeSessionName(employeeName, exeSession),
3232
- error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
3233
- };
3234
- }
3235
- effectiveInstance = free === 0 ? void 0 : free;
3236
- }
3237
- const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
3238
- if (isEmployeeAlive(sessionName)) {
3239
- const result2 = sendIntercom(sessionName);
3240
- if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
3241
- return { status: "intercom_sent", sessionName };
3242
- }
3243
- if (result2 === "delivered") {
3244
- return { status: "intercom_unprocessed", sessionName };
3245
- }
3246
- return { status: "failed", sessionName, error: "intercom delivery failed" };
3247
3722
  }
3248
- const spawnOpts = { ...opts, instance: effectiveInstance };
3249
- const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
3250
- if (result.error) {
3251
- return { status: "failed", sessionName, error: result.error };
3723
+ result = await client.execute({
3724
+ sql: "SELECT * FROM tasks WHERE title LIKE ?",
3725
+ args: [`%${identifier}%`]
3726
+ });
3727
+ if (result.rows.length === 1) return result.rows[0];
3728
+ if (result.rows.length > 1) {
3729
+ const active = result.rows.filter(
3730
+ (r) => !["done", "cancelled"].includes(String(r.status))
3731
+ );
3732
+ if (active.length === 1) return active[0];
3733
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
3734
+ throw new Error(
3735
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3736
+ );
3252
3737
  }
3253
- return { status: "spawned", sessionName };
3738
+ throw new Error(`Task not found: ${identifier}`);
3254
3739
  }
3255
- function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3256
- const transport = getTransport();
3257
- const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3258
- const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3259
- const logDir = path14.join(os6.homedir(), ".exe-os", "session-logs");
3260
- const logFile = path14.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3261
- if (!existsSync13(logDir)) {
3262
- mkdirSync5(logDir, { recursive: true });
3740
+ async function createTaskCore(input2) {
3741
+ const client = getClient();
3742
+ const id = crypto5.randomUUID();
3743
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3744
+ const slug = slugify(input2.title);
3745
+ const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
3746
+ let blockedById = null;
3747
+ const initialStatus = input2.blockedBy ? "blocked" : "open";
3748
+ if (input2.blockedBy) {
3749
+ const blocker = await resolveTask(client, input2.blockedBy);
3750
+ blockedById = String(blocker.id);
3263
3751
  }
3264
- transport.kill(sessionName);
3265
- let cleanupSuffix = "";
3266
- try {
3267
- const thisFile = fileURLToPath2(import.meta.url);
3268
- const cleanupScript = path14.join(path14.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3269
- if (existsSync13(cleanupScript)) {
3270
- cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
3752
+ let parentTaskId = null;
3753
+ let parentRef = input2.parentTaskId;
3754
+ if (!parentRef) {
3755
+ const extracted = extractParentFromContext(input2.context);
3756
+ if (extracted) {
3757
+ parentRef = extracted;
3758
+ process.stderr.write(
3759
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
3760
+ );
3271
3761
  }
3272
- } catch {
3273
3762
  }
3274
- try {
3275
- const claudeJsonPath = path14.join(os6.homedir(), ".claude.json");
3276
- let claudeJson = {};
3763
+ if (parentRef) {
3277
3764
  try {
3278
- claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
3279
- } catch {
3765
+ const parent = await resolveTask(client, parentRef);
3766
+ parentTaskId = String(parent.id);
3767
+ } catch (err) {
3768
+ if (!input2.parentTaskId) {
3769
+ throw new Error(
3770
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
3771
+ );
3772
+ }
3773
+ throw err;
3280
3774
  }
3281
- if (!claudeJson.projects) claudeJson.projects = {};
3282
- const projects = claudeJson.projects;
3283
- const trustDir = opts?.cwd ?? projectDir;
3284
- if (!projects[trustDir]) projects[trustDir] = {};
3285
- projects[trustDir].hasTrustDialogAccepted = true;
3286
- writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3287
- } catch {
3288
3775
  }
3289
- try {
3290
- const settingsDir = path14.join(os6.homedir(), ".claude", "projects");
3291
- const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3292
- const projSettingsDir = path14.join(settingsDir, normalizedKey);
3293
- const settingsPath = path14.join(projSettingsDir, "settings.json");
3294
- let settings = {};
3776
+ let warning;
3777
+ const dupCheck = await client.execute({
3778
+ sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
3779
+ args: [input2.title, input2.assignedTo]
3780
+ });
3781
+ if (dupCheck.rows.length > 0) {
3782
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
3783
+ }
3784
+ if (input2.baseDir) {
3295
3785
  try {
3296
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
3786
+ await mkdir4(path14.join(input2.baseDir, "exe", "output"), { recursive: true });
3787
+ await mkdir4(path14.join(input2.baseDir, "exe", "research"), { recursive: true });
3788
+ await ensureArchitectureDoc(input2.baseDir, input2.projectName);
3789
+ await ensureGitignoreExe(input2.baseDir);
3297
3790
  } catch {
3298
3791
  }
3299
- const perms = settings.permissions ?? {};
3300
- const allow = perms.allow ?? [];
3301
- const toolNames = [
3302
- "recall_my_memory",
3303
- "store_memory",
3304
- "create_task",
3305
- "update_task",
3306
- "list_tasks",
3307
- "get_task",
3308
- "ask_team_memory",
3309
- "store_behavior",
3310
- "get_identity",
3311
- "send_message"
3312
- ];
3313
- const requiredTools = expandDualPrefixTools(toolNames);
3314
- let changed = false;
3315
- for (const tool of requiredTools) {
3316
- if (!allow.includes(tool)) {
3317
- allow.push(tool);
3318
- changed = true;
3319
- }
3320
- }
3321
- if (changed) {
3322
- perms.allow = allow;
3323
- settings.permissions = perms;
3324
- mkdirSync5(projSettingsDir, { recursive: true });
3325
- writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3326
- }
3792
+ }
3793
+ const complexity = input2.complexity ?? "standard";
3794
+ let sessionScope = null;
3795
+ try {
3796
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3797
+ sessionScope = resolveExeSession2();
3327
3798
  } catch {
3328
3799
  }
3329
- const spawnCwd = opts?.cwd ?? projectDir;
3330
- const useExeAgent = !!(opts?.model && opts?.provider);
3331
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
3332
- const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
3333
- let identityFlag = "";
3334
- let behaviorsFlag = "";
3335
- let legacyFallbackWarned = false;
3336
- if (!useExeAgent && !useBinSymlink) {
3337
- const identityPath = path14.join(
3338
- os6.homedir(),
3339
- ".exe-os",
3340
- "identity",
3341
- `${employeeName}.md`
3342
- );
3343
- _resetCcAgentSupportCache();
3344
- const hasAgentFlag = claudeSupportsAgentFlag();
3345
- if (hasAgentFlag) {
3346
- identityFlag = ` --agent ${employeeName}`;
3347
- } else if (existsSync13(identityPath)) {
3348
- identityFlag = ` --append-system-prompt-file ${identityPath}`;
3349
- legacyFallbackWarned = true;
3800
+ await client.execute({
3801
+ 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)
3802
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3803
+ args: [
3804
+ id,
3805
+ input2.title,
3806
+ input2.assignedTo,
3807
+ input2.assignedBy,
3808
+ input2.projectName,
3809
+ input2.priority,
3810
+ initialStatus,
3811
+ taskFile,
3812
+ blockedById,
3813
+ parentTaskId,
3814
+ input2.reviewer ?? null,
3815
+ input2.context,
3816
+ complexity,
3817
+ input2.budgetTokens ?? null,
3818
+ input2.budgetFallbackModel ?? null,
3819
+ 0,
3820
+ null,
3821
+ sessionScope,
3822
+ now,
3823
+ now
3824
+ ]
3825
+ });
3826
+ return {
3827
+ id,
3828
+ title: input2.title,
3829
+ assignedTo: input2.assignedTo,
3830
+ assignedBy: input2.assignedBy,
3831
+ projectName: input2.projectName,
3832
+ priority: input2.priority,
3833
+ status: initialStatus,
3834
+ taskFile,
3835
+ createdAt: now,
3836
+ updatedAt: now,
3837
+ warning,
3838
+ budgetTokens: input2.budgetTokens ?? null,
3839
+ budgetFallbackModel: input2.budgetFallbackModel ?? null,
3840
+ tokensUsed: 0,
3841
+ tokensWarnedAt: null
3842
+ };
3843
+ }
3844
+ async function listTasks(input2) {
3845
+ const client = getClient();
3846
+ const conditions = [];
3847
+ const args = [];
3848
+ if (input2.assignedTo) {
3849
+ conditions.push("assigned_to = ?");
3850
+ args.push(input2.assignedTo);
3851
+ }
3852
+ if (input2.status) {
3853
+ conditions.push("status = ?");
3854
+ args.push(input2.status);
3855
+ } else {
3856
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
3857
+ }
3858
+ if (input2.projectName) {
3859
+ conditions.push("project_name = ?");
3860
+ args.push(input2.projectName);
3861
+ }
3862
+ if (input2.priority) {
3863
+ conditions.push("priority = ?");
3864
+ args.push(input2.priority);
3865
+ }
3866
+ try {
3867
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3868
+ const session = resolveExeSession2();
3869
+ if (session) {
3870
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
3871
+ args.push(session);
3350
3872
  }
3351
- const behaviorsFile = exportBehaviorsSync(
3352
- employeeName,
3353
- path14.basename(spawnCwd),
3354
- sessionName
3355
- );
3356
- if (behaviorsFile) {
3357
- behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
3873
+ } catch {
3874
+ }
3875
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3876
+ const result = await client.execute({
3877
+ 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`,
3878
+ args
3879
+ });
3880
+ return result.rows.map((r) => ({
3881
+ id: String(r.id),
3882
+ title: String(r.title),
3883
+ assignedTo: String(r.assigned_to),
3884
+ assignedBy: String(r.assigned_by),
3885
+ projectName: String(r.project_name),
3886
+ priority: String(r.priority),
3887
+ status: String(r.status),
3888
+ taskFile: String(r.task_file),
3889
+ createdAt: String(r.created_at),
3890
+ updatedAt: String(r.updated_at),
3891
+ checkpointCount: Number(r.checkpoint_count ?? 0),
3892
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
3893
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
3894
+ tokensUsed: Number(r.tokens_used ?? 0),
3895
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
3896
+ }));
3897
+ }
3898
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
3899
+ if (!taskContext) return null;
3900
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
3901
+ try {
3902
+ const since = new Date(taskCreatedAt).toISOString();
3903
+ const branch = execSync7(
3904
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
3905
+ { encoding: "utf8", timeout: 3e3 }
3906
+ ).trim();
3907
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
3908
+ const commitCount = execSync7(
3909
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
3910
+ { encoding: "utf8", timeout: 5e3 }
3911
+ ).trim();
3912
+ const count = parseInt(commitCount, 10);
3913
+ if (count === 0) {
3914
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
3358
3915
  }
3916
+ return null;
3917
+ } catch {
3918
+ return null;
3359
3919
  }
3360
- if (legacyFallbackWarned) {
3920
+ }
3921
+ async function updateTaskStatus(input2) {
3922
+ const client = getClient();
3923
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3924
+ const row = await resolveTask(client, input2.taskId);
3925
+ const taskId = String(row.id);
3926
+ const taskFile = String(row.task_file);
3927
+ if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
3361
3928
  process.stderr.write(
3362
- `[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.
3929
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
3363
3930
  `
3364
3931
  );
3365
3932
  }
3366
- let sessionContextFlag = "";
3367
- try {
3368
- const ctxDir = path14.join(os6.homedir(), ".exe-os", "session-cache");
3369
- mkdirSync5(ctxDir, { recursive: true });
3370
- const ctxFile = path14.join(ctxDir, `session-context-${sessionName}.md`);
3371
- const ctxContent = [
3372
- `## Session Context`,
3373
- `You are running in tmux session: ${sessionName}.`,
3374
- `Your parent exe session is ${exeSession}.`,
3375
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
3376
- ].join("\n");
3377
- writeFileSync4(ctxFile, ctxContent);
3378
- sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
3379
- } catch {
3380
- }
3381
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
3382
- if (ccProvider !== DEFAULT_PROVIDER) {
3383
- const cfg = PROVIDER_TABLE[ccProvider];
3384
- if (cfg?.apiKeyEnv) {
3385
- const keyVal = process.env[cfg.apiKeyEnv];
3386
- if (keyVal) {
3387
- envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
3933
+ if (input2.status === "done") {
3934
+ const existingRow = await client.execute({
3935
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
3936
+ args: [taskId]
3937
+ });
3938
+ if (existingRow.rows.length > 0) {
3939
+ const ctx = existingRow.rows[0];
3940
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
3941
+ if (warning) {
3942
+ input2.result = input2.result ? `\u26A0\uFE0F ${warning}
3943
+
3944
+ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
3945
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
3946
+ `);
3388
3947
  }
3389
3948
  }
3390
3949
  }
3391
- let spawnCommand;
3392
- if (useExeAgent) {
3393
- spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
3394
- } else if (useBinSymlink) {
3395
- const binName = `${employeeName}-${ccProvider}`;
3396
- process.stderr.write(
3397
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
3398
- `
3399
- );
3400
- spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
3401
- } else {
3402
- spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
3403
- }
3404
- const spawnResult = transport.spawn(sessionName, {
3405
- cwd: spawnCwd,
3406
- command: spawnCommand
3407
- });
3408
- if (spawnResult.error) {
3409
- releaseSpawnLock2(sessionName);
3410
- return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3411
- }
3412
- transport.pipeLog(sessionName, logFile);
3413
- try {
3414
- const mySession = getMySession();
3415
- const dispatchInfo = path14.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
3416
- writeFileSync4(dispatchInfo, JSON.stringify({
3417
- dispatchedBy: mySession,
3418
- rootExe: exeSession,
3419
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
3420
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
3421
- }));
3422
- } catch {
3423
- }
3424
- let booted = false;
3425
- for (let i = 0; i < 30; i++) {
3426
- try {
3427
- execSync7("sleep 0.5");
3428
- } catch {
3950
+ if (input2.status === "in_progress") {
3951
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
3952
+ const claim = await client.execute({
3953
+ sql: `UPDATE tasks
3954
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
3955
+ WHERE id = ? AND status = 'open'`,
3956
+ args: [tmuxSession, now, taskId]
3957
+ });
3958
+ if (claim.rowsAffected === 0) {
3959
+ const current = await client.execute({
3960
+ sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
3961
+ args: [taskId]
3962
+ });
3963
+ const cur = current.rows[0];
3964
+ const status = cur?.status ?? "unknown";
3965
+ const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
3966
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
3429
3967
  }
3430
3968
  try {
3431
- const pane = transport.capturePane(sessionName);
3432
- if (useExeAgent) {
3433
- if (pane.includes("[exe-agent]") || pane.includes("online")) {
3434
- booted = true;
3435
- break;
3436
- }
3437
- } else {
3438
- if (pane.includes("Claude Code") || pane.includes("\u276F")) {
3439
- booted = true;
3440
- break;
3441
- }
3442
- }
3969
+ await writeCheckpoint({
3970
+ taskId,
3971
+ step: "claimed",
3972
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3973
+ });
3443
3974
  } catch {
3444
3975
  }
3976
+ return { row, taskFile, now, taskId };
3445
3977
  }
3446
- if (!booted) {
3447
- releaseSpawnLock2(sessionName);
3448
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3978
+ if (input2.result) {
3979
+ await client.execute({
3980
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
3981
+ args: [input2.status, input2.result, now, taskId]
3982
+ });
3983
+ } else {
3984
+ await client.execute({
3985
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
3986
+ args: [input2.status, now, taskId]
3987
+ });
3449
3988
  }
3450
- if (!useExeAgent) {
3451
- try {
3452
- transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
3453
- } catch {
3989
+ try {
3990
+ await writeCheckpoint({
3991
+ taskId,
3992
+ step: `status_transition:${input2.status}`,
3993
+ contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
3994
+ });
3995
+ } catch {
3996
+ }
3997
+ return { row, taskFile, now, taskId };
3998
+ }
3999
+ async function deleteTaskCore(taskId, _baseDir) {
4000
+ const client = getClient();
4001
+ const row = await resolveTask(client, taskId);
4002
+ const id = String(row.id);
4003
+ const taskFile = String(row.task_file);
4004
+ const assignedTo = String(row.assigned_to);
4005
+ const assignedBy = String(row.assigned_by);
4006
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
4007
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
4008
+ return { taskFile, assignedTo, assignedBy, taskSlug };
4009
+ }
4010
+ async function ensureArchitectureDoc(baseDir, projectName) {
4011
+ const archPath = path14.join(baseDir, "exe", "ARCHITECTURE.md");
4012
+ try {
4013
+ if (existsSync13(archPath)) return;
4014
+ const template = [
4015
+ `# ${projectName} \u2014 System Architecture`,
4016
+ "",
4017
+ "> Employees: read this before every task. Update it when you change system structure.",
4018
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
4019
+ "",
4020
+ "## Overview",
4021
+ "",
4022
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
4023
+ "",
4024
+ "## Key Components",
4025
+ "",
4026
+ "<!-- List the major modules, services, or subsystems. -->",
4027
+ "",
4028
+ "## Data Flow",
4029
+ "",
4030
+ "<!-- How does data move through the system? What writes where? -->",
4031
+ "",
4032
+ "## Invariants",
4033
+ "",
4034
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
4035
+ "",
4036
+ "## Dependencies",
4037
+ "",
4038
+ "<!-- What depends on what? If I change X, what else is affected? -->",
4039
+ ""
4040
+ ].join("\n");
4041
+ await writeFile4(archPath, template, "utf-8");
4042
+ } catch {
4043
+ }
4044
+ }
4045
+ async function ensureGitignoreExe(baseDir) {
4046
+ const gitignorePath = path14.join(baseDir, ".gitignore");
4047
+ try {
4048
+ if (existsSync13(gitignorePath)) {
4049
+ const content = readFileSync11(gitignorePath, "utf-8");
4050
+ if (/^\/?exe\/?$/m.test(content)) return;
4051
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
4052
+ } else {
4053
+ await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
3454
4054
  }
4055
+ } catch {
3455
4056
  }
3456
- registerSession({
3457
- windowName: sessionName,
3458
- agentId: employeeName,
3459
- projectDir: spawnCwd,
3460
- parentExe: exeSession,
3461
- pid: 0,
3462
- registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3463
- });
3464
- releaseSpawnLock2(sessionName);
3465
- return { sessionName };
3466
4057
  }
3467
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3468
- var init_tmux_routing = __esm({
3469
- "src/lib/tmux-routing.ts"() {
4058
+ var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4059
+ var init_tasks_crud = __esm({
4060
+ "src/lib/tasks-crud.ts"() {
3470
4061
  "use strict";
3471
- init_session_registry();
3472
- init_session_key();
3473
- init_transport();
3474
- init_cc_agent_support();
3475
- init_mcp_prefix();
3476
- init_provider_table();
3477
- init_intercom_queue();
3478
- init_plan_limits();
3479
- SPAWN_LOCK_DIR = path14.join(os6.homedir(), ".exe-os", "spawn-locks");
3480
- SESSION_CACHE = path14.join(os6.homedir(), ".exe-os", "session-cache");
3481
- BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3482
- INTERCOM_DEBOUNCE_MS = 3e4;
3483
- INTERCOM_LOG2 = path14.join(os6.homedir(), ".exe-os", "intercom.log");
3484
- DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
3485
- DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3486
- BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4062
+ init_database();
4063
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
4064
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3487
4065
  }
3488
4066
  });
3489
4067
 
@@ -3658,6 +4236,7 @@ var init_tasks_review = __esm({
3658
4236
  init_tasks_crud();
3659
4237
  init_tmux_routing();
3660
4238
  init_session_key();
4239
+ init_state_bus();
3661
4240
  }
3662
4241
  });
3663
4242
 
@@ -3780,13 +4359,12 @@ function assertSessionScope(actionType, targetProject) {
3780
4359
  };
3781
4360
  }
3782
4361
  process.stderr.write(
3783
- `[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
4362
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3784
4363
  `
3785
4364
  );
3786
4365
  return {
3787
- allowed: true,
3788
- // v1: warn-only, don't block
3789
- reason: "cross_session_granted",
4366
+ allowed: false,
4367
+ reason: "cross_session_denied",
3790
4368
  currentProject,
3791
4369
  targetProject,
3792
4370
  targetSession: findSessionForProject(targetProject)?.windowName
@@ -3812,8 +4390,9 @@ async function dispatchTaskToEmployee(input2) {
3812
4390
  try {
3813
4391
  const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
3814
4392
  const check = assertSessionScope2("dispatch_task", input2.projectName);
3815
- if (check.reason === "cross_session_granted") {
4393
+ if (check.reason === "cross_session_denied") {
3816
4394
  crossProject = true;
4395
+ return { dispatched: "skipped", crossProject: true };
3817
4396
  }
3818
4397
  } catch {
3819
4398
  }
@@ -3870,10 +4449,10 @@ var init_tasks_notify = __esm({
3870
4449
  });
3871
4450
 
3872
4451
  // src/lib/behaviors.ts
3873
- import crypto5 from "crypto";
4452
+ import crypto6 from "crypto";
3874
4453
  async function storeBehavior(opts) {
3875
4454
  const client = getClient();
3876
- const id = crypto5.randomUUID();
4455
+ const id = crypto6.randomUUID();
3877
4456
  const now = (/* @__PURE__ */ new Date()).toISOString();
3878
4457
  await client.execute({
3879
4458
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -3902,7 +4481,7 @@ __export(skill_learning_exports, {
3902
4481
  storeTrajectory: () => storeTrajectory,
3903
4482
  sweepTrajectories: () => sweepTrajectories
3904
4483
  });
3905
- import crypto6 from "crypto";
4484
+ import crypto7 from "crypto";
3906
4485
  async function extractTrajectory(taskId, agentId) {
3907
4486
  const client = getClient();
3908
4487
  const result = await client.execute({
@@ -3931,11 +4510,11 @@ async function extractTrajectory(taskId, agentId) {
3931
4510
  return signature;
3932
4511
  }
3933
4512
  function hashSignature(signature) {
3934
- return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4513
+ return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
3935
4514
  }
3936
4515
  async function storeTrajectory(opts) {
3937
4516
  const client = getClient();
3938
- const id = crypto6.randomUUID();
4517
+ const id = crypto7.randomUUID();
3939
4518
  const now = (/* @__PURE__ */ new Date()).toISOString();
3940
4519
  const signatureHash = hashSignature(opts.signature);
3941
4520
  await client.execute({
@@ -4276,6 +4855,13 @@ async function updateTask(input2) {
4276
4855
  await cascadeUnblock(taskId, input2.baseDir, now);
4277
4856
  } catch {
4278
4857
  }
4858
+ orgBus.emit({
4859
+ type: "task_completed",
4860
+ taskId,
4861
+ employee: String(row.assigned_to),
4862
+ result: input2.result ?? "",
4863
+ timestamp: now
4864
+ });
4279
4865
  if (row.parent_task_id) {
4280
4866
  try {
4281
4867
  await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
@@ -4343,6 +4929,7 @@ var init_tasks = __esm({
4343
4929
  init_database();
4344
4930
  init_config();
4345
4931
  init_notifications();
4932
+ init_state_bus();
4346
4933
  init_tasks_crud();
4347
4934
  init_tasks_review();
4348
4935
  init_tasks_crud();
@@ -4411,7 +4998,7 @@ var init_worker_gate = __esm({
4411
4998
  });
4412
4999
 
4413
5000
  // src/adapters/claude/hooks/ingest-worker.ts
4414
- import crypto7 from "crypto";
5001
+ import crypto8 from "crypto";
4415
5002
  import { execSync as execSync8 } from "child_process";
4416
5003
  import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
4417
5004
  import path19 from "path";
@@ -4570,6 +5157,7 @@ async function getMasterKey() {
4570
5157
 
4571
5158
  // src/lib/store.ts
4572
5159
  init_config();
5160
+ init_state_bus();
4573
5161
  var INIT_MAX_RETRIES = 3;
4574
5162
  var INIT_RETRY_DELAY_MS = 1e3;
4575
5163
  function isBusyError2(err) {
@@ -4640,6 +5228,11 @@ async function initStore(options) {
4640
5228
  "version-query"
4641
5229
  );
4642
5230
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
5231
+ try {
5232
+ const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
5233
+ await loadGlobalProcedures2();
5234
+ } catch {
5235
+ }
4643
5236
  }
4644
5237
  function classifyTier(record) {
4645
5238
  if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
@@ -4681,6 +5274,12 @@ async function writeMemory(record) {
4681
5274
  supersedes_id: record.supersedes_id ?? null
4682
5275
  };
4683
5276
  _pendingRecords.push(dbRow);
5277
+ orgBus.emit({
5278
+ type: "memory_stored",
5279
+ agentId: record.agent_id,
5280
+ project: record.project_name,
5281
+ timestamp: record.timestamp
5282
+ });
4684
5283
  const MAX_PENDING = 1e3;
4685
5284
  if (_pendingRecords.length > MAX_PENDING) {
4686
5285
  const dropped = _pendingRecords.length - MAX_PENDING;
@@ -5042,7 +5641,14 @@ process.stdin.on("data", (chunk) => {
5042
5641
  });
5043
5642
  process.stdin.on("end", async () => {
5044
5643
  try {
5045
- const data = JSON.parse(input);
5644
+ let data;
5645
+ try {
5646
+ data = JSON.parse(input);
5647
+ } catch (parseErr) {
5648
+ process.stderr.write(`[ingest-worker] skipped: truncated stdin (${input.length} bytes)
5649
+ `);
5650
+ process.exit(0);
5651
+ }
5046
5652
  const rawText = extractSemanticText(data.tool_name, data.tool_input, data.tool_response);
5047
5653
  if (rawText.length < 50) {
5048
5654
  process.exit(0);
@@ -5082,7 +5688,7 @@ process.stdin.on("end", async () => {
5082
5688
  }
5083
5689
  const agentId = process.env.AGENT_ID;
5084
5690
  await writeMemory({
5085
- id: crypto7.randomUUID(),
5691
+ id: crypto8.randomUUID(),
5086
5692
  agent_id: agentId,
5087
5693
  agent_role: process.env.AGENT_ROLE ?? "unknown",
5088
5694
  session_id: data.session_id,
@@ -5192,7 +5798,7 @@ process.stdin.on("end", async () => {
5192
5798
  });
5193
5799
  const assignmentText = `TASK ASSIGNED: ${title} \u2192 ${employee} [${priority}] in ${projectName}`;
5194
5800
  await writeMemory({
5195
- id: crypto7.randomUUID(),
5801
+ id: crypto8.randomUUID(),
5196
5802
  agent_id: assignedBy,
5197
5803
  agent_role: "COO",
5198
5804
  session_id: data.session_id,