@askexenow/exe-os 0.8.40 → 0.8.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/bin/backfill-conversations.js +805 -642
  2. package/dist/bin/backfill-responses.js +804 -641
  3. package/dist/bin/backfill-vectors.js +791 -634
  4. package/dist/bin/cleanup-stale-review-tasks.js +788 -631
  5. package/dist/bin/cli.js +1376 -659
  6. package/dist/bin/exe-agent.js +20 -1
  7. package/dist/bin/exe-assign.js +1503 -1343
  8. package/dist/bin/exe-boot.js +2549 -1784
  9. package/dist/bin/exe-call.js +39 -1
  10. package/dist/bin/exe-cloud.js +12 -2
  11. package/dist/bin/exe-dispatch.js +39 -2
  12. package/dist/bin/exe-doctor.js +791 -634
  13. package/dist/bin/exe-export-behaviors.js +792 -637
  14. package/dist/bin/exe-forget.js +145 -0
  15. package/dist/bin/exe-gateway.js +2501 -1846
  16. package/dist/bin/exe-heartbeat.js +147 -1
  17. package/dist/bin/exe-kill.js +795 -640
  18. package/dist/bin/exe-launch-agent.js +2168 -2008
  19. package/dist/bin/exe-link.js +44 -12
  20. package/dist/bin/exe-new-employee.js +6 -2
  21. package/dist/bin/exe-pending-messages.js +146 -1
  22. package/dist/bin/exe-pending-notifications.js +788 -631
  23. package/dist/bin/exe-pending-reviews.js +176 -1
  24. package/dist/bin/exe-rename.js +23 -0
  25. package/dist/bin/exe-review.js +490 -327
  26. package/dist/bin/exe-search.js +157 -4
  27. package/dist/bin/exe-session-cleanup.js +2487 -403
  28. package/dist/bin/exe-settings.js +2 -1
  29. package/dist/bin/exe-status.js +474 -317
  30. package/dist/bin/exe-team.js +474 -317
  31. package/dist/bin/git-sweep.js +2691 -151
  32. package/dist/bin/graph-backfill.js +794 -637
  33. package/dist/bin/graph-export.js +798 -641
  34. package/dist/bin/scan-tasks.js +2951 -44
  35. package/dist/bin/setup.js +50 -26
  36. package/dist/bin/shard-migrate.js +792 -637
  37. package/dist/bin/wiki-sync.js +794 -637
  38. package/dist/gateway/index.js +2542 -1887
  39. package/dist/hooks/bug-report-worker.js +2118 -576
  40. package/dist/hooks/commit-complete.js +2690 -150
  41. package/dist/hooks/error-recall.js +157 -4
  42. package/dist/hooks/ingest-worker.js +1455 -803
  43. package/dist/hooks/instructions-loaded.js +151 -0
  44. package/dist/hooks/notification.js +153 -2
  45. package/dist/hooks/post-compact.js +164 -0
  46. package/dist/hooks/pre-compact.js +3073 -101
  47. package/dist/hooks/pre-tool-use.js +151 -0
  48. package/dist/hooks/prompt-ingest-worker.js +1670 -1509
  49. package/dist/hooks/prompt-submit.js +2650 -1074
  50. package/dist/hooks/response-ingest-worker.js +154 -6
  51. package/dist/hooks/session-end.js +153 -2
  52. package/dist/hooks/session-start.js +157 -4
  53. package/dist/hooks/stop.js +151 -0
  54. package/dist/hooks/subagent-stop.js +155 -2
  55. package/dist/hooks/summary-worker.js +190 -21
  56. package/dist/index.js +326 -102
  57. package/dist/lib/cloud-sync.js +31 -10
  58. package/dist/lib/config.js +2 -0
  59. package/dist/lib/consolidation.js +69 -2
  60. package/dist/lib/database.js +19 -0
  61. package/dist/lib/device-registry.js +19 -0
  62. package/dist/lib/embedder.js +3 -1
  63. package/dist/lib/employee-templates.js +20 -1
  64. package/dist/lib/exe-daemon.js +285 -18
  65. package/dist/lib/hybrid-search.js +157 -4
  66. package/dist/lib/messaging.js +39 -2
  67. package/dist/lib/schedules.js +792 -637
  68. package/dist/lib/store.js +796 -636
  69. package/dist/lib/tasks.js +1485 -918
  70. package/dist/lib/tmux-routing.js +194 -10
  71. package/dist/mcp/server.js +1643 -924
  72. package/dist/mcp/tools/create-task.js +2283 -829
  73. package/dist/mcp/tools/list-tasks.js +2788 -159
  74. package/dist/mcp/tools/send-message.js +39 -2
  75. package/dist/mcp/tools/update-task.js +79 -0
  76. package/dist/runtime/index.js +280 -68
  77. package/dist/tui/App.js +1485 -645
  78. package/package.json +3 -2
@@ -26,6 +26,61 @@ var __copyProps = (to, from, except, desc) => {
26
26
  };
27
27
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
28
 
29
+ // src/lib/state-bus.ts
30
+ var StateBus, orgBus;
31
+ var init_state_bus = __esm({
32
+ "src/lib/state-bus.ts"() {
33
+ "use strict";
34
+ StateBus = class {
35
+ handlers = /* @__PURE__ */ new Map();
36
+ globalHandlers = /* @__PURE__ */ new Set();
37
+ /** Emit an event to all subscribers */
38
+ emit(event) {
39
+ const typeHandlers = this.handlers.get(event.type);
40
+ if (typeHandlers) {
41
+ for (const handler of typeHandlers) {
42
+ try {
43
+ handler(event);
44
+ } catch {
45
+ }
46
+ }
47
+ }
48
+ for (const handler of this.globalHandlers) {
49
+ try {
50
+ handler(event);
51
+ } catch {
52
+ }
53
+ }
54
+ }
55
+ /** Subscribe to a specific event type */
56
+ on(type, handler) {
57
+ if (!this.handlers.has(type)) {
58
+ this.handlers.set(type, /* @__PURE__ */ new Set());
59
+ }
60
+ this.handlers.get(type).add(handler);
61
+ }
62
+ /** Subscribe to ALL events */
63
+ onAny(handler) {
64
+ this.globalHandlers.add(handler);
65
+ }
66
+ /** Unsubscribe from a specific event type */
67
+ off(type, handler) {
68
+ this.handlers.get(type)?.delete(handler);
69
+ }
70
+ /** Unsubscribe from ALL events */
71
+ offAny(handler) {
72
+ this.globalHandlers.delete(handler);
73
+ }
74
+ /** Remove all listeners */
75
+ clear() {
76
+ this.handlers.clear();
77
+ this.globalHandlers.clear();
78
+ }
79
+ };
80
+ orgBus = new StateBus();
81
+ }
82
+ });
83
+
29
84
  // src/gateway/crm-bridge.ts
30
85
  var crm_bridge_exports = {};
31
86
  __export(crm_bridge_exports, {
@@ -575,6 +630,13 @@ async function ensureSchema() {
575
630
  });
576
631
  } catch {
577
632
  }
