@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
@@ -39,6 +39,7 @@ var config_exports = {};
39
39
  __export(config_exports, {
40
40
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
41
41
  CONFIG_PATH: () => CONFIG_PATH,
42
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
42
43
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
43
44
  DB_PATH: () => DB_PATH,
44
45
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -194,7 +195,7 @@ async function loadConfigFrom(configPath) {
194
195
  return { ...DEFAULT_CONFIG };
195
196
  }
196
197
  }
197
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
198
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
198
199
  var init_config = __esm({
199
200
  "src/lib/config.ts"() {
200
201
  "use strict";
@@ -202,6 +203,7 @@ var init_config = __esm({
202
203
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
203
204
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
204
205
  CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
206
+ COO_AGENT_NAME = "exe";
205
207
  LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
206
208
  CURRENT_CONFIG_VERSION = 1;
207
209
  DEFAULT_CONFIG = {
@@ -985,6 +987,13 @@ async function ensureSchema() {
985
987
  });
986
988
  } catch {
987
989
  }
990
+ try {
991
+ await client.execute({
992
+ sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
993
+ args: []
994
+ });
995
+ } catch {
996
+ }
988
997
  try {
989
998
  await client.execute({
990
999
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -1431,6 +1440,18 @@ async function ensureSchema() {
1431
1440
  CREATE INDEX IF NOT EXISTS idx_session_kills_agent
1432
1441
  ON session_kills(agent_id);
1433
1442
  `);
1443
+ await client.execute(`
1444
+ CREATE TABLE IF NOT EXISTS global_procedures (
1445
+ id TEXT PRIMARY KEY,
1446
+ title TEXT NOT NULL,
1447
+ content TEXT NOT NULL,
1448
+ priority TEXT NOT NULL DEFAULT 'p0',
1449
+ domain TEXT,
1450
+ active INTEGER NOT NULL DEFAULT 1,
1451
+ created_at TEXT NOT NULL,
1452
+ updated_at TEXT NOT NULL
1453
+ )
1454
+ `);
1434
1455
  await client.executeMultiple(`
1435
1456
  CREATE TABLE IF NOT EXISTS conversations (
1436
1457
  id TEXT PRIMARY KEY,
@@ -1638,6 +1659,61 @@ var init_keychain = __esm({
1638
1659
  }
1639
1660
  });
1640
1661
 
1662
+ // src/lib/state-bus.ts
1663
+ var StateBus, orgBus;
1664
+ var init_state_bus = __esm({
1665
+ "src/lib/state-bus.ts"() {
1666
+ "use strict";
1667
+ StateBus = class {
1668
+ handlers = /* @__PURE__ */ new Map();
1669
+ globalHandlers = /* @__PURE__ */ new Set();
1670
+ /** Emit an event to all subscribers */
1671
+ emit(event) {
1672
+ const typeHandlers = this.handlers.get(event.type);
1673
+ if (typeHandlers) {
1674
+ for (const handler of typeHandlers) {
1675
+ try {
1676
+ handler(event);
1677
+ } catch {
1678
+ }
1679
+ }
1680
+ }
1681
+ for (const handler of this.globalHandlers) {
1682
+ try {
1683
+ handler(event);
1684
+ } catch {
1685
+ }
1686
+ }
1687
+ }
1688
+ /** Subscribe to a specific event type */
1689
+ on(type, handler) {
1690
+ if (!this.handlers.has(type)) {
1691
+ this.handlers.set(type, /* @__PURE__ */ new Set());
1692
+ }
1693
+ this.handlers.get(type).add(handler);
1694
+ }
1695
+ /** Subscribe to ALL events */
1696
+ onAny(handler) {
1697
+ this.globalHandlers.add(handler);
1698
+ }
1699
+ /** Unsubscribe from a specific event type */
1700
+ off(type, handler) {
1701
+ this.handlers.get(type)?.delete(handler);
1702
+ }
1703
+ /** Unsubscribe from ALL events */
1704
+ offAny(handler) {
1705
+ this.globalHandlers.delete(handler);
1706
+ }
1707
+ /** Remove all listeners */
1708
+ clear() {
1709
+ this.handlers.clear();
1710
+ this.globalHandlers.clear();
1711
+ }
1712
+ };
1713
+ orgBus = new StateBus();
1714
+ }
1715
+ });
1716
+
1641
1717
  // src/lib/shard-manager.ts
1642
1718
  var shard_manager_exports = {};
1643
1719
  __export(shard_manager_exports, {
@@ -1879,6 +1955,71 @@ var init_shard_manager = __esm({
1879
1955
  }
1880
1956
  });
1881
1957
 
1958
+ // src/lib/global-procedures.ts
1959
+ var global_procedures_exports = {};
1960
+ __export(global_procedures_exports, {
1961
+ deactivateGlobalProcedure: () => deactivateGlobalProcedure,
1962
+ getGlobalProceduresBlock: () => getGlobalProceduresBlock,
1963
+ loadGlobalProcedures: () => loadGlobalProcedures,
1964
+ storeGlobalProcedure: () => storeGlobalProcedure
1965
+ });
1966
+ import { randomUUID as randomUUID2 } from "crypto";
1967
+ async function loadGlobalProcedures() {
1968
+ const client = getClient();
1969
+ const result = await client.execute({
1970
+ sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
1971
+ args: []
1972
+ });
1973
+ const procedures = result.rows;
1974
+ if (procedures.length > 0) {
1975
+ _cache = procedures.map((p) => `### ${p.title}
1976
+ ${p.content}`).join("\n\n");
1977
+ } else {
1978
+ _cache = "";
1979
+ }
1980
+ _cacheLoaded = true;
1981
+ return procedures;
1982
+ }
1983
+ function getGlobalProceduresBlock() {
1984
+ if (!_cacheLoaded) return "";
1985
+ if (!_cache) return "";
1986
+ return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
1987
+
1988
+ ${_cache}
1989
+ `;
1990
+ }
1991
+ async function storeGlobalProcedure(input) {
1992
+ const id = randomUUID2();
1993
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1994
+ const client = getClient();
1995
+ await client.execute({
1996
+ sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
1997
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
1998
+ args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
1999
+ });
2000
+ await loadGlobalProcedures();
2001
+ return id;
2002
+ }
2003
+ async function deactivateGlobalProcedure(id) {
2004
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2005
+ const client = getClient();
2006
+ const result = await client.execute({
2007
+ sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
2008
+ args: [now, id]
2009
+ });
2010
+ await loadGlobalProcedures();
2011
+ return result.rowsAffected > 0;
2012
+ }
2013
+ var _cache, _cacheLoaded;
2014
+ var init_global_procedures = __esm({
2015
+ "src/lib/global-procedures.ts"() {
2016
+ "use strict";
2017
+ init_database();
2018
+ _cache = "";
2019
+ _cacheLoaded = false;
2020
+ }
2021
+ });
2022
+
1882
2023
  // src/lib/store.ts
1883
2024
  var store_exports = {};
1884
2025
  __export(store_exports, {
@@ -1958,6 +2099,11 @@ async function initStore(options) {
1958
2099
  "version-query"
1959
2100
  );
1960
2101
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
2102
+ try {
2103
+ const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
2104
+ await loadGlobalProcedures2();
2105
+ } catch {
2106
+ }
1961
2107
  }
1962
2108
  function classifyTier(record) {
1963
2109
  if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
@@ -1999,6 +2145,12 @@ async function writeMemory(record) {
1999
2145
  supersedes_id: record.supersedes_id ?? null
2000
2146
  };
2001
2147
  _pendingRecords.push(dbRow);
2148
+ orgBus.emit({
2149
+ type: "memory_stored",
2150
+ agentId: record.agent_id,
2151
+ project: record.project_name,
2152
+ timestamp: record.timestamp
2153
+ });
2002
2154
  const MAX_PENDING = 1e3;
2003
2155
  if (_pendingRecords.length > MAX_PENDING) {
2004
2156
  const dropped = _pendingRecords.length - MAX_PENDING;
@@ -2344,6 +2496,7 @@ var init_store = __esm({
2344
2496
  init_database();
2345
2497
  init_keychain();
2346
2498
  init_config();
2499
+ init_state_bus();
2347
2500
  INIT_MAX_RETRIES = 3;
2348
2501
  INIT_RETRY_DELAY_MS = 1e3;
2349
2502
  _pendingRecords = [];
@@ -3160,7 +3313,7 @@ __export(license_exports, {
3160
3313
  validateLicense: () => validateLicense
3161
3314
  });
3162
3315
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
3163
- import { randomUUID as randomUUID2 } from "crypto";
3316
+ import { randomUUID as randomUUID3 } from "crypto";
3164
3317
  import path10 from "path";
3165
3318
  import { jwtVerify, importSPKI } from "jose";
3166
3319
  async function fetchRetry(url, init) {
@@ -3187,7 +3340,7 @@ function loadDeviceId() {
3187
3340
  }
3188
3341
  } catch {
3189
3342
  }
3190
- const id = randomUUID2();
3343
+ const id = randomUUID3();
3191
3344
  mkdirSync3(EXE_AI_DIR, { recursive: true });
3192
3345
  writeFileSync2(DEVICE_ID_PATH, id, "utf8");
3193
3346
  return id;
@@ -3660,462 +3813,61 @@ var init_notifications = __esm({
3660
3813
  }
3661
3814
  });
3662
3815
 
3663
- // src/lib/tasks-crud.ts
3664
- import crypto6 from "crypto";
3816
+ // src/lib/session-registry.ts
3817
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
3665
3818
  import path15 from "path";
3666
- import { execSync as execSync6 } from "child_process";
3667
- import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3668
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3669
- async function writeCheckpoint(input) {
3670
- const client = getClient();
3671
- const row = await resolveTask(client, input.taskId);
3672
- const taskId = String(row.id);
3673
- const now = (/* @__PURE__ */ new Date()).toISOString();
3674
- const blockedByIds = [];
3675
- if (row.blocked_by) {
3676
- blockedByIds.push(String(row.blocked_by));
3819
+ import os4 from "os";
3820
+ function registerSession(entry) {
3821
+ const dir = path15.dirname(REGISTRY_PATH);
3822
+ if (!existsSync11(dir)) {
3823
+ mkdirSync4(dir, { recursive: true });
3677
3824
  }
3678
- const checkpoint = {
3679
- step: input.step,
3680
- context_summary: input.contextSummary,
3681
- files_touched: input.filesTouched ?? [],
3682
- blocked_by_ids: blockedByIds,
3683
- last_checkpoint_at: now
3684
- };
3685
- const result = await client.execute({
3686
- sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
3687
- args: [JSON.stringify(checkpoint), now, taskId]
3688
- });
3689
- if (result.rowsAffected === 0) {
3690
- throw new Error(`Checkpoint write failed: task ${taskId} not found`);
3825
+ const sessions = listSessions();
3826
+ const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
3827
+ if (idx >= 0) {
3828
+ sessions[idx] = entry;
3829
+ } else {
3830
+ sessions.push(entry);
3691
3831
  }
3692
- const countResult = await client.execute({
3693
- sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
3694
- args: [taskId]
3695
- });
3696
- const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
3697
- return { checkpointCount };
3698
- }
3699
- function extractParentFromContext(contextBody) {
3700
- if (!contextBody) return null;
3701
- const match = contextBody.match(
3702
- /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
3703
- );
3704
- return match ? match[1].toLowerCase() : null;
3705
- }
3706
- function slugify(title) {
3707
- return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3832
+ writeFileSync5(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
3708
3833
  }
3709
- async function resolveTask(client, identifier) {
3710
- let result = await client.execute({
3711
- sql: "SELECT * FROM tasks WHERE id = ?",
3712
- args: [identifier]
3713
- });
3714
- if (result.rows.length === 1) return result.rows[0];
3715
- result = await client.execute({
3716
- sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
3717
- args: [`%${identifier}%`]
3718
- });
3719
- if (result.rows.length === 1) return result.rows[0];
3720
- if (result.rows.length > 1) {
3721
- const exact = result.rows.filter(
3722
- (r) => String(r.task_file).endsWith(`/${identifier}.md`)
3723
- );
3724
- if (exact.length === 1) return exact[0];
3725
- const candidates = exact.length > 1 ? exact : result.rows;
3726
- const active = candidates.filter(
3727
- (r) => !["done", "cancelled"].includes(String(r.status))
3728
- );
3729
- if (active.length === 1) return active[0];
3730
- const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
3731
- throw new Error(
3732
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3733
- );
3734
- }
3735
- result = await client.execute({
3736
- sql: "SELECT * FROM tasks WHERE title LIKE ?",
3737
- args: [`%${identifier}%`]
3738
- });
3739
- if (result.rows.length === 1) return result.rows[0];
3740
- if (result.rows.length > 1) {
3741
- const active = result.rows.filter(
3742
- (r) => !["done", "cancelled"].includes(String(r.status))
3743
- );
3744
- if (active.length === 1) return active[0];
3745
- const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
3746
- throw new Error(
3747
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3748
- );
3834
+ function listSessions() {
3835
+ try {
3836
+ const raw = readFileSync9(REGISTRY_PATH, "utf8");
3837
+ return JSON.parse(raw);
3838
+ } catch {
3839
+ return [];
3749
3840
  }
3750
- throw new Error(`Task not found: ${identifier}`);
3751
3841
  }
3752
- async function createTaskCore(input) {
3753
- const client = getClient();
3754
- const id = crypto6.randomUUID();
3755
- const now = (/* @__PURE__ */ new Date()).toISOString();
3756
- const slug = slugify(input.title);
3757
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
3758
- let blockedById = null;
3759
- const initialStatus = input.blockedBy ? "blocked" : "open";
3760
- if (input.blockedBy) {
3761
- const blocker = await resolveTask(client, input.blockedBy);
3762
- blockedById = String(blocker.id);
3763
- }
3764
- let parentTaskId = null;
3765
- let parentRef = input.parentTaskId;
3766
- if (!parentRef) {
3767
- const extracted = extractParentFromContext(input.context);
3768
- if (extracted) {
3769
- parentRef = extracted;
3770
- process.stderr.write(
3771
- "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
3772
- );
3773
- }
3842
+ var REGISTRY_PATH;
3843
+ var init_session_registry = __esm({
3844
+ "src/lib/session-registry.ts"() {
3845
+ "use strict";
3846
+ REGISTRY_PATH = path15.join(os4.homedir(), ".exe-os", "session-registry.json");
3774
3847
  }
3775
- if (parentRef) {
3776
- try {
3777
- const parent = await resolveTask(client, parentRef);
3778
- parentTaskId = String(parent.id);
3779
- } catch (err) {
3780
- if (!input.parentTaskId) {
3781
- throw new Error(
3782
- `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
3783
- );
3784
- }
3785
- throw err;
3786
- }
3787
- }
3788
- let warning;
3789
- const dupCheck = await client.execute({
3790
- sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
3791
- args: [input.title, input.assignedTo]
3792
- });
3793
- if (dupCheck.rows.length > 0) {
3794
- warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
3795
- }
3796
- if (input.baseDir) {
3797
- try {
3798
- await mkdir4(path15.join(input.baseDir, "exe", "output"), { recursive: true });
3799
- await mkdir4(path15.join(input.baseDir, "exe", "research"), { recursive: true });
3800
- await ensureArchitectureDoc(input.baseDir, input.projectName);
3801
- await ensureGitignoreExe(input.baseDir);
3802
- } catch {
3803
- }
3804
- }
3805
- const complexity = input.complexity ?? "standard";
3806
- await client.execute({
3807
- 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)
3808
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3809
- args: [
3810
- id,
3811
- input.title,
3812
- input.assignedTo,
3813
- input.assignedBy,
3814
- input.projectName,
3815
- input.priority,
3816
- initialStatus,
3817
- taskFile,
3818
- blockedById,
3819
- parentTaskId,
3820
- input.reviewer ?? null,
3821
- input.context,
3822
- complexity,
3823
- input.budgetTokens ?? null,
3824
- input.budgetFallbackModel ?? null,
3825
- 0,
3826
- null,
3827
- now,
3828
- now
3829
- ]
3830
- });
3831
- return {
3832
- id,
3833
- title: input.title,
3834
- assignedTo: input.assignedTo,
3835
- assignedBy: input.assignedBy,
3836
- projectName: input.projectName,
3837
- priority: input.priority,
3838
- status: initialStatus,
3839
- taskFile,
3840
- createdAt: now,
3841
- updatedAt: now,
3842
- warning,
3843
- budgetTokens: input.budgetTokens ?? null,
3844
- budgetFallbackModel: input.budgetFallbackModel ?? null,
3845
- tokensUsed: 0,
3846
- tokensWarnedAt: null
3847
- };
3848
- }
3849
- async function listTasks(input) {
3850
- const client = getClient();
3851
- const conditions = [];
3852
- const args = [];
3853
- if (input.assignedTo) {
3854
- conditions.push("assigned_to = ?");
3855
- args.push(input.assignedTo);
3856
- }
3857
- if (input.status) {
3858
- conditions.push("status = ?");
3859
- args.push(input.status);
3860
- } else {
3861
- conditions.push("status IN ('open', 'in_progress', 'blocked')");
3862
- }
3863
- if (input.projectName) {
3864
- conditions.push("project_name = ?");
3865
- args.push(input.projectName);
3866
- }
3867
- if (input.priority) {
3868
- conditions.push("priority = ?");
3869
- args.push(input.priority);
3870
- }
3871
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3872
- const result = await client.execute({
3873
- 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`,
3874
- args
3875
- });
3876
- return result.rows.map((r) => ({
3877
- id: String(r.id),
3878
- title: String(r.title),
3879
- assignedTo: String(r.assigned_to),
3880
- assignedBy: String(r.assigned_by),
3881
- projectName: String(r.project_name),
3882
- priority: String(r.priority),
3883
- status: String(r.status),
3884
- taskFile: String(r.task_file),
3885
- createdAt: String(r.created_at),
3886
- updatedAt: String(r.updated_at),
3887
- checkpointCount: Number(r.checkpoint_count ?? 0),
3888
- budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
3889
- budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
3890
- tokensUsed: Number(r.tokens_used ?? 0),
3891
- tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
3892
- }));
3893
- }
3894
- function checkStaleCompletion(taskContext, taskCreatedAt) {
3895
- if (!taskContext) return null;
3896
- if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
3897
- try {
3898
- const since = new Date(taskCreatedAt).toISOString();
3899
- const branch = execSync6(
3900
- "git rev-parse --abbrev-ref HEAD 2>/dev/null",
3901
- { encoding: "utf8", timeout: 3e3 }
3902
- ).trim();
3903
- const branchArg = branch && branch !== "HEAD" ? branch : "";
3904
- const commitCount = execSync6(
3905
- `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
3906
- { encoding: "utf8", timeout: 5e3 }
3907
- ).trim();
3908
- const count = parseInt(commitCount, 10);
3909
- if (count === 0) {
3910
- return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
3911
- }
3912
- return null;
3913
- } catch {
3914
- return null;
3915
- }
3916
- }
3917
- async function updateTaskStatus(input) {
3918
- const client = getClient();
3919
- const now = (/* @__PURE__ */ new Date()).toISOString();
3920
- const row = await resolveTask(client, input.taskId);
3921
- const taskId = String(row.id);
3922
- const taskFile = String(row.task_file);
3923
- if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
3924
- process.stderr.write(
3925
- `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
3926
- `
3927
- );
3928
- }
3929
- if (input.status === "done") {
3930
- const existingRow = await client.execute({
3931
- sql: "SELECT context, created_at FROM tasks WHERE id = ?",
3932
- args: [taskId]
3933
- });
3934
- if (existingRow.rows.length > 0) {
3935
- const ctx = existingRow.rows[0];
3936
- const warning = checkStaleCompletion(ctx.context, ctx.created_at);
3937
- if (warning) {
3938
- input.result = input.result ? `\u26A0\uFE0F ${warning}
3939
-
3940
- ${input.result}` : `\u26A0\uFE0F ${warning}`;
3941
- process.stderr.write(`[tasks] ${warning} (task: ${taskId})
3942
- `);
3943
- }
3944
- }
3945
- }
3946
- if (input.status === "in_progress") {
3947
- const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
3948
- const claim = await client.execute({
3949
- sql: `UPDATE tasks
3950
- SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
3951
- WHERE id = ? AND status = 'open'`,
3952
- args: [tmuxSession, now, taskId]
3953
- });
3954
- if (claim.rowsAffected === 0) {
3955
- const current = await client.execute({
3956
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
3957
- args: [taskId]
3958
- });
3959
- const cur = current.rows[0];
3960
- const status = cur?.status ?? "unknown";
3961
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
3962
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
3963
- }
3964
- try {
3965
- await writeCheckpoint({
3966
- taskId,
3967
- step: "claimed",
3968
- contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3969
- });
3970
- } catch {
3971
- }
3972
- return { row, taskFile, now, taskId };
3973
- }
3974
- if (input.result) {
3975
- await client.execute({
3976
- sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
3977
- args: [input.status, input.result, now, taskId]
3978
- });
3979
- } else {
3980
- await client.execute({
3981
- sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
3982
- args: [input.status, now, taskId]
3983
- });
3984
- }
3985
- try {
3986
- await writeCheckpoint({
3987
- taskId,
3988
- step: `status_transition:${input.status}`,
3989
- contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
3990
- });
3991
- } catch {
3992
- }
3993
- return { row, taskFile, now, taskId };
3994
- }
3995
- async function deleteTaskCore(taskId, _baseDir) {
3996
- const client = getClient();
3997
- const row = await resolveTask(client, taskId);
3998
- const id = String(row.id);
3999
- const taskFile = String(row.task_file);
4000
- const assignedTo = String(row.assigned_to);
4001
- const assignedBy = String(row.assigned_by);
4002
- await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
4003
- const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
4004
- return { taskFile, assignedTo, assignedBy, taskSlug };
4005
- }
4006
- async function ensureArchitectureDoc(baseDir, projectName) {
4007
- const archPath = path15.join(baseDir, "exe", "ARCHITECTURE.md");
4008
- try {
4009
- if (existsSync11(archPath)) return;
4010
- const template = [
4011
- `# ${projectName} \u2014 System Architecture`,
4012
- "",
4013
- "> Employees: read this before every task. Update it when you change system structure.",
4014
- `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
4015
- "",
4016
- "## Overview",
4017
- "",
4018
- "<!-- Describe what this system does, its main components, and how they connect. -->",
4019
- "",
4020
- "## Key Components",
4021
- "",
4022
- "<!-- List the major modules, services, or subsystems. -->",
4023
- "",
4024
- "## Data Flow",
4025
- "",
4026
- "<!-- How does data move through the system? What writes where? -->",
4027
- "",
4028
- "## Invariants",
4029
- "",
4030
- "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
4031
- "",
4032
- "## Dependencies",
4033
- "",
4034
- "<!-- What depends on what? If I change X, what else is affected? -->",
4035
- ""
4036
- ].join("\n");
4037
- await writeFile4(archPath, template, "utf-8");
4038
- } catch {
4039
- }
4040
- }
4041
- async function ensureGitignoreExe(baseDir) {
4042
- const gitignorePath = path15.join(baseDir, ".gitignore");
4043
- try {
4044
- if (existsSync11(gitignorePath)) {
4045
- const content = readFileSync9(gitignorePath, "utf-8");
4046
- if (/^\/?exe\/?$/m.test(content)) return;
4047
- await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
4048
- } else {
4049
- await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
4050
- }
4051
- } catch {
4052
- }
4053
- }
4054
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4055
- var init_tasks_crud = __esm({
4056
- "src/lib/tasks-crud.ts"() {
4057
- "use strict";
4058
- init_database();
4059
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
4060
- TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4061
- }
4062
- });
4063
-
4064
- // src/lib/session-registry.ts
4065
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync12 } from "fs";
4066
- import path16 from "path";
4067
- import os4 from "os";
4068
- function registerSession(entry) {
4069
- const dir = path16.dirname(REGISTRY_PATH);
4070
- if (!existsSync12(dir)) {
4071
- mkdirSync4(dir, { recursive: true });
4072
- }
4073
- const sessions = listSessions();
4074
- const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
4075
- if (idx >= 0) {
4076
- sessions[idx] = entry;
4077
- } else {
4078
- sessions.push(entry);
4079
- }
4080
- writeFileSync5(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
4081
- }
4082
- function listSessions() {
4083
- try {
4084
- const raw = readFileSync10(REGISTRY_PATH, "utf8");
4085
- return JSON.parse(raw);
4086
- } catch {
4087
- return [];
4088
- }
4089
- }
4090
- var REGISTRY_PATH;
4091
- var init_session_registry = __esm({
4092
- "src/lib/session-registry.ts"() {
4093
- "use strict";
4094
- REGISTRY_PATH = path16.join(os4.homedir(), ".exe-os", "session-registry.json");
4095
- }
4096
- });
4097
-
4098
- // src/lib/tmux-transport.ts
4099
- var tmux_transport_exports = {};
4100
- __export(tmux_transport_exports, {
4101
- TmuxTransport: () => TmuxTransport
4102
- });
4103
- import { execFileSync } from "child_process";
4104
- var QUIET, TmuxTransport;
4105
- var init_tmux_transport = __esm({
4106
- "src/lib/tmux-transport.ts"() {
4107
- "use strict";
4108
- QUIET = {
4109
- encoding: "utf8",
4110
- stdio: ["pipe", "pipe", "pipe"]
4111
- };
4112
- TmuxTransport = class {
4113
- getMySession() {
4114
- try {
4115
- return execFileSync("tmux", ["display-message", "-p", "#{session_name}"], QUIET).trim() || null;
4116
- } catch {
4117
- return null;
4118
- }
3848
+ });
3849
+
3850
+ // src/lib/tmux-transport.ts
3851
+ var tmux_transport_exports = {};
3852
+ __export(tmux_transport_exports, {
3853
+ TmuxTransport: () => TmuxTransport
3854
+ });
3855
+ import { execFileSync } from "child_process";
3856
+ var QUIET, TmuxTransport;
3857
+ var init_tmux_transport = __esm({
3858
+ "src/lib/tmux-transport.ts"() {
3859
+ "use strict";
3860
+ QUIET = {
3861
+ encoding: "utf8",
3862
+ stdio: ["pipe", "pipe", "pipe"]
3863
+ };
3864
+ TmuxTransport = class {
3865
+ getMySession() {
3866
+ try {
3867
+ return execFileSync("tmux", ["display-message", "-p", "#{session_name}"], QUIET).trim() || null;
3868
+ } catch {
3869
+ return null;
3870
+ }
4119
3871
  }
4120
3872
  listSessions() {
4121
3873
  try {
@@ -4203,14 +3955,14 @@ var init_transport = __esm({
4203
3955
  });
4204
3956
 
4205
3957
  // src/lib/cc-agent-support.ts
4206
- import { execSync as execSync7 } from "child_process";
3958
+ import { execSync as execSync6 } from "child_process";
4207
3959
  function _resetCcAgentSupportCache() {
4208
3960
  _cachedSupport = null;
4209
3961
  }
4210
3962
  function claudeSupportsAgentFlag() {
4211
3963
  if (_cachedSupport !== null) return _cachedSupport;
4212
3964
  try {
4213
- const helpOutput = execSync7("claude --help 2>&1", {
3965
+ const helpOutput = execSync6("claude --help 2>&1", {
4214
3966
  encoding: "utf-8",
4215
3967
  timeout: 5e3
4216
3968
  });
@@ -4276,17 +4028,17 @@ var init_provider_table = __esm({
4276
4028
  });
4277
4029
 
4278
4030
  // src/lib/intercom-queue.ts
4279
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, renameSync as renameSync2, existsSync as existsSync13, mkdirSync as mkdirSync5 } from "fs";
4280
- import path17 from "path";
4031
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, renameSync as renameSync2, existsSync as existsSync12, mkdirSync as mkdirSync5 } from "fs";
4032
+ import path16 from "path";
4281
4033
  import os5 from "os";
4282
4034
  function ensureDir() {
4283
- const dir = path17.dirname(QUEUE_PATH);
4284
- if (!existsSync13(dir)) mkdirSync5(dir, { recursive: true });
4035
+ const dir = path16.dirname(QUEUE_PATH);
4036
+ if (!existsSync12(dir)) mkdirSync5(dir, { recursive: true });
4285
4037
  }
4286
4038
  function readQueue() {
4287
4039
  try {
4288
- if (!existsSync13(QUEUE_PATH)) return [];
4289
- return JSON.parse(readFileSync11(QUEUE_PATH, "utf8"));
4040
+ if (!existsSync12(QUEUE_PATH)) return [];
4041
+ return JSON.parse(readFileSync10(QUEUE_PATH, "utf8"));
4290
4042
  } catch {
4291
4043
  return [];
4292
4044
  }
@@ -4318,21 +4070,358 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
4318
4070
  var init_intercom_queue = __esm({
4319
4071
  "src/lib/intercom-queue.ts"() {
4320
4072
  "use strict";
4321
- QUEUE_PATH = path17.join(os5.homedir(), ".exe-os", "intercom-queue.json");
4073
+ QUEUE_PATH = path16.join(os5.homedir(), ".exe-os", "intercom-queue.json");
4322
4074
  TTL_MS = 60 * 60 * 1e3;
4323
- INTERCOM_LOG = path17.join(os5.homedir(), ".exe-os", "intercom.log");
4075
+ INTERCOM_LOG = path16.join(os5.homedir(), ".exe-os", "intercom.log");
4076
+ }
4077
+ });
4078
+
4079
+ // src/lib/session-kill-telemetry.ts
4080
+ import crypto6 from "crypto";
4081
+ async function recordSessionKill(input) {
4082
+ try {
4083
+ const client = getClient();
4084
+ await client.execute({
4085
+ sql: `INSERT INTO session_kills
4086
+ (id, session_name, agent_id, killed_at, reason,
4087
+ ticks_idle, estimated_tokens_saved)
4088
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
4089
+ args: [
4090
+ crypto6.randomUUID(),
4091
+ input.sessionName,
4092
+ input.agentId,
4093
+ (/* @__PURE__ */ new Date()).toISOString(),
4094
+ input.reason,
4095
+ input.ticksIdle ?? null,
4096
+ input.estimatedTokensSaved ?? null
4097
+ ]
4098
+ });
4099
+ } catch (err) {
4100
+ process.stderr.write(
4101
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
4102
+ `
4103
+ );
4104
+ }
4105
+ }
4106
+ var init_session_kill_telemetry = __esm({
4107
+ "src/lib/session-kill-telemetry.ts"() {
4108
+ "use strict";
4109
+ init_database();
4110
+ }
4111
+ });
4112
+
4113
+ // src/lib/capacity-monitor.ts
4114
+ var capacity_monitor_exports = {};
4115
+ __export(capacity_monitor_exports, {
4116
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
4117
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
4118
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
4119
+ confirmCapacityKill: () => confirmCapacityKill,
4120
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
4121
+ extractContextPercent: () => extractContextPercent,
4122
+ isAtCapacity: () => isAtCapacity,
4123
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
4124
+ pollCapacityDead: () => pollCapacityDead
4125
+ });
4126
+ function resumeTaskTitle(agentId) {
4127
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
4128
+ }
4129
+ function buildResumeContext(agentId, openTasks) {
4130
+ const taskList = openTasks.map(
4131
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
4132
+ ).join("\n");
4133
+ return [
4134
+ "## Context",
4135
+ "",
4136
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
4137
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
4138
+ "",
4139
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
4140
+ "",
4141
+ taskList,
4142
+ "",
4143
+ "Read each task file and chain through them. Build and commit after each one."
4144
+ ].join("\n");
4145
+ }
4146
+ function filterPaneContent(paneOutput) {
4147
+ return paneOutput.split("\n").filter((line) => {
4148
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
4149
+ for (const marker of CONTENT_LINE_MARKERS) {
4150
+ if (line.includes(marker)) return false;
4151
+ }
4152
+ for (const re of SOURCE_CODE_MARKERS) {
4153
+ if (re.test(line)) return false;
4154
+ }
4155
+ return true;
4156
+ }).join("\n");
4157
+ }
4158
+ function extractContextPercent(paneOutput) {
4159
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
4160
+ if (!match) return null;
4161
+ const parsed = Number.parseInt(match[2], 10);
4162
+ return Number.isFinite(parsed) ? parsed : null;
4163
+ }
4164
+ function isAtCapacity(paneOutput) {
4165
+ const filtered = filterPaneContent(paneOutput);
4166
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
4167
+ }
4168
+ function confirmCapacityKill(agentId, now = Date.now()) {
4169
+ const pendingSince = _pendingCapacityKill.get(agentId);
4170
+ if (pendingSince === void 0) {
4171
+ _pendingCapacityKill.set(agentId, now);
4172
+ return false;
4173
+ }
4174
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
4175
+ _pendingCapacityKill.set(agentId, now);
4176
+ return false;
4177
+ }
4178
+ _pendingCapacityKill.delete(agentId);
4179
+ return true;
4180
+ }
4181
+ function _resetPendingCapacityKills() {
4182
+ _pendingCapacityKill.clear();
4183
+ }
4184
+ function _resetLastRelaunchCache() {
4185
+ _lastRelaunch.clear();
4186
+ }
4187
+ async function lastResumeCreatedAtMs(agentId) {
4188
+ const client = getClient();
4189
+ const result = await client.execute({
4190
+ sql: `SELECT MAX(created_at) AS last_created_at
4191
+ FROM tasks
4192
+ WHERE assigned_to = ? AND title LIKE ?`,
4193
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`]
4194
+ });
4195
+ const raw = result.rows[0]?.last_created_at;
4196
+ if (raw === null || raw === void 0) return null;
4197
+ const parsed = Date.parse(String(raw));
4198
+ return Number.isNaN(parsed) ? null : parsed;
4199
+ }
4200
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
4201
+ const cached = _lastRelaunch.get(agentId);
4202
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
4203
+ const persisted = await lastResumeCreatedAtMs(agentId);
4204
+ if (persisted === null) return false;
4205
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
4206
+ _lastRelaunch.set(agentId, persisted);
4207
+ return true;
4208
+ }
4209
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
4210
+ const client = getClient();
4211
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4212
+ const context = buildResumeContext(agentId, openTasks);
4213
+ const existing = await client.execute({
4214
+ sql: `SELECT id FROM tasks
4215
+ WHERE assigned_to = ?
4216
+ AND title LIKE ?
4217
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
4218
+ ORDER BY created_at DESC
4219
+ LIMIT 1`,
4220
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
4221
+ });
4222
+ if (existing.rows.length > 0) {
4223
+ const taskId = String(existing.rows[0].id);
4224
+ await client.execute({
4225
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
4226
+ args: [context, now, taskId]
4227
+ });
4228
+ return { created: false, taskId };
4229
+ }
4230
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
4231
+ const task = await createTask2({
4232
+ title: resumeTaskTitle(agentId),
4233
+ assignedTo: agentId,
4234
+ assignedBy: "system",
4235
+ projectName: projectDir.split("/").pop() ?? "unknown",
4236
+ priority: "p0",
4237
+ context,
4238
+ baseDir: projectDir
4239
+ });
4240
+ return { created: true, taskId: task.id };
4241
+ }
4242
+ async function pollCapacityDead() {
4243
+ const transport = getTransport();
4244
+ const relaunched = [];
4245
+ const registered = listSessions().filter(
4246
+ (s) => s.agentId !== "exe"
4247
+ );
4248
+ if (registered.length === 0) return [];
4249
+ let liveSessions;
4250
+ try {
4251
+ liveSessions = transport.listSessions();
4252
+ } catch {
4253
+ return [];
4254
+ }
4255
+ for (const entry of registered) {
4256
+ const { windowName, agentId, projectDir } = entry;
4257
+ if (!liveSessions.includes(windowName)) continue;
4258
+ if (await isWithinRelaunchCooldown(agentId)) continue;
4259
+ let pane;
4260
+ try {
4261
+ pane = transport.capturePane(windowName, 15);
4262
+ } catch {
4263
+ continue;
4264
+ }
4265
+ if (!isAtCapacity(pane)) continue;
4266
+ const ctxPct = extractContextPercent(pane);
4267
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
4268
+ process.stderr.write(
4269
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
4270
+ `
4271
+ );
4272
+ continue;
4273
+ }
4274
+ if (!confirmCapacityKill(agentId)) {
4275
+ process.stderr.write(
4276
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
4277
+ `
4278
+ );
4279
+ continue;
4280
+ }
4281
+ const verify = await verifyPaneAtCapacity(windowName);
4282
+ if (!verify.atCapacity) {
4283
+ process.stderr.write(
4284
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
4285
+ `
4286
+ );
4287
+ void recordSessionKill({
4288
+ sessionName: windowName,
4289
+ agentId,
4290
+ reason: "capacity_false_positive_blocked"
4291
+ });
4292
+ continue;
4293
+ }
4294
+ process.stderr.write(
4295
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
4296
+ `
4297
+ );
4298
+ try {
4299
+ transport.kill(windowName);
4300
+ void recordSessionKill({
4301
+ sessionName: windowName,
4302
+ agentId,
4303
+ reason: "capacity"
4304
+ });
4305
+ const client = getClient();
4306
+ const openTasks = await client.execute({
4307
+ sql: `SELECT id, title, priority, task_file, status
4308
+ FROM tasks
4309
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')
4310
+ ORDER BY
4311
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
4312
+ created_at ASC
4313
+ LIMIT 10`,
4314
+ args: [agentId]
4315
+ });
4316
+ if (openTasks.rows.length === 0) {
4317
+ process.stderr.write(
4318
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
4319
+ `
4320
+ );
4321
+ continue;
4322
+ }
4323
+ const { created } = await createOrRefreshResumeTask(
4324
+ agentId,
4325
+ projectDir,
4326
+ openTasks.rows
4327
+ );
4328
+ if (created) {
4329
+ await writeNotification({
4330
+ agentId: "system",
4331
+ agentRole: "daemon",
4332
+ event: "capacity_relaunch",
4333
+ project: projectDir.split("/").pop() ?? "unknown",
4334
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
4335
+ });
4336
+ }
4337
+ _lastRelaunch.set(agentId, Date.now());
4338
+ if (created) relaunched.push(agentId);
4339
+ } catch (err) {
4340
+ process.stderr.write(
4341
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
4342
+ `
4343
+ );
4344
+ }
4345
+ }
4346
+ return relaunched;
4347
+ }
4348
+ 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;
4349
+ var init_capacity_monitor = __esm({
4350
+ "src/lib/capacity-monitor.ts"() {
4351
+ "use strict";
4352
+ init_session_registry();
4353
+ init_transport();
4354
+ init_notifications();
4355
+ init_database();
4356
+ init_session_kill_telemetry();
4357
+ init_tmux_routing();
4358
+ CAPACITY_PATTERNS = [
4359
+ /conversation is too long/i,
4360
+ /maximum context length/i,
4361
+ /context window.*(?:limit|exceed|full)/i,
4362
+ /reached.*(?:token|context).*limit/i
4363
+ ];
4364
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
4365
+ CONTENT_LINE_MARKERS = [
4366
+ "RESUME:",
4367
+ "intercom",
4368
+ "capacity-monitor",
4369
+ "CAPACITY_PATTERNS",
4370
+ "isAtCapacity",
4371
+ "CONTENT_LINE_MARKERS",
4372
+ "pollCapacityDead",
4373
+ "confirmCapacityKill",
4374
+ "session_kills",
4375
+ "capacity-monitor.test"
4376
+ ];
4377
+ SOURCE_CODE_MARKERS = [
4378
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
4379
+ /(?:maximum context length|conversation is too long).*["'`/]/i
4380
+ ];
4381
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
4382
+ _lastRelaunch = /* @__PURE__ */ new Map();
4383
+ RESUME_TITLE_PREFIX = "RESUME:";
4384
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
4385
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
4386
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
4387
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
4388
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
4389
+ CTX_FLOOR_PERCENT = 50;
4324
4390
  }
4325
4391
  });
4326
4392
 
4327
4393
  // src/lib/tmux-routing.ts
4328
- import { execFileSync as execFileSync2, execSync as execSync8 } from "child_process";
4329
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync14, appendFileSync } from "fs";
4330
- import path18 from "path";
4394
+ var tmux_routing_exports = {};
4395
+ __export(tmux_routing_exports, {
4396
+ acquireSpawnLock: () => acquireSpawnLock2,
4397
+ employeeSessionName: () => employeeSessionName,
4398
+ ensureEmployee: () => ensureEmployee,
4399
+ extractRootExe: () => extractRootExe,
4400
+ findFreeInstance: () => findFreeInstance,
4401
+ getDispatchedBy: () => getDispatchedBy,
4402
+ getMySession: () => getMySession,
4403
+ getParentExe: () => getParentExe,
4404
+ getSessionState: () => getSessionState,
4405
+ isEmployeeAlive: () => isEmployeeAlive,
4406
+ isExeSession: () => isExeSession,
4407
+ isSessionBusy: () => isSessionBusy,
4408
+ notifyParentExe: () => notifyParentExe,
4409
+ parseParentExe: () => parseParentExe,
4410
+ registerParentExe: () => registerParentExe,
4411
+ releaseSpawnLock: () => releaseSpawnLock2,
4412
+ resolveExeSession: () => resolveExeSession,
4413
+ sendIntercom: () => sendIntercom,
4414
+ spawnEmployee: () => spawnEmployee,
4415
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
4416
+ });
4417
+ import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
4418
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync13, appendFileSync } from "fs";
4419
+ import path17 from "path";
4331
4420
  import os6 from "os";