633
+ try {
634
+ await client.execute({
635
+ sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
636
+ args: []
637
+ });
638
+ } catch {
639
+ }
578
640
  try {
579
641
  await client.execute({
580
642
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -1021,6 +1083,18 @@ async function ensureSchema() {
1021
1083
  CREATE INDEX IF NOT EXISTS idx_session_kills_agent
1022
1084
  ON session_kills(agent_id);
1023
1085
  `);
1086
+ await client.execute(`
1087
+ CREATE TABLE IF NOT EXISTS global_procedures (
1088
+ id TEXT PRIMARY KEY,
1089
+ title TEXT NOT NULL,
1090
+ content TEXT NOT NULL,
1091
+ priority TEXT NOT NULL DEFAULT 'p0',
1092
+ domain TEXT,
1093
+ active INTEGER NOT NULL DEFAULT 1,
1094
+ created_at TEXT NOT NULL,
1095
+ updated_at TEXT NOT NULL
1096
+ )
1097
+ `);
1024
1098
  await client.executeMultiple(`
1025
1099
  CREATE TABLE IF NOT EXISTS conversations (
1026
1100
  id TEXT PRIMARY KEY,
@@ -1192,6 +1266,7 @@ var config_exports = {};
1192
1266
  __export(config_exports, {
1193
1267
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
1194
1268
  CONFIG_PATH: () => CONFIG_PATH,
1269
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
1195
1270
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
1196
1271
  DB_PATH: () => DB_PATH,
1197
1272
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -1347,7 +1422,7 @@ async function loadConfigFrom(configPath) {
1347
1422
  return { ...DEFAULT_CONFIG };
1348
1423
  }
1349
1424
  }
1350
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1425
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1351
1426
  var init_config = __esm({
1352
1427
  "src/lib/config.ts"() {
1353
1428
  "use strict";
@@ -1355,6 +1430,7 @@ var init_config = __esm({
1355
1430
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
1356
1431
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
1357
1432
  CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
1433
+ COO_AGENT_NAME = "exe";
1358
1434
  LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
1359
1435
  CURRENT_CONFIG_VERSION = 1;
1360
1436
  DEFAULT_CONFIG = {
@@ -2133,6 +2209,71 @@ var init_shard_manager = __esm({
2133
2209
  }
2134
2210
  });
2135
2211
 
2212
+ // src/lib/global-procedures.ts
2213
+ var global_procedures_exports = {};
2214
+ __export(global_procedures_exports, {
2215
+ deactivateGlobalProcedure: () => deactivateGlobalProcedure,
2216
+ getGlobalProceduresBlock: () => getGlobalProceduresBlock,
2217
+ loadGlobalProcedures: () => loadGlobalProcedures,
2218
+ storeGlobalProcedure: () => storeGlobalProcedure
2219
+ });
2220
+ import { randomUUID as randomUUID2 } from "crypto";
2221
+ async function loadGlobalProcedures() {
2222
+ const client = getClient();
2223
+ const result = await client.execute({
2224
+ sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
2225
+ args: []
2226
+ });
2227
+ const procedures = result.rows;
2228
+ if (procedures.length > 0) {
2229
+ _cache = procedures.map((p) => `### ${p.title}
2230
+ ${p.content}`).join("\n\n");
2231
+ } else {
2232
+ _cache = "";
2233
+ }
2234
+ _cacheLoaded = true;
2235
+ return procedures;
2236
+ }
2237
+ function getGlobalProceduresBlock() {
2238
+ if (!_cacheLoaded) return "";
2239
+ if (!_cache) return "";
2240
+ return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
2241
+
2242
+ ${_cache}
2243
+ `;
2244
+ }
2245
+ async function storeGlobalProcedure(input) {
2246
+ const id = randomUUID2();
2247
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2248
+ const client = getClient();
2249
+ await client.execute({
2250
+ sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
2251
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
2252
+ args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
2253
+ });
2254
+ await loadGlobalProcedures();
2255
+ return id;
2256
+ }
2257
+ async function deactivateGlobalProcedure(id) {
2258
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2259
+ const client = getClient();
2260
+ const result = await client.execute({
2261
+ sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
2262
+ args: [now, id]
2263
+ });
2264
+ await loadGlobalProcedures();
2265
+ return result.rowsAffected > 0;
2266
+ }
2267
+ var _cache, _cacheLoaded;
2268
+ var init_global_procedures = __esm({
2269
+ "src/lib/global-procedures.ts"() {
2270
+ "use strict";
2271
+ init_database();
2272
+ _cache = "";
2273
+ _cacheLoaded = false;
2274
+ }
2275
+ });
2276
+
2136
2277
  // src/lib/store.ts
2137
2278
  var store_exports = {};
2138
2279
  __export(store_exports, {
@@ -2212,6 +2353,11 @@ async function initStore(options) {
2212
2353
  "version-query"
2213
2354
  );
2214
2355
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
2356
+ try {
2357
+ const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
2358
+ await loadGlobalProcedures2();
2359
+ } catch {
2360
+ }
2215
2361
  }
2216
2362
  function classifyTier(record) {
2217
2363
  if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
@@ -2253,6 +2399,12 @@ async function writeMemory(record) {
2253
2399
  supersedes_id: record.supersedes_id ?? null
2254
2400
  };
2255
2401
  _pendingRecords.push(dbRow);
2402
+ orgBus.emit({
2403
+ type: "memory_stored",
2404
+ agentId: record.agent_id,
2405
+ project: record.project_name,
2406
+ timestamp: record.timestamp
2407
+ });
2256
2408
  const MAX_PENDING = 1e3;
2257
2409
  if (_pendingRecords.length > MAX_PENDING) {
2258
2410
  const dropped = _pendingRecords.length - MAX_PENDING;
@@ -2598,6 +2750,7 @@ var init_store = __esm({
2598
2750
  init_database();
2599
2751
  init_keychain();
2600
2752
  init_config();
2753
+ init_state_bus();
2601
2754
  INIT_MAX_RETRIES = 3;
2602
2755
  INIT_RETRY_DELAY_MS = 1e3;
2603
2756
  _pendingRecords = [];
@@ -2769,7 +2922,7 @@ __export(license_exports, {
2769
2922
  validateLicense: () => validateLicense
2770
2923
  });
2771
2924
  import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
2772
- import { randomUUID as randomUUID2 } from "crypto";
2925
+ import { randomUUID as randomUUID3 } from "crypto";
2773
2926
  import path5 from "path";
2774
2927
  import { jwtVerify, importSPKI } from "jose";
2775
2928
  async function fetchRetry(url, init) {
@@ -2796,7 +2949,7 @@ function loadDeviceId() {
2796
2949
  }
2797
2950
  } catch {
2798
2951
  }
2799
- const id = randomUUID2();
2952
+ const id = randomUUID3();
2800
2953
  mkdirSync2(EXE_AI_DIR, { recursive: true });
2801
2954
  writeFileSync(DEVICE_ID_PATH, id, "utf8");
2802
2955
  return id;
@@ -3097,7 +3250,7 @@ var whatsapp_exports = {};
3097
3250
  __export(whatsapp_exports, {
3098
3251
  WhatsAppAdapter: () => WhatsAppAdapter
3099
3252
  });
3100
- import { randomUUID as randomUUID3 } from "crypto";
3253
+ import { randomUUID as randomUUID4 } from "crypto";
3101
3254
  import { homedir } from "os";
3102
3255
  import { join } from "path";
3103
3256
  import { mkdirSync as mkdirSync3 } from "fs";
@@ -3283,7 +3436,7 @@ var init_whatsapp = __esm({
3283
3436
  const location = this.extractLocation(msg.message);
3284
3437
  const dataCategory = location ? "location" : "message";
3285
3438
  return {
3286
- messageId: msg.key.id ?? randomUUID3(),
3439
+ messageId: msg.key.id ?? randomUUID4(),
3287
3440
  platform: "whatsapp",
3288
3441
  senderId,
3289
3442
  senderName: msg.pushName ?? void 0,
@@ -3328,7 +3481,7 @@ var init_whatsapp = __esm({
3328
3481
  }
3329
3482
  const timestamp = receipt.readTimestamp ?? receipt.receiptTimestamp ?? Date.now() / 1e3;
3330
3483
  return {
3331
- messageId: randomUUID3(),
3484
+ messageId: randomUUID4(),
3332
3485
  platform: "whatsapp",
3333
3486
  senderId: remoteJid.replace("@s.whatsapp.net", "").replace("@g.us", ""),
3334
3487
  channelId: remoteJid,
@@ -3351,7 +3504,7 @@ var init_whatsapp = __esm({
3351
3504
  const phone = id.replace("@s.whatsapp.net", "").replace("@g.us", "");
3352
3505
  const name = contact.name ?? contact.notify ?? phone;
3353
3506
  return {
3354
- messageId: randomUUID3(),
3507
+ messageId: randomUUID4(),
3355
3508
  platform: "whatsapp",
3356
3509
  senderId: phone,
3357
3510
  senderName: name,
@@ -3375,7 +3528,7 @@ var init_whatsapp = __esm({
3375
3528
  const participants = (group.participants ?? []).map((p) => p.id ?? p);
3376
3529
  const admins = (group.participants ?? []).filter((p) => p.admin === "admin" || p.admin === "superadmin").map((p) => p.id ?? p);
3377
3530
  return {
3378
- messageId: randomUUID3(),
3531
+ messageId: randomUUID4(),
3379
3532
  platform: "whatsapp",
3380
3533
  senderId: groupId,
3381
3534
  channelId: groupId,
@@ -3400,7 +3553,7 @@ var init_whatsapp = __esm({
3400
3553
  if (!reactionData) return null;
3401
3554
  const remoteJid = key.remoteJid ?? "";
3402
3555
  return {
3403
- messageId: randomUUID3(),
3556
+ messageId: randomUUID4(),
3404
3557
  platform: "whatsapp",
3405
3558
  senderId: reactionData.key?.participant ?? reactionData.key?.remoteJid?.replace("@s.whatsapp.net", "") ?? "",
3406
3559
  channelId: remoteJid,
@@ -3422,7 +3575,7 @@ var init_whatsapp = __esm({
3422
3575
  if (!chatId) return null;
3423
3576
  const caller = call.from?.replace("@s.whatsapp.net", "") ?? "";
3424
3577
  return {
3425
- messageId: randomUUID3(),
3578
+ messageId: randomUUID4(),
3426
3579
  platform: "whatsapp",
3427
3580
  senderId: caller,
3428
3581
  channelId: chatId,
@@ -3768,7 +3921,7 @@ var slack_exports = {};
3768
3921
  __export(slack_exports, {
3769
3922
  SlackAdapter: () => SlackAdapter
3770
3923
  });
3771
- import { randomUUID as randomUUID4 } from "crypto";
3924
+ import { randomUUID as randomUUID5 } from "crypto";
3772
3925
  var SlackAdapter;
3773
3926
  var init_slack = __esm({
3774
3927
  "src/gateway/adapters/slack.ts"() {
@@ -3810,7 +3963,7 @@ var init_slack = __esm({
3810
3963
  if (event.subtype) return;
3811
3964
  const isGroup = event.channel_type !== "im";
3812
3965
  const normalized = {
3813
- messageId: event.client_msg_id ?? event.ts ?? randomUUID4(),
3966
+ messageId: event.client_msg_id ?? event.ts ?? randomUUID5(),
3814
3967
  platform: "slack",
3815
3968
  senderId: event.user ?? "",
3816
3969
  channelId: event.channel ?? "",
@@ -3872,7 +4025,7 @@ var init_slack = __esm({
3872
4025
  if (!event.text) return;
3873
4026
  const isGroup = event.channel_type !== "im";
3874
4027
  const normalized = {
3875
- messageId: event.ts ?? randomUUID4(),
4028
+ messageId: event.ts ?? randomUUID5(),
3876
4029
  platform: "slack",
3877
4030
  senderId: event.user ?? "",
3878
4031
  senderName: event.user_profile?.display_name ?? event.user_profile?.real_name ?? void 0,
@@ -4095,7 +4248,7 @@ var email_exports = {};
4095
4248
  __export(email_exports, {
4096
4249
  EmailAdapter: () => EmailAdapter
4097
4250
  });
4098
- import { randomUUID as randomUUID5 } from "crypto";
4251
+ import { randomUUID as randomUUID6 } from "crypto";
4099
4252
  import { createTransport } from "nodemailer";
4100
4253
  function extractEmailAddress(from) {
4101
4254
  const match = from.match(/<([^>]+)>/);
@@ -4177,7 +4330,7 @@ var init_email = __esm({
4177
4330
  const senderEmail = extractEmailAddress(from);
4178
4331
  const media = this.extractMedia(payload);
4179
4332
  const normalized = {
4180
- messageId: payload.message_id ?? randomUUID5(),
4333
+ messageId: payload.message_id ?? randomUUID6(),
4181
4334
  platform: "email",
4182
4335
  senderId: senderEmail,
4183
4336
  senderName: extractSenderName(from),
@@ -4238,7 +4391,7 @@ var webhook_exports = {};
4238
4391
  __export(webhook_exports, {
4239
4392
  WebhookAdapter: () => WebhookAdapter
4240
4393
  });
4241
- import { randomUUID as randomUUID6 } from "crypto";
4394
+ import { randomUUID as randomUUID7 } from "crypto";
4242
4395
  function resolvePath(obj, path20) {
4243
4396
  let current = obj;
4244
4397
  for (const segment of path20.split(".")) {
@@ -4294,7 +4447,7 @@ var init_webhook = __esm({
4294
4447
  if (!text || !senderId) return;
4295
4448
  const channelId = this.fieldMap.channelId ? String(resolvePath(rawPayload, this.fieldMap.channelId) ?? senderId) : senderId;
4296
4449
  const senderName = this.fieldMap.senderName ? String(resolvePath(rawPayload, this.fieldMap.senderName) ?? "") || void 0 : void 0;
4297
- const messageId = this.fieldMap.messageId ? String(resolvePath(rawPayload, this.fieldMap.messageId) ?? randomUUID6()) : randomUUID6();
4450
+ const messageId = this.fieldMap.messageId ? String(resolvePath(rawPayload, this.fieldMap.messageId) ?? randomUUID7()) : randomUUID7();
4298
4451
  const timestamp = this.fieldMap.timestamp ? String(resolvePath(rawPayload, this.fieldMap.timestamp) ?? (/* @__PURE__ */ new Date()).toISOString()) : (/* @__PURE__ */ new Date()).toISOString();
4299
4452
  const normalized = {
4300
4453
  messageId,
@@ -4797,2115 +4950,2607 @@ var init_plan_limits = __esm({
4797
4950
  }
4798
4951
  });
4799
4952
 
4800
- // src/lib/tmux-routing.ts
4801
- import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
4802
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, existsSync as existsSync10, appendFileSync } from "fs";
4953
+ // src/lib/notifications.ts
4954
+ import crypto3 from "crypto";
4803
4955
  import path11 from "path";
4804
4956
  import os6 from "os";
4805
- import { fileURLToPath as fileURLToPath2 } from "url";
4806
- import { unlinkSync as unlinkSync2 } from "fs";
4807
- function spawnLockPath(sessionName) {
4808
- return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4809
- }
4810
- function isProcessAlive(pid) {
4957
+ import {
4958
+ readFileSync as readFileSync9,
4959
+ readdirSync as readdirSync2,
4960
+ unlinkSync as unlinkSync2,
4961
+ existsSync as existsSync10,
4962
+ rmdirSync
4963
+ } from "fs";
4964
+ async function writeNotification(notification) {
4811
4965
  try {
4812
- process.kill(pid, 0);
4813
- return true;
4814
- } catch {
4815
- return false;
4816
- }
4817
- }
4818
- function acquireSpawnLock2(sessionName) {
4819
- if (!existsSync10(SPAWN_LOCK_DIR)) {
4820
- mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4821
- }
4822
- const lockFile = spawnLockPath(sessionName);
4823
- if (existsSync10(lockFile)) {
4824
- try {
4825
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
4826
- const age = Date.now() - lock.timestamp;
4827
- if (isProcessAlive(lock.pid) && age < 6e4) {
4828
- return false;
4829
- }
4830
- } catch {
4831
- }
4966
+ const client = getClient();
4967
+ const id = crypto3.randomUUID();
4968
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4969
+ await client.execute({
4970
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
4971
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
4972
+ args: [
4973
+ id,
4974
+ notification.agentId,
4975
+ notification.agentRole,
4976
+ notification.event,
4977
+ notification.project,
4978
+ notification.summary,
4979
+ notification.taskFile ?? null,
4980
+ now
4981
+ ]
4982
+ });
4983
+ } catch (err) {
4984
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
4985
+ `);
4832
4986
  }
4833
- writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4834
- return true;
4835
4987
  }
4836
- function releaseSpawnLock2(sessionName) {
4988
+ async function markAsReadByTaskFile(taskFile) {
4837
4989
  try {
4838
- unlinkSync2(spawnLockPath(sessionName));
4990
+ const client = getClient();
4991
+ await client.execute({
4992
+ sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
4993
+ args: [taskFile]
4994
+ });
4839
4995
  } catch {
4840
4996
  }
4841
4997
  }
4842
- function resolveBehaviorsExporterScript() {
4843
- try {
4844
- const thisFile = fileURLToPath2(import.meta.url);
4845
- const scriptPath = path11.join(
4846
- path11.dirname(thisFile),
4847
- "..",
4848
- "bin",
4849
- "exe-export-behaviors.js"
4850
- );
4851
- return existsSync10(scriptPath) ? scriptPath : null;
4852
- } catch {
4853
- return null;
4998
+ var init_notifications = __esm({
4999
+ "src/lib/notifications.ts"() {
5000
+ "use strict";
5001
+ init_database();
4854
5002
  }
4855
- }
4856
- function exportBehaviorsSync(agentId, projectName, sessionKey) {
4857
- const script = resolveBehaviorsExporterScript();
4858
- if (!script) return null;
5003
+ });
5004
+
5005
+ // src/lib/session-kill-telemetry.ts
5006
+ import crypto4 from "crypto";
5007
+ async function recordSessionKill(input) {
4859
5008
  try {
4860
- const output = execFileSync2(
4861
- process.execPath,
4862
- [script, agentId, projectName, sessionKey],
4863
- { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
4864
- ).trim();
4865
- return output.length > 0 ? output : null;
5009
+ const client = getClient();
5010
+ await client.execute({
5011
+ sql: `INSERT INTO session_kills
5012
+ (id, session_name, agent_id, killed_at, reason,
5013
+ ticks_idle, estimated_tokens_saved)
5014
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
5015
+ args: [
5016
+ crypto4.randomUUID(),
5017
+ input.sessionName,
5018
+ input.agentId,
5019
+ (/* @__PURE__ */ new Date()).toISOString(),
5020
+ input.reason,
5021
+ input.ticksIdle ?? null,
5022
+ input.estimatedTokensSaved ?? null
5023
+ ]
5024
+ });
4866
5025
  } catch (err) {
4867
5026
  process.stderr.write(
4868
- `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
5027
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
4869
5028
  `
4870
5029
  );
4871
- return null;
4872
5030
  }
4873
5031
  }
4874
- function getMySession() {
4875
- return getTransport().getMySession();
4876
- }
4877
- function employeeSessionName(employee, exeSession, instance) {
4878
- const suffix = instance != null && instance > 0 ? String(instance) : "";
4879
- return `${employee}${suffix}-${exeSession}`;
4880
- }
4881
- function extractRootExe(name) {
4882
- const match = name.match(/(exe\d+)$/);
4883
- return match?.[1] ?? null;
4884
- }
4885
- function getParentExe(sessionKey) {
4886
- try {
4887
- const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4888
- return data.parentExe || null;
4889
- } catch {
4890
- return null;
5032
+ var init_session_kill_telemetry = __esm({
5033
+ "src/lib/session-kill-telemetry.ts"() {
5034
+ "use strict";
5035
+ init_database();
4891
5036
  }
4892
- }
4893
- function getDispatchedBy(sessionKey) {
4894
- try {
4895
- const data = JSON.parse(readFileSync9(
4896
- path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4897
- "utf8"
4898
- ));
4899
- return data.dispatchedBy ?? data.parentExe ?? null;
4900
- } catch {
4901
- return null;
5037
+ });
5038
+
5039
+ // src/lib/tasks-crud.ts
5040
+ import crypto5 from "crypto";
5041
+ import path12 from "path";
5042
+ import { execSync as execSync4 } from "child_process";
5043
+ import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
5044
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
5045
+ async function writeCheckpoint(input) {
5046
+ const client = getClient();
5047
+ const row = await resolveTask(client, input.taskId);
5048
+ const taskId = String(row.id);
5049
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5050
+ const blockedByIds = [];
5051
+ if (row.blocked_by) {
5052
+ blockedByIds.push(String(row.blocked_by));
4902
5053
  }
4903
- }
4904
- function resolveExeSession() {
4905
- const mySession = getMySession();
4906
- if (!mySession) return null;
4907
- try {
4908
- const key = getSessionKey();
4909
- const parentExe = getParentExe(key);
4910
- if (parentExe) {
4911
- return extractRootExe(parentExe) ?? parentExe;
4912
- }
4913
- } catch {
5054
+ const checkpoint = {
5055
+ step: input.step,
5056
+ context_summary: input.contextSummary,
5057
+ files_touched: input.filesTouched ?? [],
5058
+ blocked_by_ids: blockedByIds,
5059
+ last_checkpoint_at: now
5060
+ };
5061
+ const result = await client.execute({
5062
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
5063
+ args: [JSON.stringify(checkpoint), now, taskId]
5064
+ });
5065
+ if (result.rowsAffected === 0) {
5066
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
4914
5067
  }
4915
- return extractRootExe(mySession) ?? mySession;
4916
- }
4917
- function isEmployeeAlive(sessionName) {
4918
- return getTransport().isAlive(sessionName);
5068
+ const countResult = await client.execute({
5069
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
5070
+ args: [taskId]
5071
+ });
5072
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
5073
+ return { checkpointCount };
4919
5074
  }
4920
- function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4921
- const base = employeeSessionName(employeeName, exeSession);
4922
- if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
4923
- for (let i = 2; i <= maxInstances; i++) {
4924
- const candidate = employeeSessionName(employeeName, exeSession, i);
4925
- if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
4926
- }
4927
- return null;
5075
+ function extractParentFromContext(contextBody) {
5076
+ if (!contextBody) return null;
5077
+ const match = contextBody.match(
5078
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
5079
+ );
5080
+ return match ? match[1].toLowerCase() : null;
4928
5081
  }
4929
- function readDebounceState() {
4930
- try {
4931
- if (!existsSync10(DEBOUNCE_FILE)) return {};
4932
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
4933
- } catch {
4934
- return {};
4935
- }
5082
+ function slugify(title) {
5083
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4936
5084
  }
4937
- function writeDebounceState(state) {
4938
- try {
4939
- if (!existsSync10(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4940
- writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
4941
- } catch {
5085
+ async function resolveTask(client, identifier) {
5086
+ let result = await client.execute({
5087
+ sql: "SELECT * FROM tasks WHERE id = ?",
5088
+ args: [identifier]
5089
+ });
5090
+ if (result.rows.length === 1) return result.rows[0];
5091
+ result = await client.execute({
5092
+ sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
5093
+ args: [`%${identifier}%`]
5094
+ });
5095
+ if (result.rows.length === 1) return result.rows[0];
5096
+ if (result.rows.length > 1) {
5097
+ const exact = result.rows.filter(
5098
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
5099
+ );
5100
+ if (exact.length === 1) return exact[0];
5101
+ const candidates = exact.length > 1 ? exact : result.rows;
5102
+ const active = candidates.filter(
5103
+ (r) => !["done", "cancelled"].includes(String(r.status))
5104
+ );
5105
+ if (active.length === 1) return active[0];
5106
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
5107
+ throw new Error(
5108
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
5109
+ );
4942
5110
  }
4943
- }
4944
- function isDebounced(targetSession) {
4945
- const state = readDebounceState();
4946
- const lastSent = state[targetSession] ?? 0;
4947
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4948
- }
4949
- function recordDebounce(targetSession) {
4950
- const state = readDebounceState();
4951
- state[targetSession] = Date.now();
4952
- const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4953
- for (const key of Object.keys(state)) {
4954
- if ((state[key] ?? 0) < cutoff) delete state[key];
5111
+ result = await client.execute({
5112
+ sql: "SELECT * FROM tasks WHERE title LIKE ?",
5113
+ args: [`%${identifier}%`]
5114
+ });
5115
+ if (result.rows.length === 1) return result.rows[0];
5116
+ if (result.rows.length > 1) {
5117
+ const active = result.rows.filter(
5118
+ (r) => !["done", "cancelled"].includes(String(r.status))
5119
+ );
5120
+ if (active.length === 1) return active[0];
5121
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
5122
+ throw new Error(
5123
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
5124
+ );
4955
5125
  }
4956
- writeDebounceState(state);
5126
+ throw new Error(`Task not found: ${identifier}`);
4957
5127
  }
4958
- function logIntercom(msg) {
4959
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
4960
- `;
4961
- process.stderr.write(`[intercom] ${msg}
4962
- `);
4963
- try {
4964
- appendFileSync(INTERCOM_LOG2, line);
4965
- } catch {
5128
+ async function createTaskCore(input) {
5129
+ const client = getClient();
5130
+ const id = crypto5.randomUUID();
5131
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5132
+ const slug = slugify(input.title);
5133
+ const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
5134
+ let blockedById = null;
5135
+ const initialStatus = input.blockedBy ? "blocked" : "open";
5136
+ if (input.blockedBy) {
5137
+ const blocker = await resolveTask(client, input.blockedBy);
5138
+ blockedById = String(blocker.id);
4966
5139
  }
4967
- }
4968
- function getSessionState(sessionName) {
4969
- const transport = getTransport();
4970
- if (!transport.isAlive(sessionName)) return "offline";
4971
- try {
4972
- const pane = transport.capturePane(sessionName, 5);
4973
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4974
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4975
- return "no_claude";
5140
+ let parentTaskId = null;
5141
+ let parentRef = input.parentTaskId;
5142
+ if (!parentRef) {
5143
+ const extracted = extractParentFromContext(input.context);
5144
+ if (extracted) {
5145
+ parentRef = extracted;
5146
+ process.stderr.write(
5147
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
5148
+ );
5149
+ }
5150
+ }
5151
+ if (parentRef) {
5152
+ try {
5153
+ const parent = await resolveTask(client, parentRef);
5154
+ parentTaskId = String(parent.id);
5155
+ } catch (err) {
5156
+ if (!input.parentTaskId) {
5157
+ throw new Error(
5158
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
5159
+ );
4976
5160
  }
5161
+ throw err;
4977
5162
  }
4978
- if (/Running…/.test(pane)) return "tool";
4979
- if (BUSY_PATTERN.test(pane)) return "thinking";
4980
- return "idle";
4981
- } catch {
4982
- return "offline";
4983
5163
  }
4984
- }
4985
- function isExeSession(sessionName) {
4986
- return /^exe\d*$/.test(sessionName);
4987
- }
4988
- function sendIntercom(targetSession) {
4989
- const transport = getTransport();
4990
- if (isExeSession(targetSession)) {
4991
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
4992
- return "skipped_exe";
5164
+ let warning;
5165
+ const dupCheck = await client.execute({
5166
+ sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
5167
+ args: [input.title, input.assignedTo]
5168
+ });
5169
+ if (dupCheck.rows.length > 0) {
5170
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
4993
5171
  }
4994
- if (isDebounced(targetSession)) {
4995
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4996
- return "debounced";
5172
+ if (input.baseDir) {
5173
+ try {
5174
+ await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
5175
+ await mkdir4(path12.join(input.baseDir, "exe", "research"), { recursive: true });
5176
+ await ensureArchitectureDoc(input.baseDir, input.projectName);
5177
+ await ensureGitignoreExe(input.baseDir);
5178
+ } catch {
5179
+ }
4997
5180
  }
5181
+ const complexity = input.complexity ?? "standard";
5182
+ let sessionScope = null;
4998
5183
  try {
4999
- const sessions = transport.listSessions();
5000
- if (!sessions.includes(targetSession)) {
5001
- logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
5002
- return "failed";
5003
- }
5004
- const sessionState = getSessionState(targetSession);
5005
- if (sessionState === "no_claude") {
5006
- queueIntercom(targetSession, "claude not running in session");
5007
- recordDebounce(targetSession);
5008
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
5009
- return "queued";
5010
- }
5011
- if (sessionState === "thinking" || sessionState === "tool") {
5012
- queueIntercom(targetSession, "session busy at send time");
5013
- recordDebounce(targetSession);
5014
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
5015
- return "queued";
5016
- }
5017
- if (transport.isPaneInCopyMode(targetSession)) {
5018
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
5019
- transport.sendKeys(targetSession, "q");
5020
- }
5021
- transport.sendKeys(targetSession, "/exe-intercom");
5022
- recordDebounce(targetSession);
5023
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
5024
- return "delivered";
5184
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
5185
+ sessionScope = resolveExeSession2();
5025
5186
  } catch {
5026
- logIntercom(`FAIL \u2192 ${targetSession}`);
5027
- return "failed";
5028
5187
  }
5188
+ await client.execute({
5189
+ 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)
5190
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5191
+ args: [
5192
+ id,
5193
+ input.title,
5194
+ input.assignedTo,
5195
+ input.assignedBy,
5196
+ input.projectName,
5197
+ input.priority,
5198
+ initialStatus,
5199
+ taskFile,
5200
+ blockedById,
5201
+ parentTaskId,
5202
+ input.reviewer ?? null,
5203
+ input.context,
5204
+ complexity,
5205
+ input.budgetTokens ?? null,
5206
+ input.budgetFallbackModel ?? null,
5207
+ 0,
5208
+ null,
5209
+ sessionScope,
5210
+ now,
5211
+ now
5212
+ ]
5213
+ });
5214
+ return {
5215
+ id,
5216
+ title: input.title,
5217
+ assignedTo: input.assignedTo,
5218
+ assignedBy: input.assignedBy,
5219
+ projectName: input.projectName,
5220
+ priority: input.priority,
5221
+ status: initialStatus,
5222
+ taskFile,
5223
+ createdAt: now,
5224
+ updatedAt: now,
5225
+ warning,
5226
+ budgetTokens: input.budgetTokens ?? null,
5227
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
5228
+ tokensUsed: 0,
5229
+ tokensWarnedAt: null
5230
+ };
5029
5231
  }
5030
- function notifyParentExe(sessionKey) {
5031
- const target = getDispatchedBy(sessionKey);
5032
- if (!target) {
5033
- process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
5034
- `);
5035
- return false;
5232
+ async function listTasks(input) {
5233
+ const client = getClient();
5234
+ const conditions = [];
5235
+ const args = [];
5236
+ if (input.assignedTo) {
5237
+ conditions.push("assigned_to = ?");
5238
+ args.push(input.assignedTo);
5036
5239
  }
5037
- process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
5038
- `);
5039
- const result = sendIntercom(target);
5040
- if (result === "failed") {
5041
- const rootExe = resolveExeSession();
5042
- if (rootExe && rootExe !== target) {
5043
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
5044
- `);
5045
- const fallback = sendIntercom(rootExe);
5046
- return fallback !== "failed";
5047
- }
5048
- return false;
5240
+ if (input.status) {
5241
+ conditions.push("status = ?");
5242
+ args.push(input.status);
5243
+ } else {
5244
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
5049
5245
  }
5050
- return true;
5051
- }
5052
- function ensureEmployee(employeeName, exeSession, projectDir, opts) {
5053
- if (employeeName === "exe") {
5054
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
5246
+ if (input.projectName) {
5247
+ conditions.push("project_name = ?");
5248
+ args.push(input.projectName);
5249
+ }
5250
+ if (input.priority) {
5251
+ conditions.push("priority = ?");
5252
+ args.push(input.priority);
5055
5253
  }
5056
5254
  try {
5057
- assertEmployeeLimitSync();
5058
- } catch (err) {
5059
- if (err instanceof PlanLimitError) {
5060
- return { status: "failed", sessionName: "", error: err.message };
5255
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
5256
+ const session = resolveExeSession2();
5257
+ if (session) {
5258
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
5259
+ args.push(session);
5061
5260
  }
5261
+ } catch {
5062
5262
  }
5063
- if (/-exe\d*$/.test(employeeName)) {
5064
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
5065
- return {
5066
- status: "failed",
5067
- sessionName: "",
5068
- error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
5069
- };
5263
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5264
+ const result = await client.execute({
5265
+ 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`,
5266
+ args
5267
+ });
5268
+ return result.rows.map((r) => ({
5269
+ id: String(r.id),
5270
+ title: String(r.title),
5271
+ assignedTo: String(r.assigned_to),
5272
+ assignedBy: String(r.assigned_by),
5273
+ projectName: String(r.project_name),
5274
+ priority: String(r.priority),
5275
+ status: String(r.status),
5276
+ taskFile: String(r.task_file),
5277
+ createdAt: String(r.created_at),
5278
+ updatedAt: String(r.updated_at),
5279
+ checkpointCount: Number(r.checkpoint_count ?? 0),
5280
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
5281
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
5282
+ tokensUsed: Number(r.tokens_used ?? 0),
5283
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
5284
+ }));
5285
+ }
5286
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
5287
+ if (!taskContext) return null;
5288
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
5289
+ try {
5290
+ const since = new Date(taskCreatedAt).toISOString();
5291
+ const branch = execSync4(
5292
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
5293
+ { encoding: "utf8", timeout: 3e3 }
5294
+ ).trim();
5295
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
5296
+ const commitCount = execSync4(
5297
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
5298
+ { encoding: "utf8", timeout: 5e3 }
5299
+ ).trim();
5300
+ const count = parseInt(commitCount, 10);
5301
+ if (count === 0) {
5302
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
5303
+ }
5304
+ return null;
5305
+ } catch {
5306
+ return null;
5070
5307
  }
5071
- let effectiveInstance = opts?.instance;
5072
- if (effectiveInstance === void 0 && opts?.autoInstance) {
5073
- const free = findFreeInstance(
5074
- employeeName,
5075
- exeSession,
5076
- opts.maxAutoInstances ?? 10
5308
+ }
5309
+ async function updateTaskStatus(input) {
5310
+ const client = getClient();
5311
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5312
+ const row = await resolveTask(client, input.taskId);
5313
+ const taskId = String(row.id);
5314
+ const taskFile = String(row.task_file);
5315
+ if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
5316
+ process.stderr.write(
5317
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
5318
+ `
5077
5319
  );
5078
- if (free === null) {
5079
- return {
5080
- status: "failed",
5081
- sessionName: employeeSessionName(employeeName, exeSession),
5082
- error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
5083
- };
5320
+ }
5321
+ if (input.status === "done") {
5322
+ const existingRow = await client.execute({
5323
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
5324
+ args: [taskId]
5325
+ });
5326
+ if (existingRow.rows.length > 0) {
5327
+ const ctx = existingRow.rows[0];
5328
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
5329
+ if (warning) {
5330
+ input.result = input.result ? `\u26A0\uFE0F ${warning}
5331
+
5332
+ ${input.result}` : `\u26A0\uFE0F ${warning}`;
5333
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
5334
+ `);
5335
+ }
5084
5336
  }
5085
- effectiveInstance = free === 0 ? void 0 : free;
5086
5337
  }
5087
- const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
5088
- if (isEmployeeAlive(sessionName)) {
5089
- const result2 = sendIntercom(sessionName);
5090
- if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
5091
- return { status: "intercom_sent", sessionName };
5338
+ if (input.status === "in_progress") {
5339
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
5340
+ const claim = await client.execute({
5341
+ sql: `UPDATE tasks
5342
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
5343
+ WHERE id = ? AND status = 'open'`,
5344
+ args: [tmuxSession, now, taskId]
5345
+ });
5346
+ if (claim.rowsAffected === 0) {
5347
+ const current = await client.execute({
5348
+ sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
5349
+ args: [taskId]
5350
+ });
5351
+ const cur = current.rows[0];
5352
+ const status = cur?.status ?? "unknown";
5353
+ const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
5354
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
5092
5355
  }
5093
- if (result2 === "delivered") {
5094
- return { status: "intercom_unprocessed", sessionName };
5356
+ try {
5357
+ await writeCheckpoint({
5358
+ taskId,
5359
+ step: "claimed",
5360
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
5361
+ });
5362
+ } catch {
5095
5363
  }
5096
- return { status: "failed", sessionName, error: "intercom delivery failed" };
5097
- }
5098
- const spawnOpts = { ...opts, instance: effectiveInstance };
5099
- const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
5100
- if (result.error) {
5101
- return { status: "failed", sessionName, error: result.error };
5364
+ return { row, taskFile, now, taskId };
5102
5365
  }
5103
- return { status: "spawned", sessionName };
5104
- }
5105
- function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5106
- const transport = getTransport();
5107
- const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
5108
- const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
5109
- const logDir = path11.join(os6.homedir(), ".exe-os", "session-logs");
5110
- const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5111
- if (!existsSync10(logDir)) {
5112
- mkdirSync6(logDir, { recursive: true });
5366
+ if (input.result) {
5367
+ await client.execute({
5368
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
5369
+ args: [input.status, input.result, now, taskId]
5370
+ });
5371
+ } else {
5372
+ await client.execute({
5373
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
5374
+ args: [input.status, now, taskId]
5375
+ });
5113
5376
  }
5114
- transport.kill(sessionName);
5115
- let cleanupSuffix = "";
5116
5377
  try {
5117
- const thisFile = fileURLToPath2(import.meta.url);
5118
- const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5119
- if (existsSync10(cleanupScript)) {
5120
- cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5121
- }
5378
+ await writeCheckpoint({
5379
+ taskId,
5380
+ step: `status_transition:${input.status}`,
5381
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
5382
+ });
5122
5383
  } catch {
5123
5384
  }
5385
+ return { row, taskFile, now, taskId };
5386
+ }
5387
+ async function deleteTaskCore(taskId, _baseDir) {
5388
+ const client = getClient();
5389
+ const row = await resolveTask(client, taskId);
5390
+ const id = String(row.id);
5391
+ const taskFile = String(row.task_file);
5392
+ const assignedTo = String(row.assigned_to);
5393
+ const assignedBy = String(row.assigned_by);
5394
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
5395
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
5396
+ return { taskFile, assignedTo, assignedBy, taskSlug };
5397
+ }
5398
+ async function ensureArchitectureDoc(baseDir, projectName) {
5399
+ const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
5124
5400
  try {
5125
- const claudeJsonPath = path11.join(os6.homedir(), ".claude.json");
5126
- let claudeJson = {};
5127
- try {
5128
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
5129
- } catch {
5130
- }
5131
- if (!claudeJson.projects) claudeJson.projects = {};
5132
- const projects = claudeJson.projects;
5133
- const trustDir = opts?.cwd ?? projectDir;
5134
- if (!projects[trustDir]) projects[trustDir] = {};
5135
- projects[trustDir].hasTrustDialogAccepted = true;
5136
- writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5401
+ if (existsSync11(archPath)) return;
5402
+ const template = [
5403
+ `# ${projectName} \u2014 System Architecture`,
5404
+ "",
5405
+ "> Employees: read this before every task. Update it when you change system structure.",
5406
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
5407
+ "",
5408
+ "## Overview",
5409
+ "",
5410
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
5411
+ "",
5412
+ "## Key Components",
5413
+ "",
5414
+ "<!-- List the major modules, services, or subsystems. -->",
5415
+ "",
5416
+ "## Data Flow",
5417
+ "",
5418
+ "<!-- How does data move through the system? What writes where? -->",
5419
+ "",
5420
+ "## Invariants",
5421
+ "",
5422
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
5423
+ "",
5424
+ "## Dependencies",
5425
+ "",
5426
+ "<!-- What depends on what? If I change X, what else is affected? -->",
5427
+ ""
5428
+ ].join("\n");
5429
+ await writeFile4(archPath, template, "utf-8");
5137
5430
  } catch {
5138
5431
  }
5432
+ }
5433
+ async function ensureGitignoreExe(baseDir) {
5434
+ const gitignorePath = path12.join(baseDir, ".gitignore");
5139
5435
  try {
5140
- const settingsDir = path11.join(os6.homedir(), ".claude", "projects");
5141
- const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
5142
- const projSettingsDir = path11.join(settingsDir, normalizedKey);
5143
- const settingsPath = path11.join(projSettingsDir, "settings.json");
5144
- let settings = {};
5145
- try {
5146
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
5147
- } catch {
5148
- }
5149
- const perms = settings.permissions ?? {};
5150
- const allow = perms.allow ?? [];
5151
- const toolNames = [
5152
- "recall_my_memory",
5153
- "store_memory",
5154
- "create_task",
5155
- "update_task",
5156
- "list_tasks",
5157
- "get_task",
5158
- "ask_team_memory",
5159
- "store_behavior",
5160
- "get_identity",
5161
- "send_message"
5162
- ];
5163
- const requiredTools = expandDualPrefixTools(toolNames);
5164
- let changed = false;
5165
- for (const tool of requiredTools) {
5166
- if (!allow.includes(tool)) {
5167
- allow.push(tool);
5168
- changed = true;
5169
- }
5170
- }
5171
- if (changed) {
5172
- perms.allow = allow;
5173
- settings.permissions = perms;
5174
- mkdirSync6(projSettingsDir, { recursive: true });
5175
- writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5436
+ if (existsSync11(gitignorePath)) {
5437
+ const content = readFileSync10(gitignorePath, "utf-8");
5438
+ if (/^\/?exe\/?$/m.test(content)) return;
5439
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
5440
+ } else {
5441
+ await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
5176
5442
  }
5177
5443
  } catch {
5178
5444
  }
5179
- const spawnCwd = opts?.cwd ?? projectDir;
5180
- const useExeAgent = !!(opts?.model && opts?.provider);
5181
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
5182
- const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
5183
- let identityFlag = "";
5184
- let behaviorsFlag = "";
5185
- let legacyFallbackWarned = false;
5186
- if (!useExeAgent && !useBinSymlink) {
5187
- const identityPath = path11.join(
5188
- os6.homedir(),
5189
- ".exe-os",
5190
- "identity",
5191
- `${employeeName}.md`
5192
- );
5193
- _resetCcAgentSupportCache();
5194
- const hasAgentFlag = claudeSupportsAgentFlag();
5195
- if (hasAgentFlag) {
5196
- identityFlag = ` --agent ${employeeName}`;
5197
- } else if (existsSync10(identityPath)) {
5198
- identityFlag = ` --append-system-prompt-file ${identityPath}`;
5199
- legacyFallbackWarned = true;
5200
- }
5201
- const behaviorsFile = exportBehaviorsSync(
5202
- employeeName,
5203
- path11.basename(spawnCwd),
5204
- sessionName
5205
- );
5206
- if (behaviorsFile) {
5207
- behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
5208
- }
5445
+ }
5446
+ var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
5447
+ var init_tasks_crud = __esm({
5448
+ "src/lib/tasks-crud.ts"() {
5449
+ "use strict";
5450
+ init_database();
5451
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
5452
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
5209
5453
  }
5210
- if (legacyFallbackWarned) {
5454
+ });
5455
+
5456
+ // src/lib/tasks-review.ts
5457
+ import path13 from "path";
5458
+ import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
5459
+ async function countPendingReviews() {
5460
+ const client = getClient();
5461
+ const result = await client.execute({
5462
+ sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
5463
+ args: []
5464
+ });
5465
+ return Number(result.rows[0]?.cnt) || 0;
5466
+ }
5467
+ async function countNewPendingReviewsSince(sinceIso) {
5468
+ const client = getClient();
5469
+ const result = await client.execute({
5470
+ sql: `SELECT COUNT(*) as cnt FROM tasks
5471
+ WHERE status = 'needs_review' AND updated_at > ?`,
5472
+ args: [sinceIso]
5473
+ });
5474
+ return Number(result.rows[0]?.cnt) || 0;
5475
+ }
5476
+ async function listPendingReviews(limit) {
5477
+ const client = getClient();
5478
+ const result = await client.execute({
5479
+ sql: `SELECT title, assigned_to, project_name FROM tasks
5480
+ WHERE status = 'needs_review'
5481
+ ORDER BY priority ASC, created_at DESC LIMIT ?`,
5482
+ args: [limit]
5483
+ });
5484
+ return result.rows;
5485
+ }
5486
+ async function cleanupOrphanedReviews() {
5487
+ const client = getClient();
5488
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5489
+ const r1 = await client.execute({
5490
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
5491
+ WHERE status = 'needs_review'
5492
+ AND assigned_by = 'system'
5493
+ AND title LIKE 'Review:%'
5494
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
5495
+ args: [now]
5496
+ });
5497
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
5498
+ const r2 = await client.execute({
5499
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
5500
+ WHERE status = 'needs_review'
5501
+ AND result IS NOT NULL
5502
+ AND updated_at < ?`,
5503
+ args: [now, staleThreshold]
5504
+ });
5505
+ const total = r1.rowsAffected + r2.rowsAffected;
5506
+ if (total > 0) {
5211
5507
  process.stderr.write(
5212
- `[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.
5508
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
5213
5509
  `
5214
5510
  );
5215
5511
  }
5216
- let sessionContextFlag = "";
5217
- try {
5218
- const ctxDir = path11.join(os6.homedir(), ".exe-os", "session-cache");
5219
- mkdirSync6(ctxDir, { recursive: true });
5220
- const ctxFile = path11.join(ctxDir, `session-context-${sessionName}.md`);
5221
- const ctxContent = [
5222
- `## Session Context`,
5223
- `You are running in tmux session: ${sessionName}.`,
5224
- `Your parent exe session is ${exeSession}.`,
5225
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
5226
- ].join("\n");
5227
- writeFileSync4(ctxFile, ctxContent);
5228
- sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5229
- } catch {
5512
+ return total;
5513
+ }
5514
+ function getReviewChecklist(role, agent, taskSlug) {
5515
+ const roleLower = role.toLowerCase();
5516
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
5517
+ return {
5518
+ lens: "Code Quality (Engineer)",
5519
+ checklist: [
5520
+ "1. Do all tests pass? Any new tests needed?",
5521
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
5522
+ "3. Does it follow existing patterns and conventions in the codebase?",
5523
+ "4. Any regressions in the test suite?"
5524
+ ]
5525
+ };
5230
5526
  }
5231
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
5232
- if (ccProvider !== DEFAULT_PROVIDER) {
5233
- const cfg = PROVIDER_TABLE[ccProvider];
5234
- if (cfg?.apiKeyEnv) {
5235
- const keyVal = process.env[cfg.apiKeyEnv];
5236
- if (keyVal) {
5237
- envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
5527
+ if (roleLower === "cto" || roleLower.includes("architect")) {
5528
+ return {
5529
+ lens: "Architecture (CTO)",
5530
+ checklist: [
5531
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
5532
+ "2. Is it backward compatible? Any breaking changes?",
5533
+ "3. Does it introduce technical debt? Is that debt justified?",
5534
+ "4. Security implications? Any new attack surface?",
5535
+ "5. Does it scale? Performance considerations?",
5536
+ "6. Coordination: does this affect other employees' work or other projects?"
5537
+ ]
5538
+ };
5539
+ }
5540
+ if (roleLower === "coo" || roleLower.includes("operations")) {
5541
+ return {
5542
+ lens: "Strategic (COO)",
5543
+ checklist: [
5544
+ "1. Does this serve the project mission?",
5545
+ "2. Is this the right work at the right time?",
5546
+ "3. Does the architectural assessment make sense for the business?",
5547
+ "4. Any cross-project implications?"
5548
+ ]
5549
+ };
5550
+ }
5551
+ return {
5552
+ lens: "General",
5553
+ checklist: [
5554
+ "1. Read the original task's acceptance criteria",
5555
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
5556
+ "3. Verify code changes match requirements",
5557
+ "4. Check if tests were added/updated",
5558
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
5559
+ ]
5560
+ };
5561
+ }
5562
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
5563
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
5564
+ try {
5565
+ const client = getClient();
5566
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5567
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
5568
+ if (parentId) {
5569
+ const result = await client.execute({
5570
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
5571
+ args: [now, parentId]
5572
+ });
5573
+ if (result.rowsAffected > 0) {
5574
+ process.stderr.write(
5575
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
5576
+ `
5577
+ );
5578
+ }
5579
+ } else {
5580
+ const fileName = taskFile.split("/").pop() ?? "";
5581
+ const reviewPrefix = fileName.replace(".md", "");
5582
+ const parts = reviewPrefix.split("-");
5583
+ if (parts.length >= 3 && parts[0] === "review") {
5584
+ const agent = parts[1];
5585
+ const slug = parts.slice(2).join("-");
5586
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
5587
+ const result = await client.execute({
5588
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
5589
+ args: [now, originalTaskFile]
5590
+ });
5591
+ if (result.rowsAffected > 0) {
5592
+ process.stderr.write(
5593
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
5594
+ `
5595
+ );
5596
+ }
5238
5597
  }
5239
5598
  }
5240
- }
5241
- let spawnCommand;
5242
- if (useExeAgent) {
5243
- spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
5244
- } else if (useBinSymlink) {
5245
- const binName = `${employeeName}-${ccProvider}`;
5599
+ } catch (err) {
5246
5600
  process.stderr.write(
5247
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
5601
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
5248
5602
  `
5249
5603
  );
5250
- spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
5251
- } else {
5252
- spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
5253
- }
5254
- const spawnResult = transport.spawn(sessionName, {
5255
- cwd: spawnCwd,
5256
- command: spawnCommand
5257
- });
5258
- if (spawnResult.error) {
5259
- releaseSpawnLock2(sessionName);
5260
- return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
5261
5604
  }
5262
- transport.pipeLog(sessionName, logFile);
5263
5605
  try {
5264
- const mySession = getMySession();
5265
- const dispatchInfo = path11.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5266
- writeFileSync4(dispatchInfo, JSON.stringify({
5267
- dispatchedBy: mySession,
5268
- rootExe: exeSession,
5269
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
5270
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
5271
- }));
5272
- } catch {
5273
- }
5274
- let booted = false;
5275
- for (let i = 0; i < 30; i++) {
5276
- try {
5277
- execSync4("sleep 0.5");
5278
- } catch {
5279
- }
5280
- try {
5281
- const pane = transport.capturePane(sessionName);
5282
- if (useExeAgent) {
5283
- if (pane.includes("[exe-agent]") || pane.includes("online")) {
5284
- booted = true;
5285
- break;
5286
- }
5287
- } else {
5288
- if (pane.includes("Claude Code") || pane.includes("\u276F")) {
5289
- booted = true;
5290
- break;
5606
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
5607
+ if (existsSync12(cacheDir)) {
5608
+ for (const f of readdirSync3(cacheDir)) {
5609
+ if (f.startsWith("review-notified-")) {
5610
+ unlinkSync3(path13.join(cacheDir, f));
5291
5611
  }
5292
5612
  }
5293
- } catch {
5294
- }
5295
- }
5296
- if (!booted) {
5297
- releaseSpawnLock2(sessionName);
5298
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
5299
- }
5300
- if (!useExeAgent) {
5301
- try {
5302
- transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
5303
- } catch {
5304
5613
  }
5614
+ } catch {
5305
5615
  }
5306
- registerSession({
5307
- windowName: sessionName,
5308
- agentId: employeeName,
5309
- projectDir: spawnCwd,
5310
- parentExe: exeSession,
5311
- pid: 0,
5312
- registeredAt: (/* @__PURE__ */ new Date()).toISOString()
5313
- });
5314
- releaseSpawnLock2(sessionName);
5315
- return { sessionName };
5316
5616
  }
5317
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
5318
- var init_tmux_routing = __esm({
5319
- "src/lib/tmux-routing.ts"() {
5617
+ var init_tasks_review = __esm({
5618
+ "src/lib/tasks-review.ts"() {
5320
5619
  "use strict";
5321
- init_session_registry();
5620
+ init_database();
5621
+ init_config();
5622
+ init_employees();
5623
+ init_notifications();
5624
+ init_tasks_crud();
5625
+ init_tmux_routing();
5322
5626
  init_session_key();
5323
- init_transport();
5324
- init_cc_agent_support();
5325
- init_mcp_prefix();
5326
- init_provider_table();
5327
- init_intercom_queue();
5328
- init_plan_limits();
5329
- SPAWN_LOCK_DIR = path11.join(os6.homedir(), ".exe-os", "spawn-locks");
5330
- SESSION_CACHE = path11.join(os6.homedir(), ".exe-os", "session-cache");
5331
- BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5332
- INTERCOM_DEBOUNCE_MS = 3e4;
5333
- INTERCOM_LOG2 = path11.join(os6.homedir(), ".exe-os", "intercom.log");
5334
- DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
5335
- DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5336
- BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
5627
+ init_state_bus();
5337
5628
  }
5338
5629
  });
5339
5630
 
5340
- // src/lib/messaging.ts
5341
- var messaging_exports = {};
5342
- __export(messaging_exports, {
5343
- deliverLocalMessage: () => deliverLocalMessage,
5344
- getFailedMessages: () => getFailedMessages,
5345
- getMessageStatus: () => getMessageStatus,
5346
- getPendingMessages: () => getPendingMessages,
5347
- getReadMessages: () => getReadMessages,
5348
- getUnacknowledgedMessages: () => getUnacknowledgedMessages,
5349
- markAcknowledged: () => markAcknowledged,
5350
- markFailed: () => markFailed,
5351
- markProcessed: () => markProcessed,
5352
- markRead: () => markRead,
5353
- retryPendingMessages: () => retryPendingMessages,
5354
- sendMessage: () => sendMessage,
5355
- setWsClientSend: () => setWsClientSend
5356
- });
5357
- import crypto3 from "crypto";
5358
- function generateUlid() {
5359
- const timestamp = Date.now().toString(36).padStart(10, "0");
5360
- const random = crypto3.randomBytes(10).toString("hex").slice(0, 16);
5361
- return (timestamp + random).toUpperCase();
5362
- }
5363
- function rowToMessage(row) {
5364
- return {
5365
- id: row.id,
5366
- fromAgent: row.from_agent,
5367
- fromDevice: row.from_device,
5368
- targetAgent: row.target_agent,
5369
- targetProject: row.target_project ?? null,
5370
- targetDevice: row.target_device,
5371
- content: row.content,
5372
- priority: row.priority ?? "normal",
5373
- status: row.status ?? "pending",
5374
- serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
5375
- retryCount: Number(row.retry_count ?? 0),
5376
- createdAt: row.created_at,
5377
- deliveredAt: row.delivered_at ?? null,
5378
- processedAt: row.processed_at ?? null,
5379
- failedAt: row.failed_at ?? null,
5380
- failureReason: row.failure_reason ?? null
5381
- };
5382
- }
5383
- async function sendMessage(input) {
5631
+ // src/lib/tasks-chain.ts
5632
+ import path14 from "path";
5633
+ import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
5634
+ async function cascadeUnblock(taskId, baseDir, now) {
5384
5635
  const client = getClient();
5385
- const id = generateUlid();
5386
- const now = (/* @__PURE__ */ new Date()).toISOString();
5387
- const targetDevice = input.targetDevice ?? "local";
5388
- await client.execute({
5389
- sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
5390
- VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
5391
- args: [
5392
- id,
5393
- input.fromAgent,
5394
- input.targetAgent,
5395
- input.targetProject ?? null,
5396
- targetDevice,
5397
- input.content,
5398
- input.priority ?? "normal",
5399
- now
5400
- ]
5636
+ const unblocked = await client.execute({
5637
+ sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
5638
+ WHERE blocked_by = ? AND status = 'blocked'`,
5639
+ args: [now, taskId]
5401
5640
  });
5402
- try {
5403
- if (targetDevice !== "local") {
5404
- await deliverCrossMachineMessage(id, targetDevice);
5405
- } else {
5406
- await deliverLocalMessage(id);
5641
+ if (baseDir && unblocked.rowsAffected > 0) {
5642
+ const unblockedRows = await client.execute({
5643
+ sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
5644
+ args: [now]
5645
+ });
5646
+ for (const ur of unblockedRows.rows) {
5647
+ try {
5648
+ const ubFile = path14.join(baseDir, String(ur.task_file));
5649
+ let ubContent = await readFile4(ubFile, "utf-8");
5650
+ ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
5651
+ ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
5652
+ await writeFile5(ubFile, ubContent, "utf-8");
5653
+ } catch {
5654
+ }
5407
5655
  }
5408
- } catch {
5409
5656
  }
5410
- const result = await client.execute({
5411
- sql: "SELECT * FROM messages WHERE id = ?",
5412
- args: [id]
5413
- });
5414
- return rowToMessage(result.rows[0]);
5415
- }
5416
- function setWsClientSend(fn) {
5417
- _wsClientSend = fn;
5418
5657
  }
5419
- async function deliverCrossMachineMessage(messageId, targetDevice) {
5658
+ async function findNextTask(assignedTo) {
5420
5659
  const client = getClient();
5421
- const result = await client.execute({
5422
- sql: "SELECT * FROM messages WHERE id = ?",
5423
- args: [messageId]
5424
- });
5425
- if (result.rows.length === 0) return false;
5426
- const msg = rowToMessage(result.rows[0]);
5427
- if (msg.status !== "pending") return false;
5428
- if (!_wsClientSend) {
5429
- return false;
5430
- }
5431
- const payload = JSON.stringify({
5432
- id: msg.id,
5433
- fromAgent: msg.fromAgent,
5434
- targetAgent: msg.targetAgent,
5435
- targetProject: msg.targetProject,
5436
- content: msg.content,
5437
- priority: msg.priority,
5438
- createdAt: msg.createdAt
5660
+ const nextResult = await client.execute({
5661
+ sql: `SELECT title, task_file, priority FROM tasks
5662
+ WHERE assigned_to = ? AND status = 'open'
5663
+ ORDER BY priority ASC, created_at ASC
5664
+ LIMIT 1`,
5665
+ args: [assignedTo]
5439
5666
  });
5440
- const sent = _wsClientSend(targetDevice, payload);
5441
- if (sent) {
5442
- await client.execute({
5443
- sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
5444
- args: [messageId]
5445
- });
5446
- return true;
5667
+ if (nextResult.rows.length === 1) {
5668
+ const nr = nextResult.rows[0];
5669
+ return {
5670
+ title: String(nr.title),
5671
+ priority: String(nr.priority),
5672
+ taskFile: String(nr.task_file)
5673
+ };
5447
5674
  }
5448
- return false;
5675
+ return void 0;
5449
5676
  }
5450
- async function deliverLocalMessage(messageId) {
5677
+ async function checkSubtaskCompletion(parentTaskId, projectName) {
5451
5678
  const client = getClient();
5452
- const result = await client.execute({
5453
- sql: "SELECT * FROM messages WHERE id = ?",
5454
- args: [messageId]
5679
+ const remaining = await client.execute({
5680
+ sql: `SELECT COUNT(*) as cnt FROM tasks
5681
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
5682
+ args: [parentTaskId]
5455
5683
  });
5456
- if (result.rows.length === 0) return false;
5457
- const msg = rowToMessage(result.rows[0]);
5458
- if (msg.status !== "pending") return false;
5459
- const targetAgent = msg.targetAgent;
5460
- const now = (/* @__PURE__ */ new Date()).toISOString();
5461
- try {
5462
- const exeSession = resolveExeSession();
5463
- if (!exeSession) {
5464
- throw new Error("No exe session found");
5465
- }
5466
- const ensureResult = ensureEmployee(targetAgent, exeSession, process.cwd());
5467
- if (ensureResult.status === "failed") {
5468
- throw new Error(ensureResult.error ?? "ensureEmployee failed");
5469
- }
5470
- await client.execute({
5471
- sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
5472
- args: [now, messageId]
5684
+ const cnt = Number(remaining.rows[0]?.cnt ?? 1);
5685
+ if (cnt === 0) {
5686
+ const parentRow = await client.execute({
5687
+ sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
5688
+ args: [parentTaskId]
5473
5689
  });
5474
- return true;
5475
- } catch {
5476
- const newRetryCount = msg.retryCount + 1;
5477
- if (newRetryCount >= MAX_RETRIES2) {
5478
- await markFailed(messageId, "session unavailable after 10 retries");
5479
- } else {
5480
- await client.execute({
5481
- sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
5482
- args: [newRetryCount, messageId]
5690
+ if (parentRow.rows.length === 1) {
5691
+ const pr = parentRow.rows[0];
5692
+ const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
5693
+ await writeNotification({
5694
+ agentId: String(pr.assigned_to),
5695
+ agentRole: "system",
5696
+ event: "subtasks_complete",
5697
+ project: parentProject,
5698
+ summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
5699
+ taskFile: String(pr.task_file)
5483
5700
  });
5484
5701
  }
5485
- return false;
5486
5702
  }
5487
5703
  }
5488
- async function getPendingMessages(targetAgent) {
5489
- const client = getClient();
5490
- const result = await client.execute({
5491
- sql: `SELECT * FROM messages
5492
- WHERE target_agent = ? AND status IN ('pending', 'delivered')
5493
- ORDER BY id`,
5494
- args: [targetAgent]
5495
- });
5496
- return result.rows.map((row) => rowToMessage(row));
5704
+ var init_tasks_chain = __esm({
5705
+ "src/lib/tasks-chain.ts"() {
5706
+ "use strict";
5707
+ init_database();
5708
+ init_notifications();
5709
+ }
5710
+ });
5711
+
5712
+ // src/lib/project-name.ts
5713
+ import { execSync as execSync5 } from "child_process";
5714
+ import path15 from "path";
5715
+ function getProjectName(cwd) {
5716
+ const dir = cwd ?? process.cwd();
5717
+ if (_cached2 && _cachedCwd === dir) return _cached2;
5718
+ try {
5719
+ let repoRoot;
5720
+ try {
5721
+ const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
5722
+ cwd: dir,
5723
+ encoding: "utf8",
5724
+ timeout: 2e3,
5725
+ stdio: ["pipe", "pipe", "pipe"]
5726
+ }).trim();
5727
+ repoRoot = path15.dirname(gitCommonDir);
5728
+ } catch {
5729
+ repoRoot = execSync5("git rev-parse --show-toplevel", {
5730
+ cwd: dir,
5731
+ encoding: "utf8",
5732
+ timeout: 2e3,
5733
+ stdio: ["pipe", "pipe", "pipe"]
5734
+ }).trim();
5735
+ }
5736
+ _cached2 = path15.basename(repoRoot);
5737
+ _cachedCwd = dir;
5738
+ return _cached2;
5739
+ } catch {
5740
+ _cached2 = path15.basename(dir);
5741
+ _cachedCwd = dir;
5742
+ return _cached2;
5743
+ }
5497
5744
  }
5498
- async function markRead(messageId) {
5499
- const client = getClient();
5500
- await client.execute({
5501
- sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
5502
- args: [messageId]
5503
- });
5745
+ var _cached2, _cachedCwd;
5746
+ var init_project_name = __esm({
5747
+ "src/lib/project-name.ts"() {
5748
+ "use strict";
5749
+ _cached2 = null;
5750
+ _cachedCwd = null;
5751
+ }
5752
+ });
5753
+
5754
+ // src/lib/session-scope.ts
5755
+ var session_scope_exports = {};
5756
+ __export(session_scope_exports, {
5757
+ assertSessionScope: () => assertSessionScope,
5758
+ findSessionForProject: () => findSessionForProject,
5759
+ getSessionProject: () => getSessionProject
5760
+ });
5761
+ function getSessionProject(sessionName) {
5762
+ const sessions = listSessions();
5763
+ const entry = sessions.find((s) => s.windowName === sessionName);
5764
+ if (!entry) return null;
5765
+ const parts = entry.projectDir.split("/").filter(Boolean);
5766
+ return parts[parts.length - 1] ?? null;
5504
5767
  }
5505
- async function markAcknowledged(messageId) {
5506
- const client = getClient();
5507
- await client.execute({
5508
- sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
5509
- args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
5510
- });
5768
+ function findSessionForProject(projectName) {
5769
+ const sessions = listSessions();
5770
+ for (const s of sessions) {
5771
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
5772
+ if (proj === projectName && s.agentId === "exe") return s;
5773
+ }
5774
+ return null;
5511
5775
  }
5512
- async function markProcessed(messageId) {
5513
- const client = getClient();
5514
- await client.execute({
5515
- sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
5516
- args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
5517
- });
5518
- }
5519
- async function getMessageStatus(messageId) {
5520
- const client = getClient();
5521
- const result = await client.execute({
5522
- sql: "SELECT status FROM messages WHERE id = ?",
5523
- args: [messageId]
5524
- });
5525
- return result.rows[0]?.status ?? null;
5526
- }
5527
- async function getUnacknowledgedMessages(targetAgent) {
5528
- const client = getClient();
5529
- const result = await client.execute({
5530
- sql: `SELECT * FROM messages
5531
- WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
5532
- ORDER BY id`,
5533
- args: [targetAgent]
5534
- });
5535
- return result.rows.map((row) => rowToMessage(row));
5536
- }
5537
- async function getReadMessages(targetAgent) {
5538
- const client = getClient();
5539
- const result = await client.execute({
5540
- sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
5541
- args: [targetAgent]
5542
- });
5543
- return result.rows.map((row) => rowToMessage(row));
5544
- }
5545
- async function markFailed(messageId, reason) {
5546
- const client = getClient();
5547
- await client.execute({
5548
- sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
5549
- args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
5550
- });
5551
- }
5552
- async function getFailedMessages() {
5553
- const client = getClient();
5554
- const result = await client.execute({
5555
- sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
5556
- args: []
5557
- });
5558
- return result.rows.map((row) => rowToMessage(row));
5559
- }
5560
- async function retryPendingMessages() {
5561
- const client = getClient();
5562
- const result = await client.execute({
5563
- sql: `SELECT * FROM messages
5564
- WHERE status = 'pending' AND retry_count < ?
5565
- ORDER BY id`,
5566
- args: [MAX_RETRIES2]
5567
- });
5568
- let delivered = 0;
5569
- for (const row of result.rows) {
5570
- const msg = rowToMessage(row);
5571
- try {
5572
- const success = await deliverLocalMessage(msg.id);
5573
- if (success) delivered++;
5574
- } catch {
5776
+ function assertSessionScope(actionType, targetProject) {
5777
+ try {
5778
+ const currentProject = getProjectName();
5779
+ const exeSession = resolveExeSession();
5780
+ if (!exeSession) {
5781
+ return { allowed: true, reason: "no_session" };
5782
+ }
5783
+ if (currentProject === targetProject) {
5784
+ return {
5785
+ allowed: true,
5786
+ reason: "same_session",
5787
+ currentProject,
5788
+ targetProject
5789
+ };
5575
5790
  }
5791
+ process.stderr.write(
5792
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
5793
+ `
5794
+ );
5795
+ return {
5796
+ allowed: false,
5797
+ reason: "cross_session_denied",
5798
+ currentProject,
5799
+ targetProject,
5800
+ targetSession: findSessionForProject(targetProject)?.windowName
5801
+ };
5802
+ } catch {
5803
+ return { allowed: true, reason: "no_session" };
5576
5804
  }
5577
- return delivered;
5578
5805
  }
5579
- var MAX_RETRIES2, _wsClientSend;
5580
- var init_messaging = __esm({
5581
- "src/lib/messaging.ts"() {
5806
+ var init_session_scope = __esm({
5807
+ "src/lib/session-scope.ts"() {
5582
5808
  "use strict";
5583
- init_database();
5809
+ init_session_registry();
5810
+ init_project_name();
5584
5811
  init_tmux_routing();
5585
- MAX_RETRIES2 = 10;
5586
- _wsClientSend = null;
5587
5812
  }
5588
5813
  });
5589
5814
 
5590
- // src/lib/notifications.ts
5591
- import crypto4 from "crypto";
5592
- import path12 from "path";
5593
- import os7 from "os";
5594
- import {
5595
- readFileSync as readFileSync10,
5596
- readdirSync as readdirSync2,
5597
- unlinkSync as unlinkSync3,
5598
- existsSync as existsSync11,
5599
- rmdirSync
5600
- } from "fs";
5601
- async function writeNotification(notification) {
5815
+ // src/lib/tasks-notify.ts
5816
+ async function dispatchTaskToEmployee(input) {
5817
+ if (input.assignedTo === "exe") return { dispatched: "skipped" };
5818
+ let crossProject = false;
5819
+ if (input.projectName) {
5820
+ try {
5821
+ const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
5822
+ const check = assertSessionScope2("dispatch_task", input.projectName);
5823
+ if (check.reason === "cross_session_denied") {
5824
+ crossProject = true;
5825
+ return { dispatched: "skipped", crossProject: true };
5826
+ }
5827
+ } catch {
5828
+ }
5829
+ }
5602
5830
  try {
5603
- const client = getClient();
5604
- const id = crypto4.randomUUID();
5605
- const now = (/* @__PURE__ */ new Date()).toISOString();
5606
- await client.execute({
5607
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
5608
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
5609
- args: [
5610
- id,
5611
- notification.agentId,
5612
- notification.agentRole,
5613
- notification.event,
5614
- notification.project,
5615
- notification.summary,
5616
- notification.taskFile ?? null,
5617
- now
5618
- ]
5619
- });
5620
- } catch (err) {
5621
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
5622
- `);
5831
+ const transport = getTransport();
5832
+ const exeSession = resolveExeSession();
5833
+ if (!exeSession) return { dispatched: "session_missing" };
5834
+ const sessionName = employeeSessionName(input.assignedTo, exeSession);
5835
+ if (transport.isAlive(sessionName)) {
5836
+ const result = sendIntercom(sessionName);
5837
+ const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
5838
+ return { dispatched, session: sessionName, crossProject };
5839
+ } else {
5840
+ const projectDir = input.projectDir ?? process.cwd();
5841
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
5842
+ autoInstance: isMultiInstance(input.assignedTo)
5843
+ });
5844
+ if (result.status === "failed") {
5845
+ process.stderr.write(
5846
+ `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
5847
+ `
5848
+ );
5849
+ return { dispatched: "session_missing" };
5850
+ }
5851
+ return { dispatched: "spawned", session: result.sessionName, crossProject };
5852
+ }
5853
+ } catch {
5854
+ return { dispatched: "session_missing" };
5623
5855
  }
5624
5856
  }
5625
- async function markAsReadByTaskFile(taskFile) {
5857
+ function notifyTaskDone() {
5626
5858
  try {
5627
- const client = getClient();
5628
- await client.execute({
5629
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
5630
- args: [taskFile]
5631
- });
5859
+ const key = getSessionKey();
5860
+ if (key && !process.env.VITEST) notifyParentExe(key);
5632
5861
  } catch {
5633
5862
  }
5634
5863
  }
5635
- var init_notifications = __esm({
5636
- "src/lib/notifications.ts"() {
5864
+ async function markTaskNotificationsRead(taskFile) {
5865
+ try {
5866
+ await markAsReadByTaskFile(taskFile);
5867
+ } catch {
5868
+ }
5869
+ }
5870
+ var init_tasks_notify = __esm({
5871
+ "src/lib/tasks-notify.ts"() {
5637
5872
  "use strict";
5638
- init_database();
5873
+ init_tmux_routing();
5874
+ init_session_key();
5875
+ init_notifications();
5876
+ init_transport();
5877
+ init_employees();
5639
5878
  }
5640
5879
  });
5641
5880
 
5642
- // src/lib/tasks-crud.ts
5643
- import crypto5 from "crypto";
5644
- import path13 from "path";
5645
- import { execSync as execSync5 } from "child_process";
5646
- import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
5647
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
5648
- async function writeCheckpoint(input) {
5881
+ // src/lib/behaviors.ts
5882
+ import crypto6 from "crypto";
5883
+ async function storeBehavior(opts) {
5649
5884
  const client = getClient();
5650
- const row = await resolveTask(client, input.taskId);
5651
- const taskId = String(row.id);
5885
+ const id = crypto6.randomUUID();
5652
5886
  const now = (/* @__PURE__ */ new Date()).toISOString();
5653
- const blockedByIds = [];
5654
- if (row.blocked_by) {
5655
- blockedByIds.push(String(row.blocked_by));
5656
- }
5657
- const checkpoint = {
5658
- step: input.step,
5659
- context_summary: input.contextSummary,
5660
- files_touched: input.filesTouched ?? [],
5661
- blocked_by_ids: blockedByIds,
5662
- last_checkpoint_at: now
5663
- };
5664
- const result = await client.execute({
5665
- sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
5666
- args: [JSON.stringify(checkpoint), now, taskId]
5887
+ await client.execute({
5888
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
5889
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
5890
+ args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
5667
5891
  });
5668
- if (result.rowsAffected === 0) {
5669
- throw new Error(`Checkpoint write failed: task ${taskId} not found`);
5892
+ return id;
5893
+ }
5894
+ var init_behaviors = __esm({
5895
+ "src/lib/behaviors.ts"() {
5896
+ "use strict";
5897
+ init_database();
5670
5898
  }
5671
- const countResult = await client.execute({
5672
- sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
5673
- args: [taskId]
5899
+ });
5900
+
5901
+ // src/lib/skill-learning.ts
5902
+ var skill_learning_exports = {};
5903
+ __export(skill_learning_exports, {
5904
+ captureAndLearn: () => captureAndLearn,
5905
+ captureTrajectory: () => captureTrajectory,
5906
+ editDistance: () => editDistance,
5907
+ extractSkill: () => extractSkill,
5908
+ extractTrajectory: () => extractTrajectory,
5909
+ findSimilarTrajectories: () => findSimilarTrajectories,
5910
+ hashSignature: () => hashSignature,
5911
+ storeTrajectory: () => storeTrajectory,
5912
+ sweepTrajectories: () => sweepTrajectories
5913
+ });
5914
+ import crypto7 from "crypto";
5915
+ async function extractTrajectory(taskId, agentId) {
5916
+ const client = getClient();
5917
+ const result = await client.execute({
5918
+ sql: `SELECT tool_name, raw_text
5919
+ FROM memories
5920
+ WHERE task_id = ? AND agent_id = ?
5921
+ ORDER BY timestamp ASC`,
5922
+ args: [taskId, agentId]
5674
5923
  });
5675
- const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
5676
- return { checkpointCount };
5677
- }
5678
- function extractParentFromContext(contextBody) {
5679
- if (!contextBody) return null;
5680
- const match = contextBody.match(
5681
- /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
5682
- );
5683
- return match ? match[1].toLowerCase() : null;
5684
- }
5685
- function slugify(title) {
5686
- return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
5687
- }
5688
- async function resolveTask(client, identifier) {
5689
- let result = await client.execute({
5690
- sql: "SELECT * FROM tasks WHERE id = ?",
5691
- args: [identifier]
5692
- });
5693
- if (result.rows.length === 1) return result.rows[0];
5694
- result = await client.execute({
5695
- sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
5696
- args: [`%${identifier}%`]
5697
- });
5698
- if (result.rows.length === 1) return result.rows[0];
5699
- if (result.rows.length > 1) {
5700
- const exact = result.rows.filter(
5701
- (r) => String(r.task_file).endsWith(`/${identifier}.md`)
5702
- );
5703
- if (exact.length === 1) return exact[0];
5704
- const candidates = exact.length > 1 ? exact : result.rows;
5705
- const active = candidates.filter(
5706
- (r) => !["done", "cancelled"].includes(String(r.status))
5707
- );
5708
- if (active.length === 1) return active[0];
5709
- const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
5710
- throw new Error(
5711
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
5712
- );
5713
- }
5714
- result = await client.execute({
5715
- sql: "SELECT * FROM tasks WHERE title LIKE ?",
5716
- args: [`%${identifier}%`]
5924
+ if (result.rows.length === 0) return [];
5925
+ const rawTools = result.rows.map((r) => {
5926
+ const toolName = String(r.tool_name);
5927
+ if (toolName === "Bash") {
5928
+ const text = String(r.raw_text);
5929
+ const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
5930
+ return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
5931
+ }
5932
+ return toolName;
5717
5933
  });
5718
- if (result.rows.length === 1) return result.rows[0];
5719
- if (result.rows.length > 1) {
5720
- const active = result.rows.filter(
5721
- (r) => !["done", "cancelled"].includes(String(r.status))
5722
- );
5723
- if (active.length === 1) return active[0];
5724
- const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
5725
- throw new Error(
5726
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
5727
- );
5934
+ const signature = [];
5935
+ for (const tool of rawTools) {
5936
+ if (signature.length === 0 || signature[signature.length - 1] !== tool) {
5937
+ signature.push(tool);
5938
+ }
5728
5939
  }
5729
- throw new Error(`Task not found: ${identifier}`);
5940
+ return signature;
5730
5941
  }
5731
- async function createTaskCore(input) {
5942
+ function hashSignature(signature) {
5943
+ return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
5944
+ }
5945
+ async function storeTrajectory(opts) {
5732
5946
  const client = getClient();
5733
- const id = crypto5.randomUUID();
5947
+ const id = crypto7.randomUUID();
5734
5948
  const now = (/* @__PURE__ */ new Date()).toISOString();
5735
- const slug = slugify(input.title);
5736
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
5737
- let blockedById = null;
5738
- const initialStatus = input.blockedBy ? "blocked" : "open";
5739
- if (input.blockedBy) {
5740
- const blocker = await resolveTask(client, input.blockedBy);
5741
- blockedById = String(blocker.id);
5742
- }
5743
- let parentTaskId = null;
5744
- let parentRef = input.parentTaskId;
5745
- if (!parentRef) {
5746
- const extracted = extractParentFromContext(input.context);
5747
- if (extracted) {
5748
- parentRef = extracted;
5749
- process.stderr.write(
5750
- "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
5751
- );
5752
- }
5753
- }
5754
- if (parentRef) {
5755
- try {
5756
- const parent = await resolveTask(client, parentRef);
5757
- parentTaskId = String(parent.id);
5758
- } catch (err) {
5759
- if (!input.parentTaskId) {
5760
- throw new Error(
5761
- `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
5762
- );
5763
- }
5764
- throw err;
5765
- }
5766
- }
5767
- let warning;
5768
- const dupCheck = await client.execute({
5769
- sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
5770
- args: [input.title, input.assignedTo]
5771
- });
5772
- if (dupCheck.rows.length > 0) {
5773
- warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
5774
- }
5775
- if (input.baseDir) {
5776
- try {
5777
- await mkdir4(path13.join(input.baseDir, "exe", "output"), { recursive: true });
5778
- await mkdir4(path13.join(input.baseDir, "exe", "research"), { recursive: true });
5779
- await ensureArchitectureDoc(input.baseDir, input.projectName);
5780
- await ensureGitignoreExe(input.baseDir);
5781
- } catch {
5782
- }
5783
- }
5784
- const complexity = input.complexity ?? "standard";
5949
+ const signatureHash = hashSignature(opts.signature);
5785
5950
  await client.execute({
5786
- 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)
5787
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5951
+ sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
5952
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5788
5953
  args: [
5789
5954
  id,
5790
- input.title,
5791
- input.assignedTo,
5792
- input.assignedBy,
5793
- input.projectName,
5794
- input.priority,
5795
- initialStatus,
5796
- taskFile,
5797
- blockedById,
5798
- parentTaskId,
5799
- input.reviewer ?? null,
5800
- input.context,
5801
- complexity,
5802
- input.budgetTokens ?? null,
5803
- input.budgetFallbackModel ?? null,
5804
- 0,
5805
- null,
5806
- now,
5955
+ opts.taskId,
5956
+ opts.agentId,
5957
+ opts.projectName,
5958
+ opts.taskTitle,
5959
+ JSON.stringify(opts.signature),
5960
+ signatureHash,
5961
+ opts.signature.length,
5807
5962
  now
5808
5963
  ]
5809
5964
  });
5810
- return {
5811
- id,
5812
- title: input.title,
5813
- assignedTo: input.assignedTo,
5814
- assignedBy: input.assignedBy,
5815
- projectName: input.projectName,
5816
- priority: input.priority,
5817
- status: initialStatus,
5818
- taskFile,
5819
- createdAt: now,
5820
- updatedAt: now,
5821
- warning,
5822
- budgetTokens: input.budgetTokens ?? null,
5823
- budgetFallbackModel: input.budgetFallbackModel ?? null,
5824
- tokensUsed: 0,
5825
- tokensWarnedAt: null
5826
- };
5965
+ return id;
5827
5966
  }
5828
- async function listTasks(input) {
5967
+ async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
5829
5968
  const client = getClient();
5830
- const conditions = [];
5831
- const args = [];
5832
- if (input.assignedTo) {
5833
- conditions.push("assigned_to = ?");
5834
- args.push(input.assignedTo);
5835
- }
5836
- if (input.status) {
5837
- conditions.push("status = ?");
5838
- args.push(input.status);
5839
- } else {
5840
- conditions.push("status IN ('open', 'in_progress', 'blocked')");
5841
- }
5842
- if (input.projectName) {
5843
- conditions.push("project_name = ?");
5844
- args.push(input.projectName);
5845
- }
5846
- if (input.priority) {
5847
- conditions.push("priority = ?");
5848
- args.push(input.priority);
5849
- }
5850
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5969
+ const hash = hashSignature(signature);
5851
5970
  const result = await client.execute({
5852
- 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`,
5853
- args
5971
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
5972
+ FROM trajectories
5973
+ WHERE signature_hash = ?
5974
+ ORDER BY created_at DESC
5975
+ LIMIT 20`,
5976
+ args: [hash]
5854
5977
  });
5855
- return result.rows.map((r) => ({
5978
+ const mapRow = (r) => ({
5856
5979
  id: String(r.id),
5857
- title: String(r.title),
5858
- assignedTo: String(r.assigned_to),
5859
- assignedBy: String(r.assigned_by),
5980
+ taskId: String(r.task_id),
5981
+ agentId: String(r.agent_id),
5860
5982
  projectName: String(r.project_name),
5861
- priority: String(r.priority),
5862
- status: String(r.status),
5863
- taskFile: String(r.task_file),
5864
- createdAt: String(r.created_at),
5865
- updatedAt: String(r.updated_at),
5866
- checkpointCount: Number(r.checkpoint_count ?? 0),
5867
- budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
5868
- budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
5869
- tokensUsed: Number(r.tokens_used ?? 0),
5870
- tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
5871
- }));
5983
+ taskTitle: String(r.task_title),
5984
+ signature: JSON.parse(String(r.signature)),
5985
+ signatureHash: String(r.signature_hash),
5986
+ toolCount: Number(r.tool_count),
5987
+ skillId: r.skill_id ? String(r.skill_id) : null,
5988
+ createdAt: String(r.created_at)
5989
+ });
5990
+ const matches = result.rows.map(mapRow);
5991
+ if (matches.length >= threshold) return matches;
5992
+ const nearResult = await client.execute({
5993
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
5994
+ FROM trajectories
5995
+ WHERE tool_count BETWEEN ? AND ?
5996
+ AND signature_hash != ?
5997
+ ORDER BY created_at DESC
5998
+ LIMIT 50`,
5999
+ args: [
6000
+ Math.max(1, signature.length - 3),
6001
+ signature.length + 3,
6002
+ hash
6003
+ ]
6004
+ });
6005
+ for (const r of nearResult.rows) {
6006
+ const candidateSig = JSON.parse(String(r.signature));
6007
+ if (editDistance(signature, candidateSig) <= 2) {
6008
+ matches.push(mapRow(r));
6009
+ }
6010
+ }
6011
+ return matches;
5872
6012
  }
5873
- function checkStaleCompletion(taskContext, taskCreatedAt) {
5874
- if (!taskContext) return null;
5875
- if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
5876
- try {
5877
- const since = new Date(taskCreatedAt).toISOString();
5878
- const branch = execSync5(
5879
- "git rev-parse --abbrev-ref HEAD 2>/dev/null",
5880
- { encoding: "utf8", timeout: 3e3 }
5881
- ).trim();
5882
- const branchArg = branch && branch !== "HEAD" ? branch : "";
5883
- const commitCount = execSync5(
5884
- `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
5885
- { encoding: "utf8", timeout: 5e3 }
5886
- ).trim();
5887
- const count = parseInt(commitCount, 10);
5888
- if (count === 0) {
5889
- return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
5890
- }
5891
- return null;
5892
- } catch {
5893
- return null;
6013
+ async function captureTrajectory(opts) {
6014
+ const signature = await extractTrajectory(opts.taskId, opts.agentId);
6015
+ if (signature.length < 3) {
6016
+ return { trajectoryId: "", similarCount: 0, similar: [] };
5894
6017
  }
6018
+ const trajectoryId = await storeTrajectory({
6019
+ taskId: opts.taskId,
6020
+ agentId: opts.agentId,
6021
+ projectName: opts.projectName,
6022
+ taskTitle: opts.taskTitle,
6023
+ signature
6024
+ });
6025
+ const similar = await findSimilarTrajectories(
6026
+ signature,
6027
+ opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
6028
+ );
6029
+ return { trajectoryId, similarCount: similar.length, similar };
5895
6030
  }
5896
- async function updateTaskStatus(input) {
5897
- const client = getClient();
5898
- const now = (/* @__PURE__ */ new Date()).toISOString();
5899
- const row = await resolveTask(client, input.taskId);
5900
- const taskId = String(row.id);
5901
- const taskFile = String(row.task_file);
5902
- if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
5903
- process.stderr.write(
5904
- `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
5905
- `
5906
- );
5907
- }
5908
- if (input.status === "done") {
5909
- const existingRow = await client.execute({
5910
- sql: "SELECT context, created_at FROM tasks WHERE id = ?",
5911
- args: [taskId]
5912
- });
5913
- if (existingRow.rows.length > 0) {
5914
- const ctx = existingRow.rows[0];
5915
- const warning = checkStaleCompletion(ctx.context, ctx.created_at);
5916
- if (warning) {
5917
- input.result = input.result ? `\u26A0\uFE0F ${warning}
6031
+ function buildExtractionPrompt(trajectories) {
6032
+ const items = trajectories.map((t, i) => {
6033
+ const sig = t.signature.join(" \u2192 ");
6034
+ return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
6035
+ Signature: ${sig}`;
6036
+ }).join("\n\n");
6037
+ return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
5918
6038
 
5919
- ${input.result}` : `\u26A0\uFE0F ${warning}`;
5920
- process.stderr.write(`[tasks] ${warning} (task: ${taskId})
5921
- `);
5922
- }
5923
- }
5924
- }
5925
- if (input.status === "in_progress") {
5926
- const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
5927
- const claim = await client.execute({
5928
- sql: `UPDATE tasks
5929
- SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
5930
- WHERE id = ? AND status = 'open'`,
5931
- args: [tmuxSession, now, taskId]
5932
- });
5933
- if (claim.rowsAffected === 0) {
5934
- const current = await client.execute({
5935
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
5936
- args: [taskId]
5937
- });
5938
- const cur = current.rows[0];
5939
- const status = cur?.status ?? "unknown";
5940
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
5941
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
5942
- }
5943
- try {
5944
- await writeCheckpoint({
5945
- taskId,
5946
- step: "claimed",
5947
- contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
5948
- });
5949
- } catch {
5950
- }
5951
- return { row, taskFile, now, taskId };
5952
- }
5953
- if (input.result) {
5954
- await client.execute({
5955
- sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
5956
- args: [input.status, input.result, now, taskId]
5957
- });
5958
- } else {
5959
- await client.execute({
5960
- sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
5961
- args: [input.status, now, taskId]
5962
- });
5963
- }
5964
- try {
5965
- await writeCheckpoint({
5966
- taskId,
5967
- step: `status_transition:${input.status}`,
5968
- contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
5969
- });
5970
- } catch {
5971
- }
5972
- return { row, taskFile, now, taskId };
5973
- }
5974
- async function deleteTaskCore(taskId, _baseDir) {
5975
- const client = getClient();
5976
- const row = await resolveTask(client, taskId);
5977
- const id = String(row.id);
5978
- const taskFile = String(row.task_file);
5979
- const assignedTo = String(row.assigned_to);
5980
- const assignedBy = String(row.assigned_by);
5981
- await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
5982
- const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
5983
- return { taskFile, assignedTo, assignedBy, taskSlug };
6039
+ ${items}
6040
+
6041
+ Extract the reusable procedure. Format your response EXACTLY like this:
6042
+
6043
+ SKILL: {name \u2014 short, descriptive}
6044
+ TRIGGER: {when to use this \u2014 one sentence}
6045
+ STEPS:
6046
+ 1. ...
6047
+ 2. ...
6048
+ PITFALLS: {common mistakes to avoid}
6049
+
6050
+ Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
5984
6051
  }
5985
- async function ensureArchitectureDoc(baseDir, projectName) {
5986
- const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
5987
- try {
5988
- if (existsSync12(archPath)) return;
5989
- const template = [
5990
- `# ${projectName} \u2014 System Architecture`,
5991
- "",
5992
- "> Employees: read this before every task. Update it when you change system structure.",
5993
- `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
5994
- "",
5995
- "## Overview",
5996
- "",
5997
- "<!-- Describe what this system does, its main components, and how they connect. -->",
5998
- "",
5999
- "## Key Components",
6000
- "",
6001
- "<!-- List the major modules, services, or subsystems. -->",
6002
- "",
6003
- "## Data Flow",
6004
- "",
6005
- "<!-- How does data move through the system? What writes where? -->",
6006
- "",
6007
- "## Invariants",
6008
- "",
6009
- "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
6010
- "",
6011
- "## Dependencies",
6012
- "",
6013
- "<!-- What depends on what? If I change X, what else is affected? -->",
6014
- ""
6015
- ].join("\n");
6016
- await writeFile4(archPath, template, "utf-8");
6017
- } catch {
6052
+ async function extractSkill(trajectories, model) {
6053
+ if (trajectories.length === 0) return null;
6054
+ const config2 = await loadConfig();
6055
+ const skillModel = model ?? config2.skillModel;
6056
+ const Anthropic2 = (await import("@anthropic-ai/sdk")).default;
6057
+ const client = new Anthropic2();
6058
+ const prompt = buildExtractionPrompt(trajectories);
6059
+ const response = await client.messages.create({
6060
+ model: skillModel,
6061
+ max_tokens: 500,
6062
+ messages: [{ role: "user", content: prompt }]
6063
+ });
6064
+ const textBlock = response.content.find((b) => b.type === "text");
6065
+ const skillText = textBlock?.text;
6066
+ if (!skillText) return null;
6067
+ const agentId = trajectories[0].agentId;
6068
+ const projectName = trajectories[0].projectName;
6069
+ const skillId = await storeBehavior({
6070
+ agentId,
6071
+ content: skillText,
6072
+ domain: "skill",
6073
+ projectName
6074
+ });
6075
+ const dbClient = getClient();
6076
+ for (const t of trajectories) {
6077
+ await dbClient.execute({
6078
+ sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
6079
+ args: [skillId, t.id]
6080
+ });
6018
6081
  }
6082
+ process.stderr.write(
6083
+ `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
6084
+ `
6085
+ );
6086
+ return skillId;
6019
6087
  }
6020
- async function ensureGitignoreExe(baseDir) {
6021
- const gitignorePath = path13.join(baseDir, ".gitignore");
6088
+ async function captureAndLearn(opts) {
6022
6089
  try {
6023
- if (existsSync12(gitignorePath)) {
6024
- const content = readFileSync11(gitignorePath, "utf-8");
6025
- if (/^\/?exe\/?$/m.test(content)) return;
6026
- await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
6027
- } else {
6028
- await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
6090
+ const config2 = await loadConfig();
6091
+ if (!config2.skillLearning) return;
6092
+ const { trajectoryId, similarCount, similar } = await captureTrajectory({
6093
+ ...opts,
6094
+ skillThreshold: config2.skillThreshold
6095
+ });
6096
+ if (!trajectoryId) return;
6097
+ if (similarCount >= config2.skillThreshold) {
6098
+ const unprocessed = similar.filter((t) => !t.skillId);
6099
+ if (unprocessed.length >= config2.skillThreshold) {
6100
+ extractSkill(unprocessed, config2.skillModel).catch((err) => {
6101
+ process.stderr.write(
6102
+ `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
6103
+ `
6104
+ );
6105
+ });
6106
+ }
6029
6107
  }
6030
- } catch {
6108
+ } catch (err) {
6109
+ process.stderr.write(
6110
+ `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
6111
+ `
6112
+ );
6031
6113
  }
6032
6114
  }
6033
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
6034
- var init_tasks_crud = __esm({
6035
- "src/lib/tasks-crud.ts"() {
6036
- "use strict";
6037
- init_database();
6038
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
6039
- TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
6040
- }
6041
- });
6042
-
6043
- // src/lib/tasks-review.ts
6044
- import path14 from "path";
6045
- import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
6046
- async function countPendingReviews() {
6115
+ async function sweepTrajectories(threshold, model) {
6116
+ const config2 = await loadConfig();
6117
+ if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
6118
+ const t = threshold ?? config2.skillThreshold;
6047
6119
  const client = getClient();
6048
6120
  const result = await client.execute({
6049
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
6050
- args: []
6121
+ sql: `SELECT signature_hash, COUNT(*) as cnt
6122
+ FROM trajectories
6123
+ WHERE skill_id IS NULL
6124
+ GROUP BY signature_hash
6125
+ HAVING cnt >= ?
6126
+ ORDER BY cnt DESC
6127
+ LIMIT 10`,
6128
+ args: [t]
6051
6129
  });
6052
- return Number(result.rows[0]?.cnt) || 0;
6053
- }
6054
- async function countNewPendingReviewsSince(sinceIso) {
6055
- const client = getClient();
6056
- const result = await client.execute({
6057
- sql: `SELECT COUNT(*) as cnt FROM tasks
6058
- WHERE status = 'needs_review' AND updated_at > ?`,
6059
- args: [sinceIso]
6060
- });
6061
- return Number(result.rows[0]?.cnt) || 0;
6062
- }
6063
- async function listPendingReviews(limit) {
6064
- const client = getClient();
6065
- const result = await client.execute({
6066
- sql: `SELECT title, assigned_to, project_name FROM tasks
6067
- WHERE status = 'needs_review'
6068
- ORDER BY priority ASC, created_at DESC LIMIT ?`,
6069
- args: [limit]
6070
- });
6071
- return result.rows;
6072
- }
6073
- function getReviewChecklist(role, agent, taskSlug) {
6074
- const roleLower = role.toLowerCase();
6075
- if (roleLower.includes("engineer") || roleLower === "principal engineer") {
6076
- return {
6077
- lens: "Code Quality (Engineer)",
6078
- checklist: [
6079
- "1. Do all tests pass? Any new tests needed?",
6080
- "2. Is the code clean \u2014 no dead code, no TODOs left?",
6081
- "3. Does it follow existing patterns and conventions in the codebase?",
6082
- "4. Any regressions in the test suite?"
6083
- ]
6084
- };
6085
- }
6086
- if (roleLower === "cto" || roleLower.includes("architect")) {
6087
- return {
6088
- lens: "Architecture (CTO)",
6089
- checklist: [
6090
- "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
6091
- "2. Is it backward compatible? Any breaking changes?",
6092
- "3. Does it introduce technical debt? Is that debt justified?",
6093
- "4. Security implications? Any new attack surface?",
6094
- "5. Does it scale? Performance considerations?",
6095
- "6. Coordination: does this affect other employees' work or other projects?"
6096
- ]
6097
- };
6098
- }
6099
- if (roleLower === "coo" || roleLower.includes("operations")) {
6100
- return {
6101
- lens: "Strategic (COO)",
6102
- checklist: [
6103
- "1. Does this serve the project mission?",
6104
- "2. Is this the right work at the right time?",
6105
- "3. Does the architectural assessment make sense for the business?",
6106
- "4. Any cross-project implications?"
6107
- ]
6108
- };
6109
- }
6110
- return {
6111
- lens: "General",
6112
- checklist: [
6113
- "1. Read the original task's acceptance criteria",
6114
- `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
6115
- "3. Verify code changes match requirements",
6116
- "4. Check if tests were added/updated",
6117
- `5. Look for output files in exe/output/${agent}-${taskSlug}*`
6118
- ]
6119
- };
6120
- }
6121
- async function cleanupReviewFile(row, taskFile, _baseDir) {
6122
- if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
6123
- try {
6124
- const client = getClient();
6125
- const now = (/* @__PURE__ */ new Date()).toISOString();
6126
- const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
6127
- if (parentId) {
6128
- const result = await client.execute({
6129
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
6130
- args: [now, parentId]
6131
- });
6132
- if (result.rowsAffected > 0) {
6133
- process.stderr.write(
6134
- `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
6135
- `
6136
- );
6137
- }
6138
- } else {
6139
- const fileName = taskFile.split("/").pop() ?? "";
6140
- const reviewPrefix = fileName.replace(".md", "");
6141
- const parts = reviewPrefix.split("-");
6142
- if (parts.length >= 3 && parts[0] === "review") {
6143
- const agent = parts[1];
6144
- const slug = parts.slice(2).join("-");
6145
- const originalTaskFile = `exe/${agent}/${slug}.md`;
6146
- const result = await client.execute({
6147
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
6148
- args: [now, originalTaskFile]
6149
- });
6150
- if (result.rowsAffected > 0) {
6151
- process.stderr.write(
6152
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
6153
- `
6154
- );
6155
- }
6156
- }
6130
+ let clustersProcessed = 0;
6131
+ let skillsExtracted = 0;
6132
+ for (const row of result.rows) {
6133
+ const hash = String(row.signature_hash);
6134
+ const trajResult = await client.execute({
6135
+ sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
6136
+ FROM trajectories
6137
+ WHERE signature_hash = ? AND skill_id IS NULL
6138
+ ORDER BY created_at DESC
6139
+ LIMIT 10`,
6140
+ args: [hash]
6141
+ });
6142
+ const trajectories = trajResult.rows.map((r) => ({
6143
+ id: String(r.id),
6144
+ taskId: String(r.task_id),
6145
+ agentId: String(r.agent_id),
6146
+ projectName: String(r.project_name),
6147
+ taskTitle: String(r.task_title),
6148
+ signature: JSON.parse(String(r.signature)),
6149
+ signatureHash: String(r.signature_hash),
6150
+ toolCount: Number(r.tool_count),
6151
+ skillId: null,
6152
+ createdAt: String(r.created_at)
6153
+ }));
6154
+ if (trajectories.length >= t) {
6155
+ clustersProcessed++;
6156
+ const skillId = await extractSkill(trajectories, model ?? config2.skillModel);
6157
+ if (skillId) skillsExtracted++;
6157
6158
  }
6158
- } catch (err) {
6159
- process.stderr.write(
6160
- `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
6161
- `
6162
- );
6163
6159
  }
6164
- try {
6165
- const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
6166
- if (existsSync13(cacheDir)) {
6167
- for (const f of readdirSync3(cacheDir)) {
6168
- if (f.startsWith("review-notified-")) {
6169
- unlinkSync4(path14.join(cacheDir, f));
6170
- }
6171
- }
6160
+ return { clustersProcessed, skillsExtracted };
6161
+ }
6162
+ function editDistance(a, b) {
6163
+ const m = a.length;
6164
+ const n = b.length;
6165
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
6166
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
6167
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
6168
+ for (let i = 1; i <= m; i++) {
6169
+ for (let j = 1; j <= n; j++) {
6170
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
6171
+ dp[i][j] = Math.min(
6172
+ dp[i - 1][j] + 1,
6173
+ dp[i][j - 1] + 1,
6174
+ dp[i - 1][j - 1] + cost
6175
+ );
6172
6176
  }
6173
- } catch {
6174
6177
  }
6178
+ return dp[m][n];
6175
6179
  }
6176
- var init_tasks_review = __esm({
6177
- "src/lib/tasks-review.ts"() {
6180
+ var DEFAULT_SKILL_THRESHOLD;
6181
+ var init_skill_learning = __esm({
6182
+ "src/lib/skill-learning.ts"() {
6178
6183
  "use strict";
6179
6184
  init_database();
6185
+ init_behaviors();
6180
6186
  init_config();
6181
- init_employees();
6182
- init_notifications();
6183
- init_tasks_crud();
6184
- init_tmux_routing();
6185
- init_session_key();
6187
+ DEFAULT_SKILL_THRESHOLD = 3;
6186
6188
  }
6187
6189
  });
6188
6190
 
6189
- // src/lib/tasks-chain.ts
6190
- import path15 from "path";
6191
- import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
6192
- async function cascadeUnblock(taskId, baseDir, now) {
6193
- const client = getClient();
6194
- const unblocked = await client.execute({
6195
- sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
6196
- WHERE blocked_by = ? AND status = 'blocked'`,
6197
- args: [now, taskId]
6198
- });
6199
- if (baseDir && unblocked.rowsAffected > 0) {
6200
- const unblockedRows = await client.execute({
6201
- sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
6202
- args: [now]
6191
+ // src/lib/tasks.ts
6192
+ var tasks_exports = {};
6193
+ __export(tasks_exports, {
6194
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
6195
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
6196
+ countPendingReviews: () => countPendingReviews,
6197
+ createTask: () => createTask,
6198
+ createTaskCore: () => createTaskCore,
6199
+ deleteTask: () => deleteTask,
6200
+ deleteTaskCore: () => deleteTaskCore,
6201
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
6202
+ ensureGitignoreExe: () => ensureGitignoreExe,
6203
+ getReviewChecklist: () => getReviewChecklist,
6204
+ listPendingReviews: () => listPendingReviews,
6205
+ listTasks: () => listTasks,
6206
+ resolveTask: () => resolveTask,
6207
+ slugify: () => slugify,
6208
+ updateTask: () => updateTask,
6209
+ updateTaskStatus: () => updateTaskStatus,
6210
+ writeCheckpoint: () => writeCheckpoint
6211
+ });
6212
+ import path16 from "path";
6213
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
6214
+ async function createTask(input) {
6215
+ const result = await createTaskCore(input);
6216
+ if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
6217
+ dispatchTaskToEmployee({
6218
+ assignedTo: input.assignedTo,
6219
+ title: input.title,
6220
+ priority: input.priority,
6221
+ taskFile: result.taskFile,
6222
+ initialStatus: result.status,
6223
+ projectName: input.projectName
6203
6224
  });
6204
- for (const ur of unblockedRows.rows) {
6225
+ }
6226
+ return result;
6227
+ }
6228
+ async function updateTask(input) {
6229
+ const { row, taskFile, now, taskId } = await updateTaskStatus(input);
6230
+ try {
6231
+ const agent = String(row.assigned_to);
6232
+ const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
6233
+ const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
6234
+ if (input.status === "in_progress") {
6235
+ mkdirSync6(cacheDir, { recursive: true });
6236
+ writeFileSync4(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
6237
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
6205
6238
  try {
6206
- const ubFile = path15.join(baseDir, String(ur.task_file));
6207
- let ubContent = await readFile4(ubFile, "utf-8");
6208
- ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
6209
- ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
6210
- await writeFile5(ubFile, ubContent, "utf-8");
6239
+ unlinkSync4(cachePath);
6211
6240
  } catch {
6212
6241
  }
6213
6242
  }
6243
+ } catch {
6214
6244
  }
6215
- }
6216
- async function findNextTask(assignedTo) {
6217
- const client = getClient();
6218
- const nextResult = await client.execute({
6219
- sql: `SELECT title, task_file, priority FROM tasks
6220
- WHERE assigned_to = ? AND status = 'open'
6221
- ORDER BY priority ASC, created_at ASC
6222
- LIMIT 1`,
6223
- args: [assignedTo]
6224
- });
6225
- if (nextResult.rows.length === 1) {
6226
- const nr = nextResult.rows[0];
6227
- return {
6228
- title: String(nr.title),
6229
- priority: String(nr.priority),
6230
- taskFile: String(nr.task_file)
6231
- };
6245
+ if (input.status === "done") {
6246
+ await cleanupReviewFile(row, taskFile, input.baseDir);
6232
6247
  }
6233
- return void 0;
6234
- }
6235
- async function checkSubtaskCompletion(parentTaskId, projectName) {
6236
- const client = getClient();
6237
- const remaining = await client.execute({
6238
- sql: `SELECT COUNT(*) as cnt FROM tasks
6239
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
6240
- args: [parentTaskId]
6241
- });
6242
- const cnt = Number(remaining.rows[0]?.cnt ?? 1);
6243
- if (cnt === 0) {
6244
- const parentRow = await client.execute({
6245
- sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
6246
- args: [parentTaskId]
6247
- });
6248
- if (parentRow.rows.length === 1) {
6249
- const pr = parentRow.rows[0];
6250
- const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
6251
- await writeNotification({
6252
- agentId: String(pr.assigned_to),
6253
- agentRole: "system",
6254
- event: "subtasks_complete",
6255
- project: parentProject,
6256
- summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
6257
- taskFile: String(pr.task_file)
6248
+ if (input.status === "done" || input.status === "cancelled") {
6249
+ try {
6250
+ const client = getClient();
6251
+ const taskTitle = String(row.title);
6252
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
6253
+ await client.execute({
6254
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
6255
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
6256
+ args: [now, `%left '${escaped}' as in\\_progress%`]
6258
6257
  });
6258
+ } catch {
6259
+ }
6260
+ try {
6261
+ const client = getClient();
6262
+ const cascaded = await client.execute({
6263
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
6264
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
6265
+ args: [now, taskId]
6266
+ });
6267
+ if (cascaded.rowsAffected > 0) {
6268
+ process.stderr.write(
6269
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
6270
+ `
6271
+ );
6272
+ }
6273
+ } catch {
6259
6274
  }
6260
6275
  }
6261
- }
6262
- var init_tasks_chain = __esm({
6263
- "src/lib/tasks-chain.ts"() {
6264
- "use strict";
6265
- init_database();
6266
- init_notifications();
6276
+ const isTerminal = input.status === "done" || input.status === "needs_review";
6277
+ if (isTerminal) {
6278
+ const isExe = String(row.assigned_to) === "exe";
6279
+ if (!isExe) {
6280
+ notifyTaskDone();
6281
+ }
6282
+ await markTaskNotificationsRead(taskFile);
6283
+ if (input.status === "done") {
6284
+ try {
6285
+ await cascadeUnblock(taskId, input.baseDir, now);
6286
+ } catch {
6287
+ }
6288
+ orgBus.emit({
6289
+ type: "task_completed",
6290
+ taskId,
6291
+ employee: String(row.assigned_to),
6292
+ result: input.result ?? "",
6293
+ timestamp: now
6294
+ });
6295
+ if (row.parent_task_id) {
6296
+ try {
6297
+ await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
6298
+ } catch {
6299
+ }
6300
+ }
6301
+ }
6267
6302
  }
6268
- });
6269
-
6270
- // src/lib/project-name.ts
6271
- import { execSync as execSync6 } from "child_process";
6272
- import path16 from "path";
6273
- function getProjectName(cwd) {
6274
- const dir = cwd ?? process.cwd();
6275
- if (_cached2 && _cachedCwd === dir) return _cached2;
6276
- try {
6277
- let repoRoot;
6303
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
6304
+ Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
6305
+ ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
6306
+ taskId,
6307
+ agentId: String(row.assigned_to),
6308
+ projectName: String(row.project_name),
6309
+ taskTitle: String(row.title)
6310
+ })
6311
+ ).catch((err) => {
6312
+ process.stderr.write(
6313
+ `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
6314
+ `
6315
+ );
6316
+ });
6317
+ }
6318
+ let nextTask;
6319
+ if (isTerminal && String(row.assigned_to) !== "exe") {
6278
6320
  try {
6279
- const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
6280
- cwd: dir,
6281
- encoding: "utf8",
6282
- timeout: 2e3,
6283
- stdio: ["pipe", "pipe", "pipe"]
6284
- }).trim();
6285
- repoRoot = path16.dirname(gitCommonDir);
6321
+ nextTask = await findNextTask(String(row.assigned_to));
6286
6322
  } catch {
6287
- repoRoot = execSync6("git rev-parse --show-toplevel", {
6288
- cwd: dir,
6289
- encoding: "utf8",
6290
- timeout: 2e3,
6291
- stdio: ["pipe", "pipe", "pipe"]
6292
- }).trim();
6293
6323
  }
6294
- _cached2 = path16.basename(repoRoot);
6295
- _cachedCwd = dir;
6296
- return _cached2;
6297
- } catch {
6298
- _cached2 = path16.basename(dir);
6299
- _cachedCwd = dir;
6300
- return _cached2;
6301
6324
  }
6325
+ return {
6326
+ id: String(row.id),
6327
+ title: String(row.title),
6328
+ assignedTo: String(row.assigned_to),
6329
+ assignedBy: String(row.assigned_by),
6330
+ projectName: String(row.project_name),
6331
+ priority: String(row.priority),
6332
+ status: input.status,
6333
+ taskFile,
6334
+ createdAt: String(row.created_at),
6335
+ updatedAt: now,
6336
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
6337
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
6338
+ tokensUsed: Number(row.tokens_used ?? 0),
6339
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
6340
+ nextTask
6341
+ };
6302
6342
  }
6303
- var _cached2, _cachedCwd;
6304
- var init_project_name = __esm({
6305
- "src/lib/project-name.ts"() {
6343
+ async function deleteTask(taskId, baseDir) {
6344
+ const client = getClient();
6345
+ const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
6346
+ const reviewer = assignedBy || "exe";
6347
+ const reviewSlug = `review-${assignedTo}-${taskSlug}`;
6348
+ const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
6349
+ await client.execute({
6350
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
6351
+ args: [reviewFile, `exe/exe/${reviewSlug}.md`]
6352
+ });
6353
+ await markAsReadByTaskFile(taskFile);
6354
+ await markAsReadByTaskFile(reviewFile);
6355
+ }
6356
+ var init_tasks = __esm({
6357
+ "src/lib/tasks.ts"() {
6306
6358
  "use strict";
6307
- _cached2 = null;
6308
- _cachedCwd = null;
6359
+ init_database();
6360
+ init_config();
6361
+ init_notifications();
6362
+ init_state_bus();
6363
+ init_tasks_crud();
6364
+ init_tasks_review();
6365
+ init_tasks_crud();
6366
+ init_tasks_chain();
6367
+ init_tasks_review();
6368
+ init_tasks_notify();
6309
6369
  }
6310
6370
  });
6311
6371
 
6312
- // src/lib/session-scope.ts
6313
- var session_scope_exports = {};
6314
- __export(session_scope_exports, {
6315
- assertSessionScope: () => assertSessionScope,
6316
- findSessionForProject: () => findSessionForProject,
6317
- getSessionProject: () => getSessionProject
6372
+ // src/lib/capacity-monitor.ts
6373
+ var capacity_monitor_exports = {};
6374
+ __export(capacity_monitor_exports, {
6375
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
6376
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
6377
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
6378
+ confirmCapacityKill: () => confirmCapacityKill,
6379
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
6380
+ extractContextPercent: () => extractContextPercent,
6381
+ isAtCapacity: () => isAtCapacity,
6382
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
6383
+ pollCapacityDead: () => pollCapacityDead
6318
6384
  });
6319
- function getSessionProject(sessionName) {
6320
- const sessions = listSessions();
6321
- const entry = sessions.find((s) => s.windowName === sessionName);
6322
- if (!entry) return null;
6323
- const parts = entry.projectDir.split("/").filter(Boolean);
6324
- return parts[parts.length - 1] ?? null;
6385
+ function resumeTaskTitle(agentId) {
6386
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
6387
+ }
6388
+ function buildResumeContext(agentId, openTasks) {
6389
+ const taskList = openTasks.map(
6390
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
6391
+ ).join("\n");
6392
+ return [
6393
+ "## Context",
6394
+ "",
6395
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
6396
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
6397
+ "",
6398
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
6399
+ "",
6400
+ taskList,
6401
+ "",
6402
+ "Read each task file and chain through them. Build and commit after each one."
6403
+ ].join("\n");
6404
+ }
6405
+ function filterPaneContent(paneOutput) {
6406
+ return paneOutput.split("\n").filter((line) => {
6407
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
6408
+ for (const marker of CONTENT_LINE_MARKERS) {
6409
+ if (line.includes(marker)) return false;
6410
+ }
6411
+ for (const re of SOURCE_CODE_MARKERS) {
6412
+ if (re.test(line)) return false;
6413
+ }
6414
+ return true;
6415
+ }).join("\n");
6416
+ }
6417
+ function extractContextPercent(paneOutput) {
6418
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
6419
+ if (!match) return null;
6420
+ const parsed = Number.parseInt(match[2], 10);
6421
+ return Number.isFinite(parsed) ? parsed : null;
6422
+ }
6423
+ function isAtCapacity(paneOutput) {
6424
+ const filtered = filterPaneContent(paneOutput);
6425
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
6426
+ }
6427
+ function confirmCapacityKill(agentId, now = Date.now()) {
6428
+ const pendingSince = _pendingCapacityKill.get(agentId);
6429
+ if (pendingSince === void 0) {
6430
+ _pendingCapacityKill.set(agentId, now);
6431
+ return false;
6432
+ }
6433
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
6434
+ _pendingCapacityKill.set(agentId, now);
6435
+ return false;
6436
+ }
6437
+ _pendingCapacityKill.delete(agentId);
6438
+ return true;
6325
6439
  }
6326
- function findSessionForProject(projectName) {
6327
- const sessions = listSessions();
6328
- for (const s of sessions) {
6329
- const proj = s.projectDir.split("/").filter(Boolean).pop();
6330
- if (proj === projectName && s.agentId === "exe") return s;
6440
+ function _resetPendingCapacityKills() {
6441
+ _pendingCapacityKill.clear();
6442
+ }
6443
+ function _resetLastRelaunchCache() {
6444
+ _lastRelaunch.clear();
6445
+ }
6446
+ async function lastResumeCreatedAtMs(agentId) {
6447
+ const client = getClient();
6448
+ const result = await client.execute({
6449
+ sql: `SELECT MAX(created_at) AS last_created_at
6450
+ FROM tasks
6451
+ WHERE assigned_to = ? AND title LIKE ?`,
6452
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`]
6453
+ });
6454
+ const raw = result.rows[0]?.last_created_at;
6455
+ if (raw === null || raw === void 0) return null;
6456
+ const parsed = Date.parse(String(raw));
6457
+ return Number.isNaN(parsed) ? null : parsed;
6458
+ }
6459
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
6460
+ const cached = _lastRelaunch.get(agentId);
6461
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
6462
+ const persisted = await lastResumeCreatedAtMs(agentId);
6463
+ if (persisted === null) return false;
6464
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
6465
+ _lastRelaunch.set(agentId, persisted);
6466
+ return true;
6467
+ }
6468
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
6469
+ const client = getClient();
6470
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6471
+ const context = buildResumeContext(agentId, openTasks);
6472
+ const existing = await client.execute({
6473
+ sql: `SELECT id FROM tasks
6474
+ WHERE assigned_to = ?
6475
+ AND title LIKE ?
6476
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
6477
+ ORDER BY created_at DESC
6478
+ LIMIT 1`,
6479
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
6480
+ });
6481
+ if (existing.rows.length > 0) {
6482
+ const taskId = String(existing.rows[0].id);
6483
+ await client.execute({
6484
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
6485
+ args: [context, now, taskId]
6486
+ });
6487
+ return { created: false, taskId };
6331
6488
  }
6332
- return null;
6489
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
6490
+ const task = await createTask2({
6491
+ title: resumeTaskTitle(agentId),
6492
+ assignedTo: agentId,
6493
+ assignedBy: "system",
6494
+ projectName: projectDir.split("/").pop() ?? "unknown",
6495
+ priority: "p0",
6496
+ context,
6497
+ baseDir: projectDir
6498
+ });
6499
+ return { created: true, taskId: task.id };
6333
6500
  }
6334
- function assertSessionScope(actionType, targetProject) {
6501
+ async function pollCapacityDead() {
6502
+ const transport = getTransport();
6503
+ const relaunched = [];
6504
+ const registered = listSessions().filter(
6505
+ (s) => s.agentId !== "exe"
6506
+ );
6507
+ if (registered.length === 0) return [];
6508
+ let liveSessions;
6335
6509
  try {
6336
- const currentProject = getProjectName();
6337
- const exeSession = resolveExeSession();
6338
- if (!exeSession) {
6339
- return { allowed: true, reason: "no_session" };
6510
+ liveSessions = transport.listSessions();
6511
+ } catch {
6512
+ return [];
6513
+ }
6514
+ for (const entry of registered) {
6515
+ const { windowName, agentId, projectDir } = entry;
6516
+ if (!liveSessions.includes(windowName)) continue;
6517
+ if (await isWithinRelaunchCooldown(agentId)) continue;
6518
+ let pane;
6519
+ try {
6520
+ pane = transport.capturePane(windowName, 15);
6521
+ } catch {
6522
+ continue;
6340
6523
  }
6341
- if (currentProject === targetProject) {
6342
- return {
6343
- allowed: true,
6344
- reason: "same_session",
6345
- currentProject,
6346
- targetProject
6347
- };
6524
+ if (!isAtCapacity(pane)) continue;
6525
+ const ctxPct = extractContextPercent(pane);
6526
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
6527
+ process.stderr.write(
6528
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
6529
+ `
6530
+ );
6531
+ continue;
6532
+ }
6533
+ if (!confirmCapacityKill(agentId)) {
6534
+ process.stderr.write(
6535
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
6536
+ `
6537
+ );
6538
+ continue;
6539
+ }
6540
+ const verify = await verifyPaneAtCapacity(windowName);
6541
+ if (!verify.atCapacity) {
6542
+ process.stderr.write(
6543
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
6544
+ `
6545
+ );
6546
+ void recordSessionKill({
6547
+ sessionName: windowName,
6548
+ agentId,
6549
+ reason: "capacity_false_positive_blocked"
6550
+ });
6551
+ continue;
6348
6552
  }
6349
6553
  process.stderr.write(
6350
- `[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
6554
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
6351
6555
  `
6352
6556
  );
6353
- return {
6354
- allowed: true,
6355
- // v1: warn-only, don't block
6356
- reason: "cross_session_granted",
6357
- currentProject,
6358
- targetProject,
6359
- targetSession: findSessionForProject(targetProject)?.windowName
6360
- };
6361
- } catch {
6362
- return { allowed: true, reason: "no_session" };
6557
+ try {
6558
+ transport.kill(windowName);
6559
+ void recordSessionKill({
6560
+ sessionName: windowName,
6561
+ agentId,
6562
+ reason: "capacity"
6563
+ });
6564
+ const client = getClient();
6565
+ const openTasks = await client.execute({
6566
+ sql: `SELECT id, title, priority, task_file, status
6567
+ FROM tasks
6568
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')
6569
+ ORDER BY
6570
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
6571
+ created_at ASC
6572
+ LIMIT 10`,
6573
+ args: [agentId]
6574
+ });
6575
+ if (openTasks.rows.length === 0) {
6576
+ process.stderr.write(
6577
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
6578
+ `
6579
+ );
6580
+ continue;
6581
+ }
6582
+ const { created } = await createOrRefreshResumeTask(
6583
+ agentId,
6584
+ projectDir,
6585
+ openTasks.rows
6586
+ );
6587
+ if (created) {
6588
+ await writeNotification({
6589
+ agentId: "system",
6590
+ agentRole: "daemon",
6591
+ event: "capacity_relaunch",
6592
+ project: projectDir.split("/").pop() ?? "unknown",
6593
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
6594
+ });
6595
+ }
6596
+ _lastRelaunch.set(agentId, Date.now());
6597
+ if (created) relaunched.push(agentId);
6598
+ } catch (err) {
6599
+ process.stderr.write(
6600
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
6601
+ `
6602
+ );
6603
+ }
6363
6604
  }
6605
+ return relaunched;
6364
6606
  }
6365
- var init_session_scope = __esm({
6366
- "src/lib/session-scope.ts"() {
6607
+ 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;
6608
+ var init_capacity_monitor = __esm({
6609
+ "src/lib/capacity-monitor.ts"() {
6367
6610
  "use strict";
6368
6611
  init_session_registry();
6369
- init_project_name();
6612
+ init_transport();
6613
+ init_notifications();
6614
+ init_database();
6615
+ init_session_kill_telemetry();
6370
6616
  init_tmux_routing();
6617
+ CAPACITY_PATTERNS = [
6618
+ /conversation is too long/i,
6619
+ /maximum context length/i,
6620
+ /context window.*(?:limit|exceed|full)/i,
6621
+ /reached.*(?:token|context).*limit/i
6622
+ ];
6623
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
6624
+ CONTENT_LINE_MARKERS = [
6625
+ "RESUME:",
6626
+ "intercom",
6627
+ "capacity-monitor",
6628
+ "CAPACITY_PATTERNS",
6629
+ "isAtCapacity",
6630
+ "CONTENT_LINE_MARKERS",
6631
+ "pollCapacityDead",
6632
+ "confirmCapacityKill",
6633
+ "session_kills",
6634
+ "capacity-monitor.test"
6635
+ ];
6636
+ SOURCE_CODE_MARKERS = [
6637
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
6638
+ /(?:maximum context length|conversation is too long).*["'`/]/i
6639
+ ];
6640
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
6641
+ _lastRelaunch = /* @__PURE__ */ new Map();
6642
+ RESUME_TITLE_PREFIX = "RESUME:";
6643
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
6644
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
6645
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
6646
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
6647
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
6648
+ CTX_FLOOR_PERCENT = 50;
6371
6649
  }
6372
6650
  });
6373
6651
 
6374
- // src/lib/tasks-notify.ts
6375
- async function dispatchTaskToEmployee(input) {
6376
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
6377
- let crossProject = false;
6378
- if (input.projectName) {
6652
+ // src/lib/tmux-routing.ts
6653
+ var tmux_routing_exports = {};
6654
+ __export(tmux_routing_exports, {
6655
+ acquireSpawnLock: () => acquireSpawnLock2,
6656
+ employeeSessionName: () => employeeSessionName,
6657
+ ensureEmployee: () => ensureEmployee,
6658
+ extractRootExe: () => extractRootExe,
6659
+ findFreeInstance: () => findFreeInstance,
6660
+ getDispatchedBy: () => getDispatchedBy,
6661
+ getMySession: () => getMySession,
6662
+ getParentExe: () => getParentExe,
6663
+ getSessionState: () => getSessionState,
6664
+ isEmployeeAlive: () => isEmployeeAlive,
6665
+ isExeSession: () => isExeSession,
6666
+ isSessionBusy: () => isSessionBusy,
6667
+ notifyParentExe: () => notifyParentExe,
6668
+ parseParentExe: () => parseParentExe,
6669
+ registerParentExe: () => registerParentExe,
6670
+ releaseSpawnLock: () => releaseSpawnLock2,
6671
+ resolveExeSession: () => resolveExeSession,
6672
+ sendIntercom: () => sendIntercom,
6673
+ spawnEmployee: () => spawnEmployee,
6674
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
6675
+ });
6676
+ import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
6677
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
6678
+ import path17 from "path";
6679
+ import os7 from "os";
6680
+ import { fileURLToPath as fileURLToPath2 } from "url";
6681
+ import { unlinkSync as unlinkSync5 } from "fs";
6682
+ function spawnLockPath(sessionName) {
6683
+ return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
6684
+ }
6685
+ function isProcessAlive(pid) {
6686
+ try {
6687
+ process.kill(pid, 0);
6688
+ return true;
6689
+ } catch {
6690
+ return false;
6691
+ }
6692
+ }
6693
+ function acquireSpawnLock2(sessionName) {
6694
+ if (!existsSync13(SPAWN_LOCK_DIR)) {
6695
+ mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
6696
+ }
6697
+ const lockFile = spawnLockPath(sessionName);
6698
+ if (existsSync13(lockFile)) {
6379
6699
  try {
6380
- const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
6381
- const check = assertSessionScope2("dispatch_task", input.projectName);
6382
- if (check.reason === "cross_session_granted") {
6383
- crossProject = true;
6700
+ const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
6701
+ const age = Date.now() - lock.timestamp;
6702
+ if (isProcessAlive(lock.pid) && age < 6e4) {
6703
+ return false;
6384
6704
  }
6385
6705
  } catch {
6386
6706
  }
6387
6707
  }
6708
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
6709
+ return true;
6710
+ }
6711
+ function releaseSpawnLock2(sessionName) {
6388
6712
  try {
6389
- const transport = getTransport();
6390
- const exeSession = resolveExeSession();
6391
- if (!exeSession) return { dispatched: "session_missing" };
6392
- const sessionName = employeeSessionName(input.assignedTo, exeSession);
6393
- if (transport.isAlive(sessionName)) {
6394
- const result = sendIntercom(sessionName);
6395
- const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
6396
- return { dispatched, session: sessionName, crossProject };
6397
- } else {
6398
- const projectDir = input.projectDir ?? process.cwd();
6399
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
6400
- autoInstance: isMultiInstance(input.assignedTo)
6401
- });
6402
- if (result.status === "failed") {
6403
- process.stderr.write(
6404
- `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
6405
- `
6406
- );
6407
- return { dispatched: "session_missing" };
6408
- }
6409
- return { dispatched: "spawned", session: result.sessionName, crossProject };
6410
- }
6713
+ unlinkSync5(spawnLockPath(sessionName));
6411
6714
  } catch {
6412
- return { dispatched: "session_missing" };
6413
6715
  }
6414
6716
  }
6415
- function notifyTaskDone() {
6717
+ function resolveBehaviorsExporterScript() {
6416
6718
  try {
6417
- const key = getSessionKey();
6418
- if (key && !process.env.VITEST) notifyParentExe(key);
6719
+ const thisFile = fileURLToPath2(import.meta.url);
6720
+ const scriptPath = path17.join(
6721
+ path17.dirname(thisFile),
6722
+ "..",
6723
+ "bin",
6724
+ "exe-export-behaviors.js"
6725
+ );
6726
+ return existsSync13(scriptPath) ? scriptPath : null;
6419
6727
  } catch {
6728
+ return null;
6420
6729
  }
6421
6730
  }
6422
- async function markTaskNotificationsRead(taskFile) {
6731
+ function exportBehaviorsSync(agentId, projectName, sessionKey) {
6732
+ const script = resolveBehaviorsExporterScript();
6733
+ if (!script) return null;
6423
6734
  try {
6424
- await markAsReadByTaskFile(taskFile);
6425
- } catch {
6735
+ const output = execFileSync2(
6736
+ process.execPath,
6737
+ [script, agentId, projectName, sessionKey],
6738
+ { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
6739
+ ).trim();
6740
+ return output.length > 0 ? output : null;
6741
+ } catch (err) {
6742
+ process.stderr.write(
6743
+ `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
6744
+ `
6745
+ );
6746
+ return null;
6426
6747
  }
6427
6748
  }
6428
- var init_tasks_notify = __esm({
6429
- "src/lib/tasks-notify.ts"() {
6430
- "use strict";
6431
- init_tmux_routing();
6432
- init_session_key();
6433
- init_notifications();
6434
- init_transport();
6435
- init_employees();
6436
- }
6437
- });
6438
-
6439
- // src/lib/behaviors.ts
6440
- import crypto6 from "crypto";
6441
- async function storeBehavior(opts) {
6442
- const client = getClient();
6443
- const id = crypto6.randomUUID();
6444
- const now = (/* @__PURE__ */ new Date()).toISOString();
6445
- await client.execute({
6446
- sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
6447
- VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
6448
- args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
6449
- });
6450
- return id;
6749
+ function getMySession() {
6750
+ return getTransport().getMySession();
6451
6751
  }
6452
- var init_behaviors = __esm({
6453
- "src/lib/behaviors.ts"() {
6454
- "use strict";
6455
- init_database();
6456
- }
6457
- });
6458
-
6459
- // src/lib/skill-learning.ts
6460
- var skill_learning_exports = {};
6461
- __export(skill_learning_exports, {
6462
- captureAndLearn: () => captureAndLearn,
6463
- captureTrajectory: () => captureTrajectory,
6464
- editDistance: () => editDistance,
6465
- extractSkill: () => extractSkill,
6466
- extractTrajectory: () => extractTrajectory,
6467
- findSimilarTrajectories: () => findSimilarTrajectories,
6468
- hashSignature: () => hashSignature,
6469
- storeTrajectory: () => storeTrajectory,
6470
- sweepTrajectories: () => sweepTrajectories
6471
- });
6472
- import crypto7 from "crypto";
6473
- async function extractTrajectory(taskId, agentId) {
6474
- const client = getClient();
6475
- const result = await client.execute({
6476
- sql: `SELECT tool_name, raw_text
6477
- FROM memories
6478
- WHERE task_id = ? AND agent_id = ?
6479
- ORDER BY timestamp ASC`,
6480
- args: [taskId, agentId]
6481
- });
6482
- if (result.rows.length === 0) return [];
6483
- const rawTools = result.rows.map((r) => {
6484
- const toolName = String(r.tool_name);
6485
- if (toolName === "Bash") {
6486
- const text = String(r.raw_text);
6487
- const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
6488
- return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
6489
- }
6490
- return toolName;
6491
- });
6492
- const signature = [];
6493
- for (const tool of rawTools) {
6494
- if (signature.length === 0 || signature[signature.length - 1] !== tool) {
6495
- signature.push(tool);
6752
+ function employeeSessionName(employee, exeSession, instance) {
6753
+ if (!/^exe\d+$/.test(exeSession)) {
6754
+ const root = extractRootExe(exeSession);
6755
+ if (root) {
6756
+ process.stderr.write(
6757
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
6758
+ `
6759
+ );
6760
+ exeSession = root;
6761
+ } else {
6762
+ throw new Error(
6763
+ `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
6764
+ );
6496
6765
  }
6497
6766
  }
6498
- return signature;
6767
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
6768
+ const name = `${employee}${suffix}-${exeSession}`;
6769
+ if (!VALID_SESSION_NAME.test(name)) {
6770
+ throw new Error(
6771
+ `Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
6772
+ );
6773
+ }
6774
+ return name;
6499
6775
  }
6500
- function hashSignature(signature) {
6501
- return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
6776
+ function parseParentExe(sessionName, agentId) {
6777
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6778
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
6779
+ const match = sessionName.match(regex);
6780
+ return match?.[1] ?? null;
6502
6781
  }
6503
- async function storeTrajectory(opts) {
6504
- const client = getClient();
6505
- const id = crypto7.randomUUID();
6506
- const now = (/* @__PURE__ */ new Date()).toISOString();
6507
- const signatureHash = hashSignature(opts.signature);
6508
- await client.execute({
6509
- sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
6510
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
6511
- args: [
6512
- id,
6513
- opts.taskId,
6514
- opts.agentId,
6515
- opts.projectName,
6516
- opts.taskTitle,
6517
- JSON.stringify(opts.signature),
6518
- signatureHash,
6519
- opts.signature.length,
6520
- now
6521
- ]
6522
- });
6523
- return id;
6782
+ function extractRootExe(name) {
6783
+ const match = name.match(/(exe\d+)$/);
6784
+ return match?.[1] ?? null;
6524
6785
  }
6525
- async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
6526
- const client = getClient();
6527
- const hash = hashSignature(signature);
6528
- const result = await client.execute({
6529
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
6530
- FROM trajectories
6531
- WHERE signature_hash = ?
6532
- ORDER BY created_at DESC
6533
- LIMIT 20`,
6534
- args: [hash]
6535
- });
6536
- const mapRow = (r) => ({
6537
- id: String(r.id),
6538
- taskId: String(r.task_id),
6539
- agentId: String(r.agent_id),
6540
- projectName: String(r.project_name),
6541
- taskTitle: String(r.task_title),
6542
- signature: JSON.parse(String(r.signature)),
6543
- signatureHash: String(r.signature_hash),
6544
- toolCount: Number(r.tool_count),
6545
- skillId: r.skill_id ? String(r.skill_id) : null,
6546
- createdAt: String(r.created_at)
6547
- });
6548
- const matches = result.rows.map(mapRow);
6549
- if (matches.length >= threshold) return matches;
6550
- const nearResult = await client.execute({
6551
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
6552
- FROM trajectories
6553
- WHERE tool_count BETWEEN ? AND ?
6554
- AND signature_hash != ?
6555
- ORDER BY created_at DESC
6556
- LIMIT 50`,
6557
- args: [
6558
- Math.max(1, signature.length - 3),
6559
- signature.length + 3,
6560
- hash
6561
- ]
6562
- });
6563
- for (const r of nearResult.rows) {
6564
- const candidateSig = JSON.parse(String(r.signature));
6565
- if (editDistance(signature, candidateSig) <= 2) {
6566
- matches.push(mapRow(r));
6567
- }
6786
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
6787
+ if (!existsSync13(SESSION_CACHE)) {
6788
+ mkdirSync7(SESSION_CACHE, { recursive: true });
6568
6789
  }
6569
- return matches;
6790
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
6791
+ const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
6792
+ writeFileSync5(filePath, JSON.stringify({
6793
+ parentExe: rootExe,
6794
+ dispatchedBy: dispatchedBy || rootExe,
6795
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
6796
+ }));
6570
6797
  }
6571
- async function captureTrajectory(opts) {
6572
- const signature = await extractTrajectory(opts.taskId, opts.agentId);
6573
- if (signature.length < 3) {
6574
- return { trajectoryId: "", similarCount: 0, similar: [] };
6798
+ function getParentExe(sessionKey) {
6799
+ try {
6800
+ const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
6801
+ return data.parentExe || null;
6802
+ } catch {
6803
+ return null;
6575
6804
  }
6576
- const trajectoryId = await storeTrajectory({
6577
- taskId: opts.taskId,
6578
- agentId: opts.agentId,
6579
- projectName: opts.projectName,
6580
- taskTitle: opts.taskTitle,
6581
- signature
6582
- });
6583
- const similar = await findSimilarTrajectories(
6584
- signature,
6585
- opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
6586
- );
6587
- return { trajectoryId, similarCount: similar.length, similar };
6588
6805
  }
6589
- function buildExtractionPrompt(trajectories) {
6590
- const items = trajectories.map((t, i) => {
6591
- const sig = t.signature.join(" \u2192 ");
6592
- return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
6593
- Signature: ${sig}`;
6594
- }).join("\n\n");
6595
- return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
6596
-
6597
- ${items}
6598
-
6599
- Extract the reusable procedure. Format your response EXACTLY like this:
6600
-
6601
- SKILL: {name \u2014 short, descriptive}
6602
- TRIGGER: {when to use this \u2014 one sentence}
6603
- STEPS:
6604
- 1. ...
6605
- 2. ...
6606
- PITFALLS: {common mistakes to avoid}
6607
-
6608
- Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
6806
+ function getDispatchedBy(sessionKey) {
6807
+ try {
6808
+ const data = JSON.parse(readFileSync11(
6809
+ path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
6810
+ "utf8"
6811
+ ));
6812
+ return data.dispatchedBy ?? data.parentExe ?? null;
6813
+ } catch {
6814
+ return null;
6815
+ }
6609
6816
  }
6610
- async function extractSkill(trajectories, model) {
6611
- if (trajectories.length === 0) return null;
6612
- const config2 = await loadConfig();
6613
- const skillModel = model ?? config2.skillModel;
6614
- const Anthropic2 = (await import("@anthropic-ai/sdk")).default;
6615
- const client = new Anthropic2();
6616
- const prompt = buildExtractionPrompt(trajectories);
6617
- const response = await client.messages.create({
6618
- model: skillModel,
6619
- max_tokens: 500,
6620
- messages: [{ role: "user", content: prompt }]
6621
- });
6622
- const textBlock = response.content.find((b) => b.type === "text");
6623
- const skillText = textBlock?.text;
6624
- if (!skillText) return null;
6625
- const agentId = trajectories[0].agentId;
6626
- const projectName = trajectories[0].projectName;
6627
- const skillId = await storeBehavior({
6628
- agentId,
6629
- content: skillText,
6630
- domain: "skill",
6631
- projectName
6632
- });
6633
- const dbClient = getClient();
6634
- for (const t of trajectories) {
6635
- await dbClient.execute({
6636
- sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
6637
- args: [skillId, t.id]
6638
- });
6817
+ function resolveExeSession() {
6818
+ const mySession = getMySession();
6819
+ if (!mySession) return null;
6820
+ try {
6821
+ const key = getSessionKey();
6822
+ const parentExe = getParentExe(key);
6823
+ if (parentExe) {
6824
+ return extractRootExe(parentExe) ?? parentExe;
6825
+ }
6826
+ } catch {
6639
6827
  }
6640
- process.stderr.write(
6641
- `[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
6828
+ return extractRootExe(mySession) ?? mySession;
6829
+ }
6830
+ function isEmployeeAlive(sessionName) {
6831
+ return getTransport().isAlive(sessionName);
6832
+ }
6833
+ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
6834
+ const base = employeeSessionName(employeeName, exeSession);
6835
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
6836
+ for (let i = 2; i <= maxInstances; i++) {
6837
+ const candidate = employeeSessionName(employeeName, exeSession, i);
6838
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
6839
+ }
6840
+ return null;
6841
+ }
6842
+ async function verifyPaneAtCapacity(sessionName) {
6843
+ const transport = getTransport();
6844
+ if (!transport.isAlive(sessionName)) {
6845
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
6846
+ }
6847
+ let pane;
6848
+ try {
6849
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
6850
+ } catch (err) {
6851
+ return {
6852
+ atCapacity: false,
6853
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
6854
+ };
6855
+ }
6856
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
6857
+ if (!isAtCapacity2(pane)) {
6858
+ return {
6859
+ atCapacity: false,
6860
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
6861
+ };
6862
+ }
6863
+ return {
6864
+ atCapacity: true,
6865
+ reason: "capacity banner matched in recent pane output"
6866
+ };
6867
+ }
6868
+ function readDebounceState() {
6869
+ try {
6870
+ if (!existsSync13(DEBOUNCE_FILE)) return {};
6871
+ return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
6872
+ } catch {
6873
+ return {};
6874
+ }
6875
+ }
6876
+ function writeDebounceState(state) {
6877
+ try {
6878
+ if (!existsSync13(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
6879
+ writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
6880
+ } catch {
6881
+ }
6882
+ }
6883
+ function isDebounced(targetSession) {
6884
+ const state = readDebounceState();
6885
+ const lastSent = state[targetSession] ?? 0;
6886
+ return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
6887
+ }
6888
+ function recordDebounce(targetSession) {
6889
+ const state = readDebounceState();
6890
+ state[targetSession] = Date.now();
6891
+ const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
6892
+ for (const key of Object.keys(state)) {
6893
+ if ((state[key] ?? 0) < cutoff) delete state[key];
6894
+ }
6895
+ writeDebounceState(state);
6896
+ }
6897
+ function logIntercom(msg) {
6898
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
6899
+ `;
6900
+ process.stderr.write(`[intercom] ${msg}
6901
+ `);
6902
+ try {
6903
+ appendFileSync(INTERCOM_LOG2, line);
6904
+ } catch {
6905
+ }
6906
+ }
6907
+ function getSessionState(sessionName) {
6908
+ const transport = getTransport();
6909
+ if (!transport.isAlive(sessionName)) return "offline";
6910
+ try {
6911
+ const pane = transport.capturePane(sessionName, 5);
6912
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
6913
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
6914
+ return "no_claude";
6915
+ }
6916
+ }
6917
+ if (/Running…/.test(pane)) return "tool";
6918
+ if (BUSY_PATTERN.test(pane)) return "thinking";
6919
+ return "idle";
6920
+ } catch {
6921
+ return "offline";
6922
+ }
6923
+ }
6924
+ function isSessionBusy(sessionName) {
6925
+ const state = getSessionState(sessionName);
6926
+ return state === "thinking" || state === "tool";
6927
+ }
6928
+ function isExeSession(sessionName) {
6929
+ return /^exe\d*$/.test(sessionName);
6930
+ }
6931
+ function sendIntercom(targetSession) {
6932
+ const transport = getTransport();
6933
+ if (isExeSession(targetSession)) {
6934
+ logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
6935
+ return "skipped_exe";
6936
+ }
6937
+ if (isDebounced(targetSession)) {
6938
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
6939
+ return "debounced";
6940
+ }
6941
+ try {
6942
+ const sessions = transport.listSessions();
6943
+ if (!sessions.includes(targetSession)) {
6944
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
6945
+ return "failed";
6946
+ }
6947
+ const sessionState = getSessionState(targetSession);
6948
+ if (sessionState === "no_claude") {
6949
+ queueIntercom(targetSession, "claude not running in session");
6950
+ recordDebounce(targetSession);
6951
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
6952
+ return "queued";
6953
+ }
6954
+ if (sessionState === "thinking" || sessionState === "tool") {
6955
+ queueIntercom(targetSession, "session busy at send time");
6956
+ recordDebounce(targetSession);
6957
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
6958
+ return "queued";
6959
+ }
6960
+ if (transport.isPaneInCopyMode(targetSession)) {
6961
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
6962
+ transport.sendKeys(targetSession, "q");
6963
+ }
6964
+ transport.sendKeys(targetSession, "/exe-intercom");
6965
+ recordDebounce(targetSession);
6966
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
6967
+ return "delivered";
6968
+ } catch {
6969
+ logIntercom(`FAIL \u2192 ${targetSession}`);
6970
+ return "failed";
6971
+ }
6972
+ }
6973
+ function notifyParentExe(sessionKey) {
6974
+ const target = getDispatchedBy(sessionKey);
6975
+ if (!target) {
6976
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
6977
+ `);
6978
+ return false;
6979
+ }
6980
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
6981
+ `);
6982
+ const result = sendIntercom(target);
6983
+ if (result === "failed") {
6984
+ const rootExe = resolveExeSession();
6985
+ if (rootExe && rootExe !== target) {
6986
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
6987
+ `);
6988
+ const fallback = sendIntercom(rootExe);
6989
+ return fallback !== "failed";
6990
+ }
6991
+ return false;
6992
+ }
6993
+ return true;
6994
+ }
6995
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
6996
+ if (employeeName === "exe") {
6997
+ return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
6998
+ }
6999
+ try {
7000
+ assertEmployeeLimitSync();
7001
+ } catch (err) {
7002
+ if (err instanceof PlanLimitError) {
7003
+ return { status: "failed", sessionName: "", error: err.message };
7004
+ }
7005
+ }
7006
+ if (/-exe\d*$/.test(employeeName)) {
7007
+ const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
7008
+ return {
7009
+ status: "failed",
7010
+ sessionName: "",
7011
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
7012
+ };
7013
+ }
7014
+ if (!/^exe\d+$/.test(exeSession)) {
7015
+ const root = extractRootExe(exeSession);
7016
+ if (root) {
7017
+ process.stderr.write(
7018
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
6642
7019
  `
6643
- );
6644
- return skillId;
7020
+ );
7021
+ exeSession = root;
7022
+ } else {
7023
+ return {
7024
+ status: "failed",
7025
+ sessionName: "",
7026
+ error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
7027
+ };
7028
+ }
7029
+ }
7030
+ let effectiveInstance = opts?.instance;
7031
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
7032
+ const free = findFreeInstance(
7033
+ employeeName,
7034
+ exeSession,
7035
+ opts.maxAutoInstances ?? 10
7036
+ );
7037
+ if (free === null) {
7038
+ return {
7039
+ status: "failed",
7040
+ sessionName: employeeSessionName(employeeName, exeSession),
7041
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
7042
+ };
7043
+ }
7044
+ effectiveInstance = free === 0 ? void 0 : free;
7045
+ }
7046
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
7047
+ if (isEmployeeAlive(sessionName)) {
7048
+ const result2 = sendIntercom(sessionName);
7049
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
7050
+ return { status: "intercom_sent", sessionName };
7051
+ }
7052
+ if (result2 === "delivered") {
7053
+ return { status: "intercom_unprocessed", sessionName };
7054
+ }
7055
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
7056
+ }
7057
+ const spawnOpts = { ...opts, instance: effectiveInstance };
7058
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
7059
+ if (result.error) {
7060
+ return { status: "failed", sessionName, error: result.error };
7061
+ }
7062
+ return { status: "spawned", sessionName };
6645
7063
  }
6646
- async function captureAndLearn(opts) {
7064
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7065
+ const transport = getTransport();
7066
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
7067
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
7068
+ const logDir = path17.join(os7.homedir(), ".exe-os", "session-logs");
7069
+ const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
7070
+ if (!existsSync13(logDir)) {
7071
+ mkdirSync7(logDir, { recursive: true });
7072
+ }
7073
+ transport.kill(sessionName);
7074
+ let cleanupSuffix = "";
6647
7075
  try {
6648
- const config2 = await loadConfig();
6649
- if (!config2.skillLearning) return;
6650
- const { trajectoryId, similarCount, similar } = await captureTrajectory({
6651
- ...opts,
6652
- skillThreshold: config2.skillThreshold
6653
- });
6654
- if (!trajectoryId) return;
6655
- if (similarCount >= config2.skillThreshold) {
6656
- const unprocessed = similar.filter((t) => !t.skillId);
6657
- if (unprocessed.length >= config2.skillThreshold) {
6658
- extractSkill(unprocessed, config2.skillModel).catch((err) => {
6659
- process.stderr.write(
6660
- `[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
7076
+ const thisFile = fileURLToPath2(import.meta.url);
7077
+ const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
7078
+ if (existsSync13(cleanupScript)) {
7079
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
7080
+ }
7081
+ } catch {
7082
+ }
7083
+ try {
7084
+ const claudeJsonPath = path17.join(os7.homedir(), ".claude.json");
7085
+ let claudeJson = {};
7086
+ try {
7087
+ claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
7088
+ } catch {
7089
+ }
7090
+ if (!claudeJson.projects) claudeJson.projects = {};
7091
+ const projects = claudeJson.projects;
7092
+ const trustDir = opts?.cwd ?? projectDir;
7093
+ if (!projects[trustDir]) projects[trustDir] = {};
7094
+ projects[trustDir].hasTrustDialogAccepted = true;
7095
+ writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
7096
+ } catch {
7097
+ }
7098
+ try {
7099
+ const settingsDir = path17.join(os7.homedir(), ".claude", "projects");
7100
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
7101
+ const projSettingsDir = path17.join(settingsDir, normalizedKey);
7102
+ const settingsPath = path17.join(projSettingsDir, "settings.json");
7103
+ let settings = {};
7104
+ try {
7105
+ settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
7106
+ } catch {
7107
+ }
7108
+ const perms = settings.permissions ?? {};
7109
+ const allow = perms.allow ?? [];
7110
+ const toolNames = [
7111
+ "recall_my_memory",
7112
+ "store_memory",
7113
+ "create_task",
7114
+ "update_task",
7115
+ "list_tasks",
7116
+ "get_task",
7117
+ "ask_team_memory",
7118
+ "store_behavior",
7119
+ "get_identity",
7120
+ "send_message"
7121
+ ];
7122
+ const requiredTools = expandDualPrefixTools(toolNames);
7123
+ let changed = false;
7124
+ for (const tool of requiredTools) {
7125
+ if (!allow.includes(tool)) {
7126
+ allow.push(tool);
7127
+ changed = true;
7128
+ }
7129
+ }
7130
+ if (changed) {
7131
+ perms.allow = allow;
7132
+ settings.permissions = perms;
7133
+ mkdirSync7(projSettingsDir, { recursive: true });
7134
+ writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
7135
+ }
7136
+ } catch {
7137
+ }
7138
+ const spawnCwd = opts?.cwd ?? projectDir;
7139
+ const useExeAgent = !!(opts?.model && opts?.provider);
7140
+ const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
7141
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
7142
+ let identityFlag = "";
7143
+ let behaviorsFlag = "";
7144
+ let legacyFallbackWarned = false;
7145
+ if (!useExeAgent && !useBinSymlink) {
7146
+ const identityPath = path17.join(
7147
+ os7.homedir(),
7148
+ ".exe-os",
7149
+ "identity",
7150
+ `${employeeName}.md`
7151
+ );
7152
+ _resetCcAgentSupportCache();
7153
+ const hasAgentFlag = claudeSupportsAgentFlag();
7154
+ if (hasAgentFlag) {
7155
+ identityFlag = ` --agent ${employeeName}`;
7156
+ } else if (existsSync13(identityPath)) {
7157
+ identityFlag = ` --append-system-prompt-file ${identityPath}`;
7158
+ legacyFallbackWarned = true;
7159
+ }
7160
+ const behaviorsFile = exportBehaviorsSync(
7161
+ employeeName,
7162
+ path17.basename(spawnCwd),
7163
+ sessionName
7164
+ );
7165
+ if (behaviorsFile) {
7166
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
7167
+ }
7168
+ }
7169
+ if (legacyFallbackWarned) {
7170
+ process.stderr.write(
7171
+ `[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.
6661
7172
  `
6662
- );
6663
- });
7173
+ );
7174
+ }
7175
+ let sessionContextFlag = "";
7176
+ try {
7177
+ const ctxDir = path17.join(os7.homedir(), ".exe-os", "session-cache");
7178
+ mkdirSync7(ctxDir, { recursive: true });
7179
+ const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
7180
+ const ctxContent = [
7181
+ `## Session Context`,
7182
+ `You are running in tmux session: ${sessionName}.`,
7183
+ `Your parent exe session is ${exeSession}.`,
7184
+ `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
7185
+ ].join("\n");
7186
+ writeFileSync5(ctxFile, ctxContent);
7187
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7188
+ } catch {
7189
+ }
7190
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
7191
+ if (ccProvider !== DEFAULT_PROVIDER) {
7192
+ const cfg = PROVIDER_TABLE[ccProvider];
7193
+ if (cfg?.apiKeyEnv) {
7194
+ const keyVal = process.env[cfg.apiKeyEnv];
7195
+ if (keyVal) {
7196
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
6664
7197
  }
6665
7198
  }
6666
- } catch (err) {
7199
+ }
7200
+ let spawnCommand;
7201
+ if (useExeAgent) {
7202
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
7203
+ } else if (useBinSymlink) {
7204
+ const binName = `${employeeName}-${ccProvider}`;
6667
7205
  process.stderr.write(
6668
- `[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
7206
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
6669
7207
  `
6670
7208
  );
7209
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
7210
+ } else {
7211
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
6671
7212
  }
6672
- }
6673
- async function sweepTrajectories(threshold, model) {
6674
- const config2 = await loadConfig();
6675
- if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
6676
- const t = threshold ?? config2.skillThreshold;
6677
- const client = getClient();
6678
- const result = await client.execute({
6679
- sql: `SELECT signature_hash, COUNT(*) as cnt
6680
- FROM trajectories
6681
- WHERE skill_id IS NULL
6682
- GROUP BY signature_hash
6683
- HAVING cnt >= ?
6684
- ORDER BY cnt DESC
6685
- LIMIT 10`,
6686
- args: [t]
7213
+ const spawnResult = transport.spawn(sessionName, {
7214
+ cwd: spawnCwd,
7215
+ command: spawnCommand
6687
7216
  });
6688
- let clustersProcessed = 0;
6689
- let skillsExtracted = 0;
6690
- for (const row of result.rows) {
6691
- const hash = String(row.signature_hash);
6692
- const trajResult = await client.execute({
6693
- sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
6694
- FROM trajectories
6695
- WHERE signature_hash = ? AND skill_id IS NULL
6696
- ORDER BY created_at DESC
6697
- LIMIT 10`,
6698
- args: [hash]
6699
- });
6700
- const trajectories = trajResult.rows.map((r) => ({
6701
- id: String(r.id),
6702
- taskId: String(r.task_id),
6703
- agentId: String(r.agent_id),
6704
- projectName: String(r.project_name),
6705
- taskTitle: String(r.task_title),
6706
- signature: JSON.parse(String(r.signature)),
6707
- signatureHash: String(r.signature_hash),
6708
- toolCount: Number(r.tool_count),
6709
- skillId: null,
6710
- createdAt: String(r.created_at)
7217
+ if (spawnResult.error) {
7218
+ releaseSpawnLock2(sessionName);
7219
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
7220
+ }
7221
+ transport.pipeLog(sessionName, logFile);
7222
+ try {
7223
+ const mySession = getMySession();
7224
+ const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
7225
+ writeFileSync5(dispatchInfo, JSON.stringify({
7226
+ dispatchedBy: mySession,
7227
+ rootExe: exeSession,
7228
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
7229
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6711
7230
  }));
6712
- if (trajectories.length >= t) {
6713
- clustersProcessed++;
6714
- const skillId = await extractSkill(trajectories, model ?? config2.skillModel);
6715
- if (skillId) skillsExtracted++;
7231
+ } catch {
7232
+ }
7233
+ let booted = false;
7234
+ for (let i = 0; i < 30; i++) {
7235
+ try {
7236
+ execSync6("sleep 0.5");
7237
+ } catch {
7238
+ }
7239
+ try {
7240
+ const pane = transport.capturePane(sessionName);
7241
+ if (useExeAgent) {
7242
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
7243
+ booted = true;
7244
+ break;
7245
+ }
7246
+ } else {
7247
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
7248
+ booted = true;
7249
+ break;
7250
+ }
7251
+ }
7252
+ } catch {
6716
7253
  }
6717
7254
  }
6718
- return { clustersProcessed, skillsExtracted };
6719
- }
6720
- function editDistance(a, b) {
6721
- const m = a.length;
6722
- const n = b.length;
6723
- const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
6724
- for (let i = 0; i <= m; i++) dp[i][0] = i;
6725
- for (let j = 0; j <= n; j++) dp[0][j] = j;
6726
- for (let i = 1; i <= m; i++) {
6727
- for (let j = 1; j <= n; j++) {
6728
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
6729
- dp[i][j] = Math.min(
6730
- dp[i - 1][j] + 1,
6731
- dp[i][j - 1] + 1,
6732
- dp[i - 1][j - 1] + cost
6733
- );
7255
+ if (!booted) {
7256
+ releaseSpawnLock2(sessionName);
7257
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
7258
+ }
7259
+ if (!useExeAgent) {
7260
+ try {
7261
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
7262
+ } catch {
6734
7263
  }
6735
7264
  }
6736
- return dp[m][n];
7265
+ registerSession({
7266
+ windowName: sessionName,
7267
+ agentId: employeeName,
7268
+ projectDir: spawnCwd,
7269
+ parentExe: exeSession,
7270
+ pid: 0,
7271
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
7272
+ });
7273
+ releaseSpawnLock2(sessionName);
7274
+ return { sessionName };
6737
7275
  }
6738
- var DEFAULT_SKILL_THRESHOLD;
6739
- var init_skill_learning = __esm({
6740
- "src/lib/skill-learning.ts"() {
7276
+ 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;
7277
+ var init_tmux_routing = __esm({
7278
+ "src/lib/tmux-routing.ts"() {
6741
7279
  "use strict";
6742
- init_database();
6743
- init_behaviors();
6744
- init_config();
6745
- DEFAULT_SKILL_THRESHOLD = 3;
7280
+ init_session_registry();
7281
+ init_session_key();
7282
+ init_transport();
7283
+ init_cc_agent_support();
7284
+ init_mcp_prefix();
7285
+ init_provider_table();
7286
+ init_intercom_queue();
7287
+ init_plan_limits();
7288
+ SPAWN_LOCK_DIR = path17.join(os7.homedir(), ".exe-os", "spawn-locks");
7289
+ SESSION_CACHE = path17.join(os7.homedir(), ".exe-os", "session-cache");
7290
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7291
+ VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
7292
+ VERIFY_PANE_LINES = 200;
7293
+ INTERCOM_DEBOUNCE_MS = 3e4;
7294
+ INTERCOM_LOG2 = path17.join(os7.homedir(), ".exe-os", "intercom.log");
7295
+ DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
7296
+ DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
7297
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
6746
7298
  }
6747
7299
  });
6748
7300
 
6749
- // src/lib/tasks.ts
6750
- var tasks_exports = {};
6751
- __export(tasks_exports, {
6752
- countNewPendingReviewsSince: () => countNewPendingReviewsSince,
6753
- countPendingReviews: () => countPendingReviews,
6754
- createTask: () => createTask,
6755
- createTaskCore: () => createTaskCore,
6756
- deleteTask: () => deleteTask,
6757
- deleteTaskCore: () => deleteTaskCore,
6758
- ensureArchitectureDoc: () => ensureArchitectureDoc,
6759
- ensureGitignoreExe: () => ensureGitignoreExe,
6760
- getReviewChecklist: () => getReviewChecklist,
6761
- listPendingReviews: () => listPendingReviews,
6762
- listTasks: () => listTasks,
6763
- resolveTask: () => resolveTask,
6764
- slugify: () => slugify,
6765
- updateTask: () => updateTask,
6766
- updateTaskStatus: () => updateTaskStatus,
6767
- writeCheckpoint: () => writeCheckpoint
7301
+ // src/lib/messaging.ts
7302
+ var messaging_exports = {};
7303
+ __export(messaging_exports, {
7304
+ deliverLocalMessage: () => deliverLocalMessage,
7305
+ getFailedMessages: () => getFailedMessages,
7306
+ getMessageStatus: () => getMessageStatus,
7307
+ getPendingMessages: () => getPendingMessages,
7308
+ getReadMessages: () => getReadMessages,
7309
+ getUnacknowledgedMessages: () => getUnacknowledgedMessages,
7310
+ markAcknowledged: () => markAcknowledged,
7311
+ markFailed: () => markFailed,
7312
+ markProcessed: () => markProcessed,
7313
+ markRead: () => markRead,
7314
+ retryPendingMessages: () => retryPendingMessages,
7315
+ sendMessage: () => sendMessage,
7316
+ setWsClientSend: () => setWsClientSend
6768
7317
  });
6769
- import path17 from "path";
6770
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
6771
- async function createTask(input) {
6772
- const result = await createTaskCore(input);
6773
- if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
6774
- dispatchTaskToEmployee({
6775
- assignedTo: input.assignedTo,
6776
- title: input.title,
6777
- priority: input.priority,
6778
- taskFile: result.taskFile,
6779
- initialStatus: result.status,
6780
- projectName: input.projectName
6781
- });
6782
- }
6783
- return result;
7318
+ import crypto8 from "crypto";
7319
+ function generateUlid() {
7320
+ const timestamp = Date.now().toString(36).padStart(10, "0");
7321
+ const random = crypto8.randomBytes(10).toString("hex").slice(0, 16);
7322
+ return (timestamp + random).toUpperCase();
6784
7323
  }
6785
- async function updateTask(input) {
6786
- const { row, taskFile, now, taskId } = await updateTaskStatus(input);
7324
+ function rowToMessage(row) {
7325
+ return {
7326
+ id: row.id,
7327
+ fromAgent: row.from_agent,
7328
+ fromDevice: row.from_device,
7329
+ targetAgent: row.target_agent,
7330
+ targetProject: row.target_project ?? null,
7331
+ targetDevice: row.target_device,
7332
+ content: row.content,
7333
+ priority: row.priority ?? "normal",
7334
+ status: row.status ?? "pending",
7335
+ serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
7336
+ retryCount: Number(row.retry_count ?? 0),
7337
+ createdAt: row.created_at,
7338
+ deliveredAt: row.delivered_at ?? null,
7339
+ processedAt: row.processed_at ?? null,
7340
+ failedAt: row.failed_at ?? null,
7341
+ failureReason: row.failure_reason ?? null
7342
+ };
7343
+ }
7344
+ async function sendMessage(input) {
7345
+ const client = getClient();
7346
+ const id = generateUlid();
7347
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7348
+ const targetDevice = input.targetDevice ?? "local";
7349
+ await client.execute({
7350
+ sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
7351
+ VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
7352
+ args: [
7353
+ id,
7354
+ input.fromAgent,
7355
+ input.targetAgent,
7356
+ input.targetProject ?? null,
7357
+ targetDevice,
7358
+ input.content,
7359
+ input.priority ?? "normal",
7360
+ now
7361
+ ]
7362
+ });
6787
7363
  try {
6788
- const agent = String(row.assigned_to);
6789
- const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
6790
- const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
6791
- if (input.status === "in_progress") {
6792
- mkdirSync7(cacheDir, { recursive: true });
6793
- writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
6794
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
6795
- try {
6796
- unlinkSync5(cachePath);
6797
- } catch {
6798
- }
7364
+ if (targetDevice !== "local") {
7365
+ await deliverCrossMachineMessage(id, targetDevice);
7366
+ } else {
7367
+ await deliverLocalMessage(id);
6799
7368
  }
6800
7369
  } catch {
6801
7370
  }
6802
- if (input.status === "done") {
6803
- await cleanupReviewFile(row, taskFile, input.baseDir);
7371
+ const result = await client.execute({
7372
+ sql: "SELECT * FROM messages WHERE id = ?",
7373
+ args: [id]
7374
+ });
7375
+ return rowToMessage(result.rows[0]);
7376
+ }
7377
+ function setWsClientSend(fn) {
7378
+ _wsClientSend = fn;
7379
+ }
7380
+ async function deliverCrossMachineMessage(messageId, targetDevice) {
7381
+ const client = getClient();
7382
+ const result = await client.execute({
7383
+ sql: "SELECT * FROM messages WHERE id = ?",
7384
+ args: [messageId]
7385
+ });
7386
+ if (result.rows.length === 0) return false;
7387
+ const msg = rowToMessage(result.rows[0]);
7388
+ if (msg.status !== "pending") return false;
7389
+ if (!_wsClientSend) {
7390
+ return false;
6804
7391
  }
6805
- if (input.status === "done" || input.status === "cancelled") {
6806
- try {
6807
- const client = getClient();
6808
- const taskTitle = String(row.title);
6809
- const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
6810
- await client.execute({
6811
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
6812
- WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
6813
- args: [now, `%left '${escaped}' as in\\_progress%`]
6814
- });
6815
- } catch {
6816
- }
7392
+ const payload = JSON.stringify({
7393
+ id: msg.id,
7394
+ fromAgent: msg.fromAgent,
7395
+ targetAgent: msg.targetAgent,
7396
+ targetProject: msg.targetProject,
7397
+ content: msg.content,
7398
+ priority: msg.priority,
7399
+ createdAt: msg.createdAt
7400
+ });
7401
+ const sent = _wsClientSend(targetDevice, payload);
7402
+ if (sent) {
7403
+ await client.execute({
7404
+ sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
7405
+ args: [messageId]
7406
+ });
7407
+ return true;
6817
7408
  }
6818
- const isTerminal = input.status === "done" || input.status === "needs_review";
6819
- if (isTerminal) {
6820
- const isExe = String(row.assigned_to) === "exe";
6821
- if (!isExe) {
6822
- notifyTaskDone();
7409
+ return false;
7410
+ }
7411
+ async function deliverLocalMessage(messageId) {
7412
+ const client = getClient();
7413
+ const result = await client.execute({
7414
+ sql: "SELECT * FROM messages WHERE id = ?",
7415
+ args: [messageId]
7416
+ });
7417
+ if (result.rows.length === 0) return false;
7418
+ const msg = rowToMessage(result.rows[0]);
7419
+ if (msg.status !== "pending") return false;
7420
+ const targetAgent = msg.targetAgent;
7421
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7422
+ try {
7423
+ const exeSession = resolveExeSession();
7424
+ if (!exeSession) {
7425
+ throw new Error("No exe session found");
6823
7426
  }
6824
- await markTaskNotificationsRead(taskFile);
6825
- if (input.status === "done") {
6826
- try {
6827
- await cascadeUnblock(taskId, input.baseDir, now);
6828
- } catch {
6829
- }
6830
- if (row.parent_task_id) {
6831
- try {
6832
- await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
6833
- } catch {
6834
- }
6835
- }
7427
+ const ensureResult = ensureEmployee(targetAgent, exeSession, process.cwd());
7428
+ if (ensureResult.status === "failed") {
7429
+ throw new Error(ensureResult.error ?? "ensureEmployee failed");
6836
7430
  }
6837
- }
6838
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
6839
- Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
6840
- ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
6841
- taskId,
6842
- agentId: String(row.assigned_to),
6843
- projectName: String(row.project_name),
6844
- taskTitle: String(row.title)
6845
- })
6846
- ).catch((err) => {
6847
- process.stderr.write(
6848
- `[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
6849
- `
6850
- );
7431
+ await client.execute({
7432
+ sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
7433
+ args: [now, messageId]
6851
7434
  });
6852
- }
6853
- let nextTask;
6854
- if (isTerminal && String(row.assigned_to) !== "exe") {
6855
- try {
6856
- nextTask = await findNextTask(String(row.assigned_to));
6857
- } catch {
7435
+ return true;
7436
+ } catch {
7437
+ const newRetryCount = msg.retryCount + 1;
7438
+ if (newRetryCount >= MAX_RETRIES2) {
7439
+ await markFailed(messageId, "session unavailable after 10 retries");
7440
+ } else {
7441
+ await client.execute({
7442
+ sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
7443
+ args: [newRetryCount, messageId]
7444
+ });
6858
7445
  }
7446
+ return false;
6859
7447
  }
6860
- return {
6861
- id: String(row.id),
6862
- title: String(row.title),
6863
- assignedTo: String(row.assigned_to),
6864
- assignedBy: String(row.assigned_by),
6865
- projectName: String(row.project_name),
6866
- priority: String(row.priority),
6867
- status: input.status,
6868
- taskFile,
6869
- createdAt: String(row.created_at),
6870
- updatedAt: now,
6871
- budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
6872
- budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
6873
- tokensUsed: Number(row.tokens_used ?? 0),
6874
- tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
6875
- nextTask
6876
- };
6877
7448
  }
6878
- async function deleteTask(taskId, baseDir) {
7449
+ async function getPendingMessages(targetAgent) {
7450
+ const client = getClient();
7451
+ const result = await client.execute({
7452
+ sql: `SELECT * FROM messages
7453
+ WHERE target_agent = ? AND status IN ('pending', 'delivered')
7454
+ ORDER BY id`,
7455
+ args: [targetAgent]
7456
+ });
7457
+ return result.rows.map((row) => rowToMessage(row));
7458
+ }
7459
+ async function markRead(messageId) {
6879
7460
  const client = getClient();
6880
- const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
6881
- const reviewer = assignedBy || "exe";
6882
- const reviewSlug = `review-${assignedTo}-${taskSlug}`;
6883
- const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
6884
7461
  await client.execute({
6885
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
6886
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
7462
+ sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
7463
+ args: [messageId]
6887
7464
  });
6888
- await markAsReadByTaskFile(taskFile);
6889
- await markAsReadByTaskFile(reviewFile);
6890
7465
  }
6891
- var init_tasks = __esm({
6892
- "src/lib/tasks.ts"() {
7466
+ async function markAcknowledged(messageId) {
7467
+ const client = getClient();
7468
+ await client.execute({
7469
+ sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
7470
+ args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
7471
+ });
7472
+ }
7473
+ async function markProcessed(messageId) {
7474
+ const client = getClient();
7475
+ await client.execute({
7476
+ sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
7477
+ args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
7478
+ });
7479
+ }
7480
+ async function getMessageStatus(messageId) {
7481
+ const client = getClient();
7482
+ const result = await client.execute({
7483
+ sql: "SELECT status FROM messages WHERE id = ?",
7484
+ args: [messageId]
7485
+ });
7486
+ return result.rows[0]?.status ?? null;
7487
+ }
7488
+ async function getUnacknowledgedMessages(targetAgent) {
7489
+ const client = getClient();
7490
+ const result = await client.execute({
7491
+ sql: `SELECT * FROM messages
7492
+ WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
7493
+ ORDER BY id`,
7494
+ args: [targetAgent]
7495
+ });
7496
+ return result.rows.map((row) => rowToMessage(row));
7497
+ }
7498
+ async function getReadMessages(targetAgent) {
7499
+ const client = getClient();
7500
+ const result = await client.execute({
7501
+ sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
7502
+ args: [targetAgent]
7503
+ });
7504
+ return result.rows.map((row) => rowToMessage(row));
7505
+ }
7506
+ async function markFailed(messageId, reason) {
7507
+ const client = getClient();
7508
+ await client.execute({
7509
+ sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
7510
+ args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
7511
+ });
7512
+ }
7513
+ async function getFailedMessages() {
7514
+ const client = getClient();
7515
+ const result = await client.execute({
7516
+ sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
7517
+ args: []
7518
+ });
7519
+ return result.rows.map((row) => rowToMessage(row));
7520
+ }
7521
+ async function retryPendingMessages() {
7522
+ const client = getClient();
7523
+ const result = await client.execute({
7524
+ sql: `SELECT * FROM messages
7525
+ WHERE status = 'pending' AND retry_count < ?
7526
+ ORDER BY id`,
7527
+ args: [MAX_RETRIES2]
7528
+ });
7529
+ let delivered = 0;
7530
+ for (const row of result.rows) {
7531
+ const msg = rowToMessage(row);
7532
+ try {
7533
+ const success = await deliverLocalMessage(msg.id);
7534
+ if (success) delivered++;
7535
+ } catch {
7536
+ }
7537
+ }
7538
+ return delivered;
7539
+ }
7540
+ var MAX_RETRIES2, _wsClientSend;
7541
+ var init_messaging = __esm({
7542
+ "src/lib/messaging.ts"() {
6893
7543
  "use strict";
6894
7544
  init_database();
6895
- init_config();
6896
- init_notifications();
6897
- init_tasks_crud();
6898
- init_tasks_review();
6899
- init_tasks_crud();
6900
- init_tasks_chain();
6901
- init_tasks_review();
6902
- init_tasks_notify();
7545
+ init_tmux_routing();
7546
+ MAX_RETRIES2 = 10;
7547
+ _wsClientSend = null;
6903
7548
  }
6904
7549
  });
6905
7550
 
6906
7551
  // src/automation/trigger-engine.ts
6907
7552
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
6908
- import { randomUUID as randomUUID7 } from "crypto";
7553
+ import { randomUUID as randomUUID8 } from "crypto";
6909
7554
  import path18 from "path";
6910
7555
  import os8 from "os";
6911
7556
  function substituteTemplate(template, record) {
@@ -7450,6 +8095,9 @@ function sendJson(res, status, data) {
7450
8095
  res.end(JSON.stringify(data));
7451
8096
  }
7452
8097
 
8098
+ // src/gateway/gateway.ts
8099
+ init_state_bus();
8100
+
7453
8101
  // src/gateway/router.ts
7454
8102
  function matchesPlatform(msgPlatform, matchPlatform) {
7455
8103
  if (!matchPlatform) return true;
@@ -7748,6 +8396,13 @@ var Gateway = class {
7748
8396
  console.log(
7749
8397
  `[gateway] ${msg.platform}/${msg.senderId} \u2192 ${route.employee} (${route.routeName})`
7750
8398
  );
8399
+ orgBus.emit({
8400
+ type: "gateway_message",
8401
+ platform: msg.platform,
8402
+ senderId: msg.senderId,
8403
+ botId: route.employee,
8404
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8405
+ });
7751
8406
  const bot = this.botRegistry.get(route.employee);
7752
8407
  if (!bot) {
7753
8408
  console.error(`[gateway] No bot registered for target: ${route.employee}`);