4332
4421
  import { fileURLToPath as fileURLToPath2 } from "url";
4333
4422
  import { unlinkSync as unlinkSync4 } from "fs";
4334
4423
  function spawnLockPath(sessionName) {
4335
- return path18.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4424
+ return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4336
4425
  }
4337
4426
  function isProcessAlive(pid) {
4338
4427
  try {
@@ -4343,13 +4432,13 @@ function isProcessAlive(pid) {
4343
4432
  }
4344
4433
  }
4345
4434
  function acquireSpawnLock2(sessionName) {
4346
- if (!existsSync14(SPAWN_LOCK_DIR)) {
4435
+ if (!existsSync13(SPAWN_LOCK_DIR)) {
4347
4436
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4348
4437
  }
4349
4438
  const lockFile = spawnLockPath(sessionName);
4350
- if (existsSync14(lockFile)) {
4439
+ if (existsSync13(lockFile)) {
4351
4440
  try {
4352
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
4441
+ const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
4353
4442
  const age = Date.now() - lock.timestamp;
4354
4443
  if (isProcessAlive(lock.pid) && age < 6e4) {
4355
4444
  return false;
@@ -4369,13 +4458,13 @@ function releaseSpawnLock2(sessionName) {
4369
4458
  function resolveBehaviorsExporterScript() {
4370
4459
  try {
4371
4460
  const thisFile = fileURLToPath2(import.meta.url);
4372
- const scriptPath = path18.join(
4373
- path18.dirname(thisFile),
4461
+ const scriptPath = path17.join(
4462
+ path17.dirname(thisFile),
4374
4463
  "..",
4375
4464
  "bin",
4376
4465
  "exe-export-behaviors.js"
4377
4466
  );
4378
- return existsSync14(scriptPath) ? scriptPath : null;
4467
+ return existsSync13(scriptPath) ? scriptPath : null;
4379
4468
  } catch {
4380
4469
  return null;
4381
4470
  }
@@ -4395,23 +4484,61 @@ function exportBehaviorsSync(agentId, projectName, sessionKey) {
4395
4484
  `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
4396
4485
  `
4397
4486
  );
4398
- return null;
4487
+ return null;
4488
+ }
4489
+ }
4490
+ function getMySession() {
4491
+ return getTransport().getMySession();
4492
+ }
4493
+ function employeeSessionName(employee, exeSession, instance) {
4494
+ if (!/^exe\d+$/.test(exeSession)) {
4495
+ const root = extractRootExe(exeSession);
4496
+ if (root) {
4497
+ process.stderr.write(
4498
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
4499
+ `
4500
+ );
4501
+ exeSession = root;
4502
+ } else {
4503
+ throw new Error(
4504
+ `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
4505
+ );
4506
+ }
4507
+ }
4508
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
4509
+ const name = `${employee}${suffix}-${exeSession}`;
4510
+ if (!VALID_SESSION_NAME.test(name)) {
4511
+ throw new Error(
4512
+ `Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
4513
+ );
4399
4514
  }
4515
+ return name;
4400
4516
  }
4401
- function getMySession() {
4402
- return getTransport().getMySession();
4403
- }
4404
- function employeeSessionName(employee, exeSession, instance) {
4405
- const suffix = instance != null && instance > 0 ? String(instance) : "";
4406
- return `${employee}${suffix}-${exeSession}`;
4517
+ function parseParentExe(sessionName, agentId) {
4518
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4519
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
4520
+ const match = sessionName.match(regex);
4521
+ return match?.[1] ?? null;
4407
4522
  }
4408
4523
  function extractRootExe(name) {
4409
4524
  const match = name.match(/(exe\d+)$/);
4410
4525
  return match?.[1] ?? null;
4411
4526
  }
4527
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
4528
+ if (!existsSync13(SESSION_CACHE)) {
4529
+ mkdirSync6(SESSION_CACHE, { recursive: true });
4530
+ }
4531
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
4532
+ const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4533
+ writeFileSync7(filePath, JSON.stringify({
4534
+ parentExe: rootExe,
4535
+ dispatchedBy: dispatchedBy || rootExe,
4536
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4537
+ }));
4538
+ }
4412
4539
  function getParentExe(sessionKey) {
4413
4540
  try {
4414
- const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4541
+ const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4415
4542
  return data.parentExe || null;
4416
4543
  } catch {
4417
4544
  return null;
@@ -4419,8 +4546,8 @@ function getParentExe(sessionKey) {
4419
4546
  }
4420
4547
  function getDispatchedBy(sessionKey) {
4421
4548
  try {
4422
- const data = JSON.parse(readFileSync12(
4423
- path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4549
+ const data = JSON.parse(readFileSync11(
4550
+ path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4424
4551
  "utf8"
4425
4552
  ));
4426
4553
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -4453,414 +4580,879 @@ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive =
4453
4580
  }
4454
4581
  return null;
4455
4582
  }
4583
+ async function verifyPaneAtCapacity(sessionName) {
4584
+ const transport = getTransport();
4585
+ if (!transport.isAlive(sessionName)) {
4586
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
4587
+ }
4588
+ let pane;
4589
+ try {
4590
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
4591
+ } catch (err) {
4592
+ return {
4593
+ atCapacity: false,
4594
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
4595
+ };
4596
+ }
4597
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
4598
+ if (!isAtCapacity2(pane)) {
4599
+ return {
4600
+ atCapacity: false,
4601
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
4602
+ };
4603
+ }
4604
+ return {
4605
+ atCapacity: true,
4606
+ reason: "capacity banner matched in recent pane output"
4607
+ };
4608
+ }
4456
4609
  function readDebounceState() {
4457
4610
  try {
4458
- if (!existsSync14(DEBOUNCE_FILE)) return {};
4459
- return JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
4611
+ if (!existsSync13(DEBOUNCE_FILE)) return {};
4612
+ return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
4613
+ } catch {
4614
+ return {};
4615
+ }
4616
+ }
4617
+ function writeDebounceState(state) {
4618
+ try {
4619
+ if (!existsSync13(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4620
+ writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
4621
+ } catch {
4622
+ }
4623
+ }
4624
+ function isDebounced(targetSession) {
4625
+ const state = readDebounceState();
4626
+ const lastSent = state[targetSession] ?? 0;
4627
+ return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4628
+ }
4629
+ function recordDebounce(targetSession) {
4630
+ const state = readDebounceState();
4631
+ state[targetSession] = Date.now();
4632
+ const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4633
+ for (const key of Object.keys(state)) {
4634
+ if ((state[key] ?? 0) < cutoff) delete state[key];
4635
+ }
4636
+ writeDebounceState(state);
4637
+ }
4638
+ function logIntercom(msg) {
4639
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
4640
+ `;
4641
+ process.stderr.write(`[intercom] ${msg}
4642
+ `);
4643
+ try {
4644
+ appendFileSync(INTERCOM_LOG2, line);
4645
+ } catch {
4646
+ }
4647
+ }
4648
+ function getSessionState(sessionName) {
4649
+ const transport = getTransport();
4650
+ if (!transport.isAlive(sessionName)) return "offline";
4651
+ try {
4652
+ const pane = transport.capturePane(sessionName, 5);
4653
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4654
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4655
+ return "no_claude";
4656
+ }
4657
+ }
4658
+ if (/Running…/.test(pane)) return "tool";
4659
+ if (BUSY_PATTERN.test(pane)) return "thinking";
4660
+ return "idle";
4661
+ } catch {
4662
+ return "offline";
4663
+ }
4664
+ }
4665
+ function isSessionBusy(sessionName) {
4666
+ const state = getSessionState(sessionName);
4667
+ return state === "thinking" || state === "tool";
4668
+ }
4669
+ function isExeSession(sessionName) {
4670
+ return /^exe\d*$/.test(sessionName);
4671
+ }
4672
+ function sendIntercom(targetSession) {
4673
+ const transport = getTransport();
4674
+ if (isExeSession(targetSession)) {
4675
+ logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
4676
+ return "skipped_exe";
4677
+ }
4678
+ if (isDebounced(targetSession)) {
4679
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4680
+ return "debounced";
4681
+ }
4682
+ try {
4683
+ const sessions = transport.listSessions();
4684
+ if (!sessions.includes(targetSession)) {
4685
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4686
+ return "failed";
4687
+ }
4688
+ const sessionState = getSessionState(targetSession);
4689
+ if (sessionState === "no_claude") {
4690
+ queueIntercom(targetSession, "claude not running in session");
4691
+ recordDebounce(targetSession);
4692
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4693
+ return "queued";
4694
+ }
4695
+ if (sessionState === "thinking" || sessionState === "tool") {
4696
+ queueIntercom(targetSession, "session busy at send time");
4697
+ recordDebounce(targetSession);
4698
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4699
+ return "queued";
4700
+ }
4701
+ if (transport.isPaneInCopyMode(targetSession)) {
4702
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4703
+ transport.sendKeys(targetSession, "q");
4704
+ }
4705
+ transport.sendKeys(targetSession, "/exe-intercom");
4706
+ recordDebounce(targetSession);
4707
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4708
+ return "delivered";
4709
+ } catch {
4710
+ logIntercom(`FAIL \u2192 ${targetSession}`);
4711
+ return "failed";
4712
+ }
4713
+ }
4714
+ function notifyParentExe(sessionKey) {
4715
+ const target = getDispatchedBy(sessionKey);
4716
+ if (!target) {
4717
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
4718
+ `);
4719
+ return false;
4720
+ }
4721
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4722
+ `);
4723
+ const result = sendIntercom(target);
4724
+ if (result === "failed") {
4725
+ const rootExe = resolveExeSession();
4726
+ if (rootExe && rootExe !== target) {
4727
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4728
+ `);
4729
+ const fallback = sendIntercom(rootExe);
4730
+ return fallback !== "failed";
4731
+ }
4732
+ return false;
4733
+ }
4734
+ return true;
4735
+ }
4736
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4737
+ if (employeeName === "exe") {
4738
+ return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
4739
+ }
4740
+ try {
4741
+ assertEmployeeLimitSync();
4742
+ } catch (err) {
4743
+ if (err instanceof PlanLimitError) {
4744
+ return { status: "failed", sessionName: "", error: err.message };
4745
+ }
4746
+ }
4747
+ if (/-exe\d*$/.test(employeeName)) {
4748
+ const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
4749
+ return {
4750
+ status: "failed",
4751
+ sessionName: "",
4752
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
4753
+ };
4754
+ }
4755
+ if (!/^exe\d+$/.test(exeSession)) {
4756
+ const root = extractRootExe(exeSession);
4757
+ if (root) {
4758
+ process.stderr.write(
4759
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
4760
+ `
4761
+ );
4762
+ exeSession = root;
4763
+ } else {
4764
+ return {
4765
+ status: "failed",
4766
+ sessionName: "",
4767
+ error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
4768
+ };
4769
+ }
4770
+ }
4771
+ let effectiveInstance = opts?.instance;
4772
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
4773
+ const free = findFreeInstance(
4774
+ employeeName,
4775
+ exeSession,
4776
+ opts.maxAutoInstances ?? 10
4777
+ );
4778
+ if (free === null) {
4779
+ return {
4780
+ status: "failed",
4781
+ sessionName: employeeSessionName(employeeName, exeSession),
4782
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4783
+ };
4784
+ }
4785
+ effectiveInstance = free === 0 ? void 0 : free;
4786
+ }
4787
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
4788
+ if (isEmployeeAlive(sessionName)) {
4789
+ const result2 = sendIntercom(sessionName);
4790
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4791
+ return { status: "intercom_sent", sessionName };
4792
+ }
4793
+ if (result2 === "delivered") {
4794
+ return { status: "intercom_unprocessed", sessionName };
4795
+ }
4796
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
4797
+ }
4798
+ const spawnOpts = { ...opts, instance: effectiveInstance };
4799
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
4800
+ if (result.error) {
4801
+ return { status: "failed", sessionName, error: result.error };
4802
+ }
4803
+ return { status: "spawned", sessionName };
4804
+ }
4805
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4806
+ const transport = getTransport();
4807
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4808
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4809
+ const logDir = path17.join(os6.homedir(), ".exe-os", "session-logs");
4810
+ const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4811
+ if (!existsSync13(logDir)) {
4812
+ mkdirSync6(logDir, { recursive: true });
4813
+ }
4814
+ transport.kill(sessionName);
4815
+ let cleanupSuffix = "";
4816
+ try {
4817
+ const thisFile = fileURLToPath2(import.meta.url);
4818
+ const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4819
+ if (existsSync13(cleanupScript)) {
4820
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4821
+ }
4822
+ } catch {
4823
+ }
4824
+ try {
4825
+ const claudeJsonPath = path17.join(os6.homedir(), ".claude.json");
4826
+ let claudeJson = {};
4827
+ try {
4828
+ claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
4829
+ } catch {
4830
+ }
4831
+ if (!claudeJson.projects) claudeJson.projects = {};
4832
+ const projects = claudeJson.projects;
4833
+ const trustDir = opts?.cwd ?? projectDir;
4834
+ if (!projects[trustDir]) projects[trustDir] = {};
4835
+ projects[trustDir].hasTrustDialogAccepted = true;
4836
+ writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4837
+ } catch {
4838
+ }
4839
+ try {
4840
+ const settingsDir = path17.join(os6.homedir(), ".claude", "projects");
4841
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4842
+ const projSettingsDir = path17.join(settingsDir, normalizedKey);
4843
+ const settingsPath = path17.join(projSettingsDir, "settings.json");
4844
+ let settings = {};
4845
+ try {
4846
+ settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
4847
+ } catch {
4848
+ }
4849
+ const perms = settings.permissions ?? {};
4850
+ const allow = perms.allow ?? [];
4851
+ const toolNames = [
4852
+ "recall_my_memory",
4853
+ "store_memory",
4854
+ "create_task",
4855
+ "update_task",
4856
+ "list_tasks",
4857
+ "get_task",
4858
+ "ask_team_memory",
4859
+ "store_behavior",
4860
+ "get_identity",
4861
+ "send_message"
4862
+ ];
4863
+ const requiredTools = expandDualPrefixTools(toolNames);
4864
+ let changed = false;
4865
+ for (const tool of requiredTools) {
4866
+ if (!allow.includes(tool)) {
4867
+ allow.push(tool);
4868
+ changed = true;
4869
+ }
4870
+ }
4871
+ if (changed) {
4872
+ perms.allow = allow;
4873
+ settings.permissions = perms;
4874
+ mkdirSync6(projSettingsDir, { recursive: true });
4875
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4876
+ }
4460
4877
  } catch {
4461
- return {};
4462
4878
  }
4463
- }
4464
- function writeDebounceState(state) {
4465
- try {
4466
- if (!existsSync14(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4467
- writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
4468
- } catch {
4879
+ const spawnCwd = opts?.cwd ?? projectDir;
4880
+ const useExeAgent = !!(opts?.model && opts?.provider);
4881
+ const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
4882
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4883
+ let identityFlag = "";
4884
+ let behaviorsFlag = "";
4885
+ let legacyFallbackWarned = false;
4886
+ if (!useExeAgent && !useBinSymlink) {
4887
+ const identityPath2 = path17.join(
4888
+ os6.homedir(),
4889
+ ".exe-os",
4890
+ "identity",
4891
+ `${employeeName}.md`
4892
+ );
4893
+ _resetCcAgentSupportCache();
4894
+ const hasAgentFlag = claudeSupportsAgentFlag();
4895
+ if (hasAgentFlag) {
4896
+ identityFlag = ` --agent ${employeeName}`;
4897
+ } else if (existsSync13(identityPath2)) {
4898
+ identityFlag = ` --append-system-prompt-file ${identityPath2}`;
4899
+ legacyFallbackWarned = true;
4900
+ }
4901
+ const behaviorsFile = exportBehaviorsSync(
4902
+ employeeName,
4903
+ path17.basename(spawnCwd),
4904
+ sessionName
4905
+ );
4906
+ if (behaviorsFile) {
4907
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
4908
+ }
4469
4909
  }
4470
- }
4471
- function isDebounced(targetSession) {
4472
- const state = readDebounceState();
4473
- const lastSent = state[targetSession] ?? 0;
4474
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4475
- }
4476
- function recordDebounce(targetSession) {
4477
- const state = readDebounceState();
4478
- state[targetSession] = Date.now();
4479
- const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4480
- for (const key of Object.keys(state)) {
4481
- if ((state[key] ?? 0) < cutoff) delete state[key];
4910
+ if (legacyFallbackWarned) {
4911
+ process.stderr.write(
4912
+ `[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.
4913
+ `
4914
+ );
4482
4915
  }
4483
- writeDebounceState(state);
4484
- }
4485
- function logIntercom(msg) {
4486
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
4487
- `;
4488
- process.stderr.write(`[intercom] ${msg}
4489
- `);
4916
+ let sessionContextFlag = "";
4490
4917
  try {
4491
- appendFileSync(INTERCOM_LOG2, line);
4918
+ const ctxDir = path17.join(os6.homedir(), ".exe-os", "session-cache");
4919
+ mkdirSync6(ctxDir, { recursive: true });
4920
+ const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
4921
+ const ctxContent = [
4922
+ `## Session Context`,
4923
+ `You are running in tmux session: ${sessionName}.`,
4924
+ `Your parent exe session is ${exeSession}.`,
4925
+ `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
4926
+ ].join("\n");
4927
+ writeFileSync7(ctxFile, ctxContent);
4928
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
4492
4929
  } catch {
4493
4930
  }
4494
- }
4495
- function getSessionState(sessionName) {
4496
- const transport = getTransport();
4497
- if (!transport.isAlive(sessionName)) return "offline";
4498
- try {
4499
- const pane = transport.capturePane(sessionName, 5);
4500
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4501
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4502
- return "no_claude";
4931
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
4932
+ if (ccProvider !== DEFAULT_PROVIDER) {
4933
+ const cfg = PROVIDER_TABLE[ccProvider];
4934
+ if (cfg?.apiKeyEnv) {
4935
+ const keyVal = process.env[cfg.apiKeyEnv];
4936
+ if (keyVal) {
4937
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
4503
4938
  }
4504
4939
  }
4505
- if (/Running…/.test(pane)) return "tool";
4506
- if (BUSY_PATTERN.test(pane)) return "thinking";
4507
- return "idle";
4508
- } catch {
4509
- return "offline";
4510
4940
  }
4511
- }
4512
- function isExeSession(sessionName) {
4513
- return /^exe\d*$/.test(sessionName);
4514
- }
4515
- function sendIntercom(targetSession) {
4516
- const transport = getTransport();
4517
- if (isExeSession(targetSession)) {
4518
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
4519
- return "skipped_exe";
4941
+ let spawnCommand;
4942
+ if (useExeAgent) {
4943
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4944
+ } else if (useBinSymlink) {
4945
+ const binName = `${employeeName}-${ccProvider}`;
4946
+ process.stderr.write(
4947
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
4948
+ `
4949
+ );
4950
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
4951
+ } else {
4952
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
4520
4953
  }
4521
- if (isDebounced(targetSession)) {
4522
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4523
- return "debounced";
4954
+ const spawnResult = transport.spawn(sessionName, {
4955
+ cwd: spawnCwd,
4956
+ command: spawnCommand
4957
+ });
4958
+ if (spawnResult.error) {
4959
+ releaseSpawnLock2(sessionName);
4960
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4524
4961
  }
4962
+ transport.pipeLog(sessionName, logFile);
4525
4963
  try {
4526
- const sessions = transport.listSessions();
4527
- if (!sessions.includes(targetSession)) {
4528
- logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4529
- return "failed";
4530
- }
4531
- const sessionState = getSessionState(targetSession);
4532
- if (sessionState === "no_claude") {
4533
- queueIntercom(targetSession, "claude not running in session");
4534
- recordDebounce(targetSession);
4535
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4536
- return "queued";
4537
- }
4538
- if (sessionState === "thinking" || sessionState === "tool") {
4539
- queueIntercom(targetSession, "session busy at send time");
4540
- recordDebounce(targetSession);
4541
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4542
- return "queued";
4964
+ const mySession = getMySession();
4965
+ const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4966
+ writeFileSync7(dispatchInfo, JSON.stringify({
4967
+ dispatchedBy: mySession,
4968
+ rootExe: exeSession,
4969
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
4970
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
4971
+ }));
4972
+ } catch {
4973
+ }
4974
+ let booted = false;
4975
+ for (let i = 0; i < 30; i++) {
4976
+ try {
4977
+ execSync7("sleep 0.5");
4978
+ } catch {
4543
4979
  }
4544
- if (transport.isPaneInCopyMode(targetSession)) {
4545
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4546
- transport.sendKeys(targetSession, "q");
4980
+ try {
4981
+ const pane = transport.capturePane(sessionName);
4982
+ if (useExeAgent) {
4983
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
4984
+ booted = true;
4985
+ break;
4986
+ }
4987
+ } else {
4988
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
4989
+ booted = true;
4990
+ break;
4991
+ }
4992
+ }
4993
+ } catch {
4547
4994
  }
4548
- transport.sendKeys(targetSession, "/exe-intercom");
4549
- recordDebounce(targetSession);
4550
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4551
- return "delivered";
4552
- } catch {
4553
- logIntercom(`FAIL \u2192 ${targetSession}`);
4554
- return "failed";
4555
4995
  }
4556
- }
4557
- function notifyParentExe(sessionKey) {
4558
- const target = getDispatchedBy(sessionKey);
4559
- if (!target) {
4560
- process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
4561
- `);
4562
- return false;
4996
+ if (!booted) {
4997
+ releaseSpawnLock2(sessionName);
4998
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4563
4999
  }
4564
- process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4565
- `);
4566
- const result = sendIntercom(target);
4567
- if (result === "failed") {
4568
- const rootExe = resolveExeSession();
4569
- if (rootExe && rootExe !== target) {
4570
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4571
- `);
4572
- const fallback = sendIntercom(rootExe);
4573
- return fallback !== "failed";
5000
+ if (!useExeAgent) {
5001
+ try {
5002
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
5003
+ } catch {
4574
5004
  }
4575
- return false;
4576
5005
  }
4577
- return true;
5006
+ registerSession({
5007
+ windowName: sessionName,
5008
+ agentId: employeeName,
5009
+ projectDir: spawnCwd,
5010
+ parentExe: exeSession,
5011
+ pid: 0,
5012
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
5013
+ });
5014
+ releaseSpawnLock2(sessionName);
5015
+ return { sessionName };
4578
5016
  }
4579
- function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4580
- if (employeeName === "exe") {
4581
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
5017
+ 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;
5018
+ var init_tmux_routing = __esm({
5019
+ "src/lib/tmux-routing.ts"() {
5020
+ "use strict";
5021
+ init_session_registry();
5022
+ init_session_key();
5023
+ init_transport();
5024
+ init_cc_agent_support();
5025
+ init_mcp_prefix();
5026
+ init_provider_table();
5027
+ init_intercom_queue();
5028
+ init_plan_limits();
5029
+ SPAWN_LOCK_DIR = path17.join(os6.homedir(), ".exe-os", "spawn-locks");
5030
+ SESSION_CACHE = path17.join(os6.homedir(), ".exe-os", "session-cache");
5031
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5032
+ VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
5033
+ VERIFY_PANE_LINES = 200;
5034
+ INTERCOM_DEBOUNCE_MS = 3e4;
5035
+ INTERCOM_LOG2 = path17.join(os6.homedir(), ".exe-os", "intercom.log");
5036
+ DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
5037
+ DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5038
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4582
5039
  }
4583
- try {
4584
- assertEmployeeLimitSync();
4585
- } catch (err) {
4586
- if (err instanceof PlanLimitError) {
4587
- return { status: "failed", sessionName: "", error: err.message };
4588
- }
5040
+ });
5041
+
5042
+ // src/lib/tasks-crud.ts
5043
+ import crypto7 from "crypto";
5044
+ import path18 from "path";
5045
+ import { execSync as execSync8 } from "child_process";
5046
+ import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
5047
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
5048
+ async function writeCheckpoint(input) {
5049
+ const client = getClient();
5050
+ const row = await resolveTask(client, input.taskId);
5051
+ const taskId = String(row.id);
5052
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5053
+ const blockedByIds = [];
5054
+ if (row.blocked_by) {
5055
+ blockedByIds.push(String(row.blocked_by));
4589
5056
  }
4590
- if (/-exe\d*$/.test(employeeName)) {
4591
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
4592
- return {
4593
- status: "failed",
4594
- sessionName: "",
4595
- error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
4596
- };
5057
+ const checkpoint = {
5058
+ step: input.step,
5059
+ context_summary: input.contextSummary,
5060
+ files_touched: input.filesTouched ?? [],
5061
+ blocked_by_ids: blockedByIds,
5062
+ last_checkpoint_at: now
5063
+ };
5064
+ const result = await client.execute({
5065
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
5066
+ args: [JSON.stringify(checkpoint), now, taskId]
5067
+ });
5068
+ if (result.rowsAffected === 0) {
5069
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
4597
5070
  }
4598
- let effectiveInstance = opts?.instance;
4599
- if (effectiveInstance === void 0 && opts?.autoInstance) {
4600
- const free = findFreeInstance(
4601
- employeeName,
4602
- exeSession,
4603
- opts.maxAutoInstances ?? 10
5071
+ const countResult = await client.execute({
5072
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
5073
+ args: [taskId]
5074
+ });
5075
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
5076
+ return { checkpointCount };
5077
+ }
5078
+ function extractParentFromContext(contextBody) {
5079
+ if (!contextBody) return null;
5080
+ const match = contextBody.match(
5081
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
5082
+ );
5083
+ return match ? match[1].toLowerCase() : null;
5084
+ }
5085
+ function slugify(title) {
5086
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
5087
+ }
5088
+ async function resolveTask(client, identifier) {
5089
+ let result = await client.execute({
5090
+ sql: "SELECT * FROM tasks WHERE id = ?",
5091
+ args: [identifier]
5092
+ });
5093
+ if (result.rows.length === 1) return result.rows[0];
5094
+ result = await client.execute({
5095
+ sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
5096
+ args: [`%${identifier}%`]
5097
+ });
5098
+ if (result.rows.length === 1) return result.rows[0];
5099
+ if (result.rows.length > 1) {
5100
+ const exact = result.rows.filter(
5101
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
5102
+ );
5103
+ if (exact.length === 1) return exact[0];
5104
+ const candidates = exact.length > 1 ? exact : result.rows;
5105
+ const active = candidates.filter(
5106
+ (r) => !["done", "cancelled"].includes(String(r.status))
5107
+ );
5108
+ if (active.length === 1) return active[0];
5109
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
5110
+ throw new Error(
5111
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
4604
5112
  );
4605
- if (free === null) {
4606
- return {
4607
- status: "failed",
4608
- sessionName: employeeSessionName(employeeName, exeSession),
4609
- error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4610
- };
4611
- }
4612
- effectiveInstance = free === 0 ? void 0 : free;
4613
- }
4614
- const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
4615
- if (isEmployeeAlive(sessionName)) {
4616
- const result2 = sendIntercom(sessionName);
4617
- if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4618
- return { status: "intercom_sent", sessionName };
4619
- }
4620
- if (result2 === "delivered") {
4621
- return { status: "intercom_unprocessed", sessionName };
4622
- }
4623
- return { status: "failed", sessionName, error: "intercom delivery failed" };
4624
5113
  }
4625
- const spawnOpts = { ...opts, instance: effectiveInstance };
4626
- const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
4627
- if (result.error) {
4628
- return { status: "failed", sessionName, error: result.error };
5114
+ result = await client.execute({
5115
+ sql: "SELECT * FROM tasks WHERE title LIKE ?",
5116
+ args: [`%${identifier}%`]
5117
+ });
5118
+ if (result.rows.length === 1) return result.rows[0];
5119
+ if (result.rows.length > 1) {
5120
+ const active = result.rows.filter(
5121
+ (r) => !["done", "cancelled"].includes(String(r.status))
5122
+ );
5123
+ if (active.length === 1) return active[0];
5124
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
5125
+ throw new Error(
5126
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
5127
+ );
4629
5128
  }
4630
- return { status: "spawned", sessionName };
5129
+ throw new Error(`Task not found: ${identifier}`);
4631
5130
  }
4632
- function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4633
- const transport = getTransport();
4634
- const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4635
- const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4636
- const logDir = path18.join(os6.homedir(), ".exe-os", "session-logs");
4637
- const logFile = path18.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4638
- if (!existsSync14(logDir)) {
4639
- mkdirSync6(logDir, { recursive: true });
5131
+ async function createTaskCore(input) {
5132
+ const client = getClient();
5133
+ const id = crypto7.randomUUID();
5134
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5135
+ const slug = slugify(input.title);
5136
+ const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
5137
+ let blockedById = null;
5138
+ const initialStatus = input.blockedBy ? "blocked" : "open";
5139
+ if (input.blockedBy) {
5140
+ const blocker = await resolveTask(client, input.blockedBy);
5141
+ blockedById = String(blocker.id);
4640
5142
  }
4641
- transport.kill(sessionName);
4642
- let cleanupSuffix = "";
4643
- try {
4644
- const thisFile = fileURLToPath2(import.meta.url);
4645
- const cleanupScript = path18.join(path18.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4646
- if (existsSync14(cleanupScript)) {
4647
- cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5143
+ let parentTaskId = null;
5144
+ let parentRef = input.parentTaskId;
5145
+ if (!parentRef) {
5146
+ const extracted = extractParentFromContext(input.context);
5147
+ if (extracted) {
5148
+ parentRef = extracted;
5149
+ process.stderr.write(
5150
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
5151
+ );
4648
5152
  }
4649
- } catch {
4650
5153
  }
4651
- try {
4652
- const claudeJsonPath = path18.join(os6.homedir(), ".claude.json");
4653
- let claudeJson = {};
5154
+ if (parentRef) {
4654
5155
  try {
4655
- claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
4656
- } catch {
5156
+ const parent = await resolveTask(client, parentRef);
5157
+ parentTaskId = String(parent.id);
5158
+ } catch (err) {
5159
+ if (!input.parentTaskId) {
5160
+ throw new Error(
5161
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
5162
+ );
5163
+ }
5164
+ throw err;
4657
5165
  }
4658
- if (!claudeJson.projects) claudeJson.projects = {};
4659
- const projects = claudeJson.projects;
4660
- const trustDir = opts?.cwd ?? projectDir;
4661
- if (!projects[trustDir]) projects[trustDir] = {};
4662
- projects[trustDir].hasTrustDialogAccepted = true;
4663
- writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4664
- } catch {
4665
5166
  }
4666
- try {
4667
- const settingsDir = path18.join(os6.homedir(), ".claude", "projects");
4668
- const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4669
- const projSettingsDir = path18.join(settingsDir, normalizedKey);
4670
- const settingsPath = path18.join(projSettingsDir, "settings.json");
4671
- let settings = {};
5167
+ let warning;
5168
+ const dupCheck = await client.execute({
5169
+ sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
5170
+ args: [input.title, input.assignedTo]
5171
+ });
5172
+ if (dupCheck.rows.length > 0) {
5173
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
5174
+ }
5175
+ if (input.baseDir) {
4672
5176
  try {
4673
- settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
5177
+ await mkdir4(path18.join(input.baseDir, "exe", "output"), { recursive: true });
5178
+ await mkdir4(path18.join(input.baseDir, "exe", "research"), { recursive: true });
5179
+ await ensureArchitectureDoc(input.baseDir, input.projectName);
5180
+ await ensureGitignoreExe(input.baseDir);
4674
5181
  } catch {
4675
5182
  }
4676
- const perms = settings.permissions ?? {};
4677
- const allow = perms.allow ?? [];
4678
- const toolNames = [
4679
- "recall_my_memory",
4680
- "store_memory",
4681
- "create_task",
4682
- "update_task",
4683
- "list_tasks",
4684
- "get_task",
4685
- "ask_team_memory",
4686
- "store_behavior",
4687
- "get_identity",
4688
- "send_message"
4689
- ];
4690
- const requiredTools = expandDualPrefixTools(toolNames);
4691
- let changed = false;
4692
- for (const tool of requiredTools) {
4693
- if (!allow.includes(tool)) {
4694
- allow.push(tool);
4695
- changed = true;
4696
- }
4697
- }
4698
- if (changed) {
4699
- perms.allow = allow;
4700
- settings.permissions = perms;
4701
- mkdirSync6(projSettingsDir, { recursive: true });
4702
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4703
- }
5183
+ }
5184
+ const complexity = input.complexity ?? "standard";
5185
+ let sessionScope = null;
5186
+ try {
5187
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
5188
+ sessionScope = resolveExeSession2();
4704
5189
  } catch {
4705
5190
  }
4706
- const spawnCwd = opts?.cwd ?? projectDir;
4707
- const useExeAgent = !!(opts?.model && opts?.provider);
4708
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
4709
- const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4710
- let identityFlag = "";
4711
- let behaviorsFlag = "";
4712
- let legacyFallbackWarned = false;
4713
- if (!useExeAgent && !useBinSymlink) {
4714
- const identityPath2 = path18.join(
4715
- os6.homedir(),
4716
- ".exe-os",
4717
- "identity",
4718
- `${employeeName}.md`
4719
- );
4720
- _resetCcAgentSupportCache();
4721
- const hasAgentFlag = claudeSupportsAgentFlag();
4722
- if (hasAgentFlag) {
4723
- identityFlag = ` --agent ${employeeName}`;
4724
- } else if (existsSync14(identityPath2)) {
4725
- identityFlag = ` --append-system-prompt-file ${identityPath2}`;
4726
- legacyFallbackWarned = true;
4727
- }
4728
- const behaviorsFile = exportBehaviorsSync(
4729
- employeeName,
4730
- path18.basename(spawnCwd),
4731
- sessionName
4732
- );
4733
- if (behaviorsFile) {
4734
- behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
4735
- }
5191
+ await client.execute({
5192
+ 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)
5193
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5194
+ args: [
5195
+ id,
5196
+ input.title,
5197
+ input.assignedTo,
5198
+ input.assignedBy,
5199
+ input.projectName,
5200
+ input.priority,
5201
+ initialStatus,
5202
+ taskFile,
5203
+ blockedById,
5204
+ parentTaskId,
5205
+ input.reviewer ?? null,
5206
+ input.context,
5207
+ complexity,
5208
+ input.budgetTokens ?? null,
5209
+ input.budgetFallbackModel ?? null,
5210
+ 0,
5211
+ null,
5212
+ sessionScope,
5213
+ now,
5214
+ now
5215
+ ]
5216
+ });
5217
+ return {
5218
+ id,
5219
+ title: input.title,
5220
+ assignedTo: input.assignedTo,
5221
+ assignedBy: input.assignedBy,
5222
+ projectName: input.projectName,
5223
+ priority: input.priority,
5224
+ status: initialStatus,
5225
+ taskFile,
5226
+ createdAt: now,
5227
+ updatedAt: now,
5228
+ warning,
5229
+ budgetTokens: input.budgetTokens ?? null,
5230
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
5231
+ tokensUsed: 0,
5232
+ tokensWarnedAt: null
5233
+ };
5234
+ }
5235
+ async function listTasks(input) {
5236
+ const client = getClient();
5237
+ const conditions = [];
5238
+ const args = [];
5239
+ if (input.assignedTo) {
5240
+ conditions.push("assigned_to = ?");
5241
+ args.push(input.assignedTo);
5242
+ }
5243
+ if (input.status) {
5244
+ conditions.push("status = ?");
5245
+ args.push(input.status);
5246
+ } else {
5247
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
5248
+ }
5249
+ if (input.projectName) {
5250
+ conditions.push("project_name = ?");
5251
+ args.push(input.projectName);
4736
5252
  }
4737
- if (legacyFallbackWarned) {
4738
- process.stderr.write(
4739
- `[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.
4740
- `
4741
- );
5253
+ if (input.priority) {
5254
+ conditions.push("priority = ?");
5255
+ args.push(input.priority);
4742
5256
  }
4743
- let sessionContextFlag = "";
4744
5257
  try {
4745
- const ctxDir = path18.join(os6.homedir(), ".exe-os", "session-cache");
4746
- mkdirSync6(ctxDir, { recursive: true });
4747
- const ctxFile = path18.join(ctxDir, `session-context-${sessionName}.md`);
4748
- const ctxContent = [
4749
- `## Session Context`,
4750
- `You are running in tmux session: ${sessionName}.`,
4751
- `Your parent exe session is ${exeSession}.`,
4752
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
4753
- ].join("\n");
4754
- writeFileSync7(ctxFile, ctxContent);
4755
- sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5258
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
5259
+ const session = resolveExeSession2();
5260
+ if (session) {
5261
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
5262
+ args.push(session);
5263
+ }
4756
5264
  } catch {
4757
5265
  }
4758
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
4759
- if (ccProvider !== DEFAULT_PROVIDER) {
4760
- const cfg = PROVIDER_TABLE[ccProvider];
4761
- if (cfg?.apiKeyEnv) {
4762
- const keyVal = process.env[cfg.apiKeyEnv];
4763
- if (keyVal) {
4764
- envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
4765
- }
5266
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5267
+ const result = await client.execute({
5268
+ 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`,
5269
+ args
5270
+ });
5271
+ return result.rows.map((r) => ({
5272
+ id: String(r.id),
5273
+ title: String(r.title),
5274
+ assignedTo: String(r.assigned_to),
5275
+ assignedBy: String(r.assigned_by),
5276
+ projectName: String(r.project_name),
5277
+ priority: String(r.priority),
5278
+ status: String(r.status),
5279
+ taskFile: String(r.task_file),
5280
+ createdAt: String(r.created_at),
5281
+ updatedAt: String(r.updated_at),
5282
+ checkpointCount: Number(r.checkpoint_count ?? 0),
5283
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
5284
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
5285
+ tokensUsed: Number(r.tokens_used ?? 0),
5286
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
5287
+ }));
5288
+ }
5289
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
5290
+ if (!taskContext) return null;
5291
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
5292
+ try {
5293
+ const since = new Date(taskCreatedAt).toISOString();
5294
+ const branch = execSync8(
5295
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
5296
+ { encoding: "utf8", timeout: 3e3 }
5297
+ ).trim();
5298
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
5299
+ const commitCount = execSync8(
5300
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
5301
+ { encoding: "utf8", timeout: 5e3 }
5302
+ ).trim();
5303
+ const count = parseInt(commitCount, 10);
5304
+ if (count === 0) {
5305
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
4766
5306
  }
5307
+ return null;
5308
+ } catch {
5309
+ return null;
4767
5310
  }
4768
- let spawnCommand;
4769
- if (useExeAgent) {
4770
- spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4771
- } else if (useBinSymlink) {
4772
- const binName = `${employeeName}-${ccProvider}`;
5311
+ }
5312
+ async function updateTaskStatus(input) {
5313
+ const client = getClient();
5314
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5315
+ const row = await resolveTask(client, input.taskId);
5316
+ const taskId = String(row.id);
5317
+ const taskFile = String(row.task_file);
5318
+ if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
4773
5319
  process.stderr.write(
4774
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
5320
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
4775
5321
  `
4776
5322
  );
4777
- spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
4778
- } else {
4779
- spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
4780
- }
4781
- const spawnResult = transport.spawn(sessionName, {
4782
- cwd: spawnCwd,
4783
- command: spawnCommand
4784
- });
4785
- if (spawnResult.error) {
4786
- releaseSpawnLock2(sessionName);
4787
- return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4788
5323
  }
4789
- transport.pipeLog(sessionName, logFile);
4790
- try {
4791
- const mySession = getMySession();
4792
- const dispatchInfo = path18.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4793
- writeFileSync7(dispatchInfo, JSON.stringify({
4794
- dispatchedBy: mySession,
4795
- rootExe: exeSession,
4796
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
4797
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
4798
- }));
4799
- } catch {
5324
+ if (input.status === "done") {
5325
+ const existingRow = await client.execute({
5326
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
5327
+ args: [taskId]
5328
+ });
5329
+ if (existingRow.rows.length > 0) {
5330
+ const ctx = existingRow.rows[0];
5331
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
5332
+ if (warning) {
5333
+ input.result = input.result ? `\u26A0\uFE0F ${warning}
5334
+
5335
+ ${input.result}` : `\u26A0\uFE0F ${warning}`;
5336
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
5337
+ `);
5338
+ }
5339
+ }
4800
5340
  }
4801
- let booted = false;
4802
- for (let i = 0; i < 30; i++) {
4803
- try {
4804
- execSync8("sleep 0.5");
4805
- } catch {
5341
+ if (input.status === "in_progress") {
5342
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
5343
+ const claim = await client.execute({
5344
+ sql: `UPDATE tasks
5345
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
5346
+ WHERE id = ? AND status = 'open'`,
5347
+ args: [tmuxSession, now, taskId]
5348
+ });
5349
+ if (claim.rowsAffected === 0) {
5350
+ const current = await client.execute({
5351
+ sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
5352
+ args: [taskId]
5353
+ });
5354
+ const cur = current.rows[0];
5355
+ const status = cur?.status ?? "unknown";
5356
+ const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
5357
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
4806
5358
  }
4807
5359
  try {
4808
- const pane = transport.capturePane(sessionName);
4809
- if (useExeAgent) {
4810
- if (pane.includes("[exe-agent]") || pane.includes("online")) {
4811
- booted = true;
4812
- break;
4813
- }
4814
- } else {
4815
- if (pane.includes("Claude Code") || pane.includes("\u276F")) {
4816
- booted = true;
4817
- break;
4818
- }
4819
- }
5360
+ await writeCheckpoint({
5361
+ taskId,
5362
+ step: "claimed",
5363
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
5364
+ });
4820
5365
  } catch {
4821
5366
  }
5367
+ return { row, taskFile, now, taskId };
4822
5368
  }
4823
- if (!booted) {
4824
- releaseSpawnLock2(sessionName);
4825
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
5369
+ if (input.result) {
5370
+ await client.execute({
5371
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
5372
+ args: [input.status, input.result, now, taskId]
5373
+ });
5374
+ } else {
5375
+ await client.execute({
5376
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
5377
+ args: [input.status, now, taskId]
5378
+ });
5379
+ }
5380
+ try {
5381
+ await writeCheckpoint({
5382
+ taskId,
5383
+ step: `status_transition:${input.status}`,
5384
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
5385
+ });
5386
+ } catch {
5387
+ }
5388
+ return { row, taskFile, now, taskId };
5389
+ }
5390
+ async function deleteTaskCore(taskId, _baseDir) {
5391
+ const client = getClient();
5392
+ const row = await resolveTask(client, taskId);
5393
+ const id = String(row.id);
5394
+ const taskFile = String(row.task_file);
5395
+ const assignedTo = String(row.assigned_to);
5396
+ const assignedBy = String(row.assigned_by);
5397
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
5398
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
5399
+ return { taskFile, assignedTo, assignedBy, taskSlug };
5400
+ }
5401
+ async function ensureArchitectureDoc(baseDir, projectName) {
5402
+ const archPath = path18.join(baseDir, "exe", "ARCHITECTURE.md");
5403
+ try {
5404
+ if (existsSync14(archPath)) return;
5405
+ const template = [
5406
+ `# ${projectName} \u2014 System Architecture`,
5407
+ "",
5408
+ "> Employees: read this before every task. Update it when you change system structure.",
5409
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
5410
+ "",
5411
+ "## Overview",
5412
+ "",
5413
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
5414
+ "",
5415
+ "## Key Components",
5416
+ "",
5417
+ "<!-- List the major modules, services, or subsystems. -->",
5418
+ "",
5419
+ "## Data Flow",
5420
+ "",
5421
+ "<!-- How does data move through the system? What writes where? -->",
5422
+ "",
5423
+ "## Invariants",
5424
+ "",
5425
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
5426
+ "",
5427
+ "## Dependencies",
5428
+ "",
5429
+ "<!-- What depends on what? If I change X, what else is affected? -->",
5430
+ ""
5431
+ ].join("\n");
5432
+ await writeFile4(archPath, template, "utf-8");
5433
+ } catch {
4826
5434
  }
4827
- if (!useExeAgent) {
4828
- try {
4829
- transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
4830
- } catch {
5435
+ }
5436
+ async function ensureGitignoreExe(baseDir) {
5437
+ const gitignorePath = path18.join(baseDir, ".gitignore");
5438
+ try {
5439
+ if (existsSync14(gitignorePath)) {
5440
+ const content = readFileSync12(gitignorePath, "utf-8");
5441
+ if (/^\/?exe\/?$/m.test(content)) return;
5442
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
5443
+ } else {
5444
+ await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
4831
5445
  }
5446
+ } catch {
4832
5447
  }
4833
- registerSession({
4834
- windowName: sessionName,
4835
- agentId: employeeName,
4836
- projectDir: spawnCwd,
4837
- parentExe: exeSession,
4838
- pid: 0,
4839
- registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4840
- });
4841
- releaseSpawnLock2(sessionName);
4842
- return { sessionName };
4843
5448
  }
4844
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
4845
- var init_tmux_routing = __esm({
4846
- "src/lib/tmux-routing.ts"() {
5449
+ var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
5450
+ var init_tasks_crud = __esm({
5451
+ "src/lib/tasks-crud.ts"() {
4847
5452
  "use strict";
4848
- init_session_registry();
4849
- init_session_key();
4850
- init_transport();
4851
- init_cc_agent_support();
4852
- init_mcp_prefix();
4853
- init_provider_table();
4854
- init_intercom_queue();
4855
- init_plan_limits();
4856
- SPAWN_LOCK_DIR = path18.join(os6.homedir(), ".exe-os", "spawn-locks");
4857
- SESSION_CACHE = path18.join(os6.homedir(), ".exe-os", "session-cache");
4858
- BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4859
- INTERCOM_DEBOUNCE_MS = 3e4;
4860
- INTERCOM_LOG2 = path18.join(os6.homedir(), ".exe-os", "intercom.log");
4861
- DEBOUNCE_FILE = path18.join(SESSION_CACHE, "intercom-debounce.json");
4862
- DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4863
- BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
5453
+ init_database();
5454
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
5455
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4864
5456
  }
4865
5457
  });
4866
5458
 
@@ -4894,6 +5486,34 @@ async function listPendingReviews(limit) {
4894
5486
  });
4895
5487
  return result.rows;
4896
5488
  }
5489
+ async function cleanupOrphanedReviews() {
5490
+ const client = getClient();
5491
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5492
+ const r1 = await client.execute({
5493
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
5494
+ WHERE status = 'needs_review'
5495
+ AND assigned_by = 'system'
5496
+ AND title LIKE 'Review:%'
5497
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
5498
+ args: [now]
5499
+ });
5500
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
5501
+ const r2 = await client.execute({
5502
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
5503
+ WHERE status = 'needs_review'
5504
+ AND result IS NOT NULL
5505
+ AND updated_at < ?`,
5506
+ args: [now, staleThreshold]
5507
+ });
5508
+ const total = r1.rowsAffected + r2.rowsAffected;
5509
+ if (total > 0) {
5510
+ process.stderr.write(
5511
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
5512
+ `
5513
+ );
5514
+ }
5515
+ return total;
5516
+ }
4897
5517
  function getReviewChecklist(role, agent, taskSlug) {
4898
5518
  const roleLower = role.toLowerCase();
4899
5519
  if (roleLower.includes("engineer") || roleLower === "principal engineer") {
@@ -5007,6 +5627,7 @@ var init_tasks_review = __esm({
5007
5627
  init_tasks_crud();
5008
5628
  init_tmux_routing();
5009
5629
  init_session_key();
5630
+ init_state_bus();
5010
5631
  }
5011
5632
  });
5012
5633
 
@@ -5129,13 +5750,12 @@ function assertSessionScope(actionType, targetProject) {
5129
5750
  };
5130
5751
  }
5131
5752
  process.stderr.write(
5132
- `[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
5753
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
5133
5754
  `
5134
5755
  );
5135
5756
  return {
5136
- allowed: true,
5137
- // v1: warn-only, don't block
5138
- reason: "cross_session_granted",
5757
+ allowed: false,
5758
+ reason: "cross_session_denied",
5139
5759
  currentProject,
5140
5760
  targetProject,
5141
5761
  targetSession: findSessionForProject(targetProject)?.windowName
@@ -5161,8 +5781,9 @@ async function dispatchTaskToEmployee(input) {
5161
5781
  try {
5162
5782
  const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
5163
5783
  const check = assertSessionScope2("dispatch_task", input.projectName);
5164
- if (check.reason === "cross_session_granted") {
5784
+ if (check.reason === "cross_session_denied") {
5165
5785
  crossProject = true;
5786
+ return { dispatched: "skipped", crossProject: true };
5166
5787
  }
5167
5788
  } catch {
5168
5789
  }
@@ -5219,10 +5840,10 @@ var init_tasks_notify = __esm({
5219
5840
  });
5220
5841
 
5221
5842
  // src/lib/behaviors.ts
5222
- import crypto7 from "crypto";
5843
+ import crypto8 from "crypto";
5223
5844
  async function storeBehavior(opts) {
5224
5845
  const client = getClient();
5225
- const id = crypto7.randomUUID();
5846
+ const id = crypto8.randomUUID();
5226
5847
  const now = (/* @__PURE__ */ new Date()).toISOString();
5227
5848
  await client.execute({
5228
5849
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -5279,7 +5900,7 @@ __export(skill_learning_exports, {
5279
5900
  storeTrajectory: () => storeTrajectory,
5280
5901
  sweepTrajectories: () => sweepTrajectories
5281
5902
  });
5282
- import crypto8 from "crypto";
5903
+ import crypto9 from "crypto";
5283
5904
  async function extractTrajectory(taskId, agentId) {
5284
5905
  const client = getClient();
5285
5906
  const result = await client.execute({
@@ -5308,11 +5929,11 @@ async function extractTrajectory(taskId, agentId) {
5308
5929
  return signature;
5309
5930
  }
5310
5931
  function hashSignature(signature) {
5311
- return crypto8.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
5932
+ return crypto9.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
5312
5933
  }
5313
5934
  async function storeTrajectory(opts) {
5314
5935
  const client = getClient();
5315
- const id = crypto8.randomUUID();
5936
+ const id = crypto9.randomUUID();
5316
5937
  const now = (/* @__PURE__ */ new Date()).toISOString();
5317
5938
  const signatureHash = hashSignature(opts.signature);
5318
5939
  await client.execute({
@@ -5559,6 +6180,7 @@ var init_skill_learning = __esm({
5559
6180
  // src/lib/tasks.ts
5560
6181
  var tasks_exports = {};
5561
6182
  __export(tasks_exports, {
6183
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
5562
6184
  countNewPendingReviewsSince: () => countNewPendingReviewsSince,
5563
6185
  countPendingReviews: () => countPendingReviews,
5564
6186
  createTask: () => createTask,
@@ -5624,6 +6246,21 @@ async function updateTask(input) {
5624
6246
  });
5625
6247
  } catch {
5626
6248
  }
6249
+ try {
6250
+ const client = getClient();
6251
+ const cascaded = await client.execute({
6252
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
6253
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
6254
+ args: [now, taskId]
6255
+ });
6256
+ if (cascaded.rowsAffected > 0) {
6257
+ process.stderr.write(
6258
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
6259
+ `
6260
+ );
6261
+ }
6262
+ } catch {
6263
+ }
5627
6264
  }
5628
6265
  const isTerminal = input.status === "done" || input.status === "needs_review";
5629
6266
  if (isTerminal) {
@@ -5637,6 +6274,13 @@ async function updateTask(input) {
5637
6274
  await cascadeUnblock(taskId, input.baseDir, now);
5638
6275
  } catch {
5639
6276
  }
6277
+ orgBus.emit({
6278
+ type: "task_completed",
6279
+ taskId,
6280
+ employee: String(row.assigned_to),
6281
+ result: input.result ?? "",
6282
+ timestamp: now
6283
+ });
5640
6284
  if (row.parent_task_id) {
5641
6285
  try {
5642
6286
  await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
@@ -5704,6 +6348,7 @@ var init_tasks = __esm({
5704
6348
  init_database();
5705
6349
  init_config();
5706
6350
  init_notifications();
6351
+ init_state_bus();
5707
6352
  init_tasks_crud();
5708
6353
  init_tasks_review();
5709
6354
  init_tasks_crud();
@@ -5713,87 +6358,6 @@ var init_tasks = __esm({
5713
6358
  }
5714
6359
  });
5715
6360
 
5716
- // src/lib/session-kill-telemetry.ts
5717
- import crypto9 from "crypto";
5718
- var init_session_kill_telemetry = __esm({
5719
- "src/lib/session-kill-telemetry.ts"() {
5720
- "use strict";
5721
- init_database();
5722
- }
5723
- });
5724
-
5725
- // src/lib/capacity-monitor.ts
5726
- function resumeTaskTitle(agentId) {
5727
- return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
5728
- }
5729
- function buildResumeContext(agentId, openTasks) {
5730
- const taskList = openTasks.map(
5731
- (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
5732
- ).join("\n");
5733
- return [
5734
- "## Context",
5735
- "",
5736
- `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
5737
- "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
5738
- "",
5739
- `You have ${openTasks.length} open task(s). Work through them in priority order:`,
5740
- "",
5741
- taskList,
5742
- "",
5743
- "Read each task file and chain through them. Build and commit after each one."
5744
- ].join("\n");
5745
- }
5746
- async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
5747
- const client = getClient();
5748
- const now = (/* @__PURE__ */ new Date()).toISOString();
5749
- const context = buildResumeContext(agentId, openTasks);
5750
- const existing = await client.execute({
5751
- sql: `SELECT id FROM tasks
5752
- WHERE assigned_to = ?
5753
- AND title LIKE ?
5754
- AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
5755
- ORDER BY created_at DESC
5756
- LIMIT 1`,
5757
- args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
5758
- });
5759
- if (existing.rows.length > 0) {
5760
- const taskId = String(existing.rows[0].id);
5761
- await client.execute({
5762
- sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
5763
- args: [context, now, taskId]
5764
- });
5765
- return { created: false, taskId };
5766
- }
5767
- const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
5768
- const task = await createTask2({
5769
- title: resumeTaskTitle(agentId),
5770
- assignedTo: agentId,
5771
- assignedBy: "system",
5772
- projectName: projectDir.split("/").pop() ?? "unknown",
5773
- priority: "p0",
5774
- context,
5775
- baseDir: projectDir
5776
- });
5777
- return { created: true, taskId: task.id };
5778
- }
5779
- var RELAUNCH_COOLDOWN_MS, RESUME_TITLE_PREFIX, RESUME_TITLE_LIKE_PATTERN, RESUME_ACTIVE_STATUSES, CONFIRMATION_WINDOW_MS;
5780
- var init_capacity_monitor = __esm({
5781
- "src/lib/capacity-monitor.ts"() {
5782
- "use strict";
5783
- init_session_registry();
5784
- init_transport();
5785
- init_notifications();
5786
- init_database();
5787
- init_session_kill_telemetry();
5788
- init_tmux_routing();
5789
- RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
5790
- RESUME_TITLE_PREFIX = "RESUME:";
5791
- RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
5792
- RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
5793
- CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
5794
- }
5795
- });
5796
-
5797
6361
  // src/lib/messaging.ts
5798
6362
  import crypto10 from "crypto";
5799
6363
  function generateUlid() {
@@ -5994,7 +6558,7 @@ __export(consolidation_exports, {
5994
6558
  selectUnconsolidated: () => selectUnconsolidated,
5995
6559
  storeConsolidation: () => storeConsolidation
5996
6560
  });
5997
- import { randomUUID as randomUUID4 } from "crypto";
6561
+ import { randomUUID as randomUUID5 } from "crypto";
5998
6562
  async function selectUnconsolidated(client, limit = 200) {
5999
6563
  const result = await client.execute({
6000
6564
  sql: `SELECT id, agent_id, project_name, tool_name, raw_text, timestamp
@@ -6090,7 +6654,7 @@ async function consolidateCluster(cluster, model) {
6090
6654
  return textBlock?.text ?? "";
6091
6655
  }
6092
6656
  async function storeConsolidation(client, cluster, synthesisText, embedFn) {
6093
- const consolidatedId = randomUUID4();
6657
+ const consolidatedId = randomUUID5();
6094
6658
  const now = (/* @__PURE__ */ new Date()).toISOString();
6095
6659
  const rawText = `CONSOLIDATION [${cluster.dateRange}, ${cluster.projectName}]:
6096
6660
 
@@ -6115,7 +6679,7 @@ ${synthesisText}`;
6115
6679
  const linkStmts = sourceIds.map((sourceId) => ({
6116
6680
  sql: `INSERT INTO consolidations (id, consolidated_memory_id, source_memory_id, created_at)
6117
6681
  VALUES (?, ?, ?, ?)`,
6118
- args: [randomUUID4(), consolidatedId, sourceId, now]
6682
+ args: [randomUUID5(), consolidatedId, sourceId, now]
6119
6683
  }));
6120
6684
  const placeholders = sourceIds.map(() => "?").join(",");
6121
6685
  const markStmt = {
@@ -9340,7 +9904,7 @@ import { z as z29 } from "zod";
9340
9904
 
9341
9905
  // src/automation/trigger-engine.ts
9342
9906
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync10, existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
9343
- import { randomUUID as randomUUID3 } from "crypto";
9907
+ import { randomUUID as randomUUID4 } from "crypto";
9344
9908
  import path23 from "path";
9345
9909
  import os7 from "os";
9346
9910
  var TRIGGERS_PATH = path23.join(os7.homedir(), ".exe-os", "triggers.json");
@@ -9366,7 +9930,7 @@ function saveTriggers(triggers) {
9366
9930
  function createNewTrigger(input) {
9367
9931
  const triggers = loadTriggers();
9368
9932
  const trigger = {
9369
- id: randomUUID3().slice(0, 8),
9933
+ id: randomUUID4().slice(0, 8),
9370
9934
  ...input
9371
9935
  };
9372
9936
  triggers.push(trigger);
@@ -9767,6 +10331,7 @@ import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as
9767
10331
  import path25 from "path";
9768
10332
 
9769
10333
  // src/lib/employee-templates.ts
10334
+ init_global_procedures();
9770
10335
  var CLIENT_COO_TEMPLATE = `---
9771
10336
  role: client-coo
9772
10337
  title: Chief Operating Officer
@@ -11335,6 +11900,157 @@ Consolidated summaries stored as tier-1 (importance=9) memories.`
11335
11900
  );
11336
11901
  }
11337
11902
 
11903
+ // src/mcp/tools/store-global-procedure.ts
11904
+ init_global_procedures();
11905
+ init_active_agent();
11906
+ import { z as z41 } from "zod";
11907
+ function registerStoreGlobalProcedure(server2) {
11908
+ server2.registerTool(
11909
+ "store_global_procedure",
11910
+ {
11911
+ title: "Store Global Procedure",
11912
+ description: "Create an organization-wide procedure (Layer 0) that supersedes identity, expertise, and experience. Use for hard rules that every employee must follow. RESTRICTED: only exe or founder sessions.",
11913
+ inputSchema: {
11914
+ title: z41.string().describe("Short title for the procedure"),
11915
+ content: z41.string().max(500).describe("The procedure content \u2014 clear, actionable instruction"),
11916
+ priority: z41.enum(["p0", "p1", "p2"]).optional().describe("Priority tier. p0 = always (default). p1 = standard. p2 = nice-to-have."),
11917
+ domain: z41.string().optional().describe("Category: workflow, code-style, communication, architecture, testing, security")
11918
+ }
11919
+ },
11920
+ async ({ title, content, priority, domain }) => {
11921
+ const caller = getActiveAgent();
11922
+ const allowed = caller.agentId === "default" || caller.agentRole === "COO";
11923
+ if (!allowed) {
11924
+ return {
11925
+ content: [{
11926
+ type: "text",
11927
+ text: `Permission denied. Only exe or founder sessions can create global procedures. You are "${caller.agentId}".`
11928
+ }],
11929
+ isError: true
11930
+ };
11931
+ }
11932
+ const id = await storeGlobalProcedure({
11933
+ title,
11934
+ content,
11935
+ priority: priority ?? "p0",
11936
+ domain: domain ?? void 0
11937
+ });
11938
+ return {
11939
+ content: [{
11940
+ type: "text",
11941
+ text: `Global procedure stored.
11942
+ ID: ${id}
11943
+ Title: ${title}
11944
+ Priority: ${priority ?? "p0"}
11945
+ Domain: ${domain ?? "none"}`
11946
+ }]
11947
+ };
11948
+ }
11949
+ );
11950
+ }
11951
+
11952
+ // src/mcp/tools/list-global-procedures.ts
11953
+ init_global_procedures();
11954
+ function registerListGlobalProcedures(server2) {
11955
+ server2.registerTool(
11956
+ "list_global_procedures",
11957
+ {
11958
+ title: "List Global Procedures",
11959
+ description: "List all active organization-wide procedures (Layer 0). These supersede identity, expertise, and experience.",
11960
+ inputSchema: {}
11961
+ },
11962
+ async () => {
11963
+ const procedures = await loadGlobalProcedures();
11964
+ if (procedures.length === 0) {
11965
+ return {
11966
+ content: [{
11967
+ type: "text",
11968
+ text: "No active global procedures."
11969
+ }]
11970
+ };
11971
+ }
11972
+ const lines = procedures.map(
11973
+ (p) => `[${p.id}] (${p.priority}) ${p.title}${p.domain ? ` [${p.domain}]` : ""}
11974
+ ${p.content}`
11975
+ );
11976
+ return {
11977
+ content: [{
11978
+ type: "text",
11979
+ text: `Active global procedures (${procedures.length}):
11980
+
11981
+ ${lines.join("\n\n")}`
11982
+ }]
11983
+ };
11984
+ }
11985
+ );
11986
+ }
11987
+
11988
+ // src/mcp/tools/deactivate-global-procedure.ts
11989
+ init_global_procedures();
11990
+ init_active_agent();
11991
+ init_database();
11992
+ import { z as z42 } from "zod";
11993
+ function registerDeactivateGlobalProcedure(server2) {
11994
+ server2.registerTool(
11995
+ "deactivate_global_procedure",
11996
+ {
11997
+ title: "Deactivate Global Procedure",
11998
+ description: "Soft-delete a global procedure by setting active = 0. RESTRICTED: only exe or founder sessions. Use list_global_procedures to find the procedure ID first.",
11999
+ inputSchema: {
12000
+ procedure_id: z42.string().describe("UUID of the global procedure to deactivate")
12001
+ }
12002
+ },
12003
+ async ({ procedure_id }) => {
12004
+ const caller = getActiveAgent();
12005
+ const allowed = caller.agentId === "default" || caller.agentRole === "COO";
12006
+ if (!allowed) {
12007
+ return {
12008
+ content: [{
12009
+ type: "text",
12010
+ text: `Permission denied. Only exe or founder sessions can deactivate global procedures. You are "${caller.agentId}".`
12011
+ }],
12012
+ isError: true
12013
+ };
12014
+ }
12015
+ const client = getClient();
12016
+ const result = await client.execute({
12017
+ sql: "SELECT id, title, content, priority, domain FROM global_procedures WHERE id = ?",
12018
+ args: [procedure_id]
12019
+ });
12020
+ if (result.rows.length === 0) {
12021
+ return {
12022
+ content: [{
12023
+ type: "text",
12024
+ text: `Global procedure not found: ${procedure_id}`
12025
+ }],
12026
+ isError: true
12027
+ };
12028
+ }
12029
+ const row = result.rows[0];
12030
+ const wasActive = await deactivateGlobalProcedure(procedure_id);
12031
+ if (!wasActive) {
12032
+ return {
12033
+ content: [{
12034
+ type: "text",
12035
+ text: `Global procedure ${procedure_id} was already inactive.`
12036
+ }]
12037
+ };
12038
+ }
12039
+ return {
12040
+ content: [{
12041
+ type: "text",
12042
+ text: `Deactivated global procedure.
12043
+ ID: ${row.id}
12044
+ Title: ${row.title}
12045
+ Priority: ${row.priority}
12046
+ Domain: ${row.domain ?? "none"}
12047
+ Content: ${row.content}`
12048
+ }]
12049
+ };
12050
+ }
12051
+ );
12052
+ }
12053
+
11338
12054
  // src/lib/telemetry.ts
11339
12055
  var ENABLED = process.env.EXE_TELEMETRY === "1";
11340
12056
  var initialized = false;
@@ -11439,6 +12155,9 @@ registerDeployClient(server);
11439
12155
  registerQueryConversations(server);
11440
12156
  registerLoadSkill(server);
11441
12157
  registerConsolidateMemories(server);
12158
+ registerStoreGlobalProcedure(server);
12159
+ registerListGlobalProcedures(server);
12160
+ registerDeactivateGlobalProcedure(server);
11442
12161
  try {
11443
12162
  await initStore();
11444
12163
  process.stderr.write("[exe-os] MCP server starting...\n");